602 lines
16 KiB
Vue
602 lines
16 KiB
Vue
<template>
|
||
<div v-loading="loading" class="door-log">
|
||
<template v-if="detail && detail.id">
|
||
<div class="door-log__head">
|
||
<span class="door-log__title">智能柜流程日志</span>
|
||
<el-tag v-if="flowStatusText" size="small" :type="flowStatusTagType" effect="plain">当前节点:{{ flowStatusText }}</el-tag>
|
||
<el-tag v-if="detail.skuSubmitRecogId" size="small" type="info" effect="plain">识别ID:{{ detail.skuSubmitRecogId }}</el-tag>
|
||
</div>
|
||
|
||
<div class="door-log__flow">
|
||
<div
|
||
v-for="node in flowNodes"
|
||
:key="node.key"
|
||
class="flow-node"
|
||
:class="[`is-${node.status}`, { 'is-idle': !node.time }]"
|
||
>
|
||
<div class="flow-node__rail">
|
||
<span class="flow-node__dot" />
|
||
</div>
|
||
<div class="flow-node__card">
|
||
<div class="flow-node__head">
|
||
<span class="flow-node__title">{{ node.title }}</span>
|
||
<el-tag v-if="node.tagText" size="small" :type="node.tagType" effect="light">{{ node.tagText }}</el-tag>
|
||
<span class="flow-node__time">{{ node.time || "--" }}</span>
|
||
</div>
|
||
<div v-if="node.desc" class="flow-node__desc">{{ node.desc }}</div>
|
||
<div v-if="node.hint" class="flow-node__hint">{{ node.hint }}</div>
|
||
<div v-if="node.payloads?.length" class="flow-node__payloads">
|
||
<el-button
|
||
v-for="payload in node.payloads"
|
||
:key="payload.label"
|
||
link
|
||
type="primary"
|
||
size="small"
|
||
@click="openPayload(node.title + ' - ' + payload.label, payload.json)"
|
||
>
|
||
{{ payload.label }}
|
||
</el-button>
|
||
</div>
|
||
<!-- 错误节点:人工处理入口 -->
|
||
<div v-if="node.key === 'error'" class="flow-node__actions">
|
||
<el-button v-if="!detail?.hardAuth?.manualChangeUserId" type="primary" link size="small" @click="visibleErrorChangeDiaog = true">
|
||
处理
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 手动结算/取消操作快照 -->
|
||
<div v-if="manualVo" class="flow-node is-manual">
|
||
<div class="flow-node__rail">
|
||
<span class="flow-node__dot" />
|
||
</div>
|
||
<div class="flow-node__card">
|
||
<div class="flow-node__head">
|
||
<span class="flow-node__title">{{ manualVo.manualCancel ? "手动取消订单" : "手动结算" }}</span>
|
||
<el-tag size="small" type="warning" effect="light">{{ manualVo.manualCancel ? "人工取消" : "人工结算" }}</el-tag>
|
||
<span class="flow-node__time">{{ manualVo.time || "--" }}</span>
|
||
</div>
|
||
<div class="flow-node__desc">
|
||
操作人:{{ manualVo.operator || "--" }}
|
||
<template v-if="manualVo.reason"> 备注:{{ manualVo.reason }}</template>
|
||
</div>
|
||
<div v-if="!manualVo.manualCancel && manualItems.length" class="flow-node__cart">
|
||
<div v-for="(item, index) in manualItems" :key="index" class="flow-node__cart-row">
|
||
<span class="flow-node__cart-name">{{ item.name }}</span>
|
||
<span class="flow-node__cart-count">{{ displayPrice(item.price) }} × {{ item.productCount }}</span>
|
||
<span class="flow-node__cart-price">{{ displayPrice(item.totalPrice) }}</span>
|
||
</div>
|
||
<div class="flow-node__cart-total">
|
||
<span>合计:<span class="flow-node__cart-price">{{ displayPrice(manualVo.totalPrice) }}</span></span>
|
||
<span v-if="manualVo.payChannel" class="flow-node__cart-channel">({{ manualVo.payChannel }})</span>
|
||
</div>
|
||
</div>
|
||
<div class="flow-node__payloads">
|
||
<el-button link type="primary" size="small" @click="openPayload((manualVo.manualCancel ? '手动取消订单' : '手动结算') + ' - 快照报文', manualRaw)">
|
||
快照报文
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 手动处理智能柜订单记录 -->
|
||
<div v-if="detail?.hardAuth?.manualChangeUserId" class="flow-node is-manual">
|
||
<div class="flow-node__rail">
|
||
<span class="flow-node__dot" />
|
||
</div>
|
||
<div class="flow-node__card">
|
||
<div class="flow-node__head">
|
||
<span class="flow-node__title">手动处理智能柜订单</span>
|
||
<el-tag size="small" type="warning" effect="light">人工处理</el-tag>
|
||
<span class="flow-node__time">{{ detail.hardAuth.manualChangeTime }}</span>
|
||
</div>
|
||
<div v-if="detail.hardAuth.manualChangeRemark" class="flow-node__desc">{{ detail.hardAuth.manualChangeRemark }}</div>
|
||
<div v-if="detail.hardAuth.manualChangePic" class="flow-node__desc">
|
||
<preview-image width="40px" height="40px" :src="detail.hardAuth.manualChangePic" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<el-empty v-else-if="!loading" description="暂无智能柜流程日志" :image-size="60" />
|
||
|
||
<!-- 报文查看抽屉 -->
|
||
<el-drawer v-model="payloadVisible" :title="payloadTitle" size="45%" append-to-body>
|
||
<vue-json-pretty v-if="payloadData !== null" :data="payloadData" showLength showLineNumber showIcon :collapsedOnClickBrackets="false" />
|
||
<pre v-else class="door-log__raw">{{ payloadRaw }}</pre>
|
||
</el-drawer>
|
||
|
||
<!-- 手动处理智能柜订单 -->
|
||
<el-dialog title="手动处理智能柜订单" width="30%" @close="dialogClose" v-model="visibleErrorChangeDiaog" :close-on-click-modal="false" draggable>
|
||
<el-form ref="formRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="formLoading">
|
||
<el-row :gutter="24">
|
||
<el-col :span="24" class="mb20">
|
||
<el-form-item label="设备名" prop="image">
|
||
<upload-imgs v-model="form.image" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24" class="mb20">
|
||
<el-form-item label="处理原因" prop="remark">
|
||
<el-input v-model="form.remark" placeholder="请输入处理原因" type="textarea" :autosize="{ minRows: 6 }" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="dialogClose">取消</el-button>
|
||
<el-button type="primary" @click="onSubmit" :disabled="formLoading">确认</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
<script setup lang="ts" name="hard-door-log">
|
||
import VueJsonPretty from "vue-json-pretty"
|
||
import "vue-json-pretty/lib/styles.css"
|
||
import * as hardFlowManager from "/@/api/hard/hard-door-flow"
|
||
import { useMessage } from "/@/hooks/message"
|
||
import { auth } from "/@/utils/authFunction"
|
||
import { displayPrice } from "/@/utils/formatMoney"
|
||
|
||
type Props = {
|
||
orderId?: string
|
||
}
|
||
const props = withDefaults(defineProps<Props>(), { orderId: "" })
|
||
|
||
const formRef = ref()
|
||
|
||
const visibleErrorChangeDiaog = ref<boolean>(false)
|
||
|
||
const detail = ref<any>({})
|
||
const loading = ref<boolean>(false)
|
||
const formLoading = ref<boolean>(false)
|
||
|
||
// 报文抽屉
|
||
const payloadVisible = ref(false)
|
||
const payloadTitle = ref("")
|
||
const payloadRaw = ref("")
|
||
const payloadData = ref<any>(null)
|
||
|
||
// 提交表单数据
|
||
const form = reactive({
|
||
flowId: "",
|
||
image: "",
|
||
remark: "用户未购买商品",
|
||
})
|
||
|
||
// 定义校验规则
|
||
const dataRules = ref({
|
||
remark: [{ required: true, message: "处理原因不能为空", trigger: "blur" }],
|
||
})
|
||
|
||
/** 流程节点状态。 */
|
||
type FlowNodeStatus = "done" | "waiting" | "warning" | "error"
|
||
|
||
/** 一个流程节点;payloads 为可查看的请求/回调报文。 */
|
||
interface FlowNode {
|
||
key: string
|
||
title: string
|
||
time?: string
|
||
status: FlowNodeStatus
|
||
desc?: string
|
||
hint?: string
|
||
payloads?: { label: string; json?: string }[]
|
||
tagText?: string
|
||
tagType?: "success" | "info" | "warning" | "danger"
|
||
}
|
||
|
||
/** 流程状态编码 → 名称。 */
|
||
const FLOW_STATUS_MAP: Record<string, string> = {
|
||
"10": "用户授权",
|
||
"20": "申请开门",
|
||
"30": "开门响应",
|
||
"40": "视频回调",
|
||
"50": "已提交识别",
|
||
"60": "识别响应",
|
||
"70": "订单结算",
|
||
}
|
||
|
||
const flowStatusText = computed(() => FLOW_STATUS_MAP[String(detail.value?.flowStatus || "")] || "")
|
||
|
||
const flowStatusTagType = computed(() => {
|
||
if (detail.value?.payTime) return "success"
|
||
if (detail.value?.error) return "danger"
|
||
return "warning"
|
||
})
|
||
|
||
/** 手动结算/取消快照原始报文:优先独立快照字段,兼容旧版本写入 skuCallbackVo 的手动标记。 */
|
||
const manualRaw = computed<string>(() => {
|
||
const dedicated = detail.value?.manualSettleVo
|
||
if (dedicated) return dedicated
|
||
const legacy = detail.value?.skuCallbackVo
|
||
if (!legacy) return ""
|
||
try {
|
||
const vo = typeof legacy === "string" ? JSON.parse(legacy) : legacy
|
||
return vo?.manual === true ? (typeof legacy === "string" ? legacy : JSON.stringify(legacy)) : ""
|
||
} catch {
|
||
return ""
|
||
}
|
||
})
|
||
|
||
/** 手动结算/取消快照对象。 */
|
||
const manualVo = computed<any>(() => {
|
||
if (!manualRaw.value) return null
|
||
try {
|
||
const vo = typeof manualRaw.value === "string" ? JSON.parse(manualRaw.value) : manualRaw.value
|
||
return vo?.manual === true ? vo : null
|
||
} catch {
|
||
return null
|
||
}
|
||
})
|
||
|
||
/** 快照中的购物车明细。 */
|
||
const manualItems = computed<any[]>(() => (Array.isArray(manualVo.value?.items) ? manualVo.value.items : []))
|
||
|
||
const STATUS_TAGS: Record<FlowNodeStatus, { text: string; type: "success" | "info" | "warning" | "danger" }> = {
|
||
done: { text: "成功", type: "success" },
|
||
waiting: { text: "等待中", type: "info" },
|
||
warning: { text: "未响应", type: "warning" },
|
||
error: { text: "错误", type: "danger" },
|
||
}
|
||
|
||
/** 按数据到达情况推导各节点状态与可查看报文。 */
|
||
const flowNodes = computed<FlowNode[]>(() => {
|
||
const d = detail.value || {}
|
||
const withTag = (node: FlowNode): FlowNode => {
|
||
if (node.time) {
|
||
node.tagText = STATUS_TAGS[node.status].text
|
||
node.tagType = STATUS_TAGS[node.status].type
|
||
}
|
||
// 只保留有报文数据的入口
|
||
node.payloads = node.payloads?.filter((payload) => !!payload.json)
|
||
return node
|
||
}
|
||
const nodes: FlowNode[] = [
|
||
withTag({
|
||
key: "auth",
|
||
title: "用户授权",
|
||
time: d.createTime,
|
||
status: "done",
|
||
desc: d.username ? `授权用户:${d.username}` : "",
|
||
}),
|
||
withTag({
|
||
key: "apply",
|
||
title: "智能柜申请开门",
|
||
time: d.requestOpenTime,
|
||
status: d.requestOpenTime ? "done" : "waiting",
|
||
payloads: [
|
||
{ label: "请求参数", json: d.requestOpenDto },
|
||
{ label: "请求返回", json: d.requestOpenVo },
|
||
],
|
||
}),
|
||
withTag({
|
||
key: "open",
|
||
title: "智能柜开门响应",
|
||
time: d.openCallbackTime,
|
||
status: d.openCallbackTime ? "done" : "waiting",
|
||
desc: d.isOpen != null && d.isOpen !== "" ? `开门结果:${String(d.isOpen) === "1" ? "已开门" : "未开门"}` : "",
|
||
payloads: [{ label: "回调报文", json: d.openCallbackVo }],
|
||
}),
|
||
withTag({
|
||
key: "video",
|
||
title: "智能柜视频回调",
|
||
time: d.videoCallbackTime,
|
||
status: d.videoCallbackTime ? "done" : "waiting",
|
||
payloads: [{ label: "回调报文", json: d.videoCallbackVo }],
|
||
}),
|
||
withTag({
|
||
key: "submit",
|
||
title: "提交商品识别",
|
||
time: d.skuSubmitTime,
|
||
status: d.skuSubmitTime ? "done" : "warning",
|
||
payloads: [
|
||
{ label: "提交参数", json: d.skuSubmitDto },
|
||
{ label: "提交返回", json: d.skuSubmitVo },
|
||
],
|
||
}),
|
||
withTag({
|
||
key: "recog",
|
||
title: "商品识别响应",
|
||
time: d.skuCallbackTime,
|
||
status: d.skuCallbackTime ? "done" : d.skuSubmitTime ? "warning" : "waiting",
|
||
desc: !d.skuCallbackTime && manualVo.value ? `已通过${manualVo.value.manualCancel ? "手动取消" : "手动结算"}人工处理,见下方快照节点` : "",
|
||
hint:
|
||
!d.skuCallbackTime && d.skuSubmitTime && !manualVo.value
|
||
? "第三方识别服务未响应(服务可能不可用),可使用「手动结算」人工核对视频后结算"
|
||
: "",
|
||
payloads: [{ label: "识别结果", json: d.skuCallbackVo }],
|
||
}),
|
||
withTag({
|
||
key: "pay",
|
||
title: "订单结算",
|
||
time: d.payTime,
|
||
status: d.payTime ? "done" : "waiting",
|
||
}),
|
||
]
|
||
if (d.error && d.errorDesc) {
|
||
nodes.push({
|
||
key: "error",
|
||
title: "流程错误",
|
||
time: d.updateTime,
|
||
status: "error",
|
||
desc: d.errorDesc,
|
||
tagText: "错误",
|
||
tagType: "danger",
|
||
})
|
||
}
|
||
return nodes
|
||
})
|
||
|
||
const openPayload = (title: string, json?: string) => {
|
||
payloadTitle.value = title
|
||
payloadRaw.value = json || ""
|
||
payloadData.value = null
|
||
if (json) {
|
||
try {
|
||
payloadData.value = typeof json === "string" ? JSON.parse(json) : json
|
||
} catch {
|
||
payloadData.value = null
|
||
}
|
||
}
|
||
payloadVisible.value = true
|
||
}
|
||
|
||
watch(
|
||
() => props.orderId,
|
||
() => getFlow(),
|
||
)
|
||
|
||
const getFlow = async () => {
|
||
if (!props.orderId) return
|
||
// 判断是否有"dzc_hardDoorFlow_view"权限
|
||
if (!auth("dzc_hardDoorFlow_view")) {
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
const _pa = { orderId: props.orderId }
|
||
const { data } = await hardFlowManager.getByOrderId(_pa)
|
||
detail.value = data
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const dialogClose = async () => {
|
||
formRef.value?.resetFields()
|
||
visibleErrorChangeDiaog.value = false
|
||
}
|
||
|
||
const onSubmit = async () => {
|
||
const valid = await formRef.value.validate().catch(() => {})
|
||
if (!valid) return false
|
||
try {
|
||
formLoading.value = true
|
||
await hardFlowManager.errorChange({ ...form, flowId: detail.value.id })
|
||
useMessage().success("处理成功")
|
||
dialogClose()
|
||
} catch (error: any) {
|
||
useMessage().error(error.msg)
|
||
} finally {
|
||
formLoading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
getFlow()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.door-log {
|
||
&__head {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
&__title {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
&__raw {
|
||
padding: 12px;
|
||
margin: 0;
|
||
overflow: auto;
|
||
font-size: 12px;
|
||
line-height: 20px;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
background-color: var(--el-fill-color-light);
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
.flow-node {
|
||
display: flex;
|
||
gap: 12px;
|
||
|
||
&:last-of-type .flow-node__rail::after {
|
||
display: none;
|
||
}
|
||
|
||
&__rail {
|
||
position: relative;
|
||
display: flex;
|
||
flex: 0 0 14px;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
padding-top: 16px;
|
||
|
||
&::after {
|
||
position: absolute;
|
||
top: 30px;
|
||
bottom: -16px;
|
||
left: 50%;
|
||
width: 2px;
|
||
content: "";
|
||
background-color: var(--el-border-color-lighter);
|
||
transform: translateX(-50%);
|
||
}
|
||
}
|
||
|
||
&__dot {
|
||
z-index: 1;
|
||
box-sizing: content-box;
|
||
width: 8px;
|
||
height: 8px;
|
||
background-color: var(--el-color-info);
|
||
border: 3px solid #fff;
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 0 2px currentColor;
|
||
color: var(--el-color-info);
|
||
}
|
||
|
||
&__card {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin-bottom: 12px;
|
||
padding: 10px 14px;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 6px;
|
||
transition: box-shadow 0.2s;
|
||
|
||
&:hover {
|
||
box-shadow: var(--el-box-shadow-light);
|
||
}
|
||
}
|
||
|
||
&__head {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
&__title {
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
&__time {
|
||
margin-left: auto;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
&__desc {
|
||
margin-top: 6px;
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
|
||
&__hint {
|
||
margin-top: 6px;
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
line-height: 20px;
|
||
color: var(--el-color-warning);
|
||
background-color: var(--el-color-warning-light-9);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
// 手动结算快照的购物车明细
|
||
&__cart {
|
||
margin-top: 6px;
|
||
padding: 8px 10px;
|
||
background-color: var(--el-fill-color-light);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
&__cart-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
line-height: 22px;
|
||
}
|
||
|
||
&__cart-name {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__cart-count {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
&__cart-price {
|
||
min-width: 64px;
|
||
color: var(--el-color-primary);
|
||
font-weight: 600;
|
||
text-align: right;
|
||
}
|
||
|
||
&__cart-total {
|
||
display: flex;
|
||
gap: 4px;
|
||
align-items: center;
|
||
margin-top: 4px;
|
||
padding-top: 6px;
|
||
font-size: 13px;
|
||
border-top: 1px dashed var(--el-border-color-lighter);
|
||
}
|
||
|
||
&__cart-channel {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
&__payloads,
|
||
&__actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
align-items: center;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
// 未发生节点弱化
|
||
&.is-idle .flow-node__card {
|
||
opacity: 0.65;
|
||
}
|
||
|
||
&.is-done .flow-node__dot {
|
||
color: var(--el-color-success);
|
||
background-color: var(--el-color-success);
|
||
}
|
||
|
||
&.is-warning .flow-node__dot {
|
||
color: var(--el-color-warning);
|
||
background-color: var(--el-color-warning);
|
||
}
|
||
|
||
&.is-error {
|
||
.flow-node__dot {
|
||
color: var(--el-color-danger);
|
||
background-color: var(--el-color-danger);
|
||
}
|
||
|
||
.flow-node__card {
|
||
border-color: var(--el-color-danger-light-7);
|
||
background-color: var(--el-color-danger-light-9);
|
||
}
|
||
}
|
||
|
||
&.is-manual .flow-node__dot {
|
||
color: var(--el-color-warning);
|
||
background-color: var(--el-color-warning);
|
||
}
|
||
}
|
||
</style>
|