删除多余代码
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -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) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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: ''
|
||||
},
|
||||
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,
|
||||
|
||||
@@ -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' // 日历
|
||||
}
|
||||
|
||||
// 主页路由
|
||||
|
||||
@@ -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[] // 兼容旧字段
|
||||
}
|
||||
|
||||
// 创建代理充值订单请求
|
||||
|
||||
@@ -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 // 支付方式
|
||||
}
|
||||
|
||||
4
src/types/components.d.ts
vendored
4
src/types/components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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: '',
|
||||
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>
|
||||
@@ -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"></i>{{ item.collection }}</span>
|
||||
<span><i class="iconfont-sys"></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"></i>{{ clickItem.collection }}</span>
|
||||
<span><i class="iconfont-sys"></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>
|
||||
@@ -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>
|
||||
@@ -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"></i>
|
||||
<span>{{ useDateFormat(item.create_time, 'YYYY-MM-DD') }}</span>
|
||||
<div class="line"></div>
|
||||
<i class="iconfont-sys"></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>
|
||||
@@ -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:9,jpg/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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 取消换货
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
})
|
||||
|
||||
// 返回列表
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
>必填字段:ICCID、virtual_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>
|
||||
@@ -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>
|
||||
@@ -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: '广电飞悦卡无预存50G(30天)',
|
||||
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>
|
||||
@@ -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
@@ -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: '广电飞悦卡无预存50G(30天)',
|
||||
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>
|
||||
@@ -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: '广电飞悦卡无预存50G(30天)',
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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: '广电飞悦卡无预存50G(30天)', 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: '广电飞悦卡无预存50G(30天)',
|
||||
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>
|
||||
@@ -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: '广电飞悦卡无预存50G(30天)',
|
||||
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: '广电飞悦卡无预存50G(30天)', 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>
|
||||
@@ -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: '广电飞悦卡无预存50G(30天)', 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: '广电飞悦卡无预存50G(30天)',
|
||||
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>
|
||||
@@ -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: '广电飞悦卡无预存50G(30天)',
|
||||
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>
|
||||
@@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h3 class="table-title"><i class="iconfont-sys"></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>
|
||||
@@ -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,
|
||||
@@ -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'
|
||||
|
||||
195
src/views/dashboard/analysis/widget/PollingStats.vue
Normal file
195
src/views/dashboard/analysis/widget/PollingStats.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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"></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"></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"></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"></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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -51,6 +51,7 @@
|
||||
const getProviderTypeText = (type: PaymentProviderType): string => {
|
||||
const typeMap: Record<PaymentProviderType, string> = {
|
||||
wechat: '微信直连',
|
||||
wechat_v2: '微信直连V2',
|
||||
fuiou: '富友支付'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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: '',
|
||||
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: '',
|
||||
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: '',
|
||||
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: '',
|
||||
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: '',
|
||||
iconColor: 'rgb(var(--art-success))',
|
||||
iconBgColor: 'rgb(var(--art-bg-success))',
|
||||
iconSize: 20
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '项目进度',
|
||||
percentage: 65,
|
||||
color: 'rgb(var(--art-primary))',
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-primary))',
|
||||
iconBgColor: 'rgb(var(--art-bg-primary))',
|
||||
iconSize: 20
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '学习进度',
|
||||
percentage: 45,
|
||||
color: 'rgb(var(--art-error))',
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-error))',
|
||||
iconBgColor: 'rgb(var(--art-bg-error))',
|
||||
iconSize: 20
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '任务进度',
|
||||
percentage: 90,
|
||||
color: 'rgb(var(--art-secondary))',
|
||||
icon: '',
|
||||
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: ''
|
||||
},
|
||||
{
|
||||
title: '归档数据',
|
||||
status: '进行中',
|
||||
time: '10分钟',
|
||||
class: 'bg-secondary',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '客户会议',
|
||||
status: '待处理',
|
||||
time: '15分钟',
|
||||
class: 'bg-warning',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '筛选任务团队',
|
||||
status: '进行中',
|
||||
time: '20分钟',
|
||||
class: 'bg-danger',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '发送信封给小王',
|
||||
status: '已完成',
|
||||
time: '20分钟',
|
||||
class: 'bg-success',
|
||||
icon: ''
|
||||
}
|
||||
]
|
||||
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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=""
|
||||
/>
|
||||
</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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user