fix: some
This commit is contained in:
74
src/api/modules/agentDistribution.ts
Normal file
74
src/api/modules/agentDistribution.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 代理扫码分销注册相关 API(公开,无需登录)
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 发送短信验证码请求参数
|
||||
*/
|
||||
export interface SendCodeParams {
|
||||
phone: string // 手机号
|
||||
scene: 'bind_phone' // 业务场景
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码响应
|
||||
*/
|
||||
export interface SendCodeResponse {
|
||||
cooldown_seconds: number // 冷却秒数,用于倒计时
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理扫码注册请求参数
|
||||
*/
|
||||
export interface AgentDistributionRegistrationParams {
|
||||
distribution_code: string // 分销码
|
||||
phone: string // 手机号
|
||||
code: string // 短信验证码
|
||||
password: string // 密码
|
||||
shop_name: string // 店铺名称
|
||||
shop_code: string // 店铺编号
|
||||
username: string // 用户名
|
||||
contact_name?: string // 联系人(选填)
|
||||
province?: string // 省(选填)
|
||||
city?: string // 市(选填)
|
||||
district?: string // 区县(选填)
|
||||
address?: string // 详细地址(选填)
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理扫码注册响应
|
||||
*/
|
||||
export interface AgentDistributionRegistrationResponse {
|
||||
id: number // 注册记录ID
|
||||
status: number // 状态(0 待审批)
|
||||
status_name: string // 状态名称
|
||||
}
|
||||
|
||||
export class AgentDistributionService extends BaseService {
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* POST /api/c/v1/auth/send-code(公开,不带 Token)
|
||||
*/
|
||||
static sendCode(params: SendCodeParams): Promise<BaseResponse<SendCodeResponse>> {
|
||||
return this.post<BaseResponse<SendCodeResponse>>('/api/c/v1/auth/send-code', params, {
|
||||
requestOptions: { withToken: false }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理扫码注册
|
||||
* POST /api/c/v1/agent-distribution-registrations(公开,不带 Token)
|
||||
*/
|
||||
static registerAgent(
|
||||
params: AgentDistributionRegistrationParams
|
||||
): Promise<BaseResponse<AgentDistributionRegistrationResponse>> {
|
||||
return this.post<BaseResponse<AgentDistributionRegistrationResponse>>(
|
||||
'/api/c/v1/agent-distribution-registrations',
|
||||
params,
|
||||
{ requestOptions: { withToken: false } }
|
||||
)
|
||||
}
|
||||
}
|
||||
93
src/api/modules/businessUserGroup.ts
Normal file
93
src/api/modules/businessUserGroup.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 业务用户组相关 API - AUG26-003
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BusinessUserGroupItem,
|
||||
BusinessUserGroupMembersResult,
|
||||
BusinessUserGroupQueryParams,
|
||||
CreateBusinessUserGroupParams,
|
||||
DeleteBusinessUserGroupParams,
|
||||
SetBusinessUserGroupMembersParams,
|
||||
UpdateBusinessUserGroupParams,
|
||||
BaseResponse,
|
||||
PaginationResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class BusinessUserGroupService extends BaseService {
|
||||
/**
|
||||
* 查询业务用户组列表
|
||||
* GET /api/admin/business-user-groups
|
||||
*/
|
||||
static getGroups(
|
||||
params?: BusinessUserGroupQueryParams
|
||||
): Promise<PaginationResponse<BusinessUserGroupItem>> {
|
||||
return this.getPage<BusinessUserGroupItem>('/api/admin/business-user-groups', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询业务用户组详情
|
||||
* GET /api/admin/business-user-groups/{id}
|
||||
*/
|
||||
static getGroup(id: number): Promise<BaseResponse<BusinessUserGroupItem>> {
|
||||
return this.getOne<BusinessUserGroupItem>(`/api/admin/business-user-groups/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建业务用户组
|
||||
* POST /api/admin/business-user-groups
|
||||
*/
|
||||
static createGroup(
|
||||
data: CreateBusinessUserGroupParams
|
||||
): Promise<BaseResponse<BusinessUserGroupItem>> {
|
||||
return this.create<BusinessUserGroupItem>('/api/admin/business-user-groups', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新业务用户组
|
||||
* PUT /api/admin/business-user-groups/{id}
|
||||
*/
|
||||
static updateGroup(
|
||||
id: number,
|
||||
data: UpdateBusinessUserGroupParams
|
||||
): Promise<BaseResponse<BusinessUserGroupItem>> {
|
||||
return this.update<BusinessUserGroupItem>(`/api/admin/business-user-groups/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除业务用户组(必须携带 confirm body)
|
||||
* DELETE /api/admin/business-user-groups/{id}
|
||||
*/
|
||||
static deleteGroup(id: number, data: DeleteBusinessUserGroupParams): Promise<BaseResponse> {
|
||||
return this.delete<BaseResponse>(`/api/admin/business-user-groups/${id}`, undefined, { data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置平台用户业务用户组归属(整体替换)
|
||||
* PUT /api/admin/business-user-groups/{id}/members
|
||||
*/
|
||||
static setGroupMembers(
|
||||
id: number,
|
||||
data: SetBusinessUserGroupMembersParams
|
||||
): Promise<BaseResponse<BusinessUserGroupMembersResult>> {
|
||||
return this.put<BaseResponse<BusinessUserGroupMembersResult>>(
|
||||
`/api/admin/business-user-groups/${id}/members`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量清空平台用户业务用户组归属(DELETE 带 body)
|
||||
* DELETE /api/admin/business-user-groups/members
|
||||
*/
|
||||
static clearGroupMembers(
|
||||
data: SetBusinessUserGroupMembersParams
|
||||
): Promise<BaseResponse<BusinessUserGroupMembersResult>> {
|
||||
return this.delete<BaseResponse<BusinessUserGroupMembersResult>>(
|
||||
'/api/admin/business-user-groups/members',
|
||||
undefined,
|
||||
{ data }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import type {
|
||||
CommissionRecordQueryParams,
|
||||
SubmitWithdrawalParams,
|
||||
ShopCommissionRecordPageResult,
|
||||
ShopCommissionRecordDetail,
|
||||
ShopCommissionRecordSource,
|
||||
ShopFundSummaryQueryParams,
|
||||
ShopFundSummaryPageResult,
|
||||
ShopCommissionStatsQueryParams,
|
||||
@@ -23,10 +25,85 @@ import type {
|
||||
DailyCommissionStatsItem,
|
||||
ResolveCommissionParams,
|
||||
MainWalletTransactionQueryParams,
|
||||
MainWalletTransactionPageResult
|
||||
MainWalletTransactionPageResult,
|
||||
WithdrawalQualificationSubmitParams,
|
||||
WithdrawalQualificationQueryParams,
|
||||
WithdrawalQualificationPageResult,
|
||||
VoidWithdrawalQualificationParams,
|
||||
ResubmitWithdrawalRequestParams,
|
||||
WithdrawalRequestDetail,
|
||||
} from '@/types/api/commission'
|
||||
|
||||
export class CommissionService extends BaseService {
|
||||
|
||||
// ==================== 提现资料资格相关 ====================
|
||||
|
||||
/**
|
||||
* 提交/替换提现资料资格
|
||||
* POST /api/admin/shops/{shop_id}/withdrawal-qualifications
|
||||
*/
|
||||
static submitWithdrawalQualification(
|
||||
shopId: number,
|
||||
data: WithdrawalQualificationSubmitParams
|
||||
): Promise<BaseResponse<{ id: number; status: number; status_name: string }>> {
|
||||
return this.post<BaseResponse<{ id: number; status: number; status_name: string }>>(
|
||||
`/api/admin/shops/${shopId}/withdrawal-qualifications`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询提现资料资格
|
||||
* GET /api/admin/shops/{shop_id}/withdrawal-qualifications
|
||||
*/
|
||||
static getWithdrawalQualifications(
|
||||
shopId: number,
|
||||
params?: WithdrawalQualificationQueryParams
|
||||
): Promise<BaseResponse<WithdrawalQualificationPageResult>> {
|
||||
return this.get<BaseResponse<WithdrawalQualificationPageResult>>(
|
||||
`/api/admin/shops/${shopId}/withdrawal-qualifications`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 作废提现资料资格(超管)
|
||||
* POST /api/admin/withdrawal-qualifications/{id}/void
|
||||
*/
|
||||
static voidWithdrawalQualification(
|
||||
id: number,
|
||||
data: VoidWithdrawalQualificationParams
|
||||
): Promise<BaseResponse> {
|
||||
return this.post<BaseResponse>(`/api/admin/withdrawal-qualifications/${id}/void`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重提被驳回的提现
|
||||
* PUT /api/admin/shops/{shop_id}/withdrawal-requests/{id}
|
||||
*/
|
||||
static resubmitWithdrawalRequest(
|
||||
shopId: number,
|
||||
id: number,
|
||||
data: ResubmitWithdrawalRequestParams
|
||||
): Promise<BaseResponse<WithdrawalRequestDetail>> {
|
||||
return this.put<BaseResponse<WithdrawalRequestDetail>>(
|
||||
`/api/admin/shops/${shopId}/withdrawal-requests/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 提现申请详情
|
||||
* GET /api/admin/shops/{shop_id}/withdrawal-requests/{id}
|
||||
*/
|
||||
static getWithdrawalRequestDetail(
|
||||
shopId: number,
|
||||
id: number
|
||||
): Promise<BaseResponse<WithdrawalRequestDetail>> {
|
||||
return this.get<BaseResponse<WithdrawalRequestDetail>>(
|
||||
`/api/admin/shops/${shopId}/withdrawal-requests/${id}`
|
||||
)
|
||||
}
|
||||
// ==================== 提现申请管理 ====================
|
||||
|
||||
/**
|
||||
@@ -107,6 +184,22 @@ export class CommissionService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取佣金明细详情
|
||||
* GET /api/admin/shops/{shop_id}/commission-records/{id}
|
||||
* source 省略时按原佣金处理;越权与不存在返回同一结果(佣金明细不存在)
|
||||
*/
|
||||
static getShopCommissionRecordDetail(
|
||||
shopId: number,
|
||||
id: number,
|
||||
source?: ShopCommissionRecordSource
|
||||
): Promise<BaseResponse<ShopCommissionRecordDetail>> {
|
||||
return this.get<BaseResponse<ShopCommissionRecordDetail>>(
|
||||
`/api/admin/shops/${shopId}/commission-records/${id}`,
|
||||
source ? { source } : undefined
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理商提现记录
|
||||
* GET /api/admin/shops/{shop_id}/withdrawal-requests
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { BaseResponse } from '@/types/api'
|
||||
export type ExchangeAssetType = 'iot_card' | 'device'
|
||||
export type ExchangeFlowType = 'shipping' | 'direct'
|
||||
export type ExchangeStatus = 1 | 2 | 3 | 4 | 5
|
||||
export type ExchangeMigrationStatus = 'not_migrated' | 'pending' | 'migrated' | 'failed'
|
||||
|
||||
// 换货单查询参数
|
||||
export interface ExchangeQueryParams {
|
||||
@@ -64,6 +65,9 @@ export interface ExchangeResponse {
|
||||
current_approver_summary?: string | null // 当前审批人摘要
|
||||
processing_status?: string | null // 业务处理状态
|
||||
processing_status_name?: string | null // 业务处理状态名称
|
||||
migration_status?: ExchangeMigrationStatus // 迁移状态(not_migrated/pending/migrated/failed)
|
||||
migration_status_name?: string // 迁移状态中文名
|
||||
migration_failure_reason?: string | null // 迁移失败原因(仅 migration_status=failed 时返回)
|
||||
remark?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
|
||||
87
src/api/modules/h5PopupConfiguration.ts
Normal file
87
src/api/modules/h5PopupConfiguration.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* H5 运营弹窗配置相关 API
|
||||
* 契约来源:docs/产品迭代8月份/通知.md
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
CreateH5PopupConfigurationRequest,
|
||||
H5PopupConfiguration,
|
||||
H5PopupConfigurationListResponse,
|
||||
H5PopupConfigurationQueryParams,
|
||||
UpdateH5PopupConfigurationRequest
|
||||
} from '@/types/api'
|
||||
|
||||
/**
|
||||
* H5 运营弹窗配置服务
|
||||
* 仅超级管理员和平台账号可访问,代理/企业/个人由后端 403 兜底。
|
||||
*/
|
||||
export class H5PopupConfigurationService extends BaseService {
|
||||
/**
|
||||
* 查询运营弹窗配置列表(分页,按最近更新时间倒序)
|
||||
* GET /api/admin/h5-popup-configurations
|
||||
*/
|
||||
static getConfigurations(
|
||||
params?: H5PopupConfigurationQueryParams
|
||||
): Promise<BaseResponse<H5PopupConfigurationListResponse>> {
|
||||
return this.get<BaseResponse<H5PopupConfigurationListResponse>>(
|
||||
'/api/admin/h5-popup-configurations',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建运营弹窗配置
|
||||
* POST /api/admin/h5-popup-configurations
|
||||
*/
|
||||
static createConfiguration(
|
||||
data: CreateH5PopupConfigurationRequest
|
||||
): Promise<BaseResponse<H5PopupConfiguration>> {
|
||||
return this.post<BaseResponse<H5PopupConfiguration>>('/api/admin/h5-popup-configurations', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询运营弹窗配置详情
|
||||
* GET /api/admin/h5-popup-configurations/{id}
|
||||
*/
|
||||
static getConfiguration(id: number): Promise<BaseResponse<H5PopupConfiguration>> {
|
||||
return this.getOne<H5PopupConfiguration>(`/api/admin/h5-popup-configurations/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新运营弹窗配置(成功即版本 +1)
|
||||
* PUT /api/admin/h5-popup-configurations/{id}
|
||||
*/
|
||||
static updateConfiguration(
|
||||
id: number,
|
||||
data: UpdateH5PopupConfigurationRequest
|
||||
): Promise<BaseResponse<H5PopupConfiguration>> {
|
||||
return this.put<BaseResponse<H5PopupConfiguration>>(
|
||||
`/api/admin/h5-popup-configurations/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用运营弹窗配置(刷新最近更新时间,不递增版本)
|
||||
* POST /api/admin/h5-popup-configurations/{id}/enable
|
||||
*/
|
||||
static enableConfiguration(id: number): Promise<BaseResponse<H5PopupConfiguration>> {
|
||||
return this.post<BaseResponse<H5PopupConfiguration>>(
|
||||
`/api/admin/h5-popup-configurations/${id}/enable`,
|
||||
{ id }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用运营弹窗配置(刷新最近更新时间,不递增版本)
|
||||
* POST /api/admin/h5-popup-configurations/{id}/disable
|
||||
*/
|
||||
static disableConfiguration(id: number): Promise<BaseResponse<H5PopupConfiguration>> {
|
||||
return this.post<BaseResponse<H5PopupConfiguration>>(
|
||||
`/api/admin/h5-popup-configurations/${id}/disable`,
|
||||
{ id }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* API 服务模块统一导出
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ export { AccountService } from './account'
|
||||
export { ShopService } from './shop'
|
||||
export { CardService } from './card'
|
||||
export { CommissionService } from './commission'
|
||||
export { BusinessUserGroupService } from './businessUserGroup'
|
||||
export { EnterpriseService } from './enterprise'
|
||||
export { StorageService } from './storage'
|
||||
export { AuthorizationService } from './authorization'
|
||||
@@ -42,5 +43,13 @@ export { AuditService } from './audit'
|
||||
export { WecomService } from './wecom'
|
||||
export { EmployeeCollectionService } from './employeeCollection'
|
||||
|
||||
export { PhoneAssetAssociationService } from './phoneAsset'
|
||||
|
||||
export { AgentDistributionService } from './agentDistribution'
|
||||
|
||||
export { H5PopupConfigurationService } from './h5PopupConfiguration'
|
||||
|
||||
// TODO: 按需添加其他业务模块
|
||||
// export { SettingService } from './setting'
|
||||
|
||||
export { PackageTrafficAlertService } from './packageTrafficAlert'
|
||||
|
||||
101
src/api/modules/packageTrafficAlert.ts
Normal file
101
src/api/modules/packageTrafficAlert.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 套餐真流量预警 API 服务
|
||||
* 6 个接口仅超级管理员/平台账号可访问,其他账号由后端 403 兜底
|
||||
* 对应 docs/产品迭代8月份/套餐真流量预警_API_前端简版.md
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
PaginationResponse,
|
||||
PackageTrafficAlertRuleItem,
|
||||
PackageTrafficAlertRuleQueryParams,
|
||||
CreatePackageTrafficAlertRuleParams,
|
||||
UpdatePackageTrafficAlertRuleParams,
|
||||
PackageTrafficAlertRecordItem,
|
||||
PackageTrafficAlertRecordDetail,
|
||||
PackageTrafficAlertRecordQueryParams,
|
||||
ExportPackageTrafficAlertParams,
|
||||
CreatePackageTrafficAlertExportResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class PackageTrafficAlertService extends BaseService {
|
||||
/**
|
||||
* 查询套餐真流量预警规则列表
|
||||
* GET /api/admin/package-traffic-alert-rules
|
||||
* 返回套餐、商品当前真流量额度、阈值、启用状态、备注与更新时间
|
||||
*/
|
||||
static getAlertRules(
|
||||
params?: PackageTrafficAlertRuleQueryParams
|
||||
): Promise<PaginationResponse<PackageTrafficAlertRuleItem>> {
|
||||
return this.getPage<PackageTrafficAlertRuleItem>(
|
||||
'/api/admin/package-traffic-alert-rules',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建套餐真流量预警规则
|
||||
* POST /api/admin/package-traffic-alert-rules
|
||||
* 同一套餐商品最多一条真流量预警规则
|
||||
*/
|
||||
static createAlertRule(
|
||||
data: CreatePackageTrafficAlertRuleParams
|
||||
): Promise<BaseResponse<PackageTrafficAlertRuleItem>> {
|
||||
return this.create<PackageTrafficAlertRuleItem>('/api/admin/package-traffic-alert-rules', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改套餐真流量预警规则
|
||||
* PUT /api/admin/package-traffic-alert-rules/{id}
|
||||
* 允许修改阈值、启停与备注;修改不影响既有预警快照,停用后扫描不再创建新预警
|
||||
*/
|
||||
static updateAlertRule(
|
||||
id: number,
|
||||
data: UpdatePackageTrafficAlertRuleParams
|
||||
): Promise<BaseResponse<PackageTrafficAlertRuleItem>> {
|
||||
return this.update<PackageTrafficAlertRuleItem>(
|
||||
`/api/admin/package-traffic-alert-rules/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询套餐真流量预警记录列表
|
||||
* GET /api/admin/package-traffic-alerts
|
||||
* 列表数据除 business_user_group_names 外均为触发时快照
|
||||
*/
|
||||
static getAlertRecords(
|
||||
params?: PackageTrafficAlertRecordQueryParams
|
||||
): Promise<PaginationResponse<PackageTrafficAlertRecordItem>> {
|
||||
return this.getPage<PackageTrafficAlertRecordItem>('/api/admin/package-traffic-alerts', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询套餐真流量预警记录详情
|
||||
* GET /api/admin/package-traffic-alerts/{id}
|
||||
* 越权查询统一按资源不可见处理
|
||||
*/
|
||||
static getAlertRecordDetail(id: number): Promise<BaseResponse<PackageTrafficAlertRecordDetail>> {
|
||||
// 越权查询详情统一按资源不可见处理,抑制全局报错提示,由页面给出统一文案
|
||||
return this.getOne<PackageTrafficAlertRecordDetail>(
|
||||
`/api/admin/package-traffic-alerts/${id}`,
|
||||
undefined,
|
||||
{ requestOptions: { errorMessageMode: 'none' } }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出套餐真流量达量预警
|
||||
* POST /api/admin/package-traffic-alerts/export
|
||||
* 只创建异步导出任务,后续通过既有导出任务列表下载文件
|
||||
*/
|
||||
static exportAlertRecords(
|
||||
data: ExportPackageTrafficAlertParams
|
||||
): Promise<BaseResponse<CreatePackageTrafficAlertExportResponse>> {
|
||||
return this.post<BaseResponse<CreatePackageTrafficAlertExportResponse>>(
|
||||
'/api/admin/package-traffic-alerts/export',
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
107
src/api/modules/phoneAsset.ts
Normal file
107
src/api/modules/phoneAsset.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 手机号资产关联相关 API
|
||||
* 契约来源:docs/产品迭代8月份/手机号资产关联.md
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
PhoneAssetAssociationListResponse,
|
||||
PhoneAssetAssociationQueryParams,
|
||||
BatchUnbindPhoneAssetAssociationRequest,
|
||||
BatchUnbindPhoneAssetAssociationResponse,
|
||||
CreatePhoneAssetUnbindImportRequest,
|
||||
PhoneAssetUnbindImportTask,
|
||||
PhoneAssetUnbindImportTaskDetail,
|
||||
PhoneAssetUnbindImportTaskListResponse,
|
||||
PhoneAssetUnbindImportTaskQueryParams,
|
||||
UnbindPhoneAssetAssociationResponse
|
||||
} from '@/types/api'
|
||||
|
||||
/**
|
||||
* 手机号资产关联服务
|
||||
* 仅超级管理员和平台账号可访问,代理/企业/个人由后端 403 兜底。
|
||||
*/
|
||||
export class PhoneAssetAssociationService extends BaseService {
|
||||
/**
|
||||
* 查询手机号资产关联列表(分页)
|
||||
* GET /api/admin/phone-asset-associations
|
||||
*/
|
||||
static getAssociations(
|
||||
params?: PhoneAssetAssociationQueryParams
|
||||
): Promise<BaseResponse<PhoneAssetAssociationListResponse>> {
|
||||
return this.get<BaseResponse<PhoneAssetAssociationListResponse>>(
|
||||
'/api/admin/phone-asset-associations',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除单条手机号资产关联
|
||||
* DELETE /api/admin/phone-asset-associations/{id}
|
||||
*
|
||||
* 注意:接口文档未定义请求体,reason 与 confirmed 当前按查询参数传递;
|
||||
* 联调时若后端以 JSON body 接收,需调整为请求体方式。
|
||||
*/
|
||||
static unbindAssociation(
|
||||
id: number,
|
||||
reason: string,
|
||||
confirmed: boolean
|
||||
): Promise<BaseResponse<UnbindPhoneAssetAssociationResponse>> {
|
||||
return this.delete<BaseResponse<UnbindPhoneAssetAssociationResponse>>(
|
||||
`/api/admin/phone-asset-associations/${id}`,
|
||||
{ reason, confirmed }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 按资产批量解除手机号关联
|
||||
* POST /api/admin/phone-asset-associations/batch-unbind
|
||||
*/
|
||||
static batchUnbind(
|
||||
data: BatchUnbindPhoneAssetAssociationRequest
|
||||
): Promise<BaseResponse<BatchUnbindPhoneAssetAssociationResponse>> {
|
||||
return this.post<BaseResponse<BatchUnbindPhoneAssetAssociationResponse>>(
|
||||
'/api/admin/phone-asset-associations/batch-unbind',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 CSV 解绑导入任务
|
||||
* POST /api/admin/phone-asset-associations/unbind-imports
|
||||
*/
|
||||
static createUnbindImport(
|
||||
data: CreatePhoneAssetUnbindImportRequest
|
||||
): Promise<BaseResponse<PhoneAssetUnbindImportTask>> {
|
||||
return this.post<BaseResponse<PhoneAssetUnbindImportTask>>(
|
||||
'/api/admin/phone-asset-associations/unbind-imports',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询解绑导入任务列表(分页)
|
||||
* GET /api/admin/phone-asset-associations/unbind-imports
|
||||
*/
|
||||
static getUnbindImportTasks(
|
||||
params?: PhoneAssetUnbindImportTaskQueryParams
|
||||
): Promise<BaseResponse<PhoneAssetUnbindImportTaskListResponse>> {
|
||||
return this.get<BaseResponse<PhoneAssetUnbindImportTaskListResponse>>(
|
||||
'/api/admin/phone-asset-associations/unbind-imports',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询解绑导入任务详情
|
||||
* GET /api/admin/phone-asset-associations/unbind-imports/{id}
|
||||
*/
|
||||
static getUnbindImportTaskDetail(
|
||||
id: number
|
||||
): Promise<BaseResponse<PhoneAssetUnbindImportTaskDetail>> {
|
||||
return this.getOne<PhoneAssetUnbindImportTaskDetail>(
|
||||
`/api/admin/phone-asset-associations/unbind-imports/${id}`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ import type {
|
||||
RefundListResponse,
|
||||
CreateRefundRequest,
|
||||
ResubmitRefundRequest,
|
||||
ApproveRefundRequest,
|
||||
RejectRefundRequest,
|
||||
ReturnRefundRequest,
|
||||
BaseResponse
|
||||
} from '@/types/api'
|
||||
|
||||
@@ -37,6 +40,33 @@ export class RefundService extends BaseService {
|
||||
return this.post<BaseResponse<Refund>>('/api/admin/refunds', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过退款申请(仅存量无企微审批实例的申请可用)
|
||||
* @param id 退款申请ID
|
||||
* @param data 审批通过请求参数
|
||||
*/
|
||||
static approveRefund(id: number, data: ApproveRefundRequest): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/approve`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批拒绝退款申请(仅存量无企微审批实例的申请可用)
|
||||
* @param id 退款申请ID
|
||||
* @param data 拒绝请求参数
|
||||
*/
|
||||
static rejectRefund(id: number, data: RejectRefundRequest): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/reject`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 退回退款申请(仅存量无企微审批实例的申请可用)
|
||||
* @param id 退款申请ID
|
||||
* @param data 退回请求参数
|
||||
*/
|
||||
static returnRefund(id: number, data: ReturnRefundRequest): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/return`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新提交退款申请
|
||||
* @param id 退款申请ID
|
||||
@@ -53,4 +83,4 @@ export class RefundService extends BaseService {
|
||||
static triggerApproval(id: number): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/trigger-approval`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ import type {
|
||||
AssignShopRolesRequest,
|
||||
UpdateShopCreditLimitRequest,
|
||||
UpdateShopCreditLimitResponse,
|
||||
BatchUpdateBusinessOwnerParams,
|
||||
BatchUpdateBusinessOwnerResult,
|
||||
CreateBusinessOwnerImportParams,
|
||||
BusinessOwnerImportDetail,
|
||||
BusinessOwnerImportQueryParams,
|
||||
BaseResponse,
|
||||
PaginationResponse,
|
||||
ShopBusinessOwnerCandidate,
|
||||
@@ -155,4 +160,56 @@ export class ShopService extends BaseService {
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
// ========== 批量交接平台业务员 ==========
|
||||
|
||||
/**
|
||||
* 勾选批量交接平台业务员
|
||||
* PUT /api/admin/shops/business-owner/batch
|
||||
* @param data shop_ids(1-500)+ business_owner_account_id(null=清空)
|
||||
*/
|
||||
static batchUpdateBusinessOwner(
|
||||
data: BatchUpdateBusinessOwnerParams
|
||||
): Promise<BaseResponse<BatchUpdateBusinessOwnerResult>> {
|
||||
return this.put<BaseResponse<BatchUpdateBusinessOwnerResult>>(
|
||||
'/api/admin/shops/business-owner/batch',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
// ========== 业务负责人 CSV 导入 ==========
|
||||
|
||||
/**
|
||||
* 创建业务负责人导入任务
|
||||
* POST /api/admin/shops/business-owner-imports
|
||||
* @param data 上传后拿到的 file_key
|
||||
*/
|
||||
static createBusinessOwnerImport(
|
||||
data: CreateBusinessOwnerImportParams
|
||||
): Promise<BaseResponse<BusinessOwnerImportDetail>> {
|
||||
return this.create<BusinessOwnerImportDetail>('/api/admin/shops/business-owner-imports', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询业务负责人导入任务详情
|
||||
* GET /api/admin/shops/business-owner-imports/{id}
|
||||
*/
|
||||
static getBusinessOwnerImportDetail(
|
||||
id: number
|
||||
): Promise<BaseResponse<BusinessOwnerImportDetail>> {
|
||||
return this.getOne<BusinessOwnerImportDetail>(`/api/admin/shops/business-owner-imports/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询业务负责人导入任务列表
|
||||
* GET /api/admin/shops/business-owner-imports
|
||||
*/
|
||||
static getBusinessOwnerImportList(
|
||||
params?: BusinessOwnerImportQueryParams
|
||||
): Promise<PaginationResponse<BusinessOwnerImportDetail>> {
|
||||
return this.getPage<BusinessOwnerImportDetail>(
|
||||
'/api/admin/shops/business-owner-imports',
|
||||
params
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export type FilePurpose =
|
||||
| 'iot_import'
|
||||
| 'batch_purchase'
|
||||
| 'device_batch_allocation'
|
||||
| 'shop_import'
|
||||
| 'phone_unbind_import'
|
||||
| 'export'
|
||||
| 'attachment'
|
||||
|
||||
|
||||
269
src/components/business/CommissionRecordDetailDialog.vue
Normal file
269
src/components/business/CommissionRecordDetailDialog.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="modelValue"
|
||||
title="佣金明细详情"
|
||||
width="780px"
|
||||
@update:model-value="handleVisibleChange"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<div v-loading="loading" class="commission-record-detail">
|
||||
<template v-if="record">
|
||||
<ElDescriptions :column="2" border size="small">
|
||||
<ElDescriptionsItem label="记录类型">
|
||||
<ElTag :type="isClawback ? 'danger' : 'primary'" size="small">
|
||||
{{ recordSourceName }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="statusTagType" size="small">
|
||||
{{ record.status_name || statusName }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金金额">
|
||||
<span :class="{ 'amount-negative': isNegative(record.amount) }">
|
||||
{{ formatMoney(record.amount) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="入账后佣金余额">
|
||||
<span :class="{ 'amount-negative': isNegative(record.balance_after) }">
|
||||
{{ formatMoney(record.balance_after) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金来源">
|
||||
<ElTag :type="sourceTagType" size="small">{{ sourceName }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="是否可提现">
|
||||
<ElTag v-if="isClawback" type="danger" size="small">不可提现</ElTag>
|
||||
<span v-else>可提现</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="订单号">{{ record.order_no || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卖家店铺">
|
||||
{{ record.seller_shop_name || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="ICCID">{{ record.iccid || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备虚拟号">
|
||||
{{ record.virtual_no || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="记录生成时间">
|
||||
{{ record.created_at ? formatDateTime(record.created_at) : '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金入账时间">
|
||||
{{ record.released_at ? formatDateTime(record.released_at) : '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<template v-if="isClawback">
|
||||
<ElDescriptionsItem label="被回溯原佣金ID">
|
||||
{{ record.original_commission_id ?? '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="来源退款单号">
|
||||
{{ record.refund_no || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
</template>
|
||||
</ElDescriptions>
|
||||
|
||||
<template v-if="!isClawback">
|
||||
<div class="detail-section-title">
|
||||
回溯明细
|
||||
<span class="detail-section-summary">
|
||||
累计回溯金额:{{ formatMoney(clawbackTotalAmount) }}
|
||||
</span>
|
||||
</div>
|
||||
<ElTable :data="clawbackRecords" size="small" border :max-height="260">
|
||||
<ElTableColumn label="回溯金额" prop="amount" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-negative">{{ formatMoney(row.amount) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="回溯后余额" prop="balance_after" width="130">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'amount-negative': isNegative(row.balance_after) }">
|
||||
{{ formatMoney(row.balance_after) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
label="来源退款单号"
|
||||
prop="refund_no"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.refund_no || '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status_name" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag type="danger" size="small">{{ row.status_name || '回溯' }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="生成时间" prop="created_at" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.created_at ? formatDateTime(row.created_at) : '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
|
||||
<template v-else-if="originalCommission">
|
||||
<div class="detail-section-title">来源原佣金</div>
|
||||
<ElDescriptions :column="2" border size="small">
|
||||
<ElDescriptionsItem label="原佣金记录ID">
|
||||
{{ originalCommission.id }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="原佣金金额">
|
||||
{{ formatMoney(originalCommission.amount) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="原佣金余额">
|
||||
{{ formatMoney(originalCommission.balance_after) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="原佣金状态">
|
||||
{{ originalCommission.status_name || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="订单号">
|
||||
{{ originalCommission.order_no || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金入账时间">
|
||||
{{
|
||||
originalCommission.released_at
|
||||
? formatDateTime(originalCommission.released_at)
|
||||
: '-'
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</template>
|
||||
</template>
|
||||
<ElEmpty v-else-if="!loading" description="佣金明细不存在" />
|
||||
</div>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CommissionService } from '@/api/modules'
|
||||
import type { ShopCommissionRecordDetail, ShopCommissionRecordSource } from '@/types/api'
|
||||
import {
|
||||
COMMISSION_RECORD_SOURCE_MAP,
|
||||
CommissionSourceMap,
|
||||
CommissionStatusMap,
|
||||
isCommissionClawbackRecord
|
||||
} from '@/config/constants/commission'
|
||||
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
|
||||
defineOptions({ name: 'CommissionRecordDetailDialog' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
shopId?: number | null
|
||||
recordId?: number | null
|
||||
source?: ShopCommissionRecordSource
|
||||
}>(),
|
||||
{
|
||||
shopId: null,
|
||||
recordId: null,
|
||||
source: 'original'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref<ShopCommissionRecordDetail | null>(null)
|
||||
|
||||
const record = computed(() => detail.value?.record || null)
|
||||
const isClawback = computed(() =>
|
||||
isCommissionClawbackRecord({
|
||||
source: detail.value?.source || props.source,
|
||||
status: detail.value?.record?.status
|
||||
})
|
||||
)
|
||||
const commissionStatusMeta = computed(
|
||||
() =>
|
||||
CommissionStatusMap[Number(record.value?.status) as keyof typeof CommissionStatusMap] || null
|
||||
)
|
||||
const commissionSourceMeta = computed(
|
||||
() =>
|
||||
CommissionSourceMap[
|
||||
(record.value?.commission_source || '') as keyof typeof CommissionSourceMap
|
||||
] || null
|
||||
)
|
||||
const recordSourceMeta = computed(
|
||||
() => COMMISSION_RECORD_SOURCE_MAP[isClawback.value ? 'clawback' : 'original']
|
||||
)
|
||||
const recordSourceName = computed(() => recordSourceMeta.value.label)
|
||||
const statusName = computed(() => commissionStatusMeta.value?.label || '-')
|
||||
const statusTagType = computed(() => commissionStatusMeta.value?.type || 'info')
|
||||
const sourceName = computed(
|
||||
() => commissionSourceMeta.value?.label || record.value?.commission_source || '-'
|
||||
)
|
||||
const sourceTagType = computed(() => commissionSourceMeta.value?.type || 'info')
|
||||
const clawbackRecords = computed(() => detail.value?.clawback_records ?? [])
|
||||
const clawbackTotalAmount = computed(() => record.value?.clawback_total_amount ?? 0)
|
||||
const originalCommission = computed(() => detail.value?.original_commission || null)
|
||||
|
||||
const isNegative = (value?: number | null) => typeof value === 'number' && value < 0
|
||||
|
||||
const loadDetail = async () => {
|
||||
if (!props.shopId || !props.recordId) return
|
||||
|
||||
loading.value = true
|
||||
detail.value = null
|
||||
try {
|
||||
const res = await CommissionService.getShopCommissionRecordDetail(
|
||||
props.shopId,
|
||||
props.recordId,
|
||||
props.source
|
||||
)
|
||||
if (res.code === 0) {
|
||||
detail.value = res.data
|
||||
} else {
|
||||
ElMessage.error(res.msg || '佣金明细不存在')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '佣金明细不存在'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleVisibleChange = (value: boolean) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
detail.value = null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.shopId, props.recordId, props.source] as const,
|
||||
([visible]) => {
|
||||
if (visible) loadDetail()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.commission-record-detail {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.amount-negative {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 16px 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-section-summary {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ElDialog v-model="dialogVisible" title="创建退款申请" width="40%" @closed="handleDialogClosed">
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
||||
<ElDialog v-model="dialogVisible" title="创建退款申请" width="45%" @closed="handleDialogClosed">
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<ElFormItem label="订单号" prop="order_id">
|
||||
<ElSelect
|
||||
v-if="!hasInitialOrder"
|
||||
@@ -23,6 +23,13 @@
|
||||
</ElSelect>
|
||||
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款方式" prop="method">
|
||||
<ElRadioGroup v-model="formData.method">
|
||||
<ElRadio v-for="opt in RefundMethodOptions" :key="opt.value" :label="opt.value">
|
||||
{{ opt.label }}
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款金额" prop="requested_refund_amount">
|
||||
<ElInputNumber
|
||||
v-model="formData.requested_refund_amount"
|
||||
@@ -46,7 +53,21 @@
|
||||
placeholder="请输入退款原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款凭证" prop="refund_voucher_key">
|
||||
<template v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT">
|
||||
<ElFormItem label="收款账户名" prop="customer_account_name">
|
||||
<ElInput v-model="formData.customer_account_name" placeholder="请输入收款账户名" maxlength="50" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="收款账号" prop="customer_account_number">
|
||||
<ElInput v-model="formData.customer_account_number" placeholder="请输入收款账号" maxlength="64" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="开户银行">
|
||||
<ElInput v-model="formData.customer_bank_name" placeholder="选填" maxlength="50" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem
|
||||
:label="formData.method === RefundMethod.CUSTOMER_ACCOUNT ? '退款凭证(必填)' : '退款凭证'"
|
||||
prop="refund_voucher_key"
|
||||
>
|
||||
<VoucherUpload
|
||||
ref="uploadRef"
|
||||
v-model="formData.refund_voucher_key"
|
||||
@@ -54,6 +75,9 @@
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="formRef?.validateField('refund_voucher_key')"
|
||||
/>
|
||||
<div v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT" class="voucher-tip">
|
||||
客户收款信息方式下须同时提供客户收款信息与至少 1 个退款凭证
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -77,7 +101,8 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { RefundService, OrderService } from '@/api/modules'
|
||||
import type { CreateRefundRequest, Order } from '@/types/api'
|
||||
import { RefundMethod, type CreateRefundRequest, type Order } from '@/types/api'
|
||||
import { RefundMethodOptions } from '@/config/constants/refund'
|
||||
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
@@ -110,16 +135,24 @@
|
||||
const formData = reactive<{
|
||||
order_id: number | null
|
||||
package_usage_id?: number
|
||||
method?: RefundMethod
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount: number
|
||||
refund_reason: string
|
||||
customer_account_name: string
|
||||
customer_account_number: string
|
||||
customer_bank_name: string
|
||||
refund_voucher_key: string[]
|
||||
}>({
|
||||
order_id: null,
|
||||
package_usage_id: undefined,
|
||||
method: RefundMethod.ORIGINAL_ROUTE,
|
||||
requested_refund_amount: undefined,
|
||||
actual_received_amount: 0,
|
||||
refund_reason: '',
|
||||
customer_account_name: '',
|
||||
customer_account_number: '',
|
||||
customer_bank_name: '',
|
||||
refund_voucher_key: []
|
||||
})
|
||||
|
||||
@@ -134,14 +167,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
const requireCustomerAccount = (field: string, message: string) => ({
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (formData.method === RefundMethod.CUSTOMER_ACCOUNT && !value?.trim()) {
|
||||
callback(new Error(message))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
})
|
||||
|
||||
const validateVoucherKey = (
|
||||
_rule: unknown,
|
||||
value: string[],
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
if (
|
||||
formData.method === RefundMethod.CUSTOMER_ACCOUNT &&
|
||||
(!Array.isArray(value) || value.length === 0)
|
||||
) {
|
||||
callback(new Error('请上传至少 1 个退款凭证'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
order_id: [{ required: true, message: '请选择订单', trigger: 'change' }],
|
||||
method: [{ required: true, message: '请选择退款方式', trigger: 'change' }],
|
||||
requested_refund_amount: [
|
||||
{ required: true, message: '请输入申请退款金额', trigger: 'blur' },
|
||||
{ validator: validateRefundAmount, trigger: 'blur' }
|
||||
],
|
||||
refund_reason: [{ required: true, message: '请输入退款原因', trigger: 'blur' }],
|
||||
refund_voucher_key: [{ required: true, message: '请上传退款凭证', trigger: 'change' }]
|
||||
customer_account_name: requireCustomerAccount('customer_account_name', '请填写收款账户名'),
|
||||
customer_account_number: requireCustomerAccount('customer_account_number', '请填写收款账号'),
|
||||
refund_voucher_key: [{ validator: validateVoucherKey, trigger: 'change' }]
|
||||
})
|
||||
|
||||
const hasInitialOrder = computed(() => !!props.initialOrderId || !!props.initialOrderNo)
|
||||
@@ -202,9 +264,13 @@
|
||||
voucherUploading.value = false
|
||||
formData.order_id = null
|
||||
formData.package_usage_id = undefined
|
||||
formData.method = RefundMethod.ORIGINAL_ROUTE
|
||||
formData.requested_refund_amount = undefined
|
||||
formData.actual_received_amount = 0
|
||||
formData.refund_reason = ''
|
||||
formData.customer_account_name = ''
|
||||
formData.customer_account_number = ''
|
||||
formData.customer_bank_name = ''
|
||||
formData.refund_voucher_key = []
|
||||
orderSearchOptions.value = []
|
||||
uploadRef.value?.clearFiles(false)
|
||||
@@ -222,20 +288,30 @@
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const data: CreateRefundRequest = {
|
||||
actual_received_amount: formData.actual_received_amount,
|
||||
order_id: formData.order_id!,
|
||||
method: formData.method!,
|
||||
package_usage_id: formData.package_usage_id ?? 0,
|
||||
refund_reason: formData.refund_reason,
|
||||
refund_voucher_key: formData.refund_voucher_key,
|
||||
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0
|
||||
}
|
||||
if (formData.method === RefundMethod.CUSTOMER_ACCOUNT) {
|
||||
data.customer_account_info = {
|
||||
account_name: formData.customer_account_name.trim(),
|
||||
account_number: formData.customer_account_number.trim(),
|
||||
...(formData.customer_bank_name.trim()
|
||||
? { bank_name: formData.customer_bank_name.trim() }
|
||||
: {})
|
||||
}
|
||||
data.refund_voucher_key = formData.refund_voucher_key
|
||||
} else if (formData.refund_voucher_key.length) {
|
||||
data.refund_voucher_key = formData.refund_voucher_key
|
||||
}
|
||||
await RefundService.createRefund(data)
|
||||
ElMessage.success('退款申请创建成功')
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error(getErrorMessage(error, '退款申请创建失败'))
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
@@ -257,3 +333,12 @@
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.voucher-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
201
src/components/business/RefundApprovalDialog.vue
Normal file
201
src/components/business/RefundApprovalDialog.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="480px"
|
||||
@update:model-value="handleVisibleChange"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<template v-if="action === 'approve'">
|
||||
<ElForm ref="approveFormRef" :model="approveForm" :rules="approveRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="审批退款金额">
|
||||
<ElInputNumber
|
||||
v-model="approveForm.approved_refund_amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
placeholder="不填则按申请金额审批"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="审批备注">
|
||||
<ElInput
|
||||
v-model="approveForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入审批备注(选填)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</template>
|
||||
<template v-else-if="action === 'reject'">
|
||||
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="拒绝原因" prop="reject_reason">
|
||||
<ElInput
|
||||
v-model="rejectForm.reject_reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入拒绝原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElForm ref="returnFormRef" :model="returnForm" :rules="returnRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退回备注">
|
||||
<ElInput
|
||||
v-model="returnForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入退回备注(选填)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="handleVisibleChange(false)">取消</ElButton>
|
||||
<ElButton type="primary" :loading="submitLoading" @click="handleConfirm">
|
||||
{{ confirmText }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { RefundService } from '@/api/modules'
|
||||
import type { Refund } from '@/types/api'
|
||||
import { yuanToFen } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'RefundApprovalDialog' })
|
||||
|
||||
type ApprovalAction = 'approve' | 'reject' | 'return'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
refund: Refund | null
|
||||
action: ApprovalAction
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const submitLoading = ref(false)
|
||||
const approveFormRef = ref<FormInstance>()
|
||||
const rejectFormRef = ref<FormInstance>()
|
||||
const returnFormRef = ref<FormInstance>()
|
||||
|
||||
const approveForm = reactive<{
|
||||
approved_refund_amount?: number
|
||||
remark: string
|
||||
}>({
|
||||
approved_refund_amount: undefined,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rejectForm = reactive<{
|
||||
reject_reason: string
|
||||
}>({
|
||||
reject_reason: ''
|
||||
})
|
||||
|
||||
const returnForm = reactive<{
|
||||
remark: string
|
||||
}>({
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const approveRules = reactive<FormRules>({})
|
||||
const rejectRules = reactive<FormRules>({
|
||||
reject_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
|
||||
})
|
||||
const returnRules = reactive<FormRules>({})
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (props.action === 'approve') return '审批通过退款申请'
|
||||
if (props.action === 'reject') return '审批拒绝退款申请'
|
||||
return '退回退款申请'
|
||||
})
|
||||
|
||||
const confirmText = computed(() => {
|
||||
if (props.action === 'approve') return '确认通过'
|
||||
if (props.action === 'reject') return '确认拒绝'
|
||||
return '确认退回'
|
||||
})
|
||||
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:modelValue', visible)
|
||||
}
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
approveForm.approved_refund_amount = undefined
|
||||
approveForm.remark = ''
|
||||
rejectForm.reject_reason = ''
|
||||
returnForm.remark = ''
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!props.refund || submitLoading.value) return
|
||||
|
||||
let valid = true
|
||||
if (props.action === 'approve') {
|
||||
valid = await approveFormRef.value?.validate().catch(() => false) !== false
|
||||
} else if (props.action === 'reject') {
|
||||
valid = await rejectFormRef.value?.validate().catch(() => false) !== false
|
||||
} else {
|
||||
valid = await returnFormRef.value?.validate().catch(() => false) !== false
|
||||
}
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const refundId = props.refund.id
|
||||
if (props.action === 'approve') {
|
||||
await RefundService.approveRefund(refundId, {
|
||||
approved_refund_amount: yuanToFen(approveForm.approved_refund_amount),
|
||||
remark: approveForm.remark.trim() || undefined
|
||||
})
|
||||
ElMessage.success('审批通过成功')
|
||||
} else if (props.action === 'reject') {
|
||||
await RefundService.rejectRefund(refundId, {
|
||||
reject_reason: rejectForm.reject_reason.trim()
|
||||
})
|
||||
ElMessage.success('审批拒绝成功')
|
||||
} else {
|
||||
await RefundService.returnRefund(refundId, {
|
||||
remark: returnForm.remark.trim() || undefined
|
||||
})
|
||||
ElMessage.success('退回成功')
|
||||
}
|
||||
emit('update:modelValue', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('操作失败,请稍后重试')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<!-- 分页 -->
|
||||
<div
|
||||
v-if="pagination && tableData.length > 0"
|
||||
v-if="pagination && (alwaysShowPagination || tableData.length > 0)"
|
||||
class="table-pagination"
|
||||
:class="paginationAlign"
|
||||
>
|
||||
@@ -113,7 +113,7 @@
|
||||
/** 是否显示加载状态 */
|
||||
loading?: boolean
|
||||
/** 行数据的 Key,用于标识每一行数据 */
|
||||
rowKey?: string
|
||||
rowKey?: string | ((row: any) => string)
|
||||
/** 行的 className 的回调方法 */
|
||||
rowClassName?: ((data: { row: any; rowIndex: number }) => string) | string
|
||||
/** 是否显示边框 */
|
||||
@@ -144,6 +144,8 @@
|
||||
pageSizes?: number[]
|
||||
/** 只有一页时是否隐藏分页器 */
|
||||
hideOnSinglePage?: boolean
|
||||
/** 数据为空时也始终显示分页器 */
|
||||
alwaysShowPagination?: boolean
|
||||
/** 分页器的对齐方式 */
|
||||
paginationAlign?: 'left' | 'center' | 'right'
|
||||
/** 分页器的大小 */
|
||||
@@ -185,6 +187,7 @@
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
hideOnSinglePage: false,
|
||||
alwaysShowPagination: false,
|
||||
pageSizes: () => [10, 20, 30, 50],
|
||||
paginationAlign: 'center',
|
||||
paginationSize: 'default',
|
||||
|
||||
@@ -11,10 +11,34 @@ export const AUGUST_PERMISSIONS = {
|
||||
applicationCreate: 'employee_collection:application_create',
|
||||
applicationDetail: 'employee_collection:application_detail',
|
||||
applicationUpdate: 'employee_collection:application_update',
|
||||
applicationViewVoucher: 'employee_collection:application_view_voucher',
|
||||
paymentMethodPage: 'employee_collection:payment_method_view',
|
||||
paymentMethodCreate: 'employee_collection:payment_method_create',
|
||||
paymentMethodEdit: 'employee_collection:payment_method_edit',
|
||||
paymentMethodDelete: 'employee_collection:payment_method_delete'
|
||||
},
|
||||
phoneAssetAssociation: {
|
||||
list: 'phone_asset_association:list',
|
||||
unbind: 'phone_asset_association:unbind',
|
||||
unbindBatch: 'phone_asset_association:unbind_batch',
|
||||
unbindImportPage: 'phone_asset_association:unbind_import_view',
|
||||
unbindImportCreate: 'phone_asset_association:unbind_import_create',
|
||||
unbindImportDetail: 'phone_asset_association:unbind_import_detail'
|
||||
},
|
||||
h5PopupConfiguration: {
|
||||
list: 'h5_popup_configuration:list',
|
||||
create: 'h5_popup_configuration:create',
|
||||
update: 'h5_popup_configuration:update',
|
||||
enable: 'h5_popup_configuration:enable',
|
||||
disable: 'h5_popup_configuration:disable'
|
||||
},
|
||||
packageTrafficAlert: {
|
||||
rulesView: 'package_traffic_alert:rules_view',
|
||||
ruleCreate: 'package_traffic_alert:rule_create',
|
||||
ruleUpdate: 'package_traffic_alert:rule_update',
|
||||
recordsView: 'package_traffic_alert:records_view',
|
||||
recordDetail: 'package_traffic_alert:record_detail',
|
||||
export: 'package_traffic_alert:export'
|
||||
}
|
||||
} as const
|
||||
|
||||
|
||||
48
src/config/constants/businessUserGroup.ts
Normal file
48
src/config/constants/businessUserGroup.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 业务用户组相关常量与权限编码
|
||||
*/
|
||||
|
||||
import type { BusinessUserGroupBusinessLine } from '@/types/api'
|
||||
|
||||
/** 业务线选项:standard 标品 / smart 智能产品 / other 其他 */
|
||||
export const BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS: Array<{
|
||||
label: string
|
||||
value: BusinessUserGroupBusinessLine
|
||||
type?: 'primary' | 'success' | 'warning' | 'info' | 'danger'
|
||||
}> = [
|
||||
{ label: '标品', value: 'standard', type: 'primary' },
|
||||
{ label: '智能产品', value: 'smart', type: 'success' },
|
||||
{ label: '其他', value: 'other', type: 'warning' }
|
||||
]
|
||||
|
||||
/** 业务线文案映射 */
|
||||
export const BUSINESS_USER_GROUP_BUSINESS_LINE_MAP = BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS.reduce(
|
||||
(map, item) => {
|
||||
map[item.value] = item
|
||||
return map
|
||||
},
|
||||
{} as Record<BusinessUserGroupBusinessLine, (typeof BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS)[number]>
|
||||
)
|
||||
|
||||
/** 获取业务线标签 */
|
||||
export function getBusinessUserGroupBusinessLineLabel(businessLine?: string | null): string {
|
||||
if (!businessLine) return '-'
|
||||
return BUSINESS_USER_GROUP_BUSINESS_LINE_MAP[businessLine as BusinessUserGroupBusinessLine]
|
||||
?.label ?? businessLine
|
||||
}
|
||||
|
||||
/** 业务用户组页面与按钮权限编码 */
|
||||
export const BUSINESS_USER_GROUP_PERMISSIONS = {
|
||||
page: 'business_user_group:page',
|
||||
create: 'business_user_group:create',
|
||||
update: 'business_user_group:update',
|
||||
delete: 'business_user_group:delete',
|
||||
members: 'business_user_group:members'
|
||||
}
|
||||
|
||||
/** 店铺业务负责人运营权限编码 */
|
||||
export const SHOP_BUSINESS_OWNER_PERMISSIONS = {
|
||||
batch: 'shop:business_owner_batch',
|
||||
importPage: 'shop:business_owner_import',
|
||||
importCreate: 'shop:business_owner_import'
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export const COMMISSION_STATUS_OPTIONS = [
|
||||
{ label: '解冻中', value: CommissionStatus.UNFREEZING, type: 'warning' as const },
|
||||
{ label: '已发放', value: CommissionStatus.RELEASED, type: 'success' as const },
|
||||
{ label: '已失效', value: CommissionStatus.INVALID, type: 'danger' as const },
|
||||
{ label: '回溯', value: CommissionStatus.CLAWBACK, type: 'danger' as const },
|
||||
{ label: '待人工修正', value: CommissionStatus.PENDING_CORRECTION, type: 'warning' as const }
|
||||
]
|
||||
|
||||
@@ -46,6 +47,7 @@ export const CommissionStatusMap = {
|
||||
2: { label: '解冻中', type: 'warning' as const, color: '#E6A23C' },
|
||||
3: { label: '已发放', type: 'success' as const, color: '#67C23A' },
|
||||
4: { label: '已失效', type: 'danger' as const, color: '#F56C6C' },
|
||||
5: { label: '回溯', type: 'danger' as const, color: '#F56C6C' },
|
||||
99: { label: '待人工修正', type: 'warning' as const, color: '#E6A23C' }
|
||||
}
|
||||
|
||||
@@ -113,3 +115,33 @@ export function getCommissionTypeType(type: string) {
|
||||
const config = CommissionTypeMap[type as keyof typeof CommissionTypeMap]
|
||||
return config?.type || 'info'
|
||||
}
|
||||
|
||||
// ========== 佣金记录(含回溯明细)映射 ==========
|
||||
|
||||
// 记录来源映射 (original:原佣金, clawback:回溯明细)
|
||||
export const COMMISSION_RECORD_SOURCE_MAP = {
|
||||
original: { label: '原佣金', type: 'primary' as const },
|
||||
clawback: { label: '回溯明细', type: 'danger' as const }
|
||||
}
|
||||
|
||||
// 佣金记录状态筛选选项(含回溯,不含待人工修正)
|
||||
export const COMMISSION_RECORD_STATUS_OPTIONS = [
|
||||
{ label: '已冻结', value: CommissionStatus.FROZEN },
|
||||
{ label: '解冻中', value: CommissionStatus.UNFREEZING },
|
||||
{ label: '已发放', value: CommissionStatus.RELEASED },
|
||||
{ label: '已失效', value: CommissionStatus.INVALID },
|
||||
{ label: '回溯', value: CommissionStatus.CLAWBACK }
|
||||
]
|
||||
|
||||
// 是否回溯明细行(source 与 status 双判断,兼容未返回 source 的响应)
|
||||
export function isCommissionClawbackRecord(record: {
|
||||
source?: string
|
||||
status?: number
|
||||
}): boolean {
|
||||
return record.source === 'clawback' || record.status === CommissionStatus.CLAWBACK
|
||||
}
|
||||
|
||||
// 列表行 key:原佣金与回溯明细 ID 空间独立,同一页可能出现相同 id
|
||||
export function getCommissionRecordRowKey(record: { id: number; source?: string }): string {
|
||||
return `${record.source || 'original'}:${record.id}`
|
||||
}
|
||||
|
||||
@@ -84,6 +84,24 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
||||
detail: 'export_task:exchange_detail',
|
||||
download: 'export_task:exchange_download'
|
||||
}
|
||||
},
|
||||
commission_record: {
|
||||
scene: 'commission_record',
|
||||
sceneName: '佣金记录',
|
||||
pageTitle: '导出佣金记录',
|
||||
permissions: {
|
||||
detail: 'export_task:commission_record_detail',
|
||||
download: 'export_task:commission_record_download'
|
||||
}
|
||||
},
|
||||
package_traffic_alert: {
|
||||
scene: 'package_traffic_alert',
|
||||
sceneName: '套餐真流量预警',
|
||||
pageTitle: '导出套餐真流量预警',
|
||||
permissions: {
|
||||
detail: 'export_task:package_traffic_alert_detail',
|
||||
download: 'export_task:package_traffic_alert_download'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,12 @@ export * from './enableStatus'
|
||||
|
||||
// 导出任务相关
|
||||
export * from './exportTask'
|
||||
export * from './businessUserGroup'
|
||||
|
||||
// 套餐真流量预警相关
|
||||
export * from './packageTrafficAlert'
|
||||
|
||||
// 批量订购相关
|
||||
export * from './bulkPurchase'
|
||||
export * from './julyIteration'
|
||||
export * from './augustIteration'
|
||||
|
||||
60
src/config/constants/packageTrafficAlert.ts
Normal file
60
src/config/constants/packageTrafficAlert.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 套餐真流量预警相关常量
|
||||
* 对应 docs/产品迭代8月份/套餐真流量预警_API_前端简版.md
|
||||
*/
|
||||
|
||||
import {
|
||||
PackageTrafficAlertNotificationStatus,
|
||||
type PackageTrafficAlertAssetType
|
||||
} from '@/types/api'
|
||||
|
||||
/** 通知投递结果选项(1:已通知, 2:待投递, 3:投递失败, 4:未通知(接收人已失效), 5:未通知(无有效业务员)) */
|
||||
export const TRAFFIC_ALERT_NOTIFICATION_STATUS_OPTIONS = [
|
||||
{
|
||||
label: '已通知',
|
||||
value: PackageTrafficAlertNotificationStatus.NOTIFIED,
|
||||
tagType: 'success' as const
|
||||
},
|
||||
{
|
||||
label: '待投递',
|
||||
value: PackageTrafficAlertNotificationStatus.PENDING_DELIVERY,
|
||||
tagType: 'info' as const
|
||||
},
|
||||
{
|
||||
label: '投递失败',
|
||||
value: PackageTrafficAlertNotificationStatus.DELIVERY_FAILED,
|
||||
tagType: 'danger' as const
|
||||
},
|
||||
{
|
||||
label: '未通知(接收人已失效)',
|
||||
value: PackageTrafficAlertNotificationStatus.NOT_NOTIFIED_RECEIVER_INVALID,
|
||||
tagType: 'warning' as const
|
||||
},
|
||||
{
|
||||
label: '未通知(无有效业务员)',
|
||||
value: PackageTrafficAlertNotificationStatus.NOT_NOTIFIED_NO_VALID_OWNER,
|
||||
tagType: 'warning' as const
|
||||
}
|
||||
]
|
||||
|
||||
export const getTrafficAlertNotificationStatusOption = (
|
||||
status?: PackageTrafficAlertNotificationStatus | null
|
||||
) => TRAFFIC_ALERT_NOTIFICATION_STATUS_OPTIONS.find((item) => item.value === status)
|
||||
|
||||
/** 资产类型选项(iot_card:物联网卡, device:设备) */
|
||||
export const TRAFFIC_ALERT_ASSET_TYPE_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PackageTrafficAlertAssetType
|
||||
}> = [
|
||||
{ label: '物联网卡', value: 'iot_card' },
|
||||
{ label: '设备', value: 'device' }
|
||||
]
|
||||
|
||||
export const getTrafficAlertAssetTypeName = (assetType?: PackageTrafficAlertAssetType | null) =>
|
||||
TRAFFIC_ALERT_ASSET_TYPE_OPTIONS.find((item) => item.value === assetType)?.label || '-'
|
||||
|
||||
/** 导出格式选项(xlsx:Excel, csv:CSV) */
|
||||
export const TRAFFIC_ALERT_EXPORT_FORMAT_OPTIONS = [
|
||||
{ label: 'XLSX', value: 'xlsx' as const },
|
||||
{ label: 'CSV', value: 'csv' as const }
|
||||
]
|
||||
17
src/config/constants/paymentMerchantPools.ts
Normal file
17
src/config/constants/paymentMerchantPools.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 支付商户与商户池管理权限编码。
|
||||
* 页面和按钮统一引用这里的常量,后端菜单权限可直接复用同名编码。
|
||||
* 编码命名参考代理充值页 agent_recharge:<action> 风格。
|
||||
*/
|
||||
export const PAYMENT_MERCHANT_POOL_PERMISSIONS = {
|
||||
merchantCreate: 'payment_merchant_pool:merchant_create',
|
||||
merchantEdit: 'payment_merchant_pool:merchant_edit',
|
||||
merchantToggle: 'payment_merchant_pool:merchant_toggle',
|
||||
merchantDelete: 'payment_merchant_pool:merchant_delete',
|
||||
merchantDetail: 'payment_merchant_pool:merchant_detail',
|
||||
poolCreate: 'payment_merchant_pool:pool_create',
|
||||
poolEdit: 'payment_merchant_pool:pool_edit',
|
||||
poolToggle: 'payment_merchant_pool:pool_toggle',
|
||||
poolDetail: 'payment_merchant_pool:pool_detail',
|
||||
wechatAuthEdit: 'payment_merchant_pool:wechat_auth_edit'
|
||||
} as const
|
||||
89
src/config/constants/refund.ts
Normal file
89
src/config/constants/refund.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 退款管理展示与规则常量
|
||||
*/
|
||||
|
||||
import {
|
||||
RefundStatus,
|
||||
RefundMethod,
|
||||
ChannelRefundStatus,
|
||||
RefundFailureReason
|
||||
} from '@/types/api/refund'
|
||||
|
||||
/** 退款状态名称映射 */
|
||||
export const RefundStatusNameMap: Record<RefundStatus, string> = {
|
||||
[RefundStatus.PENDING]: '待审批',
|
||||
[RefundStatus.APPROVED]: '已通过',
|
||||
[RefundStatus.REJECTED]: '已拒绝',
|
||||
[RefundStatus.RETURNED]: '已退回',
|
||||
[RefundStatus.REFUNDING]: '原路退款处理中',
|
||||
[RefundStatus.REFUND_FAILED]: '原路退款失败'
|
||||
}
|
||||
|
||||
/** 退款状态标签类型映射 */
|
||||
export const RefundStatusTypeMap: Record<
|
||||
RefundStatus,
|
||||
'warning' | 'success' | 'danger' | 'info'
|
||||
> = {
|
||||
[RefundStatus.PENDING]: 'warning',
|
||||
[RefundStatus.APPROVED]: 'success',
|
||||
[RefundStatus.REJECTED]: 'danger',
|
||||
[RefundStatus.RETURNED]: 'info',
|
||||
[RefundStatus.REFUNDING]: 'warning',
|
||||
[RefundStatus.REFUND_FAILED]: 'danger'
|
||||
}
|
||||
|
||||
/** 退款方式名称映射 */
|
||||
export const RefundMethodNameMap: Record<RefundMethod, string> = {
|
||||
[RefundMethod.ORIGINAL_ROUTE]: '原路退回',
|
||||
[RefundMethod.CUSTOMER_ACCOUNT]: '客户收款信息',
|
||||
[RefundMethod.ASSET_WALLET]: '资产钱包',
|
||||
[RefundMethod.AGENT_WALLET]: '代理钱包'
|
||||
}
|
||||
|
||||
/** 渠道退款状态名称映射 */
|
||||
export const ChannelRefundStatusNameMap: Record<ChannelRefundStatus, string> = {
|
||||
[ChannelRefundStatus.NOT_STARTED]: '未发起或不适用',
|
||||
[ChannelRefundStatus.PROCESSING]: '处理中',
|
||||
[ChannelRefundStatus.SUCCEEDED]: '已成功',
|
||||
[ChannelRefundStatus.FAILED]: '已失败'
|
||||
}
|
||||
|
||||
/** 失败分类名称映射 */
|
||||
export const RefundFailureReasonNameMap: Record<RefundFailureReason, string> = {
|
||||
[RefundFailureReason.CHANNEL_REJECTED]: '渠道明确拒绝',
|
||||
[RefundFailureReason.CREDENTIAL_INVALID]: '渠道凭证失效',
|
||||
[RefundFailureReason.INSUFFICIENT_BALANCE]: '渠道余额不足',
|
||||
[RefundFailureReason.TIMEOUT_UNKNOWN]: '超时或结果未知',
|
||||
[RefundFailureReason.APPROVAL_REJECTED]: '企微驳回或关闭',
|
||||
[RefundFailureReason.REVOKED_AFTER_APPROVED]: '企微通过后撤销',
|
||||
[RefundFailureReason.PAYMENT_FACT_INVALID]: '本地原支付事实不可用'
|
||||
}
|
||||
|
||||
/** 退款方式选项(表单用) */
|
||||
export const RefundMethodOptions = [
|
||||
{ label: '原路退回', value: RefundMethod.ORIGINAL_ROUTE },
|
||||
{ label: '客户收款信息', value: RefundMethod.CUSTOMER_ACCOUNT },
|
||||
{ label: '资产钱包', value: RefundMethod.ASSET_WALLET },
|
||||
{ label: '代理钱包', value: RefundMethod.AGENT_WALLET }
|
||||
]
|
||||
|
||||
/**
|
||||
* 是否可重提:3 已拒绝 / 4 已退回 / 6 原路退款失败(anomaly_flag=1 时禁止)
|
||||
*/
|
||||
export const canResubmitRefund = (item: {
|
||||
status?: RefundStatus
|
||||
anomaly_flag?: number
|
||||
}): boolean => {
|
||||
if (!item.status) return false
|
||||
if (item.status === RefundStatus.REJECTED || item.status === RefundStatus.RETURNED) return true
|
||||
if (item.status === RefundStatus.REFUND_FAILED) return item.anomaly_flag !== 1
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批操作是否可用:仅待审批且未关联企微审批实例(存量无实例)的申请
|
||||
*/
|
||||
export const canManualApproveRefund = (item: {
|
||||
status?: RefundStatus
|
||||
approval_instance_id?: number | null
|
||||
}): boolean => item.status === RefundStatus.PENDING && !item.approval_instance_id
|
||||
@@ -413,7 +413,10 @@
|
||||
"seriesAssignDetail": "Series Assignment Detail",
|
||||
"packageSeries": "Package Series",
|
||||
"packageSeriesDetail": "Package Series Detail",
|
||||
"packageCommission": "Package Commission Cards"
|
||||
"packageCommission": "Package Commission Cards",
|
||||
"trafficAlertRules": "Traffic Alert Rules",
|
||||
"trafficAlerts": "Traffic Alert Records",
|
||||
"trafficAlertDetail": "Traffic Alert Record Detail"
|
||||
},
|
||||
"accountManagement": {
|
||||
"title": "Account Management",
|
||||
@@ -455,7 +458,9 @@
|
||||
},
|
||||
"shopManagement": {
|
||||
"title": "Shop Management",
|
||||
"shopList": "Shop List"
|
||||
"shopList": "Shop List",
|
||||
"businessUserGroups": "Business User Groups",
|
||||
"businessOwnerImport": "Business Owner Import"
|
||||
},
|
||||
"assetManagement": {
|
||||
"title": "Asset Management",
|
||||
@@ -494,14 +499,18 @@
|
||||
"exportRefund": "Export Refunds",
|
||||
"exportAgentRecharge": "Export Agent Recharges",
|
||||
"exportExchange": "Export Exchanges",
|
||||
"exportCommissionRecord": "Export Commission Records",
|
||||
"exportPackageTrafficAlert": "Export Package Traffic Alerts",
|
||||
"exportTaskDetail": "Export Task Detail",
|
||||
"exchangeManagement": "Exchange Management",
|
||||
"exchangeDetail": "Exchange Order Detail"
|
||||
"exchangeDetail": "Exchange Order Detail",
|
||||
"phoneAssetAssociation": "Phone-Asset Associations",
|
||||
"phoneAssetUnbindImportTasks": "Unbind Import Tasks",
|
||||
"phoneAssetUnbindImportTaskDetail": "Unbind Import Task Detail"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings Management",
|
||||
"paymentSettings": "Payment Settings",
|
||||
"agentSelfRecharge": "Agent Self-Recharge Settings",
|
||||
"detailsOfPaymentConfiguration": "Payment Configuration Details",
|
||||
"paymentMerchant": "Payment Merchant",
|
||||
"developerApi": "Developer API",
|
||||
@@ -509,7 +518,10 @@
|
||||
"paymentMerchantPools": "Merchant Pool Management",
|
||||
"paymentMerchantPoolsTabMerchants": "Payment Merchants",
|
||||
"paymentMerchantPoolsTabPools": "Merchant Pools",
|
||||
"paymentMerchantPoolsTabWechatAuth": "WeChat Authorization"
|
||||
"paymentMerchantPoolsTabWechatAuth": "WeChat Authorization",
|
||||
"detailsOfPaymentMerchant": "Payment Merchant Details",
|
||||
"detailsOfPaymentMerchantPool": "Merchant Pool Details",
|
||||
"h5PopupConfiguration": "H5 Popup Configurations"
|
||||
},
|
||||
"batch": {
|
||||
"title": "Batch Operations",
|
||||
|
||||
@@ -380,7 +380,10 @@
|
||||
"seriesGrantsDetail": "代理系列授权详情",
|
||||
"seriesGrantPackages": "代理系列授权套餐列表",
|
||||
"packageSeries": "套餐系列",
|
||||
"packageSeriesDetail": "套餐系列详情"
|
||||
"packageSeriesDetail": "套餐系列详情",
|
||||
"trafficAlertRules": "真流量预警规则",
|
||||
"trafficAlerts": "真流量达量预警",
|
||||
"trafficAlertDetail": "真流量达量预警详情"
|
||||
},
|
||||
"accountManagement": {
|
||||
"title": "账号管理",
|
||||
@@ -391,7 +394,9 @@
|
||||
},
|
||||
"shopManagement": {
|
||||
"title": "店铺管理",
|
||||
"shopList": "店铺列表"
|
||||
"shopList": "店铺列表",
|
||||
"businessUserGroups": "业务用户组",
|
||||
"businessOwnerImport": "业务负责人导入"
|
||||
},
|
||||
"assetManagement": {
|
||||
"title": "资产管理",
|
||||
@@ -423,9 +428,14 @@
|
||||
"exportRefund": "导出退款",
|
||||
"exportAgentRecharge": "导出代理充值",
|
||||
"exportExchange": "导出换货",
|
||||
"exportCommissionRecord": "导出佣金记录",
|
||||
"exportPackageTrafficAlert": "导出套餐真流量预警",
|
||||
"exportTaskDetail": "导出任务详情",
|
||||
"exchangeManagement": "换货管理",
|
||||
"exchangeDetail": "换货单详情"
|
||||
"exchangeDetail": "换货单详情",
|
||||
"phoneAssetAssociation": "手机号资产关联",
|
||||
"phoneAssetUnbindImportTasks": "解绑导入任务",
|
||||
"phoneAssetUnbindImportTaskDetail": "解绑导入任务详情"
|
||||
},
|
||||
"orderManagement": {
|
||||
"title": "订单管理",
|
||||
@@ -453,14 +463,16 @@
|
||||
"settings": {
|
||||
"title": "设置管理",
|
||||
"paymentSettings": "支付设置",
|
||||
"agentSelfRecharge": "代理自充设置",
|
||||
"detailsOfPaymentConfiguration": "支付配置详情",
|
||||
"withdrawalSettings": "提现配置",
|
||||
"passwordSettings": "密码设置",
|
||||
"paymentMerchantPools": "商户池管理",
|
||||
"paymentMerchantPoolsTabMerchants": "支付商户",
|
||||
"paymentMerchantPoolsTabPools": "商户池",
|
||||
"paymentMerchantPoolsTabWechatAuth": "微信授权配置"
|
||||
"paymentMerchantPoolsTabWechatAuth": "微信授权配置",
|
||||
"detailsOfPaymentMerchant": "支付商户详情",
|
||||
"detailsOfPaymentMerchantPool": "商户池详情",
|
||||
"h5PopupConfiguration": "H5运营弹窗配置"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -380,6 +380,7 @@ function convertBackendMenuToRoute(
|
||||
path: menuUrl,
|
||||
name: matchedRoute.name,
|
||||
component: matchedRoute.component,
|
||||
redirect: matchedRoute.redirect,
|
||||
meta: {
|
||||
...matchedRoute.meta,
|
||||
title: menu.name,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useUserStore } from '@/store/modules/user'
|
||||
* 不需要登录的路由白名单
|
||||
*/
|
||||
export const LOGIN_WHITE_LIST = [
|
||||
'/agent-registration',
|
||||
'/auth/login',
|
||||
'/exception/403',
|
||||
'/exception/404',
|
||||
|
||||
@@ -4,6 +4,11 @@ import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants/julyIteration'
|
||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants/augustIteration'
|
||||
import { PAYMENT_MERCHANT_POOL_PERMISSIONS } from '@/config/constants/paymentMerchantPools'
|
||||
import {
|
||||
BUSINESS_USER_GROUP_PERMISSIONS,
|
||||
SHOP_BUSINESS_OWNER_PERMISSIONS
|
||||
} from '@/config/constants'
|
||||
|
||||
/**
|
||||
* 菜单列表、异步路由
|
||||
@@ -206,6 +211,37 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 真流量预警规则
|
||||
{
|
||||
path: 'traffic-alert-rules',
|
||||
name: 'TrafficAlertRules',
|
||||
component: RoutesAlias.TrafficAlertRules,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.trafficAlertRules',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 真流量达量预警记录
|
||||
{
|
||||
path: 'traffic-alerts',
|
||||
name: 'TrafficAlerts',
|
||||
component: RoutesAlias.TrafficAlerts,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.trafficAlerts',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 真流量达量预警记录详情
|
||||
{
|
||||
path: 'traffic-alerts/detail/:id',
|
||||
name: 'TrafficAlertDetail',
|
||||
component: RoutesAlias.TrafficAlertDetail,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.trafficAlertDetail',
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -240,6 +276,30 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 业务用户组
|
||||
{
|
||||
path: 'business-user-groups',
|
||||
name: 'BusinessUserGroups',
|
||||
component: RoutesAlias.BusinessUserGroups,
|
||||
meta: {
|
||||
title: 'menus.shopManagement.businessUserGroups',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN'],
|
||||
permissions: [BUSINESS_USER_GROUP_PERMISSIONS.page]
|
||||
}
|
||||
},
|
||||
// 业务负责人导入
|
||||
{
|
||||
path: 'business-owner-import',
|
||||
name: 'BusinessOwnerImport',
|
||||
component: RoutesAlias.BusinessOwnerImport,
|
||||
meta: {
|
||||
title: 'menus.shopManagement.businessOwnerImport',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN'],
|
||||
permissions: [SHOP_BUSINESS_OWNER_PERMISSIONS.importPage]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -359,6 +419,40 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 手机号资产关联
|
||||
{
|
||||
path: 'phone-asset-association',
|
||||
name: 'PhoneAssetAssociation',
|
||||
component: RoutesAlias.PhoneAssetAssociation,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.phoneAssetAssociation',
|
||||
permissions: [AUGUST_PERMISSIONS.phoneAssetAssociation.list],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 解绑导入任务列表
|
||||
{
|
||||
path: 'phone-asset-association/unbind-import-tasks',
|
||||
name: 'PhoneAssetUnbindImportTasks',
|
||||
component: RoutesAlias.PhoneAssetUnbindImportTasks,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.phoneAssetUnbindImportTasks',
|
||||
permissions: [AUGUST_PERMISSIONS.phoneAssetAssociation.unbindImportPage],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 解绑导入任务详情
|
||||
{
|
||||
path: 'phone-asset-association/unbind-import-tasks/detail',
|
||||
name: 'PhoneAssetUnbindImportTaskDetail',
|
||||
component: RoutesAlias.PhoneAssetUnbindImportTaskDetail,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.phoneAssetUnbindImportTaskDetail',
|
||||
permissions: [AUGUST_PERMISSIONS.phoneAssetAssociation.unbindImportDetail],
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 记录管理
|
||||
{
|
||||
path: 'record-management',
|
||||
@@ -590,6 +684,26 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-commission-record',
|
||||
name: 'ExportCommissionRecordTaskList',
|
||||
component: RoutesAlias.ExportCommissionRecordTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportCommissionRecord',
|
||||
exportTaskScene: 'commission_record',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-package-traffic-alert',
|
||||
name: 'ExportPackageTrafficAlertTaskList',
|
||||
component: RoutesAlias.ExportPackageTrafficAlertTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportPackageTrafficAlert',
|
||||
exportTaskScene: 'package_traffic_alert',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-task-detail',
|
||||
name: 'ExportTaskDetail',
|
||||
@@ -915,7 +1029,8 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
title: 'menus.settings.detailsOfPaymentMerchant',
|
||||
isHide: true,
|
||||
keepAlive: false,
|
||||
allowedUserTypes: [1, 2]
|
||||
allowedUserTypes: [1, 2],
|
||||
permissions: [PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantDetail]
|
||||
}
|
||||
},
|
||||
// 商户池详情
|
||||
@@ -927,7 +1042,8 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
title: 'menus.settings.detailsOfPaymentMerchantPool',
|
||||
isHide: true,
|
||||
keepAlive: false,
|
||||
allowedUserTypes: [1, 2]
|
||||
allowedUserTypes: [1, 2],
|
||||
permissions: [PAYMENT_MERCHANT_POOL_PERMISSIONS.poolDetail]
|
||||
}
|
||||
},
|
||||
// 支付设置详情
|
||||
@@ -963,17 +1079,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
// 代理自充设置(跳转系统配置并带上模块筛选)
|
||||
{
|
||||
path: 'agent-self-recharge',
|
||||
name: 'AgentSelfRechargeSettings',
|
||||
redirect: { path: RoutesAlias.SystemConfigs, query: { module: 'c2b.payment' } },
|
||||
meta: {
|
||||
title: 'menus.settings.agentSelfRecharge',
|
||||
keepAlive: false,
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wecom',
|
||||
name: 'WecomSettings',
|
||||
@@ -1016,6 +1121,18 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
// H5运营弹窗配置
|
||||
{
|
||||
path: 'h5-popup-configuration',
|
||||
name: 'H5PopupConfigurations',
|
||||
component: RoutesAlias.H5PopupConfigurations,
|
||||
meta: {
|
||||
title: 'menus.settings.h5PopupConfiguration',
|
||||
keepAlive: true,
|
||||
allowedUserTypes: [1, 2],
|
||||
permissions: [AUGUST_PERMISSIONS.h5PopupConfiguration.list]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -17,6 +17,12 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
||||
component: () => import('@views/auth/login/index.vue'),
|
||||
meta: { title: 'menus.login.title', isHideTab: true, setTheme: true }
|
||||
},
|
||||
{
|
||||
path: '/agent-registration',
|
||||
name: 'AgentRegistration',
|
||||
component: () => import('@views/agent-registration/index.vue'),
|
||||
meta: { title: '代理扫码注册', isHideTab: true, setTheme: true, noLogin: true }
|
||||
},
|
||||
{
|
||||
path: '/exception',
|
||||
component: Home,
|
||||
|
||||
@@ -38,10 +38,15 @@ export enum RoutesAlias {
|
||||
SeriesGrants = '/package-management/series-grants', // 代理系列授权
|
||||
SeriesGrantsDetail = '/package-management/series-grants/detail', // 代理系列授权详情
|
||||
SeriesGrantPackages = '/package-management/series-grants/packages', // 代理系列授权套餐列表
|
||||
TrafficAlertRules = '/package-management/traffic-alert-rules', // 真流量预警规则
|
||||
TrafficAlerts = '/package-management/traffic-alerts', // 真流量达量预警记录
|
||||
TrafficAlertDetail = '/package-management/traffic-alerts/detail', // 真流量达量预警记录详情
|
||||
|
||||
// 店铺管理
|
||||
Shop = '/shop-management/list', // 店铺列表
|
||||
ShopDetail = '/shop-management/detail', // 店铺详情
|
||||
BusinessUserGroups = '/shop-management/business-user-groups', // 业务用户组
|
||||
BusinessOwnerImport = '/shop-management/business-owner-import', // 业务负责人导入
|
||||
|
||||
// 通用页面(店铺账号-企业账号)
|
||||
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表和店铺账号列表共用
|
||||
@@ -79,6 +84,8 @@ export enum RoutesAlias {
|
||||
ExportRefundTaskList = '/asset-management/export-task-management/export-refund', // 导出退款
|
||||
ExportAgentRechargeTaskList = '/asset-management/export-task-management/export-agent-recharge', // 导出代理充值
|
||||
ExportExchangeTaskList = '/asset-management/export-task-management/export-exchange', // 导出换货
|
||||
ExportCommissionRecordTaskList = '/asset-management/export-task-management/export-commission-record', // 导出佣金记录
|
||||
ExportPackageTrafficAlertTaskList = '/asset-management/export-task-management/export-package-traffic-alert', // 导出套餐真流量预警
|
||||
ExportTaskDetail = '/asset-management/export-task-management/export-task-detail', // 导出任务详情
|
||||
|
||||
// 订单管理
|
||||
@@ -112,11 +119,11 @@ export enum RoutesAlias {
|
||||
PaymentMerchantPoolDetail = '/settings/payment-merchant-pools/pool-detail', // 商户池详情
|
||||
OperationPasswordSettings = '/settings/operation-password', // 操作密码设置
|
||||
SystemConfigs = '/settings/system-configs', // 系统配置
|
||||
AgentSelfRechargeSettings = '/settings/agent-self-recharge', // 代理自充设置
|
||||
WecomSettings = '/settings/wecom', // 企业微信配置兼容入口
|
||||
WecomApplications = '/settings/wecom/applications', // 企业微信应用
|
||||
WecomMembers = '/settings/wecom/members', // 企业微信成员
|
||||
WecomScenes = '/settings/wecom/scenes', // 企业微信审批场景
|
||||
H5PopupConfigurations = '/settings/h5-popup-configuration', // H5运营弹窗配置
|
||||
|
||||
// 轮询管理
|
||||
DataCleanup = '/polling-management/data-cleanup', // 数据清理
|
||||
@@ -137,7 +144,12 @@ export enum RoutesAlias {
|
||||
Drag = '/widgets/drag', // 拖拽
|
||||
Charts = '/template/charts', // 图表
|
||||
Map = '/template/map', // 地图
|
||||
Calendar = '/template/calendar' // 日历
|
||||
Calendar = '/template/calendar', // 日历
|
||||
|
||||
// 手机号资产关联
|
||||
PhoneAssetAssociation = '/asset-management/phone-asset-association', // 手机号资产关联列表
|
||||
PhoneAssetUnbindImportTasks = '/asset-management/phone-asset-association/unbind-import-tasks', // 解绑导入任务列表
|
||||
PhoneAssetUnbindImportTaskDetail = '/asset-management/phone-asset-association/unbind-import-tasks/detail' // 解绑导入任务详情
|
||||
}
|
||||
|
||||
// 主页路由 - 修改为资产信息页面
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AppRouteRecord } from '@/types/router'
|
||||
* @returns 处理后的路由配置
|
||||
*/
|
||||
export const menuDataToRouter = (route: AppRouteRecord, parentPath = ''): AppRouteRecord => {
|
||||
const { id, name, component, meta, children } = route
|
||||
const { id, name, component, redirect, meta, children } = route
|
||||
|
||||
const fullPath = buildRoutePath(route, parentPath)
|
||||
|
||||
@@ -16,6 +16,7 @@ export const menuDataToRouter = (route: AppRouteRecord, parentPath = ''): AppRou
|
||||
name,
|
||||
path: fullPath,
|
||||
component,
|
||||
redirect,
|
||||
meta,
|
||||
children: processChildren(children || [], fullPath)
|
||||
}
|
||||
|
||||
1
src/template/业务负责人导入模板.csv
Normal file
1
src/template/业务负责人导入模板.csv
Normal file
@@ -0,0 +1 @@
|
||||
店铺编码,操作类型,业务员登录账号,备注
|
||||
|
@@ -159,6 +159,7 @@ export interface AssetExchangeTrace {
|
||||
export interface AssetResolveResponse {
|
||||
asset_type: AssetType // 资产类型:card 或 device
|
||||
asset_id: number // 数据库 ID
|
||||
associated_phones?: string[] // 当前关联手机号列表(完整手机号,无关联时为空数组)
|
||||
identifier?: string // 原样回传本次查询所用的标识符
|
||||
virtual_no: string // 虚拟号
|
||||
status: number // 资产状态
|
||||
|
||||
74
src/types/api/businessUserGroup.ts
Normal file
74
src/types/api/businessUserGroup.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 业务用户组相关类型定义 - 匹配后端 AUG26-003 接口
|
||||
*/
|
||||
|
||||
import type { PaginationParams } from './common'
|
||||
|
||||
/** 业务线枚举:standard 标品 / smart 智能产品 / other 其他 */
|
||||
export type BusinessUserGroupBusinessLine = 'standard' | 'smart' | 'other'
|
||||
|
||||
/** 业务用户组响应实体 */
|
||||
export interface BusinessUserGroupItem {
|
||||
id: number // 业务用户组ID
|
||||
code: string // 业务用户组稳定编码,创建后不可修改
|
||||
name: string // 业务用户组名称
|
||||
business_line: string // 所属业务线,空字符串表示未设置
|
||||
business_line_name: string // 所属业务线中文名称,未设置时为空字符串
|
||||
enabled: boolean // 是否启用;停用后不得新增成员,也不得作为批量目标
|
||||
sort: number // 排序值
|
||||
remark: string // 备注
|
||||
created_at: string // 创建时间
|
||||
updated_at: string // 更新时间
|
||||
}
|
||||
|
||||
/** 业务用户组列表查询参数 */
|
||||
export interface BusinessUserGroupQueryParams extends PaginationParams {
|
||||
page?: number // 页码,默认 1
|
||||
page_size?: number // 每页数量,默认 20,最大 100
|
||||
enabled?: boolean // 按启用状态过滤;不传返回全部
|
||||
keyword?: string // 按稳定编码或名称模糊搜索,最多 100 字符
|
||||
business_line?: BusinessUserGroupBusinessLine | '' // 按所属业务线过滤
|
||||
}
|
||||
|
||||
/** 业务用户组分页响应 */
|
||||
export interface BusinessUserGroupPageResult {
|
||||
items: BusinessUserGroupItem[] | null
|
||||
page?: number
|
||||
size?: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
/** 创建业务用户组请求 */
|
||||
export interface CreateBusinessUserGroupParams {
|
||||
code: string // 稳定编码,1-64 字符,未删除组内唯一,创建后不可修改
|
||||
name: string // 名称,1-100 字符
|
||||
business_line?: BusinessUserGroupBusinessLine | '' // 所属业务线,留空表示不设置
|
||||
enabled?: boolean // 是否启用,默认启用
|
||||
sort?: number // 排序值,非负整数,默认 0
|
||||
remark?: string // 备注,最多 500 字符
|
||||
}
|
||||
|
||||
/** 更新业务用户组请求(部分更新,全部可选) */
|
||||
export interface UpdateBusinessUserGroupParams {
|
||||
name?: string // 名称,1-100 字符
|
||||
business_line?: BusinessUserGroupBusinessLine | '' | null // 三态:缺省不改 / "" 清空 / 枚举设置
|
||||
enabled?: boolean // 是否启用
|
||||
sort?: number // 排序值,非负整数
|
||||
remark?: string // 备注,最多 500 字符
|
||||
}
|
||||
|
||||
/** 删除业务用户组请求 */
|
||||
export interface DeleteBusinessUserGroupParams {
|
||||
confirm: boolean // 确认删除,必须为 true
|
||||
}
|
||||
|
||||
/** 批量设置/清空成员请求 */
|
||||
export interface SetBusinessUserGroupMembersParams {
|
||||
account_ids: number[] // 平台用户账号ID列表,至少一个;每个账号必须是启用平台用户
|
||||
}
|
||||
|
||||
/** 批量设置/清空成员结果 */
|
||||
export interface BusinessUserGroupMembersResult {
|
||||
group_id: number // 目标业务用户组ID;清空操作为 0
|
||||
account_ids: number[] | null // 本次成功维护归属的平台用户账号ID列表
|
||||
}
|
||||
@@ -384,6 +384,7 @@ export interface StandaloneIotCard {
|
||||
gateway_card_imei?: string // 网关侧返回的卡 IMEI(可空)
|
||||
id: number // 卡ID
|
||||
iccid: string // ICCID
|
||||
associated_phones?: string[] // 当前关联手机号列表(完整手机号,无关联时为空数组)
|
||||
imsi?: string // IMSI (可选)
|
||||
msisdn?: string // 卡接入号 (可选)
|
||||
virtual_no?: string // 卡虚拟号(可空)
|
||||
|
||||
@@ -4,12 +4,131 @@
|
||||
|
||||
import { PaginationParams } from '@/types'
|
||||
|
||||
// ==================== 提现资料资格相关 ====================
|
||||
|
||||
/**
|
||||
* 提交/替换提现资料资格请求参数
|
||||
*/
|
||||
export interface WithdrawalQualificationSubmitParams {
|
||||
subject_type: string // 主体类型(enterprise 等)
|
||||
subject_code: string // 主体证件号
|
||||
legal_person_id_card: string // 法人身份证号
|
||||
contract_file_key: string // 合同附件 Key
|
||||
id_card_front_file_key: string // 身份证正面 Key
|
||||
id_card_back_file_key: string // 身份证背面 Key
|
||||
business_license_file_key?: string // 营业执照 Key
|
||||
shop_front_file_key?: string // 门头照 Key
|
||||
invoice_file_key?: string // 发票附件 Key
|
||||
invoice_title?: string // 发票抬头
|
||||
invoice_subject_code?: string // 发票主体证件号
|
||||
}
|
||||
|
||||
/**
|
||||
* 提现资料资格项
|
||||
*/
|
||||
export interface WithdrawalQualificationItem {
|
||||
id: number // 资格ID
|
||||
shop_id: number // 店铺ID
|
||||
shop_name: string // 店铺名称
|
||||
subject_type: string // 主体类型
|
||||
subject_type_name: string // 主体类型名称
|
||||
subject_code_masked: string // 脱敏后主体证件号
|
||||
legal_person_id_card_masked: string // 脱敏后法人身份证号
|
||||
status: number // 状态(0 待审批等)
|
||||
status_name: string // 状态名称
|
||||
approval_instance_id?: number // 审批实例ID
|
||||
approval_status?: number // 审批状态
|
||||
approval_status_name?: string // 审批状态名称
|
||||
contract_file_key?: string // 合同附件 Key
|
||||
id_card_front_file_key?: string // 身份证正面 Key
|
||||
id_card_back_file_key?: string // 身份证背面 Key
|
||||
business_license_file_key?: string // 营业执照 Key
|
||||
shop_front_file_key?: string // 门头照 Key
|
||||
invoice_file_key?: string // 发票附件 Key
|
||||
invoice_title?: string // 发票抬头
|
||||
invalid_reason?: string // 作废原因
|
||||
invalidated_at?: string | null // 作废时间
|
||||
created_at: string // 创建时间
|
||||
updated_at: string // 更新时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 提现资料资格查询参数
|
||||
*/
|
||||
export interface WithdrawalQualificationQueryParams extends PaginationParams {
|
||||
status?: number // 状态筛选
|
||||
}
|
||||
|
||||
/**
|
||||
* 提现资料资格分页结果
|
||||
*/
|
||||
export interface WithdrawalQualificationPageResult {
|
||||
items: WithdrawalQualificationItem[] | null
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 作废提现资料资格请求参数
|
||||
*/
|
||||
export interface VoidWithdrawalQualificationParams {
|
||||
reason: string // 作废原因(必填)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重提被驳回的提现请求参数
|
||||
*/
|
||||
export interface ResubmitWithdrawalRequestParams {
|
||||
account_name: string // 收款账户名
|
||||
account_number: string // 收款账号
|
||||
amount: number // 提现金额(分)
|
||||
withdrawal_method: WithdrawalMethod // 提现方式
|
||||
invoice_keys?: string[] // 发票附件 Key 列表
|
||||
}
|
||||
|
||||
/**
|
||||
* 提现申请尝试记录项
|
||||
*/
|
||||
export interface WithdrawalAttemptItem {
|
||||
attempt: number // 第几次尝试
|
||||
status: number // 状态
|
||||
status_name: string // 状态名称
|
||||
reject_reason?: string // 驳回原因
|
||||
created_at: string // 尝试时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 提现申请详情
|
||||
*/
|
||||
export interface WithdrawalRequestDetail {
|
||||
id: number // 提现申请ID
|
||||
shop_id: number // 店铺ID
|
||||
shop_name: string // 店铺名称
|
||||
withdrawal_no: string // 提现单号
|
||||
withdrawal_method: WithdrawalMethod // 提现方式
|
||||
account_name: string // 收款账户名
|
||||
account_number: string // 收款账号
|
||||
amount: number // 提现金额(分)
|
||||
actual_amount: number // 实际到账(分)
|
||||
fee: number // 手续费(分)
|
||||
fee_rate: number // 手续费率(基点,100=1%)
|
||||
status: number // 状态
|
||||
status_name: string // 状态名称
|
||||
reject_reason?: string // 驳回原因
|
||||
anomaly_flag: number // 异常标记
|
||||
anomaly_name: string // 异常名称
|
||||
anomaly_reason?: string // 异常原因
|
||||
attempts?: WithdrawalAttemptItem[] // 尝试记录
|
||||
}
|
||||
|
||||
// 佣金状态
|
||||
export enum CommissionStatus {
|
||||
FROZEN = 1, // 已冻结
|
||||
UNFREEZING = 2, // 解冻中
|
||||
RELEASED = 3, // 已发放
|
||||
INVALID = 4, // 已失效
|
||||
CLAWBACK = 5, // 回溯
|
||||
PENDING_CORRECTION = 99 // 待人工修正
|
||||
}
|
||||
|
||||
@@ -146,7 +265,7 @@ export interface MyCommissionRecordItem {
|
||||
export interface CommissionRecordQueryParams extends PaginationParams {
|
||||
page_size?: number // 每页数量
|
||||
commission_source?: string // 佣金来源(cost_diff:成本差价,one_time:一次性佣金)
|
||||
status?: number // 状态(1已入账,2已失效)
|
||||
status?: number // 状态(1已冻结,2解冻中,3已发放,4已失效,5回溯,99待人工修正)
|
||||
start_time?: string // 开始时间
|
||||
end_time?: string // 结束时间
|
||||
iccid?: string // ICCID(模糊查询)
|
||||
@@ -189,7 +308,7 @@ export interface ShopCommissionRecordItem {
|
||||
amount: number // 佣金金额(分)
|
||||
balance_after: number // 入账后佣金余额(分)
|
||||
commission_source: string // 佣金来源(cost_diff:成本差价,one_time:一次性佣金)
|
||||
status: number // 状态(1已冻结, 2解冻中, 3已发放, 4已失效)
|
||||
status: number // 状态(1已冻结, 2解冻中, 3已发放, 4已失效, 5回溯, 99待人工修正)
|
||||
status_name: string // 状态名称
|
||||
order_id?: number // 订单ID
|
||||
order_no?: string // 订单号
|
||||
@@ -199,6 +318,45 @@ export interface ShopCommissionRecordItem {
|
||||
created_at: string // 佣金入账时间
|
||||
seller_shop_id?: number // 卖家店铺ID
|
||||
seller_shop_name?: string // 卖家店铺名称
|
||||
source?: ShopCommissionRecordSource // 记录来源(original:原佣金, clawback:回溯明细)
|
||||
released_at?: string // 佣金入账时间,回溯明细为空
|
||||
withdrawable?: boolean | null // 是否可提现,仅回溯明细返回且恒为 false
|
||||
original_commission_id?: number // 被回溯的原佣金记录ID,仅回溯明细返回
|
||||
refund_id?: number // 来源退款申请ID,仅回溯明细返回
|
||||
refund_no?: string // 来源退款单号,仅回溯明细返回
|
||||
clawback_records?: ShopCommissionClawbackItem[] | null // 该原佣金已生成的全部回溯明细,仅原佣金返回
|
||||
clawback_total_amount?: number | null // 该原佣金累计回溯金额(分,负值),仅原佣金返回
|
||||
}
|
||||
|
||||
/**
|
||||
* 佣金记录来源
|
||||
*/
|
||||
export type ShopCommissionRecordSource = 'original' | 'clawback'
|
||||
|
||||
/**
|
||||
* 佣金回溯明细项(原佣金详情中返回的回溯摘要,或回溯明细行)
|
||||
*/
|
||||
export interface ShopCommissionClawbackItem {
|
||||
id: number // 回溯明细ID
|
||||
amount: number // 回溯金额(分),恒为负数
|
||||
balance_after: number // 回溯后佣金余额(分),可为负数
|
||||
original_commission_id: number // 被回溯的原佣金记录ID
|
||||
refund_id?: number // 来源退款申请ID
|
||||
refund_no?: string // 来源退款单号
|
||||
status: number // 状态(5:回溯)
|
||||
status_name?: string // 状态名称
|
||||
withdrawable?: boolean | null // 是否可提现,回溯明细恒为不可提现
|
||||
created_at: string // 生成时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 佣金明细详情响应
|
||||
*/
|
||||
export interface ShopCommissionRecordDetail {
|
||||
source?: ShopCommissionRecordSource // 记录来源(original:原佣金, clawback:回溯明细)
|
||||
record: ShopCommissionRecordItem // 当前记录
|
||||
clawback_records?: ShopCommissionClawbackItem[] | null // 该原佣金已生成的全部回溯明细,仅原佣金详情返回
|
||||
original_commission?: ShopCommissionRecordItem | null // 回溯明细对应的原佣金,仅回溯详情返回
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,6 +39,7 @@ export type SwitchMode = '0' | '1' // 0=自动, 1=手动
|
||||
export interface Device {
|
||||
id: number // 设备ID
|
||||
virtual_no: string // 虚拟号(原 device_no)
|
||||
associated_phones?: string[] // 当前关联手机号列表(完整手机号,无关联时为空数组)
|
||||
device_name: string // 设备名称
|
||||
device_model: string // 设备型号
|
||||
device_type: string // 设备类型
|
||||
|
||||
@@ -9,6 +9,8 @@ export type ExportTaskScene =
|
||||
| 'agent_recharge'
|
||||
| 'refund'
|
||||
| 'exchange'
|
||||
| 'commission_record'
|
||||
| 'package_traffic_alert'
|
||||
|
||||
export type ExportTaskFormat = 'xlsx' | 'csv'
|
||||
|
||||
|
||||
113
src/types/api/h5PopupConfiguration.ts
Normal file
113
src/types/api/h5PopupConfiguration.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* H5 运营弹窗配置相关类型
|
||||
* 契约来源:docs/产品迭代8月份/通知.md
|
||||
*/
|
||||
|
||||
// 命中页面集合 (home:首页, asset_detail:资产详情, package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值)
|
||||
export type H5PopupPage = 'home' | 'asset_detail' | 'package_purchase' | 'asset_wallet_recharge'
|
||||
|
||||
// 投放频率 (once:每客户每配置版本仅一次, daily:每客户每配置版本每个上海自然日一次)
|
||||
export type H5PopupFrequency = 'once' | 'daily'
|
||||
|
||||
// 受控动作 (package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值);空字符串表示无受控动作
|
||||
export type H5PopupActionType = 'package_purchase' | 'asset_wallet_recharge' | ''
|
||||
|
||||
// 卡类型范围 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||
export type H5PopupCardType = 'CMCC' | 'CUCC' | 'CTCC' | 'CBN'
|
||||
|
||||
/**
|
||||
* H5 运营弹窗配置(列表项 / 详情 / 创建与更新响应)
|
||||
*/
|
||||
export interface H5PopupConfiguration {
|
||||
id: number // 配置ID
|
||||
title: string // 弹窗标题
|
||||
content: string // 弹窗正文
|
||||
pages: H5PopupPage[] | null // 命中页面集合;空数组等价于非法(页面必选)
|
||||
priority: number // 优先级,数值越大越优先
|
||||
frequency: H5PopupFrequency // 投放频率
|
||||
frequency_text: string // 投放频率名称(中文)
|
||||
enabled: boolean // 是否启用;停用后停止新投放,历史通知在展示期内仍可见
|
||||
enabled_text: string // 启停状态名称(中文)
|
||||
action_type: H5PopupActionType // 受控动作;空值表示无受控动作
|
||||
shop_ids: number[] | null // 店铺范围;空数组表示全量
|
||||
device_types: string[] | null // 设备类型范围;空数组表示全量
|
||||
card_types: H5PopupCardType[] | null // 卡类型范围;空数组表示全量
|
||||
starts_at: string // 生效开始时间(ISO 8601)
|
||||
ends_at: string // 生效结束时间(ISO 8601)
|
||||
version: number // 配置版本,每次更新递增;版本参与频率去重,旧版本通知保留原快照
|
||||
creator: number // 创建人账号ID
|
||||
updater: number // 最近更新人账号ID
|
||||
created_at: string // 创建时间(ISO 8601)
|
||||
updated_at: string // 最近更新时间(ISO 8601);启停同样刷新该时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表查询参数
|
||||
* GET /api/admin/h5-popup-configurations
|
||||
*/
|
||||
export interface H5PopupConfigurationQueryParams {
|
||||
page?: number // 页码,默认 1(1~10000)
|
||||
page_size?: number // 每页数量,默认 20,最大 100
|
||||
enabled?: boolean | null // 是否启用筛选
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表分页响应
|
||||
*/
|
||||
export interface H5PopupConfigurationListResponse {
|
||||
items: H5PopupConfiguration[] | null
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建请求
|
||||
* POST /api/admin/h5-popup-configurations
|
||||
* title/content/pages/frequency/starts_at/ends_at 必填;
|
||||
* shop_ids/device_types/card_types 空数组 = 全量。
|
||||
*/
|
||||
export interface CreateH5PopupConfigurationRequest {
|
||||
title: string // 弹窗标题,1~100 字符
|
||||
content: string // 弹窗正文,1~2000 字符,不接受 HTML、URL 或前端路由
|
||||
pages: H5PopupPage[] // 命中页面集合(非空)
|
||||
priority?: number // 优先级,数值越大越优先,0~1000000
|
||||
frequency: H5PopupFrequency // 投放频率
|
||||
starts_at: string // 生效开始时间(ISO 8601)
|
||||
ends_at: string // 生效结束时间(ISO 8601),不得早于开始时间
|
||||
enabled?: boolean // 是否启用,默认由后端决定
|
||||
action_type?: H5PopupActionType // 受控动作
|
||||
shop_ids?: number[] // 店铺范围,空数组 = 全量
|
||||
device_types?: string[] // 设备类型范围,空数组 = 全量
|
||||
card_types?: H5PopupCardType[] // 卡类型范围,空数组 = 全量
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新请求
|
||||
* PUT /api/admin/h5-popup-configurations/{id}
|
||||
* 除 id 外全部可选,不传保持原值;action_type 空字符串 = 清除受控动作;
|
||||
* shop_ids/device_types/card_types 空数组 = 改为全量;pages 空数组非法。
|
||||
*/
|
||||
export interface UpdateH5PopupConfigurationRequest {
|
||||
id: number // 运营弹窗配置ID
|
||||
title?: string // 不传保持原值,1~100 字符
|
||||
content?: string // 不传保持原值,1~2000 字符
|
||||
pages?: H5PopupPage[] // 不传保持原值;传空数组等价于非法(页面必选)
|
||||
priority?: number // 不传保持原值,0~1000000
|
||||
frequency?: H5PopupFrequency // 不传保持原值
|
||||
starts_at?: string // 不传保持原值(ISO 8601)
|
||||
ends_at?: string // 不传保持原值(ISO 8601),不得早于开始时间
|
||||
enabled?: boolean // 不传保持原值;启停会刷新最近更新时间并影响同优先级排序
|
||||
action_type?: H5PopupActionType | null // 不传保持原值;传空字符串 = 清除受控动作
|
||||
shop_ids?: number[] | null // 不传保持原值;传空数组 = 改为全量
|
||||
device_types?: string[] | null // 不传保持原值;传空数组 = 改为全量
|
||||
card_types?: H5PopupCardType[] | null // 不传保持原值;传空数组 = 改为全量
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/停用请求体
|
||||
* POST /api/admin/h5-popup-configurations/{id}/enable、/{id}/disable
|
||||
*/
|
||||
export interface H5PopupConfigurationIDParams {
|
||||
id: number // 运营弹窗配置ID
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* API 类型统一导出
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,7 @@ export * from './account'
|
||||
|
||||
// 店铺相关
|
||||
export * from './shop'
|
||||
export * from './businessUserGroup'
|
||||
|
||||
// 网卡相关
|
||||
export * from './card'
|
||||
@@ -137,3 +138,12 @@ export * from './wecom'
|
||||
|
||||
// 员工代收款相关
|
||||
export * from './employeeCollection'
|
||||
|
||||
// 手机号资产关联相关
|
||||
export * from './phoneAsset'
|
||||
|
||||
// H5 运营弹窗配置相关
|
||||
export * from './h5PopupConfiguration'
|
||||
|
||||
// 套餐真流量预警相关
|
||||
export * from './packageTrafficAlert'
|
||||
|
||||
137
src/types/api/packageTrafficAlert.ts
Normal file
137
src/types/api/packageTrafficAlert.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 套餐真流量预警相关类型
|
||||
* 对应 docs/产品迭代8月份/套餐真流量预警_API_前端简版.md
|
||||
* 6 个接口仅超级管理员/平台账号可访问,其他账号后端返回 403
|
||||
*/
|
||||
|
||||
import type { BaseResponse, PaginationParams, PaginationResponse } from './common'
|
||||
|
||||
// ========== 预警规则 ==========
|
||||
|
||||
/** 预警规则列表查询参数 */
|
||||
export interface PackageTrafficAlertRuleQueryParams extends PaginationParams {
|
||||
package_id?: number // 按套餐商品ID过滤
|
||||
enabled?: boolean // 按启用状态过滤;不传时查询全部
|
||||
}
|
||||
|
||||
/** 预警规则项 */
|
||||
export interface PackageTrafficAlertRuleItem {
|
||||
id: number // 预警规则ID
|
||||
package_id: number // 套餐商品ID
|
||||
package_name: string // 套餐名称
|
||||
real_data_mb: number // 套餐商品当前真流量额度(MB),仅用于配置校验展示,不作为预警分母
|
||||
threshold_percent: number // 真流量预警阈值百分比(1~100,允许两位小数)
|
||||
enabled: boolean // 是否启用
|
||||
enabled_name: string // 启用状态名称(中文)
|
||||
remark: string // 备注
|
||||
updated_at: string // 最近更新时间
|
||||
}
|
||||
|
||||
/** 创建预警规则参数 */
|
||||
export interface CreatePackageTrafficAlertRuleParams {
|
||||
package_id: number // 套餐商品ID;必须存在且真流量额度大于零
|
||||
threshold_percent: number // 真流量预警阈值百分比,取值 1 至 100,允许两位小数
|
||||
enabled?: boolean // 是否启用(默认 true);停用后扫描不再创建新预警
|
||||
remark?: string // 备注,最多 500 字符
|
||||
}
|
||||
|
||||
/** 修改预警规则参数(允许修改阈值、启停与备注;修改不影响既有预警快照) */
|
||||
export interface UpdatePackageTrafficAlertRuleParams {
|
||||
threshold_percent?: number // 新的真流量预警阈值百分比,取值 1 至 100,允许两位小数
|
||||
enabled?: boolean // 是否启用;停用后扫描不再创建新预警,既有预警保留
|
||||
remark?: string // 备注,最多 500 字符
|
||||
}
|
||||
|
||||
// ========== 预警记录 ==========
|
||||
|
||||
/** 资产类型(iot_card:物联网卡, device:设备) */
|
||||
export type PackageTrafficAlertAssetType = 'iot_card' | 'device'
|
||||
|
||||
/** 通知投递结果 */
|
||||
export enum PackageTrafficAlertNotificationStatus {
|
||||
/** 已通知 */
|
||||
NOTIFIED = 1,
|
||||
/** 待投递 */
|
||||
PENDING_DELIVERY = 2,
|
||||
/** 投递失败 */
|
||||
DELIVERY_FAILED = 3,
|
||||
/** 未通知(接收人已失效) */
|
||||
NOT_NOTIFIED_RECEIVER_INVALID = 4,
|
||||
/** 未通知(无有效业务员) */
|
||||
NOT_NOTIFIED_NO_VALID_OWNER = 5
|
||||
}
|
||||
|
||||
/** 预警记录列表查询参数(与导出筛选一致) */
|
||||
export interface PackageTrafficAlertRecordQueryParams extends PaginationParams {
|
||||
package_id?: number // 按阈值来源套餐商品ID过滤
|
||||
shop_id?: number // 按触发时所属店铺ID过滤
|
||||
business_owner_account_id?: number // 按触发时店铺业务员账号ID过滤
|
||||
asset_type?: PackageTrafficAlertAssetType // 资产类型 (iot_card:物联网卡, device:设备)
|
||||
asset_identifier?: string // 资产或卡标识关键词,匹配资产标识、卡标识与对应标识符快照
|
||||
threshold_percent?: number // 按触发阈值快照精确过滤,允许两位小数
|
||||
start_time?: string // 触发时间起始(RFC3339,含该时刻)
|
||||
end_time?: string // 触发时间截止(RFC3339,含该时刻)
|
||||
notification_status?: PackageTrafficAlertNotificationStatus // 通知投递结果过滤 (1-5)
|
||||
}
|
||||
|
||||
/**
|
||||
* 预警记录项
|
||||
* 除 business_user_group_names(当前值)外,其余字段均为触发时快照
|
||||
*/
|
||||
export interface PackageTrafficAlertRecordItem {
|
||||
id: number // 预警记录ID
|
||||
package_id?: number // 阈值来源套餐商品ID
|
||||
package_name?: string | null // 套餐名称快照
|
||||
shop_id?: number | null // 触发时所属店铺ID
|
||||
shop_name?: string | null // 触发时所属店铺名称
|
||||
business_owner_account_id?: number | null // 触发时店铺业务员账号ID
|
||||
business_owner_username?: string | null // 触发时店铺业务员账号名
|
||||
business_user_group_names?: string[] // 当前业务用户组名称(非触发时快照,可为空数组)
|
||||
asset_type?: PackageTrafficAlertAssetType | null // 资产类型快照
|
||||
asset_identifier?: string | null // 资产或卡标识快照
|
||||
threshold_percent?: number // 触发阈值快照
|
||||
triggered_at?: string // 触发时间
|
||||
notification_status?: PackageTrafficAlertNotificationStatus // 通知投递结果
|
||||
notification_status_name?: string // 通知投递结果名称
|
||||
}
|
||||
|
||||
/** 预警记录详情(在列表项基础上标识触发后归属是否变化) */
|
||||
export interface PackageTrafficAlertRecordDetail extends PackageTrafficAlertRecordItem {
|
||||
shop_changed_since_trigger?: boolean // 触发后店铺归属是否变化
|
||||
owner_changed_since_trigger?: boolean // 触发后业务员归属是否变化
|
||||
}
|
||||
|
||||
// ========== 导出 ==========
|
||||
|
||||
/** 创建预警记录导出任务参数(复用既有异步导出任务) */
|
||||
export interface ExportPackageTrafficAlertParams {
|
||||
format: 'xlsx' | 'csv' // 导出格式 (xlsx:Excel, csv:CSV)
|
||||
package_id?: number
|
||||
shop_id?: number
|
||||
business_owner_account_id?: number
|
||||
asset_type?: PackageTrafficAlertAssetType
|
||||
asset_identifier?: string // 最多 100 字符
|
||||
threshold_percent?: number
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
notification_status?: PackageTrafficAlertNotificationStatus
|
||||
}
|
||||
|
||||
/** 创建预警记录导出任务响应 */
|
||||
export interface CreatePackageTrafficAlertExportResponse {
|
||||
task_id: number // 导出任务ID
|
||||
task_no: string // 任务编号
|
||||
status: number // 任务状态 (1:待处理, 2:处理中, 3:已完成, 4:已失败, 5:已取消)
|
||||
status_name: string // 任务状态名称(中文)
|
||||
message: string // 提示信息
|
||||
}
|
||||
|
||||
// ========== 响应类型 ==========
|
||||
|
||||
export type PackageTrafficAlertRuleListResponse = PaginationResponse<PackageTrafficAlertRuleItem>
|
||||
export type PackageTrafficAlertRuleResponse = BaseResponse<PackageTrafficAlertRuleItem>
|
||||
export type PackageTrafficAlertRecordListResponse =
|
||||
PaginationResponse<PackageTrafficAlertRecordItem>
|
||||
export type PackageTrafficAlertRecordDetailResponse = BaseResponse<PackageTrafficAlertRecordDetail>
|
||||
export type CreatePackageTrafficAlertExportApiResponse =
|
||||
BaseResponse<CreatePackageTrafficAlertExportResponse>
|
||||
@@ -241,7 +241,7 @@ export const PAYMENT_CREDENTIAL_FIELD_SPECS: Record<
|
||||
},
|
||||
wechat_v2: {
|
||||
required: ['wx_mch_id', 'wx_api_v2_key', 'wx_notify_url'],
|
||||
optional: []
|
||||
optional: ['wx_client_cert_content', 'wx_client_key_content']
|
||||
},
|
||||
fuiou: {
|
||||
required: [
|
||||
|
||||
@@ -36,6 +36,8 @@ interface WechatPayConfigFields {
|
||||
wx_serial_no: string
|
||||
wx_cert_content: string
|
||||
wx_key_content: string
|
||||
wx_client_cert_content: string
|
||||
wx_client_key_content: string
|
||||
}
|
||||
|
||||
interface AlipayConfigFields {
|
||||
|
||||
192
src/types/api/phoneAsset.ts
Normal file
192
src/types/api/phoneAsset.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 手机号资产关联相关类型
|
||||
* 契约来源:docs/产品迭代8月份/手机号资产关联.md
|
||||
*/
|
||||
|
||||
// 资产类型
|
||||
export type PhoneAssetType = 'iot_card' | 'device'
|
||||
|
||||
// 关联状态 (0:已失效, 1:有效)
|
||||
export type PhoneAssetAssociationStatus = 0 | 1
|
||||
|
||||
// 失效方式
|
||||
export type PhoneAssetInvalidationMethod = 'backend_single' | 'backend_batch' | 'csv_import'
|
||||
|
||||
// 建立来源(固定为 H5 短信验证)
|
||||
export type PhoneAssetSource = 'h5_sms_verification'
|
||||
|
||||
/**
|
||||
* 手机号资产关联列表项
|
||||
* GET /api/admin/phone-asset-associations
|
||||
*/
|
||||
export interface PhoneAssetAssociation {
|
||||
id: number // 关联关系ID
|
||||
asset_id: number // 资产ID
|
||||
asset_identifier: string // 资产当前标识(卡为 ICCID,设备为虚拟号)
|
||||
asset_type: PhoneAssetType // 资产类型
|
||||
phone: string // 关联手机号(完整值)
|
||||
established_at: string // 建立时间(短信验证通过时间)
|
||||
source: PhoneAssetSource // 建立来源,固定为 h5_sms_verification
|
||||
status: PhoneAssetAssociationStatus // 关联状态 (0:已失效, 1:有效)
|
||||
status_name: string // 关联状态中文名称
|
||||
invalidated_at: string | null // 失效时间;有效关系为空
|
||||
invalidation_method: PhoneAssetInvalidationMethod | null // 失效方式;有效关系为空
|
||||
invalidation_method_name: string // 失效方式中文名称;有效关系为空
|
||||
invalidation_reason: string // 失效原因;有效关系为空
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联列表查询参数
|
||||
*/
|
||||
export interface PhoneAssetAssociationQueryParams {
|
||||
page?: number // 页码,默认 1
|
||||
page_size?: number // 每页数量,默认 20,最大 100
|
||||
asset_identifier?: string // 资产标识(ICCID、虚拟号、IMEI、SN 或接入号,精确匹配)
|
||||
phone?: string // 手机号(完整值精确匹配)
|
||||
status?: PhoneAssetAssociationStatus | null // 关联状态 (0:已失效, 1:有效)
|
||||
created_at_start?: string // 关联创建时间起(含)
|
||||
created_at_end?: string // 关联创建时间止(含)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联列表分页响应
|
||||
*/
|
||||
export interface PhoneAssetAssociationListResponse {
|
||||
items: PhoneAssetAssociation[] | null
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条解绑响应
|
||||
* DELETE /api/admin/phone-asset-associations/{id}
|
||||
*/
|
||||
export interface UnbindPhoneAssetAssociationResponse {
|
||||
asset_id: number // 资产ID
|
||||
asset_type: PhoneAssetType // 资产类型
|
||||
id: number // 关联关系ID
|
||||
invalidated_at: string // 失效时间
|
||||
unbound_count: number // 本次解除的有效关系数,成功时为 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解绑资产项
|
||||
*/
|
||||
export interface BatchUnbindAssetItem {
|
||||
asset_id: number // 资产ID
|
||||
asset_type: PhoneAssetType // 资产类型 (iot_card:物联网卡, device:设备)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解绑请求
|
||||
* POST /api/admin/phone-asset-associations/batch-unbind
|
||||
*/
|
||||
export interface BatchUnbindPhoneAssetAssociationRequest {
|
||||
assets: BatchUnbindAssetItem[] // 待解除的资产集合,按 (资产类型, 资产ID) 去重后执行
|
||||
confirmed: boolean // 二次确认,必须为 true
|
||||
reason: string // 解除原因(1~500 字符)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解绑逐项结果
|
||||
*/
|
||||
export interface BatchUnbindAssetResult {
|
||||
asset_id: number // 资产ID
|
||||
asset_type: PhoneAssetType // 资产类型
|
||||
success: boolean // 该项是否解除成功
|
||||
reason: string // 该项失败原因,成功时为空
|
||||
unbound_count: number // 该项解除的有效关系数
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解绑响应
|
||||
*/
|
||||
export interface BatchUnbindPhoneAssetAssociationResponse {
|
||||
success_count: number // 解除成功的资产项数
|
||||
fail_count: number // 解除失败的资产项数
|
||||
items: BatchUnbindAssetResult[] | null // 逐项结果,顺序与请求去重后的资产集合一致
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑导入任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)
|
||||
*/
|
||||
export type PhoneAssetUnbindImportTaskStatus = 1 | 2 | 3 | 4
|
||||
|
||||
/**
|
||||
* 创建解绑导入任务请求
|
||||
* POST /api/admin/phone-asset-associations/unbind-imports
|
||||
*/
|
||||
export interface CreatePhoneAssetUnbindImportRequest {
|
||||
file_key: string // CSV 对象存储 Key(phone-unbind-imports/ 开头且扩展名为 .csv)
|
||||
reason: string // 任务级解绑原因(1~500 字符)
|
||||
confirmed: boolean // 二次确认,必须为 true
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑导入任务(列表项 / 创建响应)
|
||||
*/
|
||||
export interface PhoneAssetUnbindImportTask {
|
||||
id: number // 导入任务ID
|
||||
task_no: string // 导入任务编号
|
||||
file_name: string // 上传的源 CSV 文件名
|
||||
status: PhoneAssetUnbindImportTaskStatus // 任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)
|
||||
status_name: string // 任务状态中文名称
|
||||
total_count: number // 数据行总数;任务级失败时为 0
|
||||
success_count: number // 成功行数
|
||||
fail_count: number // 失败行数
|
||||
unbind_reason: string // 任务级解绑原因
|
||||
error_message: string // 任务级失败原因;与行级失败原因分开记录
|
||||
creator_name: string // 任务创建人名称快照
|
||||
created_at: string // 创建时间
|
||||
started_at: string // 开始处理时间
|
||||
completed_at: string // 完成时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑导入任务列表分页响应
|
||||
* GET /api/admin/phone-asset-associations/unbind-imports
|
||||
*/
|
||||
export interface PhoneAssetUnbindImportTaskListResponse {
|
||||
items: PhoneAssetUnbindImportTask[] | null
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑导入任务列表查询参数
|
||||
*/
|
||||
export interface PhoneAssetUnbindImportTaskQueryParams {
|
||||
page?: number // 页码,默认 1
|
||||
page_size?: number // 每页数量,默认 20,最大 100
|
||||
status?: PhoneAssetUnbindImportTaskStatus | null // 任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐行状态 (3:成功, 4:失败)
|
||||
*/
|
||||
export type PhoneAssetUnbindImportItemStatus = 3 | 4
|
||||
|
||||
/**
|
||||
* 解绑导入任务逐行结果
|
||||
*/
|
||||
export interface PhoneAssetUnbindImportItem {
|
||||
line: number // 行号,自数据首行起计(表头不计入)
|
||||
asset_identifier: string // 导入文件中的资产标识原文
|
||||
asset_id: number // 资产ID;未定位到资产时为 0
|
||||
asset_type: PhoneAssetType | '' // 资产类型;未定位到资产时为空
|
||||
status: PhoneAssetUnbindImportItemStatus // 行状态 (3:成功, 4:失败)
|
||||
status_name: string // 行状态中文名称
|
||||
reason: string // 失败原因;成功行为空
|
||||
associated_phones: string[] | null // 解绑当时的完整关联手机号快照;失败行为空数组
|
||||
unbound_count: number // 该行解除的有效关系数
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑导入任务详情
|
||||
* GET /api/admin/phone-asset-associations/unbind-imports/{id}
|
||||
*/
|
||||
export interface PhoneAssetUnbindImportTaskDetail extends PhoneAssetUnbindImportTask {
|
||||
items: PhoneAssetUnbindImportItem[] | null // 逐行结果明细;任务级失败时为空数组
|
||||
}
|
||||
@@ -7,7 +7,36 @@ export enum RefundStatus {
|
||||
PENDING = 1, // 待审批
|
||||
APPROVED = 2, // 已通过
|
||||
REJECTED = 3, // 已拒绝
|
||||
RETURNED = 4 // 已退回
|
||||
RETURNED = 4, // 已退回
|
||||
REFUNDING = 5, // 原路退款处理中
|
||||
REFUND_FAILED = 6 // 原路退款失败
|
||||
}
|
||||
|
||||
// 退款方式
|
||||
export enum RefundMethod {
|
||||
ORIGINAL_ROUTE = 'original_route', // 原路退回
|
||||
CUSTOMER_ACCOUNT = 'customer_account', // 客户收款信息
|
||||
ASSET_WALLET = 'asset_wallet', // 资产钱包
|
||||
AGENT_WALLET = 'agent_wallet' // 代理钱包
|
||||
}
|
||||
|
||||
// 渠道退款状态
|
||||
export enum ChannelRefundStatus {
|
||||
NOT_STARTED = 0, // 未发起或不适用
|
||||
PROCESSING = 1, // 处理中
|
||||
SUCCEEDED = 2, // 已成功
|
||||
FAILED = 3 // 已失败
|
||||
}
|
||||
|
||||
// 失败原因分类
|
||||
export enum RefundFailureReason {
|
||||
CHANNEL_REJECTED = 'channel_rejected', // 渠道明确拒绝
|
||||
CREDENTIAL_INVALID = 'credential_invalid', // 渠道凭证失效
|
||||
INSUFFICIENT_BALANCE = 'insufficient_balance', // 渠道余额不足
|
||||
TIMEOUT_UNKNOWN = 'timeout_unknown', // 超时或结果未知
|
||||
APPROVAL_REJECTED = 'approval_rejected', // 企微驳回或关闭
|
||||
REVOKED_AFTER_APPROVED = 'revoked_after_approved', // 企微通过后撤销
|
||||
PAYMENT_FACT_INVALID = 'payment_fact_invalid' // 本地原支付事实不可用
|
||||
}
|
||||
|
||||
export interface RefundAttachment {
|
||||
@@ -42,6 +71,25 @@ export interface RefundApproval {
|
||||
business_process_result?: unknown
|
||||
}
|
||||
|
||||
// 退款申请审批尝试历史项
|
||||
export interface RefundAttemptItem {
|
||||
id: number // 尝试记录ID
|
||||
attempt_no: number // 第几次尝试
|
||||
method?: RefundMethod | null // 退款方式
|
||||
method_name?: string | null // 退款方式名称
|
||||
refund_amount?: number | null // 退款金额(分)
|
||||
frozen_actual_received_amount?: number | null // 冻结实收金额(分)
|
||||
refund_reason?: string | null // 退款原因
|
||||
customer_account_info?: Record<string, string> | null // 客户收款信息
|
||||
customer_voucher_key?: string[] | string | null // 客户凭证键
|
||||
channel_refund_request_no?: string | null // 渠道退款申请号
|
||||
submitted_by_account_id?: number | null // 提交人账号ID
|
||||
approval_instance_id?: number | null // 审批实例ID
|
||||
approval_status?: number | null // 审批状态
|
||||
approval_status_name?: string | null // 审批状态名称
|
||||
created_at: string // 创建时间
|
||||
}
|
||||
|
||||
// 退款申请
|
||||
export interface Refund {
|
||||
id: number
|
||||
@@ -55,9 +103,29 @@ export interface Refund {
|
||||
asset_type: string // 资产类型
|
||||
requested_refund_amount?: number | null // 申请退款金额(分)
|
||||
approved_refund_amount?: number | null // 实际退款金额(分)
|
||||
actual_received_amount?: number | null // 实收金额(分)
|
||||
actual_received_amount?: number | null // 实收金额(分,历史数据保留)
|
||||
status: RefundStatus
|
||||
status_name?: string | null
|
||||
method?: RefundMethod | null // 退款方式
|
||||
method_name?: string | null // 退款方式名称
|
||||
frozen_actual_received_amount?: number | null // 冻结实收金额(分)
|
||||
customer_account_info?: Record<string, string> | null // 客户收款信息
|
||||
channel_refund_status?: ChannelRefundStatus | null // 渠道退款状态
|
||||
channel_refund_status_name?: string | null // 渠道退款状态名称
|
||||
channel_refund_no?: string | null // 渠道退款单号
|
||||
channel_refund_request_no?: string | null // 渠道退款申请号
|
||||
channel_refund_amount?: number | null // 渠道退款金额(分)
|
||||
channel_refunded_at?: string | null // 渠道退款时间
|
||||
failure_reason?: RefundFailureReason | null // 失败分类
|
||||
failure_reason_name?: string | null // 失败分类名称
|
||||
failure_message?: string | null // 失败消息
|
||||
anomaly_flag?: number // 异常标记(0 正常、1 异常)
|
||||
anomaly_reason?: string | null // 异常原因
|
||||
latest_attempt_id?: number | null // 最新尝试记录ID
|
||||
latest_approval_instance_id?: number | null // 最新审批实例ID
|
||||
attempts?: RefundAttemptItem[] // 审批尝试历史
|
||||
refund_package_used_mb?: number // 当前退款套餐已用量(MB)
|
||||
refund_package_total_mb?: number // 当前退款套餐总量(MB)
|
||||
refund_reason: string
|
||||
refund_voucher_key?: string[] | string // 退款凭证附件列表;历史数据可能为单字符串或逗号字符串
|
||||
attachments?: RefundAttachment[]
|
||||
@@ -110,17 +178,36 @@ export interface RefundListResponse {
|
||||
// 创建退款申请请求
|
||||
export interface CreateRefundRequest {
|
||||
order_id: number
|
||||
method: RefundMethod // 退款方式(必填)
|
||||
requested_refund_amount: number // 申请退款金额(分)
|
||||
actual_received_amount: number // 实收金额(分)
|
||||
package_usage_id: number // 关联套餐使用记录 ID,无关联记录时传 0
|
||||
refund_reason: string
|
||||
refund_voucher_key: string[]
|
||||
customer_account_info?: Record<string, string> | null // 客户收款信息(customer_account 方式必填)
|
||||
refund_voucher_key?: string[] | null // 退款凭证(customer_account 方式必填至少 1 个)
|
||||
}
|
||||
|
||||
// 重新提交退款申请请求
|
||||
export interface ResubmitRefundRequest {
|
||||
method?: RefundMethod | null // 退款方式(不传沿用原值)
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount?: number
|
||||
attachments?: RefundAttachment[]
|
||||
refund_reason?: string
|
||||
customer_account_info?: Record<string, string> | null // 客户收款信息(不传沿用原值)
|
||||
refund_voucher_key?: string[] | null
|
||||
attachments?: RefundAttachment[]
|
||||
}
|
||||
|
||||
// 审批通过退款申请请求
|
||||
export interface ApproveRefundRequest {
|
||||
approved_refund_amount?: number | null // 审批退款金额(分)
|
||||
remark?: string
|
||||
}
|
||||
|
||||
// 审批拒绝退款申请请求
|
||||
export interface RejectRefundRequest {
|
||||
reject_reason: string
|
||||
}
|
||||
|
||||
// 退回退款申请请求
|
||||
export interface ReturnRefundRequest {
|
||||
remark?: string
|
||||
}
|
||||
@@ -26,7 +26,13 @@ export interface ShopResponse {
|
||||
business_owner_username: string // 平台业务员账号名
|
||||
business_owner_phone_summary: string // 平台业务员手机号摘要
|
||||
business_owner_available: boolean // 是否仍可用于通知接收
|
||||
distribution_code?: string // 分销码(用于生成注册二维码)
|
||||
client_login_disabled: boolean // 是否禁止 C 端新登录
|
||||
business_user_group_id: number | null // 业务用户组ID(实时推导,可空)
|
||||
business_user_group_code: string // 业务用户组编码
|
||||
business_user_group_name: string // 业务用户组名称
|
||||
business_user_group_enabled: boolean // 业务用户组是否启用;停用仍返回组且 enabled=false
|
||||
business_user_group_business_line: string // 业务用户组业务线
|
||||
}
|
||||
|
||||
// 店铺列表查询参数
|
||||
@@ -41,6 +47,9 @@ export interface ShopQueryParams extends PaginationParams {
|
||||
contact_phone?: string // 联系电话精确查询
|
||||
business_owner_account_id?: number | null // 平台业务员账号ID
|
||||
client_login_disabled?: boolean
|
||||
business_user_group_id?: number | null // 按业务用户组ID筛选
|
||||
business_line?: string | null // 按业务线筛选(standard/smart/other)
|
||||
ungrouped?: boolean // true=无负责人或负责人无分组
|
||||
page?: number // 页码
|
||||
page_size?: number // 每页数量
|
||||
}
|
||||
@@ -139,3 +148,69 @@ export interface UpdateShopCreditLimitResponse {
|
||||
version: number // 更新后的钱包版本
|
||||
wallet_id: number // 主钱包 ID
|
||||
}
|
||||
|
||||
// ========== 批量交接平台业务员 ==========
|
||||
|
||||
/** 批量交接平台业务员请求 */
|
||||
export interface BatchUpdateBusinessOwnerParams {
|
||||
shop_ids: number[] // 店铺ID列表,1-500,自动去重
|
||||
business_owner_account_id: number | null // 显式必传:null=清空负责人,ID=换绑
|
||||
}
|
||||
|
||||
/** 批量交接平台业务员响应 */
|
||||
export interface BatchUpdateBusinessOwnerResult {
|
||||
batch_key: string // 批次标识
|
||||
shop_count: number // 处理店铺数
|
||||
cleared: number // 清空负责人的店铺数
|
||||
}
|
||||
|
||||
// ========== 业务负责人 CSV 导入 ==========
|
||||
|
||||
/** 业务负责人导入任务行级结果 */
|
||||
export interface BusinessOwnerImportRow {
|
||||
line: number // 行号,从数据首行 1 开始
|
||||
shop_code: string // 店铺编码
|
||||
operation_type: string // 操作类型:换绑 / 清空
|
||||
status: number // 3=成功 4=失败
|
||||
status_name?: string // 状态中文名(成功/失败)
|
||||
reason: string // 失败原因
|
||||
}
|
||||
|
||||
/** 业务负责人导入任务详情 */
|
||||
export interface BusinessOwnerImportDetail {
|
||||
id: number // 导入任务ID
|
||||
task_no?: string // 任务编号
|
||||
file_name?: string // 文件名
|
||||
creator_name?: string // 创建人
|
||||
status: number // 1=待处理 2=处理中 3=已完成 4=失败
|
||||
status_name?: string // 状态中文名(待处理/处理中/已完成/失败)
|
||||
started_at?: string // 开始时间
|
||||
completed_at?: string // 完成时间
|
||||
total_count?: number
|
||||
success_count?: number
|
||||
fail_count?: number
|
||||
error_message?: string | null // 任务级失败原因
|
||||
items?: BusinessOwnerImportRow[] | null // 行级结果
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
/** 创建业务负责人导入任务请求 */
|
||||
export interface CreateBusinessOwnerImportParams {
|
||||
file_key: string // 由 /storage/upload-url(purpose=shop_import)获取
|
||||
}
|
||||
|
||||
/** 业务负责人导入任务列表查询参数 */
|
||||
export interface BusinessOwnerImportQueryParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
status?: number // 按状态筛选
|
||||
}
|
||||
|
||||
/** 业务负责人导入任务列表响应 */
|
||||
export interface BusinessOwnerImportPageResult {
|
||||
items: BusinessOwnerImportDetail[] | null
|
||||
page?: number
|
||||
size?: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ export type WecomBusinessType =
|
||||
| 'refund_approval'
|
||||
| 'offline_recharge_approval'
|
||||
| 'employee_collection_approval'
|
||||
| 'agent_distribution_approval'
|
||||
| 'withdrawal_qualification_approval'
|
||||
| 'commission_withdrawal_approval'
|
||||
|
||||
export interface WecomApplication {
|
||||
id: number
|
||||
|
||||
3
src/types/components.d.ts
vendored
3
src/types/components.d.ts
vendored
@@ -86,6 +86,7 @@ declare module 'vue' {
|
||||
CommentItem: typeof import('./../components/custom/comment-widget/widget/CommentItem.vue')['default']
|
||||
CommentWidget: typeof import('./../components/custom/comment-widget/index.vue')['default']
|
||||
CommissionDisplay: typeof import('./../components/business/CommissionDisplay.vue')['default']
|
||||
CommissionRecordDetailDialog: typeof import('./../components/business/CommissionRecordDetailDialog.vue')['default']
|
||||
ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default']
|
||||
CreateRefundDialog: typeof import('./../components/business/CreateRefundDialog.vue')['default']
|
||||
CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default']
|
||||
@@ -126,6 +127,7 @@ declare module 'vue' {
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElResult: typeof import('element-plus/es')['ElResult']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
@@ -152,6 +154,7 @@ declare module 'vue' {
|
||||
PackageSelector: typeof import('./../components/business/PackageSelector.vue')['default']
|
||||
PaymentVoucherDialog: typeof import('./../components/business/PaymentVoucherDialog.vue')['default']
|
||||
RealnamePolicyDialog: typeof import('./../components/device/RealnamePolicyDialog.vue')['default']
|
||||
RefundApprovalDialog: typeof import('./../components/business/RefundApprovalDialog.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SectionTitle: typeof import('./../components/core/layouts/art-settings-panel/widget/SectionTitle.vue')['default']
|
||||
|
||||
@@ -616,7 +616,8 @@
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
minWidth: 150,
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: any) => {
|
||||
return row.shop_name || '-'
|
||||
}
|
||||
|
||||
425
src/views/agent-registration/index.vue
Normal file
425
src/views/agent-registration/index.vue
Normal file
@@ -0,0 +1,425 @@
|
||||
<template>
|
||||
<div class="agent-registration-page">
|
||||
<div class="register-card">
|
||||
<div class="register-header">
|
||||
<h1 class="register-title">代理扫码注册</h1>
|
||||
<p class="register-subtitle">填写资料提交后,审核结果将通过通知告知</p>
|
||||
</div>
|
||||
|
||||
<div v-if="submitted" class="register-result">
|
||||
<ElResult icon="success" title="提交成功" sub-title="待审批,审核结果将通知你">
|
||||
<template #extra>
|
||||
<ElButton type="primary" @click="handleFillAgain">重新填写</ElButton>
|
||||
</template>
|
||||
</ElResult>
|
||||
</div>
|
||||
|
||||
<ElForm
|
||||
v-else
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
size="large"
|
||||
class="register-form"
|
||||
>
|
||||
<div class="form-grid">
|
||||
<ElFormItem label="分销码" prop="distribution_code">
|
||||
<ElInput
|
||||
v-model="form.distribution_code"
|
||||
:disabled="lockedDistributionCode"
|
||||
placeholder="请输入分销码"
|
||||
maxlength="64"
|
||||
clearable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号" prop="phone">
|
||||
<ElInput v-model="form.phone" placeholder="请输入手机号" maxlength="11" clearable />
|
||||
</ElFormItem>
|
||||
</div>
|
||||
|
||||
<ElFormItem label="短信验证码" prop="code">
|
||||
<div class="code-row">
|
||||
<ElInput v-model="form.code" placeholder="请输入短信验证码" maxlength="6" />
|
||||
<ElButton :disabled="!canSendCode" :loading="sending" @click="handleSendCode">
|
||||
{{ countdownText }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="密码" prop="password">
|
||||
<ElInput
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入密码(8-32 位)"
|
||||
maxlength="32"
|
||||
/>
|
||||
<div class="strength-bar" :class="strengthClass">
|
||||
<i v-for="n in 3" :key="n" :class="{ active: passwordStrength >= n }" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="form-grid">
|
||||
<ElFormItem label="店铺名称" prop="shop_name">
|
||||
<ElInput
|
||||
v-model="form.shop_name"
|
||||
:disabled="lockedShopName"
|
||||
placeholder="请输入店铺名称"
|
||||
maxlength="50"
|
||||
clearable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="店铺编号" prop="shop_code">
|
||||
<ElInput
|
||||
v-model="form.shop_code"
|
||||
:disabled="lockedShopCode"
|
||||
placeholder="请输入店铺编号"
|
||||
maxlength="50"
|
||||
clearable
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
|
||||
<ElFormItem label="用户名" prop="username">
|
||||
<ElInput v-model="form.username" placeholder="请输入登录用户名" maxlength="50" clearable />
|
||||
</ElFormItem>
|
||||
|
||||
<div class="form-grid">
|
||||
<ElFormItem label="联系人" prop="contact_name">
|
||||
<ElInput v-model="form.contact_name" placeholder="选填" maxlength="50" clearable />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="省市区">
|
||||
<ElCascader
|
||||
v-model="form.region"
|
||||
:options="regionOptions"
|
||||
placeholder="请选择省市区"
|
||||
clearable
|
||||
class="region-cascader"
|
||||
@change="handleRegionChange"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
|
||||
<ElFormItem label="详细地址" prop="address">
|
||||
<ElInput v-model="form.address" placeholder="选填,请输入详细地址" maxlength="200" clearable />
|
||||
</ElFormItem>
|
||||
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="large"
|
||||
class="submit-btn"
|
||||
:loading="submitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交注册
|
||||
</ElButton>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { CascaderOption, CascaderValue, FormInstance, FormRules } from 'element-plus'
|
||||
import {
|
||||
AgentDistributionService,
|
||||
type AgentDistributionRegistrationParams
|
||||
} from '@/api/modules/agentDistribution'
|
||||
import { regionData } from '@/utils/constants/regionData'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const regionOptions = regionData as CascaderOption[]
|
||||
|
||||
interface RegisterForm {
|
||||
distribution_code: string
|
||||
phone: string
|
||||
code: string
|
||||
password: string
|
||||
shop_name: string
|
||||
shop_code: string
|
||||
username: string
|
||||
contact_name: string
|
||||
province: string
|
||||
city: string
|
||||
district: string
|
||||
address: string
|
||||
region: string[]
|
||||
}
|
||||
|
||||
const form = reactive<RegisterForm>({
|
||||
distribution_code: (route.query.distribution_code as string) || '',
|
||||
phone: '',
|
||||
code: '',
|
||||
password: '',
|
||||
shop_name: (route.query.shop_name as string) || '',
|
||||
shop_code: (route.query.shop_code as string) || '',
|
||||
username: '',
|
||||
contact_name: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
address: '',
|
||||
region: []
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitted = ref(false)
|
||||
const sending = ref(false)
|
||||
const submitting = ref(false)
|
||||
const countdown = ref(0)
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const rules: FormRules = {
|
||||
distribution_code: [{ required: true, message: '请输入分销码', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||||
],
|
||||
code: [{ required: true, message: '请输入短信验证码', trigger: 'blur' }],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 32, message: '密码长度为 8-32 位', trigger: 'blur' }
|
||||
],
|
||||
shop_name: [{ required: true, message: '请输入店铺名称', trigger: 'blur' }],
|
||||
shop_code: [{ required: true, message: '请输入店铺编号', trigger: 'blur' }],
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const lockedDistributionCode = computed(() => !!route.query.distribution_code)
|
||||
const lockedShopName = computed(() => !!route.query.shop_name)
|
||||
const lockedShopCode = computed(() => !!route.query.shop_code)
|
||||
|
||||
const canSendCode = computed(() => countdown.value <= 0 && !sending.value)
|
||||
|
||||
const countdownText = computed(() =>
|
||||
countdown.value > 0 ? `${countdown.value}s 后重发` : '获取验证码'
|
||||
)
|
||||
|
||||
const startCountdown = () => {
|
||||
countdown.value = 60
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
countdown.value -= 1
|
||||
if (countdown.value <= 0 && timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleSendCode = async () => {
|
||||
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
|
||||
ElMessage.warning('请先输入正确的手机号')
|
||||
return
|
||||
}
|
||||
sending.value = true
|
||||
try {
|
||||
const res = await AgentDistributionService.sendCode({
|
||||
phone: form.phone,
|
||||
scene: 'bind_phone'
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('验证码已发送')
|
||||
startCountdown()
|
||||
}
|
||||
} catch {
|
||||
// 错误提示由请求层统一处理
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const passwordStrength = computed(() => {
|
||||
const pwd = form.password
|
||||
if (!pwd) return 0
|
||||
let score = 0
|
||||
if (pwd.length >= 8) score += 1
|
||||
if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) score += 1
|
||||
if (/\d/.test(pwd)) score += 1
|
||||
if (/[^A-Za-z0-9]/.test(pwd)) score += 1
|
||||
if (score >= 4) return 3
|
||||
if (score >= 2) return 2
|
||||
return 1
|
||||
})
|
||||
|
||||
const strengthClass = computed(() => {
|
||||
const map = ['', 'is-weak', 'is-medium', 'is-strong']
|
||||
return map[passwordStrength.value] || ''
|
||||
})
|
||||
|
||||
const handleRegionChange = (value: CascaderValue | null | undefined) => {
|
||||
const parts = Array.isArray(value) ? value.map((item) => String(item)) : []
|
||||
form.province = parts[0] || ''
|
||||
form.city = parts[1] || ''
|
||||
form.district = parts[2] || ''
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formRef.value) return
|
||||
formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const params: AgentDistributionRegistrationParams = {
|
||||
distribution_code: form.distribution_code.trim(),
|
||||
phone: form.phone.trim(),
|
||||
code: form.code.trim(),
|
||||
password: form.password,
|
||||
shop_name: form.shop_name.trim(),
|
||||
shop_code: form.shop_code.trim(),
|
||||
username: form.username.trim()
|
||||
}
|
||||
if (form.contact_name.trim()) params.contact_name = form.contact_name.trim()
|
||||
if (form.province) params.province = form.province
|
||||
if (form.city) params.city = form.city
|
||||
if (form.district) params.district = form.district
|
||||
if (form.address.trim()) params.address = form.address.trim()
|
||||
|
||||
const res = await AgentDistributionService.registerAgent(params)
|
||||
if (res.code === 0) {
|
||||
submitted.value = true
|
||||
} else {
|
||||
ElMessage.error('分销码不可用')
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('分销码不可用')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleFillAgain = () => {
|
||||
submitted.value = false
|
||||
formRef.value?.resetFields()
|
||||
form.distribution_code = (route.query.distribution_code as string) || form.distribution_code
|
||||
form.shop_name = (route.query.shop_name as string) || form.shop_name
|
||||
form.shop_code = (route.query.shop_code as string) || form.shop_code
|
||||
form.code = ''
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agent-registration-page {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 48px 16px 64px;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(160deg, #eef3fb 0%, #f7f9fd 40%, #eef3fb 100%);
|
||||
|
||||
.register-card {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
padding: 36px 36px 28px;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgb(93 135 255 / 12%);
|
||||
}
|
||||
|
||||
.register-header {
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
|
||||
.register-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.register-subtitle {
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 16px;
|
||||
}
|
||||
|
||||
.code-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
flex: 0 0 auto;
|
||||
width: 128px;
|
||||
}
|
||||
}
|
||||
|
||||
.strength-bar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
|
||||
i {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--el-border-color-lighter);
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background: var(--el-color-warning);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-strong i.active {
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.is-weak i.active {
|
||||
background: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.region-cascader {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.register-result {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 24px 12px 40px;
|
||||
|
||||
.register-card {
|
||||
padding: 24px 16px 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.code-row .el-button {
|
||||
width: 112px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -47,6 +47,12 @@
|
||||
border
|
||||
size="large"
|
||||
>
|
||||
<!-- 关联手机号(通用) -->
|
||||
<ElDescriptionsItem
|
||||
label="关联手机号"
|
||||
label-class-name="asset-label asset-label--associated-phones"
|
||||
>{{ (cardInfo?.associated_phones || []).join('、') || '-' }}</ElDescriptionsItem
|
||||
>
|
||||
<!-- 卡专属字段 -->
|
||||
<template v-if="cardInfo?.asset_type === 'card'">
|
||||
<ElDescriptionsItem
|
||||
@@ -698,6 +704,7 @@
|
||||
}
|
||||
|
||||
interface AssetInfo {
|
||||
associated_phones?: string[]
|
||||
asset_id: number
|
||||
asset_type: 'card' | 'device'
|
||||
identifier: string
|
||||
|
||||
@@ -135,6 +135,7 @@ export function useAssetInfo() {
|
||||
// 卡专属字段 (asset_type === 'card')
|
||||
iccid: data.iccid || '',
|
||||
msisdn: data.msisdn || '',
|
||||
associated_phones: data.associated_phones ?? [],
|
||||
imsi: data.imsi || '',
|
||||
carrier_id: data.carrier_id,
|
||||
carrier_type: data.carrier_type || '',
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface AssetInfo {
|
||||
iccid?: string
|
||||
imei?: string
|
||||
msisdn?: string
|
||||
associated_phones?: string[] // 当前关联手机号(完整值)
|
||||
carrier_type?: string
|
||||
carrier_name?: string
|
||||
real_name_status?: number
|
||||
|
||||
@@ -1617,6 +1617,7 @@
|
||||
{ label: '设备号', prop: 'virtual_no' },
|
||||
{ label: 'IMEI', prop: 'imei' },
|
||||
{ label: '设备名称', prop: 'device_name' },
|
||||
{ label: '关联手机号', prop: 'associated_phones' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '套餐系列', prop: 'series_name' },
|
||||
{ label: '预计套餐到期时间', prop: 'estimated_final_expires_at' },
|
||||
@@ -1893,10 +1894,19 @@
|
||||
showOverflowTooltip: true,
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'associated_phones',
|
||||
label: '关联手机号',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: Device) =>
|
||||
row.associated_phones?.length ? row.associated_phones.join('、') : '-'
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
minWidth: 120
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'series_name',
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import type { DetailField, DetailSection } from '@/components/common/DetailPage.vue'
|
||||
|
||||
defineOptions({ name: 'ExchangeDetail' })
|
||||
|
||||
@@ -62,6 +62,16 @@
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
const getMigrationStatusType = (migrationStatus?: string): TagProps['type'] => {
|
||||
const types: Record<string, TagProps['type']> = {
|
||||
not_migrated: 'info',
|
||||
pending: 'warning',
|
||||
migrated: 'success',
|
||||
failed: 'danger'
|
||||
}
|
||||
return types[migrationStatus || ''] || 'info'
|
||||
}
|
||||
|
||||
// 根据流程类型动态生成详情页配置
|
||||
const detailSections = computed<DetailSection[]>(() => {
|
||||
if (!exchangeDetail.value) return []
|
||||
@@ -70,9 +80,7 @@
|
||||
const sections: DetailSection[] = []
|
||||
|
||||
// 基本信息
|
||||
sections.push({
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
const basicFields: DetailField[] = [
|
||||
{ label: '换货单号', prop: 'exchange_no' },
|
||||
{
|
||||
label: '状态',
|
||||
@@ -112,9 +120,28 @@
|
||||
label: '继承归属店铺',
|
||||
formatter: (_, data) =>
|
||||
formatExchangeShopName(data.inherited_shop_name, data.inherited_shop_id)
|
||||
},
|
||||
{
|
||||
label: '迁移状态',
|
||||
render: (data) =>
|
||||
data.migration_status
|
||||
? h(
|
||||
ElTag,
|
||||
{ type: getMigrationStatusType(data.migration_status) },
|
||||
() => data.migration_status_name || data.migration_status
|
||||
)
|
||||
: h('span', '--')
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
if (exchangeDetail.value.migration_status === 'failed') {
|
||||
basicFields.push({
|
||||
label: '迁移失败原因',
|
||||
prop: 'migration_failure_reason',
|
||||
formatter: (value) => value || '--',
|
||||
fullWidth: true
|
||||
})
|
||||
}
|
||||
sections.push({ title: '基本信息', fields: basicFields })
|
||||
|
||||
// 收货信息(仅物流换货显示)
|
||||
if (flowType === 'shipping') {
|
||||
|
||||
@@ -987,6 +987,19 @@
|
||||
formatter: (row: ExchangeResponse) =>
|
||||
h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '--')
|
||||
},
|
||||
{
|
||||
prop: 'migration_status',
|
||||
label: '迁移状态',
|
||||
width: 130,
|
||||
formatter: (row: ExchangeResponse) =>
|
||||
row.migration_status
|
||||
? h(
|
||||
ElTag,
|
||||
{ type: getMigrationStatusType(row.migration_status) },
|
||||
() => row.migration_status_name || '--'
|
||||
)
|
||||
: '--'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
@@ -1006,6 +1019,18 @@
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
const getMigrationStatusType = (
|
||||
migrationStatus?: string
|
||||
): 'success' | 'info' | 'warning' | 'danger' | 'primary' => {
|
||||
const types: Record<string, 'success' | 'info' | 'warning' | 'danger' | 'primary'> = {
|
||||
not_migrated: 'info',
|
||||
pending: 'warning',
|
||||
migrated: 'success',
|
||||
failed: 'danger'
|
||||
}
|
||||
return types[migrationStatus || ''] || 'info'
|
||||
}
|
||||
|
||||
const getAssetTypeName = (assetType?: string | null) => {
|
||||
if (assetType === 'iot_card') return '物联网卡'
|
||||
if (assetType === 'device') return '设备'
|
||||
@@ -1638,7 +1663,9 @@
|
||||
|
||||
// 状态3(已发货待确认):确认完成
|
||||
if (status === 3) {
|
||||
if (hasAuth('exchange:complete')) {
|
||||
// migration_status=failed 的重试仅超管/平台账号可操作,代理/企业不渲染入口(后端以 403/1005 兜底)
|
||||
const canRetryFailed = row.migration_status !== 'failed' || userType === 1 || userType === 2
|
||||
if (hasAuth('exchange:complete') && canRetryFailed) {
|
||||
actions.push({
|
||||
label: '确认完成',
|
||||
handler: () => handleCompleteExchange(row),
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<ExportTaskList />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ExportTaskList from '../export-task-list/index.vue'
|
||||
|
||||
defineOptions({ name: 'ExportCommissionRecordTaskList' })
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<ExportTaskList />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ExportTaskList from '../export-task-list/index.vue'
|
||||
|
||||
defineOptions({ name: 'ExportPackageTrafficAlertTaskList' })
|
||||
</script>
|
||||
@@ -1946,6 +1946,7 @@
|
||||
{ label: 'ICCID', prop: 'iccid' },
|
||||
{ label: 'IMEI', prop: 'gateway_card_imei' },
|
||||
{ label: 'MSISDN', prop: 'msisdn' },
|
||||
{ label: '关联手机号', prop: 'associated_phones' },
|
||||
{ label: '卡虚拟号', prop: 'virtual_no' },
|
||||
{ label: '运营商', prop: 'carrier_name' },
|
||||
{ label: '卡业务类型', prop: 'card_category' },
|
||||
@@ -2092,6 +2093,14 @@
|
||||
label: 'MSISDN',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'associated_phones',
|
||||
label: '关联手机号',
|
||||
minWidth: 150,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: StandaloneIotCard) =>
|
||||
row.associated_phones?.length ? row.associated_phones.join('、') : '-'
|
||||
},
|
||||
{
|
||||
prop: 'virtual_no',
|
||||
label: '卡虚拟号',
|
||||
|
||||
750
src/views/asset-management/phone-asset-association/index.vue
Normal file
750
src/views/asset-management/phone-asset-association/index.vue
Normal file
@@ -0,0 +1,750 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="phone-asset-association-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
label-width="110"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #actions>
|
||||
<ElButton
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="openBatchUnbindDialog"
|
||||
v-if="canBatchUnbind"
|
||||
>
|
||||
批量解绑
|
||||
</ElButton>
|
||||
<ElButton type="primary" plain @click="openImportDialog" v-if="canImportCreate">
|
||||
CSV解绑导入
|
||||
</ElButton>
|
||||
<ElButton @click="goUnbindImportTasks" v-if="canImportPage">解绑导入任务</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="associationList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="100"
|
||||
@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>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 单条解绑 -->
|
||||
<ElDialog
|
||||
v-model="unbindDialogVisible"
|
||||
title="解除手机号资产关联"
|
||||
width="480px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="unbindFormRef" :model="unbindForm" :rules="unbindRules" label-width="90px">
|
||||
<ElFormItem label="资产标识">
|
||||
<span>{{ unbindForm.asset_identifier || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号">
|
||||
<span>{{ unbindForm.phone || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="解除原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="unbindForm.reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请填写解除原因(1~500 字符)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="二次确认" prop="confirmed">
|
||||
<ElCheckbox v-model="unbindForm.confirmed">我已确认解除该关联关系</ElCheckbox>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="unbindDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="danger" :loading="unbindLoading" @click="confirmUnbind">确认解绑</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 批量解绑 -->
|
||||
<ElDialog
|
||||
v-model="batchUnbindDialogVisible"
|
||||
title="批量解除手机号资产关联"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElAlert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
:title="`已选择 ${selectedRows.length} 项资产(按资产类型与ID去重后执行,上限 200)`"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
<ElForm ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="90px">
|
||||
<ElFormItem label="解除原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="batchForm.reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请填写解除原因(1~500 字符)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="二次确认" prop="confirmed">
|
||||
<ElCheckbox v-model="batchForm.confirmed">我已确认解除上述全部关联关系</ElCheckbox>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="batchUnbindDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="danger" :loading="batchUnbindLoading" @click="confirmBatchUnbind">
|
||||
确认批量解绑
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 批量解绑结果 -->
|
||||
<ElDialog v-model="batchResultVisible" title="批量解绑结果" width="560px">
|
||||
<div class="batch-result-summary">
|
||||
<span>成功 {{ batchResult?.success_count ?? 0 }} 项</span>
|
||||
<span class="text-danger">失败 {{ batchResult?.fail_count ?? 0 }} 项</span>
|
||||
</div>
|
||||
<ElTable :data="batchResult?.items || []" max-height="320" size="small" border>
|
||||
<ElTableColumn prop="asset_type" label="资产类型" width="110" />
|
||||
<ElTableColumn prop="asset_id" label="资产ID" width="90" />
|
||||
<ElTableColumn prop="success" label="结果" width="80" />
|
||||
<ElTableColumn prop="unbound_count" label="解除关系数" width="100" />
|
||||
<ElTableColumn prop="reason" label="失败原因" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
<template #footer>
|
||||
<ElButton type="primary" @click="batchResultVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- CSV 解绑导入 -->
|
||||
<ElDialog
|
||||
v-model="importDialogVisible"
|
||||
title="CSV 解绑导入"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleImportDialogClosed"
|
||||
>
|
||||
<div class="import-tips">
|
||||
<span>模板表头:</span>
|
||||
<ElButton link type="primary" @click="downloadTemplate">下载模板</ElButton>
|
||||
<div class="import-tips-detail">
|
||||
资产标识,备注(备注可选);支持 UTF-8(可带 BOM),非 UTF-8 按 GBK 解码;无行数上限。
|
||||
</div>
|
||||
</div>
|
||||
<ElForm ref="importFormRef" :model="importForm" :rules="importRules" label-width="90px">
|
||||
<ElFormItem label="CSV 文件" prop="file">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".csv"
|
||||
:on-change="handleFileChange"
|
||||
:on-exceed="handleFileExceed"
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text">拖拽文件到此处,或<em>点击选择 .csv 文件</em></div>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="解除原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="importForm.reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请填写任务级解除原因(1~500 字符)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="二次确认" prop="confirmed">
|
||||
<ElCheckbox v-model="importForm.confirmed">我已确认按模板批量解除关联关系</ElCheckbox>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="importDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="importLoading" @click="confirmImport"
|
||||
>创建导入任务</ElButton
|
||||
>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
ElMessage,
|
||||
ElTag,
|
||||
FormInstance,
|
||||
FormRules,
|
||||
UploadFile,
|
||||
UploadInstance
|
||||
} from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { PhoneAssetAssociationService, StorageService } from '@/api/modules'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type {
|
||||
BatchUnbindAssetItem,
|
||||
BatchUnbindPhoneAssetAssociationResponse,
|
||||
PhoneAssetAssociation,
|
||||
PhoneAssetAssociationStatus
|
||||
} from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'PhoneAssetAssociation' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const perms = AUGUST_PERMISSIONS.phoneAssetAssociation
|
||||
|
||||
const canBatchUnbind = computed(() => hasAuth(perms.unbindBatch))
|
||||
const canImportCreate = computed(() => hasAuth(perms.unbindImportCreate))
|
||||
const canImportPage = computed(() => hasAuth(perms.unbindImportPage))
|
||||
const canUnbind = computed(() => hasAuth(perms.unbind))
|
||||
|
||||
const loading = ref(false)
|
||||
const associationList = ref<PhoneAssetAssociation[]>([])
|
||||
const selectedRows = ref<PhoneAssetAssociation[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const initialSearchState = {
|
||||
asset_identifier: '',
|
||||
phone: '',
|
||||
status: undefined as PhoneAssetAssociationStatus | undefined,
|
||||
dateRange: [] as string[],
|
||||
created_at_start: '',
|
||||
created_at_end: ''
|
||||
}
|
||||
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '有效', value: 1 as const },
|
||||
{ label: '已失效', value: 0 as const }
|
||||
]
|
||||
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '资产标识',
|
||||
prop: 'asset_identifier',
|
||||
type: 'input',
|
||||
config: { clearable: true, placeholder: 'ICCID / 虚拟号 / IMEI / SN / 接入号(精确匹配)' }
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
prop: 'phone',
|
||||
type: 'input',
|
||||
config: { clearable: true, placeholder: '完整手机号(精确匹配)' }
|
||||
},
|
||||
{
|
||||
label: '关联状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: () => statusOptions
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'dateRange',
|
||||
type: 'datetimerange',
|
||||
config: {
|
||||
type: 'datetimerange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '资产类型', prop: 'asset_type' },
|
||||
{ label: '资产标识', prop: 'asset_identifier' },
|
||||
{ label: '手机号', prop: 'phone' },
|
||||
{ label: '建立时间', prop: 'established_at' },
|
||||
{ label: '来源', prop: 'source' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '失效时间', prop: 'invalidated_at' },
|
||||
{ label: '失效方式', prop: 'invalidation_method_name' },
|
||||
{ label: '失效原因', prop: 'invalidation_reason' }
|
||||
]
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'asset_type',
|
||||
label: '资产类型',
|
||||
width: 100,
|
||||
formatter: (row: PhoneAssetAssociation) =>
|
||||
row.asset_type === 'iot_card' ? '物联网卡' : '设备'
|
||||
},
|
||||
{
|
||||
prop: 'asset_identifier',
|
||||
label: '资产标识',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PhoneAssetAssociation) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: () => goToAsset(row)
|
||||
},
|
||||
row.asset_identifier
|
||||
)
|
||||
},
|
||||
{ prop: 'phone', label: '手机号', width: 140 },
|
||||
{
|
||||
prop: 'established_at',
|
||||
label: '建立时间',
|
||||
minWidth: 170,
|
||||
formatter: (row: PhoneAssetAssociation) => formatDateTime(row.established_at) || '-'
|
||||
},
|
||||
{
|
||||
prop: 'source',
|
||||
label: '来源',
|
||||
width: 120,
|
||||
formatter: () => 'H5 短信验证'
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 90,
|
||||
formatter: (row: PhoneAssetAssociation) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.status === 1 ? 'success' : 'info' },
|
||||
() => row.status_name || (row.status === 1 ? '有效' : '已失效')
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'invalidated_at',
|
||||
label: '失效时间',
|
||||
minWidth: 170,
|
||||
formatter: (row: PhoneAssetAssociation) => formatDateTime(row.invalidated_at) || '-'
|
||||
},
|
||||
{
|
||||
prop: 'invalidation_method_name',
|
||||
label: '失效方式',
|
||||
width: 130,
|
||||
formatter: (row: PhoneAssetAssociation) => row.invalidation_method_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'invalidation_reason',
|
||||
label: '失效原因',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PhoneAssetAssociation) => row.invalidation_reason || '-'
|
||||
}
|
||||
])
|
||||
|
||||
// ========== 列表数据 ==========
|
||||
|
||||
const syncDateRange = () => {
|
||||
if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
|
||||
searchForm.created_at_start = searchForm.dateRange[0] || ''
|
||||
searchForm.created_at_end = searchForm.dateRange[1] || ''
|
||||
} else {
|
||||
searchForm.created_at_start = ''
|
||||
searchForm.created_at_end = ''
|
||||
}
|
||||
}
|
||||
|
||||
const toRfc3339 = (date: string, endOfDay = false) => {
|
||||
if (!date) return undefined
|
||||
if (date.includes('T')) return date
|
||||
return `${date}T${endOfDay ? '23:59:59' : '00:00:00'}+08:00`
|
||||
}
|
||||
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
syncDateRange()
|
||||
const res = await PhoneAssetAssociationService.getAssociations({
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
asset_identifier: searchForm.asset_identifier || undefined,
|
||||
phone: searchForm.phone || undefined,
|
||||
status: searchForm.status,
|
||||
created_at_start: toRfc3339(searchForm.created_at_start),
|
||||
created_at_end: toRfc3339(searchForm.created_at_end, true)
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
associationList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取手机号资产关联列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取手机号资产关联列表失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '获取手机号资产关联列表失败')
|
||||
} 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 = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: PhoneAssetAssociation[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
const goToAsset = (row: PhoneAssetAssociation) => {
|
||||
router.push({
|
||||
path: RoutesAlias.AssetInformation,
|
||||
query:
|
||||
row.asset_type === 'iot_card'
|
||||
? { iccid: row.asset_identifier }
|
||||
: { virtual_no: row.asset_identifier }
|
||||
})
|
||||
}
|
||||
|
||||
const getActions = (row: PhoneAssetAssociation) => {
|
||||
const actions: any[] = []
|
||||
if (row.status === 1 && canUnbind.value) {
|
||||
actions.push({ label: '解绑', handler: () => openUnbindDialog(row), type: 'danger' })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
// ========== 单条解绑 ==========
|
||||
|
||||
const unbindDialogVisible = ref(false)
|
||||
const unbindLoading = ref(false)
|
||||
const unbindFormRef = ref<FormInstance>()
|
||||
const unbindForm = reactive<{
|
||||
id: number
|
||||
asset_identifier: string
|
||||
phone: string
|
||||
reason: string
|
||||
confirmed: boolean
|
||||
}>({
|
||||
id: 0,
|
||||
asset_identifier: '',
|
||||
phone: '',
|
||||
reason: '',
|
||||
confirmed: false
|
||||
})
|
||||
|
||||
const unbindRules: FormRules = {
|
||||
reason: [
|
||||
{ required: true, message: '请填写解除原因', trigger: 'blur' },
|
||||
{ min: 1, max: 500, message: '解除原因长度为 1~500 字符', trigger: 'blur' }
|
||||
],
|
||||
confirmed: [
|
||||
{
|
||||
validator: (_rule, value: boolean, callback) => {
|
||||
if (!value) callback(new Error('请先勾选二次确认'))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const openUnbindDialog = (row: PhoneAssetAssociation) => {
|
||||
unbindForm.id = row.id
|
||||
unbindForm.asset_identifier = row.asset_identifier
|
||||
unbindForm.phone = row.phone
|
||||
unbindForm.reason = ''
|
||||
unbindForm.confirmed = false
|
||||
unbindDialogVisible.value = true
|
||||
}
|
||||
|
||||
const confirmUnbind = async () => {
|
||||
if (!(await unbindFormRef.value?.validate().catch(() => false))) return
|
||||
unbindLoading.value = true
|
||||
try {
|
||||
const res = await PhoneAssetAssociationService.unbindAssociation(
|
||||
unbindForm.id,
|
||||
unbindForm.reason.trim(),
|
||||
unbindForm.confirmed
|
||||
)
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.msg || '解除关联失败')
|
||||
return
|
||||
}
|
||||
ElMessage.success('解除关联成功')
|
||||
unbindDialogVisible.value = false
|
||||
getTableData()
|
||||
} catch (error) {
|
||||
console.error('解除关联失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '解除关联失败')
|
||||
} finally {
|
||||
unbindLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 批量解绑 ==========
|
||||
|
||||
const batchUnbindDialogVisible = ref(false)
|
||||
const batchUnbindLoading = ref(false)
|
||||
const batchFormRef = ref<FormInstance>()
|
||||
const batchForm = reactive({ reason: '', confirmed: false, assets: [] as BatchUnbindAssetItem[] })
|
||||
const batchResult = ref<BatchUnbindPhoneAssetAssociationResponse | null>(null)
|
||||
const batchResultVisible = ref(false)
|
||||
|
||||
const batchRules: FormRules = {
|
||||
reason: [
|
||||
{ required: true, message: '请填写解除原因', trigger: 'blur' },
|
||||
{ min: 1, max: 500, message: '解除原因长度为 1~500 字符', trigger: 'blur' }
|
||||
],
|
||||
confirmed: [
|
||||
{
|
||||
validator: (_rule, value: boolean, callback) => {
|
||||
if (!value) callback(new Error('请先勾选二次确认'))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const buildUniqueAssets = (rows: PhoneAssetAssociation[]): BatchUnbindAssetItem[] => {
|
||||
const map = new Map<string, BatchUnbindAssetItem>()
|
||||
rows.forEach((row) => {
|
||||
if (row.status !== 1) return
|
||||
const key = `${row.asset_type}:${row.asset_id}`
|
||||
if (!map.has(key)) map.set(key, { asset_type: row.asset_type, asset_id: row.asset_id })
|
||||
})
|
||||
return Array.from(map.values()).slice(0, 200)
|
||||
}
|
||||
|
||||
const openBatchUnbindDialog = () => {
|
||||
batchForm.assets = buildUniqueAssets(selectedRows.value)
|
||||
if (batchForm.assets.length === 0) {
|
||||
ElMessage.warning('请选择有效关联的资产')
|
||||
return
|
||||
}
|
||||
batchForm.reason = ''
|
||||
batchForm.confirmed = false
|
||||
batchResult.value = null
|
||||
batchUnbindDialogVisible.value = true
|
||||
}
|
||||
|
||||
const confirmBatchUnbind = async () => {
|
||||
if (!(await batchFormRef.value?.validate().catch(() => false))) return
|
||||
batchUnbindLoading.value = true
|
||||
try {
|
||||
const res = await PhoneAssetAssociationService.batchUnbind({
|
||||
assets: batchForm.assets,
|
||||
reason: batchForm.reason.trim(),
|
||||
confirmed: batchForm.confirmed
|
||||
})
|
||||
if (res.code !== 0 || !res.data) {
|
||||
ElMessage.error(res.msg || '批量解绑失败')
|
||||
return
|
||||
}
|
||||
batchResult.value = res.data
|
||||
batchUnbindDialogVisible.value = false
|
||||
batchResultVisible.value = true
|
||||
getTableData()
|
||||
} catch (error) {
|
||||
console.error('批量解绑失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '批量解绑失败')
|
||||
} finally {
|
||||
batchUnbindLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CSV 解绑导入 ==========
|
||||
|
||||
const importDialogVisible = ref(false)
|
||||
const importLoading = ref(false)
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const importFormRef = ref<FormInstance>()
|
||||
const importForm = reactive({
|
||||
file: null as File | null,
|
||||
reason: '',
|
||||
confirmed: false
|
||||
})
|
||||
const importRules: FormRules = {
|
||||
file: [
|
||||
{
|
||||
validator: (_rule, _value, callback) => {
|
||||
if (!importForm.file) callback(new Error('请选择 CSV 文件'))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
reason: [
|
||||
{ required: true, message: '请填写任务级解除原因', trigger: 'blur' },
|
||||
{ min: 1, max: 500, message: '解除原因长度为 1~500 字符', trigger: 'blur' }
|
||||
],
|
||||
confirmed: [
|
||||
{
|
||||
validator: (_rule, value: boolean, callback) => {
|
||||
if (!value) callback(new Error('请先勾选二次确认'))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const openImportDialog = () => {
|
||||
importForm.file = null
|
||||
importForm.reason = ''
|
||||
importForm.confirmed = false
|
||||
importDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleImportDialogClosed = () => {
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
const rawFile = file.raw
|
||||
if (!rawFile) return
|
||||
if (!rawFile.name.toLowerCase().endsWith('.csv')) {
|
||||
ElMessage.error('解绑导入仅支持 .csv 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
importForm.file = null
|
||||
return
|
||||
}
|
||||
importForm.file = rawFile
|
||||
}
|
||||
|
||||
const handleFileExceed = () => {
|
||||
ElMessage.warning('仅支持上传一个 CSV 文件')
|
||||
}
|
||||
|
||||
const downloadTemplate = () => {
|
||||
const content = '\uFEFF资产标识,备注\n'
|
||||
const blob = new Blob([content], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '手机号资产解绑导入模板.csv'
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const goUnbindImportTasks = () => {
|
||||
router.push(RoutesAlias.PhoneAssetUnbindImportTasks)
|
||||
}
|
||||
|
||||
const confirmImport = async () => {
|
||||
if (!(await importFormRef.value?.validate().catch(() => false))) return
|
||||
const file = importForm.file
|
||||
if (!file) return
|
||||
importLoading.value = true
|
||||
try {
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: 'text/csv',
|
||||
purpose: 'phone_unbind_import'
|
||||
})
|
||||
if (uploadUrlRes.code !== 0 || !uploadUrlRes.data) {
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
return
|
||||
}
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
await StorageService.uploadFile(upload_url, file, 'text/csv')
|
||||
|
||||
const res = await PhoneAssetAssociationService.createUnbindImport({
|
||||
file_key,
|
||||
reason: importForm.reason.trim(),
|
||||
confirmed: importForm.confirmed
|
||||
})
|
||||
if (res.code !== 0 || !res.data) {
|
||||
ElMessage.error(res.msg || '创建解绑导入任务失败')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`解绑导入任务已创建!任务编号:${res.data.task_no}`)
|
||||
importDialogVisible.value = false
|
||||
goUnbindImportTasks()
|
||||
} catch (error) {
|
||||
console.error('创建解绑导入任务失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '创建解绑导入任务失败')
|
||||
} finally {
|
||||
importLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.phone-asset-association-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.batch-result-summary {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
|
||||
.text-danger {
|
||||
color: var(--el-color-danger);
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.import-tips {
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.import-tips-detail {
|
||||
margin-top: 4px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="unbind-import-task-detail-page" id="table-full-screen">
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<div class="detail-header">
|
||||
<ElButton @click="goBack">
|
||||
<template #icon>
|
||||
<ElIcon><ArrowLeft /></ElIcon>
|
||||
</template>
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">解绑导入任务详情</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !taskDetail" class="detail-loading">
|
||||
<ElIcon class="is-loading" :size="36">
|
||||
<Loading />
|
||||
</ElIcon>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
<template v-else-if="taskDetail">
|
||||
<DetailPage :sections="detailSections" :data="taskDetail" />
|
||||
|
||||
<div class="row-table-title">逐行结果</div>
|
||||
<ElTable :data="taskDetail.items || []" border stripe size="default" max-height="460">
|
||||
<ElTableColumn prop="line" label="行号" width="80" />
|
||||
<ElTableColumn
|
||||
prop="asset_identifier"
|
||||
label="资产标识(原文)"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn prop="asset_type" label="资产类型" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ row.asset_type ? (row.asset_type === 'iot_card' ? '物联网卡' : '设备') : '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="asset_id" label="资产ID" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.asset_id ? row.asset_id : '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" label="行状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.status === 3 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status_name || (row.status === 3 ? '成功' : '失败') }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="associated_phones"
|
||||
label="解绑时关联手机号"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ (row.associated_phones || []).join('、') || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="unbound_count" label="解除关系数" width="100" />
|
||||
<ElTableColumn prop="reason" label="失败原因" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.reason || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
<ElEmpty v-else description="暂无详情" />
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElButton, ElCard, ElEmpty, ElIcon, ElMessage, ElTag } from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { PhoneAssetAssociationService } from '@/api/modules'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants'
|
||||
import type {
|
||||
PhoneAssetUnbindImportTaskDetail,
|
||||
PhoneAssetUnbindImportTaskStatus
|
||||
} from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'PhoneAssetUnbindImportTaskDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const taskId = computed(() => Number(route.query.id || route.params.id || 0))
|
||||
const loading = ref(false)
|
||||
const taskDetail = ref<PhoneAssetUnbindImportTaskDetail | null>(null)
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const getStatusTagType = (status?: PhoneAssetUnbindImportTaskStatus) => {
|
||||
const map: Record<number, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||
1: 'info',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return status ? map[status] || 'info' : 'info'
|
||||
}
|
||||
|
||||
const fetchDetail = async () => {
|
||||
if (!taskId.value) {
|
||||
ElMessage.error('缺少任务ID')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await PhoneAssetAssociationService.getUnbindImportTaskDetail(taskId.value)
|
||||
if (res.code === 403) {
|
||||
ElMessage.error(res.msg || '无权限查看该任务详情')
|
||||
return
|
||||
}
|
||||
if (res.code !== 0 || !res.data) {
|
||||
ElMessage.error(res.msg || '获取解绑导入任务详情失败')
|
||||
return
|
||||
}
|
||||
taskDetail.value = res.data
|
||||
startPollingIfNeeded()
|
||||
} catch (error) {
|
||||
console.error('获取解绑导入任务详情失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '获取解绑导入任务详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startPollingIfNeeded = () => {
|
||||
const status = taskDetail.value?.status
|
||||
if (status === 1 || status === 2) {
|
||||
if (!pollTimer) {
|
||||
pollTimer = setInterval(fetchDetail, 5000)
|
||||
}
|
||||
} else {
|
||||
stopPolling()
|
||||
}
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const detailSections = computed((): DetailSection[] => {
|
||||
const data = taskDetail.value
|
||||
if (!data) return []
|
||||
return [
|
||||
{
|
||||
title: '任务基本信息',
|
||||
fields: [
|
||||
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
|
||||
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
|
||||
{
|
||||
label: '任务状态',
|
||||
render: (task: PhoneAssetUnbindImportTaskDetail) =>
|
||||
h(ElTag, { type: getStatusTagType(task.status) }, () => task.status_name || '-')
|
||||
},
|
||||
{
|
||||
label: '任务级解绑原因',
|
||||
prop: 'unbind_reason',
|
||||
fullWidth: true,
|
||||
formatter: (value: string) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'creator_name',
|
||||
formatter: (value: string) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'created_at',
|
||||
formatter: (value: string) => formatDateTime(value) || '-'
|
||||
},
|
||||
{
|
||||
label: '开始时间',
|
||||
prop: 'started_at',
|
||||
formatter: (value: string) => formatDateTime(value) || '-'
|
||||
},
|
||||
{
|
||||
label: '完成时间',
|
||||
prop: 'completed_at',
|
||||
formatter: (value: string) => formatDateTime(value) || '-'
|
||||
},
|
||||
{
|
||||
label: '任务级错误',
|
||||
prop: 'error_message',
|
||||
fullWidth: true,
|
||||
formatter: (value: string) => value || '-'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '处理统计',
|
||||
fields: [
|
||||
{
|
||||
label: '数据行总数',
|
||||
prop: 'total_count',
|
||||
formatter: (value: number) => String(value ?? 0)
|
||||
},
|
||||
{
|
||||
label: '成功行数',
|
||||
prop: 'success_count',
|
||||
formatter: (value: number) => String(value ?? 0)
|
||||
},
|
||||
{
|
||||
label: '失败行数',
|
||||
prop: 'fail_count',
|
||||
formatter: (value: number) => String(value ?? 0)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
router.push(RoutesAlias.PhoneAssetUnbindImportTasks)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!hasAuth(AUGUST_PERMISSIONS.phoneAssetAssociation.unbindImportDetail)) {
|
||||
ElMessage.warning('您没有查看解绑导入任务详情的权限')
|
||||
goBack()
|
||||
return
|
||||
}
|
||||
fetchDetail()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.unbind-import-task-detail-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 60px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.row-table-title {
|
||||
margin: 20px 0 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="unbind-import-tasks-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
label-width="110"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #actions>
|
||||
<ElButton @click="goAssociationList">返回关联列表</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="taskList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="90"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { PhoneAssetAssociationService } from '@/api/modules'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { PhoneAssetUnbindImportTask, PhoneAssetUnbindImportTaskStatus } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'PhoneAssetUnbindImportTasks' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const perms = AUGUST_PERMISSIONS.phoneAssetAssociation
|
||||
|
||||
const canDetail = computed(() => hasAuth(perms.unbindImportDetail))
|
||||
|
||||
const loading = ref(false)
|
||||
const taskList = ref<PhoneAssetUnbindImportTask[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const initialSearchState = {
|
||||
status: undefined as PhoneAssetUnbindImportTaskStatus | undefined
|
||||
}
|
||||
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '待处理', value: 1 },
|
||||
{ label: '处理中', value: 2 },
|
||||
{ label: '已完成', value: 3 },
|
||||
{ label: '失败', value: 4 }
|
||||
]
|
||||
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '任务状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: () => statusOptions
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '任务编号', prop: 'task_no' },
|
||||
{ label: '文件名', prop: 'file_name' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '总行数', prop: 'total_count' },
|
||||
{ label: '成功行数', prop: 'success_count' },
|
||||
{ label: '失败行数', prop: 'fail_count' },
|
||||
{ label: '任务级原因', prop: 'unbind_reason' },
|
||||
{ label: '创建人', prop: 'creator_name' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '开始时间', prop: 'started_at' },
|
||||
{ label: '完成时间', prop: 'completed_at' },
|
||||
{ label: '任务级错误', prop: 'error_message' }
|
||||
]
|
||||
|
||||
const getStatusTagType = (status?: PhoneAssetUnbindImportTaskStatus) => {
|
||||
const map: Record<number, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||
1: 'info',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return status ? map[status] || 'info' : 'info'
|
||||
}
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'task_no',
|
||||
label: '任务编号',
|
||||
minWidth: 170,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: 'task-no-link',
|
||||
onClick: () => goDetail(row)
|
||||
},
|
||||
row.task_no || '-'
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'file_name',
|
||||
label: '文件名',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) => row.file_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) =>
|
||||
h(ElTag, { type: getStatusTagType(row.status) }, () => row.status_name || '-')
|
||||
},
|
||||
{
|
||||
prop: 'total_count',
|
||||
label: '总行数',
|
||||
width: 90,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) => row.total_count ?? 0
|
||||
},
|
||||
{
|
||||
prop: 'success_count',
|
||||
label: '成功行数',
|
||||
width: 90,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) => row.success_count ?? 0
|
||||
},
|
||||
{
|
||||
prop: 'fail_count',
|
||||
label: '失败行数',
|
||||
width: 90,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) => row.fail_count ?? 0
|
||||
},
|
||||
{
|
||||
prop: 'unbind_reason',
|
||||
label: '任务级原因',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) => row.unbind_reason || '-'
|
||||
},
|
||||
{
|
||||
prop: 'creator_name',
|
||||
label: '创建人',
|
||||
width: 120,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) => row.creator_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
minWidth: 170,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) => formatDateTime(row.created_at) || '-'
|
||||
},
|
||||
{
|
||||
prop: 'started_at',
|
||||
label: '开始时间',
|
||||
minWidth: 170,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) => formatDateTime(row.started_at) || '-'
|
||||
},
|
||||
{
|
||||
prop: 'completed_at',
|
||||
label: '完成时间',
|
||||
minWidth: 170,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) => formatDateTime(row.completed_at) || '-'
|
||||
},
|
||||
{
|
||||
prop: 'error_message',
|
||||
label: '任务级错误',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PhoneAssetUnbindImportTask) => row.error_message || '-'
|
||||
}
|
||||
])
|
||||
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await PhoneAssetAssociationService.getUnbindImportTasks({
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
taskList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取解绑导入任务列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取解绑导入任务列表失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '获取解绑导入任务列表失败')
|
||||
} 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 = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const goDetail = (row: PhoneAssetUnbindImportTask) => {
|
||||
if (!canDetail.value) {
|
||||
ElMessage.warning('您没有查看解绑导入任务详情的权限')
|
||||
return
|
||||
}
|
||||
router.push({
|
||||
path: RoutesAlias.PhoneAssetUnbindImportTaskDetail,
|
||||
query: { id: String(row.id) }
|
||||
})
|
||||
}
|
||||
|
||||
const getActions = (row: PhoneAssetUnbindImportTask) => {
|
||||
const actions: any[] = []
|
||||
if (canDetail.value) {
|
||||
actions.push({ label: '详情', handler: () => goDetail(row), type: 'primary' })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
const goAssociationList = () => {
|
||||
router.push(RoutesAlias.PhoneAssetAssociation)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.unbind-import-tasks-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.task-no-link) {
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -340,7 +340,8 @@
|
||||
{
|
||||
prop: 'to_owner_name',
|
||||
label: '目标所有者',
|
||||
width: 150,
|
||||
width: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: AssetAllocationRecord) => row.to_owner_name || '-'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -54,9 +54,24 @@
|
||||
<ElTabs v-model="activeTab" class="detail-tabs">
|
||||
<!-- 佣金明细 Tab -->
|
||||
<ElTabPane label="佣金明细" name="commission">
|
||||
<div class="commission-export-actions">
|
||||
<ElButton
|
||||
v-if="hasAuth('commission_record:export')"
|
||||
@click="commissionExportDialogVisible = true"
|
||||
>
|
||||
导出
|
||||
</ElButton>
|
||||
</div>
|
||||
<ExportTaskCreateDialog
|
||||
v-model="commissionExportDialogVisible"
|
||||
scene="commission_record"
|
||||
:query="commissionExportQuery"
|
||||
confirm-permission="commission_record:export"
|
||||
title="导出佣金记录"
|
||||
/>
|
||||
<ArtTable
|
||||
ref="commissionTableRef"
|
||||
row-key="id"
|
||||
:row-key="getCommissionRowKey"
|
||||
:loading="commissionLoading"
|
||||
:data="commissionRecords"
|
||||
:currentPage="commissionPagination.page"
|
||||
@@ -70,14 +85,30 @@
|
||||
<template #default>
|
||||
<ElTableColumn label="佣金金额" prop="amount" width="120">
|
||||
<template #default="scope">
|
||||
<span style="font-weight: 500; color: var(--el-color-success)">
|
||||
<span
|
||||
:class="Number(scope.row.amount) < 0 ? 'amount-negative' : 'amount-positive'"
|
||||
>
|
||||
{{ formatMoney(scope.row.amount) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="入账后余额" prop="balance_after" width="120">
|
||||
<ElTableColumn label="记录来源" prop="source" width="150">
|
||||
<template #default="scope">
|
||||
{{ formatMoney(scope.row.balance_after) }}
|
||||
<div class="record-source-cell">
|
||||
<ElTag :type="commissionRecordSourceMeta(scope.row).type" size="small">
|
||||
{{ commissionRecordSourceMeta(scope.row).label }}
|
||||
</ElTag>
|
||||
<ElTag v-if="isCommissionClawbackRecord(scope.row)" type="danger" size="small">
|
||||
不可提现
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="入账后余额" prop="balance_after" width="130">
|
||||
<template #default="scope">
|
||||
<span :class="{ 'amount-negative': Number(scope.row.balance_after) < 0 }">
|
||||
{{ formatMoney(scope.row.balance_after) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金来源" prop="commission_source" width="120">
|
||||
@@ -133,21 +164,27 @@
|
||||
{{ formatDateTime(scope.row.created_at) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="150" fixed="right">
|
||||
<ElTableColumn label="操作" width="210" fixed="right">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.status === 99" style="display: flex; gap: 8px">
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<ArtButtonTable
|
||||
text="入账"
|
||||
iconColor="#67C23A"
|
||||
@click="handleResolveCommission(scope.row, 'release')"
|
||||
/>
|
||||
<ArtButtonTable
|
||||
text="作废"
|
||||
iconColor="#F56C6C"
|
||||
@click="handleResolveCommission(scope.row, 'invalidate')"
|
||||
text="详情"
|
||||
iconColor="#409EFF"
|
||||
@click="openCommissionRecordDetail(scope.row)"
|
||||
/>
|
||||
<template v-if="scope.row.status === 99">
|
||||
<ArtButtonTable
|
||||
text="入账"
|
||||
iconColor="#67C23A"
|
||||
@click="handleResolveCommission(scope.row, 'release')"
|
||||
/>
|
||||
<ArtButtonTable
|
||||
text="作废"
|
||||
iconColor="#F56C6C"
|
||||
@click="handleResolveCommission(scope.row, 'invalidate')"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
@@ -429,6 +466,14 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 佣金明细详情 -->
|
||||
<CommissionRecordDetailDialog
|
||||
v-model="commissionDetailVisible"
|
||||
:shop-id="currentShop?.shop_id"
|
||||
:record-id="commissionDetailId"
|
||||
:source="commissionDetailSource"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -445,7 +490,8 @@
|
||||
WithdrawalRequestItem,
|
||||
CommissionResolveAction,
|
||||
MainWalletTransactionItem,
|
||||
MainWalletTransactionQueryParams
|
||||
MainWalletTransactionQueryParams,
|
||||
ShopCommissionRecordSource
|
||||
} from '@/types/api/commission'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
@@ -456,10 +502,14 @@
|
||||
CommissionStatusMap,
|
||||
WithdrawalStatusMap,
|
||||
WithdrawalMethodMap,
|
||||
CommissionSourceMap
|
||||
CommissionSourceMap,
|
||||
COMMISSION_RECORD_SOURCE_MAP,
|
||||
getCommissionRecordRowKey,
|
||||
isCommissionClawbackRecord
|
||||
} from '@/config/constants/commission'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||
import CommissionRecordDetailDialog from '@/components/business/CommissionRecordDetailDialog.vue'
|
||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||
import { resolveFinanceAuditTarget } from '@/utils/business/auditNavigation'
|
||||
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
|
||||
@@ -518,6 +568,27 @@
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 佣金明细详情
|
||||
const commissionDetailVisible = ref(false)
|
||||
const commissionDetailId = ref<number | null>(null)
|
||||
const commissionDetailSource = ref<ShopCommissionRecordSource>('original')
|
||||
|
||||
const getCommissionRowKey = getCommissionRecordRowKey
|
||||
const commissionRecordSourceMeta = (row: ShopCommissionRecordItem) =>
|
||||
COMMISSION_RECORD_SOURCE_MAP[isCommissionClawbackRecord(row) ? 'clawback' : 'original']
|
||||
|
||||
const openCommissionRecordDetail = (row: ShopCommissionRecordItem) => {
|
||||
commissionDetailId.value = row.id
|
||||
commissionDetailSource.value = isCommissionClawbackRecord(row) ? 'clawback' : 'original'
|
||||
commissionDetailVisible.value = true
|
||||
}
|
||||
|
||||
// 导出佣金记录(仅支持 shop_id 筛选)
|
||||
const commissionExportDialogVisible = ref(false)
|
||||
const commissionExportQuery = computed(() => ({
|
||||
shop_id: currentShop.value?.shop_id
|
||||
}))
|
||||
|
||||
// 提现记录状态
|
||||
const withdrawalLoading = ref(false)
|
||||
const withdrawalTableRef = ref()
|
||||
@@ -1204,4 +1275,26 @@
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.amount-positive {
|
||||
font-weight: 500;
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.amount-negative {
|
||||
font-weight: 500;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.record-source-cell {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.commission-export-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
v-permission="'my_commission:add'"
|
||||
>发起提现</ElButton
|
||||
>
|
||||
<ElButton
|
||||
v-permission="'commission_record:export'"
|
||||
@click="exportDialogVisible = true"
|
||||
>导出</ElButton
|
||||
>
|
||||
|
||||
<div class="commission-summary">
|
||||
<div class="commission-summary-item">
|
||||
@@ -73,7 +78,7 @@
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="commissionTableRef"
|
||||
row-key="id"
|
||||
:row-key="getCommissionRowKey"
|
||||
:loading="commissionLoading"
|
||||
:data="commissionList"
|
||||
:currentPage="commissionPagination.page"
|
||||
@@ -122,6 +127,8 @@
|
||||
:currentPage="withdrawalPagination.page"
|
||||
:pageSize="withdrawalPagination.pageSize"
|
||||
:total="withdrawalPagination.total"
|
||||
:actions="getWithdrawalActions"
|
||||
:actionsWidth="150"
|
||||
:marginTop="10"
|
||||
:height="500"
|
||||
@size-change="handleWithdrawalSizeChange"
|
||||
@@ -136,6 +143,49 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElTabPane>
|
||||
<!-- 提现资料资格 -->
|
||||
<ElTabPane label="提现资料" name="qualification">
|
||||
<ArtTableHeader
|
||||
:columnList="qualificationColumnOptions"
|
||||
v-model:columns="qualificationColumnChecks"
|
||||
@refresh="getQualificationList"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
type="primary"
|
||||
v-permission="'my_commission:add'"
|
||||
@click="showQualificationDialog"
|
||||
>
|
||||
提交/更新资料
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="qualificationTableRef"
|
||||
row-key="id"
|
||||
:loading="qualificationLoading"
|
||||
:data="qualificationList"
|
||||
:currentPage="qualificationPagination.page"
|
||||
:pageSize="qualificationPagination.pageSize"
|
||||
:total="qualificationPagination.total"
|
||||
:actions="getQualificationActions"
|
||||
:actionsWidth="100"
|
||||
:marginTop="10"
|
||||
:height="500"
|
||||
@size-change="handleQualificationSizeChange"
|
||||
@current-change="handleQualificationCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn
|
||||
v-for="col in qualificationColumns"
|
||||
:key="col.prop || col.type"
|
||||
v-bind="col"
|
||||
/>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
|
||||
@@ -234,13 +284,259 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
<!-- 提交/更新提现资料资格对话框 -->
|
||||
<ElDialog
|
||||
v-model="qualificationDialogVisible"
|
||||
title="提现资料资格"
|
||||
width="720px"
|
||||
destroy-on-close
|
||||
>
|
||||
<ElForm
|
||||
ref="qualificationFormRef"
|
||||
:model="qualificationForm"
|
||||
:rules="qualificationRules"
|
||||
label-width="110px"
|
||||
>
|
||||
<ElFormItem label="主体类型" prop="subject_type">
|
||||
<ElRadioGroup v-model="qualificationForm.subject_type">
|
||||
<ElRadio label="enterprise">企业</ElRadio>
|
||||
<ElRadio label="individual">个人</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="主体证件号" prop="subject_code">
|
||||
<ElInput
|
||||
v-model="qualificationForm.subject_code"
|
||||
placeholder="统一社会信用代码/证件号"
|
||||
maxlength="50"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="法人身份证号" prop="legal_person_id_card">
|
||||
<ElInput
|
||||
v-model="qualificationForm.legal_person_id_card"
|
||||
placeholder="请输入法人/经营者身份证号"
|
||||
maxlength="18"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="合同" prop="contract_file_key">
|
||||
<VoucherUpload
|
||||
v-model="qualificationForm.contract_file_key"
|
||||
voucher-name="合同"
|
||||
:max-count="1"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="身份证正面" prop="id_card_front_file_key">
|
||||
<VoucherUpload
|
||||
v-model="qualificationForm.id_card_front_file_key"
|
||||
voucher-name="身份证正面"
|
||||
:max-count="1"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="身份证背面" prop="id_card_back_file_key">
|
||||
<VoucherUpload
|
||||
v-model="qualificationForm.id_card_back_file_key"
|
||||
voucher-name="身份证背面"
|
||||
:max-count="1"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="营业执照">
|
||||
<VoucherUpload
|
||||
v-model="qualificationForm.business_license_file_key"
|
||||
voucher-name="营业执照"
|
||||
:max-count="1"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="门头照">
|
||||
<VoucherUpload
|
||||
v-model="qualificationForm.shop_front_file_key"
|
||||
voucher-name="门头照"
|
||||
:max-count="1"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="发票">
|
||||
<VoucherUpload
|
||||
v-model="qualificationForm.invoice_file_key"
|
||||
voucher-name="发票"
|
||||
:max-count="1"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<div class="qualification-row">
|
||||
<ElFormItem label="发票抬头">
|
||||
<ElInput
|
||||
v-model="qualificationForm.invoice_title"
|
||||
placeholder="选填"
|
||||
maxlength="100"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="发票主体证件号">
|
||||
<ElInput
|
||||
v-model="qualificationForm.invoice_subject_code"
|
||||
placeholder="选填"
|
||||
maxlength="50"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="qualificationDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="qualificationSubmitting"
|
||||
@click="handleSubmitQualification"
|
||||
>
|
||||
提交
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 提现详情抽屉 -->
|
||||
<ElDrawer v-model="detailDrawerVisible" title="提现申请详情" size="520px">
|
||||
<div v-loading="detailLoading" class="withdrawal-detail">
|
||||
<template v-if="withdrawalDetail">
|
||||
<ElDescriptions :column="1" border>
|
||||
<ElDescriptionsItem label="提现单号">{{ withdrawalDetail.withdrawal_no }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">{{ withdrawalDetail.status_name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提现金额">{{ formatMoney(withdrawalDetail.amount) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实际到账">{{ formatMoney(withdrawalDetail.actual_amount) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="手续费">{{ formatMoney(withdrawalDetail.fee) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提现方式">
|
||||
{{ withdrawalMethodLabel(withdrawalDetail.withdrawal_method) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="收款账户">
|
||||
{{ withdrawalDetail.account_name }} / {{ withdrawalDetail.account_number }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="驳回原因">{{ withdrawalDetail.reject_reason || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="异常标记">
|
||||
<ElTag :type="withdrawalDetail.anomaly_flag ? 'danger' : 'success'">
|
||||
{{ withdrawalDetail.anomaly_name }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="withdrawalDetail.anomaly_reason" label="异常原因">
|
||||
{{ withdrawalDetail.anomaly_reason }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="尝试记录">
|
||||
<template v-if="withdrawalDetail.attempts && withdrawalDetail.attempts.length > 0">
|
||||
<div
|
||||
v-for="attempt in withdrawalDetail.attempts"
|
||||
:key="attempt.attempt"
|
||||
class="attempt-item"
|
||||
>
|
||||
<span>第 {{ attempt.attempt }} 次</span>
|
||||
<ElTag size="small">{{ attempt.status_name }}</ElTag>
|
||||
<span v-if="attempt.reject_reason" class="attempt-reason">
|
||||
{{ attempt.reject_reason }}
|
||||
</span>
|
||||
<span class="attempt-time">{{ formatDateTime(attempt.created_at) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</template>
|
||||
</div>
|
||||
</ElDrawer>
|
||||
|
||||
<!-- 重新提交被驳回的提现 -->
|
||||
<ElDialog
|
||||
v-model="resubmitDialogVisible"
|
||||
title="重新提交提现"
|
||||
width="560px"
|
||||
destroy-on-close
|
||||
>
|
||||
<ElForm
|
||||
ref="resubmitFormRef"
|
||||
:model="resubmitForm"
|
||||
:rules="resubmitRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<ElFormItem label="提现金额" prop="amount">
|
||||
<ElInputNumber
|
||||
v-model="resubmitForm.amount"
|
||||
:min="1"
|
||||
:max="withdrawalAmountMax"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="提现方式" prop="withdrawal_method">
|
||||
<ElRadioGroup v-model="resubmitForm.withdrawal_method">
|
||||
<ElRadio :label="WithdrawalMethod.ALIPAY">支付宝</ElRadio>
|
||||
<ElRadio :label="WithdrawalMethod.WECHAT">微信</ElRadio>
|
||||
<ElRadio :label="WithdrawalMethod.BANK">银行卡</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<template v-if="resubmitForm.withdrawal_method !== WithdrawalMethod.BANK">
|
||||
<ElFormItem label="账户名" prop="account_name">
|
||||
<ElInput v-model="resubmitForm.account_name" placeholder="请输入账户名" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账号" prop="account_number">
|
||||
<ElInput v-model="resubmitForm.account_number" placeholder="请输入账号" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElFormItem label="银行名称" prop="bank_name">
|
||||
<ElSelect v-model="resubmitForm.bank_name" placeholder="请选择银行" style="width: 100%">
|
||||
<ElOption v-for="bank in bankOptions" :key="bank" :label="bank" :value="bank" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账户名" prop="account_name">
|
||||
<ElInput v-model="resubmitForm.account_name" placeholder="请输入账户名" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="卡号" prop="account_number">
|
||||
<ElInput v-model="resubmitForm.account_number" placeholder="请输入银行卡号" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem label="发票附件">
|
||||
<VoucherUpload
|
||||
v-model="resubmitForm.invoice_keys"
|
||||
voucher-name="发票"
|
||||
:max-count="5"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="resubmitSubmitting"
|
||||
@click="handleSubmitResubmit"
|
||||
>
|
||||
提交
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 佣金明细详情 -->
|
||||
<CommissionRecordDetailDialog
|
||||
v-model="commissionDetailVisible"
|
||||
:shop-id="currentShopId"
|
||||
:record-id="commissionDetailId"
|
||||
:source="commissionDetailSource"
|
||||
/>
|
||||
|
||||
<!-- 导出佣金记录 -->
|
||||
<ExportTaskCreateDialog
|
||||
v-model="exportDialogVisible"
|
||||
scene="commission_record"
|
||||
:query="exportQuery"
|
||||
confirm-permission="commission_record:export"
|
||||
title="导出佣金记录"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { CommissionService, OrderService } from '@/api/modules'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { ElButton, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
MyCommissionSummary,
|
||||
@@ -249,11 +545,24 @@
|
||||
CommissionRecordQueryParams,
|
||||
WithdrawalRequestQueryParams,
|
||||
SubmitWithdrawalParams,
|
||||
WithdrawalStatus
|
||||
WithdrawalQualificationItem,
|
||||
WithdrawalQualificationSubmitParams,
|
||||
WithdrawalRequestDetail,
|
||||
ResubmitWithdrawalRequestParams,
|
||||
ShopCommissionRecordSource,
|
||||
} from '@/types/api/commission'
|
||||
import { WithdrawalMethod } from '@/types/api/commission'
|
||||
import { WithdrawalMethod, WithdrawalStatus } from '@/types/api/commission'
|
||||
import type { Order, OrderQueryParams } from '@/types/api/order'
|
||||
import { WithdrawalStatusMap, WithdrawalMethodMap } from '@/config/constants/commission'
|
||||
import {
|
||||
WithdrawalStatusMap,
|
||||
WithdrawalMethodMap,
|
||||
COMMISSION_RECORD_SOURCE_MAP,
|
||||
COMMISSION_RECORD_STATUS_OPTIONS,
|
||||
getCommissionRecordRowKey,
|
||||
isCommissionClawbackRecord
|
||||
} from '@/config/constants/commission'
|
||||
import CommissionRecordDetailDialog from '@/components/business/CommissionRecordDetailDialog.vue'
|
||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
@@ -349,12 +658,10 @@
|
||||
label: '结算状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '已冻结', value: 1 },
|
||||
{ label: '解冻中', value: 2 },
|
||||
{ label: '已发放', value: 3 },
|
||||
{ label: '已失效', value: 4 }
|
||||
],
|
||||
options: COMMISSION_RECORD_STATUS_OPTIONS.map((item) => ({
|
||||
label: item.label,
|
||||
value: Number(item.value)
|
||||
})),
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
@@ -431,6 +738,7 @@
|
||||
const commissionColumnOptions = [
|
||||
{ label: '佣金金额', prop: 'amount' },
|
||||
{ label: '佣金来源', prop: 'commission_source' },
|
||||
{ label: '记录来源', prop: 'source' },
|
||||
{ label: '状态', prop: 'status_name' },
|
||||
{ label: 'ICCID', prop: 'iccid' },
|
||||
{ label: '虚拟号', prop: 'virtual_no' },
|
||||
@@ -442,6 +750,28 @@
|
||||
|
||||
const commissionList = ref<ShopCommissionRecordItem[]>([])
|
||||
|
||||
// 佣金明细详情与导出
|
||||
const commissionDetailVisible = ref(false)
|
||||
const commissionDetailId = ref<number | null>(null)
|
||||
const commissionDetailSource = ref<ShopCommissionRecordSource>('original')
|
||||
const exportDialogVisible = ref(false)
|
||||
|
||||
const getCommissionRowKey = getCommissionRecordRowKey
|
||||
|
||||
const openCommissionDetail = (row: ShopCommissionRecordItem) => {
|
||||
commissionDetailId.value = row.id
|
||||
commissionDetailSource.value = isCommissionClawbackRecord(row) ? 'clawback' : 'original'
|
||||
commissionDetailVisible.value = true
|
||||
}
|
||||
|
||||
// 导出仅支持 shop_id / status / commission_source / order_no
|
||||
const exportQuery = computed(() => ({
|
||||
shop_id: currentShopId.value,
|
||||
status: commissionSearchForm.status,
|
||||
commission_source: commissionSearchForm.commission_source,
|
||||
order_no: commissionSearchForm.order_no || undefined
|
||||
}))
|
||||
|
||||
// 佣金来源映射
|
||||
const CommissionSourceMap = {
|
||||
cost_diff: { label: '成本差价', type: 'primary' as const },
|
||||
@@ -454,8 +784,27 @@
|
||||
{
|
||||
prop: 'amount',
|
||||
label: '佣金金额',
|
||||
width: 120,
|
||||
formatter: (row: ShopCommissionRecordItem) => formatMoney(row.amount)
|
||||
width: 130,
|
||||
formatter: (row: ShopCommissionRecordItem) =>
|
||||
h(
|
||||
'span',
|
||||
{ class: Number(row.amount) < 0 ? 'amount-negative' : '' },
|
||||
formatMoney(row.amount)
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'source',
|
||||
label: '记录来源',
|
||||
width: 150,
|
||||
formatter: (row: ShopCommissionRecordItem) => {
|
||||
const clawback = isCommissionClawbackRecord(row)
|
||||
const meta = COMMISSION_RECORD_SOURCE_MAP[clawback ? 'clawback' : 'original']
|
||||
const children = [h(ElTag, { type: meta.type, size: 'small' }, () => meta.label)]
|
||||
if (clawback) {
|
||||
children.push(h(ElTag, { type: 'danger', size: 'small' }, () => '不可提现'))
|
||||
}
|
||||
return h('span', { class: 'record-source-cell' }, children)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'commission_source',
|
||||
@@ -503,14 +852,30 @@
|
||||
{
|
||||
prop: 'balance_after',
|
||||
label: '入账后余额',
|
||||
width: 120,
|
||||
formatter: (row: ShopCommissionRecordItem) => formatMoney(row.balance_after)
|
||||
width: 130,
|
||||
formatter: (row: ShopCommissionRecordItem) =>
|
||||
h(
|
||||
'span',
|
||||
{ class: Number(row.balance_after) < 0 ? 'amount-negative' : '' },
|
||||
formatMoney(row.balance_after)
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: ShopCommissionRecordItem) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'actions',
|
||||
label: '操作',
|
||||
width: 90,
|
||||
formatter: (row: ShopCommissionRecordItem) =>
|
||||
h(
|
||||
ElButton,
|
||||
{ link: true, type: 'primary', onClick: () => openCommissionDetail(row) },
|
||||
() => '详情'
|
||||
)
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -966,9 +1331,418 @@
|
||||
getCommissionList()
|
||||
} else if (newTab === 'withdrawal') {
|
||||
getWithdrawalList()
|
||||
} else if (newTab === 'qualification') {
|
||||
getQualificationList()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// ==================== 提现资料资格 ====================
|
||||
|
||||
const qualificationLoading = ref(false)
|
||||
const qualificationTableRef = ref()
|
||||
const qualificationList = ref<WithdrawalQualificationItem[]>([])
|
||||
const qualificationPagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const qualificationColumnOptions = [
|
||||
{ label: '主体类型', prop: 'subject_type_name' },
|
||||
{ label: '主体证件号', prop: 'subject_code_masked' },
|
||||
{ label: '法人身份证', prop: 'legal_person_id_card_masked' },
|
||||
{ label: '状态', prop: 'status_name' },
|
||||
{ label: '审批状态', prop: 'approval_status_name' },
|
||||
{ label: '附件', prop: 'attachments' },
|
||||
{ label: '作废原因', prop: 'invalid_reason' },
|
||||
{ label: '作废时间', prop: 'invalidated_at' },
|
||||
{ label: '创建时间', prop: 'created_at' }
|
||||
]
|
||||
|
||||
const qualificationAttachmentLabels = (row: WithdrawalQualificationItem): string => {
|
||||
const labels: string[] = []
|
||||
if (row.contract_file_key) labels.push('合同')
|
||||
if (row.id_card_front_file_key) labels.push('身份证正面')
|
||||
if (row.id_card_back_file_key) labels.push('身份证背面')
|
||||
if (row.business_license_file_key) labels.push('营业执照')
|
||||
if (row.shop_front_file_key) labels.push('门头照')
|
||||
if (row.invoice_file_key) labels.push('发票')
|
||||
return labels.length > 0 ? labels.join('、') : '-'
|
||||
}
|
||||
|
||||
const {
|
||||
columnChecks: qualificationColumnChecks,
|
||||
columns: qualificationColumns
|
||||
} = useCheckedColumns(() => [
|
||||
{ prop: 'subject_type_name', label: '主体类型', width: 90 },
|
||||
{ prop: 'subject_code_masked', label: '主体证件号', minWidth: 160 },
|
||||
{ prop: 'legal_person_id_card_masked', label: '法人身份证', minWidth: 170 },
|
||||
{
|
||||
prop: 'status_name',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: WithdrawalQualificationItem) => {
|
||||
const tagType = row.status === 0 ? 'warning' : row.status === 1 ? 'success' : 'danger'
|
||||
return h(ElTag, { type: tagType }, () => row.status_name || String(row.status))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'approval_status_name',
|
||||
label: '审批状态',
|
||||
width: 100,
|
||||
formatter: (row: WithdrawalQualificationItem) => row.approval_status_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'attachments',
|
||||
label: '附件',
|
||||
minWidth: 200,
|
||||
formatter: (row: WithdrawalQualificationItem) => qualificationAttachmentLabels(row)
|
||||
},
|
||||
{
|
||||
prop: 'invalid_reason',
|
||||
label: '作废原因',
|
||||
minWidth: 160,
|
||||
formatter: (row: WithdrawalQualificationItem) => row.invalid_reason || '-'
|
||||
},
|
||||
{
|
||||
prop: 'invalidated_at',
|
||||
label: '作废时间',
|
||||
width: 170,
|
||||
formatter: (row: WithdrawalQualificationItem) =>
|
||||
row.invalidated_at ? formatDateTime(row.invalidated_at) : '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 170,
|
||||
formatter: (row: WithdrawalQualificationItem) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
|
||||
const getQualificationList = async () => {
|
||||
if (!currentShopId.value) return
|
||||
qualificationLoading.value = true
|
||||
try {
|
||||
const res = await CommissionService.getWithdrawalQualifications(currentShopId.value, {
|
||||
page: qualificationPagination.page,
|
||||
page_size: qualificationPagination.pageSize
|
||||
})
|
||||
if (res.code === 0) {
|
||||
qualificationList.value = res.data.items || []
|
||||
qualificationPagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
qualificationLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQualificationSizeChange = (newPageSize: number) => {
|
||||
qualificationPagination.pageSize = newPageSize
|
||||
getQualificationList()
|
||||
}
|
||||
|
||||
const handleQualificationCurrentChange = (newCurrentPage: number) => {
|
||||
qualificationPagination.page = newCurrentPage
|
||||
getQualificationList()
|
||||
}
|
||||
|
||||
const qualificationDialogVisible = ref(false)
|
||||
const qualificationSubmitting = ref(false)
|
||||
const qualificationFormRef = ref<FormInstance>()
|
||||
|
||||
interface QualificationForm {
|
||||
subject_type: string
|
||||
subject_code: string
|
||||
legal_person_id_card: string
|
||||
contract_file_key: string[]
|
||||
id_card_front_file_key: string[]
|
||||
id_card_back_file_key: string[]
|
||||
business_license_file_key: string[]
|
||||
shop_front_file_key: string[]
|
||||
invoice_file_key: string[]
|
||||
invoice_title: string
|
||||
invoice_subject_code: string
|
||||
}
|
||||
|
||||
const qualificationForm = reactive<QualificationForm>({
|
||||
subject_type: 'enterprise',
|
||||
subject_code: '',
|
||||
legal_person_id_card: '',
|
||||
contract_file_key: [],
|
||||
id_card_front_file_key: [],
|
||||
id_card_back_file_key: [],
|
||||
business_license_file_key: [],
|
||||
shop_front_file_key: [],
|
||||
invoice_file_key: [],
|
||||
invoice_title: '',
|
||||
invoice_subject_code: ''
|
||||
})
|
||||
|
||||
const qualificationRules = reactive<FormRules>({
|
||||
subject_type: [{ required: true, message: '请选择主体类型', trigger: 'change' }],
|
||||
subject_code: [{ required: true, message: '请输入主体证件号', trigger: 'blur' }],
|
||||
legal_person_id_card: [{ required: true, message: '请输入法人身份证号', trigger: 'blur' }],
|
||||
contract_file_key: [{ required: true, message: '请上传合同附件', trigger: 'change' }],
|
||||
id_card_front_file_key: [{ required: true, message: '请上传身份证正面', trigger: 'change' }],
|
||||
id_card_back_file_key: [{ required: true, message: '请上传身份证背面', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const showQualificationDialog = () => {
|
||||
qualificationDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmitQualification = async () => {
|
||||
if (!qualificationFormRef.value) return
|
||||
if (!currentShopId.value) {
|
||||
ElMessage.warning('未找到店铺信息')
|
||||
return
|
||||
}
|
||||
await qualificationFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
qualificationSubmitting.value = true
|
||||
try {
|
||||
const params: WithdrawalQualificationSubmitParams = {
|
||||
subject_type: qualificationForm.subject_type,
|
||||
subject_code: qualificationForm.subject_code.trim(),
|
||||
legal_person_id_card: qualificationForm.legal_person_id_card.trim(),
|
||||
contract_file_key: qualificationForm.contract_file_key[0] || '',
|
||||
id_card_front_file_key: qualificationForm.id_card_front_file_key[0] || '',
|
||||
id_card_back_file_key: qualificationForm.id_card_back_file_key[0] || ''
|
||||
}
|
||||
if (qualificationForm.business_license_file_key[0]) {
|
||||
params.business_license_file_key = qualificationForm.business_license_file_key[0]
|
||||
}
|
||||
if (qualificationForm.shop_front_file_key[0]) {
|
||||
params.shop_front_file_key = qualificationForm.shop_front_file_key[0]
|
||||
}
|
||||
if (qualificationForm.invoice_file_key[0]) {
|
||||
params.invoice_file_key = qualificationForm.invoice_file_key[0]
|
||||
}
|
||||
if (qualificationForm.invoice_title.trim()) {
|
||||
params.invoice_title = qualificationForm.invoice_title.trim()
|
||||
}
|
||||
if (qualificationForm.invoice_subject_code.trim()) {
|
||||
params.invoice_subject_code = qualificationForm.invoice_subject_code.trim()
|
||||
}
|
||||
|
||||
const res = await CommissionService.submitWithdrawalQualification(
|
||||
currentShopId.value!,
|
||||
params
|
||||
)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('提现资料资格提交成功')
|
||||
qualificationDialogVisible.value = false
|
||||
getQualificationList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
qualificationSubmitting.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getQualificationActions = (row: WithdrawalQualificationItem) => {
|
||||
if (!userStore.isSuperAdmin) return []
|
||||
return [
|
||||
{
|
||||
label: '作废',
|
||||
handler: () => handleVoidQualification(row),
|
||||
type: 'danger' as const
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const handleVoidQualification = async (row: WithdrawalQualificationItem) => {
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt('请输入作废原因', '作废提现资料资格', {
|
||||
confirmButtonText: '确认作废',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: '作废原因(必填)',
|
||||
inputValidator: (reason: string) =>
|
||||
reason && reason.trim() ? true : '作废原因必填',
|
||||
type: 'warning'
|
||||
})
|
||||
await CommissionService.voidWithdrawalQualification(row.id, { reason: value.trim() })
|
||||
ElMessage.success('资格已作废')
|
||||
getQualificationList()
|
||||
} catch {
|
||||
// 取消或请求失败
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 提现详情与重提 ====================
|
||||
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const withdrawalDetail = ref<WithdrawalRequestDetail | null>(null)
|
||||
|
||||
const withdrawalMethodLabel = (method: string) => {
|
||||
const config = WithdrawalMethodMap[method as keyof typeof WithdrawalMethodMap]
|
||||
return config ? config.label : method
|
||||
}
|
||||
|
||||
const openWithdrawalDetail = async (row: WithdrawalRequestItem) => {
|
||||
if (!currentShopId.value) return
|
||||
detailDrawerVisible.value = true
|
||||
detailLoading.value = true
|
||||
withdrawalDetail.value = null
|
||||
try {
|
||||
const res = await CommissionService.getWithdrawalRequestDetail(
|
||||
currentShopId.value,
|
||||
row.id
|
||||
)
|
||||
if (res.code === 0) {
|
||||
withdrawalDetail.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getWithdrawalActions = (row: WithdrawalRequestItem) => {
|
||||
const actions: Array<{ label: string; handler: (row: any) => void; type?: 'primary' | 'danger' }> = [
|
||||
{
|
||||
label: '详情',
|
||||
handler: () => openWithdrawalDetail(row),
|
||||
type: 'primary'
|
||||
}
|
||||
]
|
||||
if (row.status === WithdrawalStatus.REJECTED) {
|
||||
actions.push({
|
||||
label: '重新提交',
|
||||
handler: () => openResubmitDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
const resubmitDialogVisible = ref(false)
|
||||
const resubmitSubmitting = ref(false)
|
||||
const resubmitFormRef = ref<FormInstance>()
|
||||
const resubmitTargetId = ref<number | null>(null)
|
||||
const bankOptions = [
|
||||
'中国工商银行',
|
||||
'中国建设银行',
|
||||
'中国农业银行',
|
||||
'中国银行',
|
||||
'招商银行',
|
||||
'交通银行',
|
||||
'中国邮政储蓄银行',
|
||||
'其他银行'
|
||||
]
|
||||
|
||||
const resubmitForm = reactive<{
|
||||
amount: number | undefined
|
||||
withdrawal_method: WithdrawalMethod
|
||||
account_name: string
|
||||
account_number: string
|
||||
bank_name: string
|
||||
invoice_keys: string[]
|
||||
}>({
|
||||
amount: undefined,
|
||||
withdrawal_method: WithdrawalMethod.ALIPAY,
|
||||
account_name: '',
|
||||
account_number: '',
|
||||
bank_name: '',
|
||||
invoice_keys: []
|
||||
})
|
||||
|
||||
const resubmitRules = reactive<FormRules>({
|
||||
amount: [
|
||||
{ required: true, message: '请输入提现金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
const amountFen = yuanToFen(value)
|
||||
if (amountFen === undefined || amountFen <= 0) {
|
||||
callback(new Error('请输入正确的提现金额'))
|
||||
} else if (amountFen > summary.value.available_commission) {
|
||||
callback(new Error('提现金额不能大于可提现佣金'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
withdrawal_method: [{ required: true, message: '请选择提现方式', trigger: 'change' }],
|
||||
account_name: [
|
||||
{ required: true, message: '请输入账户名', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
account_number: [
|
||||
{ required: true, message: '请输入账号', trigger: 'blur' },
|
||||
{ min: 5, max: 50, message: '长度在 5 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
bank_name: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (resubmitForm.withdrawal_method === WithdrawalMethod.BANK && !value) {
|
||||
callback(new Error('请选择银行名称'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const openResubmitDialog = (row: WithdrawalRequestItem) => {
|
||||
resubmitTargetId.value = row.id
|
||||
resubmitForm.amount = Number((row.amount / 100).toFixed(2))
|
||||
resubmitForm.withdrawal_method = row.withdrawal_method || WithdrawalMethod.ALIPAY
|
||||
resubmitForm.account_name = row.account_name || ''
|
||||
resubmitForm.account_number = row.account_number || ''
|
||||
resubmitForm.bank_name = row.bank_name || ''
|
||||
resubmitForm.invoice_keys = []
|
||||
resubmitDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmitResubmit = async () => {
|
||||
if (!resubmitFormRef.value) return
|
||||
if (!currentShopId.value || !resubmitTargetId.value) return
|
||||
await resubmitFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
resubmitSubmitting.value = true
|
||||
try {
|
||||
const amount = yuanToFen(resubmitForm.amount)
|
||||
if (amount === undefined) {
|
||||
ElMessage.warning('请输入提现金额')
|
||||
return
|
||||
}
|
||||
const params: ResubmitWithdrawalRequestParams = {
|
||||
amount,
|
||||
withdrawal_method: resubmitForm.withdrawal_method,
|
||||
account_name: resubmitForm.account_name,
|
||||
account_number: resubmitForm.account_number
|
||||
}
|
||||
if (resubmitForm.invoice_keys.length > 0) {
|
||||
params.invoice_keys = resubmitForm.invoice_keys
|
||||
}
|
||||
await CommissionService.resubmitWithdrawalRequest(
|
||||
currentShopId.value!,
|
||||
resubmitTargetId.value!,
|
||||
params
|
||||
)
|
||||
ElMessage.success('重新提交成功')
|
||||
resubmitDialogVisible.value = false
|
||||
await getWithdrawalList()
|
||||
await loadSummary()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
resubmitSubmitting.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!hasShopId.value) {
|
||||
ElMessage.warning('当前账号未关联店铺,无法查看佣金信息')
|
||||
@@ -981,6 +1755,30 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.qualification-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 16px;
|
||||
}
|
||||
|
||||
.withdrawal-detail {
|
||||
.attempt-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
|
||||
.attempt-reason {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.attempt-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.commission-summary {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1018,4 +1816,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.amount-negative {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.record-source-cell {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -440,6 +440,8 @@
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PlatformAccount) => h('span', formatDateTime((row as any).created_at))
|
||||
},
|
||||
{
|
||||
|
||||
@@ -56,7 +56,15 @@
|
||||
<template #header>
|
||||
<div class="block-title">付款凭证</div>
|
||||
</template>
|
||||
<ElButton v-if="voucherKeys.length" @click="voucherVisible = true">查看付款凭证</ElButton>
|
||||
<ElButton
|
||||
v-if="
|
||||
voucherKeys.length &&
|
||||
hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationViewVoucher)
|
||||
"
|
||||
@click="voucherVisible = true"
|
||||
>
|
||||
查看付款凭证
|
||||
</ElButton>
|
||||
<span v-else class="empty-text">暂无付款凭证</span>
|
||||
</ElCard>
|
||||
|
||||
@@ -116,6 +124,8 @@
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { EmployeeCollectionService } from '@/api/modules'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants/augustIteration'
|
||||
import type {
|
||||
EmployeeCollectionAllocation,
|
||||
EmployeeCollectionApplication,
|
||||
@@ -136,6 +146,7 @@
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref<EmployeeCollectionApplicationDetailData | null>(null)
|
||||
|
||||
@@ -275,9 +275,6 @@
|
||||
|
||||
const getActions = (row: EmployeeCollectionApplication) => {
|
||||
const actions: any[] = []
|
||||
if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationDetail)) {
|
||||
actions.push({ label: '详情', handler: () => handleViewDetail(row), type: 'primary' })
|
||||
}
|
||||
if (
|
||||
hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationUpdate) &&
|
||||
canResubmitApplication(row)
|
||||
|
||||
@@ -472,9 +472,6 @@
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.billDetail)) {
|
||||
actions.push({ label: '详情', handler: () => handleViewDetail(row), type: 'primary' })
|
||||
}
|
||||
if (hasAuth(AUGUST_PERMISSIONS.employeeCollection.billClose) && canCloseBill(row)) {
|
||||
actions.push({ label: '关闭账单', handler: () => openCloseDialog(row), type: 'danger' })
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
@@ -29,11 +30,16 @@
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="filteredPaymentMethods"
|
||||
:pagination="false"
|
||||
:data="paymentMethods"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:margin-top="10"
|
||||
:actions="getActions"
|
||||
:actions-width="140"
|
||||
:actions-width="120"
|
||||
:always-show-pagination="true"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
@@ -45,7 +51,7 @@
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="editing ? '编辑收款方式' : '新增收款方式'"
|
||||
width="520px"
|
||||
width="35%"
|
||||
destroy-on-close
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
@@ -85,7 +91,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { h, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { EmployeeCollectionService } from '@/api/modules'
|
||||
@@ -100,7 +106,7 @@
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants/augustIteration'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeCollectionList } from '../employeeCollectionDisplay'
|
||||
import { normalizeCollectionPage } from '../employeeCollectionDisplay'
|
||||
|
||||
defineOptions({ name: 'EmployeeCollectionPaymentMethods' })
|
||||
|
||||
@@ -115,6 +121,8 @@
|
||||
const currentId = ref<number | null>(null)
|
||||
const paymentMethods = ref<EmployeeCollectionPaymentMethod[]>([])
|
||||
|
||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||
|
||||
const initialSearchState = {
|
||||
keyword: '',
|
||||
enabled: undefined as number | undefined
|
||||
@@ -170,21 +178,6 @@
|
||||
}
|
||||
])
|
||||
|
||||
const filteredPaymentMethods = computed(() => {
|
||||
const keyword = searchForm.keyword?.trim().toLowerCase()
|
||||
return paymentMethods.value.filter((item) => {
|
||||
const matchKeyword =
|
||||
!keyword ||
|
||||
(item.name || '').toLowerCase().includes(keyword) ||
|
||||
(item.code || '').toLowerCase().includes(keyword)
|
||||
const matchEnabled =
|
||||
searchForm.enabled === undefined ||
|
||||
searchForm.enabled === null ||
|
||||
(searchForm.enabled === 1 ? item.enabled : !item.enabled)
|
||||
return matchKeyword && matchEnabled
|
||||
})
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
code: '',
|
||||
name: '',
|
||||
@@ -201,7 +194,10 @@
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: EmployeeCollectionPaymentMethodQueryParams = { page: 1, page_size: 100 }
|
||||
const params: EmployeeCollectionPaymentMethodQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size
|
||||
}
|
||||
if (searchForm.enabled !== undefined && searchForm.enabled !== null) {
|
||||
params.enabled = searchForm.enabled === 1
|
||||
}
|
||||
@@ -210,7 +206,9 @@
|
||||
}
|
||||
const res = await EmployeeCollectionService.getPaymentMethods(params)
|
||||
if (res.code === 0) {
|
||||
paymentMethods.value = normalizeCollectionList<EmployeeCollectionPaymentMethod>(res.data)
|
||||
const { list, total } = normalizeCollectionPage<EmployeeCollectionPaymentMethod>(res.data)
|
||||
paymentMethods.value = list
|
||||
pagination.total = total
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取收款方式失败'))
|
||||
@@ -219,10 +217,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => getTableData()
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
void getTableData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
void getTableData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
pagination.page = 1
|
||||
void getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
void getTableData()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,21 @@
|
||||
>
|
||||
重新申请
|
||||
</ElButton>
|
||||
<template v-if="refund && canManualApproveRefund(refund)">
|
||||
<ElButton
|
||||
v-if="hasAuth('refund:approve')"
|
||||
type="primary"
|
||||
@click="openApprovalDialog('approve')"
|
||||
>
|
||||
通过
|
||||
</ElButton>
|
||||
<ElButton v-if="hasAuth('refund:reject')" type="danger" @click="openApprovalDialog('reject')">
|
||||
拒绝
|
||||
</ElButton>
|
||||
<ElButton v-if="hasAuth('refund:return')" type="primary" @click="openApprovalDialog('return')">
|
||||
退回
|
||||
</ElButton>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
@@ -31,6 +46,14 @@
|
||||
|
||||
<PaymentVoucherDialog :file-keys="refundVoucherFileKeys" @close="refundVoucherFileKeys = []" />
|
||||
|
||||
<!-- 退款审批操作对话框 -->
|
||||
<RefundApprovalDialog
|
||||
v-model="approvalDialogVisible"
|
||||
:refund="refund"
|
||||
:action="approvalAction"
|
||||
@success="refund && fetchRefundDetail(refund.id)"
|
||||
/>
|
||||
|
||||
<!-- 重新提交对话框 -->
|
||||
<ElDialog
|
||||
v-model="resubmitDialogVisible"
|
||||
@@ -57,16 +80,44 @@
|
||||
placeholder="不填则使用原金额"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="实收金额">
|
||||
<ElInputNumber
|
||||
v-model="resubmitForm.actual_received_amount"
|
||||
:min="1"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
<ElFormItem label="退款方式">
|
||||
<ElSelect
|
||||
v-model="resubmitForm.method"
|
||||
clearable
|
||||
placeholder="不修改则沿用原退款方式"
|
||||
style="width: 100%"
|
||||
placeholder="不填则使用原金额"
|
||||
/>
|
||||
>
|
||||
<ElOption
|
||||
v-for="opt in RefundMethodOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<template v-if="resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT">
|
||||
<ElFormItem label="收款账户名" prop="customer_account_name">
|
||||
<ElInput
|
||||
v-model="resubmitForm.customer_account_name"
|
||||
placeholder="请输入收款账户名"
|
||||
maxlength="50"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="收款账号" prop="customer_account_number">
|
||||
<ElInput
|
||||
v-model="resubmitForm.customer_account_number"
|
||||
placeholder="请输入收款账号"
|
||||
maxlength="64"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="开户银行">
|
||||
<ElInput
|
||||
v-model="resubmitForm.customer_bank_name"
|
||||
placeholder="选填"
|
||||
maxlength="50"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem label="退款原因">
|
||||
<ElInput
|
||||
v-model="resubmitForm.refund_reason"
|
||||
@@ -74,18 +125,28 @@
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入退款原因"
|
||||
/>
|
||||
placeholder="请输入退款原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款凭证" prop="refund_voucher_key">
|
||||
<ElFormItem
|
||||
:label="
|
||||
resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT ? '退款凭证(必填)' : '退款凭证'
|
||||
"
|
||||
prop="refund_voucher_key"
|
||||
>
|
||||
<VoucherUpload
|
||||
ref="resubmitUploadRef"
|
||||
v-model="resubmitForm.refund_voucher_key"
|
||||
voucher-name="退款凭证"
|
||||
@uploading-change="resubmitVoucherUploading = $event"
|
||||
@change="resubmitFormRef?.validateField('refund_voucher_key')"
|
||||
@files-change="resubmitForm.attachments = $event"
|
||||
/>
|
||||
<div
|
||||
v-if="resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT"
|
||||
class="resubmit-voucher-tip"
|
||||
>
|
||||
客户收款信息方式下须同时提供客户收款信息与至少 1 个退款凭证
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -109,15 +170,28 @@
|
||||
import { ref, onMounted, computed, h, reactive } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElButton, ElCard, ElIcon, ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { RefundService } from '@/api/modules'
|
||||
import type { Refund, ResubmitRefundRequest, RefundAttachment } from '@/types/api'
|
||||
import {
|
||||
RefundMethod,
|
||||
type Refund,
|
||||
type RefundAttemptItem,
|
||||
type ResubmitRefundRequest,
|
||||
type RefundAttachment
|
||||
} from '@/types/api'
|
||||
import {
|
||||
RefundMethodOptions,
|
||||
canManualApproveRefund,
|
||||
canResubmitRefund
|
||||
} from '@/config/constants/refund'
|
||||
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
|
||||
import { toVoucherKeyList, getErrorMessage } from '@/utils/business'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
import RefundApprovalDialog from '@/components/business/RefundApprovalDialog.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
@@ -132,6 +206,14 @@
|
||||
const loading = ref(false)
|
||||
const refund = ref<Refund | null>(null)
|
||||
const refundVoucherFileKeys = ref<string[]>([])
|
||||
const approvalDialogVisible = ref(false)
|
||||
const approvalAction = ref<'approve' | 'reject' | 'return'>('approve')
|
||||
|
||||
const openApprovalDialog = (action: 'approve' | 'reject' | 'return') => {
|
||||
if (!refund.value || !canManualApproveRefund(refund.value)) return
|
||||
approvalAction.value = action
|
||||
approvalDialogVisible.value = true
|
||||
}
|
||||
|
||||
const pageTitle = computed(() => `退款详情`)
|
||||
|
||||
@@ -143,19 +225,56 @@
|
||||
const resubmitFormRef = ref()
|
||||
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
|
||||
const resubmitRules = ref()
|
||||
const requireResubmitCustomerAccount =
|
||||
(message: string) =>
|
||||
(_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT && !value?.trim()) {
|
||||
callback(new Error(message))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const validateResubmitVoucherKey = (
|
||||
_rule: unknown,
|
||||
value: string[],
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
if (
|
||||
resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT &&
|
||||
(!Array.isArray(value) || value.length === 0)
|
||||
) {
|
||||
callback(new Error('请上传至少 1 个退款凭证'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const resubmitRules = reactive<FormRules>({
|
||||
customer_account_name: [
|
||||
{ validator: requireResubmitCustomerAccount('请填写收款账户名'), trigger: 'blur' }
|
||||
],
|
||||
customer_account_number: [
|
||||
{ validator: requireResubmitCustomerAccount('请填写收款账号'), trigger: 'blur' }
|
||||
],
|
||||
refund_voucher_key: [{ validator: validateResubmitVoucherKey, trigger: 'change' }]
|
||||
})
|
||||
|
||||
const resubmitForm = reactive<{
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount?: number
|
||||
method?: RefundMethod | ''
|
||||
customer_account_name: string
|
||||
customer_account_number: string
|
||||
customer_bank_name: string
|
||||
refund_voucher_key: string[]
|
||||
attachments: RefundAttachment[]
|
||||
refund_reason?: string
|
||||
}>({
|
||||
requested_refund_amount: undefined,
|
||||
actual_received_amount: undefined,
|
||||
method: '',
|
||||
customer_account_name: '',
|
||||
customer_account_number: '',
|
||||
customer_bank_name: '',
|
||||
refund_voucher_key: [],
|
||||
attachments: [],
|
||||
refund_reason: ''
|
||||
})
|
||||
|
||||
@@ -165,6 +284,11 @@
|
||||
return `¥${(amount / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
const formatMb = (value: number | null | undefined): string => {
|
||||
if (value === undefined || value === null || Number.isNaN(value)) return '-'
|
||||
return `${value} MB`
|
||||
}
|
||||
|
||||
const getApprovalProviderText = (item: Refund) => {
|
||||
const provider = item.approval_provider || item.approval_source || item.approval?.source
|
||||
if (provider === 'wecom') return '企微'
|
||||
@@ -224,6 +348,60 @@
|
||||
)
|
||||
}
|
||||
|
||||
const renderAttempts = (attempts: unknown) => {
|
||||
if (!Array.isArray(attempts) || attempts.length === 0) return h('span', '-')
|
||||
|
||||
const sorted = [...attempts].sort((a, b) => {
|
||||
const timeA = new Date(a.created_at || 0).getTime()
|
||||
const timeB = new Date(b.created_at || 0).getTime()
|
||||
return timeB - timeA
|
||||
})
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{ class: 'attempt-list' },
|
||||
sorted.map((item: RefundAttemptItem) => {
|
||||
const voucherKeys = toVoucherKeyList(item.customer_voucher_key ?? undefined)
|
||||
return h('div', { class: 'attempt-item' }, [
|
||||
h('div', { class: 'attempt-item__header' }, [
|
||||
h('strong', `第 ${item.attempt_no ?? '-'} 次尝试`),
|
||||
h('span', item.approval_status_name || item.approval_status || '-')
|
||||
]),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'attempt-item__meta' },
|
||||
[
|
||||
`退款方式:${item.method_name || '-'}`,
|
||||
`金额:${formatCurrency(item.refund_amount)}`,
|
||||
item.channel_refund_request_no
|
||||
? `渠道申请号:${item.channel_refund_request_no}`
|
||||
: '',
|
||||
item.created_at ? `提交时间:${formatDateTime(item.created_at)}` : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
),
|
||||
item.refund_reason
|
||||
? h('div', { class: 'attempt-item__reason' }, `退款原因:${item.refund_reason}`)
|
||||
: null,
|
||||
voucherKeys.length
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => {
|
||||
refundVoucherFileKeys.value = voucherKeys
|
||||
}
|
||||
},
|
||||
() => '查看该次凭证'
|
||||
)
|
||||
: null
|
||||
])
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const getRefundAttachmentKeys = (item: Refund) => {
|
||||
const keys = item.attachments?.map((attachment) => attachment.file_key).filter(Boolean) || []
|
||||
return keys.length ? keys : toVoucherKeyList(item.refund_voucher_key)
|
||||
@@ -260,6 +438,64 @@
|
||||
label: '实收金额',
|
||||
formatter: (_, data) => formatCurrency(data.actual_received_amount)
|
||||
},
|
||||
{
|
||||
label: '退款方式',
|
||||
formatter: (_, data) => data.method_name || '-'
|
||||
},
|
||||
{
|
||||
label: '冻结实收金额',
|
||||
formatter: (_, data) => formatCurrency(data.frozen_actual_received_amount)
|
||||
},
|
||||
{
|
||||
label: '渠道退款状态',
|
||||
formatter: (_, data) => data.channel_refund_status_name || '-'
|
||||
},
|
||||
{
|
||||
label: '渠道退款流水号',
|
||||
formatter: (_, data) => data.channel_refund_no || '-'
|
||||
},
|
||||
{
|
||||
label: '渠道退款申请号',
|
||||
formatter: (_, data) => data.channel_refund_request_no || '-'
|
||||
},
|
||||
{
|
||||
label: '渠道退款金额',
|
||||
formatter: (_, data) => formatCurrency(data.channel_refund_amount)
|
||||
},
|
||||
{
|
||||
label: '渠道退款时间',
|
||||
formatter: (_, data) =>
|
||||
data.channel_refunded_at ? formatDateTime(data.channel_refunded_at) : '-'
|
||||
},
|
||||
{
|
||||
label: '失败分类',
|
||||
formatter: (_, data) => data.failure_reason_name || data.failure_reason || '-'
|
||||
},
|
||||
{
|
||||
label: '失败原因/消息',
|
||||
formatter: (_, data) => data.failure_message || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '异常标记',
|
||||
formatter: (_, data) =>
|
||||
Number(data.anomaly_flag) === 1
|
||||
? `异常:${data.anomaly_reason || ''}`
|
||||
: '正常'
|
||||
},
|
||||
{
|
||||
label: '套餐已用量/总量',
|
||||
formatter: (_, data) =>
|
||||
`${formatMb(data.refund_package_used_mb)} / ${formatMb(data.refund_package_total_mb)}`
|
||||
},
|
||||
{
|
||||
label: '客户收款信息',
|
||||
fullWidth: true,
|
||||
render: (data) =>
|
||||
data.method === RefundMethod.CUSTOMER_ACCOUNT && data.customer_account_info
|
||||
? renderStructuredValue(data.customer_account_info)
|
||||
: h('span', '-')
|
||||
},
|
||||
...(!isRestrictedCustomerRole.value
|
||||
? [
|
||||
{
|
||||
@@ -416,6 +652,16 @@
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '审批尝试历史',
|
||||
fields: [
|
||||
{
|
||||
label: '尝试记录',
|
||||
fullWidth: true,
|
||||
render: (data) => renderAttempts(data.attempts)
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
@@ -448,39 +694,29 @@
|
||||
const handleResubmitDialogClosed = () => {
|
||||
resubmitFormRef.value?.resetFields()
|
||||
resubmitForm.requested_refund_amount = undefined
|
||||
resubmitForm.actual_received_amount = undefined
|
||||
resubmitForm.method = ''
|
||||
resubmitForm.customer_account_name = ''
|
||||
resubmitForm.customer_account_number = ''
|
||||
resubmitForm.customer_bank_name = ''
|
||||
resubmitForm.refund_voucher_key = []
|
||||
resubmitForm.attachments = []
|
||||
resubmitForm.refund_reason = ''
|
||||
resubmitVoucherUploading.value = false
|
||||
resubmitUploadRef.value?.clearFiles(false)
|
||||
}
|
||||
|
||||
const canResubmit = (item: Refund) => {
|
||||
const statusText = [item.approval_status, item.approval_status_name, item.status_name]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
|
||||
return (
|
||||
item.status === 3 ||
|
||||
statusText.includes('reject') ||
|
||||
statusText.includes('revoke') ||
|
||||
statusText.includes('delete') ||
|
||||
statusText.includes('驳回') ||
|
||||
statusText.includes('撤销') ||
|
||||
statusText.includes('删除')
|
||||
)
|
||||
return canResubmitRefund(item)
|
||||
}
|
||||
|
||||
const handleShowResubmit = () => {
|
||||
if (!refund.value || !canResubmit(refund.value)) return
|
||||
resubmitForm.requested_refund_amount =
|
||||
fenToYuan(refund.value.requested_refund_amount) || undefined
|
||||
resubmitForm.actual_received_amount =
|
||||
fenToYuan(refund.value.actual_received_amount) || undefined
|
||||
resubmitForm.method = ''
|
||||
resubmitForm.customer_account_name = ''
|
||||
resubmitForm.customer_account_number = ''
|
||||
resubmitForm.customer_bank_name = ''
|
||||
resubmitForm.refund_voucher_key = []
|
||||
resubmitForm.attachments = []
|
||||
resubmitForm.refund_reason = refund.value.refund_reason
|
||||
resubmitDialogVisible.value = true
|
||||
}
|
||||
@@ -499,9 +735,22 @@
|
||||
try {
|
||||
const data: ResubmitRefundRequest = {
|
||||
requested_refund_amount: yuanToFen(resubmitForm.requested_refund_amount),
|
||||
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
|
||||
refund_reason: resubmitForm.refund_reason || undefined,
|
||||
attachments: resubmitForm.attachments
|
||||
refund_reason: resubmitForm.refund_reason || undefined
|
||||
}
|
||||
if (resubmitForm.method) {
|
||||
data.method = resubmitForm.method
|
||||
}
|
||||
if (resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT) {
|
||||
data.customer_account_info = {
|
||||
account_name: resubmitForm.customer_account_name.trim(),
|
||||
account_number: resubmitForm.customer_account_number.trim(),
|
||||
...(resubmitForm.customer_bank_name.trim()
|
||||
? { bank_name: resubmitForm.customer_bank_name.trim() }
|
||||
: {})
|
||||
}
|
||||
data.refund_voucher_key = resubmitForm.refund_voucher_key
|
||||
} else if (resubmitForm.refund_voucher_key.length) {
|
||||
data.refund_voucher_key = resubmitForm.refund_voucher_key
|
||||
}
|
||||
|
||||
await RefundService.resubmitRefund(refundId, data)
|
||||
@@ -564,6 +813,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
.resubmit-voucher-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.attempt-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.attempt-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
&__reason {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -51,6 +51,14 @@
|
||||
<!-- 创建退款申请对话框 -->
|
||||
<CreateRefundDialog v-model="createDialogVisible" @success="handleCreateSuccess" />
|
||||
|
||||
<!-- 退款审批操作对话框 -->
|
||||
<RefundApprovalDialog
|
||||
v-model="approvalDialogVisible"
|
||||
:refund="currentRefund"
|
||||
:action="approvalAction"
|
||||
@success="getTableData"
|
||||
/>
|
||||
|
||||
<!-- 导出任务对话框 -->
|
||||
<ExportTaskCreateDialog
|
||||
v-model="exportDialogVisible"
|
||||
@@ -92,16 +100,44 @@
|
||||
placeholder="不填则使用原金额"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="实收金额">
|
||||
<ElInputNumber
|
||||
v-model="resubmitForm.actual_received_amount"
|
||||
:min="1"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
<ElFormItem label="退款方式">
|
||||
<ElSelect
|
||||
v-model="resubmitForm.method"
|
||||
clearable
|
||||
placeholder="不修改则沿用原退款方式"
|
||||
style="width: 100%"
|
||||
placeholder="不填则使用原金额"
|
||||
/>
|
||||
>
|
||||
<ElOption
|
||||
v-for="opt in RefundMethodOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<template v-if="resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT">
|
||||
<ElFormItem label="收款账户名" prop="customer_account_name">
|
||||
<ElInput
|
||||
v-model="resubmitForm.customer_account_name"
|
||||
placeholder="请输入收款账户名"
|
||||
maxlength="50"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="收款账号" prop="customer_account_number">
|
||||
<ElInput
|
||||
v-model="resubmitForm.customer_account_number"
|
||||
placeholder="请输入收款账号"
|
||||
maxlength="64"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="开户银行">
|
||||
<ElInput
|
||||
v-model="resubmitForm.customer_bank_name"
|
||||
placeholder="选填"
|
||||
maxlength="50"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem label="退款原因">
|
||||
<ElInput
|
||||
v-model="resubmitForm.refund_reason"
|
||||
@@ -112,15 +148,25 @@
|
||||
placeholder="请输入退款原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款凭证" prop="refund_voucher_key">
|
||||
<ElFormItem
|
||||
:label="
|
||||
resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT ? '退款凭证(必填)' : '退款凭证'
|
||||
"
|
||||
prop="refund_voucher_key"
|
||||
>
|
||||
<VoucherUpload
|
||||
ref="resubmitUploadRef"
|
||||
v-model="resubmitForm.refund_voucher_key"
|
||||
voucher-name="退款凭证"
|
||||
@uploading-change="resubmitVoucherUploading = $event"
|
||||
@change="resubmitFormRef?.validateField('refund_voucher_key')"
|
||||
@files-change="resubmitForm.attachments = $event"
|
||||
/>
|
||||
<div
|
||||
v-if="resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT"
|
||||
class="resubmit-voucher-tip"
|
||||
>
|
||||
客户收款信息方式下须同时提供客户收款信息与至少 1 个退款凭证
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -150,11 +196,17 @@
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import {
|
||||
RefundStatus,
|
||||
RefundMethod,
|
||||
type Refund,
|
||||
type RefundQueryParams,
|
||||
type ResubmitRefundRequest,
|
||||
type RefundAttachment
|
||||
type ResubmitRefundRequest
|
||||
} from '@/types/api'
|
||||
import {
|
||||
RefundMethodOptions,
|
||||
RefundStatusTypeMap,
|
||||
canResubmitRefund,
|
||||
canManualApproveRefund
|
||||
} from '@/config/constants/refund'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
|
||||
@@ -178,6 +230,7 @@
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
import CreateRefundDialog from '@/components/business/CreateRefundDialog.vue'
|
||||
import RefundApprovalDialog from '@/components/business/RefundApprovalDialog.vue'
|
||||
defineOptions({ name: 'RefundList' })
|
||||
|
||||
const router = useRouter()
|
||||
@@ -207,6 +260,14 @@
|
||||
const resubmitDialogVisible = ref(false)
|
||||
const currentRefund = ref<Refund | null>(null)
|
||||
const refundVoucherFileKeys = ref<string[]>([])
|
||||
const approvalDialogVisible = ref(false)
|
||||
const approvalAction = ref<'approve' | 'reject' | 'return'>('approve')
|
||||
|
||||
const openApprovalDialog = (row: Refund, action: 'approve' | 'reject' | 'return') => {
|
||||
currentRefund.value = row
|
||||
approvalAction.value = action
|
||||
approvalDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: RefundQueryParams = {
|
||||
@@ -277,7 +338,9 @@
|
||||
{ label: '待审批', value: 1 },
|
||||
{ label: '已通过', value: 2 },
|
||||
{ label: '已拒绝', value: 3 },
|
||||
{ label: '已退回', value: 4 }
|
||||
{ label: '已退回', value: 4 },
|
||||
{ label: '原路退款处理中', value: 5 },
|
||||
{ label: '原路退款失败', value: 6 }
|
||||
],
|
||||
config: {
|
||||
clearable: true
|
||||
@@ -302,6 +365,15 @@
|
||||
{ label: '申请退款金额', prop: 'requested_refund_amount' },
|
||||
{ label: '实际退款金额', prop: 'approved_refund_amount' },
|
||||
{ label: '实收金额', prop: 'actual_received_amount' },
|
||||
{ label: '退款方式', prop: 'method' },
|
||||
{ label: '冻结实收金额', prop: 'frozen_actual_received_amount' },
|
||||
{ label: '渠道退款状态', prop: 'channel_refund_status' },
|
||||
{ label: '渠道退款流水号', prop: 'channel_refund_no' },
|
||||
{ label: '渠道退款金额', prop: 'channel_refund_amount' },
|
||||
{ label: '失败分类', prop: 'failure_reason' },
|
||||
{ label: '异常标记', prop: 'anomaly_flag' },
|
||||
{ label: '套餐已用量(MB)', prop: 'refund_package_used_mb' },
|
||||
{ label: '套餐总量(MB)', prop: 'refund_package_total_mb' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '审批渠道', prop: 'approval_provider' },
|
||||
{ label: '提交人', prop: 'submitter_name' },
|
||||
@@ -320,19 +392,56 @@
|
||||
|
||||
const resubmitFormRef = ref<FormInstance>()
|
||||
|
||||
const resubmitRules = reactive<FormRules>({})
|
||||
const requireResubmitCustomerAccount =
|
||||
(message: string) =>
|
||||
(_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT && !value?.trim()) {
|
||||
callback(new Error(message))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const validateResubmitVoucherKey = (
|
||||
_rule: unknown,
|
||||
value: string[],
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
if (
|
||||
resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT &&
|
||||
(!Array.isArray(value) || value.length === 0)
|
||||
) {
|
||||
callback(new Error('请上传至少 1 个退款凭证'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const resubmitRules = reactive<FormRules>({
|
||||
customer_account_name: [
|
||||
{ validator: requireResubmitCustomerAccount('请填写收款账户名'), trigger: 'blur' }
|
||||
],
|
||||
customer_account_number: [
|
||||
{ validator: requireResubmitCustomerAccount('请填写收款账号'), trigger: 'blur' }
|
||||
],
|
||||
refund_voucher_key: [{ validator: validateResubmitVoucherKey, trigger: 'change' }]
|
||||
})
|
||||
|
||||
const resubmitForm = reactive<{
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount?: number
|
||||
method?: RefundMethod | ''
|
||||
customer_account_name: string
|
||||
customer_account_number: string
|
||||
customer_bank_name: string
|
||||
refund_voucher_key: string[]
|
||||
attachments: RefundAttachment[]
|
||||
refund_reason?: string
|
||||
}>({
|
||||
requested_refund_amount: undefined,
|
||||
actual_received_amount: undefined,
|
||||
method: '',
|
||||
customer_account_name: '',
|
||||
customer_account_number: '',
|
||||
customer_bank_name: '',
|
||||
refund_voucher_key: [],
|
||||
attachments: [],
|
||||
refund_reason: ''
|
||||
})
|
||||
|
||||
@@ -345,15 +454,14 @@
|
||||
return `¥${(amount / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
const formatMb = (value: number | null | undefined): string => {
|
||||
if (value === undefined || value === null || Number.isNaN(value)) return '-'
|
||||
return `${value} MB`
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: RefundStatus): 'warning' | 'success' | 'danger' | 'info' => {
|
||||
const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = {
|
||||
1: 'warning', // 待审批
|
||||
2: 'success', // 已通过
|
||||
3: 'danger', // 已拒绝
|
||||
4: 'info' // 已退回
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
return RefundStatusTypeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
@@ -425,6 +533,69 @@
|
||||
width: 120,
|
||||
formatter: (row: Refund) => formatCurrency(row.actual_received_amount)
|
||||
},
|
||||
{
|
||||
prop: 'method',
|
||||
label: '退款方式',
|
||||
width: 130,
|
||||
formatter: (row: Refund) => row.method_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'frozen_actual_received_amount',
|
||||
label: '冻结实收金额',
|
||||
width: 140,
|
||||
formatter: (row: Refund) => formatCurrency(row.frozen_actual_received_amount)
|
||||
},
|
||||
{
|
||||
prop: 'channel_refund_status',
|
||||
label: '渠道退款状态',
|
||||
width: 130,
|
||||
formatter: (row: Refund) => row.channel_refund_status_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'channel_refund_no',
|
||||
label: '渠道退款流水号',
|
||||
width: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: Refund) => row.channel_refund_no || '-'
|
||||
},
|
||||
{
|
||||
prop: 'channel_refund_amount',
|
||||
label: '渠道退款金额',
|
||||
width: 140,
|
||||
formatter: (row: Refund) => formatCurrency(row.channel_refund_amount)
|
||||
},
|
||||
{
|
||||
prop: 'failure_reason',
|
||||
label: '失败分类',
|
||||
width: 150,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: Refund) => row.failure_reason_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'anomaly_flag',
|
||||
label: '异常标记',
|
||||
width: 110,
|
||||
formatter: (row: Refund) => {
|
||||
if (Number(row.anomaly_flag) !== 1) return h(ElTag, { type: 'success' }, () => '正常')
|
||||
return h(
|
||||
'span',
|
||||
{ title: row.anomaly_reason || '退款流程异常' },
|
||||
h(ElTag, { type: 'danger' }, () => '异常')
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'refund_package_used_mb',
|
||||
label: '套餐已用量(MB)',
|
||||
width: 140,
|
||||
formatter: (row: Refund) => formatMb(row.refund_package_used_mb)
|
||||
},
|
||||
{
|
||||
prop: 'refund_package_total_mb',
|
||||
label: '套餐总量(MB)',
|
||||
width: 130,
|
||||
formatter: (row: Refund) => formatMb(row.refund_package_total_mb)
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
@@ -659,9 +830,11 @@
|
||||
currentRefund.value = row
|
||||
// 预填充原有数据
|
||||
resubmitForm.requested_refund_amount = fenToYuan(row.requested_refund_amount) || undefined
|
||||
resubmitForm.actual_received_amount = fenToYuan(row.actual_received_amount) || undefined
|
||||
resubmitForm.method = ''
|
||||
resubmitForm.customer_account_name = ''
|
||||
resubmitForm.customer_account_number = ''
|
||||
resubmitForm.customer_bank_name = ''
|
||||
resubmitForm.refund_voucher_key = []
|
||||
resubmitForm.attachments = []
|
||||
resubmitForm.refund_reason = row.refund_reason
|
||||
resubmitDialogVisible.value = true
|
||||
}
|
||||
@@ -670,9 +843,11 @@
|
||||
const handleResubmitDialogClosed = () => {
|
||||
resubmitFormRef.value?.resetFields()
|
||||
resubmitForm.requested_refund_amount = undefined
|
||||
resubmitForm.actual_received_amount = undefined
|
||||
resubmitForm.method = ''
|
||||
resubmitForm.customer_account_name = ''
|
||||
resubmitForm.customer_account_number = ''
|
||||
resubmitForm.customer_bank_name = ''
|
||||
resubmitForm.refund_voucher_key = []
|
||||
resubmitForm.attachments = []
|
||||
resubmitForm.refund_reason = ''
|
||||
resubmitVoucherUploading.value = false
|
||||
resubmitUploadRef.value?.clearFiles(false)
|
||||
@@ -695,9 +870,22 @@
|
||||
if (!refundId) return
|
||||
const data: ResubmitRefundRequest = {
|
||||
requested_refund_amount: yuanToFen(resubmitForm.requested_refund_amount),
|
||||
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
|
||||
refund_reason: resubmitForm.refund_reason || undefined,
|
||||
attachments: resubmitForm.attachments
|
||||
refund_reason: resubmitForm.refund_reason || undefined
|
||||
}
|
||||
if (resubmitForm.method) {
|
||||
data.method = resubmitForm.method
|
||||
}
|
||||
if (resubmitForm.method === RefundMethod.CUSTOMER_ACCOUNT) {
|
||||
data.customer_account_info = {
|
||||
account_name: resubmitForm.customer_account_name.trim(),
|
||||
account_number: resubmitForm.customer_account_number.trim(),
|
||||
...(resubmitForm.customer_bank_name.trim()
|
||||
? { bank_name: resubmitForm.customer_bank_name.trim() }
|
||||
: {})
|
||||
}
|
||||
data.refund_voucher_key = resubmitForm.refund_voucher_key
|
||||
} else if (resubmitForm.refund_voucher_key.length) {
|
||||
data.refund_voucher_key = resubmitForm.refund_voucher_key
|
||||
}
|
||||
|
||||
await RefundService.resubmitRefund(refundId, data)
|
||||
@@ -735,20 +923,7 @@
|
||||
}
|
||||
|
||||
const canResubmit = (row: Refund) => {
|
||||
const statusText = [row.approval_status, row.approval_status_name, row.status_name]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
|
||||
return (
|
||||
row.status === 3 ||
|
||||
statusText.includes('reject') ||
|
||||
statusText.includes('revoke') ||
|
||||
statusText.includes('delete') ||
|
||||
statusText.includes('驳回') ||
|
||||
statusText.includes('撤销') ||
|
||||
statusText.includes('删除')
|
||||
)
|
||||
return canResubmitRefund(row)
|
||||
}
|
||||
|
||||
const canTriggerApproval = (row: Refund) => {
|
||||
@@ -854,6 +1029,28 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (canManualApproveRefund(row) && hasAuth('refund:approve')) {
|
||||
actions.push({
|
||||
label: '通过',
|
||||
handler: () => openApprovalDialog(row, 'approve'),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
if (canManualApproveRefund(row) && hasAuth('refund:reject')) {
|
||||
actions.push({
|
||||
label: '拒绝',
|
||||
handler: () => openApprovalDialog(row, 'reject'),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
if (canManualApproveRefund(row) && hasAuth('refund:return')) {
|
||||
actions.push({
|
||||
label: '退回',
|
||||
handler: () => openApprovalDialog(row, 'return'),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (canResubmit(row) && hasAuth('refund:resubmit')) {
|
||||
actions.push({
|
||||
label: '重新申请',
|
||||
@@ -876,4 +1073,11 @@
|
||||
.refund-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resubmit-voucher-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -936,7 +936,8 @@
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
minWidth: 140
|
||||
showOverflowTooltip: true,
|
||||
minWidth: 160
|
||||
},
|
||||
{
|
||||
prop: 'allocator_shop_name',
|
||||
|
||||
499
src/views/package-management/traffic-alert-rules/index.vue
Normal file
499
src/views/package-management/traffic-alert-rules/index.vue
Normal file
@@ -0,0 +1,499 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="traffic-alert-rules-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
label-width="90"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #actions>
|
||||
<ElButton
|
||||
v-if="canCreate"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openFormDialog('create')"
|
||||
>
|
||||
新增规则
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="ruleList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="150"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<ElDialog
|
||||
v-model="formDialogVisible"
|
||||
:title="formMode === 'create' ? '新增真流量预警规则' : '编辑真流量预警规则'"
|
||||
width="560px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="120px">
|
||||
<ElFormItem label="套餐商品" prop="package_id">
|
||||
<ElSelect
|
||||
v-model="form.package_id"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchPackages"
|
||||
:loading="packageLoading"
|
||||
:disabled="formMode === 'edit'"
|
||||
placeholder="输入套餐名称搜索"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in packageOptions"
|
||||
:key="item.id"
|
||||
:label="item.package_name"
|
||||
:value="item.id"
|
||||
>
|
||||
<span>{{ item.package_name }}</span>
|
||||
<span class="package-option-extra">真流量 {{ formatMbToGb(item.real_data_mb) }}</span>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="预警阈值(%)" prop="threshold_percent">
|
||||
<ElInputNumber
|
||||
v-model="form.threshold_percent"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
:controls="false"
|
||||
placeholder="1~100,允许两位小数"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="是否启用" prop="enabled">
|
||||
<ElSwitch v-model="form.enabled" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入备注(最多 500 字符)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<ElAlert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="rule-tip-alert"
|
||||
title="同一套餐商品最多一条规则;修改不影响既有预警快照,停用后扫描不再创建新预警,既有预警保留。"
|
||||
/>
|
||||
<template #footer>
|
||||
<ElButton @click="formDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="submitting" @click="handleSubmit">确定</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag, FormInstance, FormRules } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { PackageManageService, PackageTrafficAlertService } from '@/api/modules'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type {
|
||||
PackageResponse,
|
||||
PackageTrafficAlertRuleItem,
|
||||
UpdatePackageTrafficAlertRuleParams
|
||||
} from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'TrafficAlertRules' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const perms = AUGUST_PERMISSIONS.packageTrafficAlert
|
||||
|
||||
const canCreate = computed(() => hasAuth(perms.ruleCreate))
|
||||
const canUpdate = computed(() => hasAuth(perms.ruleUpdate))
|
||||
|
||||
// ========== 列表数据 ==========
|
||||
|
||||
const loading = ref(false)
|
||||
const ruleList = ref<PackageTrafficAlertRuleItem[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const initialSearchState = {
|
||||
package_id: undefined as number | undefined,
|
||||
enabled: undefined as number | undefined
|
||||
}
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '停用', value: 0 }
|
||||
]
|
||||
|
||||
const packageOptions = ref<PackageResponse[]>([])
|
||||
const packageLoading = ref(false)
|
||||
|
||||
const searchPackages = async (query: string = '') => {
|
||||
packageLoading.value = true
|
||||
try {
|
||||
const res = await PackageManageService.getPackages({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
package_name: query || undefined
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
packageOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索套餐失败:', error)
|
||||
} finally {
|
||||
packageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '套餐',
|
||||
prop: 'package_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: searchPackages,
|
||||
loading: packageLoading.value
|
||||
},
|
||||
options: () =>
|
||||
packageOptions.value.map((item) => ({ label: item.package_name, value: item.id }))
|
||||
},
|
||||
{
|
||||
label: '启用状态',
|
||||
prop: 'enabled',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: () => statusOptions
|
||||
}
|
||||
])
|
||||
|
||||
const formatMbToGb = (mb?: number | null): string => {
|
||||
if (mb === undefined || mb === null || Number.isNaN(mb)) return '-'
|
||||
return `${(mb / 1024).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'package_name',
|
||||
label: '套餐名称',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'real_data_mb',
|
||||
label: '当前真流量额度',
|
||||
width: 150,
|
||||
formatter: (row: PackageTrafficAlertRuleItem) => formatMbToGb(row.real_data_mb)
|
||||
},
|
||||
{
|
||||
prop: 'threshold_percent',
|
||||
label: '预警阈值',
|
||||
width: 110,
|
||||
formatter: (row: PackageTrafficAlertRuleItem) =>
|
||||
row.threshold_percent === undefined || row.threshold_percent === null
|
||||
? '-'
|
||||
: `${row.threshold_percent}%`
|
||||
},
|
||||
{
|
||||
prop: 'enabled',
|
||||
label: '启用状态',
|
||||
width: 100,
|
||||
formatter: (row: PackageTrafficAlertRuleItem) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.enabled ? 'success' : 'info' },
|
||||
() => row.enabled_name || (row.enabled ? '启用' : '停用')
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'remark',
|
||||
label: '备注',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PackageTrafficAlertRuleItem) => row.remark || '-'
|
||||
},
|
||||
{
|
||||
prop: 'updated_at',
|
||||
label: '更新时间',
|
||||
minWidth: 170,
|
||||
formatter: (row: PackageTrafficAlertRuleItem) => formatDateTime(row.updated_at)
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '套餐名称', prop: 'package_name' },
|
||||
{ label: '当前真流量额度', prop: 'real_data_mb' },
|
||||
{ label: '预警阈值', prop: 'threshold_percent' },
|
||||
{ label: '启用状态', prop: 'enabled' },
|
||||
{ label: '备注', prop: 'remark' },
|
||||
{ label: '更新时间', prop: 'updated_at' }
|
||||
]
|
||||
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await PackageTrafficAlertService.getAlertRules({
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
package_id: searchForm.package_id ?? undefined,
|
||||
enabled:
|
||||
searchForm.enabled === undefined || searchForm.enabled === null
|
||||
? undefined
|
||||
: searchForm.enabled === 1
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
ruleList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取真流量预警规则列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取真流量预警规则列表失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '获取真流量预警规则列表失败')
|
||||
} 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 = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const refreshRow = (updated: PackageTrafficAlertRuleItem) => {
|
||||
const index = ruleList.value.findIndex((item) => item.id === updated.id)
|
||||
if (index >= 0) {
|
||||
ruleList.value.splice(index, 1, updated)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 行操作 ==========
|
||||
|
||||
const getActions = (row: PackageTrafficAlertRuleItem) => {
|
||||
const actions: Array<{
|
||||
label: string
|
||||
handler: () => void
|
||||
type?: 'primary' | 'danger'
|
||||
}> = []
|
||||
if (!canUpdate.value) return actions
|
||||
actions.push({ label: '编辑', handler: () => openFormDialog('edit', row) })
|
||||
if (row.enabled) {
|
||||
actions.push({
|
||||
label: '停用',
|
||||
handler: () => handleToggleEnabled(row, false),
|
||||
type: 'danger' as const
|
||||
})
|
||||
} else {
|
||||
actions.push({
|
||||
label: '启用',
|
||||
handler: () => handleToggleEnabled(row, true),
|
||||
type: 'primary' as const
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
const handleToggleEnabled = async (row: PackageTrafficAlertRuleItem, enabled: boolean) => {
|
||||
if (!enabled) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'停用后扫描不再创建新预警,既有预警保留。确认停用该规则吗?',
|
||||
'停用确认',
|
||||
{ type: 'warning', confirmButtonText: '确认停用', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await PackageTrafficAlertService.updateAlertRule(row.id, { enabled })
|
||||
if (res.code === 0 && res.data) {
|
||||
refreshRow(res.data)
|
||||
ElMessage.success(enabled ? '已启用' : '已停用,不再创建新预警')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新真流量预警规则失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 创建 / 编辑 ==========
|
||||
|
||||
const formDialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formMode = ref<'create' | 'edit'>('create')
|
||||
const formRef = ref<FormInstance>()
|
||||
const editingRuleId = ref<number | null>(null)
|
||||
|
||||
const emptyForm = () => ({
|
||||
package_id: undefined as number | undefined,
|
||||
threshold_percent: undefined as number | undefined,
|
||||
enabled: true,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const form = reactive(emptyForm())
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, emptyForm())
|
||||
editingRuleId.value = null
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const formRules: FormRules = {
|
||||
package_id: [{ required: true, message: '请选择套餐商品', trigger: 'change' }],
|
||||
threshold_percent: [
|
||||
{ required: true, message: '请输入预警阈值', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value: number | undefined, callback) => {
|
||||
if (value === undefined || value === null || Number.isNaN(Number(value))) {
|
||||
callback(new Error('请输入预警阈值'))
|
||||
} else if (Number(value) < 1 || Number(value) > 100) {
|
||||
callback(new Error('阈值取值 1 至 100'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const openFormDialog = (mode: 'create' | 'edit', row?: PackageTrafficAlertRuleItem) => {
|
||||
formMode.value = mode
|
||||
resetForm()
|
||||
if (mode === 'edit' && row) {
|
||||
editingRuleId.value = row.id
|
||||
form.package_id = row.package_id
|
||||
form.threshold_percent = row.threshold_percent
|
||||
form.enabled = row.enabled
|
||||
form.remark = row.remark || ''
|
||||
}
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitting.value = true
|
||||
try {
|
||||
let res
|
||||
if (formMode.value === 'create') {
|
||||
res = await PackageTrafficAlertService.createAlertRule({
|
||||
package_id: form.package_id as number,
|
||||
threshold_percent: form.threshold_percent as number,
|
||||
enabled: form.enabled,
|
||||
remark: form.remark || undefined
|
||||
})
|
||||
} else {
|
||||
const payload: UpdatePackageTrafficAlertRuleParams = {
|
||||
threshold_percent: form.threshold_percent as number,
|
||||
enabled: form.enabled,
|
||||
remark: form.remark
|
||||
}
|
||||
res = await PackageTrafficAlertService.updateAlertRule(
|
||||
editingRuleId.value as number,
|
||||
payload
|
||||
)
|
||||
}
|
||||
if (res.code === 0 && res.data) {
|
||||
ElMessage.success(formMode.value === 'create' ? '创建成功' : '修改成功')
|
||||
formDialogVisible.value = false
|
||||
if (formMode.value === 'create') {
|
||||
getTableData()
|
||||
} else {
|
||||
refreshRow(res.data)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存真流量预警规则失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.traffic-alert-rules-page {
|
||||
.package-option-extra {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.rule-tip-alert {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
196
src/views/package-management/traffic-alerts/detail.vue
Normal file
196
src/views/package-management/traffic-alerts/detail.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="traffic-alert-detail-page">
|
||||
<ElCard shadow="never">
|
||||
<div class="detail-header">
|
||||
<ElButton @click="handleBack">
|
||||
<template #icon>
|
||||
<ElIcon><ArrowLeft /></ElIcon>
|
||||
</template>
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">真流量达量预警详情</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-container">
|
||||
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
|
||||
<template v-else-if="detail">
|
||||
<ElAlert
|
||||
v-if="detail.shop_changed_since_trigger || detail.owner_changed_since_trigger"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="ownership-change-alert"
|
||||
>
|
||||
<template #title>{{ changeAlertText }}</template>
|
||||
</ElAlert>
|
||||
<DetailPage :sections="detailSections" :data="detail" />
|
||||
</template>
|
||||
|
||||
<ElResult
|
||||
v-else
|
||||
icon="warning"
|
||||
title="无法查看该预警记录"
|
||||
sub-title="预警记录不存在或您没有查看权限"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import { PackageTrafficAlertService } from '@/api/modules'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import {
|
||||
getTrafficAlertAssetTypeName,
|
||||
getTrafficAlertNotificationStatusOption
|
||||
} from '@/config/constants'
|
||||
import type { PackageTrafficAlertRecordDetail } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'TrafficAlertDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref<PackageTrafficAlertRecordDetail | null>(null)
|
||||
|
||||
const recordId = computed(() => Number(route.params.id))
|
||||
|
||||
const changeAlertText = computed(() => {
|
||||
if (!detail.value) return ''
|
||||
const parts: string[] = []
|
||||
if (detail.value.shop_changed_since_trigger) parts.push('触发后店铺归属已变化')
|
||||
if (detail.value.owner_changed_since_trigger) parts.push('触发后业务员归属已变化')
|
||||
return `${parts.join(',')},以下店铺/业务员信息为触发时快照。`
|
||||
})
|
||||
|
||||
const detailSections: DetailSection[] = [
|
||||
{
|
||||
title: '触发信息',
|
||||
fields: [
|
||||
{ label: '套餐', prop: 'package_name' },
|
||||
{
|
||||
label: '触发阈值',
|
||||
prop: 'threshold_percent',
|
||||
formatter: (value: unknown) => (value === undefined || value === null ? '-' : `${value}%`)
|
||||
},
|
||||
{
|
||||
label: '触发时间',
|
||||
prop: 'triggered_at',
|
||||
formatter: (value: unknown) => formatDateTime(value as string)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '触发时归属快照',
|
||||
fields: [
|
||||
{ label: '店铺', prop: 'shop_name' },
|
||||
{ label: '业务员', prop: 'business_owner_username' },
|
||||
{
|
||||
label: '业务用户组(当前)',
|
||||
prop: 'business_user_group_names',
|
||||
formatter: (value: unknown) =>
|
||||
Array.isArray(value) && value.length ? (value as string[]).join('、') : '-'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '资产信息',
|
||||
fields: [
|
||||
{
|
||||
label: '资产类型',
|
||||
prop: 'asset_type',
|
||||
formatter: (value: unknown) => getTrafficAlertAssetTypeName(value as never)
|
||||
},
|
||||
{ label: '资产标识', prop: 'asset_identifier' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '通知结果',
|
||||
fields: [
|
||||
{
|
||||
label: '通知状态',
|
||||
render: (data: PackageTrafficAlertRecordDetail) => {
|
||||
const option = getTrafficAlertNotificationStatusOption(data.notification_status)
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: option?.tagType || 'info' },
|
||||
() => data.notification_status_name || option?.label || '-'
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const fetchDetail = async () => {
|
||||
loading.value = true
|
||||
detail.value = null
|
||||
try {
|
||||
const res = await PackageTrafficAlertService.getAlertRecordDetail(recordId.value)
|
||||
if (res.code === 0 && res.data) {
|
||||
detail.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
const normalized = normalizeApiError(error)
|
||||
// 403/404 统一按资源不可见处理,不额外弹错
|
||||
if (normalized.status !== 403 && normalized.status !== 404) {
|
||||
ElMessage.error(normalized.message || '获取预警记录详情失败')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!Number.isFinite(recordId.value) || recordId.value <= 0) {
|
||||
return
|
||||
}
|
||||
fetchDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.traffic-alert-detail-page {
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.ownership-change-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
533
src/views/package-management/traffic-alerts/index.vue
Normal file
533
src/views/package-management/traffic-alerts/index.vue
Normal file
@@ -0,0 +1,533 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="traffic-alerts-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
label-width="90"
|
||||
show-expand
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #actions>
|
||||
<ElButton v-if="canExport" :icon="Download" @click="openExportDialog"> 导出 </ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="recordList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="100"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<ElDialog
|
||||
v-model="exportDialogVisible"
|
||||
title="导出真流量达量预警"
|
||||
width="40%"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<ElAlert type="info" :closable="false" show-icon class="export-rule-alert">
|
||||
<template #title>导出将基于当前筛选条件全量导出,不会仅导出当前分页数据。</template>
|
||||
</ElAlert>
|
||||
<ElForm label-width="80px" class="export-task-form">
|
||||
<ElFormItem label="导出格式">
|
||||
<ElRadioGroup v-model="exportFormat">
|
||||
<ElRadio
|
||||
v-for="item in TRAFFIC_ALERT_EXPORT_FORMAT_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="exportDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="exportSubmitting" @click="handleExportSubmit">
|
||||
创建导出任务
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, ElRadio, ElRadioGroup, ElTag } from 'element-plus'
|
||||
import { Download } from '@element-plus/icons-vue'
|
||||
import { PackageManageService, PackageTrafficAlertService, ShopService } from '@/api/modules'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import {
|
||||
AUGUST_PERMISSIONS,
|
||||
TRAFFIC_ALERT_ASSET_TYPE_OPTIONS,
|
||||
TRAFFIC_ALERT_EXPORT_FORMAT_OPTIONS,
|
||||
TRAFFIC_ALERT_NOTIFICATION_STATUS_OPTIONS,
|
||||
getTrafficAlertAssetTypeName,
|
||||
getTrafficAlertNotificationStatusOption
|
||||
} from '@/config/constants'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type {
|
||||
PackageResponse,
|
||||
PackageTrafficAlertAssetType,
|
||||
PackageTrafficAlertNotificationStatus,
|
||||
PackageTrafficAlertRecordItem,
|
||||
PackageTrafficAlertRecordQueryParams,
|
||||
ShopBusinessOwnerCandidate,
|
||||
ShopResponse
|
||||
} from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'TrafficAlerts' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const perms = AUGUST_PERMISSIONS.packageTrafficAlert
|
||||
|
||||
const canViewDetail = computed(() => hasAuth(perms.recordDetail))
|
||||
const canExport = computed(() => hasAuth(perms.export))
|
||||
|
||||
// ========== 列表数据 ==========
|
||||
|
||||
const loading = ref(false)
|
||||
const recordList = ref<PackageTrafficAlertRecordItem[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const initialSearchState = {
|
||||
package_id: undefined as number | undefined,
|
||||
shop_id: undefined as number | undefined,
|
||||
business_owner_account_id: undefined as number | undefined,
|
||||
asset_type: undefined as PackageTrafficAlertAssetType | undefined,
|
||||
asset_identifier: '',
|
||||
threshold_percent: '',
|
||||
dateRange: [] as string[],
|
||||
notification_status: undefined as PackageTrafficAlertNotificationStatus | undefined,
|
||||
start_time: '',
|
||||
end_time: ''
|
||||
}
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
// 筛选下拉选项
|
||||
const packageOptions = ref<PackageResponse[]>([])
|
||||
const packageLoading = ref(false)
|
||||
const shopOptions = ref<ShopResponse[]>([])
|
||||
const shopLoading = ref(false)
|
||||
const ownerOptions = ref<ShopBusinessOwnerCandidate[]>([])
|
||||
const ownerLoading = ref(false)
|
||||
|
||||
const searchPackages = async (query: string = '') => {
|
||||
packageLoading.value = true
|
||||
try {
|
||||
const res = await PackageManageService.getPackages({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
package_name: query || undefined
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
packageOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索套餐失败:', error)
|
||||
} finally {
|
||||
packageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchShops = async (query: string = '') => {
|
||||
shopLoading.value = true
|
||||
try {
|
||||
const res = await ShopService.getShops({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
shop_name: query || undefined
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
shopOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索店铺失败:', error)
|
||||
} finally {
|
||||
shopLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchOwners = async (query: string = '') => {
|
||||
ownerLoading.value = true
|
||||
try {
|
||||
const res = await ShopService.getBusinessOwnerCandidates({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
keyword: query || undefined
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
ownerOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索业务员失败:', error)
|
||||
} finally {
|
||||
ownerLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '套餐',
|
||||
prop: 'package_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: searchPackages,
|
||||
loading: packageLoading.value,
|
||||
placeholder: '请输入套餐名称搜索'
|
||||
},
|
||||
options: () =>
|
||||
packageOptions.value.map((item) => ({ label: item.package_name, value: item.id }))
|
||||
},
|
||||
{
|
||||
label: '店铺',
|
||||
prop: 'shop_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: searchShops,
|
||||
loading: shopLoading.value,
|
||||
placeholder: '请输入店铺名称搜索'
|
||||
},
|
||||
options: () => shopOptions.value.map((item) => ({ label: item.shop_name, value: item.id }))
|
||||
},
|
||||
{
|
||||
label: '业务员',
|
||||
prop: 'business_owner_account_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: searchOwners,
|
||||
loading: ownerLoading.value,
|
||||
placeholder: '请输入账号名或手机号搜索'
|
||||
},
|
||||
options: () => ownerOptions.value.map((item) => ({ label: item.username, value: item.id }))
|
||||
},
|
||||
{
|
||||
label: '资产类型',
|
||||
prop: 'asset_type',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: TRAFFIC_ALERT_ASSET_TYPE_OPTIONS
|
||||
},
|
||||
{
|
||||
label: '资产关键词',
|
||||
prop: 'asset_identifier',
|
||||
type: 'input',
|
||||
config: { clearable: true, placeholder: '资产标识/卡标识关键词' }
|
||||
},
|
||||
{
|
||||
label: '触发阈值(%)',
|
||||
prop: 'threshold_percent',
|
||||
type: 'input',
|
||||
config: { clearable: true, placeholder: '1~100,允许两位小数' }
|
||||
},
|
||||
{
|
||||
label: '触发时间',
|
||||
prop: 'dateRange',
|
||||
type: 'datetimerange',
|
||||
config: {
|
||||
type: 'datetimerange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '通知状态',
|
||||
prop: 'notification_status',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: TRAFFIC_ALERT_NOTIFICATION_STATUS_OPTIONS
|
||||
}
|
||||
])
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'package_name',
|
||||
label: '套餐名称',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PackageTrafficAlertRecordItem) => row.package_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PackageTrafficAlertRecordItem) => row.shop_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'business_owner_username',
|
||||
label: '业务员',
|
||||
width: 130,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PackageTrafficAlertRecordItem) => row.business_owner_username || '-'
|
||||
},
|
||||
{
|
||||
prop: 'business_user_group_names',
|
||||
label: '业务用户组',
|
||||
minWidth: 150,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PackageTrafficAlertRecordItem) =>
|
||||
row.business_user_group_names?.length ? row.business_user_group_names.join('、') : '-'
|
||||
},
|
||||
{
|
||||
prop: 'asset_type',
|
||||
label: '资产类型',
|
||||
width: 100,
|
||||
formatter: (row: PackageTrafficAlertRecordItem) =>
|
||||
getTrafficAlertAssetTypeName(row.asset_type)
|
||||
},
|
||||
{
|
||||
prop: 'asset_identifier',
|
||||
label: '资产标识',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PackageTrafficAlertRecordItem) => row.asset_identifier || '-'
|
||||
},
|
||||
{
|
||||
prop: 'threshold_percent',
|
||||
label: '触发阈值',
|
||||
width: 100,
|
||||
formatter: (row: PackageTrafficAlertRecordItem) =>
|
||||
row.threshold_percent === undefined || row.threshold_percent === null
|
||||
? '-'
|
||||
: `${row.threshold_percent}%`
|
||||
},
|
||||
{
|
||||
prop: 'triggered_at',
|
||||
label: '触发时间',
|
||||
width: 180,
|
||||
formatter: (row: PackageTrafficAlertRecordItem) => formatDateTime(row.triggered_at)
|
||||
},
|
||||
{
|
||||
prop: 'notification_status',
|
||||
label: '通知状态',
|
||||
width: 180,
|
||||
formatter: (row: PackageTrafficAlertRecordItem) => {
|
||||
const option = getTrafficAlertNotificationStatusOption(row.notification_status)
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: option?.tagType || 'info' },
|
||||
() => row.notification_status_name || option?.label || '-'
|
||||
)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '套餐名称', prop: 'package_name' },
|
||||
{ label: '店铺', prop: 'shop_name' },
|
||||
{ label: '业务员', prop: 'business_owner_username' },
|
||||
{ label: '业务用户组', prop: 'business_user_group_names' },
|
||||
{ label: '资产类型', prop: 'asset_type' },
|
||||
{ label: '资产标识', prop: 'asset_identifier' },
|
||||
{ label: '触发阈值', prop: 'threshold_percent' },
|
||||
{ label: '触发时间', prop: 'triggered_at' },
|
||||
{ label: '通知状态', prop: 'notification_status' }
|
||||
]
|
||||
|
||||
const buildQueryParams = (): PackageTrafficAlertRecordQueryParams => ({
|
||||
package_id: searchForm.package_id,
|
||||
shop_id: searchForm.shop_id,
|
||||
business_owner_account_id: searchForm.business_owner_account_id,
|
||||
asset_type: searchForm.asset_type,
|
||||
asset_identifier: searchForm.asset_identifier.trim() || undefined,
|
||||
threshold_percent: searchForm.threshold_percent
|
||||
? Number(searchForm.threshold_percent)
|
||||
: undefined,
|
||||
start_time: searchForm.start_time || undefined,
|
||||
end_time: searchForm.end_time || undefined,
|
||||
notification_status: searchForm.notification_status
|
||||
})
|
||||
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await PackageTrafficAlertService.getAlertRecords({
|
||||
...buildQueryParams(),
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
recordList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取真流量达量预警记录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取真流量达量预警记录失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '获取真流量达量预警记录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const validateThreshold = (): boolean => {
|
||||
const raw = searchForm.threshold_percent.trim()
|
||||
if (!raw) return true
|
||||
if (!/^\d+(\.\d{1,2})?$/.test(raw) || Number(raw) < 1 || Number(raw) > 100) {
|
||||
ElMessage.warning('触发阈值须为 1 至 100 之间的数字,允许两位小数')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const syncDateRange = () => {
|
||||
if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
|
||||
searchForm.start_time = searchForm.dateRange[0]
|
||||
searchForm.end_time = searchForm.dateRange[1]
|
||||
} else {
|
||||
searchForm.start_time = ''
|
||||
searchForm.end_time = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
if (!validateThreshold()) return
|
||||
syncDateRange()
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// ========== 行操作 ==========
|
||||
|
||||
const goDetail = (row: PackageTrafficAlertRecordItem) => {
|
||||
router.push(`${RoutesAlias.TrafficAlertDetail}/${row.id}`)
|
||||
}
|
||||
|
||||
const getActions = (row: PackageTrafficAlertRecordItem) => {
|
||||
if (!canViewDetail.value) return []
|
||||
return [
|
||||
{
|
||||
label: '详情',
|
||||
handler: () => goDetail(row),
|
||||
type: 'primary' as const
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// ========== 导出 ==========
|
||||
|
||||
const exportDialogVisible = ref(false)
|
||||
const exportSubmitting = ref(false)
|
||||
const exportFormat = ref<'xlsx' | 'csv'>('xlsx')
|
||||
|
||||
const openExportDialog = () => {
|
||||
if (!validateThreshold()) return
|
||||
exportFormat.value = 'xlsx'
|
||||
exportDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleExportSubmit = async () => {
|
||||
exportSubmitting.value = true
|
||||
try {
|
||||
syncDateRange()
|
||||
const res = await PackageTrafficAlertService.exportAlertRecords({
|
||||
format: exportFormat.value,
|
||||
...buildQueryParams()
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
exportDialogVisible.value = false
|
||||
if (res.data.task_id) {
|
||||
localStorage.setItem('export-task-active:package_traffic_alert', String(res.data.task_id))
|
||||
}
|
||||
ElMessageBox.alert(
|
||||
res.data.message || '导出任务已创建,请在导出任务列表中下载',
|
||||
'导出任务已创建',
|
||||
{
|
||||
confirmButtonText: '前往导出任务列表',
|
||||
type: 'success'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
router.push(RoutesAlias.ExportPackageTrafficAlertTaskList)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '创建导出任务失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建真流量达量预警导出任务失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '创建导出任务失败')
|
||||
} finally {
|
||||
exportSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
searchPackages('')
|
||||
searchShops('')
|
||||
searchOwners('')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.traffic-alerts-page {
|
||||
.export-rule-alert {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.export-task-form {
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
810
src/views/settings/h5-popup-configuration/index.vue
Normal file
810
src/views/settings/h5-popup-configuration/index.vue
Normal file
@@ -0,0 +1,810 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="h5-popup-configuration-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
label-width="110"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #actions>
|
||||
<ElButton v-if="canCreate" type="primary" :icon="Plus" @click="openCreateDialog">
|
||||
新增配置
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="configurationList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="200"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 创建 / 编辑 -->
|
||||
<ElDialog
|
||||
v-model="formDialogVisible"
|
||||
:title="formMode === 'create' ? '新增运营弹窗配置' : '编辑运营弹窗配置'"
|
||||
width="680px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="110px">
|
||||
<ElFormItem label="弹窗标题" prop="title">
|
||||
<ElInput
|
||||
v-model="form.title"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
placeholder="请输入弹窗标题(1~100 字符)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="弹窗正文" prop="content">
|
||||
<ElInput
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
placeholder="请输入弹窗正文(1~2000 字符,不接受 HTML、URL 或前端路由)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="命中页面" prop="pages">
|
||||
<ElCheckboxGroup v-model="form.pages">
|
||||
<ElCheckbox v-for="item in PAGE_OPTIONS" :key="item.value" :label="item.value">
|
||||
{{ item.label }}
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="投放频率" prop="frequency">
|
||||
<ElSelect v-model="form.frequency" placeholder="请选择投放频率" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="item in FREQUENCY_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="生效时间" prop="timeRange">
|
||||
<ElDatePicker
|
||||
v-model="form.timeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ssZ"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="优先级" prop="priority">
|
||||
<ElInputNumber
|
||||
v-model="form.priority"
|
||||
:min="0"
|
||||
:max="1000000"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="是否启用" prop="enabled">
|
||||
<ElSwitch v-model="form.enabled" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="受控动作" prop="action_type">
|
||||
<ElSelect v-model="form.action_type" placeholder="无" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="item in ACTION_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="店铺范围" prop="shop_ids">
|
||||
<ElSelect
|
||||
v-model="form.shop_ids"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchShops"
|
||||
:loading="shopLoading"
|
||||
placeholder="选择店铺(不选表示全量投放)"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="shop in shopOptions"
|
||||
:key="shop.id"
|
||||
:label="shop.shop_name"
|
||||
:value="shop.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="设备类型范围" prop="device_types">
|
||||
<ElSelect
|
||||
v-model="form.device_types"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入设备类型(不选表示全量投放)"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="卡类型范围" prop="card_types">
|
||||
<ElSelect
|
||||
v-model="form.card_types"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="选择卡类型(不选表示全量投放)"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in CARD_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="formDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="formLoading" @click="handleSubmit">
|
||||
{{ formMode === 'create' ? '创建' : '保存' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 详情 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="运营弹窗配置详情" width="680px">
|
||||
<div v-if="detailLoading" class="detail-loading">加载中...</div>
|
||||
<ElDescriptions v-else-if="detail" :column="2" border>
|
||||
<ElDescriptionsItem label="配置ID">{{ detail.id }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="版本">{{ detail.version }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="标题" :span="2">{{ detail.title }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="正文" :span="2">
|
||||
<span class="detail-content">{{ detail.content }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="命中页面" :span="2">
|
||||
{{ formatPages(detail.pages) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="店铺范围">
|
||||
{{ formatScope(detail.shop_ids) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备类型范围">
|
||||
{{ formatScope(detail.device_types) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卡类型范围">
|
||||
{{ formatCardTypes(detail.card_types) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="优先级">{{ detail.priority }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="投放频率">
|
||||
{{ detail.frequency_text || FREQUENCY_NAMES[detail.frequency] || detail.frequency }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="启停状态">
|
||||
{{ detail.enabled_text || (detail.enabled ? '启用' : '停用') }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="受控动作">
|
||||
{{ ACTION_TYPE_NAMES[detail.action_type] || '无' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="生效开始" :span="2">
|
||||
{{ formatDateTime(detail.starts_at) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="生效结束" :span="2">
|
||||
{{ formatDateTime(detail.ends_at) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建人">{{ detail.creator }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{
|
||||
formatDateTime(detail.created_at)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最近更新人">{{ detail.updater }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最近更新时间">{{
|
||||
formatDateTime(detail.updated_at)
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDialog>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag, FormInstance, FormRules } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { H5PopupConfigurationService, ShopService } from '@/api/modules'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type {
|
||||
H5PopupActionType,
|
||||
H5PopupCardType,
|
||||
H5PopupConfiguration,
|
||||
H5PopupFrequency,
|
||||
H5PopupPage
|
||||
} from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'H5PopupConfiguration' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const perms = AUGUST_PERMISSIONS.h5PopupConfiguration
|
||||
|
||||
const canCreate = computed(() => hasAuth(perms.create))
|
||||
const canUpdate = computed(() => hasAuth(perms.update))
|
||||
const canEnable = computed(() => hasAuth(perms.enable))
|
||||
const canDisable = computed(() => hasAuth(perms.disable))
|
||||
|
||||
const PAGE_NAMES: Record<H5PopupPage, string> = {
|
||||
home: '首页',
|
||||
asset_detail: '资产详情',
|
||||
package_purchase: '套餐购买',
|
||||
asset_wallet_recharge: '资产钱包充值'
|
||||
}
|
||||
const PAGE_OPTIONS: Array<{ label: string; value: H5PopupPage }> = [
|
||||
{ label: '首页', value: 'home' },
|
||||
{ label: '资产详情', value: 'asset_detail' },
|
||||
{ label: '套餐购买', value: 'package_purchase' },
|
||||
{ label: '资产钱包充值', value: 'asset_wallet_recharge' }
|
||||
]
|
||||
const FREQUENCY_NAMES: Record<H5PopupFrequency, string> = {
|
||||
once: '每客户每配置版本仅一次',
|
||||
daily: '每客户每配置版本每个上海自然日一次'
|
||||
}
|
||||
const FREQUENCY_OPTIONS: Array<{ label: string; value: H5PopupFrequency }> = [
|
||||
{ label: '仅一次(每配置版本)', value: 'once' },
|
||||
{ label: '每个上海自然日一次(每配置版本)', value: 'daily' }
|
||||
]
|
||||
const ACTION_TYPE_NAMES: Record<string, string> = {
|
||||
'': '无',
|
||||
package_purchase: '套餐购买',
|
||||
asset_wallet_recharge: '资产钱包充值'
|
||||
}
|
||||
const ACTION_TYPE_OPTIONS: Array<{ label: string; value: string }> = [
|
||||
{ label: '无', value: '' },
|
||||
{ label: '套餐购买', value: 'package_purchase' },
|
||||
{ label: '资产钱包充值', value: 'asset_wallet_recharge' }
|
||||
]
|
||||
const CARD_TYPE_NAMES: Record<H5PopupCardType, string> = {
|
||||
CMCC: '中国移动',
|
||||
CUCC: '中国联通',
|
||||
CTCC: '中国电信',
|
||||
CBN: '中国广电'
|
||||
}
|
||||
const CARD_TYPE_OPTIONS: Array<{ label: string; value: H5PopupCardType }> = [
|
||||
{ label: '中国移动', value: 'CMCC' },
|
||||
{ label: '中国联通', value: 'CUCC' },
|
||||
{ label: '中国电信', value: 'CTCC' },
|
||||
{ label: '中国广电', value: 'CBN' }
|
||||
]
|
||||
|
||||
const loading = ref(false)
|
||||
const configurationList = ref<H5PopupConfiguration[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const initialSearchState = {
|
||||
enabled: undefined as number | undefined
|
||||
}
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '停用', value: 0 }
|
||||
]
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '启停状态',
|
||||
prop: 'enabled',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: () => statusOptions
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '标题', prop: 'title' },
|
||||
{ label: '命中页面', prop: 'pages' },
|
||||
{ label: '范围', prop: 'scope' },
|
||||
{ label: '优先级', prop: 'priority' },
|
||||
{ label: '频率', prop: 'frequency_text' },
|
||||
{ label: '启停状态', prop: 'enabled' },
|
||||
{ label: '受控动作', prop: 'action_type' },
|
||||
{ label: '生效时间', prop: 'effective_time' },
|
||||
{ label: '版本', prop: 'version' },
|
||||
{ label: '创建人', prop: 'creator' },
|
||||
{ label: '最近更新人', prop: 'updater' },
|
||||
{ label: '最近更新时间', prop: 'updated_at' }
|
||||
]
|
||||
|
||||
const formatPages = (pages: H5PopupPage[] | null | undefined): string => {
|
||||
if (!pages || pages.length === 0) return '全部'
|
||||
return pages.map((page) => PAGE_NAMES[page] || page).join('、')
|
||||
}
|
||||
|
||||
const formatScope = (values: string[] | number[] | null | undefined): string => {
|
||||
if (!values || values.length === 0) return '全部'
|
||||
const text = values
|
||||
.slice(0, 3)
|
||||
.map((item) => (typeof item === 'number' && item !== 0 ? `#${item}` : String(item)))
|
||||
.join('、')
|
||||
return values.length > 3 ? `${text} 等${values.length}项` : text
|
||||
}
|
||||
|
||||
const formatCardTypes = (values: H5PopupCardType[] | null | undefined): string => {
|
||||
if (!values || values.length === 0) return '全部'
|
||||
return values.map((item) => CARD_TYPE_NAMES[item] || item).join('、')
|
||||
}
|
||||
|
||||
const formatScopeColumn = (row: H5PopupConfiguration): string => {
|
||||
const parts: string[] = []
|
||||
parts.push(`店铺:${row.shop_ids?.length ? `${row.shop_ids.length}家` : '全部'}`)
|
||||
parts.push(
|
||||
`设备:${
|
||||
row.device_types?.length
|
||||
? row.device_types.slice(0, 3).join('、') + (row.device_types.length > 3 ? ' 等' : '')
|
||||
: '全部'
|
||||
}`
|
||||
)
|
||||
parts.push(`卡型:${formatCardTypes(row.card_types)}`)
|
||||
return parts.join(';')
|
||||
}
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ prop: 'title', label: '标题', minWidth: 180, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'pages',
|
||||
label: '命中页面',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: H5PopupConfiguration) => formatPages(row.pages)
|
||||
},
|
||||
{
|
||||
prop: 'scope',
|
||||
label: '范围',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: H5PopupConfiguration) => formatScopeColumn(row)
|
||||
},
|
||||
{ prop: 'priority', label: '优先级', width: 90 },
|
||||
{
|
||||
prop: 'frequency_text',
|
||||
label: '频率',
|
||||
minWidth: 150,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: H5PopupConfiguration) =>
|
||||
row.frequency_text || FREQUENCY_NAMES[row.frequency] || row.frequency
|
||||
},
|
||||
{
|
||||
prop: 'enabled',
|
||||
label: '启停状态',
|
||||
width: 90,
|
||||
formatter: (row: H5PopupConfiguration) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.enabled ? 'success' : 'info' },
|
||||
() => row.enabled_text || (row.enabled ? '启用' : '停用')
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'action_type',
|
||||
label: '受控动作',
|
||||
width: 120,
|
||||
formatter: (row: H5PopupConfiguration) => ACTION_TYPE_NAMES[row.action_type] || '无'
|
||||
},
|
||||
{
|
||||
prop: 'effective_time',
|
||||
label: '生效时间',
|
||||
minWidth: 170,
|
||||
formatter: (row: H5PopupConfiguration) =>
|
||||
`${formatDateTime(row.starts_at)} ~ ${formatDateTime(row.ends_at)}`
|
||||
},
|
||||
{ prop: 'version', label: '版本', width: 70 },
|
||||
{
|
||||
prop: 'creator',
|
||||
label: '创建人',
|
||||
width: 90,
|
||||
formatter: (row: H5PopupConfiguration) => String(row.creator || 0)
|
||||
},
|
||||
{
|
||||
prop: 'updater',
|
||||
label: '最近更新人',
|
||||
width: 110,
|
||||
formatter: (row: H5PopupConfiguration) => String(row.updater || 0)
|
||||
},
|
||||
{
|
||||
prop: 'updated_at',
|
||||
label: '最近更新时间',
|
||||
minWidth: 170,
|
||||
formatter: (row: H5PopupConfiguration) => formatDateTime(row.updated_at)
|
||||
}
|
||||
])
|
||||
|
||||
// ========== 列表数据 ==========
|
||||
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await H5PopupConfigurationService.getConfigurations({
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
enabled:
|
||||
searchForm.enabled === undefined || searchForm.enabled === null
|
||||
? undefined
|
||||
: searchForm.enabled === 1
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
configurationList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取运营弹窗配置列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取运营弹窗配置列表失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '获取运营弹窗配置列表失败')
|
||||
} 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 = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const refreshRow = (updated: H5PopupConfiguration) => {
|
||||
const index = configurationList.value.findIndex((item) => item.id === updated.id)
|
||||
if (index >= 0) {
|
||||
configurationList.value.splice(index, 1, updated)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 行操作 ==========
|
||||
|
||||
const getActions = (row: H5PopupConfiguration) => {
|
||||
const actions: any[] = [
|
||||
{ label: '详情', handler: () => openDetailDialog(row), type: 'primary' }
|
||||
]
|
||||
if (canUpdate.value) {
|
||||
actions.push({ label: '编辑', handler: () => openEditDialog(row) })
|
||||
}
|
||||
if (canEnable.value && !row.enabled) {
|
||||
actions.push({ label: '启用', handler: () => handleEnable(row), type: 'success' })
|
||||
}
|
||||
if (canDisable.value && row.enabled) {
|
||||
actions.push({ label: '停用', handler: () => handleDisable(row), type: 'danger' })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
const handleEnable = async (row: H5PopupConfiguration) => {
|
||||
try {
|
||||
const res = await H5PopupConfigurationService.enableConfiguration(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
refreshRow(res.data)
|
||||
ElMessage.success('启用后参与候选匹配')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '启用失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启用运营弹窗配置失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '启用失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisable = async (row: H5PopupConfiguration) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'停用后停止新投放,历史通知在展示期内仍可见。确认停用该配置吗?',
|
||||
'停用确认',
|
||||
{ type: 'warning', confirmButtonText: '确认停用', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await H5PopupConfigurationService.disableConfiguration(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
refreshRow(res.data)
|
||||
ElMessage.success('停用后停止新投放,历史通知在展示期内仍可见')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '停用失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停用运营弹窗配置失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '停用失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 详情 ==========
|
||||
|
||||
const detailDialogVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref<H5PopupConfiguration | null>(null)
|
||||
|
||||
const openDetailDialog = async (row: H5PopupConfiguration) => {
|
||||
detail.value = null
|
||||
detailDialogVisible.value = true
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res = await H5PopupConfigurationService.getConfiguration(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
detail.value = res.data
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取配置详情失败')
|
||||
detailDialogVisible.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取运营弹窗配置详情失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '获取配置详情失败')
|
||||
detailDialogVisible.value = false
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 创建 / 编辑 ==========
|
||||
|
||||
const formDialogVisible = ref(false)
|
||||
const formLoading = ref(false)
|
||||
const formMode = ref<'create' | 'edit'>('create')
|
||||
const formRef = ref<FormInstance>()
|
||||
const shopOptions = ref<Array<{ id: number; shop_name: string }>>([])
|
||||
const shopLoading = ref(false)
|
||||
|
||||
const emptyForm = (): {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
pages: H5PopupPage[]
|
||||
priority: number
|
||||
frequency: H5PopupFrequency
|
||||
enabled: boolean
|
||||
action_type: H5PopupActionType
|
||||
shop_ids: number[]
|
||||
device_types: string[]
|
||||
card_types: H5PopupCardType[]
|
||||
timeRange: string[]
|
||||
} => ({
|
||||
id: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
pages: [],
|
||||
priority: 0,
|
||||
frequency: 'once',
|
||||
enabled: true,
|
||||
action_type: '',
|
||||
shop_ids: [],
|
||||
device_types: [],
|
||||
card_types: [],
|
||||
timeRange: []
|
||||
})
|
||||
|
||||
const form = reactive(emptyForm())
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, emptyForm())
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const toPickerTime = (iso: string): string => {
|
||||
if (!iso) return ''
|
||||
const date = new Date(iso)
|
||||
if (Number.isNaN(date.getTime())) return iso
|
||||
const pad = (value: number) => String(value).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(
|
||||
date.getHours()
|
||||
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}+08:00`
|
||||
}
|
||||
|
||||
const searchShops = async (query: string = '') => {
|
||||
shopLoading.value = true
|
||||
try {
|
||||
const res = await ShopService.getShops({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
shop_name: query || undefined
|
||||
})
|
||||
if (res.code === 0) {
|
||||
shopOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索店铺失败:', error)
|
||||
} finally {
|
||||
shopLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadShopNames = async (ids: number[]) => {
|
||||
const missing = ids.filter((id) => !shopOptions.value.some((shop) => shop.id === id))
|
||||
if (missing.length === 0) return
|
||||
await Promise.all(
|
||||
missing.slice(0, 50).map(async (id) => {
|
||||
try {
|
||||
const res = await ShopService.getShops({ id, page: 1, page_size: 1 })
|
||||
if (res.code === 0 && res.data.items?.length) {
|
||||
shopOptions.value = [...shopOptions.value, res.data.items[0]]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取店铺信息失败:', error)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const formRules: FormRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入弹窗标题', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '弹窗标题长度为 1~100 字符', trigger: 'blur' }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: '请输入弹窗正文', trigger: 'blur' },
|
||||
{ min: 1, max: 2000, message: '弹窗正文长度为 1~2000 字符', trigger: 'blur' }
|
||||
],
|
||||
pages: [
|
||||
{
|
||||
validator: (_rule, value: H5PopupPage[], callback) => {
|
||||
if (!value || value.length === 0) callback(new Error('请至少选择一个命中页面'))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
frequency: [{ required: true, message: '请选择投放频率', trigger: 'change' }],
|
||||
timeRange: [
|
||||
{
|
||||
validator: (_rule, value: string[] | null, callback) => {
|
||||
if (!value || value.length !== 2) {
|
||||
callback(new Error('请选择生效时间'))
|
||||
return
|
||||
}
|
||||
if (new Date(value[1]).getTime() < new Date(value[0]).getTime()) {
|
||||
callback(new Error('生效结束时间不得早于开始时间'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const openCreateDialog = () => {
|
||||
formMode.value = 'create'
|
||||
resetForm()
|
||||
formDialogVisible.value = true
|
||||
shopOptions.value = []
|
||||
searchShops('')
|
||||
}
|
||||
|
||||
const openEditDialog = async (row: H5PopupConfiguration) => {
|
||||
formMode.value = 'edit'
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
content: row.content,
|
||||
pages: [...(row.pages || [])],
|
||||
priority: row.priority,
|
||||
frequency: row.frequency,
|
||||
enabled: row.enabled,
|
||||
action_type: (row.action_type || '') as H5PopupActionType,
|
||||
shop_ids: [...(row.shop_ids || [])],
|
||||
device_types: [...(row.device_types || [])],
|
||||
card_types: [...(row.card_types || [])],
|
||||
timeRange: [toPickerTime(row.starts_at), toPickerTime(row.ends_at)]
|
||||
})
|
||||
formDialogVisible.value = true
|
||||
shopOptions.value = []
|
||||
await Promise.all([searchShops(''), loadShopNames(form.shop_ids)])
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!(await formRef.value?.validate().catch(() => false))) return
|
||||
formLoading.value = true
|
||||
const payload = {
|
||||
title: form.title.trim(),
|
||||
content: form.content.trim(),
|
||||
pages: [...form.pages],
|
||||
priority: form.priority,
|
||||
frequency: form.frequency,
|
||||
starts_at: form.timeRange[0],
|
||||
ends_at: form.timeRange[1],
|
||||
enabled: form.enabled,
|
||||
action_type: form.action_type,
|
||||
shop_ids: [...form.shop_ids],
|
||||
device_types: [...form.device_types],
|
||||
card_types: [...form.card_types]
|
||||
}
|
||||
try {
|
||||
if (formMode.value === 'create') {
|
||||
const res = await H5PopupConfigurationService.createConfiguration(payload)
|
||||
if (res.code === 0 && res.data) {
|
||||
ElMessage.success('创建成功')
|
||||
formDialogVisible.value = false
|
||||
getTableData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '创建失败')
|
||||
}
|
||||
} else {
|
||||
const res = await H5PopupConfigurationService.updateConfiguration(form.id, {
|
||||
id: form.id,
|
||||
...payload
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
ElMessage.success(
|
||||
'保存成功:每次更新版本递增,旧版本通知保留原快照,新版本可向原命中客户按频率重新投放一次'
|
||||
)
|
||||
formDialogVisible.value = false
|
||||
getTableData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '保存失败')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存运营弹窗配置失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message || '保存失败')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.h5-popup-configuration-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-loading {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@
|
||||
@refresh="loadMerchants"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton v-if="canManage" type="primary" :icon="Plus" @click="showCreateDrawer">
|
||||
<ElButton v-if="canCreateMerchant" type="primary" :icon="Plus" @click="showCreateDrawer">
|
||||
新增支付商户
|
||||
</ElButton>
|
||||
</template>
|
||||
@@ -31,7 +31,7 @@
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="160"
|
||||
:actionsWidth="120"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
@@ -48,7 +48,7 @@
|
||||
destroy-on-close
|
||||
@closed="clearSensitiveForm"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="120px">
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
<ElFormItem label="商户名称" prop="name">
|
||||
<ElInput v-model="form.name" maxlength="100" placeholder="请输入商户名称" />
|
||||
</ElFormItem>
|
||||
@@ -146,6 +146,32 @@
|
||||
</div>
|
||||
</div>
|
||||
<ElButton type="primary" plain :icon="Plus" @click="addCredential">添加凭证字段</ElButton>
|
||||
<template v-if="form.provider_type === 'wechat_v2'">
|
||||
<ElDivider content-position="left">退款专用可选凭证(原路退款)</ElDivider>
|
||||
<div class="refund-credential-fields">
|
||||
<ElFormItem label="客户端证书内容">
|
||||
<ElInput
|
||||
:model-value="refundCredentialValue('wx_client_cert_content')"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="选填,PEM 格式客户端证书内容(仅原路退款需要)"
|
||||
@update:model-value="setRefundCredentialValue('wx_client_cert_content', $event)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="客户端私钥内容">
|
||||
<ElInput
|
||||
:model-value="refundCredentialValue('wx_client_key_content')"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="选填,PEM 格式客户端私钥内容(仅原路退款需要)"
|
||||
@update:model-value="setRefundCredentialValue('wx_client_key_content', $event)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
<div v-if="refundCredentialIncomplete" class="credential-tip">
|
||||
提示:仅填写证书或私钥其一,该商户暂不可用于原路退款,不影响支付/查单/回调。
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="credentialError" class="field-error" role="alert">
|
||||
{{ credentialError }}
|
||||
</div>
|
||||
@@ -156,7 +182,7 @@
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
:disabled="!canManage"
|
||||
:disabled="formMode === 'create' ? !canCreateMerchant : !canEditMerchant"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
@@ -174,7 +200,8 @@
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { PAYMENT_MERCHANT_POOL_PERMISSIONS } from '@/config/constants/paymentMerchantPools'
|
||||
import type { SearchFormItem } from '@/types/component'
|
||||
import {
|
||||
PAYMENT_METHOD_OPTIONS,
|
||||
@@ -198,16 +225,24 @@
|
||||
|
||||
type FilterVo = string | number | undefined | null | unknown[]
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const router = useRouter()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const canCreateMerchant = computed(() =>
|
||||
hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantCreate)
|
||||
)
|
||||
const canEditMerchant = computed(() => hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantEdit))
|
||||
const canToggleMerchant = computed(() =>
|
||||
hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantToggle)
|
||||
)
|
||||
const canDeleteMerchant = computed(() =>
|
||||
hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantDelete)
|
||||
)
|
||||
const canViewMerchantDetail = computed(() =>
|
||||
hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.merchantDetail)
|
||||
)
|
||||
|
||||
const handleNameClick = (row: PaymentMerchant) => {
|
||||
if (!canManage.value) {
|
||||
ElMessage.warning('您没有查看支付商户详情的权限')
|
||||
return
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: `${RoutesAlias.PaymentMerchantPoolsDetail}/${row.id}`
|
||||
})
|
||||
@@ -262,17 +297,19 @@
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PaymentMerchant) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
},
|
||||
row.name
|
||||
)
|
||||
canViewMerchantDetail.value
|
||||
? h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
},
|
||||
row.name
|
||||
)
|
||||
: row.name
|
||||
},
|
||||
{
|
||||
label: '支付方式',
|
||||
@@ -322,7 +359,6 @@
|
||||
|
||||
// 列表加载
|
||||
const loadMerchants = async () => {
|
||||
if (!canManage.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params: PaymentMerchantQueryParams = {
|
||||
@@ -458,7 +494,7 @@
|
||||
}
|
||||
|
||||
const showCreateDrawer = () => {
|
||||
if (!canManage.value) return
|
||||
if (!canCreateMerchant.value) return
|
||||
Object.assign(form, initialFormState())
|
||||
credentialTemplateKeys.value = []
|
||||
formDrawerVisible.value = true
|
||||
@@ -468,7 +504,7 @@
|
||||
}
|
||||
|
||||
const startCredentialReplacement = () => {
|
||||
if (!canManage.value) return
|
||||
if (!canEditMerchant.value) return
|
||||
form.credentials = []
|
||||
if (credentialTemplateKeys.value.length > 0) {
|
||||
form.credentials = credentialTemplateKeys.value.map((key) => ({
|
||||
@@ -490,6 +526,33 @@
|
||||
form.credentials.splice(index, 1)
|
||||
}
|
||||
|
||||
const refundCredentialValue = (key: string): string => {
|
||||
return form.credentials.find((entry) => entry.key === key)?.value || ''
|
||||
}
|
||||
|
||||
const setRefundCredentialValue = (key: string, value: string) => {
|
||||
const index = form.credentials.findIndex((entry) => entry.key === key)
|
||||
const trimmed = value.trim()
|
||||
if (index >= 0) {
|
||||
if (trimmed) {
|
||||
form.credentials[index].value = trimmed
|
||||
} else {
|
||||
form.credentials.splice(index, 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (trimmed) {
|
||||
form.credentials.push({ localId: generateLocalId(), key, value: trimmed })
|
||||
}
|
||||
}
|
||||
|
||||
const refundCredentialIncomplete = computed(() => {
|
||||
if (form.provider_type !== 'wechat_v2') return false
|
||||
const cert = refundCredentialValue('wx_client_cert_content')
|
||||
const key = refundCredentialValue('wx_client_key_content')
|
||||
return Boolean(cert) !== Boolean(key)
|
||||
})
|
||||
|
||||
const handlePaymentMethodChange = (value: PaymentMerchantMethod) => {
|
||||
form.payment_method = value
|
||||
const firstMatch = PAYMENT_PROVIDER_OPTIONS.find((option) => option.paymentMethod === value)
|
||||
@@ -499,7 +562,7 @@
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canManage.value) return
|
||||
if (formMode.value === 'create' ? !canCreateMerchant.value : !canEditMerchant.value) return
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
@@ -532,6 +595,9 @@
|
||||
})
|
||||
ElMessage.success('支付凭证更新成功')
|
||||
}
|
||||
if (refundCredentialIncomplete.value) {
|
||||
ElMessage.warning('该商户退款凭证不完整,不可用于原路退款(不影响支付/查单/回调)')
|
||||
}
|
||||
formDrawerVisible.value = false
|
||||
clearSensitiveForm()
|
||||
await loadMerchants()
|
||||
@@ -565,7 +631,7 @@
|
||||
const tableRef = ref()
|
||||
|
||||
const openEditDrawer = async (id: number) => {
|
||||
if (!canManage.value) return
|
||||
if (!canEditMerchant.value) return
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantById(id)
|
||||
if (res.code !== 0) return
|
||||
@@ -589,7 +655,7 @@
|
||||
}
|
||||
|
||||
const toggleEnabled = async (merchant: PaymentMerchant) => {
|
||||
if (!canManage.value) return
|
||||
if (!canToggleMerchant.value) return
|
||||
const target = !merchant.enabled
|
||||
const action = target ? '启用' : '停用'
|
||||
try {
|
||||
@@ -613,7 +679,7 @@
|
||||
}
|
||||
|
||||
const confirmDeleteMerchant = async (merchant: PaymentMerchant) => {
|
||||
if (!canManage.value) return
|
||||
if (!canDeleteMerchant.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除支付商户 “${merchant.name}”?该操作不可恢复。`,
|
||||
@@ -642,26 +708,24 @@
|
||||
label: '编辑',
|
||||
type: 'primary' as const,
|
||||
handler: () => openEditDrawer(row.id),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
permission: canEditMerchant.value ? '' : 'hidden'
|
||||
},
|
||||
{
|
||||
label: row.enabled ? '停用' : '启用',
|
||||
type: 'primary' as const,
|
||||
handler: () => toggleEnabled(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
permission: canToggleMerchant.value ? '' : 'hidden'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'danger' as const,
|
||||
handler: () => confirmDeleteMerchant(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
permission: canDeleteMerchant.value ? '' : 'hidden'
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (canManage.value) {
|
||||
loadMerchants()
|
||||
}
|
||||
loadMerchants()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -700,6 +764,13 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.refund-credential-fields {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@refresh="loadPools"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton v-if="canManage" type="primary" :icon="Plus" @click="showCreateDrawer">
|
||||
<ElButton v-if="canCreatePool" type="primary" :icon="Plus" @click="showCreateDrawer">
|
||||
新增商户池
|
||||
</ElButton>
|
||||
</template>
|
||||
@@ -31,7 +31,7 @@
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="220"
|
||||
:actionsWidth="120"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
@@ -195,7 +195,7 @@
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
:disabled="!canManage"
|
||||
:disabled="formMode === 'create' ? !canCreatePool : !canEditPool"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
@@ -214,7 +214,8 @@
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { PAYMENT_MERCHANT_POOL_PERMISSIONS } from '@/config/constants/paymentMerchantPools'
|
||||
import type { SearchFormItem } from '@/types/component'
|
||||
import {
|
||||
PAYMENT_METHOD_OPTIONS,
|
||||
@@ -239,16 +240,15 @@
|
||||
|
||||
type FilterVo = string | number | undefined | null | unknown[]
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const router = useRouter()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const canCreatePool = computed(() => hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.poolCreate))
|
||||
const canEditPool = computed(() => hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.poolEdit))
|
||||
const canTogglePool = computed(() => hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.poolToggle))
|
||||
const canViewPoolDetail = computed(() => hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.poolDetail))
|
||||
|
||||
const handleNameClick = (row: PaymentMerchantPool) => {
|
||||
if (!canManage.value) {
|
||||
ElMessage.warning('您没有查看商户池详情的权限')
|
||||
return
|
||||
}
|
||||
|
||||
router.push({ path: `${RoutesAlias.PaymentMerchantPoolDetail}/${row.id}` })
|
||||
}
|
||||
|
||||
@@ -282,17 +282,19 @@
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: PaymentMerchantPool) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
},
|
||||
row.name
|
||||
)
|
||||
canViewPoolDetail.value
|
||||
? h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
},
|
||||
row.name
|
||||
)
|
||||
: row.name
|
||||
},
|
||||
{
|
||||
label: '支付方式',
|
||||
@@ -357,7 +359,6 @@
|
||||
}
|
||||
|
||||
const loadPools = async () => {
|
||||
if (!canManage.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
@@ -674,7 +675,7 @@
|
||||
}
|
||||
|
||||
const showCreateDrawer = async () => {
|
||||
if (!canManage.value) return
|
||||
if (!canCreatePool.value) return
|
||||
Object.assign(form, initialFormState())
|
||||
formDrawerVisible.value = true
|
||||
formMode.value = 'create'
|
||||
@@ -682,7 +683,7 @@
|
||||
}
|
||||
|
||||
const openEditDrawer = async (pool: PaymentMerchantPool) => {
|
||||
if (!canManage.value) return
|
||||
if (!canEditPool.value) return
|
||||
await loadAvailableMerchants(pool.payment_method)
|
||||
Object.assign(form, initialFormState(), {
|
||||
id: pool.id,
|
||||
@@ -711,7 +712,7 @@
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canManage.value) return
|
||||
if (formMode.value === 'create' ? !canCreatePool.value : !canEditPool.value) return
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
@@ -741,7 +742,7 @@
|
||||
const tableRef = ref()
|
||||
|
||||
const toggleEnabled = async (pool: PaymentMerchantPool) => {
|
||||
if (!canManage.value) return
|
||||
if (!canTogglePool.value) return
|
||||
const target = !pool.enabled
|
||||
const action = target ? '启用' : '停用'
|
||||
try {
|
||||
@@ -769,20 +770,18 @@
|
||||
label: '编辑',
|
||||
type: 'primary' as const,
|
||||
handler: () => openEditDrawer(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
permission: canEditPool.value ? '' : 'hidden'
|
||||
},
|
||||
{
|
||||
label: row.enabled ? '停用' : '启用',
|
||||
type: 'primary' as const,
|
||||
handler: () => toggleEnabled(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
permission: canTogglePool.value ? '' : 'hidden'
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (canManage.value) {
|
||||
loadPools()
|
||||
}
|
||||
loadPools()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="wechat-authorization-management">
|
||||
<ElCard v-if="canManage" shadow="never" class="art-table-card">
|
||||
<ElCard v-if="canEditWechatAuth" shadow="never" class="art-table-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">微信授权配置</span>
|
||||
@@ -93,7 +93,8 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { PAYMENT_MERCHANT_POOL_PERMISSIONS } from '@/config/constants/paymentMerchantPools'
|
||||
import type {
|
||||
UpdateWechatAuthorizationRequest,
|
||||
WechatAuthorizationConfig
|
||||
@@ -101,8 +102,10 @@
|
||||
|
||||
defineOptions({ name: 'WechatAuthorizationManagement' })
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
const { hasAuth } = useAuth()
|
||||
const canEditWechatAuth = computed(() =>
|
||||
hasAuth(PAYMENT_MERCHANT_POOL_PERMISSIONS.wechatAuthEdit)
|
||||
)
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitLoading = ref(false)
|
||||
@@ -139,7 +142,6 @@
|
||||
})
|
||||
|
||||
const loadConfig = async () => {
|
||||
if (!canManage.value) return
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getWechatAuthorization()
|
||||
if (res.code === 0) {
|
||||
@@ -151,7 +153,7 @@
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canManage.value) return
|
||||
if (!canEditWechatAuth.value) return
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
@@ -201,7 +203,7 @@
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (canManage.value) {
|
||||
if (canEditWechatAuth.value) {
|
||||
loadConfig()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -215,6 +215,16 @@
|
||||
label: '支付密钥',
|
||||
prop: 'wx_key_content',
|
||||
formatter: (value) => getPaymentConfigStatus(value)
|
||||
},
|
||||
{
|
||||
label: '客户端证书内容',
|
||||
prop: 'wx_client_cert_content',
|
||||
formatter: (value) => getPaymentConfigStatus(value)
|
||||
},
|
||||
{
|
||||
label: '客户端私钥内容',
|
||||
prop: 'wx_client_key_content',
|
||||
formatter: (value) => getPaymentConfigStatus(value)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -288,6 +288,28 @@
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="客户端证书内容" prop="wx_client_cert_content">
|
||||
<ElInput
|
||||
v-model="form.wx_client_cert_content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="选填,PEM 格式客户端证书内容,仅原路退款需要"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="客户端私钥内容" prop="wx_client_key_content">
|
||||
<ElInput
|
||||
v-model="form.wx_client_key_content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="选填,PEM 格式客户端私钥内容,仅原路退款需要"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<template v-if="form.provider_type === 'fuiou'">
|
||||
@@ -405,6 +427,8 @@
|
||||
wx_serial_no: string
|
||||
wx_cert_content: string
|
||||
wx_key_content: string
|
||||
wx_client_cert_content: string
|
||||
wx_client_key_content: string
|
||||
ali_app_id: string
|
||||
ali_private_key: string
|
||||
ali_public_key: string
|
||||
@@ -448,6 +472,8 @@
|
||||
'wx_api_v3_key',
|
||||
'wx_cert_content',
|
||||
'wx_key_content',
|
||||
'wx_client_cert_content',
|
||||
'wx_client_key_content',
|
||||
'ali_private_key',
|
||||
'ali_public_key',
|
||||
'fy_private_key',
|
||||
@@ -486,6 +512,8 @@
|
||||
wx_serial_no: '',
|
||||
wx_cert_content: '',
|
||||
wx_key_content: '',
|
||||
wx_client_cert_content: '',
|
||||
wx_client_key_content: '',
|
||||
ali_app_id: '',
|
||||
ali_private_key: '',
|
||||
ali_public_key: '',
|
||||
|
||||
@@ -65,6 +65,10 @@
|
||||
<ElOption label="退款审批" value="refund_approval" />
|
||||
<ElOption label="线下代充值审批" value="offline_recharge_approval" />
|
||||
<ElOption label="员工代收款审批" value="employee_collection_approval" />
|
||||
|
||||
<ElOption label="代理扫码分销注册审批" value="agent_distribution_approval" />
|
||||
<ElOption label="提现资料资格审批" value="withdrawal_qualification_approval" />
|
||||
<ElOption label="佣金提现终审" value="commission_withdrawal_approval" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
|
||||
666
src/views/shop-management/business-owner-import/index.vue
Normal file
666
src/views/shop-management/business-owner-import/index.vue
Normal file
@@ -0,0 +1,666 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="business-owner-import-page" id="table-full-screen">
|
||||
<!-- 导入入口卡片 -->
|
||||
<ElCard shadow="never" class="import-entry-card">
|
||||
<div class="import-entry">
|
||||
<div class="import-tip">
|
||||
<p>通过 CSV 批量交接/清空店铺平台业务员。</p>
|
||||
<p
|
||||
>表头固定:<code>店铺编码,操作类型,业务员登录账号,备注</code>;操作类型仅支持「换绑」或「清空」。</p
|
||||
>
|
||||
<p>支持 UTF-8(可带 BOM),一行失败不影响其他行。</p>
|
||||
</div>
|
||||
<div class="import-actions">
|
||||
<ElButton
|
||||
v-permission="SHOP_BUSINESS_OWNER_PERMISSIONS.importPage"
|
||||
@click="handleDownloadTemplate"
|
||||
>
|
||||
下载模板
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
v-permission="SHOP_BUSINESS_OWNER_PERMISSIONS.importCreate"
|
||||
@click="importDialogVisible = true"
|
||||
>
|
||||
导入业务员
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElSelect
|
||||
v-model="searchForm.status"
|
||||
placeholder="状态:全部"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<ElOption
|
||||
v-for="option in importStatusOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:actions="getActions"
|
||||
:actionsWidth="120"
|
||||
:marginTop="10"
|
||||
:height="400"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 新建导入弹窗 -->
|
||||
<ElDialog v-model="importDialogVisible" title="导入业务员" width="560px" destroy-on-close>
|
||||
<ElAlert type="info" :closable="false" show-icon class="import-alert">
|
||||
请先下载模板,按模板填写店铺编码与操作类型后再上传 CSV。
|
||||
</ElAlert>
|
||||
<div class="upload-tip-row">
|
||||
<ElButton @click="handleDownloadTemplate">下载模板</ElButton>
|
||||
</div>
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".csv"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
:file-list="fileList"
|
||||
>
|
||||
<el-icon class="upload-icon"><UploadFilled /></el-icon>
|
||||
<div class="upload-text">
|
||||
<div>将文件拖到此处,或 <em>点击选择 CSV</em></div>
|
||||
<div class="upload-tip">仅支持 .csv 文件(UTF-8 可带 BOM,自动兼容 GBK)</div>
|
||||
</div>
|
||||
</ElUpload>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="importDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="uploading"
|
||||
:disabled="!selectedFile"
|
||||
@click="handleStartImport"
|
||||
>
|
||||
开始导入
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 导入结果抽屉 -->
|
||||
<ElDrawer
|
||||
v-model="detailDrawerVisible"
|
||||
:title="`导入结果 - ${currentTask?.id || ''}`"
|
||||
size="70%"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="currentTask" class="import-detail">
|
||||
<ElAlert
|
||||
:type="detailAlertType"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="detailAlertTitle"
|
||||
class="detail-alert"
|
||||
>
|
||||
<template v-if="currentTask.error_message" #default>
|
||||
<div class="detail-error">{{ currentTask.error_message }}</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<div class="detail-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总数</span>
|
||||
<strong>{{ currentTask.total_count ?? 0 }}</strong>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">成功</span>
|
||||
<strong class="stat-success">{{ currentTask.success_count ?? 0 }}</strong>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">失败</span>
|
||||
<strong class="stat-fail">{{ currentTask.fail_count ?? 0 }}</strong>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">状态</span>
|
||||
<strong>{{ currentTask.status_name || importStatusLabel(currentTask.status) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-meta">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">任务编号</span>
|
||||
<span class="meta-value">{{ currentTask.task_no || '--' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">文件名</span>
|
||||
<span class="meta-value">{{ currentTask.file_name || '--' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">创建人</span>
|
||||
<span class="meta-value">{{ currentTask.creator_name || '--' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">开始时间</span>
|
||||
<span class="meta-value">
|
||||
{{ currentTask.started_at ? formatDateTime(currentTask.started_at) : '--' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">完成时间</span>
|
||||
<span class="meta-value">
|
||||
{{ currentTask.completed_at ? formatDateTime(currentTask.completed_at) : '--' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElTable v-if="rowItems.length" :data="rowItems" border stripe max-height="420">
|
||||
<ElTableColumn label="行号" prop="line" width="80" align="center" />
|
||||
<ElTableColumn label="店铺编码" prop="shop_code" min-width="140" />
|
||||
<ElTableColumn label="操作类型" prop="operation_type" width="100" align="center" />
|
||||
<ElTableColumn label="结果" width="110" align="center">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 3 ? 'success' : 'danger'" size="small">
|
||||
{{ scope.row.status_name || (scope.row.status === 3 ? '成功' : '失败') }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="失败原因" prop="reason" min-width="220" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
<ElEmpty
|
||||
v-else-if="!detailLoading && !rowItems.length && currentTask.status === 4"
|
||||
description="任务级失败,无行级明细"
|
||||
/>
|
||||
</div>
|
||||
</ElDrawer>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { UploadFile, UploadInstance, UploadUserFile } from 'element-plus'
|
||||
import { ShopService, StorageService } from '@/api/modules'
|
||||
import type { BusinessOwnerImportDetail } from '@/types/api'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { SHOP_BUSINESS_OWNER_PERMISSIONS } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'BusinessOwnerImport' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const importStatusOptions = [
|
||||
{ label: '待处理', value: 1 },
|
||||
{ label: '处理中', value: 2 },
|
||||
{ label: '已完成', value: 3 },
|
||||
{ label: '失败', value: 4 }
|
||||
]
|
||||
|
||||
const importStatusLabel = (status?: number) =>
|
||||
importStatusOptions.find((item) => item.value === status)?.label || '-'
|
||||
|
||||
// ==================== 任务列表 ====================
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const tableData = ref<BusinessOwnerImportDetail[]>([])
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const searchForm = reactive({
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '任务ID', prop: 'id', width: 90 },
|
||||
{ label: '任务编号', prop: 'task_no', width: 160 },
|
||||
{ label: '文件名', prop: 'file_name', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '创建人', prop: 'creator_name', width: 100 },
|
||||
{ label: '状态', prop: 'status', width: 100 },
|
||||
{ label: '总数', prop: 'total_count', width: 80 },
|
||||
{ label: '成功', prop: 'success_count', width: 80 },
|
||||
{ label: '失败', prop: 'fail_count', width: 80 },
|
||||
{ label: '开始时间', prop: 'started_at', width: 170 },
|
||||
{ label: '完成时间', prop: 'completed_at', width: 170 },
|
||||
{ label: '创建时间', prop: 'created_at', width: 170 }
|
||||
]
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: '任务ID',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
prop: 'task_no',
|
||||
label: '任务编号',
|
||||
width: 160,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'file_name',
|
||||
label: '文件名',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'creator_name',
|
||||
label: '创建人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: BusinessOwnerImportDetail) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: `color: ${row.status === 4 ? 'var(--el-color-danger)' : 'var(--el-color-primary)'}`
|
||||
},
|
||||
row.status_name || importStatusLabel(row.status)
|
||||
)
|
||||
},
|
||||
{ prop: 'total_count', label: '总数', width: 80 },
|
||||
{ prop: 'success_count', label: '成功', width: 80 },
|
||||
{ prop: 'fail_count', label: '失败', width: 80 },
|
||||
{
|
||||
prop: 'started_at',
|
||||
label: '开始时间',
|
||||
width: 170,
|
||||
formatter: (row: BusinessOwnerImportDetail) =>
|
||||
row.started_at ? formatDateTime(row.started_at) : '--'
|
||||
},
|
||||
{
|
||||
prop: 'completed_at',
|
||||
label: '完成时间',
|
||||
width: 170,
|
||||
formatter: (row: BusinessOwnerImportDetail) =>
|
||||
row.completed_at ? formatDateTime(row.completed_at) : '--'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 170,
|
||||
formatter: (row: BusinessOwnerImportDetail) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
|
||||
const getActions = (row: BusinessOwnerImportDetail) => {
|
||||
if (!hasAuth(SHOP_BUSINESS_OWNER_PERMISSIONS.importPage)) return []
|
||||
return [
|
||||
{
|
||||
label: '查看结果',
|
||||
handler: () => openDetail(row),
|
||||
type: 'primary' as const
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await ShopService.getBusinessOwnerImportList({
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
status: searchForm.status
|
||||
})
|
||||
tableData.value = res.data?.items || []
|
||||
pagination.total = res.data?.total || 0
|
||||
} catch {
|
||||
// 错误提示由请求层统一处理
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
pagination.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getList()
|
||||
}
|
||||
|
||||
// ==================== 模板下载 ====================
|
||||
const handleDownloadTemplate = () => {
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = new URL('@/template/业务负责人导入模板.csv', import.meta.url).href
|
||||
link.download = '业务负责人导入模板.csv'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
ElMessage.success('模板下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载模板失败:', error)
|
||||
ElMessage.error('下载模板失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 新建导入 ====================
|
||||
const importDialogVisible = ref(false)
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const fileList = ref<UploadUserFile[]>([])
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const uploading = ref(false)
|
||||
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
selectedFile.value = file.raw || null
|
||||
}
|
||||
|
||||
const handleFileRemove = () => {
|
||||
selectedFile.value = null
|
||||
}
|
||||
|
||||
const handleStartImport = async () => {
|
||||
if (!selectedFile.value) {
|
||||
ElMessage.warning('请先选择 CSV 文件')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
// 1. 获取预签名上传地址
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: selectedFile.value.name,
|
||||
content_type: 'text/csv',
|
||||
purpose: 'shop_import'
|
||||
})
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
// 2. 预签名 PUT 直传
|
||||
await StorageService.uploadFile(upload_url, selectedFile.value, 'text/csv')
|
||||
|
||||
// 3. 创建导入任务
|
||||
const createRes = await ShopService.createBusinessOwnerImport({ file_key })
|
||||
const taskId = createRes.data?.id
|
||||
importDialogVisible.value = false
|
||||
selectedFile.value = null
|
||||
fileList.value = []
|
||||
|
||||
if (taskId) {
|
||||
await openDetailById(taskId)
|
||||
} else {
|
||||
getList()
|
||||
}
|
||||
ElMessage.success('导入任务已创建')
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error)
|
||||
ElMessage.error('导入失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 结果抽屉 ====================
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const currentTask = ref<BusinessOwnerImportDetail | null>(null)
|
||||
const rowItems = ref<NonNullable<BusinessOwnerImportDetail['items']>>([])
|
||||
|
||||
let pollTimer: number | undefined
|
||||
|
||||
const isTaskRunning = (status?: number) => status === 1 || status === 2
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollTimer !== undefined) {
|
||||
window.clearTimeout(pollTimer)
|
||||
pollTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const detailAlertType = computed(() => {
|
||||
const status = currentTask.value?.status
|
||||
if (status === 4) return 'error'
|
||||
if (status === 3) return (currentTask.value?.fail_count ?? 0) > 0 ? 'warning' : 'success'
|
||||
return 'info'
|
||||
})
|
||||
|
||||
const detailAlertTitle = computed(() => {
|
||||
const status = currentTask.value?.status
|
||||
if (status === 4) return `任务失败:${currentTask.value?.error_message || '未知原因'}`
|
||||
if (status === 3) {
|
||||
const failed = currentTask.value?.fail_count ?? 0
|
||||
return failed > 0
|
||||
? `任务完成,但 ${failed} 行失败(失败行保留原负责人)`
|
||||
: '任务完成,全部成功'
|
||||
}
|
||||
return '任务处理中,请稍候…'
|
||||
})
|
||||
|
||||
const refreshDetail = async () => {
|
||||
if (!currentTask.value) return
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res = await ShopService.getBusinessOwnerImportDetail(currentTask.value.id)
|
||||
currentTask.value = res.data
|
||||
rowItems.value = res.data.items || []
|
||||
if (isTaskRunning(res.data.status)) {
|
||||
schedulePoll()
|
||||
}
|
||||
} catch {
|
||||
// 轮询失败保留当前状态
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const schedulePoll = () => {
|
||||
stopPolling()
|
||||
if (!currentTask.value || !isTaskRunning(currentTask.value.status)) return
|
||||
pollTimer = window.setTimeout(() => {
|
||||
pollTimer = undefined
|
||||
void refreshDetail()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const openDetail = (row: BusinessOwnerImportDetail) => {
|
||||
currentTask.value = row
|
||||
rowItems.value = row.items || []
|
||||
detailDrawerVisible.value = true
|
||||
if (isTaskRunning(row.status)) {
|
||||
schedulePoll()
|
||||
}
|
||||
}
|
||||
|
||||
const openDetailById = async (taskId: number) => {
|
||||
currentTask.value = null
|
||||
rowItems.value = []
|
||||
detailDrawerVisible.value = true
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res = await ShopService.getBusinessOwnerImportDetail(taskId)
|
||||
currentTask.value = res.data
|
||||
rowItems.value = res.data.items || []
|
||||
if (isTaskRunning(res.data.status)) {
|
||||
schedulePoll()
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('获取导入结果失败')
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
getList()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.import-entry-card {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.import-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.import-tip {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.8;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.import-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.import-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.upload-tip-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48px;
|
||||
color: var(--el-color-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.import-detail {
|
||||
.detail-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-error {
|
||||
margin-top: 6px;
|
||||
color: var(--el-color-danger);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.stat-success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.stat-fail {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 12px 24px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
|
||||
.meta-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
630
src/views/shop-management/business-user-groups/index.vue
Normal file
630
src/views/shop-management/business-user-groups/index.vue
Normal file
@@ -0,0 +1,630 @@
|
||||
<template>
|
||||
<div class="business-user-groups-page">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
type="primary"
|
||||
v-permission="BUSINESS_USER_GROUP_PERMISSIONS.create"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
新建用户组
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:actions="getActions"
|
||||
:actionsWidth="120"
|
||||
:marginTop="10"
|
||||
:height="500"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 新建/编辑弹窗 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新建用户组' : '编辑用户组'"
|
||||
width="620px"
|
||||
destroy-on-close
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<ElFormItem label="稳定编码" prop="code">
|
||||
<ElInput
|
||||
v-model="form.code"
|
||||
:disabled="dialogType === 'edit'"
|
||||
placeholder="1-64 字符,创建后不可修改"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="请输入名称" maxlength="100" show-word-limit />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="业务线" prop="business_line">
|
||||
<ElSelect v-model="form.business_line" placeholder="不设置" clearable style="width: 100%">
|
||||
<ElOption
|
||||
v-for="option in businessLineOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="form.sort" :min="0" :precision="0" style="width: 200px" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="启用" prop="enabled">
|
||||
<ElSwitch v-model="form.enabled" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="最多 500 字符"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="submitting" @click="handleSubmit">保存</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 成员管理弹窗 -->
|
||||
<ElDialog
|
||||
v-model="memberDialogVisible"
|
||||
:title="`成员管理 - ${currentGroup?.name || ''}`"
|
||||
width="680px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="currentGroup" class="member-group-info">
|
||||
<ElTag :type="currentGroup.enabled ? 'success' : 'danger'" size="small">
|
||||
{{ currentGroup.enabled ? '启用' : '已停用' }}
|
||||
</ElTag>
|
||||
<span class="member-group-code">{{ currentGroup.code }}</span>
|
||||
<span v-if="!currentGroup.enabled" class="member-group-tip">
|
||||
停用组保留现有成员,不能作为批量目标
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ElDivider content-position="left">设置成员(整体替换)</ElDivider>
|
||||
<ElAlert type="warning" :closable="false" show-icon class="member-alert">
|
||||
保存后将以所选账号整体替换该组现有成员;账号必须是启用平台用户。
|
||||
</ElAlert>
|
||||
<ElSelect
|
||||
v-model="memberAccountIds"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchMemberCandidates"
|
||||
:loading="memberCandidatesLoading"
|
||||
placeholder="搜索并选择平台账号"
|
||||
style="width: 100%; margin-top: 12px"
|
||||
>
|
||||
<ElOption
|
||||
v-for="candidate in memberCandidates"
|
||||
:key="candidate.id"
|
||||
:label="candidate.username"
|
||||
:value="candidate.id"
|
||||
>
|
||||
<div class="member-option">
|
||||
<span>{{ candidate.username }}</span>
|
||||
<span class="member-option-phone">{{ candidate.phone_summary }}</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div class="member-actions">
|
||||
<ElButton type="primary" :loading="settingMembers" @click="handleSetMembers">
|
||||
保存成员
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElDivider content-position="left">移出成员(回未分组)</ElDivider>
|
||||
<ElAlert type="info" :closable="false" show-icon class="member-alert">
|
||||
选择要移出用户组的平台账号,清空后回未分组。
|
||||
</ElAlert>
|
||||
<ElSelect
|
||||
v-model="clearAccountIds"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchMemberCandidates"
|
||||
:loading="memberCandidatesLoading"
|
||||
placeholder="搜索并选择要移出的平台账号"
|
||||
style="width: 100%; margin-top: 12px"
|
||||
>
|
||||
<ElOption
|
||||
v-for="candidate in memberCandidates"
|
||||
:key="candidate.id"
|
||||
:label="candidate.username"
|
||||
:value="candidate.id"
|
||||
>
|
||||
<div class="member-option">
|
||||
<span>{{ candidate.username }}</span>
|
||||
<span class="member-option-phone">{{ candidate.phone_summary }}</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div class="member-actions">
|
||||
<ElButton type="danger" :loading="clearingMembers" @click="handleClearMembers">
|
||||
移出所选账号
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { BusinessUserGroupService, ShopService } from '@/api/modules'
|
||||
import type {
|
||||
BusinessUserGroupBusinessLine,
|
||||
BusinessUserGroupItem,
|
||||
CreateBusinessUserGroupParams,
|
||||
UpdateBusinessUserGroupParams,
|
||||
ShopBusinessOwnerCandidate
|
||||
} from '@/types/api'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
BUSINESS_USER_GROUP_BUSINESS_LINE_MAP,
|
||||
BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS,
|
||||
BUSINESS_USER_GROUP_PERMISSIONS
|
||||
} from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'BusinessUserGroups' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const businessLineOptions = BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS
|
||||
|
||||
// ==================== 列表 ====================
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const tableData = ref<BusinessUserGroupItem[]>([])
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
enabled: undefined as number | undefined,
|
||||
business_line: undefined as BusinessUserGroupBusinessLine | undefined
|
||||
})
|
||||
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '名称',
|
||||
prop: 'keyword',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '按稳定编码或名称模糊搜索'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'enabled',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '停用', value: 0 }
|
||||
],
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '业务线',
|
||||
prop: 'business_line',
|
||||
type: 'select',
|
||||
options: businessLineOptions.map((item) => ({ label: item.label, value: item.value })),
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '稳定编码', prop: 'code', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '名称', prop: 'name', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '业务线', prop: 'business_line', width: 110 },
|
||||
{ label: '状态', prop: 'enabled', width: 90 },
|
||||
{ label: '排序', prop: 'sort', width: 80 },
|
||||
{ label: '备注', prop: 'remark', minWidth: 160, showOverflowTooltip: true },
|
||||
{ label: '创建时间', prop: 'created_at', width: 170 }
|
||||
]
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'code',
|
||||
label: '稳定编码',
|
||||
minWidth: 140,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'name',
|
||||
label: '名称',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'business_line',
|
||||
label: '业务线',
|
||||
width: 110,
|
||||
formatter: (row: BusinessUserGroupItem) => {
|
||||
if (!row.business_line) return '-'
|
||||
const config =
|
||||
BUSINESS_USER_GROUP_BUSINESS_LINE_MAP[row.business_line as BusinessUserGroupBusinessLine]
|
||||
return config ? config.label : row.business_line
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'enabled',
|
||||
label: '状态',
|
||||
width: 90,
|
||||
formatter: (row: BusinessUserGroupItem) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: `color: ${row.enabled ? 'var(--el-color-success)' : 'var(--el-color-danger)'}` },
|
||||
row.enabled ? '启用' : '已停用'
|
||||
)
|
||||
},
|
||||
{ prop: 'sort', label: '排序', width: 80 },
|
||||
{ prop: 'remark', label: '备注', minWidth: 160, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 170,
|
||||
formatter: (row: BusinessUserGroupItem) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
|
||||
const getActions = (row: BusinessUserGroupItem) => {
|
||||
const actions: Array<{ label: string; handler: () => void; type?: 'primary' | 'danger' }> = []
|
||||
if (hasAuth(BUSINESS_USER_GROUP_PERMISSIONS.update)) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
handler: () => openEditDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
if (hasAuth(BUSINESS_USER_GROUP_PERMISSIONS.members)) {
|
||||
actions.push({
|
||||
label: '成员',
|
||||
handler: () => openMemberDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
if (hasAuth(BUSINESS_USER_GROUP_PERMISSIONS.delete)) {
|
||||
actions.push({
|
||||
label: '删除',
|
||||
handler: () => handleDelete(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await BusinessUserGroupService.getGroups({
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
enabled: searchForm.enabled === undefined ? undefined : searchForm.enabled === 1,
|
||||
keyword: searchForm.keyword.trim() || undefined,
|
||||
business_line: searchForm.business_line
|
||||
})
|
||||
tableData.value = res.data?.items || []
|
||||
pagination.total = res.data?.total || 0
|
||||
} catch {
|
||||
// 错误提示由请求层统一处理
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
pagination.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
pagination.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getList()
|
||||
}
|
||||
|
||||
// ==================== 新建/编辑 ====================
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const submitting = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const currentGroup = ref<BusinessUserGroupItem | null>(null)
|
||||
|
||||
const form = reactive<CreateBusinessUserGroupParams>({
|
||||
code: '',
|
||||
name: '',
|
||||
business_line: undefined,
|
||||
sort: 0,
|
||||
enabled: true,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = computed<FormRules>(() => ({
|
||||
code:
|
||||
dialogType.value === 'add'
|
||||
? [
|
||||
{ required: true, message: '请输入稳定编码', trigger: 'blur' },
|
||||
{ min: 1, max: 64, message: '长度 1-64 字符', trigger: 'blur' }
|
||||
]
|
||||
: [],
|
||||
name: [
|
||||
{ required: true, message: '请输入名称', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度 1-100 字符', trigger: 'blur' }
|
||||
]
|
||||
}))
|
||||
|
||||
const resetForm = () => {
|
||||
form.code = ''
|
||||
form.name = ''
|
||||
form.business_line = undefined
|
||||
form.sort = 0
|
||||
form.enabled = true
|
||||
form.remark = ''
|
||||
}
|
||||
|
||||
const openCreateDialog = () => {
|
||||
dialogType.value = 'add'
|
||||
currentGroup.value = null
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEditDialog = (row: BusinessUserGroupItem) => {
|
||||
dialogType.value = 'edit'
|
||||
currentGroup.value = row
|
||||
form.code = row.code
|
||||
form.name = row.name
|
||||
form.business_line = (row.business_line || undefined) as
|
||||
| BusinessUserGroupBusinessLine
|
||||
| undefined
|
||||
form.sort = row.sort
|
||||
form.enabled = row.enabled
|
||||
form.remark = row.remark
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const payload: CreateBusinessUserGroupParams | UpdateBusinessUserGroupParams = {
|
||||
name: form.name,
|
||||
// 三态语义:清空选择器后回退为 ''(清空),枚举值(设置),其余字段缺失=不改
|
||||
business_line: form.business_line ?? '',
|
||||
sort: form.sort,
|
||||
enabled: form.enabled,
|
||||
remark: form.remark
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
await BusinessUserGroupService.createGroup({
|
||||
...(payload as CreateBusinessUserGroupParams),
|
||||
code: form.code
|
||||
})
|
||||
ElMessage.success('创建成功')
|
||||
} else {
|
||||
await BusinessUserGroupService.updateGroup(currentGroup.value!.id, payload)
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getList()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 删除 ====================
|
||||
const handleDelete = async (row: BusinessUserGroupItem) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除用户组「${row.name}」吗?仅无成员的用户组可删除。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确认删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await BusinessUserGroupService.deleteGroup(row.id, { confirm: true })
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
} catch {
|
||||
// 有成员等场景由后端提示
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 成员管理 ====================
|
||||
const memberDialogVisible = ref(false)
|
||||
const memberCandidates = ref<ShopBusinessOwnerCandidate[]>([])
|
||||
const memberCandidatesLoading = ref(false)
|
||||
const memberAccountIds = ref<number[]>([])
|
||||
const clearAccountIds = ref<number[]>([])
|
||||
const settingMembers = ref(false)
|
||||
const clearingMembers = ref(false)
|
||||
|
||||
const searchMemberCandidates = async (keyword?: string) => {
|
||||
memberCandidatesLoading.value = true
|
||||
try {
|
||||
const res = await ShopService.getBusinessOwnerCandidates({
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
keyword: keyword?.trim() || undefined
|
||||
})
|
||||
memberCandidates.value = res.data.items || []
|
||||
} catch {
|
||||
memberCandidates.value = []
|
||||
} finally {
|
||||
memberCandidatesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openMemberDialog = (row: BusinessUserGroupItem) => {
|
||||
currentGroup.value = row
|
||||
memberAccountIds.value = []
|
||||
clearAccountIds.value = []
|
||||
memberCandidates.value = []
|
||||
memberDialogVisible.value = true
|
||||
setTimeout(() => {
|
||||
searchMemberCandidates()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const handleSetMembers = async () => {
|
||||
if (!currentGroup.value) return
|
||||
if (!memberAccountIds.value.length) {
|
||||
ElMessage.warning('请至少选择一个平台账号')
|
||||
return
|
||||
}
|
||||
settingMembers.value = true
|
||||
try {
|
||||
const res = await BusinessUserGroupService.setGroupMembers(currentGroup.value.id, {
|
||||
account_ids: memberAccountIds.value
|
||||
})
|
||||
ElMessage.success(`已维护 ${res.data?.account_ids?.length ?? 0} 个账号归属`)
|
||||
memberDialogVisible.value = false
|
||||
getList()
|
||||
} finally {
|
||||
settingMembers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearMembers = async () => {
|
||||
if (!clearAccountIds.value.length) {
|
||||
ElMessage.warning('请至少选择一个平台账号')
|
||||
return
|
||||
}
|
||||
clearingMembers.value = true
|
||||
try {
|
||||
const res = await BusinessUserGroupService.clearGroupMembers({
|
||||
account_ids: clearAccountIds.value
|
||||
})
|
||||
ElMessage.success(`已清空 ${res.data?.account_ids?.length ?? 0} 个账号归属`)
|
||||
memberDialogVisible.value = false
|
||||
getList()
|
||||
} finally {
|
||||
clearingMembers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.member-group-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.member-group-code {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.member-group-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.member-alert {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.member-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.member-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.member-option-phone {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,6 +15,9 @@
|
||||
<ElDescriptions v-if="shop" :column="2" border>
|
||||
<ElDescriptionsItem label="店铺名称">{{ shop.shop_name || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="店铺编号">{{ shop.shop_code || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="分销码">{{
|
||||
shop.distribution_code || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="店铺层级">{{ shop.level }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="上级店铺">{{
|
||||
shop.parent_shop_name || '-'
|
||||
@@ -33,6 +36,12 @@
|
||||
: '-'
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="业务用户组">
|
||||
{{ formatBusinessUserGroup(shop) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="业务用户组业务线">
|
||||
{{ getBusinessUserGroupBusinessLineLabel(shop.business_user_group_business_line) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="省份">{{ shop.province || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="城市">{{ shop.city || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="区县">{{ shop.district || '-' }}</ElDescriptionsItem>
|
||||
@@ -43,18 +52,30 @@
|
||||
<ElDescriptionsItem label="创建时间">{{ shop.created_at || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{ shop.updated_at || '-' }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<div v-if="shop?.distribution_code" class="register-qrcode">
|
||||
<div class="qrcode-box">
|
||||
<QrcodeVue :value="registerUrl" :size="168" level="H" />
|
||||
</div>
|
||||
<div class="qrcode-tip">
|
||||
<span>扫码注册:将该二维码提供给下级代理扫码注册,或直接分享下方链接。</span>
|
||||
<code>{{ registerUrl }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import { ShopService } from '@/api/modules'
|
||||
import type { ShopResponse } from '@/types/api'
|
||||
import { getBusinessUserGroupBusinessLineLabel } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'ShopDetail' })
|
||||
|
||||
@@ -63,6 +84,14 @@
|
||||
const loading = ref(false)
|
||||
const shop = ref<ShopResponse | null>(null)
|
||||
|
||||
const registerUrl = computed(() => {
|
||||
const shopData = shop.value
|
||||
const code = shopData?.distribution_code
|
||||
if (!shopData || !code) return ''
|
||||
const base = `${window.location.origin}${window.location.pathname}`
|
||||
return `${base}#/agent-registration?distribution_code=${encodeURIComponent(code)}&shop_name=${encodeURIComponent(shopData.shop_name)}&shop_code=${encodeURIComponent(shopData.shop_code)}`
|
||||
})
|
||||
|
||||
const formatBusinessOwner = (value: ShopResponse) => {
|
||||
if (!value.business_owner_username) return '-'
|
||||
return value.business_owner_phone_summary
|
||||
@@ -70,6 +99,14 @@
|
||||
: value.business_owner_username
|
||||
}
|
||||
|
||||
const formatBusinessUserGroup = (value: ShopResponse) => {
|
||||
if (!value.business_user_group_id) return '-'
|
||||
const text = value.business_user_group_code
|
||||
? `${value.business_user_group_name || '-'}(${value.business_user_group_code})`
|
||||
: value.business_user_group_name || '-'
|
||||
return value.business_user_group_enabled ? text : `${text}(已停用)`
|
||||
}
|
||||
|
||||
const handleBack = () => router.back()
|
||||
|
||||
const fetchDetail = async () => {
|
||||
@@ -115,4 +152,41 @@
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.register-qrcode {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
padding: 24px;
|
||||
margin-top: 20px;
|
||||
background: var(--el-fill-color-blank);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
|
||||
.qrcode-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.qrcode-tip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
code {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,19 @@
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')" v-permission="'shop:add'">新增店铺</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
v-permission="SHOP_BUSINESS_OWNER_PERMISSIONS.batch"
|
||||
@click="openBatchTransferDialog"
|
||||
>
|
||||
批量交接
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="SHOP_BUSINESS_OWNER_PERMISSIONS.importPage"
|
||||
@click="goToImportPage"
|
||||
>
|
||||
导入业务员
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -40,6 +53,7 @@
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-if="canBatchTransfer" type="selection" width="55" />
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
@@ -310,6 +324,60 @@
|
||||
:shop="currentCreditShop"
|
||||
@submitted="getShopList"
|
||||
/>
|
||||
|
||||
<!-- 批量交接平台业务员弹窗 -->
|
||||
<ElDialog
|
||||
v-model="batchTransferDialogVisible"
|
||||
title="批量交接平台业务员"
|
||||
width="560px"
|
||||
destroy-on-close
|
||||
>
|
||||
<ElAlert type="info" :closable="false" show-icon class="batch-transfer-alert">
|
||||
已选择 {{ selectedRows.length }} 家店铺(单次最多 500 家)
|
||||
</ElAlert>
|
||||
<ElForm label-width="110px" class="batch-transfer-form">
|
||||
<ElFormItem label="交接方式">
|
||||
<ElRadioGroup v-model="batchTransferMode">
|
||||
<ElRadio label="rebind">换绑业务员</ElRadio>
|
||||
<ElRadio label="clear">清空负责人</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="batchTransferMode === 'rebind'" label="目标业务员">
|
||||
<ElSelect
|
||||
v-model="batchTransferAccountId"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:loading="batchOwnerLoading"
|
||||
:remote-method="searchBatchOwners"
|
||||
placeholder="请选择或搜索平台业务员"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="owner in batchOwnerOptions"
|
||||
:key="owner.id"
|
||||
:label="owner.username"
|
||||
:value="owner.id"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ owner.username }}</span>
|
||||
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
{{ owner.phone_summary }}
|
||||
</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="batchTransferDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="batchSubmitting" @click="handleBatchTransfer">
|
||||
确认交接
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -334,10 +402,17 @@
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { generateShopCode } from '@/utils/codeGenerator'
|
||||
import { CommissionService, ShopService, RoleService } from '@/api/modules'
|
||||
import {
|
||||
BusinessUserGroupService,
|
||||
CommissionService,
|
||||
RoleService,
|
||||
ShopService
|
||||
} from '@/api/modules'
|
||||
import ShopCreditLimitDialog from '@/components/business/ShopCreditLimitDialog.vue'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type {
|
||||
BatchUpdateBusinessOwnerParams,
|
||||
BusinessUserGroupItem,
|
||||
CreateShopParams,
|
||||
ShopBusinessOwnerCandidate,
|
||||
ShopResponse,
|
||||
@@ -348,7 +423,14 @@
|
||||
import { RoleType, RoleStatus } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getCompatibleNumericId } from '@/utils/business/id'
|
||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||
import {
|
||||
BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS,
|
||||
CommonStatus,
|
||||
getBusinessUserGroupBusinessLineLabel,
|
||||
getStatusText,
|
||||
SHOP_BUSINESS_OWNER_PERMISSIONS,
|
||||
STATUS_SELECT_OPTIONS
|
||||
} from '@/config/constants'
|
||||
import { regionData } from '@/utils/constants/regionData'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||
@@ -484,7 +566,10 @@
|
||||
parent_id: undefined as number | undefined,
|
||||
level: undefined as number | undefined,
|
||||
status: undefined as number | undefined,
|
||||
business_owner_account_id: undefined as number | undefined
|
||||
business_owner_account_id: undefined as number | undefined,
|
||||
business_user_group_id: undefined as number | undefined,
|
||||
business_line: undefined as string | undefined,
|
||||
ungrouped: undefined as number | undefined
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
@@ -505,6 +590,22 @@
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 业务用户组筛选选项(非代理账号可见)
|
||||
const groupOptions = ref<BusinessUserGroupItem[]>([])
|
||||
const groupLoading = ref(false)
|
||||
|
||||
// 批量交接平台业务员
|
||||
const batchTransferDialogVisible = ref(false)
|
||||
const batchTransferMode = ref<'rebind' | 'clear'>('rebind')
|
||||
const batchTransferAccountId = ref<number | null>(null)
|
||||
const batchOwnerOptions = ref<ShopBusinessOwnerCandidate[]>([])
|
||||
const batchOwnerLoading = ref(false)
|
||||
const batchSubmitting = ref(false)
|
||||
|
||||
const canBatchTransfer = computed(
|
||||
() => !isAgentAccount.value && hasAuth(SHOP_BUSINESS_OWNER_PERMISSIONS.batch)
|
||||
)
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
@@ -602,6 +703,50 @@
|
||||
label: `${salesperson.username} (${salesperson.phone_summary})`,
|
||||
value: salesperson.id
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '业务用户组',
|
||||
prop: 'business_user_group_id',
|
||||
type: 'select' as const,
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
loading: groupLoading.value,
|
||||
placeholder: '请选择业务用户组'
|
||||
},
|
||||
options: () =>
|
||||
groupOptions.value.map((group) => ({
|
||||
label: group.business_line_name
|
||||
? `${group.name}(${group.business_line_name})`
|
||||
: group.name,
|
||||
value: group.id
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '业务线',
|
||||
prop: 'business_line',
|
||||
type: 'select' as const,
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择业务线'
|
||||
},
|
||||
options: BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '未分组',
|
||||
prop: 'ungrouped',
|
||||
type: 'select' as const,
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '是=无负责人或负责人无分组'
|
||||
},
|
||||
options: [
|
||||
{ label: '是', value: 1 },
|
||||
{ label: '否', value: 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
: [])
|
||||
@@ -616,6 +761,7 @@
|
||||
{ label: '联系人', prop: 'contact_name' },
|
||||
{ label: '联系电话', prop: 'contact_phone' },
|
||||
{ label: '平台业务员', prop: 'business_owner_username' },
|
||||
{ label: '业务用户组', prop: 'business_user_group_name' },
|
||||
...(canModifyShopStatus ? [{ label: '状态', prop: 'status' }] : []),
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
@@ -776,6 +922,23 @@
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ShopResponse) => formatBusinessOwner(row)
|
||||
},
|
||||
{
|
||||
prop: 'business_user_group_name',
|
||||
label: '业务用户组',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ShopResponse) => {
|
||||
if (!row.business_user_group_id) return '-'
|
||||
const name = row.business_user_group_name || row.business_user_group_code || '-'
|
||||
const businessLine = getBusinessUserGroupBusinessLineLabel(
|
||||
row.business_user_group_business_line
|
||||
)
|
||||
const text =
|
||||
businessLine && businessLine !== '-' ? `${name}(${businessLine})` : name
|
||||
if (row.business_user_group_enabled) return text
|
||||
return h(ElTag, { type: 'danger', size: 'small' }, { default: () => `${text}(已停用)` })
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'client_login_disabled',
|
||||
label: 'C 端登录',
|
||||
@@ -1020,6 +1183,7 @@
|
||||
searchParentShops('') // 加载上级店铺选项
|
||||
if (!isAgentAccount.value) {
|
||||
searchPlatformSalespeople('') // 加载启用的平台业务员选项
|
||||
loadGroupOptions() // 加载业务用户组选项
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1038,7 +1202,11 @@
|
||||
level: searchForm.level,
|
||||
status: searchForm.status,
|
||||
...(!isAgentAccount.value && {
|
||||
business_owner_account_id: searchForm.business_owner_account_id
|
||||
business_owner_account_id: searchForm.business_owner_account_id,
|
||||
business_user_group_id: searchForm.business_user_group_id,
|
||||
business_line: searchForm.business_line || undefined,
|
||||
ungrouped:
|
||||
searchForm.ungrouped === undefined ? undefined : searchForm.ungrouped === 1
|
||||
})
|
||||
}
|
||||
const res = await ShopService.getShops(params)
|
||||
@@ -1062,6 +1230,89 @@
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 打开批量交接弹窗
|
||||
const openBatchTransferDialog = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先勾选需要交接的店铺')
|
||||
return
|
||||
}
|
||||
if (selectedRows.value.length > 500) {
|
||||
ElMessage.warning('单次最多可交接 500 家店铺')
|
||||
return
|
||||
}
|
||||
batchTransferMode.value = 'rebind'
|
||||
batchTransferAccountId.value = null
|
||||
void searchBatchOwners('')
|
||||
batchTransferDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 搜索批量交接目标业务员
|
||||
const searchBatchOwners = async (query: string) => {
|
||||
batchOwnerLoading.value = true
|
||||
try {
|
||||
const res = await ShopService.getBusinessOwnerCandidates({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
keyword: query || undefined
|
||||
})
|
||||
if (res.code === 0) {
|
||||
batchOwnerOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取平台业务员列表失败:', error)
|
||||
batchOwnerOptions.value = []
|
||||
} finally {
|
||||
batchOwnerLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交批量交接
|
||||
const handleBatchTransfer = async () => {
|
||||
if (batchTransferMode.value === 'rebind' && !batchTransferAccountId.value) {
|
||||
ElMessage.warning('请选择目标业务员')
|
||||
return
|
||||
}
|
||||
batchSubmitting.value = true
|
||||
try {
|
||||
const data: BatchUpdateBusinessOwnerParams = {
|
||||
shop_ids: Array.from(new Set(selectedRows.value.map((row: ShopResponse) => row.id))),
|
||||
business_owner_account_id:
|
||||
batchTransferMode.value === 'clear' ? null : batchTransferAccountId.value
|
||||
}
|
||||
const res = await ShopService.batchUpdateBusinessOwner(data)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(`批量交接成功,共处理 ${res.data?.shop_count ?? 0} 家店铺`)
|
||||
batchTransferDialogVisible.value = false
|
||||
selectedRows.value = []
|
||||
await getShopList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量交接失败:', error)
|
||||
} finally {
|
||||
batchSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到业务负责人导入页
|
||||
const goToImportPage = () => {
|
||||
router.push(RoutesAlias.BusinessOwnerImport)
|
||||
}
|
||||
|
||||
// 加载业务用户组选项(非代理账号)
|
||||
const loadGroupOptions = async () => {
|
||||
groupLoading.value = true
|
||||
try {
|
||||
const res = await BusinessUserGroupService.getGroups({ page: 1, page_size: 100 })
|
||||
if (res.code === 0) {
|
||||
groupOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载业务用户组失败:', error)
|
||||
} finally {
|
||||
groupLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
shop_name: [
|
||||
|
||||
Reference in New Issue
Block a user