Merge branch 'develop'
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
<ChunkErrorBoundary>
|
||||
<ElConfigProvider size="default" :locale="locales[language]" :z-index="3000">
|
||||
<RouterView></RouterView>
|
||||
<AuditInvestigationHost />
|
||||
</ElConfigProvider>
|
||||
</ChunkErrorBoundary>
|
||||
</template>
|
||||
@@ -17,6 +18,7 @@
|
||||
import { checkStorageCompatibility } from '@/utils'
|
||||
import ChunkErrorBoundary from '@/components/core/others/ChunkErrorBoundary.vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import AuditInvestigationHost from '@/components/business/audit/AuditInvestigationHost.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { language } = storeToRefs(userStore)
|
||||
@@ -74,7 +76,7 @@
|
||||
}
|
||||
|
||||
const getEntrySignature = (html: string) => {
|
||||
const matches = html.match(/(?:src|href)="[^"]*\/assets\/[^""]+\.(?:js|css)"/g)
|
||||
const matches = html.match(/(?:src|href)="[^"]*\/assets\/[^"]+\.(?:js|css)"/g)
|
||||
return matches?.sort().join('|') || ''
|
||||
}
|
||||
|
||||
@@ -104,7 +106,6 @@
|
||||
[
|
||||
navigator.userAgent,
|
||||
navigator.language,
|
||||
navigator.platform,
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
`${screen.width}x${screen.height}x${screen.colorDepth}`
|
||||
].join('|')
|
||||
|
||||
@@ -8,10 +8,14 @@ import type {
|
||||
AccountQueryParams,
|
||||
CreatePlatformAccountParams,
|
||||
BaseResponse,
|
||||
PaginationResponse
|
||||
PaginationResponse,
|
||||
WecomAccountBindingRequest
|
||||
} from '@/types/api'
|
||||
|
||||
export class AccountService extends BaseService {
|
||||
static bindWecom(id: number, data: WecomAccountBindingRequest): Promise<BaseResponse> {
|
||||
return this.put<BaseResponse>(`/api/admin/accounts/${id}/wecom-binding`, data)
|
||||
}
|
||||
// ========== 账号管理 (Account Management) ==========
|
||||
|
||||
/**
|
||||
@@ -30,7 +34,7 @@ export class AccountService extends BaseService {
|
||||
*/
|
||||
static createAccount(
|
||||
data: CreatePlatformAccountParams
|
||||
): Promise<BaseResponse<{ ID?: number; id?: number }>> {
|
||||
): Promise<BaseResponse<{ id?: number; ID?: number }>> {
|
||||
return this.create('/api/admin/accounts', data)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,23 @@ import type {
|
||||
AgentRechargeQueryParams,
|
||||
AgentRechargeListResponse,
|
||||
CreateAgentRechargeRequest,
|
||||
AgentRechargePaymentMethods,
|
||||
AgentRechargePaymentStatusResponse,
|
||||
ConfirmOfflinePaymentRequest,
|
||||
RejectAgentRechargeRequest,
|
||||
BaseResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class AgentRechargeService extends BaseService {
|
||||
/**
|
||||
* 获取代理在线充值可用支付方式和金额限制
|
||||
*/
|
||||
static getPaymentMethods(): Promise<BaseResponse<AgentRechargePaymentMethods>> {
|
||||
return this.get<BaseResponse<AgentRechargePaymentMethods>>(
|
||||
'/api/admin/agent-recharges/payment-methods'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理充值订单列表
|
||||
* @param params 查询参数
|
||||
@@ -32,6 +43,15 @@ export class AgentRechargeService extends BaseService {
|
||||
return this.getOne<AgentRecharge>(`/api/admin/agent-recharges/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询在线充值支付及钱包到账状态
|
||||
*/
|
||||
static getPaymentStatus(id: number): Promise<BaseResponse<AgentRechargePaymentStatusResponse>> {
|
||||
return this.getOne<AgentRechargePaymentStatusResponse>(
|
||||
`/api/admin/agent-recharges/${id}/payment-status`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建代理充值订单
|
||||
* @param data 创建充值订单请求参数
|
||||
@@ -68,4 +88,14 @@ export class AgentRechargeService extends BaseService {
|
||||
): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/agent-recharges/${id}/reject`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 补发历史线下代理充值审批
|
||||
* @param id 充值记录ID
|
||||
*/
|
||||
static triggerApproval(id: number): Promise<BaseResponse<AgentRecharge>> {
|
||||
return this.post<BaseResponse<AgentRecharge>>(
|
||||
`/api/admin/agent-recharges/${id}/trigger-approval`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
AssetPackageParams,
|
||||
AssetCurrentPackageResponse,
|
||||
DeviceStopResponse,
|
||||
AssetStartResponse,
|
||||
AssetWalletTransactionListResponse,
|
||||
AssetWalletTransactionParams,
|
||||
AssetWalletResponse,
|
||||
@@ -26,25 +27,33 @@ import type {
|
||||
AssetOrdersResponse,
|
||||
UpdateAssetRealnameStatusRequest,
|
||||
DtoUpdateAssetRealnameStatusResponse,
|
||||
AssetOperationLogsResponse,
|
||||
AssetOperationLogsParams,
|
||||
AssetPackageUsageRecord,
|
||||
UpdateAssetPackageUsedDataRequest,
|
||||
UpdateAssetPackageExpiresAtRequest
|
||||
UpdateAssetPackageExpiresAtRequest,
|
||||
ExpiringAssetListResponse,
|
||||
ExpiringAssetQueryParams
|
||||
} from '@/types/api'
|
||||
|
||||
const runRateLimitedAssetAction = async <T>(
|
||||
action: AssetRateLimitedAction,
|
||||
action: Exclude<AssetRateLimitedAction, 'refresh'>,
|
||||
identifier: string,
|
||||
request: () => Promise<T>
|
||||
): Promise<T> => {
|
||||
assertAssetActionAllowed(action, identifier)
|
||||
markAssetActionCalled(action, identifier)
|
||||
const response = await request()
|
||||
return response
|
||||
return request()
|
||||
}
|
||||
|
||||
export class AssetService extends BaseService {
|
||||
/**
|
||||
* 获取管理端临期资产列表
|
||||
* GET /api/admin/expiring-assets
|
||||
*/
|
||||
static getExpiringAssets(
|
||||
params?: ExpiringAssetQueryParams
|
||||
): Promise<BaseResponse<ExpiringAssetListResponse>> {
|
||||
return this.get<BaseResponse<ExpiringAssetListResponse>>('/api/admin/expiring-assets', params)
|
||||
}
|
||||
/**
|
||||
* 通过任意标识符查询设备或卡的完整详情
|
||||
* 支持虚拟号、ICCID、IMEI、SN、MSISDN
|
||||
@@ -54,9 +63,14 @@ export class AssetService extends BaseService {
|
||||
*/
|
||||
static resolveAsset(
|
||||
identifier: string,
|
||||
params?: AssetResolveParams
|
||||
params?: AssetResolveParams,
|
||||
config?: Record<string, any>
|
||||
): Promise<BaseResponse<AssetResolveResponse>> {
|
||||
return this.getOne<AssetResolveResponse>(`/api/admin/assets/resolve/${identifier}`, params)
|
||||
return this.getOne<AssetResolveResponse>(
|
||||
`/api/admin/assets/resolve/${identifier}`,
|
||||
params,
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,16 +93,13 @@ export class AssetService extends BaseService {
|
||||
/**
|
||||
* 主动调网关拉取最新数据后返回
|
||||
* POST /api/admin/assets/:identifier/refresh
|
||||
* 前端按资产标识限制 5 分钟内只能调用一次
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
*/
|
||||
static refreshAsset(identifier: string): Promise<BaseResponse<AssetRefreshResponse>> {
|
||||
return runRateLimitedAssetAction('refresh', identifier, () =>
|
||||
this.post<BaseResponse<AssetRefreshResponse>>(
|
||||
`/api/admin/assets/${identifier}/refresh`,
|
||||
{},
|
||||
{ timeout: 60000 }
|
||||
)
|
||||
return this.post<BaseResponse<AssetRefreshResponse>>(
|
||||
`/api/admin/assets/${identifier}/refresh`,
|
||||
{},
|
||||
{ timeout: 60000 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -181,9 +192,16 @@ export class AssetService extends BaseService {
|
||||
* 前端按资产标识限制 5 分钟内只能调用一次
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
*/
|
||||
static startAsset(identifier: string): Promise<BaseResponse<void>> {
|
||||
static startAsset(
|
||||
identifier: string,
|
||||
config?: Record<string, any>
|
||||
): Promise<BaseResponse<AssetStartResponse>> {
|
||||
return runRateLimitedAssetAction('start', identifier, () =>
|
||||
this.post<BaseResponse<void>>(`/api/admin/assets/${identifier}/start`, {})
|
||||
this.post<BaseResponse<AssetStartResponse>>(
|
||||
`/api/admin/assets/${identifier}/start`,
|
||||
{},
|
||||
config
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -291,22 +309,4 @@ export class AssetService extends BaseService {
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
// ========== 资产操作审计日志 ==========
|
||||
|
||||
/**
|
||||
* 查询资产操作审计日志
|
||||
* GET /api/admin/assets/:identifier/operation-logs
|
||||
* @param identifier 资产标识符
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getOperationLogs(
|
||||
identifier: string,
|
||||
params?: AssetOperationLogsParams
|
||||
): Promise<BaseResponse<AssetOperationLogsResponse>> {
|
||||
return this.get<BaseResponse<AssetOperationLogsResponse>>(
|
||||
`/api/admin/assets/${identifier}/operation-logs`,
|
||||
params
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
155
src/api/modules/audit.ts
Normal file
155
src/api/modules/audit.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { BaseService } from '../BaseService'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { isPlatformAuditAccount } from '@/utils/business/auditAccess'
|
||||
import type {
|
||||
AuditActorEventQuery,
|
||||
AuditActorKind,
|
||||
AuditEventDetail,
|
||||
AuditEventPage,
|
||||
AuditEventQuery,
|
||||
AuditFinanceQuery,
|
||||
AuditFinanceTimelinePage,
|
||||
AuditLinkTimeline,
|
||||
AuditResourceSearchPage,
|
||||
AuditResourceSearchQuery,
|
||||
AuditResourceTimelineQuery,
|
||||
AuditRiskEventPage,
|
||||
AuditRiskEventQuery,
|
||||
AuditRiskOverview,
|
||||
AuditRiskQuery,
|
||||
AuditSubjectActivityPage,
|
||||
AuditSubjectActivityQuery,
|
||||
AuditSubjectResourceType,
|
||||
BaseResponse,
|
||||
IntegrationDetailResponse,
|
||||
IntegrationListPage,
|
||||
IntegrationOverview,
|
||||
IntegrationQuery
|
||||
} from '@/types/api'
|
||||
|
||||
export class AuditService extends BaseService {
|
||||
private static ensurePlatformAuditAccess() {
|
||||
const userStore = useUserStore()
|
||||
if (!isPlatformAuditAccount(userStore.info.user_type, userStore.isSuperAdmin)) {
|
||||
throw new Error('当前账号无权访问平台审计接口')
|
||||
}
|
||||
}
|
||||
|
||||
private static ensureSubjectActivityAccess(subject: 'agent' | 'enterprise') {
|
||||
const userType = Number(useUserStore().info.user_type)
|
||||
if ((subject === 'agent' && userType !== 3) || (subject === 'enterprise' && userType !== 4)) {
|
||||
throw new Error('当前账号无权访问主体活动接口')
|
||||
}
|
||||
}
|
||||
|
||||
static getEvents(params?: AuditEventQuery): Promise<BaseResponse<AuditEventPage>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get('/api/admin/audit/events', params)
|
||||
}
|
||||
|
||||
static getEventDetail(eventId: string): Promise<BaseResponse<AuditEventDetail>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get(`/api/admin/audit/events/${eventId}`)
|
||||
}
|
||||
|
||||
static getActorEvents(
|
||||
kind: AuditActorKind,
|
||||
id: string,
|
||||
params?: AuditActorEventQuery
|
||||
): Promise<BaseResponse<AuditEventPage>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get(
|
||||
`/api/admin/audit/actors/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/events`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
static searchResources(
|
||||
params: AuditResourceSearchQuery
|
||||
): Promise<BaseResponse<AuditResourceSearchPage>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get('/api/admin/audit/resources/search', params)
|
||||
}
|
||||
|
||||
static getResourceTimeline(
|
||||
resourceType: string,
|
||||
resourceId: string,
|
||||
params?: AuditResourceTimelineQuery
|
||||
): Promise<BaseResponse<AuditEventPage>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get(
|
||||
`/api/admin/audit/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/timeline`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
static getRequestTimeline(requestId: string): Promise<BaseResponse<AuditLinkTimeline>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get(`/api/admin/audit/requests/${encodeURIComponent(requestId)}/timeline`)
|
||||
}
|
||||
|
||||
static getCorrelationTimeline(correlationId: string): Promise<BaseResponse<AuditLinkTimeline>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get(`/api/admin/audit/correlations/${encodeURIComponent(correlationId)}/timeline`)
|
||||
}
|
||||
|
||||
static getFinanceTimeline(
|
||||
params: AuditFinanceQuery
|
||||
): Promise<BaseResponse<AuditFinanceTimelinePage>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get('/api/admin/audit/finance/timeline', params)
|
||||
}
|
||||
|
||||
static getRiskOverview(params?: AuditRiskQuery): Promise<BaseResponse<AuditRiskOverview>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get('/api/admin/audit/risks/overview', params)
|
||||
}
|
||||
|
||||
static getRiskEvents(params?: AuditRiskEventQuery): Promise<BaseResponse<AuditRiskEventPage>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get('/api/admin/audit/risks/events', params)
|
||||
}
|
||||
|
||||
static getIntegrationOverview(
|
||||
params?: IntegrationQuery & { bucket?: 'hour' | 'day' }
|
||||
): Promise<BaseResponse<IntegrationOverview>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get('/api/admin/audit/integrations/overview', params)
|
||||
}
|
||||
|
||||
static getIntegrations(params?: IntegrationQuery): Promise<BaseResponse<IntegrationListPage>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get('/api/admin/audit/integrations', params)
|
||||
}
|
||||
|
||||
static getIntegrationDetail(
|
||||
integrationId: string
|
||||
): Promise<BaseResponse<IntegrationDetailResponse>> {
|
||||
this.ensurePlatformAuditAccess()
|
||||
return this.get(`/api/admin/audit/integrations/${encodeURIComponent(integrationId)}`)
|
||||
}
|
||||
|
||||
static getAgentResourceActivities(
|
||||
resourceType: AuditSubjectResourceType,
|
||||
identifier: string,
|
||||
params?: AuditSubjectActivityQuery
|
||||
): Promise<BaseResponse<AuditSubjectActivityPage>> {
|
||||
this.ensureSubjectActivityAccess('agent')
|
||||
return this.get(
|
||||
`/api/admin/agent/resource-activities/${encodeURIComponent(resourceType)}/${encodeURIComponent(identifier)}`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
static getEnterpriseResourceActivities(
|
||||
resourceType: Extract<AuditSubjectResourceType, 'iot_card' | 'device'>,
|
||||
identifier: string,
|
||||
params?: AuditSubjectActivityQuery
|
||||
): Promise<BaseResponse<AuditSubjectActivityPage>> {
|
||||
this.ensureSubjectActivityAccess('enterprise')
|
||||
return this.get(
|
||||
`/api/admin/enterprise/resource-activities/${encodeURIComponent(resourceType)}/${encodeURIComponent(identifier)}`,
|
||||
params
|
||||
)
|
||||
}
|
||||
}
|
||||
33
src/api/modules/bulkPurchase.ts
Normal file
33
src/api/modules/bulkPurchase.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import request from '@/utils/http'
|
||||
import type {
|
||||
BulkPurchaseCreateRequest,
|
||||
BulkPurchaseCreateApiResponse,
|
||||
BulkPurchaseTaskListApiResponse,
|
||||
BulkPurchaseTaskApiResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class BulkPurchaseService {
|
||||
static createTask(data: BulkPurchaseCreateRequest): Promise<BulkPurchaseCreateApiResponse> {
|
||||
return request.post<BulkPurchaseCreateApiResponse>({
|
||||
url: '/api/admin/asset-package-batch-orders',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
static getTasks(params?: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
status?: number
|
||||
}): Promise<BulkPurchaseTaskListApiResponse> {
|
||||
return request.get<BulkPurchaseTaskListApiResponse>({
|
||||
url: '/api/admin/asset-package-batch-orders',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
static getTask(taskId: number): Promise<BulkPurchaseTaskApiResponse> {
|
||||
return request.get<BulkPurchaseTaskApiResponse>({
|
||||
url: `/api/admin/asset-package-batch-orders/${taskId}`
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,11 @@ import type {
|
||||
AssetAllocationRecord,
|
||||
AssetAllocationRecordDetail,
|
||||
BatchSetCardSeriesBindingRequest,
|
||||
BatchSetCardSeriesBindingResponse
|
||||
BatchSetCardSeriesBindingResponse,
|
||||
BatchUpdateAssetRealnamePolicyRequest,
|
||||
BatchUpdateAssetRealnamePolicyResponse,
|
||||
SpeedTierCode,
|
||||
SetSpeedTierResponse
|
||||
} from '@/types/api'
|
||||
|
||||
type ApiQueryParams = PaginationParams & Record<string, unknown>
|
||||
@@ -56,6 +60,16 @@ interface CardChangeNotice {
|
||||
}
|
||||
|
||||
export class CardService extends BaseService {
|
||||
static setSpeedTier(
|
||||
iccid: string,
|
||||
code: SpeedTierCode
|
||||
): Promise<BaseResponse<SetSpeedTierResponse>> {
|
||||
return this.put<BaseResponse<SetSpeedTierResponse>>(
|
||||
`/api/admin/iot-cards/${encodeURIComponent(iccid)}/speed-tier`,
|
||||
{ code },
|
||||
{ requestOptions: { errorMessageMode: 'none' } }
|
||||
)
|
||||
}
|
||||
// ========== 号卡商品管理 ==========
|
||||
|
||||
/**
|
||||
@@ -411,6 +425,20 @@ export class CardService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新卡实名认证策略
|
||||
*/
|
||||
static batchUpdateRealnamePolicy(
|
||||
data: BatchUpdateAssetRealnamePolicyRequest,
|
||||
config?: Record<string, any>
|
||||
): Promise<BaseResponse<BatchUpdateAssetRealnamePolicyResponse>> {
|
||||
return this.post<BaseResponse<BatchUpdateAssetRealnamePolicyResponse>>(
|
||||
'/api/admin/iot-cards/batch-update-realname-policy',
|
||||
data,
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
// ========== IoT卡网关操作相关 ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,13 +17,16 @@ import type {
|
||||
RecallDevicesResponse,
|
||||
BatchSetDeviceSeriesBindingRequest,
|
||||
BatchSetDeviceSeriesBindingResponse,
|
||||
BatchUpdateAssetRealnamePolicyRequest,
|
||||
BatchUpdateAssetRealnamePolicyResponse,
|
||||
ImportDeviceRequest,
|
||||
ImportDeviceResponse,
|
||||
DeviceImportTaskQueryParams,
|
||||
DeviceImportTaskListResponse,
|
||||
DeviceImportTaskDetail,
|
||||
DeviceBatchAllocationRequest,
|
||||
DeviceBatchAllocationResponse,
|
||||
BaseResponse,
|
||||
SetSpeedLimitRequest,
|
||||
SwitchCardRequest,
|
||||
SetWiFiRequest,
|
||||
DeviceOperationResponse
|
||||
@@ -146,6 +149,15 @@ export class DeviceService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
static createAllocationTask(
|
||||
data: DeviceBatchAllocationRequest
|
||||
): Promise<BaseResponse<DeviceBatchAllocationResponse>> {
|
||||
return this.post<BaseResponse<DeviceBatchAllocationResponse>>(
|
||||
'/api/admin/devices/import/allocations',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导入任务详情
|
||||
* @param id 任务ID
|
||||
@@ -169,6 +181,20 @@ export class DeviceService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新设备实名认证策略
|
||||
*/
|
||||
static batchUpdateRealnamePolicy(
|
||||
data: BatchUpdateAssetRealnamePolicyRequest,
|
||||
config?: Record<string, any>
|
||||
): Promise<BaseResponse<BatchUpdateAssetRealnamePolicyResponse>> {
|
||||
return this.post<BaseResponse<BatchUpdateAssetRealnamePolicyResponse>>(
|
||||
'/api/admin/devices/batch-update-realname-policy',
|
||||
data,
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
// ========== 设备操作相关 ==========
|
||||
|
||||
/**
|
||||
@@ -193,21 +219,6 @@ export class DeviceService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置限速
|
||||
* @param imei 设备号(IMEI)
|
||||
* @param data 限速参数
|
||||
*/
|
||||
static setSpeedLimit(
|
||||
imei: string,
|
||||
data: SetSpeedLimitRequest
|
||||
): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||
return this.put<BaseResponse<DeviceOperationResponse>>(
|
||||
`/api/admin/devices/by-identifier/${imei}/speed-limit`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换SIM卡
|
||||
* @param imei 设备号(IMEI)
|
||||
|
||||
@@ -5,13 +5,18 @@
|
||||
import { BaseService } from '../BaseService'
|
||||
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 interface ExchangeQueryParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
status?: number // 换货状态
|
||||
flow_type?: string // 流程类型(shipping/direct)
|
||||
identifier?: string // 资产标识符(模糊匹配,同时匹配旧资产、新资产)
|
||||
status?: ExchangeStatus // 换货状态
|
||||
flow_type?: ExchangeFlowType // 流程类型(shipping/direct)
|
||||
old_asset_keyword?: string // 旧资产关键词(ICCID、接入号、虚拟号、IMEI、SN)
|
||||
new_asset_keyword?: string // 新资产关键词(ICCID、接入号、虚拟号、IMEI、SN)
|
||||
created_at_start?: string // 创建时间起始
|
||||
created_at_end?: string // 创建时间结束
|
||||
}
|
||||
@@ -19,9 +24,9 @@ export interface ExchangeQueryParams {
|
||||
// 创建换货单请求
|
||||
export interface CreateExchangeRequest {
|
||||
exchange_reason: string // 换货原因
|
||||
old_asset_type: string // 旧资产类型 (iot_card 或 device)
|
||||
old_asset_type: ExchangeAssetType // 旧资产类型 (iot_card 或 device)
|
||||
old_identifier: string // 旧资产标识符(ICCID/虚拟号/IMEI/SN)
|
||||
flow_type?: string // 流程类型(shipping/direct),默认 shipping
|
||||
flow_type?: ExchangeFlowType // 流程类型(shipping/direct),默认 shipping
|
||||
new_identifier?: string // 新资产标识符(direct 时必填)
|
||||
migrate_data?: boolean // 是否迁移数据(默认 false)
|
||||
remark?: string // 备注(可选)
|
||||
@@ -32,13 +37,16 @@ export interface ExchangeResponse {
|
||||
id: number
|
||||
exchange_no: string
|
||||
exchange_reason: string
|
||||
old_asset_type: string
|
||||
old_asset_type: ExchangeAssetType
|
||||
old_asset_id: number
|
||||
old_asset_identifier: string
|
||||
new_asset_type: string
|
||||
new_asset_identifier: string
|
||||
status: number // 换货状态(1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消)
|
||||
new_asset_type?: ExchangeAssetType | null
|
||||
new_asset_id?: number | null
|
||||
new_asset_identifier?: string | null
|
||||
status: ExchangeStatus // 换货状态(1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消)
|
||||
status_name?: string
|
||||
status_text: string
|
||||
flow_type: string // 流程类型(shipping/direct)
|
||||
flow_type: ExchangeFlowType // 流程类型(shipping/direct)
|
||||
flow_type_name: string // 流程类型名称
|
||||
shipped_at?: string | null // 发货时间(仅 shipping 发货后有值)
|
||||
completed_at?: string | null // 换货完成时间
|
||||
@@ -47,6 +55,15 @@ export interface ExchangeResponse {
|
||||
recipient_address?: string
|
||||
express_company?: string
|
||||
express_no?: string
|
||||
inherited_shop_id?: number | null
|
||||
inherited_shop_name?: string | null
|
||||
submitter_name?: string | null // 提交人名称
|
||||
approval_source?: 'none' | 'legacy' | 'wecom' | null // 审批来源
|
||||
approval_status?: string | null // 审批状态
|
||||
approval_status_name?: string | null // 审批状态名称
|
||||
current_approver_summary?: string | null // 当前审批人摘要
|
||||
processing_status?: string | null // 业务处理状态
|
||||
processing_status_name?: string | null // 业务处理状态名称
|
||||
remark?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -86,8 +103,11 @@ export class ExchangeService extends BaseService {
|
||||
* POST /api/admin/exchanges
|
||||
* @param data 创建参数
|
||||
*/
|
||||
static createExchange(data: CreateExchangeRequest): Promise<BaseResponse<ExchangeResponse>> {
|
||||
return this.create<ExchangeResponse>('/api/admin/exchanges', data)
|
||||
static createExchange(
|
||||
data: CreateExchangeRequest,
|
||||
config?: Record<string, any>
|
||||
): Promise<BaseResponse<ExchangeResponse>> {
|
||||
return this.post<BaseResponse<ExchangeResponse>>('/api/admin/exchanges', data, config)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,8 +134,8 @@ export class ExchangeService extends BaseService {
|
||||
* POST /api/admin/exchanges/{id}/complete
|
||||
* @param id 换货单ID
|
||||
*/
|
||||
static completeExchange(id: number): Promise<BaseResponse> {
|
||||
return this.post<BaseResponse>(`/api/admin/exchanges/${id}/complete`, {})
|
||||
static completeExchange(id: number): Promise<BaseResponse<ExchangeResponse>> {
|
||||
return this.post<BaseResponse<ExchangeResponse>>(`/api/admin/exchanges/${id}/complete`, {})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
CancelExportTaskApiResponse,
|
||||
CreateExportTaskApiResponse,
|
||||
CreateExportTaskRequest,
|
||||
ExportTaskDetail,
|
||||
ExportTaskDetailApiResponse,
|
||||
ExportTaskListApiResponse,
|
||||
ExportTaskQueryParams
|
||||
@@ -19,10 +17,6 @@ export class ExportTaskService extends BaseService {
|
||||
}
|
||||
|
||||
static getExportTaskDetail(id: number): Promise<ExportTaskDetailApiResponse> {
|
||||
return this.getOne<ExportTaskDetail>(`/api/admin/export-tasks/${id}`)
|
||||
}
|
||||
|
||||
static cancelExportTask(id: number): Promise<CancelExportTaskApiResponse> {
|
||||
return this.post<CancelExportTaskApiResponse>(`/api/admin/export-tasks/${id}/cancel`, {})
|
||||
return this.get<ExportTaskDetailApiResponse>(`/api/admin/export-tasks/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ export { CarrierService } from './carrier'
|
||||
export { PackageSeriesService } from './packageSeries'
|
||||
export { PackageManageService } from './packageManage'
|
||||
export { ShopSeriesGrantService } from './shopSeriesGrant'
|
||||
export { ShopPackageAllocationService } from './shopPackageAllocation'
|
||||
export { OrderService } from './order'
|
||||
export { AssetService } from './asset'
|
||||
export { AgentRechargeService } from './agentRecharge'
|
||||
export { PaymentSettingsService } from './paymentSettings'
|
||||
export { SystemConfigService } from './systemConfig'
|
||||
export { ExchangeService } from './exchange'
|
||||
export { RefundService } from './refund'
|
||||
export { DataCleanupService } from './dataCleanup'
|
||||
@@ -33,6 +35,10 @@ export { PollingMonitorService } from './pollingMonitor'
|
||||
export { SuperAdminService } from './superAdmin'
|
||||
export { ExportTaskService } from './exportTask'
|
||||
export { OrderPackageInvalidateTaskService } from './orderPackageInvalidateTask'
|
||||
export { BulkPurchaseService } from './bulkPurchase'
|
||||
export { NotificationService } from './notification'
|
||||
export { AuditService } from './audit'
|
||||
export { WecomService } from './wecom'
|
||||
|
||||
// TODO: 按需添加其他业务模块
|
||||
// export { SettingService } from './setting'
|
||||
|
||||
50
src/api/modules/notification.ts
Normal file
50
src/api/modules/notification.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
NotificationListResponse,
|
||||
NotificationQueryParams,
|
||||
NotificationReadRequest,
|
||||
NotificationReadAllRequest,
|
||||
NotificationReadAllResponse,
|
||||
NotificationReadResponse,
|
||||
NotificationUnreadCount,
|
||||
NotificationUnreadSummary,
|
||||
NotificationTarget
|
||||
} from '@/types/api'
|
||||
|
||||
export class NotificationService extends BaseService {
|
||||
static getUnreadCount(): Promise<BaseResponse<NotificationUnreadCount>> {
|
||||
return this.get<BaseResponse<NotificationUnreadCount>>('/api/admin/notifications/unread-count')
|
||||
}
|
||||
|
||||
static getUnreadSummary(): Promise<BaseResponse<NotificationUnreadSummary>> {
|
||||
return this.get<BaseResponse<NotificationUnreadSummary>>(
|
||||
'/api/admin/notifications/unread-summary'
|
||||
)
|
||||
}
|
||||
|
||||
static getNotifications(
|
||||
params?: NotificationQueryParams
|
||||
): Promise<BaseResponse<NotificationListResponse>> {
|
||||
return this.get<BaseResponse<NotificationListResponse>>('/api/admin/notifications', params)
|
||||
}
|
||||
|
||||
static markRead(id: number): Promise<BaseResponse<NotificationReadResponse>> {
|
||||
return this.put<BaseResponse<NotificationReadResponse>>(`/api/admin/notifications/${id}/read`, {
|
||||
id
|
||||
} satisfies NotificationReadRequest)
|
||||
}
|
||||
|
||||
static markAllRead(
|
||||
data: NotificationReadAllRequest = {}
|
||||
): Promise<BaseResponse<NotificationReadAllResponse>> {
|
||||
return this.put<BaseResponse<NotificationReadAllResponse>>(
|
||||
'/api/admin/notifications/read-all',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
static getTarget(id: number): Promise<BaseResponse<NotificationTarget>> {
|
||||
return this.get<BaseResponse<NotificationTarget>>(`/api/admin/notifications/${id}/target`)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
UpdatePackageStatusRequest,
|
||||
UpdatePackageShelfStatusRequest,
|
||||
BaseResponse,
|
||||
PaginationResponse,
|
||||
PaginationResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class PackageManageService extends BaseService {
|
||||
@@ -68,7 +68,7 @@ export class PackageManageService extends BaseService {
|
||||
* 更新套餐状态
|
||||
* PUT /api/admin/packages/{id}/status
|
||||
* @param id 套餐ID
|
||||
* @param status 状态 (1:启用, 2:禁用)
|
||||
* @param status 状态 (0:禁用, 1:启用)
|
||||
*/
|
||||
static updatePackageStatus(id: number, status: number): Promise<BaseResponse> {
|
||||
const data: UpdatePackageStatusRequest = { status }
|
||||
|
||||
@@ -8,10 +8,7 @@ import type {
|
||||
RefundQueryParams,
|
||||
RefundListResponse,
|
||||
CreateRefundRequest,
|
||||
ApproveRefundRequest,
|
||||
RejectRefundRequest,
|
||||
ResubmitRefundRequest,
|
||||
ReturnRefundRequest,
|
||||
BaseResponse
|
||||
} from '@/types/api'
|
||||
|
||||
@@ -40,24 +37,6 @@ 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<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/approve`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批拒绝退款申请
|
||||
* @param id 退款申请ID
|
||||
* @param data 审批拒绝请求参数
|
||||
*/
|
||||
static rejectRefund(id: number, data: RejectRefundRequest): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/reject`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新提交退款申请
|
||||
* @param id 退款申请ID
|
||||
@@ -68,11 +47,10 @@ export class RefundService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 驳回申请
|
||||
* 补发历史退款审批
|
||||
* @param id 退款申请ID
|
||||
* @param data 退回请求参数
|
||||
*/
|
||||
static returnRefund(id: number, data: ReturnRefundRequest): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/return`, data)
|
||||
static triggerApproval(id: number): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/trigger-approval`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import type {
|
||||
PlatformRole,
|
||||
RoleQueryParams,
|
||||
PlatformRoleFormData,
|
||||
PermissionTreeNode,
|
||||
UpdateRoleDefaultCreditRequest,
|
||||
UpdateRoleDefaultCreditResponse,
|
||||
BaseResponse,
|
||||
PaginationResponse
|
||||
} from '@/types/api'
|
||||
@@ -47,10 +48,26 @@ export class RoleService extends BaseService {
|
||||
* @param id 角色ID
|
||||
* @param data 角色数据
|
||||
*/
|
||||
static updateRole(id: number, data: PlatformRoleFormData): Promise<BaseResponse> {
|
||||
static updateRole(id: number, data: Partial<PlatformRoleFormData>): Promise<BaseResponse> {
|
||||
return this.update(`/api/admin/roles/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新客户角色默认信用
|
||||
* PUT /api/admin/roles/{id}/default-credit
|
||||
* @param id 角色ID
|
||||
* @param data 默认信用配置
|
||||
*/
|
||||
static updateRoleDefaultCredit(
|
||||
id: number,
|
||||
data: UpdateRoleDefaultCreditRequest
|
||||
): Promise<BaseResponse<UpdateRoleDefaultCreditResponse>> {
|
||||
return this.put<BaseResponse<UpdateRoleDefaultCreditResponse>>(
|
||||
`/api/admin/roles/${id}/default-credit`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
* DELETE /api/admin/roles/{id}
|
||||
|
||||
@@ -10,8 +10,12 @@ import type {
|
||||
UpdateShopParams,
|
||||
ShopRolesResponse,
|
||||
AssignShopRolesRequest,
|
||||
UpdateShopCreditLimitRequest,
|
||||
UpdateShopCreditLimitResponse,
|
||||
BaseResponse,
|
||||
PaginationResponse
|
||||
PaginationResponse,
|
||||
ShopBusinessOwnerCandidate,
|
||||
ShopBusinessOwnerCandidateQueryParams
|
||||
} from '@/types/api'
|
||||
|
||||
export class ShopService extends BaseService {
|
||||
@@ -24,6 +28,27 @@ export class ShopService extends BaseService {
|
||||
return this.getPage<ShopResponse>('/api/admin/shops', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取店铺业务员候选
|
||||
* GET /api/admin/shops/business-owner-candidates
|
||||
*/
|
||||
static getBusinessOwnerCandidates(
|
||||
params?: ShopBusinessOwnerCandidateQueryParams
|
||||
): Promise<PaginationResponse<ShopBusinessOwnerCandidate>> {
|
||||
return this.getPage<ShopBusinessOwnerCandidate>(
|
||||
'/api/admin/shops/business-owner-candidates',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取店铺详情
|
||||
* GET /api/admin/shops/{id}
|
||||
*/
|
||||
static getShopDetail(id: number): Promise<BaseResponse<ShopResponse>> {
|
||||
return this.getOne<ShopResponse>(`/api/admin/shops/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 店铺级联查询(树结构)
|
||||
* GET /api/admin/shops/cascade
|
||||
@@ -114,4 +139,20 @@ export class ShopService extends BaseService {
|
||||
static deleteShopRole(shopId: number, roleId: number): Promise<BaseResponse> {
|
||||
return this.delete<BaseResponse>(`/api/admin/shops/${shopId}/roles/${roleId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新店铺实际信用额度
|
||||
* PUT /api/admin/shops/{id}/credit-limit
|
||||
* @param shopId 店铺ID
|
||||
* @param data 实际信用额度配置
|
||||
*/
|
||||
static updateShopCreditLimit(
|
||||
shopId: number,
|
||||
data: UpdateShopCreditLimitRequest
|
||||
): Promise<BaseResponse<UpdateShopCreditLimitResponse>> {
|
||||
return this.put<BaseResponse<UpdateShopCreditLimitResponse>>(
|
||||
`/api/admin/shops/${shopId}/credit-limit`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
55
src/api/modules/shopPackageAllocation.ts
Normal file
55
src/api/modules/shopPackageAllocation.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 店铺套餐分配 API 服务
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
CreateShopPackageBatchAllocationsRequest,
|
||||
CreateShopPackageAllocationRequest,
|
||||
ShopPackageAllocationResponse,
|
||||
ShopPackageBatchAllocationsResponse,
|
||||
UpdateShopPackageAllocationExpiryBaseRequest
|
||||
} from '@/types/api'
|
||||
|
||||
export class ShopPackageAllocationService extends BaseService {
|
||||
/**
|
||||
* 创建店铺套餐分配
|
||||
* POST /api/admin/shop-package-allocations
|
||||
*/
|
||||
static createShopPackageAllocation(
|
||||
data: CreateShopPackageAllocationRequest
|
||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
||||
return this.post<BaseResponse<ShopPackageAllocationResponse>>(
|
||||
'/api/admin/shop-package-allocations',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建店铺套餐分配
|
||||
* POST /api/admin/shop-package-batch-allocations
|
||||
*/
|
||||
static createShopPackageBatchAllocations(
|
||||
data: CreateShopPackageBatchAllocationsRequest
|
||||
): Promise<BaseResponse<ShopPackageBatchAllocationsResponse>> {
|
||||
return this.post<BaseResponse<ShopPackageBatchAllocationsResponse>>(
|
||||
'/api/admin/shop-package-batch-allocations',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新店铺套餐分配生效条件
|
||||
* PATCH /api/admin/shop-package-allocations/{id}/expiry-base
|
||||
*/
|
||||
static updateShopPackageAllocationExpiryBase(
|
||||
id: number,
|
||||
data: UpdateShopPackageAllocationExpiryBaseRequest
|
||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
||||
return this.patch<BaseResponse<ShopPackageAllocationResponse>>(
|
||||
`/api/admin/shop-package-allocations/${id}/expiry-base`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
CreateShopSeriesGrantRequest,
|
||||
UpdateShopSeriesGrantRequest,
|
||||
ManageGrantPackagesRequest,
|
||||
ShopSeriesGrantPackageOptionsResponse,
|
||||
BaseResponse,
|
||||
PaginationResponse
|
||||
} from '@/types/api'
|
||||
@@ -45,6 +46,20 @@ export class ShopSeriesGrantService extends BaseService {
|
||||
return this.getOne<ShopSeriesGrantResponse>(`/api/admin/shop-series-grants/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理系列授权可选套餐
|
||||
* GET /api/admin/shop-series-grants/package-options
|
||||
*/
|
||||
static getPackageOptions(
|
||||
shopId: number,
|
||||
seriesId: number
|
||||
): Promise<BaseResponse<ShopSeriesGrantPackageOptionsResponse>> {
|
||||
return this.getOne<ShopSeriesGrantPackageOptionsResponse>(
|
||||
'/api/admin/shop-series-grants/package-options',
|
||||
{ shop_id: shopId, series_id: seriesId }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新代理系列授权
|
||||
* PUT /api/admin/shop-series-grants/{id}
|
||||
|
||||
@@ -23,7 +23,12 @@ const triggerBrowserDownload = (downloadUrl: string, fileName: string) => {
|
||||
/**
|
||||
* 文件用途枚举
|
||||
*/
|
||||
export type FilePurpose = 'iot_import' | 'device_import' | 'export' | 'attachment'
|
||||
export type FilePurpose =
|
||||
| 'iot_import'
|
||||
| 'batch_purchase'
|
||||
| 'device_batch_allocation'
|
||||
| 'export'
|
||||
| 'attachment'
|
||||
|
||||
/**
|
||||
* 获取上传 URL 请求参数
|
||||
|
||||
28
src/api/modules/systemConfig.ts
Normal file
28
src/api/modules/systemConfig.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
import type {
|
||||
SystemConfigItem,
|
||||
SystemConfigPageResult,
|
||||
SystemConfigQueryParams,
|
||||
UpdateSystemConfigRequest
|
||||
} from '@/types/api/systemConfig'
|
||||
|
||||
const SYSTEM_CONFIG_BASE_URL = '/api/admin/system-configs'
|
||||
|
||||
export class SystemConfigService extends BaseService {
|
||||
static getSystemConfigs(
|
||||
params?: SystemConfigQueryParams
|
||||
): Promise<BaseResponse<SystemConfigPageResult>> {
|
||||
return this.get<BaseResponse<SystemConfigPageResult>>(SYSTEM_CONFIG_BASE_URL, params)
|
||||
}
|
||||
|
||||
static updateSystemConfig(
|
||||
key: string,
|
||||
data: UpdateSystemConfigRequest
|
||||
): Promise<BaseResponse<SystemConfigItem>> {
|
||||
return this.put<BaseResponse<SystemConfigItem>>(
|
||||
`${SYSTEM_CONFIG_BASE_URL}/${encodeURIComponent(key)}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
93
src/api/modules/wecom.ts
Normal file
93
src/api/modules/wecom.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
WecomAccountBindingRequest,
|
||||
WecomApplication,
|
||||
WecomApplicationListResponse,
|
||||
WecomApplicationQueryParams,
|
||||
WecomApplicationRequest,
|
||||
WecomApplicationResponse,
|
||||
WecomBusinessFieldListResponse,
|
||||
WecomBusinessType,
|
||||
WecomMemberListResponse,
|
||||
WecomMemberQueryParams,
|
||||
WecomSceneListResponse,
|
||||
WecomSceneQueryParams,
|
||||
WecomSceneRequest,
|
||||
WecomSceneResponse,
|
||||
WecomSyncMembersApiResponse,
|
||||
WecomTemplateDetailResponse,
|
||||
WecomTemplateInspectRequest
|
||||
} from '@/types/api'
|
||||
|
||||
export class WecomService extends BaseService {
|
||||
static getApplications(
|
||||
params?: WecomApplicationQueryParams
|
||||
): Promise<WecomApplicationListResponse> {
|
||||
return this.get<WecomApplicationListResponse>('/api/admin/wecom/applications', params)
|
||||
}
|
||||
|
||||
static saveApplication(data: WecomApplicationRequest): Promise<WecomApplicationResponse> {
|
||||
return this.post<WecomApplicationResponse>('/api/admin/wecom/applications', data)
|
||||
}
|
||||
|
||||
static testApplication(id: number): Promise<BaseResponse<{ success: boolean }>> {
|
||||
return this.post<BaseResponse<{ success: boolean }>>(`/api/admin/wecom/applications/${id}/test`)
|
||||
}
|
||||
|
||||
static syncMembers(id: number): Promise<WecomSyncMembersApiResponse> {
|
||||
return this.post<WecomSyncMembersApiResponse>(
|
||||
`/api/admin/wecom/applications/${id}/members/sync`
|
||||
)
|
||||
}
|
||||
|
||||
static getMembers(id: number, params?: WecomMemberQueryParams): Promise<WecomMemberListResponse> {
|
||||
return this.get<WecomMemberListResponse>(`/api/admin/wecom/applications/${id}/members`, params)
|
||||
}
|
||||
|
||||
static setDefaultCreator(id: number, userid: string): Promise<WecomApplicationResponse> {
|
||||
return this.put<WecomApplicationResponse>(
|
||||
`/api/admin/wecom/applications/${id}/default-creator`,
|
||||
{ userid }
|
||||
)
|
||||
}
|
||||
|
||||
static getScenes(params?: WecomSceneQueryParams): Promise<WecomSceneListResponse> {
|
||||
return this.get<WecomSceneListResponse>('/api/admin/wecom/scenes', params)
|
||||
}
|
||||
|
||||
static saveScene(
|
||||
businessType: WecomBusinessType,
|
||||
data: WecomSceneRequest
|
||||
): Promise<WecomSceneResponse> {
|
||||
return this.put<WecomSceneResponse>(`/api/admin/wecom/scenes/${businessType}`, data)
|
||||
}
|
||||
|
||||
static inspectTemplate(
|
||||
applicationId: number,
|
||||
data: WecomTemplateInspectRequest
|
||||
): Promise<WecomTemplateDetailResponse> {
|
||||
return this.post<WecomTemplateDetailResponse>(
|
||||
`/api/admin/wecom/applications/${applicationId}/templates/inspect`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
static getBusinessFields(
|
||||
businessType: WecomBusinessType
|
||||
): Promise<WecomBusinessFieldListResponse> {
|
||||
return this.get<WecomBusinessFieldListResponse>(
|
||||
`/api/admin/wecom/scenes/${businessType}/fields`
|
||||
)
|
||||
}
|
||||
|
||||
static bindAccount(
|
||||
accountId: number,
|
||||
data: WecomAccountBindingRequest
|
||||
): Promise<BaseResponse<WecomApplication>> {
|
||||
return this.put<BaseResponse<WecomApplication>>(
|
||||
`/api/admin/accounts/${accountId}/wecom-binding`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -72,19 +72,24 @@
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
border-radius: 100px !important;
|
||||
border-radius: calc(var(--custom-radius) / 1.2 + 2px) !important;
|
||||
border-radius: 12px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 20px 24px 16px !important;
|
||||
margin-right: 0 !important;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.el-dialog__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 25px 0 !important;
|
||||
padding: 20px 24px !important;
|
||||
position: relative; // 为了兼容 el-pagination 样式,需要设置 relative,不然会影响 el-pagination 的样式,比如 el-pagination__jump--small 会被影响,导致 el-pagination__jump--small 按钮无法点击,详见 URL_ADDRESS.com/element-plus/element-plus/issues/5684#issuecomment-1176299275;
|
||||
}
|
||||
|
||||
|
||||
84
src/components/business/BatchRealnamePolicyDialog.vue
Normal file
84
src/components/business/BatchRealnamePolicyDialog.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" title="批量修改实名顺序" width="40%" @closed="resetPolicy">
|
||||
<ElForm label-width="80px">
|
||||
<ElFormItem label="已选数量">
|
||||
<span class="selected-count">{{ selectedCount }} {{ assetUnit }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="认证策略">
|
||||
<ElRadioGroup v-model="policy" :disabled="loading">
|
||||
<ElRadio value="none">无需实名</ElRadio>
|
||||
<ElRadio value="before_order">先实名后购买</ElRadio>
|
||||
<ElRadio value="after_order">先购买后实名</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElAlert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="error-alert"
|
||||
/>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton :disabled="loading" @click="visible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="loading" @click="emit('confirm', policy)">
|
||||
确认修改
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadio,
|
||||
ElRadioGroup
|
||||
} from 'element-plus'
|
||||
import type { AssetRealnamePolicy } from '@/types/api'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
selectedCount: number
|
||||
assetUnit: string
|
||||
loading?: boolean
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
errorMessage: ''
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
confirm: [policy: AssetRealnamePolicy]
|
||||
}>()
|
||||
|
||||
const policy = ref<AssetRealnamePolicy>('none')
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const resetPolicy = () => {
|
||||
policy.value = 'none'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.selected-count {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.error-alert {
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ElDialog v-model="dialogVisible" title="创建退款申请" width="40%" @closed="handleDialogClosed">
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
||||
<ElFormItem label="订单号" prop="order_id">
|
||||
<ElSelect
|
||||
v-if="!hasInitialOrder"
|
||||
@@ -23,7 +23,7 @@
|
||||
</ElSelect>
|
||||
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="申请退款金额" prop="requested_refund_amount">
|
||||
<ElFormItem label="退款金额" prop="requested_refund_amount">
|
||||
<ElInputNumber
|
||||
v-model="formData.requested_refund_amount"
|
||||
:min="0"
|
||||
@@ -86,6 +86,7 @@
|
||||
modelValue: boolean
|
||||
initialOrderId?: number
|
||||
initialOrderNo?: string
|
||||
initialPackageUsageId?: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -108,12 +109,14 @@
|
||||
|
||||
const formData = reactive<{
|
||||
order_id: number | null
|
||||
package_usage_id?: number
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount: number
|
||||
refund_reason: string
|
||||
refund_voucher_key: string[]
|
||||
}>({
|
||||
order_id: null,
|
||||
package_usage_id: undefined,
|
||||
requested_refund_amount: undefined,
|
||||
actual_received_amount: 0,
|
||||
refund_reason: '',
|
||||
@@ -198,6 +201,7 @@
|
||||
formRef.value?.resetFields()
|
||||
voucherUploading.value = false
|
||||
formData.order_id = null
|
||||
formData.package_usage_id = undefined
|
||||
formData.requested_refund_amount = undefined
|
||||
formData.actual_received_amount = 0
|
||||
formData.refund_reason = ''
|
||||
@@ -218,11 +222,12 @@
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const data: CreateRefundRequest = {
|
||||
order_id: formData.order_id!,
|
||||
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0,
|
||||
actual_received_amount: formData.actual_received_amount,
|
||||
order_id: formData.order_id!,
|
||||
package_usage_id: formData.package_usage_id ?? 0,
|
||||
refund_reason: formData.refund_reason,
|
||||
refund_voucher_key: formData.refund_voucher_key,
|
||||
refund_reason: formData.refund_reason || undefined
|
||||
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0
|
||||
}
|
||||
await RefundService.createRefund(data)
|
||||
ElMessage.success('退款申请创建成功')
|
||||
@@ -243,6 +248,7 @@
|
||||
if (hasInitialOrder.value && props.initialOrderId) {
|
||||
formData.order_id = props.initialOrderId
|
||||
}
|
||||
formData.package_usage_id = props.initialPackageUsageId
|
||||
if (hasInitialOrder.value && props.initialOrderNo) {
|
||||
searchOrders(props.initialOrderNo)
|
||||
} else {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="ID"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="accountList"
|
||||
height="60vh"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" :title="title" width="520px" destroy-on-close>
|
||||
<ElDialog v-model="visible" :title="title" width="40%" destroy-on-close>
|
||||
<ElAlert type="info" :closable="false" show-icon class="export-rule-alert">
|
||||
<template #title>{{ description }}</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElForm label-width="100px" class="export-task-form">
|
||||
<ElForm label-width="80px" class="export-task-form">
|
||||
<ElFormItem label="导出场景">
|
||||
<ElTag>{{ sceneName }}</ElTag>
|
||||
</ElFormItem>
|
||||
@@ -85,13 +85,22 @@
|
||||
}
|
||||
)
|
||||
|
||||
const getNormalizedQuery = () => {
|
||||
const query: Record<string, unknown> = {}
|
||||
const getNormalizedQuery = (): Record<string, string> => {
|
||||
const query: Record<string, string> = {}
|
||||
|
||||
Object.entries(props.query || {}).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null || value === '') return
|
||||
if (Array.isArray(value) && value.length === 0) return
|
||||
query[key] = value
|
||||
|
||||
if (typeof value === 'string') {
|
||||
query[key] = value
|
||||
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
query[key] = String(value)
|
||||
} else if (Array.isArray(value)) {
|
||||
query[key] = value.join(',')
|
||||
} else {
|
||||
query[key] = JSON.stringify(value)
|
||||
}
|
||||
})
|
||||
|
||||
return query
|
||||
@@ -113,6 +122,9 @@
|
||||
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(res.data?.message || '导出任务已创建')
|
||||
if (res.data?.task_id) {
|
||||
localStorage.setItem(`export-task-active:${props.scene}`, String(res.data.task_id))
|
||||
}
|
||||
emit('success', res.data)
|
||||
visible.value = false
|
||||
} else {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<ElDrawer
|
||||
v-model="dialogVisible"
|
||||
title="操作审计日志"
|
||||
direction="rtl"
|
||||
size="70%"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<OperationLogsCard :asset-identifier="identifier" :download-permission="downloadPermission" />
|
||||
</ElDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElDrawer } from 'element-plus'
|
||||
import OperationLogsCard from '@/views/asset-management/asset-information/components/OperationLogsCard.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
identifier?: string
|
||||
downloadPermission?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
const dialogVisible = ref(props.modelValue)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
dialogVisible.value = val
|
||||
}
|
||||
)
|
||||
|
||||
watch(dialogVisible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
</script>
|
||||
290
src/components/business/SeriesGrantPackageDialog.vue
Normal file
290
src/components/business/SeriesGrantPackageDialog.vue
Normal file
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="props.dialogType === 'add' ? '添加套餐' : '编辑套餐'"
|
||||
width="40%"
|
||||
:close-on-click-modal="false"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<ElForm ref="formRef" :model="props.form" :rules="props.rules" label-width="130px">
|
||||
<ElFormItem v-if="props.dialogType === 'add'" label="选择套餐" prop="package_ids">
|
||||
<ElSelect
|
||||
:model-value="props.form.package_ids"
|
||||
@update:model-value="updateForm({ package_ids: $event })"
|
||||
placeholder="请选择套餐(可多选)"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="(query: string) => emit('search', query)"
|
||||
:loading="props.packageLoading"
|
||||
clearable
|
||||
multiple
|
||||
:multiple-limit="100"
|
||||
popper-class="package-select-dropdown"
|
||||
>
|
||||
<template
|
||||
v-if="props.availablePackages.length === 0 && !props.packageLoading && props.hasSeries"
|
||||
>
|
||||
<ElOption disabled value="" label="该系列没有可选套餐" />
|
||||
</template>
|
||||
<ElOption
|
||||
v-for="pkg in props.availablePackages"
|
||||
:key="pkg.id"
|
||||
:label="getPackageOptionLabel(pkg)"
|
||||
:value="pkg.id"
|
||||
:disabled="pkg.is_authorized"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="props.dialogType === 'add' && props.form.packages.length"
|
||||
label="套餐配置"
|
||||
prop="packages"
|
||||
>
|
||||
<div class="package-config-list">
|
||||
<div v-for="pkg in props.form.packages" :key="pkg.package_id" class="package-config-item">
|
||||
<div class="package-config-name" :title="pkg.package_name || ''">
|
||||
{{ pkg.package_name || '套餐名称不可用' }}
|
||||
</div>
|
||||
<div class="package-config-cost">
|
||||
<span class="package-config-field-label">套餐成本价(元)</span>
|
||||
<ElInputNumber
|
||||
:model-value="pkg.cost_price_yuan"
|
||||
:min="pkg.original_cost_price || 0"
|
||||
:max="getPackageCostPriceMax(pkg)"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
placeholder="请输入成本价"
|
||||
style="width: 150px"
|
||||
@update:model-value="updatePackageCost(pkg.package_id, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="props.dialogType === 'edit'" label="套餐名称">
|
||||
<span>{{ props.form.package_name }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="props.dialogType === 'edit'" label="套餐编码">
|
||||
<span>{{ props.form.package_code }}</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="props.dialogType === 'edit'" label="成本价(元)" prop="cost_price_yuan">
|
||||
<ElInputNumber
|
||||
:model-value="props.form.cost_price_yuan"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
placeholder="请输入成本价"
|
||||
@update:model-value="updateForm({ cost_price_yuan: $event ?? 0 })"
|
||||
/>
|
||||
<div v-if="props.form.original_cost_price" class="form-tip">
|
||||
请参考{{ props.form.package_name || '套餐' }} - 套餐成本价: ¥{{
|
||||
props.form.original_cost_price.toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="props.dialogType === 'add' || props.canUpdateExpiryBase" label="生效条件">
|
||||
<ElSelect
|
||||
:model-value="props.form.expiry_base_override"
|
||||
placeholder="请选择生效条件"
|
||||
style="width: 100%"
|
||||
@update:model-value="updateForm({ expiry_base_override: $event })"
|
||||
>
|
||||
<ElOption label="跟随套餐默认" value="default" />
|
||||
<ElOption label="购买即生效" value="from_purchase" />
|
||||
<ElOption label="实名激活时生效" value="from_activation" />
|
||||
</ElSelect>
|
||||
<div class="form-tip">仅影响后续新订单,不影响已购买套餐</div>
|
||||
</ElFormItem>
|
||||
|
||||
<template v-if="props.dialogType === 'edit'">
|
||||
<ElFormItem label="套餐默认生效条件">
|
||||
{{ props.form.default_expiry_base_name || '-' }}
|
||||
</ElFormItem>
|
||||
<ElFormItem label="覆盖生效条件">
|
||||
{{ props.form.expiry_base_override_name || '跟随套餐默认' }}
|
||||
</ElFormItem>
|
||||
<ElFormItem label="最终生效条件">
|
||||
{{ props.form.effective_expiry_base_name || '-' }}
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="props.submitLoading" @click="handleSubmit">
|
||||
保存
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInputNumber,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
type FormInstance,
|
||||
type FormRules
|
||||
} from 'element-plus'
|
||||
import type {
|
||||
GrantPackageCandidate,
|
||||
SeriesGrantPackageForm
|
||||
} from '@/utils/business/seriesGrantPackage'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
dialogType: 'add' | 'edit'
|
||||
form: SeriesGrantPackageForm
|
||||
rules: FormRules
|
||||
availablePackages: GrantPackageCandidate[]
|
||||
packageLoading?: boolean
|
||||
submitLoading?: boolean
|
||||
canUpdateExpiryBase?: boolean
|
||||
hasSeries?: boolean
|
||||
}>(),
|
||||
{
|
||||
packageLoading: false,
|
||||
submitLoading: false,
|
||||
canUpdateExpiryBase: false,
|
||||
hasSeries: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'update:form': [form: SeriesGrantPackageForm]
|
||||
search: [query: string]
|
||||
submit: []
|
||||
closed: []
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const formatPackageOptionPrice = (price?: number | null) => {
|
||||
if (price === undefined || price === null) return '未配置'
|
||||
return `¥${(price / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
const getPackageOptionLabel = (pkg: GrantPackageCandidate) => {
|
||||
const packageName = pkg.package_name || '套餐名称不可用'
|
||||
return `${packageName} - 成本价 ${formatPackageOptionPrice(pkg.cost_price)} - 建议售价 ${formatPackageOptionPrice(pkg.suggested_retail_price)}`
|
||||
}
|
||||
|
||||
const getPackageCostPriceMax = (pkg: {
|
||||
original_cost_price?: number
|
||||
suggested_retail_price?: number | null
|
||||
}) => {
|
||||
if (pkg.suggested_retail_price !== undefined && pkg.suggested_retail_price !== null) {
|
||||
return Number((pkg.suggested_retail_price * 1.5).toFixed(2))
|
||||
}
|
||||
if (pkg.original_cost_price !== undefined && pkg.original_cost_price !== null) {
|
||||
return Number((pkg.original_cost_price * 1.5).toFixed(2))
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const updateForm = (values: Partial<SeriesGrantPackageForm>) => {
|
||||
emit('update:form', { ...props.form, ...values })
|
||||
}
|
||||
|
||||
const updatePackageCost = (packageId: number, costPrice: number | undefined) => {
|
||||
updateForm({
|
||||
packages: props.form.packages.map((pkg) =>
|
||||
pkg.package_id === packageId ? { ...pkg, cost_price_yuan: costPrice ?? 0 } : pkg
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
const valid = await formRef.value.validate()
|
||||
if (valid) emit('submit')
|
||||
} catch {
|
||||
// 表单校验失败时由表单项展示提示
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(visible) => {
|
||||
if (visible) nextTick(() => formRef.value?.clearValidate())
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
:global(.package-select-dropdown .el-select-dropdown__item) {
|
||||
height: auto;
|
||||
min-height: 34px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
line-height: 1.5;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.package-config-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.package-config-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.package-config-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.package-config-cost {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.package-config-field-label {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
80
src/components/business/SeriesGrantPackageTable.vue
Normal file
80
src/components/business/SeriesGrantPackageTable.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<ElTable
|
||||
v-loading="props.loading"
|
||||
:data="props.data"
|
||||
row-key="package_id"
|
||||
border
|
||||
stripe
|
||||
:empty-text="props.emptyText"
|
||||
>
|
||||
<ElTableColumn prop="package_name" label="套餐名称" min-width="220" show-overflow-tooltip />
|
||||
<ElTableColumn prop="package_code" label="套餐编码" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn label="成本价" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="套餐默认生效条件" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.default_expiry_base_name || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="覆盖生效条件" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.expiry_base_override_name || '跟随套餐默认' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="最终生效条件" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.effective_expiry_base_name || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="上架状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
|
||||
<ElTag v-else-if="row.shelf_status === 2" type="info" size="small">下架</ElTag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.status === 1" type="success" size="small">启用</ElTag>
|
||||
<ElTag v-else-if="row.status === 2" type="danger" size="small">禁用</ElTag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn v-if="props.showActions" label="操作" :width="props.actionsWidth" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<slot name="actions" :row="row" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElTable, ElTableColumn, ElTag } from 'element-plus'
|
||||
import type { GrantPackageInfo } from '@/types/api'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: GrantPackageInfo[]
|
||||
loading?: boolean
|
||||
emptyText?: string
|
||||
showActions?: boolean
|
||||
actionsWidth?: number | string
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
emptyText: '暂无套餐',
|
||||
showActions: false,
|
||||
actionsWidth: 240
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.amount-value {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
</style>
|
||||
242
src/components/business/ShopCreditLimitDialog.vue
Normal file
242
src/components/business/ShopCreditLimitDialog.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="`调整实际信用额度 - ${currentShop?.shop_name || ''}`"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetDialog"
|
||||
>
|
||||
<ElDescriptions :column="1" border class="credit-preview">
|
||||
<ElDescriptionsItem label="店铺名称">
|
||||
{{ currentShop?.shop_name || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="当前版本">
|
||||
{{ currentShop?.version ?? '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="修改前">
|
||||
{{ formatCreditPreview(currentShop?.credit_enabled, currentShop?.credit_limit) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="修改后">
|
||||
{{ formatCreditPreview(creditForm.credit_enabled, creditLimitFen) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElAlert
|
||||
title="实际可用金额、欠款金额和欠款状态以后端刷新后的资金概况为准。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="credit-dialog-alert"
|
||||
/>
|
||||
|
||||
<ElForm ref="creditFormRef" :model="creditForm" :rules="creditRules" label-width="110px">
|
||||
<ElFormItem label="启用信用">
|
||||
<ElSwitch v-model="creditForm.credit_enabled" @change="handleCreditEnabledChange" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="实际信用额度" prop="credit_limit_yuan">
|
||||
<ElInputNumber
|
||||
v-model="creditForm.credit_limit_yuan"
|
||||
:disabled="!creditForm.credit_enabled"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="100"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
placeholder="请输入实际信用额度"
|
||||
/>
|
||||
<div class="credit-dialog-tip">单位:元;关闭信用时额度将自动归零</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="submitting" @click="handleSubmit">确认调整</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { CommissionService, ShopService } from '@/api/modules'
|
||||
import type { ShopFundSummaryItem } from '@/types/api/commission'
|
||||
import { fenToYuan, formatMoney, yuanToFen } from '@/utils/business/format'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
shop: ShopFundSummaryItem | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
submitted: []
|
||||
}>()
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const creditFormRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const currentShop = ref<ShopFundSummaryItem | null>(null)
|
||||
const creditForm = reactive({
|
||||
credit_enabled: false,
|
||||
credit_limit_yuan: 0
|
||||
})
|
||||
|
||||
const creditLimitFen = computed(() =>
|
||||
creditForm.credit_enabled ? yuanToFen(creditForm.credit_limit_yuan) || 0 : 0
|
||||
)
|
||||
|
||||
const creditRules = computed<FormRules>(() => ({
|
||||
credit_limit_yuan: [
|
||||
{
|
||||
validator: (
|
||||
_rule: unknown,
|
||||
value: number | undefined,
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
if (!creditForm.credit_enabled) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (value === undefined || value === null || Number.isNaN(value)) {
|
||||
callback(new Error('请输入实际信用额度'))
|
||||
return
|
||||
}
|
||||
if (value <= 0) {
|
||||
callback(new Error('实际信用额度必须大于0'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
const formatCreditPreview = (enabled?: boolean, creditLimit?: number) =>
|
||||
enabled ? `启用 / ${formatMoney(creditLimit || 0)}` : '关闭 / ¥0.00'
|
||||
|
||||
const syncForm = (summary: ShopFundSummaryItem) => {
|
||||
creditForm.credit_enabled = Boolean(summary.credit_enabled)
|
||||
creditForm.credit_limit_yuan = summary.credit_enabled ? fenToYuan(summary.credit_limit) : 0
|
||||
nextTick(() => creditFormRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.shop] as const,
|
||||
([visible, shop]) => {
|
||||
if (visible && shop) {
|
||||
currentShop.value = shop
|
||||
syncForm(shop)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleCreditEnabledChange = (enabled: boolean | string | number) => {
|
||||
if (!enabled) {
|
||||
creditForm.credit_limit_yuan = 0
|
||||
creditFormRef.value?.clearValidate('credit_limit_yuan')
|
||||
}
|
||||
}
|
||||
|
||||
const resetDialog = () => {
|
||||
creditFormRef.value?.resetFields()
|
||||
currentShop.value = null
|
||||
creditForm.credit_enabled = false
|
||||
creditForm.credit_limit_yuan = 0
|
||||
}
|
||||
|
||||
const loadLatestSummary = async () => {
|
||||
if (!currentShop.value) return null
|
||||
const res = await CommissionService.getShopFundSummary({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
shop_name: currentShop.value.shop_name
|
||||
})
|
||||
if (res.code !== 0) return null
|
||||
return (
|
||||
(res.data.items || []).find((item) => item.shop_id === currentShop.value?.shop_id) || null
|
||||
)
|
||||
}
|
||||
|
||||
const isConflict = (error: unknown) => {
|
||||
const normalized = normalizeApiError(error)
|
||||
return normalized.kind === 'conflict' || normalized.status === 409
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!currentShop.value || !creditFormRef.value) return
|
||||
|
||||
try {
|
||||
await creditFormRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await ShopService.updateShopCreditLimit(currentShop.value.shop_id, {
|
||||
credit_enabled: creditForm.credit_enabled,
|
||||
credit_limit: creditLimitFen.value,
|
||||
version: currentShop.value.version
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('实际信用额度调整成功')
|
||||
dialogVisible.value = false
|
||||
emit('submitted')
|
||||
return
|
||||
}
|
||||
|
||||
if (res.code === 409) {
|
||||
const latest = await loadLatestSummary()
|
||||
if (latest) {
|
||||
currentShop.value = latest
|
||||
syncForm(latest)
|
||||
}
|
||||
ElMessage.warning('资金概况已被其他操作更新,已刷新最新版本;请确认后重新提交')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.error(res.msg || '实际信用额度调整失败')
|
||||
} catch (error) {
|
||||
if (isConflict(error)) {
|
||||
const latest = await loadLatestSummary()
|
||||
if (latest) {
|
||||
currentShop.value = latest
|
||||
syncForm(latest)
|
||||
}
|
||||
ElMessage.warning('资金概况已被其他操作更新,已刷新最新版本;请确认后重新提交')
|
||||
} else {
|
||||
console.error('实际信用额度调整失败:', error)
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.credit-preview {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.credit-dialog-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.credit-dialog-tip {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -2,10 +2,11 @@
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="手动更新卡实名状态"
|
||||
width="400px"
|
||||
width="30%"
|
||||
modal-class="asset-information-dialog"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
||||
<ElFormItem label="资产标识">
|
||||
<span style="font-weight: bold; color: #409eff">{{ assetIdentifier }}</span>
|
||||
</ElFormItem>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
drag
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:accept="accept"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleRemoveFile"
|
||||
list-type="picture"
|
||||
@@ -20,7 +21,7 @@
|
||||
<div class="voucher-upload__text">将{{ voucherName }}拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持多张图片或附件,最多上传 {{ maxCount }} 个文件;可粘贴文件。
|
||||
{{ tip || `支持多张图片或附件,最多上传 ${maxCount} 个文件;可粘贴文件。` }}
|
||||
</div>
|
||||
</template>
|
||||
<template #file="{ file }">
|
||||
@@ -56,28 +57,44 @@
|
||||
import { Close, Document, UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { UploadFile, UploadInstance, UploadRawFile } from 'element-plus'
|
||||
import { StorageService } from '@/api/modules'
|
||||
import type { RefundAttachment } from '@/types/api/refund'
|
||||
import type { FilePurpose } from '@/api/modules/storage'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string[] | string
|
||||
voucherName?: string
|
||||
maxCount?: number
|
||||
accept?: string
|
||||
tip?: string
|
||||
purpose?: FilePurpose
|
||||
maxSizeMb?: number
|
||||
singleColumnCsv?: boolean
|
||||
maxCsvRows?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
voucherName: '凭证',
|
||||
maxCount: 5
|
||||
maxCount: 5,
|
||||
accept: '',
|
||||
tip: '',
|
||||
purpose: 'attachment',
|
||||
maxSizeMb: 0,
|
||||
singleColumnCsv: false,
|
||||
maxCsvRows: 0
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string[]]
|
||||
'uploading-change': [value: boolean]
|
||||
change: [value: string[]]
|
||||
'files-change': [value: RefundAttachment[]]
|
||||
}>()
|
||||
|
||||
const rootRef = ref<HTMLElement>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadingCount = ref(0)
|
||||
const voucherFileKeyMap = new Map<number, string>()
|
||||
const voucherFileMetadataMap = new Map<number, RefundAttachment>()
|
||||
const removedUploadUids = new Set<number>()
|
||||
const fileObjectUrlMap = new Map<number, string>()
|
||||
const selectedUploadUids = new Set<number>()
|
||||
@@ -95,6 +112,10 @@
|
||||
emit('change', keys)
|
||||
}
|
||||
|
||||
const emitFileMetadata = () => {
|
||||
emit('files-change', Array.from(voucherFileMetadataMap.values()))
|
||||
}
|
||||
|
||||
const setUploadingCount = (count: number) => {
|
||||
uploadingCount.value = Math.max(0, count)
|
||||
emit('uploading-change', uploadingCount.value > 0)
|
||||
@@ -104,12 +125,14 @@
|
||||
activeUploadBatch += 1
|
||||
setUploadingCount(0)
|
||||
voucherFileKeyMap.clear()
|
||||
voucherFileMetadataMap.clear()
|
||||
removedUploadUids.clear()
|
||||
selectedUploadUids.clear()
|
||||
revokeAllFileObjectUrls()
|
||||
uploadRef.value?.clearFiles()
|
||||
if (emitValue) {
|
||||
emitVoucherKeys()
|
||||
emitFileMetadata()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +194,44 @@
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
|
||||
if (props.maxSizeMb > 0 && file.size > props.maxSizeMb * 1024 * 1024) {
|
||||
ElMessage.warning(`文件不能超过 ${props.maxSizeMb}MB`)
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.singleColumnCsv) {
|
||||
const content = await file.text()
|
||||
const rows = content
|
||||
.replace(/^\uFEFF/, '')
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
if (props.maxCsvRows > 0 && Math.max(rows.length - 1, 0) > props.maxCsvRows) {
|
||||
ElMessage.warning(`CSV 数据行不能超过 ${props.maxCsvRows} 行`)
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
if (rows.some((row) => row.includes(','))) {
|
||||
ElMessage.warning('CSV 只能包含一列资产标识')
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (props.accept) {
|
||||
const accepted = props.accept.split(',').some((type) => {
|
||||
const value = type.trim().toLowerCase()
|
||||
return value.startsWith('.')
|
||||
? file.name.toLowerCase().endsWith(value)
|
||||
: file.type.toLowerCase() === value
|
||||
})
|
||||
if (!accepted) {
|
||||
ElMessage.warning(`只能上传 ${props.accept} 格式的文件`)
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedUploadUids.has(uploadFile.uid)) {
|
||||
if (selectedUploadUids.size >= getMaxCount()) {
|
||||
showMaxCountWarning()
|
||||
@@ -192,7 +253,7 @@
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: contentType,
|
||||
purpose: 'attachment'
|
||||
purpose: props.purpose
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
@@ -209,7 +270,13 @@
|
||||
}
|
||||
|
||||
voucherFileKeyMap.set(uploadFile.uid, file_key)
|
||||
voucherFileMetadataMap.set(uploadFile.uid, {
|
||||
file_key,
|
||||
file_name: file.name,
|
||||
file_size: file.size
|
||||
})
|
||||
emitVoucherKeys()
|
||||
emitFileMetadata()
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error: any) {
|
||||
if (uploadBatch !== activeUploadBatch) return
|
||||
@@ -217,7 +284,9 @@
|
||||
console.error(`上传${props.voucherName}失败:`, error)
|
||||
ElMessage.error(error?.message || '上传失败,请重试')
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
voucherFileMetadataMap.delete(uploadFile.uid)
|
||||
emitVoucherKeys()
|
||||
emitFileMetadata()
|
||||
removeUploadFile(uploadFile)
|
||||
} finally {
|
||||
if (uploadBatch === activeUploadBatch) {
|
||||
@@ -231,7 +300,9 @@
|
||||
selectedUploadUids.delete(uploadFile.uid)
|
||||
revokeFileObjectUrl(uploadFile.uid)
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
voucherFileMetadataMap.delete(uploadFile.uid)
|
||||
emitVoucherKeys()
|
||||
emitFileMetadata()
|
||||
}
|
||||
|
||||
const focusUploadArea = () => {
|
||||
|
||||
162
src/components/business/audit/AuditDistributionChart.vue
Normal file
162
src/components/business/audit/AuditDistributionChart.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div ref="chartRef" class="distribution-chart" role="img" :aria-label="ariaLabel"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { getCssVar } from '@/utils/ui'
|
||||
|
||||
export interface AuditDistributionItem {
|
||||
code: string
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: AuditDistributionItem[]
|
||||
type?: 'donut' | 'bar'
|
||||
title: string
|
||||
}>(),
|
||||
{ type: 'donut' }
|
||||
)
|
||||
const emit = defineEmits<{ select: [code: string] }>()
|
||||
const settingStore = useSettingStore()
|
||||
const chartRef = ref<HTMLElement>()
|
||||
const ariaLabel = computed(() =>
|
||||
props.data.length
|
||||
? `${props.title}:${props.data.map((item) => `${item.name} ${item.count}`).join(',')}`
|
||||
: `${props.title}:暂无数据`
|
||||
)
|
||||
let chart: echarts.ECharts | undefined
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
|
||||
const colors = () => [
|
||||
getCssVar('--el-color-primary'),
|
||||
getCssVar('--el-color-success'),
|
||||
getCssVar('--el-color-warning'),
|
||||
getCssVar('--el-color-danger'),
|
||||
getCssVar('--el-color-info'),
|
||||
getCssVar('--el-color-primary-light-3')
|
||||
]
|
||||
const textColor = () => getCssVar('--el-text-color-regular') || '#606266'
|
||||
const splitColor = () => getCssVar('--el-border-color-lighter') || '#ebeef5'
|
||||
const options = (): EChartsOption => {
|
||||
if (props.type === 'bar') {
|
||||
return {
|
||||
color: colors(),
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
grid: { top: 8, right: 72, bottom: 8, left: 8, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
minInterval: 1,
|
||||
splitNumber: 4,
|
||||
axisLabel: {
|
||||
color: textColor(),
|
||||
hideOverlap: true,
|
||||
formatter: (value: number) => {
|
||||
const absolute = Math.abs(value)
|
||||
if (absolute >= 10000) {
|
||||
const formatted = value / 10000
|
||||
return `${Number.isInteger(formatted) ? formatted : formatted.toFixed(1)}万`
|
||||
}
|
||||
if (absolute >= 1000) {
|
||||
const formatted = value / 1000
|
||||
return `${Number.isInteger(formatted) ? formatted : formatted.toFixed(1)}k`
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
},
|
||||
splitLine: { lineStyle: { color: splitColor(), type: 'dashed' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: props.data.map((item) => item.name),
|
||||
inverse: true,
|
||||
axisLabel: { color: textColor(), overflow: 'truncate', width: 90 },
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: props.data.map((item) => ({
|
||||
value: item.count,
|
||||
code: item.code,
|
||||
itemStyle: {
|
||||
color: getCssVar('--el-color-primary'),
|
||||
borderRadius: [0, 4, 4, 0]
|
||||
}
|
||||
})),
|
||||
barMaxWidth: 24,
|
||||
label: { show: true, position: 'right', color: textColor() }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return {
|
||||
color: colors(),
|
||||
tooltip: { trigger: 'item', formatter: '{b}<br/>{c}({d}%)' },
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
bottom: 0,
|
||||
left: 'center',
|
||||
textStyle: { color: textColor() }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['42%', '68%'],
|
||||
center: ['50%', '43%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: { borderColor: getCssVar('--el-bg-color'), borderWidth: 2 },
|
||||
label: { show: false },
|
||||
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
|
||||
data: props.data.map((item) => ({
|
||||
value: item.count,
|
||||
name: item.name,
|
||||
code: item.code
|
||||
}))
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
const render = async () => {
|
||||
await nextTick()
|
||||
if (!chartRef.value) return
|
||||
if (!chart) {
|
||||
chart = echarts.init(chartRef.value)
|
||||
chart.on('click', (params) => {
|
||||
const code = (params.data as { code?: string } | undefined)?.code
|
||||
if (code) emit('select', code)
|
||||
})
|
||||
}
|
||||
chart.setOption(options(), true)
|
||||
}
|
||||
|
||||
watch(() => props.data, render, { deep: true })
|
||||
watch(() => settingStore.isDark, render)
|
||||
onMounted(() => {
|
||||
render()
|
||||
if (chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => chart?.resize())
|
||||
resizeObserver.observe(chartRef.value)
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect()
|
||||
chart?.dispose()
|
||||
chart = undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.distribution-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
58
src/components/business/audit/AuditEventTable.vue
Normal file
58
src/components/business/audit/AuditEventTable.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<ArtTable
|
||||
:data="items"
|
||||
:loading="Boolean(loading)"
|
||||
:pagination="false"
|
||||
row-key="event_id"
|
||||
stripe
|
||||
:margin-top="0"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="发生时间" prop="occurred_at" width="180">
|
||||
<template #default="{ row }">{{ formatDateTime(row.occurred_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="动作" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<ElButton v-if="detailEnabled" link type="primary" @click="$emit('detail', row)">
|
||||
{{ row.action_name || '-' }}
|
||||
</ElButton>
|
||||
<span v-else>{{ row.action_name || '-' }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="摘要" prop="summary" min-width="240" show-overflow-tooltip />
|
||||
<ElTableColumn label="操作者" min-width="150">
|
||||
<template #default="{ row }">{{ row.actor_name || row.actor_id || '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="结果" width="100" align="center">
|
||||
<template #default="{ row }"
|
||||
><ElTag :type="resultMeta(row).type">{{ resultMeta(row).label }}</ElTag></template
|
||||
>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="风险" width="90" align="center">
|
||||
<template #default="{ row }"
|
||||
><ElTag effect="plain" :type="riskMeta(row).type">{{
|
||||
riskMeta(row).label
|
||||
}}</ElTag></template
|
||||
>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AuditEventView } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { auditResultMeta, auditRiskMeta } from '@/utils/business/audit'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{ items: AuditEventView[]; loading?: boolean; detailEnabled?: boolean }>(),
|
||||
{
|
||||
detailEnabled: true
|
||||
}
|
||||
)
|
||||
defineEmits<{ detail: [row: AuditEventView] }>()
|
||||
const resultMeta = (row: AuditEventView) =>
|
||||
auditResultMeta[row.result] || { label: row.result, type: 'info' as const }
|
||||
const riskMeta = (row: AuditEventView) =>
|
||||
auditRiskMeta[row.risk_level] || { label: row.risk_level, type: 'info' as const }
|
||||
</script>
|
||||
1032
src/components/business/audit/AuditInvestigationDrawer.vue
Normal file
1032
src/components/business/audit/AuditInvestigationDrawer.vue
Normal file
File diff suppressed because it is too large
Load Diff
23
src/components/business/audit/AuditInvestigationHost.vue
Normal file
23
src/components/business/audit/AuditInvestigationHost.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<AuditInvestigationDrawer
|
||||
v-model="auditInvestigationVisible"
|
||||
:target="auditInvestigationTarget"
|
||||
/>
|
||||
<AuditResourceSearchDialog
|
||||
v-model="auditResourceSearchVisible"
|
||||
:resource-type="auditResourceSearchType"
|
||||
:initial-keyword="auditResourceSearchKeyword"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AuditInvestigationDrawer from './AuditInvestigationDrawer.vue'
|
||||
import AuditResourceSearchDialog from './AuditResourceSearchDialog.vue'
|
||||
import {
|
||||
auditInvestigationTarget,
|
||||
auditInvestigationVisible,
|
||||
auditResourceSearchKeyword,
|
||||
auditResourceSearchType,
|
||||
auditResourceSearchVisible
|
||||
} from './investigationController'
|
||||
</script>
|
||||
160
src/components/business/audit/AuditInvestigationLinks.vue
Normal file
160
src/components/business/audit/AuditInvestigationLinks.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div v-if="hasLinks" class="investigation-links" aria-label="调查入口">
|
||||
<ElButton v-if="actorRef" link type="primary" @click="openActor"> 操作者行为时间线 </ElButton>
|
||||
<ElButton
|
||||
v-show="showResources"
|
||||
v-for="resource in stableResourceRefs"
|
||||
:key="`${resource.resource_type}:${resource.resource_id}`"
|
||||
link
|
||||
type="primary"
|
||||
:title="resourceTimelineLabel(resource)"
|
||||
:aria-label="resourceTimelineLabel(resource)"
|
||||
@click="openResource(resource)"
|
||||
>
|
||||
{{ resourceTimelineLabel(resource) }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-show="showResources"
|
||||
v-for="resource in searchableResourceRefs"
|
||||
:key="`${resource.resource_type}:${resource.resource_key}`"
|
||||
link
|
||||
type="primary"
|
||||
@click="searchResource(resource)"
|
||||
>
|
||||
精确注册资源
|
||||
</ElButton>
|
||||
<ElButton v-if="requestId" link type="primary" @click="openTimeline('request', requestId)">
|
||||
请求链路
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="correlationId"
|
||||
link
|
||||
type="primary"
|
||||
@click="openTimeline('correlation', correlationId)"
|
||||
>
|
||||
业务关联链路
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-for="item in visibleIntegrationRefs"
|
||||
:key="item.integration_id"
|
||||
link
|
||||
type="primary"
|
||||
@click="openIntegration(item.integration_id)"
|
||||
>
|
||||
外部交互
|
||||
</ElButton>
|
||||
</div>
|
||||
<span v-else-if="showEmpty" class="muted">无可靠调查引用</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type {
|
||||
AuditInvestigationRefs,
|
||||
AuditInvestigationResourceRef,
|
||||
AuditSearchResourceType
|
||||
} from '@/types/api'
|
||||
import { openAuditInvestigation, openAuditResourceSearch } from './investigationController'
|
||||
import { auditResourceTypeLabels } from '@/utils/business/audit'
|
||||
import { resolveInvestigationReference } from '@/utils/business/auditNavigation'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
refs?: AuditInvestigationRefs | null
|
||||
showEmpty?: boolean
|
||||
showResources?: boolean
|
||||
}>(),
|
||||
{ showEmpty: true, showResources: true }
|
||||
)
|
||||
const searchableTypes = new Set<AuditSearchResourceType>([
|
||||
'iot_card',
|
||||
'device',
|
||||
'shop',
|
||||
'order',
|
||||
'refund'
|
||||
])
|
||||
const { hasAuth } = useAuth()
|
||||
const requestId = computed(() => resolveInvestigationReference(props.refs?.request_id) || '')
|
||||
const correlationId = computed(
|
||||
() => resolveInvestigationReference(props.refs?.correlation_id) || ''
|
||||
)
|
||||
const actorRef = computed(() => {
|
||||
const actor = props.refs?.actor_ref
|
||||
return actor?.kind && actor.id ? actor : null
|
||||
})
|
||||
const stableResourceRefs = computed(() =>
|
||||
(props.refs?.resource_refs || []).filter(
|
||||
(resource): resource is AuditInvestigationResourceRef & { resource_id: string } =>
|
||||
Boolean(resource.resource_type && resource.resource_id)
|
||||
)
|
||||
)
|
||||
const searchableResourceRefs = computed(() =>
|
||||
(props.refs?.resource_refs || []).filter(
|
||||
(
|
||||
resource
|
||||
): resource is AuditInvestigationResourceRef & {
|
||||
resource_type: AuditSearchResourceType
|
||||
resource_key: string
|
||||
} =>
|
||||
!resource.resource_id &&
|
||||
Boolean(resource.resource_key) &&
|
||||
searchableTypes.has(resource.resource_type as AuditSearchResourceType)
|
||||
)
|
||||
)
|
||||
const stableIntegrationRefs = computed(() =>
|
||||
(props.refs?.integration_refs || []).filter((item) => Boolean(item.integration_id))
|
||||
)
|
||||
const visibleIntegrationRefs = computed(() =>
|
||||
hasAuth(AUDIT_PERMISSIONS.auditEventIntegrationDetail) ? stableIntegrationRefs.value : []
|
||||
)
|
||||
const hasLinks = computed(() =>
|
||||
Boolean(
|
||||
actorRef.value ||
|
||||
requestId.value ||
|
||||
correlationId.value ||
|
||||
(props.showResources &&
|
||||
(stableResourceRefs.value.length || searchableResourceRefs.value.length)) ||
|
||||
visibleIntegrationRefs.value.length
|
||||
)
|
||||
)
|
||||
|
||||
const openIntegration = (id: string) => openAuditInvestigation({ mode: 'integration', id })
|
||||
const openTimeline = (mode: 'request' | 'correlation', id: string) =>
|
||||
openAuditInvestigation({ mode, id })
|
||||
const openActor = () => {
|
||||
const actor = actorRef.value
|
||||
if (actor) {
|
||||
openAuditInvestigation({ mode: 'actor', actorKind: actor.kind, id: actor.id })
|
||||
}
|
||||
}
|
||||
const openResource = (resource: AuditInvestigationResourceRef & { resource_id: string }) =>
|
||||
openAuditInvestigation({
|
||||
mode: 'resource',
|
||||
resourceType: resource.resource_type,
|
||||
id: resource.resource_id
|
||||
})
|
||||
const resourceTimelineLabel = (
|
||||
resource: AuditInvestigationResourceRef & { resource_id: string }
|
||||
) =>
|
||||
`资源审计时间线 · ${auditResourceTypeLabels[resource.resource_type] || resource.resource_type} · ${resource.resource_id}`
|
||||
const searchResource = (
|
||||
resource: AuditInvestigationResourceRef & {
|
||||
resource_type: AuditSearchResourceType
|
||||
resource_key: string
|
||||
}
|
||||
) => openAuditResourceSearch(resource.resource_type, resource.resource_key)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.investigation-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 8px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
269
src/components/business/audit/AuditResourceSearchDialog.vue
Normal file
269
src/components/business/audit/AuditResourceSearchDialog.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
title="精确注册资源"
|
||||
width="min(880px, 92vw)"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
>
|
||||
<div v-loading="loading" class="result-list">
|
||||
<ElCard
|
||||
v-for="row in items"
|
||||
:key="`${row.resource_type}:${row.resource_id || row.resource_key}`"
|
||||
shadow="never"
|
||||
class="resource-card"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<strong>业务信息</strong>
|
||||
<ElButton link type="primary" :disabled="!row.resource_id" @click="openTimeline(row)">
|
||||
<template #icon>
|
||||
<ElIcon><Clock /></ElIcon>
|
||||
</template>
|
||||
审计时间线
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="snapshotDetails(row).length" class="snapshot-grid">
|
||||
<div v-for="item in snapshotDetails(row)" :key="item.label" class="snapshot-item">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="snapshot-item">
|
||||
<span>业务标识</span>
|
||||
<strong>{{ row.resource_key || '-' }}</strong>
|
||||
</div>
|
||||
</ElCard>
|
||||
<ElEmpty v-if="!loading && !items.length" description="未找到匹配的注册资源" />
|
||||
</div>
|
||||
|
||||
<div v-if="total > pageSize" class="pagination">
|
||||
<ElPagination
|
||||
v-model:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="search(false)"
|
||||
/>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Clock } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { AuditService } from '@/api/modules'
|
||||
import type { AuditResourceCandidate, AuditSearchResourceType } from '@/types/api'
|
||||
import { openAuditInvestigation } from './investigationController'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
resourceType: AuditSearchResourceType
|
||||
initialKeyword?: string
|
||||
}>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = 10
|
||||
const total = ref(0)
|
||||
const items = ref<AuditResourceCandidate[]>([])
|
||||
let searchSequence = 0
|
||||
const resourceTypeLabels: Record<AuditSearchResourceType, string> = {
|
||||
iot_card: 'IoT卡',
|
||||
device: '设备',
|
||||
shop: '店铺',
|
||||
order: '订单',
|
||||
refund: '退款单'
|
||||
}
|
||||
|
||||
const resourceTypeLabel = computed(() => resourceTypeLabels[props.resourceType])
|
||||
|
||||
interface SnapshotField {
|
||||
label: string
|
||||
keys: string[]
|
||||
format?: (value: unknown) => string
|
||||
}
|
||||
|
||||
const textValue = (value: unknown) => String(value ?? '-')
|
||||
const refundStatusValue = (value: unknown) =>
|
||||
({ 1: '待审批', 2: '已通过', 3: '已拒绝', 4: '已退回' })[Number(value)] || textValue(value)
|
||||
const centAmountValue = (value: unknown) => {
|
||||
const amount = Number(value)
|
||||
return Number.isFinite(amount) ? `¥${(amount / 100).toFixed(2)}` : textValue(value)
|
||||
}
|
||||
const snapshotFields: Record<AuditSearchResourceType, SnapshotField[]> = {
|
||||
iot_card: [
|
||||
{ label: 'ICCID', keys: ['iccid', 'iccid_19', 'iccid_20'] },
|
||||
{ label: '虚拟号', keys: ['virtual_no'] },
|
||||
{ label: 'MSISDN', keys: ['msisdn'] },
|
||||
{ label: '运营商', keys: ['carrier_name', 'carrier_type'] }
|
||||
],
|
||||
device: [
|
||||
{ label: '虚拟号', keys: ['virtual_no'] },
|
||||
{ label: 'IMEI', keys: ['imei'] },
|
||||
{ label: 'SN', keys: ['sn', 'serial_no'] },
|
||||
{ label: '设备编号', keys: ['device_no'] }
|
||||
],
|
||||
shop: [
|
||||
{ label: '店铺编号', keys: ['shop_code'] },
|
||||
{ label: '店铺名称', keys: ['shop_name', 'name'] },
|
||||
{ label: '联系人', keys: ['contact_name'] },
|
||||
{ label: '联系电话', keys: ['contact_phone', 'phone'] }
|
||||
],
|
||||
order: [
|
||||
{ label: '订单号', keys: ['order_no'] },
|
||||
{ label: '资产编号', keys: ['asset_identifier'] },
|
||||
{ label: '订单状态', keys: ['status_name'] },
|
||||
{ label: '订单金额', keys: ['total_amount', 'order_amount'], format: centAmountValue }
|
||||
],
|
||||
refund: [
|
||||
{ label: '退款单号', keys: ['refund_no'] },
|
||||
{ label: '订单号', keys: ['order_no'] },
|
||||
{ label: '资产编号', keys: ['asset_identifier'] },
|
||||
{
|
||||
label: '申请退款金额',
|
||||
keys: ['requested_refund_amount'],
|
||||
format: centAmountValue
|
||||
},
|
||||
{ label: '状态', keys: ['status_name', 'status'], format: refundStatusValue }
|
||||
]
|
||||
}
|
||||
|
||||
const snapshotDetails = (row: AuditResourceCandidate) => {
|
||||
const snapshot = row.identity_snapshot || {}
|
||||
return snapshotFields[row.resource_type].flatMap((field) => {
|
||||
const key = field.keys.find((candidate) => {
|
||||
const value = snapshot[candidate]
|
||||
return value !== undefined && value !== null && value !== ''
|
||||
})
|
||||
if (!key) return []
|
||||
const value = snapshot[key]
|
||||
return [{ label: field.label, value: field.format?.(value) || textValue(value) }]
|
||||
})
|
||||
}
|
||||
const search = async (resetPage: boolean) => {
|
||||
const exactKeyword = keyword.value.trim()
|
||||
if (!exactKeyword) {
|
||||
ElMessage.warning(`当前记录缺少可用于搜索${resourceTypeLabel.value}的业务标识`)
|
||||
return
|
||||
}
|
||||
if (resetPage) page.value = 1
|
||||
const sequence = ++searchSequence
|
||||
loading.value = true
|
||||
try {
|
||||
const data = (
|
||||
await AuditService.searchResources({
|
||||
resource_type: props.resourceType,
|
||||
keyword: exactKeyword,
|
||||
page: page.value,
|
||||
page_size: pageSize
|
||||
})
|
||||
).data
|
||||
if (sequence === searchSequence) {
|
||||
items.value = data.items || []
|
||||
total.value = data.total
|
||||
}
|
||||
} catch {
|
||||
if (sequence === searchSequence) ElMessage.error('注册资源搜索失败')
|
||||
} finally {
|
||||
if (sequence === searchSequence) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openTimeline = (row: AuditResourceCandidate) => {
|
||||
if (!row.resource_id) return
|
||||
visible.value = false
|
||||
openAuditInvestigation({
|
||||
mode: 'resource',
|
||||
resourceType: row.resource_type,
|
||||
id: row.resource_id
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (!open) {
|
||||
searchSequence++
|
||||
loading.value = false
|
||||
items.value = []
|
||||
total.value = 0
|
||||
return
|
||||
}
|
||||
keyword.value = props.initialKeyword?.trim() || ''
|
||||
page.value = 1
|
||||
items.value = []
|
||||
total.value = 0
|
||||
if (keyword.value) void search(true)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result-list {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.resource-card + .resource-card {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.snapshot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.snapshot-item {
|
||||
min-width: 0;
|
||||
padding: 12px 14px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
|
||||
span,
|
||||
strong {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.snapshot-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
src/components/business/audit/investigationController.ts
Normal file
25
src/components/business/audit/investigationController.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ref } from 'vue'
|
||||
import type { AuditInvestigationTarget } from './types'
|
||||
import type { AuditSearchResourceType } from '@/types/api'
|
||||
|
||||
export const auditInvestigationVisible = ref(false)
|
||||
export const auditInvestigationTarget = ref<AuditInvestigationTarget>()
|
||||
export const auditResourceSearchVisible = ref(false)
|
||||
export const auditResourceSearchType = ref<AuditSearchResourceType>('iot_card')
|
||||
export const auditResourceSearchKeyword = ref('')
|
||||
|
||||
export const openAuditInvestigation = (target: AuditInvestigationTarget) => {
|
||||
auditResourceSearchVisible.value = false
|
||||
auditInvestigationTarget.value = target
|
||||
auditInvestigationVisible.value = true
|
||||
}
|
||||
|
||||
export const openAuditResourceSearch = (
|
||||
resourceType: AuditSearchResourceType,
|
||||
keyword?: string | null
|
||||
) => {
|
||||
auditInvestigationVisible.value = false
|
||||
auditResourceSearchType.value = resourceType
|
||||
auditResourceSearchKeyword.value = keyword?.trim() || ''
|
||||
auditResourceSearchVisible.value = true
|
||||
}
|
||||
24
src/components/business/audit/types.ts
Normal file
24
src/components/business/audit/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { AuditActorKind, AuditSubjectResourceType } from '@/types/api'
|
||||
|
||||
export type AuditFinanceField =
|
||||
| 'shop_id'
|
||||
| 'wallet_id'
|
||||
| 'order_id'
|
||||
| 'order_no'
|
||||
| 'payment_id'
|
||||
| 'payment_no'
|
||||
| 'refund_id'
|
||||
| 'refund_no'
|
||||
| 'recharge_id'
|
||||
| 'recharge_no'
|
||||
| 'approval_instance_id'
|
||||
| 'third_party_trade_no'
|
||||
| 'correlation_id'
|
||||
|
||||
export type AuditInvestigationTarget =
|
||||
| { mode: 'event'; id: string }
|
||||
| { mode: 'actor'; id: string; actorKind: AuditActorKind }
|
||||
| { mode: 'resource'; id: string; resourceType: string }
|
||||
| { mode: 'agent' | 'enterprise'; id: string; resourceType: AuditSubjectResourceType }
|
||||
| { mode: 'finance'; field: AuditFinanceField; value: string | number }
|
||||
| { mode: 'request' | 'correlation' | 'integration'; id: string }
|
||||
@@ -39,10 +39,10 @@
|
||||
})
|
||||
|
||||
// 合并默认配置和自定义配置
|
||||
const config = reactive({
|
||||
const config = computed(() => ({
|
||||
placeholder: `${t('table.searchBar.searchSelectPlaceholder')}${prop.item.label}`,
|
||||
...(prop.item.config || {})
|
||||
})
|
||||
}))
|
||||
|
||||
// 选择框值变化处理函数
|
||||
const changeValue = (val: unknown): void => {
|
||||
|
||||
@@ -65,12 +65,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 通知 -->
|
||||
<!--<div class="btn-box notice-btn" @click="visibleNotice">-->
|
||||
<!-- <div class="btn notice-button">-->
|
||||
<!-- <i class="iconfont-sys notice-btn"></i>-->
|
||||
<!-- <span class="count notice-btn"></span>-->
|
||||
<!-- </div>-->
|
||||
<!--</div>-->
|
||||
<div class="btn-box notice-btn" @click.stop="showNotice = !showNotice">
|
||||
<div class="btn notice-button">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span v-if="unreadCount > 0" class="notice-count">{{ formattedUnreadCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 聊天 -->
|
||||
<!--<div class="btn-box chat-btn" @click="openChat">-->
|
||||
<!-- <div class="btn chat-button">-->
|
||||
@@ -175,11 +175,12 @@
|
||||
</div>
|
||||
<ArtWorkTab />
|
||||
|
||||
<art-notification v-model:value="showNotice" ref="notice" />
|
||||
<art-notification v-model="showNotice" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { MenuTypeEnum, MenuWidth } from '@/enums/appEnum'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
@@ -189,12 +190,14 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { mittBus } from '@/utils/sys'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { useNotificationStore } from '@/store/modules/notification'
|
||||
import AppConfig from '@/config'
|
||||
const isWindows = navigator.userAgent.includes('Windows')
|
||||
const { locale } = useI18n()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const userStore = useUserStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
@@ -214,7 +217,6 @@
|
||||
const { menuList } = storeToRefs(useMenuStore())
|
||||
|
||||
const showNotice = ref(false)
|
||||
const notice = ref(null)
|
||||
const userMenuPopover = ref()
|
||||
|
||||
const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT)
|
||||
@@ -229,6 +231,9 @@
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
const unreadCount = computed(() => notificationStore.unreadCount)
|
||||
const formattedUnreadCount = computed(() => notificationStore.displayCount)
|
||||
|
||||
const menuTopWidth = computed(() => {
|
||||
return width.value * 0.5
|
||||
})
|
||||
@@ -251,11 +256,16 @@
|
||||
|
||||
onMounted(() => {
|
||||
initLanguage()
|
||||
document.addEventListener('click', bodyCloseNotice)
|
||||
void notificationStore.refreshUnreadCount().catch(() => undefined)
|
||||
unreadTimer = window.setInterval(() => {
|
||||
void notificationStore.refreshUnreadCount().catch(() => undefined)
|
||||
}, 30_000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', bodyCloseNotice)
|
||||
let unreadTimer: number | undefined
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (unreadTimer !== undefined) window.clearInterval(unreadTimer)
|
||||
})
|
||||
|
||||
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
|
||||
@@ -334,24 +344,6 @@
|
||||
mittBus.emit('openSearchDialog')
|
||||
}
|
||||
|
||||
const bodyCloseNotice = (e: any) => {
|
||||
let { className } = e.target
|
||||
|
||||
if (showNotice.value) {
|
||||
if (typeof className === 'object') {
|
||||
showNotice.value = false
|
||||
return
|
||||
}
|
||||
if (className.indexOf('notice-btn') === -1) {
|
||||
showNotice.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const openChat = () => {
|
||||
mittBus.emit('openChat')
|
||||
}
|
||||
|
||||
const lockScreen = () => {
|
||||
mittBus.emit('openLockScreen')
|
||||
}
|
||||
|
||||
@@ -200,6 +200,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.notice-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.chat-button:hover {
|
||||
i {
|
||||
animation: shake 0.5s ease-in-out;
|
||||
@@ -411,6 +415,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-count {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 0;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: var(--el-color-danger);
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate180 {
|
||||
|
||||
@@ -1,276 +1,181 @@
|
||||
<template>
|
||||
<div
|
||||
class="notice"
|
||||
v-show="visible"
|
||||
:style="{
|
||||
transform: show ? 'scaleY(1)' : 'scaleY(0.9)',
|
||||
opacity: show ? 1 : 0
|
||||
}"
|
||||
@click.stop=""
|
||||
>
|
||||
<div class="header">
|
||||
<span class="text">{{ $t('notice.title') }}</span>
|
||||
<span class="btn">{{ $t('notice.btnRead') }}</span>
|
||||
</div>
|
||||
<ul class="bar">
|
||||
<li
|
||||
v-for="(item, index) in barList"
|
||||
:key="index"
|
||||
:class="{ active: barActiveIndex === index }"
|
||||
@click="changeBar(index)"
|
||||
>
|
||||
{{ item.name }} ({{ item.num }})
|
||||
</li>
|
||||
</ul>
|
||||
<div class="content">
|
||||
<div class="scroll">
|
||||
<!-- 通知 -->
|
||||
<ul class="notice-list" v-show="barActiveIndex === 0">
|
||||
<li v-for="(item, index) in noticeList" :key="index">
|
||||
<div
|
||||
class="icon"
|
||||
:style="{ background: getNoticeStyle(item.type).backgroundColor + '!important' }"
|
||||
>
|
||||
<i
|
||||
class="iconfont-sys"
|
||||
:style="{ color: getNoticeStyle(item.type).iconColor + '!important' }"
|
||||
v-html="getNoticeStyle(item.type).icon"
|
||||
>
|
||||
</i>
|
||||
</div>
|
||||
<div class="text">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.time }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 消息 -->
|
||||
<ul class="user-list" v-show="barActiveIndex === 1">
|
||||
<li v-for="(item, index) in msgList" :key="index">
|
||||
<div class="avatar">
|
||||
<span>{{ item.title.slice(0, 1) }}</span>
|
||||
</div>
|
||||
<div class="text">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.time }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 待办 -->
|
||||
<ul class="base" v-show="barActiveIndex === 3">
|
||||
<li v-for="(item, index) in pendingList" :key="index">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.time }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="visible" class="notice-overlay" @click="close">
|
||||
<div class="notice" @click.stop>
|
||||
<div class="header">
|
||||
<span class="text">通知</span>
|
||||
<button class="read-all" type="button" @click="handleMarkAllRead">全部已读</button>
|
||||
</div>
|
||||
|
||||
<div class="empty-tips" v-show="barActiveIndex === 0 && noticeList.length === 0">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||
<div class="bar">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
type="button"
|
||||
:class="{ active: activeCategory === tab.value }"
|
||||
@click="handleCategoryChange(tab.value)"
|
||||
>
|
||||
{{ tab.label }}<span v-if="tab.count"> ({{ tab.count }})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div ref="notificationListRef" v-loading="notificationStore.loading" class="scroll">
|
||||
<button
|
||||
v-for="item in filteredItems"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
:class="['notification-item', { unread: !isRead(item) }]"
|
||||
@click="handleNotificationClick(item)"
|
||||
>
|
||||
<span :class="['icon', `severity-${item.severity || 'info'}`]">
|
||||
<i class="iconfont-sys"></i>
|
||||
</span>
|
||||
<span class="text">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<small>{{ item.body }}</small>
|
||||
<time>{{ formatDateTime(item.created_at) }}</time>
|
||||
</span>
|
||||
<span v-if="!isRead(item)" class="unread-dot" />
|
||||
</button>
|
||||
|
||||
<div v-if="!filteredItems.length" class="empty-tips">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>暂无通知</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-tips" v-show="barActiveIndex === 1 && msgList.length === 0">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||
</div>
|
||||
<div class="empty-tips" v-show="barActiveIndex === 2 && pendingList.length === 0">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||
<div
|
||||
v-if="notificationStore.notificationTotal > notificationStore.notificationPageSize"
|
||||
class="pagination"
|
||||
>
|
||||
<ElPagination
|
||||
:current-page="notificationStore.notificationPage"
|
||||
:page-size="notificationStore.notificationPageSize"
|
||||
:total="notificationStore.notificationTotal"
|
||||
:pager-count="5"
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
small
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-wrapper">
|
||||
<el-button class="view-all" @click="handleViewAll" v-ripple>
|
||||
{{ $t('notice.viewAll') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 100px"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { NotificationService } from '@/api/modules'
|
||||
import type { NotificationCategory, NotificationItem } from '@/types/api'
|
||||
import { useNotificationStore } from '@/store/modules/notification'
|
||||
import { navigateNotificationTarget } from '@/utils/business/notificationNavigation'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
const router = useRouter()
|
||||
const notificationStore = useNotificationStore()
|
||||
const activeCategory = ref<'all' | NotificationCategory>('all')
|
||||
const notificationListRef = ref<HTMLElement | null>(null)
|
||||
const visible = computed(() => props.modelValue)
|
||||
|
||||
const categoryMatches = (item: NotificationItem, category: string) => {
|
||||
if (category === 'all') return true
|
||||
if (category === 'system') return item.category === 'sync' || item.category === 'system'
|
||||
if (category === 'expiry') return item.category === 'expiry' || item.category === 'expiring'
|
||||
return item.category === category
|
||||
}
|
||||
|
||||
const isRead = (item: NotificationItem) => item.is_read
|
||||
|
||||
const filteredItems = computed(() =>
|
||||
notificationStore.recentNotifications.filter((item) =>
|
||||
categoryMatches(item, activeCategory.value)
|
||||
)
|
||||
)
|
||||
|
||||
const tabs = computed(() => {
|
||||
return [
|
||||
{ value: 'all', label: '全部', count: notificationStore.summary.total },
|
||||
{ value: 'approval', label: '审批', count: notificationStore.summary.approval },
|
||||
{ value: 'expiry', label: '临期', count: notificationStore.summary.expiry },
|
||||
{
|
||||
value: 'system',
|
||||
label: '同步/系统',
|
||||
count: notificationStore.summary.sync + notificationStore.summary.system
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const close = () => emit('update:modelValue', false)
|
||||
|
||||
const loadNotificationPage = async (page: number) => {
|
||||
try {
|
||||
await notificationStore.loadNotifications(page)
|
||||
notificationListRef.value?.scrollTo({ top: 0 })
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '获取通知失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCategoryChange = (category: 'all' | NotificationCategory) => {
|
||||
activeCategory.value = category
|
||||
void loadNotificationPage(1)
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
void loadNotificationPage(page)
|
||||
}
|
||||
|
||||
const handleMarkAllRead = async () => {
|
||||
try {
|
||||
const response = await notificationStore.markAllRead()
|
||||
if (response.code !== 0) ElMessage.error(response.msg || '全部已读失败')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '全部已读失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleNotificationClick = async (item: NotificationItem) => {
|
||||
try {
|
||||
const targetResponse = await NotificationService.getTarget(item.id)
|
||||
const readResponse = await notificationStore.markRead(item.id)
|
||||
if (readResponse.code !== 0) {
|
||||
ElMessage.error(readResponse.msg || '标记已读失败')
|
||||
return
|
||||
}
|
||||
if (targetResponse.code !== 0 || !targetResponse.data) {
|
||||
ElMessage.info(item.body || '该通知暂无可跳转目标')
|
||||
return
|
||||
}
|
||||
if (!navigateNotificationTarget(router, item.ref_type, targetResponse.data)) {
|
||||
ElMessage.info(item.body || '该通知暂无可跳转目标')
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '处理通知失败')
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
showNotice(props.value)
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (open) {
|
||||
activeCategory.value = 'all'
|
||||
void notificationStore.refreshSummary().catch((error) => {
|
||||
ElMessage.error(error?.message || '获取通知失败')
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const show = ref(false)
|
||||
const visible = ref(false)
|
||||
const barActiveIndex = ref(0)
|
||||
const pendingList: any = []
|
||||
|
||||
const barList = ref([
|
||||
{
|
||||
name: computed(() => t('notice.bar[0]')),
|
||||
num: 1
|
||||
},
|
||||
{
|
||||
name: computed(() => t('notice.bar[1]')),
|
||||
num: 1
|
||||
},
|
||||
{
|
||||
name: computed(() => t('notice.bar[2]')),
|
||||
num: 0
|
||||
}
|
||||
])
|
||||
|
||||
const noticeList = [
|
||||
{
|
||||
title: '新增国际化',
|
||||
time: '2024-6-13 0:10',
|
||||
type: 'notice'
|
||||
},
|
||||
{
|
||||
title: '冷月呆呆给你发了一条消息',
|
||||
time: '2024-4-21 8:05',
|
||||
type: 'message'
|
||||
},
|
||||
{
|
||||
title: '小肥猪关注了你',
|
||||
time: '2020-3-17 21:12',
|
||||
type: 'collection'
|
||||
},
|
||||
{
|
||||
title: '新增使用文档',
|
||||
time: '2024-02-14 0:20',
|
||||
type: 'notice'
|
||||
},
|
||||
{
|
||||
title: '小肥猪给你发了一封邮件',
|
||||
time: '2024-1-20 0:15',
|
||||
type: 'email'
|
||||
},
|
||||
{
|
||||
title: '菜单mock本地真实数据',
|
||||
time: '2024-1-17 22:06',
|
||||
type: 'notice'
|
||||
}
|
||||
]
|
||||
const msgList: any = [
|
||||
{
|
||||
title: '池不胖 关注了你',
|
||||
time: '2021-2-26 23:50'
|
||||
},
|
||||
{
|
||||
title: '唐不苦 关注了你',
|
||||
time: '2021-2-21 8:05'
|
||||
},
|
||||
{
|
||||
title: '中小鱼 关注了你',
|
||||
time: '2020-1-17 21:12'
|
||||
},
|
||||
{
|
||||
title: '何小荷 关注了你',
|
||||
time: '2021-01-14 0:20'
|
||||
},
|
||||
{
|
||||
title: '誶誶淰 关注了你',
|
||||
time: '2020-12-20 0:15'
|
||||
},
|
||||
{
|
||||
title: '冷月呆呆 关注了你',
|
||||
time: '2020-12-17 22:06'
|
||||
}
|
||||
]
|
||||
|
||||
const changeBar = (index: number) => {
|
||||
barActiveIndex.value = index
|
||||
}
|
||||
|
||||
const getRandomColor = () => {
|
||||
const index = Math.floor(Math.random() * AppConfig.systemMainColor.length)
|
||||
return AppConfig.systemMainColor[index]
|
||||
}
|
||||
|
||||
const noticeStyleMap = {
|
||||
email: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-warning))',
|
||||
backgroundColor: 'rgb(var(--art-bg-warning))'
|
||||
},
|
||||
message: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-success))',
|
||||
backgroundColor: 'rgb(var(--art-bg-success))'
|
||||
},
|
||||
collection: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-danger))',
|
||||
backgroundColor: 'rgb(var(--art-bg-danger))'
|
||||
},
|
||||
user: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-info))',
|
||||
backgroundColor: 'rgb(var(--art-bg-info))'
|
||||
},
|
||||
notice: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-primary))',
|
||||
backgroundColor: 'rgb(var(--art-bg-primary))'
|
||||
}
|
||||
}
|
||||
|
||||
const getNoticeStyle = (type: string) => {
|
||||
const defaultStyle = {
|
||||
icon: '',
|
||||
iconColor: '#FFFFFF',
|
||||
backgroundColor: getRandomColor()
|
||||
}
|
||||
|
||||
const style = noticeStyleMap[type as keyof typeof noticeStyleMap] || defaultStyle
|
||||
|
||||
return {
|
||||
...style,
|
||||
backgroundColor: style.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
const showNotice = (open: boolean) => {
|
||||
if (open) {
|
||||
visible.value = open
|
||||
setTimeout(() => {
|
||||
show.value = open
|
||||
}, 5)
|
||||
} else {
|
||||
show.value = open
|
||||
setTimeout(() => {
|
||||
visible.value = open
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
|
||||
// 查看全部
|
||||
const handleViewAll = () => {
|
||||
switch (barActiveIndex.value) {
|
||||
case 0:
|
||||
handleNoticeAll()
|
||||
break
|
||||
case 1:
|
||||
handleMsgAll()
|
||||
break
|
||||
case 2:
|
||||
handlePendingAll()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleNoticeAll = () => {}
|
||||
const handleMsgAll = () => {}
|
||||
const handlePendingAll = () => {}
|
||||
onMounted(() => {
|
||||
void notificationStore.refreshUnreadCount().catch(() => undefined)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,262 +1,182 @@
|
||||
@use '@styles/variables.scss' as *;
|
||||
@use '@styles/mixin.scss' as *;
|
||||
|
||||
.notice-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.notice {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
width: 360px;
|
||||
height: 500px;
|
||||
overflow: hidden;
|
||||
right: 18px;
|
||||
width: 390px;
|
||||
max-width: calc(100vw - 24px);
|
||||
background: var(--art-main-bg-color);
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 6px) !important;
|
||||
box-shadow:
|
||||
0 8px 26px -4px hsl(0deg 0% 8% / 15%),
|
||||
0 8px 9px -5px hsl(0deg 0% 8% / 6%);
|
||||
transition: all 0.2s;
|
||||
transform-origin: center top 0;
|
||||
will-change: top, left;
|
||||
border-radius: calc(var(--custom-radius) / 2 + 6px);
|
||||
box-shadow: 0 8px 26px -4px hsl(0deg 0% 8% / 15%);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
margin-top: 15px;
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-800);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--art-gray-200);
|
||||
}
|
||||
}
|
||||
.text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bar {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
padding: 0 15px;
|
||||
line-height: 50px;
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
|
||||
li {
|
||||
height: 48px;
|
||||
margin-right: 20px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-700);
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
|
||||
@include userSelect;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--art-gray-900);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--main-color) !important;
|
||||
border-bottom: 2px solid var(--main-color);
|
||||
}
|
||||
}
|
||||
.read-all {
|
||||
padding: 4px 8px;
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: calc(100% - 95px);
|
||||
.bar {
|
||||
display: flex;
|
||||
padding: 0 12px;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
|
||||
.scroll {
|
||||
height: calc(100% - 60px);
|
||||
overflow-y: scroll;
|
||||
button {
|
||||
flex: 0 0 auto;
|
||||
padding: 13px 8px 11px;
|
||||
margin-right: 12px;
|
||||
color: var(--art-gray-700);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px !important;
|
||||
}
|
||||
|
||||
.notice-list {
|
||||
li {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--art-gray-100);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
width: calc(100% - 45px);
|
||||
margin-left: 15px;
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: var(--art-gray-900);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: var(--art-gray-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-list {
|
||||
li {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--art-gray-100);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
width: calc(100% - 45px);
|
||||
margin-left: 15px;
|
||||
|
||||
h4 {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: var(--art-gray-900);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: var(--art-gray-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.base {
|
||||
li {
|
||||
box-sizing: border-box;
|
||||
padding: 15px 20px;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: var(--art-gray-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tips {
|
||||
position: relative;
|
||||
top: 100px;
|
||||
height: 100%;
|
||||
color: var(--art-gray-500);
|
||||
text-align: center;
|
||||
background: transparent !important;
|
||||
|
||||
i {
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 15px;
|
||||
font-size: 12px;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-wrapper {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
|
||||
.view-all {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
}
|
||||
&.active {
|
||||
color: var(--el-color-primary);
|
||||
border-bottom-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.notice {
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--art-main-bg-color);
|
||||
.content {
|
||||
.scroll {
|
||||
height: 390px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px 8px;
|
||||
overflow-x: auto;
|
||||
border-top: 1px solid var(--art-border-color);
|
||||
|
||||
:deep(.el-pagination) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #222 !important;
|
||||
&.unread {
|
||||
background: color-mix(in srgb, var(--el-color-primary) 5%, transparent);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--el-color-info);
|
||||
background: var(--el-color-info-light-9);
|
||||
border-radius: 8px;
|
||||
|
||||
&.severity-warning {
|
||||
color: var(--el-color-warning);
|
||||
background: var(--el-color-warning-light-9);
|
||||
}
|
||||
|
||||
&.severity-error {
|
||||
color: var(--el-color-danger);
|
||||
background: var(--el-color-danger-light-9);
|
||||
}
|
||||
|
||||
&.severity-critical {
|
||||
color: var(--el-color-danger);
|
||||
background: var(--el-color-danger-light-9);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
|
||||
strong,
|
||||
small,
|
||||
time {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
small,
|
||||
time {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.unread-dot {
|
||||
flex: 0 0 auto;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-top: 5px;
|
||||
background: var(--el-color-danger);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tips {
|
||||
padding: 70px 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
font-size: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-phone) {
|
||||
.notice {
|
||||
top: 65px;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
top: 62px;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" title="设置实名认证策略" width="35%">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
title="设置实名认证策略"
|
||||
width="40%"
|
||||
modal-class="asset-information-dialog"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||
<ElFormItem label="资产标识">
|
||||
<span style="font-weight: bold; color: #409eff">{{ assetIdentifier }}</span>
|
||||
</ElFormItem>
|
||||
@@ -9,7 +14,7 @@
|
||||
{{ getPolicyName(currentPolicy) }}
|
||||
</ElTag>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="实名认证策略" prop="realname_policy">
|
||||
<ElFormItem label="认证策略" prop="realname_policy">
|
||||
<ElRadioGroup v-model="form.realname_policy">
|
||||
<ElRadio value="none">无需实名</ElRadio>
|
||||
<ElRadio value="before_order">先实名后充值/购买</ElRadio>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" title="切换SIM卡" width="600px" @close="handleClose">
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
title="切换SIM卡"
|
||||
width="40%"
|
||||
modal-class="asset-information-dialog"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="loading" style="padding: 40px; text-align: center">
|
||||
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
|
||||
@@ -24,7 +30,7 @@
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
label-width="90px"
|
||||
>
|
||||
<ElFormItem label="设备信息">
|
||||
<span style="font-weight: bold; color: #409eff">{{ deviceInfo }}</span>
|
||||
|
||||
178
src/composables/useAsyncTaskPolling.ts
Normal file
178
src/composables/useAsyncTaskPolling.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
type Ref,
|
||||
type ShallowRef
|
||||
} from 'vue'
|
||||
import { isAsyncTaskActive, isAsyncTaskTerminal, type AsyncTaskProgress } from '@/types/api'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
|
||||
const POLL_DELAYS = [2000, 3000, 5000]
|
||||
const MAX_POLL_DELAY = 10000
|
||||
|
||||
export interface AsyncTaskPollingOptions<T extends AsyncTaskProgress> {
|
||||
storageKey: string
|
||||
fetchTask: (taskId: number) => Promise<T>
|
||||
isForbidden?: (error: unknown) => boolean
|
||||
autoRestore?: boolean
|
||||
}
|
||||
|
||||
export interface AsyncTaskPollingState<T extends AsyncTaskProgress> {
|
||||
taskId: Ref<number | null>
|
||||
task: ShallowRef<T | null>
|
||||
loading: Ref<boolean>
|
||||
forbidden: Ref<boolean>
|
||||
error: Ref<string | null>
|
||||
isActive: Readonly<Ref<boolean>>
|
||||
start: (taskId: number) => Promise<void>
|
||||
retry: () => Promise<void>
|
||||
stop: () => void
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
const readStoredTaskId = (storageKey: string): number | null => {
|
||||
try {
|
||||
const value = Number(localStorage.getItem(storageKey))
|
||||
return Number.isInteger(value) && value > 0 ? value : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const writeStoredTaskId = (storageKey: string, taskId: number | null) => {
|
||||
try {
|
||||
if (taskId) localStorage.setItem(storageKey, String(taskId))
|
||||
else localStorage.removeItem(storageKey)
|
||||
} catch {
|
||||
// Storage may be unavailable in private browsing or restricted webviews.
|
||||
}
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: unknown) => normalizeApiError(error).message
|
||||
|
||||
export function useAsyncTaskPolling<T extends AsyncTaskProgress>(
|
||||
options: AsyncTaskPollingOptions<T>
|
||||
): AsyncTaskPollingState<T> {
|
||||
const taskId = ref<number | null>(readStoredTaskId(options.storageKey))
|
||||
const task = shallowRef<T | null>(null)
|
||||
const loading = ref(false)
|
||||
const forbidden = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const isActive = computed(() => isAsyncTaskActive(task.value?.status))
|
||||
|
||||
let timer: number | undefined
|
||||
let pollIndex = 0
|
||||
let requestInFlight = false
|
||||
|
||||
const clearTimer = () => {
|
||||
if (timer !== undefined) {
|
||||
window.clearTimeout(timer)
|
||||
timer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
clearTimer()
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
stop()
|
||||
taskId.value = null
|
||||
task.value = null
|
||||
error.value = null
|
||||
forbidden.value = false
|
||||
writeStoredTaskId(options.storageKey, null)
|
||||
}
|
||||
|
||||
const schedule = () => {
|
||||
clearTimer()
|
||||
if (document.hidden || !isActive.value || !taskId.value) return
|
||||
const delay = POLL_DELAYS[pollIndex] ?? MAX_POLL_DELAY
|
||||
pollIndex += 1
|
||||
timer = window.setTimeout(() => {
|
||||
timer = undefined
|
||||
void refresh()
|
||||
}, delay)
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
if (!taskId.value || requestInFlight || document.hidden) return
|
||||
|
||||
requestInFlight = true
|
||||
loading.value = true
|
||||
forbidden.value = false
|
||||
error.value = null
|
||||
try {
|
||||
const nextTask = await options.fetchTask(taskId.value)
|
||||
task.value = nextTask
|
||||
if (isAsyncTaskTerminal(nextTask.status)) {
|
||||
stop()
|
||||
writeStoredTaskId(options.storageKey, null)
|
||||
} else {
|
||||
schedule()
|
||||
}
|
||||
} catch (requestError) {
|
||||
if (options.isForbidden?.(requestError) ?? false) {
|
||||
forbidden.value = true
|
||||
stop()
|
||||
} else {
|
||||
error.value = getErrorMessage(requestError)
|
||||
stop()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
requestInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
const start = async (nextTaskId: number) => {
|
||||
if (!Number.isInteger(nextTaskId) || nextTaskId <= 0) return
|
||||
taskId.value = nextTaskId
|
||||
task.value = null
|
||||
pollIndex = 0
|
||||
writeStoredTaskId(options.storageKey, nextTaskId)
|
||||
await refresh()
|
||||
}
|
||||
|
||||
const retry = async () => {
|
||||
if (!taskId.value) return
|
||||
await refresh()
|
||||
}
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
if (taskId.value && isActive.value) {
|
||||
pollIndex = 0
|
||||
void refresh()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
if (options.autoRestore !== false && taskId.value) void refresh()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stop()
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
})
|
||||
|
||||
return {
|
||||
taskId,
|
||||
task: task as ShallowRef<T | null>,
|
||||
loading,
|
||||
forbidden,
|
||||
error,
|
||||
isActive,
|
||||
start,
|
||||
retry,
|
||||
stop,
|
||||
clear
|
||||
}
|
||||
}
|
||||
60
src/config/constants/audit.ts
Normal file
60
src/config/constants/audit.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/** 八月审计链路页面与按钮权限。 */
|
||||
export const AUDIT_PERMISSIONS = {
|
||||
centerPage: 'audit:center_view',
|
||||
eventList: 'audit:event_list',
|
||||
eventDetail: 'audit:event_detail',
|
||||
auditEventResourceTimeline: 'audit:audit_event_resource_timeline',
|
||||
auditEventIntegrationDetail: 'audit:audit_event_integration_detail',
|
||||
integrationDetailRequestTimeline: 'audit:integration_detail_request_timeline',
|
||||
integrationDetailCorrelationTimeline: 'audit:integration_detail_correlation_timeline',
|
||||
integrationDetailResourceTimeline: 'audit:integration_detail_resource_timeline',
|
||||
actorTimeline: 'audit:actor_timeline',
|
||||
resourceSearch: 'audit:resource_search',
|
||||
resourceTimeline: 'audit:resource_timeline',
|
||||
requestTimeline: 'audit:request_timeline',
|
||||
correlationTimeline: 'audit:correlation_timeline',
|
||||
financeTimeline: 'audit:finance_timeline',
|
||||
riskPage: 'audit:risk_view',
|
||||
riskEvents: 'audit:risk_events',
|
||||
riskEventDetail: 'audit:risk_event_detail',
|
||||
integrationPage: 'audit:integration_view',
|
||||
integrationDetail: 'audit:integration_detail',
|
||||
agentActivity: 'audit:agent_resource_activity',
|
||||
enterpriseActivity: 'audit:enterprise_resource_activity',
|
||||
agentCardActivity: 'audit:agent_card_activity',
|
||||
agentDeviceActivity: 'audit:agent_device_activity',
|
||||
enterpriseCardActivity: 'audit:enterprise_card_activity',
|
||||
enterpriseDeviceActivity: 'audit:enterprise_device_activity',
|
||||
agentExchangeActivity: 'audit:agent_exchange_activity',
|
||||
cardEntry: 'audit:card_entry',
|
||||
deviceEntry: 'audit:device_entry',
|
||||
exchangeEntry: 'audit:exchange_entry',
|
||||
exchangeOldAssetEntry: 'audit:exchange_old_asset_entry',
|
||||
exchangeNewAssetEntry: 'audit:exchange_new_asset_entry',
|
||||
agentAssetAllocationActivity: 'audit:agent_asset_allocation_activity',
|
||||
assetAllocationEntry: 'audit:asset_allocation_entry',
|
||||
assetAllocationAssetEntry: 'audit:asset_allocation_asset_entry',
|
||||
assetInfoEntry: 'audit:asset_info_entry',
|
||||
agentAssetInfoCardActivity: 'audit:agent_asset_info_card_activity',
|
||||
agentAssetInfoDeviceActivity: 'audit:agent_asset_info_device_activity',
|
||||
enterpriseAssetInfoCardActivity: 'audit:enterprise_asset_info_card_activity',
|
||||
enterpriseAssetInfoDeviceActivity: 'audit:enterprise_asset_info_device_activity',
|
||||
assetInfoBindingCardEntry: 'audit:asset_info_binding_card_entry',
|
||||
agentAssetInfoBindingCardActivity: 'audit:agent_asset_info_binding_card_activity',
|
||||
enterpriseAssetInfoBindingCardActivity: 'audit:enterprise_asset_info_binding_card_activity',
|
||||
assetInfoWalletFinance: 'audit:asset_info_wallet_finance',
|
||||
assetInfoWalletAsset: 'audit:asset_info_wallet_asset',
|
||||
shopEntry: 'audit:shop_entry',
|
||||
enterpriseEntry: 'audit:enterprise_entry',
|
||||
orderEntry: 'audit:order_entry',
|
||||
orderFinanceEntry: 'audit:order_finance_entry',
|
||||
agentRechargeEntry: 'audit:agent_recharge_entry',
|
||||
agentRechargeFinanceEntry: 'audit:agent_recharge_finance_entry',
|
||||
agentRechargeApprovalEntry: 'audit:agent_recharge_approval_entry',
|
||||
refundEntry: 'audit:refund_entry',
|
||||
refundFinanceEntry: 'audit:refund_finance_entry',
|
||||
refundApprovalEntry: 'audit:refund_approval_entry',
|
||||
walletEntry: 'audit:wallet_entry'
|
||||
} as const
|
||||
|
||||
export type AuditPermission = (typeof AUDIT_PERMISSIONS)[keyof typeof AUDIT_PERMISSIONS]
|
||||
7
src/config/constants/bulkPurchase.ts
Normal file
7
src/config/constants/bulkPurchase.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const BULK_PURCHASE_PERMISSIONS = {
|
||||
page: 'bulk_purchase:view',
|
||||
create: 'bulk_purchase:create',
|
||||
detail: 'bulk_purchase:detail',
|
||||
items: 'bulk_purchase:items',
|
||||
template: 'bulk_purchase:template'
|
||||
} as const
|
||||
@@ -3,7 +3,6 @@ import type { ExportTaskScene } from '@/types/api'
|
||||
export interface ExportTaskScenePermissions {
|
||||
detail: string
|
||||
download: string
|
||||
cancel: string
|
||||
}
|
||||
|
||||
export interface ExportTaskSceneConfig {
|
||||
@@ -20,8 +19,7 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
||||
pageTitle: '导出设备',
|
||||
permissions: {
|
||||
detail: 'export_task:device_detail',
|
||||
download: 'export_task:device_download',
|
||||
cancel: 'export_task:device_cancel'
|
||||
download: 'export_task:device_download'
|
||||
}
|
||||
},
|
||||
iot_card: {
|
||||
@@ -30,8 +28,7 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
||||
pageTitle: '导出IOT卡',
|
||||
permissions: {
|
||||
detail: 'export_task:iot_card_detail',
|
||||
download: 'export_task:iot_card_download',
|
||||
cancel: 'export_task:iot_card_cancel'
|
||||
download: 'export_task:iot_card_download'
|
||||
}
|
||||
},
|
||||
order: {
|
||||
@@ -40,8 +37,52 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
||||
pageTitle: '导出订单',
|
||||
permissions: {
|
||||
detail: 'export_task:order_detail',
|
||||
download: 'export_task:order_download',
|
||||
cancel: 'export_task:order_cancel'
|
||||
download: 'export_task:order_download'
|
||||
}
|
||||
},
|
||||
package: {
|
||||
scene: 'package',
|
||||
sceneName: '套餐管理',
|
||||
pageTitle: '导出套餐',
|
||||
permissions: {
|
||||
detail: 'export_task:package_detail',
|
||||
download: 'export_task:package_download'
|
||||
}
|
||||
},
|
||||
agent_wallet_transaction: {
|
||||
scene: 'agent_wallet_transaction',
|
||||
sceneName: '代理主钱包流水',
|
||||
pageTitle: '导出代理主钱包流水',
|
||||
permissions: {
|
||||
detail: 'export_task:agent_wallet_transaction_detail',
|
||||
download: 'export_task:agent_wallet_transaction_download'
|
||||
}
|
||||
},
|
||||
agent_recharge: {
|
||||
scene: 'agent_recharge',
|
||||
sceneName: '代理充值',
|
||||
pageTitle: '导出代理充值',
|
||||
permissions: {
|
||||
detail: 'export_task:agent_recharge_detail',
|
||||
download: 'export_task:agent_recharge_download'
|
||||
}
|
||||
},
|
||||
refund: {
|
||||
scene: 'refund',
|
||||
sceneName: '退款',
|
||||
pageTitle: '导出退款',
|
||||
permissions: {
|
||||
detail: 'export_task:refund_detail',
|
||||
download: 'export_task:refund_download'
|
||||
}
|
||||
},
|
||||
exchange: {
|
||||
scene: 'exchange',
|
||||
sceneName: '换货',
|
||||
pageTitle: '导出换货',
|
||||
permissions: {
|
||||
detail: 'export_task:exchange_detail',
|
||||
download: 'export_task:exchange_download'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,3 +34,7 @@ export * from './enableStatus'
|
||||
|
||||
// 导出任务相关
|
||||
export * from './exportTask'
|
||||
|
||||
// 批量订购相关
|
||||
export * from './bulkPurchase'
|
||||
export * from './julyIteration'
|
||||
|
||||
40
src/config/constants/julyIteration.ts
Normal file
40
src/config/constants/julyIteration.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 七月迭代新增后台权限编码。
|
||||
* 页面和按钮统一引用这里的常量,后端菜单权限可直接复用同名编码。
|
||||
*/
|
||||
export const JULY_PERMISSIONS = {
|
||||
wecom: {
|
||||
page: 'wecom:config',
|
||||
applicationCreate: 'wecom:application_create',
|
||||
applicationTest: 'wecom:application_test',
|
||||
applicationEdit: 'wecom:application_edit',
|
||||
memberManage: 'wecom:member_manage',
|
||||
memberSync: 'wecom:member_sync',
|
||||
memberDefaultCreator: 'wecom:member_default_creator',
|
||||
sceneCreate: 'wecom:scene_create',
|
||||
sceneEdit: 'wecom:scene_edit',
|
||||
binding: 'wecom:account_binding'
|
||||
},
|
||||
systemConfig: {
|
||||
payment: 'system_config:payment_methods'
|
||||
},
|
||||
speedTier: {
|
||||
view: 'iot_card:speed_tier',
|
||||
update: 'iot_card:speed_tier_update'
|
||||
},
|
||||
deviceAllocation: {
|
||||
page: 'device_task:allocation',
|
||||
create: 'device_task:allocation_create',
|
||||
detail: 'device_task:allocation_detail'
|
||||
},
|
||||
seriesGrants: {
|
||||
updateExpiryBase: 'series_grants:update_expiry_base'
|
||||
},
|
||||
batchPurchase: {
|
||||
page: 'bulk_purchase:view',
|
||||
create: 'bulk_purchase:create',
|
||||
detail: 'bulk_purchase:detail'
|
||||
}
|
||||
} as const
|
||||
|
||||
export type JulyPermission = string
|
||||
@@ -406,6 +406,14 @@
|
||||
"orderList": "Order List",
|
||||
"orderDetail": "Order Details"
|
||||
},
|
||||
"financialManagement": {
|
||||
"title": "Financial Management",
|
||||
"agentRecharge": "Agent Recharge",
|
||||
"agentRechargeDetail": "Agent Recharge Details",
|
||||
"refundManagement": "Refund Management",
|
||||
"refundDetail": "Refund Details",
|
||||
"agentFundOverview": "Agent Fund Overview"
|
||||
},
|
||||
"deviceManagement": {
|
||||
"title": "Device Management",
|
||||
"devices": "Device Management"
|
||||
@@ -432,6 +440,9 @@
|
||||
"standaloneCardList": "IoT Card Management",
|
||||
"iotCardTask": "IoT Card Tasks",
|
||||
"deviceTask": "Device Tasks",
|
||||
"deviceBatchAllocation": "Device Batch Tasks",
|
||||
"bulkPurchase": "Bulk Package Purchase",
|
||||
"bulkPurchaseDetail": "Bulk Purchase Task Detail",
|
||||
"orderPackageInvalidateTask": "Order Package Void Tasks",
|
||||
"orderPackageInvalidateTaskDetail": "Order Package Void Task Detail",
|
||||
"taskDetail": "Task Details",
|
||||
@@ -452,6 +463,11 @@
|
||||
"exportDevice": "Export Devices",
|
||||
"exportIotCard": "Export IOT Cards",
|
||||
"exportOrder": "Export Orders",
|
||||
"exportPackage": "Export Packages",
|
||||
"exportAgentWalletTransaction": "Export Agent Wallet Transactions",
|
||||
"exportRefund": "Export Refunds",
|
||||
"exportAgentRecharge": "Export Agent Recharges",
|
||||
"exportExchange": "Export Exchanges",
|
||||
"exportTaskDetail": "Export Task Detail",
|
||||
"exchangeManagement": "Exchange Management",
|
||||
"exchangeDetail": "Exchange Order Detail"
|
||||
|
||||
@@ -370,6 +370,9 @@
|
||||
"standaloneCardList": "IoT卡管理",
|
||||
"iotCardTask": "IoT卡任务",
|
||||
"deviceTask": "设备任务",
|
||||
"deviceBatchAllocation": "设备批量任务",
|
||||
"bulkPurchase": "批量订购套餐",
|
||||
"bulkPurchaseDetail": "批量订购任务详情",
|
||||
"orderPackageInvalidateTask": "订单套餐批量作废",
|
||||
"orderPackageInvalidateTaskDetail": "订单套餐作废任务详情",
|
||||
"taskDetail": "任务详情",
|
||||
@@ -386,6 +389,11 @@
|
||||
"exportDevice": "导出设备",
|
||||
"exportIotCard": "导出IOT卡",
|
||||
"exportOrder": "导出订单",
|
||||
"exportPackage": "导出套餐",
|
||||
"exportAgentWalletTransaction": "导出代理主钱包流水",
|
||||
"exportRefund": "导出退款",
|
||||
"exportAgentRecharge": "导出代理充值",
|
||||
"exportExchange": "导出换货",
|
||||
"exportTaskDetail": "导出任务详情",
|
||||
"exchangeManagement": "换货管理",
|
||||
"exchangeDetail": "换货单详情"
|
||||
@@ -400,13 +408,13 @@
|
||||
"agentRecharge": "代理充值",
|
||||
"agentRechargeDetail": "代理充值详情",
|
||||
"refundManagement": "退款管理",
|
||||
"refundDetail": "退款详情"
|
||||
"refundDetail": "退款详情",
|
||||
"agentFundOverview": "代理商资金概况"
|
||||
},
|
||||
"commission": {
|
||||
"title": "佣金管理",
|
||||
"withdrawal": "提现审批",
|
||||
"myCommission": "我的佣金",
|
||||
"agentCommission": "代理商资金概况"
|
||||
"myCommission": "我的佣金"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置管理",
|
||||
|
||||
@@ -14,6 +14,7 @@ import { loadingService } from '@/utils/ui'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
import { useWorktabStore } from '@/store/modules/worktab'
|
||||
import { isInWhiteList, hasRoutePermission, isTokenValid, buildLoginRedirect } from './permission'
|
||||
import { isPlatformAuditAccount } from '@/utils/business/auditAccess'
|
||||
|
||||
// 是否已注册动态路由
|
||||
const isRouteRegistered = ref(false)
|
||||
@@ -95,6 +96,14 @@ async function handleRouteGuard(
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
to.path.startsWith('/audit') &&
|
||||
!isPlatformAuditAccount(userStore.info.user_type, userStore.isSuperAdmin)
|
||||
) {
|
||||
next(RoutesAlias.Exception403 || '/exception/403')
|
||||
return
|
||||
}
|
||||
|
||||
// 处理动态路由注册
|
||||
if (!isRouteRegistered.value && userStore.isLogin) {
|
||||
await handleDynamicRoutes(to, router, next)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { RoutesAlias } from '../routesAlias'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants/julyIteration'
|
||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||
|
||||
/**
|
||||
* 菜单列表、异步路由
|
||||
@@ -74,6 +77,18 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'role/detail/:id',
|
||||
name: 'RoleDetail',
|
||||
component: RoutesAlias.RoleDetail,
|
||||
meta: {
|
||||
title: '角色详情',
|
||||
permissions: ['role:detail'],
|
||||
isHide: true,
|
||||
keepAlive: false,
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'permission',
|
||||
name: 'PermissionManagement',
|
||||
@@ -213,6 +228,17 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
title: 'menus.shopManagement.shopList',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 店铺详情
|
||||
{
|
||||
path: 'detail/:id',
|
||||
name: 'ShopDetail',
|
||||
component: RoutesAlias.ShopDetail,
|
||||
meta: {
|
||||
title: '店铺详情',
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -281,6 +307,16 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 临期资产
|
||||
{
|
||||
path: 'expiring-assets',
|
||||
name: 'ExpiringAssets',
|
||||
component: RoutesAlias.ExpiringAssets,
|
||||
meta: {
|
||||
title: '临期资产',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// IoT卡管理
|
||||
{
|
||||
path: 'iot-card-management',
|
||||
@@ -365,17 +401,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 授权记录详情(放在 asset-management 层级)
|
||||
{
|
||||
path: 'record-management/authorization-records/detail/:id',
|
||||
name: 'AuthorizationRecordDetail',
|
||||
component: RoutesAlias.AuthorizationRecordDetail,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.authorizationRecordDetail',
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 任务管理
|
||||
{
|
||||
path: 'task-management',
|
||||
@@ -406,6 +431,28 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 设备批量任务
|
||||
{
|
||||
path: 'device-batch-allocation',
|
||||
name: 'DeviceBatchAllocation',
|
||||
component: RoutesAlias.DeviceBatchAllocation,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.deviceBatchAllocation',
|
||||
permissions: [JULY_PERMISSIONS.deviceAllocation.page],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 批量订购套餐任务
|
||||
{
|
||||
path: 'bulk-purchase',
|
||||
name: 'BulkPurchase',
|
||||
component: RoutesAlias.BulkPurchase,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.bulkPurchase',
|
||||
permissions: [BULK_PURCHASE_PERMISSIONS.page],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 订单套餐批量作废任务
|
||||
{
|
||||
path: 'order-package-invalidate-task',
|
||||
@@ -440,6 +487,18 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 批量订购套餐任务详情
|
||||
{
|
||||
path: 'task-management/bulk-purchase/detail/:id',
|
||||
name: 'BulkPurchaseDetail',
|
||||
component: RoutesAlias.BulkPurchaseDetail,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.bulkPurchaseDetail',
|
||||
isHide: true,
|
||||
permissions: [BULK_PURCHASE_PERMISSIONS.detail],
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 导出管理
|
||||
{
|
||||
path: 'export-task-management',
|
||||
@@ -480,6 +539,56 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-package',
|
||||
name: 'ExportPackageTaskList',
|
||||
component: RoutesAlias.ExportPackageTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportPackage',
|
||||
exportTaskScene: 'package',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-agent-wallet-transaction',
|
||||
name: 'ExportAgentWalletTransactionTaskList',
|
||||
component: RoutesAlias.ExportAgentWalletTransactionTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportAgentWalletTransaction',
|
||||
exportTaskScene: 'agent_wallet_transaction',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-refund',
|
||||
name: 'ExportRefundTaskList',
|
||||
component: RoutesAlias.ExportRefundTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportRefund',
|
||||
exportTaskScene: 'refund',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-agent-recharge',
|
||||
name: 'ExportAgentRechargeTaskList',
|
||||
component: RoutesAlias.ExportAgentRechargeTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportAgentRecharge',
|
||||
exportTaskScene: 'agent_recharge',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-exchange',
|
||||
name: 'ExportExchangeTaskList',
|
||||
component: RoutesAlias.ExportExchangeTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportExchange',
|
||||
exportTaskScene: 'exchange',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-task-detail',
|
||||
name: 'ExportTaskDetail',
|
||||
@@ -580,6 +689,83 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 代理商资金概况
|
||||
{
|
||||
path: 'agent-fund-overview',
|
||||
name: 'AgentFundOverview',
|
||||
component: RoutesAlias.AgentFundOverviewComponent,
|
||||
meta: {
|
||||
title: 'menus.financialManagement.agentFundOverview',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 审计中心
|
||||
{
|
||||
path: '/audit',
|
||||
name: 'AuditCenter',
|
||||
component: RoutesAlias.Home,
|
||||
meta: {
|
||||
title: '审计中心',
|
||||
icon: '',
|
||||
permissions: [AUDIT_PERMISSIONS.centerPage]
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'events',
|
||||
name: 'AuditEvents',
|
||||
component: RoutesAlias.AuditEvents,
|
||||
meta: {
|
||||
title: '审计事件',
|
||||
permissions: [AUDIT_PERMISSIONS.eventList],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'events/:eventId',
|
||||
name: 'AuditEventDetail',
|
||||
component: RoutesAlias.AuditEventDetail,
|
||||
meta: {
|
||||
title: '审计事件详情',
|
||||
permissions: [AUDIT_PERMISSIONS.eventDetail],
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'risks',
|
||||
name: 'AuditRisks',
|
||||
component: RoutesAlias.AuditRisks,
|
||||
meta: {
|
||||
title: '风险中心',
|
||||
permissions: [AUDIT_PERMISSIONS.riskPage],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'integrations',
|
||||
name: 'AuditIntegrations',
|
||||
component: RoutesAlias.AuditIntegrations,
|
||||
meta: {
|
||||
title: '外部交互',
|
||||
permissions: [AUDIT_PERMISSIONS.integrationPage],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'integrations/:integrationId',
|
||||
name: 'AuditIntegrationDetail',
|
||||
component: RoutesAlias.AuditIntegrationDetail,
|
||||
meta: {
|
||||
title: '外部交互详情',
|
||||
permissions: [AUDIT_PERMISSIONS.integrationDetail],
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -615,17 +801,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true,
|
||||
roles: ['R_AGENT']
|
||||
}
|
||||
},
|
||||
// 代理商资金概况
|
||||
{
|
||||
path: 'agent-fund-overview',
|
||||
name: 'AgentFundOverview',
|
||||
component: RoutesAlias.AgentFundOverview,
|
||||
meta: {
|
||||
title: 'menus.commission.agentCommission',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -682,6 +857,60 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
// 系统配置
|
||||
{
|
||||
path: 'system-configs',
|
||||
name: 'SystemConfigs',
|
||||
component: RoutesAlias.SystemConfigs,
|
||||
meta: {
|
||||
title: '系统配置',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wecom',
|
||||
name: 'WecomSettings',
|
||||
redirect: RoutesAlias.WecomApplications,
|
||||
meta: {
|
||||
title: '企业微信配置',
|
||||
isHide: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
// 企业微信应用
|
||||
{
|
||||
path: 'wecom/applications',
|
||||
name: 'WecomApplications',
|
||||
component: RoutesAlias.WecomApplications,
|
||||
meta: {
|
||||
title: '企微应用管理',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
// 企业微信成员及默认发起人
|
||||
{
|
||||
path: 'wecom/members',
|
||||
name: 'WecomMembers',
|
||||
component: RoutesAlias.WecomMembers,
|
||||
meta: {
|
||||
title: '企微成员管理',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
// 企业微信审批场景
|
||||
{
|
||||
path: 'wecom/scenes',
|
||||
name: 'WecomScenes',
|
||||
component: RoutesAlias.WecomScenes,
|
||||
meta: {
|
||||
title: '企微审批场景',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -18,10 +18,18 @@ export enum RoutesAlias {
|
||||
|
||||
// 系统管理
|
||||
Role = '/system/role', // 角色
|
||||
RoleDetail = '/system/role/detail', // 角色详情
|
||||
Permission = '/system/permission', // 权限管理
|
||||
CarrierManagement = '/system/carrier-management', // 运营商管理
|
||||
UserCenter = '/system/user-center', // 用户中心
|
||||
|
||||
// 审计中心
|
||||
AuditEvents = '/audit/events',
|
||||
AuditEventDetail = '/audit/events/detail',
|
||||
AuditRisks = '/audit/risks',
|
||||
AuditIntegrations = '/audit/integrations',
|
||||
AuditIntegrationDetail = '/audit/integrations/detail',
|
||||
|
||||
// 套餐管理
|
||||
PackageList = '/package-management/package-list', // 套餐列表
|
||||
PackageDetail = '/package-management/package-list/detail', // 套餐列表详情
|
||||
@@ -33,6 +41,7 @@ export enum RoutesAlias {
|
||||
|
||||
// 店铺管理
|
||||
Shop = '/shop-management/list', // 店铺列表
|
||||
ShopDetail = '/shop-management/detail', // 店铺详情
|
||||
|
||||
// 通用页面(店铺账号-企业账号)
|
||||
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表和店铺账号列表共用
|
||||
@@ -50,41 +59,55 @@ export enum RoutesAlias {
|
||||
|
||||
// 记录管理
|
||||
AuthorizationRecords = '/asset-management/record-management/authorization-records', // 授权记录
|
||||
AuthorizationRecordDetail = '/asset-management/record-management/authorization-records/detail', // 授权记录详情
|
||||
AssetAssign = '/asset-management/record-management/asset-assign', // 分配记录
|
||||
AssetAssignDetail = '/asset-management/record-management/asset-assign/detail', // 分配记录详情
|
||||
|
||||
// 任务管理
|
||||
IotCardTask = '/asset-management/task-management/iot-card-task', // IoT卡任务
|
||||
DeviceTask = '/asset-management/task-management/device-task', // 设备任务
|
||||
DeviceBatchAllocation = '/asset-management/task-management/device-batch-allocation', // 设备批量任务
|
||||
BulkPurchase = '/asset-management/task-management/bulk-purchase', // 批量订购套餐
|
||||
BulkPurchaseDetail = '/asset-management/task-management/bulk-purchase/detail', // 批量订购套餐详情
|
||||
OrderPackageInvalidateTask = '/asset-management/task-management/order-package-invalidate-task', // 订单套餐批量作废任务
|
||||
OrderPackageInvalidateTaskDetail = '/asset-management/task-management/order-package-invalidate-task/detail', // 订单套餐作废任务详情
|
||||
TaskDetail = '/asset-management/task-management/task-detail', // 任务详情(IoT卡/设备任务详情)
|
||||
ExportDeviceTaskList = '/asset-management/export-task-management/export-device', // 导出设备
|
||||
ExportIotCardTaskList = '/asset-management/export-task-management/export-iot-card', // 导出IOT卡
|
||||
ExportOrderTaskList = '/asset-management/export-task-management/export-order', // 导出订单
|
||||
ExportPackageTaskList = '/asset-management/export-task-management/export-package', // 导出套餐
|
||||
ExportAgentWalletTransactionTaskList = '/asset-management/export-task-management/export-agent-wallet-transaction', // 导出代理主钱包流水
|
||||
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', // 导出换货
|
||||
ExportTaskDetail = '/asset-management/export-task-management/export-task-detail', // 导出任务详情
|
||||
|
||||
// 订单管理
|
||||
OrderList = '/order-management/order-list', // 订单列表
|
||||
OrderDetail = '/order-management/order-list/detail', // 订单详情
|
||||
|
||||
// 财务管理
|
||||
AgentRecharge = '/finance/agent-recharge', // 代理充值
|
||||
AgentRechargeDetail = '/finance/agent-recharge/detail', // 代理充值详情
|
||||
RefundManagement = '/finance/refund', // 退款管理
|
||||
RefundDetail = '/finance/refund/detail', // 退款详情
|
||||
AgentFundOverview = '/finance/agent-fund-overview', // 代理商资金概况
|
||||
|
||||
ExpiringAssets = '/asset-management/expiring-assets', // 临期资产
|
||||
|
||||
// 佣金管理
|
||||
WithdrawalApproval = '/commission-management/withdrawal-approval', // 提现审批
|
||||
MyCommission = '/commission-management/my-commission', // 我的佣金
|
||||
AgentFundOverview = '/commission-management/agent-fund-overview', // 代理商资金概况
|
||||
AgentFundOverviewComponent = '/commission-management/agent-fund-overview', // 代理商资金概况组件路径
|
||||
|
||||
// 设置管理
|
||||
WithdrawalSettings = '/settings/withdrawal-settings', // 提现配置
|
||||
PaymentSettings = '/settings/payment-settings', // 支付设置
|
||||
PaymentSettingsDetail = '/settings/payment-settings/detail', // 支付设置详情
|
||||
OperationPasswordSettings = '/settings/operation-password', // 操作密码设置
|
||||
SystemConfigs = '/settings/system-configs', // 系统配置
|
||||
WecomSettings = '/settings/wecom', // 企业微信配置兼容入口
|
||||
WecomApplications = '/settings/wecom/applications', // 企业微信应用
|
||||
WecomMembers = '/settings/wecom/members', // 企业微信成员
|
||||
WecomScenes = '/settings/wecom/scenes', // 企业微信审批场景
|
||||
|
||||
// 轮询管理
|
||||
DataCleanup = '/polling-management/data-cleanup', // 数据清理
|
||||
|
||||
114
src/store/modules/notification.ts
Normal file
114
src/store/modules/notification.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { NotificationService } from '@/api/modules/notification'
|
||||
import type {
|
||||
BaseResponse,
|
||||
NotificationItem,
|
||||
NotificationListResponse,
|
||||
NotificationUnreadSummary
|
||||
} from '@/types/api'
|
||||
|
||||
const emptySummary = (): NotificationUnreadSummary => ({
|
||||
approval: 0,
|
||||
expiry: 0,
|
||||
sync: 0,
|
||||
system: 0,
|
||||
total: 0
|
||||
})
|
||||
|
||||
export const useNotificationStore = defineStore('notificationStore', () => {
|
||||
const unreadCount = ref(0)
|
||||
const displayCount = ref('0')
|
||||
const summary = ref<NotificationUnreadSummary>(emptySummary())
|
||||
const recentNotifications = ref<NotificationItem[]>([])
|
||||
const notificationPage = ref(1)
|
||||
const notificationPageSize = 10
|
||||
const notificationTotal = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const applyNotificationList = (
|
||||
response: BaseResponse<NotificationListResponse>,
|
||||
fallbackPage: number
|
||||
) => {
|
||||
if (response.code !== 0 || !response.data) return
|
||||
|
||||
recentNotifications.value = response.data.items
|
||||
notificationPage.value = response.data.page || fallbackPage
|
||||
notificationTotal.value = Math.max(0, response.data.total || 0)
|
||||
}
|
||||
|
||||
const refreshUnreadCount = async () => {
|
||||
const response = await NotificationService.getUnreadCount()
|
||||
if (response.code === 0 && response.data) {
|
||||
unreadCount.value = Math.max(0, response.data.count)
|
||||
displayCount.value = response.data.display_count || '0'
|
||||
}
|
||||
return unreadCount.value
|
||||
}
|
||||
|
||||
const loadNotifications = async (page = notificationPage.value) => {
|
||||
const targetPage = Math.max(1, page)
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await NotificationService.getNotifications({
|
||||
page: targetPage,
|
||||
page_size: notificationPageSize
|
||||
})
|
||||
applyNotificationList(response, targetPage)
|
||||
return response
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshSummary = async (page = 1) => {
|
||||
const targetPage = Math.max(1, page)
|
||||
loading.value = true
|
||||
try {
|
||||
const [summaryResponse, listResponse] = await Promise.all([
|
||||
NotificationService.getUnreadSummary(),
|
||||
NotificationService.getNotifications({ page: targetPage, page_size: notificationPageSize })
|
||||
])
|
||||
if (summaryResponse.code === 0 && summaryResponse.data) {
|
||||
summary.value = summaryResponse.data
|
||||
}
|
||||
applyNotificationList(listResponse, targetPage)
|
||||
await refreshUnreadCount()
|
||||
return summary.value
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const markRead = async (id: number) => {
|
||||
const response = await NotificationService.markRead(id)
|
||||
if (response.code === 0) {
|
||||
await refreshSummary(notificationPage.value)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
const markAllRead = async (category?: string) => {
|
||||
const response = await NotificationService.markAllRead(category ? { category } : {})
|
||||
if (response.code === 0) {
|
||||
await refreshSummary(notificationPage.value)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
return {
|
||||
unreadCount,
|
||||
displayCount,
|
||||
summary,
|
||||
recentNotifications,
|
||||
notificationPage,
|
||||
notificationPageSize,
|
||||
notificationTotal,
|
||||
loading,
|
||||
refreshUnreadCount,
|
||||
loadNotifications,
|
||||
refreshSummary,
|
||||
markRead,
|
||||
markAllRead
|
||||
}
|
||||
})
|
||||
@@ -51,7 +51,7 @@ export const useUserStore = defineStore(
|
||||
}
|
||||
|
||||
// 检查是否是超级管理员
|
||||
const isSuperAdmin = computed(() => info.value.user_type === 1)
|
||||
const isSuperAdmin = computed(() => Number(info.value.user_type) === 1)
|
||||
|
||||
// 检查是否有某个权限
|
||||
const hasPermission = (permission: string): boolean => {
|
||||
|
||||
1
src/template/批量订购套餐模板.csv
Normal file
1
src/template/批量订购套餐模板.csv
Normal file
@@ -0,0 +1 @@
|
||||
89861590172420377385
|
||||
|
1
src/template/设备批量任务.csv
Normal file
1
src/template/设备批量任务.csv
Normal file
@@ -0,0 +1 @@
|
||||
89861590172420377385
|
||||
|
@@ -12,7 +12,8 @@ export enum AccountStatus {
|
||||
|
||||
// 账号实体(统一账号模型,匹配后端 ModelAccountResponse)
|
||||
export interface PlatformAccount {
|
||||
ID: number
|
||||
id?: number
|
||||
ID?: number
|
||||
CreatedAt?: string
|
||||
UpdatedAt?: string
|
||||
DeletedAt?: string | null
|
||||
@@ -25,6 +26,10 @@ export interface PlatformAccount {
|
||||
enterprise_name?: string // ⭐ 新增:企业名称
|
||||
shop_id?: number | null // 关联店铺ID
|
||||
shop_name?: string // ⭐ 新增:店铺名称
|
||||
wecom_bound?: boolean // 是否已绑定企业微信账号
|
||||
wecom_corp_id?: string // 企业微信 CorpID
|
||||
wecom_name?: string // 企业微信成员名称
|
||||
wecom_userid?: string // 企业微信成员 UserID
|
||||
status: AccountStatus // 状态 (0:禁用, 1:启用)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,22 @@ export enum AgentRechargeStatus {
|
||||
PAID = 2, // 已支付
|
||||
COMPLETED = 3, // 已完成
|
||||
CLOSED = 4, // 已关闭
|
||||
REFUNDED = 5 // 已退款
|
||||
REFUNDED = 5, // 已退款
|
||||
REJECTED = 6 // 已驳回
|
||||
}
|
||||
|
||||
// 支付方式
|
||||
export type AgentRechargePaymentMethod = 'wechat' | 'offline'
|
||||
export type AgentRechargeOnlinePaymentMethod = 'wechat' | 'alipay'
|
||||
export type AgentRechargePaymentMethod = AgentRechargeOnlinePaymentMethod | 'offline'
|
||||
|
||||
// 充值来源
|
||||
export type AgentRechargeSource = 'agent_online' | 'platform_offline'
|
||||
|
||||
// 第三方支付状态
|
||||
export type AgentRechargePaymentStatus = 0 | 1 | 2 | 3
|
||||
|
||||
// 支付通道
|
||||
export type AgentRechargePaymentChannel = 'wechat_direct' | 'fuyou' | 'offline'
|
||||
export type AgentRechargePaymentChannel = string
|
||||
|
||||
// 代理充值订单
|
||||
export interface AgentRecharge {
|
||||
@@ -30,10 +38,27 @@ export interface AgentRecharge {
|
||||
payment_method: AgentRechargePaymentMethod
|
||||
payment_channel: AgentRechargePaymentChannel
|
||||
payment_config_id: number | null
|
||||
payment_transaction_id: string
|
||||
payment_transaction_id: string | null
|
||||
payment_no?: string | null
|
||||
request_id?: string | null
|
||||
qr_content?: string | null
|
||||
payment_status?: AgentRechargePaymentStatus | null
|
||||
payment_status_name?: string | null
|
||||
recharge_source?: AgentRechargeSource | null
|
||||
recharge_source_name?: string | null
|
||||
payment_voucher_key?: string[] | string // 凭证附件列表;历史数据可能为单字符串或逗号字符串
|
||||
rejection_reason?: string | null // 拒绝原因
|
||||
remark?: string // 运营备注
|
||||
submitter_name?: string | null // 提交人名称
|
||||
submitter_id?: number | null
|
||||
approval_source?: 'none' | 'legacy' | 'wecom' | null // 审批来源
|
||||
approval_provider?: 'wecom' | null
|
||||
approval_instance_id?: number | null
|
||||
approval_status?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | null // 企微审批状态
|
||||
approval_status_name?: string | null // 审批状态名称
|
||||
current_approver_summary?: string | null // 当前审批人摘要
|
||||
processing_status?: string | null // 业务处理状态
|
||||
processing_status_name?: string | null // 业务处理状态名称
|
||||
created_at: string
|
||||
paid_at: string | null
|
||||
completed_at: string | null
|
||||
@@ -46,9 +71,10 @@ export interface AgentRechargeQueryParams {
|
||||
page_size?: number
|
||||
shop_id?: number
|
||||
status?: AgentRechargeStatus
|
||||
recharge_source?: AgentRechargeSource
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
dateRange?: string[] | any // For date range picker in UI
|
||||
dateRange?: string[] // For date range picker in UI
|
||||
}
|
||||
|
||||
// 代理充值订单列表响应
|
||||
@@ -61,14 +87,45 @@ export interface AgentRechargeListResponse {
|
||||
}
|
||||
|
||||
// 创建代理充值订单请求
|
||||
export interface CreateAgentRechargeRequest {
|
||||
export interface CreateAgentRechargeOnlineRequest {
|
||||
amount: number // 充值金额(单位:分),最小 1(即 ¥0.01)
|
||||
payment_method: AgentRechargePaymentMethod
|
||||
payment_method: AgentRechargeOnlinePaymentMethod
|
||||
request_id: string
|
||||
}
|
||||
|
||||
export interface CreateAgentRechargeOfflineRequest {
|
||||
amount: number // 充值金额(单位:分),最小 1(即 ¥0.01)
|
||||
payment_method: 'offline'
|
||||
shop_id: number
|
||||
payment_voucher_key?: string[] // 线下支付凭证附件列表(payment_method=offline 时必填)
|
||||
payment_voucher_key: string[] // 线下支付凭证附件列表(payment_method=offline 时必填)
|
||||
remark?: string // 运营备注,最多 1000 字
|
||||
}
|
||||
|
||||
export type CreateAgentRechargeRequest =
|
||||
| CreateAgentRechargeOnlineRequest
|
||||
| CreateAgentRechargeOfflineRequest
|
||||
|
||||
// 在线充值可用支付方式和金额范围
|
||||
export interface AgentRechargePaymentMethods {
|
||||
methods: AgentRechargeOnlinePaymentMethod[]
|
||||
min_amount: number
|
||||
max_amount: number
|
||||
}
|
||||
|
||||
// 在线充值支付及钱包到账状态
|
||||
export interface AgentRechargePaymentStatusResponse {
|
||||
recharge_id: number
|
||||
recharge_no: string
|
||||
recharge_source: AgentRechargeSource
|
||||
recharge_source_name?: string | null
|
||||
status: AgentRechargeStatus
|
||||
status_name?: string | null
|
||||
payment_status: AgentRechargePaymentStatus
|
||||
payment_status_name?: string | null
|
||||
paid_at: string | null
|
||||
completed_at: string | null
|
||||
}
|
||||
|
||||
// 确认线下充值请求
|
||||
export interface ConfirmOfflinePaymentRequest {
|
||||
operation_password: string
|
||||
|
||||
@@ -10,6 +10,8 @@ type ExpiryBase = 'from_activation' | 'from_purchase'
|
||||
|
||||
export type AssetType = 'card' | 'device'
|
||||
|
||||
import type { AllowedPaymentMethod } from './systemConfig'
|
||||
|
||||
// 网络状态
|
||||
export enum NetworkStatus {
|
||||
OFFLINE = 0, // 停机
|
||||
@@ -46,6 +48,107 @@ export interface AssetResolveParams {
|
||||
include_usage_summary?: boolean // 是否返回当前世代流量汇总字段
|
||||
}
|
||||
|
||||
// 资产轮询状态
|
||||
export interface AssetPollingStatus {
|
||||
enabled: boolean // 是否启用轮询
|
||||
activity_level: string // 活跃级别
|
||||
last_activity_at?: string | null // 最后活跃时间
|
||||
last_activity_scene?: string | null // 最后活跃场景
|
||||
next_poll_at?: string | null // 下次轮询时间
|
||||
}
|
||||
|
||||
// 资产复机结果
|
||||
export interface AssetStartResponse {
|
||||
status: number // 最新资产状态
|
||||
status_name: string // 最新资产状态名称
|
||||
real_name_status: RealNameStatus // 最新实名状态
|
||||
real_name_status_name: string // 最新实名状态名称
|
||||
}
|
||||
|
||||
// 资产实名认证策略
|
||||
export type AssetRealnamePolicy = 'none' | 'before_order' | 'after_order'
|
||||
|
||||
// 预计最终到期时间状态
|
||||
export type ExpiryEstimateStatus = 'exact' | 'waiting_activation' | 'none' | 'invalid_data'
|
||||
|
||||
// 临期等级由后端返回,前端仅用于展示和筛选结果标识
|
||||
export type ExpiryLevel = 'red' | 'purple' | 'pink' | string
|
||||
|
||||
export type ExpiringAssetType = 'iot_card' | 'device'
|
||||
|
||||
export interface ExpiringAssetItem {
|
||||
asset_id: number
|
||||
asset_type: ExpiringAssetType
|
||||
days_until_final_expiry: number | null
|
||||
estimated_final_expires_at: string | null
|
||||
expiry_estimate_status: ExpiryEstimateStatus
|
||||
expiry_estimate_status_name: string
|
||||
expiry_level: ExpiryLevel
|
||||
expiry_level_name: string
|
||||
identifier: string
|
||||
is_expiring: boolean
|
||||
is_priority: boolean
|
||||
package_id?: number
|
||||
package_name: string
|
||||
package_usage_id?: number
|
||||
shop_id?: number | null
|
||||
shop_name: string
|
||||
}
|
||||
|
||||
export interface ExpiringAssetSummary {
|
||||
card_count: number
|
||||
device_count: number
|
||||
total_count: number
|
||||
window_days: number
|
||||
}
|
||||
|
||||
export interface ExpiringAssetListResponse {
|
||||
items: ExpiringAssetItem[]
|
||||
page: number
|
||||
size: number
|
||||
summary: ExpiringAssetSummary
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface ExpiringAssetQueryParams {
|
||||
asset_type?: ExpiringAssetType
|
||||
keyword?: string
|
||||
shop_id?: number
|
||||
package_id?: number
|
||||
days_min?: number
|
||||
days_max?: number
|
||||
expires_from?: string
|
||||
expires_to?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
// 批量更新资产实名认证策略请求
|
||||
export interface BatchUpdateAssetRealnamePolicyRequest {
|
||||
asset_ids: number[]
|
||||
realname_policy: AssetRealnamePolicy
|
||||
}
|
||||
|
||||
export interface BatchUpdateAssetRealnamePolicyResponse {
|
||||
realname_policy: AssetRealnamePolicy
|
||||
success_count: number
|
||||
}
|
||||
|
||||
// 换货关联资产
|
||||
export interface AssetExchangeTraceAsset {
|
||||
asset_type: string
|
||||
asset_id: number | null
|
||||
identifier: string
|
||||
exchange_no: string
|
||||
can_view: boolean
|
||||
}
|
||||
|
||||
// 换货前后代链路
|
||||
export interface AssetExchangeTrace {
|
||||
previous_asset?: AssetExchangeTraceAsset | null
|
||||
next_asset?: AssetExchangeTraceAsset | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产解析/详情响应
|
||||
* 对应接口:GET /api/admin/assets/resolve/:identifier
|
||||
@@ -62,7 +165,10 @@ export interface AssetResolveResponse {
|
||||
series_id: number // 套餐系列 ID
|
||||
series_name: string // 套餐系列名称
|
||||
real_name_status: RealNameStatus // 实名状态:0 未实名 / 1 已实名
|
||||
realname_policy?: string // 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
realname_policy?: AssetRealnamePolicy // 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
effective_realname_policy?: AssetRealnamePolicy
|
||||
realname_required?: boolean
|
||||
allowed_payment_methods?: AllowedPaymentMethod[]
|
||||
real_name_at?: string | null // 实名时间
|
||||
network_status?: NetworkStatus // 网络状态:0 停机 / 1 开机(仅 card)
|
||||
current_package: string // 当前套餐名称(无则空)
|
||||
@@ -73,6 +179,11 @@ export interface AssetResolveResponse {
|
||||
virtual_used_mb: number // 展示已用量
|
||||
total_virtual_used_mb?: number | null // 所有套餐已用量(MB),未请求时为 null
|
||||
total_virtual_remaining_mb?: number | null // 所有套餐剩余量(MB),未请求时为 null
|
||||
estimated_final_expires_at?: string | null // 当前及排队主套餐接续后的预计最终到期时间
|
||||
days_until_final_expiry?: number | null // 距预计最终到期的剩余天数
|
||||
expiry_estimate_status?: ExpiryEstimateStatus // 预计最终到期时间状态
|
||||
expiry_estimate_status_name?: string | null // 预计最终到期时间状态名称
|
||||
is_expiring?: boolean // 是否临期,由后端判断
|
||||
reduction_pct: number // 展示增幅比例(小数,如 0.428571)
|
||||
device_protect_status?: DeviceProtectStatus // 保护期状态:none / stop / start(仅 device)
|
||||
activated_at: string // 激活时间
|
||||
@@ -80,6 +191,8 @@ export interface AssetResolveResponse {
|
||||
updated_at: string // 更新时间
|
||||
accumulated_recharge?: number // 累计充值金额(分)
|
||||
first_commission_paid?: boolean // 一次性佣金是否已发放
|
||||
polling?: AssetPollingStatus | null // 资产轮询状态
|
||||
exchange_trace?: AssetExchangeTrace | null // 换货前后代链路
|
||||
|
||||
// ===== 卡专属字段 (asset_type === 'card' 时) =====
|
||||
iccid?: string // ICCID
|
||||
@@ -126,7 +239,7 @@ export interface AssetBoundCard {
|
||||
network_status: NetworkStatus // 网络状态
|
||||
real_name_status: RealNameStatus // 实名状态
|
||||
real_name_at?: string | null // 实名时间
|
||||
realname_policy?: string // 实名认证策略
|
||||
realname_policy?: AssetRealnamePolicy // 实名认证策略
|
||||
slot_position: number // 插槽位置
|
||||
is_current?: boolean // 是否为设备当前使用的卡
|
||||
gateway_extend?: string // Gateway 卡状态扩展字段,原样返回上游 extend
|
||||
@@ -187,7 +300,7 @@ export interface AssetRealtimeStatusResponse {
|
||||
// ===== 卡专属字段 =====
|
||||
network_status?: NetworkStatus // 网络状态(仅 card)
|
||||
real_name_status?: RealNameStatus // 实名状态(仅 card)
|
||||
realname_policy?: string // 实名认证策略(仅 card)
|
||||
realname_policy?: AssetRealnamePolicy // 实名认证策略(仅 card)
|
||||
gateway_card_imei?: string // Gateway 返回的卡 IMEI
|
||||
current_month_usage_mb?: number // 本月已用流量 MB(仅 card)
|
||||
last_gateway_reading_mb?: number // 运营商周期月内已用流量 MB(仅 card)
|
||||
@@ -393,7 +506,7 @@ export interface AssetWalletResponse {
|
||||
resource_type?: string // 资源类型:iot_card 或 device
|
||||
balance?: number // 总余额(分)
|
||||
frozen_balance?: number // 冻结余额(分)
|
||||
available_balance?: number // 可用余额 = balance - frozen_balance(分)
|
||||
available_balance?: number // 后端返回的可用余额(分),前端不得自行计算
|
||||
currency?: string // 币种,目前固定 CNY
|
||||
status?: number // 钱包状态:1-正常 2-冻结 3-关闭
|
||||
status_text?: string // 状态文本
|
||||
@@ -481,52 +594,3 @@ export interface DtoUpdateAssetRealnameStatusResponse {
|
||||
real_name_status: number // 更新后的实名状态 (0:未实名, 1:已实名)
|
||||
real_name_status_name: string // 实名状态名称(中文)
|
||||
}
|
||||
|
||||
// ========== 资产操作审计日志 ==========
|
||||
|
||||
/**
|
||||
* 操作审计日志项
|
||||
*/
|
||||
export interface AssetOperationLogItem {
|
||||
id: number // 日志ID
|
||||
asset_id: number // 资产ID
|
||||
asset_identifier: string // 资产标识符
|
||||
asset_type: string // 资产类型
|
||||
operation_type: string // 操作类型
|
||||
operation_desc: string // 操作描述
|
||||
result_status: 'success' | 'failed' | 'denied' // 执行结果
|
||||
created_at: string // 创建时间
|
||||
operator_id?: number // 操作人ID
|
||||
operator_name: string // 操作人名称
|
||||
operator_type: string // 操作人类型
|
||||
before_data: Record<string, any> // 操作前数据(原始)
|
||||
after_data: Record<string, any> // 操作后数据(原始)
|
||||
operation_content_before?: Record<string, any> // 操作内容(变更前)
|
||||
operation_content_after?: Record<string, any> // 操作内容(变更后)
|
||||
operation_fields_desc?: Record<string, string> // 字段中文说明
|
||||
batch_total: number // 批量总数
|
||||
success_count: number // 成功数量
|
||||
fail_count: number // 失败数量
|
||||
failed_items?: Array<{ iccid?: string; reason?: string }> // 失败明细
|
||||
reason?: string // 原因说明
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产操作审计日志响应
|
||||
*/
|
||||
export interface AssetOperationLogsResponse {
|
||||
items: AssetOperationLogItem[] // 日志列表
|
||||
page: number // 当前页码
|
||||
page_size: number // 每页数量
|
||||
total: number // 总记录数
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产操作审计日志查询参数
|
||||
*/
|
||||
export interface AssetOperationLogsParams {
|
||||
page?: number // 页码,默认1
|
||||
page_size?: number // 每页条数,默认20,最大100
|
||||
operation_type?: string // 操作类型
|
||||
result_status?: 'success' | 'failed' | 'denied' // 执行结果
|
||||
}
|
||||
|
||||
28
src/types/api/asyncTask.ts
Normal file
28
src/types/api/asyncTask.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export enum AsyncTaskStatus {
|
||||
PENDING = 1,
|
||||
PROCESSING = 2,
|
||||
COMPLETED = 3,
|
||||
FAILED = 4,
|
||||
CANCELED = 5
|
||||
}
|
||||
|
||||
export interface AsyncTaskProgress {
|
||||
task_id?: number
|
||||
status: number
|
||||
status_name?: string
|
||||
total_count?: number
|
||||
success_count?: number
|
||||
failed_count?: number
|
||||
failure_details?: unknown[] | null
|
||||
error_code?: string
|
||||
error_summary?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export const isAsyncTaskActive = (status?: AsyncTaskStatus) =>
|
||||
status === AsyncTaskStatus.PENDING || status === AsyncTaskStatus.PROCESSING
|
||||
|
||||
export const isAsyncTaskTerminal = (status?: AsyncTaskStatus) =>
|
||||
status === AsyncTaskStatus.COMPLETED ||
|
||||
status === AsyncTaskStatus.FAILED ||
|
||||
status === AsyncTaskStatus.CANCELED
|
||||
449
src/types/api/audit.ts
Normal file
449
src/types/api/audit.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import type { PaginationParams } from './common'
|
||||
|
||||
export type AuditActorKind =
|
||||
| 'account'
|
||||
| 'personal_customer'
|
||||
| 'openapi'
|
||||
| 'system_task'
|
||||
| 'scheduled_job'
|
||||
| 'external_system'
|
||||
export type AuditResult = 'success' | 'failed' | 'denied' | 'partial' | 'unknown'
|
||||
export type AuditRiskLevel = 'low' | 'normal' | 'high' | 'critical'
|
||||
export type AuditCategory =
|
||||
| 'configuration'
|
||||
| 'reliability'
|
||||
| 'asset'
|
||||
| 'security'
|
||||
| 'identity'
|
||||
| 'business'
|
||||
export type AuditSource =
|
||||
| 'admin_api'
|
||||
| 'personal_api'
|
||||
| 'openapi'
|
||||
| 'worker'
|
||||
| 'scheduler'
|
||||
| 'callback'
|
||||
export type AuditScopeType = 'platform' | 'shop' | 'personal_customer'
|
||||
export type IntegrationProvider =
|
||||
| 'ctcc'
|
||||
| 'cmcc'
|
||||
| 'cucc'
|
||||
| 'wechat_pay'
|
||||
| 'alipay'
|
||||
| 'fuiou'
|
||||
| 'wecom'
|
||||
| 'gateway'
|
||||
export type IntegrationDirection = 'inbound' | 'outbound'
|
||||
export type IntegrationResult =
|
||||
| 'pending'
|
||||
| 'success'
|
||||
| 'failed'
|
||||
| 'unknown'
|
||||
| 'not_found'
|
||||
| 'invalid_payload'
|
||||
| 'conflict'
|
||||
| 'ignored'
|
||||
| 'merged'
|
||||
| 'rate_limited'
|
||||
| 'completed'
|
||||
| 'cancelled'
|
||||
export type IntegrationResultCategory =
|
||||
| 'processing'
|
||||
| 'succeeded'
|
||||
| 'indeterminate'
|
||||
| 'failed'
|
||||
| 'not_sent'
|
||||
export type AuditSubjectResourceType =
|
||||
| 'iot_card'
|
||||
| 'device'
|
||||
| 'asset_allocation_record'
|
||||
| 'exchange_order'
|
||||
| 'shop'
|
||||
| 'enterprise'
|
||||
|
||||
export interface AuditRetentionInfo {
|
||||
online_from: string
|
||||
archived_before: string
|
||||
timezone: string
|
||||
}
|
||||
export interface AuditActorRef {
|
||||
kind: AuditActorKind
|
||||
id: string
|
||||
}
|
||||
export interface AuditInvestigationResourceRef {
|
||||
resource_type: string
|
||||
resource_id?: string | null
|
||||
resource_key?: string | null
|
||||
display_name?: string | null
|
||||
}
|
||||
export interface AuditInvestigationRefs {
|
||||
event_id?: string | null
|
||||
actor_ref?: AuditActorRef | null
|
||||
resource_refs?: AuditInvestigationResourceRef[]
|
||||
request_id?: string | null
|
||||
correlation_id?: string | null
|
||||
integration_refs?: Array<{ integration_id: string }>
|
||||
}
|
||||
export interface AuditResourceView extends AuditInvestigationResourceRef {
|
||||
relation: 'primary' | 'affected' | 'reference'
|
||||
role: string
|
||||
display_name: string
|
||||
subject_summary?: string | null
|
||||
subject_visibility?: 'internal_only' | 'subject_result' | 'subject_detail'
|
||||
identity_snapshot?: Record<string, unknown> | null
|
||||
before_data?: Record<string, unknown> | null
|
||||
after_data?: Record<string, unknown> | null
|
||||
subject_data?: Record<string, unknown> | null
|
||||
sort_order?: number | null
|
||||
created_at?: string | null
|
||||
}
|
||||
export interface AuditEventView {
|
||||
event_id: string
|
||||
parent_event_id?: string | null
|
||||
action_code: string
|
||||
action_name: string
|
||||
category: AuditCategory
|
||||
summary: string
|
||||
result: AuditResult
|
||||
risk_level: AuditRiskLevel
|
||||
source: AuditSource
|
||||
actor_kind: AuditActorKind
|
||||
actor_id: string
|
||||
actor_name: string
|
||||
actor_shop_id?: number | null
|
||||
actor_shop_name?: string | null
|
||||
actor_enterprise_id?: number | null
|
||||
actor_enterprise_name?: string | null
|
||||
scope_type: AuditScopeType
|
||||
scope_id?: string | null
|
||||
scope_name?: string | null
|
||||
request_id?: string | null
|
||||
correlation_id?: string | null
|
||||
request_method?: string | null
|
||||
request_path?: string | null
|
||||
ip_address?: string | null
|
||||
user_agent?: string | null
|
||||
success_count?: number | null
|
||||
fail_count?: number | null
|
||||
batch_total?: number | null
|
||||
error_code?: string | null
|
||||
error_summary?: string | null
|
||||
content_hash?: string | null
|
||||
occurred_at: string
|
||||
created_at: string
|
||||
resources: AuditResourceView[]
|
||||
investigation_refs?: AuditInvestigationRefs | null
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
export interface AuditEventDetail extends AuditEventView {
|
||||
retention: AuditRetentionInfo
|
||||
}
|
||||
export interface AuditPage<T> {
|
||||
items: T[]
|
||||
page: number
|
||||
page_size: number
|
||||
total: number
|
||||
retention: AuditRetentionInfo
|
||||
}
|
||||
export type AuditEventPage = AuditPage<AuditEventView>
|
||||
export interface AuditEventQuery extends PaginationParams {
|
||||
created_from?: string
|
||||
created_to?: string
|
||||
action?: string
|
||||
category?: AuditCategory
|
||||
actor_kind?: AuditActorKind
|
||||
actor_id?: string
|
||||
source?: AuditSource
|
||||
result?: AuditResult
|
||||
risk?: AuditRiskLevel
|
||||
scope_type?: AuditScopeType
|
||||
scope_id?: string
|
||||
resource_type?: string
|
||||
resource_id?: string
|
||||
resource_key?: string
|
||||
request_id?: string
|
||||
correlation_id?: string
|
||||
}
|
||||
export interface AuditActorEventQuery extends PaginationParams {
|
||||
action?: string
|
||||
result?: AuditResult
|
||||
risk?: AuditRiskLevel
|
||||
resource_type?: string
|
||||
resource_id?: string
|
||||
created_from?: string
|
||||
created_to?: string
|
||||
}
|
||||
export interface AuditResourceTimelineQuery extends PaginationParams {
|
||||
created_from?: string
|
||||
created_to?: string
|
||||
action?: string
|
||||
result?: AuditResult
|
||||
}
|
||||
export type AuditSearchResourceType = 'iot_card' | 'device' | 'shop' | 'order' | 'refund'
|
||||
export interface AuditResourceSearchQuery extends PaginationParams {
|
||||
resource_type: AuditSearchResourceType
|
||||
keyword: string
|
||||
}
|
||||
export interface AuditResourceCandidate extends AuditInvestigationResourceRef {
|
||||
resource_type: AuditSearchResourceType
|
||||
display_name: string
|
||||
historical: boolean
|
||||
identity_snapshot?: Record<string, unknown> | null
|
||||
}
|
||||
export type AuditResourceSearchPage = AuditPage<AuditResourceCandidate>
|
||||
|
||||
export interface AuditLinkTimelineNode {
|
||||
node_id: string
|
||||
record_source: 'audit_event' | 'integration_log' | 'outbox_event'
|
||||
code: string
|
||||
title: string
|
||||
summary: string
|
||||
result: string
|
||||
result_name: string
|
||||
occurred_at: string
|
||||
request_id?: string | null
|
||||
correlation_id?: string | null
|
||||
parent_event_id?: string | null
|
||||
reference_only?: boolean
|
||||
resources?: AuditInvestigationResourceRef[]
|
||||
investigation_refs?: AuditInvestigationRefs | null
|
||||
fidelity?: Record<string, boolean | null> | null
|
||||
}
|
||||
export interface AuditLinkTimeline {
|
||||
request_id?: string | null
|
||||
correlation_id?: string | null
|
||||
access_log_lookup_request_id?: string | null
|
||||
nodes: AuditLinkTimelineNode[]
|
||||
retention: AuditRetentionInfo
|
||||
}
|
||||
|
||||
export interface AuditFinanceQuery extends PaginationParams {
|
||||
shop_id?: number
|
||||
wallet_id?: number
|
||||
order_id?: number
|
||||
order_no?: string
|
||||
payment_id?: number
|
||||
payment_no?: string
|
||||
refund_id?: number
|
||||
refund_no?: string
|
||||
recharge_id?: number
|
||||
recharge_no?: string
|
||||
approval_instance_id?: number
|
||||
third_party_trade_no?: string
|
||||
actor_kind?: AuditActorKind
|
||||
actor_id?: string
|
||||
correlation_id?: string
|
||||
created_from?: string
|
||||
created_to?: string
|
||||
}
|
||||
export interface AuditFinanceTimelineNode {
|
||||
node_id: string
|
||||
record_source: string
|
||||
code: string
|
||||
title: string
|
||||
result: string
|
||||
result_name: string
|
||||
occurred_at: string
|
||||
shop_id?: number | null
|
||||
amount?: number | null
|
||||
balance_before?: number | null
|
||||
balance_after?: number | null
|
||||
currency?: string | null
|
||||
wallet?: { resource_type: 'agent_wallet' | 'asset_wallet'; wallet_id: number } | null
|
||||
amount_authority?: {
|
||||
authoritative: boolean
|
||||
table?: string
|
||||
field?: string
|
||||
conflict_rule?: string
|
||||
} | null
|
||||
facts?: Record<string, unknown> | null
|
||||
investigation_refs?: AuditInvestigationRefs | null
|
||||
}
|
||||
export type AuditFinanceTimelinePage = AuditPage<AuditFinanceTimelineNode>
|
||||
|
||||
export interface AuditRiskQuery {
|
||||
created_from?: string
|
||||
created_to?: string
|
||||
risk?: AuditRiskLevel
|
||||
result?: AuditResult
|
||||
action?: string
|
||||
source?: AuditSource
|
||||
}
|
||||
export interface AuditRiskEventQuery extends AuditRiskQuery, PaginationParams {}
|
||||
export interface AuditNamedCount {
|
||||
code: string
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
export interface AuditRiskOverview {
|
||||
total: number
|
||||
bucket: 'hour' | 'day'
|
||||
risks: AuditNamedCount[]
|
||||
results: AuditNamedCount[]
|
||||
actions: AuditNamedCount[]
|
||||
sources: AuditNamedCount[]
|
||||
signals: AuditNamedCount[]
|
||||
trend: Array<Record<string, string | number>>
|
||||
retention: AuditRetentionInfo
|
||||
}
|
||||
export type AuditRiskEventPage = AuditPage<AuditEventView>
|
||||
|
||||
export interface IntegrationQuery extends PaginationParams {
|
||||
created_from?: string
|
||||
created_to?: string
|
||||
integration_id?: string
|
||||
provider?: IntegrationProvider
|
||||
direction?: IntegrationDirection
|
||||
operation?: string
|
||||
result?: IntegrationResult
|
||||
result_category?: IntegrationResultCategory
|
||||
external_id?: string
|
||||
resource_type?: string
|
||||
resource_id?: string
|
||||
resource_key?: string
|
||||
trigger_source?: string
|
||||
trigger_scene?: string
|
||||
trigger_series?: string
|
||||
state_changed?: boolean
|
||||
http_status?: number
|
||||
provider_code?: string
|
||||
request_id?: string
|
||||
correlation_id?: string
|
||||
}
|
||||
export interface IntegrationResourceView {
|
||||
type?: string | null
|
||||
id?: string | null
|
||||
key?: string | null
|
||||
}
|
||||
export interface IntegrationIdentityView {
|
||||
integration_id: string
|
||||
provider: IntegrationProvider
|
||||
provider_name: string
|
||||
direction: IntegrationDirection
|
||||
direction_name: string
|
||||
operation: string
|
||||
operation_name: string
|
||||
external_id?: string | null
|
||||
}
|
||||
export interface IntegrationResultView {
|
||||
category: IntegrationResultCategory
|
||||
code: IntegrationResult
|
||||
name: string
|
||||
duration_ms: number
|
||||
http_status?: number | null
|
||||
provider_code?: string | null
|
||||
provider_message?: string | null
|
||||
recovery_strategy?: string | null
|
||||
state_changed: boolean
|
||||
}
|
||||
export interface IntegrationLinkageView {
|
||||
audit_event_id?: number | null
|
||||
request_id?: string | null
|
||||
correlation_id?: string | null
|
||||
}
|
||||
export interface IntegrationTriggerView {
|
||||
attempt: number
|
||||
scene?: string | null
|
||||
series?: string | null
|
||||
source?: string | null
|
||||
}
|
||||
export interface IntegrationTimestampView {
|
||||
created_at: string
|
||||
scheduled_at?: string | null
|
||||
started_at?: string | null
|
||||
updated_at: string
|
||||
}
|
||||
export interface IntegrationContentView {
|
||||
content_hash: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
request_summary?: Record<string, unknown> | null
|
||||
response_summary?: Record<string, unknown> | null
|
||||
}
|
||||
export interface IntegrationFidelityView {
|
||||
attempt_sequence_reliable: boolean
|
||||
correlation_available: boolean
|
||||
provider_message_fidelity: string
|
||||
resource_id_available: boolean
|
||||
trigger_series_available: boolean
|
||||
}
|
||||
export interface IntegrationAttemptView {
|
||||
attempt: number
|
||||
created_at: string
|
||||
duration_ms: number
|
||||
integration_id: string
|
||||
operation: string
|
||||
operation_name: string
|
||||
result: IntegrationResult
|
||||
result_category: IntegrationResultCategory
|
||||
result_name: string
|
||||
sent: boolean
|
||||
state_changed: boolean
|
||||
}
|
||||
export interface IntegrationListItem {
|
||||
integration_id: string
|
||||
provider: IntegrationProvider
|
||||
provider_name: string
|
||||
direction: IntegrationDirection
|
||||
direction_name: string
|
||||
operation: string
|
||||
operation_name: string
|
||||
result: IntegrationResult
|
||||
result_name: string
|
||||
result_category: IntegrationResultCategory
|
||||
state_changed: boolean
|
||||
duration_ms?: number | null
|
||||
request_id?: string | null
|
||||
correlation_id?: string | null
|
||||
created_at: string
|
||||
resource?: IntegrationResourceView | null
|
||||
}
|
||||
export type IntegrationListPage = AuditPage<IntegrationListItem>
|
||||
export interface IntegrationOverview {
|
||||
total: number
|
||||
anomaly_count: number
|
||||
stale_pending_count: number
|
||||
state_changed_count: number
|
||||
unknown_count: number
|
||||
average_duration_ms: number
|
||||
p95_duration_ms: number
|
||||
providers: AuditNamedCount[]
|
||||
directions: AuditNamedCount[]
|
||||
results: Array<AuditNamedCount & { category: IntegrationResultCategory }>
|
||||
trend: Array<Record<string, string | number>>
|
||||
retention: AuditRetentionInfo
|
||||
}
|
||||
export interface IntegrationDetailResponse {
|
||||
identity: IntegrationIdentityView
|
||||
result: IntegrationResultView
|
||||
resource?: IntegrationResourceView | null
|
||||
linkage: IntegrationLinkageView
|
||||
trigger: IntegrationTriggerView
|
||||
timestamps: IntegrationTimestampView
|
||||
content: IntegrationContentView
|
||||
fidelity: IntegrationFidelityView
|
||||
attempts: IntegrationAttemptView[]
|
||||
retention: AuditRetentionInfo
|
||||
}
|
||||
|
||||
export interface AuditSubjectActivityQuery extends PaginationParams {
|
||||
created_from?: string
|
||||
created_to?: string
|
||||
}
|
||||
export interface AuditSubjectResourceSummary {
|
||||
resource_type?: string
|
||||
identifier?: string
|
||||
display_name?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
export interface AuditSubjectActivity {
|
||||
action_code: string
|
||||
action_name: string
|
||||
occurred_at: string
|
||||
result: AuditResult
|
||||
subject_summary: string
|
||||
subject_data?: Record<string, unknown> | null
|
||||
related_resources?: AuditSubjectResourceSummary[]
|
||||
}
|
||||
export interface AuditSubjectActivityPage extends AuditPage<AuditSubjectActivity> {
|
||||
resource: AuditSubjectResourceSummary
|
||||
}
|
||||
91
src/types/api/bulkPurchase.ts
Normal file
91
src/types/api/bulkPurchase.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { BaseResponse } from './common'
|
||||
|
||||
export type BulkPurchasePaymentMethod = 'wallet' | 'offline'
|
||||
|
||||
export interface BulkPurchaseCreateRequest {
|
||||
file_key: string
|
||||
package_id: number
|
||||
payment_method: BulkPurchasePaymentMethod
|
||||
voucher_keys?: string[]
|
||||
}
|
||||
|
||||
export enum BulkPurchaseTaskStatus {
|
||||
PENDING = 1,
|
||||
PROCESSING = 2,
|
||||
COMPLETED = 3,
|
||||
FAILED = 4,
|
||||
CANCELED = 5
|
||||
}
|
||||
|
||||
export interface BulkPurchaseCreateResponse {
|
||||
id?: number
|
||||
task_id?: number
|
||||
task_no?: string
|
||||
status?: BulkPurchaseTaskStatus
|
||||
status_name?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface BulkPurchaseTask {
|
||||
id: number
|
||||
task_id?: number
|
||||
task_no: string
|
||||
file_name: string
|
||||
package_id: number
|
||||
package_code: string
|
||||
package_name: string
|
||||
payment_method: BulkPurchasePaymentMethod
|
||||
status: BulkPurchaseTaskStatus
|
||||
status_name: string
|
||||
total_count: number
|
||||
success_count: number
|
||||
fail_count: number
|
||||
failed_count?: number
|
||||
total_amount?: number
|
||||
success_amount?: number
|
||||
failed_amount?: number
|
||||
error_summary?: string
|
||||
error_message: string
|
||||
creator_name: string
|
||||
voucher_keys: string[] | null
|
||||
started_at: string | null
|
||||
completed_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
items?: BulkPurchaseItem[]
|
||||
}
|
||||
|
||||
export interface BulkPurchaseItem {
|
||||
line: number
|
||||
asset_identifier: string
|
||||
package_code?: string
|
||||
status: BulkPurchaseTaskStatus
|
||||
status_name: string
|
||||
order_id: number
|
||||
order_no: string
|
||||
amount: number
|
||||
reason: string
|
||||
iccid?: string
|
||||
virtual_no?: string
|
||||
error_reason?: string
|
||||
error_summary?: string
|
||||
}
|
||||
|
||||
export interface BulkPurchaseItemsResponse {
|
||||
items: BulkPurchaseItem[]
|
||||
page?: number
|
||||
size?: number
|
||||
page_size?: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
export type BulkPurchaseCreateApiResponse = BaseResponse<BulkPurchaseCreateResponse>
|
||||
export type BulkPurchaseTaskApiResponse = BaseResponse<BulkPurchaseTask>
|
||||
export interface BulkPurchaseTaskListResponse {
|
||||
items: BulkPurchaseTask[]
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type BulkPurchaseTaskListApiResponse = BaseResponse<BulkPurchaseTaskListResponse>
|
||||
@@ -3,6 +3,17 @@
|
||||
*/
|
||||
|
||||
import { PaginationParams, ImportTask } from './common'
|
||||
import type { ExpiryEstimateStatus } from './asset'
|
||||
|
||||
export type SpeedTierCode = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
|
||||
|
||||
export interface SetSpeedTierResponse {
|
||||
code: SpeedTierCode
|
||||
iccid: string
|
||||
integration_id: string
|
||||
iot_card_id: number
|
||||
speed_tier_name: string
|
||||
}
|
||||
|
||||
// 运营商类型
|
||||
export enum Operator {
|
||||
@@ -346,6 +357,7 @@ export enum StandaloneCardStatus {
|
||||
export interface StandaloneCardQueryParams extends PaginationParams {
|
||||
keyword?: string // 通用关键字搜索
|
||||
has_active_package?: boolean // has active package filter
|
||||
real_name_status?: 0 | 1 // 实名状态:0 未实名 / 1 已实名
|
||||
carrier_name?: string // carrier name filter
|
||||
is_standalone?: boolean // standalone filter
|
||||
status?: StandaloneCardStatus // 状态
|
||||
@@ -384,7 +396,8 @@ export interface StandaloneIotCard {
|
||||
status: StandaloneCardStatus // 状态
|
||||
activation_status: number // 激活状态 (0:未激活, 1:已激活)
|
||||
network_status: number // 网络状态 (0:停机, 1:开机)
|
||||
real_name_status: number // 实名状态 (0:未实名, 1:已实名)
|
||||
real_name_status: 0 | 1 // 实名状态 (0:未实名, 1:已实名)
|
||||
real_name_status_name: string // 实名状态名称
|
||||
generation?: number // 资产世代编号(初始值1,每次换货转新后+1)
|
||||
asset_status?: AssetStatus // 业务状态 (1:在库, 2:已销售, 3:已换货, 4:已停用)
|
||||
asset_status_name?: string // 业务状态名称(中文)
|
||||
@@ -397,6 +410,14 @@ export interface StandaloneIotCard {
|
||||
data_usage_mb: number // 累计流量使用(MB)
|
||||
current_month_usage_mb?: number // 自然月累计流量(MB)
|
||||
current_month_start_date?: string | null // 本月开始日期
|
||||
estimated_final_expires_at?: string | null // 当前及排队主套餐接续后的预计最终到期时间
|
||||
days_until_final_expiry?: number | null // 距预计最终到期的剩余天数
|
||||
expiry_estimate_status?: ExpiryEstimateStatus // 预计最终到期时间状态
|
||||
expiry_estimate_status_name?: string | null // 预计最终到期时间状态名称
|
||||
is_expiring?: boolean // 是否临期,由后端判断
|
||||
expiry_level?: string | null // 临期等级
|
||||
expiry_level_name?: string | null // 临期等级名称
|
||||
can_renew?: boolean // 是否允许续费
|
||||
last_month_total_mb?: number // 上月流量总量(MB)
|
||||
last_data_check_at?: string | null // 最后流量检查时间
|
||||
last_real_name_check_at?: string | null // 最后实名检查时间
|
||||
|
||||
@@ -164,6 +164,7 @@ export interface MyCommissionSummary {
|
||||
frozen_commission: number // 冻结中佣金(分)
|
||||
withdrawing_commission: number // 提现中佣金(分)
|
||||
withdrawn_commission: number // 已提现佣金(分)
|
||||
unwithdraw_commission?: number // 未提现佣金(分)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,6 +212,14 @@ export interface ShopFundSummaryItem {
|
||||
phone?: string // 主账号手机号
|
||||
main_balance: number // 预充值余额(分)
|
||||
main_frozen_balance: number // 预充值冻结余额(分)
|
||||
cash_available_balance: number // 现金可用余额(分)
|
||||
low_balance_warning?: boolean // 现金可用余额不足100元预警
|
||||
credit_enabled: boolean // 实际信用额度是否启用
|
||||
credit_limit: number // 实际信用额度(分)
|
||||
available_balance: number // 可用金额(分),以后端返回为准
|
||||
is_in_debt: boolean // 是否欠款,以后端返回为准
|
||||
debt_amount: number // 欠款金额(分),以后端返回为准
|
||||
version: number // 资金概况版本,用于并发控制
|
||||
total_commission: number // 总佣金(分)
|
||||
available_commission: number // 可提现佣金(分)
|
||||
frozen_commission: number // 冻结中佣金(分)
|
||||
@@ -229,6 +238,7 @@ export type ShopCommissionSummaryItem = ShopFundSummaryItem
|
||||
* 代理商资金汇总查询参数(原佣金汇总查询参数)
|
||||
*/
|
||||
export interface ShopFundSummaryQueryParams extends PaginationParams {
|
||||
shop_id?: number // 店铺ID
|
||||
shop_name?: string // 店铺名称
|
||||
username?: string // 用户名
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { PaginationParams } from './common'
|
||||
import type { AssetRealnamePolicy, ExpiryEstimateStatus } from './asset'
|
||||
|
||||
// ========== 设备状态枚举 ==========
|
||||
|
||||
@@ -64,7 +65,17 @@ export interface Device {
|
||||
last_gateway_sync_at?: string | null // 最后 sync-info 同步时间
|
||||
imei?: string // IMEI(设备国际移动设备识别码)
|
||||
sn?: string // 设备序列号
|
||||
realname_policy?: string // 实名认证策略
|
||||
realname_policy?: AssetRealnamePolicy // 实名认证策略
|
||||
real_name_status: 0 | 1 // 实名状态 (0:未实名, 1:已实名)
|
||||
real_name_status_name: string // 实名状态名称
|
||||
estimated_final_expires_at?: string | null // 当前及排队主套餐接续后的预计最终到期时间
|
||||
days_until_final_expiry?: number | null // 距预计最终到期的剩余天数
|
||||
expiry_estimate_status?: ExpiryEstimateStatus // 预计最终到期时间状态
|
||||
expiry_estimate_status_name?: string | null // 预计最终到期时间状态名称
|
||||
is_expiring?: boolean // 是否临期,由后端判断
|
||||
expiry_level?: string | null // 临期等级
|
||||
expiry_level_name?: string | null // 临期等级名称
|
||||
can_renew?: boolean // 是否允许续费
|
||||
authorized_enterprise_id?: number | null // 当前有效授权企业ID
|
||||
authorized_enterprise_name?: string | null // 当前有效授权企业名称
|
||||
}
|
||||
@@ -72,6 +83,7 @@ export interface Device {
|
||||
// 设备查询参数
|
||||
export interface DeviceQueryParams extends PaginationParams {
|
||||
has_active_package?: boolean // has active package filter
|
||||
real_name_status?: 0 | 1 // 实名状态:0 未实名 / 1 已实名
|
||||
virtual_no?: string // 虚拟号(模糊查询,原 device_no)
|
||||
device_name?: string // 设备名称(模糊查询)
|
||||
status?: DeviceStatus // 状态
|
||||
@@ -203,8 +215,11 @@ export enum DeviceImportTaskStatus {
|
||||
}
|
||||
|
||||
// 导入任务查询参数
|
||||
export type DeviceImportTaskOperationType = 'import' | 'assign_shop' | 'assign_series' | 'recall'
|
||||
|
||||
export interface DeviceImportTaskQueryParams extends PaginationParams {
|
||||
status?: DeviceImportTaskStatus // 任务状态
|
||||
operation_type?: DeviceImportTaskOperationType
|
||||
batch_no?: string // 批次号(模糊查询)
|
||||
start_time?: string // 创建时间起始
|
||||
end_time?: string // 创建时间结束
|
||||
@@ -219,12 +234,19 @@ export interface DeviceImportTask {
|
||||
file_key?: string // 原始导入文件存储键
|
||||
creator_name?: string // 创建人姓名
|
||||
status: DeviceImportTaskStatus // 任务状态
|
||||
status_name?: string // 任务状态名称
|
||||
status_text: string // 任务状态文本
|
||||
total_count: number // 总数
|
||||
success_count: number // 成功数
|
||||
fail_count: number // 失败数
|
||||
skip_count: number // 跳过数
|
||||
error_message: string // 错误信息
|
||||
operation_type: DeviceImportTaskOperationType
|
||||
operation_name: string
|
||||
target_id: number | null
|
||||
target_name?: string | null
|
||||
realname_policy: 'none' | 'before_order' | 'after_order'
|
||||
warning_count: number
|
||||
created_at: string // 创建时间
|
||||
started_at: string | null // 开始处理时间
|
||||
completed_at: string | null // 完成时间
|
||||
@@ -232,17 +254,17 @@ export interface DeviceImportTask {
|
||||
|
||||
// 导入任务列表响应
|
||||
export interface DeviceImportTaskListResponse {
|
||||
list: DeviceImportTask[] | null // 任务列表
|
||||
items: DeviceImportTask[] // 任务列表
|
||||
page: number // 当前页码
|
||||
page_size: number // 每页数量
|
||||
size: number // 每页数量
|
||||
total: number // 总数
|
||||
total_pages: number // 总页数
|
||||
}
|
||||
|
||||
// 导入结果详细项
|
||||
export interface DeviceImportResultItem {
|
||||
line: number // 行号
|
||||
virtual_no: string // 虚拟号(原 device_no)
|
||||
device_identifier: string // CSV 原始设备标识
|
||||
virtual_no: string // 设备虚拟号
|
||||
reason: string // 原因
|
||||
}
|
||||
|
||||
@@ -250,6 +272,19 @@ export interface DeviceImportResultItem {
|
||||
export interface DeviceImportTaskDetail extends DeviceImportTask {
|
||||
failed_items: DeviceImportResultItem[] | null // 失败记录详情
|
||||
skipped_items: DeviceImportResultItem[] | null // 跳过记录详情
|
||||
warning_items: DeviceImportResultItem[] | null // 警告记录详情
|
||||
}
|
||||
|
||||
export interface DeviceBatchAllocationRequest {
|
||||
file_key: string
|
||||
operation_type: Exclude<DeviceImportTaskOperationType, 'import'>
|
||||
target_id?: number
|
||||
}
|
||||
|
||||
export interface DeviceBatchAllocationResponse {
|
||||
message: string
|
||||
task_id: number
|
||||
task_no: string
|
||||
}
|
||||
|
||||
// ========== 批量设置设备的套餐系列绑定相关 ==========
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import type { BaseResponse, PaginationParams } from './common'
|
||||
|
||||
export type ExportTaskScene = 'device' | 'iot_card' | 'order'
|
||||
export type ExportTaskScene =
|
||||
| 'device'
|
||||
| 'iot_card'
|
||||
| 'order'
|
||||
| 'package'
|
||||
| 'agent_wallet_transaction'
|
||||
| 'agent_recharge'
|
||||
| 'refund'
|
||||
| 'exchange'
|
||||
|
||||
export type ExportTaskFormat = 'xlsx' | 'csv'
|
||||
|
||||
@@ -21,23 +29,34 @@ export interface ExportTaskQueryParams extends PaginationParams {
|
||||
|
||||
export interface ExportTaskItem {
|
||||
id: number
|
||||
task_id?: number
|
||||
task_no: string
|
||||
scene: ExportTaskScene
|
||||
status: ExportTaskStatus
|
||||
status_name: string
|
||||
progress: number
|
||||
status_name?: string
|
||||
progress?: number
|
||||
format: ExportTaskFormat
|
||||
total_rows: number
|
||||
processed_rows: number
|
||||
total_shards: number
|
||||
success_shards: number
|
||||
failed_shards: number
|
||||
file_key: string
|
||||
error_message: string
|
||||
cancel_requested: boolean
|
||||
total_rows?: number
|
||||
processed_rows?: number
|
||||
total_shards?: number
|
||||
success_shards?: number
|
||||
failed_shards?: number
|
||||
file_key?: string
|
||||
error_message?: string
|
||||
cancel_requested?: boolean
|
||||
total_count?: number
|
||||
success_count?: number
|
||||
failed_count?: number
|
||||
error_code?: string
|
||||
error_summary?: string
|
||||
updated_at?: string
|
||||
created_at: string
|
||||
started_at: string
|
||||
completed_at: string
|
||||
started_at?: string
|
||||
completed_at?: string
|
||||
creator_enterprise_id?: number | null
|
||||
creator_shop_id?: number | null
|
||||
creator_user_id?: number
|
||||
creator_user_type?: number
|
||||
}
|
||||
|
||||
export interface ExportTaskListResponse {
|
||||
@@ -50,7 +69,7 @@ export interface ExportTaskListResponse {
|
||||
export interface CreateExportTaskRequest {
|
||||
scene: ExportTaskScene
|
||||
format: ExportTaskFormat
|
||||
query?: Record<string, unknown>
|
||||
query?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface CreateExportTaskResponse {
|
||||
@@ -66,15 +85,6 @@ export interface ExportTaskDetail extends ExportTaskItem {
|
||||
download_expires_at?: string
|
||||
}
|
||||
|
||||
export interface CancelExportTaskResponse {
|
||||
task_id: number
|
||||
status: ExportTaskStatus
|
||||
status_name: string
|
||||
cancel_requested: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export type ExportTaskListApiResponse = BaseResponse<ExportTaskListResponse>
|
||||
export type CreateExportTaskApiResponse = BaseResponse<CreateExportTaskResponse>
|
||||
export type ExportTaskDetailApiResponse = BaseResponse<ExportTaskDetail>
|
||||
export type CancelExportTaskApiResponse = BaseResponse<CancelExportTaskResponse>
|
||||
|
||||
@@ -78,6 +78,9 @@ export * from './agentRecharge'
|
||||
// 支付设置相关
|
||||
export * from './paymentSettings'
|
||||
|
||||
// 系统配置相关
|
||||
export * from './systemConfig'
|
||||
|
||||
// 退款管理相关
|
||||
export * from './refund'
|
||||
|
||||
@@ -111,5 +114,20 @@ export * from './pollingMonitor'
|
||||
// 导出任务相关
|
||||
export * from './exportTask'
|
||||
|
||||
// 通用异步任务相关
|
||||
export * from './asyncTask'
|
||||
|
||||
// 订单套餐批量作废任务相关
|
||||
export * from './orderPackageInvalidateTask'
|
||||
|
||||
// 批量订购任务相关
|
||||
export * from './bulkPurchase'
|
||||
|
||||
// 站内通知相关
|
||||
export * from './notification'
|
||||
|
||||
// 审计链路与调查中心
|
||||
export * from './audit'
|
||||
|
||||
// 企业微信审批配置相关
|
||||
export * from './wecom'
|
||||
|
||||
85
src/types/api/notification.ts
Normal file
85
src/types/api/notification.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
export type NotificationCategory = 'approval' | 'expiry' | 'sync' | 'system' | string
|
||||
export type NotificationSeverity = 'info' | 'warning' | 'error' | 'critical' | string
|
||||
|
||||
/** 后台通知引用的受控资源类型。 */
|
||||
export type NotificationRefType =
|
||||
| 'system_config'
|
||||
| 'integration_log'
|
||||
| 'package'
|
||||
| 'asset'
|
||||
| 'refund'
|
||||
| 'agent_recharge'
|
||||
| 'wecom_approval'
|
||||
| 'iot_card'
|
||||
| 'device'
|
||||
| 'expiring_asset'
|
||||
| 'shop_fund'
|
||||
| 'card_sync'
|
||||
| (string & {})
|
||||
|
||||
export interface NotificationItem {
|
||||
id: number
|
||||
title: string
|
||||
body: string
|
||||
category: NotificationCategory
|
||||
type: string
|
||||
severity: NotificationSeverity
|
||||
is_read: boolean
|
||||
created_at: string
|
||||
read_at: string | null
|
||||
ref_type: NotificationRefType | null
|
||||
ref_id: string | null
|
||||
ref_key: string | null
|
||||
}
|
||||
|
||||
export interface NotificationQueryParams {
|
||||
category?: string
|
||||
type?: string
|
||||
severity?: string
|
||||
is_read?: boolean
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface NotificationListResponse {
|
||||
items: NotificationItem[]
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface NotificationUnreadCount {
|
||||
count: number
|
||||
display_count: string
|
||||
}
|
||||
|
||||
export interface NotificationUnreadSummary {
|
||||
approval: number
|
||||
expiry: number
|
||||
sync: number
|
||||
system: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface NotificationReadResponse {
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export interface NotificationReadRequest {
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface NotificationReadAllRequest {
|
||||
category?: string
|
||||
}
|
||||
|
||||
export interface NotificationReadAllResponse {
|
||||
updated_count: number
|
||||
}
|
||||
|
||||
export interface NotificationTarget {
|
||||
available: boolean
|
||||
target_id: number | string | null
|
||||
target_key: string
|
||||
target_type: string
|
||||
}
|
||||
@@ -22,6 +22,14 @@ export type OrderPaymentMethod = 'wallet' | 'wechat' | 'alipay' | 'offline'
|
||||
// 订单操作者类型
|
||||
export type OrderOperatorType = 'platform' | 'agent' | 'enterprise' | 'personal_customer'
|
||||
|
||||
export type PurchaseRole =
|
||||
| 'self_purchase'
|
||||
| 'purchased_by_parent'
|
||||
| 'purchased_by_platform'
|
||||
| 'purchase_for_subordinate'
|
||||
|
||||
export type OrderAssetType = 'card' | 'device'
|
||||
|
||||
// 订单佣金流程状态
|
||||
export enum OrderCommissionStatus {
|
||||
PENDING = 1, // 待计算
|
||||
@@ -71,9 +79,10 @@ export interface Order {
|
||||
device_id: number | null
|
||||
iot_card_id: number | null
|
||||
asset_identifier?: string // 资产标识符快照(ICCID 或 VirtualNo)
|
||||
asset_type?: string // 资产类型:single_card 或 device
|
||||
purchase_role?: string // 订单渠道
|
||||
asset_type?: OrderAssetType // 资产类型:card 或 device
|
||||
purchase_role?: PurchaseRole // 订单渠道
|
||||
purchase_remark?: string // 购买备注
|
||||
is_purchase_on_behalf?: boolean // 是否为代购订单
|
||||
is_purchased_by_parent?: boolean // 是否由上级代购
|
||||
is_expired?: boolean // 是否已过期
|
||||
expires_at?: string | null // 订单超时时间
|
||||
@@ -97,13 +106,13 @@ export interface OrderQueryParams {
|
||||
order_type?: OrderType
|
||||
order_no?: string
|
||||
identifier?: string // 按资产标识符过滤(ICCID 或 VirtualNo)
|
||||
purchase_role?: string // 订单渠道
|
||||
purchase_role?: PurchaseRole // 订单渠道
|
||||
buyer_phone?: string // 按买家手机号精确过滤
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
is_expired?: boolean // 是否已过期
|
||||
seller_shop_id?: number // 所属代理商ID(销售来源店铺ID)
|
||||
dateRange?: string[] | any // For date range picker in UI
|
||||
dateRange?: string[] // For date range picker in UI
|
||||
}
|
||||
|
||||
// 订单列表响应
|
||||
|
||||
@@ -101,10 +101,57 @@ export interface CommissionTierInfo {
|
||||
*/
|
||||
export type ExpiryBase = 'from_activation' | 'from_purchase'
|
||||
|
||||
// 套餐分配生效条件覆盖值
|
||||
export type PackageAllocationExpiryBaseOverride = 'from_purchase' | 'from_activation'
|
||||
|
||||
// 店铺套餐分配生效条件字段
|
||||
export interface PackageAllocationExpiryBaseFields {
|
||||
default_expiry_base?: string | null
|
||||
default_expiry_base_name?: string | null
|
||||
expiry_base_override?: PackageAllocationExpiryBaseOverride | null
|
||||
expiry_base_override_name?: string | null
|
||||
effective_expiry_base?: string | null
|
||||
effective_expiry_base_name?: string | null
|
||||
}
|
||||
|
||||
// 创建店铺套餐分配请求
|
||||
export interface CreateShopPackageAllocationRequest {
|
||||
shop_id: number
|
||||
package_id: number
|
||||
cost_price: number
|
||||
expiry_base_override: PackageAllocationExpiryBaseOverride | null
|
||||
}
|
||||
|
||||
// 更新店铺套餐分配生效条件请求
|
||||
export interface UpdateShopPackageAllocationExpiryBaseRequest {
|
||||
expiry_base_override: PackageAllocationExpiryBaseOverride | null
|
||||
}
|
||||
|
||||
// 店铺套餐分配响应
|
||||
export interface ShopPackageAllocationResponse extends PackageAllocationExpiryBaseFields {
|
||||
id: number
|
||||
shop_id: number
|
||||
package_id: number
|
||||
cost_price: number
|
||||
}
|
||||
|
||||
// 批量创建店铺套餐分配请求
|
||||
export interface CreateShopPackageBatchAllocationsRequest {
|
||||
shop_id: number
|
||||
package_ids: number[]
|
||||
cost_price?: number
|
||||
expiry_base_override: PackageAllocationExpiryBaseOverride | null
|
||||
}
|
||||
|
||||
// 批量创建店铺套餐分配响应
|
||||
export interface ShopPackageBatchAllocationsResponse {
|
||||
allocations: ShopPackageAllocationResponse[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 套餐响应
|
||||
*/
|
||||
export interface PackageResponse {
|
||||
export interface PackageResponse extends PackageAllocationExpiryBaseFields {
|
||||
id: number
|
||||
package_code: string
|
||||
package_name: string
|
||||
@@ -124,7 +171,7 @@ export interface PackageResponse {
|
||||
suggested_retail_price?: number | null // 建议零售价(分),可能为 null 表示"未配置"
|
||||
retail_price?: number | null // 原始代理零售价(分),可能为 null
|
||||
is_gift?: boolean // 是否为赠送套餐
|
||||
price_config_status?: string // 价格配置状态
|
||||
price_config_status?: number // 价格配置状态:0未配置,1赠送0价,2已配置非0
|
||||
price_config_status_name?: string // 价格配置状态名称
|
||||
effective_retail_price?: number | null // 有效零售价(分)
|
||||
retail_price_config_status?: string // 零售价配置状态(代理视角)
|
||||
@@ -133,7 +180,7 @@ export interface PackageResponse {
|
||||
profit_margin?: number | null // 利润空间(分,仅代理用户可见)
|
||||
tier_info?: CommissionTierInfo // 返佣档位信息
|
||||
shelf_status?: number // 上架状态 (1:上架, 2:下架)
|
||||
status?: number // 状态 (1:启用, 2:禁用)
|
||||
status?: number // 状态 (0:禁用, 1:启用)
|
||||
description?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
@@ -146,7 +193,6 @@ export interface PackageQueryParams extends PaginationParams {
|
||||
package_name?: string // 套餐名称(模糊搜索)
|
||||
series_id?: number // 系列ID
|
||||
package_type?: string // 套餐类型
|
||||
data_type?: string // 流量类型
|
||||
shelf_status?: number // 上架状态
|
||||
status?: number // 状态
|
||||
}
|
||||
@@ -197,7 +243,7 @@ export interface UpdatePackageRequest {
|
||||
* 更新套餐状态请求
|
||||
*/
|
||||
export interface UpdatePackageStatusRequest {
|
||||
status: number // 1:启用, 2:禁用
|
||||
status: number // 0:禁用, 1:启用
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,8 +280,9 @@ export interface CommissionTier {
|
||||
/**
|
||||
* 套餐信息(用于系列授权)
|
||||
*/
|
||||
export interface GrantPackageInfo {
|
||||
export interface GrantPackageInfo extends PackageAllocationExpiryBaseFields {
|
||||
package_id: number
|
||||
allocation_id: number // 套餐分配记录 ID,用于修改生效条件
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
cost_price: number // 成本价(分)
|
||||
@@ -278,6 +325,60 @@ export interface ShopSeriesGrantQueryParams extends PaginationParams {
|
||||
status?: number // 状态筛选
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理系列授权套餐候选项
|
||||
*/
|
||||
export interface ShopSeriesGrantPackageOption {
|
||||
authorized: boolean
|
||||
id: number
|
||||
package_code: string
|
||||
package_name: string
|
||||
package_type: 'formal' | 'addon'
|
||||
series_id: number | null
|
||||
series_name: string | null
|
||||
is_gift: boolean
|
||||
status: number
|
||||
status_name: string
|
||||
shelf_status: number
|
||||
shelf_status_name: string
|
||||
calendar_type: 'natural_month' | 'by_day'
|
||||
duration_days: number | null
|
||||
duration_months: number
|
||||
real_data_mb: number
|
||||
virtual_data_mb: number
|
||||
enable_virtual_data: boolean
|
||||
virtual_ratio: number
|
||||
data_reset_cycle: string
|
||||
cost_price: number
|
||||
retail_price: number | null
|
||||
suggested_retail_price: number | null
|
||||
effective_retail_price: number | null
|
||||
expiry_base: string
|
||||
expiry_base_override: PackageAllocationExpiryBaseOverride | null
|
||||
expiry_base_override_name: string
|
||||
default_expiry_base: string
|
||||
default_expiry_base_name: string
|
||||
effective_expiry_base: string
|
||||
effective_expiry_base_name: string
|
||||
price_config_status: number
|
||||
price_config_status_name: string
|
||||
retail_price_config_status: number | null
|
||||
current_commission_rate: string
|
||||
profit_margin: number | null
|
||||
one_time_commission_amount: number | null
|
||||
tier_info?: {
|
||||
current_rate?: string
|
||||
next_rate?: string
|
||||
next_threshold?: number | null
|
||||
}
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ShopSeriesGrantPackageOptionsResponse {
|
||||
items: ShopSeriesGrantPackageOption[] | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建代理系列授权请求
|
||||
*/
|
||||
@@ -288,7 +389,8 @@ export interface CreateShopSeriesGrantRequest {
|
||||
commission_tiers?: CommissionTier[] // 梯度配置列表,梯度模式时必填
|
||||
enable_force_recharge?: boolean // 是否启用强充
|
||||
force_recharge_amount?: number // 强充金额(分)
|
||||
packages?: GrantPackageInfo[] // 套餐列表
|
||||
packages: GrantPackageItem[] // 初始套餐列表,1~100项
|
||||
expiry_base_override?: PackageAllocationExpiryBaseOverride | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,8 +407,9 @@ export interface UpdateShopSeriesGrantRequest {
|
||||
* 管理套餐请求中的套餐项
|
||||
*/
|
||||
export interface GrantPackageItem {
|
||||
package_id?: number // 套餐ID
|
||||
package_id: number // 套餐ID
|
||||
cost_price?: number // 成本价(分)
|
||||
expiry_base_override?: PackageAllocationExpiryBaseOverride | null
|
||||
remove?: boolean | null // 是否删除该套餐授权(true=删除)
|
||||
}
|
||||
|
||||
@@ -314,5 +417,6 @@ export interface GrantPackageItem {
|
||||
* 管理套餐请求
|
||||
*/
|
||||
export interface ManageGrantPackagesRequest {
|
||||
packages?: GrantPackageItem[] | null // 套餐操作列表
|
||||
packages: GrantPackageItem[] // 套餐操作列表,1~100项
|
||||
expiry_base_override?: PackageAllocationExpiryBaseOverride | null // 新增套餐统一生效条件
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ export enum PermissionStatus {
|
||||
|
||||
// 权限实体(匹配后端 ModelPermission)
|
||||
export interface Permission {
|
||||
ID: number
|
||||
id?: number
|
||||
ID?: number
|
||||
CreatedAt?: string
|
||||
UpdatedAt?: string
|
||||
DeletedAt?: string | null
|
||||
|
||||
@@ -10,6 +10,38 @@ export enum RefundStatus {
|
||||
RETURNED = 4 // 已退回
|
||||
}
|
||||
|
||||
export interface RefundAttachment {
|
||||
file_key: string
|
||||
file_name?: string
|
||||
file_size?: number
|
||||
}
|
||||
|
||||
export interface RefundApprovalTimelineItem {
|
||||
time?: string
|
||||
timestamp?: string
|
||||
status?: string
|
||||
status_name?: string
|
||||
operator?: string
|
||||
operator_name?: string
|
||||
comment?: string
|
||||
content?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface RefundApproval {
|
||||
source?: string | null
|
||||
sp_no?: string | null
|
||||
status?: string | null
|
||||
status_name?: string | null
|
||||
template_version?: string | null
|
||||
applicant?: unknown
|
||||
approvers?: unknown[]
|
||||
comments?: unknown[]
|
||||
attachments?: RefundAttachment[]
|
||||
timeline?: RefundApprovalTimelineItem[]
|
||||
business_process_result?: unknown
|
||||
}
|
||||
|
||||
// 退款申请
|
||||
export interface Refund {
|
||||
id: number
|
||||
@@ -25,12 +57,28 @@ export interface Refund {
|
||||
approved_refund_amount?: number | null // 实际退款金额(分)
|
||||
actual_received_amount?: number | null // 实收金额(分)
|
||||
status: RefundStatus
|
||||
status_name?: string | null
|
||||
refund_reason: string
|
||||
refund_voucher_key?: string[] | string // 退款凭证附件列表;历史数据可能为单字符串或逗号字符串
|
||||
attachments?: RefundAttachment[]
|
||||
reject_reason: string
|
||||
remark: string
|
||||
asset_reset: boolean
|
||||
commission_deducted: boolean
|
||||
submitter_name?: string | null // 提交人名称
|
||||
submitter_id?: number | null
|
||||
approval_provider?: 'wecom' | null
|
||||
approval_instance_id?: number | null
|
||||
approval_source?: 'none' | 'legacy' | 'wecom' | null // 审批来源
|
||||
approval_status?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | null // 审批状态
|
||||
approval_status_name?: string | null // 审批状态名称
|
||||
current_approver_summary?: string | null // 当前审批人摘要
|
||||
processing_status?: string | null // 业务处理状态
|
||||
processing_status_name?: string | null // 业务处理状态名称
|
||||
processing_failure_summary?: string | null
|
||||
processing_message?: string | null
|
||||
business_process_result?: unknown
|
||||
approval?: RefundApproval | null
|
||||
creator: number
|
||||
processor_id: number | null
|
||||
processed_at: string | null
|
||||
@@ -64,30 +112,15 @@ export interface CreateRefundRequest {
|
||||
order_id: number
|
||||
requested_refund_amount: number // 申请退款金额(分)
|
||||
actual_received_amount: number // 实收金额(分)
|
||||
refund_voucher_key: string[] // 退款凭证附件列表
|
||||
refund_reason?: string
|
||||
}
|
||||
|
||||
// 审批通过退款申请请求
|
||||
export interface ApproveRefundRequest {
|
||||
approved_refund_amount?: number // 实际退款金额(分)
|
||||
remark?: string
|
||||
}
|
||||
|
||||
// 审批拒绝退款申请请求
|
||||
export interface RejectRefundRequest {
|
||||
reject_reason: string
|
||||
package_usage_id: number // 关联套餐使用记录 ID,无关联记录时传 0
|
||||
refund_reason: string
|
||||
refund_voucher_key: string[]
|
||||
}
|
||||
|
||||
// 重新提交退款申请请求
|
||||
export interface ResubmitRefundRequest {
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount?: number
|
||||
refund_voucher_key?: string[]
|
||||
attachments?: RefundAttachment[]
|
||||
refund_reason?: string
|
||||
}
|
||||
|
||||
// 退回退款申请请求
|
||||
export interface ReturnRefundRequest {
|
||||
remark?: string
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ export enum RoleStatus {
|
||||
|
||||
// 角色实体(匹配后端 ModelRole)
|
||||
export interface PlatformRole {
|
||||
ID: number
|
||||
id?: number
|
||||
ID?: number
|
||||
CreatedAt?: string
|
||||
UpdatedAt?: string
|
||||
DeletedAt?: string | null
|
||||
@@ -28,6 +29,21 @@ export interface PlatformRole {
|
||||
role_desc: string // 角色描述
|
||||
role_type: RoleType // 角色类型
|
||||
status: RoleStatus // 状态
|
||||
credit_enabled?: boolean // 新建代理默认信用是否启用(仅客户角色)
|
||||
credit_limit?: number // 新建代理默认信用额度(分,仅客户角色)
|
||||
default_credit_enabled?: boolean // 接口返回的新建代理默认信用状态
|
||||
default_credit_limit?: number // 接口返回的新建代理默认信用额度(分)
|
||||
default_credit_scope?: string // 接口返回的信用模板生效范围
|
||||
created_at?: string // 接口返回的创建时间
|
||||
updated_at?: string // 接口返回的更新时间
|
||||
}
|
||||
|
||||
export interface UpdateRoleDefaultCreditResponse {
|
||||
affects_existing_wallets: boolean
|
||||
credit_enabled: boolean
|
||||
credit_limit: number
|
||||
role_id: number
|
||||
scope: string
|
||||
}
|
||||
|
||||
// 角色查询参数
|
||||
@@ -44,3 +60,9 @@ export interface PlatformRoleFormData {
|
||||
role_type: RoleType
|
||||
status: RoleStatus
|
||||
}
|
||||
|
||||
// 更新客户角色默认信用参数
|
||||
export interface UpdateRoleDefaultCreditRequest {
|
||||
credit_enabled: boolean
|
||||
credit_limit: number
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ export interface ShopResponse {
|
||||
contact_name: string // 联系人姓名
|
||||
contact_phone: string // 联系人电话
|
||||
status: number // 状态 (0:禁用, 1:启用)
|
||||
status_name?: string // 状态名称
|
||||
business_owner_account_id: number | null // 平台业务员账号ID
|
||||
business_owner_username: string // 平台业务员账号名
|
||||
business_owner_phone_summary: string // 平台业务员手机号摘要
|
||||
business_owner_available: boolean // 是否仍可用于通知接收
|
||||
client_login_disabled: boolean // 是否禁止 C 端新登录
|
||||
}
|
||||
|
||||
// 店铺列表查询参数
|
||||
@@ -32,6 +38,9 @@ export interface ShopQueryParams extends PaginationParams {
|
||||
parent_id?: number | null // 上级店铺ID
|
||||
level?: number | null // 店铺层级 (1-7级)
|
||||
status?: number | null // 状态 (0:禁用, 1:启用)
|
||||
contact_phone?: string // 联系电话精确查询
|
||||
business_owner_account_id?: number | null // 平台业务员账号ID
|
||||
client_login_disabled?: boolean
|
||||
page?: number // 页码
|
||||
page_size?: number // 每页数量
|
||||
}
|
||||
@@ -43,6 +52,7 @@ export interface CreateShopParams {
|
||||
init_username: string // 初始账号用户名(必填)
|
||||
init_password: string // 初始账号密码(必填)
|
||||
init_phone: string // 初始账号手机号(必填)
|
||||
default_role_id: number // 初始账号默认客户角色(必填)
|
||||
parent_id?: number | null // 上级店铺ID(一级店铺可不填)
|
||||
province?: string // 省份
|
||||
city?: string // 城市
|
||||
@@ -50,6 +60,8 @@ export interface CreateShopParams {
|
||||
address?: string // 详细地址
|
||||
contact_name?: string // 联系人姓名
|
||||
contact_phone?: string // 联系人电话
|
||||
business_owner_account_id?: number | null // 平台业务员账号ID
|
||||
client_login_disabled?: boolean
|
||||
}
|
||||
|
||||
// 更新店铺参数
|
||||
@@ -62,6 +74,20 @@ export interface UpdateShopParams {
|
||||
address?: string // 详细地址
|
||||
contact_name?: string // 联系人姓名
|
||||
contact_phone?: string // 联系人电话
|
||||
business_owner_account_id?: number | null // 平台业务员账号ID
|
||||
client_login_disabled?: boolean
|
||||
}
|
||||
|
||||
// 店铺业务员候选
|
||||
export interface ShopBusinessOwnerCandidate {
|
||||
id: number // 平台业务员账号ID
|
||||
username: string // 平台业务员账号名
|
||||
phone_summary: string // 手机号摘要
|
||||
}
|
||||
|
||||
// 店铺业务员候选查询参数
|
||||
export interface ShopBusinessOwnerCandidateQueryParams extends PaginationParams {
|
||||
keyword?: string // 用户名或手机号关键词
|
||||
}
|
||||
|
||||
// 店铺列表分页响应
|
||||
@@ -94,3 +120,22 @@ export interface ShopRolesResponse {
|
||||
export interface AssignShopRolesRequest {
|
||||
role_ids: number[] | null // 角色ID列表
|
||||
}
|
||||
|
||||
// 更新店铺实际信用额度请求
|
||||
export interface UpdateShopCreditLimitRequest {
|
||||
credit_enabled: boolean // 是否启用实际信用额度
|
||||
credit_limit: number // 实际信用额度(分)
|
||||
version: number // 资金概况版本,用于并发控制
|
||||
}
|
||||
|
||||
// 更新店铺实际信用额度响应
|
||||
export interface UpdateShopCreditLimitResponse {
|
||||
available_balance: number // 总可用金额(分)
|
||||
balance: number // 账面余额(分)
|
||||
credit_enabled: boolean // 是否启用实际信用额度
|
||||
credit_limit: number // 实际信用额度(分)
|
||||
frozen_balance: number // 冻结金额(分)
|
||||
shop_id: number // 店铺 ID
|
||||
version: number // 更新后的钱包版本
|
||||
wallet_id: number // 主钱包 ID
|
||||
}
|
||||
|
||||
56
src/types/api/systemConfig.ts
Normal file
56
src/types/api/systemConfig.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { PaginationParams } from './common'
|
||||
|
||||
export type SystemConfigModule = 'carrier_callback' | 'c2b.payment'
|
||||
|
||||
export type AllowedPaymentMethod = 'wallet' | 'wechat' | 'alipay'
|
||||
export const PAYMENT_CONFIG_KEYS = {
|
||||
card: 'c2b.payment.card_allowed_methods',
|
||||
device: 'c2b.payment.device_allowed_methods'
|
||||
} as const
|
||||
|
||||
export type SystemConfigValueType = 'string' | 'int' | 'bool' | 'json'
|
||||
|
||||
export interface SystemConfigItem {
|
||||
config_key: string
|
||||
module: SystemConfigModule
|
||||
value: string
|
||||
value_type: SystemConfigValueType
|
||||
description: string
|
||||
control: string
|
||||
enum_values?: string[]
|
||||
min: number | null
|
||||
max: number | null
|
||||
readonly: boolean
|
||||
registered: boolean
|
||||
sensitive: boolean
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface SystemConfigQueryParams extends PaginationParams {
|
||||
module?: SystemConfigModule
|
||||
}
|
||||
|
||||
export interface SystemConfigPageResult {
|
||||
list: SystemConfigItem[]
|
||||
page: number
|
||||
page_size: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface UpdateSystemConfigRequest {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export function parsePaymentMethods(value: string): AllowedPaymentMethod[] {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((method): method is AllowedPaymentMethod =>
|
||||
['wallet', 'wechat', 'alipay'].includes(method)
|
||||
)
|
||||
: []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
128
src/types/api/wecom.ts
Normal file
128
src/types/api/wecom.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { BaseResponse, PaginationData, PaginationParams } from './common'
|
||||
|
||||
export type WecomBusinessType = 'refund_approval' | 'offline_recharge_approval'
|
||||
|
||||
export interface WecomApplication {
|
||||
id: number
|
||||
corp_id: string
|
||||
agent_id: number
|
||||
name: string
|
||||
secret?: string
|
||||
callback_token?: string
|
||||
encoding_aes_key?: string
|
||||
credentials_set: boolean
|
||||
default_creator_userid?: string | null
|
||||
default_creator_name?: string | null
|
||||
status: 0 | 1
|
||||
status_name: string
|
||||
last_connected_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface WecomApplicationRequest {
|
||||
corp_id?: string
|
||||
agent_id?: number
|
||||
name?: string
|
||||
secret?: string
|
||||
callback_token?: string
|
||||
encoding_aes_key?: string
|
||||
status?: 0 | 1
|
||||
}
|
||||
|
||||
export interface WecomMember {
|
||||
application_id: number
|
||||
corp_id: string
|
||||
userid: string
|
||||
name: string
|
||||
department_ids: number[] | null
|
||||
synced_at: string
|
||||
}
|
||||
|
||||
export interface WecomSceneControlMapping {
|
||||
business_field: string
|
||||
control_id: string
|
||||
control_type: string
|
||||
option_mapping: Record<string, string>
|
||||
}
|
||||
|
||||
export interface WecomScene {
|
||||
id: number
|
||||
application_id: number
|
||||
business_type: WecomBusinessType
|
||||
business_type_name: string
|
||||
template_id: string
|
||||
template_name: string
|
||||
control_mapping: WecomSceneControlMapping[]
|
||||
template_fingerprint: string
|
||||
last_verified_at: string
|
||||
status: 0 | 1
|
||||
status_name: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface WecomSceneRequest {
|
||||
application_id?: number
|
||||
template_id?: string
|
||||
control_mapping?: WecomSceneControlMapping[]
|
||||
status?: 0 | 1
|
||||
}
|
||||
|
||||
export interface WecomTemplateInspectRequest {
|
||||
template_id: string
|
||||
}
|
||||
|
||||
export interface WecomTemplateControl {
|
||||
id: string
|
||||
option_keys?: string[] | null
|
||||
required: boolean
|
||||
title: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface WecomTemplateDetail {
|
||||
controls?: WecomTemplateControl[] | null
|
||||
name: string
|
||||
template_id: string
|
||||
}
|
||||
|
||||
export interface WecomBusinessField {
|
||||
code: string
|
||||
description: string
|
||||
name: string
|
||||
value_type: string
|
||||
}
|
||||
|
||||
export interface WecomBusinessFieldList {
|
||||
business_type: WecomBusinessType
|
||||
business_type_name: string
|
||||
items?: WecomBusinessField[] | null
|
||||
}
|
||||
|
||||
export interface WecomAccountBindingRequest {
|
||||
application_id: number
|
||||
userid: string
|
||||
}
|
||||
|
||||
export interface WecomSyncMembersResponse {
|
||||
application_id: number
|
||||
synced_at: string
|
||||
synced_count: number
|
||||
}
|
||||
|
||||
export type WecomApplicationListResponse = BaseResponse<PaginationData<WecomApplication>>
|
||||
export type WecomMemberListResponse = BaseResponse<PaginationData<WecomMember>>
|
||||
export type WecomSceneListResponse = BaseResponse<PaginationData<WecomScene>>
|
||||
export type WecomApplicationResponse = BaseResponse<WecomApplication>
|
||||
export type WecomSceneResponse = BaseResponse<WecomScene>
|
||||
export type WecomSyncMembersApiResponse = BaseResponse<WecomSyncMembersResponse>
|
||||
export type WecomTemplateDetailResponse = BaseResponse<WecomTemplateDetail>
|
||||
export type WecomBusinessFieldListResponse = BaseResponse<WecomBusinessFieldList>
|
||||
|
||||
export interface WecomMemberQueryParams extends PaginationParams {
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
export type WecomApplicationQueryParams = PaginationParams
|
||||
|
||||
export type WecomSceneQueryParams = PaginationParams
|
||||
1
src/types/auto-imports.d.ts
vendored
1
src/types/auto-imports.d.ts
vendored
@@ -7,7 +7,6 @@
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElButton: (typeof import('element-plus/es'))['ElButton']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
|
||||
14
src/types/components.d.ts
vendored
14
src/types/components.d.ts
vendored
@@ -68,8 +68,15 @@ declare module 'vue' {
|
||||
ArtWangEditor: typeof import('./../components/core/forms/ArtWangEditor.vue')['default']
|
||||
ArtWatermark: typeof import('./../components/core/others/ArtWatermark.vue')['default']
|
||||
ArtWorkTab: typeof import('./../components/core/layouts/art-work-tab/index.vue')['default']
|
||||
AuditDistributionChart: typeof import('./../components/business/audit/AuditDistributionChart.vue')['default']
|
||||
AuditEventTable: typeof import('./../components/business/audit/AuditEventTable.vue')['default']
|
||||
AuditInvestigationDrawer: typeof import('./../components/business/audit/AuditInvestigationDrawer.vue')['default']
|
||||
AuditInvestigationHost: typeof import('./../components/business/audit/AuditInvestigationHost.vue')['default']
|
||||
AuditInvestigationLinks: typeof import('./../components/business/audit/AuditInvestigationLinks.vue')['default']
|
||||
AuditResourceSearchDialog: typeof import('./../components/business/audit/AuditResourceSearchDialog.vue')['default']
|
||||
BasicSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/BasicSettings.vue')['default']
|
||||
BatchOperationDialog: typeof import('./../components/business/BatchOperationDialog.vue')['default']
|
||||
BatchRealnamePolicyDialog: typeof import('./../components/business/BatchRealnamePolicyDialog.vue')['default']
|
||||
BoxStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue')['default']
|
||||
CardOperationDialog: typeof import('./../components/business/CardOperationDialog.vue')['default']
|
||||
CardStatusTag: typeof import('./../components/business/CardStatusTag.vue')['default']
|
||||
@@ -90,7 +97,10 @@ declare module 'vue' {
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
@@ -138,7 +148,6 @@ declare module 'vue' {
|
||||
LoginLeftView: typeof import('./../components/core/views/login/LoginLeftView.vue')['default']
|
||||
MenuLayoutSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue')['default']
|
||||
MenuStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue')['default']
|
||||
OperationLogsDialog: typeof import('./../components/business/OperationLogsDialog.vue')['default']
|
||||
OperatorSelect: typeof import('./../components/business/OperatorSelect.vue')['default']
|
||||
PackageSelector: typeof import('./../components/business/PackageSelector.vue')['default']
|
||||
PaymentVoucherDialog: typeof import('./../components/business/PaymentVoucherDialog.vue')['default']
|
||||
@@ -146,9 +155,12 @@ declare module 'vue' {
|
||||
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']
|
||||
SeriesGrantPackageDialog: typeof import('./../components/business/SeriesGrantPackageDialog.vue')['default']
|
||||
SeriesGrantPackageTable: typeof import('./../components/business/SeriesGrantPackageTable.vue')['default']
|
||||
SettingDrawer: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingDrawer.vue')['default']
|
||||
SettingHeader: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingHeader.vue')['default']
|
||||
SettingItem: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default']
|
||||
ShopCreditLimitDialog: typeof import('./../components/business/ShopCreditLimitDialog.vue')['default']
|
||||
SidebarSubmenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default']
|
||||
SpeedLimitDialog: typeof import('./../components/device/SpeedLimitDialog.vue')['default']
|
||||
SwitchCardDialog: typeof import('./../components/device/SwitchCardDialog.vue')['default']
|
||||
|
||||
@@ -34,6 +34,8 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
|
||||
isFirstLevel?: boolean
|
||||
/** 角色权限 */
|
||||
roles?: string[]
|
||||
/** 页面访问权限,满足任意一个即可 */
|
||||
permissions?: string[]
|
||||
/** 导出任务固定场景 */
|
||||
exportTaskScene?: ExportTaskScene
|
||||
/** 是否固定标签页 */
|
||||
|
||||
@@ -59,7 +59,7 @@ export const strongPasswordRules = (t: (key: string) => string): FormItemRule[]
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
validator: (_rule: any, value: any, callback: any) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
@@ -93,7 +93,7 @@ export const confirmPasswordRules = (
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
validator: (_rule: any, value: any, callback: any) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
|
||||
85
src/utils/business/apiError.ts
Normal file
85
src/utils/business/apiError.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export type ApiErrorKind =
|
||||
| 'auth'
|
||||
| 'forbidden'
|
||||
| 'validation'
|
||||
| 'conflict'
|
||||
| 'timeout'
|
||||
| 'server'
|
||||
| 'unknown'
|
||||
|
||||
export interface NormalizedApiError {
|
||||
kind: ApiErrorKind
|
||||
status?: number
|
||||
code?: number | string
|
||||
message: string
|
||||
unknownResult: boolean
|
||||
}
|
||||
|
||||
const getResponse = (error: unknown) => (axios.isAxiosError(error) ? error.response : undefined)
|
||||
|
||||
export const normalizeApiError = (error: unknown): NormalizedApiError => {
|
||||
const response = getResponse(error)
|
||||
const status = response?.status
|
||||
const code = response?.data?.code
|
||||
const backendMessage = response?.data?.msg
|
||||
const timeout = axios.isAxiosError(error) && error.code === 'ECONNABORTED'
|
||||
|
||||
if (timeout) {
|
||||
return {
|
||||
kind: 'timeout',
|
||||
status,
|
||||
code,
|
||||
message: '请求超时,结果可能未知,请刷新确认',
|
||||
unknownResult: true
|
||||
}
|
||||
}
|
||||
if (status === 401 || code === 401)
|
||||
return {
|
||||
kind: 'auth',
|
||||
status,
|
||||
code,
|
||||
message: '登录状态已失效,请重新登录',
|
||||
unknownResult: false
|
||||
}
|
||||
if (status === 403)
|
||||
return {
|
||||
kind: 'forbidden',
|
||||
status,
|
||||
code,
|
||||
message: '没有执行此操作的权限',
|
||||
unknownResult: false
|
||||
}
|
||||
if (status === 400)
|
||||
return {
|
||||
kind: 'validation',
|
||||
status,
|
||||
code,
|
||||
message: backendMessage || '请求参数或业务条件不符合要求',
|
||||
unknownResult: false
|
||||
}
|
||||
if (status === 409)
|
||||
return {
|
||||
kind: 'conflict',
|
||||
status,
|
||||
code,
|
||||
message: backendMessage || '数据已发生变化,请刷新后重试',
|
||||
unknownResult: false
|
||||
}
|
||||
if (status && status >= 500)
|
||||
return {
|
||||
kind: 'server',
|
||||
status,
|
||||
code,
|
||||
message: '服务器处理失败,请稍后重试',
|
||||
unknownResult: false
|
||||
}
|
||||
return {
|
||||
kind: 'unknown',
|
||||
status,
|
||||
code,
|
||||
message: backendMessage || '操作失败,请稍后重试',
|
||||
unknownResult: false
|
||||
}
|
||||
}
|
||||
50
src/utils/business/approvalSummary.ts
Normal file
50
src/utils/business/approvalSummary.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export type ApprovalSource = 'none' | 'legacy' | 'wecom'
|
||||
|
||||
export interface ApprovalSummary {
|
||||
approval_source?: ApprovalSource | null
|
||||
approval_provider?: 'wecom' | null
|
||||
approval_instance_id?: number | null
|
||||
approval_status?: number | string | null
|
||||
approval_status_name?: string | null
|
||||
current_approver_summary?: string | null
|
||||
processing_status_name?: string | null
|
||||
}
|
||||
|
||||
export const getApprovalStatusText = (summary: ApprovalSummary): string => {
|
||||
if (summary.approval_source === 'legacy') return '历史审批'
|
||||
if (
|
||||
summary.approval_source === 'wecom' ||
|
||||
summary.approval_provider === 'wecom' ||
|
||||
summary.approval_instance_id
|
||||
) {
|
||||
return (
|
||||
summary.approval_status_name || approvalStatusNames[Number(summary.approval_status)] || '-'
|
||||
)
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
export const getCurrentApproverSummaryText = (summary: ApprovalSummary): string => {
|
||||
if (
|
||||
summary.approval_source !== 'wecom' &&
|
||||
summary.approval_provider !== 'wecom' &&
|
||||
!summary.approval_instance_id
|
||||
)
|
||||
return '-'
|
||||
return summary.current_approver_summary || '-'
|
||||
}
|
||||
|
||||
export const getProcessingStatusText = (summary: ApprovalSummary): string =>
|
||||
summary.processing_status_name || '-'
|
||||
|
||||
export const approvalStatusNames: Record<number, string> = {
|
||||
0: '提交中',
|
||||
1: '审批中',
|
||||
2: '已通过',
|
||||
3: '已拒绝',
|
||||
4: '已撤销',
|
||||
5: '通过后撤销',
|
||||
6: '已删除',
|
||||
7: '提交失败',
|
||||
8: '提交结果未知'
|
||||
}
|
||||
137
src/utils/business/audit.ts
Normal file
137
src/utils/business/audit.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type {
|
||||
AuditActorKind,
|
||||
AuditCategory,
|
||||
AuditResult,
|
||||
AuditRiskLevel,
|
||||
AuditScopeType,
|
||||
AuditSource,
|
||||
IntegrationResultCategory
|
||||
} from '@/types/api'
|
||||
|
||||
export const auditCategoryLabels: Record<AuditCategory, string> = {
|
||||
configuration: '配置',
|
||||
reliability: '可靠性',
|
||||
asset: '资产',
|
||||
security: '安全',
|
||||
identity: '身份',
|
||||
business: '业务'
|
||||
}
|
||||
|
||||
export const auditActorKindLabels: Record<AuditActorKind, string> = {
|
||||
account: '人工账号',
|
||||
personal_customer: '个人客户',
|
||||
openapi: '开放接口账号',
|
||||
system_task: '系统任务',
|
||||
scheduled_job: '计划任务',
|
||||
external_system: '外部系统'
|
||||
}
|
||||
|
||||
export const auditScopeTypeLabels: Record<AuditScopeType, string> = {
|
||||
platform: '平台',
|
||||
shop: '店铺',
|
||||
personal_customer: '个人客户'
|
||||
}
|
||||
|
||||
export const auditSourceLabels: Record<AuditSource, string> = {
|
||||
admin_api: '后台管理 API',
|
||||
personal_api: '个人客户 API',
|
||||
openapi: '代理 OpenAPI',
|
||||
worker: '异步 Worker',
|
||||
scheduler: '计划任务',
|
||||
callback: '外部系统回调'
|
||||
}
|
||||
|
||||
export const auditResourceRelationLabels: Record<string, string> = {
|
||||
primary: '主要资源',
|
||||
affected: '受影响资源',
|
||||
reference: '引用资源'
|
||||
}
|
||||
|
||||
export const auditSubjectVisibilityLabels: Record<string, string> = {
|
||||
internal_only: '仅平台可见',
|
||||
subject_result: '主体可见结论',
|
||||
subject_detail: '主体可见安全详情'
|
||||
}
|
||||
|
||||
export const auditResourceTypeLabels: Record<string, string> = {
|
||||
iot_card: 'IoT 卡',
|
||||
device: '设备',
|
||||
asset_allocation_record: '资产分配记录',
|
||||
exchange_order: '换卡订单',
|
||||
shop: '店铺',
|
||||
enterprise: '企业',
|
||||
order: '订单',
|
||||
refund: '退款',
|
||||
integration_log: '外部集成记录'
|
||||
}
|
||||
|
||||
export const auditResultMeta: Record<
|
||||
AuditResult,
|
||||
{ label: string; type: 'success' | 'danger' | 'warning' | 'info' }
|
||||
> = {
|
||||
success: { label: '成功', type: 'success' },
|
||||
failed: { label: '失败', type: 'danger' },
|
||||
denied: { label: '拒绝', type: 'danger' },
|
||||
partial: { label: '部分成功', type: 'warning' },
|
||||
unknown: { label: '未知', type: 'info' }
|
||||
}
|
||||
|
||||
const auditResultLabels: Record<string, string> = {
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
denied: '拒绝',
|
||||
partial: '部分成功',
|
||||
unknown: '未知',
|
||||
pending: '处理中',
|
||||
not_found: '未找到',
|
||||
invalid_payload: '无效载荷',
|
||||
conflict: '冲突',
|
||||
ignored: '已忽略',
|
||||
merged: '已合并',
|
||||
rate_limited: '已限流',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
}
|
||||
|
||||
export function auditResultDisplay(result?: string | null, resultName?: string | null): string {
|
||||
const name = resultName?.trim()
|
||||
if (name) return name
|
||||
return result ? auditResultLabels[result] || result : '-'
|
||||
}
|
||||
|
||||
export const auditRiskMeta: Record<
|
||||
AuditRiskLevel,
|
||||
{ label: string; type: 'success' | 'danger' | 'warning' | 'info' }
|
||||
> = {
|
||||
low: { label: '低', type: 'success' },
|
||||
normal: { label: '普通', type: 'info' },
|
||||
high: { label: '高', type: 'warning' },
|
||||
critical: { label: '严重', type: 'danger' }
|
||||
}
|
||||
|
||||
export const integrationCategoryMeta: Record<
|
||||
IntegrationResultCategory,
|
||||
{ label: string; type: 'success' | 'danger' | 'warning' | 'info' }
|
||||
> = {
|
||||
processing: { label: '处理中', type: 'warning' },
|
||||
succeeded: { label: '成功', type: 'success' },
|
||||
indeterminate: { label: '结果不确定', type: 'warning' },
|
||||
failed: { label: '失败', type: 'danger' },
|
||||
not_sent: { label: '未发送', type: 'info' }
|
||||
}
|
||||
|
||||
export function toRfc3339(value?: string | Date | null): string | undefined {
|
||||
if (!value) return undefined
|
||||
const date = value instanceof Date ? value : new Date(value)
|
||||
return Number.isNaN(date.getTime()) ? undefined : date.toISOString()
|
||||
}
|
||||
|
||||
export function fenDisplay(value?: number | null): string {
|
||||
return typeof value === 'number' ? `¥${(value / 100).toFixed(2)}` : '-'
|
||||
}
|
||||
|
||||
export function auditJson(value: unknown): string {
|
||||
if (value === null || value === undefined) return '-'
|
||||
if (typeof value === 'string') return value
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
14
src/utils/business/auditAccess.ts
Normal file
14
src/utils/business/auditAccess.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { AuditInvestigationTarget } from '@/components/business/audit/types'
|
||||
|
||||
export const isPlatformAuditAccount = (userType?: number | null, isSuperAdmin = false) =>
|
||||
isSuperAdmin || [1, 2].includes(Number(userType))
|
||||
|
||||
export const canLoadAuditInvestigation = (
|
||||
target: AuditInvestigationTarget,
|
||||
userType?: number | null,
|
||||
isSuperAdmin = false
|
||||
) => {
|
||||
if (target.mode === 'agent') return Number(userType) === 3
|
||||
if (target.mode === 'enterprise') return Number(userType) === 4
|
||||
return isPlatformAuditAccount(userType, isSuperAdmin)
|
||||
}
|
||||
85
src/utils/business/auditNavigation.ts
Normal file
85
src/utils/business/auditNavigation.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { AuditSubjectResourceType } from '@/types/api'
|
||||
import type { AuditFinanceField, AuditInvestigationTarget } from '@/components/business/audit/types'
|
||||
|
||||
interface AuditResourceRouteOptions {
|
||||
userType?: number
|
||||
resourceType: AuditSubjectResourceType | string
|
||||
internalId?: string | number | null
|
||||
businessIdentifier?: string | null
|
||||
}
|
||||
|
||||
interface AuditReferenceOptions {
|
||||
currentId?: string | null
|
||||
fidelity?: boolean | null
|
||||
}
|
||||
|
||||
/** 仅保留非空、可靠且不会回到当前目标的显式调查引用。 */
|
||||
export function resolveInvestigationReference(
|
||||
value?: string | null,
|
||||
options: AuditReferenceOptions = {}
|
||||
): string | null {
|
||||
if (options.fidelity === false) return null
|
||||
const normalizedValue = value?.trim()
|
||||
if (!normalizedValue || normalizedValue === options.currentId?.trim()) return null
|
||||
return normalizedValue
|
||||
}
|
||||
|
||||
/** 根据当前主体选择平台内部 ID 或主体安全业务标识。 */
|
||||
export function resolveAuditResourceTarget(
|
||||
options: AuditResourceRouteOptions
|
||||
): AuditInvestigationTarget | null {
|
||||
const { userType, resourceType, internalId, businessIdentifier } = options
|
||||
const normalizedBusinessIdentifier = businessIdentifier?.trim()
|
||||
if (userType === 3) {
|
||||
if (
|
||||
!normalizedBusinessIdentifier ||
|
||||
![
|
||||
'iot_card',
|
||||
'device',
|
||||
'asset_allocation_record',
|
||||
'exchange_order',
|
||||
'shop',
|
||||
'enterprise'
|
||||
].includes(resourceType)
|
||||
)
|
||||
return null
|
||||
return {
|
||||
mode: 'agent',
|
||||
resourceType: resourceType as AuditSubjectResourceType,
|
||||
id: normalizedBusinessIdentifier
|
||||
}
|
||||
}
|
||||
if (userType === 4) {
|
||||
if (!normalizedBusinessIdentifier || !['iot_card', 'device'].includes(resourceType)) return null
|
||||
return {
|
||||
mode: 'enterprise',
|
||||
resourceType: resourceType as AuditSubjectResourceType,
|
||||
id: normalizedBusinessIdentifier
|
||||
}
|
||||
}
|
||||
if (
|
||||
internalId === undefined ||
|
||||
internalId === null ||
|
||||
internalId === '' ||
|
||||
(typeof internalId === 'string' && !internalId.trim()) ||
|
||||
(typeof internalId === 'number' && (!Number.isFinite(internalId) || internalId <= 0))
|
||||
)
|
||||
return null
|
||||
return { mode: 'resource', resourceType, id: String(internalId).trim() }
|
||||
}
|
||||
|
||||
export function resolveFinanceAuditTarget(
|
||||
field: AuditFinanceField,
|
||||
value?: string | number | null
|
||||
): AuditInvestigationTarget | null {
|
||||
const normalizedValue = typeof value === 'string' ? value.trim() : value
|
||||
if (
|
||||
normalizedValue === undefined ||
|
||||
normalizedValue === null ||
|
||||
normalizedValue === '' ||
|
||||
(typeof normalizedValue === 'number' &&
|
||||
(!Number.isFinite(normalizedValue) || normalizedValue <= 0))
|
||||
)
|
||||
return null
|
||||
return { mode: 'finance', field, value: normalizedValue }
|
||||
}
|
||||
39
src/utils/business/expiryEstimate.ts
Normal file
39
src/utils/business/expiryEstimate.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { formatDateTime } from './format'
|
||||
import type { ExpiryEstimateStatus } from '@/types/api'
|
||||
|
||||
export interface ExpiryEstimate {
|
||||
estimated_final_expires_at?: string | null
|
||||
days_until_final_expiry?: number | null
|
||||
expiry_estimate_status?: ExpiryEstimateStatus | null
|
||||
expiry_estimate_status_name?: string | null
|
||||
is_expiring?: boolean
|
||||
}
|
||||
|
||||
export const getExpiryEstimateText = (estimate: ExpiryEstimate): string => {
|
||||
switch (estimate.expiry_estimate_status) {
|
||||
case 'exact':
|
||||
return estimate.estimated_final_expires_at
|
||||
? formatDateTime(estimate.estimated_final_expires_at)
|
||||
: '-'
|
||||
case 'waiting_activation':
|
||||
return '待激活后起算'
|
||||
case 'invalid_data':
|
||||
return '数据异常'
|
||||
case 'none':
|
||||
default:
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
export const getExpiryEstimateTooltip = (estimate: ExpiryEstimate): string => {
|
||||
if (!estimate.is_expiring || estimate.days_until_final_expiry == null) return ''
|
||||
return `剩余${estimate.days_until_final_expiry}天`
|
||||
}
|
||||
|
||||
export const getExpiryEstimateClass = (estimate: ExpiryEstimate): string => {
|
||||
if (!estimate.is_expiring || estimate.days_until_final_expiry == null) return ''
|
||||
if (estimate.days_until_final_expiry <= 3) return 'expiry-estimate--critical'
|
||||
if (estimate.days_until_final_expiry <= 7) return 'expiry-estimate--warning'
|
||||
if (estimate.days_until_final_expiry <= 15) return 'expiry-estimate--notice'
|
||||
return ''
|
||||
}
|
||||
58
src/utils/business/id.ts
Normal file
58
src/utils/business/id.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 兼容后端实体 ID 字段的大小写差异。
|
||||
* 后端历史接口可能返回 `ID`,新接口返回 `id`,前端统一优先使用小写 id。
|
||||
*/
|
||||
export type CompatibleId = number | string
|
||||
|
||||
export interface CompatibleIdFields {
|
||||
id?: CompatibleId | null
|
||||
ID?: CompatibleId | null
|
||||
}
|
||||
|
||||
/** 从实体中读取兼容的 ID 字段。0 也是有效 ID,不能使用 ||。 */
|
||||
export const getCompatibleId = (value: CompatibleIdFields | null | undefined) => {
|
||||
const id = value?.id ?? value?.ID
|
||||
return id === null || id === undefined || id === '' ? undefined : id
|
||||
}
|
||||
|
||||
/** 读取并转换为数字 ID,供只接受 number 的接口参数使用。 */
|
||||
export const getCompatibleNumericId = (
|
||||
value: CompatibleIdFields | null | undefined
|
||||
): number | undefined => {
|
||||
const id = getCompatibleId(value)
|
||||
if (id === undefined) return undefined
|
||||
|
||||
const numericId = Number(id)
|
||||
return Number.isFinite(numericId) ? numericId : undefined
|
||||
}
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||
if (value === null || typeof value !== 'object') return false
|
||||
const prototype = Object.getPrototypeOf(value)
|
||||
return prototype === Object.prototype || prototype === null
|
||||
}
|
||||
|
||||
/** 递归补齐响应中的 id/ID 别名,不影响 Blob、FormData、Date 等特殊数据。 */
|
||||
export const syncCompatibleIdFields = <T>(value: T): T => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => syncCompatibleIdFields(item))
|
||||
return value
|
||||
}
|
||||
|
||||
if (!isPlainObject(value)) return value
|
||||
|
||||
const record = value as Record<string, unknown>
|
||||
const hasId = Object.prototype.hasOwnProperty.call(record, 'id')
|
||||
const hasUpperId = Object.prototype.hasOwnProperty.call(record, 'ID')
|
||||
|
||||
if (hasId || hasUpperId) {
|
||||
const id = record.id ?? record.ID
|
||||
if (id !== null && id !== undefined && id !== '') {
|
||||
if (!hasId) record.id = id
|
||||
if (!hasUpperId) record.ID = id
|
||||
}
|
||||
}
|
||||
|
||||
Object.values(record).forEach((item) => syncCompatibleIdFields(item))
|
||||
return value
|
||||
}
|
||||
@@ -7,3 +7,6 @@ export * from './validate'
|
||||
export * from './calculate'
|
||||
export * from './voucher'
|
||||
export * from './apiRateLimit'
|
||||
export * from './approvalSummary'
|
||||
export * from './expiryEstimate'
|
||||
export * from './id'
|
||||
|
||||
62
src/utils/business/notificationNavigation.ts
Normal file
62
src/utils/business/notificationNavigation.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { RouteLocationRaw, Router } from 'vue-router'
|
||||
import type { NotificationRefType, NotificationTarget } from '@/types/api'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
type TargetRoute = RouteLocationRaw
|
||||
|
||||
const resolveTargetRoute = (
|
||||
refType: NotificationRefType | null,
|
||||
target: NotificationTarget
|
||||
): TargetRoute | null => {
|
||||
const targetId = target.target_id === null ? '' : String(target.target_id)
|
||||
const targetType = target.target_type || refType
|
||||
|
||||
if (targetType === 'integration_log') {
|
||||
const integrationId = target.target_key?.trim()
|
||||
return integrationId
|
||||
? { path: `/audit/integrations/${encodeURIComponent(integrationId)}` }
|
||||
: null
|
||||
}
|
||||
|
||||
switch (targetType) {
|
||||
case 'system_config':
|
||||
return { path: RoutesAlias.SystemConfigs }
|
||||
case 'refund':
|
||||
return { path: RoutesAlias.RefundManagement }
|
||||
case 'agent_recharge':
|
||||
return { path: RoutesAlias.AgentRecharge }
|
||||
case 'iot_card':
|
||||
return targetId ? { path: RoutesAlias.AssetInformation, query: { iccid: targetId } } : null
|
||||
case 'device':
|
||||
return targetId
|
||||
? { path: RoutesAlias.AssetInformation, query: { virtual_no: targetId } }
|
||||
: null
|
||||
case 'expiring_asset':
|
||||
return { path: RoutesAlias.ExpiringAssets }
|
||||
case 'shop_fund':
|
||||
return { path: RoutesAlias.AgentFundOverview }
|
||||
case 'package':
|
||||
case 'asset':
|
||||
case 'wecom_approval':
|
||||
case 'card_sync':
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const navigateNotificationTarget = (
|
||||
router: Router,
|
||||
refType: NotificationRefType | null,
|
||||
target?: NotificationTarget | null
|
||||
) => {
|
||||
if (!target) return false
|
||||
// 目标本身不可用时由调用方提示通知正文,不应将用户带到 403 页面。
|
||||
// 目标可用但进入页面后没有页面权限时,交由路由守卫处理 403。
|
||||
if (!target.available) return false
|
||||
|
||||
const route = resolveTargetRoute(refType, target)
|
||||
if (!route) return false
|
||||
|
||||
void router.push(route)
|
||||
return true
|
||||
}
|
||||
@@ -25,7 +25,10 @@ export const getPaymentMerchantId = (
|
||||
}
|
||||
|
||||
export const getPaymentNotifyUrl = (
|
||||
config: Pick<PaymentSettings, 'provider_type' | 'wx_notify_url' | 'fy_notify_url' | 'ali_notify_url'>
|
||||
config: Pick<
|
||||
PaymentSettings,
|
||||
'provider_type' | 'wx_notify_url' | 'fy_notify_url' | 'ali_notify_url'
|
||||
>
|
||||
): string => {
|
||||
if (config.provider_type === 'wechat' || config.provider_type === 'wechat_v2') {
|
||||
return config.wx_notify_url || '-'
|
||||
|
||||
76
src/utils/business/seriesGrantPackage.ts
Normal file
76
src/utils/business/seriesGrantPackage.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type {
|
||||
GrantPackageInfo,
|
||||
PackageAllocationExpiryBaseOverride,
|
||||
PackageResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
||||
|
||||
export type PackageCostFormItem = {
|
||||
package_id: number
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
cost_price_yuan: number
|
||||
original_cost_price?: number
|
||||
suggested_retail_price?: number
|
||||
}
|
||||
|
||||
export type SeriesGrantPackageForm = {
|
||||
package_id?: number
|
||||
allocation_id?: number
|
||||
package_ids: number[]
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
cost_price_yuan: number
|
||||
original_cost_price?: number
|
||||
suggested_retail_price?: number
|
||||
expiry_base_override: ExpiryBaseSelection
|
||||
initial_expiry_base_override: ExpiryBaseSelection
|
||||
default_expiry_base_name?: string | null
|
||||
expiry_base_override_name?: string | null
|
||||
effective_expiry_base_name?: string | null
|
||||
packages: PackageCostFormItem[]
|
||||
}
|
||||
|
||||
export interface GrantPackageCandidate {
|
||||
id: number
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
cost_price?: number
|
||||
suggested_retail_price?: number | null
|
||||
is_authorized: boolean
|
||||
authorized_package?: GrantPackageInfo
|
||||
}
|
||||
|
||||
export const mergeGrantPackageCandidates = (
|
||||
candidates: PackageResponse[],
|
||||
authorizedPackages: GrantPackageInfo[]
|
||||
): GrantPackageCandidate[] => {
|
||||
const merged = new Map<number, GrantPackageCandidate>()
|
||||
|
||||
candidates.forEach((candidate) => {
|
||||
merged.set(candidate.id, {
|
||||
id: candidate.id,
|
||||
package_name: candidate.package_name,
|
||||
package_code: candidate.package_code,
|
||||
cost_price: candidate.cost_price,
|
||||
suggested_retail_price: candidate.suggested_retail_price,
|
||||
is_authorized: false
|
||||
})
|
||||
})
|
||||
|
||||
authorizedPackages.forEach((authorizedPackage) => {
|
||||
const candidate = merged.get(authorizedPackage.package_id)
|
||||
merged.set(authorizedPackage.package_id, {
|
||||
id: authorizedPackage.package_id,
|
||||
package_name: candidate?.package_name ?? authorizedPackage.package_name,
|
||||
package_code: candidate?.package_code ?? authorizedPackage.package_code,
|
||||
cost_price: authorizedPackage.cost_price ?? candidate?.cost_price,
|
||||
suggested_retail_price: candidate?.suggested_retail_price,
|
||||
is_authorized: true,
|
||||
authorized_package: authorizedPackage
|
||||
})
|
||||
})
|
||||
|
||||
return [...merged.values()]
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { ApiStatus } from './status'
|
||||
import type { RequestOptions, ErrorMessageMode } from '@/types/api'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import { syncCompatibleIdFields } from '@/utils/business/id'
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
timeout: 15000, // 请求超时时间(毫秒)
|
||||
@@ -89,6 +91,9 @@ const processQueue = (error: any, token: string | null = null) => {
|
||||
// 响应拦截器
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 兼容历史接口返回的 ID 与新接口返回的 id。
|
||||
syncCompatibleIdFields(response.data)
|
||||
|
||||
// 401 未授权 - 尝试刷新 token
|
||||
if (response.data.code === ApiStatus.unauthorized) {
|
||||
const userStore = useUserStore()
|
||||
@@ -220,7 +225,8 @@ function handleErrorMessage(
|
||||
) {
|
||||
if (mode === 'none') return
|
||||
|
||||
const backendMessage = error.response?.data?.msg
|
||||
const normalized = normalizeApiError(error)
|
||||
const backendMessage = normalized.message
|
||||
const httpStatus = error.response?.status
|
||||
|
||||
// 401 过期类错误由 clearLocalStateAndRedirect 统一提示,避免重复弹出后端报错。
|
||||
@@ -245,7 +251,7 @@ function handleErrorMessage(
|
||||
}
|
||||
|
||||
// 其他错误显示通用提示
|
||||
const message = '请求超时或服务器异常!'
|
||||
const message = normalized.message
|
||||
if (mode === 'modal') {
|
||||
// TODO: 可以使用 ElMessageBox 显示模态框
|
||||
ElMessage.error(message)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!--suppress ALL -->
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="account-page" id="table-full-screen">
|
||||
@@ -82,9 +83,9 @@
|
||||
>
|
||||
<ElOption
|
||||
v-for="role in availableRolesForCreate"
|
||||
:key="role.ID"
|
||||
:key="getRoleId(role)"
|
||||
:label="role.role_name"
|
||||
:value="role.ID"
|
||||
:value="getRoleId(role)"
|
||||
>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span>{{ role.role_name }}</span>
|
||||
@@ -133,8 +134,15 @@
|
||||
<ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" />
|
||||
<template v-else-if="isPlatformUser">
|
||||
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
|
||||
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
|
||||
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
|
||||
<div
|
||||
v-for="role in filteredAvailableRoles"
|
||||
:key="getRoleId(role)"
|
||||
class="role-item"
|
||||
>
|
||||
<ElCheckbox
|
||||
:label="getRoleId(role)"
|
||||
:disabled="selectedRoles.includes(getRoleId(role))"
|
||||
>
|
||||
<span class="role-info">
|
||||
<span>{{ role.role_name }}</span>
|
||||
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
|
||||
@@ -147,8 +155,15 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElRadioGroup v-model="roleToAdd" class="role-list">
|
||||
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
|
||||
<ElRadio :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
|
||||
<div
|
||||
v-for="role in filteredAvailableRoles"
|
||||
:key="getRoleId(role)"
|
||||
class="role-item"
|
||||
>
|
||||
<ElRadio
|
||||
:label="getRoleId(role)"
|
||||
:disabled="selectedRoles.includes(getRoleId(role))"
|
||||
>
|
||||
<span class="role-info">
|
||||
<span>{{ role.role_name }}</span>
|
||||
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
|
||||
@@ -190,7 +205,7 @@
|
||||
<div class="role-list">
|
||||
<div
|
||||
v-for="role in filteredAssignedRoles"
|
||||
:key="role.ID"
|
||||
:key="getRoleId(role)"
|
||||
class="role-item assigned-role-item"
|
||||
>
|
||||
<span class="role-info">
|
||||
@@ -199,7 +214,12 @@
|
||||
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||
</ElTag>
|
||||
</span>
|
||||
<ElButton type="danger" size="small" link @click="removeSingleRole(role.ID)">
|
||||
<ElButton
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@click="removeSingleRole(getRoleId(role))"
|
||||
>
|
||||
移除
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -213,6 +233,74 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 绑定企微账号对话框 -->
|
||||
<ElDialog
|
||||
v-model="wecomBindingDialogVisible"
|
||||
title="绑定企微账号"
|
||||
width="520px"
|
||||
destroy-on-close
|
||||
@closed="resetWecomBindingForm"
|
||||
>
|
||||
<ElForm
|
||||
ref="wecomBindingFormRef"
|
||||
:model="wecomBindingForm"
|
||||
:rules="wecomBindingRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<ElFormItem label="账号名称">
|
||||
<span>{{ currentWecomAccount?.username || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="企微应用" prop="application_id">
|
||||
<ElSelect
|
||||
v-model="wecomBindingForm.application_id"
|
||||
placeholder="请选择企微应用"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
:loading="wecomApplicationsLoading"
|
||||
@change="handleWecomApplicationChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="application in wecomApplications"
|
||||
:key="application.id"
|
||||
:label="application.name"
|
||||
:value="application.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="企微成员" prop="userid">
|
||||
<ElSelect
|
||||
v-model="wecomBindingForm.userid"
|
||||
placeholder="请先选择企微应用"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
:loading="wecomMembersLoading"
|
||||
:disabled="!wecomBindingForm.application_id"
|
||||
>
|
||||
<ElOption
|
||||
v-for="member in wecomMembers"
|
||||
:key="member.userid"
|
||||
:label="member.name"
|
||||
:value="member.userid"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="wecomBindingDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="wecomBindingSubmitting"
|
||||
@click="submitWecomBinding"
|
||||
>
|
||||
确认绑定
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -237,21 +325,34 @@
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { AccountService } from '@/api/modules/account'
|
||||
import { RoleService } from '@/api/modules/role'
|
||||
import { ShopService, EnterpriseService } from '@/api/modules'
|
||||
import { ShopService, EnterpriseService, WecomService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { PlatformRole } from '@/types/api'
|
||||
import type { PlatformRole, WecomApplication, WecomMember } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||
import { getCompatibleNumericId } from '@/utils/business/id'
|
||||
import {
|
||||
CommonStatus,
|
||||
getStatusText,
|
||||
JULY_PERMISSIONS,
|
||||
STATUS_SELECT_OPTIONS
|
||||
} from '@/config/constants'
|
||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||
import { resolveAuditResourceTarget } from '@/utils/business/auditNavigation'
|
||||
import { isPlatformAuditAccount } from '@/utils/business/auditAccess'
|
||||
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const userStore = useUserStore()
|
||||
const canModifyAccountStatus = hasAuth('account:modify_status')
|
||||
const route = useRoute()
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const roleDialogVisible = ref(false)
|
||||
const wecomBindingDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const currentAccountId = ref<number>(0)
|
||||
const currentAccountName = ref<string>('')
|
||||
@@ -262,10 +363,28 @@
|
||||
const roleToAdd = ref<number | undefined>(undefined) // 单选时使用
|
||||
const leftRoleFilter = ref('')
|
||||
const rightRoleFilter = ref('')
|
||||
const currentWecomAccount = ref<any | null>(null)
|
||||
const wecomApplications = ref<WecomApplication[]>([])
|
||||
const wecomMembers = ref<WecomMember[]>([])
|
||||
const wecomApplicationsLoading = ref(false)
|
||||
const wecomMembersLoading = ref(false)
|
||||
const wecomBindingSubmitting = ref(false)
|
||||
const wecomBindingFormRef = ref<FormInstance>()
|
||||
const wecomBindingForm = reactive({
|
||||
application_id: undefined as number | undefined,
|
||||
userid: ''
|
||||
})
|
||||
const wecomBindingRules = reactive<FormRules>({
|
||||
application_id: [{ required: true, message: '请选择企微应用', trigger: 'change' }],
|
||||
userid: [{ required: true, message: '请选择企微成员', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 是否为平台用户(平台用户可多选,其他单选)
|
||||
const isPlatformUser = computed(() => currentAccountType.value === 2)
|
||||
|
||||
const getAccountId = (account: any) => getCompatibleNumericId(account) ?? 0
|
||||
const getRoleId = (role: PlatformRole | any) => getCompatibleNumericId(role) ?? 0
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
@@ -400,6 +519,8 @@
|
||||
{ label: '账号类型', prop: 'user_type' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '企业名称', prop: 'enterprise_name' },
|
||||
{ label: '企微绑定', prop: 'wecom_bound' },
|
||||
{ label: '企微用户名称', prop: 'wecom_name' },
|
||||
...(canModifyAccountStatus ? [{ label: '状态', prop: 'status' }] : []),
|
||||
{ label: '创建时间', prop: 'created_at' }
|
||||
]
|
||||
@@ -503,11 +624,27 @@
|
||||
{
|
||||
prop: 'enterprise_name',
|
||||
label: '企业名称',
|
||||
minWidth: 150,
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: any) => {
|
||||
return row.enterprise_name || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'wecom_bound',
|
||||
label: '企微绑定',
|
||||
width: 100,
|
||||
formatter: (row: any) =>
|
||||
h(ElTag, { type: row.wecom_bound ? 'success' : 'info', size: 'small' }, () =>
|
||||
row.wecom_bound ? '已绑定' : '未绑定'
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'wecom_name',
|
||||
label: '企微用户名称',
|
||||
minWidth: 130,
|
||||
formatter: (row: any) => row.wecom_name || '-'
|
||||
},
|
||||
...(canModifyAccountStatus
|
||||
? [
|
||||
{
|
||||
@@ -541,6 +678,23 @@
|
||||
const getActions = (row: any) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (
|
||||
isPlatformAuditAccount(userStore.info.user_type, userStore.isSuperAdmin) &&
|
||||
hasAuth(AUDIT_PERMISSIONS.resourceTimeline)
|
||||
) {
|
||||
const auditTarget = resolveAuditResourceTarget({
|
||||
userType: userStore.info.user_type,
|
||||
resourceType: 'account',
|
||||
internalId: row.id
|
||||
})
|
||||
if (auditTarget)
|
||||
actions.push({
|
||||
label: '审计记录',
|
||||
handler: () => openAuditInvestigation(auditTarget),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('account:patch_role')) {
|
||||
actions.push({
|
||||
label: '分配角色',
|
||||
@@ -549,6 +703,14 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (row.user_type === 2 && hasAuth(JULY_PERMISSIONS.wecom.binding)) {
|
||||
actions.push({
|
||||
label: '绑定账号',
|
||||
handler: () => showWecomBindingDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('account:edit')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
@@ -568,6 +730,104 @@
|
||||
return actions
|
||||
}
|
||||
|
||||
const showWecomBindingDialog = async (row: any) => {
|
||||
if (row.user_type !== 2) {
|
||||
ElMessage.warning('只有平台用户可以绑定企微账号')
|
||||
return
|
||||
}
|
||||
|
||||
currentWecomAccount.value = row
|
||||
wecomBindingForm.application_id = undefined
|
||||
wecomBindingForm.userid = ''
|
||||
wecomMembers.value = []
|
||||
wecomBindingDialogVisible.value = true
|
||||
await loadWecomApplications()
|
||||
|
||||
if (row.wecom_bound && row.wecom_corp_id) {
|
||||
const boundApplication = wecomApplications.value.find(
|
||||
(application) => application.corp_id === row.wecom_corp_id
|
||||
)
|
||||
if (boundApplication) {
|
||||
wecomBindingForm.application_id = boundApplication.id
|
||||
await handleWecomApplicationChange(boundApplication.id, row.wecom_userid || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadWecomApplications = async () => {
|
||||
wecomApplicationsLoading.value = true
|
||||
try {
|
||||
const response = await WecomService.getApplications({ page: 1, page_size: 100 })
|
||||
if (response.code === 0) wecomApplications.value = response.data.items || []
|
||||
} finally {
|
||||
wecomApplicationsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleWecomApplicationChange = async (applicationId?: number, selectedUserid = '') => {
|
||||
wecomBindingForm.userid = selectedUserid
|
||||
wecomMembers.value = []
|
||||
if (!applicationId) return
|
||||
|
||||
wecomMembersLoading.value = true
|
||||
try {
|
||||
const response = await WecomService.getMembers(applicationId, {
|
||||
page: 1,
|
||||
page_size: 100
|
||||
})
|
||||
if (response.code === 0) {
|
||||
wecomMembers.value = response.data.items || []
|
||||
if (
|
||||
selectedUserid &&
|
||||
!wecomMembers.value.some((member) => member.userid === selectedUserid)
|
||||
) {
|
||||
wecomBindingForm.userid = selectedUserid
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
wecomMembersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitWecomBinding = async () => {
|
||||
const valid = await wecomBindingFormRef.value?.validate().catch(() => false)
|
||||
if (!valid || !currentWecomAccount.value || !wecomBindingForm.application_id) return
|
||||
|
||||
if (currentWecomAccount.value.user_type !== 2) {
|
||||
ElMessage.error('只有平台用户可以绑定企微账号')
|
||||
return
|
||||
}
|
||||
|
||||
const accountId = getAccountId(currentWecomAccount.value)
|
||||
if (!accountId) {
|
||||
ElMessage.error('当前账号缺少账号标识,无法绑定企微')
|
||||
return
|
||||
}
|
||||
|
||||
wecomBindingSubmitting.value = true
|
||||
try {
|
||||
const response = await AccountService.bindWecom(accountId, {
|
||||
application_id: wecomBindingForm.application_id,
|
||||
userid: wecomBindingForm.userid
|
||||
})
|
||||
if (response.code === 0) {
|
||||
ElMessage.success('账号企微绑定成功')
|
||||
wecomBindingDialogVisible.value = false
|
||||
await getAccountList()
|
||||
}
|
||||
} finally {
|
||||
wecomBindingSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetWecomBindingForm = () => {
|
||||
currentWecomAccount.value = null
|
||||
wecomBindingForm.application_id = undefined
|
||||
wecomBindingForm.userid = ''
|
||||
wecomMembers.value = []
|
||||
wecomBindingFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
@@ -646,7 +906,9 @@
|
||||
|
||||
// 计算属性:过滤后的已分配角色
|
||||
const filteredAssignedRoles = computed(() => {
|
||||
const assignedRolesList = allRoles.value.filter((role) => selectedRoles.value.includes(role.ID))
|
||||
const assignedRolesList = allRoles.value.filter((role) =>
|
||||
selectedRoles.value.includes(getRoleId(role))
|
||||
)
|
||||
if (!rightRoleFilter.value) return assignedRolesList
|
||||
const keyword = rightRoleFilter.value.toLowerCase()
|
||||
return assignedRolesList.filter((role) => role.role_name.toLowerCase().includes(keyword))
|
||||
@@ -658,7 +920,12 @@
|
||||
ElMessage.warning('超级管理员无法分配角色')
|
||||
return
|
||||
}
|
||||
currentAccountId.value = row.id
|
||||
const accountId = getAccountId(row)
|
||||
if (!accountId) {
|
||||
ElMessage.error('当前账号缺少账号标识,无法分配角色')
|
||||
return
|
||||
}
|
||||
currentAccountId.value = accountId
|
||||
currentAccountName.value = row.username
|
||||
currentAccountType.value = row.user_type
|
||||
selectedRoles.value = []
|
||||
@@ -675,12 +942,12 @@
|
||||
await loadAllRoles('', roleType)
|
||||
|
||||
// 先加载当前账号的角色,再打开对话框
|
||||
const res = await AccountService.getAccountRoles(row.id)
|
||||
const res = await AccountService.getAccountRoles(accountId)
|
||||
if (res.code === 0) {
|
||||
// 提取角色ID数组
|
||||
const roles = res.data || []
|
||||
// 兼容 ID 和 id 两种字段名
|
||||
selectedRoles.value = roles.map((role: any) => role.ID || role.id)
|
||||
selectedRoles.value = roles.map((role: any) => getRoleId(role)).filter(Boolean)
|
||||
// 数据加载完成后再打开对话框
|
||||
roleDialogVisible.value = true
|
||||
}
|
||||
@@ -836,7 +1103,7 @@
|
||||
|
||||
// 创建成功后分配角色(如果选择了)
|
||||
if (createRes.code === 0) {
|
||||
const accountId = createRes.data?.ID || createRes.data?.id
|
||||
const accountId = getCompatibleNumericId(createRes.data)
|
||||
if (formData.role_id && accountId) {
|
||||
try {
|
||||
await AccountService.assignRolesToAccount(accountId, [formData.role_id])
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<!--suppress VueUnrecognizedDirective -->
|
||||
<ElButton @click="showDialog('add')" v-permission="'enterprise_customer:add'"
|
||||
>新增企业客户</ElButton
|
||||
>
|
||||
@@ -204,6 +205,9 @@
|
||||
import { h, onActivated } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||
import { resolveAuditResourceTarget } from '@/utils/business/auditNavigation'
|
||||
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
|
||||
import { EnterpriseService, ShopService } from '@/api/modules'
|
||||
import { ElMessage, ElSwitch, ElCascader } from 'element-plus'
|
||||
import type { CascaderOption, CascaderValue, FormInstance, FormRules } from 'element-plus'
|
||||
@@ -436,7 +440,7 @@
|
||||
{
|
||||
prop: 'contact_name',
|
||||
label: '联系人姓名',
|
||||
width: 120
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'contact_phone',
|
||||
@@ -502,6 +506,24 @@
|
||||
const getActions = (row: EnterpriseItem) => {
|
||||
const actions: any[] = []
|
||||
|
||||
const userType = Number(userStore.getUserInfo.user_type)
|
||||
const auditPermission =
|
||||
userType === 3 ? AUDIT_PERMISSIONS.agentActivity : AUDIT_PERMISSIONS.enterpriseEntry
|
||||
if (hasAuth(auditPermission)) {
|
||||
const auditTarget = resolveAuditResourceTarget({
|
||||
userType,
|
||||
resourceType: 'enterprise',
|
||||
internalId: row.id,
|
||||
businessIdentifier: row.enterprise_code
|
||||
})
|
||||
if (auditTarget)
|
||||
actions.push({
|
||||
label: userType === 3 ? '活动记录' : '审计记录',
|
||||
handler: () => openAuditInvestigation(auditTarget),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:look_customer')) {
|
||||
actions.push({
|
||||
label: '账号列表',
|
||||
@@ -786,7 +808,7 @@
|
||||
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
getTableData()
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user