fetch(add): 订单管理-企业设备
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m30s

This commit is contained in:
sexygoat
2026-01-29 15:43:45 +08:00
parent 1812b7a6c4
commit 841cf0442b
58 changed files with 8948 additions and 1164 deletions

View File

@@ -61,6 +61,24 @@ export class BaseService {
})
}
/**
* PATCH 请求
* @param url 请求URL
* @param data 请求数据
* @param config 额外配置
*/
protected static patch<T = any>(
url: string,
data?: Record<string, any>,
config?: Record<string, any>
): Promise<T> {
return request.patch<T>({
url,
data,
...config
})
}
/**
* DELETE 请求
* @param url 请求URL

View File

@@ -354,4 +354,17 @@ export class CardService extends BaseService {
static getAssetAllocationRecordDetail(id: number): Promise<BaseResponse<any>> {
return this.getOne(`/api/admin/asset-allocation-records/${id}`)
}
// ========== 批量设置卡的套餐系列绑定相关 ==========
/**
* 批量设置卡的套餐系列绑定
* @param data 请求参数
*/
static batchSetCardSeriesBinding(data: {
iccids: string[]
series_allocation_id: number
}): Promise<BaseResponse<any>> {
return this.patch('/api/admin/iot-cards/series-binding', data)
}
}

View File

@@ -18,7 +18,11 @@ import type {
SubmitWithdrawalParams,
ShopCommissionRecordPageResult,
ShopCommissionSummaryQueryParams,
ShopCommissionSummaryPageResult
ShopCommissionSummaryPageResult,
MyCommissionStatsQueryParams,
MyCommissionStatsResponse,
MyDailyCommissionStatsQueryParams,
DailyCommissionStatsItem
} from '@/types/api/commission'
export class CommissionService extends BaseService {
@@ -130,6 +134,32 @@ export class CommissionService extends BaseService {
return this.post<BaseResponse>('/api/admin/my/withdrawal-requests', params)
}
/**
* 获取我的佣金统计
* GET /api/admin/my/commission-stats
*/
static getMyCommissionStats(
params?: MyCommissionStatsQueryParams
): Promise<BaseResponse<MyCommissionStatsResponse>> {
return this.get<BaseResponse<MyCommissionStatsResponse>>(
'/api/admin/my/commission-stats',
params
)
}
/**
* 获取我的每日佣金统计
* GET /api/admin/my/commission-daily-stats
*/
static getMyDailyCommissionStats(
params?: MyDailyCommissionStatsQueryParams
): Promise<BaseResponse<DailyCommissionStatsItem[]>> {
return this.get<BaseResponse<DailyCommissionStatsItem[]>>(
'/api/admin/my/commission-daily-stats',
params
)
}
// ==================== 代理商佣金管理 ====================
/**

View File

@@ -154,4 +154,17 @@ export class DeviceService extends BaseService {
static getImportTaskDetail(id: number): Promise<BaseResponse<DeviceImportTaskDetail>> {
return this.getOne<DeviceImportTaskDetail>(`/api/admin/devices/import/tasks/${id}`)
}
// ========== 批量设置设备的套餐系列绑定相关 ==========
/**
* 批量设置设备的套餐系列绑定
* @param data 请求参数
*/
static batchSetDeviceSeriesBinding(data: {
device_ids: number[]
series_allocation_id: number
}): Promise<BaseResponse<any>> {
return this.patch('/api/admin/devices/series-binding', data)
}
}

View File

@@ -24,6 +24,14 @@ import type {
RecallCardsRequest,
RecallCardsResponse
} from '@/types/api/enterpriseCard'
import type {
EnterpriseDeviceListParams,
EnterpriseDevicePageResult,
AllocateDevicesRequest,
AllocateDevicesResponse,
RecallDevicesRequest,
RecallDevicesResponse
} from '@/types/api/enterpriseDevice'
export class EnterpriseService extends BaseService {
/**
@@ -157,4 +165,51 @@ export class EnterpriseService extends BaseService {
data
)
}
// ========== 企业设备授权相关 ==========
/**
* 授权设备给企业
* @param enterpriseId 企业ID
* @param data 授权请求数据
*/
static allocateDevices(
enterpriseId: number,
data: AllocateDevicesRequest
): Promise<BaseResponse<AllocateDevicesResponse>> {
return this.post<BaseResponse<AllocateDevicesResponse>>(
`/api/admin/enterprises/${enterpriseId}/allocate-devices`,
data
)
}
/**
* 获取企业设备列表
* @param enterpriseId 企业ID
* @param params 查询参数
*/
static getEnterpriseDevices(
enterpriseId: number,
params?: EnterpriseDeviceListParams
): Promise<BaseResponse<EnterpriseDevicePageResult>> {
return this.get<BaseResponse<EnterpriseDevicePageResult>>(
`/api/admin/enterprises/${enterpriseId}/devices`,
params
)
}
/**
* 撤销设备授权
* @param enterpriseId 企业ID
* @param data 撤销请求数据
*/
static recallDevices(
enterpriseId: number,
data: RecallDevicesRequest
): Promise<BaseResponse<RecallDevicesResponse>> {
return this.post<BaseResponse<RecallDevicesResponse>>(
`/api/admin/enterprises/${enterpriseId}/recall-devices`,
data
)
}
}

View File

@@ -21,7 +21,12 @@ export { StorageService } from './storage'
export { AuthorizationService } from './authorization'
export { DeviceService } from './device'
export { CarrierService } from './carrier'
export { PackageSeriesService } from './packageSeries'
export { PackageManageService } from './packageManage'
export { MyPackageService } from './myPackage'
export { ShopPackageAllocationService } from './shopPackageAllocation'
export { ShopSeriesAllocationService } from './shopSeriesAllocation'
export { OrderService } from './order'
// TODO: 按需添加其他业务模块
// export { PackageService } from './package'
// export { SettingService } from './setting'

View File

@@ -0,0 +1,45 @@
/**
* 代理可售套餐 API 服务
*/
import { BaseService } from '../BaseService'
import type {
MyPackageResponse,
MyPackageQueryParams,
MySeriesAllocationResponse,
BaseResponse,
PaginationResponse
} from '@/types/api'
export class MyPackageService extends BaseService {
/**
* 获取我的可售套餐列表
* GET /api/admin/my-packages
* @param params 查询参数
*/
static getMyPackages(
params?: MyPackageQueryParams
): Promise<PaginationResponse<MyPackageResponse>> {
return this.getPage<MyPackageResponse>('/api/admin/my-packages', params)
}
/**
* 获取我的可售套餐详情
* GET /api/admin/my-packages/{id}
* @param id 套餐ID
*/
static getMyPackageDetail(id: number): Promise<BaseResponse<MyPackageResponse>> {
return this.getOne<MyPackageResponse>(`/api/admin/my-packages/${id}`)
}
/**
* 获取我的被分配系列列表
* GET /api/admin/my-series-allocations
* @param params 查询参数(支持分页)
*/
static getMySeriesAllocations(
params?: Record<string, any>
): Promise<PaginationResponse<MySeriesAllocationResponse>> {
return this.getPage<MySeriesAllocationResponse>('/api/admin/my-series-allocations', params)
}
}

47
src/api/modules/order.ts Normal file
View File

