重命名前端目录

This commit is contained in:
yml2213
2026-07-07 19:55:18 +08:00
parent 8268a3b906
commit dbcb5a4979
182 changed files with 7019 additions and 7017 deletions
@@ -0,0 +1,28 @@
import type { AdminTaskDetail } from '@/types/admin'
export type KuaishouCloudFlow = NonNullable<AdminTaskDetail['kuaishouCloudFulfillment']>
export type SummaryCard = {
label: string
value: string
tone: 'blue' | 'green' | 'amber' | 'slate'
}
export type RoleInfoEntry = {
key: string
label: string
value: string
}
export type ChecklistItem = {
key: string
label: string
status: string
detail: string
}
export type ManualDispatchForm = {
deliveryReference: string
deliveredCredential: string
resultMessage: string
}
@@ -0,0 +1,210 @@
import { computed, onBeforeUnmount, ref } from 'vue'
import { useRoute } from 'vue-router'
import { showConfirm, showError, showSuccess } from '@/lib/feedback'
import {
completeAdminTaskManualDispatch,
fetchAdminTaskScreenshot,
fetchAdminTaskDetail,
} from '@/services/admin'
import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import type { ManualDispatchForm } from './types'
export function useAdminTaskDetail() {
const route = useRoute()
const loading = ref(true)
const actionLoading = ref(false)
const errorMessage = ref('')
const detail = ref<AdminTaskDetail | null>(null)
const lastClaimUrl = ref('')
const screenshotPreviewUrl = ref('')
const manualDispatchForm = ref<ManualDispatchForm>({
deliveryReference: '',
deliveredCredential: '',
resultMessage: '',
})
const canManageTaskLifecycle = computed(() => hasAdminRole('operator'))
const canCloseTasks = computed(() => hasAdminRole('support'))
const canViewSensitiveTaskData = computed(() =>
Boolean(detail.value?.operations.canViewSensitiveTaskData),
)
const claimUrl = computed(() => {
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
const taskStatus = String(detail.value?.task.status || '').trim()
if (tokenStatus !== 'active' || ['closed', 'expired'].includes(taskStatus)) {
return ''
}
return lastClaimUrl.value || detail.value?.claimToken?.claimUrl || ''
})
const claimLinkInvalid = computed(() => {
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
return Boolean(
detail.value?.claimToken?.claimUrl &&
tokenStatus &&
tokenStatus !== 'active',
)
})
const screenshotSectionTitle = computed(() => {
if (detail.value?.task.status === 'redeemed') {
return '结果截图'
}
return detail.value?.review.required ? '客服复核截图' : '结果截图'
})
async function loadDetail() {
loading.value = true
errorMessage.value = ''
try {
const response = await fetchAdminTaskDetail(String(route.params.taskId || ''))
detail.value = response.data
syncManualDispatchForm(response.data)
await loadScreenshotPreview(response.data)
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取任务详情失败'
} finally {
loading.value = false
}
}
async function runAction(
action: () => Promise<{ data: AdminTaskActionResponse }>,
successMessage: string,
confirmText: string,
) {
try {
await showConfirm(confirmText, '确认操作', {
type: 'warning',
confirmButtonText: '继续执行',
cancelButtonText: '取消',
})
} catch {
return
}
actionLoading.value = true
try {
const response = await action()
if (response.data.claimUrl) {
lastClaimUrl.value = response.data.claimUrl
}
showSuccess(successMessage)
await loadDetail()
} catch (error) {
showError(error instanceof Error ? error.message : '操作失败')
} finally {
actionLoading.value = false
}
}
async function loadScreenshotPreview(taskDetail: AdminTaskDetail) {
clearScreenshotPreview()
if (!taskDetail.screenshotUrl) {
return
}
try {
const blob = await fetchAdminTaskScreenshot(taskDetail.task.taskId)
screenshotPreviewUrl.value = URL.createObjectURL(blob)
} catch {
screenshotPreviewUrl.value = ''
}
}
function clearScreenshotPreview() {
if (screenshotPreviewUrl.value) {
URL.revokeObjectURL(screenshotPreviewUrl.value)
screenshotPreviewUrl.value = ''
}
}
function syncManualDispatchForm(taskDetail: AdminTaskDetail) {
manualDispatchForm.value = {
deliveryReference: taskDetail.manualDispatch?.deliveryReference || '',
deliveredCredential: taskDetail.manualDispatch?.deliveredCredential || '',
resultMessage:
taskDetail.manualDispatch?.resultMessage || taskDetail.task.resultMessage || '',
}
}
async function submitManualDispatch(outcome: 'delivered' | 'failed') {
if (!detail.value) {
return
}
const actionLabel = outcome === 'failed' ? '标记履约失败' : '标记已完成履约'
const confirmText =
outcome === 'failed'
? `确认把任务 ${detail.value.task.taskNo} 回写为人工履约失败吗?这会把任务直接收口并保留失败原因。`
: `确认把任务 ${detail.value.task.taskNo} 回写为人工履约完成吗?这会把任务直接标记为已发放。`
await runAction(
() =>
completeAdminTaskManualDispatch(detail.value!.task.taskId, {
outcome,
resultMessage: manualDispatchForm.value.resultMessage,
deliveryReference: manualDispatchForm.value.deliveryReference,
deliveredCredential: manualDispatchForm.value.deliveredCredential,
}),
actionLabel,
confirmText,
)
}
async function copyText(value: string, successMessage: string) {
const text = String(value || '').trim()
if (!text) {
showError('当前没有可复制的内容')
return
}
try {
await navigator.clipboard.writeText(text)
showSuccess(successMessage)
} catch {
showError('复制失败,请手动复制')
}
}
function openExternalLink(value: string) {
const text = String(value || '').trim()
if (!text) {
return
}
window.open(text, '_blank', 'noopener,noreferrer')
}
onBeforeUnmount(clearScreenshotPreview)
return {
loading,
actionLoading,
errorMessage,
detail,
screenshotPreviewUrl,
manualDispatchForm,
canManageTaskLifecycle,
canCloseTasks,
canViewSensitiveTaskData,
claimUrl,
claimLinkInvalid,
screenshotSectionTitle,
loadDetail,
runAction,
submitManualDispatch,
copyText,
openExternalLink,
}
}
@@ -0,0 +1,193 @@
import { computed, type Ref } from 'vue'
import {
dispatchAdminTaskKuaishouCloudFulfillment,
prepareAdminTaskKuaishouCloudFulfillment,
refreshAdminTaskKuaishouCloudRoleInfo,
returnNumberAdminTaskKuaishouCloudFulfillment,
} from '@/services/admin'
import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
import { formatKuaishouRoleInfoLabel } from '@/utils/admin-display'
import { formatAdminDateTime } from '@/utils/admin-time'
import type { ChecklistItem, RoleInfoEntry, SummaryCard } from './types'
export function useAdminTaskKuaishouCloud(
detail: Ref<AdminTaskDetail | null>,
runAction: (
action: () => Promise<{ data: AdminTaskActionResponse }>,
successMessage: string,
confirmText: string,
) => Promise<void>,
) {
const kuaishouCloudFlow = computed(() => detail.value?.kuaishouCloudFulfillment || null)
const kuaishouCloudRole = computed(() => kuaishouCloudFlow.value?.role || null)
const isKuaishouCloudTask = computed(
() => detail.value?.task.executorKey === 'kuaishou_ct_assisted',
)
const summaryCards = computed<SummaryCard[]>(() => {
if (!detail.value) {
return []
}
return [
{
label: '任务状态',
value: detail.value.task.status || '-',
tone: 'blue',
},
{
label: '履约状态',
value: detail.value.task.deliveryStatus || '-',
tone: 'green',
},
{
label: '当前角色',
value: kuaishouCloudRole.value?.name || detail.value.task.roleName || '-',
tone: 'amber',
},
{
label: '角色 ID',
value: kuaishouCloudRole.value?.rid || detail.value.task.roleId || '-',
tone: 'slate',
},
]
})
const kuaishouRoleInfoEntries = computed<RoleInfoEntry[]>(() => {
const rawInfo = kuaishouCloudRole.value?.rawInfo
if (!rawInfo) {
return []
}
return Object.entries(rawInfo)
.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '')
.map(([key, value]) => ({
key,
label: formatKuaishouRoleInfoLabel(key),
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
}))
})
const kuaishouCloudChecklist = computed<ChecklistItem[]>(() => {
const flow = kuaishouCloudFlow.value
const role = kuaishouCloudRole.value
if (!flow) {
return []
}
return [
{
key: 'ticket',
label: '核销码校验',
status: flow.ticket.status || 'pending',
detail: flow.ticket.verifiedAt
? `已于 ${formatAdminDateTime(flow.ticket.verifiedAt)} 校验`
: '等待客户提交并校验核销码',
},
{
key: 'bind',
label: '绑定资源',
status: flow.binding.prepareStatus || 'pending',
detail: flow.binding.bindPreparedAt
? `绑定资源已准备,VN ${flow.binding.vnId || '-'}`
: '等待准备 Cloud 绑定资源',
},
{
key: 'role',
label: '角色识别',
status: role?.status || 'pending',
detail:
role?.name || role?.rid
? `${role?.name || '-'} / ${role?.rid || '-'}`
: '客户绑定后刷新角色信息',
},
{
key: 'dispatch',
label: '发货执行',
status: flow.dispatch.status || 'pending',
detail: flow.dispatch.dispatchAt
? `已于 ${formatAdminDateTime(flow.dispatch.dispatchAt)} 发货`
: '等待客服确认绑定并发货',
},
{
key: 'return',
label: '退还号码',
status: flow.returnNumber.status || 'pending',
detail: flow.returnNumber.returnedAt
? `已于 ${formatAdminDateTime(flow.returnNumber.returnedAt)} 退号`
: '发货后执行退还号码',
},
{
key: 'consume',
label: '快手核销',
status: flow.consume.status || 'pending',
detail: flow.consume.consumedAt
? `已于 ${formatAdminDateTime(flow.consume.consumedAt)} 完成核销`
: flow.consume.errorMessage || '等待退号后核销收口',
},
]
})
async function submitKuaishouCloudPrepare() {
if (!detail.value) {
return
}
await runAction(
() => prepareAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId),
'绑定资源已准备完成',
`确认开始为任务 ${detail.value.task.taskNo} 准备绑定资源吗?系统会自动检查背包、余额并申请虚拟号。`,
)
}
async function submitKuaishouCloudDispatch() {
if (!detail.value) {
return
}
await runAction(
() => dispatchAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId),
'已完成绑定确认并发货',
`确认客户已经完成绑定,并立即为任务 ${detail.value.task.taskNo} 执行发货吗?这个动作会把"确认绑定完成"和"发货"合并为一步。`,
)
}
async function submitKuaishouCloudRefreshRoleInfo() {
if (!detail.value) {
return
}
await runAction(
() => refreshAdminTaskKuaishouCloudRoleInfo(detail.value!.task.taskId),
'角色信息已刷新',
`确认刷新任务 ${detail.value.task.taskNo} 当前云绑定角色信息吗?系统会重新查询 cloudtentacles 的绑定结果。`,
)
}
async function submitKuaishouCloudReturnNumber() {
if (!detail.value) {
return
}
await runAction(
() => returnNumberAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId),
'号码已退还',
`确认退还任务 ${detail.value.task.taskNo} 当前使用的虚拟号吗?退号后该流程会正式收口。`,
)
}
return {
kuaishouCloudFlow,
kuaishouCloudRole,
isKuaishouCloudTask,
summaryCards,
kuaishouRoleInfoEntries,
kuaishouCloudChecklist,
submitKuaishouCloudPrepare,
submitKuaishouCloudDispatch,
submitKuaishouCloudRefreshRoleInfo,
submitKuaishouCloudReturnNumber,
}
}