删除多余代码

This commit is contained in:
sexygoat
2026-04-08 19:31:22 +08:00
parent b510b4539f
commit d1c6588d8f
110 changed files with 897 additions and 24613 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -88,13 +88,6 @@
iconColor: '#ff3b30',
path: RoutesAlias.Analysis
},
{
name: '礼花效果',
description: '动画特效展示',
icon: '',
iconColor: '#7A7FFF',
path: RoutesAlias.Fireworks
},
{
name: '聊天',
description: '即时通讯功能',
@@ -116,13 +109,6 @@
iconColor: '#ff6b6b',
path: WEB_LINKS.COMMUNITY
},
{
name: '更新日志',
description: '版本更新与变更记录',
icon: '',
iconColor: '#38C0FC',
path: RoutesAlias.ChangeLog
},
{
name: '哔哩哔哩',
description: '技术分享与交流',
@@ -134,11 +120,7 @@
const quickLinks: QuickLink[] = [
{ name: '登录', path: RoutesAlias.Login },
{ name: '注册', path: RoutesAlias.Register },
{ name: '忘记密码', path: RoutesAlias.ForgetPassword },
{ name: '定价', path: RoutesAlias.Pricing },
{ name: '个人中心', path: RoutesAlias.UserCenter },
{ name: '留言管理', path: RoutesAlias.Comment }
{ name: '个人中心', path: RoutesAlias.UserCenter }
]
const handleAppClick = (path: string) => {

View File

@@ -362,6 +362,15 @@
const actionList = props.actions(row)
// 如果没有操作按钮,显示提示
if (actionList.length === 0) {
return h(
'span',
{ style: 'color: var(--el-text-color-placeholder); font-size: 12px;' },
'暂无可用操作'
)
}
// 如果操作数量 <= 2直接显示所有按钮
if (actionList.length <= 2) {
return h(

View File

@@ -312,38 +312,17 @@
"ecommerce": "电子商务"
},
"widgets": {
"title": "组件中心",
"iconList": "Icon 图标",
"iconSelector": "图标选择器",
"imageCrop": "图像裁剪",
"excel": "Excel 导入导出",
"video": "视频播放器",
"countTo": "数字滚动",
"wangEditor": "富文本编辑器",
"watermark": "水印",
"contextMenu": "右键菜单",
"qrcode": "二维码",
"drag": "拖拽",
"textScroll": "文字滚动",
"fireworks": "礼花",
"elementUI": "组件总览"
"drag": "拖拽"
},
"template": {
"title": "模板中心",
"chat": "聊天",
"cards": "卡片",
"banners": "横幅",
"charts": "图表",
"map": "地图",
"calendar": "日历",
"pricing": "定价"
},
"article": {
"title": "文章管理",
"articleList": "文章列表",
"articleDetail": "文章详情",
"comment": "留言管理",
"articlePublish": "文章发布"
"calendar": "日历"
},
"result": {
"title": "结果页面",
@@ -361,55 +340,16 @@
"role": "角色管理",
"permission": "权限管理",
"carrierManagement": "运营商管理",
"userCenter": "个人中心",
"menu": "菜单管理",
"nested": "嵌套菜单",
"menu1": "菜单1",
"menu2": "菜单2",
"menu21": "菜单2-1",
"menu3": "菜单3",
"menu31": "菜单3-1",
"menu32": "菜单3-2",
"menu321": "菜单3-2-1"
},
"safeguard": {
"title": "运维管理",
"server": "服务器管理"
},
"plan": {
"title": "版本计划",
"log": "更新日志"
},
"help": {
"title": "帮助中心",
"document": "官方文档"
},
"cardManagement": {
"title": "我的网卡",
"singleCard": "资产信息",
"cardList": "网卡管理",
"cardDetail": "网卡明细",
"cardAssign": "网卡分配",
"cardShutdown": "停机管理",
"myCards": "我的网卡",
"offlineBatchRecharge": "线下批量充值",
"cardTransfer": "网卡转接",
"cardReplacement": "换卡管理",
"packageGift": "套餐赠送",
"cardChange": "换卡网卡"
"userCenter": "个人中心"
},
"packageManagement": {
"title": "套餐管理",
"packageCreate": "新建套餐",
"packageBatch": "批量管理",
"packageList": "套餐列表",
"packageDetail": "套餐详情",
"packageChange": "套餐变更",
"seriesGrants": "代理系列授权",
"seriesGrantsDetail": "代理系列授权详情",
"packageSeries": "套餐系列",
"packageSeriesDetail": "套餐系列详情",
"packageCommission": "套餐佣金网卡"
"packageSeriesDetail": "套餐系列详情"
},
"accountManagement": {
"title": "账号管理",
@@ -425,15 +365,6 @@
"enterpriseCards": "企业卡管理",
"customerCommission": "客户账号佣金"
},
"product": {
"title": "商品管理",
"simCard": "号卡管理",
"simCardAssign": "号卡分配",
"packageSeries": "套餐系列管理",
"packageList": "套餐列表",
"packageAssign": "套餐分配",
"shopAccounts": "店铺账号列表"
},
"shopManagement": {
"title": "店铺管理",
"shopList": "店铺列表"
@@ -469,20 +400,24 @@
"orderList": "订单列表",
"orderDetail": "订单详情"
},
"financialManagement": {
"title": "财务管理",
"agentRecharge": "代理充值",
"agentRechargeDetail": "代理充值详情",
"refundManagement": "退款管理",
"refundDetail": "退款详情"
},
"commission": {
"menu": {
"management": "佣金管理",
"withdrawal": "提现审批",
"withdrawalSettings": "提现配置",
"myCommission": "我的佣金",
"agentCommission": "代理商佣金"
}
"title": "佣金管理",
"withdrawal": "提现审批",
"withdrawalSettings": "提现配置",
"myCommission": "我的佣金",
"agentCommission": "代理商佣金"
},
"settings": {
"title": "设置管理",
"paymentMerchant": "支付商户",
"developerApi": "开发者API",
"commissionTemplate": "分佣模板"
"wechatPayConfiguration": "微信支付配置",
"detailsOfPaymentConfiguration": "支付配置详情"
}
},
"table": {

View File

@@ -11,8 +11,6 @@ import { useUserStore } from '@/store/modules/user'
*/
export const LOGIN_WHITE_LIST = [
'/auth/login',
'/auth/register',
'/auth/forget-password',
'/exception/403',
'/exception/404',
'/exception/500'

File diff suppressed because it is too large Load Diff

View File

@@ -17,18 +17,6 @@ export const staticRoutes: AppRouteRecordRaw[] = [
component: () => import('@views/auth/login/index.vue'),
meta: { title: 'menus.login.title', isHideTab: true, setTheme: true }
},
{
path: RoutesAlias.Register,
name: 'Register',
component: () => import('@views/auth/register/index.vue'),
meta: { title: 'menus.register.title', isHideTab: true, noLogin: true, setTheme: true }
},
{
path: RoutesAlias.ForgetPassword,
name: 'ForgetPassword',
component: () => import('@views/auth/forget-password/index.vue'),
meta: { title: 'menus.forgetPassword.title', isHideTab: true, noLogin: true, setTheme: true }
},
{
path: '/exception',
component: Home,
@@ -55,6 +43,35 @@ export const staticRoutes: AppRouteRecordRaw[] = [
}
]
},
{
path: '/result',
name: 'Result',
component: Home,
meta: {
title: 'menus.result.title',
icon: '&#xe715;'
},
children: [
{
path: 'success',
name: 'ResultSuccess',
component: () => import('@views/result/success/index.vue'),
meta: {
title: 'menus.result.success',
keepAlive: true
}
},
{
path: 'fail',
name: 'ResultFail',
component: () => import('@views/result/fail/index.vue'),
meta: {
title: 'menus.result.fail',
keepAlive: true
}
}
]
},
{
path: '/outside',
component: Home,

View File

@@ -34,12 +34,12 @@ export enum RoutesAlias {
Shop = '/shop-management/list', // 店铺列表
// 通用页面(店铺账号-企业账号)
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表(通用)
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表和店铺账号列表共用
// 账号管理
Account = '/account-management/account', // 账号列表
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
EnterpriseCards = '/account-management/enterprise-cards', // 企业卡授权
EnterpriseCards = '/asset-management/enterprise-cards', // 企业卡授权
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备授权
// 资产管理
@@ -48,13 +48,17 @@ export enum RoutesAlias {
DeviceList = '/asset-management/device-list', // 设备列表
ExchangeManagement = '/asset-management/exchange-management', // 换货管理
ExchangeDetail = '/asset-management/exchange-management/detail', // 换货单详情
AuthorizationRecords = '/asset-management/authorization-records', // 授权记录
AuthorizationRecordDetail = '/asset-management/authorization-records/detail', // 授权记录详情
AssetAssign = '/asset-management/asset-assign', // 分配记录
AssetAssignDetail = '/asset-management/asset-assign/detail', // 分配记录详情
IotCardTask = '/asset-management/iot-card-task', // IoT卡任务
DeviceTask = '/asset-management/device-task', // 设备任务
TaskDetail = '/asset-management/task-detail', // 任务详情IoT卡/设备任务详情)
// 记录管理
AuthorizationRecords = '/asset-management/record-management/authorization-records', // 授权记录
AuthorizationRecordDetail = '/asset-management/record-management/authorization-records/detail', // 授权记录详情
AssetAssign = '/asset-management/record-management/asset-assign', // 分配记录
AssetAssignDetail = '/asset-management/record-management/asset-assign/detail', // 分配记录详情
// 任务管理
IotCardTask = '/asset-management/task-management/iot-card-task', // IoT卡任务
DeviceTask = '/asset-management/task-management/device-task', // 设备任务
TaskDetail = '/asset-management/task-management/task-detail', // 任务详情IoT卡/设备任务详情)
// 订单管理
OrderList = '/order-management/order-list', // 订单列表
@@ -67,10 +71,10 @@ export enum RoutesAlias {
RefundDetail = '/finance/refund/detail', // 退款详情
// 佣金管理
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批
CommissionWithdrawalSettings = '/finance/commission/withdrawal-settings', // 提现配置
MyCommission = '/finance/commission/my-commission', // 我的佣金
AgentCommission = '/finance/commission/agent-commission', // 代理商佣金管理
WithdrawalApproval = '/commission-management/withdrawal-approval', // 提现审批
CommissionWithdrawalSettings = '/commission-management/withdrawal-settings', // 提现配置
MyCommission = '/commission-management/my-commission', // 我的佣金
AgentCommission = '/commission-management/agent-commission', // 代理商佣金管理
// 设置管理
WechatConfig = '/settings/wechat-config', // 微信支付配置
@@ -85,83 +89,18 @@ export enum RoutesAlias {
ManualTrigger = '/polling-management/manual-trigger', // 手动触发
PollingMonitor = '/polling-management/monitor', // 轮询监控
// 其他页面------------------------------------------------暂时不知道有没有用到不要动
Register = '/auth/register', // 注册
ForgetPassword = '/auth/forget-password', // 忘记密码
// 公共组件
Success = '/result/success', // 成功
Fail = '/result/fail', // 失败
IconList = '/widgets/icon-list', // 图标列表
IconSelector = '/widgets/icon-selector', // 图标选择器
ImageCrop = '/widgets/image-crop', // 图片裁剪
Excel = '/widgets/excel', // Excel
Video = '/widgets/video', // 视频
CountTo = '/widgets/count-to', // 计数
WangEditor = '/widgets/wang-editor', // 富文本编辑器
Watermark = '/widgets/watermark', // 水印
ContextMenu = '/widgets/context-menu', // 上下文菜单
Qrcode = '/widgets/qrcode', // 二维码
Drag = '/widgets/drag', // 拖拽
TextScroll = '/widgets/text-scroll', // 文字滚动
Fireworks = '/widgets/fireworks', // 礼花效果
Chat = '/template/chat', // 聊天
Cards = '/template/cards', // 卡片
Banners = '/template/banners', // 横幅
Charts = '/template/charts', // 图表
Map = '/template/map', // 地图
Calendar = '/template/calendar', // 日历
Pricing = '/template/pricing', // 定价
ArticleList = '/article/list', // 文章列表
ArticleDetail = '/article/detail', // 文章详情
Comment = '/article/comment', // 评论
ArticlePublish = '/article/publish', // 文章发布
Server = '/safeguard/server', // 服务器
ChangeLog = '/change/log', // 更新日志
// 物联网卡管理系统模块
// 我的网卡
CardList = '/card-management/card-list', // 网卡管理
CardDetail = '/card-management/card-detail', // 网卡明细
CardAssign = '/card-management/card-assign', // 网卡分配
CardShutdown = '/card-management/card-shutdown', // 停机管理
MyCards = '/card-management/my-cards', // 我的网卡
OfflineBatchRecharge = '/card-management/offline-batch-recharge', // 线下批量充值
CardTransfer = '/card-management/card-transfer', // 网卡转接
CardReplacement = '/card-management/card-replacement', // 换卡管理
PackageGift = '/card-management/package-gift', // 套餐赠送
CardChangeCard = '/card-management/card-change-card', // 换卡网卡
// other
PackageCreate = '/package-management/package-create', // 新建套餐
PackageBatch = '/package-management/package-batch', // 批量管理
PackageChange = '/package-management/package-change', // 套餐变更
PackageCommission = '/package-management/package-commission', // 套餐佣金网卡
// PlatformAccount = '/account-management/platform-account', // 平台账号管理
CustomerManagement = '/account-management/customer', // 客户管理
CustomerRole = '/account-management/customer-role', // 客户角色
AgentManagement = '/account-management/agent', // 代理商管理
ShopAccount = '/account-management/shop-account', // 代理账号管理
CustomerCommission = '/account-management/customer-commission', // 客户账号佣金
// 产品管理
SimCardManagement = '/product/sim-card', // 网卡产品管理
SimCardAssign = '/product/sim-card-assign', // 号卡分配
ShopAccounts = '/common/account-list', // 店铺账号列表(通用)
// 资产管理
DeviceDetail = '/asset-management/device-detail', // 设备详情
CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请
// 账户管理
CustomerAccountList = '/finance/customer-account', // 客户账号
MyAccount = '/finance/my-account', // 我的账户
// 设置管理
PaymentMerchant = '/settings/payment-merchant', // 支付商户
DeveloperApi = '/settings/developer-api', // 开发者API
CommissionTemplate = '/settings/commission-template' // 分佣模板
Calendar = '/template/calendar' // 日历
}
// 主页路由

View File

@@ -42,6 +42,7 @@ export interface AgentRechargeQueryParams {
status?: AgentRechargeStatus
start_date?: string
end_date?: string
dateRange?: string[] | any // For date range picker in UI
}
// 代理充值订单列表响应
@@ -49,7 +50,8 @@ export interface AgentRechargeListResponse {
page: number
page_size: number
total: number
list: AgentRecharge[]
items: AgentRecharge[]
list: AgentRecharge[] // 兼容旧字段
}
// 创建代理充值订单请求

View File

@@ -47,6 +47,7 @@ export interface Order {
payment_status_text: string
payment_method: OrderPaymentMethod
total_amount: number // 订单总金额(分)
actual_paid_amount?: number // 实付金额(分)
paid_at: string | null
commission_status: OrderCommissionStatus
commission_config_version: number
@@ -54,6 +55,9 @@ export interface Order {
iot_card_id: number | null
asset_identifier?: string // 资产标识符快照ICCID 或 VirtualNo
asset_type?: string // 资产类型single_card 或 device
purchase_role?: string // 订单渠道
purchase_remark?: string // 购买备注
operator_name?: string // 操作者
items: OrderItem[] | null
created_at: string
updated_at: string
@@ -67,6 +71,7 @@ export interface OrderQueryParams {
order_type?: OrderType
order_no?: string
identifier?: string // 按资产标识符过滤
purchase_role?: string // 订单渠道
start_time?: string
end_time?: string
dateRange?: string[] | any // For date range picker in UI
@@ -83,7 +88,10 @@ export interface OrderListResponse {
// 创建订单请求
export interface CreateOrderRequest {
identifier: string // 资产标识符ICCID 或 VirtualNo
order_type?: OrderType // 订单类型
iot_card_id?: number | null // IoT卡ID
device_id?: number | null // 设备ID
identifier?: string // 资产标识符ICCID 或 VirtualNo
package_ids: number[] // 套餐ID列表
payment_method: OrderPaymentMethod // 支付方式
}

View File

@@ -85,8 +85,10 @@ declare module 'vue' {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
@@ -109,6 +111,7 @@ declare module 'vue' {
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
@@ -120,6 +123,7 @@ declare module 'vue' {
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']

View File

@@ -1,476 +0,0 @@
<template>
<div class="page-content">
<ElRow>
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="searchQuery" placeholder="代理商名称/联系人" clearable></ElInput>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="levelFilter" placeholder="代理商等级" clearable style="width: 100%">
<ElOption label="一级代理" value="1" />
<ElOption label="二级代理" value="2" />
<ElOption label="三级代理" value="3" />
</ElSelect>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
<ElButton v-ripple @click="showDialog('add')">新增代理商</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredData" index row-key="id">
<template #default>
<ElTableColumn label="代理商名称" prop="agentName" min-width="150" />
<ElTableColumn label="代理商编码" prop="agentCode" />
<ElTableColumn label="等级" prop="level">
<template #default="scope">
<ElTag :type="getLevelTagType(scope.row.level)">
{{ getLevelText(scope.row.level) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="上级代理" prop="parentName" show-overflow-tooltip />
<ElTableColumn label="联系人" prop="contactPerson" />
<ElTableColumn label="联系电话" prop="contactPhone" />
<ElTableColumn label="账号数量" prop="accountCount" />
<ElTableColumn label="网卡数量" prop="simCardCount" />
<ElTableColumn label="佣金总额" prop="totalCommission">
<template #default="scope"> ¥{{ scope.row.totalCommission.toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="280">
<template #default="scope">
<el-button link @click="viewAccountList(scope.row)">账号管理</el-button>
<el-button link @click="viewCommission(scope.row)">佣金配置</el-button>
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button link @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">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="代理商名称" prop="agentName">
<ElInput v-model="form.agentName" placeholder="请输入代理商名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="代理商编码" prop="agentCode">
<ElInput v-model="form.agentCode" placeholder="请输入代理商编码" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="代理商等级" prop="level">
<ElSelect v-model="form.level" placeholder="请选择代理商等级" style="width: 100%">
<ElOption label="一级代理" :value="1" />
<ElOption label="二级代理" :value="2" />
<ElOption label="三级代理" :value="3" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="上级代理" prop="parentId">
<ElSelect
v-model="form.parentId"
placeholder="请选择上级代理"
clearable
style="width: 100%"
>
<ElOption label="无" :value="undefined" />
<ElOption
v-for="agent in parentAgentOptions"
:key="agent.id"
:label="agent.agentName"
:value="agent.id"
/>
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="联系人" prop="contactPerson">
<ElInput v-model="form.contactPerson" placeholder="请输入联系人" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="联系电话" prop="contactPhone">
<ElInput v-model="form.contactPhone" placeholder="请输入联系电话" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="邮箱" prop="email">
<ElInput v-model="form.email" placeholder="请输入邮箱" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="公司地址" prop="address">
<ElInput v-model="form.address" placeholder="请输入公司地址" />
</ElFormItem>
</ElCol>
</ElRow>
<ElFormItem label="备注" prop="remark">
<ElInput v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注信息" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<!-- 账号管理对话框 -->
<ElDialog v-model="accountDialogVisible" title="代理商账号管理" width="900px" align-center>
<div style="margin-bottom: 16px">
<ElButton type="primary" size="small" @click="addSubAccount">创建子账号</ElButton>
</div>
<ArtTable :data="currentAgentAccounts" index>
<template #default>
<ElTableColumn label="账号" prop="username" />
<ElTableColumn label="真实姓名" prop="realName" />
<ElTableColumn label="手机号" prop="phone" />
<ElTableColumn label="角色" prop="role" />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '正常' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" />
<ElTableColumn fixed="right" label="操作" width="150">
<template #default>
<el-button link>编辑</el-button>
<el-button link>禁用</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
</ElDialog>
<!-- 佣金配置对话框 -->
<ElDialog v-model="commissionDialogVisible" title="佣金配置" width="600px" align-center>
<ElForm label-width="120px">
<ElFormItem label="佣金模式">
<ElRadioGroup v-model="commissionForm.mode">
<ElRadio value="fixed">固定佣金</ElRadio>
<ElRadio value="percent">比例佣金</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem v-if="commissionForm.mode === 'fixed'" label="固定金额">
<ElInputNumber v-model="commissionForm.fixedAmount" :min="0" :precision="2" />
<span style="margin-left: 8px">/</span>
</ElFormItem>
<ElFormItem v-if="commissionForm.mode === 'percent'" label="佣金比例">
<ElInputNumber v-model="commissionForm.percent" :min="0" :max="100" :precision="2" />
<span style="margin-left: 8px">%</span>
</ElFormItem>
<ElFormItem label="结算周期">
<ElSelect v-model="commissionForm.settlementCycle" style="width: 100%">
<ElOption label="实时结算" value="realtime" />
<ElOption label="日结" value="daily" />
<ElOption label="周结" value="weekly" />
<ElOption label="月结" value="monthly" />
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="commissionDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="saveCommissionConfig">保存</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'AgentManagement' })
interface Agent {
id?: string
agentName: string
agentCode: string
level: number
parentId?: string | null
parentName?: string
contactPerson: string
contactPhone: string
email?: string
address?: string
remark?: string
accountCount?: number
simCardCount?: number
totalCommission?: number
status: 'active' | 'inactive'
createTime?: string
}
// Mock 数据
const mockData = ref<Agent[]>([
{
id: '1',
agentName: '华东区总代理',
agentCode: 'AGENT_HD_001',
level: 1,
parentId: null,
parentName: '无',
contactPerson: '张三',
contactPhone: '13800138001',
email: 'zhangsan@example.com',
address: '上海市浦东新区',
accountCount: 5,
simCardCount: 1000,
totalCommission: 158900.5,
status: 'active',
createTime: '2026-01-01 10:00:00'
},
{
id: '2',
agentName: '江苏省代理',
agentCode: 'AGENT_JS_001',
level: 2,
parentId: '1',
parentName: '华东区总代理',
contactPerson: '李四',
contactPhone: '13800138002',
email: 'lisi@example.com',
address: '江苏省南京市',
accountCount: 3,
simCardCount: 500,
totalCommission: 78500.0,
status: 'active',
createTime: '2026-01-05 11:00:00'
},
{
id: '3',
agentName: '南京市代理',
agentCode: 'AGENT_NJ_001',
level: 3,
parentId: '2',
parentName: '江苏省代理',
contactPerson: '王五',
contactPhone: '13800138003',
email: 'wangwu@example.com',
address: '江苏省南京市玄武区',
accountCount: 2,
simCardCount: 200,
totalCommission: 32800.0,
status: 'active',
createTime: '2026-01-10 12:00:00'
}
])
const searchQuery = ref('')
const levelFilter = ref('')
const dialogVisible = ref(false)
const accountDialogVisible = ref(false)
const commissionDialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const form = reactive<Agent>({
agentName: '',
agentCode: '',
level: 1,
parentId: null,
contactPerson: '',
contactPhone: '',
email: '',
address: '',
remark: '',
status: 'active'
})
const commissionForm = reactive({
mode: 'fixed',
fixedAmount: 10,
percent: 5,
settlementCycle: 'monthly'
})
const currentAgentAccounts = ref([
{
username: 'agent001',
realName: '张三',
phone: '13800138001',
role: '管理员',
status: 'active',
createTime: '2026-01-01 10:00:00'
}
])
const rules = reactive<FormRules>({
agentName: [{ required: true, message: '请输入代理商名称', trigger: 'blur' }],
agentCode: [{ required: true, message: '请输入代理商编码', trigger: 'blur' }],
level: [{ required: true, message: '请选择代理商等级', trigger: 'change' }],
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
})
const parentAgentOptions = computed(() => {
return mockData.value.filter((item) => item.level < form.level)
})
const filteredData = computed(() => {
let data = mockData.value
if (searchQuery.value) {
data = data.filter(
(item) =>
item.agentName.includes(searchQuery.value) ||
item.contactPerson.includes(searchQuery.value)
)
}
if (levelFilter.value) {
data = data.filter((item) => item.level === parseInt(levelFilter.value))
}
return data
})
const getLevelText = (level: number) => {
const levelMap: Record<number, string> = { 1: '一级代理', 2: '二级代理', 3: '三级代理' }
return levelMap[level] || '未知'
}
const getLevelTagType = (level: number) => {
const typeMap: Record<number, string> = { 1: 'primary', 2: 'success', 3: 'warning' }
return typeMap[level] || 'info'
}
const handleSearch = () => {
// 搜索逻辑已通过 computed 实现
}
const showDialog = (type: 'add' | 'edit', row?: Agent) => {
dialogType.value = type
dialogVisible.value = true
if (type === 'edit' && row) {
Object.assign(form, row)
} else {
resetForm()
}
}
const resetForm = () => {
Object.assign(form, {
agentName: '',
agentCode: '',
level: 1,
parentId: null,
contactPerson: '',
contactPhone: '',
email: '',
address: '',
remark: '',
status: 'active'
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (dialogType.value === 'add') {
const parentAgent = mockData.value.find((item) => item.id === form.parentId)
mockData.value.push({
...form,
id: Date.now().toString(),
parentName: parentAgent ? parentAgent.agentName : '无',
accountCount: 0,
simCardCount: 0,
totalCommission: 0,
createTime: new Date().toLocaleString('zh-CN')
})
ElMessage.success('新增成功')
} else {
const index = mockData.value.findIndex((item) => item.id === form.id)
if (index !== -1) {
const parentAgent = mockData.value.find((item) => item.id === form.parentId)
mockData.value[index] = {
...form,
parentName: parentAgent ? parentAgent.agentName : '无'
}
}
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
}
})
}
const handleDelete = (row: Agent) => {
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 viewAccountList = (row: Agent) => {
accountDialogVisible.value = true
// 实际应用中应该根据代理商ID加载账号列表
console.log(row)
}
const viewCommission = (row: Agent) => {
commissionDialogVisible.value = true
// 实际应用中应该根据代理商ID加载佣金配置
console.log(row)
}
const addSubAccount = () => {
ElMessage.info('创建子账号功能')
}
const saveCommissionConfig = () => {
ElMessage.success('佣金配置保存成功')
commissionDialogVisible.value = false
}
</script>
<style lang="scss" scoped>
.page-content {
// 自定义样式
}
</style>

View File

@@ -1,457 +0,0 @@
<template>
<div class="page-content">
<!-- 统计卡片 -->
<ElRow :gutter="20" style="margin-bottom: 20px">
<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">{{ statistics.totalCustomers }}</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><User /></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.totalCommission.toFixed(2) }}
</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-success)"><Money /></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-warning)">
¥{{ statistics.totalWithdrawn.toFixed(2) }}
</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-warning)"
><WalletFilled
/></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.pendingWithdrawal.toFixed(2) }}
</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-danger)"><Wallet /></el-icon>
</ElCard>
</ElCol>
</ElRow>
<!-- 搜索和筛选区 -->
<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="customerTypeFilter" placeholder="客户类型" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="代理商" value="agent" />
<ElOption label="企业客户" value="enterprise" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect
v-model="commissionRangeFilter"
placeholder="佣金范围"
clearable
style="width: 100%"
>
<ElOption label="全部" value="" />
<ElOption label="0-1000元" value="0-1000" />
<ElOption label="1000-5000元" value="1000-5000" />
<ElOption label="5000-10000元" value="5000-10000" />
<ElOption label="10000元以上" value="10000+" />
</ElSelect>
</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>
<!-- 客户佣金列表 -->
<ArtTable :data="filteredData" index style="margin-top: 20px">
<template #default>
<ElTableColumn label="客户名称" prop="customerName" min-width="150" show-overflow-tooltip />
<ElTableColumn label="客户类型" prop="customerType" width="120">
<template #default="scope">
<ElTag :type="scope.row.customerType === 'agent' ? 'warning' : 'primary'">
{{ scope.row.customerType === 'agent' ? '代理商' : '企业客户' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="联系电话" prop="phone" width="130" />
<ElTableColumn label="累计佣金" prop="totalCommission" width="130" sortable>
<template #default="scope">
<span style="font-weight: 600; color: var(--el-color-success)">
¥{{ scope.row.totalCommission.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="已提现" prop="withdrawnAmount" width="130" sortable>
<template #default="scope">
<span style="color: var(--el-color-warning)">
¥{{ scope.row.withdrawnAmount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="待提现" prop="pendingAmount" width="130" sortable>
<template #default="scope">
<span style="font-weight: 600; color: var(--el-color-danger)">
¥{{ scope.row.pendingAmount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="提现次数" prop="withdrawalCount" width="100" align="center" />
<ElTableColumn label="卡片数量" prop="cardCount" width="100" align="center">
<template #default="scope">
<span style="color: var(--el-color-primary)">{{ scope.row.cardCount }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="最近提现" prop="lastWithdrawalTime" width="180" />
<ElTableColumn label="注册时间" prop="registerTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="220">
<template #default="scope">
<el-button link :icon="View" @click="viewCommissionDetail(scope.row)"
>佣金详情</el-button
>
<el-button link :icon="List" @click="viewWithdrawalHistory(scope.row)"
>提现记录</el-button
>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 佣金详情对话框 -->
<ElDialog v-model="commissionDialogVisible" title="佣金详情" width="900px" align-center>
<ElDescriptions :column="2" border style="margin-bottom: 20px">
<ElDescriptionsItem label="客户名称">{{ currentCustomer.customerName }}</ElDescriptionsItem>
<ElDescriptionsItem label="客户类型">
<ElTag :type="currentCustomer.customerType === 'agent' ? 'warning' : 'primary'">
{{ currentCustomer.customerType === 'agent' ? '代理商' : '企业客户' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="累计佣金">
<span style="font-weight: 600; color: var(--el-color-success)">
¥{{ currentCustomer.totalCommission.toFixed(2) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="已提现">
<span style="color: var(--el-color-warning)">
¥{{ currentCustomer.withdrawnAmount.toFixed(2) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="待提现">
<span style="font-weight: 600; color: var(--el-color-danger)">
¥{{ currentCustomer.pendingAmount.toFixed(2) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="提现次数">{{
currentCustomer.withdrawalCount
}}</ElDescriptionsItem>
</ElDescriptions>
<ElDivider content-position="left">佣金明细</ElDivider>
<ArtTable :data="commissionDetails" index>
<template #default>
<ElTableColumn label="来源" prop="source" width="120" />
<ElTableColumn label="订单号" prop="orderNo" width="180" />
<ElTableColumn label="佣金金额" prop="amount" width="120">
<template #default="scope">
<span style="color: var(--el-color-success)"
>+¥{{ scope.row.amount.toFixed(2) }}</span
>
</template>
</ElTableColumn>
<ElTableColumn label="佣金比例" prop="rate" width="100" />
<ElTableColumn label="订单金额" prop="orderAmount" width="120" />
<ElTableColumn label="获得时间" prop="createTime" width="180" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag v-if="scope.row.status === 'completed'" type="success">已结算</ElTag>
<ElTag v-else-if="scope.row.status === 'pending'" type="warning">待结算</ElTag>
</template>
</ElTableColumn>
</template>
</ArtTable>
<template #footer>
<ElButton @click="commissionDialogVisible = false">关闭</ElButton>
</template>
</ElDialog>
<!-- 提现记录对话框 -->
<ElDialog v-model="withdrawalDialogVisible" title="提现记录" width="900px" align-center>
<ArtTable :data="withdrawalHistory" index>
<template #default>
<ElTableColumn label="提现单号" prop="withdrawalNo" width="180" />
<ElTableColumn label="提现金额" prop="amount" width="120">
<template #default="scope">
<span style="font-weight: 600; color: var(--el-color-danger)">
¥{{ scope.row.amount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="手续费" prop="fee" width="100">
<template #default="scope"> ¥{{ scope.row.fee.toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn label="实际到账" prop="actualAmount" width="120">
<template #default="scope">
<span style="color: var(--el-color-success)">
¥{{ scope.row.actualAmount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="提现方式" prop="method" width="100" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag v-if="scope.row.status === 'completed'" type="success">已完成</ElTag>
<ElTag v-else-if="scope.row.status === 'processing'" type="warning">处理中</ElTag>
<ElTag v-else-if="scope.row.status === 'pending'" type="info">待审核</ElTag>
<ElTag v-else type="danger">已拒绝</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="申请时间" prop="applyTime" width="180" />
<ElTableColumn label="完成时间" prop="completeTime" width="180">
<template #default="scope">
{{ scope.row.completeTime || '-' }}
</template>
</ElTableColumn>
</template>
</ArtTable>
<template #footer>
<ElButton @click="withdrawalDialogVisible = false">关闭</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ElMessage } from 'element-plus'
import { View, List, User, Money, WalletFilled, Wallet } from '@element-plus/icons-vue'
defineOptions({ name: 'CustomerCommission' })
interface CustomerCommission {
id: string
customerName: string
customerType: 'agent' | 'enterprise'
phone: string
totalCommission: number
withdrawnAmount: number
pendingAmount: number
withdrawalCount: number
cardCount: number
lastWithdrawalTime: string
registerTime: string
}
const searchQuery = ref('')
const customerTypeFilter = ref('')
const commissionRangeFilter = ref('')
const commissionDialogVisible = ref(false)
const withdrawalDialogVisible = ref(false)
const statistics = reactive({
totalCustomers: 156,
totalCommission: 580000.0,
totalWithdrawn: 420000.0,
pendingWithdrawal: 160000.0
})
const mockData = ref<CustomerCommission[]>([
{
id: '1',
customerName: '华东区总代理',
customerType: 'agent',
phone: '13800138000',
totalCommission: 85000.0,
withdrawnAmount: 70000.0,
pendingAmount: 15000.0,
withdrawalCount: 12,
cardCount: 500,
lastWithdrawalTime: '2026-01-08 10:00:00',
registerTime: '2025-06-01 09:00:00'
},
{
id: '2',
customerName: '深圳市科技有限公司',
customerType: 'enterprise',
phone: '13900139000',
totalCommission: 45000.0,
withdrawnAmount: 30000.0,
pendingAmount: 15000.0,
withdrawalCount: 8,
cardCount: 300,
lastWithdrawalTime: '2026-01-05 14:30:00',
registerTime: '2025-07-15 10:00:00'
},
{
id: '3',
customerName: '北京智能制造',
customerType: 'enterprise',
phone: '13700137000',
totalCommission: 68000.0,
withdrawnAmount: 55000.0,
pendingAmount: 13000.0,
withdrawalCount: 10,
cardCount: 450,
lastWithdrawalTime: '2026-01-07 16:00:00',
registerTime: '2025-08-20 11:00:00'
}
])
const currentCustomer = ref<CustomerCommission>({
id: '',
customerName: '',
customerType: 'agent',
phone: '',
totalCommission: 0,
withdrawnAmount: 0,
pendingAmount: 0,
withdrawalCount: 0,
cardCount: 0,
lastWithdrawalTime: '',
registerTime: ''
})
const commissionDetails = ref([
{
id: '1',
source: '套餐销售',
orderNo: 'ORD202601090001',
amount: 150.0,
rate: '10%',
orderAmount: '¥1,500.00',
createTime: '2026-01-09 09:30:00',
status: 'completed'
},
{
id: '2',
source: '卡片激活',
orderNo: 'ORD202601080025',
amount: 80.0,
rate: '8%',
orderAmount: '¥1,000.00',
createTime: '2026-01-08 15:20:00',
status: 'completed'
}
])
const withdrawalHistory = ref([
{
id: '1',
withdrawalNo: 'WD202601080001',
amount: 10000.0,
fee: 20.0,
actualAmount: 9980.0,
method: '银行卡',
status: 'completed',
applyTime: '2026-01-08 10:00:00',
completeTime: '2026-01-08 15:30:00'
},
{
id: '2',
withdrawalNo: 'WD202601050002',
amount: 5000.0,
fee: 10.0,
actualAmount: 4990.0,
method: '支付宝',
status: 'completed',
applyTime: '2026-01-05 14:00:00',
completeTime: '2026-01-05 18:20:00'
}
])
const filteredData = computed(() => {
let data = mockData.value
if (searchQuery.value) {
data = data.filter(
(item) =>
item.customerName.includes(searchQuery.value) || item.phone.includes(searchQuery.value)
)
}
if (customerTypeFilter.value) {
data = data.filter((item) => item.customerType === customerTypeFilter.value)
}
if (commissionRangeFilter.value) {
data = data.filter((item) => {
const commission = item.totalCommission
if (commissionRangeFilter.value === '0-1000') return commission >= 0 && commission < 1000
if (commissionRangeFilter.value === '1000-5000')
return commission >= 1000 && commission < 5000
if (commissionRangeFilter.value === '5000-10000')
return commission >= 5000 && commission < 10000
if (commissionRangeFilter.value === '10000+') return commission >= 10000
return true
})
}
return data
})
const handleSearch = () => {}
const exportData = () => {
ElMessage.success('数据导出中...')
}
const viewCommissionDetail = (row: CustomerCommission) => {
currentCustomer.value = { ...row }
commissionDialogVisible.value = true
}
const viewWithdrawalHistory = (row: CustomerCommission) => {
currentCustomer.value = { ...row }
withdrawalDialogVisible.value = true
}
</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 {
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.stat-icon {
font-size: 40px;
opacity: 0.6;
}
}
}
</style>

View File

@@ -1,253 +0,0 @@
<template>
<div class="page-content">
<ElRow>
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="searchQuery" placeholder="角色名称" clearable></ElInput>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
<ElButton v-ripple @click="showDialog('add')">新增角色</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredData" index>
<template #default>
<ElTableColumn label="角色名称" prop="roleName" />
<ElTableColumn label="角色编码" prop="roleCode" />
<ElTableColumn label="能力边界" prop="abilities">
<template #default="scope">
<ElTag
v-for="(ability, index) in scope.row.abilities"
:key="index"
size="small"
style="margin-right: 4px"
>
{{ ability }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="描述" prop="description" show-overflow-tooltip />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="180">
<template #default="scope">
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button link @click="handleDelete(scope.row)">删除</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增客户角色' : '编辑客户角色'"
width="600px"
align-center
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="角色名称" prop="roleName">
<ElInput v-model="form.roleName" placeholder="请输入角色名称" />
</ElFormItem>
<ElFormItem label="角色编码" prop="roleCode">
<ElInput v-model="form.roleCode" placeholder="请输入角色编码" />
</ElFormItem>
<ElFormItem label="能力边界" prop="abilities">
<ElCheckboxGroup v-model="form.abilities">
<ElCheckbox label="查看网卡">查看网卡</ElCheckbox>
<ElCheckbox label="操作网卡">操作网卡</ElCheckbox>
<ElCheckbox label="查看套餐">查看套餐</ElCheckbox>
<ElCheckbox label="购买套餐">购买套餐</ElCheckbox>
<ElCheckbox label="查看设备">查看设备</ElCheckbox>
<ElCheckbox label="管理设备">管理设备</ElCheckbox>
<ElCheckbox label="查看佣金">查看佣金</ElCheckbox>
<ElCheckbox label="提现佣金">提现佣金</ElCheckbox>
</ElCheckboxGroup>
</ElFormItem>
<ElFormItem label="描述" prop="description">
<ElInput
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入角色描述"
/>
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'CustomerRole' })
interface CustomerRole {
id?: string
roleName: string
roleCode: string
abilities: string[]
description: string
status: 'active' | 'inactive'
createTime?: string
}
// Mock 数据
const mockData = ref<CustomerRole[]>([
{
id: '1',
roleName: '标准客户',
roleCode: 'CUSTOMER_STANDARD',
abilities: ['查看网卡', '查看套餐', '查看设备'],
description: '标准客户角色,拥有基本查看权限',
status: 'active',
createTime: '2026-01-01 10:00:00'
},
{
id: '2',
roleName: 'VIP客户',
roleCode: 'CUSTOMER_VIP',
abilities: ['查看网卡', '操作网卡', '查看套餐', '购买套餐', '查看设备', '管理设备'],
description: 'VIP客户角色拥有更多操作权限',
status: 'active',
createTime: '2026-01-02 11:00:00'
},
{
id: '3',
roleName: '企业客户',
roleCode: 'CUSTOMER_ENTERPRISE',
abilities: [
'查看网卡',
'操作网卡',
'查看套餐',
'购买套餐',
'查看设备',
'管理设备',
'查看佣金'
],
description: '企业客户角色,拥有完整业务权限',
status: 'active',
createTime: '2026-01-03 12:00:00'
}
])
const searchQuery = ref('')
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const form = reactive<CustomerRole>({
roleName: '',
roleCode: '',
abilities: [],
description: '',
status: 'active'
})
const rules = reactive<FormRules>({
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
roleCode: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
abilities: [{ required: true, message: '请至少选择一项能力', trigger: 'change' }]
})
const filteredData = computed(() => {
if (!searchQuery.value) return mockData.value
return mockData.value.filter((item) =>
item.roleName.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const handleSearch = () => {
// 搜索逻辑已通过 computed 实现
}
const showDialog = (type: 'add' | 'edit', row?: CustomerRole) => {
dialogType.value = type
dialogVisible.value = true
if (type === 'edit' && row) {
form.id = row.id
form.roleName = row.roleName
form.roleCode = row.roleCode
form.abilities = [...row.abilities]
form.description = row.description
form.status = row.status
} else {
resetForm()
}
}
const resetForm = () => {
form.id = undefined
form.roleName = ''
form.roleCode = ''
form.abilities = []
form.description = ''
form.status = 'active'
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (dialogType.value === 'add') {
mockData.value.push({
...form,
id: Date.now().toString(),
createTime: new Date().toLocaleString('zh-CN')
})
ElMessage.success('新增成功')
} else {
const index = mockData.value.findIndex((item) => item.id === form.id)
if (index !== -1) {
mockData.value[index] = { ...form }
}
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
}
})
}
const handleDelete = (row: CustomerRole) => {
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('删除成功')
})
}
</script>
<style lang="scss" scoped>
.page-content {
:deep(.el-checkbox) {
margin-right: 20px;
margin-bottom: 10px;
}
}
</style>

View File

@@ -1,359 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="customer-management-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="handleSearch">查询</ElButton>
<ElButton @click="exportExcel">导出excel</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElMessage } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'CustomerManagement' })
const loading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
distributorName: '',
distributorAccount: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 模拟数据 - 客户管理(代理商)
const mockData = [
{
id: 1,
distributorName: '北京优享科技有限公司',
distributorAccount: 'bjyx2024',
totalCommission: 12560.8,
instantCommission: 8320.5,
pendingCommission: 4240.3,
prepaidCommission: 2400.0,
withdrawnCommission: 6800.2,
withdrawingCommission: 1520.3,
availableCommission: 2000.0
},
{
id: 2,
distributorName: '上海智联通信技术公司',
distributorAccount: 'shzl2024',
totalCommission: 18750.25,
instantCommission: 12500.75,
pendingCommission: 6249.5,
prepaidCommission: 3600.0,
withdrawnCommission: 8900.25,
withdrawingCommission: 2100.0,
availableCommission: 3750.0
},
{
id: 3,
distributorName: '广州物联网络有限公司',
distributorAccount: 'gzwl2024',
totalCommission: 9876.4,
instantCommission: 6584.3,
pendingCommission: 3292.1,
prepaidCommission: 1800.0,
withdrawnCommission: 4938.2,
withdrawingCommission: 980.2,
availableCommission: 1958.0
},
{
id: 4,
distributorName: '深圳云联科技股份公司',
distributorAccount: 'szyl2024',
totalCommission: 24500.6,
instantCommission: 16333.73,
pendingCommission: 8166.87,
prepaidCommission: 4800.0,
withdrawnCommission: 11225.3,
withdrawingCommission: 2450.06,
availableCommission: 4900.12
},
{
id: 5,
distributorName: '杭州通达网络服务公司',
distributorAccount: 'hztd2024',
totalCommission: 15200.35,
instantCommission: 10133.57,
pendingCommission: 5066.78,
prepaidCommission: 2900.0,
withdrawnCommission: 7600.18,
withdrawingCommission: 1520.04,
availableCommission: 3040.07
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getCustomerList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getCustomerList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '代理商名称',
prop: 'distributorName',
type: 'input',
config: {
clearable: true,
placeholder: '请输入代理商名称'
},
onChange: handleFormChange
},
{
label: '代理商账号',
prop: 'distributorAccount',
type: 'input',
config: {
clearable: true,
placeholder: '请输入代理商账号'
},
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '代理商名称', prop: 'distributorName' },
{ label: '代理商账号', prop: 'distributorAccount' },
{ label: '总佣金', prop: 'totalCommission' },
{ label: '已秒返佣金', prop: 'instantCommission' },
{ label: '未秒返佣金', prop: 'pendingCommission' },
{ label: '预存佣金', prop: 'prepaidCommission' },
{ label: '已提现佣金', prop: 'withdrawnCommission' },
{ label: '提现中佣金', prop: 'withdrawingCommission' },
{ label: '可提现佣金', prop: 'availableCommission' },
{ label: '操作', prop: 'operation' }
]
// 导出Excel
const exportExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条客户记录`)
}
// 查看提现详情
const viewWithdrawDetail = (row: any) => {
ElMessage.info(`查看 ${row.distributorName} 的提现详情`)
}
// 查看佣金明细
const viewCommissionDetail = (row: any) => {
ElMessage.info(`查看 ${row.distributorName} 的佣金明细`)
}
// 查看自动提现详情
const viewAutoWithdrawDetail = (row: any) => {
ElMessage.info(`查看 ${row.distributorName} 的自动提现详情`)
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'distributorName',
label: '代理商名称',
minWidth: 180
},
{
prop: 'distributorAccount',
label: '代理商账号',
width: 120
},
{
prop: 'totalCommission',
label: '总佣金',
width: 120,
formatter: (row) => `¥${row.totalCommission.toFixed(2)}`
},
{
prop: 'instantCommission',
label: '已秒返佣金',
width: 120,
formatter: (row) => `¥${row.instantCommission.toFixed(2)}`
},
{
prop: 'pendingCommission',
label: '未秒返佣金',
width: 120,
formatter: (row) => `¥${row.pendingCommission.toFixed(2)}`
},
{
prop: 'prepaidCommission',
label: '预存佣金',
width: 120,
formatter: (row) => `¥${row.prepaidCommission.toFixed(2)}`
},
{
prop: 'withdrawnCommission',
label: '已提现佣金',
width: 120,
formatter: (row) => `¥${row.withdrawnCommission.toFixed(2)}`
},
{
prop: 'withdrawingCommission',
label: '提现中佣金',
width: 120,
formatter: (row) => `¥${row.withdrawingCommission.toFixed(2)}`
},
{
prop: 'availableCommission',
label: '可提现佣金',
width: 120,
formatter: (row) => `¥${row.availableCommission.toFixed(2)}`
},
{
prop: 'operation',
label: '操作',
width: 200,
formatter: (row: any) => {
return h('div', { style: 'display: flex; gap: 5px; flex-wrap: wrap;' }, [
h(ArtButtonTable, {
type: 'view',
text: '提现详情',
onClick: () => viewWithdrawDetail(row)
}),
h(ArtButtonTable, {
type: 'detail',
text: '佣金明细',
onClick: () => viewCommissionDetail(row)
}),
h(ArtButtonTable, {
type: 'info',
text: '自动提现详情',
onClick: () => viewAutoWithdrawDetail(row)
})
])
}
}
])
onMounted(() => {
getCustomerList()
})
// 获取客户列表
const getCustomerList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取客户列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getCustomerList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getCustomerList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getCustomerList()
}
</script>
<style lang="scss" scoped>
.customer-management-page {
// 可以添加特定样式
}
</style>

View File

@@ -207,6 +207,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { RoutesAlias } from '@/router/routesAlias'
import { EnterpriseService, ShopService } from '@/api/modules'
import { ElMessage, ElSwitch, ElCascader, ElTreeSelect } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
@@ -797,7 +798,7 @@
// 查看客户账号
const viewCustomerAccounts = (row: EnterpriseItem) => {
router.push({
path: `/account-management/enterprise-customer/customer-accounts/${row.id}`,
path: `${RoutesAlias.EnterpriseCustomer}/customer-accounts/${row.id}`,
query: { type: 'enterprise' }
})
}
@@ -805,14 +806,14 @@
// 卡管理
const manageCards = (row: EnterpriseItem) => {
router.push({
path: '/account-management/enterprise-cards',
path: RoutesAlias.EnterpriseCards,
query: { id: row.id }
})
}
const manageDevices = (row: EnterpriseItem) => {
router.push({
path: '/asset-management/enterprise-devices',
path: RoutesAlias.EnterpriseDevices,
query: { id: row.id }
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,599 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="shop-account-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton @click="showDialog('add')" v-permission="'shop_account:add'"
>新增代理账号</ElButton
>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增代理账号' : '编辑代理账号'"
width="500px"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
<ElFormItem label="用户名" prop="username">
<ElInput v-model="formData.username" placeholder="请输入用户名" />
</ElFormItem>
<ElFormItem v-if="dialogType === 'add'" label="密码" prop="password">
<ElInput
v-model="formData.password"
type="password"
placeholder="请输入密码"
show-password
/>
</ElFormItem>
<ElFormItem label="手机号" prop="phone">
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
</ElFormItem>
<ElFormItem label="店铺" prop="shop_id">
<ElSelect
v-model="formData.shop_id"
placeholder="请选择店铺"
filterable
remote
:remote-method="searchShops"
:loading="shopLoading"
clearable
style="width: 100%"
>
<ElOption
v-for="shop in shopList"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading"
>提交</ElButton
>
</div>
</template>
</ElDialog>
<!-- 修改密码对话框 -->
<ElDialog v-model="passwordDialogVisible" title="重置密码" width="400px">
<ElForm ref="passwordFormRef" :model="passwordForm" :rules="passwordRules">
<ElFormItem label="新密码" prop="new_password">
<ElInput
v-model="passwordForm.new_password"
type="password"
placeholder="请输入新密码"
show-password
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="passwordDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleChangePassword"
:loading="passwordSubmitLoading"
>提交</ElButton
>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { FormInstance, ElMessage, ElMessageBox, ElSwitch, ElSelect, ElOption } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { AccountService, ShopService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { PlatformAccount, ShopResponse } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
defineOptions({ name: 'ShopAccount' }) // 定义组件名称,用于 KeepAlive 缓存控制
const { hasAuth } = useAuth()
const dialogType = ref('add')
const dialogVisible = ref(false)
const passwordDialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const passwordSubmitLoading = ref(false)
const shopLoading = ref(false)
const currentAccountId = ref<number>(0)
const shopList = ref<ShopResponse[]>([])
const searchShopList = ref<ShopResponse[]>([])
// 定义表单搜索初始值
const initialSearchState = {
username: '',
phone: '',
shop_id: undefined as number | undefined,
status: undefined as number | undefined
}
// 响应式表单数据
const searchForm = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<PlatformAccount[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 重置表单
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.currentPage = 1 // 重置到第一页
getAccountList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', searchForm)
pagination.currentPage = 1 // 搜索时重置到第一页
getAccountList()
}
// 表单配置项
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '用户名',
prop: 'username',
type: 'input',
config: {
clearable: true,
placeholder: '请输入用户名'
}
},
{
label: '手机号',
prop: 'phone',
type: 'input',
config: {
clearable: true,
placeholder: '请输入手机号'
}
},
{
label: '店铺',
prop: 'shop_id',
type: 'select',
options: searchShopList.value.map((shop) => ({ label: shop.shop_name, value: shop.id })),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchShop,
loading: shopLoading.value,
placeholder: '请选择或搜索店铺'
}
},
{
label: '状态',
prop: 'status',
type: 'select',
options: STATUS_SELECT_OPTIONS,
config: {
clearable: true,
placeholder: '请选择状态'
}
}
])
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '用户名', prop: 'username' },
{ label: '手机号', prop: 'phone' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' }
]
// 显示对话框
const showDialog = (type: string, row?: PlatformAccount) => {
dialogType.value = type
if (type === 'edit' && row) {
formData.id = row.id
formData.username = row.username
formData.phone = row.phone
formData.shop_id = row.shop_id || 0
formData.password = ''
} else {
formData.id = 0
formData.username = ''
formData.phone = ''
formData.shop_id = 0
formData.password = ''
}
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
dialogVisible.value = true
}
// 显示修改密码对话框
const showPasswordDialog = (row: PlatformAccount) => {
currentAccountId.value = row.id
passwordForm.new_password = ''
// 重置表单验证状态
nextTick(() => {
passwordFormRef.value?.clearValidate()
})
passwordDialogVisible.value = true
}
// 状态切换处理
const handleStatusChange = async (row: PlatformAccount, newStatus: number) => {
const oldStatus = row.status
// 先更新UI
row.status = newStatus
try {
await AccountService.updateAccountStatus(row.id, newStatus as 0 | 1)
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
row.status = oldStatus
console.error(error)
}
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID'
},
{
prop: 'username',
label: '用户名'
},
{
prop: 'phone',
label: '手机号'
},
{
prop: 'shop_name',
label: '店铺名称',
showOverflowTooltip: true
},
{
prop: 'status',
label: '状态',
formatter: (row: PlatformAccount) => {
return h(ElSwitch, {
modelValue: row.status,
activeValue: CommonStatus.ENABLED,
inactiveValue: CommonStatus.DISABLED,
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
'onUpdate:modelValue': (val: string | number | boolean) =>
handleStatusChange(row, val as number)
})
}
},
{
prop: 'created_at',
label: '创建时间',
formatter: (row: PlatformAccount) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 120,
fixed: 'right',
formatter: (row: PlatformAccount) => {
const buttons = []
if (hasAuth('shop_account:update_psd')) {
buttons.push(
h(ArtButtonTable, {
icon: '&#xe722;',
onClick: () => showPasswordDialog(row)
})
)
}
if (hasAuth('shop_account:edit')) {
buttons.push(
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
// 表单实例
const formRef = ref<FormInstance>()
const passwordFormRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
id: 0,
username: '',
password: '',
phone: '',
shop_id: 0
})
// 密码表单数据
const passwordForm = reactive({
new_password: ''
})
onMounted(() => {
getAccountList()
loadShopList()
loadSearchShopList()
})
// 加载店铺列表(用于新增/编辑对话框,默认加载20条)
const loadShopList = async (shopName?: string) => {
shopLoading.value = true
try {
const params: any = {
page: 1,
pageSize: 20
}
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
shopList.value = res.data.items || []
}
} catch (error) {
console.error('获取店铺列表失败:', error)
} finally {
shopLoading.value = false
}
}
// 加载搜索栏店铺列表(默认加载20条)
const loadSearchShopList = async (shopName?: string) => {
try {
const params: any = {
page: 1,
pageSize: 20
}
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
searchShopList.value = res.data.items || []
}
} catch (error) {
console.error('获取店铺列表失败:', error)
}
}
// 搜索店铺(用于新增/编辑对话框)
const searchShops = (query: string) => {
if (query) {
loadShopList(query)
} else {
loadShopList()
}
}
// 搜索店铺(用于搜索栏)
const handleSearchShop = (query: string) => {
if (query) {
loadSearchShopList(query)
} else {
loadSearchShopList()
}
}
// 提交修改密码
const handleChangePassword = async () => {
if (!passwordFormRef.value) return
await passwordFormRef.value.validate(async (valid) => {
if (valid) {
passwordSubmitLoading.value = true
try {
await AccountService.updateAccountPassword(
currentAccountId.value,
passwordForm.new_password
)
ElMessage.success('重置密码成功')
passwordDialogVisible.value = false
} catch (error) {
console.error(error)
} finally {
passwordSubmitLoading.value = false
}
}
})
}
// 获取账号列表
const getAccountList = async () => {
loading.value = true
try {
const params = {
page: pagination.currentPage,
pageSize: pagination.pageSize,
user_type: 3, // 筛选代理账号
username: searchForm.username || undefined,
phone: searchForm.phone || undefined,
shop_id: searchForm.shop_id,
status: searchForm.status
}
const res = await AccountService.getAccounts(params)
if (res.code === 0) {
tableData.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error('获取代理账号列表失败:', error)
} finally {
loading.value = false
}
}
const handleRefresh = () => {
getAccountList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 表单验证规则
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
],
shop_id: [
{ required: true, message: '请选择店铺', trigger: 'change' },
{ type: 'number', min: 0, message: '店铺ID必须大于0', trigger: 'change' }
]
})
// 密码验证规则
const passwordRules = reactive<FormRules>({
new_password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
if (dialogType.value === 'add') {
const data = {
username: formData.username,
password: formData.password,
phone: formData.phone,
user_type: 3, // 代理账号
shop_id: formData.shop_id
}
await AccountService.createAccount(data)
ElMessage.success('新增成功')
} else {
const data = {
username: formData.username
}
await AccountService.updateAccount(formData.id, data)
ElMessage.success('更新成功')
}
dialogVisible.value = false
getAccountList()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getAccountList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getAccountList()
}
</script>
<style lang="scss" scoped>
.shop-account-page {
// 代理账号管理页面样式
}
</style>

View File

@@ -1,269 +0,0 @@
<template>
<div class="page-content">
<h1 class="title">留言墙</h1>
<p class="desc">每一份留言都记录了您的想法也为我们提供了珍贵的回忆</p>
<div class="list">
<ul class="offset">
<li
class="comment-box"
v-for="item in commentList"
:key="item.id"
:style="{ background: randomColor() }"
@click="openDrawer(item)"
>
<p class="date">{{ item.date }}</p>
<p class="content">{{ item.content }}</p>
<div class="bottom">
<div class="left">
<span><i class="iconfont-sys">&#xe6eb;</i>{{ item.collection }}</span>
<span><i class="iconfont-sys">&#xe6e9;</i>{{ item.comment }}</span>
</div>
<div class="right">
<span>{{ item.userName }}</span>
</div>
</div>
</li>
</ul>
</div>
<ElDrawer
lDrawer
v-model="showDrawer"
:lock-scroll="false"
:size="360"
modal-class="comment-modal"
>
<template #header>
<h4>详情</h4>
</template>
<template #default>
<div class="drawer-default">
<div class="comment-box" :style="{ background: randomColor() }">
<p class="date">{{ clickItem.date }}</p>
<p class="content">{{ clickItem.content }}</p>
<div class="bottom">
<div class="left">
<span><i class="iconfont-sys">&#xe6eb;</i>{{ clickItem.collection }}</span>
<span><i class="iconfont-sys">&#xe6e9;</i>{{ clickItem.comment }}</span>
</div>
<div class="right">
<span>{{ clickItem.userName }}</span>
</div>
</div>
</div>
<!-- 评论组件 -->
<CommentWidget />
</div>
</template>
<template #footer>
<div>
<!-- <ElButton @click="cancelClick">cancel</ElButton> -->
<!-- <ElButton type="primary" @click="confirmClick">confirm</ElButton> -->
</div>
</template>
</ElDrawer>
</div>
</template>
<script setup lang="ts">
import { commentList } from '@/mock/temp/commentList'
const showDrawer = ref(false)
defineOptions({ name: 'ArticleComment' })
// const colorList = reactive([
// 'rgba(216, 248, 255, 0.8)',
// 'rgba(253, 223, 217, 0.8)',
// 'rgba(252, 230, 240, 0.8)',
// 'rgba(211, 248, 240, 0.8)',
// 'rgba(255, 234, 188, 0.8)',
// 'rgba(245, 225, 255, 0.8)',
// 'rgba(225, 230, 254, 0.8)'
// ])
const colorList = reactive([
'#D8F8FF',
'#FDDFD9',
'#FCE6F0',
'#D3F8F0',
'#FFEABC',
'#F5E1FF',
'#E1E6FE'
])
let lastColor: string | null = null
const randomColor = () => {
let newColor: string
do {
const index = Math.floor(Math.random() * colorList.length)
newColor = colorList[index]
} while (newColor === lastColor)
lastColor = newColor
return newColor
}
const clickItem = ref({
id: 1,
date: '2024-9-3',
content: '加油学好Node 自己写个小Demo',
collection: 5,
comment: 8,
userName: '匿名'
})
const openDrawer = (item: any) => {
showDrawer.value = true
clickItem.value = item
}
</script>
<style lang="scss" scoped>
.page-content {
background-color: transparent !important;
box-shadow: none !important;
:deep(.comment-modal) {
background-color: transparent;
}
.title {
margin-top: 20px;
font-size: 36px;
font-weight: 500;
}
.desc {
margin-top: 15px;
font-size: 14px;
color: var(--art-text-gray-600);
}
.list {
margin-top: 40px;
.offset {
display: flex;
flex-wrap: wrap;
width: calc(100% + 16px);
}
}
.comment-box {
position: relative;
box-sizing: border-box;
width: calc(20% - 16px);
aspect-ratio: 16 / 12;
padding: 16px;
margin: 0 16px 16px 0;
cursor: pointer;
background-color: #eae2cb;
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
}
.date {
font-size: 12px;
color: #949494;
}
.content {
margin-top: 16px;
font-size: 14px;
color: #333;
}
.bottom {
position: absolute;
bottom: 16px;
left: 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 16px;
.left {
display: flex;
align-items: center;
span {
display: flex;
align-items: center;
margin-right: 20px;
font-size: 12px;
color: #949494;
i {
margin-right: 5px;
}
}
}
.right {
span {
font-size: 14px;
color: #333;
}
}
}
}
.drawer-default {
.comment-box {
width: 100%;
&:hover {
transform: translateY(0);
}
}
}
}
@media screen and (max-width: $device-notebook) {
.page-content {
.comment-box {
width: calc(25% - 16px);
}
}
}
@media screen and (max-width: $device-ipad-pro) {
.page-content {
.comment-box {
width: calc(33.333% - 16px);
}
}
}
@media screen and (max-width: $device-ipad) {
.page-content {
.comment-box {
width: calc(50% - 16px);
}
}
}
@media screen and (max-width: $device-phone) {
.page-content {
.comment-box {
width: calc(100% - 16px);
}
}
}
.dark {
.page-content {
.comment-box {
color: #333 !important;
}
}
}
</style>

View File

@@ -1,122 +0,0 @@
<template>
<div class="article-detail page-content">
<div class="content">
<h1>{{ articleTitle }}</h1>
<div class="markdown-body" v-highlight v-html="articleHtml"></div>
</div>
<ArtBackToTop />
</div>
</template>
<script setup lang="ts">
import '@/assets/styles/markdown.scss'
import '@/assets/styles/one-dark-pro.scss'
import { useCommon } from '@/composables/useCommon'
import axios from 'axios'
// import 'highlight.js/styles/atom-one-dark.css';
// import 'highlight.js/styles/vs2015.css';
defineOptions({ name: 'ArticleDetail' })
const articleId = ref(0)
const router = useRoute()
const articleTitle = ref('')
const articleHtml = ref('')
onMounted(() => {
useCommon().scrollToTop()
articleId.value = Number(router.query.id)
getArticleDetail()
})
const getArticleDetail = async () => {
if (articleId.value) {
const res = await axios.get('https://www.qiniu.lingchen.kim/blog_detail.json')
if (res.data.code === 200) {
articleTitle.value = res.data.data.title
articleHtml.value = res.data.data.html_content
}
// const res = await ArticleService.getArticleDetail(articleId.value)
// if (res.code === ApiStatus.success) {
// articleTitle.value = res.data.title;;
// articleHtml.value = res.data.html_content;
// }
}
}
</script>
<style lang="scss">
.article-detail {
.content {
max-width: 800px;
margin: auto;
margin-top: 60px;
.markdown-body {
margin-top: 60px;
img {
width: 100%;
border: 1px solid var(--art-gray-200);
}
pre {
position: relative;
&:hover {
.copy-button {
opacity: 1;
}
}
&::before {
position: absolute;
top: 0;
left: 50px;
width: 1px;
height: 100%;
content: '';
background: #0a0a0e;
}
}
.code-wrapper {
overflow-x: auto;
}
.line-number {
position: sticky;
left: 0;
z-index: 2;
box-sizing: border-box;
display: inline-block;
width: 50px;
margin-right: 10px;
font-size: 14px;
color: #9e9e9e;
text-align: center;
}
.copy-button {
position: absolute;
top: 6px;
right: 6px;
z-index: 1;
width: 40px;
height: 40px;
font-size: 20px;
line-height: 40px;
color: #999;
text-align: center;
cursor: pointer;
background-color: #000;
border: none;
border-radius: 8px;
opacity: 0;
transition: all 0.2s;
}
}
}
}
</style>

View File

@@ -1,380 +0,0 @@
<template>
<div class="page-content article-list">
<ElRow justify="space-between" :gutter="10">
<ElCol :lg="6" :md="6" :sm="14" :xs="16">
<ElInput
v-model="searchVal"
:prefix-icon="Search"
clearable
placeholder="输入文章标题查询"
@keyup.enter="searchArticle"
/>
</ElCol>
<ElCol :lg="12" :md="12" :sm="0" :xs="0">
<div class="custom-segmented">
<ElSegmented v-model="yearVal" :options="options" @change="searchArticleByYear" />
</div>
</ElCol>
<ElCol :lg="6" :md="6" :sm="10" :xs="6" style="display: flex; justify-content: end">
<ElButton @click="toAddArticle" v-auth="'add'">新增文章</ElButton>
</ElCol>
</ElRow>
<div class="list">
<div class="offset">
<div class="item" v-for="item in articleList" :key="item.id" @click="toDetail(item)">
<!-- 骨架屏 -->
<ElSkeleton animated :loading="isLoading" style="width: 100%; height: 100%">
<template #template>
<div class="top">
<ElSkeletonItem
variant="image"
style="width: 100%; height: 100%; border-radius: 10px"
/>
<div style="padding: 16px 0">
<ElSkeletonItem variant="p" style="width: 80%" />
<ElSkeletonItem variant="p" style="width: 40%; margin-top: 10px" />
</div>
</div>
</template>
<template #default>
<div class="top">
<ElImage class="cover" :src="item.home_img" lazy fit="cover">
<template #error>
<div class="image-slot">
<ElIcon><icon-picture /></ElIcon>
</div>
</template>
</ElImage>
<span class="type">{{ item.type_name }}</span>
</div>
<div class="bottom">
<h2>{{ item.title }}</h2>
<div class="info">
<div class="text">
<i class="iconfont-sys">&#xe6f7;</i>
<span>{{ useDateFormat(item.create_time, 'YYYY-MM-DD') }}</span>
<div class="line"></div>
<i class="iconfont-sys">&#xe689;</i>
<span>{{ item.count }}</span>
</div>
<ElButton v-auth="'edit'" size="small" @click.stop="toEdit(item)">编辑</ElButton>
</div>
</div>
</template>
</ElSkeleton>
</div>
</div>
</div>
<div style="margin-top: 16vh" v-if="showEmpty">
<ElEmpty description="未找到相关数据" />
</div>
<div style="display: flex; justify-content: center; margin-top: 20px">
<ElPagination
size="default"
background
v-model:current-page="currentPage"
:page-size="pageSize"
:pager-count="9"
layout="prev, pager, next, total,jumper"
:total="total"
:hide-on-single-page="true"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { Picture as IconPicture } from '@element-plus/icons-vue'
import { ref, onMounted, computed } from 'vue'
import { router } from '@/router'
import { useDateFormat } from '@vueuse/core'
import { Search } from '@element-plus/icons-vue'
import { ArticleList } from '@/mock/temp/articleList'
import { useCommon } from '@/composables/useCommon'
import { RoutesAlias } from '@/router/routesAlias'
import { ArticleType } from '@/api/modules'
defineOptions({ name: 'ArticleList' })
const yearVal = ref('All')
const options = ['All', '2024', '2023', '2022', '2021', '2020', '2019']
const searchVal = ref('')
const articleList = ref<ArticleType[]>([])
const currentPage = ref(1)
const pageSize = ref(40)
// const lastPage = ref(0)
const total = ref(0)
const isLoading = ref(true)
const showEmpty = computed(() => {
return articleList.value.length === 0 && !isLoading.value
})
onMounted(() => {
getArticleList({ backTop: false })
})
// 搜索文章
const searchArticle = () => {
getArticleList({ backTop: true })
}
// 根据年份查询文章
const searchArticleByYear = () => {
getArticleList({ backTop: true })
}
const getArticleList = async ({ backTop = false }) => {
isLoading.value = true
// let year = yearVal.value
if (searchVal.value) {
yearVal.value = 'All'
}
if (yearVal.value === 'All') {
// year = ''
}
// const params = {
// page: currentPage.value,
// size: pageSize.value,
// searchVal: searchVal.value,
// year
// }
articleList.value = ArticleList
isLoading.value = false
if (backTop) {
useCommon().scrollToTop()
}
// const res = await ArticleService.getArticleList(params)
// if (res.code === ApiStatus.success) {
// currentPage.value = res.currentPage
// pageSize.value = res.pageSize
// lastPage.value = res.lastPage
// total.value = res.total
// articleList.value = res.data
// // setTimeout(() => {
// isLoading.value = false
// // }, 3000)
// if (searchVal.value) {
// searchVal.value = ''
// }
// }
}
const handleCurrentChange = (val: number) => {
currentPage.value = val
getArticleList({ backTop: true })
}
const toDetail = (item: ArticleType) => {
router.push({
path: RoutesAlias.ArticleDetail,
query: {
id: item.id
}
})
}
const toEdit = (item: ArticleType) => {
router.push({
path: RoutesAlias.ArticlePublish,
query: {
id: item.id
}
})
}
const toAddArticle = () => {
router.push({
path: RoutesAlias.ArticlePublish
})
}
</script>
<style lang="scss" scoped>
.article-list {
.custom-segmented .el-segmented {
height: 40px;
padding: 6px;
--el-border-radius-base: 8px;
}
.list {
margin-top: 20px;
.offset {
display: flex;
flex-wrap: wrap;
width: calc(100% + 20px);
.item {
box-sizing: border-box;
width: calc(20% - 20px);
margin: 0 20px 20px 0;
cursor: pointer;
border: 1px solid var(--art-border-color);
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
&:hover {
.el-button {
opacity: 1 !important;
}
}
.top {
position: relative;
aspect-ratio: 16/9.5;
.cover {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
object-fit: cover;
background: var(--art-gray-200);
border-radius: calc(var(--custom-radius) / 2 + 2px)
calc(var(--custom-radius) / 2 + 2px) 0 0;
.image-slot {
font-size: 26px;
color: var(--art-gray-400);
}
}
.type {
position: absolute;
top: 5px;
right: 5px;
padding: 5px 4px;
font-size: 12px;
color: rgba(#fff, 0.8);
background: rgba($color: #000, $alpha: 60%);
border-radius: 4px;
}
}
.bottom {
padding: 5px 10px;
h2 {
font-size: 16px;
font-weight: 500;
color: #333;
@include ellipsis();
}
.info {
display: flex;
justify-content: space-between;
width: 100%;
height: 25px;
margin-top: 6px;
line-height: 25px;
.text {
display: flex;
align-items: center;
color: var(--art-text-gray-600);
i {
margin-right: 5px;
font-size: 14px;
}
span {
font-size: 13px;
color: var(--art-gray-600);
}
.line {
width: 1px;
height: 12px;
margin: 0 15px;
background-color: var(--art-border-dashed-color);
}
}
.el-button {
opacity: 0;
transition: all 0.3s;
}
}
}
}
}
}
}
@media only screen and (max-width: $device-notebook) {
.article-list {
.list {
.offset {
.item {
width: calc(25% - 20px);
}
}
}
}
}
@media only screen and (max-width: $device-ipad-pro) {
.article-list {
.list {
.offset {
.item {
width: calc(33.333% - 20px);
.bottom {
h2 {
font-size: 16px;
}
}
}
}
}
}
}
@media only screen and (max-width: $device-ipad) {
.article-list {
.list {
.offset {
.item {
width: calc(50% - 20px);
}
}
}
}
}
@media only screen and (max-width: $device-phone) {
.article-list {
.list {
.offset {
.item {
width: calc(100% - 20px);
}
}
}
}
}
</style>

View File

@@ -1,411 +0,0 @@
<template>
<div class="article-edit">
<div>
<div class="editor-wrap">
<!-- 文章标题类型 -->
<ElRow :gutter="10">
<ElCol :span="18">
<ElInput
v-model.trim="articleName"
placeholder="请输入文章标题最多100个字符"
maxlength="100"
/>
</ElCol>
<ElCol :span="6">
<ElSelect v-model="articleType" placeholder="请选择文章类型" filterable>
<ElOption
v-for="item in articleTypes"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElCol>
</ElRow>
<!-- 富文本编辑器 -->
<ArtWangEditor class="el-top" v-model="editorHtml" />
<div class="form-wrap">
<h2>发布设置</h2>
<!-- 图片上传 -->
<ElForm>
<ElFormItem label="封面">
<div class="el-top upload-container">
<ElUpload
class="cover-uploader"
:action="uploadImageUrl"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="onSuccess"
:on-error="onError"
:before-upload="beforeUpload"
>
<div v-if="!cover" class="upload-placeholder">
<ElIcon class="upload-icon"><Plus /></ElIcon>
<div class="upload-text">点击上传封面</div>
</div>
<img v-else :src="cover" class="cover-image" />
</ElUpload>
<div class="el-upload__tip">建议尺寸 16:9jpg/png 格式</div>
</div>
</ElFormItem>
<ElFormItem label="可见">
<ElSwitch v-model="visible" />
</ElFormItem>
</ElForm>
<div style="display: flex; justify-content: flex-end">
<ElButton type="primary" @click="submit" style="width: 100px">
{{ pageMode === PageModeEnum.Edit ? '保存' : '发布' }}
</ElButton>
</div>
</div>
</div>
</div>
<!-- <div class="outline-wrap">
<div class="item" v-for="(item, index) in outlineList" :key="index">
<p :class="`level${item.level}`">{{ item.text }}</p>
</div>
</div> -->
</div>
</template>
<script setup lang="ts">
import { Plus } from '@element-plus/icons-vue'
import { ArticleService } from '@/api/articleApi'
import { ApiStatus } from '@/utils/http/status'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
import { PageModeEnum } from '@/enums/formEnum'
import axios from 'axios'
import { useCommon } from '@/composables/useCommon'
defineOptions({ name: 'ArticlePublish' })
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
let { accessToken } = userStore
// 上传路径
const uploadImageUrl = `${import.meta.env.VITE_API_URL}/api/common/upload`
// 传递 token
const uploadHeaders = { Authorization: accessToken }
let pageMode: PageModeEnum = PageModeEnum.Add // 页面类型 新增 编辑
const articleName = ref('') // 文章标题
const articleType = ref() // 文章类型
const articleTypes = ref() // 类型列表
const editorHtml = ref('') // 编辑器内容
const createDate = ref('') // 创建时间
const cover = ref('') // 图片
const visible = ref(true) // 可见
// const outlineList = ref()
onMounted(() => {
useCommon().scrollToTop()
getArticleTypes()
initPageMode()
})
// 初始化页面类型 新增 编辑
const initPageMode = () => {
const { id } = route.query
pageMode = id ? PageModeEnum.Edit : PageModeEnum.Add
if (pageMode === PageModeEnum.Edit && id) {
initEditArticle(Number(id))
} else {
initAddArticle()
}
}
// 初始化编辑文章的逻辑
const initEditArticle = (id: number) => {
articleId = id
getArticleDetail()
}
// 初始化新增文章逻辑
const initAddArticle = () => {
createDate.value = formDate(useNow().value)
}
// 获取文章类型
const getArticleTypes = async () => {
try {
const response = await axios.get('https://www.qiniu.lingchen.kim/classify.json')
if (response.data.code === 200) {
articleTypes.value = response.data.data
}
} catch (error) {
console.error('Error fetching JSON data:', error)
}
// try {
// const res = await ArticleService.getArticleTypes({})
// if (res.code === ApiStatus.success) {
// articleTypes.value = res.data
// }
// } catch (err) { }
}
// 获取文章详情内容
let articleId: number = 0
const getArticleDetail = async () => {
const res = await axios.get('https://www.qiniu.lingchen.kim/blog_list.json')
if (res.data.code === ApiStatus.success) {
let { title, blog_class, html_content } = res.data.data
articleName.value = title
articleType.value = Number(blog_class)
editorHtml.value = html_content
}
// const res = await ArticleService.getArticleDetail(articleId)
// if (res.code === ApiStatus.success) {
// let { title, blog_class, create_time, home_img, html_content } = res.data
// articleName.value = title
// articleType.value = Number(blog_class)
// editorHtml.value = html_content
// cover.value = home_img
// createDate.value = formDate(create_time)
// // getOutline(html_content)
// }
}
// const getOutline = (content: string) => {
// const regex = /<h([1-3])>(.*?)<\/h\1>/g
// const headings = []
// let match
// while ((match = regex.exec(content)) !== null) {
// headings.push({ level: match[1], text: match[2] })
// }
// outlineList.value = headings
// }
// 提交
const submit = () => {
if (pageMode === PageModeEnum.Edit) {
editArticle()
} else {
addArticle()
}
}
// 格式化日期
const formDate = (date: string | Date): string => {
return useDateFormat(date, 'YYYY-MM-DD').value
}
// 验证输入
const validateArticle = () => {
if (!articleName.value) {
ElMessage.error(`请输入文章标题`)
return false
}
if (!articleType.value) {
ElMessage.error(`请选择文章类型`)
return false
}
if (editorHtml.value === '<p><br></p>') {
ElMessage.error(`请输入文章内容`)
return false
}
if (!cover.value) {
ElMessage.error(`请上传图片`)
return false
}
return true
}
// 构建参数
const buildParams = () => {
return {
title: articleName.value,
html_content: editorHtml.value,
home_img: cover.value,
blog_class: articleType.value,
create_time: createDate.value
}
}
// 添加文章
const addArticle = async () => {
try {
if (!validateArticle()) return
editorHtml.value = delCodeTrim(editorHtml.value)
const params = buildParams()
const res = await ArticleService.addArticle(params)
if (res.code === ApiStatus.success) {
ElMessage.success('发布成功')
goBack()
}
} catch (err) {
console.error(err)
}
}
// 编辑文章
const editArticle = async () => {
try {
if (!validateArticle()) return
editorHtml.value = delCodeTrim(editorHtml.value)
const params = buildParams()
const res = await ArticleService.editArticle(articleId, params)
if (res.code === ApiStatus.success) {
ElMessage.success('修改成功')
goBack()
}
} catch (err) {
console.error(err)
}
}
const delCodeTrim = (content: string): string => {
return content.replace(/(\s*)<\/code>/g, '</code>')
}
const onSuccess = (response: any) => {
cover.value = response.data.url
ElMessage.success('图片上传成功')
}
const onError = () => {
ElMessage.error('图片上传失败')
}
// 返回上一页
const goBack = () => {
setTimeout(() => {
router.go(-1)
}, 800)
}
// 添加上传前的校验
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
</script>
<style lang="scss" scoped>
.article-edit {
.editor-wrap {
max-width: 1000px;
margin: 20px auto;
.el-top {
margin-top: 10px;
}
.form-wrap {
padding: 20px;
margin-top: 20px;
background-color: var(--art-main-bg-color);
border: 1px solid var(--art-border-color);
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
h2 {
margin-bottom: 20px;
font-size: 20px;
font-weight: 500;
}
}
}
.outline-wrap {
box-sizing: border-box;
width: 280px;
padding: 20px;
border: 1px solid #e3e3e3;
border-radius: 8px;
.item {
p {
height: 30px;
font-size: 13px;
line-height: 30px;
cursor: pointer;
}
.level3 {
padding-left: 10px;
}
}
}
.upload-container {
.cover-uploader {
position: relative;
overflow: hidden;
cursor: pointer;
border-radius: 6px;
transition: var(--el-transition-duration);
&:hover {
border-color: var(--el-color-primary);
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 260px;
height: 160px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
.upload-icon {
font-size: 28px;
color: #8c939d;
}
.upload-text {
margin-top: 8px;
font-size: 14px;
color: #8c939d;
}
}
.cover-image {
display: block;
width: 260px;
height: 160px;
object-fit: cover;
}
}
.el-upload__tip {
margin-top: 8px;
font-size: 12px;
color: #666;
}
}
}
</style>

View File

@@ -3811,27 +3811,27 @@
.wallet-balance-cards {
display: flex;
flex-direction: column;
gap: 14px;
gap: 8px;
.balance-card {
display: flex;
gap: 18px;
gap: 10px;
align-items: center;
padding: 18px 20px;
padding: 10px 14px;
background: var(--el-fill-color-light);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
border-radius: 4px;
transition: all 0.3s ease;
&:hover {
border-color: var(--el-color-primary);
box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
transform: translateX(4px);
box-shadow: 0 2px 6px rgb(0 0 0 / 6%);
transform: translateX(2px);
}
.balance-icon {
flex-shrink: 0;
font-size: 32px;
font-size: 20px;
line-height: 1;
}
@@ -3839,17 +3839,17 @@
display: flex;
flex: 1;
flex-direction: column;
gap: 6px;
gap: 2px;
.balance-label {
font-size: 13px;
font-size: 11px;
font-weight: 500;
color: var(--el-text-color-secondary);
}
.balance-value {
font-size: 22px;
font-weight: 700;
font-size: 16px;
font-weight: 600;
line-height: 1.2;
color: var(--el-text-color-primary);
}

View File

@@ -1,542 +0,0 @@
<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 {
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.stat-icon {
font-size: 40px;
opacity: 0.6;
}
}
}
</style>

View File

@@ -439,46 +439,6 @@
</div>
</ElDialog>
<!-- 设置限速对话框 -->
<ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="50%">
<ElForm
ref="speedLimitFormRef"
:model="speedLimitForm"
:rules="speedLimitRules"
label-width="120px"
>
<ElFormItem label="设备号">
<span style="font-weight: bold; color: #409eff">{{ currentOperatingImei }}</span>
</ElFormItem>
<ElFormItem label="下行速率" prop="download_speed">
<ElInputNumber
v-model="speedLimitForm.download_speed"
:min="1"
:step="128"
controls-position="right"
style="width: 100%"
/>
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
</ElFormItem>
<ElFormItem label="上行速率" prop="upload_speed">
<ElInputNumber
v-model="speedLimitForm.upload_speed"
:min="1"
:step="128"
controls-position="right"
style="width: 100%"
/>
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="speedLimitDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleConfirmSpeedLimit" :loading="speedLimitLoading">
确认设置
</ElButton>
</template>
</ElDialog>
<!-- 切换SIM卡对话框 -->
<ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="600px">
<div v-if="loadingDeviceCards" style="padding: 40px; text-align: center">
@@ -604,6 +564,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { RoutesAlias } from '@/router/routesAlias'
import {
DeviceService,
ShopService,
@@ -615,10 +576,8 @@
ElMessage,
ElMessageBox,
ElTag,
ElSwitch,
ElIcon,
ElTreeSelect,
ElInputNumber,
ElRadioGroup,
ElRadio
} from 'element-plus'
@@ -634,9 +593,7 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants'
import type { PackageSeriesResponse } from '@/types/api'
defineOptions({ name: 'DeviceList' })
@@ -694,23 +651,6 @@
})
// 设备操作相关对话框
const speedLimitDialogVisible = ref(false)
const speedLimitLoading = ref(false)
const speedLimitFormRef = ref<FormInstance>()
const speedLimitForm = reactive({
download_speed: 1024,
upload_speed: 512
})
const speedLimitRules = reactive<FormRules>({
download_speed: [
{ required: true, message: '请输入下行速率', trigger: 'blur' },
{ type: 'number', min: 1, message: '速率不能小于1KB/s', trigger: 'blur' }
],
upload_speed: [
{ required: true, message: '请输入上行速率', trigger: 'blur' },
{ type: 'number', min: 1, message: '速率不能小于1KB/s', trigger: 'blur' }
]
})
const currentOperatingImei = ref<string>('') // 用于存储当前操作设备的IMEI
const currentOperatingDevice = ref<Device | null>(null)
@@ -872,7 +812,7 @@
const goToDeviceSearchDetail = (deviceNo: string) => {
if (hasAuth('device:view_detail')) {
router.push({
path: '/asset-management/asset-information',
path: RoutesAlias.AssetInformation,
query: {
virtual_no: deviceNo
}
@@ -975,7 +915,7 @@
// 重置表单
bindCardForm.iot_card_id = undefined
bindCardForm.slot_position = 1
bindCardFormRef.value.resetFields()
bindCardFormRef.value?.resetFields()
// 重新加载卡列表
await loadDeviceCards(currentDeviceDetail.value.virtual_no)
// 刷新设备详情以更新绑定卡数量
@@ -1016,7 +956,7 @@
currentDeviceDetail.value = detailRes.data
}
} else {
ElMessage.error(res.message || '解绑失败')
ElMessage.error(res.msg || '解绑失败')
}
} catch (error: any) {
console.error('解绑卡失败:', error)
@@ -1027,28 +967,6 @@
})
}
// 获取卡状态标签类型
const getCardStatusTagType = (status: number) => {
const typeMap: Record<number, any> = {
1: 'info', // 在库
2: 'warning', // 已分销
3: 'success', // 已激活
4: 'danger' // 已停用
}
return typeMap[status] || 'info'
}
// 获取卡状态文本
const getCardStatusText = (status: number) => {
const textMap: Record<number, string> = {
1: '在库',
2: '已分销',
3: '已激活',
4: '已停用'
}
return textMap[status] || '未知'
}
// 获取网络状态标签类型
const getNetworkStatusTagType = (networkStatus: number) => {
// 0: 停机 (danger 红色)
@@ -1581,22 +1499,13 @@
handleResetDevice(imei)
break
}
case 'speed-limit': {
const imei = device?.imei || (await getDeviceImei(deviceNo))
if (!imei) {
ElMessage.error('无法获取设备IMEI,无法执行限速操作')
return
}
showSpeedLimitDialog(imei)
break
}
case 'switch-card': {
const imei = device?.imei || (await getDeviceImei(deviceNo))
if (!imei) {
ElMessage.error('无法获取设备IMEI,无法执行切卡操作')
return
}
showSwitchCardDialog(imei)
await showSwitchCardDialog(imei)
break
}
case 'set-wifi': {
@@ -1612,7 +1521,7 @@
handleManualDeactivateDevice()
break
case 'delete':
handleDeleteDeviceByNo(deviceNo)
await handleDeleteDeviceByNo(deviceNo)
break
}
}
@@ -1649,7 +1558,7 @@
const res = await AssetService.deactivateAsset(device.virtual_no)
if (res.code === 0) {
ElMessage.success('手动停用成功')
loadDeviceList()
await getTableData()
}
} catch (error: any) {
console.error('手动停用失败:', error)
@@ -1684,7 +1593,7 @@
if (res.code === 0) {
ElMessage.success('重启指令已发送')
} else {
ElMessage.error(res.message || '重启失败')
ElMessage.error(res.msg || '重启失败')
}
} catch (error: any) {
console.error('重启设备失败:', error)
@@ -1712,7 +1621,7 @@
if (res.code === 0) {
ElMessage.success('恢复出厂设置指令已发送')
} else {
ElMessage.error(res.message || '操作失败')
ElMessage.error(res.msg || '操作失败')
}
} catch (error: any) {
console.error('恢复出厂设置失败:', error)
@@ -1723,41 +1632,6 @@
})
}
// 显示设置限速对话框
const showSpeedLimitDialog = (imei: string) => {
currentOperatingImei.value = imei
speedLimitForm.download_speed = 1024
speedLimitForm.upload_speed = 512
speedLimitDialogVisible.value = true
}
// 确认设置限速
const handleConfirmSpeedLimit = async () => {
if (!speedLimitFormRef.value) return
await speedLimitFormRef.value.validate(async (valid) => {
if (valid) {
speedLimitLoading.value = true
try {
const res = await DeviceService.setSpeedLimit(currentOperatingImei.value, {
download_speed: speedLimitForm.download_speed,
upload_speed: speedLimitForm.upload_speed
})
if (res.code === 0) {
ElMessage.success('限速设置成功')
speedLimitDialogVisible.value = false
} else {
ElMessage.error(res.message || '设置失败')
}
} catch (error: any) {
console.error('设置限速失败:', error)
} finally {
speedLimitLoading.value = false
}
}
})
}
// 显示切换SIM卡对话框
const showSwitchCardDialog = async (imei: string) => {
currentOperatingImei.value = imei
@@ -1812,7 +1686,7 @@
ElMessage.success('切换SIM卡指令已发送')
switchCardDialogVisible.value = false
} else {
ElMessage.error(res.message || '切换失败')
ElMessage.error(res.msg || '切换失败')
}
} catch (error: any) {
console.error('切换SIM卡失败:', error)
@@ -1849,7 +1723,7 @@
ElMessage.success('WiFi设置成功')
setWiFiDialogVisible.value = false
} else {
ElMessage.error(res.message || '设置失败')
ElMessage.error(res.msg || '设置失败')
}
} catch (error: any) {
console.error('设置WiFi失败:', error)

View File

@@ -1,215 +0,0 @@
<template>
<div class="device-search-page">
<!-- 搜索区域 -->
<ElCard shadow="never" class="search-card">
<template #header>
<div class="card-header">
<span>设备查询</span>
</div>
</template>
<div class="search-content">
<ElForm :model="searchForm" label-width="100px">
<ElFormItem label="设备号(IMEI)">
<ElInput
v-model="searchForm.imei"
placeholder="请输入设备号(IMEI)"
clearable
@keyup.enter="handleSearch"
>
<template #append>
<ElButton type="primary" :loading="loading" @click="handleSearch"> 查询 </ElButton>
</template>
</ElInput>
</ElFormItem>
</ElForm>
</div>
</ElCard>
<!-- 设备详情区域 -->
<ElCard v-if="deviceDetail" shadow="never" class="detail-card" style="margin-top: 16px">
<template #header>
<div class="card-header">
<span>设备详情</span>
</div>
</template>
<ElDescriptions :column="3" border>
<ElDescriptionsItem label="设备ID">{{ deviceDetail.id }}</ElDescriptionsItem>
<ElDescriptionsItem label="设备号" :span="2">{{
deviceDetail.virtual_no
}}</ElDescriptionsItem>
<ElDescriptionsItem label="设备名称">{{
deviceDetail.device_name || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="设备型号">{{
deviceDetail.device_model || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="设备类型">{{
deviceDetail.device_type || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="制造商">{{
deviceDetail.manufacturer || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="最大插槽数">{{ deviceDetail.max_sim_slots }}</ElDescriptionsItem>
<ElDescriptionsItem label="已绑定卡数量">{{
deviceDetail.bound_card_count
}}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="getStatusTagType(deviceDetail.status)">
{{ deviceDetail.status_name }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="在线状态">
<ElTag :type="getOnlineStatusTagType(deviceDetail.online_status)">
{{ getOnlineStatusText(deviceDetail.online_status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="店铺名称">{{
deviceDetail.shop_name || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="固件版本">{{
deviceDetail.software_version || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="切卡模式">{{
getSwitchModeText(deviceDetail.switch_mode)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="批次号">{{ deviceDetail.batch_no }}</ElDescriptionsItem>
<ElDescriptionsItem label="最后在线时间">{{
deviceDetail.last_online_time || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="最后同步时间">{{
deviceDetail.last_gateway_sync_at || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="激活时间">{{
deviceDetail.activated_at || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{ deviceDetail.created_at }}</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间">{{ deviceDetail.updated_at }}</ElDescriptionsItem>
<ElDescriptionsItem></ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<!-- 空状态 -->
<ElEmpty v-if="searched && !deviceDetail && !loading" description="未找到相关设备信息" />
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { AssetService } from '@/api/modules'
defineOptions({ name: 'DeviceSearch' })
const loading = ref(false)
const searched = ref(false)
const searchForm = reactive({
imei: ''
})
const deviceDetail = ref<any>(null)
// 查询设备详情
const handleSearch = async () => {
if (!searchForm.imei.trim()) {
ElMessage.warning('请输入设备号(IMEI)')
return
}
loading.value = true
searched.value = true
deviceDetail.value = null
try {
const res = await AssetService.resolveAsset(searchForm.imei.trim())
if (res.code === 0 && res.data) {
deviceDetail.value = res.data
ElMessage.success('查询成功')
} else {
ElMessage.error(res.msg || '查询失败')
}
} catch (error: any) {
console.error('查询设备详情失败:', error)
console.log(error?.message || '查询失败,请检查设备号是否正确')
} finally {
loading.value = false
}
}
// 获取状态标签类型
const getStatusTagType = (status: number) => {
const typeMap: Record<number, any> = {
1: 'info', // 在库
2: 'warning', // 已分销
3: 'success', // 已激活
4: 'danger' // 已停用
}
return typeMap[status] || 'info'
}
// 获取在线状态标签类型
const getOnlineStatusTagType = (status?: number) => {
const typeMap: Record<number, any> = {
0: 'info', // 未知
1: 'success', // 在线
2: 'danger' // 离线
}
return typeMap[status ?? 0] || 'info'
}
// 获取在线状态文本
const getOnlineStatusText = (status?: number) => {
const textMap: Record<number, string> = {
0: '未知',
1: '在线',
2: '离线'
}
return textMap[status ?? 0] || '未知'
}
// 获取切卡模式文本
const getSwitchModeText = (mode?: string) => {
const modeMap: Record<string, string> = {
'0': '自动',
'1': '手动'
}
return modeMap[mode || ''] || '--'
}
</script>
<style lang="scss" scoped>
.device-search-page {
padding: 16px;
.card-header {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.search-content {
max-width: 800px;
}
.detail-card {
animation: fadeIn 0.3s ease-in;
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -31,28 +31,16 @@
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:actionsWidth="120"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="140"
@select="handleContextMenuSelect"
/>
<!-- 创建换货单对话框 -->
<ElDialog
v-model="createDialogVisible"
@@ -217,25 +205,15 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
defineOptions({ name: 'ExchangeManagement' })
const { hasAuth } = useAuth()
const router = useRouter()
// 右键菜单相关
const { showContextMenuHint, hintPosition, handleCellMouseEnter, handleCellMouseLeave } =
useTableContextMenu()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentExchangeRow = ref<ExchangeResponse | null>(null)
const loading = ref(false)
const shipDialogVisible = ref(false)
const shipLoading = ref(false)
@@ -621,17 +599,18 @@
router.push(`${RoutesAlias.ExchangeDetail}/${row.id}`)
}
// 右键菜单项
const contextMenuItems = computed<MenuItemType[]>(() => {
const items: MenuItemType[] = []
const status = currentExchangeRow.value?.status
// 获取操作按钮
const getActions = (row: ExchangeResponse) => {
const actions: any[] = []
const status = row.status
// 状态1待填写信息取消换货
if (status === 1) {
if (hasAuth('exchange:cancel')) {
items.push({
key: 'cancel',
label: '取消换货'
actions.push({
label: '取消换货',
handler: () => handleCancelExchange(row),
type: 'danger'
})
}
}
@@ -639,15 +618,17 @@
// 状态2待发货发货、取消换货
if (status === 2) {
if (hasAuth('exchange:ship')) {
items.push({
key: 'ship',
label: '发货'
actions.push({
label: '发货',
handler: () => handleShipExchange(row),
type: 'primary'
})
}
if (hasAuth('exchange:cancel')) {
items.push({
key: 'cancel',
label: '取消换货'
actions.push({
label: '取消换货',
handler: () => handleCancelExchange(row),
type: 'danger'
})
}
}
@@ -655,9 +636,10 @@
// 状态3已发货待确认确认完成
if (status === 3) {
if (hasAuth('exchange:complete')) {
items.push({
key: 'complete',
label: '确认完成'
actions.push({
label: '确认完成',
handler: () => handleCompleteExchange(row),
type: 'success'
})
}
}
@@ -665,42 +647,15 @@
// 状态4已完成旧资产转新
if (status === 4) {
if (hasAuth('exchange:renew')) {
items.push({
key: 'renew',
label: '旧资产转新'
actions.push({
label: '旧资产转新',
handler: () => handleRenewExchange(row),
type: 'warning'
})
}
}
return items
})
// 处理右键菜单
const handleRowContextMenu = (row: ExchangeResponse, _column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentExchangeRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentExchangeRow.value) return
switch (item.key) {
case 'cancel':
handleCancelExchange(currentExchangeRow.value)
break
case 'ship':
handleShipExchange(currentExchangeRow.value)
break
case 'complete':
handleCompleteExchange(currentExchangeRow.value)
break
case 'renew':
handleRenewExchange(currentExchangeRow.value)
break
}
return actions
}
// 取消换货

View File

@@ -572,6 +572,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { RoutesAlias } from '@/router/routesAlias'
import { CardService, ShopService, PackageSeriesService, AssetService } from '@/api/modules'
import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
@@ -906,7 +907,7 @@
const goToCardDetail = (iccid: string) => {
if (hasAuth('iot_card:view_detail')) {
router.push({
path: '/asset-management/asset-information',
path: RoutesAlias.AssetInformation,
query: {
iccid: iccid
}

View File

@@ -1,217 +0,0 @@
<template>
<div class="card-search-page">
<!-- 搜索区域 -->
<ElCard shadow="never" class="search-card">
<template #header>
<div class="card-header">
<span>单卡查询</span>
</div>
</template>
<div class="search-content">
<ElForm :model="searchForm" label-width="100px">
<ElFormItem label="ICCID">
<ElInput
v-model="searchForm.iccid"
placeholder="请输入ICCID"
clearable
@keyup.enter="handleSearch"
>
<template #append>
<ElButton type="primary" :loading="loading" @click="handleSearch"> 查询 </ElButton>
</template>
</ElInput>
</ElFormItem>
</ElForm>
</div>
</ElCard>
<!-- 卡片详情区域 -->
<ElCard v-if="cardDetail" shadow="never" class="detail-card" style="margin-top: 16px">
<template #header>
<div class="card-header">
<span>卡片详情</span>
</div>
</template>
<ElDescriptions :column="3" border>
<ElDescriptionsItem label="卡ID">{{ cardDetail.id }}</ElDescriptionsItem>
<ElDescriptionsItem label="ICCID" :span="2">{{ cardDetail.iccid }}</ElDescriptionsItem>
<ElDescriptionsItem label="IMSI">{{ cardDetail.imsi || '--' }}</ElDescriptionsItem>
<ElDescriptionsItem label="卡接入号">{{ cardDetail.msisdn || '--' }}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商">{{ cardDetail.carrier_name }}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商类型">{{
getCarrierTypeText(cardDetail.carrier_type)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="卡类型">{{ cardDetail.card_type }}</ElDescriptionsItem>
<ElDescriptionsItem label="卡业务类型">{{
getCardCategoryText(cardDetail.card_category)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="getStatusTagType(cardDetail.status)">
{{ getStatusText(cardDetail.status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="激活状态">
<ElTag :type="cardDetail.activation_status === 1 ? 'success' : 'info'">
{{ cardDetail.activation_status === 1 ? '已激活' : '未激活' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="网络状态">
<ElTag :type="cardDetail.network_status === 1 ? 'success' : 'danger'">
{{ cardDetail.network_status === 1 ? '开机' : '停机' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="实名状态">
<ElTag :type="cardDetail.real_name_status === 1 ? 'success' : 'warning'">
{{ cardDetail.real_name_status === 1 ? '已实名' : '未实名' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="批次号">{{ cardDetail.batch_no }}</ElDescriptionsItem>
<ElDescriptionsItem label="供应商">{{ cardDetail.supplier || '--' }}</ElDescriptionsItem>
<ElDescriptionsItem label="店铺名称">{{ cardDetail.shop_name || '--' }}</ElDescriptionsItem>
<ElDescriptionsItem label="成本价">{{
formatPrice(cardDetail.cost_price)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="分销价">{{
formatPrice(cardDetail.distribute_price)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="累计流量使用"
>{{ cardDetail.data_usage_mb }} MB</ElDescriptionsItem
>
<ElDescriptionsItem label="激活时间">{{
cardDetail.activated_at || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{ cardDetail.created_at }}</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<!-- 空状态 -->
<ElEmpty v-if="searched && !cardDetail && !loading" description="未找到相关卡片信息" />
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { CardService } from '@/api/modules/card'
defineOptions({ name: 'CardSearch' })
const loading = ref(false)
const searched = ref(false)
const searchForm = reactive({
iccid: ''
})
const cardDetail = ref<any>(null)
// 查询卡片详情
const handleSearch = async () => {
if (!searchForm.iccid.trim()) {
ElMessage.warning('请输入ICCID')
return
}
loading.value = true
searched.value = true
cardDetail.value = null
try {
const res = await CardService.getIotCardDetailByIccid(searchForm.iccid.trim())
if (res.code === 0 && res.data) {
cardDetail.value = res.data
ElMessage.success('查询成功')
} else {
ElMessage.error(res.message || '查询失败')
}
} catch (error: any) {
console.error('查询卡片详情失败:', error)
} finally {
loading.value = false
}
}
// 获取运营商类型文本
const getCarrierTypeText = (type: string) => {
const typeMap: Record<string, string> = {
CMCC: '中国移动',
CUCC: '中国联通',
CTCC: '中国电信',
CBN: '中国广电'
}
return typeMap[type] || type
}
// 获取卡业务类型文本
const getCardCategoryText = (category: string) => {
const categoryMap: Record<string, string> = {
normal: '普通卡',
industry: '行业卡'
}
return categoryMap[category] || category
}
// 获取状态文本
const getStatusText = (status: number) => {
const statusMap: Record<number, string> = {
1: '在库',
2: '已分销',
3: '已激活',
4: '已停用'
}
return statusMap[status] || '未知'
}
// 获取状态标签类型
const getStatusTagType = (status: number) => {
const typeMap: Record<number, any> = {
1: 'info',
2: 'warning',
3: 'success',
4: 'danger'
}
return typeMap[status] || 'info'
}
// 格式化价格(分转元)
const formatPrice = (price: number) => {
return `¥${(price / 100).toFixed(2)}`
}
</script>
<style lang="scss" scoped>
.card-search-page {
padding: 16px;
.card-header {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.search-content {
max-width: 800px;
}
.detail-card {
animation: fadeIn 0.3s ease-in;
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -39,28 +39,15 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="120"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</ElCard>
</div>
@@ -124,6 +111,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { ref, reactive, onMounted } from 'vue'
import { DeviceService } from '@/api/modules'
import { ElMessage, ElTag } from 'element-plus'
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
@@ -131,12 +119,7 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { StorageService } from '@/api/modules/storage'
import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device'
import { RoutesAlias } from '@/router/routesAlias'
@@ -146,28 +129,18 @@
const router = useRouter()
const { hasAuth } = useAuth()
// 使
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false)
const tableRef = ref()
const uploadRef = ref<UploadInstance>()
const fileList = ref<File[]>([])
const uploading = ref(false)
const importDialogVisible = ref(false)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<DeviceImportTask | null>(null)
//
const initialSearchState = {
status: undefined,
batch_no: '',
dateRange: undefined as any,
start_time: '',
end_time: ''
}
@@ -560,7 +533,8 @@
})
if (uploadUrlRes.code !== 0) {
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
return
}
const { upload_url, file_key } = uploadUrlRes.data
@@ -579,13 +553,14 @@
})
if (importRes.code !== 0) {
throw new Error(importRes.msg || '创建导入任务失败')
ElMessage.error(importRes.msg || '创建导入任务失败')
return
}
const taskNo = importRes.data.task_no
handleCancelImport()
getTableData()
await getTableData()
ElMessage.success({
message: `导入任务已创建!任务编号:${taskNo}`,
@@ -594,6 +569,7 @@
})
} catch (error: any) {
console.error('设备导入失败:', error)
ElMessage.error(error.message || '设备导入失败')
} finally {
uploading.value = false
}
@@ -646,36 +622,19 @@
}
}
//
const contextMenuItems = computed((): MenuItemType[] => {
if (!currentRow.value) return []
//
const getActions = (row: DeviceImportTask) => {
const actions: any[] = []
const items: MenuItemType[] = []
if (currentRow.value.fail_count > 0 && hasAuth('device_task:download_fail_data')) {
items.push({ key: 'failData', label: '失败数据' })
if (row.fail_count > 0 && hasAuth('device_task:download_fail_data')) {
actions.push({
label: '失败数据',
handler: () => downloadFailDataByRow(row),
type: 'danger'
})
}
return items
})
//
const handleRowContextMenu = (row: DeviceImportTask, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
//
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'failData':
downloadFailDataByRow(currentRow.value)
break
}
return actions
}
</script>
@@ -697,8 +656,4 @@
}
}
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -39,28 +39,15 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="120"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</ElCard>
</div>
@@ -185,6 +172,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { ref, reactive, computed, onMounted } from 'vue'
import { CardService, CarrierService } from '@/api/modules'
import {
ElMessage,
@@ -200,11 +188,7 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { StorageService } from '@/api/modules/storage'
import { RoutesAlias } from '@/router/routesAlias'
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
@@ -216,15 +200,6 @@
const { hasAuth } = useAuth()
// 使
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false)
const tableRef = ref()
const uploadRef = ref<UploadInstance>()
@@ -234,8 +209,6 @@
const selectedCarrierId = ref<number>()
const carrierList = ref<Carrier[]>([])
const carrierLoading = ref(false)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<IotCardImportTask | null>(null)
const failDataDialogVisible = ref(false)
const failDataLoading = ref(false)
const failedItems = ref<any[]>([])
@@ -280,7 +253,7 @@
label: '运营商',
prop: 'carrier_id',
type: 'select',
options: carrierList.value.map((carrier) => ({
options: carrierList.value.map((carrier: Carrier) => ({
label: carrier.carrier_name,
value: carrier.id
})),
@@ -738,7 +711,8 @@
})
if (uploadUrlRes.code !== 0) {
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
return
}
const { upload_url, file_key } = uploadUrlRes.data
@@ -758,7 +732,8 @@
})
if (importRes.code !== 0) {
throw new Error(importRes.msg || '创建导入任务失败')
ElMessage.error(importRes.msg || '创建导入任务失败')
return
}
const taskNo = importRes.data.task_no
@@ -773,41 +748,25 @@
})
} catch (error: any) {
console.error('IoT卡导入失败:', error)
ElMessage.error(error.message || 'IoT卡导入失败')
} finally {
uploading.value = false
}
}
//
const contextMenuItems = computed((): MenuItemType[] => {
if (!currentRow.value) return []
//
const getActions = (row: IotCardImportTask) => {
const actions: any[] = []
const items: MenuItemType[] = []
if (currentRow.value.fail_count > 0 && hasAuth('iot_card_task:download_fail_data')) {
items.push({ key: 'failData', label: '失败数据' })
if (row.fail_count > 0 && hasAuth('iot_card_task:download_fail_data')) {
actions.push({
label: '失败数据',
handler: () => showFailDataDialog(row),
type: 'danger'
})
}
return items
})
//
const handleRowContextMenu = (row: IotCardImportTask, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
//
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'failData':
showFailDataDialog(currentRow.value)
break
}
return actions
}
</script>
@@ -829,8 +788,4 @@
}
}
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -47,16 +47,17 @@
</template>
<script setup lang="ts">
import { h, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { computed, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ref, onMounted } from 'vue'
import { CardService, DeviceService } from '@/api/modules'
import { ElMessage, ElTag, ElDivider, ElTable, ElTableColumn, ElIcon } from 'element-plus'
import { ElDivider, ElIcon, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/business/format'
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
import type { DeviceImportTaskDetail } from '@/types/api/device'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import DetailPage from '@/components/common/DetailPage.vue'
defineOptions({ name: 'TaskDetail' })
@@ -89,11 +90,11 @@
//
const detailSections = computed((): DetailSection[] => {
const sections: DetailSection[] = [
return [
{
title: '任务基本信息',
fields: [
{ label: '任务编号', prop: 'task_no', formatter: (value) => value || '-' },
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
{
label: '任务类型',
render: () => {
@@ -104,8 +105,8 @@
)
}
},
{ label: '批次号', prop: 'batch_no', formatter: (value) => value || '-' },
{ label: '文件名', prop: 'file_name', formatter: (value) => value || '-' },
{ label: '批次号', prop: 'batch_no', formatter: (value: string) => value || '-' },
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
{
label: '任务状态',
render: (data: TaskDetail) => {
@@ -124,17 +125,17 @@
{
label: '创建时间',
prop: 'created_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
},
{
label: '开始处理时间',
prop: 'started_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
},
{
label: '完成时间',
prop: 'completed_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
},
...(taskDetail.value?.error_message
? [
@@ -195,8 +196,6 @@
]
}
]
return sections
})
//

View File

@@ -1,63 +0,0 @@
<template>
<div class="login register">
<LoginLeftView></LoginLeftView>
<div class="right-wrap">
<div class="header">
<ArtLogo class="icon" />
<h1>{{ systemName }}</h1>
</div>
<div class="login-wrap">
<div class="form">
<h3 class="title">{{ $t('forgetPassword.title') }}</h3>
<p class="sub-title">{{ $t('forgetPassword.subTitle') }}</p>
<div class="input-wrap">
<span class="input-label" v-if="showInputLabel">账号</span>
<ElInput :placeholder="$t('forgetPassword.placeholder')" v-model.trim="username" />
</div>
<div style="margin-top: 15px">
<ElButton
class="login-btn"
type="primary"
@click="register"
:loading="loading"
v-ripple
>
{{ $t('forgetPassword.submitBtnText') }}
</ElButton>
</div>
<div style="margin-top: 15px">
<ElButton class="back-btn" plain @click="toLogin">
{{ $t('forgetPassword.backBtnText') }}
</ElButton>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import AppConfig from '@/config'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'ForgetPassword' })
const router = useRouter()
const showInputLabel = ref(false)
const systemName = AppConfig.systemInfo.name
const username = ref('')
const loading = ref(false)
const register = async () => {}
const toLogin = () => {
router.push(RoutesAlias.Login)
}
</script>
<style lang="scss" scoped>
@use '../login/index';
</style>

View File

@@ -1,29 +0,0 @@
.register {
.right-wrap {
.login-wrap {
.form {
.el-form {
margin-top: 20px;
}
.privacy-policy {
margin-top: 15px;
:deep(.el-checkbox__label) {
color: #333 !important;
}
a {
color: var(--main-color);
text-decoration: none;
}
}
.register-btn {
width: 100%;
height: 40px !important;
}
}
}
}
}

View File

@@ -1,173 +0,0 @@
<template>
<div class="login register">
<LoginLeftView></LoginLeftView>
<div class="right-wrap">
<div class="header">
<ArtLogo class="icon" />
<h1>{{ systemName }}</h1>
</div>
<div class="login-wrap">
<div class="form">
<h3 class="title">{{ $t('register.title') }}</h3>
<p class="sub-title">{{ $t('register.subTitle') }}</p>
<ElForm ref="formRef" :model="formData" :rules="rules" label-position="top">
<ElFormItem prop="username">
<ElInput
v-model.trim="formData.username"
:placeholder="$t('register.placeholder[0]')"
/>
</ElFormItem>
<ElFormItem prop="password">
<ElInput
v-model.trim="formData.password"
:placeholder="$t('register.placeholder[1]')"
type="password"
autocomplete="off"
/>
</ElFormItem>
<ElFormItem prop="confirmPassword">
<ElInput
v-model.trim="formData.confirmPassword"
:placeholder="$t('register.placeholder[2]')"
type="password"
autocomplete="off"
@keyup.enter="register"
/>
</ElFormItem>
<ElFormItem prop="agreement">
<ElCheckbox v-model="formData.agreement">
{{ $t('register.agreeText') }}
<router-link
style="color: var(--main-color); text-decoration: none"
to="/privacy-policy"
>{{ $t('register.privacyPolicy') }}</router-link
>
</ElCheckbox>
</ElFormItem>
<div style="margin-top: 15px">
<ElButton
class="register-btn"
type="primary"
@click="register"
:loading="loading"
v-ripple
>
{{ $t('register.submitBtnText') }}
</ElButton>
</div>
<div class="footer">
<p>
{{ $t('register.hasAccount') }}
<router-link :to="RoutesAlias.Login">{{ $t('register.toLogin') }}</router-link>
</p>
</div>
</ElForm>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import AppConfig from '@/config'
import { RoutesAlias } from '@/router/routesAlias'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useI18n } from 'vue-i18n'
defineOptions({ name: 'Register' })
const { t } = useI18n()
const router = useRouter()
const formRef = ref<FormInstance>()
const systemName = AppConfig.systemInfo.name
const loading = ref(false)
const formData = reactive({
username: '',
password: '',
confirmPassword: '',
agreement: false
})
const validatePass = (rule: any, value: string, callback: any) => {
if (value === '') {
callback(new Error(t('register.placeholder[1]')))
} else {
if (formData.confirmPassword !== '') {
formRef.value?.validateField('confirmPassword')
}
callback()
}
}
const validatePass2 = (rule: any, value: string, callback: any) => {
if (value === '') {
callback(new Error(t('register.rule[0]')))
} else if (value !== formData.password) {
callback(new Error(t('register.rule[1]')))
} else {
callback()
}
}
const rules = reactive<FormRules>({
username: [
{ required: true, message: t('register.placeholder[0]'), trigger: 'blur' },
{ min: 3, max: 20, message: t('register.rule[2]'), trigger: 'blur' }
],
password: [
{ required: true, validator: validatePass, trigger: 'blur' },
{ min: 6, message: t('register.rule[3]'), trigger: 'blur' }
],
confirmPassword: [{ required: true, validator: validatePass2, trigger: 'blur' }],
agreement: [
{
validator: (rule: any, value: boolean, callback: any) => {
if (!value) {
callback(new Error(t('register.rule[4]')))
} else {
callback()
}
},
trigger: 'change'
}
]
})
const register = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
loading.value = true
// 模拟注册请求
setTimeout(() => {
loading.value = false
ElMessage.success('注册成功')
toLogin()
}, 1000)
} catch (error) {
console.log('验证失败', error)
}
}
const toLogin = () => {
setTimeout(() => {
router.push(RoutesAlias.Login)
}, 1000)
}
</script>
<style lang="scss" scoped>
@use '../login/index' as login;
@use './index' as register;
</style>

View File

@@ -1,570 +0,0 @@
<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>

View File

@@ -1,694 +0,0 @@
<template>
<div class="page-content">
<!-- 导入操作区 -->
<ElCard shadow="never">
<template #header>
<span style="font-weight: 500">批量导入设备</span>
</template>
<ElRow :gutter="20">
<ElCol :xs="24" :lg="12">
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
<template #title>
<div style="line-height: 1.8">
<p><strong>导入说明</strong></p>
<p>1. 请先下载 CSV 模板文件按照模板格式填写设备信息</p>
<p>2. 支持 CSV 格式.csv单次最多导入 1000 </p>
<p>3. CSV 文件编码UTF-8推荐 GBK</p>
<p
>4.
<strong>必填字段virtual_no设备虚拟号</strong
>device_name设备名称device_model设备型号</p
>
<p
>5.
可选字段device_type设备类型manufacturer制造商max_sim_slots最大插槽数默认1</p
>
<p
>6. virtual_no
为空将导入失败设备号重复将自动跳过导入后可在任务管理中查看详情</p
>
</div>
</template>
</ElAlert>
<ElButton type="primary" :icon="Download" @click="downloadTemplate"
>下载导入模板</ElButton
>
</ElCol>
<ElCol :xs="24" :lg="12">
<div class="upload-area">
<ElUpload
ref="uploadRef"
drag
:auto-upload="false"
:on-change="handleFileChange"
:limit="1"
accept=".csv"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text"> CSV 文件拖到此处<em>点击选择</em></div>
<template #tip>
<div class="el-upload__tip">只能上传 CSV 文件且不超过 10MB</div>
</template>
</ElUpload>
<div style="margin-top: 16px; text-align: center">
<ElButton
type="success"
:loading="uploading"
:disabled="!fileList.length"
@click="submitUpload"
>
开始导入
</ElButton>
<ElButton @click="clearFiles">清空</ElButton>
</div>
</div>
</ElCol>
</ElRow>
</ElCard>
<!-- 导入记录 -->
<ElCard shadow="never" style="margin-top: 20px">
<template #header>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-weight: 500">导入记录</span>
<div>
<ElSelect
v-model="statusFilter"
placeholder="状态筛选"
style="width: 120px; margin-right: 12px"
clearable
>
<ElOption label="全部" :value="null" />
<ElOption label="待处理" :value="1" />
<ElOption label="处理中" :value="2" />
<ElOption label="已完成" :value="3" />
<ElOption label="失败" :value="4" />
</ElSelect>
<ElButton @click="refreshList">刷新</ElButton>
</div>
</div>
</template>
<ArtTable
rowKey="id"
ref="tableRef"
:loading="loading"
:data="filteredRecords"
:marginTop="10"
:stripe="false"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
</ElCard>
<!-- 导入详情对话框 -->
<ElDialog v-model="detailDialogVisible" title="设备导入详情" width="900px" align-center>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="任务编号" :span="2">{{
currentDetail.taskNo
}}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">{{ currentDetail.statusText }}</ElDescriptionsItem>
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
<ElDescriptionsItem label="文件名" :span="2">{{
currentDetail.fileName
}}</ElDescriptionsItem>
<ElDescriptionsItem label="总数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
<ElDescriptionsItem label="成功数">
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="跳过数">{{ currentDetail.skipCount }}</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="警告数">
<span style="color: var(--el-color-warning)">{{ currentDetail.warningCount }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="开始时间">{{ currentDetail.startedAt }}</ElDescriptionsItem>
<ElDescriptionsItem label="完成时间">{{ currentDetail.completedAt }}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{ currentDetail.createdAt }}</ElDescriptionsItem>
<ElDescriptionsItem label="错误信息" :span="2">{{
currentDetail.errorMessage
}}</ElDescriptionsItem>
</ElDescriptions>
<ElDivider content-position="left">失败明细</ElDivider>
<div
v-if="currentDetail.failReasons && currentDetail.failReasons.length"
style="max-height: 300px; overflow-y: auto"
>
<ElTable :data="currentDetail.failReasons" border size="small">
<ElTableColumn label="行号" prop="row" width="80" />
<ElTableColumn label="设备编号" prop="deviceCode" width="150" />
<ElTableColumn label="ICCID" prop="iccid" width="200" />
<ElTableColumn label="失败原因" prop="message" min-width="200" />
</ElTable>
</div>
<ElEmpty v-else description="无失败记录" />
<template #footer>
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
<ElButton
v-if="currentDetail.failCount > 0"
type="primary"
:icon="Download"
@click="downloadFailData(currentDetail)"
>
下载失败数据
</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { h, computed, watch } from 'vue'
import { ElMessage, ElTag, ElProgress, ElIcon, ElButton } from 'element-plus'
import { useRouter } from 'vue-router'
import { Download, UploadFilled, View, Loading } from '@element-plus/icons-vue'
import type { UploadInstance } from 'element-plus'
import { StorageService } from '@/api/modules/storage'
import { DeviceService } from '@/api/modules'
import { formatDateTime } from '@/utils/business/format'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
defineOptions({ name: 'DeviceImport' })
const router = useRouter()
interface FailReason {
row: number
deviceCode: string
iccid: string
message: string
}
interface ImportRecord {
id: string
taskNo: string
status: number
statusText: string
batchNo: string
fileName: string
totalCount: number
successCount: number
skipCount: number
failCount: number
warningCount: number
startedAt: string
completedAt: string
errorMessage: string
createdAt: string
failReasons?: FailReason[]
}
const uploadRef = ref<UploadInstance>()
const tableRef = ref()
const fileList = ref<File[]>([])
const uploading = ref(false)
const detailDialogVisible = ref(false)
const statusFilter = ref<number | null>(null)
const loading = ref(false)
const importRecords = ref<ImportRecord[]>([])
const currentDetail = ref<ImportRecord>({
id: '',
taskNo: '',
status: 1,
statusText: '',
batchNo: '',
fileName: '',
totalCount: 0,
successCount: 0,
skipCount: 0,
failCount: 0,
warningCount: 0,
startedAt: '',
completedAt: '',
errorMessage: '',
createdAt: ''
})
const filteredRecords = computed(() => {
if (statusFilter.value === null) return importRecords.value
return importRecords.value.filter((item) => item.status === statusFilter.value)
})
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'taskNo',
label: '任务编号',
width: 200,
showOverflowTooltip: true
},
{
prop: 'status',
label: '状态',
width: 120,
formatter: (row: ImportRecord) => {
if (row.status === 1) {
return h(ElTag, { type: 'info' }, () => '待处理')
} else if (row.status === 2) {
return h(
ElTag,
{ type: 'warning' },
{
default: () => [
h(ElIcon, { class: 'is-loading' }, () => h(Loading)),
h('span', { style: { marginLeft: '4px' } }, '处理中')
]
}
)
} else if (row.status === 3) {
return h(ElTag, { type: 'success' }, () => '已完成')
} else if (row.status === 4) {
return h(ElTag, { type: 'danger' }, () => '失败')
} else {
return h(ElTag, { type: 'info' }, () => row.statusText || '-')
}
}
},
{
prop: 'batchNo',
label: '批次号',
width: 180,
showOverflowTooltip: true
},
{
prop: 'fileName',
label: '文件名',
minWidth: 200,
showOverflowTooltip: true
},
{
prop: 'totalCount',
label: '总数',
width: 100
},
{
prop: 'successCount',
label: '成功数',
width: 100,
formatter: (row: ImportRecord) => {
return h('span', { style: { color: 'var(--el-color-success)' } }, row.successCount)
}
},
{
prop: 'skipCount',
label: '跳过数',
width: 100
},
{
prop: 'failCount',
label: '失败数',
width: 100,
formatter: (row: ImportRecord) => {
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.failCount)
}
},
{
prop: 'warningCount',
label: '警告数',
width: 100,
formatter: (row: ImportRecord) => {
return h('span', { style: { color: 'var(--el-color-warning)' } }, row.warningCount)
}
},
{
prop: 'startedAt',
label: '开始时间',
width: 180
},
{
prop: 'completedAt',
label: '完成时间',
width: 180
},
{
prop: 'errorMessage',
label: '错误信息',
minWidth: 200,
showOverflowTooltip: true
},
{
prop: 'createdAt',
label: '创建时间',
width: 180
},
{
prop: 'operation',
label: '操作',
width: 180,
fixed: 'right',
formatter: (row: ImportRecord) => {
const buttons = []
// 显示"查看详情"按钮
buttons.push(
h(ArtButtonTable, {
text: '详情',
onClick: () => viewDetail(row)
})
),
buttons.push(
h(ArtButtonTable, {
text: '失败数据',
type: 'danger',
onClick: () => downloadFailData(row)
})
)
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
const downloadTemplate = () => {
// CSV模板内容 - 包含表头和示例数据
const csvContent = [
// 表头
'virtual_no,device_name,device_model,device_type,manufacturer,max_sim_slots',
// 示例数据
'DEV001,智能水表01,WM-2000,智能水表,华为,1',
'DEV002,GPS定位器01,GPS-3000,定位设备,小米,2',
'DEV003,智能燃气表01,GM-1500,智能燃气表,海尔,1'
].join('\n')
// 添加 BOM 头确保 Excel 正确识别 UTF-8 编码
const BOM = '\uFEFF'
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
// 创建下载链接
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', '设备导入模板.csv')
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('设备导入模板下载成功')
}
const handleFileChange = (uploadFile: any) => {
// 验证文件大小10MB限制
const maxSize = 10 * 1024 * 1024
if (uploadFile.raw && uploadFile.raw.size > maxSize) {
ElMessage.error('文件大小不能超过 10MB')
uploadRef.value?.clearFiles()
fileList.value = []
return
}
// 验证文件类型
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.csv')) {
ElMessage.error('只能上传 CSV 文件')
uploadRef.value?.clearFiles()
fileList.value = []
return
}
fileList.value = uploadFile.raw ? [uploadFile.raw] : []
}
const clearFiles = () => {
uploadRef.value?.clearFiles()
fileList.value = []
}
/**
* 三步上传流程
* 1. 调用 StorageService.getUploadUrl() 获取预签名 URL 和 file_key
* 2. 调用 StorageService.uploadFile() 上传文件到对象存储
* 3. 调用 DeviceService.importDevices() 触发后端导入任务
*/
const submitUpload = async () => {
if (!fileList.value.length) {
ElMessage.warning('请先选择CSV文件')
return
}
const file = fileList.value[0]
uploading.value = true
try {
// 第一步:获取预签名上传 URL
ElMessage.info('正在准备上传...')
const uploadUrlRes = await StorageService.getUploadUrl({
file_name: file.name,
content_type: 'text/csv',
purpose: 'iot_import'
})
if (uploadUrlRes.code !== 0) {
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
}
const { upload_url, file_key } = uploadUrlRes.data
// 第二步:上传文件到对象存储
ElMessage.info('正在上传文件...')
await StorageService.uploadFile(upload_url, file, 'text/csv')
// 第三步调用设备导入API
ElMessage.info('正在创建导入任务...')
const importRes = await DeviceService.importDevices({
file_key,
batch_no: `DEV-${Date.now()}`
})
if (importRes.code !== 0) {
throw new Error(importRes.msg || '创建导入任务失败')
}
const taskNo = importRes.data.task_no
// 清空文件列表
clearFiles()
// 刷新任务列表
await fetchImportTasks()
// 显示成功消息
ElMessage.success({
message: `导入任务已创建!任务编号:${taskNo}`,
duration: 3000,
showClose: true
})
} catch (error: any) {
console.error('设备导入失败:', error)
ElMessage.error(error.message || '设备导入失败')
} finally {
uploading.value = false
}
}
// 获取导入任务列表
const fetchImportTasks = async () => {
loading.value = true
try {
const params: any = {
page: 1,
page_size: 100
}
// 如果有状态筛选,添加到参数
if (statusFilter.value !== null) {
params.status = statusFilter.value
}
const res = await DeviceService.getImportTasks(params)
if (res.code === 0 && res.data) {
// 将API返回的数据映射到本地格式
importRecords.value = res.data.items.map((item: any) => ({
id: item.id.toString(),
taskNo: item.task_no || '-',
status: item.status,
statusText: item.status_text || '-',
batchNo: item.batch_no || '-',
fileName: item.file_name || '-',
totalCount: item.total_count || 0,
successCount: item.success_count || 0,
skipCount: item.skip_count || 0,
failCount: item.fail_count || 0,
warningCount: item.warning_count || 0,
startedAt: item.started_at ? formatDateTime(item.started_at) : '-',
completedAt: item.completed_at ? formatDateTime(item.completed_at) : '-',
errorMessage: item.error_message || '-',
createdAt: item.created_at ? formatDateTime(item.created_at) : '-'
}))
}
} catch (error) {
console.error('获取导入任务列表失败:', error)
ElMessage.error('获取导入任务列表失败')
} finally {
loading.value = false
}
}
const refreshList = () => {
fetchImportTasks()
}
const viewDetail = async (row: ImportRecord) => {
try {
const res = await DeviceService.getImportTaskDetail(Number(row.id))
if (res.code === 0 && res.data) {
const detail = res.data
currentDetail.value = {
id: detail.id.toString(),
taskNo: detail.task_no || '-',
status: detail.status,
statusText: detail.status_text || '-',
batchNo: detail.batch_no || '-',
fileName: detail.file_name || '-',
totalCount: detail.total_count || 0,
successCount: detail.success_count || 0,
skipCount: detail.skip_count || 0,
failCount: detail.fail_count || 0,
warningCount: detail.warning_count || 0,
startedAt: detail.started_at ? formatDateTime(detail.started_at) : '-',
completedAt: detail.completed_at ? formatDateTime(detail.completed_at) : '-',
errorMessage: detail.error_message || '-',
createdAt: detail.created_at ? formatDateTime(detail.created_at) : '-',
failReasons:
detail.failed_items?.map((item: any, index: number) => ({
row: index + 1,
deviceCode: item.virtual_no || '-',
iccid: item.iccid || '-',
message: item.reason || item.error || '未知错误'
})) || []
}
detailDialogVisible.value = true
}
} catch (error) {
console.error('获取任务详情失败:', error)
ElMessage.error('获取任务详情失败')
}
}
const downloadFailData = (row: ImportRecord) => {
if (!row.failReasons || row.failReasons.length === 0) {
ElMessage.warning('没有失败数据可下载')
return
}
// 生成失败数据CSV
const headers = ['行号', '设备编号', 'ICCID', '失败原因']
const csvRows = [
headers.join(','),
...row.failReasons.map((item) =>
[item.row, item.deviceCode, item.iccid, `"${item.message}"`].join(',')
)
]
const csvContent = csvRows.join('\n')
// 添加 BOM 头确保 Excel 正确识别 UTF-8 编码
const BOM = '\uFEFF'
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
// 创建下载链接
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', `导入失败数据_${row.batchNo}.csv`)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('失败数据下载成功')
}
// 页面加载时获取任务列表
onMounted(() => {
fetchImportTasks()
})
// 监听状态筛选变化
watch(statusFilter, () => {
fetchImportTasks()
})
</script>
<style lang="scss" scoped>
.page-content {
.upload-area {
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
width: 100%;
}
}
:deep(.el-icon--upload) {
margin-bottom: 16px;
font-size: 67px;
color: var(--el-text-color-placeholder);
}
:deep(.el-upload__text) {
font-size: 14px;
color: var(--el-text-color-regular);
em {
font-style: normal;
color: var(--el-color-primary);
}
}
:deep(.is-loading) {
animation: rotating 2s linear infinite;
}
@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.stat-card {
:deep(.el-card__body) {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
}
.stat-content {
.stat-label {
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.stat-icon {
font-size: 40px;
opacity: 0.6;
}
}
}
</style>

View File

@@ -1,386 +0,0 @@
<template>
<div class="page-content">
<!-- 导入操作区 -->
<ElCard shadow="never">
<template #header>
<span style="font-weight: 500">批量导入网卡</span>
</template>
<ElRow :gutter="20">
<ElCol :xs="24" :lg="12">
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
<template #title>
<div style="line-height: 1.8">
<p><strong>导入说明</strong></p>
<p>1. 请先下载模板文件按照模板格式填写网卡信息</p>
<p>2. 支持 Excel 格式.xlsx, .xls单次最多导入 1000 </p>
<p
>3.
<strong
>必填字段ICCIDvirtual_no虚拟号运营商套餐类型流量规格</strong
></p
>
<p>4. <strong>Excel 文件必须包含 virtual_no 否则整批导入将失败</strong></p>
<p>5. virtual_no 为空将导入失败重复 ICCID 将跳过</p>
</div>
</template>
</ElAlert>
<ElButton type="primary" :icon="Download" @click="downloadTemplate"
>下载导入模板</ElButton
>
</ElCol>
<ElCol :xs="24" :lg="12">
<div class="upload-area">
<ElUpload
ref="uploadRef"
drag
:action="uploadUrl"
:on-change="handleFileChange"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:auto-upload="false"
:limit="1"
accept=".xlsx,.xls"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">只能上传 xlsx/xls 文件且不超过 5MB</div>
</template>
</ElUpload>
<div style="margin-top: 16px; text-align: center">
<ElButton
type="success"
:loading="uploading"
:disabled="!fileList.length"
@click="submitUpload"
>
开始导入
</ElButton>
<ElButton @click="clearFiles">清空</ElButton>
</div>
</div>
</ElCol>
</ElRow>
</ElCard>
<!-- 导入记录 -->
<ElCard shadow="never" style="margin-top: 20px">
<template #header>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-weight: 500">导入记录</span>
<ElButton size="small" @click="refreshList">刷新</ElButton>
</div>
</template>
<ArtTable :data="importRecords" index>
<template #default>
<ElTableColumn label="导入批次号" prop="batchNo" width="180" />
<ElTableColumn label="文件名" prop="fileName" min-width="200" show-overflow-tooltip />
<ElTableColumn label="总条数" prop="totalCount" width="100" />
<ElTableColumn label="成功数" prop="successCount" width="100">
<template #default="scope">
<span style="color: var(--el-color-success)">{{ scope.row.successCount }}</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="status" width="120">
<template #default="scope">
<ElTag v-if="scope.row.status === 'processing'" type="warning">
<el-icon class="is-loading"><Loading /></el-icon>
处理中
</ElTag>
<ElTag v-else-if="scope.row.status === 'success'" type="success">完成</ElTag>
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">失败</ElTag>
<ElTag v-else type="info">待处理</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="导入进度" prop="progress" width="150">
<template #default="scope">
<ElProgress
:percentage="scope.row.progress"
:status="scope.row.status === 'failed' ? 'exception' : undefined"
/>
</template>
</ElTableColumn>
<ElTableColumn label="导入时间" prop="importTime" width="180" />
<ElTableColumn label="操作人" prop="operator" width="120" />
<ElTableColumn fixed="right" label="操作" width="180">
<template #default="scope">
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
<el-button
v-if="scope.row.failCount > 0"
link
type="primary"
:icon="Download"
@click="downloadFailData(scope.row)"
>
下载失败数据
</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
</ElCard>
<!-- 导入详情对话框 -->
<ElDialog v-model="detailDialogVisible" title="导入详情" width="800px" align-center>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
<ElDescriptionsItem label="文件名">{{ currentDetail.fileName }}</ElDescriptionsItem>
<ElDescriptionsItem label="总条数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
<ElDescriptionsItem label="成功数">
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="跳过数">{{ currentDetail.skipCount || 0 }}</ElDescriptionsItem>
<ElDescriptionsItem label="导入时间">{{ currentDetail.importTime }}</ElDescriptionsItem>
<ElDescriptionsItem label="操作人">{{ currentDetail.operator }}</ElDescriptionsItem>
<ElDescriptionsItem label="失败原因" :span="2">
<div v-if="currentDetail.failReasons && currentDetail.failReasons.length">
<div
v-for="(reason, index) in currentDetail.failReasons"
:key="index"
style="margin-bottom: 4px"
>
<ElTag type="danger" size="small">{{ reason.row }}</ElTag>
{{ reason.message }}
</div>
</div>
<span v-else style="color: var(--el-text-color-secondary)"></span>
</ElDescriptionsItem>
</ElDescriptions>
<template #footer>
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { Download, UploadFilled, View, Loading } from '@element-plus/icons-vue'
import type { UploadInstance, UploadRawFile } from 'element-plus'
defineOptions({ name: 'SimImport' })
interface ImportRecord {
id: string
batchNo: string
fileName: string
totalCount: number
successCount: number
failCount: number
skipCount?: number
status: 'pending' | 'processing' | 'success' | 'failed'
progress: number
importTime: string
operator: string
failReasons?: Array<{ row: number; message: string }>
}
const uploadRef = ref<UploadInstance>()
const uploadUrl = ref('/api/batch/sim-import')
const fileList = ref<UploadRawFile[]>([])
const uploading = ref(false)
const detailDialogVisible = ref(false)
const importRecords = ref<ImportRecord[]>([
{
id: '1',
batchNo: 'IMP20260109001',
fileName: '网卡导入模板_20260109.xlsx',
totalCount: 500,
successCount: 495,
failCount: 5,
skipCount: 0,
status: 'success',
progress: 100,
importTime: '2026-01-09 10:30:00',
operator: 'admin',
failReasons: [
{ row: 23, message: 'ICCID 格式错误' },
{ row: 45, message: 'ICCID 已存在' },
{ row: 67, message: '套餐类型不存在' },
{ row: 89, message: '流量规格格式错误' },
{ row: 123, message: '运营商代码无效' }
]
},
{
id: '2',
batchNo: 'IMP20260108001',
fileName: '网卡批量导入.xlsx',
totalCount: 1000,
successCount: 1000,
failCount: 0,
skipCount: 0,
status: 'success',
progress: 100,
importTime: '2026-01-08 15:20:00',
operator: 'admin'
},
{
id: '3',
batchNo: 'IMP20260107001',
fileName: '测试数据.xlsx',
totalCount: 200,
successCount: 150,
failCount: 50,
skipCount: 0,
status: 'success',
progress: 100,
importTime: '2026-01-07 09:15:00',
operator: 'operator01',
failReasons: [
{ row: 10, message: 'ICCID 重复' },
{ row: 20, message: '运营商字段为空' }
]
}
])
const currentDetail = ref<ImportRecord>({
id: '',
batchNo: '',
fileName: '',
totalCount: 0,
successCount: 0,
failCount: 0,
status: 'pending',
progress: 0,
importTime: '',
operator: ''
})
const downloadTemplate = () => {
ElMessage.success('模板下载中...')
// 实际项目中应该调用下载接口
setTimeout(() => {
ElMessage.success('模板下载成功')
}, 1000)
}
const handleFileChange = (file: any, files: any[]) => {
fileList.value = files
}
const clearFiles = () => {
uploadRef.value?.clearFiles()
fileList.value = []
}
const submitUpload = async () => {
if (!fileList.value.length) {
ElMessage.warning('请先选择文件')
return
}
uploading.value = true
ElMessage.info('正在导入,请稍候...')
// 模拟上传和导入过程
setTimeout(() => {
const newRecord: ImportRecord = {
id: Date.now().toString(),
batchNo: `IMP${new Date().getTime()}`,
fileName: fileList.value[0].name,
totalCount: 300,
successCount: 295,
failCount: 5,
skipCount: 0,
status: 'success',
progress: 100,
importTime: new Date().toLocaleString('zh-CN'),
operator: 'admin',
failReasons: [
{ row: 12, message: 'ICCID 格式错误' },
{ row: 34, message: 'ICCID 已存在' }
]
}
importRecords.value.unshift(newRecord)
uploading.value = false
clearFiles()
ElMessage.success(
`导入完成!成功 ${newRecord.successCount} 条,失败 ${newRecord.failCount}`
)
}, 2000)
}
const handleUploadSuccess = () => {
ElMessage.success('上传成功')
}
const handleUploadError = () => {
uploading.value = false
ElMessage.error('上传失败')
}
const refreshList = () => {
ElMessage.success('刷新成功')
}
const viewDetail = (row: ImportRecord) => {
currentDetail.value = { ...row }
detailDialogVisible.value = true
}
const downloadFailData = (row: ImportRecord) => {
ElMessage.info(`正在下载批次 ${row.batchNo} 的失败数据...`)
setTimeout(() => {
ElMessage.success('下载完成')
}, 1000)
}
</script>
<style lang="scss" scoped>
.page-content {
.upload-area {
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
width: 100%;
}
}
:deep(.el-icon--upload) {
margin-bottom: 16px;
font-size: 67px;
color: var(--el-text-color-placeholder);
}
:deep(.el-upload__text) {
font-size: 14px;
color: var(--el-text-color-regular);
em {
font-style: normal;
color: var(--el-color-primary);
}
}
:deep(.is-loading) {
animation: rotating 2s linear infinite;
}
@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
</style>

View File

@@ -1,383 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="card-template-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showDialog('add')">新增</ElButton>
<ElButton @click="batchOperation" :disabled="selectedRows.length === 0"
>批量操作</ElButton
>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 操作对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增' : '编辑'"
width="400px"
align-center
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
<ElFormItem label="名称" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名称" />
</ElFormItem>
<ElFormItem label="描述" prop="description">
<ElInput
v-model="formData.description"
type="textarea"
placeholder="请输入描述"
:rows="3"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElTag, ElMessageBox, ElMessage, FormInstance } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'CardTemplate' })
const dialogType = ref('add')
const dialogVisible = ref(false)
const loading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
name: '',
status: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 模拟数据
const mockData = [
{
id: 1,
name: '示例项目1',
description: '这是一个示例项目',
status: '1',
statusName: '正常',
createTime: '2024-11-07 10:00:00'
},
{
id: 2,
name: '示例项目2',
description: '这是另一个示例项目',
status: '2',
statusName: '禁用',
createTime: '2024-11-06 15:30:00'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getDataList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getDataList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '名称',
prop: 'name',
type: 'input',
config: {
clearable: true,
placeholder: '请输入名称'
},
onChange: handleFormChange
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '请选择状态'
},
options: () => [
{ label: '正常', value: '1' },
{ label: '禁用', value: '2' }
],
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: 'ID', prop: 'id' },
{ label: '名称', prop: 'name' },
{ label: '描述', prop: 'description' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'createTime' },
{ label: '操作', prop: 'operation' }
]
// 获取标签类型
const getTagType = (status: string) => {
switch (status) {
case '1':
return 'success'
case '2':
return 'danger'
default:
return 'info'
}
}
// 构建标签文本
const buildTagText = (status: string) => {
switch (status) {
case '1':
return '正常'
case '2':
return '禁用'
default:
return '未知'
}
}
// 显示对话框
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (formRef.value) {
formRef.value.resetFields()
}
if (type === 'edit' && row) {
formData.name = row.name
formData.description = row.description
} else {
formData.name = ''
formData.description = ''
}
}
// 删除操作
const deleteItem = () => {
ElMessageBox.confirm('确定要删除该项目吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(() => {
ElMessage.success('删除成功')
getDataList()
})
}
// 批量操作
const batchOperation = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要操作的项目')
return
}
ElMessage.info(`已选择 ${selectedRows.value.length} 个项目`)
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'name',
label: '名称',
minWidth: 120
},
{
prop: 'description',
label: '描述',
minWidth: 200
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row) => {
return h(ElTag, { type: getTagType(row.status) }, () => buildTagText(row.status))
}
},
{
prop: 'createTime',
label: '创建时间',
width: 160
},
{
prop: 'operation',
label: '操作',
width: 150,
formatter: (row: any) => {
return h('div', [
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
}),
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteItem()
})
])
}
}
])
// 表单实例
const formRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
name: '',
description: ''
})
onMounted(() => {
getDataList()
})
// 获取数据列表
const getDataList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取数据列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getDataList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 表单验证规则
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
dialogVisible.value = false
getDataList()
}
})
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getDataList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getDataList()
}
</script>
<style lang="scss" scoped>
.card-template-page {
// 可以添加特定样式
}
</style>

View File

@@ -1,436 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="card-assign-page" id="table-full-screen">
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showDistributeDialog">分销网卡</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 分销网卡对话框 -->
<ElDialog
v-model="dialogVisible"
title="分销网卡"
width="500px"
align-center
:close-on-click-modal="false"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
<ElFormItem label="上传Excel文件" prop="excelFile">
<ElUpload
ref="uploadRef"
:limit="1"
:on-exceed="handleExceed"
:before-upload="beforeUpload"
:on-change="handleFileChange"
:auto-upload="false"
accept=".xlsx,.xls"
drag
>
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
<div class="el-upload__text"> 将文件拖到此处<em>点击上传</em> </div>
<template #tip>
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件且不超过 10MB </div>
</template>
</ElUpload>
</ElFormItem>
<ElFormItem label="选择代理商" prop="agent">
<ElSelect
v-model="formData.agent"
placeholder="请选择代理商"
style="width: 100%"
clearable
>
<ElOption
v-for="agent in agentOptions"
:key="agent.value"
:label="agent.label"
:value="agent.value"
/>
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
确认分销
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import {
ElTag,
ElMessage,
ElMessageBox,
ElUpload,
ElIcon,
ElSelect,
ElOption
} from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
defineOptions({ name: 'CardAssign' })
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 表单实例
const formRef = ref<FormInstance>()
const uploadRef = ref<UploadInstance>()
// 表单数据
const formData = reactive({
excelFile: null as File | null,
agent: ''
})
// 代理商选项
const agentOptions = ref([
{ label: '张丽丽', value: 'zhangll' },
{ label: '王小明', value: 'wangxm' },
{ label: 'HNSXKJ', value: 'hnsxkj' },
{ label: '孔丽娟', value: 'konglj' },
{ label: '李佳音', value: 'lijy' },
{ label: '赵强', value: 'zhaoq' }
])
// 模拟数据
const mockData = [
{
id: 1,
importTime: '2025-11-07 15:01:37',
packageName: '随意联畅玩年卡套餐12个月',
importCount: 1000,
successCount: 998,
failCount: 2,
distributor: '张丽丽',
operator: '张若暄'
},
{
id: 2,
importTime: '2025-11-05 12:07:41',
packageName: '如意包年3G流量包',
importCount: 500,
successCount: 500,
failCount: 0,
distributor: '王小明',
operator: '张若暄'
},
{
id: 3,
importTime: '2025-11-03 11:21:42',
packageName: 'Y-NB专享套餐',
importCount: 200,
successCount: 195,
failCount: 5,
distributor: 'HNSXKJ',
operator: '张若暄'
},
{
id: 4,
importTime: '2025-10-29 16:01:16',
packageName: '100G全国流量月卡套餐',
importCount: 2500,
successCount: 2500,
failCount: 0,
distributor: '未分销',
operator: '张若暄'
},
{
id: 5,
importTime: '2025-10-22 15:44:31',
packageName: '广电飞悦卡无预存50G30天',
importCount: 800,
successCount: 795,
failCount: 5,
distributor: '孔丽娟',
operator: '孔丽娟'
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '导入时间', prop: 'importTime' },
{ label: '套餐名称', prop: 'packageName' },
{ label: '导入张数(张)', prop: 'importCount' },
{ label: '导入成功张数(张)', prop: 'successCount' },
{ label: '导入失败张数(张)', prop: 'failCount' },
{ label: '分销商', prop: 'distributor' },
{ label: '操作人', prop: 'operator' },
{ label: '操作', prop: 'operation' }
]
// 查看详情
const viewDetails = (row: any) => {
ElMessage.info(`查看 ${row.packageName} 的分配详情`)
}
// 删除记录
const deleteRecord = (row: any) => {
ElMessageBox.confirm(`确定要删除该分配记录吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('删除成功')
getCardAssignList()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'importTime',
label: '导入时间',
width: 180
},
{
prop: 'packageName',
label: '套餐名称',
minWidth: 200
},
{
prop: 'importCount',
label: '导入张数(张)',
width: 130
},
{
prop: 'successCount',
label: '导入成功张数(张)',
width: 150
},
{
prop: 'failCount',
label: '导入失败张数(张)',
width: 150,
formatter: (row) => {
return h(ElTag, { type: row.failCount > 0 ? 'danger' : 'success' }, () => row.failCount)
}
},
{
prop: 'distributor',
label: '分销商',
width: 120
},
{
prop: 'operator',
label: '操作人',
width: 100
},
{
prop: 'operation',
label: '操作',
width: 150,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '查看',
onClick: () => viewDetails(row)
}),
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteRecord(row)
})
])
}
}
])
onMounted(() => {
getCardAssignList()
})
// 获取网卡分配列表
const getCardAssignList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取网卡分配列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getCardAssignList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getCardAssignList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getCardAssignList()
}
// 显示分销对话框
const showDistributeDialog = () => {
dialogVisible.value = true
// 重置表单
if (formRef.value) {
formRef.value.resetFields()
}
formData.excelFile = null
formData.agent = ''
}
// 文件上传限制
const handleExceed = () => {
ElMessage.warning('最多只能上传一个文件')
}
// 文件上传前检查
const beforeUpload = (file: UploadRawFile) => {
const isExcel =
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.type === 'application/vnd.ms-excel'
const isLt10M = file.size / 1024 / 1024 < 10
if (!isExcel) {
ElMessage.error('只能上传 Excel 文件!')
return false
}
if (!isLt10M) {
ElMessage.error('上传文件大小不能超过 10MB!')
return false
}
return false // 阻止自动上传
}
// 文件变化处理
const handleFileChange = (file: UploadFile) => {
if (file.raw) {
formData.excelFile = file.raw
}
}
// 表单验证规则
const rules = reactive<FormRules>({
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }],
agent: [{ required: true, message: '请选择代理商', trigger: 'change' }]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
// 检查文件是否上传
if (!formData.excelFile) {
ElMessage.error('请先上传Excel文件')
return
}
await formRef.value.validate((valid) => {
if (valid) {
submitLoading.value = true
// 模拟提交过程
setTimeout(() => {
ElMessage.success(
`已成功分销给代理商:${agentOptions.value.find((a) => a.value === formData.agent)?.label}`
)
dialogVisible.value = false
submitLoading.value = false
getCardAssignList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.card-assign-page {
// 可以添加特定样式
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-upload-dragger) {
padding: 40px;
}
</style>

View File

@@ -1,549 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="card-change-card-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
<ElButton type="success" @click="showAddDialog">新增</ElButton>
<ElButton @click="exportExcel">导出excel</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 新增换卡网卡对话框 -->
<ElDialog
v-model="dialogVisible"
title="新增换卡网卡"
width="500px"
align-center
:close-on-click-modal="false"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
<ElFormItem label="ICCID" prop="iccid">
<ElInput v-model="formData.iccid" placeholder="请输入ICCID" clearable />
</ElFormItem>
<ElFormItem label="是否收费" prop="isCharged">
<ElSelect
v-model="formData.isCharged"
placeholder="请选择是否收费"
style="width: 100%"
clearable
@change="handleChargeChange"
>
<ElOption label="收费" value="true" />
<ElOption label="免费" value="false" />
</ElSelect>
</ElFormItem>
<ElFormItem label="收费金额" prop="chargeAmount" v-if="formData.isCharged === 'true'">
<ElInputNumber
v-model="formData.chargeAmount"
:min="0"
:max="9999"
:precision="2"
placeholder="请输入收费金额"
style="width: 100%"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
确认新增
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElTag, ElMessage, ElMessageBox, ElSelect, ElOption, ElInputNumber } from 'element-plus'
import type { FormInstance } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'CardChangeCard' })
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
cardNumber: '',
accessNumber: '',
cardCompany: '',
operationDateRange: '',
isCharged: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 表单实例
const formRef = ref<FormInstance>()
// 新增表单数据
const formData = reactive({
iccid: '',
isCharged: '',
chargeAmount: 0
})
// 模拟数据
const mockData = [
{
id: 1,
cardNumber: '89860621370079892035',
accessNumber: '1440012345678',
cardCompany: '联通2',
agentLevel: '一级代理',
operator: '张若暄',
operationTime: '2025-11-08 10:30:00',
isCharged: '收费',
amount: '50.00'
},
{
id: 2,
cardNumber: '89860621370079892036',
accessNumber: '1440012345679',
cardCompany: 'SXKJ-NB',
agentLevel: '二级代理',
operator: '孔丽娟',
operationTime: '2025-11-07 14:15:00',
isCharged: '免费',
amount: '0.00'
},
{
id: 3,
cardNumber: '89860621370079892037',
accessNumber: '1440012345680',
cardCompany: '联通36',
agentLevel: '终端用户',
operator: '李佳音',
operationTime: '2025-11-06 09:45:00',
isCharged: '收费',
amount: '30.00'
},
{
id: 4,
cardNumber: '89860621370079892038',
accessNumber: '1440012345681',
cardCompany: '联通1-1',
agentLevel: '一级代理',
operator: '赵强',
operationTime: '2025-11-05 16:20:00',
isCharged: '收费',
amount: '100.00'
},
{
id: 5,
cardNumber: '89860621370079892039',
accessNumber: '1440012345682',
cardCompany: '广电4',
agentLevel: '分销商',
operator: '张若暄',
operationTime: '2025-11-04 11:30:00',
isCharged: '免费',
amount: '0.00'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getCardChangeCardList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getCardChangeCardList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '卡号',
prop: 'cardNumber',
type: 'input',
config: {
clearable: true,
placeholder: '请输入卡号'
},
onChange: handleFormChange
},
{
label: '接入号',
prop: 'accessNumber',
type: 'input',
config: {
clearable: true,
placeholder: '请输入接入号'
},
onChange: handleFormChange
},
{
label: '开卡公司',
prop: 'cardCompany',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '联通2', value: 'unicom2' },
{ label: '联通36', value: 'unicom36' },
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
{ label: '联通1-1', value: 'unicom1_1' },
{ label: '联通8', value: 'unicom8' },
{ label: '移动21', value: 'mobile21' },
{ label: '广电4', value: 'gdtv4' },
{ label: '电信9', value: 'telecom9' }
],
onChange: handleFormChange
},
{
label: '操作时间',
prop: 'operationDateRange',
type: 'daterange',
config: {
type: 'daterange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
onChange: handleFormChange
},
{
label: '是否收费',
prop: 'isCharged',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '收费', value: 'charged' },
{ label: '免费', value: 'free' }
],
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '卡号', prop: 'cardNumber' },
{ label: '接入号', prop: 'accessNumber' },
{ label: '开卡公司', prop: 'cardCompany' },
{ label: '代理商层级', prop: 'agentLevel' },
{ label: '操作人', prop: 'operator' },
{ label: '操作时间', prop: 'operationTime' },
{ label: '是否收费', prop: 'isCharged' },
{ label: '金额', prop: 'amount' },
{ label: '操作', prop: 'operation' }
]
// 获取收费状态标签类型
const getChargeStatusType = (status: string) => {
switch (status) {
case '收费':
return 'warning'
case '免费':
return 'success'
default:
return 'info'
}
}
// 导出Excel
const exportExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条换卡网卡记录`)
}
// 显示新增对话框
const showAddDialog = () => {
dialogVisible.value = true
// 重置表单
if (formRef.value) {
formRef.value.resetFields()
}
formData.iccid = ''
formData.isCharged = ''
formData.chargeAmount = 0
}
// 收费方式变化处理
const handleChargeChange = (value: string) => {
if (value === 'false') {
formData.chargeAmount = 0
}
}
// 查看详情
const viewDetails = (row: any) => {
ElMessage.info(`查看换卡网卡详情: ${row.cardNumber}`)
}
// 编辑记录
const editRecord = (row: any) => {
ElMessage.info(`编辑换卡网卡记录: ${row.cardNumber}`)
}
// 删除记录
const deleteRecord = (row: any) => {
ElMessageBox.confirm(`确定要删除该换卡网卡记录吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('删除成功')
getCardChangeCardList()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'cardNumber',
label: '卡号',
width: 200
},
{
prop: 'accessNumber',
label: '接入号'
},
{
prop: 'cardCompany',
label: '开卡公司'
},
{
prop: 'agentLevel',
label: '代理商层级'
},
{
prop: 'operator',
label: '操作人'
},
{
prop: 'operationTime',
label: '操作时间',
width: 160
},
{
prop: 'isCharged',
label: '是否收费',
formatter: (row) => {
return h(ElTag, { type: getChargeStatusType(row.isCharged) }, () => row.isCharged)
}
},
{
prop: 'amount',
label: '金额',
formatter: (row) => `¥${row.amount}`
},
{
prop: 'operation',
label: '操作',
width: 220,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '查看',
onClick: () => viewDetails(row)
}),
h(ArtButtonTable, {
text: '编辑',
onClick: () => editRecord(row)
}),
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteRecord(row)
})
])
}
}
])
onMounted(() => {
getCardChangeCardList()
})
// 获取换卡网卡列表
const getCardChangeCardList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取换卡网卡列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getCardChangeCardList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getCardChangeCardList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getCardChangeCardList()
}
// 表单验证规则
const rules = reactive<FormRules>({
iccid: [
{ required: true, message: '请输入ICCID', trigger: 'blur' },
{ min: 15, max: 20, message: 'ICCID长度在 15 到 20 个字符', trigger: 'blur' }
],
isCharged: [{ required: true, message: '请选择是否收费', trigger: 'change' }],
chargeAmount: [
{
required: true,
message: '请输入收费金额',
trigger: 'blur',
validator: (rule, value, callback) => {
if (formData.isCharged === 'true' && (!value || value <= 0)) {
callback(new Error('收费时金额必须大于0'))
} else {
callback()
}
}
}
]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
submitLoading.value = true
// 模拟提交过程
setTimeout(() => {
const chargeText = formData.isCharged === 'true' ? '收费' : '免费'
const amountText =
formData.isCharged === 'true' ? `,金额:¥${formData.chargeAmount}` : ''
ElMessage.success(
`新增换卡网卡成功ICCID${formData.iccid}${chargeText}${amountText}`
)
dialogVisible.value = false
submitLoading.value = false
getCardChangeCardList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.card-change-card-page {
// 可以添加特定样式
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,607 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="card-list-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 数据视图组件 -->
<ArtDataViewer
ref="dataViewerRef"
:data="tableData"
:loading="loading"
:table-columns="columns"
:descriptions-fields="descriptionsFields"
:descriptions-columns="2"
:pagination="pagination"
:card-title-field="'importBatch'"
:label-width="'150px'"
:field-columns="columnChecks"
:show-card-actions="true"
:show-card-selection="true"
:default-view="currentView"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@view-change="handleViewChange"
>
<template #header-left>
<ElButton type="primary" @click="showDialog('add')">导入网卡</ElButton>
<ElButton @click="showDialog('single')">单卡导入</ElButton>
</template>
<template #header-right>
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
v-model:currentView="currentView"
@refresh="handleRefresh"
@viewChange="handleViewChange"
:show-title="false"
:show-view-toggle="true"
/>
</template>
<template #card-actions="{ item }">
<ArtButtonTable type="view" text="查看失败" @click="viewFailures(item)" />
</template>
</ArtDataViewer>
<!-- 网卡信息对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '导入网卡' : dialogType === 'single' ? '单卡导入' : '编辑'"
width="40%"
align-center
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
<ElFormItem label="ICCID" prop="iccid">
<ElInput v-model="formData.iccid" placeholder="请输入ICCID" />
</ElFormItem>
<ElFormItem label="IMSI" prop="imsi">
<ElInput v-model="formData.imsi" placeholder="请输入IMSI" />
</ElFormItem>
<ElFormItem label="手机号码" prop="msisdn">
<ElInput v-model="formData.msisdn" placeholder="请输入手机号码" />
</ElFormItem>
<ElFormItem label="运营商" prop="operator">
<ElSelect v-model="formData.operator" placeholder="请选择运营商">
<ElOption label="中国移动" value="mobile" />
<ElOption label="中国联通" value="unicom" />
<ElOption label="中国电信" value="telecom" />
</ElSelect>
</ElFormItem>
<ElFormItem label="网络类型" prop="networkType">
<ElSelect v-model="formData.networkType" placeholder="请选择网络类型">
<ElOption label="2G" value="2G" />
<ElOption label="3G" value="3G" />
<ElOption label="4G" value="4G" />
<ElOption label="5G" value="5G" />
</ElSelect>
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSelect v-model="formData.status" placeholder="请选择状态">
<ElOption label="激活" value="1" />
<ElOption label="停用" value="2" />
<ElOption label="测试" value="3" />
<ElOption label="库存" value="4" />
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElDialog, FormInstance, ElTag } from 'element-plus'
import { ElMessageBox, ElMessage } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtDataViewer from '@/components/core/views/ArtDataViewer.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'CardList' })
const dialogType = ref('add')
const dialogVisible = ref(false)
const loading = ref(false)
// 当前视图模式
const currentView = ref('table')
// 定义表单搜索初始值
const initialSearchState = {
operator: '',
distributor: '',
importDateRange: '',
cardCompany: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 数据视图组件引用
const dataViewerRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 模拟数据
const mockData = [
{
id: 1,
importTime: '2025-11-07 15:01:37.0',
importBatch: '650',
importCount: 1,
successCount: 1,
failCount: 0,
distributorName: '未分销',
packageName: '随意联畅玩年卡套餐12个月',
cardCompanyName: '联通2',
operatorName: '中国联通',
cardType: '月卡',
operatorUser: '张若暄'
},
{
id: 2,
importTime: '2025-11-05 12:07:41.0',
importBatch: '649',
importCount: 100,
successCount: 100,
failCount: 0,
distributorName: '未分销',
packageName: '如意包年3G流量包',
cardCompanyName: '联通36',
operatorName: '中国联通',
cardType: '月卡',
operatorUser: '张若暄'
},
{
id: 3,
importTime: '2025-11-03 11:21:42.0',
importBatch: '648',
importCount: 200,
successCount: 200,
failCount: 0,
distributorName: 'HNSXKJ',
packageName: 'Y-NB专享套餐',
cardCompanyName: 'SXKJ-NB',
operatorName: 'GS移动',
cardType: '月卡',
operatorUser: '张若暄'
},
{
id: 4,
importTime: '2025-10-29 16:01:16.0',
importBatch: '647',
importCount: 2500,
successCount: 2500,
failCount: 0,
distributorName: '未分销',
packageName: '100G全国流量月卡套餐',
cardCompanyName: '联通1-1',
operatorName: '中国联通',
cardType: '月卡',
operatorUser: '张若暄'
},
{
id: 5,
importTime: '2025-10-22 15:44:31.0',
importBatch: '644',
importCount: 500,
successCount: 499,
failCount: 1,
distributorName: '未分销',
packageName: '广电飞悦卡无预存50G30天',
cardCompanyName: '广电4',
operatorName: 'GDWL',
cardType: '月卡',
operatorUser: '孔丽娟'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getCardList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getCardList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '运营商',
prop: 'operator',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '中国移动', value: 'mobile' },
{ label: '中国联通', value: 'unicom' },
{ label: '中国电信', value: 'telecom' },
{ label: 'GS移动', value: 'gs_mobile' },
{ label: 'DC物联', value: 'dc_iot' },
{ label: 'GDWL', value: 'gdwl' },
{ label: 'GS联通', value: 'gs_unicom' },
{ label: 'GS电信', value: 'gs_telecom' }
],
onChange: handleFormChange
},
{
label: '分销商',
prop: 'distributor',
type: 'input',
config: {
clearable: true,
placeholder: '请输入分销商'
},
onChange: handleFormChange
},
{
label: '导入时间',
prop: 'importDateRange',
type: 'daterange',
config: {
type: 'daterange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
onChange: handleFormChange
},
{
label: '开卡公司',
prop: 'cardCompany',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '联通2', value: 'unicom2' },
{ label: '联通36', value: 'unicom36' },
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
{ label: '联通1-1', value: 'unicom1_1' },
{ label: '联通8', value: 'unicom8' },
{ label: '新移动22', value: 'new_mobile22' },
{ label: '广电4', value: 'gdtv4' },
{ label: '移动21', value: 'mobile21' },
{ label: '联通38', value: 'unicom38' },
{ label: '电信9', value: 'telecom9' },
{ label: '联通10-1', value: 'unicom10_1' }
],
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '导入时间', prop: 'importTime' },
{ label: '导入批次', prop: 'importBatch' },
{ label: '导入张数(张)', prop: 'importCount' },
{ label: '导入成功张数(张)', prop: 'successCount' },
{ label: '导入失败张数(张)', prop: 'failCount' },
{ label: '分销代理商', prop: 'distributorName' },
{ label: '套餐名称', prop: 'packageName' },
{ label: '开卡公司', prop: 'cardCompanyName' },
{ label: '运营商', prop: 'operatorName' },
{ label: '卡类型', prop: 'cardType' },
{ label: '操作人', prop: 'operatorUser' },
{ label: '操作', prop: 'operation' }
]
// 描述字段配置
const descriptionsFields = [
{ prop: 'importTime', label: '导入时间' },
{ prop: 'importBatch', label: '导入批次' },
{ prop: 'importCount', label: '导入张数(张)' },
{ prop: 'successCount', label: '导入成功张数(张)' },
{
prop: 'failCount',
label: '导入失败张数(张)',
formatter: (row: any) => {
const type = row.failCount > 0 ? 'danger' : 'success'
return `<el-tag type="${type}" size="small">${row.failCount}</el-tag>`
}
},
{ prop: 'distributorName', label: '分销代理商' },
{ prop: 'packageName', label: '套餐名称', span: 1 },
{ prop: 'cardCompanyName', label: '开卡公司' },
{ prop: 'operatorName', label: '运营商' },
{ prop: 'cardType', label: '卡类型' },
{ prop: 'operatorUser', label: '操作人' }
]
// 获取标签类型
const getTagType = (status: string) => {
switch (status) {
case '1':
return 'success'
case '2':
return 'danger'
case '3':
return 'warning'
case '4':
return 'info'
default:
return 'info'
}
}
// 构建标签文本
const buildTagText = (status: string) => {
switch (status) {
case '1':
return '激活'
case '2':
return '停用'
case '3':
return '测试'
case '4':
return '库存'
default:
return '未知'
}
}
// 显示对话框
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (formRef.value) {
formRef.value.resetFields()
}
if (type === 'edit' && row) {
formData.iccid = row.iccid
formData.imsi = row.imsi
formData.msisdn = row.msisdn
formData.operator = row.operator
formData.networkType = row.networkType
formData.status = row.status
} else {
formData.iccid = ''
formData.imsi = ''
formData.msisdn = ''
formData.operator = ''
formData.networkType = ''
formData.status = ''
}
}
// 查看失败
const viewFailures = (row: any) => {
if (row.failCount > 0) {
ElMessage.info(`查看导入批次 ${row.importBatch} 的失败记录`)
// 这里可以跳转到失败详情页面或显示失败详情对话框
} else {
ElMessage.info('该批次没有失败记录')
}
}
// 批量操作
const batchOperation = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要操作的导入记录')
return
}
ElMessage.info(`已选择 ${selectedRows.value.length} 条导入记录`)
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'importTime',
label: '导入时间',
width: 180
},
{
prop: 'importBatch',
label: '导入批次',
width: 100
},
{
prop: 'importCount',
label: '导入张数(张)',
width: 130
},
{
prop: 'successCount',
label: '导入成功张数(张)',
width: 150
},
{
prop: 'failCount',
label: '导入失败张数(张)',
width: 150
},
{
prop: 'distributorName',
label: '分销代理商',
width: 120
},
{
prop: 'packageName',
label: '套餐名称',
minWidth: 200
},
{
prop: 'cardCompanyName',
label: '开卡公司',
width: 120
},
{
prop: 'operatorName',
label: '运营商',
width: 100
},
{
prop: 'cardType',
label: '卡类型',
width: 80
},
{
prop: 'operatorUser',
label: '操作人',
width: 100
},
{
prop: 'operation',
label: '操作',
width: 120,
formatter: (row: any) => {
return h('div', [
h(ArtButtonTable, {
type: 'view',
text: '查看失败',
onClick: () => viewFailures(row)
})
])
}
}
])
// 表单实例
const formRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
iccid: '',
imsi: '',
msisdn: '',
operator: '',
networkType: '',
status: ''
})
onMounted(() => {
getCardList()
})
// 获取网卡列表
const getCardList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取网卡列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getCardList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 表单验证规则
const rules = reactive<FormRules>({
iccid: [
{ required: true, message: '请输入ICCID', trigger: 'blur' },
{ min: 19, max: 20, message: 'ICCID长度应为19-20位', trigger: 'blur' }
],
imsi: [
{ required: true, message: '请输入IMSI', trigger: 'blur' },
{ min: 15, max: 15, message: 'IMSI长度应为15位', trigger: 'blur' }
],
msisdn: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
],
operator: [{ required: true, message: '请选择运营商', trigger: 'change' }],
networkType: [{ required: true, message: '请选择网络类型', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
dialogVisible.value = false
getCardList()
}
})
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getCardList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getCardList()
}
// 处理视图切换
const handleViewChange = (view: string) => {
console.log('视图切换到:', view)
// 可以在这里添加视图切换的额外逻辑,比如保存用户偏好
}
</script>
<style lang="scss" scoped>
.card-list-page {
// Card list page styles
}
</style>

View File

@@ -1,695 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="card-replacement-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
<ElButton @click="exportExcel">导出excel</ElButton>
<ElButton type="success" @click="showImportDialog">导入excel</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 导入Excel对话框 -->
<ElDialog
v-model="importDialogVisible"
title="导入换卡记录"
width="500px"
align-center
:close-on-click-modal="false"
>
<!-- 顶部下载模板按钮 -->
<div class="template-section">
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
<span class="template-tip">请先下载模板按模板格式填写后上传</span>
</div>
<ElDivider />
<ElForm
ref="importFormRef"
:model="importFormData"
:rules="importRules"
label-width="120px"
>
<ElFormItem label="上传Excel文件" prop="excelFile">
<ElUpload
ref="uploadRef"
:limit="1"
:on-exceed="handleExceed"
:before-upload="beforeUpload"
:on-change="handleFileChange"
:auto-upload="false"
accept=".xlsx,.xls"
drag
>
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
<div class="el-upload__text"> 将文件拖到此处<em>点击上传</em> </div>
<template #tip>
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件且不超过 10MB </div>
</template>
</ElUpload>
</ElFormItem>
<ElFormItem label="备注说明" prop="remark">
<ElInput
v-model="importFormData.remark"
type="textarea"
placeholder="请输入备注说明(可选)"
:rows="3"
maxlength="200"
show-word-limit
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="importDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleImportSubmit" :loading="importLoading">
确认导入
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElTag, ElMessage, ElMessageBox, ElUpload, ElIcon, ElDivider } from 'element-plus'
import { UploadFilled, Download } from '@element-plus/icons-vue'
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'CardReplacement' })
const importDialogVisible = ref(false)
const loading = ref(false)
const importLoading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
processResult: '',
cardCompany: '',
oldSimNumber: '',
newSimNumber: '',
newCardOperator: '',
submitDateRange: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 导入表单实例
const importFormRef = ref<FormInstance>()
const uploadRef = ref<UploadInstance>()
// 导入表单数据
const importFormData = reactive({
excelFile: null as File | null,
remark: ''
})
// 模拟数据
const mockData = [
{
id: 1,
iccid: '89860621370079892035',
currentPackage: '随意联畅玩年卡套餐12个月',
cardCompany: '联通2',
hierarchyRelation: '一级代理->二级代理->终端用户',
recipientName: '张丽丽',
recipientPhone: '138****5678',
recipientAddress: '北京市朝阳区建国门外大街1号',
newSimNumber: '1440012345778',
processor: '张若暄',
processTime: '2025-11-08 14:30:00',
processDescription: '用户申请换卡,原卡损坏',
processStatus: '处理完成',
submitTime: '2025-11-08 10:00:00'
},
{
id: 2,
iccid: '89860621370079892036',
currentPackage: '如意包年3G流量包',
cardCompany: '联通36',
hierarchyRelation: '总代理->分销商',
recipientName: '王小明',
recipientPhone: '139****1234',
recipientAddress: '上海市浦东新区陆家嘴环路1000号',
newSimNumber: '1440012345779',
processor: '孔丽娟',
processTime: '2025-11-07 16:15:00',
processDescription: '卡槽损坏需要更换新卡',
processStatus: '处理中',
submitTime: '2025-11-07 09:30:00'
},
{
id: 3,
iccid: '89860621370079892037',
currentPackage: 'Y-NB专享套餐',
cardCompany: 'SXKJ-NB',
hierarchyRelation: '直营',
recipientName: 'HNSXKJ',
recipientPhone: '135****9876',
recipientAddress: '广州市天河区珠江新城花城大道5号',
newSimNumber: '1440012345780',
processor: '李佳音',
processTime: '2025-11-06 11:20:00',
processDescription: '设备升级需要更换新规格SIM卡',
processStatus: '审核中',
submitTime: '2025-11-06 08:45:00'
},
{
id: 4,
iccid: '89860621370079892038',
currentPackage: '100G全国流量月卡套餐',
cardCompany: '联通1-1',
hierarchyRelation: '一级代理->终端用户',
recipientName: '赵强',
recipientPhone: '137****5555',
recipientAddress: '深圳市南山区科技园南区深南大道10000号',
newSimNumber: '1440012345781',
processor: '张若暄',
processTime: '',
processDescription: '',
processStatus: '待处理',
submitTime: '2025-11-05 15:10:00'
},
{
id: 5,
iccid: '89860621370079892039',
currentPackage: '广电飞悦卡无预存50G30天',
cardCompany: '广电4',
hierarchyRelation: '二级代理->终端用户',
recipientName: '李丽',
recipientPhone: '133****7777',
recipientAddress: '杭州市西湖区文三路90号',
newSimNumber: '1440012345782',
processor: '赵强',
processTime: '2025-11-04 09:30:00',
processDescription: '换卡申请已拒绝,原因:信息不完整',
processStatus: '已拒绝',
submitTime: '2025-11-04 08:00:00'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getCardReplacementList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getCardReplacementList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '处理结果',
prop: 'processResult',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '处理完成', value: 'completed' },
{ label: '处理中', value: 'processing' },
{ label: '审核中', value: 'reviewing' },
{ label: '待处理', value: 'pending' },
{ label: '已拒绝', value: 'rejected' }
],
onChange: handleFormChange
},
{
label: '开卡公司',
prop: 'cardCompany',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '联通2', value: 'unicom2' },
{ label: '联通36', value: 'unicom36' },
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
{ label: '联通1-1', value: 'unicom1_1' },
{ label: '联通8', value: 'unicom8' },
{ label: '移动21', value: 'mobile21' },
{ label: '广电4', value: 'gdtv4' },
{ label: '电信9', value: 'telecom9' }
],
onChange: handleFormChange
},
{
label: '换卡SIM号',
prop: 'oldSimNumber',
type: 'input',
config: {
clearable: true,
placeholder: '请输入原SIM号'
},
onChange: handleFormChange
},
{
label: '新SIM号',
prop: 'newSimNumber',
type: 'input',
config: {
clearable: true,
placeholder: '请输入新SIM号'
},
onChange: handleFormChange
},
{
label: '新卡运营商',
prop: 'newCardOperator',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '中国移动', value: 'mobile' },
{ label: '中国联通', value: 'unicom' },
{ label: '中国电信', value: 'telecom' },
{ label: 'GS移动', value: 'gs_mobile' },
{ label: 'DC物联', value: 'dc_iot' },
{ label: 'GDWL', value: 'gdwl' }
],
onChange: handleFormChange
},
{
label: '提交时间',
prop: 'submitDateRange',
type: 'daterange',
config: {
type: 'daterange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: 'ICCID号', prop: 'iccid' },
{ label: '当前套餐', prop: 'currentPackage' },
{ label: '开卡公司', prop: 'cardCompany' },
{ label: '层级关系', prop: 'hierarchyRelation' },
{ label: '收货人姓名', prop: 'recipientName' },
{ label: '收货人电话', prop: 'recipientPhone' },
{ label: '收货人地址', prop: 'recipientAddress' },
{ label: '新sim号', prop: 'newSimNumber' },
{ label: '处理人', prop: 'processor' },
{ label: '处理时间', prop: 'processTime' },
{ label: '处理描述', prop: 'processDescription' },
{ label: '处理状态', prop: 'processStatus' },
{ label: '提交时间', prop: 'submitTime' },
{ label: '操作', prop: 'operation' }
]
// 获取处理状态标签类型
const getProcessStatusType = (status: string) => {
switch (status) {
case '处理完成':
return 'success'
case '处理中':
return 'warning'
case '审核中':
return 'info'
case '待处理':
return 'primary'
case '已拒绝':
return 'danger'
default:
return 'info'
}
}
// 导出Excel
const exportExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条换卡记录`)
}
// 显示导入对话框
const showImportDialog = () => {
importDialogVisible.value = true
// 重置表单
if (importFormRef.value) {
importFormRef.value.resetFields()
}
importFormData.excelFile = null
importFormData.remark = ''
}
// 下载模板
const downloadTemplate = () => {
ElMessage.success('正在下载换卡记录导入模板...')
// 这里可以实现实际的模板下载功能
}
// 查看详情
const viewDetails = (row: any) => {
ElMessage.info(`查看换卡详情: ${row.iccid}`)
}
// 编辑记录
const editRecord = (row: any) => {
ElMessage.info(`编辑换卡记录: ${row.iccid}`)
}
// 删除记录
const deleteRecord = (row: any) => {
ElMessageBox.confirm(`确定要删除该换卡记录吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('删除成功')
getCardReplacementList()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
// 处理换卡申请
const processApplication = (row: any) => {
if (row.processStatus === '处理完成' || row.processStatus === '已拒绝') {
ElMessage.warning('该申请已处理完成')
return
}
ElMessage.info(`处理换卡申请: ${row.iccid}`)
// 这里可以打开处理对话框或跳转到处理页面
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'iccid',
label: 'ICCID号',
minWidth: 180
},
{
prop: 'currentPackage',
label: '当前套餐',
minWidth: 200
},
{
prop: 'cardCompany',
label: '开卡公司',
width: 120
},
{
prop: 'hierarchyRelation',
label: '层级关系',
minWidth: 150
},
{
prop: 'recipientName',
label: '收货人姓名',
width: 120
},
{
prop: 'recipientPhone',
label: '收货人电话',
width: 130
},
{
prop: 'recipientAddress',
label: '收货人地址',
minWidth: 200
},
{
prop: 'newSimNumber',
label: '新sim号',
width: 140
},
{
prop: 'processor',
label: '处理人',
width: 100
},
{
prop: 'processTime',
label: '处理时间',
width: 160
},
{
prop: 'processDescription',
label: '处理描述',
minWidth: 200
},
{
prop: 'processStatus',
label: '处理状态',
width: 120,
formatter: (row) => {
return h(ElTag, { type: getProcessStatusType(row.processStatus) }, () => row.processStatus)
}
},
{
prop: 'submitTime',
label: '提交时间',
width: 160
},
{
prop: 'operation',
label: '操作',
width: 280,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '查看',
onClick: () => viewDetails(row)
}),
h(ArtButtonTable, {
text: '处理',
disabled: row.processStatus === '处理完成' || row.processStatus === '已拒绝',
onClick: () => processApplication(row)
}),
h(ArtButtonTable, {
text: '编辑',
onClick: () => editRecord(row)
}),
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteRecord(row)
})
])
}
}
])
onMounted(() => {
getCardReplacementList()
})
// 获取换卡管理列表
const getCardReplacementList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取换卡管理列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getCardReplacementList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getCardReplacementList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getCardReplacementList()
}
// 文件上传限制
const handleExceed = () => {
ElMessage.warning('最多只能上传一个文件')
}
// 文件上传前检查
const beforeUpload = (file: UploadRawFile) => {
const isExcel =
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.type === 'application/vnd.ms-excel'
const isLt10M = file.size / 1024 / 1024 < 10
if (!isExcel) {
ElMessage.error('只能上传 Excel 文件!')
return false
}
if (!isLt10M) {
ElMessage.error('上传文件大小不能超过 10MB!')
return false
}
return false // 阻止自动上传
}
// 文件变化处理
const handleFileChange = (file: UploadFile) => {
if (file.raw) {
importFormData.excelFile = file.raw
}
}
// 导入表单验证规则
const importRules = reactive<FormRules>({
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }]
})
// 提交导入
const handleImportSubmit = async () => {
if (!importFormRef.value) return
// 检查文件是否上传
if (!importFormData.excelFile) {
ElMessage.error('请先上传Excel文件')
return
}
await importFormRef.value.validate((valid) => {
if (valid) {
importLoading.value = true
// 模拟导入过程
setTimeout(() => {
ElMessage.success('换卡记录导入成功!')
importDialogVisible.value = false
importLoading.value = false
getCardReplacementList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.card-replacement-page {
// 可以添加特定样式
}
.template-section {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
.template-tip {
font-size: 12px;
color: #909399;
}
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-upload-dragger) {
padding: 40px;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

View File

@@ -1,460 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="card-shutdown-page" id="table-full-screen">
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showShutdownDialog">设置停机时间</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 设置停机时间对话框 -->
<ElDialog
v-model="dialogVisible"
title="设置停机时间"
width="500px"
align-center
:close-on-click-modal="false"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
<ElFormItem label="停机类型" prop="shutdownType">
<ElSelect
v-model="formData.shutdownType"
placeholder="请选择停机类型"
style="width: 100%"
clearable
>
<ElOption label="立即停机" value="immediate" />
<ElOption label="定时停机" value="scheduled" />
<ElOption label="批量停机" value="batch" />
</ElSelect>
</ElFormItem>
<ElFormItem
label="停机时间"
prop="shutdownTime"
v-if="formData.shutdownType === 'scheduled'"
>
<ElDatePicker
v-model="formData.shutdownTime"
type="datetime"
placeholder="请选择停机时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</ElFormItem>
<ElFormItem label="停机原因" prop="reason">
<ElInput
v-model="formData.reason"
type="textarea"
placeholder="请输入停机原因"
:rows="3"
maxlength="200"
show-word-limit
/>
</ElFormItem>
<ElFormItem label="影响范围" prop="affectedCards">
<ElInputNumber
v-model="formData.affectedCards"
:min="1"
:max="10000"
placeholder="请输入影响的网卡数量"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="通知方式" prop="notifyMethod">
<ElCheckboxGroup v-model="formData.notifyMethod">
<ElCheckbox label="短信通知">短信通知</ElCheckbox>
<ElCheckbox label="邮件通知">邮件通知</ElCheckbox>
<ElCheckbox label="系统通知">系统通知</ElCheckbox>
</ElCheckboxGroup>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
确认设置
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import {
ElTag,
ElMessage,
ElMessageBox,
ElSelect,
ElOption,
ElDatePicker,
ElInputNumber,
ElCheckboxGroup,
ElCheckbox
} from 'element-plus'
import type { FormInstance } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
defineOptions({ name: 'CardShutdown' })
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 表单实例
const formRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
shutdownType: '',
shutdownTime: '',
reason: '',
affectedCards: 1,
notifyMethod: [] as string[]
})
// 模拟数据
const mockData = [
{
id: 1,
totalCount: 1000,
operationTime: '2025-11-08 14:30:00',
successCount: 998,
failureCount: 2,
operationType: '立即停机',
operator: '张若暄',
reason: '系统维护'
},
{
id: 2,
totalCount: 500,
operationTime: '2025-11-07 10:15:00',
successCount: 500,
failureCount: 0,
operationType: '定时停机',
operator: '孔丽娟',
reason: '网络升级'
},
{
id: 3,
totalCount: 200,
operationTime: '2025-11-06 16:45:00',
successCount: 195,
failureCount: 5,
operationType: '批量停机',
operator: '张若暄',
reason: '安全检查'
},
{
id: 4,
totalCount: 1500,
operationTime: '2025-11-05 09:30:00',
successCount: 1500,
failureCount: 0,
operationType: '立即停机',
operator: '李佳音',
reason: '应急处理'
},
{
id: 5,
totalCount: 800,
operationTime: '2025-11-04 13:20:00',
successCount: 792,
failureCount: 8,
operationType: '定时停机',
operator: '张若暄',
reason: '定期维护'
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '总数', prop: 'totalCount' },
{ label: '时间', prop: 'operationTime' },
{ label: '成功', prop: 'successCount' },
{ label: '失败', prop: 'failureCount' },
{ label: '操作类型', prop: 'operationType' },
{ label: '操作', prop: 'operation' }
]
// 获取操作类型标签类型
const getOperationTagType = (type: string) => {
switch (type) {
case '立即停机':
return 'danger'
case '定时停机':
return 'warning'
case '批量停机':
return 'info'
default:
return 'primary'
}
}
// 查看详情
const viewDetails = (row: any) => {
ElMessage.info(`查看停机操作的详细信息: ${row.operationType}`)
}
// 删除记录
const deleteRecord = (row: any) => {
ElMessageBox.confirm(`确定要删除该停机记录吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('删除成功')
getShutdownList()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
// 重新执行
const retryOperation = (row: any) => {
ElMessageBox.confirm(`确定要重新执行该停机操作吗?`, '操作确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
.then(() => {
ElMessage.success('重新执行成功')
getShutdownList()
})
.catch(() => {
ElMessage.info('已取消操作')
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'totalCount',
label: '总数',
formatter: (row) => `${row.totalCount} `
},
{
prop: 'operationTime',
label: '时间'
},
{
prop: 'successCount',
label: '成功',
formatter: (row) => {
return h(ElTag, { type: 'success' }, () => `${row.successCount}`)
}
},
{
prop: 'failureCount',
label: '失败',
formatter: (row) => {
return h(
ElTag,
{ type: row.failureCount > 0 ? 'danger' : 'success' },
() => `${row.failureCount}`
)
}
},
{
prop: 'operationType',
label: '操作类型',
formatter: (row) => {
return h(ElTag, { type: getOperationTagType(row.operationType) }, () => row.operationType)
}
},
{
prop: 'operation',
label: '操作',
width: 220,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '查看',
onClick: () => viewDetails(row)
}),
h(ArtButtonTable, {
text: '重试',
onClick: () => retryOperation(row)
}),
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteRecord(row)
})
])
}
}
])
onMounted(() => {
getShutdownList()
})
// 获取停机管理列表
const getShutdownList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取停机管理列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getShutdownList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getShutdownList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getShutdownList()
}
// 显示停机设置对话框
const showShutdownDialog = () => {
dialogVisible.value = true
// 重置表单
if (formRef.value) {
formRef.value.resetFields()
}
formData.shutdownType = ''
formData.shutdownTime = ''
formData.reason = ''
formData.affectedCards = 1
formData.notifyMethod = []
}
// 表单验证规则
const rules = reactive<FormRules>({
shutdownType: [{ required: true, message: '请选择停机类型', trigger: 'change' }],
shutdownTime: [{ required: true, message: '请选择停机时间', trigger: 'change' }],
reason: [
{ required: true, message: '请输入停机原因', trigger: 'blur' },
{ min: 5, max: 200, message: '停机原因长度在 5 到 200 个字符', trigger: 'blur' }
],
affectedCards: [{ required: true, message: '请输入影响的网卡数量', trigger: 'blur' }]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
// 如果是定时停机,验证停机时间
if (formData.shutdownType === 'scheduled' && !formData.shutdownTime) {
ElMessage.error('定时停机必须选择停机时间')
return
}
await formRef.value.validate((valid) => {
if (valid) {
submitLoading.value = true
// 模拟提交过程
setTimeout(() => {
ElMessage.success(
`停机设置成功!类型:${formData.shutdownType},影响网卡:${formData.affectedCards}`
)
dialogVisible.value = false
submitLoading.value = false
getShutdownList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.card-shutdown-page {
// 可以添加特定样式
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

View File

@@ -1,739 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="card-transfer-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
<ElButton type="success" @click="showBatchTransferDialog">批量转卡</ElButton>
<ElButton @click="exportData">导出</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 批量转卡对话框 -->
<ElDialog
v-model="dialogVisible"
title="批量转卡"
width="600px"
align-center
:close-on-click-modal="false"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
<ElFormItem label="转卡类型" prop="transferType">
<ElSelect
v-model="formData.transferType"
placeholder="请选择转卡类型"
style="width: 100%"
clearable
>
<ElOption label="运营商转接" value="operator_transfer" />
<ElOption label="套餐转换" value="package_transfer" />
<ElOption label="紧急转接" value="emergency_transfer" />
<ElOption label="升级转接" value="upgrade_transfer" />
</ElSelect>
</ElFormItem>
<ElFormItem label="目标运营商" prop="targetOperator">
<ElSelect
v-model="formData.targetOperator"
placeholder="请选择目标运营商"
style="width: 100%"
clearable
>
<ElOption
v-for="operator in operatorOptions"
:key="operator.value"
:label="operator.label"
:value="operator.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="目标套餐" prop="targetPackage">
<ElSelect
v-model="formData.targetPackage"
placeholder="请选择目标套餐"
style="width: 100%"
clearable
>
<ElOption
v-for="pkg in packageOptions"
:key="pkg.value"
:label="pkg.label"
:value="pkg.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="生效时间" prop="effectiveTime">
<ElDatePicker
v-model="formData.effectiveTime"
type="datetime"
placeholder="请选择生效时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</ElFormItem>
<ElFormItem label="是否保留流量" prop="keepTraffic">
<ElRadioGroup v-model="formData.keepTraffic">
<ElRadio :label="true">保留剩余流量</ElRadio>
<ElRadio :label="false">清零重新计算</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="转卡原因" prop="reason">
<ElInput
v-model="formData.reason"
type="textarea"
placeholder="请输入转卡原因"
:rows="3"
maxlength="200"
show-word-limit
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
确认转卡
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import {
ElTag,
ElMessage,
ElMessageBox,
ElSelect,
ElOption,
ElDatePicker,
ElRadioGroup,
ElRadio
} from 'element-plus'
import type { FormInstance } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'CardTransfer' })
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
oldOperator: '',
transferType: '',
iccid: '',
transferDateRange: '',
newOperator: '',
status: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 表单实例
const formRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
transferType: '',
targetOperator: '',
targetPackage: '',
effectiveTime: '',
keepTraffic: true,
reason: ''
})
// 运营商选项
const operatorOptions = ref([
{ label: '中国移动', value: 'mobile' },
{ label: '中国联通', value: 'unicom' },
{ label: '中国电信', value: 'telecom' },
{ label: 'GS移动', value: 'gs_mobile' },
{ label: 'DC物联', value: 'dc_iot' },
{ label: 'GDWL', value: 'gdwl' },
{ label: 'GS联通', value: 'gs_unicom' },
{ label: 'GS电信', value: 'gs_telecom' }
])
// 套餐选项
const packageOptions = ref([
{ label: '随意联畅玩年卡套餐12个月', value: 'package1' },
{ label: '如意包年3G流量包', value: 'package2' },
{ label: 'Y-NB专享套餐', value: 'package3' },
{ label: '100G全国流量月卡套餐', value: 'package4' },
{ label: '广电飞悦卡无预存50G30天', value: 'package5' },
{ label: '5G畅享套餐', value: 'package6' },
{ label: '移动物联网专用套餐', value: 'package7' },
{ label: '电信天翼套餐', value: 'package8' }
])
// 模拟数据
const mockData = [
{
id: 1,
oldCardNumber: '89860621370079892035',
oldAccessNumber: '1440012345678',
newCardNumber: '89860621370079892135',
newAccessNumber: '1440012345778',
transferType: '运营商转接',
oldOperator: '中国移动',
newOperator: '中国联通',
packageName: '随意联畅玩年卡套餐12个月',
totalTraffic: '100GB',
usedTraffic: '25.5GB',
transferTime: '2025-11-08 10:30:00',
operator: '张若暄',
status: '转接成功'
},
{
id: 2,
oldCardNumber: '89860621370079892036',
oldAccessNumber: '1440012345679',
newCardNumber: '89860621370079892136',
newAccessNumber: '1440012345779',
transferType: '套餐转换',
oldOperator: '中国联通',
newOperator: '中国联通',
packageName: '如意包年3G流量包',
totalTraffic: '50GB',
usedTraffic: '12.8GB',
transferTime: '2025-11-07 14:15:00',
operator: '孔丽娟',
status: '转接进行中'
},
{
id: 3,
oldCardNumber: '89860621370079892037',
oldAccessNumber: '1440012345680',
newCardNumber: '89860621370079892137',
newAccessNumber: '1440012345780',
transferType: '紧急转接',
oldOperator: 'GS移动',
newOperator: '中国移动',
packageName: 'Y-NB专享套餐',
totalTraffic: '30GB',
usedTraffic: '28.9GB',
transferTime: '2025-11-06 09:45:00',
operator: '李佳音',
status: '转接失败'
},
{
id: 4,
oldCardNumber: '89860621370079892038',
oldAccessNumber: '1440012345681',
newCardNumber: '89860621370079892138',
newAccessNumber: '1440012345781',
transferType: '升级转接',
oldOperator: '中国电信',
newOperator: '中国电信',
packageName: '100G全国流量月卡套餐',
totalTraffic: '100GB',
usedTraffic: '0GB',
transferTime: '2025-11-05 16:20:00',
operator: '赵强',
status: '等待确认'
},
{
id: 5,
oldCardNumber: '89860621370079892039',
oldAccessNumber: '1440012345682',
newCardNumber: '89860621370079892139',
newAccessNumber: '1440012345782',
transferType: '运营商转接',
oldOperator: 'GDWL',
newOperator: '中国联通',
packageName: '广电飞悦卡无预存50G30天',
totalTraffic: '50GB',
usedTraffic: '35.2GB',
transferTime: '2025-11-04 11:30:00',
operator: '张若暄',
status: '转接成功'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getCardTransferList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getCardTransferList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '老卡运营商',
prop: 'oldOperator',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => operatorOptions.value,
onChange: handleFormChange
},
{
label: '类型',
prop: 'transferType',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '运营商转接', value: 'operator_transfer' },
{ label: '套餐转换', value: 'package_transfer' },
{ label: '紧急转接', value: 'emergency_transfer' },
{ label: '升级转接', value: 'upgrade_transfer' }
],
onChange: handleFormChange
},
{
label: 'ICCID',
prop: 'iccid',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID'
},
onChange: handleFormChange
},
{
label: '转卡时间',
prop: 'transferDateRange',
type: 'daterange',
config: {
type: 'daterange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
onChange: handleFormChange
},
{
label: '新卡运营商',
prop: 'newOperator',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => operatorOptions.value,
onChange: handleFormChange
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '转接成功', value: 'success' },
{ label: '转接进行中', value: 'in_progress' },
{ label: '转接失败', value: 'failed' },
{ label: '等待确认', value: 'pending' }
],
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '老卡卡号', prop: 'oldCardNumber' },
{ label: '老卡接入号', prop: 'oldAccessNumber' },
{ label: '新卡卡号', prop: 'newCardNumber' },
{ label: '新卡接入号', prop: 'newAccessNumber' },
{ label: '类型', prop: 'transferType' },
{ label: '老卡运营商', prop: 'oldOperator' },
{ label: '新卡运营商', prop: 'newOperator' },
{ label: '套餐名称', prop: 'packageName' },
{ label: '总流量', prop: 'totalTraffic' },
{ label: '转卡已使用流量', prop: 'usedTraffic' },
{ label: '转卡时间', prop: 'transferTime' },
{ label: '操作人', prop: 'operator' },
{ label: '状态', prop: 'status' },
{ label: '操作', prop: 'operation' }
]
// 获取状态标签类型
const getStatusType = (status: string) => {
switch (status) {
case '转接成功':
return 'success'
case '转接进行中':
return 'warning'
case '转接失败':
return 'danger'
case '等待确认':
return 'info'
default:
return 'info'
}
}
// 获取转卡类型标签类型
const getTransferTypeTagType = (type: string) => {
switch (type) {
case '运营商转接':
return 'primary'
case '套餐转换':
return 'success'
case '紧急转接':
return 'danger'
case '升级转接':
return 'warning'
default:
return 'info'
}
}
// 导出数据
const exportData = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条转卡记录`)
}
// 查看详情
const viewDetails = (row: any) => {
ElMessage.info(`查看转卡详情: ${row.oldCardNumber} -> ${row.newCardNumber}`)
}
// 取消转卡
const cancelTransfer = (row: any) => {
if (row.status === '转接成功') {
ElMessage.warning('已成功转接的记录无法取消')
return
}
ElMessageBox.confirm(`确定要取消该转卡操作吗?`, '取消确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('转卡操作已取消')
getCardTransferList()
})
.catch(() => {
ElMessage.info('已取消操作')
})
}
// 重新转卡
const retryTransfer = (row: any) => {
if (row.status !== '转接失败') {
ElMessage.warning('只有失败的记录才能重新转卡')
return
}
ElMessageBox.confirm(`确定要重新执行转卡操作吗?`, '操作确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
.then(() => {
ElMessage.success('重新转卡操作已开始')
getCardTransferList()
})
.catch(() => {
ElMessage.info('已取消操作')
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'oldCardNumber',
label: '老卡卡号',
minWidth: 180
},
{
prop: 'oldAccessNumber',
label: '老卡接入号',
width: 140
},
{
prop: 'newCardNumber',
label: '新卡卡号',
minWidth: 180
},
{
prop: 'newAccessNumber',
label: '新卡接入号',
width: 140
},
{
prop: 'transferType',
label: '类型',
width: 120,
formatter: (row) => {
return h(ElTag, { type: getTransferTypeTagType(row.transferType) }, () => row.transferType)
}
},
{
prop: 'oldOperator',
label: '老卡运营商',
width: 120
},
{
prop: 'newOperator',
label: '新卡运营商',
width: 120
},
{
prop: 'packageName',
label: '套餐名称',
minWidth: 200
},
{
prop: 'totalTraffic',
label: '总流量',
width: 100
},
{
prop: 'usedTraffic',
label: '转卡已使用流量',
width: 140
},
{
prop: 'transferTime',
label: '转卡时间',
width: 160
},
{
prop: 'operator',
label: '操作人',
width: 100
},
{
prop: 'status',
label: '状态',
width: 120,
formatter: (row) => {
return h(ElTag, { type: getStatusType(row.status) }, () => row.status)
}
},
{
prop: 'operation',
label: '操作',
width: 220,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '查看',
onClick: () => viewDetails(row)
}),
h(ArtButtonTable, {
text: '重试',
disabled: row.status !== '转接失败',
onClick: () => retryTransfer(row)
}),
h(ArtButtonTable, {
text: '取消',
disabled: row.status === '转接成功',
onClick: () => cancelTransfer(row)
})
])
}
}
])
onMounted(() => {
getCardTransferList()
})
// 获取网卡转接列表
const getCardTransferList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取网卡转接列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getCardTransferList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getCardTransferList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getCardTransferList()
}
// 显示批量转卡对话框
const showBatchTransferDialog = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要转卡的记录')
return
}
dialogVisible.value = true
// 重置表单
if (formRef.value) {
formRef.value.resetFields()
}
formData.transferType = ''
formData.targetOperator = ''
formData.targetPackage = ''
formData.effectiveTime = ''
formData.keepTraffic = true
formData.reason = ''
}
// 表单验证规则
const rules = reactive<FormRules>({
transferType: [{ required: true, message: '请选择转卡类型', trigger: 'change' }],
targetOperator: [{ required: true, message: '请选择目标运营商', trigger: 'change' }],
targetPackage: [{ required: true, message: '请选择目标套餐', trigger: 'change' }],
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }],
reason: [
{ required: true, message: '请输入转卡原因', trigger: 'blur' },
{ min: 5, max: 200, message: '转卡原因长度在 5 到 200 个字符', trigger: 'blur' }
]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
submitLoading.value = true
// 模拟提交过程
setTimeout(() => {
const targetOperatorName = operatorOptions.value.find(
(op) => op.value === formData.targetOperator
)?.label
const targetPackageName = packageOptions.value.find(
(pkg) => pkg.value === formData.targetPackage
)?.label
ElMessage.success(
`批量转卡操作提交成功!目标运营商:${targetOperatorName},目标套餐:${targetPackageName},影响${selectedRows.value.length}张网卡`
)
dialogVisible.value = false
submitLoading.value = false
getCardTransferList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.card-transfer-page {
// 可以添加特定样式
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

View File

@@ -1,699 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="my-cards-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton @click="exportExcel">导出excel</ElButton>
<ElButton @click="cardDistribution">网卡分销</ElButton>
<ElButton @click="batchRecharge">批量充值</ElButton>
<ElButton @click="cardRecycle">网卡回收</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
</ElCard>
<!-- 网卡分销弹框 -->
<CardOperationDialog
v-model:visible="distributionDialogVisible"
title="网卡分销"
select-label="代理商"
select-placeholder="请搜索并选择代理商"
:selected-cards="selectedRows"
:remote-search="searchAgents"
@confirm="handleDistributionConfirm"
@close="handleDialogClose"
/>
<!-- 批量充值弹框 -->
<CardOperationDialog
v-model:visible="rechargeDialogVisible"
title="批量充值"
select-label="套餐"
select-placeholder="请搜索并选择套餐"
:show-amount="true"
:selected-cards="selectedRows"
:remote-search="searchPackages"
@confirm="handleRechargeConfirm"
@close="handleDialogClose"
/>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
import CardOperationDialog from '@/components/business/CardOperationDialog.vue'
defineOptions({ name: 'MyCards' })
const loading = ref(false)
// 弹框状态
const distributionDialogVisible = ref(false)
const rechargeDialogVisible = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
distributor: '',
cardStatus: '',
importDateRange: '',
cardCompany: '',
virtualNumber: '',
iccid: '',
iccidRange: '',
cardPackage: '',
virtualNumberRange: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 模拟数据
const mockData = [
{
id: 1,
cardCompany: '联通2',
iccid: '89860621370079892035',
virtualNumber: '10655001234',
expireTime: '2025-12-31',
packageName: '随意联畅玩年卡套餐12个月',
distributorName: '张丽丽',
cardStatus: '正常'
},
{
id: 2,
cardCompany: '联通36',
iccid: '89860621370079892036',
virtualNumber: '10655001235',
expireTime: '2025-11-30',
packageName: '如意包年3G流量包',
distributorName: '王小明',
cardStatus: '正常'
},
{
id: 3,
cardCompany: 'SXKJ-NB',
iccid: '89860621370079892037',
virtualNumber: '10655001236',
expireTime: '2025-10-31',
packageName: 'Y-NB专享套餐',
distributorName: 'HNSXKJ',
cardStatus: '待激活'
},
{
id: 4,
cardCompany: '联通1-1',
iccid: '89860621370079892038',
virtualNumber: '10655001237',
expireTime: '2026-01-31',
packageName: '100G全国流量月卡套餐',
distributorName: '未分销',
cardStatus: '正常'
},
{
id: 5,
cardCompany: '广电4',
iccid: '89860621370079892039',
virtualNumber: '10655001238',
expireTime: '2025-08-31',
packageName: '广电飞悦卡无预存50G30天',
distributorName: '孔丽娟',
cardStatus: '停机'
},
{
id: 6,
cardCompany: '联通8',
iccid: '89860621370079892040',
virtualNumber: '10655001239',
expireTime: '2025-09-15',
packageName: '5G畅享套餐',
distributorName: '李佳音',
cardStatus: '正常'
},
{
id: 7,
cardCompany: '移动21',
iccid: '89860621370079892041',
virtualNumber: '10655001240',
expireTime: '2025-07-20',
packageName: '移动物联网专用套餐',
distributorName: '赵强',
cardStatus: '已过期'
},
{
id: 8,
cardCompany: '电信9',
iccid: '89860621370079892042',
virtualNumber: '10655001241',
expireTime: '2026-02-28',
packageName: '电信天翼套餐',
distributorName: '未分销',
cardStatus: '正常'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getMyCardsList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getMyCardsList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '分销商',
prop: 'distributor',
type: 'input',
config: {
clearable: true,
placeholder: '请输入分销商'
},
onChange: handleFormChange
},
{
label: '网卡状态',
prop: 'cardStatus',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '正常', value: 'normal' },
{ label: '待激活', value: 'pending' },
{ label: '停机', value: 'suspended' },
{ label: '已过期', value: 'expired' },
{ label: '注销', value: 'cancelled' }
],
onChange: handleFormChange
},
{
label: '导入时间',
prop: 'importDateRange',
type: 'daterange',
config: {
type: 'daterange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
onChange: handleFormChange
},
{
label: '开卡公司',
prop: 'cardCompany',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '联通2', value: 'unicom2' },
{ label: '联通36', value: 'unicom36' },
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
{ label: '联通1-1', value: 'unicom1_1' },
{ label: '联通8', value: 'unicom8' },
{ label: '移动21', value: 'mobile21' },
{ label: '广电4', value: 'gdtv4' },
{ label: '电信9', value: 'telecom9' }
],
onChange: handleFormChange
},
{
label: '虚拟号',
prop: 'virtualNumber',
type: 'input',
config: {
clearable: true,
placeholder: '请输入虚拟号'
},
onChange: handleFormChange
},
{
label: 'ICCID号',
prop: 'iccid',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID号'
},
onChange: handleFormChange
},
{
label: 'ICCID号段',
prop: 'iccidRange',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID号段8986001-8986999'
},
onChange: handleFormChange
},
{
label: '卡套餐',
prop: 'cardPackage',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '随意联畅玩年卡套餐', value: 'package1' },
{ label: '如意包年3G流量包', value: 'package2' },
{ label: 'Y-NB专享套餐', value: 'package3' },
{ label: '100G全国流量月卡套餐', value: 'package4' },
{ label: '广电飞悦卡无预存50G', value: 'package5' },
{ label: '5G畅享套餐', value: 'package6' },
{ label: '移动物联网专用套餐', value: 'package7' },
{ label: '电信天翼套餐', value: 'package8' }
],
onChange: handleFormChange
},
{
label: '虚拟号段',
prop: 'virtualNumberRange',
type: 'input',
config: {
clearable: true,
placeholder: '请输入虚拟号段10655001-10655999'
},
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '开卡公司', prop: 'cardCompany' },
{ label: 'ICCID卡号', prop: 'iccid' },
{ label: '虚拟号', prop: 'virtualNumber' },
{ label: '到期时间', prop: 'expireTime' },
{ label: '套餐', prop: 'packageName' },
{ label: '分销商姓名', prop: 'distributorName' },
{ label: '卡状态', prop: 'cardStatus' },
{ label: '操作', prop: 'operation' }
]
// 获取卡状态标签类型
const getCardStatusType = (status: string) => {
switch (status) {
case '正常':
return 'success'
case '待激活':
return 'warning'
case '停机':
case '注销':
return 'danger'
case '已过期':
return 'info'
default:
return 'info'
}
}
// 导出Excel
const exportExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条记录到Excel`)
}
// 网卡分销
const cardDistribution = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要分销的网卡')
return
}
distributionDialogVisible.value = true
}
// 批量充值
const batchRecharge = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要充值的网卡')
return
}
rechargeDialogVisible.value = true
}
// 网卡回收
const cardRecycle = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要回收的网卡')
return
}
ElMessageBox.confirm(
`确定要回收所选的 ${selectedRows.value.length} 张网卡吗?回收后将无法恢复。`,
'网卡回收确认',
{
confirmButtonText: '确认回收',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: false
}
)
.then(() => {
handleCardRecycle()
})
.catch(() => {
ElMessage.info('已取消回收操作')
})
}
// 查看详情
const viewDetails = (row: any) => {
ElMessage.info(`查看网卡 ${row.iccid} 的详细信息`)
}
// 编辑网卡
const editCard = (row: any) => {
ElMessage.info(`编辑网卡 ${row.iccid}`)
}
// 删除网卡
const deleteCard = (row: any) => {
ElMessageBox.confirm(`确定要删除ICCID为 ${row.iccid} 的网卡吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success(`已删除网卡 ${row.iccid}`)
getMyCardsList()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'cardCompany',
label: '开卡公司',
width: 120
},
{
prop: 'iccid',
label: 'ICCID卡号',
minWidth: 180
},
{
prop: 'virtualNumber',
label: '虚拟号',
width: 120
},
{
prop: 'expireTime',
label: '到期时间',
width: 120
},
{
prop: 'packageName',
label: '套餐',
minWidth: 200
},
{
prop: 'distributorName',
label: '分销商姓名',
width: 120
},
{
prop: 'cardStatus',
label: '卡状态',
width: 100,
formatter: (row) => {
return h(ElTag, { type: getCardStatusType(row.cardStatus) }, () => row.cardStatus)
}
},
{
prop: 'operation',
label: '操作',
width: 220,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '查看',
onClick: () => viewDetails(row)
}),
h(ArtButtonTable, {
text: '编辑',
onClick: () => editCard(row)
}),
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteCard(row)
})
])
}
}
])
onMounted(() => {
getMyCardsList()
})
// 获取我的网卡列表
const getMyCardsList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取我的网卡列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getMyCardsList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getMyCardsList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getMyCardsList()
}
// 远程搜索代理商
const searchAgents = async (query: string) => {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 300))
// 模拟代理商数据
const allAgents = [
{ label: '张丽丽代理商', value: 'agent_001' },
{ label: '王小明代理商', value: 'agent_002' },
{ label: 'HNSXKJ代理商', value: 'agent_003' },
{ label: '孔丽娟代理商', value: 'agent_004' },
{ label: '李四代理商', value: 'agent_005' },
{ label: '赵六代理商', value: 'agent_006' },
{ label: '刘备代理商', value: 'agent_007' },
{ label: '关羽代理商', value: 'agent_008' },
{ label: '张飞代理商', value: 'agent_009' },
{ label: '赵云代理商', value: 'agent_010' },
{ label: '黄忠代理商', value: 'agent_011' },
{ label: '马超代理商', value: 'agent_012' },
{ label: '诸葛亮代理商', value: 'agent_013' },
{ label: '周瑜代理商', value: 'agent_014' },
{ label: '孙权代理商', value: 'agent_015' }
]
// 根据查询条件过滤
let filteredAgents = allAgents
if (query) {
filteredAgents = allAgents.filter((agent) =>
agent.label.toLowerCase().includes(query.toLowerCase())
)
}
// 返回前10条数据
return filteredAgents.slice(0, 10)
}
// 远程搜索套餐
const searchPackages = async (query: string) => {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 300))
// 模拟套餐数据
const allPackages = [
{ label: '随意联畅玩年卡套餐12个月', value: 'package_001' },
{ label: '如意包年3G流量包', value: 'package_002' },
{ label: 'Y-NB专享套餐', value: 'package_003' },
{ label: '100G全国流量月卡套餐', value: 'package_004' },
{ label: '广电飞悦卡无预存50G30天', value: 'package_005' },
{ label: '移动畅享套餐50G', value: 'package_006' },
{ label: '联通大王卡19元', value: 'package_007' },
{ label: '电信星卡29元', value: 'package_008' },
{ label: '移动无限流量卡99元', value: 'package_009' },
{ label: '联通冰淇淋套餐199元', value: 'package_010' },
{ label: '电信天翼云卡59元', value: 'package_011' },
{ label: '移动神州行卡39元', value: 'package_012' },
{ label: '联通腾讯王卡免流', value: 'package_013' },
{ label: '电信达量降速套餐', value: 'package_014' },
{ label: '广电5G畅享套餐', value: 'package_015' }
]
// 根据查询条件过滤
let filteredPackages = allPackages
if (query) {
filteredPackages = allPackages.filter((pkg) =>
pkg.label.toLowerCase().includes(query.toLowerCase())
)
}
// 返回前10条数据
return filteredPackages.slice(0, 10)
}
// 处理网卡分销确认
const handleDistributionConfirm = async (data: any) => {
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 1000))
ElMessage.success(`成功将 ${data.selectedCards.length} 张网卡分销给代理商`)
distributionDialogVisible.value = false
// 刷新列表
getMyCardsList()
} catch (error) {
console.log('分销操作失败,请重试')
}
}
// 处理批量充值确认
const handleRechargeConfirm = async (data: any) => {
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 1000))
ElMessage.success(`成功为 ${data.selectedCards.length} 张网卡充值 ${data.amount}`)
rechargeDialogVisible.value = false
// 刷新列表
getMyCardsList()
} catch (error) {
console.log('充值操作失败,请重试')
}
}
// 处理网卡回收
const handleCardRecycle = async () => {
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 1000))
ElMessage.success(`成功回收 ${selectedRows.value.length} 张网卡`)
// 从列表中移除回收的网卡
const recycledIds = selectedRows.value.map((row) => row.id)
tableData.value = tableData.value.filter((item) => !recycledIds.includes(item.id))
selectedRows.value = []
} catch (error) {
console.log('回收操作失败,请重试')
}
}
// 处理弹框关闭
const handleDialogClose = () => {
// 可以在这里添加额外的关闭逻辑
}
</script>
<style lang="scss" scoped>
.my-cards-page {
// 可以添加特定样式
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
</style>

View File

@@ -1,705 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="offline-batch-recharge-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 数据视图组件 -->
<ArtDataViewer
ref="dataViewerRef"
:data="tableData"
:loading="loading"
:table-columns="columns"
:descriptions-fields="descriptionsFields"
:descriptions-columns="2"
:pagination="pagination"
:label-width="'120px'"
:field-columns="columnChecks"
:show-card-actions="true"
:show-card-selection="true"
:default-view="currentView"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@view-change="handleViewChange"
>
<template #header-left>
<ElButton type="primary" @click="handleSearch">搜索查询</ElButton>
<ElButton type="success" @click="showBatchRechargeDialog">批量充值</ElButton>
<ElButton @click="exportExcel">导出excel</ElButton>
</template>
<template #header-right>
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
v-model:currentView="currentView"
@refresh="handleRefresh"
@viewChange="handleViewChange"
:show-title="false"
:show-view-toggle="true"
/>
</template>
<template #card-actions="{ item }">
<ArtButtonTable text="查看" @click="viewDetails(item)" />
<ArtButtonTable text="重试" @click="retryRecharge(item)" />
<ArtButtonTable text="删除" @click="deleteRecord(item)" />
</template>
</ArtDataViewer>
<!-- 批量充值对话框 -->
<ElDialog
v-model="dialogVisible"
title="批量充值"
width="600px"
align-center
:close-on-click-modal="false"
>
<!-- 顶部下载模板按钮 -->
<div class="template-section">
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
<span class="template-tip">请先下载模板按模板格式填写后上传</span>
</div>
<ElDivider />
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
<ElFormItem label="上传Excel文件" prop="excelFile">
<ElUpload
ref="uploadRef"
:limit="1"
:on-exceed="handleExceed"
:before-upload="beforeUpload"
:on-change="handleFileChange"
:auto-upload="false"
accept=".xlsx,.xls"
drag
>
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
<div class="el-upload__text"> 将文件拖到此处<em>点击上传</em> </div>
<template #tip>
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件且不超过 10MB </div>
</template>
</ElUpload>
</ElFormItem>
<ElFormItem label="选择套餐" prop="packageId">
<ElSelect
v-model="formData.packageId"
placeholder="请选择套餐"
style="width: 100%"
clearable
>
<ElOption
v-for="pkg in packageOptions"
:key="pkg.value"
:label="pkg.label"
:value="pkg.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="生效方式" prop="effectiveMethod">
<ElSelect
v-model="formData.effectiveMethod"
placeholder="请选择生效方式"
style="width: 100%"
clearable
>
<ElOption
v-for="method in effectiveMethods"
:key="method.value"
:label="method.label"
:value="method.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
label="生效时间"
prop="effectiveTime"
v-if="formData.effectiveMethod === 'scheduled'"
>
<ElDatePicker
v-model="formData.effectiveTime"
type="datetime"
placeholder="请选择生效时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</ElFormItem>
<ElFormItem label="备注说明" prop="remark">
<ElInput
v-model="formData.remark"
type="textarea"
placeholder="请输入备注说明(可选)"
:rows="3"
maxlength="200"
show-word-limit
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
确认充值
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import {
ElTag,
ElMessage,
ElMessageBox,
ElUpload,
ElIcon,
ElSelect,
ElOption,
ElDatePicker,
ElDivider,
ElButton
} from 'element-plus'
import { UploadFilled, Download } from '@element-plus/icons-vue'
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtDataViewer from '@/components/core/views/ArtDataViewer.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'OfflineBatchRecharge' })
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
// 当前视图模式
const currentView = ref('table')
// 数据视图组件引用
const dataViewerRef = ref()
// 定义表单搜索初始值
const initialSearchState = {
importDateRange: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 表单实例
const formRef = ref<FormInstance>()
const uploadRef = ref<UploadInstance>()
// 表单数据
const formData = reactive({
excelFile: null as File | null,
packageId: '',
effectiveMethod: '',
effectiveTime: '',
remark: ''
})
// 套餐选项
const packageOptions = ref([
{ label: '随意联畅玩年卡套餐12个月', value: 'package1' },
{ label: '如意包年3G流量包', value: 'package2' },
{ label: 'Y-NB专享套餐', value: 'package3' },
{ label: '100G全国流量月卡套餐', value: 'package4' },
{ label: '广电飞悦卡无预存50G30天', value: 'package5' },
{ label: '5G畅享套餐', value: 'package6' },
{ label: '移动物联网专用套餐', value: 'package7' },
{ label: '电信天翼套餐', value: 'package8' }
])
// 生效方式选项
const effectiveMethods = ref([
{ label: '立即生效', value: 'immediate' },
{ label: '定时生效', value: 'scheduled' },
{ label: '次月生效', value: 'next_month' },
{ label: '手动激活', value: 'manual' }
])
// 模拟数据
const mockData = [
{
id: 1,
iccidStart: '89860621370079892001',
iccidEnd: '89860621370079892100',
importTime: '2025-11-08 10:30:00',
importCount: 100,
successCount: 98,
failureCount: 2,
packageName: '随意联畅玩年卡套餐12个月',
packageCount: 100,
operator: '张若暄'
},
{
id: 2,
iccidStart: '89860621370079892101',
iccidEnd: '89860621370079892200',
importTime: '2025-11-07 14:15:00',
importCount: 100,
successCount: 100,
failureCount: 0,
packageName: '如意包年3G流量包',
packageCount: 100,
operator: '孔丽娟'
},
{
id: 3,
iccidStart: '89860621370079892201',
iccidEnd: '89860621370079892250',
importTime: '2025-11-06 09:45:00',
importCount: 50,
successCount: 45,
failureCount: 5,
packageName: 'Y-NB专享套餐',
packageCount: 50,
operator: '李佳音'
},
{
id: 4,
iccidStart: '89860621370079892251',
iccidEnd: '89860621370079892350',
importTime: '2025-11-05 16:20:00',
importCount: 100,
successCount: 97,
failureCount: 3,
packageName: '100G全国流量月卡套餐',
packageCount: 100,
operator: '张若暄'
},
{
id: 5,
iccidStart: '89860621370079892351',
iccidEnd: '89860621370079892400',
importTime: '2025-11-04 11:30:00',
importCount: 50,
successCount: 50,
failureCount: 0,
packageName: '广电飞悦卡无预存50G30天',
packageCount: 50,
operator: '赵强'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getOfflineBatchRechargeList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getOfflineBatchRechargeList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '导入时间',
prop: 'importDateRange',
type: 'daterange',
config: {
type: 'daterange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: 'ICCID开始号段', prop: 'iccidStart' },
{ label: 'ICCID结束号段', prop: 'iccidEnd' },
{ label: '导入时间', prop: 'importTime' },
{ label: '导入数量', prop: 'importCount' },
{ label: '成功数量', prop: 'successCount' },
{ label: '失败数量', prop: 'failureCount' },
{ label: '套餐名称', prop: 'packageName' },
{ label: '套餐数量', prop: 'packageCount' },
{ label: '操作人', prop: 'operator' },
{ label: '操作', prop: 'operation' }
]
// 描述字段配置
const descriptionsFields = [
{ prop: 'iccidStart', label: 'ICCID开始号段' },
{ prop: 'iccidEnd', label: 'ICCID结束号段' },
{ prop: 'importTime', label: '导入时间' },
{ prop: 'importCount', label: '导入数量', formatter: (row: any) => `${row.importCount}` },
{
prop: 'successCount',
label: '成功数量',
formatter: (row: any) => {
return `<el-tag type="success" size="small">${row.successCount} 张</el-tag>`
}
},
{
prop: 'failureCount',
label: '失败数量',
formatter: (row: any) => {
const type = row.failureCount > 0 ? 'danger' : 'success'
return `<el-tag type="${type}" size="small">${row.failureCount} 张</el-tag>`
}
},
{ prop: 'packageName', label: '套餐名称', span: 2 },
{ prop: 'packageCount', label: '套餐数量', formatter: (row: any) => `${row.packageCount}` },
{ prop: 'operator', label: '操作人' }
]
// 导出Excel
const exportExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条记录到Excel`)
}
// 下载模板
const downloadTemplate = () => {
ElMessage.success('正在下载充值模板文件...')
// 这里可以实现实际的模板下载功能
}
// 查看详情
const viewDetails = (row: any) => {
ElMessage.info(`查看充值记录详情: ${row.iccidStart} - ${row.iccidEnd}`)
}
// 删除记录
const deleteRecord = (row: any) => {
ElMessageBox.confirm(`确定要删除该充值记录吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('删除成功')
getOfflineBatchRechargeList()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
// 重新充值
const retryRecharge = (row: any) => {
ElMessageBox.confirm(`确定要重新执行该批次的充值操作吗?`, '操作确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
.then(() => {
ElMessage.success('重新充值操作已开始')
getOfflineBatchRechargeList()
})
.catch(() => {
ElMessage.info('已取消操作')
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'iccidStart',
label: 'ICCID开始号段',
minWidth: 220
},
{
prop: 'iccidEnd',
label: 'ICCID结束号段',
minWidth: 220
},
{
prop: 'importTime',
label: '导入时间',
width: 160
},
{
prop: 'importCount',
label: '导入数量',
width: 100,
formatter: (row) => `${row.importCount}`
},
{
prop: 'successCount',
label: '成功数量',
width: 100,
formatter: (row) => {
return h(ElTag, { type: 'success' }, () => `${row.successCount}`)
}
},
{
prop: 'failureCount',
label: '失败数量',
width: 100,
formatter: (row) => {
return h(
ElTag,
{ type: row.failureCount > 0 ? 'danger' : 'success' },
() => `${row.failureCount}`
)
}
},
{
prop: 'packageName',
label: '套餐名称',
minWidth: 200
},
{
prop: 'packageCount',
label: '套餐数量',
width: 100,
formatter: (row) => `${row.packageCount}`
},
{
prop: 'operator',
label: '操作人',
width: 100
},
{
prop: 'operation',
label: '操作',
width: 220,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '查看',
onClick: () => viewDetails(row)
}),
h(ArtButtonTable, {
text: '重试',
onClick: () => retryRecharge(row)
}),
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteRecord(row)
})
])
}
}
])
onMounted(() => {
getOfflineBatchRechargeList()
})
// 获取线下批量充值列表
const getOfflineBatchRechargeList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取线下批量充值列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getOfflineBatchRechargeList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getOfflineBatchRechargeList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getOfflineBatchRechargeList()
}
// 处理视图切换
const handleViewChange = (view: string) => {
console.log('视图切换到:', view)
// 可以在这里添加视图切换的额外逻辑,比如保存用户偏好
}
// 显示批量充值对话框
const showBatchRechargeDialog = () => {
dialogVisible.value = true
// 重置表单
if (formRef.value) {
formRef.value.resetFields()
}
formData.excelFile = null
formData.packageId = ''
formData.effectiveMethod = ''
formData.effectiveTime = ''
formData.remark = ''
}
// 文件上传限制
const handleExceed = () => {
ElMessage.warning('最多只能上传一个文件')
}
// 文件上传前检查
const beforeUpload = (file: UploadRawFile) => {
const isExcel =
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.type === 'application/vnd.ms-excel'
const isLt10M = file.size / 1024 / 1024 < 10
if (!isExcel) {
ElMessage.error('只能上传 Excel 文件!')
return false
}
if (!isLt10M) {
ElMessage.error('上传文件大小不能超过 10MB!')
return false
}
return false // 阻止自动上传
}
// 文件变化处理
const handleFileChange = (file: UploadFile) => {
if (file.raw) {
formData.excelFile = file.raw
}
}
// 表单验证规则
const rules = reactive<FormRules>({
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }],
packageId: [{ required: true, message: '请选择套餐', trigger: 'change' }],
effectiveMethod: [{ required: true, message: '请选择生效方式', trigger: 'change' }],
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
// 检查文件是否上传
if (!formData.excelFile) {
ElMessage.error('请先上传Excel文件')
return
}
// 如果是定时生效,验证生效时间
if (formData.effectiveMethod === 'scheduled' && !formData.effectiveTime) {
ElMessage.error('定时生效必须选择生效时间')
return
}
await formRef.value.validate((valid) => {
if (valid) {
submitLoading.value = true
// 模拟提交过程
setTimeout(() => {
const packageName = packageOptions.value.find(
(p) => p.value === formData.packageId
)?.label
const effectiveMethodName = effectiveMethods.value.find(
(m) => m.value === formData.effectiveMethod
)?.label
ElMessage.success(
`批量充值提交成功!套餐:${packageName},生效方式:${effectiveMethodName}`
)
dialogVisible.value = false
submitLoading.value = false
getOfflineBatchRechargeList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.offline-batch-recharge-page {
// 可以添加特定样式
}
.template-section {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
.template-tip {
font-size: 12px;
color: #909399;
}
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-upload-dragger) {
padding: 40px;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

View File

@@ -1,689 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="package-gift-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
<ElButton type="success" @click="showBatchImportDialog">批量导入</ElButton>
<ElButton type="danger" @click="batchDelete">批量删除</ElButton>
<ElButton @click="exportExcel">导出excel</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 批量导入对话框 -->
<ElDialog
v-model="importDialogVisible"
title="批量导入套餐赠送"
width="500px"
align-center
:close-on-click-modal="false"
>
<!-- 顶部下载模板按钮 -->
<div class="template-section">
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
<span class="template-tip">请先下载模板按模板格式填写后上传</span>
</div>
<ElDivider />
<ElForm
ref="importFormRef"
:model="importFormData"
:rules="importRules"
label-width="120px"
>
<ElFormItem label="上传Excel文件" prop="excelFile">
<ElUpload
ref="uploadRef"
:limit="1"
:on-exceed="handleExceed"
:before-upload="beforeUpload"
:on-change="handleFileChange"
:auto-upload="false"
accept=".xlsx,.xls"
drag
>
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
<div class="el-upload__text"> 将文件拖到此处<em>点击上传</em> </div>
<template #tip>
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件且不超过 10MB </div>
</template>
</ElUpload>
</ElFormItem>
<ElFormItem label="备注说明" prop="remark">
<ElInput
v-model="importFormData.remark"
type="textarea"
placeholder="请输入备注说明(可选)"
:rows="3"
maxlength="200"
show-word-limit
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="importDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleImportSubmit" :loading="importLoading">
确认导入
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElTag, ElMessage, ElMessageBox, ElUpload, ElIcon, ElDivider } from 'element-plus'
import { UploadFilled, Download } from '@element-plus/icons-vue'
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'PackageGift' })
const importDialogVisible = ref(false)
const loading = ref(false)
const importLoading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
iccid: '',
accessNumber: '',
cardCompany: '',
isReceived: '',
endDateRange: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 导入表单实例
const importFormRef = ref<FormInstance>()
const uploadRef = ref<UploadInstance>()
// 导入表单数据
const importFormData = reactive({
excelFile: null as File | null,
remark: ''
})
// 模拟数据
const mockData = [
{
id: 1,
iccid: '89860621370079892035',
accessNumber: '1440012345678',
giftPackage: '随意联畅玩年卡套餐12个月',
cardCompany: '联通2',
isReceived: '已领取',
operator: '张若暄',
operationTime: '2025-11-08 10:30:00',
receiveTime: '2025-11-08 14:20:00',
importStatus: '导入成功',
failureReason: ''
},
{
id: 2,
iccid: '89860621370079892036',
accessNumber: '1440012345679',
giftPackage: 'Y-NB专享套餐',
cardCompany: 'SXKJ-NB',
isReceived: '未领取',
operator: '孔丽娟',
operationTime: '2025-11-07 14:15:00',
receiveTime: '',
importStatus: '导入成功',
failureReason: ''
},
{
id: 3,
iccid: '89860621370079892037',
accessNumber: '1440012345680',
giftPackage: '如意包年3G流量包',
cardCompany: '联通36',
isReceived: '已过期',
operator: '李佳音',
operationTime: '2025-11-06 09:45:00',
receiveTime: '',
importStatus: '导入成功',
failureReason: ''
},
{
id: 4,
iccid: '89860621370079892038',
accessNumber: '1440012345681',
giftPackage: '100G全国流量月卡套餐',
cardCompany: '联通1-1',
isReceived: '未领取',
operator: '赵强',
operationTime: '2025-11-05 16:20:00',
receiveTime: '',
importStatus: '导入失败',
failureReason: 'ICCID格式错误'
},
{
id: 5,
iccid: '89860621370079892039',
accessNumber: '1440012345682',
giftPackage: '广电飞悦卡无预存50G30天',
cardCompany: '广电4',
isReceived: '已领取',
operator: '张若暄',
operationTime: '2025-11-04 11:30:00',
receiveTime: '2025-11-05 08:15:00',
importStatus: '导入成功',
failureReason: ''
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getPackageGiftList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getPackageGiftList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: 'ICCID号',
prop: 'iccid',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID号'
},
onChange: handleFormChange
},
{
label: '接入号',
prop: 'accessNumber',
type: 'input',
config: {
clearable: true,
placeholder: '请输入接入号'
},
onChange: handleFormChange
},
{
label: '开卡公司',
prop: 'cardCompany',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '联通2', value: 'unicom2' },
{ label: '联通36', value: 'unicom36' },
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
{ label: '联通1-1', value: 'unicom1_1' },
{ label: '联通8', value: 'unicom8' },
{ label: '移动21', value: 'mobile21' },
{ label: '广电4', value: 'gdtv4' },
{ label: '电信9', value: 'telecom9' }
],
onChange: handleFormChange
},
{
label: '是否领取',
prop: 'isReceived',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '已领取', value: 'received' },
{ label: '未领取', value: 'not_received' },
{ label: '已过期', value: 'expired' }
],
onChange: handleFormChange
},
{
label: '结束时间',
prop: 'endDateRange',
type: 'daterange',
config: {
type: 'daterange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: 'ICCID', prop: 'iccid' },
{ label: '接入号码', prop: 'accessNumber' },
{ label: '赠送套餐', prop: 'giftPackage' },
{ label: '开卡公司', prop: 'cardCompany' },
{ label: '是否领取', prop: 'isReceived' },
{ label: '操作人', prop: 'operator' },
{ label: '操作时间', prop: 'operationTime' },
{ label: '领取时间', prop: 'receiveTime' },
{ label: '导入状态', prop: 'importStatus' },
{ label: '失败原因', prop: 'failureReason' },
{ label: '操作', prop: 'operation' }
]
// 获取是否领取标签类型
const getReceiveStatusType = (status: string) => {
switch (status) {
case '已领取':
return 'success'
case '未领取':
return 'warning'
case '已过期':
return 'danger'
default:
return 'info'
}
}
// 获取导入状态标签类型
const getImportStatusType = (status: string) => {
switch (status) {
case '导入成功':
return 'success'
case '导入失败':
return 'danger'
default:
return 'info'
}
}
// 导出Excel
const exportExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐赠送记录`)
}
// 批量删除
const batchDelete = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要删除的数据')
return
}
ElMessageBox.confirm(
`确定要删除选中的 ${selectedRows.value.length} 条套餐赠送记录吗?`,
'批量删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(() => {
ElMessage.success(`批量删除 ${selectedRows.value.length} 条记录成功`)
getPackageGiftList()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
// 显示导入对话框
const showBatchImportDialog = () => {
importDialogVisible.value = true
// 重置表单
if (importFormRef.value) {
importFormRef.value.resetFields()
}
importFormData.excelFile = null
importFormData.remark = ''
}
// 下载模板
const downloadTemplate = () => {
ElMessage.success('正在下载套餐赠送导入模板...')
// 这里可以实现实际的模板下载功能
}
// 查看详情
const viewDetails = (row: any) => {
ElMessage.info(`查看套餐赠送详情: ${row.iccid}`)
}
// 编辑记录
const editRecord = (row: any) => {
ElMessage.info(`编辑套餐赠送记录: ${row.iccid}`)
}
// 删除记录
const deleteRecord = (row: any) => {
ElMessageBox.confirm(`确定要删除该套餐赠送记录吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('删除成功')
getPackageGiftList()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
// 手动发放
const manualGrant = (row: any) => {
if (row.isReceived === '已领取') {
ElMessage.warning('该套餐已被领取')
return
}
ElMessageBox.confirm(`确定要手动发放该套餐吗?`, '发放确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
.then(() => {
ElMessage.success('套餐发放成功')
getPackageGiftList()
})
.catch(() => {
ElMessage.info('已取消发放')
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'iccid',
label: 'ICCID',
minWidth: 180
},
{
prop: 'accessNumber',
label: '接入号码',
width: 140
},
{
prop: 'giftPackage',
label: '赠送套餐',
minWidth: 200
},
{
prop: 'cardCompany',
label: '开卡公司',
width: 120
},
{
prop: 'isReceived',
label: '是否领取',
width: 100,
formatter: (row) => {
return h(ElTag, { type: getReceiveStatusType(row.isReceived) }, () => row.isReceived)
}
},
{
prop: 'operator',
label: '操作人',
width: 100
},
{
prop: 'operationTime',
label: '操作时间',
width: 160
},
{
prop: 'receiveTime',
label: '领取时间',
width: 160,
formatter: (row) => row.receiveTime || '未领取'
},
{
prop: 'importStatus',
label: '导入状态',
width: 100,
formatter: (row) => {
return h(ElTag, { type: getImportStatusType(row.importStatus) }, () => row.importStatus)
}
},
{
prop: 'failureReason',
label: '失败原因',
width: 140,
formatter: (row) => row.failureReason || '-'
},
{
prop: 'operation',
label: '操作',
width: 280,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '查看',
onClick: () => viewDetails(row)
}),
h(ArtButtonTable, {
text: '发放',
disabled: row.isReceived === '已领取',
onClick: () => manualGrant(row)
}),
h(ArtButtonTable, {
text: '编辑',
onClick: () => editRecord(row)
}),
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteRecord(row)
})
])
}
}
])
onMounted(() => {
getPackageGiftList()
})
// 获取套餐赠送列表
const getPackageGiftList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取套餐赠送列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getPackageGiftList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getPackageGiftList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getPackageGiftList()
}
// 文件上传限制
const handleExceed = () => {
ElMessage.warning('最多只能上传一个文件')
}
// 文件上传前检查
const beforeUpload = (file: UploadRawFile) => {
const isExcel =
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.type === 'application/vnd.ms-excel'
const isLt10M = file.size / 1024 / 1024 < 10
if (!isExcel) {
ElMessage.error('只能上传 Excel 文件!')
return false
}
if (!isLt10M) {
ElMessage.error('上传文件大小不能超过 10MB!')
return false
}
return false // 阻止自动上传
}
// 文件变化处理
const handleFileChange = (file: UploadFile) => {
if (file.raw) {
importFormData.excelFile = file.raw
}
}
// 导入表单验证规则
const importRules = reactive<FormRules>({
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }]
})
// 提交导入
const handleImportSubmit = async () => {
if (!importFormRef.value) return
// 检查文件是否上传
if (!importFormData.excelFile) {
ElMessage.error('请先上传Excel文件')
return
}
await importFormRef.value.validate((valid) => {
if (valid) {
importLoading.value = true
// 模拟导入过程
setTimeout(() => {
ElMessage.success('套餐赠送导入成功!')
importDialogVisible.value = false
importLoading.value = false
getPackageGiftList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.package-gift-page {
// 可以添加特定样式
}
.template-section {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
.template-tip {
font-size: 12px;
color: #909399;
}
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-upload-dragger) {
padding: 40px;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

View File

@@ -1,52 +0,0 @@
<template>
<div class="page-content">
<h3 class="table-title"><i class="iconfont-sys">&#xe74d;</i>更新日志</h3>
<ArtTable :data="upgradeLogList" :pagination="false">
<ElTableColumn label="版本号" prop="version" width="200" />
<ElTableColumn label="内容">
<template #default="scope">
<div class="title">{{ scope.row.title }}</div>
<div v-if="scope.row.detail" style="margin-top: 10px">
<div class="detail-item" v-for="(item, index) in scope.row.detail" :key="index">
{{ index + 1 }}. {{ item }}
</div>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="时间" prop="date" />
</ArtTable>
</div>
</template>
<script setup lang="ts">
import ArtTable from '@/components/core/tables/ArtTable.vue'
import { upgradeLogList } from '@/mock/upgrade/changeLog'
defineOptions({ name: 'ChangeLog' })
</script>
<style lang="scss" scoped>
.page-content {
.table-title {
display: flex;
align-items: center;
padding: 10px 0 0;
font-size: 18px;
font-weight: 500;
i {
margin-right: 10px;
font-size: 24px;
}
}
.title {
color: var(--art-gray-800);
}
.detail-item {
color: var(--art-gray-600);
}
}
</style>

View File

@@ -46,28 +46,15 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="120"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -323,11 +310,7 @@
} from '@/types/api/commission'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime, formatMoney } from '@/utils/business/format'
import {
CommissionStatusMap,
@@ -342,21 +325,10 @@
const route = useRoute()
// 使
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
//
const loading = ref(false)
const tableRef = ref()
const summaryList = ref<ShopCommissionSummaryItem[]>([])
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<ShopCommissionSummaryItem | null>(null)
//
const searchForm = reactive({
@@ -604,6 +576,21 @@
loadCommissionRecords()
}
//
const getActions = (row: ShopCommissionSummaryItem) => {
const actions: any[] = []
if (hasAuth('agent_commission:detail')) {
actions.push({
label: '查看详情',
handler: () => showDetail(row),
type: 'primary'
})
}
return actions
}
// tab
watch(activeTab, (newTab) => {
if (newTab === 'commission') {
@@ -704,30 +691,6 @@
loadWithdrawalRecords()
}
//
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
//
return items
})
//
const handleRowContextMenu = (row: ShopCommissionSummaryItem, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
//
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
//
}
//
const handleResolveCommission = (
row: ShopCommissionRecordItem,

View File

@@ -1,5 +1,9 @@
<template>
<div class="analysis-dashboard">
<!-- 轮询统计 -->
<div class="section-title">轮询统计</div>
<PollingStats />
<!-- 佣金概览 -->
<div class="section-title">佣金概览</div>
<CommissionSummary />
@@ -56,6 +60,7 @@
import TopProducts from './widget/TopProducts.vue'
import SalesMappingByCountry from './widget/SalesMappingByCountry.vue'
import VolumeServiceLevel from './widget/VolumeServiceLevel.vue'
import PollingStats from './widget/PollingStats.vue'
import CommissionSummary from './widget/CommissionSummary.vue'
import WithdrawalSettings from './widget/WithdrawalSettings.vue'
import ActiveWechatConfig from './widget/ActiveWechatConfig.vue'

View File

@@ -0,0 +1,195 @@
<template>
<ElCard shadow="never" class="polling-stats-widget">
<template #header>
<div class="card-header">
<div class="header-left">
<span class="header-title">实时统计</span>
<ElTag type="primary" effect="dark" size="small">实时</ElTag>
</div>
<div class="header-right">
<ElButton size="small" @click="loadOverviewStats" :loading="loading">刷新</ElButton>
</div>
</div>
</template>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)">
<el-icon :size="24"><Document /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">总卡数</div>
<div class="stat-value">{{ overviewStats?.total_cards?.toLocaleString() || 0 }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)">
<el-icon :size="24"><CircleCheck /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">已初始化</div>
<div class="stat-value">
{{ overviewStats?.initialized_cards?.toLocaleString() || 0 }} / {{ overviewStats?.total_cards?.toLocaleString() || 0 }}
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)">
<el-icon :size="24"><User /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">实名队列</div>
<div class="stat-value">{{ overviewStats?.realname_queue_size?.toLocaleString() || 0 }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)">
<el-icon :size="24"><Box /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">套餐队列</div>
<div class="stat-value">{{ overviewStats?.package_queue_size?.toLocaleString() || 0 }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%)">
<el-icon :size="24"><DataLine /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">流量队列</div>
<div class="stat-value">{{ overviewStats?.carddata_queue_size?.toLocaleString() || 0 }}</div>
</div>
</div>
</div>
</ElCard>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, ElTag, ElButton } from 'element-plus'
import { Document, CircleCheck, User, Box, DataLine } from '@element-plus/icons-vue'
import { PollingMonitorService } from '@/api/modules'
import type { PollingOverviewStats } from '@/types/api'
defineOptions({ name: 'PollingStats' })
const overviewStats = ref<PollingOverviewStats>()
const loading = ref(false)
// 加载总览统计
const loadOverviewStats = async () => {
loading.value = true
try {
const { data } = await PollingMonitorService.getOverviewStats()
overviewStats.value = data
} catch (error) {
ElMessage.error('加载轮询统计失败')
} finally {
loading.value = false
}
}
onMounted(() => {
loadOverviewStats()
})
</script>
<style lang="scss" scoped>
.polling-stats-widget {
:deep(.el-card__header) {
padding: 18px 20px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-lighter);
}
:deep(.el-card__body) {
padding: 20px;
}
.card-header {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
.header-left {
display: flex;
gap: 10px;
align-items: center;
.header-title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.header-right {
display: flex;
gap: 8px;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
.stat-card {
display: flex;
gap: 12px;
align-items: center;
padding: 16px;
background: var(--el-fill-color-blank);
border-radius: 8px;
.stat-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
color: white;
border-radius: 8px;
}
.stat-content {
flex: 1;
min-width: 0;
.stat-label {
margin-bottom: 4px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.stat-value {
overflow: hidden;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
@media (width <= 768px) {
.polling-stats-widget {
.card-header {
.header-left,
.header-right {
width: 100%;
}
}
.stats-grid {
grid-template-columns: 1fr;
gap: 12px;
}
}
}
</style>

View File

@@ -18,7 +18,7 @@
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showCreateDialog">创建充值订单</ElButton>
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('agent_recharge:create')">创建充值订单</ElButton>
</template>
</ArtTableHeader>
@@ -32,29 +32,16 @@
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="120"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 充值订单操作右键菜单 -->
<ArtMenuRight
ref="rechargeOperationMenuRef"
:menu-items="rechargeOperationMenuItems"
:menu-width="140"
@select="handleRechargeOperationMenuSelect"
/>
<!-- 创建充值订单对话框 -->
<ElDialog
v-model="createDialogVisible"
@@ -181,17 +168,15 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useUserStore } from '@/store/modules/user'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import { useAuth } from '@/composables/useAuth'
defineOptions({ name: 'AgentRechargeList' })
const router = useRouter()
const userStore = useUserStore()
const { hasAuth } = useAuth()
const loading = ref(false)
const createLoading = ref(false)
@@ -201,10 +186,6 @@
const confirmPayDialogVisible = ref(false)
const currentRecharge = ref<AgentRecharge | null>(null)
// 右键菜单
const rechargeOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingRecharge = ref<AgentRecharge | null>(null)
// 搜索表单初始值
const initialSearchState: AgentRechargeQueryParams = {
shop_id: undefined,
@@ -609,71 +590,25 @@
})
}
// 查看详情
const handleViewDetail = (row: AgentRecharge) => {
router.push({
path: `${RoutesAlias.AgentRecharge}/detail/${row.id}`
})
}
// 充值订单操作菜单项配置
const rechargeOperationMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
// 获取操作按钮
const getActions = (row: AgentRecharge) => {
const actions: any[] = []
// 待支付且线下转账的订单可以确认支付
if (
currentOperatingRecharge.value?.status === 1 &&
currentOperatingRecharge.value?.payment_method === 'offline'
) {
items.push({
key: 'confirm_pay',
label: '确认支付'
if (row.status === 1 && row.payment_method === 'offline' && hasAuth('agent_recharge:confirm_payment')) {
actions.push({
label: '确认支付',
handler: () => handleShowConfirmPay(row),
type: 'primary'
})
}
return items
})
// 显示充值订单操作右键菜单
const showRechargeOperationMenu = (e: MouseEvent, row: AgentRecharge) => {
e.preventDefault()
e.stopPropagation()
currentOperatingRecharge.value = row
rechargeOperationMenuRef.value?.show(e)
return actions
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: AgentRecharge, column: any, event: MouseEvent) => {
showRechargeOperationMenu(event, row)
}
// 处理充值订单操作菜单选择
const handleRechargeOperationMenuSelect = (item: MenuItemType) => {
if (!currentOperatingRecharge.value) return
switch (item.key) {
case 'confirm_pay':
handleShowConfirmPay(currentOperatingRecharge.value)
break
}
}
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
</script>
<style scoped lang="scss">
.agent-recharge-page {
height: 100%;
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -1,241 +0,0 @@
<template>
<div class="page-content">
<ElCard shadow="never">
<!-- 搜索栏 -->
<ElForm :inline="true" :model="searchForm" class="search-form">
<ElFormItem label="客户账号">
<ElInput
v-model="searchForm.accountNo"
placeholder="请输入客户账号"
clearable
style="width: 200px"
/>
</ElFormItem>
<ElFormItem label="客户名称">
<ElInput
v-model="searchForm.customerName"
placeholder="请输入客户名称"
clearable
style="width: 200px"
/>
</ElFormItem>
<ElFormItem label="客户类型">
<ElSelect
v-model="searchForm.customerType"
placeholder="请选择"
clearable
style="width: 150px"
>
<ElOption label="代理商" value="agent" />
<ElOption label="企业客户" value="enterprise" />
</ElSelect>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="handleSearch">查询</ElButton>
<ElButton @click="handleReset">重置</ElButton>
</ElFormItem>
</ElForm>
<!-- 数据表格 -->
<ArtTable :data="tableData" index stripe>
<template #default>
<ElTableColumn label="客户账号" prop="accountNo" min-width="150" />
<ElTableColumn label="客户名称" prop="customerName" min-width="150" />
<ElTableColumn label="客户类型" prop="customerType" width="120">
<template #default="scope">
<ElTag :type="scope.row.customerType === 'agent' ? 'success' : 'primary'">
{{ scope.row.customerType === 'agent' ? '代理商' : '企业客户' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="佣金总额" prop="totalCommission" width="150">
<template #default="scope"> ¥{{ scope.row.totalCommission.toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn label="可提现金额" prop="availableAmount" width="150">
<template #default="scope">
<span style="color: var(--el-color-success)">
¥{{ scope.row.availableAmount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="待入账金额" prop="pendingAmount" width="150">
<template #default="scope">
<span style="color: var(--el-color-warning)">
¥{{ scope.row.pendingAmount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="已提现金额" prop="withdrawnAmount" width="150">
<template #default="scope"> ¥{{ scope.row.withdrawnAmount.toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn label="提现次数" prop="withdrawCount" width="100" />
<ElTableColumn label="最后提现时间" prop="lastWithdrawTime" width="180" />
<ElTableColumn label="操作" width="180" fixed="right">
<template #default="scope">
<ElButton link type="primary" @click="handleViewDetail(scope.row)">查看详情</ElButton>
<ElButton link type="primary" @click="handleViewFlow(scope.row)">流水记录</ElButton>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 分页 -->
<ElPagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
style="justify-content: flex-end; margin-top: 20px"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</ElCard>
<!-- 详情对话框 -->
<ElDialog v-model="detailDialogVisible" title="客户账号详情" width="800px" align-center>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="客户账号">{{ currentRow?.accountNo }}</ElDescriptionsItem>
<ElDescriptionsItem label="客户名称">{{ currentRow?.customerName }}</ElDescriptionsItem>
<ElDescriptionsItem label="客户类型">
<ElTag :type="currentRow?.customerType === 'agent' ? 'success' : 'primary'">
{{ currentRow?.customerType === 'agent' ? '代理商' : '企业客户' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="联系电话">{{ currentRow?.phone }}</ElDescriptionsItem>
<ElDescriptionsItem label="佣金总额"
>¥{{ currentRow?.totalCommission.toFixed(2) }}</ElDescriptionsItem
>
<ElDescriptionsItem label="可提现金额">
<span style="color: var(--el-color-success)">
¥{{ currentRow?.availableAmount.toFixed(2) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="待入账金额">
<span style="color: var(--el-color-warning)">
¥{{ currentRow?.pendingAmount.toFixed(2) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="已提现金额"
>¥{{ currentRow?.withdrawnAmount.toFixed(2) }}</ElDescriptionsItem
>
<ElDescriptionsItem label="提现次数">{{ currentRow?.withdrawCount }}</ElDescriptionsItem>
<ElDescriptionsItem label="最后提现时间">{{
currentRow?.lastWithdrawTime
}}</ElDescriptionsItem>
<ElDescriptionsItem label="注册时间">{{ currentRow?.createTime }}</ElDescriptionsItem>
<ElDescriptionsItem label="备注" :span="2">{{
currentRow?.remark || '无'
}}</ElDescriptionsItem>
</ElDescriptions>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
defineOptions({ name: 'CustomerAccount' })
const searchForm = reactive({
accountNo: '',
customerName: '',
customerType: ''
})
const pagination = reactive({
page: 1,
pageSize: 20,
total: 100
})
const detailDialogVisible = ref(false)
const currentRow = ref<any>(null)
// 模拟表格数据
const tableData = ref([
{
id: '1',
accountNo: 'ACC20260001',
customerName: '深圳市科技有限公司',
customerType: 'agent',
phone: '13800138000',
totalCommission: 158900.5,
availableAmount: 58900.5,
pendingAmount: 50000.0,
withdrawnAmount: 50000.0,
withdrawCount: 12,
lastWithdrawTime: '2026-01-08 15:00:00',
createTime: '2025-06-01 10:00:00',
remark: '优质代理商'
},
{
id: '2',
accountNo: 'ACC20260002',
customerName: '广州智能设备公司',
customerType: 'enterprise',
phone: '13900139000',
totalCommission: 89600.0,
availableAmount: 35600.0,
pendingAmount: 24000.0,
withdrawnAmount: 30000.0,
withdrawCount: 8,
lastWithdrawTime: '2026-01-05 10:30:00',
createTime: '2025-07-15 14:20:00',
remark: ''
},
{
id: '3',
accountNo: 'ACC20260003',
customerName: '北京物联网代理',
customerType: 'agent',
phone: '13700137000',
totalCommission: 256700.0,
availableAmount: 106700.0,
pendingAmount: 80000.0,
withdrawnAmount: 70000.0,
withdrawCount: 15,
lastWithdrawTime: '2026-01-09 09:15:00',
createTime: '2025-05-10 09:00:00',
remark: '金牌代理商'
}
])
const handleSearch = () => {
ElMessage.success('查询成功')
}
const handleReset = () => {
searchForm.accountNo = ''
searchForm.customerName = ''
searchForm.customerType = ''
ElMessage.info('已重置')
}
const handleViewDetail = (row: any) => {
currentRow.value = row
detailDialogVisible.value = true
}
const handleViewFlow = (row: any) => {
ElMessage.info(`查看 ${row.customerName} 的流水记录`)
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
ElMessage.info(`每页显示 ${size}`)
}
const handleCurrentChange = (page: number) => {
pagination.page = page
ElMessage.info(`当前第 ${page}`)
}
</script>
<style lang="scss" scoped>
.page-content {
.search-form {
margin-bottom: 20px;
}
}
</style>

View File

@@ -1,333 +0,0 @@
<template>
<div class="page-content">
<!-- 统计卡片 -->
<ElRow :gutter="20" style="margin-bottom: 20px">
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
>
<i class="iconfont-sys">&#xe71d;</i>
</div>
<div class="stat-content">
<div class="stat-label">佣金总额</div>
<div class="stat-value">¥{{ accountInfo.totalCommission.toFixed(2) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
>
<i class="iconfont-sys">&#xe71e;</i>
</div>
<div class="stat-content">
<div class="stat-label">可提现金额</div>
<div class="stat-value">¥{{ accountInfo.availableAmount.toFixed(2) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
>
<i class="iconfont-sys">&#xe720;</i>
</div>
<div class="stat-content">
<div class="stat-label">待入账金额</div>
<div class="stat-value">¥{{ accountInfo.pendingAmount.toFixed(2) }}</div>
</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover">
<div class="stat-card">
<div
class="stat-icon"
style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%)"
>
<i class="iconfont-sys">&#xe71f;</i>
</div>
<div class="stat-content">
<div class="stat-label">已提现金额</div>
<div class="stat-value">¥{{ accountInfo.withdrawnAmount.toFixed(2) }}</div>
</div>
</div>
</ElCard>
</ElCol>
</ElRow>
<!-- 操作按钮 -->
<ElRow style="margin-bottom: 20px">
<ElButton type="primary" @click="showWithdrawDialog">申请提现</ElButton>
<ElButton @click="viewWithdrawHistory">提现记录</ElButton>
</ElRow>
<!-- 收支流水 -->
<ElCard shadow="never">
<template #header>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-weight: 500">收支流水</span>
<ElRadioGroup v-model="flowType" size="small">
<ElRadioButton value="all">全部</ElRadioButton>
<ElRadioButton value="income">收入</ElRadioButton>
<ElRadioButton value="withdraw">提现</ElRadioButton>
</ElRadioGroup>
</div>
</template>
<ArtTable :data="filteredFlowData" index max-height="500">
<template #default>
<ElTableColumn label="流水号" prop="flowNo" min-width="180" />
<ElTableColumn label="类型" prop="type">
<template #default="scope">
<ElTag :type="scope.row.type === 'income' ? 'success' : 'warning'">
{{ scope.row.type === 'income' ? '收入' : '提现' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="金额" prop="amount">
<template #default="scope">
<span
:style="{
color:
scope.row.type === 'income'
? 'var(--el-color-success)'
: 'var(--el-color-danger)'
}"
>
{{ scope.row.type === 'income' ? '+' : '-' }}¥{{ scope.row.amount.toFixed(2) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="余额" prop="balance">
<template #default="scope"> ¥{{ scope.row.balance.toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn label="说明" prop="description" show-overflow-tooltip />
<ElTableColumn label="时间" prop="createTime" width="180" />
</template>
</ArtTable>
</ElCard>
<!-- 提现申请对话框 -->
<ElDialog v-model="withdrawDialogVisible" title="申请提现" width="600px" align-center>
<ElForm
ref="withdrawFormRef"
:model="withdrawForm"
:rules="withdrawRules"
label-width="120px"
>
<ElFormItem label="可提现金额">
<span style="font-size: 20px; font-weight: 500; color: var(--el-color-success)">
¥{{ accountInfo.availableAmount.toFixed(2) }}
</span>
</ElFormItem>
<ElFormItem label="提现金额" prop="amount">
<ElInputNumber
v-model="withdrawForm.amount"
:min="1"
:max="accountInfo.availableAmount"
:precision="2"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="手续费">
<span>¥{{ calculatedFee.toFixed(2) }}</span>
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
(费率: {{ feeRate * 100 }}%)
</span>
</ElFormItem>
<ElFormItem label="实际到账">
<span style="font-size: 18px; font-weight: 500; color: var(--el-color-success)">
¥{{ actualAmount.toFixed(2) }}
</span>
</ElFormItem>
<ElFormItem label="收款银行" prop="bankName">
<ElSelect
v-model="withdrawForm.bankName"
placeholder="请选择收款银行"
style="width: 100%"
>
<ElOption label="中国工商银行" value="工商银行" />
<ElOption label="中国建设银行" value="建设银行" />
<ElOption label="中国农业银行" value="农业银行" />
<ElOption label="中国银行" value="中国银行" />
<ElOption label="招商银行" value="招商银行" />
</ElSelect>
</ElFormItem>
<ElFormItem label="银行账户" prop="bankAccount">
<ElInput v-model="withdrawForm.bankAccount" placeholder="请输入银行账户" />
</ElFormItem>
<ElFormItem label="开户姓名" prop="accountName">
<ElInput v-model="withdrawForm.accountName" placeholder="请输入开户姓名" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="withdrawDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="submitWithdraw">提交申请</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'MyAccount' })
const accountInfo = reactive({
totalCommission: 158900.5,
availableAmount: 58900.5,
pendingAmount: 50000.0,
withdrawnAmount: 50000.0
})
const flowType = ref('all')
const withdrawDialogVisible = ref(false)
const withdrawFormRef = ref<FormInstance>()
const feeRate = 0.002 // 手续费率 0.2%
const withdrawForm = reactive({
amount: 0,
bankName: '',
bankAccount: '',
accountName: ''
})
const withdrawRules = reactive<FormRules>({
amount: [
{ required: true, message: '请输入提现金额', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value > accountInfo.availableAmount) {
callback(new Error('提现金额不能大于可提现金额'))
} else if (value < 1) {
callback(new Error('提现金额不能小于1元'))
} else {
callback()
}
},
trigger: 'blur'
}
],
bankName: [{ required: true, message: '请选择收款银行', trigger: 'change' }],
bankAccount: [{ required: true, message: '请输入银行账户', trigger: 'blur' }],
accountName: [{ required: true, message: '请输入开户姓名', trigger: 'blur' }]
})
const flowData = ref([
{
id: '1',
flowNo: 'FL202601090001',
type: 'income',
amount: 1580.0,
balance: 58900.5,
description: '套餐销售佣金',
createTime: '2026-01-09 10:30:00'
},
{
id: '2',
flowNo: 'FL202601080001',
type: 'withdraw',
amount: 5000.0,
balance: 57320.5,
description: '提现到账',
createTime: '2026-01-08 15:00:00'
},
{
id: '3',
flowNo: 'FL202601070001',
type: 'income',
amount: 2360.0,
balance: 62320.5,
description: '号卡分配佣金',
createTime: '2026-01-07 14:20:00'
}
])
const calculatedFee = computed(() => {
return withdrawForm.amount * feeRate
})
const actualAmount = computed(() => {
return withdrawForm.amount - calculatedFee.value
})
const filteredFlowData = computed(() => {
if (flowType.value === 'all') return flowData.value
return flowData.value.filter((item) => item.type === flowType.value)
})
const showWithdrawDialog = () => {
withdrawForm.amount = 0
withdrawForm.bankName = ''
withdrawForm.bankAccount = ''
withdrawForm.accountName = ''
withdrawDialogVisible.value = true
}
const submitWithdraw = async () => {
if (!withdrawFormRef.value) return
await withdrawFormRef.value.validate((valid) => {
if (valid) {
ElMessage.success('提现申请提交成功,请等待审核')
withdrawDialogVisible.value = false
}
})
}
const viewWithdrawHistory = () => {
ElMessage.info('查看提现记录')
}
</script>
<style lang="scss" scoped>
.page-content {
.stat-card {
display: flex;
gap: 16px;
align-items: center;
.stat-icon {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
font-size: 28px;
color: white;
border-radius: 12px;
}
.stat-content {
flex: 1;
.stat-label {
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
}
</style>

View File

@@ -18,7 +18,9 @@
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showCreateDialog">创建退款申请</ElButton>
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('refund:create')"
>创建退款申请</ElButton
>
</template>
</ArtTableHeader>
@@ -32,34 +34,21 @@
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="160"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 退款操作右键菜单 -->
<ArtMenuRight
ref="refundOperationMenuRef"
:menu-items="refundOperationMenuItems"
:menu-width="140"
@select="handleRefundOperationMenuSelect"
/>
<!-- 创建退款申请对话框 -->
<ElDialog
v-model="createDialogVisible"
title="创建退款申请"
width="500px"
width="30%"
@closed="handleCreateDialogClosed"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
@@ -321,17 +310,15 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useUserStore } from '@/store/modules/user'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import { useAuth } from '@/composables/useAuth'
defineOptions({ name: 'RefundList' })
const router = useRouter()
const userStore = useUserStore()
const { hasAuth } = useAuth()
const loading = ref(false)
const createLoading = ref(false)
@@ -348,10 +335,6 @@
const resubmitDialogVisible = ref(false)
const currentRefund = ref<Refund | null>(null)
// 右键菜单
const refundOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingRefund = ref<Refund | null>(null)
// 搜索表单初始值
const initialSearchState: RefundQueryParams = {
status: undefined,
@@ -971,88 +954,52 @@
})
}
// 退款操作菜单项配置
const refundOperationMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (!currentOperatingRefund.value) return items
// 获取操作按钮
const getActions = (row: Refund) => {
const actions: any[] = []
// 待审批状态可以审批通过、拒绝或退回
if (currentOperatingRefund.value.status === 1) {
items.push({
key: 'approve',
label: '审批通过'
})
items.push({
key: 'reject',
label: '审批拒绝'
})
items.push({
key: 'return',
label: '退回'
})
if (row.status === 1) {
// 退回按钮直接显示
if (hasAuth('refund:return')) {
actions.push({
label: '退回',
handler: () => handleShowReturn(row),
type: 'primary'
})
}
// 审批通过和审批拒绝放到更多里
if (hasAuth('refund:approve')) {
actions.push({
label: '审批通过',
handler: () => handleShowApprove(row),
type: 'primary'
})
}
if (hasAuth('refund:reject')) {
actions.push({
label: '审批拒绝',
handler: () => handleShowReject(row),
type: 'danger'
})
}
}
// 已拒绝或已退回状态可以重新提交
if (currentOperatingRefund.value.status === 3 || currentOperatingRefund.value.status === 4) {
items.push({
key: 'resubmit',
label: '重新提交'
if (row.status === 3 || row.status === 4) {
actions.push({
label: '重新提交',
handler: () => handleShowResubmit(row),
type: 'primary'
})
}
return items
})
// 显示退款操作右键菜单
const showRefundOperationMenu = (e: MouseEvent, row: Refund) => {
e.preventDefault()
e.stopPropagation()
currentOperatingRefund.value = row
refundOperationMenuRef.value?.show(e)
return actions
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: Refund, column: any, event: MouseEvent) => {
showRefundOperationMenu(event, row)
}
// 处理退款操作菜单选择
const handleRefundOperationMenuSelect = (item: MenuItemType) => {
if (!currentOperatingRefund.value) return
switch (item.key) {
case 'approve':
handleShowApprove(currentOperatingRefund.value)
break
case 'reject':
handleShowReject(currentOperatingRefund.value)
break
case 'return':
handleShowReturn(currentOperatingRefund.value)
break
case 'resubmit':
handleShowResubmit(currentOperatingRefund.value)
break
}
}
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
</script>
<style scoped lang="scss">
.refund-page {
height: 100%;
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -19,7 +19,7 @@
>
<template #left>
<ElButton @click="showCreateDialog" v-permission="'orders:add'">{{
t('orderManagement.createOrder')
'创建订单'
}}</ElButton>
</template>
</ArtTableHeader>
@@ -34,50 +34,37 @@
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="120"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 创建订单对话框 -->
<ElDialog
v-model="createDialogVisible"
:title="t('orderManagement.createOrder')"
:title="'创建订单'"
width="600px"
@closed="handleCreateDialogClosed"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
<ElFormItem :label="t('orderManagement.table.orderType')" prop="order_type">
<ElFormItem :label="'订单类型'" prop="order_type">
<ElSelect
v-model="createForm.order_type"
:placeholder="t('orderManagement.searchForm.orderTypePlaceholder')"
:placeholder="'请选择订单类型'"
style="width: 100%"
>
<ElOption :label="t('orderManagement.orderType.singleCard')" value="single_card" />
<ElOption :label="t('orderManagement.orderType.device')" value="device" />
<ElOption :label="'单卡订单'" value="single_card" />
<ElOption :label="'设备订单'" value="device" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="createForm.order_type === 'single_card'"
:label="t('orderManagement.createForm.iotCardId')"
:label="'IoT卡'"
prop="iot_card_id"
>
<ElSelect
@@ -85,7 +72,7 @@
filterable
remote
reserve-keyword
:placeholder="t('orderManagement.createForm.iotCardIdPlaceholder')"
:placeholder="'请选择IoT卡'"
:remote-method="searchIotCards"
:loading="cardSearchLoading"
style="width: 100%"
@@ -106,17 +93,13 @@
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="createForm.order_type === 'device'"
:label="t('orderManagement.createForm.deviceId')"
prop="device_id"
>
<ElFormItem v-if="createForm.order_type === 'device'" :label="'设备'" prop="device_id">
<ElSelect
v-model="createForm.device_id"
filterable
remote
reserve-keyword
:placeholder="t('orderManagement.createForm.deviceIdPlaceholder')"
:placeholder="'请选择设备'"
:remote-method="searchDevices"
:loading="deviceSearchLoading"
style="width: 100%"
@@ -137,10 +120,10 @@
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem :label="t('orderManagement.createForm.packageIds')" prop="package_ids">
<ElFormItem :label="'套餐'" prop="package_ids">
<ElSelect
v-model="createForm.package_ids"
:placeholder="t('orderManagement.createForm.packageIdsPlaceholder')"
:placeholder="'请选择套餐'"
multiple
filterable
remote
@@ -158,13 +141,13 @@
<ElOption
v-for="pkg in packageOptions"
:key="pkg.id"
:label="`${pkg.package_name} (¥${(pkg.cost_price / 100).toFixed(2)})`"
:label="`${pkg.package_name} (¥${((pkg.cost_price || 0) / 100).toFixed(2)})`"
:value="pkg.id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ pkg.package_name }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
¥{{ (pkg.cost_price / 100).toFixed(2) }}
¥{{ ((pkg.cost_price || 0) / 100).toFixed(2) }}
</span>
</div>
</ElOption>
@@ -196,58 +179,52 @@
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="createDialogVisible = false">{{
t('orderManagement.actions.cancel')
}}</ElButton>
<ElButton @click="createDialogVisible = false">{{ '取消' }}</ElButton>
<ElButton
type="primary"
@click="handleCreateOrder(createFormRef)"
:loading="createLoading"
>
{{ t('orderManagement.actions.submit') }}
{{ '提交' }}
</ElButton>
</div>
</template>
</ElDialog>
<!-- 订单详情对话框 -->
<ElDialog
v-model="detailDialogVisible"
:title="t('orderManagement.orderDetail')"
width="800px"
>
<ElDialog v-model="detailDialogVisible" :title="'订单详情'" width="800px">
<div v-if="currentOrder" class="order-detail">
<ElDescriptions :column="2" border>
<ElDescriptionsItem :label="t('orderManagement.table.orderNo')">
<ElDescriptionsItem :label="'订单编号'">
{{ currentOrder.order_no }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.orderType')">
<ElDescriptionsItem :label="'订单类型'">
{{ getOrderTypeText(currentOrder.order_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.paymentStatus')">
<ElDescriptionsItem :label="'支付状态'">
<ElTag :type="getPaymentStatusType(currentOrder.payment_status)">
{{ currentOrder.payment_status_text }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.totalAmount')">
<ElDescriptionsItem :label="'订单金额'">
{{ formatCurrency(currentOrder.total_amount) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.paymentMethod')">
<ElDescriptionsItem :label="'支付方式'">
{{ getPaymentMethodText(currentOrder.payment_method) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.buyerType')">
<ElDescriptionsItem :label="'买家类型'">
{{ getBuyerTypeText(currentOrder.buyer_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.commissionStatus')">
<ElDescriptionsItem :label="'佣金状态'">
{{ getCommissionStatusText(currentOrder.commission_status) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.paidAt')">
<ElDescriptionsItem :label="'支付时间'">
{{ currentOrder.paid_at ? formatDateTime(currentOrder.paid_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.createdAt')">
<ElDescriptionsItem :label="'创建时间'">
{{ formatDateTime(currentOrder.created_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.updatedAt')">
<ElDescriptionsItem :label="'更新时间'">
{{ formatDateTime(currentOrder.updated_at) }}
</ElDescriptionsItem>
</ElDescriptions>
@@ -257,28 +234,16 @@
v-if="currentOrder.items && currentOrder.items.length > 0"
style="margin-top: 20px"
>
<h4>{{ t('orderManagement.orderItems') }}</h4>
<h4>{{ '订单项' }}</h4>
<ElTable :data="currentOrder.items" border style="margin-top: 10px">
<ElTableColumn
prop="package_name"
:label="t('orderManagement.items.packageName')"
min-width="150"
/>
<ElTableColumn
prop="quantity"
:label="t('orderManagement.items.quantity')"
width="100"
/>
<ElTableColumn
prop="unit_price"
:label="t('orderManagement.items.unitPrice')"
width="120"
>
<ElTableColumn prop="package_name" :label="'套餐名称'" min-width="150" />
<ElTableColumn prop="quantity" :label="'数量'" width="100" />
<ElTableColumn prop="unit_price" :label="'单价'" width="120">
<template #default="{ row }">
{{ formatCurrency(row.unit_price) }}
</template>
</ElTableColumn>
<ElTableColumn prop="amount" :label="t('orderManagement.items.amount')" width="120">
<ElTableColumn prop="amount" :label="'金额'" width="120">
<template #default="{ row }">
{{ formatCurrency(row.amount) }}
</template>
@@ -288,9 +253,7 @@
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="detailDialogVisible = false">{{
t('orderManagement.actions.close')
}}</ElButton>
<ElButton @click="detailDialogVisible = false">{{ '关闭' }}</ElButton>
</div>
</template>
</ElDialog>
@@ -301,7 +264,6 @@
<script setup lang="ts">
import { h } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { OrderService, CardService, DeviceService, PackageManageService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
@@ -318,45 +280,27 @@
StandaloneIotCard,
Device,
PackageResponse,
PurchaseCheckRequest,
PurchaseCheckResponse
PurchaseCheckRequest
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { useUserStore } from '@/store/modules/user'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'OrderList' })
const { t } = useI18n()
const router = useRouter()
const { hasAuth } = useAuth()
const userStore = useUserStore()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false)
const createLoading = ref(false)
const tableRef = ref()
const createDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const currentOrder = ref<Order | null>(null)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<Order | null>(null)
// 搜索表单初始值
const initialSearchState: OrderQueryParams = {
@@ -385,37 +329,28 @@
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: t('orderManagement.searchForm.orderNo'),
prop: 'order_no',
type: 'input',
placeholder: t('orderManagement.searchForm.orderNoPlaceholder'),
config: {
clearable: true
}
},
{
label: t('orderManagement.searchForm.paymentStatus'),
label: '支付状态',
prop: 'payment_status',
type: 'select',
placeholder: t('orderManagement.searchForm.paymentStatusPlaceholder'),
placeholder: '请选择支付状态',
options: [
{ label: t('orderManagement.paymentStatus.pending'), value: 1 },
{ label: t('orderManagement.paymentStatus.paid'), value: 2 },
{ label: t('orderManagement.paymentStatus.cancelled'), value: 3 },
{ label: t('orderManagement.paymentStatus.refunded'), value: 4 }
{ label: '待支付', value: 1 },
{ label: '已支付', value: 2 },
{ label: '已取消', value: 3 },
{ label: '已退款', value: 4 }
],
config: {
clearable: true
}
},
{
label: t('orderManagement.searchForm.orderType'),
label: '订单类型',
prop: 'order_type',
type: 'select',
placeholder: t('orderManagement.searchForm.orderTypePlaceholder'),
placeholder: '请选择订单类型',
options: [
{ label: t('orderManagement.orderType.singleCard'), value: 'single_card' },
{ label: t('orderManagement.orderType.device'), value: 'device' }
{ label: '单卡订单', value: 'single_card' },
{ label: '设备订单', value: 'device' }
],
config: {
clearable: true
@@ -437,13 +372,13 @@
}
},
{
label: t('orderManagement.searchForm.dateRange'),
label: '创建时间',
prop: 'dateRange',
type: 'daterange',
config: {
clearable: true,
startPlaceholder: t('orderManagement.searchForm.startDate'),
endPlaceholder: t('orderManagement.searchForm.endDate'),
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
}
@@ -458,19 +393,18 @@
// 列配置
const columnOptions = [
{ label: t('orderManagement.table.id'), prop: 'id' },
{ label: t('orderManagement.table.orderNo'), prop: 'order_no' },
{ label: t('orderManagement.table.orderType'), prop: 'order_type' },
{ label: t('orderManagement.table.buyerType'), prop: 'buyer_type' },
{ label: '订单编号', prop: 'order_no' },
{ label: '订单类型', prop: 'order_type' },
{ label: '买家类型', prop: 'buyer_type' },
{ label: '订单渠道', prop: 'purchase_role' },
{ label: '购买备注', prop: 'purchase_remark' },
{ label: '操作者', prop: 'operator_name' },
{ label: t('orderManagement.table.paymentStatus'), prop: 'payment_status' },
{ label: t('orderManagement.table.totalAmount'), prop: 'total_amount' },
{ label: '支付状态', prop: 'payment_status' },
{ label: '订单金额', prop: 'total_amount' },
{ label: '实付金额', prop: 'actual_paid_amount' },
{ label: t('orderManagement.table.paymentMethod'), prop: 'payment_method' },
{ label: t('orderManagement.table.paidAt'), prop: 'paid_at' },
{ label: t('orderManagement.table.createdAt'), prop: 'created_at' }
{ label: '支付方式', prop: 'payment_method' },
{ label: '支付时间', prop: 'paid_at' },
{ label: '创建时间', prop: 'created_at' }
]
const createFormRef = ref<FormInstance>()
@@ -479,14 +413,14 @@
order_type: [
{
required: true,
message: t('orderManagement.validation.orderTypeRequired'),
message: '请选择订单类型',
trigger: 'change'
}
],
package_ids: [
{
required: true,
message: t('orderManagement.validation.packageIdsRequired'),
message: '请选择套餐',
trigger: 'change'
}
],
@@ -623,25 +557,21 @@
// 获取订单类型文本
const getOrderTypeText = (type: OrderType): string => {
return type === 'single_card'
? t('orderManagement.orderType.singleCard')
: t('orderManagement.orderType.device')
return type === 'single_card' ? '单卡订单' : '设备订单'
}
// 获取买家类型文本
const getBuyerTypeText = (type: BuyerType): string => {
return type === 'personal'
? t('orderManagement.buyerType.personal')
: t('orderManagement.buyerType.agent')
return type === 'personal' ? '个人' : '代理'
}
// 获取支付方式文本
const getPaymentMethodText = (method: OrderPaymentMethod): string => {
const methodMap: Record<OrderPaymentMethod, string> = {
wallet: t('orderManagement.paymentMethod.wallet'),
wechat: t('orderManagement.paymentMethod.wechat'),
alipay: t('orderManagement.paymentMethod.alipay'),
offline: t('orderManagement.paymentMethod.offline')
wallet: '钱包',
wechat: '微信',
alipay: '支付宝',
offline: '线下'
}
return methodMap[method] || method
}
@@ -649,23 +579,18 @@
// 获取佣金状态文本
const getCommissionStatusText = (status: OrderCommissionStatus): string => {
const statusMap: Record<OrderCommissionStatus, string> = {
0: t('orderManagement.commissionStatus.notApplicable'),
1: t('orderManagement.commissionStatus.pending'),
2: t('orderManagement.commissionStatus.settled')
0: '不适用',
1: '待结算',
2: '已结算'
}
return statusMap[status] || '-'
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: t('orderManagement.table.id'),
width: 80
},
{
prop: 'order_no',
label: t('orderManagement.table.orderNo'),
label: '订单编号',
minWidth: 220,
formatter: (row: Order) => {
return h(
@@ -683,7 +608,7 @@
},
{
prop: 'order_type',
label: t('orderManagement.table.orderType'),
label: '订单类型',
width: 120,
formatter: (row: Order) => {
return h(ElTag, { type: row.order_type === 'single_card' ? 'primary' : 'success' }, () =>
@@ -693,7 +618,7 @@
},
{
prop: 'buyer_type',
label: t('orderManagement.table.buyerType'),
label: '买家类型',
width: 120,
formatter: (row: Order) => {
return h(ElTag, { type: row.buyer_type === 'personal' ? 'info' : 'warning' }, () =>
@@ -713,8 +638,8 @@
purchased_by_platform: 'danger',
purchase_for_subordinate: 'info'
}
return h(ElTag, { type: roleTypeMap[row.purchase_role] || 'info', size: 'small' }, () =>
getPurchaseRoleText(row.purchase_role)
return h(ElTag, { type: roleTypeMap[row.purchase_role || ''] || 'info', size: 'small' }, () =>
getPurchaseRoleText(row.purchase_role || '')
)
}
},
@@ -733,7 +658,7 @@
},
{
prop: 'payment_status',
label: t('orderManagement.table.paymentStatus'),
label: '支付状态',
width: 120,
formatter: (row: Order) => {
return h(
@@ -745,7 +670,7 @@
},
{
prop: 'total_amount',
label: t('orderManagement.table.totalAmount'),
label: '订单金额',
width: 120,
formatter: (row: Order) => formatCurrency(row.total_amount)
},
@@ -760,19 +685,19 @@
},
{
prop: 'payment_method',
label: t('orderManagement.table.paymentMethod'),
label: '支付方式',
width: 120,
formatter: (row: Order) => getPaymentMethodText(row.payment_method)
},
{
prop: 'paid_at',
label: t('orderManagement.table.paidAt'),
label: '支付时间',
width: 180,
formatter: (row: Order) => (row.paid_at ? formatDateTime(row.paid_at) : '-')
},
{
prop: 'created_at',
label: t('orderManagement.table.createdAt'),
label: '创建时间',
width: 180,
formatter: (row: Order) => formatDateTime(row.created_at)
}
@@ -1006,8 +931,11 @@
// 调用套餐购买预检接口
const checkData: PurchaseCheckRequest = {
order_type: createForm.order_type,
resource_id: createForm.order_type === 'single_card' ? createForm.iot_card_id! : createForm.device_id!,
order_type: createForm.order_type!,
resource_id:
createForm.order_type === 'single_card'
? createForm.iot_card_id!
: createForm.device_id!,
package_ids: createForm.package_ids
}
@@ -1042,7 +970,7 @@
if (createForm.payment_method === 'wallet') {
ElMessage.success('订单创建成功,已自动完成支付')
} else {
ElMessage.success(t('orderManagement.messages.createSuccess'))
ElMessage.success('订单创建成功')
}
createDialogVisible.value = false
@@ -1081,23 +1009,19 @@
const handleCancelOrder = (row: Order) => {
// 已支付的订单不能取消
if (row.payment_status === 2) {
ElMessage.warning(t('orderManagement.messages.cannotCancelPaid'))
ElMessage.warning('已支付的订单无法取消')
return
}
ElMessageBox.confirm(
t('orderManagement.messages.cancelConfirmText'),
t('orderManagement.messages.cancelConfirm'),
{
confirmButtonText: t('orderManagement.actions.confirm'),
cancelButtonText: t('orderManagement.actions.cancel'),
type: 'warning'
}
)
ElMessageBox.confirm('确定要取消该订单吗?', '取消订单', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
await OrderService.cancelOrder(row.id)
ElMessage.success(t('orderManagement.messages.cancelSuccess'))
ElMessage.success('订单已取消')
await getTableData()
} catch (error) {
console.error(error)
@@ -1108,40 +1032,20 @@
})
}
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
if (!currentRow.value) return []
const items: MenuItemType[] = []
// 获取操作按钮
const getActions = (row: Order) => {
const actions: any[] = []
// 只有待支付和已支付的订单可以删除
if (
(currentRow.value.payment_status === 1 || currentRow.value.payment_status === 2) &&
hasAuth('orders:delete')
) {
items.push({ key: 'cancel', label: '删除' })
if ((row.payment_status === 1 || row.payment_status === 2) && hasAuth('orders:delete')) {
actions.push({
label: '删除',
handler: () => handleCancelOrder(row),
type: 'danger'
})
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: Order, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'cancel':
handleCancelOrder(currentRow.value)
break
}
return actions
}
</script>

View File

@@ -1,523 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="package-batch-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="handleSearch">查询</ElButton>
<ElButton type="success" @click="showImportDialog">导入</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 导入对话框 -->
<ElDialog
v-model="importDialogVisible"
title="批量导入套餐"
width="500px"
align-center
:close-on-click-modal="false"
>
<!-- 顶部下载模板按钮 -->
<div class="template-section">
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
<span class="template-tip">请先下载模板按模板格式填写后上传</span>
</div>
<ElDivider />
<ElForm
ref="importFormRef"
:model="importFormData"
:rules="importRules"
label-width="120px"
>
<ElFormItem label="导入类型" prop="importType">
<ElSelect
v-model="importFormData.importType"
placeholder="请选择导入类型"
style="width: 100%"
clearable
>
<ElOption label="套餐批量导入" value="package_batch" />
<ElOption label="套餐更新导入" value="package_update" />
<ElOption label="套餐停用导入" value="package_disable" />
<ElOption label="套餐启用导入" value="package_enable" />
</ElSelect>
</ElFormItem>
<ElFormItem label="上传Excel文件" prop="excelFile">
<ElUpload
ref="uploadRef"
:limit="1"
:on-exceed="handleExceed"
:before-upload="beforeUpload"
:on-change="handleFileChange"
:auto-upload="false"
accept=".xlsx,.xls"
drag
>
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
<div class="el-upload__text"> 将文件拖到此处<em>点击上传</em> </div>
<template #tip>
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件且不超过 10MB </div>
</template>
</ElUpload>
</ElFormItem>
<ElFormItem label="备注说明" prop="remark">
<ElInput
v-model="importFormData.remark"
type="textarea"
placeholder="请输入备注说明(可选)"
:rows="3"
maxlength="200"
show-word-limit
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="importDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleImportSubmit" :loading="importLoading">
确认导入
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElTag, ElMessage, ElUpload, ElIcon, ElDivider } from 'element-plus'
import { UploadFilled, Download } from '@element-plus/icons-vue'
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'PackageBatch' })
const importDialogVisible = ref(false)
const loading = ref(false)
const importLoading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
importType: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 导入表单实例
const importFormRef = ref<FormInstance>()
const uploadRef = ref<UploadInstance>()
// 导入表单数据
const importFormData = reactive({
importType: '',
excelFile: null as File | null,
remark: ''
})
// 模拟数据
const mockData = [
{
id: 1,
importTotal: 1000,
success: 980,
failed: 20,
importType: '套餐批量导入',
operator: '张若暄',
operationTime: '2025-11-08 10:30:00'
},
{
id: 2,
importTotal: 500,
success: 500,
failed: 0,
importType: '套餐更新导入',
operator: '孔丽娟',
operationTime: '2025-11-07 14:15:00'
},
{
id: 3,
importTotal: 300,
success: 285,
failed: 15,
importType: '套餐停用导入',
operator: '李佳音',
operationTime: '2025-11-06 09:45:00'
},
{
id: 4,
importTotal: 800,
success: 795,
failed: 5,
importType: '套餐启用导入',
operator: '赵强',
operationTime: '2025-11-05 16:20:00'
},
{
id: 5,
importTotal: 150,
success: 145,
failed: 5,
importType: '套餐批量导入',
operator: '张若暄',
operationTime: '2025-11-04 11:30:00'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getBatchImportList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getBatchImportList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '导入类型',
prop: 'importType',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '套餐批量导入', value: 'package_batch' },
{ label: '套餐更新导入', value: 'package_update' },
{ label: '套餐停用导入', value: 'package_disable' },
{ label: '套餐启用导入', value: 'package_enable' }
],
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '导入总数', prop: 'importTotal' },
{ label: '成功', prop: 'success' },
{ label: '失败', prop: 'failed' },
{ label: '导入类型', prop: 'importType' },
{ label: '操作人', prop: 'operator' },
{ label: '操作时间', prop: 'operationTime' },
{ label: '操作', prop: 'operation' }
]
// 获取导入类型标签类型
const getImportTypeTagType = (type: string) => {
switch (type) {
case '套餐批量导入':
return 'primary'
case '套餐更新导入':
return 'success'
case '套餐停用导入':
return 'danger'
case '套餐启用导入':
return 'warning'
default:
return 'info'
}
}
// 显示导入对话框
const showImportDialog = () => {
importDialogVisible.value = true
// 重置表单
if (importFormRef.value) {
importFormRef.value.resetFields()
}
importFormData.importType = ''
importFormData.excelFile = null
importFormData.remark = ''
}
// 下载模板
const downloadTemplate = () => {
ElMessage.success('正在下载批量导入模板...')
// 这里可以实现实际的模板下载功能
}
// 查看详情
const viewDetails = (row: any) => {
ElMessage.info(`查看导入详情: ${row.importType} - 导入总数:${row.importTotal}`)
}
// 重新导入
const retryImport = (row: any) => {
ElMessage.info(`重新导入: ${row.importType}`)
}
// 删除记录
const deleteRecord = (row: any) => {
ElMessage.info(`删除导入记录: ${row.id}`)
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'importTotal',
label: '导入总数',
formatter: (row) => `${row.importTotal} `
},
{
prop: 'success',
label: '成功',
formatter: (row) => {
return h(ElTag, { type: 'success' }, () => `${row.success}`)
}
},
{
prop: 'failed',
label: '失败',
formatter: (row) => {
return h(ElTag, { type: row.failed > 0 ? 'danger' : 'success' }, () => `${row.failed}`)
}
},
{
prop: 'importType',
label: '导入类型',
formatter: (row) => {
return h(ElTag, { type: getImportTypeTagType(row.importType) }, () => row.importType)
}
},
{
prop: 'operator',
label: '操作人'
},
{
prop: 'operationTime',
label: '操作时间'
},
{
prop: 'operation',
label: '操作',
width: 220,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '查看',
onClick: () => viewDetails(row)
}),
h(ArtButtonTable, {
text: '重试',
onClick: () => retryImport(row)
}),
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteRecord(row)
})
])
}
}
])
onMounted(() => {
getBatchImportList()
})
// 获取批量导入列表
const getBatchImportList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取批量导入列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getBatchImportList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getBatchImportList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getBatchImportList()
}
// 文件上传限制
const handleExceed = () => {
ElMessage.warning('最多只能上传一个文件')
}
// 文件上传前检查
const beforeUpload = (file: UploadRawFile) => {
const isExcel =
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.type === 'application/vnd.ms-excel'
const isLt10M = file.size / 1024 / 1024 < 10
if (!isExcel) {
ElMessage.error('只能上传 Excel 文件!')
return false
}
if (!isLt10M) {
ElMessage.error('上传文件大小不能超过 10MB!')
return false
}
return false // 阻止自动上传
}
// 文件变化处理
const handleFileChange = (file: UploadFile) => {
if (file.raw) {
importFormData.excelFile = file.raw
}
}
// 导入表单验证规则
const importRules = reactive<FormRules>({
importType: [{ required: true, message: '请选择导入类型', trigger: 'change' }],
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }]
})
// 提交导入
const handleImportSubmit = async () => {
if (!importFormRef.value) return
// 检查文件是否上传
if (!importFormData.excelFile) {
ElMessage.error('请先上传Excel文件')
return
}
await importFormRef.value.validate((valid) => {
if (valid) {
importLoading.value = true
// 模拟导入过程
setTimeout(() => {
ElMessage.success(`${importFormData.importType}批量导入提交成功!`)
importDialogVisible.value = false
importLoading.value = false
getBatchImportList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.package-batch-page {
// 可以添加特定样式
}
.template-section {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
.template-tip {
font-size: 12px;
color: #909399;
}
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-upload-dragger) {
padding: 40px;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

View File

@@ -1,585 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="package-change-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="handleSearch">查询</ElButton>
<ElButton @click="exportExcel">导出excel</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 套餐变更对话框 -->
<ElDialog
v-model="changeDialogVisible"
title="套餐变更"
width="600px"
align-center
:close-on-click-modal="false"
>
<ElForm
ref="changeFormRef"
:model="changeFormData"
:rules="changeRules"
label-width="120px"
>
<ElFormItem label="当前套餐" prop="currentPackage">
<ElInput
v-model="changeFormData.currentPackage"
placeholder="当前套餐"
readonly
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="变更套餐" prop="newPackage">
<ElSelect
v-model="changeFormData.newPackage"
placeholder="请选择要变更的套餐"
style="width: 100%"
clearable
>
<ElOption label="随意联畅玩年卡套餐" value="changwan_yearly" />
<ElOption label="如意包年3G流量包" value="ruyi_3g" />
<ElOption label="Y-NB专享套餐" value="nb_special" />
<ElOption label="100G全国流量月卡套餐" value="big_data_100g" />
<ElOption label="广电飞悦卡无预存50G" value="gdtv_50g" />
<ElOption label="联通大王卡" value="unicom_dawang" />
<ElOption label="移动花卡宝藏版" value="mobile_huaka" />
<ElOption label="电信星卡" value="telecom_star" />
</ElSelect>
</ElFormItem>
<ElFormItem label="变更原因" prop="changeReason">
<ElSelect
v-model="changeFormData.changeReason"
placeholder="请选择变更原因"
style="width: 100%"
clearable
>
<ElOption label="用户主动变更" value="user_request" />
<ElOption label="套餐到期升级" value="package_upgrade" />
<ElOption label="业务需求调整" value="business_adjustment" />
<ElOption label="系统自动变更" value="system_auto" />
<ElOption label="其他原因" value="other" />
</ElSelect>
</ElFormItem>
<ElFormItem label="备注说明" prop="remark">
<ElInput
v-model="changeFormData.remark"
type="textarea"
placeholder="请输入备注说明(可选)"
:rows="3"
maxlength="200"
show-word-limit
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="changeDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleChangeSubmit" :loading="changeLoading">
确认变更
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'PackageChange' })
const changeDialogVisible = ref(false)
const loading = ref(false)
const changeLoading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
isDistribution: '',
cardStatus: '',
cardType: '',
cardCompany: '',
distributor: '',
iccid: '',
accessNumber: '',
virtualNumber: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 套餐变更表单实例
const changeFormRef = ref<FormInstance>()
// 套餐变更表单数据
const changeFormData = reactive({
currentPackage: '',
newPackage: '',
changeReason: '',
remark: ''
})
// 选中的行数据
const selectedRowData = ref<any>({})
// 模拟数据
const mockData = [
{
id: 1,
iccid: '89860621370079892035',
accessNumber: '1440012345678',
virtualNumber: '100001',
cardCompany: '联通',
expiryDate: '2025-12-31',
packageName: '随意联畅玩年卡套餐',
cardType: '2G/3G/4G',
distributorName: '北京优享科技有限公司',
cardStatus: '正常'
},
{
id: 2,
iccid: '89860621370079892036',
accessNumber: '1440012345679',
virtualNumber: '100002',
cardCompany: '移动',
expiryDate: '2025-11-30',
packageName: '如意包年3G流量包',
cardType: 'NB-IoT',
distributorName: '上海智联通信技术公司',
cardStatus: '停机'
},
{
id: 3,
iccid: '89860621370079892037',
accessNumber: '1440012345680',
virtualNumber: '100003',
cardCompany: '电信',
expiryDate: '2026-01-15',
packageName: 'Y-NB专享套餐',
cardType: '4G/5G',
distributorName: '广州物联网络有限公司',
cardStatus: '正常'
},
{
id: 4,
iccid: '89860621370079892038',
accessNumber: '1440012345681',
virtualNumber: '100004',
cardCompany: '联通',
expiryDate: '2025-10-20',
packageName: '100G全国流量月卡套餐',
cardType: '2G/3G/4G',
distributorName: '深圳云联科技股份公司',
cardStatus: '欠费停机'
},
{
id: 5,
iccid: '89860621370079892039',
accessNumber: '1440012345682',
virtualNumber: '100005',
cardCompany: '广电',
expiryDate: '2025-09-10',
packageName: '广电飞悦卡无预存50G',
cardType: '4G/5G',
distributorName: '杭州通达网络服务公司',
cardStatus: '正常'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getPackageChangeList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getPackageChangeList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '是否分销',
prop: 'isDistribution',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '是', value: 'yes' },
{ label: '否', value: 'no' }
],
onChange: handleFormChange
},
{
label: '卡状态',
prop: 'cardStatus',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '正常', value: 'normal' },
{ label: '停机', value: 'suspended' },
{ label: '欠费停机', value: 'overdue_suspended' },
{ label: '已销户', value: 'closed' },
{ label: '测试期', value: 'testing' }
],
onChange: handleFormChange
},
{
label: '卡片类型',
prop: 'cardType',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '2G/3G/4G', value: '2g_3g_4g' },
{ label: '4G/5G', value: '4g_5g' },
{ label: 'NB-IoT', value: 'nb_iot' },
{ label: 'eMTC', value: 'emtc' }
],
onChange: handleFormChange
},
{
label: '开卡公司',
prop: 'cardCompany',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '联通', value: 'unicom' },
{ label: '移动', value: 'mobile' },
{ label: '电信', value: 'telecom' },
{ label: '广电', value: 'gdtv' }
],
onChange: handleFormChange
},
{
label: '分销商',
prop: 'distributor',
type: 'input',
config: {
clearable: true,
placeholder: '请输入分销商'
},
onChange: handleFormChange
},
{
label: 'ICCID号',
prop: 'iccid',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID号'
},
onChange: handleFormChange
},
{
label: '接入号',
prop: 'accessNumber',
type: 'input',
config: {
clearable: true,
placeholder: '请输入接入号'
},
onChange: handleFormChange
},
{
label: '虚拟号',
prop: 'virtualNumber',
type: 'input',
config: {
clearable: true,
placeholder: '请输入虚拟号'
},
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: 'ICCID号', prop: 'iccid' },
{ label: '接入号码', prop: 'accessNumber' },
{ label: '虚拟号', prop: 'virtualNumber' },
{ label: '开卡公司', prop: 'cardCompany' },
{ label: '到期时间', prop: 'expiryDate' },
{ label: '套餐', prop: 'packageName' },
{ label: '卡片类型', prop: 'cardType' },
{ label: '分销商姓名', prop: 'distributorName' },
{ label: '卡状态', prop: 'cardStatus' },
{ label: '操作', prop: 'operation' }
]
// 获取卡状态标签类型
const getCardStatusType = (status: string) => {
switch (status) {
case '正常':
return 'success'
case '停机':
return 'warning'
case '欠费停机':
return 'danger'
case '已销户':
return 'info'
case '测试期':
return 'primary'
default:
return 'info'
}
}
// 导出Excel
const exportExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐变更记录`)
}
// 显示套餐变更对话框
const showChangeDialog = (row: any) => {
changeDialogVisible.value = true
selectedRowData.value = row
// 重置表单
if (changeFormRef.value) {
changeFormRef.value.resetFields()
}
changeFormData.currentPackage = row.packageName
changeFormData.newPackage = ''
changeFormData.changeReason = ''
changeFormData.remark = ''
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'iccid',
label: 'ICCID号',
minWidth: 180
},
{
prop: 'accessNumber',
label: '接入号码',
width: 140
},
{
prop: 'virtualNumber',
label: '虚拟号',
width: 100
},
{
prop: 'cardCompany',
label: '开卡公司',
width: 100
},
{
prop: 'expiryDate',
label: '到期时间',
width: 120
},
{
prop: 'packageName',
label: '套餐',
minWidth: 180
},
{
prop: 'cardType',
label: '卡片类型',
width: 120
},
{
prop: 'distributorName',
label: '分销商姓名',
minWidth: 180
},
{
prop: 'cardStatus',
label: '卡状态',
width: 100,
formatter: (row) => {
return h(ElTag, { type: getCardStatusType(row.cardStatus) }, () => row.cardStatus)
}
},
{
prop: 'operation',
label: '操作',
width: 120,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '套餐变更',
onClick: () => showChangeDialog(row)
})
])
}
}
])
onMounted(() => {
getPackageChangeList()
})
// 获取套餐变更列表
const getPackageChangeList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取套餐变更列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getPackageChangeList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getPackageChangeList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getPackageChangeList()
}
// 套餐变更表单验证规则
const changeRules = reactive<FormRules>({
newPackage: [{ required: true, message: '请选择要变更的套餐', trigger: 'change' }],
changeReason: [{ required: true, message: '请选择变更原因', trigger: 'change' }]
})
// 提交套餐变更
const handleChangeSubmit = async () => {
if (!changeFormRef.value) return
await changeFormRef.value.validate((valid) => {
if (valid) {
changeLoading.value = true
// 模拟变更过程
setTimeout(() => {
ElMessage.success(
`套餐变更成功ICCID${selectedRowData.value.iccid},从${changeFormData.currentPackage}变更为${changeFormData.newPackage}`
)
changeDialogVisible.value = false
changeLoading.value = false
getPackageChangeList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.package-change-page {
// 可以添加特定样式
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

View File

@@ -1,452 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="package-commission-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
<ElButton type="success" @click="showAddDialog">新增</ElButton>
<ElButton @click="exportExcel">导出excel</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 新增套餐佣金网卡对话框 -->
<ElDialog
v-model="addDialogVisible"
title="新增套餐佣金网卡"
width="500px"
align-center
:close-on-click-modal="false"
>
<ElForm ref="addFormRef" :model="addFormData" :rules="addRules" label-width="120px">
<ElFormItem label="卡号" prop="cardNumber">
<ElInput v-model="addFormData.cardNumber" placeholder="请输入卡号" clearable />
</ElFormItem>
<ElFormItem label="月份" prop="month">
<ElDatePicker
v-model="addFormData.month"
type="month"
placeholder="请选择月份"
style="width: 100%"
format="YYYY-MM"
value-format="YYYY-MM"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="addDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleAddSubmit" :loading="addLoading">
确认新增
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElTag, ElMessage, ElMessageBox, ElDatePicker } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'PackageCommission' })
const addDialogVisible = ref(false)
const loading = ref(false)
const addLoading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
cardNumber: '',
virtualNumber: '',
createDateRange: []
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 新增表单实例
const addFormRef = ref<FormInstance>()
// 新增表单数据
const addFormData = reactive({
cardNumber: '',
month: ''
})
// 模拟数据
const mockData = [
{
id: 1,
cardNumber: '89860621370079892035',
accessNumber: '1440012345678',
virtualNumber: '100001',
cardCompany: '联通',
month: '2025-11',
createTime: '2025-11-08 10:30:00',
creator: '张若暄'
},
{
id: 2,
cardNumber: '89860621370079892036',
accessNumber: '1440012345679',
virtualNumber: '100002',
cardCompany: '移动',
month: '2025-11',
createTime: '2025-11-07 14:15:00',
creator: '孔丽娟'
},
{
id: 3,
cardNumber: '89860621370079892037',
accessNumber: '1440012345680',
virtualNumber: '100003',
cardCompany: '电信',
month: '2025-11',
createTime: '2025-11-06 09:45:00',
creator: '李佳音'
},
{
id: 4,
cardNumber: '89860621370079892038',
accessNumber: '1440012345681',
virtualNumber: '100004',
cardCompany: '联通',
month: '2025-10',
createTime: '2025-10-20 16:20:00',
creator: '赵强'
},
{
id: 5,
cardNumber: '89860621370079892039',
accessNumber: '1440012345682',
virtualNumber: '100005',
cardCompany: '广电',
month: '2025-10',
createTime: '2025-10-15 11:30:00',
creator: '张若暄'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getCommissionCardList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getCommissionCardList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '卡号',
prop: 'cardNumber',
type: 'input',
config: {
clearable: true,
placeholder: '请输入卡号'
},
onChange: handleFormChange
},
{
label: '虚拟号',
prop: 'virtualNumber',
type: 'input',
config: {
clearable: true,
placeholder: '请输入虚拟号'
},
onChange: handleFormChange
},
{
label: '创建时间',
prop: 'createDateRange',
type: 'daterange',
config: {
type: 'daterange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '卡号', prop: 'cardNumber' },
{ label: '接入号', prop: 'accessNumber' },
{ label: '虚拟号', prop: 'virtualNumber' },
{ label: '开卡公司', prop: 'cardCompany' },
{ label: '月份', prop: 'month' },
{ label: '创建时间', prop: 'createTime' },
{ label: '创建人', prop: 'creator' },
{ label: '操作', prop: 'operation' }
]
// 导出Excel
const exportExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐佣金网卡记录`)
}
// 显示新增对话框
const showAddDialog = () => {
addDialogVisible.value = true
// 重置表单
if (addFormRef.value) {
addFormRef.value.resetFields()
}
addFormData.cardNumber = ''
addFormData.month = ''
}
// 查看详情
const viewDetails = (row: any) => {
ElMessage.info(`查看详情: ${row.cardNumber}`)
}
// 编辑记录
const editRecord = (row: any) => {
ElMessage.info(`编辑记录: ${row.cardNumber}`)
}
// 删除记录
const deleteRecord = (row: any) => {
ElMessageBox.confirm(`确定要删除卡号为"${row.cardNumber}"的佣金记录吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('删除成功')
getCommissionCardList()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'cardNumber',
label: '卡号',
minWidth: 180
},
{
prop: 'accessNumber',
label: '接入号',
width: 140
},
{
prop: 'virtualNumber',
label: '虚拟号',
width: 100
},
{
prop: 'cardCompany',
label: '开卡公司',
width: 100
},
{
prop: 'month',
label: '月份',
width: 100
},
{
prop: 'createTime',
label: '创建时间',
width: 160
},
{
prop: 'creator',
label: '创建人',
width: 100
},
{
prop: 'operation',
label: '操作',
width: 220,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '查看',
onClick: () => viewDetails(row)
}),
h(ArtButtonTable, {
text: '编辑',
onClick: () => editRecord(row)
}),
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteRecord(row)
})
])
}
}
])
onMounted(() => {
getCommissionCardList()
})
// 获取套餐佣金网卡列表
const getCommissionCardList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取套餐佣金网卡列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getCommissionCardList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getCommissionCardList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getCommissionCardList()
}
// 新增表单验证规则
const addRules = reactive<FormRules>({
cardNumber: [
{ required: true, message: '请输入卡号', trigger: 'blur' },
{ min: 15, max: 20, message: '卡号长度在 15 到 20 个字符', trigger: 'blur' }
],
month: [{ required: true, message: '请选择月份', trigger: 'change' }]
})
// 提交新增
const handleAddSubmit = async () => {
if (!addFormRef.value) return
await addFormRef.value.validate((valid) => {
if (valid) {
addLoading.value = true
// 模拟新增过程
setTimeout(() => {
ElMessage.success(
`新增套餐佣金网卡成功!卡号:${addFormData.cardNumber},月份:${addFormData.month}`
)
addDialogVisible.value = false
addLoading.value = false
getCommissionCardList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.package-commission-page {
// 可以添加特定样式
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

View File

@@ -1,955 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="package-create-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 数据视图组件 -->
<ArtDataViewer
ref="dataViewerRef"
:data="tableData"
:loading="loading"
:table-columns="columns"
:descriptions-fields="descriptionsFields"
:descriptions-columns="2"
:pagination="pagination"
:label-width="'140px'"
:field-columns="columnChecks"
:show-card-actions="true"
:show-card-selection="true"
:default-view="currentView"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@view-change="handleViewChange"
>
<template #header-left>
<ElButton type="success" @click="showCreateDialog">新增套餐</ElButton>
<ElButton @click="exportExcel">导出excel</ElButton>
</template>
<template #header-right>
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
v-model:currentView="currentView"
@refresh="handleRefresh"
@viewChange="handleViewChange"
:show-title="false"
:show-view-toggle="true"
/>
</template>
<template #card-actions="{ item }">
<ElButton type="primary" size="small" @click="editPackage(item)"> 编辑 </ElButton>
<ElButton type="warning" size="small" @click="offlinePackage(item)"> 下架 </ElButton>
<ElButton type="success" size="small" @click="copyPackage(item)"> 复制套餐 </ElButton>
</template>
</ArtDataViewer>
<!-- 新增套餐对话框 -->
<ElDialog
v-model="dialogVisible"
title="新增套餐"
width="55%"
align-center
:close-on-click-modal="false"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="150px">
<ElRow :gutter="24">
<ElCol :span="12">
<ElFormItem label="套餐系列" prop="series">
<ElSelect
v-model="formData.series"
placeholder="请选择套餐系列"
style="width: 100%"
>
<ElOption label="畅玩系列" value="changwan" />
<ElOption label="如意系列" value="ruyi" />
<ElOption label="NB专享" value="nb_special" />
<ElOption label="大流量系列" value="big_data" />
<ElOption label="广电系列" value="gdtv" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="余额充值梯度" prop="balanceRechargeGradient">
<ElSelect
v-model="formData.balanceRechargeGradient"
placeholder="请选择充值梯度"
style="width: 100%"
>
<ElOption label="10,20,50,100" value="10,20,50,100" />
<ElOption label="5,10,20,50" value="5,10,20,50" />
<ElOption label="10,50,100" value="10,50,100" />
<ElOption label="20,50,100,200" value="20,50,100,200" />
<ElOption label="10,30,50" value="10,30,50" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="24">
<ElCol :span="12">
<ElFormItem label="套餐名称" prop="name">
<ElInput v-model="formData.name" placeholder="请输入套餐名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="套餐类型" prop="type">
<ElSelect
v-model="formData.type"
placeholder="请选择套餐类型"
style="width: 100%"
>
<ElOption label="月套餐" value="monthly" />
<ElOption label="年套餐" value="yearly" />
<ElOption label="流量包" value="data_package" />
<ElOption label="季度套餐" value="quarterly" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="24">
<ElCol :span="12">
<ElFormItem label="本月次月加餐包" prop="monthlyNextMonthAddon">
<ElSelect
v-model="formData.monthlyNextMonthAddon"
placeholder="请选择加餐包支持"
style="width: 100%"
>
<ElOption label="支持" value="support" />
<ElOption label="不支持" value="not_support" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="本月最大充值数量" prop="maxMonthlyRecharge">
<ElInputNumber
v-model="formData.maxMonthlyRecharge"
:min="1"
:max="1000"
placeholder="请输入本月最大充值数量"
style="width: 100%"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="24">
<ElCol :span="12">
<ElFormItem label="次月最大充值数量" prop="maxNextMonthRecharge">
<ElInputNumber
v-model="formData.maxNextMonthRecharge"
:min="1"
:max="1000"
placeholder="请输入次月最大充值数量"
style="width: 100%"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="套餐数量" prop="packageCount">
<ElInputNumber
v-model="formData.packageCount"
:min="1"
:max="999999"
placeholder="请输入套餐数量"
style="width: 100%"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="24">
<ElCol :span="12">
<ElFormItem label="成本价" prop="costPrice">
<ElInputNumber
v-model="formData.costPrice"
:min="0"
:max="9999"
:precision="2"
placeholder="请输入成本价"
style="width: 100%"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="套餐金额" prop="packageAmount">
<ElInputNumber
v-model="formData.packageAmount"
:min="0"
:max="9999"
:precision="2"
placeholder="请输入套餐金额"
style="width: 100%"
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
确认新增
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElTag, ElMessage, ElMessageBox, ElInputNumber } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtDataViewer from '@/components/core/views/ArtDataViewer.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
defineOptions({ name: 'PackageCreate' })
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
// 当前视图模式
const currentView = ref('table')
// 数据视图组件引用
const dataViewerRef = ref()
// 定义表单搜索初始值
const initialSearchState = {
packageType: '',
packageName: '',
packageCode: '',
status: '',
packageSeries: '',
levelOneMerge: '',
levelTwoMerge: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 表单实例
const formRef = ref<FormInstance>()
// 新增表单数据
const formData = reactive({
series: '',
balanceRechargeGradient: '',
name: '',
type: '',
monthlyNextMonthAddon: '',
maxMonthlyRecharge: 0,
maxNextMonthRecharge: 0,
packageCount: 0,
costPrice: 0,
packageAmount: 0
})
// 模拟数据
const mockData = [
{
id: 1,
packageSeries: '畅玩系列',
packageName: '随意联畅玩年卡套餐',
packageTraffic: 120,
packageCount: 1000,
packageCountType: '张',
packageType: '年套餐',
packageAmount: '168.00',
description: '年卡套餐含120GB流量',
frontendSort: 1,
sort: 1,
maxMonthlyRecharge: 50,
maxNextMonthRecharge: 50,
monthlyNextMonthAddon: '支持',
status: '启用',
levelOneMerge: '是',
levelOneMergeCode: 'L1_001',
levelTwoMerge: '否',
levelTwoMergeCode: '',
isSpecial: '否',
creator: '张若暄',
modifier: '张若暄',
addTime: '2025-11-08 10:30:00',
balanceRechargeGradient: '10,20,50,100',
packageCode: 'PKG_001'
},
{
id: 2,
packageSeries: '如意系列',
packageName: '如意包年3G流量包',
packageTraffic: 36,
packageCount: 800,
packageCountType: '张',
packageType: '年套餐',
packageAmount: '98.00',
description: '年包3GB月套餐',
frontendSort: 2,
sort: 2,
maxMonthlyRecharge: 30,
maxNextMonthRecharge: 30,
monthlyNextMonthAddon: '支持',
status: '启用',
levelOneMerge: '是',
levelOneMergeCode: 'L1_002',
levelTwoMerge: '是',
levelTwoMergeCode: 'L2_001',
isSpecial: '否',
creator: '孔丽娟',
modifier: '孔丽娟',
addTime: '2025-11-07 14:15:00',
balanceRechargeGradient: '5,10,20,50',
packageCode: 'PKG_002'
},
{
id: 3,
packageSeries: 'NB专享',
packageName: 'Y-NB专享套餐',
packageTraffic: 24,
packageCount: 500,
packageCountType: '张',
packageType: '月套餐',
packageAmount: '25.00',
description: 'NB-IoT专用套餐',
frontendSort: 3,
sort: 3,
maxMonthlyRecharge: 100,
maxNextMonthRecharge: 100,
monthlyNextMonthAddon: '不支持',
status: '停用',
levelOneMerge: '否',
levelOneMergeCode: '',
levelTwoMerge: '否',
levelTwoMergeCode: '',
isSpecial: '是',
creator: '李佳音',
modifier: '李佳音',
addTime: '2025-11-06 09:45:00',
balanceRechargeGradient: '10,50,100',
packageCode: 'PKG_003'
},
{
id: 4,
packageSeries: '大流量系列',
packageName: '100G全国流量月卡套餐',
packageTraffic: 100,
packageCount: 1200,
packageCountType: '张',
packageType: '月套餐',
packageAmount: '59.00',
description: '100GB大流量月卡',
frontendSort: 4,
sort: 4,
maxMonthlyRecharge: 80,
maxNextMonthRecharge: 80,
monthlyNextMonthAddon: '支持',
status: '启用',
levelOneMerge: '是',
levelOneMergeCode: 'L1_003',
levelTwoMerge: '否',
levelTwoMergeCode: '',
isSpecial: '否',
creator: '赵强',
modifier: '赵强',
addTime: '2025-11-05 16:20:00',
balanceRechargeGradient: '20,50,100,200',
packageCode: 'PKG_004'
},
{
id: 5,
packageSeries: '广电系列',
packageName: '广电飞悦卡无预存50G',
packageTraffic: 50,
packageCount: 600,
packageCountType: '张',
packageType: '月套餐',
packageAmount: '39.00',
description: '广电30天50GB流量',
frontendSort: 5,
sort: 5,
maxMonthlyRecharge: 60,
maxNextMonthRecharge: 60,
monthlyNextMonthAddon: '支持',
status: '启用',
levelOneMerge: '否',
levelOneMergeCode: '',
levelTwoMerge: '是',
levelTwoMergeCode: 'L2_002',
isSpecial: '是',
creator: '张若暄',
modifier: '张若暄',
addTime: '2025-11-04 11:30:00',
balanceRechargeGradient: '10,30,50',
packageCode: 'PKG_005'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getPackageList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getPackageList()
}
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '套餐类型',
prop: 'packageType',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '月套餐', value: 'monthly' },
{ label: '年套餐', value: 'yearly' },
{ label: '流量包', value: 'data_package' },
{ label: '季度套餐', value: 'quarterly' }
],
onChange: handleFormChange
},
{
label: '套餐名称',
prop: 'packageName',
type: 'input',
config: {
clearable: true,
placeholder: '请输入套餐名称'
},
onChange: handleFormChange
},
{
label: '套餐编码',
prop: 'packageCode',
type: 'input',
config: {
clearable: true,
placeholder: '请输入套餐编码'
},
onChange: handleFormChange
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '启用', value: 'active' },
{ label: '停用', value: 'inactive' }
],
onChange: handleFormChange
},
{
label: '套餐系列',
prop: 'packageSeries',
type: 'input',
config: {
clearable: true,
placeholder: '请输入套餐系列'
},
onChange: handleFormChange
},
{
label: '一级合并',
prop: 'levelOneMerge',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '是', value: 'yes' },
{ label: '否', value: 'no' }
],
onChange: handleFormChange
},
{
label: '二级合并',
prop: 'levelTwoMerge',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '是', value: 'yes' },
{ label: '否', value: 'no' }
],
onChange: handleFormChange
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '套餐系列', prop: 'packageSeries' },
{ label: '套餐名称', prop: 'packageName' },
{ label: '套餐流量(GB)', prop: 'packageTraffic' },
{ label: '套餐数量', prop: 'packageCount' },
{ label: '套餐数量类型', prop: 'packageCountType' },
{ label: '套餐类型', prop: 'packageType' },
{ label: '套餐金额', prop: 'packageAmount' },
{ label: '描述', prop: 'description' },
{ label: '前端排序', prop: 'frontendSort' },
{ label: '排序', prop: 'sort' },
{ label: '本月最大充值数量', prop: 'maxMonthlyRecharge' },
{ label: '次月最大充值数量', prop: 'maxNextMonthRecharge' },
{ label: '本月次月加餐包', prop: 'monthlyNextMonthAddon' },
{ label: '状态', prop: 'status' },
{ label: '一级是否合并', prop: 'levelOneMerge' },
{ label: '一级合并编号', prop: 'levelOneMergeCode' },
{ label: '二级是否合并', prop: 'levelTwoMerge' },
{ label: '二级合并编号', prop: 'levelTwoMergeCode' },
{ label: '是否特殊', prop: 'isSpecial' },
{ label: '创建人', prop: 'creator' },
{ label: '修改人', prop: 'modifier' },
{ label: '添加时间', prop: 'addTime' },
{ label: '余额充值梯度', prop: 'balanceRechargeGradient' },
{ label: '套餐编码', prop: 'packageCode' },
{ label: '操作', prop: 'operation' }
]
// 描述字段配置(用于卡片视图)
const descriptionsFields = [
{ prop: 'packageName', label: '套餐名称', span: 2 },
{ prop: 'packageSeries', label: '套餐系列' },
{ prop: 'packageCode', label: '套餐编码' },
{
prop: 'packageTraffic',
label: '套餐流量',
formatter: (row: any) => `${row.packageTraffic}GB`
},
{
prop: 'packageCount',
label: '套餐数量',
formatter: (row: any) => `${row.packageCount}${row.packageCountType}`
},
{ prop: 'packageType', label: '套餐类型' },
{
prop: 'packageAmount',
label: '套餐金额',
formatter: (row: any) => `¥${row.packageAmount}`
},
{ prop: 'description', label: '描述', span: 2 },
{
prop: 'status',
label: '状态',
formatter: (row: any) => {
const type = getStatusType(row.status)
return `<el-tag type="${type}" size="small">${row.status}</el-tag>`
}
},
{
prop: 'monthlyNextMonthAddon',
label: '本月次月加餐包',
formatter: (row: any) => {
const type = row.monthlyNextMonthAddon === '支持' ? 'success' : 'info'
return `<el-tag type="${type}" size="small">${row.monthlyNextMonthAddon}</el-tag>`
}
},
{ prop: 'maxMonthlyRecharge', label: '本月最大充值数量' },
{ prop: 'maxNextMonthRecharge', label: '次月最大充值数量' },
{
prop: 'levelOneMerge',
label: '一级是否合并',
formatter: (row: any) => {
const type = getMergeType(row.levelOneMerge)
return `<el-tag type="${type}" size="small">${row.levelOneMerge}</el-tag>`
}
},
{
prop: 'levelOneMergeCode',
label: '一级合并编号',
formatter: (row: any) => row.levelOneMergeCode || '-'
},
{
prop: 'levelTwoMerge',
label: '二级是否合并',
formatter: (row: any) => {
const type = getMergeType(row.levelTwoMerge)
return `<el-tag type="${type}" size="small">${row.levelTwoMerge}</el-tag>`
}
},
{
prop: 'levelTwoMergeCode',
label: '二级合并编号',
formatter: (row: any) => row.levelTwoMergeCode || '-'
},
{
prop: 'isSpecial',
label: '是否特殊',
formatter: (row: any) => {
const type = getSpecialType(row.isSpecial)
return `<el-tag type="${type}" size="small">${row.isSpecial}</el-tag>`
}
},
{ prop: 'creator', label: '创建人' },
{ prop: 'addTime', label: '添加时间', span: 2 },
{ prop: 'balanceRechargeGradient', label: '余额充值梯度' },
{ prop: 'frontendSort', label: '前端排序' },
{ prop: 'sort', label: '排序' }
]
// 获取状态标签类型
const getStatusType = (status: string) => {
switch (status) {
case '启用':
return 'success'
case '停用':
return 'danger'
default:
return 'info'
}
}
// 获取是否合并标签类型
const getMergeType = (merge: string) => {
switch (merge) {
case '是':
return 'success'
case '否':
return 'info'
default:
return 'info'
}
}
// 获取是否特殊标签类型
const getSpecialType = (special: string) => {
switch (special) {
case '是':
return 'warning'
case '否':
return 'info'
default:
return 'info'
}
}
// 导出Excel
const exportExcel = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要导出的数据')
return
}
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐记录`)
}
// 显示新增对话框
const showCreateDialog = () => {
dialogVisible.value = true
// 重置表单
if (formRef.value) {
formRef.value.resetFields()
}
formData.series = ''
formData.balanceRechargeGradient = ''
formData.name = ''
formData.type = ''
formData.monthlyNextMonthAddon = ''
formData.maxMonthlyRecharge = 0
formData.maxNextMonthRecharge = 0
formData.packageCount = 0
formData.costPrice = 0
formData.packageAmount = 0
}
// 编辑套餐
const editPackage = (row: any) => {
ElMessage.info(`编辑套餐: ${row.packageName}`)
}
// 下架套餐
const offlinePackage = (row: any) => {
ElMessageBox.confirm(`确定要下架套餐"${row.packageName}"吗?`, '下架确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('套餐下架成功')
getPackageList()
})
.catch(() => {
ElMessage.info('已取消下架')
})
}
// 复制套餐
const copyPackage = (row: any) => {
ElMessage.info(`复制套餐: ${row.packageName}`)
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'packageSeries',
label: '套餐系列',
width: 120
},
{
prop: 'packageName',
label: '套餐名称',
minWidth: 180
},
{
prop: 'packageTraffic',
label: '套餐流量(GB)',
width: 120,
formatter: (row) => `${row.packageTraffic}GB`
},
{
prop: 'packageCount',
label: '套餐数量',
width: 100,
formatter: (row) => `${row.packageCount}${row.packageCountType}`
},
{
prop: 'packageType',
label: '套餐类型',
width: 100
},
{
prop: 'packageAmount',
label: '套餐金额',
width: 100,
formatter: (row) => `¥${row.packageAmount}`
},
{
prop: 'status',
label: '状态',
width: 80,
formatter: (row) => {
return h(ElTag, { type: getStatusType(row.status) }, () => row.status)
}
},
{
prop: 'levelOneMerge',
label: '一级是否合并',
width: 120,
formatter: (row) => {
return h(ElTag, { type: getMergeType(row.levelOneMerge) }, () => row.levelOneMerge)
}
},
{
prop: 'levelOneMergeCode',
label: '一级合并编号',
width: 120,
formatter: (row) => row.levelOneMergeCode || '-'
},
{
prop: 'levelTwoMerge',
label: '二级是否合并',
width: 120,
formatter: (row) => {
return h(ElTag, { type: getMergeType(row.levelTwoMerge) }, () => row.levelTwoMerge)
}
},
{
prop: 'levelTwoMergeCode',
label: '二级合并编号',
width: 120,
formatter: (row) => row.levelTwoMergeCode || '-'
},
{
prop: 'isSpecial',
label: '是否特殊',
width: 100,
formatter: (row) => {
return h(ElTag, { type: getSpecialType(row.isSpecial) }, () => row.isSpecial)
}
},
{
prop: 'creator',
label: '创建人',
width: 100
},
{
prop: 'addTime',
label: '添加时间',
width: 160
},
{
prop: 'packageCode',
label: '套餐编码',
width: 120
},
{
prop: 'operation',
label: '操作',
width: 240,
formatter: (row: any) => {
return h('div', { class: 'operation-buttons' }, [
h(ArtButtonTable, {
text: '编辑',
onClick: () => editPackage(row)
}),
h(ArtButtonTable, {
text: '下架',
onClick: () => offlinePackage(row)
}),
h(ArtButtonTable, {
text: '复制套餐',
onClick: () => copyPackage(row)
})
])
}
}
])
onMounted(() => {
getPackageList()
})
// 获取套餐列表
const getPackageList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const paginatedData = mockData.slice(startIndex, endIndex)
tableData.value = paginatedData
pagination.total = mockData.length
loading.value = false
} catch (error) {
console.error('获取套餐列表失败:', error)
loading.value = false
}
}
const handleRefresh = () => {
getPackageList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getPackageList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getPackageList()
}
// 处理视图切换
const handleViewChange = (view: string) => {
console.log('视图切换到:', view)
// 可以在这里添加视图切换的额外逻辑,比如保存用户偏好
}
// 表单验证规则
const rules = reactive<FormRules>({
series: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
balanceRechargeGradient: [{ required: true, message: '请选择余额充值梯度', trigger: 'change' }],
name: [
{ required: true, message: '请输入套餐名称', trigger: 'blur' },
{ min: 2, max: 50, message: '套餐名称长度在 2 到 50 个字符', trigger: 'blur' }
],
type: [{ required: true, message: '请选择套餐类型', trigger: 'change' }],
monthlyNextMonthAddon: [{ required: true, message: '请选择本月次月加餐包', trigger: 'change' }],
maxMonthlyRecharge: [{ required: true, message: '请输入本月最大充值数量', trigger: 'blur' }],
maxNextMonthRecharge: [{ required: true, message: '请输入次月最大充值数量', trigger: 'blur' }],
packageCount: [{ required: true, message: '请输入套餐数量', trigger: 'blur' }],
costPrice: [{ required: true, message: '请输入成本价', trigger: 'blur' }],
packageAmount: [{ required: true, message: '请输入套餐金额', trigger: 'blur' }]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
submitLoading.value = true
// 模拟提交过程
setTimeout(() => {
ElMessage.success(
`新增套餐成功!套餐名称:${formData.name},套餐系列:${formData.series}`
)
dialogVisible.value = false
submitLoading.value = false
getPackageList()
}, 2000)
}
})
}
</script>
<style lang="scss" scoped>
.package-create-page {
// 可以添加特定样式
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

View File

@@ -9,7 +9,9 @@
@refresh="handleRefresh"
>
<template #left>
<el-button type="primary" @click="showCreateDialog">新增规则</el-button>
<el-button type="primary" @click="showCreateDialog" v-if="hasAuth('alert_rules:create')"
>新增规则</el-button
>
</template>
</ArtTableHeader>
@@ -20,26 +22,13 @@
:loading="loading"
:data="tableData"
:marginTop="10"
:row-class-name="getRowClassName"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
:actions="getActions"
:actionsWidth="160"
>
<template #default>
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</el-card>
<!-- 新增/编辑规则弹窗 -->
@@ -164,26 +153,16 @@
import { PollingAlertService } from '@/api/modules'
import type { PollingAlertRule, CreatePollingAlertRuleRequest } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
const loading = ref(false)
const tableData = ref<PollingAlertRule[]>([])
const dialogVisible = ref(false)
const dialogType = ref<'create' | 'edit'>('create')
const formRef = ref<FormInstance>()
const contextMenuRef = ref()
const currentRow = ref<PollingAlertRule | null>(null)
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const form = reactive<CreatePollingAlertRuleRequest & { id?: number }>({
rule_name: '',
@@ -301,14 +280,38 @@
}))
)
// 右键菜单项
const contextMenuItems = computed<MenuItemType[]>(() => {
return [
{ key: 'edit', label: '编辑' },
{ key: 'toggle', label: currentRow.value?.status === 1 ? '禁用' : '启用' },
{ key: 'delete', label: '删除' }
]
})
// 获取操作按钮
const getActions = (row: PollingAlertRule) => {
const actions: any[] = []
// 禁用/启用按钮直接显示
if (hasAuth('alert_rules:toggle')) {
actions.push({
label: row.status === 1 ? '禁用' : '启用',
handler: () => toggleStatus(row),
type: 'primary'
})
}
// 编辑和删除放到更多里
if (hasAuth('alert_rules:edit')) {
actions.push({
label: '编辑',
handler: () => showEditDialog(row),
type: 'primary'
})
}
if (hasAuth('alert_rules:delete')) {
actions.push({
label: '删除',
handler: () => handleDelete(row),
type: 'danger'
})
}
return actions
}
watch(dialogVisible, (val) => {
if (!val) {
@@ -444,31 +447,6 @@
})
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: PollingAlertRule, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'edit':
showEditDialog(currentRow.value)
break
case 'toggle':
toggleStatus(currentRow.value)
break
case 'delete':
handleDelete(currentRow.value)
break
}
}
loadData()
</script>

View File

@@ -27,9 +27,18 @@
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showCreateDialog">新增配置</ElButton>
<ElButton @click="handlePreview">预览清理</ElButton>
<ElButton type="warning" @click="handleTriggerCleanup" :loading="triggering">
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('data_cleanup:create')"
>新增配置</ElButton
>
<ElButton @click="handlePreview" v-if="hasAuth('data_cleanup:preview')"
>预览清理</ElButton
>
<ElButton
type="warning"
@click="handleTriggerCleanup"
:loading="triggering"
v-if="hasAuth('data_cleanup:trigger')"
>
手动清理
</ElButton>
</template>
@@ -42,28 +51,15 @@
:loading="loading"
:data="configList"
:marginTop="10"
:row-class-name="getRowClassName"
:show-pagination="false"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
:actions="getActions"
:actionsWidth="180"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="140"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -131,8 +127,8 @@
<ElTable :data="previewData" border>
<ElTableColumn prop="table_name" label="表名" width="150" />
<ElTableColumn prop="description" label="说明" min-width="180" />
<ElTableColumn prop="retention_days" label="保留天数" width="100" align="center" />
<ElTableColumn prop="record_count" label="待清理记录数" width="130" align="right">
<ElTableColumn prop="retention_days" label="保留天数" width="100" />
<ElTableColumn prop="record_count" label="待清理记录数" width="130">
<template #default="{ row }">
<span :class="{ 'text-danger': row.record_count > 10000 }">
{{ row.record_count.toLocaleString() }}
@@ -146,7 +142,7 @@
</ElDialog>
<!-- 清理日志对话框 -->
<ElDialog v-model="logVisible" title="清理日志" width="1000px">
<ElDialog v-model="logVisible" title="清理日志" width="60%">
<div style="margin-bottom: 16px">
<ElInput
v-model="logFilter.table_name"
@@ -183,14 +179,14 @@
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="retention_days" label="保留天数" width="100" align="center" />
<ElTableColumn prop="deleted_count" label="删除记录数" width="110" align="right">
<ElTableColumn prop="retention_days" label="保留天数" width="100" />
<ElTableColumn prop="deleted_count" label="删除记录数" width="110">
<template #default="{ row }">
{{ row.deleted_count.toLocaleString() }}
</template>
</ElTableColumn>
<ElTableColumn prop="duration_ms" label="耗时(ms)" width="100" align="right" />
<ElTableColumn prop="started_at" label="开始时间" width="160">
<ElTableColumn prop="duration_ms" label="耗时(ms)" width="100" />
<ElTableColumn prop="started_at" label="开始时间" width="180">
<template #default="{ row }">
{{ formatDateTime(row.started_at) }}
</template>
@@ -235,14 +231,13 @@
DataCleanupProgress
} from '@/types/api'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { useAuth } from '@/composables/useAuth'
defineOptions({ name: 'DataCleanup' })
const { hasAuth } = useAuth()
const loading = ref(false)
const submitLoading = ref(false)
const triggering = ref(false)
@@ -253,10 +248,6 @@
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
// 右键菜单
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<DataCleanupConfig | null>(null)
// 列配置
const columnOptions = [
{ label: '表名', prop: 'table_name' },
@@ -329,14 +320,12 @@
{
prop: 'retention_days',
label: '保留天数',
width: 100,
align: 'center'
width: 100
},
{
prop: 'batch_size',
label: '每批删除条数',
width: 130,
align: 'right',
formatter: (row: DataCleanupConfig) => row.batch_size.toLocaleString()
},
{
@@ -429,7 +418,7 @@
})
dialogType.value = 'edit'
dialogVisible.value = true
nextTick(() => {
await nextTick(() => {
formRef.value?.clearValidate()
})
}
@@ -537,14 +526,6 @@
}
}
// 查看日志
const handleViewLogs = () => {
logFilter.table_name = ''
logPagination.page = 1
logVisible.value = true
getCleanupLogs()
}
// 获取清理日志
const getCleanupLogs = async () => {
try {
@@ -562,51 +543,43 @@
}
}
// 右键菜单项
const contextMenuItems = computed((): MenuItemType[] => {
return [
{ key: 'edit', label: '编辑' },
{ key: 'logs', label: '查看日志' },
{ key: 'delete', label: '删除' }
]
})
// 获取操作按钮
const getActions = (row: DataCleanupConfig) => {
const actions: any[] = []
// 处理表格行右键菜单
const handleRowContextMenu = (row: DataCleanupConfig, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'edit':
showEditDialog(currentRow.value)
break
case 'logs':
logFilter.table_name = currentRow.value.table_name
logPagination.page = 1
logVisible.value = true
getCleanupLogs()
break
case 'delete':
handleDelete(currentRow.value)
break
// 查看日志按钮直接显示
if (hasAuth('data_cleanup:logs')) {
actions.push({
label: '查看日志',
handler: () => {
logFilter.table_name = row.table_name
logPagination.page = 1
logVisible.value = true
getCleanupLogs()
},
type: 'primary'
})
}
}
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 编辑和删除放到更多里
if (hasAuth('data_cleanup:edit')) {
actions.push({
label: '编辑',
handler: () => showEditDialog(row),
type: 'primary'
})
}
if (hasAuth('data_cleanup:delete')) {
actions.push({
label: '删除',
handler: () => handleDelete(row),
type: 'danger'
})
}
return actions
}
</script>
<style scoped lang="scss">
@@ -614,10 +587,6 @@
height: 100%;
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.text-danger {
font-weight: 600;
color: var(--el-color-danger);

View File

@@ -1,81 +1,5 @@
<template>
<div class="polling-monitor-page">
<!-- 总览统计卡片 -->
<el-row :gutter="20" class="stats-row">
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon total">
<el-icon :size="32"><Document /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">总卡数</div>
<div class="stat-value">{{ overviewStats?.total_cards || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon initialized">
<el-icon :size="32"><CircleCheck /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">已初始化</div>
<div class="stat-value">{{ overviewStats?.initialized_cards || 0 }}</div>
<el-progress
v-if="overviewStats"
:percentage="overviewStats.init_progress"
:stroke-width="6"
:show-text="false"
style="margin-top: 8px"
/>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon realname">
<el-icon :size="32"><User /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">实名队列</div>
<div class="stat-value">{{ overviewStats?.realname_queue_size || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon package">
<el-icon :size="32"><Box /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">套餐队列</div>
<div class="stat-value">{{ overviewStats?.package_queue_size || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon carddata">
<el-icon :size="32"><DataLine /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">流量队列</div>
<div class="stat-value">{{ overviewStats?.carddata_queue_size || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 队列状态 -->
<el-card shadow="never" class="section-card">
<template #header>
@@ -173,33 +97,19 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Document, CircleCheck, User, Box, DataLine } from '@element-plus/icons-vue'
import { PollingMonitorService } from '@/api/modules'
import type {
PollingOverviewStats,
PollingInitProgress,
PollingQueueStatus,
PollingTaskStats
} from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
const overviewStats = ref<PollingOverviewStats>()
const initProgress = ref<PollingInitProgress>()
const queueStatusList = ref<PollingQueueStatus[]>([])
const taskStatsList = ref<PollingTaskStats[]>([])
let refreshTimer: NodeJS.Timeout | null = null
// 加载总览统计
const loadOverviewStats = async () => {
try {
const { data } = await PollingMonitorService.getOverviewStats()
overviewStats.value = data
} catch (error) {
ElMessage.error('加载总览统计失败')
}
}
// 加载初始化进度
const loadInitProgress = async (showMessage = false) => {
try {
@@ -244,7 +154,6 @@
// 加载所有数据
const loadAllData = () => {
loadOverviewStats()
loadInitProgress()
loadQueueStatus()
loadTaskStats()

View File

@@ -1,473 +0,0 @@
<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="operatorFilter" placeholder="运营商筛选" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="中国移动" value="CMCC" />
<ElOption label="中国联通" value="CUCC" />
<ElOption label="中国电信" value="CTCC" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="statusFilter" placeholder="分配状态" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="已分配" value="assigned" />
<ElOption label="未分配" value="unassigned" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
<ElButton v-ripple type="primary" @click="showAssignDialog">批量分配</ElButton>
</ElCol>
</ElRow>
<!-- 号卡产品列表 -->
<ArtTable
:data="filteredData"
index
style="margin-top: 20px"
@selection-change="handleSelectionChange"
>
<template #default>
<ElTableColumn type="selection" width="55" />
<ElTableColumn label="产品名称" prop="productName" min-width="180" show-overflow-tooltip />
<ElTableColumn label="运营商" prop="operator" width="100">
<template #default="scope">
<ElTag :type="getOperatorTagType(scope.row.operator)">
{{ getOperatorText(scope.row.operator) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="套餐规格" prop="packageSpec" min-width="150" />
<ElTableColumn label="产品价格" prop="price" width="120">
<template #default="scope"> ¥{{ scope.row.price.toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn label="库存数量" prop="stock" width="100" align="center">
<template #default="scope">
<span :style="{ color: scope.row.stock < 100 ? 'var(--el-color-danger)' : '' }">
{{ scope.row.stock }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="已分配数量" prop="assignedCount" width="120" align="center">
<template #default="scope">
<span style="color: var(--el-color-primary)">{{ scope.row.assignedCount }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="分配状态" prop="assignStatus" width="100">
<template #default="scope">
<ElTag v-if="scope.row.assignedCount > 0" type="success">已分配</ElTag>
<ElTag v-else type="info">未分配</ElTag>
</template>
</ElTableColumn>
<ElTableColumn fixed="right" label="操作" width="200">
<template #default="scope">
<el-button link :icon="View" @click="viewAssignDetail(scope.row)">分配记录</el-button>
<el-button link type="primary" @click="assignToAgent(scope.row)">分配</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 分配对话框 -->
<ElDialog v-model="assignDialogVisible" title="分配号卡产品" width="600px" align-center>
<ElForm ref="formRef" :model="assignForm" :rules="assignRules" label-width="120px">
<ElFormItem label="选择代理商" prop="agentId">
<ElSelect
v-model="assignForm.agentId"
placeholder="请选择代理商"
filterable
style="width: 100%"
@change="handleAgentChange"
>
<ElOption
v-for="agent in agentList"
:key="agent.id"
:label="`${agent.agentName} (${agent.phone})`"
:value="agent.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="分配数量" prop="quantity">
<ElInputNumber
v-model="assignForm.quantity"
:min="1"
:max="currentProduct.stock"
style="width: 100%"
/>
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
当前库存{{ currentProduct.stock }}
</div>
</ElFormItem>
<ElFormItem label="分佣模式" prop="commissionMode">
<ElRadioGroup v-model="assignForm.commissionMode">
<ElRadio value="fixed">固定佣金</ElRadio>
<ElRadio value="percent">比例佣金</ElRadio>
<ElRadio value="template">使用模板</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="assignForm.commissionMode === 'fixed'"
label="固定金额"
prop="fixedAmount"
>
<ElInputNumber
v-model="assignForm.fixedAmount"
:min="0"
:precision="2"
style="width: 100%"
/>
<span style="margin-left: 8px">/</span>
</ElFormItem>
<ElFormItem v-if="assignForm.commissionMode === 'percent'" label="佣金比例" prop="percent">
<ElInputNumber
v-model="assignForm.percent"
:min="0"
:max="100"
:precision="2"
style="width: 100%"
/>
<span style="margin-left: 8px">%</span>
</ElFormItem>
<ElFormItem
v-if="assignForm.commissionMode === 'template'"
label="分佣模板"
prop="templateId"
>
<ElSelect
v-model="assignForm.templateId"
placeholder="请选择分佣模板"
style="width: 100%"
>
<ElOption
v-for="template in commissionTemplates"
:key="template.id"
:label="template.templateName"
:value="template.id"
>
<span>{{ template.templateName }}</span>
<span style="float: right; font-size: 12px; color: var(--el-text-color-secondary)">
{{ template.mode === 'fixed' ? `¥${template.value}元/张` : `${template.value}%` }}
</span>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem label="特殊折扣" prop="discount">
<ElInputNumber
v-model="assignForm.discount"
:min="0"
:max="100"
:precision="2"
style="width: 100%"
/>
<span style="margin-left: 8px">%</span>
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
0表示无折扣设置后代理商可以此折扣价格销售
</div>
</ElFormItem>
<ElFormItem label="备注" prop="remark">
<ElInput
v-model="assignForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="assignDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleAssignSubmit">确认分配</ElButton>
</div>
</template>
</ElDialog>
<!-- 分配记录对话框 -->
<ElDialog v-model="detailDialogVisible" title="分配记录" width="900px" align-center>
<ArtTable :data="assignRecords" index>
<template #default>
<ElTableColumn label="代理商名称" prop="agentName" min-width="150" />
<ElTableColumn label="分配数量" prop="quantity" width="100" align="center" />
<ElTableColumn label="分佣模式" prop="commissionMode" width="120">
<template #default="scope">
<ElTag v-if="scope.row.commissionMode === 'fixed'" type="warning">固定佣金</ElTag>
<ElTag v-else-if="scope.row.commissionMode === 'percent'" type="success"
>比例佣金</ElTag
>
<ElTag v-else>模板佣金</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="佣金规则" prop="commissionRule" width="150" />
<ElTableColumn label="特殊折扣" prop="discount" width="100">
<template #default="scope">
{{ scope.row.discount > 0 ? `${scope.row.discount}%` : '无' }}
</template>
</ElTableColumn>
<ElTableColumn label="分配时间" prop="assignTime" width="180" />
<ElTableColumn label="操作人" prop="operator" width="100" />
<ElTableColumn fixed="right" label="操作" width="120">
<template #default="scope">
<el-button link type="danger" @click="handleCancelAssign(scope.row)"
>取消分配</el-button
>
</template>
</ElTableColumn>
</template>
</ArtTable>
<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 } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'SimCardAssign' })
interface SimCardProduct {
id: string
productName: string
operator: string
packageSpec: string
price: number
stock: number
assignedCount: number
}
const searchQuery = ref('')
const operatorFilter = ref('')
const statusFilter = ref('')
const assignDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const formRef = ref<FormInstance>()
const selectedRows = ref<SimCardProduct[]>([])
const currentProduct = ref<SimCardProduct>({
id: '',
productName: '',
operator: '',
packageSpec: '',
price: 0,
stock: 0,
assignedCount: 0
})
const assignForm = reactive({
agentId: '',
quantity: 1,
commissionMode: 'percent',
fixedAmount: 0,
percent: 10,
templateId: '',
discount: 0,
remark: ''
})
const assignRules = reactive<FormRules>({
agentId: [{ required: true, message: '请选择代理商', trigger: 'change' }],
quantity: [{ required: true, message: '请输入分配数量', trigger: 'blur' }],
commissionMode: [{ required: true, message: '请选择分佣模式', trigger: 'change' }]
})
const agentList = ref([
{ id: '1', agentName: '华东区总代理', phone: '13800138000', level: 1 },
{ id: '2', agentName: '华南区代理', phone: '13900139000', level: 2 },
{ id: '3', agentName: '华北区代理', phone: '13700137000', level: 1 }
])
const commissionTemplates = ref([
{ id: '1', templateName: '标准代理商佣金', mode: 'percent', value: 10 },
{ id: '2', templateName: '特殊套餐固定佣金', mode: 'fixed', value: 50 },
{ id: '3', templateName: '高端代理商佣金', mode: 'percent', value: 15 }
])
const mockData = ref<SimCardProduct[]>([
{
id: '1',
productName: '移动4G流量卡-月包100GB',
operator: 'CMCC',
packageSpec: '100GB/月有效期1年',
price: 80.0,
stock: 1000,
assignedCount: 500
},
{
id: '2',
productName: '联通5G流量卡-季包300GB',
operator: 'CUCC',
packageSpec: '300GB/季有效期1年',
price: 220.0,
stock: 500,
assignedCount: 200
},
{
id: '3',
productName: '电信物联网卡-年包1TB',
operator: 'CTCC',
packageSpec: '1TB/年有效期2年',
price: 800.0,
stock: 80,
assignedCount: 0
}
])
const assignRecords = ref([
{
id: '1',
agentName: '华东区总代理',
quantity: 200,
commissionMode: 'percent',
commissionRule: '10%',
discount: 5,
assignTime: '2026-01-08 10:00:00',
operator: 'admin'
},
{
id: '2',
agentName: '华南区代理',
quantity: 150,
commissionMode: 'fixed',
commissionRule: '¥5.00/张',
discount: 0,
assignTime: '2026-01-07 14:30:00',
operator: 'admin'
}
])
const filteredData = computed(() => {
let data = mockData.value
if (searchQuery.value) {
data = data.filter((item) => item.productName.includes(searchQuery.value))
}
if (operatorFilter.value) {
data = data.filter((item) => item.operator === operatorFilter.value)
}
if (statusFilter.value) {
if (statusFilter.value === 'assigned') {
data = data.filter((item) => item.assignedCount > 0)
} else if (statusFilter.value === 'unassigned') {
data = data.filter((item) => item.assignedCount === 0)
}
}
return data
})
const getOperatorText = (operator: string) => {
const map: Record<string, string> = {
CMCC: '中国移动',
CUCC: '中国联通',
CTCC: '中国电信'
}
return map[operator] || operator
}
const getOperatorTagType = (operator: string) => {
const map: Record<string, any> = {
CMCC: 'success',
CUCC: 'primary',
CTCC: 'warning'
}
return map[operator] || ''
}
const handleSearch = () => {}
const handleSelectionChange = (rows: SimCardProduct[]) => {
selectedRows.value = rows
}
const showAssignDialog = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要分配的产品')
return
}
if (selectedRows.value.length > 1) {
ElMessage.warning('批量分配功能开发中,请单个选择')
return
}
currentProduct.value = selectedRows.value[0]
assignDialogVisible.value = true
}
const assignToAgent = (row: SimCardProduct) => {
currentProduct.value = row
assignDialogVisible.value = true
}
const handleAgentChange = () => {
// 可以根据代理商自动填充默认佣金设置
}
const handleAssignSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
if (assignForm.quantity > currentProduct.value.stock) {
ElMessage.error('分配数量不能超过库存数量')
return
}
// 更新分配数量
currentProduct.value.assignedCount += assignForm.quantity
currentProduct.value.stock -= assignForm.quantity
assignDialogVisible.value = false
formRef.value.resetFields()
ElMessage.success('分配成功')
}
})
}
const viewAssignDetail = (row: SimCardProduct) => {
currentProduct.value = row
detailDialogVisible.value = true
}
const handleCancelAssign = (row: any) => {
ElMessageBox.confirm('取消分配后,该代理商将无法继续销售此产品,确定取消吗?', '取消分配', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 恢复库存
currentProduct.value.stock += row.quantity
currentProduct.value.assignedCount -= row.quantity
const index = assignRecords.value.findIndex((item) => item.id === row.id)
if (index !== -1) assignRecords.value.splice(index, 1)
ElMessage.success('取消分配成功')
})
}
</script>
<style lang="scss" scoped>
.page-content {
:deep(.el-select-dropdown__item) {
display: flex;
justify-content: space-between;
}
}
</style>

View File

@@ -1,286 +0,0 @@
<template>
<div class="page-content">
<ElRow>
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="searchQuery" placeholder="号卡名称/编码" clearable></ElInput>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="operatorFilter" placeholder="运营商" clearable style="width: 100%">
<ElOption label="中国移动" value="cmcc" />
<ElOption label="中国联通" value="cucc" />
<ElOption label="中国电信" value="ctcc" />
</ElSelect>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
<ElButton v-ripple @click="showDialog('add')">新增号卡</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredData" index>
<template #default>
<ElTableColumn label="号卡名称" prop="cardName" min-width="150" />
<ElTableColumn label="号卡编码" prop="cardCode" />
<ElTableColumn label="运营商" prop="operator">
<template #default="scope">
<ElTag :type="getOperatorTagType(scope.row.operator)">
{{ getOperatorText(scope.row.operator) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="套餐类型" prop="packageType" />
<ElTableColumn label="月租(元)" prop="monthlyFee">
<template #default="scope"> ¥{{ scope.row.monthlyFee.toFixed(2) }} </template>
</ElTableColumn>
<ElTableColumn label="库存" prop="stock" />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status === 'online' ? 'success' : 'info'">
{{ scope.row.status === 'online' ? '上架' : '下架' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="180">
<template #default="scope">
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button
link
:type="scope.row.status === 'online' ? 'danger' : 'primary'"
@click="toggleStatus(scope.row)"
>
{{ scope.row.status === 'online' ? '下架' : '上架' }}
</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">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="号卡名称" prop="cardName">
<ElInput v-model="form.cardName" placeholder="请输入号卡名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="号卡编码" prop="cardCode">
<ElInput v-model="form.cardCode" placeholder="请输入号卡编码" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="运营商" prop="operator">
<ElSelect v-model="form.operator" placeholder="请选择运营商" style="width: 100%">
<ElOption label="中国移动" value="cmcc" />
<ElOption label="中国联通" value="cucc" />
<ElOption label="中国电信" value="ctcc" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="套餐类型" prop="packageType">
<ElInput v-model="form.packageType" placeholder="例如:流量套餐" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="月租(元)" prop="monthlyFee">
<ElInputNumber
v-model="form.monthlyFee"
:min="0"
:precision="2"
style="width: 100%"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="初始库存" prop="stock">
<ElInputNumber v-model="form.stock" :min="0" style="width: 100%" />
</ElFormItem>
</ElCol>
</ElRow>
<ElFormItem label="号卡描述" prop="description">
<ElInput
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入号卡描述"
/>
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" active-value="online" inactive-value="offline" />
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">{{
form.status === 'online' ? '上架' : '下架'
}}</span>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'SimCard' })
interface SimCard {
id?: string
cardName: string
cardCode: string
operator: string
packageType: string
monthlyFee: number
stock: number
description?: string
status: 'online' | 'offline'
createTime?: string
}
const mockData = ref<SimCard[]>([
{
id: '1',
cardName: '移动流量卡30GB',
cardCode: 'CARD_CMCC_30GB',
operator: 'cmcc',
packageType: '流量套餐',
monthlyFee: 29.9,
stock: 1000,
description: '移动30GB流量卡全国通用',
status: 'online',
createTime: '2026-01-01 10:00:00'
},
{
id: '2',
cardName: '联通流量卡50GB',
cardCode: 'CARD_CUCC_50GB',
operator: 'cucc',
packageType: '流量套餐',
monthlyFee: 49.9,
stock: 800,
description: '联通50GB流量卡全国通用',
status: 'online',
createTime: '2026-01-02 11:00:00'
}
])
const searchQuery = ref('')
const operatorFilter = ref('')
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const form = reactive<SimCard>({
cardName: '',
cardCode: '',
operator: '',
packageType: '',
monthlyFee: 0,
stock: 0,
description: '',
status: 'online'
})
const rules = reactive<FormRules>({
cardName: [{ required: true, message: '请输入号卡名称', trigger: 'blur' }],
cardCode: [{ required: true, message: '请输入号卡编码', trigger: 'blur' }],
operator: [{ required: true, message: '请选择运营商', trigger: 'change' }],
monthlyFee: [{ required: true, message: '请输入月租', trigger: 'blur' }]
})
const filteredData = computed(() => {
let data = mockData.value
if (searchQuery.value) {
data = data.filter(
(item) =>
item.cardName.includes(searchQuery.value) || item.cardCode.includes(searchQuery.value)
)
}
if (operatorFilter.value) {
data = data.filter((item) => item.operator === operatorFilter.value)
}
return data
})
const getOperatorText = (operator: string) => {
const map: Record<string, string> = { cmcc: '中国移动', cucc: '中国联通', ctcc: '中国电信' }
return map[operator] || '未知'
}
const getOperatorTagType = (operator: string) => {
const map: Record<string, string> = { cmcc: '', cucc: 'success', ctcc: 'warning' }
return map[operator] || 'info'
}
const handleSearch = () => {}
const showDialog = (type: 'add' | 'edit', row?: SimCard) => {
dialogType.value = type
dialogVisible.value = true
if (type === 'edit' && row) {
Object.assign(form, row)
} else {
Object.assign(form, {
cardName: '',
cardCode: '',
operator: '',
packageType: '',
monthlyFee: 0,
stock: 0,
description: '',
status: 'online'
})
}
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (dialogType.value === 'add') {
mockData.value.push({
...form,
id: Date.now().toString(),
createTime: new Date().toLocaleString('zh-CN')
})
ElMessage.success('新增成功')
} else {
const index = mockData.value.findIndex((item) => item.id === form.id)
if (index !== -1) mockData.value[index] = { ...form }
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
}
})
}
const toggleStatus = (row: SimCard) => {
const action = row.status === 'online' ? '下架' : '上架'
ElMessageBox.confirm(`确定要${action}该号卡吗?`, `${action}确认`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
row.status = row.status === 'online' ? 'offline' : 'online'
ElMessage.success(`${action}成功`)
})
}
</script>

View File

@@ -1,287 +0,0 @@
<template>
<div class="page-content server">
<div class="list">
<div class="middle">
<div class="item" v-for="item in serverList" :key="item.name">
<div class="header">
<span class="name">{{ item.name }}</span>
<span class="ip">{{ item.ip }}</span>
</div>
<div class="box">
<div class="left">
<img src="@imgs/safeguard/server.png" alt="服务器" />
<ElButtonGroup class="ml-4">
<ElButton type="primary" size="default">开机</ElButton>
<ElButton type="danger" size="default">关机</ElButton>
<ElButton type="warning" size="default">重启</ElButton>
</ElButtonGroup>
</div>
<div class="right">
<div>
<p>CPU</p>
<ElProgress :percentage="item.cup" :text-inside="true" :stroke-width="17" />
</div>
<div>
<p>RAM</p>
<ElProgress
:percentage="item.memory"
status="success"
:text-inside="true"
:stroke-width="17"
/>
</div>
<div>
<p>SWAP</p>
<ElProgress
:percentage="item.swap"
status="warning"
:text-inside="true"
:stroke-width="17"
/>
</div>
<div>
<p>DISK</p>
<ElProgress
:percentage="item.disk"
status="success"
:text-inside="true"
:stroke-width="17"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted, onUnmounted } from 'vue'
defineOptions({ name: 'SafeguardServer' })
interface ServerInfo {
name: string
ip: string
cup: number
memory: number
swap: number
disk: number
}
const serverList = reactive<ServerInfo[]>([
{
name: '开发服务器',
ip: '192.168.1.100',
cup: 85,
memory: 65,
swap: 45,
disk: 92
},
{
name: '测试服务器',
ip: '192.168.1.101',
cup: 32,
memory: 78,
swap: 90,
disk: 45
},
{
name: '预发布服务器',
ip: '192.168.1.102',
cup: 95,
memory: 42,
swap: 67,
disk: 88
},
{
name: '线上服务器',
ip: '192.168.1.103',
cup: 58,
memory: 93,
swap: 25,
disk: 73
}
])
// 生成随机数据的函数
function generateRandomValue(min = 0, max = 100): number {
return Math.floor(Math.random() * (max - min + 1)) + min
}
// 更新服务器数据
function updateServerData() {
serverList.forEach((server) => {
server.cup = generateRandomValue()
server.memory = generateRandomValue()
server.swap = generateRandomValue()
server.disk = generateRandomValue()
})
}
// 修改 timer 类型为 number | null
let timer: number | null = null
onMounted(() => {
timer = window.setInterval(updateServerData, 3000)
})
onUnmounted(() => {
if (timer !== null) {
window.clearInterval(timer)
timer = null
}
})
</script>
<style lang="scss" scoped>
.server {
.list {
width: 100%;
.middle {
display: flex;
flex-wrap: wrap;
width: calc(100% + 20px);
.item {
box-sizing: border-box;
width: calc(50% - 20px);
margin: 0 20px 20px 0;
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
.name {
font-size: 15px;
font-weight: 500;
}
.ip {
font-size: 14px;
color: var(--el-text-color-secondary);
}
}
.box {
display: flex;
align-items: center;
padding: 40px;
.left {
margin: 0 40px;
img {
display: block;
width: 190px;
}
.el-button-group {
display: flex;
justify-content: center;
margin-top: -10px;
}
}
.right {
flex: 1;
margin-top: 5px;
> div {
margin: 15px 0;
p {
margin-bottom: 4px;
font-size: 14px;
}
}
}
}
}
}
}
}
@media (max-width: $device-notebook) {
.server {
.list {
.middle {
.item {
.header {
padding: 10px 20px;
}
.box {
padding: 20px;
.left {
margin: 0 20px 0 0;
}
.right {
margin-top: 0;
}
}
}
}
}
}
}
@media (max-width: $device-ipad-pro) {
.server {
.list {
.middle {
.item {
width: 100%;
}
}
}
}
}
@media (max-width: $device-phone) {
.server {
.list {
.middle {
.item {
width: 100%;
.header {
padding: 10px 20px;
}
.box {
display: block;
padding: 20px;
.left {
margin: 0;
img {
width: 150px;
margin: 0 auto;
}
.el-button-group {
margin-top: 10px;
}
}
.right {
margin-top: 30px;
}
}
}
}
}
}
}
</style>

View File

@@ -1,306 +0,0 @@
<template>
<div class="page-content">
<ElRow>
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="searchQuery" placeholder="模板名称" clearable></ElInput>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
<ElButton v-ripple @click="showDialog('add')">新增模板</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredData" index>
<template #default>
<ElTableColumn label="模板名称" prop="templateName" />
<ElTableColumn label="分佣模式" prop="commissionMode">
<template #default="scope">
<ElTag :type="scope.row.commissionMode === 'fixed' ? '' : 'success'">
{{ scope.row.commissionMode === 'fixed' ? '固定佣金' : '比例佣金' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="佣金规则" prop="rule">
<template #default="scope">
{{
scope.row.commissionMode === 'fixed'
? `¥${scope.row.fixedAmount.toFixed(2)}/笔`
: `${scope.row.percent}%`
}}
</template>
</ElTableColumn>
<ElTableColumn label="适用范围" prop="scope" show-overflow-tooltip />
<ElTableColumn label="应用次数" prop="usageCount" />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="220">
<template #default="scope">
<el-button link @click="viewUsage(scope.row)">应用记录</el-button>
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增分佣模板' : '编辑分佣模板'"
width="600px"
align-center
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="模板名称" prop="templateName">
<ElInput v-model="form.templateName" placeholder="请输入模板名称" />
</ElFormItem>
<ElFormItem label="分佣模式" prop="commissionMode">
<ElRadioGroup v-model="form.commissionMode">
<ElRadio value="fixed">固定佣金</ElRadio>
<ElRadio value="percent">比例佣金</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem v-if="form.commissionMode === 'fixed'" label="固定金额" prop="fixedAmount">
<ElInputNumber v-model="form.fixedAmount" :min="0" :precision="2" style="width: 100%" />
<span style="margin-left: 8px">/</span>
</ElFormItem>
<ElFormItem v-if="form.commissionMode === 'percent'" label="佣金比例" prop="percent">
<ElInputNumber
v-model="form.percent"
:min="0"
:max="100"
:precision="2"
style="width: 100%"
/>
<span style="margin-left: 8px">%</span>
</ElFormItem>
<ElFormItem label="适用范围" prop="scope">
<ElInput v-model="form.scope" placeholder="例如:全部套餐、特定代理商等" />
</ElFormItem>
<ElFormItem label="分佣说明" prop="description">
<ElInput
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入分佣规则说明"
/>
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<!-- 应用记录对话框 -->
<ElDialog v-model="usageDialogVisible" title="模板应用记录" width="900px" align-center>
<ArtTable :data="usageRecords" index>
<template #default>
<ElTableColumn label="应用对象" prop="targetName" />
<ElTableColumn label="对象类型" prop="targetType">
<template #default="scope">
<ElTag :type="scope.row.targetType === 'agent' ? '' : 'success'">
{{ scope.row.targetType === 'agent' ? '代理商' : '套餐' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="应用时间" prop="applyTime" width="180" />
<ElTableColumn label="操作人" prop="operator" />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '生效中' : '已失效' }}
</ElTag>
</template>
</ElTableColumn>
</template>
</ArtTable>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'CommissionTemplate' })
interface Template {
id?: string
templateName: string
commissionMode: 'fixed' | 'percent'
fixedAmount: number
percent: number
scope: string
description?: string
usageCount?: number
status: 'active' | 'inactive'
createTime?: string
}
const mockData = ref<Template[]>([
{
id: '1',
templateName: '标准代理商佣金',
commissionMode: 'percent',
fixedAmount: 0,
percent: 10,
scope: '全部套餐',
description: '适用于一级代理商的标准佣金模板',
usageCount: 25,
status: 'active',
createTime: '2026-01-01 10:00:00'
},
{
id: '2',
templateName: '特殊套餐固定佣金',
commissionMode: 'fixed',
fixedAmount: 50,
percent: 0,
scope: '高端套餐系列',
description: '适用于高端套餐的固定佣金',
usageCount: 8,
status: 'active',
createTime: '2026-01-05 11:00:00'
}
])
const usageRecords = ref([
{
id: '1',
targetName: '华东区总代理',
targetType: 'agent',
applyTime: '2026-01-02 10:00:00',
operator: 'admin',
status: 'active'
}
])
const searchQuery = ref('')
const dialogVisible = ref(false)
const usageDialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const form = reactive<Template>({
templateName: '',
commissionMode: 'percent',
fixedAmount: 0,
percent: 0,
scope: '',
description: '',
status: 'active'
})
const rules = reactive<FormRules>({
templateName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
commissionMode: [{ required: true, message: '请选择分佣模式', trigger: 'change' }],
fixedAmount: [
{
validator: (rule, value, callback) => {
if (form.commissionMode === 'fixed' && value <= 0) {
callback(new Error('固定金额必须大于0'))
} else {
callback()
}
},
trigger: 'blur'
}
],
percent: [
{
validator: (rule, value, callback) => {
if (form.commissionMode === 'percent' && (value <= 0 || value > 100)) {
callback(new Error('比例必须在0-100之间'))
} else {
callback()
}
},
trigger: 'blur'
}
],
scope: [{ required: true, message: '请输入适用范围', trigger: 'blur' }]
})
const filteredData = computed(() => {
if (!searchQuery.value) return mockData.value
return mockData.value.filter((item) => item.templateName.includes(searchQuery.value))
})
const handleSearch = () => {}
const showDialog = (type: 'add' | 'edit', row?: Template) => {
dialogType.value = type
dialogVisible.value = true
if (type === 'edit' && row) {
Object.assign(form, row)
} else {
Object.assign(form, {
templateName: '',
commissionMode: 'percent',
fixedAmount: 0,
percent: 0,
scope: '',
description: '',
status: 'active'
})
}
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (dialogType.value === 'add') {
mockData.value.push({
...form,
id: Date.now().toString(),
usageCount: 0,
createTime: new Date().toLocaleString('zh-CN')
})
ElMessage.success('新增成功')
} else {
const index = mockData.value.findIndex((item) => item.id === form.id)
if (index !== -1) mockData.value[index] = { ...form }
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
}
})
}
const handleDelete = (row: Template) => {
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 viewUsage = (row: Template) => {
usageDialogVisible.value = true
}
</script>

View File

@@ -1,306 +0,0 @@
<template>
<div class="page-content">
<!-- API密钥管理 -->
<ElCard shadow="never">
<template #header>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-weight: 500">API密钥管理</span>
<ElButton type="primary" size="small" @click="showCreateDialog">生成新密钥</ElButton>
</div>
</template>
<ArtTable :data="apiKeyList" index>
<template #default>
<ElTableColumn label="密钥名称" prop="keyName" />
<ElTableColumn label="AppKey" prop="appKey" min-width="200">
<template #default="scope">
<div style="display: flex; gap: 8px; align-items: center">
<code style="color: var(--el-color-primary)">{{ scope.row.appKey }}</code>
<ElButton link :icon="CopyDocument" @click="copyToClipboard(scope.row.appKey)" />
</div>
</template>
</ElTableColumn>
<ElTableColumn label="AppSecret" prop="appSecret" min-width="200">
<template #default="scope">
<div style="display: flex; gap: 8px; align-items: center">
<code>{{ scope.row.showSecret ? scope.row.appSecret : '••••••••••••••••' }}</code>
<ElButton link :icon="View" @click="scope.row.showSecret = !scope.row.showSecret" />
<ElButton link :icon="CopyDocument" @click="copyToClipboard(scope.row.appSecret)" />
</div>
</template>
</ElTableColumn>
<ElTableColumn label="权限" prop="permissions">
<template #default="scope">
<ElTag
v-for="(perm, index) in scope.row.permissions"
:key="index"
size="small"
style="margin-right: 4px"
>
{{ perm }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
<ElTableColumn fixed="right" label="操作" width="180">
<template #default="scope">
<el-button link @click="handleResetKey(scope.row)">重置密钥</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
</ElCard>
<!-- Webhook配置 -->
<ElCard shadow="never" style="margin-top: 20px">
<template #header>
<span style="font-weight: 500">Webhook配置</span>
</template>
<ElForm :model="webhookForm" label-width="120px" style="max-width: 800px">
<ElFormItem label="回调地址">
<ElInput v-model="webhookForm.url" placeholder="https://your-domain.com/webhook">
<template #prepend>POST</template>
</ElInput>
</ElFormItem>
<ElFormItem label="签名密钥">
<ElInput
v-model="webhookForm.secret"
:type="showWebhookSecret ? 'text' : 'password'"
placeholder="用于验证webhook请求签名"
>
<template #append>
<ElButton :icon="View" @click="showWebhookSecret = !showWebhookSecret" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem label="事件订阅">
<ElCheckboxGroup v-model="webhookForm.events">
<ElCheckbox value="order.created">订单创建</ElCheckbox>
<ElCheckbox value="order.paid">订单支付</ElCheckbox>
<ElCheckbox value="card.activated">卡片激活</ElCheckbox>
<ElCheckbox value="card.expired">卡片过期</ElCheckbox>
<ElCheckbox value="recharge.success">充值成功</ElCheckbox>
</ElCheckboxGroup>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="saveWebhook">保存配置</ElButton>
<ElButton @click="testWebhook">测试推送</ElButton>
</ElFormItem>
</ElForm>
</ElCard>
<!-- API调用统计 -->
<ElCard shadow="never" style="margin-top: 20px">
<template #header>
<span style="font-weight: 500">API调用统计最近7天</span>
</template>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :lg="6">
<div class="stat-box">
<div class="stat-label">总调用次数</div>
<div class="stat-value">12,580</div>
</div>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<div class="stat-box">
<div class="stat-label">成功次数</div>
<div class="stat-value" style="color: var(--el-color-success)">12,453</div>
</div>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<div class="stat-box">
<div class="stat-label">失败次数</div>
<div class="stat-value" style="color: var(--el-color-danger)">127</div>
</div>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<div class="stat-box">
<div class="stat-label">成功率</div>
<div class="stat-value">99.0%</div>
</div>
</ElCol>
</ElRow>
</ElCard>
<!-- 生成密钥对话框 -->
<ElDialog v-model="createDialogVisible" title="生成新密钥" width="500px" align-center>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
<ElFormItem label="密钥名称" prop="keyName">
<ElInput v-model="createForm.keyName" placeholder="请输入密钥名称" />
</ElFormItem>
<ElFormItem label="权限设置">
<ElCheckboxGroup v-model="createForm.permissions">
<ElCheckbox value="读取">读取</ElCheckbox>
<ElCheckbox value="写入">写入</ElCheckbox>
<ElCheckbox value="删除">删除</ElCheckbox>
</ElCheckboxGroup>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="createDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleCreateKey">生成</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { CopyDocument, View } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'DeveloperApi' })
interface ApiKey {
id: string
keyName: string
appKey: string
appSecret: string
permissions: string[]
status: 'active' | 'inactive'
createTime: string
showSecret?: boolean
}
const apiKeyList = ref<ApiKey[]>([
{
id: '1',
keyName: '生产环境密钥',
appKey: 'ak_prod_1234567890abcdef',
appSecret: 'sk_prod_abcdefghijklmnopqrstuvwxyz123456',
permissions: ['读取', '写入'],
status: 'active',
createTime: '2026-01-01 10:00:00',
showSecret: false
}
])
const webhookForm = reactive({
url: 'https://your-domain.com/webhook',
secret: 'webhook_secret_key_123456',
events: ['order.created', 'order.paid']
})
const showWebhookSecret = ref(false)
const createDialogVisible = ref(false)
const createFormRef = ref<FormInstance>()
const createForm = reactive({
keyName: '',
permissions: ['读取']
})
const createRules = reactive<FormRules>({
keyName: [{ required: true, message: '请输入密钥名称', trigger: 'blur' }]
})
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
ElMessage.success('已复制到剪贴板')
})
}
const showCreateDialog = () => {
createForm.keyName = ''
createForm.permissions = ['读取']
createDialogVisible.value = true
}
const handleCreateKey = async () => {
if (!createFormRef.value) return
await createFormRef.value.validate((valid) => {
if (valid) {
const newKey: ApiKey = {
id: Date.now().toString(),
keyName: createForm.keyName,
appKey: `ak_${Date.now()}`,
appSecret: `sk_${Math.random().toString(36).substring(2)}`,
permissions: createForm.permissions,
status: 'active',
createTime: new Date().toLocaleString('zh-CN'),
showSecret: false
}
apiKeyList.value.push(newKey)
createDialogVisible.value = false
ElMessage.success('密钥生成成功,请妥善保管')
}
})
}
const handleResetKey = (row: ApiKey) => {
ElMessageBox.confirm('重置后原密钥将失效,确定要重置吗?', '重置密钥', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
row.appSecret = `sk_${Math.random().toString(36).substring(2)}`
ElMessage.success('密钥重置成功')
})
}
const handleDelete = (row: ApiKey) => {
ElMessageBox.confirm('删除后无法恢复,确定要删除吗?', '删除密钥', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(() => {
const index = apiKeyList.value.findIndex((item) => item.id === row.id)
if (index !== -1) apiKeyList.value.splice(index, 1)
ElMessage.success('删除成功')
})
}
const saveWebhook = () => {
ElMessage.success('Webhook配置保存成功')
}
const testWebhook = () => {
ElMessage.info('正在发送测试推送...')
setTimeout(() => {
ElMessage.success('测试推送成功')
}, 1500)
}
</script>
<style lang="scss" scoped>
.page-content {
.stat-box {
padding: 20px;
text-align: center;
background: var(--el-bg-color);
border-radius: 8px;
.stat-label {
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
:deep(.el-checkbox) {
margin-right: 20px;
margin-bottom: 10px;
}
}
</style>

View File

@@ -1,216 +0,0 @@
<template>
<div class="page-content">
<ElCard shadow="never">
<template #header>
<span style="font-weight: 500">支付商户配置</span>
</template>
<ElForm
ref="formRef"
:model="form"
:rules="rules"
label-width="150px"
style="max-width: 900px"
>
<ElDivider content-position="left">基础信息</ElDivider>
<ElFormItem label="商户名称" prop="merchantName">
<ElInput v-model="form.merchantName" placeholder="请输入商户名称" />
</ElFormItem>
<ElFormItem label="商户编号" prop="merchantId">
<ElInput v-model="form.merchantId" placeholder="请输入商户编号" />
</ElFormItem>
<ElDivider content-position="left">API配置</ElDivider>
<ElFormItem label="AppID" prop="appId">
<ElInput v-model="form.appId" placeholder="请输入AppID">
<template #append>
<ElButton :icon="View" @click="toggleShow('appId')">
{{ showFields.appId ? '隐藏' : '显示' }}
</ElButton>
</template>
</ElInput>
</ElFormItem>
<ElFormItem label="AppSecret" prop="appSecret">
<ElInput
v-model="form.appSecret"
:type="showFields.appSecret ? 'text' : 'password'"
placeholder="请输入AppSecret"
>
<template #append>
<ElButton :icon="View" @click="toggleShow('appSecret')">
{{ showFields.appSecret ? '隐藏' : '显示' }}
</ElButton>
</template>
</ElInput>
</ElFormItem>
<ElFormItem label="API密钥" prop="apiKey">
<ElInput
v-model="form.apiKey"
:type="showFields.apiKey ? 'text' : 'password'"
placeholder="请输入API密钥"
>
<template #append>
<ElButton :icon="View" @click="toggleShow('apiKey')">
{{ showFields.apiKey ? '隐藏' : '显示' }}
</ElButton>
</template>
</ElInput>
</ElFormItem>
<ElDivider content-position="left">回调配置</ElDivider>
<ElFormItem label="支付回调地址" prop="notifyUrl">
<ElInput v-model="form.notifyUrl" placeholder="https://your-domain.com/api/notify">
<template #prepend>POST</template>
</ElInput>
</ElFormItem>
<ElFormItem label="退款回调地址" prop="refundNotifyUrl">
<ElInput
v-model="form.refundNotifyUrl"
placeholder="https://your-domain.com/api/refund-notify"
>
<template #prepend>POST</template>
</ElInput>
</ElFormItem>
<ElDivider content-position="left">支付方式</ElDivider>
<ElFormItem label="启用的支付方式">
<ElCheckboxGroup v-model="form.paymentMethods">
<ElCheckbox value="wechat">
<div style="display: flex; gap: 8px; align-items: center">
<span style="font-size: 20px; color: #09bb07">💬</span>
<span>微信支付</span>
</div>
</ElCheckbox>
<ElCheckbox value="alipay">
<div style="display: flex; gap: 8px; align-items: center">
<span style="font-size: 20px; color: #1677ff">💳</span>
<span>支付宝</span>
</div>
</ElCheckbox>
<ElCheckbox value="bank">
<div style="display: flex; gap: 8px; align-items: center">
<span style="font-size: 20px">🏦</span>
<span>银行卡</span>
</div>
</ElCheckbox>
</ElCheckboxGroup>
</ElFormItem>
<ElFormItem label="测试模式">
<ElSwitch v-model="form.testMode" />
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
{{ form.testMode ? '开启(使用沙箱环境)' : '关闭(生产环境)' }}
</span>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="handleSave">保存配置</ElButton>
<ElButton @click="handleTest">测试连接</ElButton>
<ElButton @click="resetForm">重置</ElButton>
</ElFormItem>
</ElForm>
</ElCard>
<ElCard shadow="never" style="margin-top: 20px">
<template #header>
<span style="font-weight: 500">配置说明</span>
</template>
<ElAlert type="info" :closable="false">
<template #title>
<div style="line-height: 1.8">
<p><strong>AppID/AppSecret</strong>: 从支付服务商后台获取</p>
<p><strong>API密钥</strong>: 用于签名验证请妥善保管</p>
<p><strong>回调地址</strong>: 支付完成后支付平台会向该地址发送支付结果通知</p>
<p><strong>测试模式</strong>: 开启后使用沙箱环境不会产生真实交易</p>
</div>
</template>
</ElAlert>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { View } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'PaymentMerchant' })
const formRef = ref<FormInstance>()
const form = reactive({
merchantName: '某某科技有限公司',
merchantId: 'MCH123456789',
appId: 'wx1234567890abcdef',
appSecret: '********************************',
apiKey: '********************************',
notifyUrl: 'https://your-domain.com/api/payment/notify',
refundNotifyUrl: 'https://your-domain.com/api/payment/refund-notify',
paymentMethods: ['wechat', 'alipay'],
testMode: true
})
const showFields = reactive({
appId: false,
appSecret: false,
apiKey: false
})
const rules = reactive<FormRules>({
merchantName: [{ required: true, message: '请输入商户名称', trigger: 'blur' }],
merchantId: [{ required: true, message: '请输入商户编号', trigger: 'blur' }],
appId: [{ required: true, message: '请输入AppID', trigger: 'blur' }],
appSecret: [{ required: true, message: '请输入AppSecret', trigger: 'blur' }],
apiKey: [{ required: true, message: '请输入API密钥', trigger: 'blur' }],
notifyUrl: [
{ required: true, message: '请输入支付回调地址', trigger: 'blur' },
{ type: 'url', message: '请输入正确的URL地址', trigger: 'blur' }
]
})
const toggleShow = (field: 'appId' | 'appSecret' | 'apiKey') => {
showFields[field] = !showFields[field]
}
const handleSave = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
ElMessage.success('配置保存成功')
}
})
}
const handleTest = () => {
ElMessage.info('正在测试连接...')
setTimeout(() => {
ElMessage.success('连接测试成功')
}, 1500)
}
const resetForm = () => {
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
.page-content {
:deep(.el-checkbox) {
margin-right: 30px;
margin-bottom: 12px;
}
:deep(.el-divider__text) {
font-weight: 500;
color: var(--el-text-color-primary);
}
}
</style>

View File

@@ -51,6 +51,7 @@
const getProviderTypeText = (type: PaymentProviderType): string => {
const typeMap: Record<PaymentProviderType, string> = {
wechat: '微信直连',
wechat_v2: '微信直连V2',
fuiou: '富友支付'
}
return typeMap[type] || type

View File

@@ -19,7 +19,7 @@
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showCreateDialog">新增支付配置</ElButton>
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('wechat_config:create')">新增支付配置</ElButton>
</template>
</ArtTableHeader>
@@ -33,29 +33,16 @@
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
:actionsWidth="120"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 支付配置操作右键菜单 -->
<ArtMenuRight
ref="configOperationMenuRef"
:menu-items="configOperationMenuItems"
:menu-width="140"
@select="handleConfigOperationMenuSelect"
/>
<!-- 创建/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -436,7 +423,7 @@
import { h, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { WechatConfigService } from '@/api/modules'
import { ElMessage, ElTag, ElMessageBox, ElSwitch, ElButton } from 'element-plus'
import { ElMessage, ElMessageBox, ElSwitch, ElButton } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
WechatConfig,
@@ -448,10 +435,6 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
@@ -467,10 +450,6 @@
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
// 右键菜单
const configOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingConfig = ref<WechatConfig | null>(null)
// 搜索表单初始值
const initialSearchState = {
provider_type: undefined as PaymentProviderType | undefined,
@@ -533,7 +512,7 @@
]
// URL格式验证规则
const urlValidator = (rule: any, value: any, callback: any) => {
const urlValidator = (_rule: any, value: any, callback: any) => {
if (!value) {
callback()
return
@@ -692,6 +671,7 @@
activeText: '已激活',
inactiveText: '未激活',
inlinePrompt: true,
disabled: !hasAuth('wechat_config:status'),
'onUpdate:modelValue': (val) => handleStatusChange(row, val)
})
}
@@ -848,7 +828,7 @@
})
dialogType.value = 'edit'
dialogVisible.value = true
nextTick(() => {
await nextTick(() => {
formRef.value?.clearValidate()
})
}
@@ -956,6 +936,11 @@
// 激活/停用状态切换
const handleStatusChange = async (row: WechatConfig, newStatus: string | number | boolean) => {
if (!hasAuth('wechat_config:status')) {
ElMessage.warning('您没有修改激活状态的权限')
return
}
const newBoolStatus = Boolean(newStatus)
const oldStatus = row.is_active
row.is_active = newBoolStatus
@@ -1002,63 +987,35 @@
// 处理名称点击
const handleNameClick = (row: WechatConfig) => {
handleViewDetail(row)
}
// 支付配置操作菜单项配置
const configOperationMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
// 编辑
items.push({
key: 'edit',
label: '编辑'
})
// 删除
items.push({
key: 'delete',
label: '删除'
})
return items
})
// 显示支付配置操作右键菜单
const showConfigOperationMenu = (e: MouseEvent, row: WechatConfig) => {
e.preventDefault()
e.stopPropagation()
currentOperatingConfig.value = row
configOperationMenuRef.value?.show(e)
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: WechatConfig, column: any, event: MouseEvent) => {
showConfigOperationMenu(event, row)
}
// 处理支付配置操作菜单选择
const handleConfigOperationMenuSelect = (item: MenuItemType) => {
if (!currentOperatingConfig.value) return
switch (item.key) {
case 'edit':
showEditDialog(currentOperatingConfig.value)
break
case 'delete':
handleDelete(currentOperatingConfig.value)
break
if (hasAuth('wechat_config:detail')) {
handleViewDetail(row)
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 获取操作按钮
const getActions = (row: WechatConfig) => {
const actions: any[] = []
if (hasAuth('wechat_config:edit')) {
actions.push({
label: '编辑',
handler: () => showEditDialog(row),
type: 'primary'
})
}
if (hasAuth('wechat_config:delete')) {
actions.push({
label: '删除',
handler: () => handleDelete(row),
type: 'danger'
})
}
return actions
}
</script>
<style scoped lang="scss">
@@ -1066,10 +1023,6 @@
height: 100%;
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.config-form {
max-height: 600px;
padding-right: 10px;

View File

@@ -1,142 +0,0 @@
<template>
<div class="banners">
<h1 class="page-title">基础横幅</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="12">
<ArtBasicBanner
title="欢迎回来,管理员!"
subtitle="今日系统访问量增长了23%,运行状态良好。"
buttonText="查看详情"
buttonColor="rgb(var(--art-secondary))"
buttonTextColor="#fff"
@click="handleBannerClick"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12">
<ArtBasicBanner
title="欢迎使用本系统!"
subtitle="这是一个基于Vue3和Element Plus的后台管理系统模板。"
buttonText="开始使用"
buttonColor="rgb(var(--art-success))"
buttonTextColor="#fff"
backgroundColor="#D4F1F7"
titleColor="#333"
subtitleColor="#666"
@click="handleBannerClick"
/>
</ElCol>
</ElRow>
<h1 class="page-title">基础横幅图片</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="12">
<ArtBasicBanner
title="欢迎回来,管理员!"
subtitle="今日系统访问量增长了23%,运行状态良好。"
buttonText="查看详情"
buttonColor="#FF3E76"
backgroundColor="#FF80A4"
buttonTextColor="#fff"
:backgroundImage="icon3"
@click="handleBannerClick"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12">
<ArtBasicBanner
title="欢迎使用本系统!"
subtitle="这是一个基于Vue3和Element Plus的后台管理系统模板。"
buttonText="开始使用"
buttonColor="#1385FF"
buttonTextColor="#fff"
backgroundColor="#88A7FF"
:backgroundImage="icon5"
@click="handleBannerClick"
/>
</ElCol>
</ElRow>
<h1 class="page-title">卡片横幅</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
<ArtCardBanner
title="系统状态良好"
description="所有服务运行正常,无异常情况。"
buttonText="查看详情"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
<ArtCardBanner
:icon="icon2"
title="新消息提醒"
description="您有3条未读消息需要处理。"
buttonText="立即查看"
buttonColor="rgb(var(--art-warning))"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
<ArtCardBanner
:icon="icon3"
title="数据分析报告"
description="本周数据分析报告已生成完毕。"
buttonText="下载报告"
buttonColor="rgb(var(--art-error))"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
<ArtCardBanner
:icon="icon4"
title="系统更新提示"
description="新版本V2.1.0已发布,建议及时更新。"
buttonText="更新"
buttonColor="rgb(var(--art-primary))"
:showCancel="true"
cancelButtonText="取消"
cancelButtonColor="#eee"
cancelButtonTextColor="#333"
@click="handleConfirm"
@cancel="handleCancel"
/>
</ElCol>
</ElRow>
</div>
</template>
<script setup lang="ts">
import icon2 from '@imgs/3d/icon2.webp'
import icon3 from '@imgs/3d/icon3.webp'
import icon4 from '@imgs/3d/icon4.webp'
import icon5 from '@imgs/3d/icon7.webp'
import { ElRow } from 'element-plus'
const handleBannerClick = () => {
console.log('banner clicked')
}
const handleConfirm = () => {
console.log('confirm clicked')
}
const handleCancel = () => {
console.log('cancel clicked')
}
</script>
<style lang="scss" scoped>
.banners {
padding-top: 20px;
.page-title {
margin: 20px 0 15px;
font-size: 22px;
font-weight: 500;
&:first-child {
margin-top: 0;
}
}
.el-col {
margin-bottom: 20px;
}
}
</style>

View File

@@ -1,442 +0,0 @@
<template>
<div class="cards">
<h1 class="page-title">统计卡片文字</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="6" v-for="card in statsCards" :key="card.id">
<ArtStatsCard
:icon="card.icon"
:title="card.title"
:description="card.description"
:iconSize="card.iconSize"
iconColor="#fff"
:iconBgColor="card.iconBgColor"
:showArrow="card.showArrow"
/>
</ElCol>
</ElRow>
<h1 class="page-title">统计卡片数字滚动</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="6" v-for="card in statsCards" :key="card.id">
<ArtStatsCard
:icon="card.icon"
:count="card.count"
:description="card.description"
:iconSize="card.iconSize"
iconColor="#fff"
:iconBgColor="card.iconBgColor"
:showArrow="card.showArrow"
/>
</ElCol>
</ElRow>
<h1 class="page-title">统计卡片自定义样式</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="6" v-for="card in statsCards" :key="card.id">
<ArtStatsCard
:icon="card.icon"
:title="card.title"
:description="card.description"
:iconColor="card.iconColor"
:textColor="card.textColor"
:backgroundColor="card.backgroundColor"
:showArrow="card.showArrow"
/>
</ElCol>
</ElRow>
<h1 class="page-title">进度卡片</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="6" v-for="card in progressCards" :key="card.id">
<ArtProgressCard :percentage="card.percentage" :title="card.title" :color="card.color" />
</ElCol>
</ElRow>
<h1 class="page-title">进度卡片icon</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="6" v-for="card in progressCards" :key="card.id">
<ArtProgressCard
:percentage="card.percentage"
:title="card.title"
:color="card.color"
:icon="card.icon"
:iconColor="card.iconColor"
:iconBgColor="card.iconBgColor"
:iconSize="card.iconSize"
/>
</ElCol>
</ElRow>
<h1 class="page-title">图表卡片小图表</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="6">
<ArtLineChartCard
:isMiniChart="true"
:value="2545"
label="新用户"
date="过去7天"
:percentage="1.2"
:height="9.5"
:chartData="[120, 132, 101, 134, 90, 230, 210]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6">
<ArtBarChartCard
:isMiniChart="true"
:value="15480"
label="浏览量"
date="过去 14 天"
:percentage="-4.15"
:height="9.5"
:chartData="[120, 100, 150, 140, 90, 120, 130]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6">
<ArtLineChartCard
:isMiniChart="true"
:value="2545"
label="粉丝数"
date="过去 30 天"
:percentage="1.2"
:height="9.5"
:showAreaColor="true"
:chartData="[150, 180, 160, 200, 180, 220, 240]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6">
<ArtDonutChartCard
:value="36358"
title="粉丝量"
:percentage="18"
:data="[50, 40]"
:height="9.5"
currentYear="2022"
previousYear="2021"
:radius="['50%', '70%']"
/>
</ElCol>
</ElRow>
<h1 class="page-title">图表卡片大图表</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="6">
<ArtLineChartCard
:value="2545"
label="新用户"
:percentage="1.2"
:height="11"
:chartData="[120, 132, 101, 134, 90, 230, 210]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6">
<ArtBarChartCard
:value="15480"
label="浏览量"
:percentage="-4.15"
:height="11"
:chartData="[120, 100, 150, 140, 90, 120, 130, 110]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6">
<ArtLineChartCard
:value="2545"
label="粉丝数"
:percentage="1.2"
:height="11"
:showAreaColor="true"
:chartData="[150, 180, 160, 200, 180, 220, 240]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6">
<ArtDonutChartCard
:value="36358"
title="粉丝量"
:percentage="18"
:data="[70, 30]"
:height="11"
currentYear="2022"
previousYear="2021"
/>
</ElCol>
</ElRow>
<h1 class="page-title">数据列表卡片</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :lg="8">
<ArtDataListCard :list="dataList" title="待办事项" subtitle="今日待处理任务" />
</ElCol>
<ElCol :xs="24" :sm="12" :lg="8">
<ArtDataListCard
:maxCount="4"
:list="dataList"
title="最近活动"
subtitle="近期活动列表"
:showMoreButton="true"
@more="handleMore"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="8">
<ArtTimelineListCard :list="timelineData" title="最近交易" subtitle="2024年12月20日" />
</ElCol>
</ElRow>
<h1 class="page-title">图片卡片</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="6" v-for="card in imageCards" :key="card.id">
<ArtImageCard
:imageUrl="card.imageUrl"
:title="card.title"
:category="card.category"
:readTime="card.readTime"
:views="card.views"
:comments="card.comments"
:date="card.date"
/>
</ElCol>
</ElRow>
</div>
</template>
<script setup lang="ts">
import cover1 from '@imgs/cover/img1.webp'
import cover2 from '@imgs/cover/img2.webp'
import cover3 from '@imgs/cover/img3.webp'
import cover4 from '@imgs/cover/img4.webp'
const statsCards = [
{
id: 1,
title: '销售产品',
count: 1235,
description: '鞋子、牛仔裤、派对服装、手表',
icon: '&#xe812;',
iconColor: 'rgb(var(--art-primary))',
iconSize: 20,
iconBgColor: 'rgb(var(--art-info))',
textColor: 'rgb(var(--art-primary))',
backgroundColor: 'rgb(var(--art-bg-primary))',
showArrow: false
},
{
id: 2,
title: '活跃用户',
count: 5000,
description: '日活跃用户超过5,000+',
icon: '&#xe724;',
iconColor: 'rgb(var(--art-warning))',
iconSize: 20,
iconBgColor: 'rgb(var(--art-success))',
textColor: 'rgb(var(--art-warning))',
backgroundColor: 'rgb(var(--art-bg-warning))',
showArrow: false
},
{
id: 3,
title: '总收入',
count: 35000,
description: '月收入超过¥350,000+',
icon: '&#xe70e;',
iconColor: 'rgb(var(--art-secondary))',
iconSize: 20,
iconBgColor: 'rgb(var(--art-secondary))',
textColor: 'rgb(var(--art-secondary))',
backgroundColor: 'rgb(var(--art-bg-secondary))',
showArrow: false
},
{
id: 4,
title: '客户评价',
count: 4800,
description: '平均评分4.8/5',
icon: '&#xe82d;',
iconColor: 'rgb(var(--art-error))',
iconSize: 20,
iconBgColor: 'rgb(var(--art-error))',
textColor: 'rgb(var(--art-error))',
backgroundColor: 'rgb(var(--art-bg-error))',
showArrow: false
}
]
const progressCards = [
{
id: 1,
title: '完成进度',
percentage: 75,
color: 'rgb(var(--art-success))',
icon: '&#xe812;',
iconColor: 'rgb(var(--art-success))',
iconBgColor: 'rgb(var(--art-bg-success))',
iconSize: 20
},
{
id: 2,
title: '项目进度',
percentage: 65,
color: 'rgb(var(--art-primary))',
icon: '&#xe724;',
iconColor: 'rgb(var(--art-primary))',
iconBgColor: 'rgb(var(--art-bg-primary))',
iconSize: 20
},
{
id: 3,
title: '学习进度',
percentage: 45,
color: 'rgb(var(--art-error))',
icon: '&#xe724;',
iconColor: 'rgb(var(--art-error))',
iconBgColor: 'rgb(var(--art-bg-error))',
iconSize: 20
},
{
id: 4,
title: '任务进度',
percentage: 90,
color: 'rgb(var(--art-secondary))',
icon: '&#xe724;',
iconColor: 'rgb(var(--art-secondary))',
iconBgColor: 'rgb(var(--art-bg-secondary))',
iconSize: 20
}
]
const imageCards = [
{
id: 1,
imageUrl: cover1,
title: 'AI技术在医疗领域的创新应用与发展前景',
category: '社交',
readTime: '2分钟',
views: 9125,
comments: 3,
date: '12月19日 周一'
},
{
id: 2,
imageUrl: cover2,
title: '大数据分析助力企业决策的实践案例',
category: '技术',
readTime: '3分钟',
views: 7234,
comments: 5,
date: '12月20日 周二'
},
{
id: 3,
imageUrl: cover3,
title: '区块链技术在供应链管理中的应用',
category: '科技',
readTime: '4分钟',
views: 5678,
comments: 8,
date: '12月21日 周三'
},
{
id: 4,
imageUrl: cover4,
title: '云计算技术发展趋势与未来展望',
category: '云技术',
readTime: '5分钟',
views: 4321,
comments: 6,
date: '12月22日 周四'
}
]
const dataList = [
{
title: '新加坡之行',
status: '进行中',
time: '5分钟',
class: 'bg-primary',
icon: '&#xe6f2;'
},
{
title: '归档数据',
status: '进行中',
time: '10分钟',
class: 'bg-secondary',
icon: '&#xe806;'
},
{
title: '客户会议',
status: '待处理',
time: '15分钟',
class: 'bg-warning',
icon: '&#xe6fb;'
},
{
title: '筛选任务团队',
status: '进行中',
time: '20分钟',
class: 'bg-danger',
icon: '&#xe813;'
},
{
title: '发送信封给小王',
status: '已完成',
time: '20分钟',
class: 'bg-success',
icon: '&#xe70c;'
}
]
const timelineData = [
{
time: '上午 09:30',
status: 'rgb(73, 190, 255)',
content: '收到 John Doe 支付的 385.90 美元'
},
{
time: '上午 10:00',
status: 'rgb(54, 158, 255)',
content: '新销售记录',
code: 'ML-3467'
},
{
time: '上午 12:00',
status: 'rgb(103, 232, 207)',
content: '向 Michael 支付了 64.95 美元'
},
{
time: '下午 14:30',
status: 'rgb(255, 193, 7)',
content: '系统维护通知',
code: 'MT-2023'
},
{
time: '下午 15:45',
status: 'rgb(255, 105, 105)',
content: '紧急订单取消提醒',
code: 'OR-9876'
},
{
time: '下午 17:00',
status: 'rgb(103, 232, 207)',
content: '完成每日销售报表'
}
]
const handleMore = () => {}
</script>
<style lang="scss" scoped>
.cards {
padding: 20px 0;
.page-title {
margin: 20px 0 15px;
font-size: 22px;
font-weight: 500;
&:first-child {
margin-top: 0;
}
}
.el-col {
margin-bottom: 20px;
}
}
</style>

View File

@@ -1,278 +0,0 @@
<template>
<div class="pricing-container">
<div class="pricing-header">
<h1 class="title">超过 53,476 位信赖的开发者</h1>
<h2 class="subtitle">以及众多科技巨头的选择</h2>
</div>
<div class="pricing-cards">
<ElRow :gutter="20" justify="center">
<ElCol v-for="plan in pricingPlans" :key="plan.type" :xs="24" :sm="12" :md="6">
<ElCard class="pricing-card" :class="{ popular: plan.isPopular }" shadow="never">
<div class="card-header">
<h3>{{ plan.title }}</h3>
<p class="description">{{ plan.description }}</p>
<div class="price">
<span class="amount">¥{{ plan.price }}</span>
<span class="period">/一次性付款</span>
</div>
</div>
<div class="features">
<div v-for="(feature, index) in plan.features" :key="index" class="feature-item">
<ElIcon :class="feature.available ? 'available' : 'unavailable'">
<Check v-if="feature.available" />
<Close v-else />
</ElIcon>
<span>{{ feature.text }}</span>
</div>
</div>
<div class="card-footer">
<ElButton type="primary" class="purchase-btn" v-ripple>立即购买</ElButton>
</div>
</ElCard>
</ElCol>
</ElRow>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Check, Close } from '@element-plus/icons-vue'
interface Feature {
text: string
available: boolean
}
interface PricingPlan {
type: string
title: string
description: string
price: number
isPopular: boolean
features: Feature[]
}
const pricingPlans = ref<PricingPlan[]>([
{
type: 'single',
title: '单次使用版',
description: '适用于单个最终产品,最终用户无需付费。',
price: 349,
isPopular: false,
features: [
{ text: '完整源代码', available: true },
{ text: '技术文档', available: true },
{ text: 'SaaS应用授权', available: false },
{ text: '单个项目使用', available: true },
{ text: '一年技术支持', available: true },
{ text: '一年免费更新', available: true }
]
},
{
type: 'multiple',
title: '多次使用版',
description: '适用于无限个最终产品,最终用户无需付费。',
price: 629,
isPopular: false,
features: [
{ text: '完整源代码', available: true },
{ text: '技术文档', available: true },
{ text: 'SaaS应用授权', available: false },
{ text: '无限项目使用', available: true },
{ text: '一年技术支持', available: true },
{ text: '一年免费更新', available: true }
]
},
{
type: 'extended',
title: '扩展授权版',
description: '适用于单个最终产品,最终用户需要付费。',
price: 2099,
isPopular: false,
features: [
{ text: '完整源代码', available: true },
{ text: '技术文档', available: true },
{ text: 'SaaS应用授权', available: true },
{ text: '单个项目使用', available: true },
{ text: '一年技术支持', available: true },
{ text: '一年免费更新', available: true }
]
},
{
type: 'unlimited',
title: '无限授权版',
description: '适用于无限个最终产品,最终用户需要付费。',
price: 3499,
isPopular: false,
features: [
{ text: '完整源代码', available: true },
{ text: '技术文档', available: true },
{ text: 'SaaS应用授权', available: true },
{ text: '无限项目使用', available: true },
{ text: '一年技术支持', available: true },
{ text: '一年免费更新', available: true }
]
}
])
</script>
<style lang="scss" scoped>
.pricing-container {
padding: 5rem 1rem !important;
background-color: transparent !important;
border: none !important;
.pricing-header {
margin-bottom: 40px;
text-align: center;
.title {
margin-bottom: 0.5rem;
font-size: 2.5rem;
font-weight: 500;
}
.subtitle {
font-size: 1.4rem;
font-weight: 400;
color: #666;
}
}
.pricing-cards {
margin-top: 80px;
.el-col {
margin-bottom: 20px;
}
.pricing-card {
display: flex;
flex-direction: column;
height: 100%;
border-radius: 10px;
&.popular {
position: relative;
border: 2px solid var(--el-color-primary);
&::after {
position: absolute;
top: 10px;
right: 10px;
padding: 2px 8px;
font-size: 12px;
color: var(--el-color-primary);
content: '热门';
background-color: var(--el-color-primary-light-9);
border-radius: 12px;
}
}
.card-header {
margin-bottom: 20px;
h3 {
margin-bottom: 10px;
font-size: 1.3rem;
color: var(--art-text-gray-900) !important;
}
.description {
display: -webkit-box;
height: 40px;
padding-bottom: 20px;
margin-bottom: 20px;
overflow: hidden;
font-size: 14px;
color: var(--art-text-gray-600);
text-overflow: ellipsis;
border-bottom: 1px solid var(--art-border-color);
-webkit-box-orient: vertical;
}
.price {
margin-top: 30px;
.amount {
font-size: 1.8rem;
font-weight: 600;
}
.period {
margin-left: 10px;
font-size: 14px;
color: var(--art-text-gray-600);
}
}
}
.features {
flex-grow: 1;
margin-bottom: 20px;
.feature-item {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 14px;
.el-icon {
margin-right: 10px;
&.available {
color: var(--el-color-primary);
}
&.unavailable {
color: var(--el-color-danger);
}
}
}
}
.card-footer {
margin-top: auto;
text-align: center;
.purchase-btn {
width: 100%;
height: 40px;
}
}
}
}
}
@media only screen and (max-width: $device-notebook) {
.pricing-container {
padding: 2rem 0 !important;
.pricing-cards {
margin-top: 0;
}
}
}
@media only screen and (max-width: $device-phone) {
.pricing-container {
.pricing-header {
.title {
font-size: 2rem;
}
.subtitle {
font-size: 1.5rem;
}
}
.el-col {
margin-bottom: 20px;
}
}
}
</style>

View File

@@ -1,103 +0,0 @@
<!-- 文档https://github.com/PanJiaChen/vue-countTo -->
<template>
<div class="page-content">
<!-- 基础用法 -->
<h2>基础用法</h2>
<CountTo :endVal="count1" :duration="1000"></CountTo>
<!-- 带前缀后缀 -->
<h2>带前缀后缀</h2>
<CountTo prefix="¥" suffix="元" :startVal="0" :endVal="count2" :duration="2000"></CountTo>
<!-- 小数点和分隔符 -->
<h2>小数点和分隔符</h2>
<CountTo
:startVal="0"
:endVal="count3"
:decimals="2"
decimal="."
separator=","
:duration="2500"
></CountTo>
<!-- 控制按钮 -->
<h2>控制按钮</h2>
<CountTo
ref="countTo"
:startVal="0"
:endVal="count4"
:duration="3000"
:autoplay="false"
></CountTo>
<div class="mt-4">
<ElButtonGroup>
<ElButton @click="start" v-ripple>开始</ElButton>
<ElButton @click="pause" v-ripple>暂停</ElButton>
<ElButton @click="reset" v-ripple>重置</ElButton>
</ElButtonGroup>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { CountTo } from 'vue3-count-to'
const count1 = ref(1000)
const count2 = ref(19999.99)
const count3 = ref(2023.45)
const count4 = ref(5000)
const countTo = ref()
const isCounting = ref(false)
// 控制方法
const start = () => {
if (isCounting.value) return
try {
countTo.value?.reset()
countTo.value?.start()
isCounting.value = true
} catch (error) {
console.error('启动计数器失败:', error)
}
}
const pause = () => {
if (!isCounting.value) return
try {
countTo.value?.pause()
isCounting.value = false
} catch (error) {
console.error('暂停计数器失败:', error)
}
}
const reset = () => {
try {
countTo.value?.reset()
isCounting.value = false
} catch (error) {
console.error('重置计数器失败:', error)
}
}
</script>
<style scoped>
.page-content {
padding: 20px;
}
h2 {
margin: 20px 0;
font-size: 18px;
color: #333;
}
.mt-4 {
margin-top: 16px;
}
</style>

View File

@@ -1,89 +0,0 @@
<template>
<div class="page-content">
<ArtExcelImport @import-success="handleImportSuccess" @import-error="handleImportError">
<template #import-text> 上传 Excel </template>
</ArtExcelImport>
<ArtExcelExport
style="margin-left: 10px"
:data="tableData"
filename="用户数据"
sheetName="用户列表"
type="success"
:headers="headers"
@export-success="handleExportSuccess"
@export-error="handleExportError"
>
导出 Excel
</ArtExcelExport>
<ElButton type="danger" @click="handleClear" v-ripple>清除数据</ElButton>
<ArtTable :data="tableData" style="margin-top: 10px">
<ElTableColumn
v-for="key in Object.keys(headers)"
:key="key"
:prop="key"
:label="headers[key as keyof typeof headers]"
/>
</ArtTable>
</div>
</template>
<script setup lang="ts">
interface TableData {
name: string
age: number
city: string
}
const handleImportSuccess = (data: any[]) => {
// 将导入的数据转换为正确的格式
const formattedData = data.map((item) => ({
name: item['姓名'],
age: Number(item['年龄']),
city: item['城市']
}))
tableData.value = formattedData
// tableData.value = data
}
const handleImportError = (error: Error) => {
// 处理导入错误
console.error('导入失败:', error)
}
// 使用类型化的ref
const tableData = ref<TableData[]>([
{ name: '李四', age: 20, city: '上海' },
{ name: '张三', age: 25, city: '北京' },
{ name: '王五', age: 30, city: '广州' },
{ name: '赵六', age: 35, city: '深圳' },
{ name: '孙七', age: 28, city: '杭州' },
{ name: '周八', age: 32, city: '成都' },
{ name: '吴九', age: 27, city: '武汉' },
{ name: '郑十', age: 40, city: '南京' },
{ name: '刘一', age: 22, city: '重庆' },
{ name: '陈二', age: 33, city: '西安' }
])
// 自定义表头映射
const headers = {
name: '姓名',
age: '年龄',
city: '城市'
}
const handleExportSuccess = () => {
ElMessage.success('导出成功')
}
const handleExportError = (error: Error) => {
ElMessage.error(`导出失败: ${error.message}`)
}
const handleClear = () => {
tableData.value = []
}
</script>

View File

@@ -1,102 +0,0 @@
<template>
<div class="page-content">
<div class="action-buttons">
<ElButton :disabled="isLaunching" v-ripple @click="handleSingleLaunch"
> 放个小烟花</ElButton
>
<ElButton :disabled="isLaunching" v-ripple @click="handleImageLaunch(bp)"
>🎉 打开幸运红包</ElButton
>
<ElButton :disabled="isLaunching" v-ripple @click="handleMultipleLaunch('')"
>🎆 璀璨烟火秀</ElButton
>
<ElButton :disabled="isLaunching" v-ripple @click="handleImageLaunch(sd)"
> 飘点小雪花</ElButton
>
<ElButton :disabled="isLaunching" v-ripple @click="handleMultipleLaunch(sd)"
> 浪漫暴风雪</ElButton
>
</div>
<ElDescriptions
title="礼花组件说明"
direction="vertical"
:column="1"
border
style="margin-top: 50px"
>
<ElDescriptionsItem label="显示时机">
礼花效果组件全局注册了在节假日的时候会自动显示你可以通过配置文件来控制显示时机
</ElDescriptionsItem>
<ElDescriptionsItem label="礼花样式">
默认显示几何图形可以配置图片图片需要提前在 components/Ceremony/Fireworks 文件预先定义
</ElDescriptionsItem>
<ElDescriptionsItem label="节日配置">
src/config/festival.ts 文件中可以配置节日和对应的礼花样式
</ElDescriptionsItem>
<ElDescriptionsItem label="快捷键">
command + shift + p 或者 ctrl + shift + p
</ElDescriptionsItem>
</ElDescriptions>
</div>
</template>
<script setup lang="ts">
import { mittBus } from '@/utils/sys'
import bp from '@imgs/ceremony/hb.png'
import sd from '@imgs/ceremony/sd.png'
const timerRef = ref<ReturnType<typeof setInterval> | null>(null)
const isLaunching = ref(false)
const triggerFireworks = (count: number, src: string) => {
// 清除之前的定时器
if (timerRef.value) {
clearInterval(timerRef.value)
timerRef.value = null
}
isLaunching.value = true // 开始发射时设置状态
let fired = 0
timerRef.value = setInterval(() => {
mittBus.emit('triggerFireworks', src)
fired++
// 达到指定次数后清除定时器
if (fired >= count) {
clearInterval(timerRef.value!)
timerRef.value = null
isLaunching.value = false // 发射完成后解除禁用
}
}, 1000)
}
// 简化后的处理函数
const handleSingleLaunch = () => {
mittBus.emit('triggerFireworks')
}
const handleMultipleLaunch = (src: string) => {
triggerFireworks(10, src)
}
const handleImageLaunch = (src: string) => {
mittBus.emit('triggerFireworks', src)
}
// 组件卸载时清理定时器
onUnmounted(() => {
if (timerRef.value) {
clearInterval(timerRef.value)
timerRef.value = null
}
})
</script>
<style lang="scss" scoped>
.action-buttons {
margin-bottom: 20px;
}
</style>

View File

@@ -1,175 +0,0 @@
<template>
<div class="page-content">
<div class="form">
<ElSelect v-model="iconType" placeholder="Select" style="width: 240px">
<ElOption
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
<div class="colors-icon">
<ElCheckbox v-model="isColorsIcon" label="彩色图标" />
</div>
</div>
<div class="list">
<ul class="icon-list">
<li v-for="icon in systemIconClasses" :key="icon.className" @click="copyIcon(icon)">
<i
class="iconfont-sys"
v-if="iconType === 'unicode'"
v-html="icon.unicode"
:style="getIconStyle()"
></i>
<i :class="`iconfont-sys ${icon.className}`" v-else :style="getIconStyle()"></i>
<span>{{ iconType === 'unicode' ? icon.unicode : icon.className }}</span>
</li>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
import { extractIconClasses, IconfontType } from '@/utils/constants'
import { ElMessage } from 'element-plus'
const iconType = ref('unicode')
const options = [
{
value: 'unicode',
label: 'Unicode'
},
{
value: 'fontClass',
label: 'Font class'
}
]
const systemIconClasses = ref<IconfontType[]>([])
const isColorsIcon = ref(false)
onMounted(() => {
systemIconClasses.value = extractIconClasses()
})
const copyIcon = (text: IconfontType) => {
if (!text) return
let copyipt = document.createElement('input')
copyipt.setAttribute(
'value',
(iconType.value === 'unicode' ? text.unicode : text.className) || ''
)
document.body.appendChild(copyipt)
copyipt.select()
document.execCommand('copy')
document.body.removeChild(copyipt)
ElMessage.success(`已复制`)
}
const getRandomColor = () => {
const colors = ['#2d8cf0', '#19be6b', '#ff9900', '#f24965', '#9463f7']
return colors[Math.floor(Math.random() * colors.length)]
}
const getIconStyle = () => {
return isColorsIcon.value ? { color: getRandomColor() } : { color: 'var(--art-text-gray-700)' }
}
</script>
<style lang="scss" scoped>
.page-content {
width: 100%;
height: 100%;
$border-color: #eee;
.form {
display: flex;
align-items: center;
.colors-icon {
box-sizing: border-box;
height: var(--el-component-custom-height);
padding: 0 30px;
margin-left: 10px;
border: 1px solid var(--art-border-dashed-color);
border-radius: calc(var(--custom-radius) / 3 + 2px) !important;
}
}
.list {
margin-top: 20px;
.icon-list {
display: grid;
grid-template-columns: repeat(12, 1fr);
width: calc(100% + 16px);
li {
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
aspect-ratio: 1 / 1;
padding: 0 8px;
margin: 0 16px 16px 0;
overflow: hidden;
color: rgba(#fff, 0.8);
text-align: center;
border: 1px solid rgb(var(--art-gray-300-rgb), 0.8);
border-radius: 12px !important;
&:hover {
cursor: pointer;
background: var(--art-gray-100);
}
i {
font-size: 26px;
transition: color 0.3s ease;
}
span {
display: block;
margin-top: 10px;
font-size: 12px;
color: var(--art-text-gray-600);
}
}
}
}
}
@media screen and (max-width: $device-notebook) {
.page-content {
.list {
.icon-list {
grid-template-columns: repeat(8, 1fr);
}
}
}
}
@media screen and (max-width: $device-ipad-vertical) {
.page-content {
.list {
.icon-list {
grid-template-columns: repeat(5, 1fr);
}
}
}
}
@media screen and (max-width: $device-phone) {
.page-content {
.list {
.icon-list {
grid-template-columns: repeat(3, 1fr);
}
}
}
}
</style>

View File

@@ -1,56 +0,0 @@
<template>
<div class="page-content">
<div class="select">
<div class="item">
<h3>Unicode</h3>
<ArtIconSelector
:iconType="IconTypeEnum.UNICODE"
@getIcon="getIcon"
defaultIcon="&#xe6b5;"
/>
</div>
<div class="item">
<h3>ClassName</h3>
<ArtIconSelector
:iconType="IconTypeEnum.CLASS_NAME"
@getIcon="getIcon"
width="260px"
defaultIcon="iconsys-baitianmoshi3"
/>
</div>
<div class="item">
<h3>禁用</h3>
<ArtIconSelector
:iconType="IconTypeEnum.CLASS_NAME"
@getIcon="getIcon"
width="260px"
defaultIcon="iconsys-baitianmoshi3"
disabled
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { IconTypeEnum } from '@/enums/appEnum'
// 获取选择的图标
const getIcon = (icon: string) => {
console.log(icon)
}
</script>
<style scoped lang="scss">
.select {
.item {
margin-bottom: 30px;
h3 {
padding-bottom: 10px;
font-size: 16px;
font-weight: 500;
}
}
}
</style>

View File

@@ -1,39 +0,0 @@
<template>
<div class="page-content">
<ArtCutterImg
style="margin-top: 20px"
v-model:imgUrl="imageUrl"
:boxWidth="540"
:boxHeight="300"
:cutWidth="360"
:cutHeight="200"
:quality="1"
:tool="true"
:watermarkText="'My Watermark'"
watermarkColor="#ff0000"
:showPreview="true"
:originalGraph="false"
:previewTitle="'预览效果'"
@error="handleError"
@imageLoadComplete="handleLoadComplete"
@imageLoadError="handleLoadError"
/>
</div>
</template>
<script setup lang="ts">
import lockImg from '@imgs/lock/lock_screen_1.webp'
const imageUrl = ref(lockImg)
const handleError = (error: any) => {
console.error('裁剪错误:', error)
}
const handleLoadComplete = (result: any) => {
console.log('图片加载完成:', result)
}
const handleLoadError = (error: any) => {
console.error('图片加载失败:', error)
}
</script>

View File

@@ -1,28 +0,0 @@
<template>
<div class="page-content">
<!-- 基础用法 -->
<ArtTextScroll
text="Art Design Pro 是一款专注于用户体验和视觉设计的后台管理系统模版 <a target='_blank' href='https://www.lingchen.kim/art-design-pro/docs/'>点击我 </a>访问官方文档"
/>
<!-- 使用不同的类型 -->
<ArtTextScroll type="success" text="这是一条成功类型的滚动公告" />
<ArtTextScroll type="warning" text="这是一条警告类型的滚动公告" />
<ArtTextScroll type="danger" text="这是一条危险类型的滚动公告" />
<ArtTextScroll type="info" text="这是一条信息类型的滚动公告" />
<!-- 自定义速度和方向 -->
<ArtTextScroll text="这是一条速度较慢、向右滚动的公告" :speed="30" direction="right" />
</div>
</template>
<style lang="scss" scoped>
.page-content {
:deep(.text-scroll-container) {
margin-bottom: 20px;
}
}
</style>

View File

@@ -1,31 +0,0 @@
<template>
<div class="page-content">
<div class="video-container">
<ArtVideoPlayer
playerId="my-video-1"
:videoUrl="videoUrl"
:posterUrl="posterUrl"
:autoplay="false"
:volume="0.5"
:playbackRates="[0.5, 1, 1.5, 2]"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import lockImg from '@imgs/lock/lock_screen_1.webp'
// 视频源和封面图片URL
const videoUrl = ref(
'//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/xgplayer-demo.mp4'
)
const posterUrl = ref(lockImg)
</script>
<style scoped>
.video-container {
max-width: 600px;
}
</style>