This commit is contained in:
@@ -32,7 +32,7 @@ import type {
|
|||||||
AssetPackageUsageRecord,
|
AssetPackageUsageRecord,
|
||||||
UpdateAssetPackageUsedDataRequest,
|
UpdateAssetPackageUsedDataRequest,
|
||||||
UpdateAssetPackageExpiresAtRequest,
|
UpdateAssetPackageExpiresAtRequest,
|
||||||
ExpiringAssetItem,
|
ExpiringAssetListResponse,
|
||||||
ExpiringAssetQueryParams
|
ExpiringAssetQueryParams
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
@@ -53,12 +53,8 @@ export class AssetService extends BaseService {
|
|||||||
*/
|
*/
|
||||||
static getExpiringAssets(
|
static getExpiringAssets(
|
||||||
params?: ExpiringAssetQueryParams
|
params?: ExpiringAssetQueryParams
|
||||||
): Promise<
|
): Promise<BaseResponse<ExpiringAssetListResponse>> {
|
||||||
BaseResponse<{ items: ExpiringAssetItem[]; total: number; page: number; size: number }>
|
return this.get<BaseResponse<ExpiringAssetListResponse>>('/api/admin/expiring-assets', params)
|
||||||
> {
|
|
||||||
return this.get<
|
|
||||||
BaseResponse<{ items: ExpiringAssetItem[]; total: number; page: number; size: number }>
|
|
||||||
>('/api/admin/expiring-assets', params)
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 通过任意标识符查询设备或卡的完整详情
|
* 通过任意标识符查询设备或卡的完整详情
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ export class CardService extends BaseService {
|
|||||||
): Promise<BaseResponse<SetSpeedTierResponse>> {
|
): Promise<BaseResponse<SetSpeedTierResponse>> {
|
||||||
return this.put<BaseResponse<SetSpeedTierResponse>>(
|
return this.put<BaseResponse<SetSpeedTierResponse>>(
|
||||||
`/api/admin/iot-cards/${encodeURIComponent(iccid)}/speed-tier`,
|
`/api/admin/iot-cards/${encodeURIComponent(iccid)}/speed-tier`,
|
||||||
{ code }
|
{ code },
|
||||||
|
{ requestOptions: { errorMessageMode: 'none' } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// ========== 号卡商品管理 ==========
|
// ========== 号卡商品管理 ==========
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { BaseService } from '../BaseService'
|
import { BaseService } from '../BaseService'
|
||||||
import type {
|
import type {
|
||||||
CancelExportTaskApiResponse,
|
|
||||||
CreateExportTaskApiResponse,
|
CreateExportTaskApiResponse,
|
||||||
CreateExportTaskRequest,
|
CreateExportTaskRequest,
|
||||||
ExportTaskDetail,
|
|
||||||
ExportTaskDetailApiResponse,
|
ExportTaskDetailApiResponse,
|
||||||
ExportTaskListApiResponse,
|
ExportTaskListApiResponse,
|
||||||
ExportTaskQueryParams
|
ExportTaskQueryParams
|
||||||
@@ -21,9 +19,4 @@ export class ExportTaskService extends BaseService {
|
|||||||
static getExportTaskDetail(id: number): Promise<ExportTaskDetailApiResponse> {
|
static getExportTaskDetail(id: number): Promise<ExportTaskDetailApiResponse> {
|
||||||
return this.get<ExportTaskDetailApiResponse>(`/api/admin/export-tasks/${id}`)
|
return this.get<ExportTaskDetailApiResponse>(`/api/admin/export-tasks/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保留旧页面的取消入口,后端支持时由页面权限控制显示。 */
|
|
||||||
static cancelExportTask(id: number): Promise<CancelExportTaskApiResponse> {
|
|
||||||
return this.post<CancelExportTaskApiResponse>(`/api/admin/export-tasks/${id}/cancel`, {})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
BaseResponse,
|
BaseResponse,
|
||||||
NotificationListResponse,
|
NotificationListResponse,
|
||||||
NotificationQueryParams,
|
NotificationQueryParams,
|
||||||
|
NotificationReadRequest,
|
||||||
NotificationReadAllRequest,
|
NotificationReadAllRequest,
|
||||||
NotificationReadAllResponse,
|
NotificationReadAllResponse,
|
||||||
NotificationReadResponse,
|
NotificationReadResponse,
|
||||||
@@ -29,10 +30,9 @@ export class NotificationService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static markRead(id: number): Promise<BaseResponse<NotificationReadResponse>> {
|
static markRead(id: number): Promise<BaseResponse<NotificationReadResponse>> {
|
||||||
return this.put<BaseResponse<NotificationReadResponse>>(
|
return this.put<BaseResponse<NotificationReadResponse>>(`/api/admin/notifications/${id}/read`, {
|
||||||
`/api/admin/notifications/${id}/read`,
|
id
|
||||||
{}
|
} satisfies NotificationReadRequest)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static markAllRead(
|
static markAllRead(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
WecomApplicationQueryParams,
|
WecomApplicationQueryParams,
|
||||||
WecomApplicationRequest,
|
WecomApplicationRequest,
|
||||||
WecomApplicationResponse,
|
WecomApplicationResponse,
|
||||||
|
WecomBusinessFieldListResponse,
|
||||||
WecomBusinessType,
|
WecomBusinessType,
|
||||||
WecomMemberListResponse,
|
WecomMemberListResponse,
|
||||||
WecomMemberQueryParams,
|
WecomMemberQueryParams,
|
||||||
@@ -14,7 +15,9 @@ import type {
|
|||||||
WecomSceneQueryParams,
|
WecomSceneQueryParams,
|
||||||
WecomSceneRequest,
|
WecomSceneRequest,
|
||||||
WecomSceneResponse,
|
WecomSceneResponse,
|
||||||
WecomSyncMembersApiResponse
|
WecomSyncMembersApiResponse,
|
||||||
|
WecomTemplateDetailResponse,
|
||||||
|
WecomTemplateInspectRequest
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
export class WecomService extends BaseService {
|
export class WecomService extends BaseService {
|
||||||
@@ -38,14 +41,8 @@ export class WecomService extends BaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getMembers(
|
static getMembers(id: number, params?: WecomMemberQueryParams): Promise<WecomMemberListResponse> {
|
||||||
id: number,
|
return this.get<WecomMemberListResponse>(`/api/admin/wecom/applications/${id}/members`, params)
|
||||||
params?: WecomMemberQueryParams
|
|
||||||
): Promise<WecomMemberListResponse> {
|
|
||||||
return this.get<WecomMemberListResponse>(
|
|
||||||
`/api/admin/wecom/applications/${id}/members`,
|
|
||||||
params
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static setDefaultCreator(id: number, userid: string): Promise<WecomApplicationResponse> {
|
static setDefaultCreator(id: number, userid: string): Promise<WecomApplicationResponse> {
|
||||||
@@ -66,6 +63,24 @@ export class WecomService extends BaseService {
|
|||||||
return this.put<WecomSceneResponse>(`/api/admin/wecom/scenes/${businessType}`, data)
|
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(
|
static bindAccount(
|
||||||
accountId: number,
|
accountId: number,
|
||||||
data: WecomAccountBindingRequest
|
data: WecomAccountBindingRequest
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
initialOrderId?: number
|
initialOrderId?: number
|
||||||
initialOrderNo?: string
|
initialOrderNo?: string
|
||||||
|
initialPackageUsageId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -119,6 +120,7 @@
|
|||||||
|
|
||||||
const formData = reactive<{
|
const formData = reactive<{
|
||||||
order_id: number | null
|
order_id: number | null
|
||||||
|
package_usage_id?: number
|
||||||
requested_refund_amount?: number
|
requested_refund_amount?: number
|
||||||
actual_received_amount: number
|
actual_received_amount: number
|
||||||
refund_reason: string
|
refund_reason: string
|
||||||
@@ -127,6 +129,7 @@
|
|||||||
attachments: RefundAttachment[]
|
attachments: RefundAttachment[]
|
||||||
}>({
|
}>({
|
||||||
order_id: null,
|
order_id: null,
|
||||||
|
package_usage_id: undefined,
|
||||||
requested_refund_amount: undefined,
|
requested_refund_amount: undefined,
|
||||||
actual_received_amount: 0,
|
actual_received_amount: 0,
|
||||||
refund_reason: '',
|
refund_reason: '',
|
||||||
@@ -213,6 +216,7 @@
|
|||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
voucherUploading.value = false
|
voucherUploading.value = false
|
||||||
formData.order_id = null
|
formData.order_id = null
|
||||||
|
formData.package_usage_id = undefined
|
||||||
formData.requested_refund_amount = undefined
|
formData.requested_refund_amount = undefined
|
||||||
formData.actual_received_amount = 0
|
formData.actual_received_amount = 0
|
||||||
formData.refund_reason = ''
|
formData.refund_reason = ''
|
||||||
@@ -239,6 +243,7 @@
|
|||||||
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0,
|
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0,
|
||||||
actual_received_amount: formData.actual_received_amount,
|
actual_received_amount: formData.actual_received_amount,
|
||||||
attachments: formData.attachments,
|
attachments: formData.attachments,
|
||||||
|
package_usage_id: formData.package_usage_id,
|
||||||
refund_reason: formData.refund_reason || undefined,
|
refund_reason: formData.refund_reason || undefined,
|
||||||
remark: formData.remark || undefined
|
remark: formData.remark || undefined
|
||||||
}
|
}
|
||||||
@@ -261,6 +266,7 @@
|
|||||||
if (hasInitialOrder.value && props.initialOrderId) {
|
if (hasInitialOrder.value && props.initialOrderId) {
|
||||||
formData.order_id = props.initialOrderId
|
formData.order_id = props.initialOrderId
|
||||||
}
|
}
|
||||||
|
formData.package_usage_id = props.initialPackageUsageId
|
||||||
if (hasInitialOrder.value && props.initialOrderNo) {
|
if (hasInitialOrder.value && props.initialOrderNo) {
|
||||||
searchOrders(props.initialOrderNo)
|
searchOrders(props.initialOrderNo)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -85,13 +85,22 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const getNormalizedQuery = () => {
|
const getNormalizedQuery = (): Record<string, string> => {
|
||||||
const query: Record<string, unknown> = {}
|
const query: Record<string, string> = {}
|
||||||
|
|
||||||
Object.entries(props.query || {}).forEach(([key, value]) => {
|
Object.entries(props.query || {}).forEach(([key, value]) => {
|
||||||
if (value === undefined || value === null || value === '') return
|
if (value === undefined || value === null || value === '') return
|
||||||
if (Array.isArray(value) && value.length === 0) 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
|
return query
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<span class="text">
|
<span class="text">
|
||||||
<strong>{{ item.title }}</strong>
|
<strong>{{ item.title }}</strong>
|
||||||
<small>{{ item.body }}</small>
|
<small>{{ item.body }}</small>
|
||||||
<time>{{ item.created_at || '-' }}</time>
|
<time>{{ formatDateTime(item.created_at) }}</time>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!isRead(item)" class="unread-dot" />
|
<span v-if="!isRead(item)" class="unread-dot" />
|
||||||
</button>
|
</button>
|
||||||
@@ -50,9 +50,6 @@
|
|||||||
<p>暂无通知</p>
|
<p>暂无通知</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-wrapper">
|
|
||||||
<ElButton class="view-all" @click="handleViewAll">进入通知中心</ElButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +65,7 @@
|
|||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { NOTIFICATION_PERMISSIONS } from '@/config/constants/notification'
|
import { NOTIFICATION_PERMISSIONS } from '@/config/constants/notification'
|
||||||
import { navigateNotificationTarget } from '@/utils/business/notificationNavigation'
|
import { navigateNotificationTarget } from '@/utils/business/notificationNavigation'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: boolean }>()
|
const props = defineProps<{ modelValue: boolean }>()
|
||||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||||
@@ -108,11 +106,6 @@
|
|||||||
|
|
||||||
const close = () => emit('update:modelValue', false)
|
const close = () => emit('update:modelValue', false)
|
||||||
|
|
||||||
const handleViewAll = () => {
|
|
||||||
close()
|
|
||||||
void router.push('/notifications')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMarkAllRead = async () => {
|
const handleMarkAllRead = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await notificationStore.markAllRead()
|
const response = await notificationStore.markAllRead()
|
||||||
|
|||||||
@@ -158,15 +158,6 @@
|
|||||||
font-size: 42px;
|
font-size: 42px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-wrapper {
|
|
||||||
padding: 12px 16px 16px;
|
|
||||||
border-top: 1px solid var(--art-border-color);
|
|
||||||
|
|
||||||
.view-all {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $device-phone) {
|
@media only screen and (max-width: $device-phone) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { ExportTaskScene } from '@/types/api'
|
|||||||
export interface ExportTaskScenePermissions {
|
export interface ExportTaskScenePermissions {
|
||||||
detail: string
|
detail: string
|
||||||
download: string
|
download: string
|
||||||
cancel: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportTaskSceneConfig {
|
export interface ExportTaskSceneConfig {
|
||||||
@@ -20,8 +19,7 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
|||||||
pageTitle: '导出设备',
|
pageTitle: '导出设备',
|
||||||
permissions: {
|
permissions: {
|
||||||
detail: 'export_task:device_detail',
|
detail: 'export_task:device_detail',
|
||||||
download: 'export_task:device_download',
|
download: 'export_task:device_download'
|
||||||
cancel: 'export_task:device_cancel'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
iot_card: {
|
iot_card: {
|
||||||
@@ -30,8 +28,7 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
|||||||
pageTitle: '导出IOT卡',
|
pageTitle: '导出IOT卡',
|
||||||
permissions: {
|
permissions: {
|
||||||
detail: 'export_task:iot_card_detail',
|
detail: 'export_task:iot_card_detail',
|
||||||
download: 'export_task:iot_card_download',
|
download: 'export_task:iot_card_download'
|
||||||
cancel: 'export_task:iot_card_cancel'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
@@ -40,8 +37,7 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
|||||||
pageTitle: '导出订单',
|
pageTitle: '导出订单',
|
||||||
permissions: {
|
permissions: {
|
||||||
detail: 'export_task:order_detail',
|
detail: 'export_task:order_detail',
|
||||||
download: 'export_task:order_download',
|
download: 'export_task:order_download'
|
||||||
cancel: 'export_task:order_cancel'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
package: {
|
package: {
|
||||||
@@ -50,8 +46,7 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
|||||||
pageTitle: '导出套餐',
|
pageTitle: '导出套餐',
|
||||||
permissions: {
|
permissions: {
|
||||||
detail: 'export_task:package_detail',
|
detail: 'export_task:package_detail',
|
||||||
download: 'export_task:package_download',
|
download: 'export_task:package_download'
|
||||||
cancel: 'export_task:package_cancel'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
agent_wallet_transaction: {
|
agent_wallet_transaction: {
|
||||||
@@ -60,8 +55,7 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
|||||||
pageTitle: '导出代理主钱包流水',
|
pageTitle: '导出代理主钱包流水',
|
||||||
permissions: {
|
permissions: {
|
||||||
detail: 'export_task:agent_wallet_transaction_detail',
|
detail: 'export_task:agent_wallet_transaction_detail',
|
||||||
download: 'export_task:agent_wallet_transaction_download',
|
download: 'export_task:agent_wallet_transaction_download'
|
||||||
cancel: 'export_task:agent_wallet_transaction_cancel'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
agent_recharge: {
|
agent_recharge: {
|
||||||
@@ -70,8 +64,7 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
|||||||
pageTitle: '导出代理充值',
|
pageTitle: '导出代理充值',
|
||||||
permissions: {
|
permissions: {
|
||||||
detail: 'export_task:agent_recharge_detail',
|
detail: 'export_task:agent_recharge_detail',
|
||||||
download: 'export_task:agent_recharge_download',
|
download: 'export_task:agent_recharge_download'
|
||||||
cancel: 'export_task:agent_recharge_cancel'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refund: {
|
refund: {
|
||||||
@@ -80,8 +73,7 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
|||||||
pageTitle: '导出退款',
|
pageTitle: '导出退款',
|
||||||
permissions: {
|
permissions: {
|
||||||
detail: 'export_task:refund_detail',
|
detail: 'export_task:refund_detail',
|
||||||
download: 'export_task:refund_download',
|
download: 'export_task:refund_download'
|
||||||
cancel: 'export_task:refund_cancel'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exchange: {
|
exchange: {
|
||||||
@@ -90,8 +82,7 @@ export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneCo
|
|||||||
pageTitle: '导出换货',
|
pageTitle: '导出换货',
|
||||||
permissions: {
|
permissions: {
|
||||||
detail: 'export_task:exchange_detail',
|
detail: 'export_task:exchange_detail',
|
||||||
download: 'export_task:exchange_download',
|
download: 'export_task:exchange_download'
|
||||||
cancel: 'export_task:exchange_cancel'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export const JULY_PERMISSIONS = {
|
|||||||
create: 'device_task:allocation:create',
|
create: 'device_task:allocation:create',
|
||||||
detail: 'device_task:allocation:detail'
|
detail: 'device_task:allocation:detail'
|
||||||
},
|
},
|
||||||
|
seriesGrants: {
|
||||||
|
updateExpiryBase: 'series_grants:update_expiry_base'
|
||||||
|
},
|
||||||
batchPurchase: {
|
batchPurchase: {
|
||||||
page: 'bulk_purchase:view',
|
page: 'bulk_purchase:view',
|
||||||
create: 'bulk_purchase:create',
|
create: 'bulk_purchase:create',
|
||||||
|
|||||||
@@ -432,6 +432,9 @@
|
|||||||
"standaloneCardList": "IoT Card Management",
|
"standaloneCardList": "IoT Card Management",
|
||||||
"iotCardTask": "IoT Card Tasks",
|
"iotCardTask": "IoT Card Tasks",
|
||||||
"deviceTask": "Device Tasks",
|
"deviceTask": "Device Tasks",
|
||||||
|
"deviceBatchAllocation": "Device Batch Allocation",
|
||||||
|
"bulkPurchase": "Bulk Package Purchase",
|
||||||
|
"bulkPurchaseDetail": "Bulk Purchase Task Detail",
|
||||||
"orderPackageInvalidateTask": "Order Package Void Tasks",
|
"orderPackageInvalidateTask": "Order Package Void Tasks",
|
||||||
"orderPackageInvalidateTaskDetail": "Order Package Void Task Detail",
|
"orderPackageInvalidateTaskDetail": "Order Package Void Task Detail",
|
||||||
"taskDetail": "Task Details",
|
"taskDetail": "Task Details",
|
||||||
@@ -452,6 +455,11 @@
|
|||||||
"exportDevice": "Export Devices",
|
"exportDevice": "Export Devices",
|
||||||
"exportIotCard": "Export IOT Cards",
|
"exportIotCard": "Export IOT Cards",
|
||||||
"exportOrder": "Export Orders",
|
"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",
|
"exportTaskDetail": "Export Task Detail",
|
||||||
"exchangeManagement": "Exchange Management",
|
"exchangeManagement": "Exchange Management",
|
||||||
"exchangeDetail": "Exchange Order Detail"
|
"exchangeDetail": "Exchange Order Detail"
|
||||||
|
|||||||
@@ -370,6 +370,9 @@
|
|||||||
"standaloneCardList": "IoT卡管理",
|
"standaloneCardList": "IoT卡管理",
|
||||||
"iotCardTask": "IoT卡任务",
|
"iotCardTask": "IoT卡任务",
|
||||||
"deviceTask": "设备任务",
|
"deviceTask": "设备任务",
|
||||||
|
"deviceBatchAllocation": "设备批量分配",
|
||||||
|
"bulkPurchase": "批量订购套餐",
|
||||||
|
"bulkPurchaseDetail": "批量订购任务详情",
|
||||||
"orderPackageInvalidateTask": "订单套餐批量作废",
|
"orderPackageInvalidateTask": "订单套餐批量作废",
|
||||||
"orderPackageInvalidateTaskDetail": "订单套餐作废任务详情",
|
"orderPackageInvalidateTaskDetail": "订单套餐作废任务详情",
|
||||||
"taskDetail": "任务详情",
|
"taskDetail": "任务详情",
|
||||||
@@ -386,6 +389,11 @@
|
|||||||
"exportDevice": "导出设备",
|
"exportDevice": "导出设备",
|
||||||
"exportIotCard": "导出IOT卡",
|
"exportIotCard": "导出IOT卡",
|
||||||
"exportOrder": "导出订单",
|
"exportOrder": "导出订单",
|
||||||
|
"exportPackage": "导出套餐",
|
||||||
|
"exportAgentWalletTransaction": "导出代理主钱包流水",
|
||||||
|
"exportRefund": "导出退款",
|
||||||
|
"exportAgentRecharge": "导出代理充值",
|
||||||
|
"exportExchange": "导出换货",
|
||||||
"exportTaskDetail": "导出任务详情",
|
"exportTaskDetail": "导出任务详情",
|
||||||
"exchangeManagement": "换货管理",
|
"exchangeManagement": "换货管理",
|
||||||
"exchangeDetail": "换货单详情"
|
"exchangeDetail": "换货单详情"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { RoutesAlias } from '../routesAlias'
|
import { RoutesAlias } from '../routesAlias'
|
||||||
import { AppRouteRecord } from '@/types/router'
|
import { AppRouteRecord } from '@/types/router'
|
||||||
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
||||||
import { NOTIFICATION_PERMISSIONS } from '@/config/constants/notification'
|
import { JULY_PERMISSIONS } from '@/config/constants/julyIteration'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 菜单列表、异步路由
|
* 菜单列表、异步路由
|
||||||
@@ -14,31 +14,6 @@ import { NOTIFICATION_PERMISSIONS } from '@/config/constants/notification'
|
|||||||
* 可以是 i18n 的 key,也可以是字符串,比如:'用户列表'
|
* 可以是 i18n 的 key,也可以是字符串,比如:'用户列表'
|
||||||
*/
|
*/
|
||||||
export const asyncRoutes: AppRouteRecord[] = [
|
export const asyncRoutes: AppRouteRecord[] = [
|
||||||
// 站内通知
|
|
||||||
{
|
|
||||||
path: '/notifications',
|
|
||||||
name: 'Notifications',
|
|
||||||
component: RoutesAlias.Home,
|
|
||||||
meta: {
|
|
||||||
title: '通知中心',
|
|
||||||
isHide: true
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'NotificationCenter',
|
|
||||||
component: RoutesAlias.Notifications,
|
|
||||||
meta: {
|
|
||||||
title: '通知中心',
|
|
||||||
permissions: [NOTIFICATION_PERMISSIONS.page],
|
|
||||||
keepAlive: true,
|
|
||||||
isHide: true,
|
|
||||||
isHideTab: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// 仪表台
|
// 仪表台
|
||||||
{
|
{
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
@@ -466,6 +441,28 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: true
|
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',
|
path: 'order-package-invalidate-task',
|
||||||
@@ -500,6 +497,18 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: false
|
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',
|
path: 'export-task-management',
|
||||||
@@ -540,6 +549,56 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: true
|
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',
|
path: 'export-task-detail',
|
||||||
name: 'ExportTaskDetail',
|
name: 'ExportTaskDetail',
|
||||||
@@ -585,17 +644,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
isHide: true,
|
isHide: true,
|
||||||
keepAlive: false
|
keepAlive: false
|
||||||
}
|
}
|
||||||
},
|
|
||||||
// 批量订购
|
|
||||||
{
|
|
||||||
path: 'bulk-purchase',
|
|
||||||
name: 'BulkPurchase',
|
|
||||||
component: RoutesAlias.BulkPurchase,
|
|
||||||
meta: {
|
|
||||||
title: '批量订购套餐',
|
|
||||||
permissions: [BULK_PURCHASE_PERMISSIONS.page],
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -786,29 +834,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
roles: ['R_SUPER', 'R_ADMIN']
|
roles: ['R_SUPER', 'R_ADMIN']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 企业微信应用配置详情
|
|
||||||
{
|
|
||||||
path: 'wecom/applications/create',
|
|
||||||
name: 'WecomApplicationCreate',
|
|
||||||
component: RoutesAlias.WecomApplicationDetail,
|
|
||||||
meta: {
|
|
||||||
title: '新增企微应用',
|
|
||||||
isHide: true,
|
|
||||||
keepAlive: false,
|
|
||||||
roles: ['R_SUPER', 'R_ADMIN']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'wecom/applications/detail/:id',
|
|
||||||
name: 'WecomApplicationDetail',
|
|
||||||
component: RoutesAlias.WecomApplicationDetail,
|
|
||||||
meta: {
|
|
||||||
title: '企微应用配置',
|
|
||||||
isHide: true,
|
|
||||||
keepAlive: false,
|
|
||||||
roles: ['R_SUPER', 'R_ADMIN']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 企业微信成员及默认发起人
|
// 企业微信成员及默认发起人
|
||||||
{
|
{
|
||||||
path: 'wecom/members',
|
path: 'wecom/members',
|
||||||
|
|||||||
@@ -59,27 +59,31 @@ export enum RoutesAlias {
|
|||||||
// 任务管理
|
// 任务管理
|
||||||
IotCardTask = '/asset-management/task-management/iot-card-task', // IoT卡任务
|
IotCardTask = '/asset-management/task-management/iot-card-task', // IoT卡任务
|
||||||
DeviceTask = '/asset-management/task-management/device-task', // 设备任务
|
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', // 订单套餐批量作废任务
|
OrderPackageInvalidateTask = '/asset-management/task-management/order-package-invalidate-task', // 订单套餐批量作废任务
|
||||||
OrderPackageInvalidateTaskDetail = '/asset-management/task-management/order-package-invalidate-task/detail', // 订单套餐作废任务详情
|
OrderPackageInvalidateTaskDetail = '/asset-management/task-management/order-package-invalidate-task/detail', // 订单套餐作废任务详情
|
||||||
TaskDetail = '/asset-management/task-management/task-detail', // 任务详情(IoT卡/设备任务详情)
|
TaskDetail = '/asset-management/task-management/task-detail', // 任务详情(IoT卡/设备任务详情)
|
||||||
ExportDeviceTaskList = '/asset-management/export-task-management/export-device', // 导出设备
|
ExportDeviceTaskList = '/asset-management/export-task-management/export-device', // 导出设备
|
||||||
ExportIotCardTaskList = '/asset-management/export-task-management/export-iot-card', // 导出IOT卡
|
ExportIotCardTaskList = '/asset-management/export-task-management/export-iot-card', // 导出IOT卡
|
||||||
ExportOrderTaskList = '/asset-management/export-task-management/export-order', // 导出订单
|
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', // 导出任务详情
|
ExportTaskDetail = '/asset-management/export-task-management/export-task-detail', // 导出任务详情
|
||||||
|
|
||||||
// 订单管理
|
// 订单管理
|
||||||
OrderList = '/order-management/order-list', // 订单列表
|
OrderList = '/order-management/order-list', // 订单列表
|
||||||
OrderDetail = '/order-management/order-list/detail', // 订单详情
|
OrderDetail = '/order-management/order-list/detail', // 订单详情
|
||||||
BulkPurchase = '/order-management/bulk-purchase', // 批量订购
|
|
||||||
|
|
||||||
// 财务管理
|
// 财务管理
|
||||||
AgentRecharge = '/finance/agent-recharge', // 代理充值
|
AgentRecharge = '/finance/agent-recharge', // 代理充值
|
||||||
AgentRechargeDetail = '/finance/agent-recharge/detail', // 代理充值详情
|
AgentRechargeDetail = '/finance/agent-recharge/detail', // 代理充值详情
|
||||||
RefundManagement = '/finance/refund', // 退款管理
|
RefundManagement = '/finance/refund', // 退款管理
|
||||||
RefundDetail = '/finance/refund/detail', // 退款详情
|
RefundDetail = '/finance/refund/detail', // 退款详情
|
||||||
|
|
||||||
// 站内通知
|
|
||||||
Notifications = '/notifications', // 通知中心
|
|
||||||
ExpiringAssets = '/asset-management/expiring-assets', // 临期资产
|
ExpiringAssets = '/asset-management/expiring-assets', // 临期资产
|
||||||
|
|
||||||
// 佣金管理
|
// 佣金管理
|
||||||
@@ -95,7 +99,6 @@ export enum RoutesAlias {
|
|||||||
SystemConfigs = '/settings/system-configs', // 系统配置
|
SystemConfigs = '/settings/system-configs', // 系统配置
|
||||||
WecomSettings = '/settings/wecom', // 企业微信配置兼容入口
|
WecomSettings = '/settings/wecom', // 企业微信配置兼容入口
|
||||||
WecomApplications = '/settings/wecom/applications', // 企业微信应用
|
WecomApplications = '/settings/wecom/applications', // 企业微信应用
|
||||||
WecomApplicationDetail = '/settings/wecom/applications/detail', // 企业微信应用配置
|
|
||||||
WecomMembers = '/settings/wecom/members', // 企业微信成员
|
WecomMembers = '/settings/wecom/members', // 企业微信成员
|
||||||
WecomScenes = '/settings/wecom/scenes', // 企业微信审批场景
|
WecomScenes = '/settings/wecom/scenes', // 企业微信审批场景
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface AgentRecharge {
|
|||||||
remark?: string // 运营备注
|
remark?: string // 运营备注
|
||||||
submitter_name?: string | null // 提交人名称
|
submitter_name?: string | null // 提交人名称
|
||||||
submitter_id?: number | null
|
submitter_id?: number | null
|
||||||
|
approval_source?: 'none' | 'legacy' | 'wecom' | null // 审批来源
|
||||||
approval_provider?: 'wecom' | null
|
approval_provider?: 'wecom' | null
|
||||||
approval_instance_id?: number | null
|
approval_instance_id?: number | null
|
||||||
approval_status?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | null // 企微审批状态
|
approval_status?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | null // 企微审批状态
|
||||||
|
|||||||
@@ -72,23 +72,46 @@ export type AssetRealnamePolicy = 'none' | 'before_order' | 'after_order'
|
|||||||
export type ExpiryEstimateStatus = 'exact' | 'waiting_activation' | 'none' | 'invalid_data'
|
export type ExpiryEstimateStatus = 'exact' | 'waiting_activation' | 'none' | 'invalid_data'
|
||||||
|
|
||||||
// 临期等级由后端返回,前端仅用于展示和筛选结果标识
|
// 临期等级由后端返回,前端仅用于展示和筛选结果标识
|
||||||
export type ExpiryLevel = string
|
export type ExpiryLevel = 'red' | 'purple' | 'pink' | string
|
||||||
|
|
||||||
|
export type ExpiringAssetType = 'iot_card' | 'device'
|
||||||
|
|
||||||
export interface ExpiringAssetItem {
|
export interface ExpiringAssetItem {
|
||||||
asset_type: AssetType
|
|
||||||
asset_id: number
|
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
|
identifier: string
|
||||||
shop_name?: string | null
|
is_expiring: boolean
|
||||||
package_name?: string | null
|
is_priority: boolean
|
||||||
estimated_final_expires_at: string
|
package_id?: number
|
||||||
days_until_final_expiry: number
|
package_name: string
|
||||||
expiry_level?: ExpiryLevel | null
|
package_usage_id?: number
|
||||||
expiry_level_name?: string | null
|
shop_id?: number | null
|
||||||
can_renew: boolean
|
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 {
|
export interface ExpiringAssetQueryParams {
|
||||||
asset_type?: AssetType
|
asset_type?: ExpiringAssetType
|
||||||
keyword?: string
|
keyword?: string
|
||||||
shop_id?: number
|
shop_id?: number
|
||||||
package_id?: number
|
package_id?: number
|
||||||
@@ -97,7 +120,7 @@ export interface ExpiringAssetQueryParams {
|
|||||||
expires_from?: string
|
expires_from?: string
|
||||||
expires_to?: string
|
expires_to?: string
|
||||||
page?: number
|
page?: number
|
||||||
size?: number
|
page_size?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量更新资产实名认证策略请求
|
// 批量更新资产实名认证策略请求
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export enum BulkPurchaseTaskStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BulkPurchaseCreateResponse {
|
export interface BulkPurchaseCreateResponse {
|
||||||
task_id: number
|
id?: number
|
||||||
|
task_id?: number
|
||||||
task_no?: string
|
task_no?: string
|
||||||
status?: BulkPurchaseTaskStatus
|
status?: BulkPurchaseTaskStatus
|
||||||
status_name?: string
|
status_name?: string
|
||||||
|
|||||||
@@ -215,9 +215,11 @@ export enum DeviceImportTaskStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导入任务查询参数
|
// 导入任务查询参数
|
||||||
|
export type DeviceImportTaskOperationType = 'import' | 'assign_shop' | 'assign_series' | 'recall'
|
||||||
|
|
||||||
export interface DeviceImportTaskQueryParams extends PaginationParams {
|
export interface DeviceImportTaskQueryParams extends PaginationParams {
|
||||||
status?: DeviceImportTaskStatus // 任务状态
|
status?: DeviceImportTaskStatus // 任务状态
|
||||||
operation_type?: 'import' | 'assign_shop' | 'assign_series'
|
operation_type?: DeviceImportTaskOperationType
|
||||||
batch_no?: string // 批次号(模糊查询)
|
batch_no?: string // 批次号(模糊查询)
|
||||||
start_time?: string // 创建时间起始
|
start_time?: string // 创建时间起始
|
||||||
end_time?: string // 创建时间结束
|
end_time?: string // 创建时间结束
|
||||||
@@ -232,13 +234,14 @@ export interface DeviceImportTask {
|
|||||||
file_key?: string // 原始导入文件存储键
|
file_key?: string // 原始导入文件存储键
|
||||||
creator_name?: string // 创建人姓名
|
creator_name?: string // 创建人姓名
|
||||||
status: DeviceImportTaskStatus // 任务状态
|
status: DeviceImportTaskStatus // 任务状态
|
||||||
|
status_name?: string // 任务状态名称
|
||||||
status_text: string // 任务状态文本
|
status_text: string // 任务状态文本
|
||||||
total_count: number // 总数
|
total_count: number // 总数
|
||||||
success_count: number // 成功数
|
success_count: number // 成功数
|
||||||
fail_count: number // 失败数
|
fail_count: number // 失败数
|
||||||
skip_count: number // 跳过数
|
skip_count: number // 跳过数
|
||||||
error_message: string // 错误信息
|
error_message: string // 错误信息
|
||||||
operation_type: 'import' | 'assign_shop' | 'assign_series'
|
operation_type: DeviceImportTaskOperationType
|
||||||
operation_name: string
|
operation_name: string
|
||||||
target_id: number | null
|
target_id: number | null
|
||||||
realname_policy: 'none' | 'before_order' | 'after_order'
|
realname_policy: 'none' | 'before_order' | 'after_order'
|
||||||
@@ -273,8 +276,8 @@ export interface DeviceImportTaskDetail extends DeviceImportTask {
|
|||||||
|
|
||||||
export interface DeviceBatchAllocationRequest {
|
export interface DeviceBatchAllocationRequest {
|
||||||
file_key: string
|
file_key: string
|
||||||
operation_type: 'assign_shop' | 'assign_series'
|
operation_type: Exclude<DeviceImportTaskOperationType, 'import'>
|
||||||
target_id: number
|
target_id?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceBatchAllocationResponse {
|
export interface DeviceBatchAllocationResponse {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export interface ExportTaskListResponse {
|
|||||||
export interface CreateExportTaskRequest {
|
export interface CreateExportTaskRequest {
|
||||||
scene: ExportTaskScene
|
scene: ExportTaskScene
|
||||||
format: ExportTaskFormat
|
format: ExportTaskFormat
|
||||||
query?: Record<string, unknown>
|
query?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateExportTaskResponse {
|
export interface CreateExportTaskResponse {
|
||||||
@@ -85,15 +85,6 @@ export interface ExportTaskDetail extends ExportTaskItem {
|
|||||||
download_expires_at?: string
|
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 ExportTaskListApiResponse = BaseResponse<ExportTaskListResponse>
|
||||||
export type CreateExportTaskApiResponse = BaseResponse<CreateExportTaskResponse>
|
export type CreateExportTaskApiResponse = BaseResponse<CreateExportTaskResponse>
|
||||||
export type ExportTaskDetailApiResponse = BaseResponse<ExportTaskDetail>
|
export type ExportTaskDetailApiResponse = BaseResponse<ExportTaskDetail>
|
||||||
export type CancelExportTaskApiResponse = BaseResponse<CancelExportTaskResponse>
|
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export interface NotificationReadResponse {
|
|||||||
success: boolean
|
success: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NotificationReadRequest {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotificationReadAllRequest {
|
export interface NotificationReadAllRequest {
|
||||||
category?: string
|
category?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,8 +281,8 @@ export interface CommissionTier {
|
|||||||
* 套餐信息(用于系列授权)
|
* 套餐信息(用于系列授权)
|
||||||
*/
|
*/
|
||||||
export interface GrantPackageInfo extends PackageAllocationExpiryBaseFields {
|
export interface GrantPackageInfo extends PackageAllocationExpiryBaseFields {
|
||||||
allocation_id?: number
|
|
||||||
package_id: number
|
package_id: number
|
||||||
|
allocation_id: number // 套餐分配记录 ID,用于修改生效条件
|
||||||
package_name?: string
|
package_name?: string
|
||||||
package_code?: string
|
package_code?: string
|
||||||
cost_price: number // 成本价(分)
|
cost_price: number // 成本价(分)
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export interface CreateRefundRequest {
|
|||||||
requested_refund_amount: number // 申请退款金额(分)
|
requested_refund_amount: number // 申请退款金额(分)
|
||||||
actual_received_amount: number // 实收金额(分)
|
actual_received_amount: number // 实收金额(分)
|
||||||
attachments: RefundAttachment[]
|
attachments: RefundAttachment[]
|
||||||
|
package_usage_id?: number // 关联套餐使用记录 ID
|
||||||
refund_reason?: string
|
refund_reason?: string
|
||||||
remark?: string
|
remark?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,37 @@ export interface WecomSceneRequest {
|
|||||||
status?: 0 | 1
|
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 {
|
export interface WecomAccountBindingRequest {
|
||||||
application_id: number
|
application_id: number
|
||||||
userid: string
|
userid: string
|
||||||
@@ -85,11 +116,13 @@ export type WecomSceneListResponse = BaseResponse<PaginationData<WecomScene>>
|
|||||||
export type WecomApplicationResponse = BaseResponse<WecomApplication>
|
export type WecomApplicationResponse = BaseResponse<WecomApplication>
|
||||||
export type WecomSceneResponse = BaseResponse<WecomScene>
|
export type WecomSceneResponse = BaseResponse<WecomScene>
|
||||||
export type WecomSyncMembersApiResponse = BaseResponse<WecomSyncMembersResponse>
|
export type WecomSyncMembersApiResponse = BaseResponse<WecomSyncMembersResponse>
|
||||||
|
export type WecomTemplateDetailResponse = BaseResponse<WecomTemplateDetail>
|
||||||
|
export type WecomBusinessFieldListResponse = BaseResponse<WecomBusinessFieldList>
|
||||||
|
|
||||||
export interface WecomMemberQueryParams extends PaginationParams {
|
export interface WecomMemberQueryParams extends PaginationParams {
|
||||||
keyword?: string
|
keyword?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WecomApplicationQueryParams extends PaginationParams {}
|
export type WecomApplicationQueryParams = PaginationParams
|
||||||
|
|
||||||
export interface WecomSceneQueryParams extends PaginationParams {}
|
export type WecomSceneQueryParams = PaginationParams
|
||||||
|
|||||||
@@ -8,13 +8,52 @@ type TargetRoute = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolveTargetRoute = (target: NotificationTarget): TargetRoute | null => {
|
const resolveTargetRoute = (target: NotificationTarget): TargetRoute | null => {
|
||||||
const targetValues = [target.target_key, target.target_type]
|
const targetId = target.target_id === null ? undefined : String(target.target_id)
|
||||||
|
const targetKey = target.target_key || ''
|
||||||
|
|
||||||
if (targetValues.includes('expiring_assets') || targetValues.includes('expiring_asset_list')) {
|
if (target.target_type === 'refund_detail' && targetId) {
|
||||||
|
return { path: `${RoutesAlias.RefundDetail}/${targetId}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.target_type === 'agent_recharge_detail' && targetId) {
|
||||||
|
return { path: `${RoutesAlias.AgentRechargeDetail}/${targetId}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.target_type === 'iot_card_detail' && (targetKey || targetId)) {
|
||||||
|
return {
|
||||||
|
path: RoutesAlias.AssetInformation,
|
||||||
|
query: { iccid: targetKey || targetId || '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.target_type === 'device_detail' && (targetKey || targetId)) {
|
||||||
|
return {
|
||||||
|
path: RoutesAlias.AssetInformation,
|
||||||
|
query: { virtual_no: targetKey || targetId || '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
target.target_type === 'expiring_asset_list' ||
|
||||||
|
target.target_key === 'expiring_assets' ||
|
||||||
|
target.target_key === 'expiring_asset_list'
|
||||||
|
) {
|
||||||
return { path: RoutesAlias.ExpiringAssets }
|
return { path: RoutesAlias.ExpiringAssets }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetValues.includes('notification_center')) return { path: RoutesAlias.Notifications }
|
if (target.target_type === 'shop_fund_summary') {
|
||||||
|
return {
|
||||||
|
path: RoutesAlias.AgentFundOverview,
|
||||||
|
query: targetId ? { shop_id: targetId } : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.target_type === 'system_config') {
|
||||||
|
return {
|
||||||
|
path: RoutesAlias.SystemConfigs,
|
||||||
|
query: targetKey ? { config_key: targetKey } : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,75 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</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
|
||||||
|
v-permission="JULY_PERMISSIONS.wecom.binding"
|
||||||
|
type="primary"
|
||||||
|
:loading="wecomBindingSubmitting"
|
||||||
|
@click="submitWecomBinding"
|
||||||
|
>
|
||||||
|
确认绑定
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
@@ -237,11 +306,16 @@
|
|||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { AccountService } from '@/api/modules/account'
|
import { AccountService } from '@/api/modules/account'
|
||||||
import { RoleService } from '@/api/modules/role'
|
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 { SearchFormItem } from '@/types'
|
||||||
import type { PlatformRole } from '@/types/api'
|
import type { PlatformRole, WecomApplication, WecomMember } from '@/types/api'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
import {
|
||||||
|
CommonStatus,
|
||||||
|
getStatusText,
|
||||||
|
JULY_PERMISSIONS,
|
||||||
|
STATUS_SELECT_OPTIONS
|
||||||
|
} from '@/config/constants'
|
||||||
|
|
||||||
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||||
|
|
||||||
@@ -252,6 +326,7 @@
|
|||||||
const dialogType = ref('add')
|
const dialogType = ref('add')
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const roleDialogVisible = ref(false)
|
const roleDialogVisible = ref(false)
|
||||||
|
const wecomBindingDialogVisible = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const currentAccountId = ref<number>(0)
|
const currentAccountId = ref<number>(0)
|
||||||
const currentAccountName = ref<string>('')
|
const currentAccountName = ref<string>('')
|
||||||
@@ -262,6 +337,21 @@
|
|||||||
const roleToAdd = ref<number | undefined>(undefined) // 单选时使用
|
const roleToAdd = ref<number | undefined>(undefined) // 单选时使用
|
||||||
const leftRoleFilter = ref('')
|
const leftRoleFilter = ref('')
|
||||||
const rightRoleFilter = 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 isPlatformUser = computed(() => currentAccountType.value === 2)
|
||||||
@@ -541,6 +631,14 @@
|
|||||||
const getActions = (row: any) => {
|
const getActions = (row: any) => {
|
||||||
const actions: any[] = []
|
const actions: any[] = []
|
||||||
|
|
||||||
|
if (hasAuth(JULY_PERMISSIONS.wecom.binding)) {
|
||||||
|
actions.push({
|
||||||
|
label: '绑定账号',
|
||||||
|
handler: () => showWecomBindingDialog(row),
|
||||||
|
type: 'primary'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (hasAuth('account:patch_role')) {
|
if (hasAuth('account:patch_role')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: '分配角色',
|
label: '分配角色',
|
||||||
@@ -568,6 +666,75 @@
|
|||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showWecomBindingDialog = (row: any) => {
|
||||||
|
currentWecomAccount.value = row
|
||||||
|
wecomBindingForm.application_id = undefined
|
||||||
|
wecomBindingForm.userid = ''
|
||||||
|
wecomMembers.value = []
|
||||||
|
wecomBindingDialogVisible.value = true
|
||||||
|
void loadWecomApplications()
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
wecomBindingForm.userid = ''
|
||||||
|
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 || []
|
||||||
|
} finally {
|
||||||
|
wecomMembersLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitWecomBinding = async () => {
|
||||||
|
const valid = await wecomBindingFormRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid || !currentWecomAccount.value || !wecomBindingForm.application_id) return
|
||||||
|
|
||||||
|
const accountId = Number(currentWecomAccount.value.id ?? currentWecomAccount.value.ID)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
wecomBindingSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetWecomBindingForm = () => {
|
||||||
|
currentWecomAccount.value = null
|
||||||
|
wecomBindingForm.application_id = undefined
|
||||||
|
wecomBindingForm.userid = ''
|
||||||
|
wecomMembers.value = []
|
||||||
|
wecomBindingFormRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
// 表单实例
|
// 表单实例
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-header-right">
|
<div class="card-header-right">
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="cardInfo?.asset_type === 'card' && hasAuth(JULY_PERMISSIONS.speedTier.view)"
|
v-if="
|
||||||
|
cardInfo?.asset_type === 'card' &&
|
||||||
|
hasAuth(JULY_PERMISSIONS.speedTier.view) &&
|
||||||
|
hasAuth(JULY_PERMISSIONS.speedTier.update)
|
||||||
|
"
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
@click="emit('showSpeedLimit')"
|
@click="emit('showSpeedLimit')"
|
||||||
|
|||||||
@@ -270,6 +270,7 @@
|
|||||||
v-model="createRefundDialogVisible"
|
v-model="createRefundDialogVisible"
|
||||||
:initial-order-id="currentRefundOrderId"
|
:initial-order-id="currentRefundOrderId"
|
||||||
:initial-order-no="currentRefundOrderNo"
|
:initial-order-no="currentRefundOrderNo"
|
||||||
|
:initial-package-usage-id="currentRefundPackageUsageId"
|
||||||
@success="handleRefundSuccess"
|
@success="handleRefundSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -279,12 +280,7 @@
|
|||||||
width="30%"
|
width="30%"
|
||||||
@closed="resetUsedDataForm"
|
@closed="resetUsedDataForm"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm ref="usedDataFormRef" :model="usedDataForm" :rules="usedDataRules" label-width="90px">
|
||||||
ref="usedDataFormRef"
|
|
||||||
:model="usedDataForm"
|
|
||||||
:rules="usedDataRules"
|
|
||||||
label-width="90px"
|
|
||||||
>
|
|
||||||
<ElFormItem label="套餐名称">
|
<ElFormItem label="套餐名称">
|
||||||
<span>{{ selectedPackage?.package_name || '-' }}</span>
|
<span>{{ selectedPackage?.package_name || '-' }}</span>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -416,6 +412,7 @@
|
|||||||
const createRefundDialogVisible = ref(false)
|
const createRefundDialogVisible = ref(false)
|
||||||
const currentRefundOrderId = ref<number | undefined>(undefined)
|
const currentRefundOrderId = ref<number | undefined>(undefined)
|
||||||
const currentRefundOrderNo = ref<string | undefined>(undefined)
|
const currentRefundOrderNo = ref<string | undefined>(undefined)
|
||||||
|
const currentRefundPackageUsageId = ref<number | undefined>(undefined)
|
||||||
const selectedPackage = ref<PackageInfo>()
|
const selectedPackage = ref<PackageInfo>()
|
||||||
const usedDataDialogVisible = ref(false)
|
const usedDataDialogVisible = ref(false)
|
||||||
const expiresAtDialogVisible = ref(false)
|
const expiresAtDialogVisible = ref(false)
|
||||||
@@ -474,10 +471,12 @@
|
|||||||
}
|
}
|
||||||
currentRefundOrderId.value = row.order_id
|
currentRefundOrderId.value = row.order_id
|
||||||
currentRefundOrderNo.value = row.order_no
|
currentRefundOrderNo.value = row.order_no
|
||||||
|
currentRefundPackageUsageId.value = row.package_usage_id ?? row.id
|
||||||
createRefundDialogVisible.value = true
|
createRefundDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefundSuccess = () => {
|
const handleRefundSuccess = () => {
|
||||||
|
currentRefundPackageUsageId.value = undefined
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<ElDialog v-model="visible" title="设置限速" width="500px">
|
<ElDialog
|
||||||
|
v-model="visible"
|
||||||
|
title="设置限速"
|
||||||
|
width="500px"
|
||||||
|
:lock-scroll="true"
|
||||||
|
:z-index="4000"
|
||||||
|
modal-class="speed-limit-modal"
|
||||||
|
>
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
<ElFormItem label="固定档位" prop="code">
|
<ElFormItem label="固定档位" prop="code">
|
||||||
<ElSelect v-model="form.code" style="width: 100%">
|
<ElSelect
|
||||||
<ElOption v-for="tier in speedTiers" :key="tier.code" :label="tier.label" :value="tier.code" />
|
v-model="form.code"
|
||||||
|
style="width: 100%"
|
||||||
|
:teleported="true"
|
||||||
|
append-to="body"
|
||||||
|
popper-class="speed-limit-select-popper"
|
||||||
|
:popper-style="{ zIndex: 4001 }"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="tier in speedTiers"
|
||||||
|
:key="tier.code"
|
||||||
|
:label="tier.label"
|
||||||
|
:value="tier.code"
|
||||||
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<ElButton @click="handleCancel">取消</ElButton>
|
<ElButton @click="handleCancel">取消</ElButton>
|
||||||
<ElButton type="primary" @click="handleConfirm" :loading="loading"> 确认设置 </ElButton>
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
@click="handleConfirm"
|
||||||
|
:loading="confirmLoading"
|
||||||
|
:disabled="confirmLoading"
|
||||||
|
>
|
||||||
|
确认设置
|
||||||
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, watch } from 'vue'
|
import { ref, reactive, computed, onUnmounted, watch } from 'vue'
|
||||||
import { ElDialog, ElForm, ElFormItem, ElSelect, ElOption, ElButton } from 'element-plus'
|
import { ElDialog, ElForm, ElFormItem, ElSelect, ElOption, ElButton } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
|
confirmLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:modelValue', value: boolean): void
|
(e: 'update:modelValue', value: boolean): void
|
||||||
(e: 'confirm', data: { code: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 }): void
|
(e: 'confirm', data: { code: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 }): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
confirmLoading: false
|
||||||
|
})
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const speedTiers = [
|
const speedTiers = [
|
||||||
{ code: -1, label: '不限速' },
|
{ code: -1, label: '不限速' },
|
||||||
{ code: 0, label: '0kbps' },
|
{ code: 0, label: '0kbps' },
|
||||||
@@ -59,10 +86,25 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 监听对话框打开,重置表单
|
// 监听对话框打开,重置表单
|
||||||
watch(visible, (newVal) => {
|
const setLayoutScrollLock = (locked: boolean) => {
|
||||||
if (newVal) {
|
document
|
||||||
form.code = 3
|
.querySelector<HTMLElement>('.layouts')
|
||||||
}
|
?.classList.toggle('speed-limit-scroll-locked', locked)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
visible,
|
||||||
|
(newVal) => {
|
||||||
|
setLayoutScrollLock(newVal)
|
||||||
|
if (newVal) {
|
||||||
|
form.code = 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
setLayoutScrollLock(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -70,6 +112,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
|
if (props.confirmLoading) return
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -83,4 +126,28 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss">
|
||||||
|
.speed-limit-modal .el-dialog__body {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-limit-select-popper {
|
||||||
|
z-index: 4001 !important;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-limit-select-popper .el-select-dropdown__wrap,
|
||||||
|
.speed-limit-select-popper .el-scrollbar__wrap {
|
||||||
|
max-height: none !important;
|
||||||
|
overflow-y: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-limit-modal {
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layouts.speed-limit-scroll-locked {
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
@show-switch-card="showSwitchCardDialog"
|
@show-switch-card="showSwitchCardDialog"
|
||||||
@show-switch-mode="showSwitchModeDialog"
|
@show-switch-mode="showSwitchModeDialog"
|
||||||
@show-set-wifi="showSetWiFiDialog"
|
@show-set-wifi="showSetWiFiDialog"
|
||||||
@show-realname-policy="showRealnamePolicyDialog"
|
@show-realname-policy="showRealnamePolicyDialog"
|
||||||
@show-speed-limit="speedLimitDialogVisible = true"
|
@show-speed-limit="speedLimitDialogVisible = true"
|
||||||
@show-update-realname-status="showUpdateRealnameStatusDialog"
|
@show-update-realname-status="showUpdateRealnameStatusDialog"
|
||||||
@enable-binding-card="handleEnableBindingCard"
|
@enable-binding-card="handleEnableBindingCard"
|
||||||
@disable-binding-card="handleDisableBindingCard"
|
@disable-binding-card="handleDisableBindingCard"
|
||||||
@@ -174,7 +174,11 @@
|
|||||||
:current-realname-status="bindingCardRealnameStatusValue"
|
:current-realname-status="bindingCardRealnameStatusValue"
|
||||||
@success="handleBindingCardRealnameStatusSuccess"
|
@success="handleBindingCardRealnameStatusSuccess"
|
||||||
/>
|
/>
|
||||||
<SpeedLimitDialog v-model="speedLimitDialogVisible" @confirm="handleSpeedTierConfirm" />
|
<SpeedLimitDialog
|
||||||
|
v-model="speedLimitDialogVisible"
|
||||||
|
:confirm-loading="speedTierSubmitting"
|
||||||
|
@confirm="handleSpeedTierConfirm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -183,7 +187,10 @@
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElCard, ElEmpty } from 'element-plus'
|
import { ElMessage, ElCard, ElEmpty } from 'element-plus'
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
import { CardService, DeviceService } from '@/api/modules'
|
import { CardService, DeviceService } from '@/api/modules'
|
||||||
|
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
import {
|
import {
|
||||||
formatRemainingTime,
|
formatRemainingTime,
|
||||||
FrontendRateLimitError,
|
FrontendRateLimitError,
|
||||||
@@ -204,9 +211,9 @@
|
|||||||
SwitchModeDialog,
|
SwitchModeDialog,
|
||||||
WiFiConfigDialog,
|
WiFiConfigDialog,
|
||||||
PackageRechargeDialog,
|
PackageRechargeDialog,
|
||||||
DailyRecordsDialog,
|
DailyRecordsDialog,
|
||||||
OrderHistoryDialog,
|
OrderHistoryDialog,
|
||||||
SpeedLimitDialog
|
SpeedLimitDialog
|
||||||
} from './components/dialogs'
|
} from './components/dialogs'
|
||||||
import SwitchCardDialog from '@/components/device/SwitchCardDialog.vue'
|
import SwitchCardDialog from '@/components/device/SwitchCardDialog.vue'
|
||||||
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
|
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
|
||||||
@@ -235,6 +242,7 @@
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
|
||||||
// ========== 组件引用 ==========
|
// ========== 组件引用 ==========
|
||||||
const assetSearchCardRef = ref<InstanceType<typeof AssetSearchCard>>()
|
const assetSearchCardRef = ref<InstanceType<typeof AssetSearchCard>>()
|
||||||
@@ -294,6 +302,7 @@
|
|||||||
// 绑定卡实名状态更新
|
// 绑定卡实名状态更新
|
||||||
const bindingCardRealnameStatusDialogVisible = ref(false)
|
const bindingCardRealnameStatusDialogVisible = ref(false)
|
||||||
const speedLimitDialogVisible = ref(false)
|
const speedLimitDialogVisible = ref(false)
|
||||||
|
const speedTierSubmitting = ref(false)
|
||||||
const bindingCardRealnameStatusIccid = ref('')
|
const bindingCardRealnameStatusIccid = ref('')
|
||||||
const bindingCardRealnameStatusValue = ref<number>(0)
|
const bindingCardRealnameStatusValue = ref<number>(0)
|
||||||
|
|
||||||
@@ -806,12 +815,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleSpeedTierConfirm = async (data: { code: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 }) => {
|
const handleSpeedTierConfirm = async (data: { code: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 }) => {
|
||||||
if (!cardInfo.value?.iccid) return
|
if (!hasAuth(JULY_PERMISSIONS.speedTier.update)) {
|
||||||
const response = await CardService.setSpeedTier(cardInfo.value.iccid, data.code)
|
ElMessage.error('暂无设置限速权限')
|
||||||
if (response.code === 0) {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const iccid = cardInfo.value?.iccid
|
||||||
|
if (!iccid) {
|
||||||
|
ElMessage.error('当前卡缺少 ICCID,无法设置限速')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (speedTierSubmitting.value) return
|
||||||
|
|
||||||
|
speedTierSubmitting.value = true
|
||||||
|
try {
|
||||||
|
const response = await CardService.setSpeedTier(iccid, data.code)
|
||||||
|
if (response.code !== 0) {
|
||||||
|
ElMessage.error(response.msg || '限速设置失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ElMessage.success(`限速设置成功:${response.data.speed_tier_name}`)
|
ElMessage.success(`限速设置成功:${response.data.speed_tier_name}`)
|
||||||
speedLimitDialogVisible.value = false
|
speedLimitDialogVisible.value = false
|
||||||
await handleRefresh()
|
await handleRefresh()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置限速失败:', error)
|
||||||
|
ElMessage.error(normalizeApiError(error).message)
|
||||||
|
} finally {
|
||||||
|
speedTierSubmitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -17,29 +17,6 @@
|
|||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="handleBatchAllocate"
|
|
||||||
:disabled="!selectedDevices.length"
|
|
||||||
v-permission="'device:batch_allocate'"
|
|
||||||
>
|
|
||||||
批量分配
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
@click="handleBatchRecall"
|
|
||||||
:disabled="!selectedDevices.length"
|
|
||||||
v-permission="'device:batch_recall'"
|
|
||||||
>
|
|
||||||
批量回收
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="info"
|
|
||||||
:disabled="!selectedDevices.length"
|
|
||||||
@click="handleBatchSetSeries"
|
|
||||||
v-permission="'device:batch_set_series'"
|
|
||||||
>
|
|
||||||
批量设置套餐系列
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
<ElButton
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="!selectedDevices.length"
|
:disabled="!selectedDevices.length"
|
||||||
@@ -2299,8 +2276,8 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const successCount = res.data?.success_count ?? assetIds.length
|
const successCount = res.data?.success_count ?? assetIds.length
|
||||||
ElMessage.success(`批量修改实名顺序成功:${successCount}/${assetIds.length}`)
|
ElMessage.success(`批量修改实名顺序成功:${successCount}/${assetIds.length}`)
|
||||||
batchRealnamePolicyDialogVisible.value = false
|
batchRealnamePolicyDialogVisible.value = false
|
||||||
selectedDevices.value = []
|
selectedDevices.value = []
|
||||||
await getTableData()
|
await getTableData()
|
||||||
@@ -2373,19 +2350,6 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量分配
|
|
||||||
const handleBatchAllocate = async () => {
|
|
||||||
if (selectedDevices.value.length === 0) {
|
|
||||||
ElMessage.warning('请先选择要分配的设备')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
allocateForm.target_shop_id = undefined
|
|
||||||
allocateForm.remark = ''
|
|
||||||
allocateResult.value = null
|
|
||||||
shopCascadeOptions.value = []
|
|
||||||
allocateDialogVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认批量分配
|
// 确认批量分配
|
||||||
const handleConfirmAllocate = async () => {
|
const handleConfirmAllocate = async () => {
|
||||||
if (!allocateFormRef.value) return
|
if (!allocateFormRef.value) return
|
||||||
@@ -2436,17 +2400,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量回收
|
|
||||||
const handleBatchRecall = () => {
|
|
||||||
if (selectedDevices.value.length === 0) {
|
|
||||||
ElMessage.warning('请先选择要回收的设备')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
recallForm.remark = ''
|
|
||||||
recallResult.value = null
|
|
||||||
recallDialogVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认批量回收
|
// 确认批量回收
|
||||||
const handleConfirmRecall = async () => {
|
const handleConfirmRecall = async () => {
|
||||||
recallLoading.value = true
|
recallLoading.value = true
|
||||||
@@ -2636,22 +2589,6 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量设置套餐系列
|
|
||||||
const handleBatchSetSeries = async () => {
|
|
||||||
seriesBindingResult.value = null
|
|
||||||
Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
|
|
||||||
selection_type:
|
|
||||||
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER
|
|
||||||
})
|
|
||||||
await Promise.all([
|
|
||||||
loadPackageSeriesList(),
|
|
||||||
loadSearchBatchNoOptions(),
|
|
||||||
loadSeriesBindingShopCascadeOptions()
|
|
||||||
])
|
|
||||||
seriesBindingDialogVisible.value = true
|
|
||||||
seriesBindingFormRef.value?.clearValidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载套餐系列列表(支持名称搜索,默认20条)
|
// 加载套餐系列列表(支持名称搜索,默认20条)
|
||||||
const loadPackageSeriesList = async (seriesName?: string) => {
|
const loadPackageSeriesList = async (seriesName?: string) => {
|
||||||
seriesLoading.value = true
|
seriesLoading.value = true
|
||||||
@@ -2680,24 +2617,6 @@
|
|||||||
await loadPackageSeriesList(query || undefined)
|
await loadPackageSeriesList(query || undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadSeriesBindingShopCascadeOptions = async () => {
|
|
||||||
try {
|
|
||||||
const res = await ShopService.getShopsCascade({
|
|
||||||
parent_id: undefined,
|
|
||||||
exclude_self: true
|
|
||||||
})
|
|
||||||
if (res.code === 0) {
|
|
||||||
seriesBindingShopCascadeOptions.value = (res.data || []).map((item: any) => ({
|
|
||||||
value: item.id,
|
|
||||||
label: item.shop_name,
|
|
||||||
leaf: !item.has_children
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载批量设置套餐系列根级店铺失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildSeriesBindingRequest = (): BatchSetDeviceSeriesBindingRequest | null => {
|
const buildSeriesBindingRequest = (): BatchSetDeviceSeriesBindingRequest | null => {
|
||||||
const request: BatchSetDeviceSeriesBindingRequest = {
|
const request: BatchSetDeviceSeriesBindingRequest = {
|
||||||
selection_type: seriesBindingForm.selection_type,
|
selection_type: seriesBindingForm.selection_type,
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
<ElButton type="primary" @click="showCreateDialog" v-permission="'exchange:create'">
|
<ElButton type="primary" @click="showCreateDialog" v-permission="'exchange:create'">
|
||||||
创建换货单
|
创建换货单
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
<ElButton v-if="hasAuth('exchange:export')" @click="exportDialogVisible = true">
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
@@ -41,6 +44,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</ArtTable>
|
</ArtTable>
|
||||||
|
|
||||||
|
<ExportTaskCreateDialog
|
||||||
|
v-model="exportDialogVisible"
|
||||||
|
scene="exchange"
|
||||||
|
:query="exportQuery"
|
||||||
|
confirm-permission="exchange:export"
|
||||||
|
title="导出换货"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 创建换货单对话框 -->
|
<!-- 创建换货单对话框 -->
|
||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="createDialogVisible"
|
v-model="createDialogVisible"
|
||||||
@@ -393,6 +404,7 @@
|
|||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import {
|
import {
|
||||||
getApprovalStatusText,
|
getApprovalStatusText,
|
||||||
@@ -413,6 +425,7 @@
|
|||||||
const shipFormRef = ref<FormInstance>()
|
const shipFormRef = ref<FormInstance>()
|
||||||
const createDialogVisible = ref(false)
|
const createDialogVisible = ref(false)
|
||||||
const createLoading = ref(false)
|
const createLoading = ref(false)
|
||||||
|
const exportDialogVisible = ref(false)
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
const createFormRef = ref<FormInstance>()
|
const createFormRef = ref<FormInstance>()
|
||||||
|
|
||||||
@@ -438,6 +451,18 @@
|
|||||||
created_at_end: ''
|
created_at_end: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const exportQuery = computed(() => {
|
||||||
|
const [startDate, endDate] = searchForm.created_at_range || []
|
||||||
|
return {
|
||||||
|
status: searchForm.status,
|
||||||
|
flow_type: searchForm.flow_type,
|
||||||
|
old_asset_keyword: searchForm.old_asset_keyword || undefined,
|
||||||
|
new_asset_keyword: searchForm.new_asset_keyword || undefined,
|
||||||
|
created_at_start: startDate || searchForm.created_at_start || undefined,
|
||||||
|
created_at_end: endDate || searchForm.created_at_end || undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 创建换货单表单
|
// 创建换货单表单
|
||||||
const createForm = reactive<{
|
const createForm = reactive<{
|
||||||
exchange_reason: string
|
exchange_reason: string
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
v-model:filter="searchForm"
|
v-model:filter="searchForm"
|
||||||
:items="searchFormItems"
|
:items="searchFormItems"
|
||||||
:show-expand="true"
|
:show-expand="true"
|
||||||
label-width="85"
|
label-width="100px"
|
||||||
@reset="handleReset"
|
@reset="handleReset"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
/>
|
/>
|
||||||
@@ -17,7 +17,10 @@
|
|||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #left>
|
<template #left>
|
||||||
<div class="page-intro"> 仅展示可预计且尚未过期的资产,0-3 天资产优先显示。 </div>
|
<div class="page-intro">
|
||||||
|
临期资产 {{ summary.total_count }} 条(卡 {{ summary.card_count }},设备
|
||||||
|
{{ summary.device_count }}),0-3 天资产优先显示。
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
@@ -47,11 +50,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, onMounted, reactive, ref, computed } from 'vue'
|
import { h, onMounted, reactive, ref, computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElTag } from 'element-plus'
|
import { ElMessage, ElTag } from 'element-plus'
|
||||||
import { AssetService, PackageManageService, ShopService } from '@/api/modules'
|
import { AssetService, PackageManageService, ShopService } from '@/api/modules'
|
||||||
import type {
|
import type {
|
||||||
AssetType,
|
|
||||||
ExpiringAssetItem,
|
ExpiringAssetItem,
|
||||||
|
ExpiringAssetSummary,
|
||||||
|
ExpiringAssetType,
|
||||||
ExpiringAssetQueryParams,
|
ExpiringAssetQueryParams,
|
||||||
PackageResponse,
|
PackageResponse,
|
||||||
ShopResponse
|
ShopResponse
|
||||||
@@ -72,6 +76,12 @@
|
|||||||
const shopOptions = ref<ShopResponse[]>([])
|
const shopOptions = ref<ShopResponse[]>([])
|
||||||
const packageOptions = ref<PackageResponse[]>([])
|
const packageOptions = ref<PackageResponse[]>([])
|
||||||
const items = ref<ExpiringAssetItem[]>([])
|
const items = ref<ExpiringAssetItem[]>([])
|
||||||
|
const summary = reactive<ExpiringAssetSummary>({
|
||||||
|
card_count: 0,
|
||||||
|
device_count: 0,
|
||||||
|
total_count: 0,
|
||||||
|
window_days: 15
|
||||||
|
})
|
||||||
const searchForm = reactive<ExpiringAssetQueryParams>({
|
const searchForm = reactive<ExpiringAssetQueryParams>({
|
||||||
asset_type: undefined,
|
asset_type: undefined,
|
||||||
keyword: '',
|
keyword: '',
|
||||||
@@ -91,7 +101,7 @@
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
config: { clearable: true, placeholder: '全部' },
|
config: { clearable: true, placeholder: '全部' },
|
||||||
options: [
|
options: [
|
||||||
{ label: '网卡', value: 'card' },
|
{ label: '物联网卡', value: 'iot_card' },
|
||||||
{ label: '设备', value: 'device' }
|
{ label: '设备', value: 'device' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -137,13 +147,25 @@
|
|||||||
label: '最小剩余天数',
|
label: '最小剩余天数',
|
||||||
prop: 'days_min',
|
prop: 'days_min',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
config: { clearable: true, inputmode: 'numeric', placeholder: '最小天数' }
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 15,
|
||||||
|
placeholder: '最小天数'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '最大剩余天数',
|
label: '最大剩余天数',
|
||||||
prop: 'days_max',
|
prop: 'days_max',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
config: { clearable: true, inputmode: 'numeric', placeholder: '最大天数' }
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 15,
|
||||||
|
placeholder: '最大天数'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '预计到期起始',
|
label: '预计到期起始',
|
||||||
@@ -170,15 +192,15 @@
|
|||||||
]
|
]
|
||||||
|
|
||||||
const getExpiryClass = (level?: string | null) => {
|
const getExpiryClass = (level?: string | null) => {
|
||||||
if (level === 'critical' || level === '0_3') return 'expiry-critical'
|
if (level === 'red') return 'expiry-critical'
|
||||||
if (level === 'warning' || level === '4_7') return 'expiry-warning'
|
if (level === 'purple') return 'expiry-warning'
|
||||||
if (level === 'notice' || level === '8_15') return 'expiry-notice'
|
if (level === 'pink') return 'expiry-notice'
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const getExpiryTagType = (level?: string | null) => {
|
const getExpiryTagType = (level?: string | null) => {
|
||||||
if (level === 'critical' || level === '0_3') return 'danger'
|
if (level === 'red') return 'danger'
|
||||||
if (level === 'warning' || level === '4_7') return 'warning'
|
if (level === 'purple') return 'warning'
|
||||||
return 'info'
|
return 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +237,7 @@
|
|||||||
prop: 'asset_type',
|
prop: 'asset_type',
|
||||||
label: '资产类型',
|
label: '资产类型',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: ExpiringAssetItem) => (row.asset_type === 'card' ? '网卡' : '设备')
|
formatter: (row: ExpiringAssetItem) => (row.asset_type === 'iot_card' ? '物联网卡' : '设备')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'identifier',
|
prop: 'identifier',
|
||||||
@@ -246,7 +268,7 @@
|
|||||||
h(
|
h(
|
||||||
'span',
|
'span',
|
||||||
{ class: getExpiryClass(row.expiry_level) },
|
{ class: getExpiryClass(row.expiry_level) },
|
||||||
formatDateTime(row.estimated_final_expires_at)
|
formatDateTime(row.estimated_final_expires_at) || '-'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -254,10 +276,8 @@
|
|||||||
label: '剩余天数',
|
label: '剩余天数',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: ExpiringAssetItem) =>
|
formatter: (row: ExpiringAssetItem) =>
|
||||||
h(
|
h(ElTag, { type: getExpiryTagType(row.expiry_level), size: 'small' }, () =>
|
||||||
ElTag,
|
row.days_until_final_expiry == null ? '-' : `${row.days_until_final_expiry}天`
|
||||||
{ type: getExpiryTagType(row.expiry_level), size: 'small' },
|
|
||||||
() => `${row.days_until_final_expiry}天`
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -271,15 +291,43 @@
|
|||||||
const loadAssets = async () => {
|
const loadAssets = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await AssetService.getExpiringAssets({
|
const toInteger = (value: unknown) => {
|
||||||
...searchForm,
|
if (value === undefined || value === null || value === '') return undefined
|
||||||
|
const numberValue = Number(value)
|
||||||
|
return Number.isInteger(numberValue) ? numberValue : undefined
|
||||||
|
}
|
||||||
|
const params: ExpiringAssetQueryParams = {
|
||||||
page: pagination.currentPage,
|
page: pagination.currentPage,
|
||||||
size: pagination.pageSize
|
page_size: pagination.pageSize
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if (searchForm.asset_type) params.asset_type = searchForm.asset_type
|
||||||
|
const keyword = searchForm.keyword?.trim()
|
||||||
|
if (keyword) params.keyword = keyword
|
||||||
|
if (searchForm.shop_id !== undefined && searchForm.shop_id !== null) {
|
||||||
|
params.shop_id = searchForm.shop_id
|
||||||
|
}
|
||||||
|
if (searchForm.package_id !== undefined && searchForm.package_id !== null) {
|
||||||
|
params.package_id = searchForm.package_id
|
||||||
|
}
|
||||||
|
const daysMin = toInteger(searchForm.days_min)
|
||||||
|
const daysMax = toInteger(searchForm.days_max)
|
||||||
|
if (daysMin !== undefined) params.days_min = daysMin
|
||||||
|
if (daysMax !== undefined) params.days_max = daysMax
|
||||||
|
if (searchForm.expires_from) params.expires_from = searchForm.expires_from
|
||||||
|
if (searchForm.expires_to) params.expires_to = searchForm.expires_to
|
||||||
|
|
||||||
|
const response = await AssetService.getExpiringAssets(params)
|
||||||
if (response.code === 0 && response.data) {
|
if (response.code === 0 && response.data) {
|
||||||
items.value = response.data.items || []
|
items.value = response.data.items || []
|
||||||
pagination.total = response.data.total || 0
|
pagination.total = response.data.total || 0
|
||||||
|
Object.assign(summary, response.data.summary)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.msg || '获取临期资产失败')
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取临期资产失败:', error)
|
||||||
|
ElMessage.error('获取临期资产失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -318,21 +366,20 @@
|
|||||||
const goToAsset = (row: ExpiringAssetItem) => {
|
const goToAsset = (row: ExpiringAssetItem) => {
|
||||||
router.push({
|
router.push({
|
||||||
path: RoutesAlias.AssetInformation,
|
path: RoutesAlias.AssetInformation,
|
||||||
query: row.asset_type === 'card' ? { iccid: row.identifier } : { virtual_no: row.identifier }
|
query:
|
||||||
|
row.asset_type === 'iot_card' ? { iccid: row.identifier } : { virtual_no: row.identifier }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getActions = (row: ExpiringAssetItem) => [
|
const getActions = (row: ExpiringAssetItem) => [
|
||||||
{ label: '查看资产', type: 'primary' as const, handler: () => goToAsset(row) },
|
{ label: '查看资产', type: 'primary' as const, handler: () => goToAsset(row) }
|
||||||
...(row.can_renew
|
|
||||||
? [{ label: '续费', type: 'primary' as const, handler: () => goToAsset(row) }]
|
|
||||||
: [])
|
|
||||||
]
|
]
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const assetType = route.query.asset_type
|
const assetType = route.query.asset_type
|
||||||
if (assetType === 'card' || assetType === 'device')
|
if (assetType === 'card' || assetType === 'iot_card' || assetType === 'device') {
|
||||||
searchForm.asset_type = assetType as AssetType
|
searchForm.asset_type = assetType === 'card' ? 'iot_card' : (assetType as ExpiringAssetType)
|
||||||
|
}
|
||||||
void Promise.all([loadAssets(), searchShops(), searchPackages()])
|
void Promise.all([loadAssets(), searchShops(), searchPackages()])
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<ExportTaskList />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ExportTaskList from '../export-task-list/index.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ExportAgentRechargeTaskList' })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<ExportTaskList />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ExportTaskList from '../export-task-list/index.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ExportAgentWalletTransactionTaskList' })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<ExportTaskList />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ExportTaskList from '../export-task-list/index.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ExportExchangeTaskList' })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<ExportTaskList />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ExportTaskList from '../export-task-list/index.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ExportPackageTaskList' })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<ExportTaskList />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ExportTaskList from '../export-task-list/index.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ExportRefundTaskList' })
|
||||||
|
</script>
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
<ElButton v-if="canDownloadTask" type="primary" @click="downloadTask">
|
<ElButton v-if="canDownloadTask" type="primary" @click="downloadTask">
|
||||||
下载文件
|
下载文件
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton v-if="canCancelTask" type="danger" @click="cancelTask"> 取消任务 </ElButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, onMounted } from 'vue'
|
import { computed, h, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElButton, ElCard, ElEmpty, ElIcon, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
import { ElButton, ElCard, ElEmpty, ElIcon, ElMessage, ElTag } from 'element-plus'
|
||||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||||
import DetailPage from '@/components/common/DetailPage.vue'
|
import DetailPage from '@/components/common/DetailPage.vue'
|
||||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||||
@@ -104,22 +103,11 @@
|
|||||||
return status ? statusTypeMap[status] : 'info'
|
return status ? statusTypeMap[status] : 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
const canCancelTask = computed(
|
|
||||||
() =>
|
|
||||||
canViewDetail.value &&
|
|
||||||
[ExportTaskStatus.PENDING, ExportTaskStatus.PROCESSING].includes(
|
|
||||||
taskDetail.value?.status as ExportTaskStatus
|
|
||||||
) &&
|
|
||||||
!!taskPermissions.value &&
|
|
||||||
hasAuth(taskPermissions.value.cancel)
|
|
||||||
)
|
|
||||||
|
|
||||||
const detailSections = computed((): DetailSection[] => [
|
const detailSections = computed((): DetailSection[] => [
|
||||||
{
|
{
|
||||||
title: '任务基本信息',
|
title: '任务基本信息',
|
||||||
fields: [
|
fields: [
|
||||||
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
|
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
|
||||||
{ label: '任务ID', prop: 'task_id', formatter: (value: number) => String(value ?? '-') },
|
|
||||||
{
|
{
|
||||||
label: '导出场景',
|
label: '导出场景',
|
||||||
render: (data: ExportTaskDetail) => h(ElTag, {}, () => getExportTaskSceneName(data.scene))
|
render: (data: ExportTaskDetail) => h(ElTag, {}, () => getExportTaskSceneName(data.scene))
|
||||||
@@ -269,35 +257,6 @@
|
|||||||
window.open(taskDetail.value.download_url, '_blank')
|
window.open(taskDetail.value.download_url, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelTask = () => {
|
|
||||||
if (!taskDetail.value) return
|
|
||||||
if (!canCancelTask.value) {
|
|
||||||
ElMessage.warning('您没有取消该导出任务的权限')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessageBox.confirm(`确定取消导出任务 ${taskDetail.value.task_no} 吗?`, '取消确认', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(async () => {
|
|
||||||
try {
|
|
||||||
const res = await ExportTaskService.cancelExportTask(
|
|
||||||
taskDetail.value!.task_id ?? taskDetail.value!.id
|
|
||||||
)
|
|
||||||
if (res.code === 0) {
|
|
||||||
ElMessage.success(res.data?.message || '任务取消成功')
|
|
||||||
polling.retry()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || '取消导出任务失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('取消导出任务失败:', error)
|
|
||||||
ElMessage.error('取消导出任务失败')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadTaskDetail()
|
loadTaskDetail()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
:marginTop="10"
|
:marginTop="10"
|
||||||
:actions="getActions"
|
:actions="getActions"
|
||||||
:actionsWidth="180"
|
:actionsWidth="120"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
>
|
>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, h, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox, ElProgress, ElTag } from 'element-plus'
|
import { ElMessage, ElProgress, ElTag } from 'element-plus'
|
||||||
import { ExportTaskService } from '@/api/modules'
|
import { ExportTaskService } from '@/api/modules'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
@@ -231,6 +231,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toRfc3339 = (date: string, endOfDay = false) => {
|
||||||
|
if (!date) return undefined
|
||||||
|
if (date.includes('T')) return date
|
||||||
|
return `${date}T${endOfDay ? '23:59:59' : '00:00:00'}+08:00`
|
||||||
|
}
|
||||||
|
|
||||||
const getTableData = async () => {
|
const getTableData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -240,8 +246,8 @@
|
|||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
scene: currentScene.value,
|
scene: currentScene.value,
|
||||||
status: searchForm.status,
|
status: searchForm.status,
|
||||||
start_time: searchForm.start_time || undefined,
|
start_time: toRfc3339(searchForm.start_time),
|
||||||
end_time: searchForm.end_time || undefined
|
end_time: toRfc3339(searchForm.end_time, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
@@ -347,32 +353,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelTask = (row: ExportTaskItem) => {
|
|
||||||
if (!hasAuth(getRowPermissions(row).cancel)) {
|
|
||||||
ElMessage.warning('您没有取消导出任务的权限')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessageBox.confirm(`确定取消导出任务 ${row.task_no} 吗?`, '取消确认', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(async () => {
|
|
||||||
try {
|
|
||||||
const res = await ExportTaskService.cancelExportTask(row.task_id ?? row.id)
|
|
||||||
if (res.code === 0) {
|
|
||||||
ElMessage.success(res.data?.message || '任务取消成功')
|
|
||||||
getTableData()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || '取消导出任务失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('取消导出任务失败:', error)
|
|
||||||
ElMessage.error('取消导出任务失败')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getActions = (row: ExportTaskItem) => {
|
const getActions = (row: ExportTaskItem) => {
|
||||||
const actions: any[] = []
|
const actions: any[] = []
|
||||||
const permissions = getRowPermissions(row)
|
const permissions = getRowPermissions(row)
|
||||||
@@ -381,13 +361,6 @@
|
|||||||
actions.push({ label: '下载', handler: () => downloadTask(row), type: 'primary' })
|
actions.push({ label: '下载', handler: () => downloadTask(row), type: 'primary' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
[ExportTaskStatus.PENDING, ExportTaskStatus.PROCESSING].includes(row.status) &&
|
|
||||||
hasAuth(permissions.cancel)
|
|
||||||
) {
|
|
||||||
actions.push({ label: '取消', handler: () => cancelTask(row), type: 'danger' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
<template>
|
||||||
|
<ArtTableFullScreen>
|
||||||
|
<div class="bulk-purchase-detail-page" id="table-full-screen">
|
||||||
|
<ElCard shadow="never" class="art-table-card">
|
||||||
|
<div class="detail-header">
|
||||||
|
<ElButton @click="goBack">
|
||||||
|
<template #icon>
|
||||||
|
<ElIcon><ArrowLeft /></ElIcon>
|
||||||
|
</template>
|
||||||
|
返回
|
||||||
|
</ElButton>
|
||||||
|
<h2 class="detail-title">批量订购任务详情</h2>
|
||||||
|
<ElTag v-if="taskDetail" :type="getStatusType(taskDetail.status)">
|
||||||
|
{{ taskDetail.status_name || getStatusName(taskDetail.status) }}
|
||||||
|
</ElTag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading && !taskDetail" class="detail-loading">
|
||||||
|
<ElIcon class="is-loading" :size="36"><Loading /></ElIcon>
|
||||||
|
<div>加载中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else-if="taskDetail">
|
||||||
|
<ElDescriptions :column="4" border>
|
||||||
|
<ElDescriptionsItem label="任务号">{{ taskDetail.task_no || '-' }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="订单文件" :span="2">
|
||||||
|
<span class="ellipsis-value" :title="taskDetail.file_name || '-'">
|
||||||
|
{{ taskDetail.file_name || '-' }}
|
||||||
|
</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="套餐名称">
|
||||||
|
<span class="ellipsis-value" :title="taskDetail.package_name || '-'">
|
||||||
|
{{ taskDetail.package_name || '-' }}
|
||||||
|
</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="套餐编码">
|
||||||
|
<span class="ellipsis-value" :title="taskDetail.package_code || '-'">
|
||||||
|
{{ taskDetail.package_code || '-' }}
|
||||||
|
</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="支付方式">
|
||||||
|
{{ getPaymentMethodName(taskDetail.payment_method) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="总数">{{ taskDetail.total_count ?? 0 }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="成功数">{{
|
||||||
|
taskDetail.success_count ?? 0
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="失败数">{{ taskDetail.fail_count ?? 0 }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="创建人">{{
|
||||||
|
taskDetail.creator_name || '-'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="支付凭证">
|
||||||
|
{{
|
||||||
|
taskDetail.voucher_keys?.length
|
||||||
|
? `已上传 ${taskDetail.voucher_keys.length} 个`
|
||||||
|
: '未上传'
|
||||||
|
}}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="创建时间">{{
|
||||||
|
formatDateTime(taskDetail.created_at)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="开始时间">{{
|
||||||
|
formatDateTime(taskDetail.started_at)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="完成时间">{{
|
||||||
|
formatDateTime(taskDetail.completed_at)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="更新时间">{{
|
||||||
|
formatDateTime(taskDetail.updated_at)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem v-if="taskDetail.error_message" label="错误信息" :span="4">
|
||||||
|
{{ taskDetail.error_message }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
|
||||||
|
<ElAlert
|
||||||
|
v-if="taskDetail.error_summary"
|
||||||
|
class="task-error"
|
||||||
|
type="error"
|
||||||
|
:closable="false"
|
||||||
|
:title="taskDetail.error_summary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.items)">
|
||||||
|
<div class="items-toolbar">
|
||||||
|
<ElSelect
|
||||||
|
v-model="itemStatus"
|
||||||
|
clearable
|
||||||
|
placeholder="筛选行状态"
|
||||||
|
style="width: 140px"
|
||||||
|
>
|
||||||
|
<ElOption label="成功" :value="BulkPurchaseTaskStatus.COMPLETED" />
|
||||||
|
<ElOption label="失败" :value="BulkPurchaseTaskStatus.FAILED" />
|
||||||
|
</ElSelect>
|
||||||
|
<ElButton :loading="loading" @click="refreshTask">刷新结果</ElButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElTable v-loading="loading" :data="paginatedItems" border>
|
||||||
|
<ElTableColumn label="行号" width="90">
|
||||||
|
<template #default="scope">{{ scope.row.line ?? '-' }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="资产" min-width="180">
|
||||||
|
<template #default="scope">{{ getAssetIdentifier(scope.row) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="状态" width="110">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElTag :type="getItemStatusType(scope.row.status)">
|
||||||
|
{{ scope.row.status_name || getItemStatusName(scope.row.status) }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="order_no" label="订单号" min-width="180" show-overflow-tooltip />
|
||||||
|
<ElTableColumn label="金额" width="120">
|
||||||
|
<template #default="scope">{{ formatMoney(scope.row.amount) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="处理结果" min-width="240" show-overflow-tooltip>
|
||||||
|
<template #default="scope">{{ getItemReason(scope.row) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<ElPagination
|
||||||
|
v-model:current-page="itemsPage"
|
||||||
|
v-model:page-size="itemsSize"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:total="filteredItems.length"
|
||||||
|
@size-change="handleItemSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ElEmpty v-else :description="forbidden ? '暂无查看权限' : '暂无任务详情'" />
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</ArtTableFullScreen>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
ElAlert,
|
||||||
|
ElButton,
|
||||||
|
ElCard,
|
||||||
|
ElDescriptions,
|
||||||
|
ElDescriptionsItem,
|
||||||
|
ElEmpty,
|
||||||
|
ElIcon,
|
||||||
|
ElMessage,
|
||||||
|
ElOption,
|
||||||
|
ElPagination,
|
||||||
|
ElSelect,
|
||||||
|
ElTable,
|
||||||
|
ElTableColumn,
|
||||||
|
ElTag
|
||||||
|
} from 'element-plus'
|
||||||
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||||
|
import { BulkPurchaseService } from '@/api/modules'
|
||||||
|
import type { BulkPurchaseItem, BulkPurchasePaymentMethod, BulkPurchaseTask } from '@/types/api'
|
||||||
|
import { BulkPurchaseTaskStatus } from '@/types/api'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { useAsyncTaskPolling } from '@/composables/useAsyncTaskPolling'
|
||||||
|
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
||||||
|
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
||||||
|
|
||||||
|
defineOptions({ name: 'BulkPurchaseDetail' })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const itemStatus = ref<BulkPurchaseTaskStatus | undefined>()
|
||||||
|
const itemsPage = ref(1)
|
||||||
|
const itemsSize = ref(20)
|
||||||
|
const taskId = Number(route.params.id)
|
||||||
|
|
||||||
|
const polling = useAsyncTaskPolling<BulkPurchaseTask>({
|
||||||
|
storageKey: `bulk-purchase-detail:${taskId}`,
|
||||||
|
autoRestore: false,
|
||||||
|
fetchTask: async (id) => {
|
||||||
|
const res = await BulkPurchaseService.getTask(id)
|
||||||
|
if (res.code === 403) {
|
||||||
|
const error = new Error('暂无查看该批量订购任务详情的权限') as Error & { status?: number }
|
||||||
|
error.status = 403
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
if (res.code !== 0 || !res.data) {
|
||||||
|
throw new Error(res.msg || '获取批量订购任务详情失败')
|
||||||
|
}
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
isForbidden: (error: any) => error?.status === 403 || error?.response?.status === 403
|
||||||
|
})
|
||||||
|
const taskDetail = polling.task
|
||||||
|
const loading = polling.loading
|
||||||
|
const forbidden = polling.forbidden
|
||||||
|
|
||||||
|
const loadTaskDetail = async () => {
|
||||||
|
if (!taskId) {
|
||||||
|
ElMessage.error('缺少任务 ID 参数')
|
||||||
|
goBack()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await polling.start(taskId)
|
||||||
|
if (polling.error.value) ElMessage.error(polling.error.value)
|
||||||
|
if (polling.forbidden.value) ElMessage.warning('暂无查看该批量订购任务详情的权限')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTask = () => {
|
||||||
|
void polling.retry()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusName = (status?: BulkPurchaseTaskStatus) => {
|
||||||
|
const names: Record<BulkPurchaseTaskStatus, string> = {
|
||||||
|
[BulkPurchaseTaskStatus.PENDING]: '待处理',
|
||||||
|
[BulkPurchaseTaskStatus.PROCESSING]: '处理中',
|
||||||
|
[BulkPurchaseTaskStatus.COMPLETED]: '已完成',
|
||||||
|
[BulkPurchaseTaskStatus.FAILED]: '已失败',
|
||||||
|
[BulkPurchaseTaskStatus.CANCELED]: '已取消'
|
||||||
|
}
|
||||||
|
return status ? names[status] || '-' : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusType = (status?: BulkPurchaseTaskStatus) => {
|
||||||
|
if (status === BulkPurchaseTaskStatus.COMPLETED) return 'success'
|
||||||
|
if (status === BulkPurchaseTaskStatus.FAILED) return 'danger'
|
||||||
|
if (status === BulkPurchaseTaskStatus.PROCESSING) return 'warning'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPaymentMethodName = (method?: BulkPurchasePaymentMethod) => {
|
||||||
|
return method === 'offline' ? '线下支付' : method === 'wallet' ? '代理钱包' : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAssetIdentifier = (item: BulkPurchaseItem) =>
|
||||||
|
item.asset_identifier || item.iccid || item.virtual_no || '-'
|
||||||
|
|
||||||
|
const getItemStatusName = (status?: BulkPurchaseTaskStatus) => {
|
||||||
|
if (status === BulkPurchaseTaskStatus.COMPLETED) return '成功'
|
||||||
|
if (status === BulkPurchaseTaskStatus.FAILED) return '失败'
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getItemStatusType = (status?: BulkPurchaseTaskStatus) => {
|
||||||
|
if (status === BulkPurchaseTaskStatus.COMPLETED) return 'success'
|
||||||
|
if (status === BulkPurchaseTaskStatus.FAILED) return 'danger'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getItemReason = (item: BulkPurchaseItem) =>
|
||||||
|
item.reason || item.error_summary || item.error_reason || '-'
|
||||||
|
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
const items = taskDetail.value?.items || []
|
||||||
|
if (itemStatus.value === undefined) return items
|
||||||
|
return items.filter((item) => item.status === itemStatus.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const paginatedItems = computed(() => {
|
||||||
|
const start = (itemsPage.value - 1) * itemsSize.value
|
||||||
|
return filteredItems.value.slice(start, start + itemsSize.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleItemSizeChange = () => {
|
||||||
|
itemsPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(itemStatus, () => {
|
||||||
|
itemsPage.value = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadTaskDetail()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.bulk-purchase-detail-page {
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 220px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis-value {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: bottom;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-error {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin: 20px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,506 @@
|
|||||||
|
<template>
|
||||||
|
<ArtTableFullScreen>
|
||||||
|
<div class="bulk-purchase-page" id="table-full-screen">
|
||||||
|
<ElDrawer
|
||||||
|
v-model="createDrawerVisible"
|
||||||
|
title="批量订购套餐"
|
||||||
|
direction="rtl"
|
||||||
|
size="520px"
|
||||||
|
:close-on-click-modal="!createDrawerBusy"
|
||||||
|
:close-on-press-escape="!createDrawerBusy"
|
||||||
|
@closed="handleCreateDrawerClosed"
|
||||||
|
>
|
||||||
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="create-form">
|
||||||
|
<ElFormItem label="套餐" prop="package_id">
|
||||||
|
<ElSelect
|
||||||
|
v-model="form.package_id"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
clearable
|
||||||
|
:remote-method="handlePackageSearch"
|
||||||
|
:loading="packageLoading"
|
||||||
|
placeholder="请输入套餐名称搜索"
|
||||||
|
style="width: 100%"
|
||||||
|
@visible-change="handlePackageVisibleChange"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="pkg in packageOptions"
|
||||||
|
:key="pkg.id"
|
||||||
|
:label="pkg.package_name"
|
||||||
|
:value="pkg.id"
|
||||||
|
>
|
||||||
|
<span class="package-option">{{ pkg.package_name }}</span>
|
||||||
|
</ElOption>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem label="支付方式" prop="payment_method">
|
||||||
|
<ElRadioGroup v-model="form.payment_method" @change="handlePaymentMethodChange">
|
||||||
|
<ElRadio value="wallet">代理钱包</ElRadio>
|
||||||
|
<ElRadio value="offline">线下支付</ElRadio>
|
||||||
|
</ElRadioGroup>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem label="订单文件" prop="orderFile">
|
||||||
|
<VoucherUpload
|
||||||
|
ref="orderUploadRef"
|
||||||
|
v-model="orderFileKeys"
|
||||||
|
voucher-name="订单文件"
|
||||||
|
:max-count="1"
|
||||||
|
purpose="batch_purchase"
|
||||||
|
:max-size-mb="10"
|
||||||
|
single-column-csv
|
||||||
|
:max-csv-rows="1000"
|
||||||
|
accept=".csv"
|
||||||
|
tip="仅支持 UTF-8 单列 CSV,最多 1000 行,最大 10MB"
|
||||||
|
@uploading-change="orderFileUploading = $event"
|
||||||
|
@change="formRef?.validateField('orderFile')"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem
|
||||||
|
v-if="form.payment_method === 'offline'"
|
||||||
|
label="整批支付凭证"
|
||||||
|
prop="voucherFile"
|
||||||
|
>
|
||||||
|
<VoucherUpload
|
||||||
|
ref="voucherUploadRef"
|
||||||
|
v-model="voucherFileKeys"
|
||||||
|
voucher-name="整批支付凭证"
|
||||||
|
@uploading-change="voucherUploading = $event"
|
||||||
|
@change="formRef?.validateField('voucherFile')"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElAlert
|
||||||
|
v-if="createDrawerBusy"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
:title="
|
||||||
|
voucherUploading
|
||||||
|
? '支付凭证上传中,请稍候'
|
||||||
|
: orderFileUploading
|
||||||
|
? '订单文件上传中,请稍候'
|
||||||
|
: '批量订购文件上传及任务创建中,请勿重复提交'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="drawer-footer">
|
||||||
|
<ElButton :disabled="createDrawerBusy" @click="createDrawerVisible = false">
|
||||||
|
取消
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
:loading="createDrawerBusy"
|
||||||
|
:disabled="!hasAuth(BULK_PURCHASE_PERMISSIONS.create)"
|
||||||
|
@click="submitTask"
|
||||||
|
>
|
||||||
|
创建批量订购任务
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDrawer>
|
||||||
|
|
||||||
|
<ElCard
|
||||||
|
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.detail)"
|
||||||
|
shadow="never"
|
||||||
|
class="art-table-card task-list-card"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="task-list-header">
|
||||||
|
<span>批量订购任务</span>
|
||||||
|
<div class="task-list-toolbar">
|
||||||
|
<ElButton
|
||||||
|
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.template)"
|
||||||
|
tag="a"
|
||||||
|
href="/templates/bulk-purchase-template.csv"
|
||||||
|
download="批量订购套餐模板.csv"
|
||||||
|
>
|
||||||
|
下载模板
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.create)"
|
||||||
|
type="primary"
|
||||||
|
@click="openCreateDrawer"
|
||||||
|
>
|
||||||
|
批量订购套餐
|
||||||
|
</ElButton>
|
||||||
|
<ElSelect
|
||||||
|
v-model="taskListStatus"
|
||||||
|
clearable
|
||||||
|
placeholder="筛选任务状态"
|
||||||
|
style="width: 150px"
|
||||||
|
@change="handleTaskListStatusChange"
|
||||||
|
>
|
||||||
|
<ElOption label="待处理" :value="BulkPurchaseTaskStatus.PENDING" />
|
||||||
|
<ElOption label="处理中" :value="BulkPurchaseTaskStatus.PROCESSING" />
|
||||||
|
<ElOption label="已完成" :value="BulkPurchaseTaskStatus.COMPLETED" />
|
||||||
|
<ElOption label="已失败" :value="BulkPurchaseTaskStatus.FAILED" />
|
||||||
|
<ElOption label="已取消" :value="BulkPurchaseTaskStatus.CANCELED" />
|
||||||
|
</ElSelect>
|
||||||
|
<ElButton :loading="taskListLoading" @click="loadTaskList">刷新</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ElTable v-loading="taskListLoading" :data="taskList" border>
|
||||||
|
<ElTableColumn label="任务号" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<ElButton type="primary" link @click="showTask(scope.row)">
|
||||||
|
{{ scope.row.task_no || '-' }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="file_name" label="订单文件" min-width="180" show-overflow-tooltip />
|
||||||
|
<ElTableColumn
|
||||||
|
prop="package_name"
|
||||||
|
label="套餐名称"
|
||||||
|
min-width="180"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<ElTableColumn
|
||||||
|
prop="package_code"
|
||||||
|
label="套餐编码"
|
||||||
|
min-width="180"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<ElTableColumn label="支付方式" width="110">
|
||||||
|
<template #default="scope">{{
|
||||||
|
getPaymentMethodName(scope.row.payment_method)
|
||||||
|
}}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="状态" width="110">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElTag :type="getStatusType(scope.row.status)">
|
||||||
|
{{ scope.row.status_name || getStatusName(scope.row.status) }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="total_count" label="总数" width="90" />
|
||||||
|
<ElTableColumn prop="success_count" label="成功数" width="90" />
|
||||||
|
<ElTableColumn prop="fail_count" label="失败数" width="90" />
|
||||||
|
<ElTableColumn label="创建时间" width="180">
|
||||||
|
<template #default="scope">{{ formatDateTime(scope.row.created_at) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<ElPagination
|
||||||
|
v-model:current-page="taskListPage"
|
||||||
|
v-model:page-size="taskListSize"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:total="taskListTotal"
|
||||||
|
@current-change="loadTaskList"
|
||||||
|
@size-change="handleTaskListSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</ArtTableFullScreen>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
ElAlert,
|
||||||
|
ElButton,
|
||||||
|
ElCard,
|
||||||
|
ElDrawer,
|
||||||
|
ElForm,
|
||||||
|
ElMessage,
|
||||||
|
ElOption,
|
||||||
|
ElPagination,
|
||||||
|
ElRadio,
|
||||||
|
ElRadioGroup,
|
||||||
|
ElSelect,
|
||||||
|
ElTable,
|
||||||
|
ElTableColumn,
|
||||||
|
ElTag
|
||||||
|
} from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||||
|
import { BulkPurchaseService, PackageManageService } from '@/api/modules'
|
||||||
|
import type { BulkPurchasePaymentMethod, BulkPurchaseTask, PackageResponse } from '@/types/api'
|
||||||
|
import { BulkPurchaseTaskStatus } from '@/types/api'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
||||||
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
|
|
||||||
|
defineOptions({ name: 'BulkPurchase' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const orderUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||||
|
const voucherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||||
|
const createDrawerVisible = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const orderFileKeys = ref<string[]>([])
|
||||||
|
const orderFileUploading = ref(false)
|
||||||
|
const voucherFileKeys = ref<string[]>([])
|
||||||
|
const voucherUploading = ref(false)
|
||||||
|
const packageOptions = ref<PackageResponse[]>([])
|
||||||
|
const packageLoading = ref(false)
|
||||||
|
const taskList = ref<BulkPurchaseTask[]>([])
|
||||||
|
const taskListLoading = ref(false)
|
||||||
|
const taskListPage = ref(1)
|
||||||
|
const taskListSize = ref(20)
|
||||||
|
const taskListTotal = ref(0)
|
||||||
|
const taskListStatus = ref<BulkPurchaseTaskStatus | undefined>()
|
||||||
|
const createDrawerBusy = computed(
|
||||||
|
() => submitting.value || voucherUploading.value || orderFileUploading.value
|
||||||
|
)
|
||||||
|
|
||||||
|
const form = reactive<{
|
||||||
|
package_id?: number
|
||||||
|
payment_method: BulkPurchasePaymentMethod
|
||||||
|
}>({
|
||||||
|
package_id: undefined,
|
||||||
|
payment_method: 'wallet'
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = computed<FormRules>(() => ({
|
||||||
|
package_id: [{ required: true, message: '请选择套餐', trigger: 'change' }],
|
||||||
|
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
||||||
|
orderFile: [
|
||||||
|
{
|
||||||
|
validator: (_rule: unknown, _value: unknown, callback: (error?: Error) => void) => {
|
||||||
|
if (orderFileKeys.value.length === 0) callback(new Error('请上传订单文件'))
|
||||||
|
else callback()
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
voucherFile: [
|
||||||
|
{
|
||||||
|
validator: (_rule: unknown, _value: unknown, callback: (error?: Error) => void) => {
|
||||||
|
if (form.payment_method === 'offline' && voucherFileKeys.value.length === 0) {
|
||||||
|
callback(new Error('线下支付必须上传整批支付凭证'))
|
||||||
|
} else callback()
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
|
const loadPackages = async (query = '') => {
|
||||||
|
packageLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await PackageManageService.getPackages({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
package_name: query.trim() || undefined,
|
||||||
|
status: 1,
|
||||||
|
shelf_status: 1
|
||||||
|
})
|
||||||
|
if (res.code !== 0) {
|
||||||
|
ElMessage.error(res.msg || '获取套餐列表失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
packageOptions.value = (res.data?.items || []).filter(
|
||||||
|
(item) => item.status === 1 && item.shelf_status === 1
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error?.message || '获取套餐列表失败')
|
||||||
|
} finally {
|
||||||
|
packageLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePackageSearch = (query: string) => {
|
||||||
|
void loadPackages(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePackageVisibleChange = (visible: boolean) => {
|
||||||
|
if (visible && packageOptions.value.length === 0) void loadPackages()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateDrawer = () => {
|
||||||
|
createDrawerVisible.value = true
|
||||||
|
if (packageOptions.value.length === 0) void loadPackages()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateDrawerClosed = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
form.package_id = undefined
|
||||||
|
form.payment_method = 'wallet'
|
||||||
|
orderFileKeys.value = []
|
||||||
|
voucherFileKeys.value = []
|
||||||
|
orderUploadRef.value?.clearFiles(false)
|
||||||
|
voucherUploadRef.value?.clearFiles(false)
|
||||||
|
orderFileUploading.value = false
|
||||||
|
voucherUploading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaymentMethodChange = (value: string | number | boolean | undefined) => {
|
||||||
|
if (value === 'wallet') {
|
||||||
|
voucherFileKeys.value = []
|
||||||
|
voucherUploadRef.value?.clearFiles()
|
||||||
|
formRef.value?.clearValidate('voucherFile')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitTask = async () => {
|
||||||
|
if (
|
||||||
|
!hasAuth(BULK_PURCHASE_PERMISSIONS.create) ||
|
||||||
|
submitting.value ||
|
||||||
|
voucherUploading.value ||
|
||||||
|
orderFileUploading.value
|
||||||
|
)
|
||||||
|
return
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid || !form.package_id || orderFileKeys.value.length === 0) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
file_key: orderFileKeys.value[0],
|
||||||
|
package_id: form.package_id,
|
||||||
|
payment_method: form.payment_method,
|
||||||
|
...(form.payment_method === 'offline' ? { voucher_keys: voucherFileKeys.value } : {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await BulkPurchaseService.createTask(data)
|
||||||
|
const taskId = res.data?.task_id ?? res.data?.id
|
||||||
|
if (res.code !== 0 || !taskId) {
|
||||||
|
ElMessage.error(res.msg || '创建批量订购任务失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success(res.data.message || '批量订购任务已创建')
|
||||||
|
createDrawerVisible.value = false
|
||||||
|
await router.push(`${RoutesAlias.BulkPurchaseDetail}/${taskId}`)
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error?.message || '创建批量订购任务失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTaskList = async () => {
|
||||||
|
if (!hasAuth(BULK_PURCHASE_PERMISSIONS.detail)) return
|
||||||
|
taskListLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await BulkPurchaseService.getTasks({
|
||||||
|
page: taskListPage.value,
|
||||||
|
page_size: taskListSize.value,
|
||||||
|
status: taskListStatus.value
|
||||||
|
})
|
||||||
|
if (res.code !== 0) {
|
||||||
|
ElMessage.error(res.msg || '获取批量订购任务列表失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taskList.value = res.data?.items || []
|
||||||
|
taskListTotal.value = res.data?.total || 0
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error?.message || '获取批量订购任务列表失败')
|
||||||
|
} finally {
|
||||||
|
taskListLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTaskListStatusChange = () => {
|
||||||
|
taskListPage.value = 1
|
||||||
|
void loadTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTaskListSizeChange = () => {
|
||||||
|
taskListPage.value = 1
|
||||||
|
void loadTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const showTask = (task: BulkPurchaseTask) => {
|
||||||
|
const taskId = task.task_id ?? task.id
|
||||||
|
if (!taskId) return
|
||||||
|
|
||||||
|
void router.push(`${RoutesAlias.BulkPurchaseDetail}/${taskId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusName = (status: BulkPurchaseTaskStatus) => {
|
||||||
|
const names: Record<BulkPurchaseTaskStatus, string> = {
|
||||||
|
[BulkPurchaseTaskStatus.PENDING]: '待处理',
|
||||||
|
[BulkPurchaseTaskStatus.PROCESSING]: '处理中',
|
||||||
|
[BulkPurchaseTaskStatus.COMPLETED]: '已完成',
|
||||||
|
[BulkPurchaseTaskStatus.FAILED]: '已失败',
|
||||||
|
[BulkPurchaseTaskStatus.CANCELED]: '已取消'
|
||||||
|
}
|
||||||
|
return names[status] || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusType = (status: BulkPurchaseTaskStatus) => {
|
||||||
|
if (status === BulkPurchaseTaskStatus.COMPLETED) return 'success'
|
||||||
|
if (status === BulkPurchaseTaskStatus.FAILED) return 'danger'
|
||||||
|
if (status === BulkPurchaseTaskStatus.PROCESSING) return 'warning'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPaymentMethodName = (method?: BulkPurchasePaymentMethod) => {
|
||||||
|
return method === 'offline' ? '线下支付' : method === 'wallet' ? '代理钱包' : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadTaskList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.bulk-purchase-page {
|
||||||
|
.task-list-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-option {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-form {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.task-list-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list-toolbar {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-select {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,506 @@
|
|||||||
|
<template>
|
||||||
|
<ArtTableFullScreen>
|
||||||
|
<div class="device-batch-allocation-page" id="table-full-screen">
|
||||||
|
<ElDrawer
|
||||||
|
v-model="createDrawerVisible"
|
||||||
|
title="设备批量分配"
|
||||||
|
direction="rtl"
|
||||||
|
size="520px"
|
||||||
|
:close-on-click-modal="!createDrawerBusy"
|
||||||
|
:close-on-press-escape="!createDrawerBusy"
|
||||||
|
@closed="handleCreateDrawerClosed"
|
||||||
|
>
|
||||||
|
<ElForm ref="formRef" :model="form" label-width="110px" class="create-form">
|
||||||
|
<ElFormItem label="操作类型" prop="operation_type">
|
||||||
|
<ElSelect
|
||||||
|
v-model="form.operation_type"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleOperationTypeChange"
|
||||||
|
>
|
||||||
|
<ElOption label="分配目标代理" value="assign_shop" />
|
||||||
|
<ElOption label="设置套餐系列" value="assign_series" />
|
||||||
|
<ElOption label="批量回收设备" value="recall" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem v-if="requiresTargetId" label="目标 ID" prop="target_id">
|
||||||
|
<ElSelect
|
||||||
|
v-model="form.target_id"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
clearable
|
||||||
|
:remote-method="handleTargetSearch"
|
||||||
|
:loading="targetLoading"
|
||||||
|
:placeholder="targetPlaceholder"
|
||||||
|
style="width: 100%"
|
||||||
|
@visible-change="handleTargetVisibleChange"
|
||||||
|
>
|
||||||
|
<template v-if="form.operation_type === 'assign_shop'">
|
||||||
|
<ElOption
|
||||||
|
v-for="shop in shopOptions"
|
||||||
|
:key="shop.id"
|
||||||
|
:label="shop.shop_name"
|
||||||
|
:value="shop.id"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ElOption
|
||||||
|
v-for="series in seriesOptions"
|
||||||
|
:key="series.id"
|
||||||
|
:label="series.series_name"
|
||||||
|
:value="series.id"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem v-else label="回收目标">
|
||||||
|
<ElTag type="warning">回收到平台库存</ElTag>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem label="设备文件" prop="file_key">
|
||||||
|
<VoucherUpload
|
||||||
|
ref="uploadRef"
|
||||||
|
v-model="fileKeys"
|
||||||
|
voucher-name="设备标识文件"
|
||||||
|
:max-count="1"
|
||||||
|
purpose="device_batch_allocation"
|
||||||
|
:max-size-mb="10"
|
||||||
|
single-column-csv
|
||||||
|
:max-csv-rows="1000"
|
||||||
|
accept=".csv"
|
||||||
|
tip="仅支持 UTF-8 单列 CSV,最多 1000 行,最大 10MB"
|
||||||
|
@uploading-change="fileUploading = $event"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElAlert
|
||||||
|
v-if="createDrawerBusy"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
:title="fileUploading ? '设备文件上传中,请稍候' : '任务创建中,请勿重复提交'"
|
||||||
|
/>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="drawer-footer">
|
||||||
|
<ElButton :disabled="createDrawerBusy" @click="createDrawerVisible = false">
|
||||||
|
取消
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
:loading="submitting"
|
||||||
|
:disabled="!hasAuth(JULY_PERMISSIONS.deviceAllocation.create)"
|
||||||
|
@click="submitTask"
|
||||||
|
>
|
||||||
|
创建分配任务
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDrawer>
|
||||||
|
|
||||||
|
<ElCard shadow="never" class="art-table-card task-list-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="task-list-header">
|
||||||
|
<span>设备批量分配任务</span>
|
||||||
|
<div class="task-list-toolbar">
|
||||||
|
<ElButton
|
||||||
|
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.create)"
|
||||||
|
tag="a"
|
||||||
|
href="/templates/device-batch-allocation-template.csv"
|
||||||
|
download="设备批量分配模板.csv"
|
||||||
|
>
|
||||||
|
下载模板
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.create)"
|
||||||
|
type="primary"
|
||||||
|
@click="openCreateDrawer"
|
||||||
|
>
|
||||||
|
设备批量分配
|
||||||
|
</ElButton>
|
||||||
|
<ElSelect
|
||||||
|
v-model="taskListOperationType"
|
||||||
|
clearable
|
||||||
|
placeholder="筛选操作类型"
|
||||||
|
style="width: 170px"
|
||||||
|
@change="handleTaskListFilterChange"
|
||||||
|
>
|
||||||
|
<ElOption label="分配目标代理" value="assign_shop" />
|
||||||
|
<ElOption label="设置套餐系列" value="assign_series" />
|
||||||
|
<ElOption label="批量回收设备" value="recall" />
|
||||||
|
</ElSelect>
|
||||||
|
<ElSelect
|
||||||
|
v-model="taskListStatus"
|
||||||
|
clearable
|
||||||
|
placeholder="筛选任务状态"
|
||||||
|
style="width: 150px"
|
||||||
|
@change="handleTaskListFilterChange"
|
||||||
|
>
|
||||||
|
<ElOption label="待处理" :value="DeviceImportTaskStatus.PENDING" />
|
||||||
|
<ElOption label="处理中" :value="DeviceImportTaskStatus.PROCESSING" />
|
||||||
|
<ElOption label="已完成" :value="DeviceImportTaskStatus.COMPLETED" />
|
||||||
|
<ElOption label="失败" :value="DeviceImportTaskStatus.FAILED" />
|
||||||
|
</ElSelect>
|
||||||
|
<ElButton :loading="taskListLoading" @click="loadTaskList">刷新</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ElTable v-loading="taskListLoading" :data="taskList" border>
|
||||||
|
<ElTableColumn label="任务号" min-width="210" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<ElButton
|
||||||
|
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.detail)"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="showTask(scope.row)"
|
||||||
|
>
|
||||||
|
{{ scope.row.task_no || '-' }}
|
||||||
|
</ElButton>
|
||||||
|
<span v-else>{{ scope.row.task_no || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="操作类型" width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElTag :type="getOperationTagType(scope.row.operation_type)">
|
||||||
|
{{ getOperationLabel(scope.row.operation_type) }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn
|
||||||
|
prop="operation_name"
|
||||||
|
label="操作名称"
|
||||||
|
min-width="170"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.operation_name || getOperationLabel(scope.row.operation_type) }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="目标 ID" width="110">
|
||||||
|
<template #default="scope">{{ scope.row.target_id ?? '-' }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="状态" width="110">
|
||||||
|
<template #default="scope">
|
||||||
|
<ElTag :type="getStatusTagType(scope.row.status)">
|
||||||
|
{{ scope.row.status_name || scope.row.status_text || '-' }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="total_count" label="总数" width="90" />
|
||||||
|
<ElTableColumn prop="success_count" label="成功数" width="90" />
|
||||||
|
<ElTableColumn prop="fail_count" label="失败数" width="90" />
|
||||||
|
<ElTableColumn prop="skip_count" label="跳过数" width="90" />
|
||||||
|
<ElTableColumn label="创建时间" width="180">
|
||||||
|
<template #default="scope">{{ formatDateTime(scope.row.created_at) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="完成时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.completed_at ? formatDateTime(scope.row.completed_at) : '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<ElPagination
|
||||||
|
v-model:current-page="taskListPage"
|
||||||
|
v-model:page-size="taskListSize"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:total="taskListTotal"
|
||||||
|
@current-change="loadTaskList"
|
||||||
|
@size-change="handleTaskListSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</ArtTableFullScreen>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
ElAlert,
|
||||||
|
ElButton,
|
||||||
|
ElCard,
|
||||||
|
ElDrawer,
|
||||||
|
ElForm,
|
||||||
|
ElMessage,
|
||||||
|
ElOption,
|
||||||
|
ElPagination,
|
||||||
|
ElSelect,
|
||||||
|
ElTable,
|
||||||
|
ElTableColumn,
|
||||||
|
ElTag
|
||||||
|
} from 'element-plus'
|
||||||
|
import type { FormInstance } from 'element-plus'
|
||||||
|
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||||
|
import { DeviceService, PackageSeriesService, ShopService } from '@/api/modules'
|
||||||
|
import { DeviceImportTaskStatus } from '@/types/api/device'
|
||||||
|
import type {
|
||||||
|
DeviceBatchAllocationRequest,
|
||||||
|
DeviceImportTask,
|
||||||
|
DeviceImportTaskOperationType
|
||||||
|
} from '@/types/api/device'
|
||||||
|
import type { PackageSeriesResponse, ShopResponse } from '@/types/api'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||||
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
|
|
||||||
|
defineOptions({ name: 'DeviceBatchAllocation' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||||
|
const createDrawerVisible = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const fileUploading = ref(false)
|
||||||
|
const fileKeys = ref<string[]>([])
|
||||||
|
const taskList = ref<DeviceImportTask[]>([])
|
||||||
|
const taskListLoading = ref(false)
|
||||||
|
const taskListPage = ref(1)
|
||||||
|
const taskListSize = ref(20)
|
||||||
|
const taskListTotal = ref(0)
|
||||||
|
const taskListStatus = ref<DeviceImportTaskStatus | undefined>()
|
||||||
|
const taskListOperationType = ref<Exclude<DeviceImportTaskOperationType, 'import'> | undefined>()
|
||||||
|
const shopOptions = ref<ShopResponse[]>([])
|
||||||
|
const seriesOptions = ref<PackageSeriesResponse[]>([])
|
||||||
|
const targetLoading = ref(false)
|
||||||
|
const form = reactive<{
|
||||||
|
operation_type: Exclude<DeviceImportTaskOperationType, 'import'>
|
||||||
|
target_id?: number
|
||||||
|
}>({
|
||||||
|
operation_type: 'assign_shop',
|
||||||
|
target_id: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const requiresTargetId = computed(() =>
|
||||||
|
['assign_shop', 'assign_series'].includes(form.operation_type)
|
||||||
|
)
|
||||||
|
const targetPlaceholder = computed(() =>
|
||||||
|
form.operation_type === 'assign_shop' ? '请选择启用的目标店铺' : '请选择启用的套餐系列'
|
||||||
|
)
|
||||||
|
const createDrawerBusy = computed(() => submitting.value || fileUploading.value)
|
||||||
|
|
||||||
|
const getOperationLabel = (operationType: DeviceImportTaskOperationType) => {
|
||||||
|
switch (operationType) {
|
||||||
|
case 'assign_shop':
|
||||||
|
return '分配目标代理'
|
||||||
|
case 'assign_series':
|
||||||
|
return '设置套餐系列'
|
||||||
|
case 'recall':
|
||||||
|
return '批量回收设备'
|
||||||
|
default:
|
||||||
|
return '设备导入'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOperationTagType = (operationType: DeviceImportTaskOperationType) =>
|
||||||
|
operationType === 'recall' ? 'warning' : 'primary'
|
||||||
|
|
||||||
|
const getStatusTagType = (status: DeviceImportTaskStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case DeviceImportTaskStatus.PENDING:
|
||||||
|
return 'info'
|
||||||
|
case DeviceImportTaskStatus.PROCESSING:
|
||||||
|
return 'warning'
|
||||||
|
case DeviceImportTaskStatus.COMPLETED:
|
||||||
|
return 'success'
|
||||||
|
case DeviceImportTaskStatus.FAILED:
|
||||||
|
return 'danger'
|
||||||
|
default:
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateDrawer = () => {
|
||||||
|
if (!hasAuth(JULY_PERMISSIONS.deviceAllocation.create)) return
|
||||||
|
createDrawerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTargetOptions = async (query = '') => {
|
||||||
|
if (form.operation_type === 'recall') return
|
||||||
|
targetLoading.value = true
|
||||||
|
try {
|
||||||
|
if (form.operation_type === 'assign_shop') {
|
||||||
|
const res = await ShopService.getShops({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
status: 1,
|
||||||
|
shop_name: query || undefined
|
||||||
|
})
|
||||||
|
if (res.code === 0) shopOptions.value = res.data.items || []
|
||||||
|
} else {
|
||||||
|
const res = await PackageSeriesService.getPackageSeries({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
status: 1,
|
||||||
|
series_name: query || undefined
|
||||||
|
})
|
||||||
|
if (res.code === 0) seriesOptions.value = res.data.items || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取批量分配目标失败:', error)
|
||||||
|
ElMessage.error(normalizeApiError(error).message)
|
||||||
|
} finally {
|
||||||
|
targetLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTargetSearch = (query: string) => {
|
||||||
|
void loadTargetOptions(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTargetVisibleChange = (visible: boolean) => {
|
||||||
|
if (
|
||||||
|
visible &&
|
||||||
|
(form.operation_type === 'assign_shop'
|
||||||
|
? !shopOptions.value.length
|
||||||
|
: !seriesOptions.value.length)
|
||||||
|
) {
|
||||||
|
void loadTargetOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOperationTypeChange = () => {
|
||||||
|
form.target_id = undefined
|
||||||
|
shopOptions.value = []
|
||||||
|
seriesOptions.value = []
|
||||||
|
if (form.operation_type !== 'recall') void loadTargetOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateDrawerClosed = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
form.operation_type = 'assign_shop'
|
||||||
|
form.target_id = undefined
|
||||||
|
shopOptions.value = []
|
||||||
|
seriesOptions.value = []
|
||||||
|
fileKeys.value = []
|
||||||
|
fileUploading.value = false
|
||||||
|
uploadRef.value?.clearFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTaskListFilterChange = () => {
|
||||||
|
taskListPage.value = 1
|
||||||
|
void loadTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTaskList = async () => {
|
||||||
|
taskListLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await DeviceService.getImportTasks({
|
||||||
|
page: taskListPage.value,
|
||||||
|
page_size: taskListSize.value,
|
||||||
|
status: taskListStatus.value,
|
||||||
|
operation_type: taskListOperationType.value
|
||||||
|
})
|
||||||
|
if (res.code !== 0) {
|
||||||
|
ElMessage.error(res.msg || '获取设备批量分配任务失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskList.value = (res.data.items || []).filter((item) => item.operation_type !== 'import')
|
||||||
|
taskListTotal.value = res.data.total || 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取设备批量分配任务失败:', error)
|
||||||
|
ElMessage.error(normalizeApiError(error).message)
|
||||||
|
} finally {
|
||||||
|
taskListLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTaskListSizeChange = () => {
|
||||||
|
taskListPage.value = 1
|
||||||
|
void loadTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const showTask = (row: DeviceImportTask) => {
|
||||||
|
router.push({
|
||||||
|
path: RoutesAlias.TaskDetail,
|
||||||
|
query: { id: row.id, task_type: 'device' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitTask = async () => {
|
||||||
|
if (createDrawerBusy.value) return
|
||||||
|
if (!fileKeys.value[0]) {
|
||||||
|
ElMessage.warning('请先上传设备标识文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (requiresTargetId.value && !form.target_id) {
|
||||||
|
ElMessage.warning('请选择目标店铺或套餐系列')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const request: DeviceBatchAllocationRequest = {
|
||||||
|
file_key: fileKeys.value[0],
|
||||||
|
operation_type: form.operation_type
|
||||||
|
}
|
||||||
|
if (requiresTargetId.value) request.target_id = form.target_id
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const res = await DeviceService.createAllocationTask(request)
|
||||||
|
if (res.code !== 0) {
|
||||||
|
ElMessage.error(res.msg || '创建设备批量分配任务失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
createDrawerVisible.value = false
|
||||||
|
await loadTaskList()
|
||||||
|
ElMessage.success(`任务已创建:${res.data.task_no}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建设备批量分配任务失败:', error)
|
||||||
|
ElMessage.error(normalizeApiError(error).message)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadTaskList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.device-batch-allocation-page {
|
||||||
|
.task-list-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.task-list-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list-toolbar {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<ArtTableFullScreen>
|
<ArtTableFullScreen>
|
||||||
<div v-if="isPlatformAccount" class="device-task-page" id="table-full-screen">
|
<div v-if="isPlatformAccount" class="device-task-page" id="table-full-screen">
|
||||||
<!-- 搜索栏 -->
|
|
||||||
<ArtSearchBar
|
<ArtSearchBar
|
||||||
v-model:filter="searchForm"
|
v-model:filter="searchForm"
|
||||||
:items="searchFormItems"
|
:items="searchFormItems"
|
||||||
:show-expand="false"
|
:show-expand="false"
|
||||||
@reset="handleReset"
|
@reset="handleReset"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
></ArtSearchBar>
|
/>
|
||||||
|
|
||||||
<div v-if="pollingError || pollingForbidden" class="task-polling-error">
|
<div v-if="pollingError || pollingForbidden" class="task-polling-error">
|
||||||
<ElAlert
|
<ElAlert
|
||||||
@@ -21,26 +20,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ElCard shadow="never" class="art-table-card">
|
<ElCard shadow="never" class="art-table-card">
|
||||||
<!-- 表格头部 -->
|
|
||||||
<ArtTableHeader
|
<ArtTableHeader
|
||||||
:columnList="columnOptions"
|
:columnList="columnOptions"
|
||||||
v-model:columns="columnChecks"
|
v-model:columns="columnChecks"
|
||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElButton
|
<ElButton type="primary" :icon="Upload" @click="importDialogVisible = true">
|
||||||
v-if="isPlatformAccount && hasAuth(JULY_PERMISSIONS.deviceAllocation.page)"
|
|
||||||
type="primary"
|
|
||||||
:icon="Upload"
|
|
||||||
@click="importDialogVisible = true"
|
|
||||||
v-permission="JULY_PERMISSIONS.deviceAllocation.page"
|
|
||||||
>
|
|
||||||
批量导入设备
|
批量导入设备
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
<!-- 表格 -->
|
|
||||||
<ArtTable
|
<ArtTable
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@@ -62,21 +53,20 @@
|
|||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 导入对话框 -->
|
|
||||||
<ElDialog v-model="importDialogVisible" title="批量导入设备" width="40%" align-center>
|
<ElDialog v-model="importDialogVisible" title="批量导入设备" width="40%" align-center>
|
||||||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div style="line-height: 1.8">
|
<div style="line-height: 1.8">
|
||||||
<p><strong>导入说明:</strong></p>
|
<p><strong>导入说明:</strong></p>
|
||||||
<p>1. 设备分配使用单列 UTF-8 CSV,导入设备仍使用 Excel 模板</p>
|
<p>1. 设备导入使用 Excel 模板,文件大小不能超过 300MB。</p>
|
||||||
<p>2. CSV 单次最多 1000 条,文件不超过 10MB</p>
|
<p>2. 列格式请设置为文本格式,避免长数字被转为科学计数法。</p>
|
||||||
<p>3. 列格式请设置为文本格式,避免长数字被转为科学计数法</p>
|
<p>3. <strong>列顺序固定,不可调整。</strong>系统按位置读取,不识别列名。</p>
|
||||||
<p>4. <strong>重要:列顺序固定,不可调整。</strong>系统按位置读取,不识别列名</p>
|
<p style="color: var(--el-color-primary)">
|
||||||
<p style="color: var(--el-color-primary)">5. 必填列:虚拟号(第1列)</p>
|
4. 必填列:设备标识(第 1 列,支持 VirtualNo、IMEI 或 SN)。
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
6.
|
5. 可选列:SN、设备名称、设备型号、设备类型、IMEI、制造商、最大 SIM 槽数、卡 1~卡 4
|
||||||
可选列:SN、设备名称、设备型号、设备类型、IMEI、制造商、最大SIM槽数(默认4,有效范围1-4)、卡1~卡4
|
ICCID。
|
||||||
ICCID
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,23 +78,7 @@
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ElRow :gutter="20">
|
<ElRow :gutter="20">
|
||||||
<ElCol :span="12">
|
|
||||||
<ElFormItem label="任务类型">
|
|
||||||
<ElSelect v-model="importForm.operation_type" style="width: 100%">
|
|
||||||
<ElOption label="导入设备" value="import" />
|
|
||||||
<ElOption label="分配目标代理" value="assign_shop" />
|
|
||||||
<ElOption label="设置套餐系列" value="assign_series" />
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol v-if="importForm.operation_type !== 'import'" :span="12">
|
|
||||||
<ElFormItem label="目标 ID">
|
|
||||||
<ElInput v-model="importForm.target_id" placeholder="代理店铺或套餐系列 ID" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
</ElRow>
|
|
||||||
<ElRow :gutter="20">
|
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="批次号" prop="batch_no">
|
<ElFormItem label="批次号" prop="batch_no">
|
||||||
<ElInput
|
<ElInput
|
||||||
@@ -135,18 +109,18 @@
|
|||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
|
|
||||||
<ElUpload
|
<ElUpload
|
||||||
ref="uploadRef"
|
ref="uploadRef"
|
||||||
drag
|
drag
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:on-change="handleFileChange"
|
:on-change="handleFileChange"
|
||||||
:limit="1"
|
:limit="1"
|
||||||
accept=".xlsx,.csv"
|
accept=".xlsx"
|
||||||
>
|
>
|
||||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
||||||
<template #tip>
|
<template #tip>
|
||||||
<div class="el-upload__tip">导入设备支持 .xlsx;批量分配支持单列 .csv</div>
|
<div class="el-upload__tip">设备导入仅支持 .xlsx 文件</div>
|
||||||
</template>
|
</template>
|
||||||
</ElUpload>
|
</ElUpload>
|
||||||
|
|
||||||
@@ -166,12 +140,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h, onMounted, reactive, ref, watch, computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
|
||||||
import { DeviceService } from '@/api/modules'
|
import { DeviceService } from '@/api/modules'
|
||||||
import { ElMessage, ElTag } from 'element-plus'
|
import { ElMessage, ElTag } from 'element-plus'
|
||||||
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
import { Download, Upload, UploadFilled } from '@element-plus/icons-vue'
|
||||||
import type { UploadInstance } from 'element-plus'
|
import type { UploadInstance } from 'element-plus'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
@@ -189,7 +162,6 @@
|
|||||||
} from '@/types/api/device'
|
} from '@/types/api/device'
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
import { generatePackageCode } from '@/utils/codeGenerator'
|
import { generatePackageCode } from '@/utils/codeGenerator'
|
||||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
|
||||||
import { normalizeApiError } from '@/utils/business/apiError'
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceTask' })
|
defineOptions({ name: 'DeviceTask' })
|
||||||
@@ -208,40 +180,23 @@
|
|||||||
const importDialogVisible = ref(false)
|
const importDialogVisible = ref(false)
|
||||||
const importForm = reactive({
|
const importForm = reactive({
|
||||||
batch_no: '',
|
batch_no: '',
|
||||||
realname_policy: '' as '' | 'none' | 'before_order' | 'after_order',
|
realname_policy: '' as '' | RealnamePolicy
|
||||||
operation_type: 'import' as 'import' | 'assign_shop' | 'assign_series',
|
|
||||||
target_id: ''
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 搜索表单初始值
|
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
status: undefined,
|
status: undefined as DeviceImportTaskStatus | undefined,
|
||||||
batch_no: '',
|
batch_no: '',
|
||||||
dateRange: undefined as string[] | undefined,
|
dateRange: undefined as string[] | undefined
|
||||||
start_time: '',
|
|
||||||
end_time: ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索表单
|
|
||||||
const searchForm = reactive({ ...initialSearchState })
|
const searchForm = reactive({ ...initialSearchState })
|
||||||
|
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||||
|
|
||||||
// 分页
|
|
||||||
const pagination = reactive({
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
total: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 搜索表单配置
|
|
||||||
const searchFormItems: SearchFormItem[] = [
|
const searchFormItems: SearchFormItem[] = [
|
||||||
{
|
{
|
||||||
label: '任务状态',
|
label: '任务状态',
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
config: {
|
config: { clearable: true, placeholder: '全部' },
|
||||||
clearable: true,
|
|
||||||
placeholder: '全部'
|
|
||||||
},
|
|
||||||
options: () => [
|
options: () => [
|
||||||
{ label: '待处理', value: 1 },
|
{ label: '待处理', value: 1 },
|
||||||
{ label: '处理中', value: 2 },
|
{ label: '处理中', value: 2 },
|
||||||
@@ -253,10 +208,7 @@
|
|||||||
label: '批次号',
|
label: '批次号',
|
||||||
prop: 'batch_no',
|
prop: 'batch_no',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
config: {
|
config: { clearable: true, placeholder: '请输入批次号' }
|
||||||
clearable: true,
|
|
||||||
placeholder: '请输入批次号'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
@@ -271,7 +223,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 列配置
|
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: '任务编号', prop: 'task_no' },
|
{ label: '任务编号', prop: 'task_no' },
|
||||||
{ label: '任务状态', prop: 'status' },
|
{ label: '任务状态', prop: 'status' },
|
||||||
@@ -288,7 +239,6 @@
|
|||||||
]
|
]
|
||||||
|
|
||||||
const taskList = ref<DeviceImportTask[]>([])
|
const taskList = ref<DeviceImportTask[]>([])
|
||||||
|
|
||||||
const polling = useAsyncTaskPolling<DeviceImportTaskDetail>({
|
const polling = useAsyncTaskPolling<DeviceImportTaskDetail>({
|
||||||
storageKey: 'device-import-active-task',
|
storageKey: 'device-import-active-task',
|
||||||
autoRestore: false,
|
autoRestore: false,
|
||||||
@@ -306,7 +256,6 @@
|
|||||||
const pollingError = polling.error
|
const pollingError = polling.error
|
||||||
const pollingForbidden = polling.forbidden
|
const pollingForbidden = polling.forbidden
|
||||||
|
|
||||||
// 获取状态标签类型
|
|
||||||
const getStatusType = (status: DeviceImportTaskStatus) => {
|
const getStatusType = (status: DeviceImportTaskStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -322,19 +271,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看详情
|
|
||||||
const viewDetail = (row: DeviceImportTask) => {
|
const viewDetail = (row: DeviceImportTask) => {
|
||||||
if (!isPlatformAccount.value) return
|
if (!isPlatformAccount.value) return
|
||||||
router.push({
|
router.push({
|
||||||
path: RoutesAlias.TaskDetail,
|
path: RoutesAlias.TaskDetail,
|
||||||
query: {
|
query: { id: row.id, task_type: 'device' }
|
||||||
id: row.id,
|
|
||||||
task_type: 'device'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理名称点击
|
|
||||||
const handleNameClick = (row: DeviceImportTask) => {
|
const handleNameClick = (row: DeviceImportTask) => {
|
||||||
if (isPlatformAccount.value && hasAuth('device_task:view_detail')) {
|
if (isPlatformAccount.value && hasAuth('device_task:view_detail')) {
|
||||||
viewDetail(row)
|
viewDetail(row)
|
||||||
@@ -343,15 +287,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态列配置
|
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
{
|
||||||
prop: 'task_no',
|
prop: 'task_no',
|
||||||
label: '任务编号',
|
label: '任务编号',
|
||||||
width: 220,
|
width: 220,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
formatter: (row: DeviceImportTask) => {
|
formatter: (row: DeviceImportTask) =>
|
||||||
return h(
|
h(
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||||
@@ -362,48 +305,31 @@
|
|||||||
},
|
},
|
||||||
row.task_no
|
row.task_no
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
label: '任务状态',
|
label: '任务状态',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: DeviceImportTask) => {
|
formatter: (row: DeviceImportTask) =>
|
||||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
|
h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || row.status_text)
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'total_count',
|
|
||||||
label: '总数',
|
|
||||||
width: 80
|
|
||||||
},
|
},
|
||||||
|
{ prop: 'total_count', label: '总数', width: 80 },
|
||||||
{
|
{
|
||||||
prop: 'success_count',
|
prop: 'success_count',
|
||||||
label: '成功数',
|
label: '成功数',
|
||||||
width: 80,
|
width: 80,
|
||||||
formatter: (row: DeviceImportTask) => {
|
formatter: (row: DeviceImportTask) =>
|
||||||
return h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'fail_count',
|
prop: 'fail_count',
|
||||||
label: '失败数',
|
label: '失败数',
|
||||||
width: 80,
|
width: 80,
|
||||||
formatter: (row: DeviceImportTask) => {
|
formatter: (row: DeviceImportTask) =>
|
||||||
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'skip_count',
|
|
||||||
label: '跳过数',
|
|
||||||
width: 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'batch_no',
|
|
||||||
label: '批次号',
|
|
||||||
width: 180,
|
|
||||||
showOverflowTooltip: true
|
|
||||||
},
|
},
|
||||||
|
{ prop: 'skip_count', label: '跳过数', width: 80 },
|
||||||
|
{ prop: 'batch_no', label: '批次号', width: 180, showOverflowTooltip: true },
|
||||||
{
|
{
|
||||||
prop: 'started_at',
|
prop: 'started_at',
|
||||||
label: '开始时间',
|
label: '开始时间',
|
||||||
@@ -424,12 +350,7 @@
|
|||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
formatter: (row: DeviceImportTask) => row.error_message || '-'
|
formatter: (row: DeviceImportTask) => row.error_message || '-'
|
||||||
},
|
},
|
||||||
{
|
{ prop: 'file_name', label: '文件名', minWidth: 180, showOverflowTooltip: true },
|
||||||
prop: 'file_name',
|
|
||||||
label: '文件名',
|
|
||||||
minWidth: 180,
|
|
||||||
showOverflowTooltip: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'creator_name',
|
prop: 'creator_name',
|
||||||
label: '操作人',
|
label: '操作人',
|
||||||
@@ -460,7 +381,6 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取设备任务列表
|
|
||||||
const getTableData = async () => {
|
const getTableData = async () => {
|
||||||
if (!isPlatformAccount.value) {
|
if (!isPlatformAccount.value) {
|
||||||
taskList.value = []
|
taskList.value = []
|
||||||
@@ -473,10 +393,9 @@
|
|||||||
page: pagination.page,
|
page: pagination.page,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
status: searchForm.status,
|
status: searchForm.status,
|
||||||
|
operation_type: 'import',
|
||||||
batch_no: searchForm.batch_no || undefined
|
batch_no: searchForm.batch_no || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理时间范围
|
|
||||||
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
|
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
|
||||||
params.start_time = searchForm.dateRange[0]
|
params.start_time = searchForm.dateRange[0]
|
||||||
params.end_time = searchForm.dateRange[1]
|
params.end_time = searchForm.dateRange[1]
|
||||||
@@ -484,9 +403,7 @@
|
|||||||
|
|
||||||
const res = await DeviceService.getImportTasks(params)
|
const res = await DeviceService.getImportTasks(params)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
const taskItems = (res.data as typeof res.data & { items?: DeviceImportTask[] | null })
|
taskList.value = res.data.items || []
|
||||||
.items
|
|
||||||
taskList.value = taskItems || []
|
|
||||||
pagination.total = res.data.total || 0
|
pagination.total = res.data.total || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -497,37 +414,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置搜索
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
Object.assign(searchForm, { ...initialSearchState })
|
Object.assign(searchForm, { ...initialSearchState })
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
getTableData()
|
void getTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
getTableData()
|
void getTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新表格
|
const handleRefresh = () => void getTableData()
|
||||||
const handleRefresh = () => {
|
|
||||||
getTableData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理表格分页变化
|
|
||||||
const handleSizeChange = (newPageSize: number) => {
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
pagination.pageSize = newPageSize
|
pagination.pageSize = newPageSize
|
||||||
getTableData()
|
void getTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCurrentChange = (newCurrentPage: number) => {
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
pagination.page = newCurrentPage
|
pagination.page = newCurrentPage
|
||||||
getTableData()
|
void getTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载模板
|
const downloadTemplate = () => {
|
||||||
const downloadTemplate = async () => {
|
|
||||||
try {
|
try {
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = new URL('@/template/设备导入模板.xlsx', import.meta.url).href
|
link.href = new URL('@/template/设备导入模板.xlsx', import.meta.url).href
|
||||||
@@ -542,107 +452,75 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件选择变化
|
const handleFileChange = (uploadFile: any) => {
|
||||||
const handleFileChange = async (uploadFile: any) => {
|
const file = uploadFile.raw as File | undefined
|
||||||
const isCsvAllocation = importForm.operation_type !== 'import'
|
if (!file) return
|
||||||
const maxSize = (isCsvAllocation ? 10 : 300) * 1024 * 1024
|
if (file.size > 300 * 1024 * 1024) {
|
||||||
if (uploadFile.raw && uploadFile.raw.size > maxSize) {
|
|
||||||
ElMessage.error('文件大小不能超过 300MB')
|
ElMessage.error('文件大小不能超过 300MB')
|
||||||
uploadRef.value?.clearFiles()
|
uploadRef.value?.clearFiles()
|
||||||
fileList.value = []
|
fileList.value = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!file.name.toLowerCase().endsWith('.xlsx')) {
|
||||||
if (uploadFile.raw && (isCsvAllocation ? !uploadFile.raw.name.endsWith('.csv') : !uploadFile.raw.name.endsWith('.xlsx'))) {
|
ElMessage.error('设备导入只能上传 .xlsx 文件')
|
||||||
ElMessage.error(isCsvAllocation ? '批量分配只能上传 .csv 文件' : '设备导入只能上传 .xlsx 文件')
|
|
||||||
uploadRef.value?.clearFiles()
|
uploadRef.value?.clearFiles()
|
||||||
fileList.value = []
|
fileList.value = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fileList.value = [file]
|
||||||
if (isCsvAllocation && uploadFile.raw) {
|
|
||||||
const rows = (await uploadFile.raw.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter(Boolean)
|
|
||||||
if (rows.length > 1001 || rows.some((row: string) => row.includes(','))) {
|
|
||||||
ElMessage.error('设备分配 CSV 必须是单列且最多 1000 行数据')
|
|
||||||
uploadRef.value?.clearFiles()
|
|
||||||
fileList.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileList.value = uploadFile.raw ? [uploadFile.raw] : []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空文件
|
|
||||||
const clearFiles = () => {
|
const clearFiles = () => {
|
||||||
uploadRef.value?.clearFiles()
|
uploadRef.value?.clearFiles()
|
||||||
fileList.value = []
|
fileList.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成批次号
|
|
||||||
const handleGenerateBatchNo = () => {
|
const handleGenerateBatchNo = () => {
|
||||||
const code = generatePackageCode().replace('PKG', 'DEV')
|
importForm.batch_no = generatePackageCode().replace('PKG', 'DEV')
|
||||||
importForm.batch_no = code
|
|
||||||
ElMessage.success('批次号生成成功')
|
ElMessage.success('批次号生成成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消导入
|
|
||||||
const handleCancelImport = () => {
|
const handleCancelImport = () => {
|
||||||
clearFiles()
|
clearFiles()
|
||||||
importForm.batch_no = ''
|
importForm.batch_no = ''
|
||||||
importForm.realname_policy = ''
|
importForm.realname_policy = ''
|
||||||
importForm.operation_type = 'import'
|
|
||||||
importForm.target_id = ''
|
|
||||||
importDialogVisible.value = false
|
importDialogVisible.value = false
|
||||||
}
|
}
|
||||||
// 提交上传
|
|
||||||
const submitUpload = async () => {
|
const submitUpload = async () => {
|
||||||
if (!isPlatformAccount.value || !hasAuth(JULY_PERMISSIONS.deviceAllocation.page)) return
|
if (!isPlatformAccount.value || !fileList.value.length) {
|
||||||
if (!fileList.value.length) {
|
ElMessage.warning('请先选择文件')
|
||||||
ElMessage.warning('请先选择文件')
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
if (importForm.operation_type !== 'import' && !Number(importForm.target_id)) {
|
|
||||||
ElMessage.warning('请输入有效的目标 ID')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = fileList.value[0]
|
const file = fileList.value[0]
|
||||||
uploading.value = true
|
uploading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ElMessage.info('正在准备上传...')
|
ElMessage.info('正在准备上传...')
|
||||||
const isAllocation = importForm.operation_type !== 'import'
|
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||||
const contentType = isAllocation ? 'text/csv' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
file_name: file.name,
|
||||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
file_name: file.name,
|
purpose: 'iot_import'
|
||||||
content_type: contentType,
|
|
||||||
purpose: isAllocation ? 'device_batch_allocation' : 'iot_import'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (uploadUrlRes.code !== 0) {
|
if (uploadUrlRes.code !== 0) {
|
||||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { upload_url, file_key } = uploadUrlRes.data
|
const { upload_url, file_key } = uploadUrlRes.data
|
||||||
|
|
||||||
ElMessage.info('正在上传文件...')
|
ElMessage.info('正在上传文件...')
|
||||||
await StorageService.uploadFile(upload_url, file, contentType)
|
await StorageService.uploadFile(
|
||||||
|
upload_url,
|
||||||
ElMessage.info(isAllocation ? '正在创建分配任务...' : '正在创建导入任务...')
|
file,
|
||||||
const importRes = isAllocation
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
? await DeviceService.createAllocationTask({
|
)
|
||||||
file_key,
|
|
||||||
operation_type: importForm.operation_type as 'assign_shop' | 'assign_series',
|
|
||||||
target_id: Number(importForm.target_id)
|
|
||||||
})
|
|
||||||
: await DeviceService.importDevices({
|
|
||||||
file_key,
|
|
||||||
batch_no: importForm.batch_no || undefined,
|
|
||||||
realname_policy: (importForm.realname_policy || undefined) as RealnamePolicy | undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
|
ElMessage.info('正在创建设备导入任务...')
|
||||||
|
const importRes = await DeviceService.importDevices({
|
||||||
|
file_key,
|
||||||
|
batch_no: importForm.batch_no || undefined,
|
||||||
|
realname_policy: (importForm.realname_policy || undefined) as RealnamePolicy | undefined
|
||||||
|
})
|
||||||
if (importRes.code !== 0) {
|
if (importRes.code !== 0) {
|
||||||
ElMessage.error(importRes.msg || '创建导入任务失败')
|
ElMessage.error(importRes.msg || '创建导入任务失败')
|
||||||
return
|
return
|
||||||
@@ -650,12 +528,10 @@
|
|||||||
|
|
||||||
const taskNo = importRes.data.task_no
|
const taskNo = importRes.data.task_no
|
||||||
const taskId = importRes.data.task_id
|
const taskId = importRes.data.task_id
|
||||||
|
|
||||||
handleCancelImport()
|
handleCancelImport()
|
||||||
await router.replace({ path: route.path, query: { task_id: String(taskId) } })
|
await router.replace({ path: route.path, query: { task_id: String(taskId) } })
|
||||||
await polling.start(taskId)
|
await polling.start(taskId)
|
||||||
await getTableData()
|
await getTableData()
|
||||||
|
|
||||||
ElMessage.success({
|
ElMessage.success({
|
||||||
message: `导入任务已创建!任务编号:${taskNo}`,
|
message: `导入任务已创建!任务编号:${taskNo}`,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
@@ -669,62 +545,51 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从行数据下载失败数据
|
|
||||||
const downloadFailDataByRow = async (row: DeviceImportTask) => {
|
const downloadFailDataByRow = async (row: DeviceImportTask) => {
|
||||||
if (!isPlatformAccount.value) return
|
if (!isPlatformAccount.value) return
|
||||||
try {
|
try {
|
||||||
const res = await DeviceService.getImportTaskDetail(row.id)
|
const res = await DeviceService.getImportTaskDetail(row.id)
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code !== 0 || !res.data) return
|
||||||
const detail = res.data
|
const failReasons =
|
||||||
const failReasons =
|
res.data.failed_items?.map((item) => ({
|
||||||
detail.failed_items?.map((item: any) => ({
|
line: item.line || '-',
|
||||||
line: item.line || '-',
|
deviceNo: item.device_identifier || item.virtual_no || '-',
|
||||||
deviceNo: item.virtual_no || '-',
|
message: item.reason || '未知错误'
|
||||||
message: item.reason || '未知错误'
|
})) || []
|
||||||
})) || []
|
if (failReasons.length === 0) {
|
||||||
|
ElMessage.warning('没有失败数据可下载')
|
||||||
if (failReasons.length === 0) {
|
return
|
||||||
ElMessage.warning('没有失败数据可下载')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = ['行号', '设备编号', '失败原因']
|
|
||||||
const csvRows = [
|
|
||||||
headers.join(','),
|
|
||||||
...failReasons.map((item: any) =>
|
|
||||||
[item.line, `\t${item.deviceNo}`, `"${item.message}"`].join(',')
|
|
||||||
)
|
|
||||||
]
|
|
||||||
const csvContent = csvRows.join('\n')
|
|
||||||
|
|
||||||
const BOM = '\uFEFF'
|
|
||||||
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
|
|
||||||
|
|
||||||
const link = document.createElement('a')
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
link.setAttribute('href', url)
|
|
||||||
link.setAttribute('download', `导入失败数据_${row.batch_no}.csv`)
|
|
||||||
link.style.visibility = 'hidden'
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
ElMessage.success('失败数据下载成功')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const csvRows = [
|
||||||
|
['行号', '设备编号', '失败原因'].join(','),
|
||||||
|
...failReasons.map((item) =>
|
||||||
|
[item.line, `\t${item.deviceNo}`, `"${item.message.replaceAll('"', '""')}"`].join(',')
|
||||||
|
)
|
||||||
|
]
|
||||||
|
const blob = new Blob(['\uFEFF' + csvRows.join('\n')], {
|
||||||
|
type: 'text/csv;charset=utf-8;'
|
||||||
|
})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `导入失败数据_${row.batch_no}.csv`
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
ElMessage.success('失败数据下载成功')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('下载失败数据失败:', error)
|
console.error('下载失败数据失败:', error)
|
||||||
|
ElMessage.error('下载失败数据失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadTaskFile = async (row: DeviceImportTask) => {
|
const downloadTaskFile = async (row: DeviceImportTask) => {
|
||||||
if (!isPlatformAccount.value) return
|
if (!isPlatformAccount.value) return
|
||||||
const fileKey = row.file_name?.trim()
|
const fileKey = row.file_key || row.file_name?.trim()
|
||||||
if (!fileKey) {
|
if (!fileKey) {
|
||||||
ElMessage.warning('当前任务没有可下载的原始文件')
|
ElMessage.warning('当前任务没有可下载的原始文件')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await StorageService.downloadFileByKey(fileKey)
|
await StorageService.downloadFileByKey(fileKey)
|
||||||
ElMessage.success('原始文件下载已开始')
|
ElMessage.success('原始文件下载已开始')
|
||||||
@@ -734,20 +599,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取操作按钮
|
|
||||||
const getActions = (row: DeviceImportTask) => {
|
const getActions = (row: DeviceImportTask) => {
|
||||||
if (!isPlatformAccount.value) return []
|
if (!isPlatformAccount.value) return []
|
||||||
const actions: any[] = []
|
const actions: any[] = []
|
||||||
const showDownloadFileAction = false
|
if (row.file_key && hasAuth('device_task:download_file')) {
|
||||||
|
actions.push({ label: '下载文件', handler: () => downloadTaskFile(row), type: 'primary' })
|
||||||
if (showDownloadFileAction && row.file_name?.trim() && hasAuth('device_task:download_file')) {
|
|
||||||
actions.push({
|
|
||||||
label: '下载文件',
|
|
||||||
handler: () => downloadTaskFile(row),
|
|
||||||
type: 'primary'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.fail_count > 0 && hasAuth('device_task:download_fail_data')) {
|
if (row.fail_count > 0 && hasAuth('device_task:download_fail_data')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: '失败数据',
|
label: '失败数据',
|
||||||
@@ -755,7 +612,6 @@
|
|||||||
type: 'danger'
|
type: 'danger'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<ArtTableFullScreen>
|
<ArtTableFullScreen>
|
||||||
<div class="task-detail-page" id="table-full-screen">
|
<div class="task-detail-page" id="table-full-screen">
|
||||||
<ElCard shadow="never" class="art-table-card">
|
<ElCard shadow="never" class="art-table-card">
|
||||||
<!-- 页面头部 -->
|
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<ElButton @click="goBack">
|
<ElButton @click="goBack">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -10,35 +9,59 @@
|
|||||||
</template>
|
</template>
|
||||||
返回
|
返回
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<h2 class="detail-title">任务详情</h2>
|
<h2 class="detail-title">{{ taskTitle }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 使用 DetailPage 组件显示任务信息 -->
|
|
||||||
<DetailPage v-if="taskDetail" :sections="detailSections" :data="taskDetail" />
|
<DetailPage v-if="taskDetail" :sections="detailSections" :data="taskDetail" />
|
||||||
|
|
||||||
<!-- 失败记录 -->
|
<div v-if="taskDetail?.fail_count && taskDetail.fail_count > 0" class="result-section">
|
||||||
<div class="failure-section" v-if="taskDetail?.fail_count && taskDetail.fail_count > 0">
|
|
||||||
<ElDivider content-position="left">
|
<ElDivider content-position="left">
|
||||||
<span class="section-title">失败记录 ({{ taskDetail.fail_count }})</span>
|
<span class="section-title"
|
||||||
|
>{{ resultAction }}失败记录 ({{ taskDetail.fail_count }})</span
|
||||||
|
>
|
||||||
</ElDivider>
|
</ElDivider>
|
||||||
<ElTable :data="taskDetail.failed_items || []" border style="width: 100%">
|
<ElTable :data="taskDetail.failed_items || []" border style="width: 100%">
|
||||||
<ElTableColumn prop="line" label="行号" width="100" />
|
<ElTableColumn prop="line" label="行号" width="100" />
|
||||||
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
||||||
<ElTableColumn v-else prop="virtual_no" label="设备号" min-width="180" />
|
<ElTableColumn v-else label="设备标识" min-width="180" show-overflow-tooltip>
|
||||||
<ElTableColumn prop="reason" label="失败原因" min-width="300" />
|
<template #default="scope">
|
||||||
|
{{ scope.row.device_identifier || scope.row.virtual_no || '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="reason" :label="`${resultAction}失败原因`" min-width="300" />
|
||||||
</ElTable>
|
</ElTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 跳过记录 -->
|
<div v-if="taskDetail?.skip_count && taskDetail.skip_count > 0" class="result-section">
|
||||||
<div class="skipped-section" v-if="taskDetail?.skip_count && taskDetail.skip_count > 0">
|
|
||||||
<ElDivider content-position="left">
|
<ElDivider content-position="left">
|
||||||
<span class="section-title">跳过记录 ({{ taskDetail.skip_count }})</span>
|
<span class="section-title"
|
||||||
|
>{{ resultAction }}跳过记录 ({{ taskDetail.skip_count }})</span
|
||||||
|
>
|
||||||
</ElDivider>
|
</ElDivider>
|
||||||
<ElTable :data="taskDetail.skipped_items || []" border style="width: 100%">
|
<ElTable :data="taskDetail.skipped_items || []" border style="width: 100%">
|
||||||
<ElTableColumn prop="line" label="行号" width="100" />
|
<ElTableColumn prop="line" label="行号" width="100" />
|
||||||
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
||||||
<ElTableColumn v-else prop="virtual_no" label="设备号" min-width="180" />
|
<ElTableColumn v-else label="设备标识" min-width="180" show-overflow-tooltip>
|
||||||
<ElTableColumn prop="reason" label="跳过原因" min-width="300" />
|
<template #default="scope">
|
||||||
|
{{ scope.row.device_identifier || scope.row.virtual_no || '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="reason" :label="`${resultAction}跳过原因`" min-width="300" />
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="warningCount > 0" class="result-section">
|
||||||
|
<ElDivider content-position="left">
|
||||||
|
<span class="section-title">{{ resultAction }}警告记录 ({{ warningCount }})</span>
|
||||||
|
</ElDivider>
|
||||||
|
<ElTable :data="warningItems" border style="width: 100%">
|
||||||
|
<ElTableColumn prop="line" label="行号" width="100" />
|
||||||
|
<ElTableColumn label="设备标识" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.device_identifier || scope.row.virtual_no || '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="reason" label="警告原因" min-width="300" />
|
||||||
</ElTable>
|
</ElTable>
|
||||||
</div>
|
</div>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
@@ -47,16 +70,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h } from 'vue'
|
import { computed, h, onMounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { CardService, DeviceService } from '@/api/modules'
|
import { CardService, DeviceService } from '@/api/modules'
|
||||||
import { ElDivider, ElIcon, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
|
import { ElDivider, ElIcon, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
|
||||||
import type { TagProps } from 'element-plus'
|
import type { TagProps } from 'element-plus'
|
||||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import type { IotCardImportTaskDetail } from '@/types/api/card'
|
import type { IotCardImportTaskDetail } from '@/types/api/card'
|
||||||
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
import type { DeviceImportTaskDetail, DeviceImportTaskOperationType } from '@/types/api/device'
|
||||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||||
import DetailPage from '@/components/common/DetailPage.vue'
|
import DetailPage from '@/components/common/DetailPage.vue'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
@@ -72,12 +94,53 @@
|
|||||||
type TaskDetail = IotCardImportTaskDetail | DeviceImportTaskDetail
|
type TaskDetail = IotCardImportTaskDetail | DeviceImportTaskDetail
|
||||||
|
|
||||||
const taskDetail = ref<TaskDetail | null>(null)
|
const taskDetail = ref<TaskDetail | null>(null)
|
||||||
const loading = ref(false)
|
|
||||||
const taskType = ref<TaskType>('card')
|
const taskType = ref<TaskType>('card')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const getOperationLabel = (operationType?: DeviceImportTaskOperationType) => {
|
||||||
|
switch (operationType) {
|
||||||
|
case 'assign_shop':
|
||||||
|
return '分配目标代理'
|
||||||
|
case 'assign_series':
|
||||||
|
return '设置套餐系列'
|
||||||
|
case 'recall':
|
||||||
|
return '批量回收设备'
|
||||||
|
case 'import':
|
||||||
|
return '导入设备'
|
||||||
|
default:
|
||||||
|
return '设备任务'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceTask = computed(() =>
|
||||||
|
taskType.value === 'device' ? (taskDetail.value as DeviceImportTaskDetail | null) : null
|
||||||
|
)
|
||||||
|
const taskTitle = computed(() =>
|
||||||
|
taskType.value === 'device'
|
||||||
|
? `${getOperationLabel(deviceTask.value?.operation_type)}任务详情`
|
||||||
|
: 'ICCID导入任务详情'
|
||||||
|
)
|
||||||
|
const resultAction = computed(() => {
|
||||||
|
if (taskType.value !== 'device') return '导入'
|
||||||
|
switch (deviceTask.value?.operation_type) {
|
||||||
|
case 'assign_shop':
|
||||||
|
return '分配'
|
||||||
|
case 'assign_series':
|
||||||
|
return '设置套餐系列'
|
||||||
|
case 'recall':
|
||||||
|
return '回收'
|
||||||
|
default:
|
||||||
|
return '导入'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const warningCount = computed(() =>
|
||||||
|
taskType.value === 'device' ? deviceTask.value?.warning_count || 0 : 0
|
||||||
|
)
|
||||||
|
const warningItems = computed(() =>
|
||||||
|
taskType.value === 'device' ? deviceTask.value?.warning_items || [] : []
|
||||||
|
)
|
||||||
|
|
||||||
// 获取状态标签类型
|
|
||||||
const getStatusType = (status?: number): TagProps['type'] => {
|
const getStatusType = (status?: number): TagProps['type'] => {
|
||||||
if (!status) return 'info'
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 1:
|
case 1:
|
||||||
return 'info'
|
return 'info'
|
||||||
@@ -92,8 +155,54 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 详情页配置
|
|
||||||
const detailSections = computed((): DetailSection[] => {
|
const detailSections = computed((): DetailSection[] => {
|
||||||
|
const deviceFields: DetailSection['fields'] =
|
||||||
|
taskType.value === 'device'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: '操作名称',
|
||||||
|
prop: 'operation_name',
|
||||||
|
formatter: (value: string) =>
|
||||||
|
value || getOperationLabel(deviceTask.value?.operation_type)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '目标 ID',
|
||||||
|
prop: 'target_id',
|
||||||
|
formatter: (value: number | null) => String(value ?? '-')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
|
||||||
|
const cardFields: DetailSection['fields'] =
|
||||||
|
taskType.value === 'card'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: '运营商',
|
||||||
|
prop: 'carrier_name',
|
||||||
|
formatter: (value: string) => value || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '卡业务类型',
|
||||||
|
render: (data: any) => {
|
||||||
|
if (!data.card_category) return h('span', '-')
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{
|
||||||
|
type: data.card_category === 'industry' ? 'warning' : 'success',
|
||||||
|
size: 'small'
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
data.card_category === 'normal'
|
||||||
|
? '普通卡'
|
||||||
|
: data.card_category === 'industry'
|
||||||
|
? '行业卡'
|
||||||
|
: data.card_category
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: '任务基本信息',
|
title: '任务基本信息',
|
||||||
@@ -101,69 +210,44 @@
|
|||||||
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
|
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
|
||||||
{
|
{
|
||||||
label: '任务类型',
|
label: '任务类型',
|
||||||
render: () => {
|
render: () =>
|
||||||
return h(
|
h(
|
||||||
ElTag,
|
ElTag,
|
||||||
{ type: taskType.value === 'device' ? 'warning' : 'primary', size: 'small' },
|
{ type: taskType.value === 'device' ? 'warning' : 'primary', size: 'small' },
|
||||||
() => (taskType.value === 'device' ? '设备导入' : 'ICCID导入')
|
() =>
|
||||||
|
taskType.value === 'device'
|
||||||
|
? getOperationLabel(deviceTask.value?.operation_type)
|
||||||
|
: 'ICCID导入'
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
...deviceFields,
|
||||||
{ label: '批次号', prop: 'batch_no', formatter: (value: string) => value || '-' },
|
{ label: '批次号', prop: 'batch_no', formatter: (value: string) => value || '-' },
|
||||||
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
|
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
|
||||||
{
|
{ label: '操作人', prop: 'creator_name', formatter: (value: string) => value || '-' },
|
||||||
label: '操作人',
|
|
||||||
prop: 'creator_name',
|
|
||||||
formatter: (value: string) => value || '-'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: '任务状态',
|
label: '任务状态',
|
||||||
render: (data: TaskDetail) => {
|
render: (data: TaskDetail) =>
|
||||||
return h(ElTag, { type: getStatusType(data.status) }, () => data.status_text)
|
h(
|
||||||
}
|
ElTag,
|
||||||
|
{ type: getStatusType(data.status) },
|
||||||
|
() => (data as DeviceImportTaskDetail).status_name || data.status_text || '-'
|
||||||
|
)
|
||||||
},
|
},
|
||||||
...(taskType.value === 'card'
|
...cardFields,
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: '运营商',
|
|
||||||
prop: 'carrier_name',
|
|
||||||
formatter: (value: any) => value || '-'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '卡业务类型',
|
|
||||||
render: (data: any) => {
|
|
||||||
if (!data.card_category) return h('span', '-')
|
|
||||||
return h(
|
|
||||||
ElTag,
|
|
||||||
{
|
|
||||||
type: data.card_category === 'industry' ? 'warning' : 'success',
|
|
||||||
size: 'small'
|
|
||||||
},
|
|
||||||
() =>
|
|
||||||
data.card_category === 'normal'
|
|
||||||
? '普通卡'
|
|
||||||
: data.card_category === 'industry'
|
|
||||||
? '行业卡'
|
|
||||||
: data.card_category
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
prop: 'created_at',
|
prop: 'created_at',
|
||||||
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
|
formatter: (value: string) => (value ? formatDateTime(value) : '-')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '开始处理时间',
|
label: '开始处理时间',
|
||||||
prop: 'started_at',
|
prop: 'started_at',
|
||||||
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
|
formatter: (value: string | null) => (value ? formatDateTime(value) : '-')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '完成时间',
|
label: '完成时间',
|
||||||
prop: 'completed_at',
|
prop: 'completed_at',
|
||||||
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
|
formatter: (value: string | null) => (value ? formatDateTime(value) : '-')
|
||||||
},
|
},
|
||||||
...(taskDetail.value?.error_message
|
...(taskDetail.value?.error_message
|
||||||
? [
|
? [
|
||||||
@@ -171,13 +255,12 @@
|
|||||||
label: '错误信息',
|
label: '错误信息',
|
||||||
prop: 'error_message',
|
prop: 'error_message',
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
render: (data: TaskDetail) => {
|
render: (data: TaskDetail) =>
|
||||||
return h(
|
h(
|
||||||
'span',
|
'span',
|
||||||
{ style: { color: 'var(--el-color-danger)' } },
|
{ style: { color: 'var(--el-color-danger)' } },
|
||||||
data.error_message || ''
|
data.error_message || ''
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: [])
|
: [])
|
||||||
@@ -189,64 +272,57 @@
|
|||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: '总数',
|
label: '总数',
|
||||||
render: (data: TaskDetail) => {
|
render: (data: TaskDetail) =>
|
||||||
return h(
|
h(
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
style: {
|
style: { fontSize: '16px', fontWeight: 'bold', color: 'var(--el-color-primary)' }
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: 'var(--el-color-primary)'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
String(data.total_count || 0)
|
String(data.total_count || 0)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '成功数',
|
label: '成功数',
|
||||||
render: (data: TaskDetail) => {
|
render: (data: TaskDetail) =>
|
||||||
return h(ElTag, { type: 'success' }, () => String(data.success_count || 0))
|
h(ElTag, { type: 'success' }, () => String(data.success_count || 0))
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '失败数',
|
label: '失败数',
|
||||||
render: (data: TaskDetail) => {
|
render: (data: TaskDetail) =>
|
||||||
return h(ElTag, { type: 'danger' }, () => String(data.fail_count || 0))
|
h(ElTag, { type: 'danger' }, () => String(data.fail_count || 0))
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '跳过数',
|
label: '跳过数',
|
||||||
render: (data: TaskDetail) => {
|
render: (data: TaskDetail) =>
|
||||||
return h(ElTag, { type: 'warning' }, () => String(data.skip_count || 0))
|
h(ElTag, { type: 'warning' }, () => String(data.skip_count || 0))
|
||||||
}
|
},
|
||||||
}
|
...(taskType.value === 'device'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: '警告数',
|
||||||
|
render: (data: TaskDetail) =>
|
||||||
|
h(ElTag, { type: 'info' }, () =>
|
||||||
|
String((data as DeviceImportTaskDetail).warning_count || 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
// 返回列表
|
const goBack = () => router.back()
|
||||||
const goBack = () => {
|
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取任务详情
|
|
||||||
const getTaskDetail = async () => {
|
const getTaskDetail = async () => {
|
||||||
const taskId = route.query.id
|
const taskId = route.query.id
|
||||||
const queryTaskType = route.query.task_type as TaskType | undefined
|
const queryTaskType = route.query.task_type as TaskType | undefined
|
||||||
|
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
ElMessage.error('缺少任务ID参数')
|
ElMessage.error('缺少任务 ID 参数')
|
||||||
goBack()
|
goBack()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (queryTaskType) taskType.value = queryTaskType
|
||||||
// 设置任务类型
|
|
||||||
if (queryTaskType) {
|
|
||||||
taskType.value = queryTaskType
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskType.value === 'device' && !isPlatformAccount.value) {
|
if (taskType.value === 'device' && !isPlatformAccount.value) {
|
||||||
ElMessage.error('当前账号无权查看设备任务详情')
|
ElMessage.error('当前账号无权查看设备任务详情')
|
||||||
goBack()
|
goBack()
|
||||||
@@ -256,28 +332,22 @@
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
if (taskType.value === 'device') {
|
if (taskType.value === 'device') {
|
||||||
// 获取设备导入任务详情
|
|
||||||
const res = await DeviceService.getImportTaskDetail(Number(taskId))
|
const res = await DeviceService.getImportTaskDetail(Number(taskId))
|
||||||
if (res.code === 0) {
|
if (res.code === 0) taskDetail.value = res.data
|
||||||
taskDetail.value = res.data
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 获取ICCID导入任务详情
|
|
||||||
const res = await CardService.getIotCardImportTaskDetail(Number(taskId))
|
const res = await CardService.getIotCardImportTaskDetail(Number(taskId))
|
||||||
if (res.code === 0) {
|
if (res.code === 0) taskDetail.value = res.data
|
||||||
taskDetail.value = res.data
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error('获取任务详情失败:', error)
|
||||||
console.log('获取任务详情失败')
|
ElMessage.error('获取任务详情失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getTaskDetail()
|
void getTaskDetail()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -285,8 +355,8 @@
|
|||||||
.task-detail-page {
|
.task-detail-page {
|
||||||
.detail-header {
|
.detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
|
|
||||||
.detail-title {
|
.detail-title {
|
||||||
@@ -297,8 +367,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.failure-section,
|
.result-section {
|
||||||
.skipped-section {
|
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
|
|||||||
@@ -269,9 +269,22 @@
|
|||||||
<ElFormItem class="main-wallet-filter__actions">
|
<ElFormItem class="main-wallet-filter__actions">
|
||||||
<ElButton type="primary" @click="handleMainWalletSearch">搜索</ElButton>
|
<ElButton type="primary" @click="handleMainWalletSearch">搜索</ElButton>
|
||||||
<ElButton @click="handleMainWalletReset">重置</ElButton>
|
<ElButton @click="handleMainWalletReset">重置</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="hasAuth('agent_wallet_transaction:export')"
|
||||||
|
@click="mainWalletExportDialogVisible = true"
|
||||||
|
>
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</div>
|
</div>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
<ExportTaskCreateDialog
|
||||||
|
v-model="mainWalletExportDialogVisible"
|
||||||
|
scene="agent_wallet_transaction"
|
||||||
|
:query="mainWalletExportQuery"
|
||||||
|
confirm-permission="agent_wallet_transaction:export"
|
||||||
|
title="导出代理主钱包流水"
|
||||||
|
/>
|
||||||
<ArtTable
|
<ArtTable
|
||||||
ref="mainWalletTableRef"
|
ref="mainWalletTableRef"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@@ -508,6 +521,7 @@
|
|||||||
} from '@/config/constants/commission'
|
} from '@/config/constants/commission'
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
import { normalizeApiError } from '@/utils/business/apiError'
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
|
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'AgentCommission' })
|
defineOptions({ name: 'AgentCommission' })
|
||||||
|
|
||||||
@@ -619,10 +633,10 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value <= 0) {
|
if (value <= 0) {
|
||||||
callback(new Error('实际信用额度必须大于0'))
|
callback(new Error('实际信用额度必须大于0'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
callback()
|
callback()
|
||||||
},
|
},
|
||||||
@@ -649,6 +663,17 @@
|
|||||||
date_range: [],
|
date_range: [],
|
||||||
asset_identifier: ''
|
asset_identifier: ''
|
||||||
})
|
})
|
||||||
|
const mainWalletExportDialogVisible = ref(false)
|
||||||
|
const mainWalletExportQuery = computed(() => {
|
||||||
|
const [startDate, endDate] = mainWalletSearchForm.date_range || []
|
||||||
|
return {
|
||||||
|
shop_id: currentShop.value?.shop_id,
|
||||||
|
transaction_type: mainWalletSearchForm.transaction_type,
|
||||||
|
start_date: startDate || undefined,
|
||||||
|
end_date: endDate || undefined,
|
||||||
|
asset_identifier: mainWalletSearchForm.asset_identifier.trim() || undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 列配置
|
// 列配置
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
@@ -878,10 +903,6 @@
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getTableData()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取代理商资金汇总列表
|
// 获取代理商资金汇总列表
|
||||||
const getTableData = async () => {
|
const getTableData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -905,6 +926,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadInitialData = async () => {
|
||||||
|
await getTableData()
|
||||||
|
const shopId = Number(route.query.shop_id)
|
||||||
|
if (!shopId) return
|
||||||
|
const targetShop = summaryList.value.find((item) => item.shop_id === shopId)
|
||||||
|
if (targetShop) showDetail(targetShop)
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
@@ -953,6 +982,10 @@
|
|||||||
loadCommissionRecords()
|
loadCommissionRecords()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadInitialData()
|
||||||
|
})
|
||||||
|
|
||||||
// 获取操作按钮
|
// 获取操作按钮
|
||||||
const getActions = (row: ShopFundSummaryItem) => {
|
const getActions = (row: ShopFundSummaryItem) => {
|
||||||
const actions: any[] = []
|
const actions: any[] = []
|
||||||
|
|||||||
@@ -23,20 +23,24 @@
|
|||||||
import { onMounted, reactive } from 'vue'
|
import { onMounted, reactive } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { AssetService } from '@/api/modules'
|
import { AssetService } from '@/api/modules'
|
||||||
import type { AssetType } from '@/types/api'
|
import type { AssetType, ExpiringAssetType } from '@/types/api'
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
|
|
||||||
defineOptions({ name: 'ExpiringAssetStats' })
|
defineOptions({ name: 'ExpiringAssetStats' })
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const counts = reactive<Record<AssetType, number>>({ card: 0, device: 0 })
|
const counts = reactive<Record<AssetType, number>>({ card: 0, device: 0 })
|
||||||
|
const apiAssetTypes: Record<AssetType, ExpiringAssetType> = {
|
||||||
|
card: 'iot_card',
|
||||||
|
device: 'device'
|
||||||
|
}
|
||||||
|
|
||||||
const loadCount = async (assetType: AssetType) => {
|
const loadCount = async (assetType: AssetType) => {
|
||||||
try {
|
try {
|
||||||
const response = await AssetService.getExpiringAssets({
|
const response = await AssetService.getExpiringAssets({
|
||||||
asset_type: assetType,
|
asset_type: apiAssetTypes[assetType],
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 1
|
page_size: 1
|
||||||
})
|
})
|
||||||
if (response.code === 0 && response.data) counts[assetType] = response.data.total || 0
|
if (response.code === 0 && response.data) counts[assetType] = response.data.total || 0
|
||||||
} catch {
|
} catch {
|
||||||
@@ -47,7 +51,7 @@
|
|||||||
const goToList = (assetType?: AssetType) => {
|
const goToList = (assetType?: AssetType) => {
|
||||||
router.push({
|
router.push({
|
||||||
path: RoutesAlias.ExpiringAssets,
|
path: RoutesAlias.ExpiringAssets,
|
||||||
query: assetType ? { asset_type: assetType } : {}
|
query: assetType ? { asset_type: apiAssetTypes[assetType] } : {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export const buildAgentRechargeActions = (
|
|||||||
options: BuildAgentRechargeActionsOptions
|
options: BuildAgentRechargeActionsOptions
|
||||||
): AgentRechargeAction[] => {
|
): AgentRechargeAction[] => {
|
||||||
const actions: AgentRechargeAction[] = []
|
const actions: AgentRechargeAction[] = []
|
||||||
|
const hasApprovalRecord =
|
||||||
|
row.approval_provider === 'wecom' ||
|
||||||
|
row.approval_source === 'wecom' ||
|
||||||
|
row.approval_source === 'legacy' ||
|
||||||
|
(row.approval_instance_id !== undefined && row.approval_instance_id !== null)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
row.payment_method === 'offline' &&
|
row.payment_method === 'offline' &&
|
||||||
@@ -33,6 +38,7 @@ export const buildAgentRechargeActions = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!hasApprovalRecord &&
|
||||||
row.status === AgentRechargeStatus.PENDING &&
|
row.status === AgentRechargeStatus.PENDING &&
|
||||||
row.payment_method === 'offline' &&
|
row.payment_method === 'offline' &&
|
||||||
options.hasAuth('agent_recharge:confirm_payment')
|
options.hasAuth('agent_recharge:confirm_payment')
|
||||||
@@ -44,7 +50,11 @@ export const buildAgentRechargeActions = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.status === AgentRechargeStatus.PENDING && options.hasAuth('agent_recharge:reject')) {
|
if (
|
||||||
|
!hasApprovalRecord &&
|
||||||
|
row.status === AgentRechargeStatus.PENDING &&
|
||||||
|
options.hasAuth('agent_recharge:reject')
|
||||||
|
) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: '拒绝',
|
label: '拒绝',
|
||||||
handler: () => options.onReject(row),
|
handler: () => options.onReject(row),
|
||||||
|
|||||||
@@ -68,26 +68,12 @@
|
|||||||
2: 'success', // 已支付
|
2: 'success', // 已支付
|
||||||
3: 'success', // 已完成
|
3: 'success', // 已完成
|
||||||
4: 'info', // 已关闭
|
4: 'info', // 已关闭
|
||||||
5: 'danger', // 已退款
|
5: 'danger', // 已退款
|
||||||
6: 'danger' // 已驳回
|
6: 'danger' // 已驳回
|
||||||
}
|
}
|
||||||
return statusMap[status] || 'info'
|
return statusMap[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取状态文本(优先使用 status_name,否则使用本地映射)
|
|
||||||
const getStatusText = (status: AgentRechargeStatus, statusName?: string): string => {
|
|
||||||
if (statusName) return statusName
|
|
||||||
const statusMap: Record<AgentRechargeStatus, string> = {
|
|
||||||
1: '待支付',
|
|
||||||
2: '已支付',
|
|
||||||
3: '已完成',
|
|
||||||
4: '已关闭',
|
|
||||||
5: '已退款',
|
|
||||||
6: '已驳回'
|
|
||||||
}
|
|
||||||
return statusMap[status] || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取支付方式文本
|
// 获取支付方式文本
|
||||||
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
|
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
|
||||||
const methodMap: Record<AgentRechargePaymentMethod, string> = {
|
const methodMap: Record<AgentRechargePaymentMethod, string> = {
|
||||||
@@ -97,6 +83,12 @@
|
|||||||
return methodMap[method] || method
|
return methodMap[method] || method
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getApprovalProviderText = (data: AgentRecharge) => {
|
||||||
|
if (data.approval_provider === 'wecom' || data.approval_source === 'wecom') return '企微'
|
||||||
|
if (data.approval_source === 'legacy') return '历史审批'
|
||||||
|
return data.approval_provider || data.approval_source || '-'
|
||||||
|
}
|
||||||
|
|
||||||
// 详情配置
|
// 详情配置
|
||||||
const detailSections = computed((): DetailSection[] => [
|
const detailSections = computed((): DetailSection[] => [
|
||||||
{
|
{
|
||||||
@@ -104,6 +96,7 @@
|
|||||||
fields: [
|
fields: [
|
||||||
{ label: '充值单号', prop: 'recharge_no' },
|
{ label: '充值单号', prop: 'recharge_no' },
|
||||||
{ label: '店铺名称', prop: 'shop_name' },
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
|
{ label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' },
|
||||||
{
|
{
|
||||||
label: '充值金额',
|
label: '充值金额',
|
||||||
formatter: (_, data) => formatCurrency(data.amount)
|
formatter: (_, data) => formatCurrency(data.amount)
|
||||||
@@ -111,9 +104,7 @@
|
|||||||
{
|
{
|
||||||
label: '状态',
|
label: '状态',
|
||||||
render: (data) =>
|
render: (data) =>
|
||||||
h(ElTag, { type: getStatusType(data.status) }, () =>
|
h(ElTag, { type: getStatusType(data.status) }, () => data.status_name || '-')
|
||||||
getStatusText(data.status, data.status_name)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '驳回原因',
|
label: '驳回原因',
|
||||||
@@ -123,6 +114,30 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '企微审批信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '审批渠道', formatter: (_, data) => getApprovalProviderText(data) },
|
||||||
|
{
|
||||||
|
label: '审批状态',
|
||||||
|
formatter: (_, data) => data.approval_status_name || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '当前审批人摘要',
|
||||||
|
formatter: (_, data) => data.current_approver_summary || '-',
|
||||||
|
fullWidth: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '业务处理结果',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: '处理状态',
|
||||||
|
formatter: (_, data) => data.processing_status_name || '-'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '支付信息',
|
title: '支付信息',
|
||||||
fields: [
|
fields: [
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
v-if="hasAuth('agent_recharge:create')"
|
v-if="hasAuth('agent_recharge:create')"
|
||||||
>创建充值订单</ElButton
|
>创建充值订单</ElButton
|
||||||
>
|
>
|
||||||
|
<ElButton v-if="hasAuth('agent_recharge:export')" @click="exportDialogVisible = true">
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@
|
|||||||
placeholder="请输入充值金额(元)"
|
placeholder="请输入充值金额(元)"
|
||||||
/>
|
/>
|
||||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||||
最小: ¥0.01,最大: 不限制
|
最小: ¥0.01,最大: ¥1,000,000.00
|
||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="支付方式" prop="payment_method">
|
<ElFormItem label="支付方式" prop="payment_method">
|
||||||
@@ -129,6 +132,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 导出任务对话框 -->
|
||||||
|
<ExportTaskCreateDialog
|
||||||
|
v-model="exportDialogVisible"
|
||||||
|
scene="agent_recharge"
|
||||||
|
:query="exportQuery"
|
||||||
|
confirm-permission="agent_recharge:export"
|
||||||
|
title="导出代理充值"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 确认线下支付对话框 -->
|
<!-- 确认线下支付对话框 -->
|
||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="confirmPayDialogVisible"
|
v-model="confirmPayDialogVisible"
|
||||||
@@ -245,6 +257,7 @@
|
|||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||||
|
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||||
import { buildAgentRechargeActions } from './agentRechargeActions'
|
import { buildAgentRechargeActions } from './agentRechargeActions'
|
||||||
import { formatRejectionReason } from './agentRechargeDisplay'
|
import { formatRejectionReason } from './agentRechargeDisplay'
|
||||||
|
|
||||||
@@ -260,6 +273,7 @@
|
|||||||
const rejectLoading = ref(false)
|
const rejectLoading = ref(false)
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
const createDialogVisible = ref(false)
|
const createDialogVisible = ref(false)
|
||||||
|
const exportDialogVisible = ref(false)
|
||||||
const confirmPayDialogVisible = ref(false)
|
const confirmPayDialogVisible = ref(false)
|
||||||
const rejectDialogVisible = ref(false)
|
const rejectDialogVisible = ref(false)
|
||||||
const currentRecharge = ref<AgentRecharge | null>(null)
|
const currentRecharge = ref<AgentRecharge | null>(null)
|
||||||
@@ -336,8 +350,11 @@
|
|||||||
placeholder: '请选择状态',
|
placeholder: '请选择状态',
|
||||||
options: [
|
options: [
|
||||||
{ label: '待支付', value: 1 },
|
{ label: '待支付', value: 1 },
|
||||||
{ label: '已完成', value: 2 },
|
{ label: '已支付', value: 2 },
|
||||||
{ label: '已取消', value: 3 }
|
{ label: '已完成', value: 3 },
|
||||||
|
{ label: '已关闭', value: 4 },
|
||||||
|
{ label: '已退款', value: 5 },
|
||||||
|
{ label: '已驳回', value: 6 }
|
||||||
],
|
],
|
||||||
config: {
|
config: {
|
||||||
clearable: true
|
clearable: true
|
||||||
@@ -370,16 +387,19 @@
|
|||||||
{ label: '店铺名称', prop: 'shop_name' },
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
{ label: '充值金额', prop: 'amount' },
|
{ label: '充值金额', prop: 'amount' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
|
{ label: '审批渠道', prop: 'approval_provider' },
|
||||||
{ label: '提交人', prop: 'submitter_name' },
|
{ label: '提交人', prop: 'submitter_name' },
|
||||||
{ label: '审批状态', prop: 'approval_status' },
|
{ label: '审批状态', prop: 'approval_status' },
|
||||||
{ label: '当前审批人摘要', prop: 'current_approver_summary' },
|
{ label: '当前审批人摘要', prop: 'current_approver_summary' },
|
||||||
{ label: '业务处理状态', prop: 'processing_status_name' },
|
{ label: '业务处理状态', prop: 'processing_status_name' },
|
||||||
{ label: '支付方式', prop: 'payment_method' },
|
{ label: '支付方式', prop: 'payment_method' },
|
||||||
{ label: '支付通道', prop: 'payment_channel' },
|
{ label: '支付通道', prop: 'payment_channel' },
|
||||||
|
{ label: '运营备注', prop: 'remark' },
|
||||||
{ label: '驳回原因', prop: 'rejection_reason' },
|
{ label: '驳回原因', prop: 'rejection_reason' },
|
||||||
{ label: '创建时间', prop: 'created_at' },
|
{ label: '创建时间', prop: 'created_at' },
|
||||||
{ label: '支付时间', prop: 'paid_at' },
|
{ label: '支付时间', prop: 'paid_at' },
|
||||||
{ label: '完成时间', prop: 'completed_at' }
|
{ label: '完成时间', prop: 'completed_at' },
|
||||||
|
{ label: '更新时间', prop: 'updated_at' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const createFormRef = ref<FormInstance>()
|
const createFormRef = ref<FormInstance>()
|
||||||
@@ -387,6 +407,7 @@
|
|||||||
const rejectFormRef = ref<FormInstance>()
|
const rejectFormRef = ref<FormInstance>()
|
||||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||||
const MIN_RECHARGE_AMOUNT = 0.01
|
const MIN_RECHARGE_AMOUNT = 0.01
|
||||||
|
const MAX_RECHARGE_AMOUNT = 1_000_000
|
||||||
|
|
||||||
const createRules = computed<FormRules>(() => {
|
const createRules = computed<FormRules>(() => {
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
@@ -402,6 +423,10 @@
|
|||||||
callback(new Error(`充值金额最小为 ¥${MIN_RECHARGE_AMOUNT.toFixed(2)}`))
|
callback(new Error(`充值金额最小为 ¥${MIN_RECHARGE_AMOUNT.toFixed(2)}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (Number(value) > MAX_RECHARGE_AMOUNT) {
|
||||||
|
callback(new Error(`充值金额最大为 ¥${MAX_RECHARGE_AMOUNT.toFixed(2)}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
callback()
|
callback()
|
||||||
},
|
},
|
||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
@@ -464,26 +489,12 @@
|
|||||||
2: 'success', // 已支付
|
2: 'success', // 已支付
|
||||||
3: 'success', // 已完成
|
3: 'success', // 已完成
|
||||||
4: 'info', // 已关闭
|
4: 'info', // 已关闭
|
||||||
5: 'danger', // 已退款
|
5: 'danger', // 已退款
|
||||||
6: 'danger' // 已驳回
|
6: 'danger' // 已驳回
|
||||||
}
|
}
|
||||||
return statusMap[status] || 'info'
|
return statusMap[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取状态文本(优先使用 status_name,否则使用本地映射)
|
|
||||||
const getStatusText = (status: AgentRechargeStatus, statusName?: string): string => {
|
|
||||||
if (statusName) return statusName
|
|
||||||
const statusMap: Record<AgentRechargeStatus, string> = {
|
|
||||||
1: '待支付',
|
|
||||||
2: '已支付',
|
|
||||||
3: '已完成',
|
|
||||||
4: '已关闭',
|
|
||||||
5: '已退款',
|
|
||||||
6: '已驳回'
|
|
||||||
}
|
|
||||||
return statusMap[status] || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取支付方式文本
|
// 获取支付方式文本
|
||||||
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
|
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
|
||||||
const methodMap: Record<AgentRechargePaymentMethod, string> = {
|
const methodMap: Record<AgentRechargePaymentMethod, string> = {
|
||||||
@@ -530,9 +541,17 @@
|
|||||||
label: '状态',
|
label: '状态',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: AgentRecharge) => {
|
formatter: (row: AgentRecharge) => {
|
||||||
return h(ElTag, { type: getStatusType(row.status) }, () =>
|
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '-')
|
||||||
getStatusText(row.status, row.status_name)
|
}
|
||||||
)
|
},
|
||||||
|
{
|
||||||
|
prop: 'approval_provider',
|
||||||
|
label: '审批渠道',
|
||||||
|
width: 110,
|
||||||
|
formatter: (row: AgentRecharge) => {
|
||||||
|
if (row.approval_provider === 'wecom' || row.approval_source === 'wecom') return '企微'
|
||||||
|
if (row.approval_source === 'legacy') return '历史审批'
|
||||||
|
return row.approval_provider || row.approval_source || '-'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -605,6 +624,12 @@
|
|||||||
label: '完成时间',
|
label: '完成时间',
|
||||||
width: 180,
|
width: 180,
|
||||||
formatter: (row: AgentRecharge) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
|
formatter: (row: AgentRecharge) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'updated_at',
|
||||||
|
label: '更新时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: (row: AgentRecharge) => formatDateTime(row.updated_at)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -711,6 +736,13 @@
|
|||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportQuery = computed(() => ({
|
||||||
|
shop_id: searchForm.shop_id,
|
||||||
|
status: searchForm.status,
|
||||||
|
start_date: searchForm.start_date || searchForm.dateRange?.[0],
|
||||||
|
end_date: searchForm.end_date || searchForm.dateRange?.[1]
|
||||||
|
}))
|
||||||
|
|
||||||
// 刷新表格
|
// 刷新表格
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
getTableData()
|
getTableData()
|
||||||
|
|||||||
@@ -162,6 +162,34 @@
|
|||||||
return `¥${(amount / 100).toFixed(2)}`
|
return `¥${(amount / 100).toFixed(2)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getApprovalProviderText = (item: Refund) => {
|
||||||
|
const provider = item.approval_provider || item.approval_source || item.approval?.source
|
||||||
|
if (provider === 'wecom') return '企微'
|
||||||
|
if (provider === 'legacy') return '历史审批'
|
||||||
|
if (provider === 'none') return '无'
|
||||||
|
return provider || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRefundApprovalStatusText = (item: Refund) => {
|
||||||
|
return item.approval_status_name || item.approval?.status_name || item.approval?.status || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHiddenIdKey = (key: string) => {
|
||||||
|
const normalizedKey = key.toLowerCase()
|
||||||
|
return normalizedKey === 'id' || normalizedKey === 'userid' || normalizedKey.endsWith('_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizeStructuredValue = (value: unknown): unknown => {
|
||||||
|
if (Array.isArray(value)) return value.map((item) => sanitizeStructuredValue(item))
|
||||||
|
if (!value || typeof value !== 'object') return value
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value)
|
||||||
|
.filter(([key]) => !isHiddenIdKey(key))
|
||||||
|
.map(([key, item]) => [key, sanitizeStructuredValue(item)])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const formatStructuredValue = (value: unknown) => {
|
const formatStructuredValue = (value: unknown) => {
|
||||||
if (value === undefined || value === null || value === '') return '-'
|
if (value === undefined || value === null || value === '') return '-'
|
||||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||||
@@ -171,7 +199,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderStructuredValue = (value: unknown) =>
|
const renderStructuredValue = (value: unknown) =>
|
||||||
h('pre', { class: 'structured-value' }, formatStructuredValue(value))
|
h('pre', { class: 'structured-value' }, formatStructuredValue(sanitizeStructuredValue(value)))
|
||||||
|
|
||||||
const renderTimeline = (timeline: unknown) => {
|
const renderTimeline = (timeline: unknown) => {
|
||||||
if (!Array.isArray(timeline) || timeline.length === 0) return h('span', '-')
|
if (!Array.isArray(timeline) || timeline.length === 0) return h('span', '-')
|
||||||
@@ -181,7 +209,7 @@
|
|||||||
{ class: 'approval-timeline' },
|
{ class: 'approval-timeline' },
|
||||||
timeline.map((item: any) => {
|
timeline.map((item: any) => {
|
||||||
const title = item.status_name || item.status || item.content || '审批节点'
|
const title = item.status_name || item.status || item.content || '审批节点'
|
||||||
const operator = item.operator_name || item.operator || ''
|
const operator = item.operator_name || ''
|
||||||
const time = item.time || item.timestamp || ''
|
const time = item.time || item.timestamp || ''
|
||||||
const comment = item.comment || item.content || ''
|
const comment = item.comment || item.content || ''
|
||||||
return h('div', { class: 'approval-timeline__item' }, [
|
return h('div', { class: 'approval-timeline__item' }, [
|
||||||
@@ -207,6 +235,8 @@
|
|||||||
{ label: '订单号', prop: 'order_no' },
|
{ label: '订单号', prop: 'order_no' },
|
||||||
{ label: '资产标识符', prop: 'asset_identifier' },
|
{ label: '资产标识符', prop: 'asset_identifier' },
|
||||||
{ label: '资产类型', prop: 'asset_type' },
|
{ label: '资产类型', prop: 'asset_type' },
|
||||||
|
{ label: '退款状态', prop: 'status_name', formatter: (value) => value || '-' },
|
||||||
|
{ label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' },
|
||||||
{
|
{
|
||||||
label: '申请退款金额',
|
label: '申请退款金额',
|
||||||
formatter: (_, data) => formatCurrency(data.requested_refund_amount)
|
formatter: (_, data) => formatCurrency(data.requested_refund_amount)
|
||||||
@@ -225,6 +255,12 @@
|
|||||||
formatter: (value) => value || '-',
|
formatter: (value) => value || '-',
|
||||||
fullWidth: true
|
fullWidth: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '拒绝原因',
|
||||||
|
prop: 'reject_reason',
|
||||||
|
formatter: (value) => value || '-',
|
||||||
|
fullWidth: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '备注',
|
label: '备注',
|
||||||
prop: 'remark',
|
prop: 'remark',
|
||||||
@@ -258,17 +294,30 @@
|
|||||||
formatter: (_, data) => (data.commission_deducted ? '是' : '否')
|
formatter: (_, data) => (data.commission_deducted ? '是' : '否')
|
||||||
},
|
},
|
||||||
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
|
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
|
||||||
|
{
|
||||||
|
label: '审批时间',
|
||||||
|
prop: 'processed_at',
|
||||||
|
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||||
|
},
|
||||||
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
|
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '企微审批信息',
|
title: '企微审批信息',
|
||||||
fields: [
|
fields: [
|
||||||
{ label: '审批来源', prop: 'approval.source' },
|
{ label: '审批渠道', formatter: (_, data) => getApprovalProviderText(data) },
|
||||||
|
{
|
||||||
|
label: '审批来源',
|
||||||
|
formatter: (_, data) => data.approval?.source || data.approval_source || '-'
|
||||||
|
},
|
||||||
{ label: '审批单号', prop: 'approval.sp_no' },
|
{ label: '审批单号', prop: 'approval.sp_no' },
|
||||||
{
|
{
|
||||||
label: '审批状态',
|
label: '审批状态',
|
||||||
formatter: (_, data) => data.approval?.status_name || data.approval?.status || '-'
|
formatter: (_, data) => getRefundApprovalStatusText(data)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '当前审批人摘要',
|
||||||
|
formatter: (_, data) => data.current_approver_summary || '-'
|
||||||
},
|
},
|
||||||
{ label: '模板版本', prop: 'approval.template_version' },
|
{ label: '模板版本', prop: 'approval.template_version' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('refund:create')"
|
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('refund:create')"
|
||||||
>创建退款申请</ElButton
|
>创建退款申请</ElButton
|
||||||
>
|
>
|
||||||
|
<ElButton v-if="hasAuth('refund:export')" @click="exportDialogVisible = true">
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
@@ -48,6 +51,15 @@
|
|||||||
<!-- 创建退款申请对话框 -->
|
<!-- 创建退款申请对话框 -->
|
||||||
<CreateRefundDialog v-model="createDialogVisible" @success="handleCreateSuccess" />
|
<CreateRefundDialog v-model="createDialogVisible" @success="handleCreateSuccess" />
|
||||||
|
|
||||||
|
<!-- 导出任务对话框 -->
|
||||||
|
<ExportTaskCreateDialog
|
||||||
|
v-model="exportDialogVisible"
|
||||||
|
scene="refund"
|
||||||
|
:query="exportQuery"
|
||||||
|
confirm-permission="refund:export"
|
||||||
|
title="导出退款"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 退款凭证预览 -->
|
<!-- 退款凭证预览 -->
|
||||||
<PaymentVoucherDialog
|
<PaymentVoucherDialog
|
||||||
:file-keys="refundVoucherFileKeys"
|
:file-keys="refundVoucherFileKeys"
|
||||||
@@ -147,7 +159,6 @@
|
|||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
|
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
|
||||||
import {
|
import {
|
||||||
getApprovalStatusText,
|
|
||||||
getCurrentApproverSummaryText,
|
getCurrentApproverSummaryText,
|
||||||
getProcessingStatusText
|
getProcessingStatusText
|
||||||
} from '@/utils/business/approvalSummary'
|
} from '@/utils/business/approvalSummary'
|
||||||
@@ -155,6 +166,7 @@
|
|||||||
import { RoutesAlias } from '@/router/routesAlias'
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||||
|
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||||
|
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
|
||||||
@@ -170,6 +182,7 @@
|
|||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||||
const createDialogVisible = ref(false)
|
const createDialogVisible = ref(false)
|
||||||
|
const exportDialogVisible = ref(false)
|
||||||
const resubmitDialogVisible = ref(false)
|
const resubmitDialogVisible = ref(false)
|
||||||
const currentRefund = ref<Refund | null>(null)
|
const currentRefund = ref<Refund | null>(null)
|
||||||
const refundVoucherFileKeys = ref<string[]>([])
|
const refundVoucherFileKeys = ref<string[]>([])
|
||||||
@@ -269,6 +282,7 @@
|
|||||||
{ label: '实际退款金额', prop: 'approved_refund_amount' },
|
{ label: '实际退款金额', prop: 'approved_refund_amount' },
|
||||||
{ label: '实收金额', prop: 'actual_received_amount' },
|
{ label: '实收金额', prop: 'actual_received_amount' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
|
{ label: '审批渠道', prop: 'approval_provider' },
|
||||||
{ label: '提交人', prop: 'submitter_name' },
|
{ label: '提交人', prop: 'submitter_name' },
|
||||||
{ label: '审批状态', prop: 'approval_status' },
|
{ label: '审批状态', prop: 'approval_status' },
|
||||||
{ label: '当前审批人摘要', prop: 'current_approver_summary' },
|
{ label: '当前审批人摘要', prop: 'current_approver_summary' },
|
||||||
@@ -279,7 +293,8 @@
|
|||||||
{ label: '资产重置', prop: 'asset_reset' },
|
{ label: '资产重置', prop: 'asset_reset' },
|
||||||
{ label: '佣金扣除', prop: 'commission_deducted' },
|
{ label: '佣金扣除', prop: 'commission_deducted' },
|
||||||
{ label: '创建时间', prop: 'created_at' },
|
{ label: '创建时间', prop: 'created_at' },
|
||||||
{ label: '审批时间', prop: 'processed_at' }
|
{ label: '审批时间', prop: 'processed_at' },
|
||||||
|
{ label: '更新时间', prop: 'updated_at' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const resubmitFormRef = ref<FormInstance>()
|
const resubmitFormRef = ref<FormInstance>()
|
||||||
@@ -320,17 +335,6 @@
|
|||||||
return statusMap[status] || 'info'
|
return statusMap[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取状态文本
|
|
||||||
const getStatusText = (status: RefundStatus): string => {
|
|
||||||
const statusMap: Record<RefundStatus, string> = {
|
|
||||||
1: '待审批',
|
|
||||||
2: '已通过',
|
|
||||||
3: '已拒绝',
|
|
||||||
4: '已退回'
|
|
||||||
}
|
|
||||||
return statusMap[status] || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
{
|
||||||
@@ -404,7 +408,17 @@
|
|||||||
label: '状态',
|
label: '状态',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: Refund) => {
|
formatter: (row: Refund) => {
|
||||||
return h(ElTag, { type: getStatusType(row.status) }, () => getStatusText(row.status))
|
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '-')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'approval_provider',
|
||||||
|
label: '审批渠道',
|
||||||
|
width: 110,
|
||||||
|
formatter: (row: Refund) => {
|
||||||
|
if (row.approval_provider === 'wecom' || row.approval_source === 'wecom') return '企微'
|
||||||
|
if (row.approval_source === 'legacy') return '历史审批'
|
||||||
|
return row.approval_provider || row.approval_source || '-'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -418,7 +432,7 @@
|
|||||||
prop: 'approval_status',
|
prop: 'approval_status',
|
||||||
label: '审批状态',
|
label: '审批状态',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: (row: Refund) => getApprovalStatusText(row)
|
formatter: (row: Refund) => row.approval_status_name || '-'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'current_approver_summary',
|
prop: 'current_approver_summary',
|
||||||
@@ -483,6 +497,12 @@
|
|||||||
label: '审批时间',
|
label: '审批时间',
|
||||||
width: 180,
|
width: 180,
|
||||||
formatter: (row: Refund) => (row.processed_at ? formatDateTime(row.processed_at) : '-')
|
formatter: (row: Refund) => (row.processed_at ? formatDateTime(row.processed_at) : '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'updated_at',
|
||||||
|
label: '更新时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: (row: Refund) => formatDateTime(row.updated_at)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -564,6 +584,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportQuery = computed(() => ({
|
||||||
|
status: searchForm.status,
|
||||||
|
order_id: searchForm.order_id,
|
||||||
|
shop_id: searchForm.shop_id,
|
||||||
|
asset_identifier: searchForm.asset_identifier?.trim() || undefined
|
||||||
|
}))
|
||||||
|
|
||||||
// 重置搜索
|
// 重置搜索
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
Object.assign(searchForm, { ...initialSearchState })
|
Object.assign(searchForm, { ...initialSearchState })
|
||||||
|
|||||||
@@ -1,277 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ArtTableFullScreen>
|
|
||||||
<div class="notification-center-page" id="table-full-screen">
|
|
||||||
<ElCard shadow="never" class="art-table-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="page-header">
|
|
||||||
<div>
|
|
||||||
<h2>通知中心</h2>
|
|
||||||
<p>查看余额预警、临期提醒、审批结果和系统告警。</p>
|
|
||||||
</div>
|
|
||||||
<ElButton
|
|
||||||
v-if="hasAuth(NOTIFICATION_PERMISSIONS.readAll)"
|
|
||||||
:loading="markingAllRead"
|
|
||||||
@click="handleMarkAllRead"
|
|
||||||
>
|
|
||||||
全部已读
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="filters">
|
|
||||||
<ElSelect v-model="filters.category" clearable placeholder="通知分类" @change="reload">
|
|
||||||
<ElOption label="临期提醒" value="expiry" />
|
|
||||||
<ElOption label="审批" value="approval" />
|
|
||||||
<ElOption label="同步" value="sync" />
|
|
||||||
<ElOption label="系统" value="system" />
|
|
||||||
</ElSelect>
|
|
||||||
<ElSelect v-model="filters.type" clearable placeholder="通知类型" @change="reload">
|
|
||||||
<ElOption v-for="type in typeOptions" :key="type" :label="type" :value="type" />
|
|
||||||
</ElSelect>
|
|
||||||
<ElSelect v-model="filters.severity" clearable placeholder="严重级别" @change="reload">
|
|
||||||
<ElOption label="提示" value="info" />
|
|
||||||
<ElOption label="警告" value="warning" />
|
|
||||||
<ElOption label="严重" value="error" />
|
|
||||||
<ElOption label="严重" value="critical" />
|
|
||||||
</ElSelect>
|
|
||||||
<ElSelect v-model="filters.is_read" clearable placeholder="已读状态" @change="reload">
|
|
||||||
<ElOption label="未读" :value="false" />
|
|
||||||
<ElOption label="已读" :value="true" />
|
|
||||||
</ElSelect>
|
|
||||||
<ElButton @click="loadNotifications">刷新</ElButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ElTable v-loading="loading" :data="notifications" row-key="id" class="notification-table">
|
|
||||||
<ElTableColumn label="状态" width="90">
|
|
||||||
<template #default="scope">
|
|
||||||
<span :class="['read-dot', { unread: !isRead(scope.row) }]" />
|
|
||||||
{{ isRead(scope.row) ? '已读' : '未读' }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="title" label="通知标题" min-width="200" show-overflow-tooltip />
|
|
||||||
<ElTableColumn label="分类" width="120">
|
|
||||||
<template #default="scope">{{ getCategoryLabel(scope.row.category) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="严重级别" width="110">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElTag :type="getSeverityType(scope.row.severity)">
|
|
||||||
{{ getSeverityLabel(scope.row.severity) }}
|
|
||||||
</ElTag>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="body" label="通知内容" min-width="300" show-overflow-tooltip />
|
|
||||||
<ElTableColumn prop="read_at" label="已读时间" width="180" />
|
|
||||||
<ElTableColumn prop="created_at" label="时间" width="180" />
|
|
||||||
<ElTableColumn label="操作" width="100" fixed="right">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElButton
|
|
||||||
v-if="hasAuth(NOTIFICATION_PERMISSIONS.read)"
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
@click="handleNotificationClick(scope.row)"
|
|
||||||
>
|
|
||||||
查看
|
|
||||||
</ElButton>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
|
|
||||||
<div class="pagination-wrapper">
|
|
||||||
<ElPagination
|
|
||||||
v-model:current-page="page"
|
|
||||||
v-model:page-size="pageSize"
|
|
||||||
layout="total, sizes, prev, pager, next"
|
|
||||||
:total="total"
|
|
||||||
@current-change="loadNotifications"
|
|
||||||
@size-change="reload"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</ArtTableFullScreen>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { NotificationService } from '@/api/modules'
|
|
||||||
import type { NotificationItem, NotificationQueryParams } from '@/types/api'
|
|
||||||
import { useNotificationStore } from '@/store/modules/notification'
|
|
||||||
import { useAuth } from '@/composables/useAuth'
|
|
||||||
import { NOTIFICATION_PERMISSIONS } from '@/config/constants/notification'
|
|
||||||
import { navigateNotificationTarget } from '@/utils/business/notificationNavigation'
|
|
||||||
|
|
||||||
defineOptions({ name: 'NotificationCenter' })
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const notificationStore = useNotificationStore()
|
|
||||||
const { hasAuth } = useAuth()
|
|
||||||
const notifications = ref<NotificationItem[]>([])
|
|
||||||
const loading = ref(false)
|
|
||||||
const markingAllRead = ref(false)
|
|
||||||
const total = ref(0)
|
|
||||||
const page = ref(1)
|
|
||||||
const pageSize = ref(20)
|
|
||||||
const typeOptions = ref<string[]>([])
|
|
||||||
const filters = reactive<NotificationQueryParams>({
|
|
||||||
category: undefined,
|
|
||||||
type: undefined,
|
|
||||||
severity: undefined,
|
|
||||||
is_read: undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const isRead = (item: NotificationItem) => item.is_read
|
|
||||||
|
|
||||||
const getCategoryLabel = (category?: string | null) =>
|
|
||||||
({
|
|
||||||
balance: '余额预警',
|
|
||||||
expiry: '临期提醒',
|
|
||||||
expiring: '临期提醒',
|
|
||||||
approval: '审批',
|
|
||||||
sync: '同步/系统',
|
|
||||||
system: '同步/系统'
|
|
||||||
})[category || ''] ||
|
|
||||||
category ||
|
|
||||||
'-'
|
|
||||||
|
|
||||||
const getSeverityLabel = (severity?: string | null) =>
|
|
||||||
({ info: '提示', warning: '警告', error: '严重', critical: '严重' })[severity || ''] ||
|
|
||||||
severity ||
|
|
||||||
'-'
|
|
||||||
|
|
||||||
const getSeverityType = (severity?: string | null) => {
|
|
||||||
if (severity === 'error') return 'danger'
|
|
||||||
if (severity === 'warning') return 'warning'
|
|
||||||
if (severity === 'critical') return 'danger'
|
|
||||||
return 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadNotifications = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const response = await NotificationService.getNotifications({
|
|
||||||
page: page.value,
|
|
||||||
page_size: pageSize.value,
|
|
||||||
...filters
|
|
||||||
})
|
|
||||||
if (response.code !== 0 || !response.data) {
|
|
||||||
ElMessage.error(response.msg || '获取通知列表失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
notifications.value = response.data.items || []
|
|
||||||
total.value = response.data.total || 0
|
|
||||||
typeOptions.value = Array.from(
|
|
||||||
new Set(
|
|
||||||
notifications.value
|
|
||||||
.map((item) => item.type)
|
|
||||||
.filter((type): type is string => Boolean(type))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await notificationStore.refreshUnreadCount()
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '获取通知列表失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = () => {
|
|
||||||
page.value = 1
|
|
||||||
void loadNotifications()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMarkAllRead = async () => {
|
|
||||||
markingAllRead.value = true
|
|
||||||
try {
|
|
||||||
const response = await notificationStore.markAllRead()
|
|
||||||
if (response.code !== 0) ElMessage.error(response.msg || '全部已读失败')
|
|
||||||
else await loadNotifications()
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '全部已读失败')
|
|
||||||
} finally {
|
|
||||||
markingAllRead.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 || '该通知暂无可跳转目标')
|
|
||||||
await loadNotifications()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!navigateNotificationTarget(router, targetResponse.data)) {
|
|
||||||
ElMessage.info(item.body || '该通知暂无可跳转目标')
|
|
||||||
}
|
|
||||||
await loadNotifications()
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '处理通知失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
void loadNotifications()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.notification-center-page {
|
|
||||||
.page-header,
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 8px 0 0;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
.el-select {
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-dot {
|
|
||||||
display: inline-block;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
margin-right: 6px;
|
|
||||||
vertical-align: middle;
|
|
||||||
background: var(--el-border-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
&.unread {
|
|
||||||
background: var(--el-color-danger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ArtTableFullScreen>
|
|
||||||
<div class="bulk-purchase-page" id="table-full-screen">
|
|
||||||
<ElCard shadow="never" class="art-table-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="page-header">
|
|
||||||
<div>
|
|
||||||
<h2>批量订购套餐</h2>
|
|
||||||
<p>同一批次只能选择一种支付方式,系统按任务结果逐行处理订单。</p>
|
|
||||||
</div>
|
|
||||||
<ElButton
|
|
||||||
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.template)"
|
|
||||||
tag="a"
|
|
||||||
href="/templates/bulk-purchase-template.csv"
|
|
||||||
download="批量订购套餐模板.csv"
|
|
||||||
>
|
|
||||||
下载模板
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="create-form">
|
|
||||||
<ElFormItem label="套餐 ID" prop="package_id">
|
|
||||||
<ElInputNumber v-model="form.package_id" :min="1" controls-position="right" />
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="支付方式" prop="payment_method">
|
|
||||||
<ElRadioGroup v-model="form.payment_method" @change="handlePaymentMethodChange">
|
|
||||||
<ElRadio value="wallet">代理钱包</ElRadio>
|
|
||||||
<ElRadio value="offline">线下支付</ElRadio>
|
|
||||||
</ElRadioGroup>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="订单文件" prop="orderFile">
|
|
||||||
<VoucherUpload
|
|
||||||
ref="orderUploadRef"
|
|
||||||
v-model="orderFileKeys"
|
|
||||||
voucher-name="订单文件"
|
|
||||||
:max-count="1"
|
|
||||||
purpose="batch_purchase"
|
|
||||||
:max-size-mb="10"
|
|
||||||
single-column-csv
|
|
||||||
:max-csv-rows="1000"
|
|
||||||
accept=".csv"
|
|
||||||
tip="仅支持 UTF-8 单列 CSV,最多 1000 行,最大 10MB"
|
|
||||||
@uploading-change="orderFileUploading = $event"
|
|
||||||
@change="formRef?.validateField('orderFile')"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem
|
|
||||||
v-if="form.payment_method === 'offline'"
|
|
||||||
label="整批支付凭证"
|
|
||||||
prop="voucherFile"
|
|
||||||
>
|
|
||||||
<VoucherUpload
|
|
||||||
ref="voucherUploadRef"
|
|
||||||
v-model="voucherFileKeys"
|
|
||||||
voucher-name="整批支付凭证"
|
|
||||||
@uploading-change="voucherUploading = $event"
|
|
||||||
@change="formRef?.validateField('voucherFile')"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElAlert
|
|
||||||
v-if="submitting || voucherUploading || orderFileUploading"
|
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
:title="
|
|
||||||
voucherUploading
|
|
||||||
? '支付凭证上传中,请稍候'
|
|
||||||
: orderFileUploading
|
|
||||||
? '订单文件上传中,请稍候'
|
|
||||||
: '批量订购文件上传及任务创建中,请勿重复提交'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
:loading="submitting || voucherUploading || orderFileUploading"
|
|
||||||
:disabled="
|
|
||||||
voucherUploading || orderFileUploading || !hasAuth(BULK_PURCHASE_PERMISSIONS.create)
|
|
||||||
"
|
|
||||||
@click="submitTask"
|
|
||||||
>
|
|
||||||
创建批量订购任务
|
|
||||||
</ElButton>
|
|
||||||
<ElButton v-if="taskDetail" @click="resetTask">重新创建</ElButton>
|
|
||||||
</div>
|
|
||||||
</ElForm>
|
|
||||||
</ElCard>
|
|
||||||
|
|
||||||
<ElCard
|
|
||||||
v-if="taskDetail && hasAuth(BULK_PURCHASE_PERMISSIONS.detail)"
|
|
||||||
shadow="never"
|
|
||||||
class="art-table-card task-card"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="task-header">
|
|
||||||
<span>任务结果</span>
|
|
||||||
<ElTag :type="getStatusType(taskDetail.status)">
|
|
||||||
{{ taskDetail.status_name || getStatusName(taskDetail.status) }}
|
|
||||||
</ElTag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElDescriptions :column="4" border>
|
|
||||||
<ElDescriptionsItem label="任务号">{{
|
|
||||||
taskDetail.task_no || taskDetail.task_id
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="总数">{{ taskDetail.total_count ?? 0 }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="成功数">{{
|
|
||||||
taskDetail.success_count ?? 0
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="失败数">{{ taskDetail.failed_count ?? 0 }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="总金额">{{
|
|
||||||
formatAmount(taskDetail.total_amount)
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="成功金额">{{
|
|
||||||
formatAmount(taskDetail.success_amount)
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="失败金额">{{
|
|
||||||
formatAmount(taskDetail.failed_amount)
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="更新时间">{{
|
|
||||||
formatDateTime(taskDetail.updated_at)
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
|
|
||||||
<ElAlert
|
|
||||||
v-if="taskDetail.error_summary"
|
|
||||||
class="task-error"
|
|
||||||
type="error"
|
|
||||||
:closable="false"
|
|
||||||
:title="taskDetail.error_summary"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.items)" class="items-toolbar">
|
|
||||||
<ElSelect v-model="itemStatus" clearable placeholder="筛选行状态" @change="reloadItems">
|
|
||||||
<ElOption label="成功" value="success" />
|
|
||||||
<ElOption label="失败" value="failed" />
|
|
||||||
<ElOption label="处理中" value="processing" />
|
|
||||||
</ElSelect>
|
|
||||||
<ElButton @click="reloadItems">刷新结果</ElButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ElTable
|
|
||||||
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.items)"
|
|
||||||
v-loading="itemsLoading"
|
|
||||||
:data="items"
|
|
||||||
border
|
|
||||||
>
|
|
||||||
<ElTableColumn label="行号" width="90">
|
|
||||||
<template #default="scope">{{
|
|
||||||
scope.row.row_number ?? scope.row.line ?? '-'
|
|
||||||
}}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="资产" min-width="180">
|
|
||||||
<template #default="scope">{{ getAssetIdentifier(scope.row) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="package_code" label="套餐编码" min-width="150" />
|
|
||||||
<ElTableColumn label="状态" width="110">
|
|
||||||
<template #default="scope">{{
|
|
||||||
scope.row.status_name || scope.row.status || '-'
|
|
||||||
}}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="错误原因" min-width="240" show-overflow-tooltip>
|
|
||||||
<template #default="scope">
|
|
||||||
{{ scope.row.error_summary || scope.row.error_reason || scope.row.error_code || '-' }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
|
|
||||||
<div v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.items)" class="pagination-wrapper">
|
|
||||||
<ElPagination
|
|
||||||
v-model:current-page="itemsPage"
|
|
||||||
v-model:page-size="itemsSize"
|
|
||||||
layout="total, sizes, prev, pager, next"
|
|
||||||
:total="itemsTotal"
|
|
||||||
@current-change="loadItems"
|
|
||||||
@size-change="reloadItems"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</ArtTableFullScreen>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import {
|
|
||||||
ElAlert,
|
|
||||||
ElButton,
|
|
||||||
ElCard,
|
|
||||||
ElDescriptions,
|
|
||||||
ElDescriptionsItem,
|
|
||||||
ElInputNumber,
|
|
||||||
ElForm,
|
|
||||||
ElMessage,
|
|
||||||
ElOption,
|
|
||||||
ElPagination,
|
|
||||||
ElRadio,
|
|
||||||
ElRadioGroup,
|
|
||||||
ElSelect,
|
|
||||||
ElTable,
|
|
||||||
ElTableColumn,
|
|
||||||
ElTag
|
|
||||||
} from 'element-plus'
|
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
|
||||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
|
||||||
import { BulkPurchaseService } from '@/api/modules'
|
|
||||||
import type {
|
|
||||||
BulkPurchaseItem,
|
|
||||||
BulkPurchasePaymentMethod,
|
|
||||||
BulkPurchaseTask
|
|
||||||
} from '@/types/api'
|
|
||||||
import { BulkPurchaseTaskStatus } from '@/types/api'
|
|
||||||
import { useAuth } from '@/composables/useAuth'
|
|
||||||
import { useAsyncTaskPolling } from '@/composables/useAsyncTaskPolling'
|
|
||||||
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
|
||||||
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
|
||||||
|
|
||||||
defineOptions({ name: 'BulkPurchase' })
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
const { hasAuth } = useAuth()
|
|
||||||
const formRef = ref<FormInstance>()
|
|
||||||
const orderUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
|
||||||
const voucherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
|
||||||
const submitting = ref(false)
|
|
||||||
const orderFileKeys = ref<string[]>([])
|
|
||||||
const orderFileUploading = ref(false)
|
|
||||||
const voucherFileKeys = ref<string[]>([])
|
|
||||||
const voucherUploading = ref(false)
|
|
||||||
const itemStatus = ref<string | undefined>()
|
|
||||||
const items = ref<BulkPurchaseItem[]>([])
|
|
||||||
const itemsLoading = ref(false)
|
|
||||||
const itemsPage = ref(1)
|
|
||||||
const itemsSize = ref(20)
|
|
||||||
const itemsTotal = ref(0)
|
|
||||||
const requestId = ref('')
|
|
||||||
|
|
||||||
const form = reactive<{
|
|
||||||
package_id?: number
|
|
||||||
payment_method: BulkPurchasePaymentMethod
|
|
||||||
}>({
|
|
||||||
package_id: undefined,
|
|
||||||
payment_method: 'wallet'
|
|
||||||
})
|
|
||||||
|
|
||||||
const rules = computed<FormRules>(() => ({
|
|
||||||
package_id: [{ required: true, message: '请输入套餐 ID', trigger: 'change' }],
|
|
||||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
|
||||||
orderFile: [
|
|
||||||
{
|
|
||||||
validator: (_rule: unknown, _value: unknown, callback: (error?: Error) => void) => {
|
|
||||||
if (orderFileKeys.value.length === 0) callback(new Error('请上传订单文件'))
|
|
||||||
else callback()
|
|
||||||
},
|
|
||||||
trigger: 'change'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
voucherFile: [
|
|
||||||
{
|
|
||||||
validator: (_rule: unknown, _value: unknown, callback: (error?: Error) => void) => {
|
|
||||||
if (form.payment_method === 'offline' && voucherFileKeys.value.length === 0) {
|
|
||||||
callback(new Error('线下支付必须上传整批支付凭证'))
|
|
||||||
} else callback()
|
|
||||||
},
|
|
||||||
trigger: 'change'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}))
|
|
||||||
|
|
||||||
const polling = useAsyncTaskPolling<BulkPurchaseTask>({
|
|
||||||
storageKey: 'bulk-purchase-active-task',
|
|
||||||
fetchTask: async (taskId) => {
|
|
||||||
const res = await BulkPurchaseService.getTask(taskId)
|
|
||||||
if (res.code !== 0 || !res.data) throw new Error(res.msg || '获取批量订购任务失败')
|
|
||||||
return res.data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const taskDetail = polling.task
|
|
||||||
|
|
||||||
const getRequestId = () => {
|
|
||||||
if (requestId.value) return requestId.value
|
|
||||||
requestId.value =
|
|
||||||
typeof crypto !== 'undefined' && crypto.randomUUID
|
|
||||||
? crypto.randomUUID()
|
|
||||||
: `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
||||||
return requestId.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePaymentMethodChange = (value: string | number | boolean | undefined) => {
|
|
||||||
if (value === 'wallet') {
|
|
||||||
voucherFileKeys.value = []
|
|
||||||
voucherUploadRef.value?.clearFiles()
|
|
||||||
formRef.value?.clearValidate('voucherFile')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitTask = async () => {
|
|
||||||
if (
|
|
||||||
!hasAuth(BULK_PURCHASE_PERMISSIONS.create) ||
|
|
||||||
submitting.value ||
|
|
||||||
voucherUploading.value ||
|
|
||||||
orderFileUploading.value
|
|
||||||
)
|
|
||||||
return
|
|
||||||
const valid = await formRef.value?.validate().catch(() => false)
|
|
||||||
if (!valid || !form.package_id || orderFileKeys.value.length === 0) return
|
|
||||||
|
|
||||||
submitting.value = true
|
|
||||||
try {
|
|
||||||
const data = {
|
|
||||||
file_key: orderFileKeys.value[0],
|
|
||||||
package_id: form.package_id,
|
|
||||||
payment_method: form.payment_method,
|
|
||||||
...(form.payment_method === 'offline' ? { voucher_keys: voucherFileKeys.value } : {})
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await BulkPurchaseService.createTask(data)
|
|
||||||
if (res.code !== 0 || !res.data?.task_id) {
|
|
||||||
ElMessage.error(res.msg || '创建批量订购任务失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.success(res.data.message || '批量订购任务已创建')
|
|
||||||
requestId.value = ''
|
|
||||||
await router.replace({
|
|
||||||
path: route.path,
|
|
||||||
query: { task_id: String(res.data.task_id) }
|
|
||||||
})
|
|
||||||
await polling.start(res.data.task_id)
|
|
||||||
await loadItems()
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '创建批量订购任务失败')
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadItems = async () => {
|
|
||||||
const taskId = taskDetail.value?.id || taskDetail.value?.task_id
|
|
||||||
if (!taskId || !hasAuth(BULK_PURCHASE_PERMISSIONS.items)) return
|
|
||||||
items.value = taskDetail.value?.items || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const reloadItems = () => {
|
|
||||||
itemsPage.value = 1
|
|
||||||
void loadItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetTask = () => {
|
|
||||||
polling.clear()
|
|
||||||
requestId.value = ''
|
|
||||||
items.value = []
|
|
||||||
itemsTotal.value = 0
|
|
||||||
itemsPage.value = 1
|
|
||||||
router.replace({ path: route.path })
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusName = (status: BulkPurchaseTaskStatus) => {
|
|
||||||
const names: Record<BulkPurchaseTaskStatus, string> = {
|
|
||||||
[BulkPurchaseTaskStatus.PENDING]: '待处理',
|
|
||||||
[BulkPurchaseTaskStatus.PROCESSING]: '处理中',
|
|
||||||
[BulkPurchaseTaskStatus.COMPLETED]: '已完成',
|
|
||||||
[BulkPurchaseTaskStatus.FAILED]: '已失败',
|
|
||||||
[BulkPurchaseTaskStatus.CANCELED]: '已取消'
|
|
||||||
}
|
|
||||||
return names[status] || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusType = (status: BulkPurchaseTaskStatus) => {
|
|
||||||
if (status === BulkPurchaseTaskStatus.COMPLETED) return 'success'
|
|
||||||
if (status === BulkPurchaseTaskStatus.FAILED) return 'danger'
|
|
||||||
if (status === BulkPurchaseTaskStatus.PROCESSING) return 'warning'
|
|
||||||
return 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatAmount = (amount?: number) => (amount === undefined ? '-' : formatMoney(amount))
|
|
||||||
|
|
||||||
const getAssetIdentifier = (item: BulkPurchaseItem) =>
|
|
||||||
item.asset_identifier || item.iccid || item.virtual_no || '-'
|
|
||||||
|
|
||||||
watch(taskDetail, () => {
|
|
||||||
itemsPage.value = 1
|
|
||||||
void loadItems()
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const routeTaskId = Number(route.query.task_id)
|
|
||||||
if (routeTaskId && routeTaskId !== polling.taskId.value) void polling.start(routeTaskId)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.bulk-purchase-page {
|
|
||||||
.page-header,
|
|
||||||
.task-header,
|
|
||||||
.form-actions,
|
|
||||||
.items-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header p {
|
|
||||||
margin: 8px 0 0;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-form {
|
|
||||||
max-width: 760px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-error {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-toolbar {
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin: 20px 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="生效条件">
|
<ElFormItem v-if="packageDialogType === 'add' || canUpdateExpiryBase" label="生效条件">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="packageForm.expiry_base_override"
|
v-model="packageForm.expiry_base_override"
|
||||||
placeholder="请选择生效条件"
|
placeholder="请选择生效条件"
|
||||||
@@ -352,7 +352,8 @@
|
|||||||
PackageResponse
|
PackageResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { getEnableStatusText } from '@/config/constants'
|
import { getEnableStatusText, JULY_PERMISSIONS } from '@/config/constants'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import {
|
import {
|
||||||
mergeGrantPackageCandidates,
|
mergeGrantPackageCandidates,
|
||||||
type GrantPackageCandidate
|
type GrantPackageCandidate
|
||||||
@@ -362,6 +363,8 @@
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const canUpdateExpiryBase = hasAuth(JULY_PERMISSIONS.seriesGrants.updateExpiryBase)
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
@@ -378,11 +381,11 @@
|
|||||||
// 套餐表单
|
// 套餐表单
|
||||||
const packageForm = ref<{
|
const packageForm = ref<{
|
||||||
package_id?: number
|
package_id?: number
|
||||||
|
allocation_id?: number
|
||||||
package_name?: string
|
package_name?: string
|
||||||
package_code?: string
|
package_code?: string
|
||||||
original_cost_price?: number
|
original_cost_price?: number
|
||||||
cost_price_yuan: number
|
cost_price_yuan: number
|
||||||
allocation_id?: number
|
|
||||||
expiry_base_override: ExpiryBaseSelection
|
expiry_base_override: ExpiryBaseSelection
|
||||||
initial_expiry_base_override: ExpiryBaseSelection
|
initial_expiry_base_override: ExpiryBaseSelection
|
||||||
default_expiry_base_name?: string | null
|
default_expiry_base_name?: string | null
|
||||||
@@ -509,6 +512,7 @@
|
|||||||
packageDialogType.value = 'add'
|
packageDialogType.value = 'add'
|
||||||
packageForm.value = {
|
packageForm.value = {
|
||||||
package_id: undefined,
|
package_id: undefined,
|
||||||
|
allocation_id: undefined,
|
||||||
package_name: undefined,
|
package_name: undefined,
|
||||||
package_code: undefined,
|
package_code: undefined,
|
||||||
original_cost_price: undefined,
|
original_cost_price: undefined,
|
||||||
@@ -530,11 +534,11 @@
|
|||||||
packageDialogType.value = 'edit'
|
packageDialogType.value = 'edit'
|
||||||
packageForm.value = {
|
packageForm.value = {
|
||||||
package_id: row.package_id,
|
package_id: row.package_id,
|
||||||
|
allocation_id: row.allocation_id,
|
||||||
package_name: row.package_name,
|
package_name: row.package_name,
|
||||||
package_code: row.package_code,
|
package_code: row.package_code,
|
||||||
original_cost_price: row.cost_price / 100,
|
original_cost_price: row.cost_price / 100,
|
||||||
cost_price_yuan: row.cost_price / 100,
|
cost_price_yuan: row.cost_price / 100,
|
||||||
allocation_id: row.allocation_id,
|
|
||||||
expiry_base_override: row.expiry_base_override ?? 'default',
|
expiry_base_override: row.expiry_base_override ?? 'default',
|
||||||
initial_expiry_base_override: row.expiry_base_override ?? 'default',
|
initial_expiry_base_override: row.expiry_base_override ?? 'default',
|
||||||
default_expiry_base_name: row.default_expiry_base_name,
|
default_expiry_base_name: row.default_expiry_base_name,
|
||||||
@@ -619,8 +623,12 @@
|
|||||||
const expiryBaseChanged =
|
const expiryBaseChanged =
|
||||||
packageForm.value.expiry_base_override !==
|
packageForm.value.expiry_base_override !==
|
||||||
packageForm.value.initial_expiry_base_override
|
packageForm.value.initial_expiry_base_override
|
||||||
if (expiryBaseChanged && !packageForm.value.allocation_id) {
|
if (
|
||||||
ElMessage.error('该套餐分配记录缺少ID,无法更新生效条件')
|
expiryBaseChanged &&
|
||||||
|
(packageForm.value.allocation_id === undefined ||
|
||||||
|
packageForm.value.allocation_id === null)
|
||||||
|
) {
|
||||||
|
ElMessage.error('该套餐分配记录缺少套餐分配 ID,无法修改生效条件')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,22 @@
|
|||||||
:marginTop="10"
|
:marginTop="10"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
:actions="getActions"
|
:actions="getActions"
|
||||||
:actionsWidth="150"
|
:actionsWidth="240"
|
||||||
:inlineActionsCount="2"
|
:inlineActionsCount="3"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn prop="package_name" label="套餐名称" minWidth="150" />
|
<ElTableColumn
|
||||||
<ElTableColumn prop="package_code" label="套餐编码" minWidth="120" />
|
prop="package_name"
|
||||||
|
label="套餐名称"
|
||||||
|
minWidth="220"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<ElTableColumn
|
||||||
|
prop="package_code"
|
||||||
|
label="套餐编码"
|
||||||
|
minWidth="180"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
<ElTableColumn label="成本价" width="120">
|
<ElTableColumn label="成本价" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
|
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
|
||||||
@@ -81,16 +91,18 @@
|
|||||||
@closed="handlePackageDialogClosed"
|
@closed="handlePackageDialogClosed"
|
||||||
>
|
>
|
||||||
<ElForm ref="packageFormRef" :model="packageForm" :rules="packageRules" label-width="140px">
|
<ElForm ref="packageFormRef" :model="packageForm" :rules="packageRules" label-width="140px">
|
||||||
<ElFormItem label="选择套餐" prop="package_id" v-if="packageDialogType === 'add'">
|
<ElFormItem label="选择套餐" prop="package_ids" v-if="packageDialogType === 'add'">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="packageForm.package_id"
|
v-model="packageForm.package_ids"
|
||||||
placeholder="请选择套餐"
|
placeholder="请选择套餐(可多选)"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
filterable
|
filterable
|
||||||
remote
|
remote
|
||||||
:remote-method="searchAvailablePackages"
|
:remote-method="searchAvailablePackages"
|
||||||
:loading="packageLoading"
|
:loading="packageLoading"
|
||||||
clearable
|
clearable
|
||||||
|
multiple
|
||||||
|
:multiple-limit="100"
|
||||||
>
|
>
|
||||||
<template v-if="availablePackages.length === 0 && !packageLoading && seriesId">
|
<template v-if="availablePackages.length === 0 && !packageLoading && seriesId">
|
||||||
<ElOption disabled value="" label="该系列没有可选套餐" />
|
<ElOption disabled value="" label="该系列没有可选套餐" />
|
||||||
@@ -105,6 +117,33 @@
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem
|
||||||
|
v-if="packageDialogType === 'add' && packageForm.packages.length"
|
||||||
|
label="套餐配置"
|
||||||
|
prop="packages"
|
||||||
|
>
|
||||||
|
<div class="package-config-list">
|
||||||
|
<div
|
||||||
|
v-for="pkg in packageForm.packages"
|
||||||
|
:key="pkg.package_id"
|
||||||
|
class="package-config-item"
|
||||||
|
>
|
||||||
|
<div class="package-config-name" :title="pkg.package_name || ''">
|
||||||
|
{{ pkg.package_name || '套餐名称不可用' }}
|
||||||
|
</div>
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="pkg.cost_price_yuan"
|
||||||
|
:min="pkg.original_cost_price || 0"
|
||||||
|
:max="getPackageCostPriceMax(pkg)"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.01"
|
||||||
|
:controls="false"
|
||||||
|
placeholder="成本价(元)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="套餐名称" v-if="packageDialogType === 'edit'">
|
<ElFormItem label="套餐名称" v-if="packageDialogType === 'edit'">
|
||||||
<span>{{ packageForm.package_name }}</span>
|
<span>{{ packageForm.package_name }}</span>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -127,7 +166,7 @@
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="生效条件">
|
<ElFormItem v-if="packageDialogType === 'add' || canUpdateExpiryBase" label="生效条件">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="packageForm.expiry_base_override"
|
v-model="packageForm.expiry_base_override"
|
||||||
placeholder="请选择生效条件"
|
placeholder="请选择生效条件"
|
||||||
@@ -161,6 +200,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<ElDialog
|
||||||
|
v-model="expiryDialogVisible"
|
||||||
|
title="修改生效条件"
|
||||||
|
width="460px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<ElForm label-width="130px">
|
||||||
|
<ElFormItem label="套餐名称">
|
||||||
|
<span class="ellipsis-value" :title="expiryForm.package_name || ''">
|
||||||
|
{{ expiryForm.package_name || '-' }}
|
||||||
|
</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="套餐编码">
|
||||||
|
<span class="ellipsis-value" :title="expiryForm.package_code || ''">
|
||||||
|
{{ expiryForm.package_code || '-' }}
|
||||||
|
</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="套餐默认生效条件">
|
||||||
|
{{ expiryForm.default_expiry_base_name || '-' }}
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="覆盖生效条件">
|
||||||
|
<ElSelect v-model="expiryForm.expiry_base_override" style="width: 100%">
|
||||||
|
<ElOption label="跟随套餐默认" value="default" />
|
||||||
|
<ElOption label="购买即生效" value="from_purchase" />
|
||||||
|
<ElOption label="实名激活时生效" value="from_activation" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="最终生效条件">
|
||||||
|
{{ expiryForm.effective_expiry_base_name || '-' }}
|
||||||
|
</ElFormItem>
|
||||||
|
<div class="form-tip">仅影响后续新订单,不影响已购买套餐</div>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="expiryDialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" :loading="expirySubmitLoading" @click="saveExpiryBase">
|
||||||
|
保存
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
</template>
|
</template>
|
||||||
@@ -196,6 +278,7 @@
|
|||||||
PackageAllocationExpiryBaseOverride
|
PackageAllocationExpiryBaseOverride
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||||
import {
|
import {
|
||||||
mergeGrantPackageCandidates,
|
mergeGrantPackageCandidates,
|
||||||
type GrantPackageCandidate
|
type GrantPackageCandidate
|
||||||
@@ -206,6 +289,7 @@
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { hasAuth } = useAuth()
|
const { hasAuth } = useAuth()
|
||||||
|
const canUpdateExpiryBase = hasAuth(JULY_PERMISSIONS.seriesGrants.updateExpiryBase)
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
@@ -221,33 +305,61 @@
|
|||||||
|
|
||||||
const packageDialogVisible = ref(false)
|
const packageDialogVisible = ref(false)
|
||||||
const packageDialogType = ref<'add' | 'edit'>('add')
|
const packageDialogType = ref<'add' | 'edit'>('add')
|
||||||
|
const expiryDialogVisible = ref(false)
|
||||||
|
const expirySubmitLoading = ref(false)
|
||||||
|
|
||||||
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
||||||
|
|
||||||
type PackageFormState = {
|
type PackageCostFormItem = {
|
||||||
package_id?: number
|
package_id: number
|
||||||
package_name?: string
|
package_name?: string
|
||||||
package_code?: string
|
package_code?: string
|
||||||
cost_price_yuan: number
|
cost_price_yuan: number
|
||||||
original_cost_price?: number
|
original_cost_price?: number
|
||||||
suggested_retail_price?: number
|
suggested_retail_price?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type PackageFormState = {
|
||||||
|
package_id?: number
|
||||||
allocation_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
|
expiry_base_override: ExpiryBaseSelection
|
||||||
initial_expiry_base_override: ExpiryBaseSelection
|
initial_expiry_base_override: ExpiryBaseSelection
|
||||||
default_expiry_base_name?: string | null
|
default_expiry_base_name?: string | null
|
||||||
expiry_base_override_name?: string | null
|
expiry_base_override_name?: string | null
|
||||||
effective_expiry_base_name?: string | null
|
effective_expiry_base_name?: string | null
|
||||||
|
packages: PackageCostFormItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpiryFormState = {
|
||||||
|
package_id?: number
|
||||||
|
allocation_id?: number
|
||||||
|
package_name?: string
|
||||||
|
package_code?: string
|
||||||
|
default_expiry_base_name?: string | null
|
||||||
|
expiry_base_override: ExpiryBaseSelection
|
||||||
|
expiry_base_override_name?: string | null
|
||||||
|
effective_expiry_base_name?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const createDefaultPackageForm = (): PackageFormState => ({
|
const createDefaultPackageForm = (): PackageFormState => ({
|
||||||
package_id: undefined,
|
package_id: undefined,
|
||||||
|
allocation_id: undefined,
|
||||||
|
package_ids: [],
|
||||||
package_name: undefined,
|
package_name: undefined,
|
||||||
package_code: undefined,
|
package_code: undefined,
|
||||||
cost_price_yuan: 0,
|
cost_price_yuan: 0,
|
||||||
expiry_base_override: 'default',
|
expiry_base_override: 'default',
|
||||||
initial_expiry_base_override: 'default'
|
initial_expiry_base_override: 'default',
|
||||||
|
packages: []
|
||||||
})
|
})
|
||||||
const packageForm = ref<PackageFormState>(createDefaultPackageForm())
|
const packageForm = ref<PackageFormState>(createDefaultPackageForm())
|
||||||
|
const expiryForm = ref<ExpiryFormState>({ expiry_base_override: 'default' })
|
||||||
|
|
||||||
const assignPackageForm = (values: Partial<PackageFormState> = {}) => {
|
const assignPackageForm = (values: Partial<PackageFormState> = {}) => {
|
||||||
Object.assign(packageForm.value, createDefaultPackageForm(), values)
|
Object.assign(packageForm.value, createDefaultPackageForm(), values)
|
||||||
@@ -271,11 +383,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const packageRules = computed<FormRules>(() => ({
|
const packageRules = computed<FormRules>(() => ({
|
||||||
package_id: [
|
package_ids: [
|
||||||
{ required: packageDialogType.value === 'add', message: '请选择套餐', trigger: 'change' }
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (packageDialogType.value !== 'add') {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!Array.isArray(value) || value.length === 0) {
|
||||||
|
callback(new Error('请至少选择一个套餐'))
|
||||||
|
} else if (value.length > 100) {
|
||||||
|
callback(new Error('最多选择100个套餐'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
cost_price_yuan: [
|
cost_price_yuan: [
|
||||||
{ required: true, message: '请输入成本价', trigger: 'blur' },
|
{
|
||||||
|
required: packageDialogType.value === 'edit',
|
||||||
|
message: '请输入成本价',
|
||||||
|
trigger: 'blur'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '') {
|
||||||
@@ -294,6 +425,14 @@
|
|||||||
const getActions = (row: GrantPackageInfo) => {
|
const getActions = (row: GrantPackageInfo) => {
|
||||||
const actions: any[] = []
|
const actions: any[] = []
|
||||||
|
|
||||||
|
if (canUpdateExpiryBase) {
|
||||||
|
actions.push({
|
||||||
|
label: '修改生效条件',
|
||||||
|
handler: () => showExpiryBaseDialog(row),
|
||||||
|
type: 'primary'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (hasAuth('series_grants:edit_packages_list')) {
|
if (hasAuth('series_grants:edit_packages_list')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
@@ -338,7 +477,7 @@
|
|||||||
|
|
||||||
const showAddPackageDialog = () => {
|
const showAddPackageDialog = () => {
|
||||||
packageDialogType.value = 'add'
|
packageDialogType.value = 'add'
|
||||||
assignPackageForm()
|
assignPackageForm({ package_ids: [], packages: [] })
|
||||||
loadAvailablePackages()
|
loadAvailablePackages()
|
||||||
packageDialogVisible.value = true
|
packageDialogVisible.value = true
|
||||||
nextTick(() => packageFormRef.value?.clearValidate())
|
nextTick(() => packageFormRef.value?.clearValidate())
|
||||||
@@ -350,12 +489,12 @@
|
|||||||
|
|
||||||
assignPackageForm({
|
assignPackageForm({
|
||||||
package_id: row.package_id,
|
package_id: row.package_id,
|
||||||
|
allocation_id: row.allocation_id,
|
||||||
package_name: row.package_name,
|
package_name: row.package_name,
|
||||||
package_code: row.package_code,
|
package_code: row.package_code,
|
||||||
cost_price_yuan: row.cost_price / 100,
|
cost_price_yuan: row.cost_price / 100,
|
||||||
original_cost_price: packagePricing.original_cost_price,
|
original_cost_price: packagePricing.original_cost_price,
|
||||||
suggested_retail_price: packagePricing.suggested_retail_price,
|
suggested_retail_price: packagePricing.suggested_retail_price,
|
||||||
allocation_id: row.allocation_id,
|
|
||||||
expiry_base_override: row.expiry_base_override ?? 'default',
|
expiry_base_override: row.expiry_base_override ?? 'default',
|
||||||
initial_expiry_base_override: row.expiry_base_override ?? 'default',
|
initial_expiry_base_override: row.expiry_base_override ?? 'default',
|
||||||
default_expiry_base_name: row.default_expiry_base_name,
|
default_expiry_base_name: row.default_expiry_base_name,
|
||||||
@@ -366,25 +505,43 @@
|
|||||||
nextTick(() => packageFormRef.value?.clearValidate())
|
nextTick(() => packageFormRef.value?.clearValidate())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showExpiryBaseDialog = (row: GrantPackageInfo) => {
|
||||||
|
if (row.allocation_id === undefined || row.allocation_id === null) {
|
||||||
|
ElMessage.error('该套餐分配记录缺少套餐分配 ID,无法修改生效条件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiryForm.value = {
|
||||||
|
package_id: row.package_id,
|
||||||
|
allocation_id: row.allocation_id,
|
||||||
|
package_name: row.package_name,
|
||||||
|
package_code: row.package_code,
|
||||||
|
default_expiry_base_name: row.default_expiry_base_name,
|
||||||
|
expiry_base_override: row.expiry_base_override ?? 'default',
|
||||||
|
expiry_base_override_name: row.expiry_base_override_name,
|
||||||
|
effective_expiry_base_name: row.effective_expiry_base_name
|
||||||
|
}
|
||||||
|
expiryDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const loadAvailablePackages = async (packageName?: string) => {
|
const loadAvailablePackages = async (packageName?: string) => {
|
||||||
if (!seriesId.value) return
|
if (!seriesId.value) return
|
||||||
|
|
||||||
packageLoading.value = true
|
packageLoading.value = true
|
||||||
try {
|
try {
|
||||||
const pageSize = 100
|
const pageSize = 100
|
||||||
const allPackages: any[] = []
|
const allPackages: any[] = []
|
||||||
let page = 1
|
let page = 1
|
||||||
let total = 0
|
let total = 0
|
||||||
do {
|
do {
|
||||||
const params: any = { page, page_size: pageSize, series_id: seriesId.value }
|
const params: any = { page, page_size: pageSize, series_id: seriesId.value }
|
||||||
if (packageName) params.package_name = packageName
|
if (packageName) params.package_name = packageName
|
||||||
const res = await PackageManageService.getPackages(params)
|
const res = await PackageManageService.getPackages(params)
|
||||||
if (res.code !== 0) break
|
if (res.code !== 0) break
|
||||||
allPackages.push(...(res.data.items || []))
|
allPackages.push(...(res.data.items || []))
|
||||||
total = res.data.total || allPackages.length
|
total = res.data.total || allPackages.length
|
||||||
page += 1
|
page += 1
|
||||||
} while (allPackages.length < total)
|
} while (allPackages.length < total)
|
||||||
availablePackages.value = mergeGrantPackageCandidates(allPackages, packageList.value)
|
availablePackages.value = mergeGrantPackageCandidates(allPackages, packageList.value)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载套餐选项失败:', error)
|
console.error('加载套餐选项失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -400,6 +557,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createPackageCostFormItem = (packageId: number): PackageCostFormItem => {
|
||||||
|
const selectedPackage = availablePackages.value.find((pkg) => pkg.id === packageId)
|
||||||
|
const originalCostPrice = selectedPackage?.cost_price ? selectedPackage.cost_price / 100 : 0
|
||||||
|
return {
|
||||||
|
package_id: packageId,
|
||||||
|
package_name: selectedPackage?.package_name,
|
||||||
|
package_code: selectedPackage?.package_code,
|
||||||
|
cost_price_yuan: originalCostPrice,
|
||||||
|
original_cost_price: originalCostPrice,
|
||||||
|
suggested_retail_price:
|
||||||
|
selectedPackage?.suggested_retail_price !== undefined &&
|
||||||
|
selectedPackage?.suggested_retail_price !== null
|
||||||
|
? selectedPackage.suggested_retail_price / 100
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => packageForm.value.package_ids,
|
||||||
|
(packageIds) => {
|
||||||
|
if (packageDialogType.value !== 'add') return
|
||||||
|
const existingItems = new Map(
|
||||||
|
packageForm.value.packages.map((item) => [item.package_id, item])
|
||||||
|
)
|
||||||
|
packageForm.value.packages = packageIds.map(
|
||||||
|
(packageId) => existingItems.get(packageId) || createPackageCostFormItem(packageId)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => packageForm.value.package_id,
|
() => packageForm.value.package_id,
|
||||||
(packageId) => {
|
(packageId) => {
|
||||||
@@ -436,19 +637,51 @@
|
|||||||
}
|
}
|
||||||
const costPrice = Math.round(currentCostPriceYuan * 100)
|
const costPrice = Math.round(currentCostPriceYuan * 100)
|
||||||
if (packageDialogType.value === 'add') {
|
if (packageDialogType.value === 'add') {
|
||||||
|
if (packageForm.value.packages.length === 0) {
|
||||||
|
ElMessage.error('请至少选择一个套餐')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const packages: GrantPackageItem[] = packageForm.value.packages.map((pkg) => {
|
||||||
|
const currentCostPrice = Number(pkg.cost_price_yuan)
|
||||||
|
if (!Number.isFinite(currentCostPrice) || currentCostPrice < 0) {
|
||||||
|
throw new Error(`套餐 ${pkg.package_name || pkg.package_id} 的成本价无效`)
|
||||||
|
}
|
||||||
|
const maxCostPrice = getPackageCostPriceMax(pkg)
|
||||||
|
if (maxCostPrice !== undefined && currentCostPrice > maxCostPrice) {
|
||||||
|
throw new Error(
|
||||||
|
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能高于 ¥${maxCostPrice.toFixed(2)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
pkg.original_cost_price !== undefined &&
|
||||||
|
currentCostPrice < pkg.original_cost_price
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能低于 ¥${pkg.original_cost_price.toFixed(2)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
package_id: pkg.package_id,
|
||||||
|
cost_price: Math.round(currentCostPrice * 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
await ShopSeriesGrantService.manageGrantPackages(grantId.value, {
|
await ShopSeriesGrantService.manageGrantPackages(grantId.value, {
|
||||||
expiry_base_override:
|
expiry_base_override:
|
||||||
packageForm.value.expiry_base_override === 'default'
|
packageForm.value.expiry_base_override === 'default'
|
||||||
? null
|
? null
|
||||||
: packageForm.value.expiry_base_override,
|
: packageForm.value.expiry_base_override,
|
||||||
packages: [{ package_id: packageForm.value.package_id!, cost_price: costPrice }]
|
packages
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const expiryBaseChanged =
|
const expiryBaseChanged =
|
||||||
packageForm.value.expiry_base_override !==
|
packageForm.value.expiry_base_override !==
|
||||||
packageForm.value.initial_expiry_base_override
|
packageForm.value.initial_expiry_base_override
|
||||||
if (expiryBaseChanged && !packageForm.value.allocation_id) {
|
if (
|
||||||
ElMessage.error('该套餐分配记录缺少ID,无法更新生效条件')
|
expiryBaseChanged &&
|
||||||
|
(packageForm.value.allocation_id === undefined ||
|
||||||
|
packageForm.value.allocation_id === null)
|
||||||
|
) {
|
||||||
|
ElMessage.error('该套餐分配记录缺少套餐分配 ID,无法修改生效条件')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,13 +710,39 @@
|
|||||||
packageDialogVisible.value = false
|
packageDialogVisible.value = false
|
||||||
await fetchPackageList()
|
await fetchPackageList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
ElMessage.error(error instanceof Error ? error.message : '保存套餐失败')
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveExpiryBase = async () => {
|
||||||
|
if (expiryForm.value.allocation_id === undefined || expiryForm.value.allocation_id === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expirySubmitLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
||||||
|
expiryForm.value.allocation_id,
|
||||||
|
{
|
||||||
|
expiry_base_override:
|
||||||
|
expiryForm.value.expiry_base_override === 'default'
|
||||||
|
? null
|
||||||
|
: expiryForm.value.expiry_base_override
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (response.code === 0) {
|
||||||
|
ElMessage.success('生效条件更新成功')
|
||||||
|
expiryDialogVisible.value = false
|
||||||
|
await fetchPackageList()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
expirySubmitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDeletePackage = (row: GrantPackageInfo) => {
|
const handleDeletePackage = (row: GrantPackageInfo) => {
|
||||||
import('element-plus').then(({ ElMessageBox }) => {
|
import('element-plus').then(({ ElMessageBox }) => {
|
||||||
ElMessageBox.confirm(`确定删除套餐 ${row.package_name} 的授权吗?`, '删除确认', {
|
ElMessageBox.confirm(`确定删除套餐 ${row.package_name} 的授权吗?`, '删除确认', {
|
||||||
@@ -572,6 +831,38 @@
|
|||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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,
|
||||||
|
.ellipsis-value {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-config-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis-value {
|
||||||
|
display: block;
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,16 +44,13 @@
|
|||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
>
|
>
|
||||||
<ElForm label-width="120px">
|
<ElForm label-width="120px">
|
||||||
<ElFormItem label="配置 Key">
|
|
||||||
<ElInput :model-value="currentConfig?.config_key || '-'" disabled />
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="配置说明">
|
<ElFormItem label="配置说明">
|
||||||
<span>{{ currentConfig?.description || '-' }}</span>
|
<span>{{ currentConfig?.description || '-' }}</span>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="配置值">
|
<ElFormItem label="配置值">
|
||||||
<ElCheckboxGroup v-if="isPaymentConfig" v-model="editPaymentMethods">
|
<ElCheckboxGroup v-if="isPaymentConfig" v-model="editPaymentMethods">
|
||||||
<ElCheckbox v-for="method in paymentMethods" :key="method" :label="method">
|
<ElCheckbox v-for="method in paymentMethods" :key="method" :label="method">
|
||||||
{{ method }}
|
{{ paymentMethodLabels[method] }}
|
||||||
</ElCheckbox>
|
</ElCheckbox>
|
||||||
</ElCheckboxGroup>
|
</ElCheckboxGroup>
|
||||||
<ElSwitch v-else-if="isBooleanConfig" v-model="editBoolean" />
|
<ElSwitch v-else-if="isBooleanConfig" v-model="editBoolean" />
|
||||||
@@ -89,7 +86,6 @@
|
|||||||
placeholder="请输入配置值"
|
placeholder="请输入配置值"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<div v-if="valueHint" class="config-hint">{{ valueHint }}</div>
|
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -103,6 +99,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElTag } from 'element-plus'
|
import { ElMessage, ElTag } from 'element-plus'
|
||||||
import { SystemConfigService } from '@/api/modules'
|
import { SystemConfigService } from '@/api/modules'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
@@ -120,6 +117,7 @@
|
|||||||
|
|
||||||
defineOptions({ name: 'SystemConfigs' })
|
defineOptions({ name: 'SystemConfigs' })
|
||||||
const { hasAuth } = useAuth()
|
const { hasAuth } = useAuth()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const moduleOptions = [
|
const moduleOptions = [
|
||||||
{ label: '运营商回调配置', value: 'carrier_callback' },
|
{ label: '运营商回调配置', value: 'carrier_callback' },
|
||||||
@@ -149,14 +147,18 @@
|
|||||||
const editNumber = ref<number | null>(null)
|
const editNumber = ref<number | null>(null)
|
||||||
const editPaymentMethods = ref<AllowedPaymentMethod[]>([])
|
const editPaymentMethods = ref<AllowedPaymentMethod[]>([])
|
||||||
const paymentMethods: AllowedPaymentMethod[] = ['wallet', 'wechat', 'alipay']
|
const paymentMethods: AllowedPaymentMethod[] = ['wallet', 'wechat', 'alipay']
|
||||||
|
const paymentMethodLabels: Record<AllowedPaymentMethod, string> = {
|
||||||
|
wallet: '钱包',
|
||||||
|
wechat: '微信',
|
||||||
|
alipay: '支付宝'
|
||||||
|
}
|
||||||
|
const targetConfigHandled = ref(false)
|
||||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||||
|
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: '配置 Key', prop: 'config_key' },
|
|
||||||
{ label: '模块', prop: 'module' },
|
{ label: '模块', prop: 'module' },
|
||||||
{ label: '配置说明', prop: 'description' },
|
{ label: '配置说明', prop: 'description' },
|
||||||
{ label: '配置值', prop: 'value' },
|
{ label: '配置值', prop: 'value' },
|
||||||
{ label: '控件', prop: 'control' },
|
|
||||||
{ label: '注册状态', prop: 'registered' },
|
{ label: '注册状态', prop: 'registered' },
|
||||||
{ label: '只读', prop: 'readonly' },
|
{ label: '只读', prop: 'readonly' },
|
||||||
{ label: '敏感', prop: 'sensitive' },
|
{ label: '敏感', prop: 'sensitive' },
|
||||||
@@ -167,7 +169,6 @@
|
|||||||
moduleOptions.find((item) => item.value === module)?.label || module
|
moduleOptions.find((item) => item.value === module)?.label || module
|
||||||
|
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{ prop: 'config_key', label: '配置 Key', minWidth: 220, showOverflowTooltip: true },
|
|
||||||
{
|
{
|
||||||
prop: 'module',
|
prop: 'module',
|
||||||
label: '模块',
|
label: '模块',
|
||||||
@@ -178,10 +179,10 @@
|
|||||||
{
|
{
|
||||||
prop: 'value',
|
prop: 'value',
|
||||||
label: '配置值',
|
label: '配置值',
|
||||||
minWidth: 150,
|
minWidth: 260,
|
||||||
formatter: (row: SystemConfigItem) => row.value || '-'
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: SystemConfigItem) => formatConfigValue(row)
|
||||||
},
|
},
|
||||||
{ prop: 'control', label: '控件', width: 100 },
|
|
||||||
{
|
{
|
||||||
prop: 'registered',
|
prop: 'registered',
|
||||||
label: '注册状态',
|
label: '注册状态',
|
||||||
@@ -216,7 +217,8 @@
|
|||||||
)
|
)
|
||||||
const isPaymentConfig = computed(() =>
|
const isPaymentConfig = computed(() =>
|
||||||
[PAYMENT_CONFIG_KEYS.card, PAYMENT_CONFIG_KEYS.device].includes(
|
[PAYMENT_CONFIG_KEYS.card, PAYMENT_CONFIG_KEYS.device].includes(
|
||||||
currentConfig.value?.config_key as (typeof PAYMENT_CONFIG_KEYS)[keyof typeof PAYMENT_CONFIG_KEYS]
|
currentConfig.value
|
||||||
|
?.config_key as (typeof PAYMENT_CONFIG_KEYS)[keyof typeof PAYMENT_CONFIG_KEYS]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
const isIntegerConfig = computed(
|
const isIntegerConfig = computed(
|
||||||
@@ -233,14 +235,18 @@
|
|||||||
!isBooleanConfig.value && !isIntegerConfig.value && currentConfig.value?.value_type === 'json'
|
!isBooleanConfig.value && !isIntegerConfig.value && currentConfig.value?.value_type === 'json'
|
||||||
)
|
)
|
||||||
|
|
||||||
const valueHint = computed(() => {
|
const isPaymentConfigKey = (configKey: string) =>
|
||||||
if (!currentConfig.value) return ''
|
Object.values(PAYMENT_CONFIG_KEYS).includes(
|
||||||
const { min, max } = currentConfig.value
|
configKey as (typeof PAYMENT_CONFIG_KEYS)[keyof typeof PAYMENT_CONFIG_KEYS]
|
||||||
if (min !== null && max !== null) return `取值范围:${min}~${max}`
|
)
|
||||||
if (min !== null) return `最小值:${min}`
|
|
||||||
if (max !== null) return `最大值:${max}`
|
const formatConfigValue = (config: SystemConfigItem) => {
|
||||||
return ''
|
if (isPaymentConfigKey(config.config_key)) {
|
||||||
})
|
const methods = parsePaymentMethods(config.value)
|
||||||
|
return methods.length ? methods.map((method) => paymentMethodLabels[method]).join('、') : '-'
|
||||||
|
}
|
||||||
|
return config.value || '-'
|
||||||
|
}
|
||||||
|
|
||||||
const getSubmittedValue = () => {
|
const getSubmittedValue = () => {
|
||||||
if (isPaymentConfig.value) return JSON.stringify(editPaymentMethods.value)
|
if (isPaymentConfig.value) return JSON.stringify(editPaymentMethods.value)
|
||||||
@@ -293,6 +299,12 @@
|
|||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
configList.value = res.data.list || []
|
configList.value = res.data.list || []
|
||||||
pagination.total = res.data.total || 0
|
pagination.total = res.data.total || 0
|
||||||
|
const targetKey = String(route.query.config_key || '')
|
||||||
|
const targetConfig = configList.value.find((config) => config.config_key === targetKey)
|
||||||
|
if (targetKey && targetConfig && !targetConfigHandled.value) {
|
||||||
|
targetConfigHandled.value = true
|
||||||
|
showEditDialog(targetConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取系统配置失败:', error)
|
console.error('获取系统配置失败:', error)
|
||||||
@@ -370,10 +382,5 @@
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.system-configs-page {
|
.system-configs-page {
|
||||||
.config-hint {
|
|
||||||
margin: -12px 0 0 120px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<ElDialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="isEdit ? '编辑企微应用' : '新增企微应用'"
|
||||||
|
width="50%"
|
||||||
|
destroy-on-close
|
||||||
|
@closed="resetForm"
|
||||||
|
>
|
||||||
|
<div class="dialog-description">应用凭据由平台管理员维护,保存后可在应用列表测试连接。</div>
|
||||||
|
|
||||||
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="130px" class="application-form">
|
||||||
|
<ElRow :gutter="24">
|
||||||
|
<ElCol :xs="24" :sm="12">
|
||||||
|
<ElFormItem label="企业 ID" prop="corp_id">
|
||||||
|
<ElInput v-model="form.corp_id" placeholder="请输入企业微信 CorpID" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :xs="24" :sm="12">
|
||||||
|
<ElFormItem label="AgentID" prop="agent_id">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="form.agent_id"
|
||||||
|
:min="1"
|
||||||
|
controls-position="right"
|
||||||
|
class="full-width"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :xs="24" :sm="12">
|
||||||
|
<ElFormItem label="应用名称" prop="name">
|
||||||
|
<ElInput v-model="form.name" placeholder="请输入应用名称" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :xs="24" :sm="12">
|
||||||
|
<ElFormItem label="Secret" prop="secret">
|
||||||
|
<ElInput
|
||||||
|
v-model="form.secret"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="请输入应用 Secret"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
|
||||||
|
<ElDivider content-position="left">回调配置</ElDivider>
|
||||||
|
<ElRow :gutter="24">
|
||||||
|
<ElCol :span="24">
|
||||||
|
<ElFormItem label="回调 Token" prop="callback_token">
|
||||||
|
<ElInput
|
||||||
|
v-model="form.callback_token"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
resize="vertical"
|
||||||
|
placeholder="请输入回调 Token"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="24">
|
||||||
|
<ElFormItem label="EncodingAESKey" prop="encoding_aes_key">
|
||||||
|
<ElInput
|
||||||
|
v-model="form.encoding_aes_key"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
resize="vertical"
|
||||||
|
placeholder="请输入 43 位 EncodingAESKey"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
|
||||||
|
<ElFormItem label="启用状态">
|
||||||
|
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="禁用" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="visible = false">取消</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="isEdit"
|
||||||
|
v-permission="JULY_PERMISSIONS.wecom.application"
|
||||||
|
:loading="testing"
|
||||||
|
@click="testApplication"
|
||||||
|
>
|
||||||
|
测试连接
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-permission="JULY_PERMISSIONS.wecom.application"
|
||||||
|
type="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="saveApplication"
|
||||||
|
>
|
||||||
|
保存应用
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { WecomService } from '@/api/modules'
|
||||||
|
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||||
|
import type { WecomApplication } from '@/types/api'
|
||||||
|
|
||||||
|
defineOptions({ name: 'WecomApplicationFormDialog' })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
application: WecomApplication | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: boolean): void
|
||||||
|
(event: 'success'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: boolean) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
const isEdit = computed(() => Boolean(props.application?.id))
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const saving = ref(false)
|
||||||
|
const testing = ref(false)
|
||||||
|
const form = reactive({
|
||||||
|
corp_id: '',
|
||||||
|
agent_id: 0,
|
||||||
|
name: '',
|
||||||
|
secret: '',
|
||||||
|
callback_token: '',
|
||||||
|
encoding_aes_key: '',
|
||||||
|
enabled: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
corp_id: [{ required: true, message: '请输入企业 ID', trigger: 'blur' }],
|
||||||
|
agent_id: [{ required: true, message: '请输入 AgentID', trigger: 'change' }],
|
||||||
|
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
|
||||||
|
secret: [{ required: true, message: '请输入 Secret', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const fillForm = (application: WecomApplication | null) => {
|
||||||
|
Object.assign(
|
||||||
|
form,
|
||||||
|
application
|
||||||
|
? {
|
||||||
|
corp_id: application.corp_id,
|
||||||
|
agent_id: application.agent_id,
|
||||||
|
name: application.name,
|
||||||
|
secret: application.secret || '',
|
||||||
|
callback_token: application.callback_token || '',
|
||||||
|
encoding_aes_key: application.encoding_aes_key || '',
|
||||||
|
enabled: application.status === 1
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
corp_id: '',
|
||||||
|
agent_id: 0,
|
||||||
|
name: '',
|
||||||
|
secret: '',
|
||||||
|
callback_token: '',
|
||||||
|
encoding_aes_key: '',
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
fillForm(null)
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveApplication = async () => {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const response = await WecomService.saveApplication({
|
||||||
|
corp_id: form.corp_id,
|
||||||
|
agent_id: form.agent_id,
|
||||||
|
name: form.name,
|
||||||
|
secret: form.secret,
|
||||||
|
callback_token: form.callback_token,
|
||||||
|
encoding_aes_key: form.encoding_aes_key,
|
||||||
|
status: form.enabled ? 1 : 0
|
||||||
|
})
|
||||||
|
if (response.code === 0) {
|
||||||
|
ElMessage.success('企微应用保存成功')
|
||||||
|
emit('success')
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testApplication = async () => {
|
||||||
|
if (!props.application?.id) return
|
||||||
|
|
||||||
|
testing.value = true
|
||||||
|
try {
|
||||||
|
const response = await WecomService.testApplication(props.application.id)
|
||||||
|
if (response.code === 0) {
|
||||||
|
if (response.data.success) ElMessage.success('连接成功,已取得 access_token')
|
||||||
|
else ElMessage.warning('连接失败,请检查应用凭据')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
testing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.modelValue, props.application] as const,
|
||||||
|
([opened]) => {
|
||||||
|
if (opened) fillForm(props.application)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dialog-description {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-form {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="wecom-application-detail-page">
|
|
||||||
<ElCard shadow="never">
|
|
||||||
<template #header>
|
|
||||||
<div class="page-header">
|
|
||||||
<div>
|
|
||||||
<div class="page-title">{{ isEdit ? '编辑企微应用' : '新增企微应用' }}</div>
|
|
||||||
<div class="page-description"
|
|
||||||
>应用凭据由平台管理员维护,保存后可在应用列表测试连接。</div
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<ElButton @click="goBack">返回应用列表</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElForm
|
|
||||||
ref="formRef"
|
|
||||||
:model="form"
|
|
||||||
:rules="rules"
|
|
||||||
label-width="140px"
|
|
||||||
class="application-form"
|
|
||||||
>
|
|
||||||
<ElRow :gutter="24">
|
|
||||||
<ElCol :xs="24" :sm="12">
|
|
||||||
<ElFormItem label="企业 ID" prop="corp_id">
|
|
||||||
<ElInput v-model="form.corp_id" placeholder="请输入企业微信 CorpID" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :xs="24" :sm="12">
|
|
||||||
<ElFormItem label="AgentID" prop="agent_id">
|
|
||||||
<ElInputNumber
|
|
||||||
v-model="form.agent_id"
|
|
||||||
:min="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="full-width"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :xs="24" :sm="12">
|
|
||||||
<ElFormItem label="应用名称" prop="name">
|
|
||||||
<ElInput v-model="form.name" placeholder="请输入应用名称" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :xs="24" :sm="12">
|
|
||||||
<ElFormItem label="Secret" prop="secret">
|
|
||||||
<ElInput
|
|
||||||
v-model="form.secret"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="请输入应用 Secret"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
</ElRow>
|
|
||||||
|
|
||||||
<ElDivider content-position="left">回调配置</ElDivider>
|
|
||||||
<ElRow :gutter="24">
|
|
||||||
<ElCol :xs="24" :sm="12">
|
|
||||||
<ElFormItem label="回调 Token" prop="callback_token">
|
|
||||||
<ElInput v-model="form.callback_token" placeholder="请输入回调 Token" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :xs="24" :sm="12">
|
|
||||||
<ElFormItem label="EncodingAESKey" prop="encoding_aes_key">
|
|
||||||
<ElInput v-model="form.encoding_aes_key" placeholder="请输入 43 位 EncodingAESKey" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
</ElRow>
|
|
||||||
|
|
||||||
<ElFormItem label="启用状态">
|
|
||||||
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="禁用" />
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<ElButton @click="goBack">取消</ElButton>
|
|
||||||
<ElButton
|
|
||||||
v-if="isEdit"
|
|
||||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
|
||||||
:loading="testing"
|
|
||||||
@click="testApplication"
|
|
||||||
>
|
|
||||||
测试连接
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
|
||||||
type="primary"
|
|
||||||
:loading="saving"
|
|
||||||
@click="saveApplication"
|
|
||||||
>
|
|
||||||
保存应用
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</ElForm>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import { WecomService } from '@/api/modules'
|
|
||||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
|
||||||
import type { WecomApplication } from '@/types/api'
|
|
||||||
|
|
||||||
defineOptions({ name: 'WecomApplicationDetail' })
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
const formRef = ref<FormInstance>()
|
|
||||||
const saving = ref(false)
|
|
||||||
const testing = ref(false)
|
|
||||||
const applicationId = computed(() => Number(route.params.id) || 0)
|
|
||||||
const isEdit = computed(() => applicationId.value > 0)
|
|
||||||
const form = reactive({
|
|
||||||
corp_id: '',
|
|
||||||
agent_id: 0,
|
|
||||||
name: '',
|
|
||||||
secret: '',
|
|
||||||
callback_token: '',
|
|
||||||
encoding_aes_key: '',
|
|
||||||
enabled: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const rules = reactive<FormRules>({
|
|
||||||
corp_id: [{ required: true, message: '请输入企业 ID', trigger: 'blur' }],
|
|
||||||
agent_id: [{ required: true, message: '请输入 AgentID', trigger: 'change' }],
|
|
||||||
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
|
|
||||||
secret: [{ required: true, message: '请输入 Secret', trigger: 'blur' }]
|
|
||||||
})
|
|
||||||
|
|
||||||
const fillForm = (application?: WecomApplication) => {
|
|
||||||
Object.assign(
|
|
||||||
form,
|
|
||||||
application
|
|
||||||
? {
|
|
||||||
corp_id: application.corp_id,
|
|
||||||
agent_id: application.agent_id,
|
|
||||||
name: application.name,
|
|
||||||
secret: application.secret || '',
|
|
||||||
callback_token: application.callback_token || '',
|
|
||||||
encoding_aes_key: application.encoding_aes_key || '',
|
|
||||||
enabled: application.status === 1
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
corp_id: '',
|
|
||||||
agent_id: 0,
|
|
||||||
name: '',
|
|
||||||
secret: '',
|
|
||||||
callback_token: '',
|
|
||||||
encoding_aes_key: '',
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadApplication = async () => {
|
|
||||||
if (!isEdit.value) {
|
|
||||||
fillForm()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const response = await WecomService.getApplications({ page: 1, page_size: 100 })
|
|
||||||
if (response.code === 0) {
|
|
||||||
const application = response.data.items?.find((item) => item.id === applicationId.value)
|
|
||||||
if (application) fillForm(application)
|
|
||||||
else {
|
|
||||||
ElMessage.error('未找到对应的企微应用')
|
|
||||||
goBack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveApplication = async () => {
|
|
||||||
const valid = await formRef.value?.validate().catch(() => false)
|
|
||||||
if (!valid) return
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
const response = await WecomService.saveApplication({
|
|
||||||
corp_id: form.corp_id,
|
|
||||||
agent_id: form.agent_id,
|
|
||||||
name: form.name,
|
|
||||||
secret: form.secret,
|
|
||||||
callback_token: form.callback_token,
|
|
||||||
encoding_aes_key: form.encoding_aes_key,
|
|
||||||
status: form.enabled ? 1 : 0
|
|
||||||
})
|
|
||||||
if (response.code === 0) {
|
|
||||||
ElMessage.success('企微应用保存成功')
|
|
||||||
await router.push(RoutesAlias.WecomApplications)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const testApplication = async () => {
|
|
||||||
if (!applicationId.value) return
|
|
||||||
testing.value = true
|
|
||||||
try {
|
|
||||||
const response = await WecomService.testApplication(applicationId.value)
|
|
||||||
if (response.code === 0) {
|
|
||||||
if (response.data.success) ElMessage.success('连接成功,已取得 access_token')
|
|
||||||
else ElMessage.warning('连接失败,请检查应用凭据')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
testing.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const goBack = () => void router.push(RoutesAlias.WecomApplications)
|
|
||||||
|
|
||||||
onMounted(() => void loadApplication())
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.wecom-application-detail-page {
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-description {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-form {
|
|
||||||
max-width: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-width {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,107 +1,128 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wecom-applications-page">
|
<ArtTableFullScreen>
|
||||||
<ElCard shadow="never">
|
<div id="table-full-screen" class="wecom-applications-page">
|
||||||
<template #header>
|
<ElCard shadow="never" class="art-table-card">
|
||||||
<div class="page-header">
|
<ArtTableHeader
|
||||||
<div>
|
:column-list="columnOptions"
|
||||||
<div class="page-title">企微应用管理</div>
|
v-model:columns="columnChecks"
|
||||||
<div class="page-description">管理企业微信应用凭据,并查看连接和默认发起人状态。</div>
|
@refresh="loadApplications"
|
||||||
</div>
|
>
|
||||||
<ElButton
|
<template #left>
|
||||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
|
||||||
type="primary"
|
|
||||||
@click="goToCreate"
|
|
||||||
>
|
|
||||||
新增应用
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElTable v-loading="loading" :data="applications" border>
|
|
||||||
<ElTableColumn prop="name" label="应用名称" min-width="160" show-overflow-tooltip />
|
|
||||||
<ElTableColumn label="凭据状态" width="110">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElTag :type="scope.row.credentials_set ? 'success' : 'warning'" size="small">
|
|
||||||
{{ scope.row.credentials_set ? '已完整' : '未完整' }}
|
|
||||||
</ElTag>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="启用状态" width="100">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElTag :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
|
|
||||||
{{ scope.row.status_name }}
|
|
||||||
</ElTag>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="默认发起人" min-width="150">
|
|
||||||
<template #default="scope">{{ scope.row.default_creator_name || '未设置' }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="最近连接" min-width="170">
|
|
||||||
<template #default="scope">{{ formatDate(scope.row.last_connected_at) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="操作" width="300" fixed="right">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElButton
|
<ElButton
|
||||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
v-permission="JULY_PERMISSIONS.wecom.application"
|
||||||
link
|
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="testApplication(scope.row.id)"
|
@click="openCreateDialog"
|
||||||
>
|
>
|
||||||
测试连接
|
新增应用
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
v-permission="JULY_PERMISSIONS.wecom.member"
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
@click="goToMembers(scope.row.id)"
|
|
||||||
>
|
|
||||||
成员管理
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
|
||||||
link
|
|
||||||
@click="goToDetail(scope.row.id)"
|
|
||||||
>
|
|
||||||
编辑配置
|
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ArtTableHeader>
|
||||||
</ElTable>
|
|
||||||
|
|
||||||
<div class="pagination-wrapper">
|
<ArtTable
|
||||||
<ElPagination
|
ref="tableRef"
|
||||||
v-model:current-page="pagination.page"
|
row-key="id"
|
||||||
v-model:page-size="pagination.page_size"
|
:loading="loading"
|
||||||
|
:data="applications"
|
||||||
|
:current-page="pagination.page"
|
||||||
|
:page-size="pagination.page_size"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
:page-sizes="[20, 50, 100]"
|
:margin-top="10"
|
||||||
layout="total, sizes, prev, pager, next"
|
:actions="getActions"
|
||||||
|
:actions-width="160"
|
||||||
|
:inline-actions-count="1"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="loadApplications"
|
@current-change="loadApplications"
|
||||||
/>
|
>
|
||||||
</div>
|
<template #default>
|
||||||
</ElCard>
|
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||||
</div>
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WecomApplicationFormDialog
|
||||||
|
v-model="applicationDialogVisible"
|
||||||
|
:application="editingApplication"
|
||||||
|
@success="handleApplicationSaved"
|
||||||
|
/>
|
||||||
|
</ArtTableFullScreen>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { h, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElTag } from 'element-plus'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { WecomService } from '@/api/modules'
|
import { WecomService } from '@/api/modules'
|
||||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
import type { WecomApplication } from '@/types/api'
|
import type { WecomApplication } from '@/types/api'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import WecomApplicationFormDialog from './components/WecomApplicationFormDialog.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'WecomApplications' })
|
defineOptions({ name: 'WecomApplications' })
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const tableRef = ref()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const applications = ref<WecomApplication[]>([])
|
const applications = ref<WecomApplication[]>([])
|
||||||
|
const applicationDialogVisible = ref(false)
|
||||||
|
const editingApplication = ref<WecomApplication | null>(null)
|
||||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||||
|
|
||||||
const formatDate = (value?: string | null) => (value ? formatDateTime(value) : '-')
|
const formatDate = (value?: string | null) => (value ? formatDateTime(value) : '-')
|
||||||
|
|
||||||
|
const columnOptions = [
|
||||||
|
{ label: '应用名称', prop: 'name' },
|
||||||
|
{ label: '凭据状态', prop: 'credentials_set' },
|
||||||
|
{ label: '启用状态', prop: 'status' },
|
||||||
|
{ label: '默认发起人', prop: 'default_creator_name' },
|
||||||
|
{ label: '最近连接', prop: 'last_connected_at' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
|
{
|
||||||
|
prop: 'name',
|
||||||
|
label: '应用名称',
|
||||||
|
minWidth: 180,
|
||||||
|
showOverflowTooltip: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'credentials_set',
|
||||||
|
label: '凭据状态',
|
||||||
|
width: 110,
|
||||||
|
formatter: (row: WecomApplication) =>
|
||||||
|
h(ElTag, { type: row.credentials_set ? 'success' : 'warning', size: 'small' }, () =>
|
||||||
|
row.credentials_set ? '已完整' : '未完整'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'status',
|
||||||
|
label: '启用状态',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: WecomApplication) =>
|
||||||
|
h(
|
||||||
|
ElTag,
|
||||||
|
{ type: row.status === 1 ? 'success' : 'info', size: 'small' },
|
||||||
|
() => row.status_name
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'default_creator_name',
|
||||||
|
label: '默认发起人',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: (row: WecomApplication) => row.default_creator_name || '未设置'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'last_connected_at',
|
||||||
|
label: '最近连接',
|
||||||
|
width: 180,
|
||||||
|
formatter: (row: WecomApplication) => formatDate(row.last_connected_at)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
const loadApplications = async () => {
|
const loadApplications = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -124,8 +145,20 @@
|
|||||||
void loadApplications()
|
void loadApplications()
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToCreate = () => void router.push(`${RoutesAlias.WecomApplications}/create`)
|
const openCreateDialog = () => {
|
||||||
const goToDetail = (id: number) => void router.push(`${RoutesAlias.WecomApplicationDetail}/${id}`)
|
editingApplication.value = null
|
||||||
|
applicationDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditDialog = (application: WecomApplication) => {
|
||||||
|
editingApplication.value = application
|
||||||
|
applicationDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApplicationSaved = () => {
|
||||||
|
void loadApplications()
|
||||||
|
}
|
||||||
|
|
||||||
const goToMembers = (id: number) =>
|
const goToMembers = (id: number) =>
|
||||||
void router.push({ path: RoutesAlias.WecomMembers, query: { application_id: String(id) } })
|
void router.push({ path: RoutesAlias.WecomMembers, query: { application_id: String(id) } })
|
||||||
|
|
||||||
@@ -138,34 +171,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getActions = (row: WecomApplication) => {
|
||||||
|
const actions: any[] = []
|
||||||
|
if (hasAuth(JULY_PERMISSIONS.wecom.application)) {
|
||||||
|
actions.push({
|
||||||
|
label: '测试连接',
|
||||||
|
handler: () => testApplication(row.id),
|
||||||
|
type: 'primary'
|
||||||
|
})
|
||||||
|
actions.push({
|
||||||
|
label: '编辑配置',
|
||||||
|
handler: () => openEditDialog(row),
|
||||||
|
type: 'primary'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (hasAuth(JULY_PERMISSIONS.wecom.member)) {
|
||||||
|
actions.push({
|
||||||
|
label: '成员管理',
|
||||||
|
handler: () => goToMembers(row.id),
|
||||||
|
type: 'primary'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => void loadApplications())
|
onMounted(() => void loadApplications())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss"></style>
|
||||||
.wecom-applications-page {
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-description {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,167 +1,94 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wecom-members-page">
|
<ArtTableFullScreen>
|
||||||
<ElCard shadow="never">
|
<div id="table-full-screen" class="wecom-members-page">
|
||||||
<template #header>
|
<ArtSearchBar
|
||||||
<div class="page-header">
|
v-model:filter="searchForm"
|
||||||
<div>
|
:items="searchFormItems"
|
||||||
<div class="page-title">企微成员管理</div>
|
:show-expand="false"
|
||||||
<div class="page-description"
|
@reset="handleReset"
|
||||||
>同步应用可见成员,设置默认审批发起人,并完成平台账号绑定。</div
|
@search="handleSearch"
|
||||||
>
|
/>
|
||||||
</div>
|
|
||||||
<ElButton @click="goToApplications">返回应用列表</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElForm inline class="search-form" @submit.prevent>
|
<ElCard v-loading="loading" shadow="never" class="art-table-card members-card">
|
||||||
<ElFormItem label="企微应用">
|
<div class="members-toolbar">
|
||||||
<ElSelect
|
<div class="toolbar-actions">
|
||||||
v-model="selectedApplicationId"
|
|
||||||
filterable
|
|
||||||
class="application-select"
|
|
||||||
placeholder="请选择企微应用"
|
|
||||||
@change="handleApplicationChange"
|
|
||||||
>
|
|
||||||
<ElOption
|
|
||||||
v-for="application in applications"
|
|
||||||
:key="application.id"
|
|
||||||
:label="application.name"
|
|
||||||
:value="application.id"
|
|
||||||
/>
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="成员搜索">
|
|
||||||
<ElInput
|
|
||||||
v-model="keyword"
|
|
||||||
clearable
|
|
||||||
placeholder="姓名"
|
|
||||||
class="keyword-input"
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem>
|
|
||||||
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
|
||||||
<ElButton @click="handleReset">重置</ElButton>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElForm>
|
|
||||||
|
|
||||||
<div class="toolbar">
|
|
||||||
<div class="selected-application">
|
|
||||||
当前应用:<span>{{ selectedApplication?.name || '未选择' }}</span>
|
|
||||||
<ElTag v-if="selectedApplication?.default_creator_name" type="success" size="small">
|
|
||||||
默认发起人:{{ selectedApplication.default_creator_name }}
|
|
||||||
</ElTag>
|
|
||||||
</div>
|
|
||||||
<ElButton
|
|
||||||
v-permission="JULY_PERMISSIONS.wecom.member"
|
|
||||||
:disabled="!selectedApplicationId"
|
|
||||||
:loading="syncing"
|
|
||||||
@click="syncMembers"
|
|
||||||
>
|
|
||||||
同步成员
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ElTable
|
|
||||||
v-loading="loading"
|
|
||||||
:data="members"
|
|
||||||
row-key="userid"
|
|
||||||
highlight-current-row
|
|
||||||
border
|
|
||||||
@row-click="selectMember"
|
|
||||||
>
|
|
||||||
<ElTableColumn label="选择" width="70" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElRadio v-model="selectedUserid" :label="scope.row.userid">
|
|
||||||
<span class="sr-only">选择 {{ scope.row.name }}</span>
|
|
||||||
</ElRadio>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="name" label="成员姓名" min-width="150" />
|
|
||||||
<ElTableColumn prop="synced_at" label="同步时间" min-width="180" />
|
|
||||||
<ElTableColumn label="操作" width="230" fixed="right">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElButton
|
<ElButton
|
||||||
v-permission="JULY_PERMISSIONS.wecom.member"
|
v-permission="JULY_PERMISSIONS.wecom.member"
|
||||||
link
|
:disabled="!selectedApplicationId"
|
||||||
|
:loading="syncing"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click.stop="setDefaultCreator(scope.row)"
|
@click="syncMembers"
|
||||||
>
|
>
|
||||||
设为默认发起人
|
同步成员
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton @click="goToApplications">返回应用列表</ElButton>
|
||||||
v-permission="JULY_PERMISSIONS.wecom.binding"
|
</div>
|
||||||
link
|
</div>
|
||||||
@click.stop="selectMember(scope.row)"
|
|
||||||
>
|
|
||||||
绑定账号
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
|
|
||||||
<div class="pagination-wrapper">
|
<ElEmpty v-if="!members.length && !loading" description="暂无企微成员" />
|
||||||
<ElPagination
|
<div v-else class="member-grid">
|
||||||
v-model:current-page="pagination.page"
|
<ElCard v-for="member in members" :key="member.userid" shadow="hover" class="member-card">
|
||||||
v-model:page-size="pagination.page_size"
|
<div class="member-card__header">
|
||||||
:total="pagination.total"
|
<div class="member-card__identity">
|
||||||
:page-sizes="[20, 50, 100]"
|
<ElAvatar :size="36" class="member-card__avatar">
|
||||||
layout="total, sizes, prev, pager, next"
|
{{ getMemberInitial(member.name) }}
|
||||||
@size-change="handleSizeChange"
|
</ElAvatar>
|
||||||
@current-change="loadMembers"
|
<div class="member-card__name" :title="member.name">
|
||||||
/>
|
{{ member.name || '-' }}
|
||||||
</div>
|
</div>
|
||||||
</ElCard>
|
</div>
|
||||||
|
<ElTag v-if="isDefaultCreator(member)" type="success" size="small">
|
||||||
|
默认发起人
|
||||||
|
</ElTag>
|
||||||
|
<ElTag
|
||||||
|
v-else
|
||||||
|
v-permission="JULY_PERMISSIONS.wecom.member"
|
||||||
|
class="member-card__action-tag"
|
||||||
|
type="primary"
|
||||||
|
effect="plain"
|
||||||
|
size="small"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click.stop="setDefaultCreator(member)"
|
||||||
|
@keydown.enter.stop="setDefaultCreator(member)"
|
||||||
|
>
|
||||||
|
设为默认发起人
|
||||||
|
</ElTag>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ElCard shadow="never" class="operation-card">
|
<div class="member-card__info">
|
||||||
<template #header>当前成员操作</template>
|
<span class="member-card__label">同步时间</span>
|
||||||
<ElAlert
|
<span>{{ formatDateTime(member.synced_at) }}</span>
|
||||||
v-if="selectedMember"
|
</div>
|
||||||
:title="`已选择:${selectedMember.name}`"
|
</ElCard>
|
||||||
type="info"
|
</div>
|
||||||
:closable="false"
|
|
||||||
/>
|
|
||||||
<ElEmpty v-else description="请先在上方列表选择成员" :image-size="70" />
|
|
||||||
|
|
||||||
<div class="operation-row">
|
<div class="pagination-wrapper">
|
||||||
<span class="operation-label">绑定平台账号</span>
|
<ElPagination
|
||||||
<ElInputNumber
|
v-model:current-page="pagination.page"
|
||||||
v-model="bindingAccountId"
|
v-model:page-size="pagination.page_size"
|
||||||
:min="1"
|
:total="pagination.total"
|
||||||
:disabled="!selectedMember"
|
:page-sizes="[20, 50, 100]"
|
||||||
controls-position="right"
|
layout="total, sizes, prev, pager, next"
|
||||||
placeholder="账号 ID"
|
@size-change="handleSizeChange"
|
||||||
/>
|
@current-change="loadMembers"
|
||||||
<ElButton
|
/>
|
||||||
v-permission="JULY_PERMISSIONS.wecom.binding"
|
</div>
|
||||||
type="primary"
|
</ElCard>
|
||||||
:disabled="!selectedMember || !bindingAccountId"
|
</div>
|
||||||
:loading="binding"
|
</ArtTableFullScreen>
|
||||||
@click="bindAccount"
|
|
||||||
>
|
|
||||||
绑定账号
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
v-permission="JULY_PERMISSIONS.wecom.member"
|
|
||||||
:disabled="!selectedMember"
|
|
||||||
:loading="savingDefault"
|
|
||||||
@click="saveSelectedDefaultCreator"
|
|
||||||
>
|
|
||||||
保存为默认发起人
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { AccountService, WecomService } from '@/api/modules'
|
import type { SearchFormItem } from '@/types'
|
||||||
|
import { WecomService } from '@/api/modules'
|
||||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
import type { WecomApplication, WecomMember } from '@/types/api'
|
import type { WecomApplication, WecomMember } from '@/types/api'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
|
||||||
defineOptions({ name: 'WecomMembers' })
|
defineOptions({ name: 'WecomMembers' })
|
||||||
|
|
||||||
@@ -170,30 +97,65 @@
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const syncing = ref(false)
|
const syncing = ref(false)
|
||||||
const savingDefault = ref(false)
|
const savingDefault = ref(false)
|
||||||
const binding = ref(false)
|
const pendingDefaultUserid = ref('')
|
||||||
const applications = ref<WecomApplication[]>([])
|
const applications = ref<WecomApplication[]>([])
|
||||||
const members = ref<WecomMember[]>([])
|
const members = ref<WecomMember[]>([])
|
||||||
const selectedApplicationId = ref<number>()
|
const searchForm = reactive({
|
||||||
const selectedUserid = ref('')
|
application_id: undefined as number | undefined,
|
||||||
const bindingAccountId = ref<number>()
|
keyword: ''
|
||||||
const keyword = ref('')
|
})
|
||||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||||
|
|
||||||
|
const selectedApplicationId = computed(() => searchForm.application_id)
|
||||||
const selectedApplication = computed(() =>
|
const selectedApplication = computed(() =>
|
||||||
applications.value.find((application) => application.id === selectedApplicationId.value)
|
applications.value.find((application) => application.id === selectedApplicationId.value)
|
||||||
)
|
)
|
||||||
const selectedMember = computed(() =>
|
|
||||||
members.value.find((member) => member.userid === selectedUserid.value)
|
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||||
)
|
{
|
||||||
|
label: '企微应用',
|
||||||
|
prop: 'application_id',
|
||||||
|
type: 'select',
|
||||||
|
options: applications.value.map((application) => ({
|
||||||
|
label: application.name,
|
||||||
|
value: application.id
|
||||||
|
})),
|
||||||
|
onChange: () => {
|
||||||
|
pagination.page = 1
|
||||||
|
void loadMembers()
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
filterable: true,
|
||||||
|
clearable: false,
|
||||||
|
placeholder: '请选择企微应用'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '成员搜索',
|
||||||
|
prop: 'keyword',
|
||||||
|
type: 'input',
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入成员姓名'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const isDefaultCreator = (member: WecomMember) =>
|
||||||
|
member.userid === selectedApplication.value?.default_creator_userid
|
||||||
|
|
||||||
|
const getMemberInitial = (name: string) => name.trim().slice(0, 1) || '-'
|
||||||
|
|
||||||
const loadApplications = async () => {
|
const loadApplications = async () => {
|
||||||
const response = await WecomService.getApplications({ page: 1, page_size: 100 })
|
const response = await WecomService.getApplications({ page: 1, page_size: 100 })
|
||||||
if (response.code !== 0) return
|
if (response.code !== 0) return
|
||||||
applications.value = response.data.items || []
|
applications.value = response.data.items || []
|
||||||
const queryApplicationId = Number(route.query.application_id)
|
const queryApplicationId = Number(route.query.application_id)
|
||||||
selectedApplicationId.value =
|
const preferredApplication =
|
||||||
applications.value.find((application) => application.id === queryApplicationId)?.id ||
|
applications.value.find((application) => application.id === queryApplicationId) ||
|
||||||
applications.value[0]?.id
|
applications.value.find((application) => application.id === searchForm.application_id) ||
|
||||||
|
applications.value[0]
|
||||||
|
searchForm.application_id = preferredApplication?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadMembers = async () => {
|
const loadMembers = async () => {
|
||||||
@@ -207,33 +169,25 @@
|
|||||||
const response = await WecomService.getMembers(selectedApplicationId.value, {
|
const response = await WecomService.getMembers(selectedApplicationId.value, {
|
||||||
page: pagination.page,
|
page: pagination.page,
|
||||||
page_size: pagination.page_size,
|
page_size: pagination.page_size,
|
||||||
keyword: keyword.value.trim() || undefined
|
keyword: searchForm.keyword.trim() || undefined
|
||||||
})
|
})
|
||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
members.value = response.data.items || []
|
members.value = response.data.items || []
|
||||||
pagination.total = response.data.total || 0
|
pagination.total = response.data.total || 0
|
||||||
if (!members.value.some((member) => member.userid === selectedUserid.value)) {
|
|
||||||
selectedUserid.value = ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleApplicationChange = () => {
|
|
||||||
pagination.page = 1
|
|
||||||
selectedUserid.value = ''
|
|
||||||
void loadMembers()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
void loadMembers()
|
void loadMembers()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
keyword.value = ''
|
searchForm.keyword = ''
|
||||||
|
searchForm.application_id = applications.value[0]?.id
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
void loadMembers()
|
void loadMembers()
|
||||||
}
|
}
|
||||||
@@ -244,10 +198,6 @@
|
|||||||
void loadMembers()
|
void loadMembers()
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectMember = (member: WecomMember) => {
|
|
||||||
selectedUserid.value = member.userid
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncMembers = async () => {
|
const syncMembers = async () => {
|
||||||
if (!selectedApplicationId.value) return
|
if (!selectedApplicationId.value) return
|
||||||
syncing.value = true
|
syncing.value = true
|
||||||
@@ -263,49 +213,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setDefaultCreator = async (member: WecomMember) => {
|
const setDefaultCreator = async (member: WecomMember) => {
|
||||||
if (!selectedApplicationId.value) return
|
if (!selectedApplicationId.value || savingDefault.value) return
|
||||||
selectedUserid.value = member.userid
|
|
||||||
await saveSelectedDefaultCreator()
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveSelectedDefaultCreator = async () => {
|
try {
|
||||||
if (!selectedApplicationId.value || !selectedMember.value) {
|
await ElMessageBox.confirm(
|
||||||
ElMessage.warning('请选择要设置的成员')
|
`确定将 ${member.name} 设置为当前应用的默认发起人吗?`,
|
||||||
|
'确认设置默认发起人',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定设置',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
savingDefault.value = true
|
savingDefault.value = true
|
||||||
|
pendingDefaultUserid.value = member.userid
|
||||||
try {
|
try {
|
||||||
const response = await WecomService.setDefaultCreator(
|
const response = await WecomService.setDefaultCreator(
|
||||||
selectedApplicationId.value,
|
selectedApplicationId.value,
|
||||||
selectedMember.value.userid
|
member.userid
|
||||||
)
|
)
|
||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
ElMessage.success('默认发起人已保存')
|
ElMessage.success('默认发起人已保存')
|
||||||
const application = selectedApplication.value
|
const application = selectedApplication.value
|
||||||
if (application) {
|
if (application) {
|
||||||
application.default_creator_userid = selectedMember.value.userid
|
application.default_creator_userid = member.userid
|
||||||
application.default_creator_name = selectedMember.value.name
|
application.default_creator_name = member.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
savingDefault.value = false
|
savingDefault.value = false
|
||||||
}
|
pendingDefaultUserid.value = ''
|
||||||
}
|
|
||||||
|
|
||||||
const bindAccount = async () => {
|
|
||||||
if (!selectedApplicationId.value || !selectedMember.value || !bindingAccountId.value) {
|
|
||||||
ElMessage.warning('请选择成员并输入账号 ID')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
binding.value = true
|
|
||||||
try {
|
|
||||||
const response = await AccountService.bindWecom(bindingAccountId.value, {
|
|
||||||
application_id: selectedApplicationId.value,
|
|
||||||
userid: selectedMember.value.userid
|
|
||||||
})
|
|
||||||
if (response.code === 0) ElMessage.success('账号企微绑定成功')
|
|
||||||
} finally {
|
|
||||||
binding.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,105 +260,152 @@
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.wecom-members-page {
|
.wecom-members-page {
|
||||||
.page-header,
|
.members-card {
|
||||||
.toolbar,
|
min-height: 0;
|
||||||
.operation-row {
|
overflow: hidden;
|
||||||
|
|
||||||
|
:deep(> .el-card__body) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card {
|
||||||
|
min-width: 0;
|
||||||
|
transition:
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
display: flex;
|
||||||
|
height: auto;
|
||||||
|
min-height: 108px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card__header,
|
||||||
|
.member-card__info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header,
|
.member-card__header {
|
||||||
.toolbar {
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.member-card__identity {
|
||||||
color: var(--el-text-color-primary);
|
display: flex;
|
||||||
font-size: 16px;
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card__avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-description {
|
.member-card__name {
|
||||||
margin-top: 6px;
|
min-width: 0;
|
||||||
color: var(--el-text-color-secondary);
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card__info {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
||||||
|
span:last-child {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.member-card__label {
|
||||||
margin-top: 4px;
|
color: var(--el-text-color-secondary);
|
||||||
padding: 14px 16px 0;
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-select {
|
.member-card__action-tag {
|
||||||
width: 280px;
|
cursor: pointer;
|
||||||
}
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
.keyword-input {
|
&:hover {
|
||||||
width: 220px;
|
opacity: 0.8;
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
margin: 20px 0 12px;
|
|
||||||
|
|
||||||
.selected-application {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tag {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-wrapper {
|
.pagination-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 16px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-card {
|
@media (max-width: 1400px) {
|
||||||
margin-top: 16px;
|
.member-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-row {
|
@media (max-width: 1024px) {
|
||||||
flex-wrap: wrap;
|
.member-grid {
|
||||||
gap: 12px;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
margin-top: 18px;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-label {
|
@media (max-width: 640px) {
|
||||||
color: var(--el-text-color-primary);
|
.member-grid {
|
||||||
font-weight: 500;
|
grid-template-columns: 1fr;
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.page-header,
|
|
||||||
.toolbar {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-select,
|
.members-toolbar {
|
||||||
.keyword-input {
|
justify-content: flex-start;
|
||||||
width: 100%;
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
justify-content: center;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,158 +1,240 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wecom-scenes-page">
|
<ArtTableFullScreen>
|
||||||
<ElCard shadow="never">
|
<div id="table-full-screen" class="wecom-scenes-page">
|
||||||
<template #header>
|
<ElCard shadow="never" class="art-table-card">
|
||||||
<div class="page-header">
|
<ArtTableHeader
|
||||||
<div>
|
:column-list="columnOptions"
|
||||||
<div class="page-title">企微审批场景</div>
|
v-model:columns="columnChecks"
|
||||||
<div class="page-description"
|
@refresh="loadData"
|
||||||
>分别维护退款和线下代充值审批模板,并提交后由后端校验控件映射。</div
|
>
|
||||||
|
<template #left>
|
||||||
|
<ElButton
|
||||||
|
v-permission="JULY_PERMISSIONS.wecom.scene"
|
||||||
|
type="primary"
|
||||||
|
@click="createScene"
|
||||||
>
|
>
|
||||||
|
新增场景配置
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ArtTableHeader>
|
||||||
|
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="business_type"
|
||||||
|
:loading="loading"
|
||||||
|
:data="scenes"
|
||||||
|
:current-page="pagination.page"
|
||||||
|
:page-size="pagination.page_size"
|
||||||
|
:total="pagination.total"
|
||||||
|
:margin-top="10"
|
||||||
|
:actions="getActions"
|
||||||
|
:actions-width="140"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentPageChange"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElDialog
|
||||||
|
v-model="editorVisible"
|
||||||
|
:title="editing ? '编辑审批场景' : '新增审批场景'"
|
||||||
|
width="55%"
|
||||||
|
class="wecom-scene-dialog"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<ElForm :model="sceneForm" label-width="80px" class="scene-editor-form">
|
||||||
|
<div class="dialog-section">
|
||||||
|
<div class="dialog-section__header">
|
||||||
|
<div class="dialog-section__title">基础配置</div>
|
||||||
|
<div class="dialog-section__description">选择审批业务和对应的企业微信模板</div>
|
||||||
</div>
|
</div>
|
||||||
<ElButton v-permission="JULY_PERMISSIONS.wecom.scene" type="primary" @click="createScene">
|
|
||||||
新增场景配置
|
<ElRow :gutter="24">
|
||||||
|
<ElCol :xs="24" :sm="12">
|
||||||
|
<ElFormItem label="业务类型" required>
|
||||||
|
<ElSelect
|
||||||
|
v-model="sceneForm.business_type"
|
||||||
|
:disabled="editing"
|
||||||
|
class="full-width"
|
||||||
|
@change="handleBusinessTypeChange"
|
||||||
|
>
|
||||||
|
<ElOption label="退款审批" value="refund_approval" />
|
||||||
|
<ElOption label="线下代充值审批" value="offline_recharge_approval" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :xs="24" :sm="12">
|
||||||
|
<ElFormItem label="企微应用">
|
||||||
|
<ElSelect
|
||||||
|
v-model="sceneForm.application_id"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="application in applications"
|
||||||
|
:key="application.id"
|
||||||
|
:label="application.name"
|
||||||
|
:value="application.id"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :xs="24" :sm="12">
|
||||||
|
<ElFormItem label="模板 ID" required>
|
||||||
|
<ElInput v-model="sceneForm.template_id" placeholder="请输入企微后台模板 ID">
|
||||||
|
<template #append>
|
||||||
|
<ElButton
|
||||||
|
v-permission="JULY_PERMISSIONS.wecom.scene"
|
||||||
|
:loading="inspectingTemplate"
|
||||||
|
@click="syncTemplate()"
|
||||||
|
>
|
||||||
|
同步模板
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
|
<div v-if="templateName" class="template-name">当前模板:{{ templateName }}</div>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :xs="24" :sm="12">
|
||||||
|
<ElFormItem label="启用状态">
|
||||||
|
<ElSwitch v-model="sceneForm.enabled" active-text="启用" inactive-text="禁用" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-section mapping-section">
|
||||||
|
<div class="dialog-section__header mapping-heading">
|
||||||
|
<div>
|
||||||
|
<div class="dialog-section__title">控件映射</div>
|
||||||
|
<div class="dialog-section__description">将业务字段绑定到企业微信模板控件</div>
|
||||||
|
</div>
|
||||||
|
<ElTag v-if="!templateControls.length" type="info" effect="plain" class="mapping-tip">
|
||||||
|
请先填写模板 ID 并点击“同步模板”,再选择模板控件。
|
||||||
|
</ElTag>
|
||||||
|
</div>
|
||||||
|
<div v-if="mappingRows.length" class="mapping-list">
|
||||||
|
<div v-for="mapping in mappingRows" :key="mapping.key" class="mapping-row">
|
||||||
|
<div class="mapping-row__grid">
|
||||||
|
<div class="mapping-field">
|
||||||
|
<span class="mapping-field__label">业务字段</span>
|
||||||
|
<ElSelect
|
||||||
|
v-model="mapping.business_field"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
:loading="fieldsLoading"
|
||||||
|
placeholder="选择业务字段"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="field in businessFields"
|
||||||
|
:key="field.code"
|
||||||
|
:label="field.name"
|
||||||
|
:value="field.code"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</div>
|
||||||
|
<div class="mapping-field">
|
||||||
|
<span class="mapping-field__label">模板控件</span>
|
||||||
|
<ElInput
|
||||||
|
:model-value="mapping.control_title"
|
||||||
|
readonly
|
||||||
|
placeholder="模板控件名称"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mapping-field">
|
||||||
|
<span class="mapping-field__label">控件 ID</span>
|
||||||
|
<ElInput
|
||||||
|
:model-value="mapping.control_id"
|
||||||
|
readonly
|
||||||
|
placeholder="选择模板控件后自动填充"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mapping-field">
|
||||||
|
<span class="mapping-field__label">控件类型</span>
|
||||||
|
<ElInput
|
||||||
|
:model-value="mapping.control_type"
|
||||||
|
readonly
|
||||||
|
placeholder="同步模板后自动填充"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mapping-field mapping-field--option">
|
||||||
|
<span class="mapping-field__label">选择项映射</span>
|
||||||
|
<ElInput v-model="mapping.option_mapping" placeholder="JSON,可为空对象" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElEmpty v-else description="暂无控件映射,请新增一条" :image-size="60" />
|
||||||
|
</div>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="editorVisible = false">取消</ElButton>
|
||||||
|
<ElButton @click="createScene">清空表单</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-permission="JULY_PERMISSIONS.wecom.scene"
|
||||||
|
type="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="saveScene"
|
||||||
|
>
|
||||||
|
保存并校验
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</ElDialog>
|
||||||
<div class="scene-layout">
|
</ArtTableFullScreen>
|
||||||
<div class="scene-list">
|
|
||||||
<div class="section-heading">场景配置列表</div>
|
|
||||||
<ElTable
|
|
||||||
v-loading="loading"
|
|
||||||
:data="scenes"
|
|
||||||
row-key="business_type"
|
|
||||||
highlight-current-row
|
|
||||||
border
|
|
||||||
@row-click="selectScene"
|
|
||||||
>
|
|
||||||
<ElTableColumn prop="business_type_name" label="业务类型" min-width="150" />
|
|
||||||
<ElTableColumn
|
|
||||||
prop="template_name"
|
|
||||||
label="模板名称"
|
|
||||||
min-width="180"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<ElTableColumn label="状态" width="90">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElTag :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
|
|
||||||
{{ scope.row.status_name }}
|
|
||||||
</ElTag>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="last_verified_at" label="最近校验" min-width="170" />
|
|
||||||
<ElTableColumn label="操作" width="90" fixed="right">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElButton
|
|
||||||
v-permission="JULY_PERMISSIONS.wecom.scene"
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
@click.stop="selectScene(scope.row)"
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ElCard shadow="never" class="editor-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="editor-header">
|
|
||||||
<span>{{ editing ? '编辑审批场景' : '新增审批场景' }}</span>
|
|
||||||
<ElTag v-if="editing" size="small" type="info">{{ sceneForm.business_type }}</ElTag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElForm :model="sceneForm" label-width="110px">
|
|
||||||
<ElFormItem label="业务类型" required>
|
|
||||||
<ElSelect v-model="sceneForm.business_type" :disabled="editing" class="full-width">
|
|
||||||
<ElOption label="退款审批" value="refund_approval" />
|
|
||||||
<ElOption label="线下代充值审批" value="offline_recharge_approval" />
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="企微应用">
|
|
||||||
<ElSelect v-model="sceneForm.application_id" clearable filterable class="full-width">
|
|
||||||
<ElOption
|
|
||||||
v-for="application in applications"
|
|
||||||
:key="application.id"
|
|
||||||
:label="application.name"
|
|
||||||
:value="application.id"
|
|
||||||
/>
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="模板 ID" required>
|
|
||||||
<ElInput v-model="sceneForm.template_id" placeholder="请输入企微后台模板 ID" />
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="启用状态">
|
|
||||||
<ElSwitch v-model="sceneForm.enabled" active-text="启用" inactive-text="禁用" />
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElDivider content-position="left">控件映射</ElDivider>
|
|
||||||
<div class="mapping-heading">
|
|
||||||
<span>业务字段与模板控件的对应关系</span>
|
|
||||||
<ElButton text type="primary" @click="addMapping()">新增映射</ElButton>
|
|
||||||
</div>
|
|
||||||
<div v-if="mappingRows.length" class="mapping-list">
|
|
||||||
<div v-for="(mapping, index) in mappingRows" :key="mapping.key" class="mapping-row">
|
|
||||||
<ElInput v-model="mapping.business_field" placeholder="业务字段,如 refund_no" />
|
|
||||||
<ElInput v-model="mapping.control_id" placeholder="控件 ID" />
|
|
||||||
<ElInput v-model="mapping.control_type" placeholder="控件类型,如 Text" />
|
|
||||||
<ElInput
|
|
||||||
v-model="mapping.option_mapping"
|
|
||||||
placeholder="选择项映射 JSON,可为空对象"
|
|
||||||
/>
|
|
||||||
<ElButton text type="danger" @click="removeMapping(index)">删除</ElButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ElEmpty v-else description="暂无控件映射,请新增一条" :image-size="60" />
|
|
||||||
|
|
||||||
<div class="editor-actions">
|
|
||||||
<ElButton @click="createScene">清空表单</ElButton>
|
|
||||||
<ElButton
|
|
||||||
v-permission="JULY_PERMISSIONS.wecom.scene"
|
|
||||||
type="primary"
|
|
||||||
:loading="saving"
|
|
||||||
@click="saveScene"
|
|
||||||
>
|
|
||||||
保存并校验
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</ElForm>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElTag } from 'element-plus'
|
||||||
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { WecomService } from '@/api/modules'
|
import { WecomService } from '@/api/modules'
|
||||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||||
import type {
|
import type {
|
||||||
WecomApplication,
|
WecomApplication,
|
||||||
|
WecomBusinessField,
|
||||||
WecomBusinessType,
|
WecomBusinessType,
|
||||||
WecomScene,
|
WecomScene,
|
||||||
WecomSceneControlMapping
|
WecomSceneControlMapping,
|
||||||
|
WecomTemplateControl
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
|
||||||
defineOptions({ name: 'WecomScenes' })
|
defineOptions({ name: 'WecomScenes' })
|
||||||
|
|
||||||
interface MappingEditor {
|
interface MappingEditor {
|
||||||
key: number
|
key: number
|
||||||
business_field: string
|
business_field: string
|
||||||
|
control_title: string
|
||||||
control_id: string
|
control_id: string
|
||||||
control_type: string
|
control_type: string
|
||||||
|
required: boolean
|
||||||
option_mapping: string
|
option_mapping: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const tableRef = ref()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
const fieldsLoading = ref(false)
|
||||||
|
const inspectingTemplate = ref(false)
|
||||||
|
const editorVisible = ref(false)
|
||||||
const applications = ref<WecomApplication[]>([])
|
const applications = ref<WecomApplication[]>([])
|
||||||
|
const businessFields = ref<WecomBusinessField[]>([])
|
||||||
|
const templateControls = ref<WecomTemplateControl[]>([])
|
||||||
|
const templateName = ref('')
|
||||||
const scenes = ref<WecomScene[]>([])
|
const scenes = ref<WecomScene[]>([])
|
||||||
const selectedBusinessType = ref<WecomBusinessType>()
|
const selectedBusinessType = ref<WecomBusinessType>()
|
||||||
const nextMappingKey = ref(1)
|
const nextMappingKey = ref(1)
|
||||||
|
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||||
const sceneForm = reactive({
|
const sceneForm = reactive({
|
||||||
business_type: 'refund_approval' as WecomBusinessType,
|
business_type: 'refund_approval' as WecomBusinessType,
|
||||||
application_id: undefined as number | undefined,
|
application_id: undefined as number | undefined,
|
||||||
@@ -162,19 +244,69 @@
|
|||||||
const mappingRows = ref<MappingEditor[]>([])
|
const mappingRows = ref<MappingEditor[]>([])
|
||||||
const editing = computed(() => Boolean(selectedBusinessType.value))
|
const editing = computed(() => Boolean(selectedBusinessType.value))
|
||||||
|
|
||||||
const addMapping = (mapping?: Partial<MappingEditor>) => {
|
const columnOptions = [
|
||||||
mappingRows.value.push({
|
{ label: '业务类型', prop: 'business_type_name' },
|
||||||
key: nextMappingKey.value++,
|
{ label: '模板名称', prop: 'template_name' },
|
||||||
business_field: mapping?.business_field || '',
|
{ label: '状态', prop: 'status' },
|
||||||
control_id: mapping?.control_id || '',
|
{ label: '最近校验', prop: 'last_verified_at' }
|
||||||
control_type: mapping?.control_type || '',
|
]
|
||||||
option_mapping: mapping?.option_mapping || '{}'
|
|
||||||
})
|
const formatDate = (value?: string | null) => (value ? formatDateTime(value) : '-')
|
||||||
|
|
||||||
|
const loadBusinessFields = async (businessType: WecomBusinessType) => {
|
||||||
|
fieldsLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await WecomService.getBusinessFields(businessType)
|
||||||
|
if (response.code === 0) {
|
||||||
|
businessFields.value = response.data.items || []
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
fieldsLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeMapping = (index: number) => mappingRows.value.splice(index, 1)
|
const handleBusinessTypeChange = () => {
|
||||||
|
businessFields.value = []
|
||||||
|
mappingRows.value.forEach((mapping) => {
|
||||||
|
mapping.business_field = ''
|
||||||
|
})
|
||||||
|
void loadBusinessFields(sceneForm.business_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
|
{
|
||||||
|
prop: 'business_type_name',
|
||||||
|
label: '业务类型',
|
||||||
|
minWidth: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'template_name',
|
||||||
|
label: '模板名称',
|
||||||
|
minWidth: 220,
|
||||||
|
showOverflowTooltip: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'status',
|
||||||
|
label: '状态',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: WecomScene) =>
|
||||||
|
h(
|
||||||
|
ElTag,
|
||||||
|
{ type: row.status === 1 ? 'success' : 'info', size: 'small' },
|
||||||
|
() => row.status_name
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'last_verified_at',
|
||||||
|
label: '最近校验',
|
||||||
|
width: 180,
|
||||||
|
formatter: (row: WecomScene) => formatDate(row.last_verified_at)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
const fillEditor = (scene?: WecomScene) => {
|
const fillEditor = (scene?: WecomScene) => {
|
||||||
|
templateControls.value = []
|
||||||
|
templateName.value = ''
|
||||||
if (!scene) {
|
if (!scene) {
|
||||||
selectedBusinessType.value = undefined
|
selectedBusinessType.value = undefined
|
||||||
Object.assign(sceneForm, {
|
Object.assign(sceneForm, {
|
||||||
@@ -184,7 +316,6 @@
|
|||||||
enabled: true
|
enabled: true
|
||||||
})
|
})
|
||||||
mappingRows.value = []
|
mappingRows.value = []
|
||||||
addMapping()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectedBusinessType.value = scene.business_type
|
selectedBusinessType.value = scene.business_type
|
||||||
@@ -197,41 +328,124 @@
|
|||||||
mappingRows.value = (scene.control_mapping || []).map((mapping) => ({
|
mappingRows.value = (scene.control_mapping || []).map((mapping) => ({
|
||||||
key: nextMappingKey.value++,
|
key: nextMappingKey.value++,
|
||||||
business_field: mapping.business_field,
|
business_field: mapping.business_field,
|
||||||
|
control_title: '',
|
||||||
control_id: mapping.control_id,
|
control_id: mapping.control_id,
|
||||||
control_type: mapping.control_type,
|
control_type: mapping.control_type,
|
||||||
|
required: true,
|
||||||
option_mapping: JSON.stringify(mapping.option_mapping || {})
|
option_mapping: JSON.stringify(mapping.option_mapping || {})
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectScene = (scene: WecomScene) => fillEditor(scene)
|
const createScene = () => {
|
||||||
const createScene = () => fillEditor()
|
fillEditor()
|
||||||
|
editorVisible.value = true
|
||||||
|
void loadBusinessFields(sceneForm.business_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const editScene = (scene: WecomScene) => {
|
||||||
|
fillEditor(scene)
|
||||||
|
editorVisible.value = true
|
||||||
|
void loadBusinessFields(scene.business_type)
|
||||||
|
void syncTemplate(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncMappingRows = (controls: WecomTemplateControl[]) => {
|
||||||
|
const existingMappings = new Map(
|
||||||
|
mappingRows.value.map((mapping) => [mapping.control_id, mapping])
|
||||||
|
)
|
||||||
|
mappingRows.value = controls.map((control) => {
|
||||||
|
const existing = existingMappings.get(control.id)
|
||||||
|
return {
|
||||||
|
key: existing?.key || nextMappingKey.value++,
|
||||||
|
business_field: existing?.business_field || '',
|
||||||
|
control_title: control.title,
|
||||||
|
control_id: control.id,
|
||||||
|
control_type: control.type,
|
||||||
|
required: control.required,
|
||||||
|
option_mapping: existing?.option_mapping || '{}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncTemplate = async (showMessage = true) => {
|
||||||
|
if (!sceneForm.application_id) {
|
||||||
|
ElMessage.warning('请先选择企微应用')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!sceneForm.template_id.trim()) {
|
||||||
|
ElMessage.warning('请输入模板 ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inspectingTemplate.value = true
|
||||||
|
try {
|
||||||
|
const response = await WecomService.inspectTemplate(sceneForm.application_id, {
|
||||||
|
template_id: sceneForm.template_id.trim()
|
||||||
|
})
|
||||||
|
if (response.code === 0) {
|
||||||
|
templateControls.value = response.data.controls || []
|
||||||
|
templateName.value = response.data.name || ''
|
||||||
|
syncMappingRows(templateControls.value)
|
||||||
|
if (showMessage) {
|
||||||
|
ElMessage.success(`模板控件已同步,共 ${templateControls.value.length} 项`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
inspectingTemplate.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [applicationResponse, sceneResponse] = await Promise.all([
|
const [applicationResponse, sceneResponse] = await Promise.all([
|
||||||
WecomService.getApplications({ page: 1, page_size: 100 }),
|
WecomService.getApplications({ page: 1, page_size: 100 }),
|
||||||
WecomService.getScenes({ page: 1, page_size: 100 })
|
WecomService.getScenes({
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.page_size
|
||||||
|
})
|
||||||
])
|
])
|
||||||
if (applicationResponse.code === 0) applications.value = applicationResponse.data.items || []
|
if (applicationResponse.code === 0) applications.value = applicationResponse.data.items || []
|
||||||
if (sceneResponse.code === 0) {
|
if (sceneResponse.code === 0) {
|
||||||
scenes.value = sceneResponse.data.items || []
|
scenes.value = sceneResponse.data.items || []
|
||||||
const current = scenes.value.find(
|
pagination.total = sceneResponse.data.total || 0
|
||||||
(scene) => scene.business_type === selectedBusinessType.value
|
if (selectedBusinessType.value) {
|
||||||
)
|
const current = scenes.value.find(
|
||||||
fillEditor(current || scenes.value[0])
|
(scene) => scene.business_type === selectedBusinessType.value
|
||||||
|
)
|
||||||
|
if (current) fillEditor(current)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pagination.page_size = size
|
||||||
|
pagination.page = 1
|
||||||
|
void loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentPageChange = (page: number) => {
|
||||||
|
pagination.page = page
|
||||||
|
void loadData()
|
||||||
|
}
|
||||||
|
|
||||||
const parseMapping = (): WecomSceneControlMapping[] | undefined => {
|
const parseMapping = (): WecomSceneControlMapping[] | undefined => {
|
||||||
const result: WecomSceneControlMapping[] = []
|
const result: WecomSceneControlMapping[] = []
|
||||||
for (const mapping of mappingRows.value) {
|
for (const mapping of mappingRows.value) {
|
||||||
if (!mapping.business_field && !mapping.control_id && !mapping.control_type) continue
|
if (!mapping.business_field) {
|
||||||
if (!mapping.business_field || !mapping.control_id || !mapping.control_type) {
|
if (mapping.required) {
|
||||||
ElMessage.warning('请完整填写控件映射字段')
|
ElMessage.warning(
|
||||||
|
`请为必填控件“${mapping.control_title || mapping.control_id}”选择业务字段`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!mapping.control_id || !mapping.control_type) {
|
||||||
|
ElMessage.warning(`控件“${mapping.control_title || mapping.control_id}”信息不完整`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let optionMapping: Record<string, string>
|
let optionMapping: Record<string, string>
|
||||||
@@ -271,6 +485,7 @@
|
|||||||
})
|
})
|
||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
ElMessage.success('场景保存并校验成功')
|
ElMessage.success('场景保存并校验成功')
|
||||||
|
editorVisible.value = false
|
||||||
await loadData()
|
await loadData()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -278,98 +493,164 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getActions = (row: WecomScene) => {
|
||||||
|
if (!hasAuth(JULY_PERMISSIONS.wecom.scene)) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '编辑',
|
||||||
|
handler: () => editScene(row),
|
||||||
|
type: 'primary' as const
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => void loadData())
|
onMounted(() => void loadData())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.wecom-scenes-page {
|
.full-width {
|
||||||
.page-header,
|
width: 100%;
|
||||||
.editor-header,
|
}
|
||||||
.mapping-heading,
|
|
||||||
.editor-actions {
|
.scene-editor-form {
|
||||||
display: flex;
|
padding-bottom: 12px;
|
||||||
align-items: center;
|
|
||||||
|
.dialog-section {
|
||||||
|
padding: 18px 20px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header,
|
.dialog-section + .dialog-section {
|
||||||
.editor-header,
|
margin-top: 18px;
|
||||||
.mapping-heading {
|
}
|
||||||
|
|
||||||
|
.dialog-section__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.dialog-section__title {
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-description {
|
.dialog-section__description {
|
||||||
margin-top: 6px;
|
margin-top: 4px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-layout {
|
.mapping-tip {
|
||||||
display: grid;
|
flex-shrink: 0;
|
||||||
grid-template-columns: minmax(0, 1.1fr) minmax(480px, 1fr);
|
margin-top: 0;
|
||||||
gap: 16px;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-heading {
|
.template-name {
|
||||||
margin-bottom: 12px;
|
margin-top: 6px;
|
||||||
color: var(--el-text-color-primary);
|
overflow: hidden;
|
||||||
font-weight: 600;
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-card {
|
.mapping-section {
|
||||||
min-width: 0;
|
padding-bottom: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.full-width {
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapping-heading {
|
.mapping-heading {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 0;
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapping-list {
|
.mapping-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
max-height: 420px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 4px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapping-row {
|
.mapping-row {
|
||||||
display: grid;
|
padding: 14px;
|
||||||
grid-template-columns: 1.05fr 1fr 0.8fr 1.3fr auto;
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
gap: 8px;
|
border-radius: 6px;
|
||||||
align-items: center;
|
margin-top: 6px;
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-actions {
|
.mapping-row__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns:
|
||||||
|
minmax(120px, 1.1fr) minmax(140px, 1.3fr) minmax(120px, 1fr) minmax(100px, 0.8fr)
|
||||||
|
minmax(140px, 1.2fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-field {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-field__label {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.scene-layout {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.page-header {
|
.dialog-section {
|
||||||
align-items: flex-start;
|
padding: 14px;
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapping-row {
|
.dialog-section__header {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-tip {
|
||||||
|
align-self: flex-start;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-row__grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mapping-field--option {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.mapping-row__grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-field--option {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.wecom-scene-dialog) {
|
||||||
|
width: calc(100vw - 32px) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user