@@ -0,0 +1,47 @@
/**
* 订单管理相关 API
*/
import { BaseService } from '../BaseService'
import type {
Order,
OrderQueryParams,
OrderListResponse,
CreateOrderRequest,
CreateOrderResponse,
BaseResponse
} from '@/types/api'
export class OrderService extends BaseService {
/**
* 获取订单列表
* @param params 查询参数
*/
static getOrders(params?: OrderQueryParams): Promise<BaseResponse<OrderListResponse>> {
return this.get<BaseResponse<OrderListResponse>>('/api/admin/orders', params)
}
/**
* 获取订单详情
* @param id 订单ID
*/
static getOrderById(id: number): Promise<BaseResponse<Order>> {
return this.getOne<Order>(`/api/admin/orders/${id}`)
}
/**
* 创建订单
* @param data 创建订单请求参数
*/
static createOrder(data: CreateOrderRequest): Promise<BaseResponse<CreateOrderResponse>> {
return this.post<BaseResponse<CreateOrderResponse>>('/api/admin/orders', data)
}
/**
* 取消订单
* @param id 订单ID
*/
static cancelOrder(id: number): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/admin/orders/${id}/cancel`, {})
}
}

View File

@@ -0,0 +1,91 @@
/**
* 套餐管理 API 服务
*/
import { BaseService } from '../BaseService'
import type {
PackageResponse,
PackageQueryParams,
CreatePackageRequest,
UpdatePackageRequest,
UpdatePackageStatusRequest,
UpdatePackageShelfStatusRequest,
SeriesSelectOption,
BaseResponse,
PaginationResponse,
ListResponse
} from '@/types/api'
export class PackageManageService extends BaseService {
/**
* 获取套餐分页列表
* GET /api/admin/packages
* @param params 查询参数
*/
static getPackages(params?: PackageQueryParams): Promise<PaginationResponse<PackageResponse>> {
return this.getPage<PackageResponse>('/api/admin/packages', params)
}
/**
* 创建套餐
* POST /api/admin/packages
* @param data 套餐数据
*/
static createPackage(data: CreatePackageRequest): Promise<BaseResponse<PackageResponse>> {
return this.create<PackageResponse>('/api/admin/packages', data)
}
/**
* 获取套餐详情
* GET /api/admin/packages/{id}
* @param id 套餐ID
*/
static getPackageDetail(id: number): Promise<BaseResponse<PackageResponse>> {
return this.getOne<PackageResponse>(`/api/admin/packages/${id}`)
}
/**
* 更新套餐
* PUT /api/admin/packages/{id}
* @param id 套餐ID
* @param data 套餐数据
*/
static updatePackage(
id: number,
data: UpdatePackageRequest
): Promise<BaseResponse<PackageResponse>> {
return this.update<PackageResponse>(`/api/admin/packages/${id}`, data)
}
/**
* 删除套餐
* DELETE /api/admin/packages/{id}
* @param id 套餐ID
*/
static deletePackage(id: number): Promise<BaseResponse> {
return this.remove(`/api/admin/packages/${id}`)
}
/**
* 更新套餐状态
* PUT /api/admin/packages/{id}/status
* @param id 套餐ID
* @param status 状态 (1:启用, 2:禁用)
*/
static updatePackageStatus(id: number, status: number): Promise<BaseResponse> {
const data: UpdatePackageStatusRequest = { status }
return this.put<BaseResponse>(`/api/admin/packages/${id}/status`, data)
}
/**
* 更新套餐上架状态
* PATCH /api/admin/packages/{id}/shelf
* @param id 套餐ID
* @param shelf_status 上架状态 (1:上架, 2:下架)
*/
static updatePackageShelfStatus(id: number, shelf_status: number): Promise<BaseResponse> {
const data: UpdatePackageShelfStatusRequest = { shelf_status }
return this.patch<BaseResponse>(`/api/admin/packages/${id}/shelf`, data)
}
}

View File

@@ -0,0 +1,83 @@
/**
* 套餐系列管理 API 服务
*/
import { BaseService } from '../BaseService'
import type {
PackageSeriesResponse,
PackageSeriesQueryParams,
CreatePackageSeriesRequest,
UpdatePackageSeriesRequest,
UpdatePackageSeriesStatusRequest,
BaseResponse,
PaginationResponse
} from '@/types/api'
export class PackageSeriesService extends BaseService {
/**
* 获取套餐系列分页列表
* GET /api/admin/package-series
* @param params 查询参数
*/
static getPackageSeries(
params?: PackageSeriesQueryParams
): Promise<PaginationResponse<PackageSeriesResponse>> {
return this.getPage<PackageSeriesResponse>('/api/admin/package-series', params)
}
/**
* 创建套餐系列
* POST /api/admin/package-series
* @param data 套餐系列数据
*/
static createPackageSeries(
data: CreatePackageSeriesRequest
): Promise<BaseResponse<PackageSeriesResponse>> {
return this.create<PackageSeriesResponse>('/api/admin/package-series', data)
}
/**
* 获取套餐系列详情
* GET /api/admin/package-series/{id}
* @param id 系列ID
*/
static getPackageSeriesDetail(id: number): Promise<BaseResponse<PackageSeriesResponse>> {
return this.getOne<PackageSeriesResponse>(`/api/admin/package-series/${id}`)
}
/**
* 更新套餐系列
* PUT /api/admin/package-series/{id}
* @param id 系列ID
* @param data 套餐系列数据
*/
static updatePackageSeries(
id: number,
data: UpdatePackageSeriesRequest
): Promise<BaseResponse<PackageSeriesResponse>> {
return this.update<PackageSeriesResponse>(`/api/admin/package-series/${id}`, data)
}
/**
* 删除套餐系列
* DELETE /api/admin/package-series/{id}
* @param id 系列ID
*/
static deletePackageSeries(id: number): Promise<BaseResponse> {
return this.remove(`/api/admin/package-series/${id}`)
}
/**
* 更新套餐系列状态
* PUT /api/admin/package-series/{id}/status
* @param id 系列ID
* @param status 状态 (1:启用, 2:禁用)
*/
static updatePackageSeriesStatus(
id: number,
status: number
): Promise<BaseResponse> {
const data: UpdatePackageSeriesStatusRequest = { status }
return this.put<BaseResponse>(`/api/admin/package-series/${id}/status`, data)
}
}

View File

@@ -0,0 +1,109 @@
/**
* 单套餐分配 API 服务
*/
import { BaseService } from '../BaseService'
import type {
ShopPackageAllocationResponse,
ShopPackageAllocationQueryParams,
CreateShopPackageAllocationRequest,
UpdateShopPackageAllocationRequest,
UpdateShopPackageAllocationStatusRequest,
BaseResponse,
PaginationResponse
} from '@/types/api'
export class ShopPackageAllocationService extends BaseService {
/**
* 获取单套餐分配列表
* GET /api/admin/shop-package-allocations
* @param params 查询参数
*/
static getShopPackageAllocations(
params?: ShopPackageAllocationQueryParams
): Promise<PaginationResponse<ShopPackageAllocationResponse>> {
return this.getPage<ShopPackageAllocationResponse>(
'/api/admin/shop-package-allocations',
params
)
}
/**
* 创建单套餐分配
* POST /api/admin/shop-package-allocations
* @param data 分配数据
*/
static createShopPackageAllocation(
data: CreateShopPackageAllocationRequest
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
return this.create<ShopPackageAllocationResponse>(
'/api/admin/shop-package-allocations',
data
)
}
/**
* 获取单套餐分配详情
* GET /api/admin/shop-package-allocations/{id}
* @param id 分配ID
*/
static getShopPackageAllocationDetail(
id: number
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
return this.getOne<ShopPackageAllocationResponse>(
`/api/admin/shop-package-allocations/${id}`
)
}
/**
* 更新单套餐分配
* PUT /api/admin/shop-package-allocations/{id}
* @param id 分配ID
* @param data 分配数据(只允许修改成本价)
*/
static updateShopPackageAllocation(
id: number,
data: UpdateShopPackageAllocationRequest
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
return this.update<ShopPackageAllocationResponse>(
`/api/admin/shop-package-allocations/${id}`,
data
)
}
/**
* 删除单套餐分配
* DELETE /api/admin/shop-package-allocations/{id}
* @param id 分配ID
*/
static deleteShopPackageAllocation(id: number): Promise<BaseResponse> {
return this.remove(`/api/admin/shop-package-allocations/${id}`)
}
/**
* 更新单套餐分配成本价
* PUT /api/admin/shop-package-allocations/{id}/cost-price
* @param id 分配ID
* @param costPrice 成本价(分)
*/
static updateShopPackageAllocationCostPrice(
id: number,
costPrice: number
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
return this.put<BaseResponse<ShopPackageAllocationResponse>>(
`/api/admin/shop-package-allocations/${id}/cost-price`,
{ cost_price: costPrice }
)
}
/**
* 更新单套餐分配状态
* PUT /api/admin/shop-package-allocations/{id}/status
* @param id 分配ID
* @param status 状态 (1:启用, 2:禁用)
*/
static updateShopPackageAllocationStatus(id: number, status: number): Promise<BaseResponse> {
const data: UpdateShopPackageAllocationStatusRequest = { status }
return this.put<BaseResponse>(`/api/admin/shop-package-allocations/${id}/status`, data)
}
}

View File

@@ -0,0 +1,88 @@
/**
* 套餐系列分配 API 服务
*/
import { BaseService } from '../BaseService'
import type {
ShopSeriesAllocationResponse,
ShopSeriesAllocationQueryParams,
CreateShopSeriesAllocationRequest,
UpdateShopSeriesAllocationRequest,
UpdateShopSeriesAllocationStatusRequest,
BaseResponse,
PaginationResponse
} from '@/types/api'
export class ShopSeriesAllocationService extends BaseService {
/**
* 获取套餐系列分配分页列表
* GET /api/admin/shop-series-allocations
* @param params 查询参数
*/
static getShopSeriesAllocations(
params?: ShopSeriesAllocationQueryParams
): Promise<PaginationResponse<ShopSeriesAllocationResponse>> {
return this.getPage<ShopSeriesAllocationResponse>(
'/api/admin/shop-series-allocations',
params
)
}
/**
* 创建套餐系列分配
* POST /api/admin/shop-series-allocations
* @param data 分配数据
*/
static createShopSeriesAllocation(
data: CreateShopSeriesAllocationRequest
): Promise<BaseResponse<ShopSeriesAllocationResponse>> {
return this.create<ShopSeriesAllocationResponse>('/api/admin/shop-series-allocations', data)
}
/**
* 获取套餐系列分配详情
* GET /api/admin/shop-series-allocations/{id}
* @param id 分配ID
*/
static getShopSeriesAllocationDetail(
id: number
): Promise<BaseResponse<ShopSeriesAllocationResponse>> {
return this.getOne<ShopSeriesAllocationResponse>(`/api/admin/shop-series-allocations/${id}`)
}
/**
* 更新套餐系列分配
* PUT /api/admin/shop-series-allocations/{id}
* @param id 分配ID
* @param data 分配数据
*/
static updateShopSeriesAllocation(
id: number,
data: UpdateShopSeriesAllocationRequest
): Promise<BaseResponse<ShopSeriesAllocationResponse>> {
return this.update<ShopSeriesAllocationResponse>(
`/api/admin/shop-series-allocations/${id}`,
data
)
}
/**
* 删除套餐系列分配
* DELETE /api/admin/shop-series-allocations/{id}
* @param id 分配ID
*/
static deleteShopSeriesAllocation(id: number): Promise<BaseResponse> {
return this.remove(`/api/admin/shop-series-allocations/${id}`)
}
/**
* 更新套餐系列分配状态
* PUT /api/admin/shop-series-allocations/{id}/status
* @param id 分配ID
* @param status 状态 (1:启用, 2:禁用)
*/
static updateShopSeriesAllocationStatus(id: number, status: number): Promise<BaseResponse> {
const data: UpdateShopSeriesAllocationStatusRequest = { status }
return this.put<BaseResponse>(`/api/admin/shop-series-allocations/${id}/status`, data)
}
}

View File

@@ -139,7 +139,7 @@
<div class="user-info-display">
<div class="avatar">{{ getUserAvatar }}</div>
<div class="info">
<div class="username">{{ userInfo.username || '用户' }}</div>
<div class="username">{{ userInfo.username || '' }}</div>
<div class="user-type">{{ userInfo.user_type_name || '' }}</div>
</div>
</div>
@@ -243,7 +243,7 @@
*/
const getUserAvatar = computed(() => {
const username = userInfo.value.username
if (!username) return 'U'
if (!username) return ''
const firstChar = username.charAt(0)
// 检查是否为中文字符Unicode 范围:\u4e00-\u9fa5

View File

@@ -22,3 +22,6 @@ export * from './iotCard'
// 运营商类型相关
export * from './carrierTypes'
// 套餐管理相关
export * from './package'

View File

@@ -0,0 +1,293 @@
/**
* 套餐管理相关常量配置
*/
// ==================== 套餐类型 ====================
/**
* 套餐类型枚举
*/
export enum PackageType {
FORMAL = 'formal', // 正式套餐
ADDON = 'addon' // 加油包
}
/**
* 套餐类型选项
*/
export const PACKAGE_TYPE_OPTIONS = [
{
label: '正式套餐',
value: PackageType.FORMAL,
type: 'primary' as const
},
{
label: '加油包',
value: PackageType.ADDON,
type: 'success' as const
}
]
/**
* 套餐类型映射
*/
export const PACKAGE_TYPE_MAP = PACKAGE_TYPE_OPTIONS.reduce(
(map, item) => {
map[item.value] = item
return map
},
{} as Record<PackageType, { label: string; value: PackageType; type: 'primary' | 'success' }>
)
/**
* 获取套餐类型标签
*/
export function getPackageTypeLabel(type: string): string {
return PACKAGE_TYPE_MAP[type as PackageType]?.label || '未知'
}
/**
* 获取套餐类型标签类型
*/
export function getPackageTypeTag(type: string) {
return PACKAGE_TYPE_MAP[type as PackageType]?.type || 'info'
}
// ==================== 流量类型 ====================
/**
* 流量类型枚举
*/
export enum DataType {
REAL = 'real', // 真实流量
VIRTUAL = 'virtual' // 虚拟流量
}
/**
* 流量类型选项
*/
export const DATA_TYPE_OPTIONS = [
{
label: '真实流量',
value: DataType.REAL,
type: 'success' as const
},
{
label: '虚拟流量',
value: DataType.VIRTUAL,
type: 'warning' as const
}
]
/**
* 流量类型映射
*/
export const DATA_TYPE_MAP = DATA_TYPE_OPTIONS.reduce(
(map, item) => {
map[item.value] = item
return map
},
{} as Record<DataType, { label: string; value: DataType; type: 'success' | 'warning' }>
)
/**
* 获取流量类型标签
*/
export function getDataTypeLabel(type: string): string {
return DATA_TYPE_MAP[type as DataType]?.label || '未知'
}
/**
* 获取流量类型标签类型
*/
export function getDataTypeTag(type: string) {
return DATA_TYPE_MAP[type as DataType]?.type || 'info'
}
// ==================== 上架状态 ====================
/**
* 上架状态枚举
*/
export enum ShelfStatus {
ON_SHELF = 1, // 上架
OFF_SHELF = 2 // 下架
}
/**
* 上架状态选项
*/
export const SHELF_STATUS_OPTIONS = [
{
label: '上架',
value: ShelfStatus.ON_SHELF,
type: 'success' as const,
text: '上架'
},
{
label: '下架',
value: ShelfStatus.OFF_SHELF,
type: 'info' as const,
text: '下架'
}
]
/**
* 上架状态映射
*/
export const SHELF_STATUS_MAP = SHELF_STATUS_OPTIONS.reduce(
(map, item) => {
map[item.value] = item
return map
},
{} as Record<
ShelfStatus,
{ label: string; value: ShelfStatus; type: 'success' | 'info'; text: string }
>
)
/**
* 获取上架状态标签
*/
export function getShelfStatusLabel(status: number): string {
return SHELF_STATUS_MAP[status as ShelfStatus]?.label || '未知'
}
/**
* 获取上架状态文本
*/
export function getShelfStatusText(status: number): string {
return SHELF_STATUS_MAP[status as ShelfStatus]?.text || '未知'
}
/**
* 获取上架状态标签类型
*/
export function getShelfStatusType(status: number) {
return SHELF_STATUS_MAP[status as ShelfStatus]?.type || 'info'
}
// ==================== 定价模式 ====================
/**
* 定价模式枚举
*/
export enum PricingMode {
FIXED = 'fixed', // 固定金额
PERCENT = 'percent' // 百分比
}
/**
* 定价模式选项
*/
export const PRICING_MODE_OPTIONS = [
{
label: '固定金额',
value: PricingMode.FIXED,
type: 'primary' as const
},
{
label: '百分比',
value: PricingMode.PERCENT,
type: 'success' as const
}
]
/**
* 定价模式映射
*/
export const PRICING_MODE_MAP = PRICING_MODE_OPTIONS.reduce(
(map, item) => {
map[item.value] = item
return map
},
{} as Record<PricingMode, { label: string; value: PricingMode; type: 'primary' | 'success' }>
)
/**
* 获取定价模式标签
*/
export function getPricingModeLabel(mode: string): string {
return PRICING_MODE_MAP[mode as PricingMode]?.label || '未知'
}
/**
* 获取定价模式标签类型
*/
export function getPricingModeTag(mode: string) {
return PRICING_MODE_MAP[mode as PricingMode]?.type || 'info'
}
// ==================== 价格来源 ====================
/**
* 价格来源枚举
*/
export enum PriceSource {
SERIES_PRICING = 'series_pricing', // 系列加价
PACKAGE_OVERRIDE = 'package_override' // 单套餐覆盖
}
/**
* 价格来源选项
*/
export const PRICE_SOURCE_OPTIONS = [
{
label: '系列加价',
value: PriceSource.SERIES_PRICING,
type: 'primary' as const
},
{
label: '单套餐覆盖',
value: PriceSource.PACKAGE_OVERRIDE,
type: 'warning' as const
}
]
/**
* 价格来源映射
*/
export const PRICE_SOURCE_MAP = PRICE_SOURCE_OPTIONS.reduce(
(map, item) => {
map[item.value] = item
return map
},
{} as Record<
PriceSource,
{ label: string; value: PriceSource; type: 'primary' | 'warning' }
>
)
/**
* 获取价格来源标签
*/
export function getPriceSourceLabel(source: string): string {
return PRICE_SOURCE_MAP[source as PriceSource]?.label || '未知'
}
/**
* 获取价格来源标签类型
*/
export function getPriceSourceTag(source: string) {
return PRICE_SOURCE_MAP[source as PriceSource]?.type || 'info'
}
// ==================== 状态值映射工具 ====================
/**
* 前端状态值 (CommonStatus: 0/1) 转换为后端 API 状态值 (1/2)
* @param frontendStatus 前端状态值 (0:禁用, 1:启用)
* @returns 后端状态值 (1:启用, 2:禁用)
*/
export function frontendStatusToApi(frontendStatus: number): number {
return frontendStatus === 1 ? 1 : 2
}
/**
* 后端 API 状态值 (1/2) 转换为前端状态值 (CommonStatus: 0/1)
* @param apiStatus 后端状态值 (1:启用, 2:禁用)
* @returns 前端状态值 (0:禁用, 1:启用)
*/
export function apiStatusToFrontend(apiStatus: number): number {
return apiStatus === 1 ? 1 : 0
}

View File

@@ -392,6 +392,7 @@
"packageList": "My Packages",
"packageChange": "Package Change",
"packageAssign": "Package Assignment",
"seriesAssign": "Series Assignment",
"packageSeries": "Package Series",
"packageCommission": "Package Commission Cards"
},
@@ -414,7 +415,8 @@
"withdrawal": "Commission Withdrawal",
"withdrawalSettings": "Withdrawal Settings",
"myAccount": "My Account",
"carrierManagement": "Carrier Management"
"carrierManagement": "Carrier Management",
"orders": "Order Management"
},
"deviceManagement": {
"title": "Device Management",
@@ -443,7 +445,8 @@
"allocationRecordDetail": "Allocation Record Details",
"cardReplacementRequest": "Card Replacement Request",
"authorizationRecords": "Authorization Records",
"authorizationDetail": "Authorization Details"
"authorizationDetail": "Authorization Details",
"enterpriseDevices": "Enterprise Devices"
},
"settings": {
"title": "Settings Management",
@@ -741,5 +744,206 @@
"pendingAmount": "Pending Amount",
"todayCommission": "Today's Commission"
}
},
"seriesBinding": {
"buttons": {
"batchSetSeries": "Batch Set Series",
"clearBinding": "Clear Binding"
},
"dialog": {
"title": "Batch Set Series Binding",
"titleCard": "Batch Set Card Series Binding",
"titleDevice": "Batch Set Device Series Binding"
},
"form": {
"seriesAllocation": "Series Allocation",
"seriesAllocationPlaceholder": "Please select series allocation",
"clearBindingOption": "Clear Association",
"selectedCount": "{count} items selected"
},
"messages": {
"noSelection": "Please select items to set",
"setSuccess": "Series binding set successfully",
"setFailed": "Failed to set series binding",
"partialSuccess": "Partial success: {success} succeeded, {fail} failed",
"confirmSet": "Are you sure to set series binding for {count} selected items?",
"confirmClear": "Are you sure to clear series binding for {count} selected items?"
},
"result": {
"title": "Set Result",
"successCount": "Success Count",
"failCount": "Fail Count",
"failedItems": "Failed Items",
"iccid": "ICCID",
"deviceId": "Device ID",
"deviceNo": "Device No.",
"reason": "Reason"
}
},
"orderManagement": {
"title": "Order Management",
"orderList": "Order List",
"orderDetail": "Order Details",
"orderItems": "Order Items",
"createOrder": "Create Order",
"searchForm": {
"orderNo": "Order No.",
"orderNoPlaceholder": "Please enter order number",
"paymentStatus": "Payment Status",
"paymentStatusPlaceholder": "Please select payment status",
"orderType": "Order Type",
"orderTypePlaceholder": "Please select order type",
"dateRange": "Created Time",
"startDate": "Start Time",
"endDate": "End Time"
},
"table": {
"id": "Order ID",
"orderNo": "Order No.",
"orderType": "Order Type",
"buyerId": "Buyer ID",
"buyerType": "Buyer Type",
"paymentStatus": "Payment Status",
"paymentMethod": "Payment Method",
"totalAmount": "Total Amount",
"paidAt": "Paid At",
"commissionStatus": "Commission Status",
"createdAt": "Created At",
"updatedAt": "Updated At",
"operation": "Actions"
},
"orderType": {
"singleCard": "Single Card Purchase",
"device": "Device Purchase"
},
"buyerType": {
"personal": "Personal Customer",
"agent": "Agent"
},
"paymentStatus": {
"pending": "Pending",
"paid": "Paid",
"cancelled": "Cancelled",
"refunded": "Refunded"
},
"paymentMethod": {
"wallet": "Wallet Payment",
"wechat": "WeChat Payment",
"alipay": "Alipay Payment"
},
"commissionStatus": {
"notApplicable": "Not Applicable",
"pending": "Pending",
"settled": "Settled"
},
"actions": {
"viewDetail": "View Details",
"cancel": "Cancel",
"confirm": "Confirm",
"submit": "Submit",
"close": "Close"
},
"createForm": {
"packageIds": "Select Packages",
"packageIdsPlaceholder": "Please select packages",
"iotCardId": "IoT Card ID",
"iotCardIdPlaceholder": "Please enter IoT card ID",
"deviceId": "Device ID",
"deviceIdPlaceholder": "Please enter device ID"
},
"items": {
"packageName": "Package Name",
"quantity": "Quantity",
"unitPrice": "Unit Price",
"amount": "Subtotal"
},
"messages": {
"createSuccess": "Order created successfully",
"createFailed": "Failed to create order",
"cancelSuccess": "Order cancelled successfully",
"cancelFailed": "Failed to cancel order",
"cancelConfirm": "Cancel Order Confirmation",
"cancelConfirmText": "Are you sure to cancel this order? This action cannot be undone",
"cannotCancelPaid": "Cannot cancel a paid order",
"cannotCancelCancelled": "Order is already cancelled",
"cannotCancelRefunded": "Cannot cancel a refunded order",
"loadFailed": "Failed to load order data",
"noData": "No order data available"
},
"validation": {
"orderTypeRequired": "Please select order type",
"packageIdsRequired": "Please select at least one package",
"iotCardRequired": "Please select IoT card",
"deviceRequired": "Please select device",
"packagesMaxLimit": "Can select up to 10 packages"
}
},
"enterpriseDevices": {
"title": "Enterprise Device List",
"searchForm": {
"enterpriseId": "Enterprise ID",
"enterpriseIdPlaceholder": "Please enter enterprise ID",
"deviceNo": "Device No.",
"deviceNoPlaceholder": "Please enter device number"
},
"table": {
"deviceId": "Device ID",
"deviceNo": "Device No.",
"deviceName": "Device Name",
"deviceModel": "Device Model",
"cardCount": "Card Count",
"authorizedAt": "Authorized At",
"operation": "Actions"
},
"buttons": {
"allocateDevices": "Allocate Devices",
"recallDevices": "Recall Authorization",
"refresh": "Refresh"
},
"dialog": {
"allocateTitle": "Allocate Devices to Enterprise",
"recallTitle": "Recall Device Authorization",
"resultTitle": "Operation Result"
},
"form": {
"deviceNos": "Device Numbers",
"deviceNosPlaceholder": "Enter device numbers, separated by newlines or commas",
"deviceNosHint": "One device number per line or comma-separated, maximum 100 devices",
"remark": "Remark",
"remarkPlaceholder": "Enter allocation remark (optional)",
"selectedDevices": "Selected Devices",
"selectedCount": "{count} devices selected"
},
"result": {
"successCount": "Success Count",
"failCount": "Fail Count",
"authorizedDevices": "Authorized Devices",
"failedItems": "Failed Items",
"deviceNo": "Device No.",
"deviceId": "Device ID",
"cardCount": "Card Count",
"reason": "Failure Reason"
},
"messages": {
"allocateSuccess": "Device allocation successful",
"allocateFailed": "Device allocation failed",
"allocatePartialSuccess": "Partial success: {success} succeeded, {fail} failed",
"recallSuccess": "Authorization recall successful",
"recallFailed": "Authorization recall failed",
"recallPartialSuccess": "Partial recall success: {success} succeeded, {fail} failed",
"recallConfirm": "Recall Authorization Confirmation",
"recallConfirmText": "Are you sure to recall authorization for {count} selected devices?",
"noSelection": "Please select devices to recall first",
"deviceNosRequired": "Please enter device numbers",
"deviceNosEmpty": "Device number list cannot be empty",
"deviceNosMaxLimit": "Cannot exceed 100 device numbers",
"invalidDeviceNos": "Invalid device number format exists",
"loadFailed": "Failed to load device list",
"noData": "No device data available"
},
"validation": {
"deviceNosRequired": "Please enter device number list",
"deviceNosMaxLength": "Cannot exceed 100 device numbers"
}
}
}

View File

@@ -398,13 +398,15 @@
"cardChange": "换卡网卡"
},
"packageManagement": {
"title": "我的套餐",
"title": "套餐管理",
"packageCreate": "新建套餐",
"packageBatch": "批量管理",
"packageList": "我的套餐",
"packageList": "套餐管理",
"packageChange": "套餐变更",
"packageAssign": "套餐分配",
"packageAssign": "套餐分配",
"seriesAssign": "套餐系列分配",
"packageSeries": "套餐系列",
"myPackages": "代理可售套餐",
"packageCommission": "套餐佣金网卡"
},
"accountManagement": {
@@ -447,13 +449,15 @@
"allocationRecordDetail": "分配记录详情",
"cardReplacementRequest": "换卡申请",
"authorizationRecords": "授权记录",
"authorizationDetail": "授权记录详情"
"authorizationDetail": "授权记录详情",
"enterpriseDevices": "企业设备列表"
},
"account": {
"title": "账户管理",
"customerAccount": "客户账号",
"myAccount": "我的账户",
"carrierManagement": "运营商管理"
"carrierManagement": "运营商管理",
"orders": "订单管理"
},
"commission": {
"menu": {
@@ -751,5 +755,206 @@
"pendingAmount": "待审核金额",
"todayCommission": "今日佣金"
}
},
"seriesBinding": {
"buttons": {
"batchSetSeries": "批量设置套餐系列",
"clearBinding": "清除绑定"
},
"dialog": {
"title": "批量设置套餐系列绑定",
"titleCard": "批量设置卡的套餐系列绑定",
"titleDevice": "批量设置设备的套餐系列绑定"
},
"form": {
"seriesAllocation": "套餐系列分配",
"seriesAllocationPlaceholder": "请选择套餐系列分配",
"clearBindingOption": "清除关联",
"selectedCount": "已选择 {count} 项"
},
"messages": {
"noSelection": "请先选择要设置的项",
"setSuccess": "套餐系列绑定设置成功",
"setFailed": "套餐系列绑定设置失败",
"partialSuccess": "部分设置成功:成功 {success} 项,失败 {fail} 项",
"confirmSet": "确定要为选中的 {count} 项设置套餐系列绑定吗?",
"confirmClear": "确定要清除选中的 {count} 项的套餐系列绑定吗?"
},
"result": {
"title": "设置结果",
"successCount": "成功数量",
"failCount": "失败数量",
"failedItems": "失败项详情",
"iccid": "ICCID",
"deviceId": "设备ID",
"deviceNo": "设备号",
"reason": "失败原因"
}
},
"orderManagement": {
"title": "订单管理",
"orderList": "订单列表",
"orderDetail": "订单详情",
"orderItems": "订单明细",
"createOrder": "创建订单",
"searchForm": {
"orderNo": "订单号",
"orderNoPlaceholder": "请输入订单号",
"paymentStatus": "支付状态",
"paymentStatusPlaceholder": "请选择支付状态",
"orderType": "订单类型",
"orderTypePlaceholder": "请选择订单类型",
"dateRange": "创建时间",
"startDate": "开始时间",
"endDate": "结束时间"
},
"table": {
"id": "订单ID",
"orderNo": "订单号",
"orderType": "订单类型",
"buyerId": "买家ID",
"buyerType": "买家类型",
"paymentStatus": "支付状态",
"paymentMethod": "支付方式",
"totalAmount": "订单金额",
"paidAt": "支付时间",
"commissionStatus": "佣金状态",
"createdAt": "创建时间",
"updatedAt": "更新时间",
"operation": "操作"
},
"orderType": {
"singleCard": "单卡购买",
"device": "设备购买"
},
"buyerType": {
"personal": "个人客户",
"agent": "代理商"
},
"paymentStatus": {
"pending": "待支付",
"paid": "已支付",
"cancelled": "已取消",
"refunded": "已退款"
},
"paymentMethod": {
"wallet": "钱包支付",
"wechat": "微信支付",
"alipay": "支付宝支付"
},
"commissionStatus": {
"notApplicable": "不适用",
"pending": "待结算",
"settled": "已结算"
},
"actions": {
"viewDetail": "查看详情",
"cancel": "取消",
"confirm": "确定",
"submit": "提交",
"close": "关闭"
},
"createForm": {
"packageIds": "选择套餐",
"packageIdsPlaceholder": "请选择套餐",
"iotCardId": "IoT卡ID",
"iotCardIdPlaceholder": "请输入IoT卡ID",
"deviceId": "设备ID",
"deviceIdPlaceholder": "请输入设备ID"
},
"items": {
"packageName": "套餐名称",
"quantity": "数量",
"unitPrice": "单价",
"amount": "小计"
},
"messages": {
"createSuccess": "订单创建成功",
"createFailed": "订单创建失败",
"cancelSuccess": "订单取消成功",
"cancelFailed": "订单取消失败",
"cancelConfirm": "取消订单确认",
"cancelConfirmText": "确定要取消该订单吗?取消后不可恢复",
"cannotCancelPaid": "已支付的订单无法取消",
"cannotCancelCancelled": "订单已取消",
"cannotCancelRefunded": "已退款的订单无法取消",
"loadFailed": "加载订单数据失败",
"noData": "暂无订单数据"
},
"validation": {
"orderTypeRequired": "请选择订单类型",
"packageIdsRequired": "请至少选择一个套餐",
"iotCardRequired": "请选择IoT卡",
"deviceRequired": "请选择设备",
"packagesMaxLimit": "最多只能选择10个套餐"
}
},
"enterpriseDevices": {
"title": "企业设备列表",
"searchForm": {
"enterpriseId": "企业ID",
"enterpriseIdPlaceholder": "请输入企业ID",
"deviceNo": "设备号",
"deviceNoPlaceholder": "请输入设备号"
},
"table": {
"deviceId": "设备ID",
"deviceNo": "设备号",
"deviceName": "设备名称",
"deviceModel": "设备型号",
"cardCount": "绑定卡数量",
"authorizedAt": "授权时间",
"operation": "操作"
},
"buttons": {
"allocateDevices": "授权设备",
"recallDevices": "撤销授权",
"refresh": "刷新"
},
"dialog": {
"allocateTitle": "授权设备给企业",
"recallTitle": "撤销设备授权",
"resultTitle": "操作结果"
},
"form": {
"deviceNos": "设备号列表",
"deviceNosPlaceholder": "请输入设备号,支持换行或逗号分隔",
"deviceNosHint": "每行一个设备号或使用逗号分隔最多100个",
"remark": "备注",
"remarkPlaceholder": "请输入授权备注(可选)",
"selectedDevices": "已选择设备",
"selectedCount": "已选择 {count} 个设备"
},
"result": {
"successCount": "成功数量",
"failCount": "失败数量",
"authorizedDevices": "已授权设备",
"failedItems": "失败项",
"deviceNo": "设备号",
"deviceId": "设备ID",
"cardCount": "绑定卡数",
"reason": "失败原因"
},
"messages": {
"allocateSuccess": "设备授权成功",
"allocateFailed": "设备授权失败",
"allocatePartialSuccess": "部分设备授权成功:成功 {success} 个,失败 {fail} 个",
"recallSuccess": "撤销授权成功",
"recallFailed": "撤销授权失败",
"recallPartialSuccess": "部分设备撤销成功:成功 {success} 个,失败 {fail} 个",
"recallConfirm": "撤销授权确认",
"recallConfirmText": "确定要撤销选中的 {count} 个设备的授权吗?",
"noSelection": "请先选择要撤销的设备",
"deviceNosRequired": "请输入设备号",
"deviceNosEmpty": "设备号列表不能为空",
"deviceNosMaxLimit": "设备号数量不能超过100个",
"invalidDeviceNos": "存在无效的设备号格式",
"loadFailed": "加载设备列表失败",
"noData": "暂无设备数据"
},
"validation": {
"deviceNosRequired": "请输入设备号列表",
"deviceNosMaxLength": "设备号数量不能超过100个"
}
}
}

View File

@@ -694,6 +694,62 @@ export const asyncRoutes: AppRouteRecord[] = [
}
]
},
{
path: '/package-management',
name: 'PackageManagement',
component: RoutesAlias.Home,
meta: {
title: 'menus.packageManagement.title',
icon: '&#xe81a;'
},
children: [
{
path: 'package-series',
name: 'PackageSeries',
component: RoutesAlias.PackageSeries,
meta: {
title: 'menus.packageManagement.packageSeries',
keepAlive: true
}
},
{
path: 'package-list',
name: 'PackageList',
component: RoutesAlias.PackageList,
meta: {
title: 'menus.packageManagement.packageList',
keepAlive: true
}
},
{
path: 'my-packages',
name: 'MyPackages',
component: RoutesAlias.MyPackages,
meta: {
title: 'menus.packageManagement.myPackages',
keepAlive: true
}
},
{
path: 'package-assign',
name: 'PackageAssign',
component: RoutesAlias.PackageAssign,
meta: {
title: 'menus.packageManagement.packageAssign',
keepAlive: true
}
},
{
path: 'series-assign',
name: 'SeriesAssign',
component: RoutesAlias.SeriesAssign,
meta: {
title: 'menus.packageManagement.seriesAssign',
keepAlive: true
}
}
]
},
{
path: '/account-management',
name: 'AccountManagement',
@@ -954,6 +1010,16 @@ export const asyncRoutes: AppRouteRecord[] = [
isHide: true,
keepAlive: false
}
},
{
path: 'enterprise-devices',
name: 'EnterpriseDevices',
component: RoutesAlias.EnterpriseDevices,
meta: {
title: 'menus.assetManagement.enterpriseDevices',
isHide: true,
keepAlive: false
}
}
]
},
@@ -993,6 +1059,15 @@ export const asyncRoutes: AppRouteRecord[] = [
keepAlive: true
}
},
{
path: 'orders',
name: 'OrderManagement',
component: RoutesAlias.OrderList,
meta: {
title: 'menus.account.orders',
keepAlive: true
}
},
// {
// path: 'my-account',
// name: 'MyAccount',

View File

@@ -67,10 +67,12 @@ export enum RoutesAlias {
// 我的套餐
PackageCreate = '/package-management/package-create', // 新建套餐
PackageBatch = '/package-management/package-batch', // 批量管理
PackageList = '/package-management/package-list', // 我的套餐
PackageList = '/package-management/package-list', // 套餐管理
PackageChange = '/package-management/package-change', // 套餐变更
PackageAssign = '/package-management/package-assign', // 套餐分配
PackageAssign = '/package-management/package-assign', // 套餐分配
SeriesAssign = '/package-management/series-assign', // 套餐系列分配
PackageSeries = '/package-management/package-series', // 套餐系列
MyPackages = '/package-management/my-packages', // 代理可售套餐
PackageCommission = '/package-management/package-commission', // 套餐佣金网卡
// 账号管理
@@ -102,11 +104,13 @@ export enum RoutesAlias {
CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请
AuthorizationRecords = '/asset-management/authorization-records', // 授权记录
AuthorizationDetail = '/asset-management/authorization-detail', // 授权记录详情
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备列表
// 账户管理
CustomerAccountList = '/finance/customer-account', // 客户账号
MyAccount = '/finance/my-account', // 我的账户
CarrierManagement = '/finance/carrier-management', // 运营商管理
OrderList = '/order-management/order-list', // 订单管理
// 佣金管理
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批

View File

@@ -478,3 +478,24 @@ export interface IotCardDetailResponse {
created_at: string // 创建时间
updated_at: string // 更新时间
}
// ========== 批量设置卡的套餐系列绑定相关 ==========
// 批量设置卡的套餐系列绑定请求参数
export interface BatchSetCardSeriesBindingRequest {
iccids: string[] // ICCID列表最多500个
series_allocation_id: number // 套餐系列分配ID0表示清除关联
}
// 卡套餐系列绑定失败项
export interface CardSeriesBindingFailedItem {
iccid: string // ICCID
reason: string // 失败原因
}
// 批量设置卡的套餐系列绑定响应
export interface BatchSetCardSeriesBindingResponse {
success_count: number // 成功数量
fail_count: number // 失败数量
failed_items: CardSeriesBindingFailedItem[] | null // 失败详情列表
}

View File

@@ -119,7 +119,6 @@ export interface CreateWithdrawalSettingParams {
min_withdrawal_amount: number // 最低提现金额(分)
fee_rate: number // 手续费比率(基点,100=1%)
daily_withdrawal_limit: number // 每日提现次数限制
arrival_days: number // 到账天数
}
// ==================== 佣金记录相关 ====================
@@ -248,3 +247,50 @@ export interface ShopCommissionSummaryPageResult {
size: number
total: number
}
// ==================== 我的佣金统计相关 ====================
/**
* 我的佣金统计查询参数
*/
export interface MyCommissionStatsQueryParams {
shop_id?: number // 店铺ID
start_time?: string // 开始时间
end_time?: string // 结束时间
}
/**
* 我的佣金统计响应
*/
export interface MyCommissionStatsResponse {
cost_diff_amount: number // 成本价差收入(分)
cost_diff_count: number // 成本价差笔数
cost_diff_percent: number // 成本价差占比(千分比)
one_time_amount: number // 一次性佣金收入(分)
one_time_count: number // 一次性佣金笔数
one_time_percent: number // 一次性佣金占比(千分比)
tier_bonus_amount: number // 梯度奖励收入(分)
tier_bonus_count: number // 梯度奖励笔数
tier_bonus_percent: number // 梯度奖励占比(千分比)
total_amount: number // 总收入(分)
total_count: number // 总笔数
}
/**
* 我的每日佣金统计查询参数
*/
export interface MyDailyCommissionStatsQueryParams {
shop_id?: number // 店铺ID
start_date?: string // 开始日期YYYY-MM-DD
end_date?: string // 结束日期YYYY-MM-DD
days?: number // 查询天数默认30天最大365天
}
/**
* 每日佣金统计项
*/
export interface DailyCommissionStatsItem {
date: string // 日期YYYY-MM-DD
total_amount: number // 当日总收入(分)
total_count: number // 当日总笔数
}

View File

@@ -29,11 +29,25 @@ export interface PaginationData<T> {
pages?: number
}
// 新版分页响应数据(使用 list 字段)
export interface PaginationDataV2<T> {
list: T[] // 新版API使用 list 字段
total: number
page_size: number
page: number
total_pages: number // 总页数
}
// 分页响应
export interface PaginationResponse<T = any> extends BaseResponse {
data: PaginationData<T>
}
// 新版分页响应(使用 list 字段)
export interface PaginationResponseV2<T = any> extends BaseResponse {
data: PaginationDataV2<T>
}
// 列表响应
export interface ListResponse<T = any> extends BaseResponse {
data: T[]

View File

@@ -202,3 +202,25 @@ export interface DeviceImportTaskDetail extends DeviceImportTask {
failed_items: DeviceImportResultItem[] | null // 失败记录详情
skipped_items: DeviceImportResultItem[] | null // 跳过记录详情
}
// ========== 批量设置设备的套餐系列绑定相关 ==========
// 批量设置设备的套餐系列绑定请求参数
export interface BatchSetDeviceSeriesBindingRequest {
device_ids: number[] // 设备ID列表最多500个
series_allocation_id: number // 套餐系列分配ID0表示清除关联
}
// 设备套餐系列绑定失败项
export interface DeviceSeriesBindingFailedItem {
device_id: number // 设备ID
device_no: string // 设备号
reason: string // 失败原因
}
// 批量设置设备的套餐系列绑定响应
export interface BatchSetDeviceSeriesBindingResponse {
success_count: number // 成功数量
fail_count: number // 失败数量
failed_items: DeviceSeriesBindingFailedItem[] | null // 失败详情列表
}

View File

@@ -0,0 +1,89 @@
/**
* 企业设备授权相关类型定义
*/
import { PaginationParams } from '@/types'
// ==================== 企业设备列表相关 ====================
/**
* 企业设备列表项
*/
export interface EnterpriseDeviceItem {
device_id: number // 设备ID
device_no: string // 设备号
device_name: string // 设备名称
device_model: string // 设备型号
card_count: number // 绑定卡数量
authorized_at: string // 授权时间
}
/**
* 企业设备列表查询参数
*/
export interface EnterpriseDeviceListParams extends PaginationParams {
device_no?: string // 设备号(模糊搜索)
}
/**
* 企业设备列表分页结果
*/
export interface EnterpriseDevicePageResult {
list: EnterpriseDeviceItem[] | null // 设备列表
total: number // 总数
}
// ==================== 授权设备相关 ====================
/**
* 授权设备请求参数
*/
export interface AllocateDevicesRequest {
device_nos: string[] | null // 设备号列表最多100个
remark?: string // 授权备注
}
/**
* 已授权设备项
*/
export interface AuthorizedDeviceItem {
device_id: number // 设备ID
device_no: string // 设备号
card_count: number // 绑定卡数量
}
/**
* 失败设备项
*/
export interface FailedDeviceItem {
device_no: string // 设备号
reason: string // 失败原因
}
/**
* 授权设备响应
*/
export interface AllocateDevicesResponse {
success_count: number // 成功数量
fail_count: number // 失败数量
authorized_devices: AuthorizedDeviceItem[] | null // 已授权设备列表
failed_items: FailedDeviceItem[] | null // 失败项列表
}
// ==================== 撤销授权相关 ====================
/**
* 撤销设备授权请求参数
*/
export interface RecallDevicesRequest {
device_nos: string[] | null // 设备号列表最多100个
}
/**
* 撤销设备授权响应
*/
export interface RecallDevicesResponse {
success_count: number // 成功数量
fail_count: number // 失败数量
failed_items: FailedDeviceItem[] | null // 失败项列表
}

View File

@@ -29,8 +29,23 @@ export * from './shop'
// 网卡相关
export * from './card'
// 套餐相关
export * from './package'
// 套餐相关(旧)- 为避免冲突,保留旧类型但使用别名
export type {
PackageStatus,
PackageType as OldPackageType,
PackageSeries as OldPackageSeries,
Package,
PackageQueryParams as OldPackageQueryParams,
PackageSeriesQueryParams as OldPackageSeriesQueryParams,
PackageSeriesFormData,
PackageFormData,
PackageAssignParams,
PackageChangeRecord,
PackageAssignRecord
} from './package'
// 套餐管理系统相关(新)
export * from './packageManagement'
// 设备相关
export * from './device'
@@ -53,5 +68,11 @@ export * from './authorization'
// 企业卡授权相关
export * from './enterpriseCard'
// 企业设备授权相关
export * from './enterpriseDevice'
// 运营商相关
export * from './carrier'
// 订单相关
export * from './order'

90
src/types/api/order.ts Normal file
View File

@@ -0,0 +1,90 @@
/**
* 订单管理相关类型定义
*/
// 支付状态枚举
export enum PaymentStatus {
PENDING = 1, // 待支付
PAID = 2, // 已支付
CANCELLED = 3, // 已取消
REFUNDED = 4 // 已退款
}
// 订单类型
export type OrderType = 'single_card' | 'device'
// 买家类型
export type BuyerType = 'personal' | 'agent'
// 订单支付方式
export type OrderPaymentMethod = 'wallet' | 'wechat' | 'alipay'
// 订单佣金状态
export enum OrderCommissionStatus {
NOT_APPLICABLE = 0, // 不适用
PENDING = 1, // 待结算
SETTLED = 2 // 已结算
}
// 订单明细
export interface OrderItem {
id: number
package_id: number
package_name: string
quantity: number
unit_price: number // 单价(分)
amount: number // 小计金额(分)
}
// 订单响应
export interface Order {
id: number
order_no: string
order_type: OrderType
buyer_id: number
buyer_type: BuyerType
payment_status: PaymentStatus
payment_status_text: string
payment_method: OrderPaymentMethod
total_amount: number // 订单总金额(分)
paid_at: string | null
commission_status: OrderCommissionStatus
commission_config_version: number
device_id: number | null
iot_card_id: number | null
items: OrderItem[] | null
created_at: string
updated_at: string
}
// 订单查询参数
export interface OrderQueryParams {
page?: number
page_size?: number
payment_status?: PaymentStatus
order_type?: OrderType
order_no?: string
start_time?: string
end_time?: string
dateRange?: string[] | any // For date range picker in UI
}
// 订单列表响应
export interface OrderListResponse {
items: Order[]
page: number
page_size: number
total: number
total_pages: number
}
// 创建订单请求
export interface CreateOrderRequest {
order_type: OrderType
package_ids: number[]
iot_card_id?: number | null
device_id?: number | null
}
// 创建订单响应 (返回订单详情)
export type CreateOrderResponse = Order

View File

@@ -0,0 +1,332 @@
/**
* 套餐管理系统类型定义
* 使用下划线命名与后端 API 保持一致
*/
import { PaginationParams } from './common'
// ==================== 套餐系列管理 ====================
/**
* 套餐系列响应
*/
export interface PackageSeriesResponse {
id: number
series_code: string
series_name: string
description?: string
status: number // 1:启用, 2:禁用
created_at: string
updated_at: string
}
/**
* 套餐系列查询参数
*/
export interface PackageSeriesQueryParams extends PaginationParams {
series_name?: string // 系列名称(模糊搜索)
status?: number // 状态筛选
}
/**
* 创建套餐系列请求
*/
export interface CreatePackageSeriesRequest {
series_code: string // 系列编码,必填
series_name: string // 系列名称,必填
description?: string // 描述,可选
}
/**
* 更新套餐系列请求
*/
export interface UpdatePackageSeriesRequest {
series_code?: string
series_name?: string
description?: string
}
/**
* 更新套餐系列状态请求
*/
export interface UpdatePackageSeriesStatusRequest {
status: number // 1:启用, 2:禁用
}
// ==================== 套餐管理 ====================
/**
* 套餐响应
*/
export interface PackageResponse {
id: number
package_code: string
package_name: string
series_id: number
series_name?: string
package_type: string // 'formal':正式套餐, 'addon':加油包
data_type: string // 'real':真实流量, 'virtual':虚拟流量
real_data_mb: number // 真流量额度MB
virtual_data_mb: number // 虚流量额度MB
duration_months: number // 有效期(月)
price: number // 价格(分)
shelf_status: number // 上架状态 (1:上架, 2:下架)
status: number // 状态 (1:启用, 2:禁用)
description?: string
created_at: string
updated_at: string
}
/**
* 套餐查询参数
*/
export interface PackageQueryParams extends PaginationParams {
package_name?: string // 套餐名称(模糊搜索)
series_id?: number // 系列ID
package_type?: string // 套餐类型
data_type?: string // 流量类型
shelf_status?: number // 上架状态
status?: number // 状态
}
/**
* 创建套餐请求
*/
export interface CreatePackageRequest {
package_code: string // 套餐编码,必填
package_name: string // 套餐名称,必填
series_id: number // 所属系列ID必填
package_type: string // 套餐类型,必填
data_type: string // 流量类型,必填
real_data_mb: number // 真流量额度MB必填
virtual_data_mb: number // 虚流量额度MB必填
duration_months: number // 有效期(月),必填
price: number // 价格(分),必填
description?: string // 描述,可选
}
/**
* 更新套餐请求
*/
export interface UpdatePackageRequest {
package_code?: string
package_name?: string
series_id?: number
package_type?: string
data_type?: string
real_data_mb?: number
virtual_data_mb?: number
duration_months?: number
price?: number
description?: string
}
/**
* 更新套餐状态请求
*/
export interface UpdatePackageStatusRequest {
status: number // 1:启用, 2:禁用
}
/**
* 更新套餐上架状态请求
*/
export interface UpdatePackageShelfStatusRequest {
shelf_status: number // 1:上架, 2:下架
}
/**
* 系列下拉选项响应
*/
export interface SeriesSelectOption {
id: number
series_name: string
series_code: string
}
// ==================== 代理可售套餐 ====================
/**
* 我的可售套餐响应
*/
export interface MyPackageResponse {
id: number
package_code: string
package_name: string
series_id: number
series_name: string
package_type: string
data_type: string
real_data_mb: number // 真流量额度MB
virtual_data_mb: number // 虚流量额度MB
duration_months: number
price: number // 套餐原价(分)
cost_price: number // 成本价(分)
suggested_retail_price: number // 建议售价(分)
profit_margin: number // 利润空间(分)
price_source: string // 价格来源:'series_pricing':系列加价, 'package_override':单套餐覆盖
shelf_status: number
status: number
description?: string
created_at: string
updated_at: string
}
/**
* 我的可售套餐查询参数
*/
export interface MyPackageQueryParams extends PaginationParams {
series_id?: number // 系列ID筛选
package_type?: string // 套餐类型筛选
}
/**
* 我的被分配系列响应
*/
export interface MySeriesAllocationResponse {
id: number // 分配ID
series_id: number
series_code: string
series_name: string
pricing_mode: string // 定价模式:'fixed':固定金额, 'percent':百分比
pricing_value: number // 定价值
allocator_shop_name: string // 分配者店铺名称
package_count: number // 可售套餐数量
status: number
created_at: string
}
// ==================== 单套餐分配 ====================
/**
* 单套餐分配响应
*/
export interface ShopPackageAllocationResponse {
id: number
package_id: number
package_code: string
package_name: string
shop_id: number
shop_name: string
cost_price: number // 覆盖的成本价(分)
calculated_cost_price: number // 原计算成本价(分,供参考)
allocation_id?: number // 关联的系列分配ID
status: number // 1:启用, 2:禁用
created_at: string
updated_at: string
}
/**
* 单套餐分配查询参数
*/
export interface ShopPackageAllocationQueryParams extends PaginationParams {
shop_id?: number // 店铺ID筛选
package_id?: number // 套餐ID筛选
status?: number // 状态筛选
}
/**
* 创建单套餐分配请求
*/
export interface CreateShopPackageAllocationRequest {
package_id: number // 套餐ID必填
shop_id: number // 店铺ID必填
cost_price: number // 覆盖的成本价(分),必填
}
/**
* 更新单套餐分配请求
*/
export interface UpdateShopPackageAllocationRequest {
cost_price: number // 只允许修改成本价
}
/**
* 更新单套餐分配状态请求
*/
export interface UpdateShopPackageAllocationStatusRequest {
status: number // 1:启用, 2:禁用
}
// ==================== 套餐系列分配 ====================
/**
* 基础返佣配置
*/
export interface BaseCommissionConfig {
mode: 'fixed' | 'percent' // 返佣模式:'fixed':固定金额, 'percent':百分比
value: number // 返佣值:固定金额(分)或百分比的千分比(如200表示20%)
}
/**
* 梯度档位配置
*/
export interface TierEntry {
threshold: number // 阈值(销量或销售额)
mode: 'fixed' | 'percent' // 返佣模式
value: number // 返佣值
}
/**
* 梯度返佣配置
*/
export interface TierCommissionConfig {
period_type: 'monthly' | 'quarterly' | 'yearly' // 周期类型
tier_type: 'sales_count' | 'sales_amount' // 梯度类型:销量或销售额
tiers: TierEntry[] // 梯度档位数组
}
/**
* 套餐系列分配响应
*/
export interface ShopSeriesAllocationResponse {
id: number
series_id: number
series_name: string
shop_id: number
shop_name: string
allocator_shop_id: number // 分配者店铺ID
allocator_shop_name: string // 分配者店铺名称
base_commission: BaseCommissionConfig // 基础返佣配置
enable_tier_commission: boolean // 是否启用梯度返佣
tier_config?: TierCommissionConfig // 梯度返佣配置(可选)
status: number // 1:启用, 2:禁用
created_at: string
updated_at: string
}
/**
* 套餐系列分配查询参数
*/
export interface ShopSeriesAllocationQueryParams extends PaginationParams {
shop_id?: number // 店铺ID筛选
series_id?: number // 系列ID筛选
status?: number // 状态筛选
}
/**
* 创建套餐系列分配请求
*/
export interface CreateShopSeriesAllocationRequest {
series_id: number // 套餐系列ID必填
shop_id: number // 店铺ID必填
base_commission: BaseCommissionConfig // 基础返佣配置,必填
enable_tier_commission?: boolean // 是否启用梯度返佣可选默认false
tier_config?: TierCommissionConfig // 梯度返佣配置当enable_tier_commission为true时必填
}
/**
* 更新套餐系列分配请求
*/
export interface UpdateShopSeriesAllocationRequest {
base_commission?: BaseCommissionConfig // 基础返佣配置
enable_tier_commission?: boolean // 是否启用梯度返佣
tier_config?: TierCommissionConfig // 梯度返佣配置
}
/**
* 更新套餐系列分配状态请求
*/
export interface UpdateShopSeriesAllocationStatusRequest {
status: number // 1:启用, 2:禁用
}

View File

@@ -222,10 +222,11 @@ function handleErrorMessage(error: any, mode: ErrorMessageMode = 'message') {
async function request<T = any>(config: ExtendedRequestConfig): Promise<T> {
const processedConfig = processRequestConfig(config)
// 对 POST | PUT 请求特殊处理
// 对 POST | PUT | PATCH 请求特殊处理
if (
processedConfig.method?.toUpperCase() === 'POST' ||
processedConfig.method?.toUpperCase() === 'PUT'
processedConfig.method?.toUpperCase() === 'PUT' ||
processedConfig.method?.toUpperCase() === 'PATCH'
) {
// 如果已经有 data则保留原有的 data
if (processedConfig.params && !processedConfig.data) {
@@ -258,6 +259,9 @@ const api = {
put<T>(config: ExtendedRequestConfig): Promise<T> {
return request({ ...config, method: 'PUT' }) // PUT 请求
},
patch<T>(config: ExtendedRequestConfig): Promise<T> {
return request({ ...config, method: 'PATCH' }) // PATCH 请求
},
del<T>(config: ExtendedRequestConfig): Promise<T> {
return request({ ...config, method: 'DELETE' }) // DELETE 请求
},

View File

@@ -427,7 +427,7 @@
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请输入账号名称', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },

View File

@@ -536,7 +536,7 @@
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请输入账号名称', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },

View File

@@ -497,7 +497,7 @@
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },

View File

@@ -25,6 +25,9 @@
<ElButton type="warning" :disabled="selectedCards.length === 0" @click="showRecallDialog">
批量回收
</ElButton>
<ElButton type="info" :disabled="selectedCards.length === 0" @click="showSeriesBindingDialog">
批量设置套餐系列
</ElButton>
</template>
</ArtTableHeader>
@@ -259,6 +262,75 @@
</template>
</ElDialog>
<!-- 批量设置套餐系列绑定对话框 -->
<ElDialog
v-model="seriesBindingDialogVisible"
title="批量设置套餐系列绑定"
width="600px"
@close="handleSeriesBindingDialogClose"
>
<ElForm ref="seriesBindingFormRef" :model="seriesBindingForm" :rules="seriesBindingRules" label-width="120px">
<ElFormItem label="已选择卡数">
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem label="套餐系列分配" prop="series_allocation_id">
<ElSelect
v-model="seriesBindingForm.series_allocation_id"
placeholder="请选择套餐系列分配(选择清除关联将解除绑定)"
style="width: 100%"
:loading="seriesLoading"
>
<ElOption label="清除关联" :value="0" />
<ElOption
v-for="series in seriesAllocationList"
:key="series.id"
:label="`${series.series_name} (${series.shop_name})`"
:value="series.id"
:disabled="series.status !== 1"
/>
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="seriesBindingDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSeriesBinding" :loading="seriesBindingLoading">
确认设置
</ElButton>
</div>
</template>
</ElDialog>
<!-- 套餐系列绑定结果对话框 -->
<ElDialog
v-model="seriesBindingResultDialogVisible"
title="设置结果"
width="700px"
>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="成功数">
<ElTag type="success">{{ seriesBindingResult.success_count }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<ElTag type="danger">{{ seriesBindingResult.fail_count }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<div v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0" style="margin-top: 20px">
<ElDivider content-position="left">失败项详情</ElDivider>
<ElTable :data="seriesBindingResult.failed_items" border max-height="300">
<ElTableColumn prop="iccid" label="ICCID" width="180" />
<ElTableColumn prop="reason" label="失败原因" />
</ElTable>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="seriesBindingResultDialogVisible = false">确定</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -267,6 +339,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { CardService, StorageService } from '@/api/modules'
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
import { ElMessage, ElTag, ElUpload } from 'element-plus'
import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
import type { SearchFormItem } from '@/types'
@@ -277,8 +350,10 @@
StandaloneCardStatus,
AllocateStandaloneCardsRequest,
RecallStandaloneCardsRequest,
AllocateStandaloneCardsResponse
AllocateStandaloneCardsResponse,
BatchSetCardSeriesBindingResponse
} from '@/types/api/card'
import type { ShopSeriesAllocationResponse } from '@/types/api'
defineOptions({ name: 'StandaloneCardList' })
@@ -306,6 +381,25 @@
failed_items: null
})
// 套餐系列绑定相关
const seriesBindingDialogVisible = ref(false)
const seriesBindingLoading = ref(false)
const seriesBindingResultDialogVisible = ref(false)
const seriesBindingFormRef = ref<FormInstance>()
const seriesLoading = ref(false)
const seriesAllocationList = ref<ShopSeriesAllocationResponse[]>([])
const seriesBindingForm = reactive({
series_allocation_id: undefined as number | undefined
})
const seriesBindingRules = reactive<FormRules>({
series_allocation_id: [{ required: true, message: '请选择套餐系列分配', trigger: 'change' }]
})
const seriesBindingResult = ref<BatchSetCardSeriesBindingResponse>({
success_count: 0,
fail_count: 0,
failed_items: null
})
// 搜索表单初始值
const initialSearchState = {
status: undefined,
@@ -937,6 +1031,102 @@
}
})
}
// 显示套餐系列绑定对话框
const showSeriesBindingDialog = async () => {
if (selectedCards.value.length === 0) {
ElMessage.warning('请先选择要设置的卡')
return
}
// 加载套餐系列分配列表
await loadSeriesAllocationList()
seriesBindingDialogVisible.value = true
seriesBindingForm.series_allocation_id = undefined
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}
}
// 加载套餐系列分配列表
const loadSeriesAllocationList = async () => {
seriesLoading.value = true
try {
const res = await ShopSeriesAllocationService.getShopSeriesAllocations({
page: 1,
page_size: 1000 // 获取所有可用的套餐系列分配
})
if (res.code === 0 && res.data.list) {
seriesAllocationList.value = res.data.list
}
} catch (error) {
console.error(error)
ElMessage.error('获取套餐系列分配列表失败')
} finally {
seriesLoading.value = false
}
}
// 关闭套餐系列绑定对话框
const handleSeriesBindingDialogClose = () => {
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}
}
// 执行套餐系列绑定
const handleSeriesBinding = async () => {
if (!seriesBindingFormRef.value) return
await seriesBindingFormRef.value.validate(async (valid) => {
if (valid) {
const iccids = selectedCards.value.map((card) => card.iccid)
if (iccids.length === 0) {
ElMessage.warning('请先选择要设置的卡')
return
}
seriesBindingLoading.value = true
try {
const res = await CardService.batchSetCardSeriesBinding({
iccids,
series_allocation_id: seriesBindingForm.series_allocation_id!
})
if (res.code === 0) {
seriesBindingResult.value = res.data
seriesBindingDialogVisible.value = false
seriesBindingResultDialogVisible.value = true
// 清空选择
if (tableRef.value) {
tableRef.value.clearSelection()
}
selectedCards.value = []
// 刷新列表
getTableData()
// 显示消息提示
if (res.data.fail_count === 0) {
ElMessage.success('套餐系列绑定设置成功')
} else if (res.data.success_count === 0) {
ElMessage.error('套餐系列绑定设置失败')
} else {
ElMessage.warning(`部分设置成功:成功 ${res.data.success_count} 项,失败 ${res.data.fail_count}`)
}
}
} catch (error) {
console.error(error)
ElMessage.error('套餐系列绑定设置失败,请重试')
} finally {
seriesBindingLoading.value = false
}
}
})
}
</script>
<style lang="scss" scoped>

View File

@@ -27,6 +27,9 @@
<ElButton @click="handleBatchRecall" :disabled="!selectedDevices.length">
批量回收
</ElButton>
<ElButton type="info" @click="handleBatchSetSeries" :disabled="!selectedDevices.length">
批量设置套餐系列
</ElButton>
<ElButton @click="handleImportDevice">导入设备</ElButton>
</template>
</ArtTableHeader>
@@ -177,6 +180,71 @@
</div>
</template>
</ElDialog>
<!-- 批量设置套餐系列绑定对话框 -->
<ElDialog v-model="seriesBindingDialogVisible" title="批量设置设备套餐系列绑定" width="600px">
<ElForm ref="seriesBindingFormRef" :model="seriesBindingForm" :rules="seriesBindingRules" label-width="120px">
<ElFormItem label="已选设备数">
<span style="color: #409eff; font-weight: bold">{{ selectedDevices.length }}</span>
</ElFormItem>
<ElFormItem label="套餐系列分配" prop="series_allocation_id">
<ElSelect
v-model="seriesBindingForm.series_allocation_id"
placeholder="请选择套餐系列分配(选择清除关联将解除绑定)"
style="width: 100%"
:loading="seriesLoading"
>
<ElOption label="清除关联" :value="0" />
<ElOption
v-for="series in seriesAllocationList"
:key="series.id"
:label="`${series.series_name} (${series.shop_name})`"
:value="series.id"
:disabled="series.status !== 1"
/>
</ElSelect>
</ElFormItem>
</ElForm>
<!-- 设置结果 -->
<div v-if="seriesBindingResult" style="margin-top: 20px">
<ElAlert
:type="seriesBindingResult.fail_count === 0 ? 'success' : 'warning'"
:closable="false"
style="margin-bottom: 10px"
>
<template #title>
成功设置 {{ seriesBindingResult.success_count }} 失败 {{ seriesBindingResult.fail_count }}
</template>
</ElAlert>
<div v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0">
<div style="margin-bottom: 10px; font-weight: bold">失败详情</div>
<div
v-for="item in seriesBindingResult.failed_items"
:key="item.device_id"
style="margin-bottom: 8px; color: #f56c6c; font-size: 12px"
>
设备号: {{ item.device_no }} - {{ item.reason }}
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="handleCloseSeriesBindingDialog">
{{ seriesBindingResult ? '关闭' : '取消' }}
</ElButton>
<ElButton
v-if="!seriesBindingResult"
type="primary"
@click="handleConfirmSeriesBinding"
:loading="seriesBindingLoading"
>
确认设置
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -186,19 +254,22 @@
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { DeviceService, ShopService } from '@/api/modules'
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
Device,
DeviceStatus,
AllocateDevicesResponse,
RecallDevicesResponse
RecallDevicesResponse,
BatchSetDeviceSeriesBindingResponse
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants'
import type { ShopSeriesAllocationResponse } from '@/types/api'
defineOptions({ name: 'DeviceList' })
@@ -216,6 +287,20 @@
const allocateResult = ref<AllocateDevicesResponse | null>(null)
const recallResult = ref<RecallDevicesResponse | null>(null)
// 套餐系列绑定相关
const seriesBindingDialogVisible = ref(false)
const seriesBindingLoading = ref(false)
const seriesBindingFormRef = ref<FormInstance>()
const seriesLoading = ref(false)
const seriesAllocationList = ref<ShopSeriesAllocationResponse[]>([])
const seriesBindingForm = reactive({
series_allocation_id: undefined as number | undefined
})
const seriesBindingRules = reactive<FormRules>({
series_allocation_id: [{ required: true, message: '请选择套餐系列分配', trigger: 'change' }]
})
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
// 搜索表单初始值
const initialSearchState = {
device_no: '',
@@ -665,6 +750,81 @@
const handleImportDevice = () => {
router.push('/batch/device-import')
}
// 批量设置套餐系列
const handleBatchSetSeries = async () => {
if (selectedDevices.value.length === 0) {
ElMessage.warning('请先选择要设置的设备')
return
}
seriesBindingForm.series_allocation_id = undefined
seriesBindingResult.value = null
await loadSeriesAllocationList()
seriesBindingDialogVisible.value = true
}
// 加载套餐系列分配列表
const loadSeriesAllocationList = async () => {
seriesLoading.value = true
try {
const res = await ShopSeriesAllocationService.getShopSeriesAllocations({
page: 1,
page_size: 1000
})
if (res.code === 0 && res.data.list) {
seriesAllocationList.value = res.data.list
}
} catch (error) {
console.error('获取套餐系列分配列表失败:', error)
ElMessage.error('获取套餐系列分配列表失败')
} finally {
seriesLoading.value = false
}
}
// 确认设置套餐系列绑定
const handleConfirmSeriesBinding = async () => {
if (!seriesBindingFormRef.value) return
await seriesBindingFormRef.value.validate(async (valid) => {
if (valid) {
seriesBindingLoading.value = true
try {
const data = {
device_ids: selectedDevices.value.map((d) => d.id),
series_allocation_id: seriesBindingForm.series_allocation_id!
}
const res = await DeviceService.batchSetDeviceSeriesBinding(data)
if (res.code === 0) {
seriesBindingResult.value = res.data
if (res.data.fail_count === 0) {
ElMessage.success('全部设置成功')
setTimeout(() => {
handleCloseSeriesBindingDialog()
getTableData()
}, 1500)
} else {
ElMessage.warning(`部分设置失败,请查看失败详情`)
}
}
} catch (error) {
console.error(error)
ElMessage.error('设置套餐系列绑定失败')
} finally {
seriesBindingLoading.value = false
}
}
})
}
// 关闭套餐系列绑定对话框
const handleCloseSeriesBindingDialog = () => {
seriesBindingDialogVisible.value = false
seriesBindingResult.value = null
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}
}
</script>
<style scoped lang="scss">

View File

@@ -0,0 +1,642 @@
<template>
<ArtTableFullScreen>
<div class="enterprise-devices-page" id="table-full-screen">
<!-- 企业信息卡片 -->
<ElCard shadow="never" style="margin-bottom: 16px">
<template #header>
<div class="card-header">
<span>{{ $t('enterpriseDevices.title') }}</span>
<ElButton @click="goBack">{{ $t('common.cancel') }}</ElButton>
</div>
</template>
<ElDescriptions :column="3" border v-if="enterpriseInfo">
<ElDescriptionsItem label="企业名称">{{
enterpriseInfo.enterprise_name
}}</ElDescriptionsItem>
<ElDescriptionsItem label="企业编号">{{
enterpriseInfo.enterprise_code
}}</ElDescriptionsItem>
<ElDescriptionsItem label="联系人">{{ enterpriseInfo.contact_name }}</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<!-- 搜索栏 -->
<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 type="primary" @click="showAllocateDialog">{{
$t('enterpriseDevices.buttons.allocateDevices')
}}</ElButton>
<ElButton
type="warning"
:disabled="selectedDevices.length === 0"
@click="showRecallDialog"
>
{{ $t('enterpriseDevices.buttons.recallDevices') }}
</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="device_id"
:loading="loading"
:data="deviceList"
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
>
<template #default>
<ElTableColumn type="selection" width="55" />
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 授权设备对话框 -->
<ElDialog
v-model="allocateDialogVisible"
:title="$t('enterpriseDevices.dialog.allocateTitle')"
width="700px"
@close="handleAllocateDialogClose"
>
<ElForm
ref="allocateFormRef"
:model="allocateForm"
:rules="allocateRules"
label-width="120px"
>
<ElFormItem :label="$t('enterpriseDevices.form.deviceNos')" prop="device_nos">
<ElInput
v-model="deviceNosText"
type="textarea"
:rows="6"
:placeholder="$t('enterpriseDevices.form.deviceNosPlaceholder')"
@input="handleDeviceNosChange"
/>
<div style="color: var(--el-color-info); margin-top: 4px; font-size: 12px">
{{
$t('enterpriseDevices.form.selectedCount', {
count: allocateForm.device_nos?.length || 0
})
}}
</div>
</ElFormItem>
<ElFormItem :label="$t('enterpriseDevices.form.remark')">
<ElInput
v-model="allocateForm.remark"
type="textarea"
:rows="3"
:placeholder="$t('enterpriseDevices.form.remarkPlaceholder')"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="allocateDialogVisible = false">{{
$t('common.cancel')
}}</ElButton>
<ElButton type="primary" @click="handleAllocate" :loading="allocateLoading">
{{ $t('common.confirm') }}
</ElButton>
</div>
</template>
</ElDialog>
<!-- 撤销授权对话框 -->
<ElDialog
v-model="recallDialogVisible"
:title="$t('enterpriseDevices.dialog.recallTitle')"
width="600px"
@close="handleRecallDialogClose"
>
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules">
<ElFormItem :label="$t('enterpriseDevices.form.selectedDevices')">
<div>
{{
$t('enterpriseDevices.form.selectedCount', {
count: selectedDevices.length
})
}}
</div>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="recallDialogVisible = false">{{ $t('common.cancel') }}</ElButton>
<ElButton type="primary" @click="handleRecall" :loading="recallLoading">
{{ $t('common.confirm') }}
</ElButton>
</div>
</template>
</ElDialog>
<!-- 结果对话框 -->
<ElDialog
v-model="resultDialogVisible"
:title="$t('enterpriseDevices.dialog.resultTitle')"
width="700px"
>
<ElDescriptions :column="2" border>
<ElDescriptionsItem :label="$t('enterpriseDevices.result.successCount')">
<ElTag type="success">{{ operationResult.success_count }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('enterpriseDevices.result.failCount')">
<ElTag type="danger">{{ operationResult.fail_count }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<!-- 失败项详情 -->
<div
v-if="operationResult.failed_items && operationResult.failed_items.length > 0"
style="margin-top: 20px"
>
<ElDivider content-position="left">{{
$t('enterpriseDevices.result.failedItems')
}}</ElDivider>
<ElTable :data="operationResult.failed_items" border max-height="300">
<ElTableColumn
prop="device_no"
:label="$t('enterpriseDevices.result.deviceNo')"
width="180"
/>
<ElTableColumn prop="reason" :label="$t('enterpriseDevices.result.reason')" />
</ElTable>
</div>
<!-- 显示已授权设备 -->
<div
v-if="
operationResult.authorized_devices && operationResult.authorized_devices.length > 0
"
style="margin-top: 20px"
>
<ElDivider content-position="left">{{
$t('enterpriseDevices.result.authorizedDevices')
}}</ElDivider>
<ElTable :data="operationResult.authorized_devices" border max-height="200">
<ElTableColumn
prop="device_no"
:label="$t('enterpriseDevices.result.deviceNo')"
width="180"
/>
<ElTableColumn
prop="device_id"
:label="$t('enterpriseDevices.result.deviceId')"
width="100"
/>
<ElTableColumn :label="$t('enterpriseDevices.result.cardCount')">
<template #default="{ row }">
{{ row.card_count || 0 }}
</template>
</ElTableColumn>
</ElTable>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="resultDialogVisible = false">{{
$t('common.confirm')
}}</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { EnterpriseService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import type {
EnterpriseDeviceItem,
AllocateDevicesResponse,
RecallDevicesResponse,
AuthorizedDeviceItem,
FailedDeviceItem
} from '@/types/api/enterpriseDevice'
import type { EnterpriseItem } from '@/types/api'
defineOptions({ name: 'EnterpriseDevices' })
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const allocateDialogVisible = ref(false)
const allocateLoading = ref(false)
const recallDialogVisible = ref(false)
const recallLoading = ref(false)
const resultDialogVisible = ref(false)
const tableRef = ref()
const allocateFormRef = ref<FormInstance>()
const recallFormRef = ref<FormInstance>()
const selectedDevices = ref<EnterpriseDeviceItem[]>([])
const enterpriseId = ref<number>(0)
const enterpriseInfo = ref<EnterpriseItem | null>(null)
const deviceNosText = ref('')
const operationResult = ref<AllocateDevicesResponse | RecallDevicesResponse>({
success_count: 0,
fail_count: 0,
failed_items: null,
authorized_devices: null
})
// 搜索表单初始值
const initialSearchState = {
device_no: ''
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 授权表单
const allocateForm = reactive({
device_nos: [] as string[],
remark: ''
})
// 授权表单验证规则
const allocateRules = reactive<FormRules>({
device_nos: [
{
required: true,
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error(t('enterpriseDevices.validation.deviceNosRequired')))
} else if (value.length > 100) {
callback(new Error(t('enterpriseDevices.validation.deviceNosMaxLength')))
} else {
callback()
}
},
trigger: 'change'
}
]
})
// 撤销表单
const recallForm = reactive({})
// 撤销表单验证规则
const recallRules = reactive<FormRules>({})
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: t('enterpriseDevices.searchForm.deviceNo'),
prop: 'device_no',
type: 'input',
config: {
clearable: true,
placeholder: t('enterpriseDevices.searchForm.deviceNoPlaceholder')
}
}
]
// 列配置
const columnOptions = [
{ label: t('enterpriseDevices.table.deviceId'), prop: 'device_id' },
{ label: t('enterpriseDevices.table.deviceNo'), prop: 'device_no' },
{ label: t('enterpriseDevices.table.deviceName'), prop: 'device_name' },
{ label: t('enterpriseDevices.table.deviceModel'), prop: 'device_model' },
{ label: t('enterpriseDevices.table.cardCount'), prop: 'card_count' },
{ label: t('enterpriseDevices.table.authorizedAt'), prop: 'authorized_at' }
]
const deviceList = ref<EnterpriseDeviceItem[]>([])
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'device_id',
label: t('enterpriseDevices.table.deviceId'),
width: 100
},
{
prop: 'device_no',
label: t('enterpriseDevices.table.deviceNo'),
minWidth: 150
},
{
prop: 'device_name',
label: t('enterpriseDevices.table.deviceName'),
minWidth: 150
},
{
prop: 'device_model',
label: t('enterpriseDevices.table.deviceModel'),
width: 120
},
{
prop: 'card_count',
label: t('enterpriseDevices.table.cardCount'),
width: 100
},
{
prop: 'authorized_at',
label: t('enterpriseDevices.table.authorizedAt'),
width: 180,
formatter: (row: EnterpriseDeviceItem) =>
row.authorized_at ? formatDateTime(row.authorized_at) : '-'
}
])
onMounted(() => {
const id = route.query.id
if (id) {
enterpriseId.value = Number(id)
getEnterpriseInfo()
getTableData()
} else {
ElMessage.error(t('enterpriseDevices.messages.loadFailed'))
goBack()
}
})
// 获取企业信息
const getEnterpriseInfo = async () => {
try {
const res = await EnterpriseService.getEnterprises({
page: 1,
page_size: 1,
id: enterpriseId.value
})
if (res.code === 0 && res.data.items && res.data.items.length > 0) {
enterpriseInfo.value = res.data.items[0]
}
} catch (error) {
console.error('获取企业信息失败:', error)
}
}
// 获取企业设备列表
const getTableData = async () => {
loading.value = true
try {
const params: any = {
page: pagination.page,
page_size: pagination.pageSize,
device_no: searchForm.device_no || undefined
}
// 清理空值
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) {
delete params[key]
}
})
const res = await EnterpriseService.getEnterpriseDevices(enterpriseId.value, params)
if (res.code === 0) {
deviceList.value = res.data.list || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
ElMessage.error(t('enterpriseDevices.messages.loadFailed'))
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 表格选择变化
const handleSelectionChange = (selection: EnterpriseDeviceItem[]) => {
selectedDevices.value = selection
}
// 返回
const goBack = () => {
router.back()
}
// 显示授权对话框
const showAllocateDialog = () => {
allocateDialogVisible.value = true
deviceNosText.value = ''
allocateForm.device_nos = []
allocateForm.remark = ''
if (allocateFormRef.value) {
allocateFormRef.value.resetFields()
}
}
// 处理设备号输入变化
const handleDeviceNosChange = () => {
// 解析输入的设备号,支持逗号、空格、换行分隔
const deviceNos = deviceNosText.value
.split(/[,\s\n]+/)
.map((no) => no.trim())
.filter((no) => no.length > 0)
allocateForm.device_nos = deviceNos
}
// 执行授权
const handleAllocate = async () => {
if (!allocateFormRef.value) return
await allocateFormRef.value.validate(async (valid) => {
if (valid) {
if (allocateForm.device_nos.length === 0) {
ElMessage.warning(t('enterpriseDevices.messages.deviceNosEmpty'))
return
}
if (allocateForm.device_nos.length > 100) {
ElMessage.warning(t('enterpriseDevices.messages.deviceNosMaxLimit'))
return
}
allocateLoading.value = true
try {
const res = await EnterpriseService.allocateDevices(enterpriseId.value, {
device_nos: allocateForm.device_nos,
remark: allocateForm.remark || undefined
})
if (res.code === 0) {
operationResult.value = res.data
allocateDialogVisible.value = false
resultDialogVisible.value = true
// 显示成功消息
if (res.data.success_count > 0 && res.data.fail_count === 0) {
ElMessage.success(t('enterpriseDevices.messages.allocateSuccess'))
} else if (res.data.success_count > 0 && res.data.fail_count > 0) {
ElMessage.warning(
t('enterpriseDevices.messages.allocatePartialSuccess', {
success: res.data.success_count,
fail: res.data.fail_count
})
)
} else {
ElMessage.error(t('enterpriseDevices.messages.allocateFailed'))
}
getTableData()
}
} catch (error) {
console.error(error)
ElMessage.error(t('enterpriseDevices.messages.allocateFailed'))
} finally {
allocateLoading.value = false
}
}
})
}
// 关闭授权对话框
const handleAllocateDialogClose = () => {
if (allocateFormRef.value) {
allocateFormRef.value.resetFields()
}
}
// 显示撤销授权对话框
const showRecallDialog = () => {
if (selectedDevices.value.length === 0) {
ElMessage.warning(t('enterpriseDevices.messages.noSelection'))
return
}
recallDialogVisible.value = true
if (recallFormRef.value) {
recallFormRef.value.resetFields()
}
}
// 执行撤销授权
const handleRecall = async () => {
if (!recallFormRef.value) return
ElMessageBox.confirm(
t('enterpriseDevices.messages.recallConfirmText', {
count: selectedDevices.value.length
}),
t('enterpriseDevices.messages.recallConfirm'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
)
.then(async () => {
recallLoading.value = true
try {
const res = await EnterpriseService.recallDevices(enterpriseId.value, {
device_nos: selectedDevices.value.map((device) => device.device_no)
})
if (res.code === 0) {
operationResult.value = res.data
recallDialogVisible.value = false
resultDialogVisible.value = true
// 显示成功消息
if (res.data.success_count > 0 && res.data.fail_count === 0) {
ElMessage.success(t('enterpriseDevices.messages.recallSuccess'))
} else if (res.data.success_count > 0 && res.data.fail_count > 0) {
ElMessage.warning(
t('enterpriseDevices.messages.recallPartialSuccess', {
success: res.data.success_count,
fail: res.data.fail_count
})
)
} else {
ElMessage.error(t('enterpriseDevices.messages.recallFailed'))
}
// 清空选择
if (tableRef.value) {
tableRef.value.clearSelection()
}
selectedDevices.value = []
getTableData()
}
} catch (error) {
console.error(error)
ElMessage.error(t('enterpriseDevices.messages.recallFailed'))
} finally {
recallLoading.value = false
}
})
.catch(() => {})
}
// 关闭撤销授权对话框
const handleRecallDialogClose = () => {
if (recallFormRef.value) {
recallFormRef.value.resetFields()
}
}
</script>
<style lang="scss" scoped>
.enterprise-devices-page {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
</style>

View File

@@ -100,7 +100,7 @@
<!-- 新增配置对话框 -->
<ElDialog v-model="dialogVisible" title="新增提现配置" width="500px">
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px">
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px">
<ElFormItem label="最低提现金额" prop="min_withdrawal_amount">
<ElInputNumber
v-model="form.min_withdrawal_amount"
@@ -134,17 +134,6 @@
/>
<div class="form-tip">单位</div>
</ElFormItem>
<ElFormItem label="到账天数" prop="arrival_days">
<ElInputNumber
v-model="form.arrival_days"
:min="0"
:max="30"
:step="1"
style="width: 100%"
/>
<div class="form-tip">单位0表示实时到账</div>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
@@ -186,8 +175,7 @@
const form = reactive({
min_withdrawal_amount: 100,
fee_rate: 0.2,
daily_withdrawal_limit: 3,
arrival_days: 1
daily_withdrawal_limit: 3
})
// 表单验证规则
@@ -203,10 +191,6 @@
daily_withdrawal_limit: [
{ required: true, message: '请输入每日提现次数', trigger: 'blur' },
{ type: 'number', min: 1, message: '每日提现次数必须大于0', trigger: 'blur' }
],
arrival_days: [
{ required: true, message: '请输入到账天数', trigger: 'blur' },
{ type: 'number', min: 0, message: '到账天数不能为负数', trigger: 'blur' }
]
})
@@ -314,7 +298,6 @@
form.min_withdrawal_amount = 100
form.fee_rate = 0.2
form.daily_withdrawal_limit = 3
form.arrival_days = 1
formRef.value?.clearValidate()
}
@@ -330,8 +313,7 @@
const params = {
min_withdrawal_amount: Math.round(form.min_withdrawal_amount * 100), // 元转分
fee_rate: Math.round(form.fee_rate * 100), // 百分比转基点
daily_withdrawal_limit: form.daily_withdrawal_limit,
arrival_days: form.arrival_days
daily_withdrawal_limit: form.daily_withdrawal_limit
}
await CommissionService.createWithdrawalSetting(params)

View File

@@ -0,0 +1,663 @@
<template>
<ArtTableFullScreen>
<div class="order-list-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@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="showCreateDialog">{{ t('orderManagement.createOrder') }}</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="orderList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
@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="createDialogVisible"
:title="t('orderManagement.createOrder')"
width="600px"
@closed="handleCreateDialogClosed"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
<ElFormItem :label="t('orderManagement.table.orderType')" prop="order_type">
<ElSelect
v-model="createForm.order_type"
:placeholder="t('orderManagement.searchForm.orderTypePlaceholder')"
style="width: 100%"
>
<ElOption
:label="t('orderManagement.orderType.singleCard')"
value="single_card"
/>
<ElOption :label="t('orderManagement.orderType.device')" value="device" />
</ElSelect>
</ElFormItem>
<ElFormItem :label="t('orderManagement.createForm.packageIds')" prop="package_ids">
<ElSelect
v-model="createForm.package_ids"
:placeholder="t('orderManagement.createForm.packageIdsPlaceholder')"
multiple
style="width: 100%"
>
<!-- TODO: Load actual packages from API -->
<ElOption label="套餐示例" :value="1" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="createForm.order_type === 'single_card'"
:label="t('orderManagement.createForm.iotCardId')"
prop="iot_card_id"
>
<ElInputNumber
v-model="createForm.iot_card_id"
:placeholder="t('orderManagement.createForm.iotCardIdPlaceholder')"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem
v-if="createForm.order_type === 'device'"
:label="t('orderManagement.createForm.deviceId')"
prop="device_id"
>
<ElInputNumber
v-model="createForm.device_id"
:placeholder="t('orderManagement.createForm.deviceIdPlaceholder')"
style="width: 100%"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="createDialogVisible = false">{{
t('orderManagement.actions.cancel')
}}</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"
>
<div v-if="currentOrder" class="order-detail">
<ElDescriptions :column="2" border>
<ElDescriptionsItem :label="t('orderManagement.table.orderNo')">
{{ currentOrder.order_no }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.orderType')">
{{ getOrderTypeText(currentOrder.order_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.paymentStatus')">
<ElTag :type="getPaymentStatusType(currentOrder.payment_status)">
{{ currentOrder.payment_status_text }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.totalAmount')">
{{ formatCurrency(currentOrder.total_amount) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.paymentMethod')">
{{ getPaymentMethodText(currentOrder.payment_method) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.buyerType')">
{{ getBuyerTypeText(currentOrder.buyer_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.commissionStatus')">
{{ getCommissionStatusText(currentOrder.commission_status) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.paidAt')">
{{ currentOrder.paid_at ? formatDateTime(currentOrder.paid_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.createdAt')">
{{ formatDateTime(currentOrder.created_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="t('orderManagement.table.updatedAt')">
{{ formatDateTime(currentOrder.updated_at) }}
</ElDescriptionsItem>
</ElDescriptions>
<!-- 订单项列表 -->
<div v-if="currentOrder.items && currentOrder.items.length > 0" style="margin-top: 20px">
<h4>{{ t('orderManagement.orderItems') }}</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"
>
<template #default="{ row }">
{{ formatCurrency(row.unit_price) }}
</template>
</ElTableColumn>
<ElTableColumn
prop="amount"
:label="t('orderManagement.items.amount')"
width="120"
>
<template #default="{ row }">
{{ formatCurrency(row.amount) }}
</template>
</ElTableColumn>
</ElTable>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="detailDialogVisible = false">{{
t('orderManagement.actions.close')
}}</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useI18n } from 'vue-i18n'
import { OrderService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
Order,
OrderQueryParams,
CreateOrderRequest,
PaymentStatus,
OrderType,
BuyerType,
OrderPaymentMethod,
OrderCommissionStatus
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'OrderList' })
const { t } = useI18n()
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 initialSearchState: OrderQueryParams = {
order_no: '',
payment_status: undefined,
order_type: undefined,
start_time: '',
end_time: ''
}
// 搜索表单
const searchForm = reactive<OrderQueryParams>({ ...initialSearchState })
// 搜索表单配置
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'),
prop: 'payment_status',
type: 'select',
placeholder: t('orderManagement.searchForm.paymentStatusPlaceholder'),
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 }
],
config: {
clearable: true
}
},
{
label: t('orderManagement.searchForm.orderType'),
prop: 'order_type',
type: 'select',
placeholder: t('orderManagement.searchForm.orderTypePlaceholder'),
options: [
{ label: t('orderManagement.orderType.singleCard'), value: 'single_card' },
{ label: t('orderManagement.orderType.device'), value: 'device' }
],
config: {
clearable: true
}
},
{
label: t('orderManagement.searchForm.dateRange'),
prop: 'dateRange',
type: 'daterange',
config: {
clearable: true,
startPlaceholder: t('orderManagement.searchForm.startDate'),
endPlaceholder: t('orderManagement.searchForm.endDate'),
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
}
]
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
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: t('orderManagement.table.paymentStatus'), prop: 'payment_status' },
{ label: t('orderManagement.table.totalAmount'), prop: 'total_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: t('orderManagement.table.operation'), prop: 'operation' }
]
const createFormRef = ref<FormInstance>()
const createRules = reactive<FormRules>({
order_type: [
{
required: true,
message: t('orderManagement.validation.orderTypeRequired'),
trigger: 'change'
}
],
package_ids: [
{
required: true,
message: t('orderManagement.validation.packageIdsRequired'),
trigger: 'change'
}
]
})
const createForm = reactive<CreateOrderRequest>({
order_type: 'single_card',
package_ids: [],
iot_card_id: null,
device_id: null
})
const orderList = ref<Order[]>([])
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => {
return `¥${(amount / 100).toFixed(2)}`
}
// 获取支付状态标签类型
const getPaymentStatusType = (
status: PaymentStatus
): 'success' | 'info' | 'warning' | 'danger' => {
const statusMap: Record<PaymentStatus, 'success' | 'info' | 'warning' | 'danger'> = {
1: 'warning', // 待支付
2: 'success', // 已支付
3: 'info', // 已取消
4: 'danger' // 已退款
}
return statusMap[status] || 'info'
}
// 获取订单类型文本
const getOrderTypeText = (type: OrderType): string => {
return type === 'single_card'
? t('orderManagement.orderType.singleCard')
: t('orderManagement.orderType.device')
}
// 获取买家类型文本
const getBuyerTypeText = (type: BuyerType): string => {
return type === 'personal'
? t('orderManagement.buyerType.personal')
: t('orderManagement.buyerType.agent')
}
// 获取支付方式文本
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')
}
return methodMap[method] || method
}
// 获取佣金状态文本
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')
}
return statusMap[status] || '-'
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: t('orderManagement.table.id'),
width: 80
},
{
prop: 'order_no',
label: t('orderManagement.table.orderNo'),
minWidth: 180
},
{
prop: 'order_type',
label: t('orderManagement.table.orderType'),
width: 120,
formatter: (row: Order) => {
return h(
ElTag,
{ type: row.order_type === 'single_card' ? 'primary' : 'success' },
() => getOrderTypeText(row.order_type)
)
}
},
{
prop: 'buyer_type',
label: t('orderManagement.table.buyerType'),
width: 120,
formatter: (row: Order) => {
return h(
ElTag,
{ type: row.buyer_type === 'personal' ? 'info' : 'warning' },
() => getBuyerTypeText(row.buyer_type)
)
}
},
{
prop: 'payment_status',
label: t('orderManagement.table.paymentStatus'),
width: 120,
formatter: (row: Order) => {
return h(ElTag, { type: getPaymentStatusType(row.payment_status) }, () =>
row.payment_status_text
)
}
},
{
prop: 'total_amount',
label: t('orderManagement.table.totalAmount'),
width: 120,
formatter: (row: Order) => formatCurrency(row.total_amount)
},
{
prop: 'payment_method',
label: t('orderManagement.table.paymentMethod'),
width: 120,
formatter: (row: Order) => getPaymentMethodText(row.payment_method)
},
{
prop: 'paid_at',
label: t('orderManagement.table.paidAt'),
width: 180,
formatter: (row: Order) => (row.paid_at ? formatDateTime(row.paid_at) : '-')
},
{
prop: 'created_at',
label: t('orderManagement.table.createdAt'),
width: 180,
formatter: (row: Order) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: t('orderManagement.table.operation'),
width: 180,
fixed: 'right',
formatter: (row: Order) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
icon: '&#xe6cf;',
tooltip: t('orderManagement.actions.viewDetail'),
onClick: () => showOrderDetail(row)
}),
// 只有待支付和已支付的订单可以取消
row.payment_status === 1 || row.payment_status === 2
? h(ArtButtonTable, {
type: 'delete',
tooltip: t('orderManagement.actions.cancel'),
onClick: () => handleCancelOrder(row)
})
: null
])
}
}
])
onMounted(() => {
getTableData()
})
// 获取订单列表
const getTableData = async () => {
loading.value = true
try {
const params: OrderQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
order_no: searchForm.order_no || undefined,
payment_status: searchForm.payment_status,
order_type: searchForm.order_type,
start_time: searchForm.start_time || undefined,
end_time: searchForm.end_time || undefined
}
const res = await OrderService.getOrders(params)
if (res.code === 0) {
orderList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
// 处理日期范围
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
searchForm.start_time = searchForm.dateRange[0]
searchForm.end_time = searchForm.dateRange[1]
} else {
searchForm.start_time = ''
searchForm.end_time = ''
}
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示创建订单对话框
const showCreateDialog = () => {
createDialogVisible.value = true
}
// 对话框关闭后的清理
const handleCreateDialogClosed = () => {
// 重置表单(会同时清除验证状态)
createFormRef.value?.resetFields()
// 重置表单数据到初始值
createForm.order_type = 'single_card'
createForm.package_ids = []
createForm.iot_card_id = null
createForm.device_id = null
}
// 创建订单
const handleCreateOrder = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
createLoading.value = true
try {
const data: CreateOrderRequest = {
order_type: createForm.order_type,
package_ids: createForm.package_ids,
iot_card_id: createForm.order_type === 'single_card' ? createForm.iot_card_id : null,
device_id: createForm.order_type === 'device' ? createForm.device_id : null
}
await OrderService.createOrder(data)
ElMessage.success(t('orderManagement.messages.createSuccess'))
createDialogVisible.value = false
formEl.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
createLoading.value = false
}
}
})
}
// 查看订单详情
const showOrderDetail = async (row: Order) => {
try {
const res = await OrderService.getOrderById(row.id)
if (res.code === 0) {
currentOrder.value = res.data
detailDialogVisible.value = true
}
} catch (error) {
console.error(error)
}
}
// 取消订单
const handleCancelOrder = (row: Order) => {
// 已支付的订单不能取消
if (row.payment_status === 2) {
ElMessage.warning(t('orderManagement.messages.cannotCancelPaid'))
return
}
ElMessageBox.confirm(
t('orderManagement.messages.cancelConfirmText'),
t('orderManagement.messages.cancelConfirm'),
{
confirmButtonText: t('orderManagement.actions.confirm'),
cancelButtonText: t('orderManagement.actions.cancel'),
type: 'warning'
}
)
.then(async () => {
try {
await OrderService.cancelOrder(row.id)
ElMessage.success(t('orderManagement.messages.cancelSuccess'))
await getTableData()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消操作
})
}
</script>
<style scoped lang="scss">
.order-list-page {
height: 100%;
}
.order-detail {
:deep(.el-descriptions__label) {
width: 140px;
}
}
</style>

View File

@@ -0,0 +1,382 @@
<template>
<ArtTableFullScreen>
<div class="my-packages-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="packageList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
@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="detailDialogVisible" title="套餐详情" width="600px">
<ElDescriptions :column="2" border v-if="currentPackage">
<ElDescriptionsItem label="套餐编码">{{
currentPackage.package_code
}}</ElDescriptionsItem>
<ElDescriptionsItem label="套餐名称">{{
currentPackage.package_name
}}</ElDescriptionsItem>
<ElDescriptionsItem label="所属系列">{{
currentPackage.series_name
}}</ElDescriptionsItem>
<ElDescriptionsItem label="套餐类型">
<ElTag :type="getPackageTypeTag(currentPackage.package_type)">{{
getPackageTypeLabel(currentPackage.package_type)
}}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="流量类型">
<ElTag :type="getDataTypeTag(currentPackage.data_type)">{{
getDataTypeLabel(currentPackage.data_type)
}}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="真流量">{{
currentPackage.real_data_mb
}}MB</ElDescriptionsItem>
<ElDescriptionsItem label="虚流量">{{
currentPackage.virtual_data_mb
}}MB</ElDescriptionsItem>
<ElDescriptionsItem label="有效期">{{
currentPackage.duration_months
}}</ElDescriptionsItem>
<ElDescriptionsItem label="套餐价格">¥{{
(currentPackage.price / 100).toFixed(2)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="上架状态">
<ElTag :type="currentPackage.shelf_status === 1 ? 'success' : 'info'">{{
currentPackage.shelf_status === 1 ? '上架' : '下架'
}}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="currentPackage.status === 1 ? 'success' : 'danger'">{{
currentPackage.status === 1 ? '启用' : '禁用'
}}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="2">{{
currentPackage.description || '无'
}}</ElDescriptionsItem>
</ElDescriptions>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="detailDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { PackageManageService, PackageSeriesService } from '@/api/modules'
import { ElMessage, ElTag, ElDescriptions, ElDescriptionsItem } from 'element-plus'
import type { PackageResponse } from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { formatDateTime } from '@/utils/business/format'
import {
PACKAGE_TYPE_OPTIONS,
getPackageTypeLabel,
getPackageTypeTag,
getDataTypeLabel,
getDataTypeTag
} from '@/config/constants'
defineOptions({ name: 'MyPackages' })
const loading = ref(false)
const seriesLoading = ref(false)
const detailDialogVisible = ref(false)
const tableRef = ref()
const currentPackage = ref<PackageResponse | null>(null)
const seriesOptions = ref<any[]>([])
// 搜索表单初始值
const initialSearchState = {
series_id: undefined as number | undefined,
package_type: undefined as string | undefined
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '套餐系列',
prop: 'series_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: searchSeries,
loading: seriesLoading.value,
placeholder: '请选择或搜索套餐系列'
},
options: () =>
seriesOptions.value.map((s: any) => ({
label: s.series_name,
value: s.id
}))
},
{
label: '套餐类型',
prop: 'package_type',
type: 'select',
config: {
clearable: true,
placeholder: '请选择套餐类型'
},
options: () =>
PACKAGE_TYPE_OPTIONS.map((o) => ({
label: o.label,
value: o.value
}))
}
])
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '套餐编码', prop: 'package_code' },
{ label: '套餐名称', prop: 'package_name' },
{ label: '所属系列', prop: 'series_name' },
{ label: '套餐类型', prop: 'package_type' },
{ label: '流量类型', prop: 'data_type' },
{ label: '真流量', prop: 'real_data_mb' },
{ label: '虚流量', prop: 'virtual_data_mb' },
{ label: '有效期', prop: 'duration_months' },
{ label: '操作', prop: 'operation' }
]
const packageList = ref<PackageResponse[]>([])
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'package_code',
label: '套餐编码',
minWidth: 150
},
{
prop: 'package_name',
label: '套餐名称',
minWidth: 180
},
{
prop: 'series_name',
label: '所属系列',
width: 120
},
{
prop: 'package_type',
label: '套餐类型',
width: 100,
formatter: (row: PackageResponse) => {
return h(
ElTag,
{ type: getPackageTypeTag(row.package_type), size: 'small' },
() => getPackageTypeLabel(row.package_type)
)
}
},
{
prop: 'data_type',
label: '流量类型',
width: 100,
formatter: (row: PackageResponse) => {
return h(
ElTag,
{ type: getDataTypeTag(row.data_type), size: 'small' },
() => getDataTypeLabel(row.data_type)
)
}
},
{
prop: 'real_data_mb',
label: '真流量',
width: 100,
formatter: (row: PackageResponse) => `${row.real_data_mb}MB`
},
{
prop: 'virtual_data_mb',
label: '虚流量',
width: 100,
formatter: (row: PackageResponse) => `${row.virtual_data_mb}MB`
},
{
prop: 'duration_months',
label: '有效期',
width: 100,
formatter: (row: PackageResponse) => `${row.duration_months}`
},
{
prop: 'operation',
label: '操作',
width: 100,
fixed: 'right',
formatter: (row: PackageResponse) => {
return h(ArtButtonTable, {
text: '查看详情',
onClick: () => showDetail(row)
})
}
}
])
onMounted(() => {
loadSeriesOptions()
getTableData()
})
// 加载套餐系列选项(默认加载10条)
const loadSeriesOptions = async (seriesName?: string) => {
seriesLoading.value = true
try {
const params: any = {
page: 1,
page_size: 10,
status: 1
}
if (seriesName) {
params.series_name = seriesName
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
seriesOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载系列选项失败:', error)
} finally {
seriesLoading.value = false
}
}
// 搜索系列
const searchSeries = (query: string) => {
if (query) {
loadSeriesOptions(query)
} else {
loadSeriesOptions()
}
}
// 获取我的可售套餐列表
const getTableData = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.page_size,
series_id: searchForm.series_id || undefined,
package_type: searchForm.package_type || undefined
}
const res = await PackageManageService.getPackages(params)
if (res.code === 0) {
packageList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示详情
const showDetail = async (row: PackageResponse) => {
try {
const res = await PackageManageService.getPackageDetail(row.id)
if (res.code === 0) {
currentPackage.value = res.data
detailDialogVisible.value = true
}
} catch (error) {
console.error(error)
ElMessage.error('获取套餐详情失败')
}
}
</script>
<style lang="scss" scoped>
.my-packages-page {
// 可以添加特定样式
}
.dialog-footer {
text-align: right;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,9 @@
<div class="package-series-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -17,8 +18,7 @@
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
<ElButton type="success" @click="showAddDialog">新增</ElButton>
<ElButton type="primary" @click="showDialog('add')">新增套餐系列</ElButton>
</template>
</ArtTableHeader>
@@ -27,12 +27,11 @@
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:data="seriesList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
@@ -41,57 +40,41 @@
</template>
</ArtTable>
<!-- 新增套餐系列对话框 -->
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="addDialogVisible"
title="新增套餐系列"
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增套餐系列' : '编辑套餐系列'"
width="500px"
align-center
:close-on-click-modal="false"
>
<ElForm ref="addFormRef" :model="addFormData" :rules="addRules" label-width="120px">
<ElFormItem label="系列名称" prop="seriesName">
<ElInput v-model="addFormData.seriesName" placeholder="请输入系列名称" clearable />
</ElFormItem>
<ElFormItem label="包含套餐" prop="packageNames">
<ElSelect
v-model="addFormData.packageNames"
placeholder="请选择要包含的套餐"
style="width: 100%"
multiple
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="系列编码" prop="series_code">
<ElInput
v-model="form.series_code"
placeholder="请输入系列编码"
:disabled="dialogType === 'edit'"
clearable
>
<ElOption label="随意联畅玩年卡套餐" value="changwan_yearly" />
<ElOption label="随意联畅玩月卡套餐" value="changwan_monthly" />
<ElOption label="如意包年3G流量包" value="ruyi_3g" />
<ElOption label="如意包月流量包" value="ruyi_monthly" />
<ElOption label="Y-NB专享套餐" value="nb_special" />
<ElOption label="NB-IoT基础套餐" value="nb_basic" />
<ElOption label="100G全国流量月卡套餐" value="big_data_100g" />
<ElOption label="200G超值流量包" value="big_data_200g" />
<ElOption label="广电飞悦卡无预存50G" value="gdtv_50g" />
<ElOption label="广电天翼卡" value="gdtv_tianyi" />
</ElSelect>
/>
</ElFormItem>
<ElFormItem label="系列名称" prop="series_name">
<ElInput v-model="form.series_name" placeholder="请输入系列名称" clearable />
</ElFormItem>
<ElFormItem label="系列描述" prop="description">
<ElInput
v-model="addFormData.description"
v-model="form.description"
type="textarea"
placeholder="请输入系列描述(可选)"
:rows="3"
maxlength="200"
placeholder="请输入系列描述(可选)"
maxlength="500"
show-word-limit
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="addDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleAddSubmit" :loading="addLoading">
确认新增
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
提交
</ElButton>
</div>
</template>
@@ -103,343 +86,328 @@
<script setup lang="ts">
import { h } from 'vue'
import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
import { PackageSeriesService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { PackageSeriesResponse } from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { SearchChangeParams, SearchFormItem } from '@/types'
import { formatDateTime } from '@/utils/business/format'
import {
CommonStatus,
getStatusText,
frontendStatusToApi,
apiStatusToFrontend
} from '@/config/constants'
defineOptions({ name: 'PackageSeries' })
const addDialogVisible = ref(false)
const dialogVisible = ref(false)
const loading = ref(false)
const addLoading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
seriesName: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const submitLoading = ref(false)
const tableRef = ref()
const formRef = ref<FormInstance>()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 新增表单实例
const addFormRef = ref<FormInstance>()
// 新增表单数据
const addFormData = reactive({
seriesName: '',
packageNames: [] as string[],
description: ''
})
// 模拟数据
const mockData = [
{
id: 1,
seriesName: '畅玩系列',
operator: '张若暄',
operationTime: '2025-11-08 10:30:00',
status: '启用'
},
{
id: 2,
seriesName: '如意系列',
operator: '孔丽娟',
operationTime: '2025-11-07 14:15:00',
status: '启用'
},
{
id: 3,
seriesName: 'NB专享',
operator: '李佳音',
operationTime: '2025-11-06 09:45:00',
status: '禁用'
},
{
id: 4,
seriesName: '大流量系列',
operator: '赵强',
operationTime: '2025-11-05 16:20:00',
status: '启用'
},
{
id: 5,
seriesName: '广电系列',
operator: '张若暄',
operationTime: '2025-11-04 11:30:00',
status: '禁用'
}
]
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1
getPackageSeriesList()
// 搜索表单初始值
const initialSearchState = {
series_name: '',
status: undefined as number | undefined
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1
getPackageSeriesList()
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 表单项变更处理
const handleFormChange = (params: SearchChangeParams): void => {
console.log('表单项变更:', params)
}
// 表单配置项
const formItems: SearchFormItem[] = [
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '系列名称',
prop: 'seriesName',
prop: 'series_name',
type: 'input',
config: {
clearable: true,
placeholder: '请输入系列名称'
}
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '请选择状态'
},
onChange: handleFormChange
options: () => [
{ label: '启用', value: 1 },
{ label: '禁用', value: 2 }
]
}
]
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '系列名称', prop: 'seriesName' },
{ label: '操作人', prop: 'operator' },
{ label: '操作时间', prop: 'operationTime' },
{ label: 'ID', prop: 'id' },
{ label: '系列编码', prop: 'series_code' },
{ label: '系列名称', prop: 'series_name' },
{ label: '描述', prop: 'description' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' },
{ label: '操作', prop: 'operation' }
]
// 获取状态标签类型
const getStatusType = (status: string) => {
switch (status) {
case '启用':
return 'success'
case '禁用':
return 'danger'
default:
return 'info'
}
}
// 表单验证规则
const rules = reactive<FormRules>({
series_code: [
{ required: true, message: '请输入系列编码', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
],
series_name: [
{ required: true, message: '请输入系列名称', trigger: 'blur' },
{ min: 1, max: 255, message: '长度在 1 到 255 个字符', trigger: 'blur' }
],
description: [{ max: 500, message: '描述不能超过 500 个字符', trigger: 'blur' }]
})
// 显示新增对话框
const showAddDialog = () => {
addDialogVisible.value = true
// 重置表单
if (addFormRef.value) {
addFormRef.value.resetFields()
}
addFormData.seriesName = ''
addFormData.packageNames = []
addFormData.description = ''
}
// 表单数据
const form = reactive<any>({
id: 0,
series_code: '',
series_name: '',
description: ''
})
// 启用系列
const enableSeries = (row: any) => {
ElMessageBox.confirm(`确定要启用套餐系列"${row.seriesName}"吗?`, '启用确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
.then(() => {
ElMessage.success('启用成功')
getPackageSeriesList()
})
.catch(() => {
ElMessage.info('已取消启用')
})
}
// 禁用系列
const disableSeries = (row: any) => {
ElMessageBox.confirm(`确定要禁用套餐系列"${row.seriesName}"吗?`, '禁用确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('禁用成功')
getPackageSeriesList()
})
.catch(() => {
ElMessage.info('已取消禁用')
})
}
// 删除系列
const deleteSeries = (row: any) => {
ElMessageBox.confirm(
`确定要删除套餐系列"${row.seriesName}"吗?删除后将无法恢复。`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}
)
.then(() => {
ElMessage.success('删除成功')
getPackageSeriesList()
})
.catch(() => {
ElMessage.info('已取消删除')
})
}
const seriesList = ref<PackageSeriesResponse[]>([])
const dialogType = ref('add')
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{ type: 'selection' },
{
prop: 'seriesName',
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'series_code',
label: '系列编码',
minWidth: 150
},
{
prop: 'series_name',
label: '系列名称',
minWidth: 180
minWidth: 150
},
{
prop: 'operator',
label: '操作人',
width: 120
},
{
prop: 'operationTime',
label: '操作时间',
width: 160
prop: 'description',
label: '描述',
minWidth: 200
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row) => {
return h(ElTag, { type: getStatusType(row.status) }, () => row.status)
formatter: (row: PackageSeriesResponse) => {
const frontendStatus = apiStatusToFrontend(row.status)
return h(ElSwitch, {
modelValue: frontendStatus,
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: '创建时间',
width: 180,
formatter: (row: PackageSeriesResponse) => formatDateTime(row.created_at)
},
{
prop: 'updated_at',
label: '更新时间',
width: 180,
formatter: (row: PackageSeriesResponse) => formatDateTime(row.updated_at)
},
{
prop: 'operation',
label: '操作',
width: 180,
formatter: (row: any) => {
const buttons = []
if (row.status === '启用') {
buttons.push(
h(ArtButtonTable, {
text: '禁用',
onClick: () => disableSeries(row)
})
)
} else {
buttons.push(
h(ArtButtonTable, {
text: '启用',
onClick: () => enableSeries(row)
})
)
}
buttons.push(
width: 150,
fixed: 'right',
formatter: (row: PackageSeriesResponse) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
text: '删除',
type: 'edit',
onClick: () => showDialog('edit', row)
}),
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteSeries(row)
})
)
return h('div', { class: 'operation-buttons' }, buttons)
])
}
}
])
onMounted(() => {
getPackageSeriesList()
getTableData()
})
// 获取套餐系列列表
const getPackageSeriesList = async () => {
const getTableData = 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
const params = {
page: pagination.page,
page_size: pagination.page_size,
series_name: searchForm.series_name || undefined,
status: searchForm.status || undefined
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
seriesList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error('获取套餐系列列表失败:', error)
console.error(error)
ElMessage.error('获取套餐系列列表失败')
} finally {
loading.value = false
}
}
const handleRefresh = () => {
getPackageSeriesList()
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getPackageSeriesList()
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getPackageSeriesList()
pagination.page = newCurrentPage
getTableData()
}
// 新增表单验证规则
const addRules = reactive<FormRules>({
seriesName: [
{ required: true, message: '请输入系列名称', trigger: 'blur' },
{ min: 2, max: 20, message: '系列名称长度在 2 到 20 个字符', trigger: 'blur' }
],
packageNames: [{ required: true, message: '请选择要包含的套餐', trigger: 'change' }]
})
// 显示新增/编辑对话框
const showDialog = (type: string, row?: PackageSeriesResponse) => {
dialogVisible.value = true
dialogType.value = type
// 提交新增
const handleAddSubmit = async () => {
if (!addFormRef.value) return
if (type === 'edit' && row) {
form.id = row.id
form.series_code = row.series_code
form.series_name = row.series_name
form.description = row.description || ''
} else {
form.id = 0
form.series_code = ''
form.series_name = ''
form.description = ''
}
await addFormRef.value.validate((valid) => {
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
}
// 删除套餐系列
const deleteSeries = (row: PackageSeriesResponse) => {
ElMessageBox.confirm(`确定删除套餐系列 ${row.series_name} 吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
})
.then(async () => {
try {
await PackageSeriesService.deletePackageSeries(row.id)
ElMessage.success('删除成功')
await getTableData()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消删除
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
addLoading.value = true
submitLoading.value = true
try {
const data = {
series_code: form.series_code,
series_name: form.series_name,
description: form.description || undefined
}
// 模拟新增过程
setTimeout(() => {
ElMessage.success(
`新增套餐系列成功!系列名称:${addFormData.seriesName},包含套餐:${addFormData.packageNames.length}`
)
addDialogVisible.value = false
addLoading.value = false
getPackageSeriesList()
}, 2000)
if (dialogType.value === 'add') {
await PackageSeriesService.createPackageSeries(data)
ElMessage.success('新增成功')
} else {
await PackageSeriesService.updatePackageSeries(form.id, data)
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 状态切换
const handleStatusChange = async (row: PackageSeriesResponse, newFrontendStatus: number) => {
const oldStatus = row.status
const newApiStatus = frontendStatusToApi(newFrontendStatus)
// 先更新UI将后端状态转换
row.status = newApiStatus
try {
await PackageSeriesService.updatePackageSeriesStatus(row.id, newApiStatus)
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
row.status = oldStatus
console.error(error)
}
}
</script>
<style lang="scss" scoped>
@@ -447,16 +415,7 @@
// 可以添加特定样式
}
:deep(.operation-buttons) {
display: flex;
gap: 8px;
}
.dialog-footer {
text-align: right;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,867 @@
<template>
<ArtTableFullScreen>
<div class="series-assign-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@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>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="allocationList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
@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="650px"
:close-on-click-modal="false"
@closed="handleDialogClosed"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="160px">
<!-- 基本信息 -->
<ElFormItem label="选择套餐系列" prop="series_id" v-if="dialogType === 'add'">
<ElSelect
v-model="form.series_id"
placeholder="请选择套餐系列"
style="width: 100%"
filterable
remote
:remote-method="searchSeries"
:loading="seriesLoading"
clearable
>
<ElOption
v-for="series in seriesOptions"
:key="series.id"
:label="`${series.series_name} (${series.series_code})`"
:value="series.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选择店铺" prop="shop_id" v-if="dialogType === 'add'">
<ElSelect
v-model="form.shop_id"
placeholder="请选择店铺"
style="width: 100%"
filterable
remote
:remote-method="searchShop"
:loading="shopLoading"
clearable
>
<ElOption
v-for="shop in shopOptions"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElFormItem>
<!-- 基础返佣配置 -->
<ElDivider content-position="left">基础返佣配置</ElDivider>
<ElFormItem label="返佣模式" prop="base_commission.mode">
<ElRadioGroup v-model="form.base_commission.mode">
<ElRadio value="fixed">固定金额</ElRadio>
<ElRadio value="percent">百分比</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
:label="form.base_commission.mode === 'fixed' ? '返佣金额(分)' : '返佣百分比(千分比)'"
prop="base_commission.value"
>
<ElInputNumber
v-model="form.base_commission.value"
:min="0"
:controls="false"
style="width: 100%"
:placeholder="
form.base_commission.mode === 'fixed'
? '请输入固定返佣金额(分)'
: '请输入返佣百分比的千分比(如200表示20%)'
"
/>
<div class="form-tip">
{{
form.base_commission.mode === 'fixed'
? '每笔交易返佣该固定金额(单位:分)'
: '返佣百分比的千分比如200表示20%,即每笔交易返佣 = 交易金额 × 20%'
}}
</div>
</ElFormItem>
<!-- 梯度返佣配置 -->
<ElDivider content-position="left">梯度返佣设置可选</ElDivider>
<ElFormItem label="启用梯度返佣">
<ElSwitch v-model="form.enable_tier_commission" />
</ElFormItem>
<template v-if="form.enable_tier_commission">
<ElFormItem label="周期类型" prop="tier_config.period_type">
<ElSelect v-model="form.tier_config.period_type" placeholder="请选择周期类型" style="width: 100%">
<ElOption label="月度" value="monthly" />
<ElOption label="季度" value="quarterly" />
<ElOption label="年度" value="yearly" />
</ElSelect>
</ElFormItem>
<ElFormItem label="梯度类型" prop="tier_config.tier_type">
<ElSelect v-model="form.tier_config.tier_type" placeholder="请选择梯度类型" style="width: 100%">
<ElOption label="按销量" value="sales_count" />
<ElOption label="按销售额" value="sales_amount" />
</ElSelect>
</ElFormItem>
<ElFormItem label="梯度档位">
<div class="tier-list">
<div v-for="(tier, index) in form.tier_config.tiers" :key="index" class="tier-item">
<ElInputNumber
v-model="tier.threshold"
:min="1"
:controls="false"
placeholder="阈值"
style="width: 120px"
/>
<ElSelect v-model="tier.mode" placeholder="模式" style="width: 100px">
<ElOption label="固定" value="fixed" />
<ElOption label="百分比" value="percent" />
</ElSelect>
<ElInputNumber
v-model="tier.value"
:min="0"
:controls="false"
placeholder="返佣值"
style="width: 120px"
/>
<ElButton type="danger" @click="removeTier(index)">删除</ElButton>
</div>
<ElButton type="primary" @click="addTier">添加档位</ElButton>
</div>
</ElFormItem>
</template>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
提交
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ShopSeriesAllocationService, PackageSeriesService, ShopService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElSwitch, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
ShopSeriesAllocationResponse,
PackageSeriesResponse,
ShopResponse
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { formatDateTime } from '@/utils/business/format'
import {
CommonStatus,
getStatusText,
frontendStatusToApi,
apiStatusToFrontend
} from '@/config/constants'
defineOptions({ name: 'SeriesAssign' })
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const seriesLoading = ref(false)
const shopLoading = ref(false)
const tableRef = ref()
const formRef = ref<FormInstance>()
const seriesOptions = ref<PackageSeriesResponse[]>([])
const shopOptions = ref<ShopResponse[]>([])
const searchSeriesOptions = ref<PackageSeriesResponse[]>([])
const searchShopOptions = ref<ShopResponse[]>([])
// 搜索表单初始值
const initialSearchState = {
shop_id: undefined as number | undefined,
series_id: undefined as number | undefined,
status: undefined as number | undefined
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '店铺',
prop: 'shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchShop,
loading: shopLoading.value,
placeholder: '请选择或搜索店铺'
},
options: () =>
searchShopOptions.value.map((s) => ({
label: s.shop_name,
value: s.id
}))
},
{
label: '套餐系列',
prop: 'series_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchSeries,
loading: seriesLoading.value,
placeholder: '请选择或搜索套餐系列'
},
options: () =>
searchSeriesOptions.value.map((s) => ({
label: s.series_name,
value: s.id
}))
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '请选择状态'
},
options: () => [
{ label: '启用', value: 1 },
{ label: '禁用', value: 2 }
]
}
])
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '系列名称', prop: 'series_name' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '分配者店铺', prop: 'allocator_shop_name' },
{ label: '基础返佣', prop: 'base_commission' },
{ label: '梯度返佣', prop: 'enable_tier_commission' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' }
]
// 表单数据
const form = reactive<any>({
id: 0,
series_id: undefined,
shop_id: undefined,
base_commission: {
mode: 'fixed',
value: 0
},
enable_tier_commission: false,
tier_config: {
period_type: 'monthly',
tier_type: 'sales_count',
tiers: []
}
})
// 动态验证规则
const rules = computed<FormRules>(() => {
const baseRules: FormRules = {
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
shop_id: [{ required: true, message: '请选择店铺', trigger: 'change' }],
'base_commission.mode': [{ required: true, message: '请选择返佣模式', trigger: 'change' }],
'base_commission.value': [
{ required: true, message: '请输入返佣值', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback(new Error('请输入返佣值'))
} else if (value < 0) {
callback(new Error('返佣值不能小于0'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
// 如果启用了梯度返佣,添加梯度返佣的验证规则
if (form.enable_tier_commission) {
baseRules['tier_config.period_type'] = [
{ required: true, message: '请选择周期类型', trigger: 'change' }
]
baseRules['tier_config.tier_type'] = [
{ required: true, message: '请选择梯度类型', trigger: 'change' }
]
}
return baseRules
})
const allocationList = ref<ShopSeriesAllocationResponse[]>([])
const dialogType = ref('add')
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'series_name',
label: '系列名称',
minWidth: 150
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 180
},
{
prop: 'allocator_shop_name',
label: '分配者店铺',
minWidth: 150,
formatter: (row: ShopSeriesAllocationResponse) => {
return row.allocator_shop_name || '-'
}
},
{
prop: 'base_commission',
label: '基础返佣',
width: 150,
formatter: (row: ShopSeriesAllocationResponse) => {
if (!row.base_commission) return '-'
const { mode, value } = row.base_commission
if (mode === 'fixed') {
return `固定 ¥${(value / 100).toFixed(2)}`
} else {
return `百分比 ${(value / 10).toFixed(1)}%`
}
}
},
{
prop: 'enable_tier_commission',
label: '梯度返佣',
width: 100,
formatter: (row: ShopSeriesAllocationResponse) => {
return h(
ElTag,
{ type: row.enable_tier_commission ? 'success' : 'info', size: 'small' },
() => (row.enable_tier_commission ? '已启用' : '未启用')
)
}
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: ShopSeriesAllocationResponse) => {
const frontendStatus = apiStatusToFrontend(row.status)
return h(ElSwitch, {
modelValue: frontendStatus,
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: '创建时间',
width: 180,
formatter: (row: ShopSeriesAllocationResponse) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 150,
fixed: 'right',
formatter: (row: ShopSeriesAllocationResponse) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
}),
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteAllocation(row)
})
])
}
}
])
onMounted(() => {
loadSeriesOptions()
loadShopOptions()
loadSearchSeriesOptions()
loadSearchShopOptions()
getTableData()
})
// 加载系列选项(用于新增对话框,默认加载10条
const loadSeriesOptions = async (seriesName?: string) => {
seriesLoading.value = true
try {
const params: any = {
page: 1,
page_size: 10,
status: 1
}
if (seriesName) {
params.series_name = seriesName
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
seriesOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载系列选项失败:', error)
} finally {
seriesLoading.value = false
}
}
// 加载店铺选项(用于新增对话框,默认加载10条
const loadShopOptions = async (shopName?: string) => {
shopLoading.value = true
try {
const params: any = {
page: 1,
page_size: 10
}
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
shopOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载店铺选项失败:', error)
} finally {
shopLoading.value = false
}
}
// 加载搜索栏系列选项(默认加载10条)
const loadSearchSeriesOptions = async () => {
try {
const res = await PackageSeriesService.getPackageSeries({
page: 1,
page_size: 10,
status: 1
})
if (res.code === 0) {
searchSeriesOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载搜索栏系列选项失败:', error)
}
}
// 加载搜索栏店铺选项(默认加载10条)
const loadSearchShopOptions = async () => {
try {
const res = await ShopService.getShops({ page: 1, page_size: 10 })
if (res.code === 0) {
searchShopOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载搜索栏店铺选项失败:', error)
}
}
// 搜索系列(用于新增对话框)
const searchSeries = (query: string) => {
if (query) {
loadSeriesOptions(query)
} else {
loadSeriesOptions()
}
}
// 搜索店铺(用于新增对话框)
const searchShop = (query: string) => {
if (query) {
loadShopOptions(query)
} else {
loadShopOptions()
}
}
// 搜索系列(用于搜索栏)
const handleSearchSeries = async (query: string) => {
if (!query) {
loadSearchSeriesOptions()
return
}
try {
const res = await PackageSeriesService.getPackageSeries({
page: 1,
page_size: 10,
series_name: query,
status: 1
})
if (res.code === 0) {
searchSeriesOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索系列失败:', error)
}
}
// 搜索店铺(用于搜索栏)
const handleSearchShop = async (query: string) => {
if (!query) {
loadSearchShopOptions()
return
}
try {
const res = await ShopService.getShops({
page: 1,
page_size: 10,
shop_name: query
})
if (res.code === 0) {
searchShopOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索店铺失败:', error)
}
}
// 获取分配列表
const getTableData = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.page_size,
shop_id: searchForm.shop_id || undefined,
series_id: searchForm.series_id || undefined,
status: searchForm.status || undefined
}
const res = await ShopSeriesAllocationService.getShopSeriesAllocations(params)
if (res.code === 0) {
allocationList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
ElMessage.error('获取系列分配列表失败')
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 添加档位
const addTier = () => {
form.tier_config.tiers.push({
threshold: 0,
mode: 'fixed',
value: 0
})
}
// 删除档位
const removeTier = (index: number) => {
form.tier_config.tiers.splice(index, 1)
}
// 显示新增/编辑对话框
const showDialog = (type: string, row?: ShopSeriesAllocationResponse) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
form.id = row.id
form.series_id = row.series_id
form.shop_id = row.shop_id
form.base_commission = {
mode: row.base_commission.mode,
value: row.base_commission.value
}
form.enable_tier_commission = row.enable_tier_commission
if (row.enable_tier_commission && row.tier_config) {
form.tier_config = {
period_type: row.tier_config.period_type,
tier_type: row.tier_config.tier_type,
tiers: row.tier_config.tiers.map((t) => ({ ...t }))
}
} else {
form.tier_config = {
period_type: 'monthly',
tier_type: 'sales_count',
tiers: []
}
}
} else {
form.id = 0
form.series_id = undefined
form.shop_id = undefined
form.base_commission = {
mode: 'fixed',
value: 0
}
form.enable_tier_commission = false
form.tier_config = {
period_type: 'monthly',
tier_type: 'sales_count',
tiers: []
}
}
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
}
// 处理弹窗关闭事件
const handleDialogClosed = () => {
// 清除表单验证状态
formRef.value?.clearValidate()
// 重置表单数据
form.id = 0
form.series_id = undefined
form.shop_id = undefined
form.base_commission = {
mode: 'fixed',
value: 0
}
form.enable_tier_commission = false
form.tier_config = {
period_type: 'monthly',
tier_type: 'sales_count',
tiers: []
}
}
// 删除分配
const deleteAllocation = (row: ShopSeriesAllocationResponse) => {
ElMessageBox.confirm(
`确定删除系列 ${row.series_name} 对店铺 ${row.shop_name} 的分配吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}
)
.then(async () => {
try {
await ShopSeriesAllocationService.deleteShopSeriesAllocation(row.id)
ElMessage.success('删除成功')
await getTableData()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消删除
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
// 验证梯度档位
if (form.enable_tier_commission) {
if (form.tier_config.tiers.length === 0) {
ElMessage.warning('启用梯度返佣时至少需要添加一个档位')
return
}
// 验证档位阈值递增
const thresholds = form.tier_config.tiers.map((t: any) => t.threshold)
for (let i = 1; i < thresholds.length; i++) {
if (thresholds[i] <= thresholds[i - 1]) {
ElMessage.warning('档位阈值必须递增')
return
}
}
}
submitLoading.value = true
try {
const data: any = {
base_commission: {
mode: form.base_commission.mode,
value: form.base_commission.value
},
enable_tier_commission: form.enable_tier_commission
}
// 如果启用了梯度返佣,加入梯度配置
if (form.enable_tier_commission) {
data.tier_config = {
period_type: form.tier_config.period_type,
tier_type: form.tier_config.tier_type,
tiers: form.tier_config.tiers.map((t: any) => ({
threshold: t.threshold,
mode: t.mode,
value: t.value
}))
}
}
if (dialogType.value === 'add') {
data.series_id = form.series_id
data.shop_id = form.shop_id
await ShopSeriesAllocationService.createShopSeriesAllocation(data)
ElMessage.success('新增成功')
} else {
await ShopSeriesAllocationService.updateShopSeriesAllocation(form.id, data)
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 状态切换
const handleStatusChange = async (
row: ShopSeriesAllocationResponse,
newFrontendStatus: number
) => {
const oldStatus = row.status
const newApiStatus = frontendStatusToApi(newFrontendStatus)
row.status = newApiStatus
try {
await ShopSeriesAllocationService.updateShopSeriesAllocationStatus(row.id, newApiStatus)
ElMessage.success('状态切换成功')
} catch (error) {
row.status = oldStatus
console.error(error)
}
}
</script>
<style lang="scss" scoped>
.series-assign-page {
// 可以添加特定样式
}
.dialog-footer {
text-align: right;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.tier-list {
width: 100%;
}
.tier-item {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
</style>