feat: confirm report delivery in admin
This commit is contained in:
parent
9199072435
commit
21cb134a29
@ -24,6 +24,10 @@ export const cancelAppointment = (id: number) =>
|
||||
export const getReportList = (params: Record<string, unknown> = {}) =>
|
||||
apiGet('/report/list', params)
|
||||
|
||||
/** 员工在真实发送完成后显式确认;复制链接或打开 H5 不会自动确认。 */
|
||||
export const confirmReportSent = (reportId: number, channel: 'wechat' | 'qr' | 'other') =>
|
||||
apiPost('/report/confirm-sent', { reportId, channel })
|
||||
|
||||
export const startHighlight = (reportId: number) =>
|
||||
apiPost('/report/highlight/start', { reportId })
|
||||
|
||||
|
||||
@ -8,6 +8,11 @@
|
||||
<el-option label="失败" value="failed" />
|
||||
<el-option label="异常(失败+超时)" value="anomaly" />
|
||||
</el-select>
|
||||
<el-select v-model="sendStatus" clearable placeholder="发送状态" style="width: 150px">
|
||||
<el-option label="待发送" value="unsent" />
|
||||
<el-option label="已发送" value="sent" />
|
||||
<el-option label="历史未知" value="unknown" />
|
||||
</el-select>
|
||||
<el-input v-model="keyword" clearable placeholder="宠物名 / 报告ID" style="width: 180px" />
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
</div>
|
||||
@ -22,10 +27,34 @@
|
||||
<div v-if="row.highlightFailReasonLabel" class="fail">{{ row.highlightFailReasonLabel }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<el-table-column label="发送" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="sendTagType(row.sendStatus)" effect="light">
|
||||
{{ sendLabel(row.sendStatus) }}
|
||||
</el-tag>
|
||||
<div v-if="row.sentAt" class="sent-at">{{ row.sentAt }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="380" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
||||
<el-button link type="primary" @click="copyLink(row)">复制链接</el-button>
|
||||
<el-dropdown
|
||||
v-if="row.sendStatus !== 'sent'"
|
||||
:disabled="confirmingId === row.id"
|
||||
@command="onTableConfirm(row, $event)"
|
||||
>
|
||||
<el-button link type="success" :loading="confirmingId === row.id">
|
||||
确认已发
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="wechat">微信消息</el-dropdown-item>
|
||||
<el-dropdown-item command="qr">当面扫码</el-dropdown-item>
|
||||
<el-dropdown-item command="other">其他方式</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button
|
||||
v-if="row.highlightVideoStatus === 'failed' || isStale(row)"
|
||||
link
|
||||
@ -43,6 +72,9 @@
|
||||
<el-descriptions-item label="服务">{{ current.serviceType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预约时间">{{ current.appointmentTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="成片">{{ hlLabel(current) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发送状态">{{ sendLabel(current.sendStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="current.sentAt" label="首次确认发送">{{ current.sentAt }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="current.sendChannel" label="发送方式">{{ channelLabel(current.sendChannel) }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="current.highlightFailReasonLabel" label="失败原因">
|
||||
{{ current.highlightFailReasonLabel }}
|
||||
</el-descriptions-item>
|
||||
@ -57,6 +89,20 @@
|
||||
<div class="drawer-actions">
|
||||
<el-button type="primary" @click="copyLink(current)">复制公开链接</el-button>
|
||||
<el-button v-if="current.reportToken" @click="openH5(current)">打开 H5</el-button>
|
||||
<el-dropdown
|
||||
v-if="current.sendStatus !== 'sent'"
|
||||
:disabled="confirmingId === current.id"
|
||||
@command="onCurrentConfirm"
|
||||
>
|
||||
<el-button type="success" :loading="confirmingId === current.id">确认已发给宠主</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="wechat">微信消息</el-dropdown-item>
|
||||
<el-dropdown-item command="qr">当面扫码</el-dropdown-item>
|
||||
<el-dropdown-item command="other">其他方式</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button
|
||||
v-if="current.highlightVideoStatus === 'failed' || isStale(current)"
|
||||
type="warning"
|
||||
@ -71,8 +117,8 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getReportList, startHighlight } from '@/api'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { confirmReportSent, getReportList, startHighlight } from '@/api'
|
||||
import { isHighlightProcessingStale, reportPublicUrl } from '@/utils/publicLinks'
|
||||
|
||||
type ReportRow = {
|
||||
@ -85,6 +131,9 @@ type ReportRow = {
|
||||
highlightVideoStatus?: string
|
||||
highlightFailReasonLabel?: string
|
||||
reportToken?: string
|
||||
sendStatus?: 'unknown' | 'unsent' | 'sent'
|
||||
sentAt?: string
|
||||
sendChannel?: 'wechat' | 'qr' | 'other'
|
||||
beforePhotos?: string[]
|
||||
afterPhotos?: string[]
|
||||
}
|
||||
@ -93,7 +142,9 @@ const route = useRoute()
|
||||
const loading = ref(false)
|
||||
const rows = ref<ReportRow[]>([])
|
||||
const highlightStatus = ref((route.query.highlightStatus as string) || '')
|
||||
const sendStatus = ref((route.query.sendStatus as string) || '')
|
||||
const keyword = ref((route.query.q as string) || '')
|
||||
const confirmingId = ref<number | null>(null)
|
||||
const drawer = ref(false)
|
||||
const current = ref<ReportRow | null>(null)
|
||||
|
||||
@ -114,13 +165,20 @@ function hlLabel(row: ReportRow) {
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getReportList({ page: 1, pageSize: 200 })
|
||||
const res = await getReportList({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
sendStatus: sendStatus.value || undefined,
|
||||
})
|
||||
let list = Array.isArray(res.data) ? (res.data as ReportRow[]) : []
|
||||
if (highlightStatus.value === 'anomaly' || highlightStatus.value === 'failed') {
|
||||
list = list.filter((r) => r.highlightVideoStatus === 'failed' || isStale(r))
|
||||
} else if (highlightStatus.value) {
|
||||
list = list.filter((r) => r.highlightVideoStatus === highlightStatus.value)
|
||||
}
|
||||
if (sendStatus.value) {
|
||||
list = list.filter((r) => r.sendStatus === sendStatus.value)
|
||||
}
|
||||
if (keyword.value.trim()) {
|
||||
const q = keyword.value.trim()
|
||||
list = list.filter((r) =>
|
||||
@ -133,6 +191,67 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function sendLabel(status?: ReportRow['sendStatus']) {
|
||||
if (status === 'sent') return '已发送'
|
||||
if (status === 'unsent') return '待发送'
|
||||
if (status === 'unknown') return '历史未知'
|
||||
return '—'
|
||||
}
|
||||
|
||||
function sendTagType(status?: ReportRow['sendStatus']) {
|
||||
if (status === 'sent') return 'success'
|
||||
if (status === 'unsent') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function channelLabel(channel?: ReportRow['sendChannel']) {
|
||||
if (channel === 'wechat') return '微信消息'
|
||||
if (channel === 'qr') return '当面扫码'
|
||||
if (channel === 'other') return '其他方式'
|
||||
return '—'
|
||||
}
|
||||
|
||||
async function confirmSent(row: ReportRow, rawChannel: unknown) {
|
||||
const channel = rawChannel as 'wechat' | 'qr' | 'other'
|
||||
if (!['wechat', 'qr', 'other'].includes(channel) || row.sendStatus === 'sent') return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认宠主已通过“${channelLabel(channel)}”实际收到报告?复制链接或预览不算发送。`,
|
||||
'确认已发送',
|
||||
{ confirmButtonText: '确认已发送', cancelButtonText: '取消', type: 'warning' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
confirmingId.value = row.id
|
||||
try {
|
||||
const res = await confirmReportSent(row.id, channel)
|
||||
if (res.code !== 200 || !res.data) {
|
||||
ElMessage.error(res.message || '确认失败')
|
||||
return
|
||||
}
|
||||
const data = res.data as Record<string, unknown>
|
||||
row.sendStatus = 'sent'
|
||||
row.sentAt = String(data.sentAt || '')
|
||||
row.sendChannel = (data.sendChannel as ReportRow['sendChannel']) || channel
|
||||
if (current.value?.id === row.id) current.value = row
|
||||
ElMessage.success(data.alreadyConfirmed ? '此前已确认发送' : '已记录发送')
|
||||
} catch {
|
||||
ElMessage.error('确认失败,请检查网络后重试')
|
||||
} finally {
|
||||
confirmingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function onTableConfirm(row: ReportRow, channel: unknown) {
|
||||
void confirmSent(row, channel)
|
||||
}
|
||||
|
||||
function onCurrentConfirm(channel: unknown) {
|
||||
if (current.value) void confirmSent(current.value, channel)
|
||||
}
|
||||
|
||||
function openDetail(row: ReportRow) {
|
||||
current.value = row
|
||||
drawer.value = true
|
||||
@ -170,6 +289,11 @@ onMounted(load)
|
||||
color: #c45656;
|
||||
font-size: 12px;
|
||||
}
|
||||
.sent-at {
|
||||
margin-top: 4px;
|
||||
color: #909399;
|
||||
font-size: 11px;
|
||||
}
|
||||
.media {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@
|
||||
<span class="section-kicker">服务结果</span>
|
||||
<h3>今天的报告走到哪一步</h3>
|
||||
</div>
|
||||
<span class="funnel-note">发送与再次预约将在下一阶段接入真实状态</span>
|
||||
<span class="funnel-note">发送已接入真实确认;再次预约仍待归因</span>
|
||||
</div>
|
||||
<div class="funnel-grid">
|
||||
<div v-for="step in funnelSteps" :key="step.label" class="funnel-step">
|
||||
@ -129,6 +129,7 @@ type WorkbenchMetrics = {
|
||||
inServiceCount: number
|
||||
upcomingTodayCount: number
|
||||
highlightAnomalyCount: number
|
||||
unsentReportCount: number
|
||||
dueFollowUpCount: number
|
||||
}
|
||||
|
||||
@ -169,6 +170,7 @@ const emptyMetrics: WorkbenchMetrics = {
|
||||
inServiceCount: 0,
|
||||
upcomingTodayCount: 0,
|
||||
highlightAnomalyCount: 0,
|
||||
unsentReportCount: 0,
|
||||
dueFollowUpCount: 0,
|
||||
}
|
||||
|
||||
@ -235,6 +237,11 @@ const groupDefinitions: Record<string, { title: string; description: string; to:
|
||||
description: '失败或处理超过 30 分钟,确认素材后再重试。',
|
||||
to: { path: '/reports', query: { highlightStatus: 'anomaly' } },
|
||||
},
|
||||
unsent_report: {
|
||||
title: '报告尚未发送',
|
||||
description: '复制链接不等于发送;宠主实际收到后,请在报告列表显式确认。',
|
||||
to: { path: '/reports', query: { sendStatus: 'unsent' } },
|
||||
},
|
||||
follow_up_due: {
|
||||
title: '回访日期已到',
|
||||
description: '只显示低敏摘要;进入回访池后再按权限查看联系信息。',
|
||||
@ -294,7 +301,12 @@ function goItem(kind: string, item: ActionItem) {
|
||||
return
|
||||
}
|
||||
if (item.resourceType === 'report') {
|
||||
go({ path: '/reports', query: { q: String(item.resourceId), highlightStatus: 'anomaly' } })
|
||||
go({
|
||||
path: '/reports',
|
||||
query: kind === 'unsent_report'
|
||||
? { q: String(item.resourceId), sendStatus: 'unsent' }
|
||||
: { q: String(item.resourceId), highlightStatus: 'anomaly' },
|
||||
})
|
||||
return
|
||||
}
|
||||
go(groupMeta(kind).to)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user