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> = {}) =>
|
export const getReportList = (params: Record<string, unknown> = {}) =>
|
||||||
apiGet('/report/list', params)
|
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) =>
|
export const startHighlight = (reportId: number) =>
|
||||||
apiPost('/report/highlight/start', { reportId })
|
apiPost('/report/highlight/start', { reportId })
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,11 @@
|
|||||||
<el-option label="失败" value="failed" />
|
<el-option label="失败" value="failed" />
|
||||||
<el-option label="异常(失败+超时)" value="anomaly" />
|
<el-option label="异常(失败+超时)" value="anomaly" />
|
||||||
</el-select>
|
</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-input v-model="keyword" clearable placeholder="宠物名 / 报告ID" style="width: 180px" />
|
||||||
<el-button type="primary" @click="load">查询</el-button>
|
<el-button type="primary" @click="load">查询</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -22,10 +27,34 @@
|
|||||||
<div v-if="row.highlightFailReasonLabel" class="fail">{{ row.highlightFailReasonLabel }}</div>
|
<div v-if="row.highlightFailReasonLabel" class="fail">{{ row.highlightFailReasonLabel }}</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
||||||
<el-button link type="primary" @click="copyLink(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
|
<el-button
|
||||||
v-if="row.highlightVideoStatus === 'failed' || isStale(row)"
|
v-if="row.highlightVideoStatus === 'failed' || isStale(row)"
|
||||||
link
|
link
|
||||||
@ -43,6 +72,9 @@
|
|||||||
<el-descriptions-item label="服务">{{ current.serviceType }}</el-descriptions-item>
|
<el-descriptions-item label="服务">{{ current.serviceType }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="预约时间">{{ current.appointmentTime }}</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="成片">{{ 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="失败原因">
|
<el-descriptions-item v-if="current.highlightFailReasonLabel" label="失败原因">
|
||||||
{{ current.highlightFailReasonLabel }}
|
{{ current.highlightFailReasonLabel }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@ -57,6 +89,20 @@
|
|||||||
<div class="drawer-actions">
|
<div class="drawer-actions">
|
||||||
<el-button type="primary" @click="copyLink(current)">复制公开链接</el-button>
|
<el-button type="primary" @click="copyLink(current)">复制公开链接</el-button>
|
||||||
<el-button v-if="current.reportToken" @click="openH5(current)">打开 H5</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
|
<el-button
|
||||||
v-if="current.highlightVideoStatus === 'failed' || isStale(current)"
|
v-if="current.highlightVideoStatus === 'failed' || isStale(current)"
|
||||||
type="warning"
|
type="warning"
|
||||||
@ -71,8 +117,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { getReportList, startHighlight } from '@/api'
|
import { confirmReportSent, getReportList, startHighlight } from '@/api'
|
||||||
import { isHighlightProcessingStale, reportPublicUrl } from '@/utils/publicLinks'
|
import { isHighlightProcessingStale, reportPublicUrl } from '@/utils/publicLinks'
|
||||||
|
|
||||||
type ReportRow = {
|
type ReportRow = {
|
||||||
@ -85,6 +131,9 @@ type ReportRow = {
|
|||||||
highlightVideoStatus?: string
|
highlightVideoStatus?: string
|
||||||
highlightFailReasonLabel?: string
|
highlightFailReasonLabel?: string
|
||||||
reportToken?: string
|
reportToken?: string
|
||||||
|
sendStatus?: 'unknown' | 'unsent' | 'sent'
|
||||||
|
sentAt?: string
|
||||||
|
sendChannel?: 'wechat' | 'qr' | 'other'
|
||||||
beforePhotos?: string[]
|
beforePhotos?: string[]
|
||||||
afterPhotos?: string[]
|
afterPhotos?: string[]
|
||||||
}
|
}
|
||||||
@ -93,7 +142,9 @@ const route = useRoute()
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rows = ref<ReportRow[]>([])
|
const rows = ref<ReportRow[]>([])
|
||||||
const highlightStatus = ref((route.query.highlightStatus as string) || '')
|
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 keyword = ref((route.query.q as string) || '')
|
||||||
|
const confirmingId = ref<number | null>(null)
|
||||||
const drawer = ref(false)
|
const drawer = ref(false)
|
||||||
const current = ref<ReportRow | null>(null)
|
const current = ref<ReportRow | null>(null)
|
||||||
|
|
||||||
@ -114,13 +165,20 @@ function hlLabel(row: ReportRow) {
|
|||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
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[]) : []
|
let list = Array.isArray(res.data) ? (res.data as ReportRow[]) : []
|
||||||
if (highlightStatus.value === 'anomaly' || highlightStatus.value === 'failed') {
|
if (highlightStatus.value === 'anomaly' || highlightStatus.value === 'failed') {
|
||||||
list = list.filter((r) => r.highlightVideoStatus === 'failed' || isStale(r))
|
list = list.filter((r) => r.highlightVideoStatus === 'failed' || isStale(r))
|
||||||
} else if (highlightStatus.value) {
|
} else if (highlightStatus.value) {
|
||||||
list = list.filter((r) => r.highlightVideoStatus === 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()) {
|
if (keyword.value.trim()) {
|
||||||
const q = keyword.value.trim()
|
const q = keyword.value.trim()
|
||||||
list = list.filter((r) =>
|
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) {
|
function openDetail(row: ReportRow) {
|
||||||
current.value = row
|
current.value = row
|
||||||
drawer.value = true
|
drawer.value = true
|
||||||
@ -170,6 +289,11 @@ onMounted(load)
|
|||||||
color: #c45656;
|
color: #c45656;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.sent-at {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
.media {
|
.media {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,7 +102,7 @@
|
|||||||
<span class="section-kicker">服务结果</span>
|
<span class="section-kicker">服务结果</span>
|
||||||
<h3>今天的报告走到哪一步</h3>
|
<h3>今天的报告走到哪一步</h3>
|
||||||
</div>
|
</div>
|
||||||
<span class="funnel-note">发送与再次预约将在下一阶段接入真实状态</span>
|
<span class="funnel-note">发送已接入真实确认;再次预约仍待归因</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="funnel-grid">
|
<div class="funnel-grid">
|
||||||
<div v-for="step in funnelSteps" :key="step.label" class="funnel-step">
|
<div v-for="step in funnelSteps" :key="step.label" class="funnel-step">
|
||||||
@ -129,6 +129,7 @@ type WorkbenchMetrics = {
|
|||||||
inServiceCount: number
|
inServiceCount: number
|
||||||
upcomingTodayCount: number
|
upcomingTodayCount: number
|
||||||
highlightAnomalyCount: number
|
highlightAnomalyCount: number
|
||||||
|
unsentReportCount: number
|
||||||
dueFollowUpCount: number
|
dueFollowUpCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +170,7 @@ const emptyMetrics: WorkbenchMetrics = {
|
|||||||
inServiceCount: 0,
|
inServiceCount: 0,
|
||||||
upcomingTodayCount: 0,
|
upcomingTodayCount: 0,
|
||||||
highlightAnomalyCount: 0,
|
highlightAnomalyCount: 0,
|
||||||
|
unsentReportCount: 0,
|
||||||
dueFollowUpCount: 0,
|
dueFollowUpCount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,6 +237,11 @@ const groupDefinitions: Record<string, { title: string; description: string; to:
|
|||||||
description: '失败或处理超过 30 分钟,确认素材后再重试。',
|
description: '失败或处理超过 30 分钟,确认素材后再重试。',
|
||||||
to: { path: '/reports', query: { highlightStatus: 'anomaly' } },
|
to: { path: '/reports', query: { highlightStatus: 'anomaly' } },
|
||||||
},
|
},
|
||||||
|
unsent_report: {
|
||||||
|
title: '报告尚未发送',
|
||||||
|
description: '复制链接不等于发送;宠主实际收到后,请在报告列表显式确认。',
|
||||||
|
to: { path: '/reports', query: { sendStatus: 'unsent' } },
|
||||||
|
},
|
||||||
follow_up_due: {
|
follow_up_due: {
|
||||||
title: '回访日期已到',
|
title: '回访日期已到',
|
||||||
description: '只显示低敏摘要;进入回访池后再按权限查看联系信息。',
|
description: '只显示低敏摘要;进入回访池后再按权限查看联系信息。',
|
||||||
@ -294,7 +301,12 @@ function goItem(kind: string, item: ActionItem) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (item.resourceType === 'report') {
|
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
|
return
|
||||||
}
|
}
|
||||||
go(groupMeta(kind).to)
|
go(groupMeta(kind).to)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user