Files
pj_admin_cloud/src/views/hard/components/hard-door-log.vue
T
2026-09-09 20:22:46 +08:00

602 lines
16 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>