feat:咖啡机
This commit is contained in:
1 parent
4dac32d89c
commit
11e46b06b3
7 files changed
+751
-12
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
/**
|
||||
* 咖啡机(屋里咖啡)订单与生命周期日志。
|
||||
* 订单金额单位为分,展示时由页面统一换算。
|
||||
*/
|
||||
export function fetchOrderPage(query?: Object) {
|
||||
return request({
|
||||
url: "/hard/hard-coffee-manager/order/page",
|
||||
method: "get",
|
||||
params: query,
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchOrderDetail(id: string) {
|
||||
return request({
|
||||
url: "/hard/hard-coffee-manager/order/" + id,
|
||||
method: "get",
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchLogPage(query?: Object) {
|
||||
return request({
|
||||
url: "/hard/hard-coffee-manager/log/page",
|
||||
method: "get",
|
||||
params: query,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 按本地订单id(hard_order.id)查询生命周期日志,
|
||||
* 供订单售后详情页内嵌时间线使用。
|
||||
*/
|
||||
export function fetchLogListByOrderId(orderId: string) {
|
||||
return request({
|
||||
url: "/hard/hard-coffee-manager/log/list-by-order/" + orderId,
|
||||
method: "get",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<el-timeline v-if="logs.length" class="coffee-timeline">
|
||||
<el-timeline-item v-for="log in logs" :key="log.id" :timestamp="log.startTime || ''"
|
||||
timestamp-placement="top" :type="stageMeta(log.stage).timelineType"
|
||||
:hollow="log.direction === '2'">
|
||||
<div class="coffee-timeline__card" :class="{ 'has-error': Boolean(log.error) }">
|
||||
<div class="coffee-timeline__headline">
|
||||
<el-tag :type="stageMeta(log.stage).tagType" effect="light" size="small">
|
||||
{{ stageMeta(log.stage).label }}
|
||||
</el-tag>
|
||||
<el-tag effect="plain" size="small" :type="log.direction === '2' ? 'warning' : 'info'">
|
||||
{{ log.direction === '2' ? "厂商调我方" : "我方调厂商" }}
|
||||
</el-tag>
|
||||
<span class="coffee-timeline__title">{{ log.title }}</span>
|
||||
<span v-if="log.cost != null" class="coffee-timeline__cost">{{ log.cost }}ms</span>
|
||||
</div>
|
||||
<dl v-if="log.uniqueCode || log.orderNo" class="coffee-timeline__facts">
|
||||
<div v-if="log.orderNo">
|
||||
<dt>厂商订单号</dt>
|
||||
<dd class="break-all">{{ log.orderNo }}</dd>
|
||||
</div>
|
||||
<div v-if="log.uniqueCode">
|
||||
<dt>机器号</dt>
|
||||
<dd>{{ log.uniqueCode }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p v-if="log.error" class="coffee-timeline__error break-all">{{ log.error }}</p>
|
||||
<div v-if="log.req || log.res" class="coffee-timeline__payload">
|
||||
<div v-if="log.req">
|
||||
<span>请求报文</span>
|
||||
<preview-json :json="log.req" />
|
||||
</div>
|
||||
<div v-if="log.res">
|
||||
<span>响应报文</span>
|
||||
<preview-json :json="log.res" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else-if="!loading" :image-size="54" description="该订单暂无咖啡机交互日志" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="hardCoffeeLog">
|
||||
import * as hardCoffeeApi from "/@/api/hard/hard-coffee"
|
||||
import { auth } from "/@/utils/authFunction"
|
||||
|
||||
type Props = {
|
||||
/** 本地硬件订单id(hard_order.id) */
|
||||
orderId?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), { orderId: "" })
|
||||
|
||||
/** 生命周期日志查看权限。 */
|
||||
const LOG_VIEW_PERMISSION = "hard_hardCoffeeLog_view"
|
||||
|
||||
interface CoffeeLogItem {
|
||||
id?: string | number
|
||||
orderNo?: string
|
||||
uniqueCode?: string
|
||||
stage?: string
|
||||
direction?: string
|
||||
title?: string
|
||||
req?: string
|
||||
res?: string
|
||||
error?: string
|
||||
startTime?: string
|
||||
cost?: number
|
||||
}
|
||||
|
||||
/** 阶段展示元数据,编码与后端 HardCoffeeLogStageEnum 保持一致。 */
|
||||
const STAGE_META: Record<string, { label: string; tagType: "primary" | "success" | "warning" | "danger" | "info"; timelineType: "primary" | "success" | "warning" | "danger" }> = {
|
||||
GET_ORDER: { label: "扫码拉取厂商订单", tagType: "info", timelineType: "primary" },
|
||||
CREATE_ORDER: { label: "创建本地订单", tagType: "primary", timelineType: "primary" },
|
||||
WALLET_PAY: { label: "饭卡扣款", tagType: "success", timelineType: "success" },
|
||||
NOTIFY_PAY: { label: "通知厂商支付结果", tagType: "warning", timelineType: "warning" },
|
||||
QUERY: { label: "厂商查单", tagType: "primary", timelineType: "primary" },
|
||||
REFUND_NOTIFY: { label: "通知厂商退款", tagType: "danger", timelineType: "danger" },
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const logs = ref<CoffeeLogItem[]>([])
|
||||
|
||||
/** 未识别的阶段编码按原始编码兜底展示,避免字典缺项时整块空白。 */
|
||||
function stageMeta(stage?: string) {
|
||||
return (
|
||||
STAGE_META[String(stage || "")] || {
|
||||
label: stage || "未知阶段",
|
||||
tagType: "info" as const,
|
||||
timelineType: "primary" as const,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
if (!props.orderId || !auth(LOG_VIEW_PERMISSION)) return
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await hardCoffeeApi.fetchLogListByOrderId(props.orderId)
|
||||
logs.value = data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.orderId,
|
||||
() => {
|
||||
loadLogs()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
loadLogs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.coffee-timeline {
|
||||
margin-top: 8px;
|
||||
padding-left: 4px;
|
||||
|
||||
.coffee-timeline__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-extra-light);
|
||||
|
||||
&.has-error {
|
||||
border-color: var(--el-color-danger-light-7);
|
||||
background-color: var(--el-color-danger-light-9);
|
||||
}
|
||||
}
|
||||
|
||||
.coffee-timeline__headline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.coffee-timeline__title {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.coffee-timeline__cost {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.coffee-timeline__facts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 24px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.coffee-timeline__error {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.coffee-timeline__payload {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 24px;
|
||||
font-size: 12px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -795,6 +795,16 @@
|
||||
<section v-if="isSmartCabinetOrder" id="section-cabinet" class="detail-section detail-anchor">
|
||||
<hard-door-log v-auth="'dzc_hardDoorFlow_view'" :order-id="String(globalData.detail.id)" />
|
||||
</section>
|
||||
|
||||
<section v-if="isCoffeeOrder" id="section-coffee" class="detail-section detail-anchor">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>咖啡机生命周期</h2>
|
||||
<p>该订单与屋里咖啡的扫码拉单、饭卡扣款、支付通知和退款同步交互记录</p>
|
||||
</div>
|
||||
</div>
|
||||
<hard-coffee-log v-auth="'hard_hardCoffeeLog_view'" :order-id="String(globalData.detail.id)" />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -818,7 +828,7 @@ import * as hardInvoiceApi from "/@/api/hard/hard-invoice"
|
||||
import * as mapAddressApi from "/@/api/hard/hard-order-address"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
import { getAutoRefundTaskStatus } from "/@/constants/autoRefund"
|
||||
import { Box, Calendar, Check, CreditCard, Document, List, Location, Money, Picture, Printer, ShoppingCart, Timer } from "@element-plus/icons-vue"
|
||||
import { Box, Calendar, Check, Coffee, CreditCard, Document, List, Location, Money, Picture, Printer, ShoppingCart, Timer } from "@element-plus/icons-vue"
|
||||
import type { Component } from "vue"
|
||||
import dayjs from "dayjs"
|
||||
|
||||
@@ -826,6 +836,8 @@ import dayjs from "dayjs"
|
||||
const RefundRejectForm = defineAsyncComponent(() => import("./refund-reject.vue"))
|
||||
/** 智能柜订单流程日志,仅设备类型为生鲜柜时加载。 */
|
||||
const HardDoorLog = defineAsyncComponent(() => import("../components/hard-door-log.vue"))
|
||||
/** 咖啡机生命周期日志,仅设备类型为咖啡机时加载。 */
|
||||
const HardCoffeeLog = defineAsyncComponent(() => import("../components/hard-coffee-log.vue"))
|
||||
/** 当前订单的全部售后记录与日志入口。 */
|
||||
const SaleTable = defineAsyncComponent(() => import("../components/sale-table.vue"))
|
||||
/** 商城订单发货弹窗,沿用现有物流单号提交接口。 */
|
||||
@@ -833,8 +845,10 @@ const ShipmentsForm = defineAsyncComponent(() => import("./shipments-form.vue"))
|
||||
|
||||
/** 售后状态 10 表示当前申请仍等待人工审核,属于既有前后端协议编码。 */
|
||||
const AFTER_SALE_APPLY_STATUS = "10"
|
||||
/** 设备类型 4 表示生鲜智能柜,只有该类型存在开门与识别流程日志。 */
|
||||
const SMART_CABINET_HARD_TYPE = "4"
|
||||
/** 设备类型 4 表示生鲜智能柜,只有该类型存在开门与识别流程日志。 */
|
||||
const SMART_CABINET_HARD_TYPE = "4"
|
||||
/** 设备类型 13 表示屋里咖啡咖啡机,订单存在厂商交互生命周期日志。 */
|
||||
const COFFEE_HARD_TYPE = "13"
|
||||
/** 消费机支付订单类型编码,后端允许该类型没有购物车明细。 */
|
||||
const SIMPLE_PAYMENT_ORDER_TYPE = "9"
|
||||
/** 商城订单类型编码,使用发货、收货履约状态。 */
|
||||
@@ -1256,6 +1270,10 @@ const isSimplePaymentOrder = computed(() => orderTypeCode.value === SIMPLE_PAYME
|
||||
const isSmartCabinetOrder = computed(
|
||||
() => String(globalData.detail.hardType || globalData.detail.sysType || "") === SMART_CABINET_HARD_TYPE,
|
||||
)
|
||||
/** 咖啡机订单展示屋里咖啡交互生命周期时间线。 */
|
||||
const isCoffeeOrder = computed(
|
||||
() => String(globalData.detail.hardType || globalData.detail.sysType || "") === COFFEE_HARD_TYPE,
|
||||
)
|
||||
|
||||
/** 当前订单在管理端的业务流程摘要。 */
|
||||
const orderScenarioLabel = computed(() => getOrderScenarioLabel(orderTypeCode.value))
|
||||
@@ -1486,6 +1504,9 @@ const sectionNavItems = computed<SectionNavItem[]>(() => {
|
||||
if (isSmartCabinetOrder.value) {
|
||||
items.push({ key: "cabinet", label: "智能柜日志", targetId: "section-cabinet", icon: Box })
|
||||
}
|
||||
if (isCoffeeOrder.value) {
|
||||
items.push({ key: "coffee", label: "咖啡机生命周期", targetId: "section-coffee", icon: Coffee })
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row>
|
||||
<SearchPanel v-show="showSearch" ref="queryRef" :model="state.queryForm"
|
||||
:loading="Boolean(state.loading)" :search-text="$t('common.queryBtn')" :reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList(false)" @reset="resetQuery">
|
||||
<el-form-item style="width: 300px" label="厂商订单号" prop="orderNo">
|
||||
<el-input v-model="state.queryForm.orderNo" placeholder="19位数字" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="机器号" prop="uniqueCode">
|
||||
<el-input v-model="state.queryForm.uniqueCode" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="发生时间" prop="startTimeList">
|
||||
<el-date-picker v-model="state.queryForm.startTimeList" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="datetimerange" />
|
||||
</el-form-item>
|
||||
<template #more>
|
||||
<el-form-item style="width: 300px" label="生命周期阶段" prop="stage">
|
||||
<el-select v-model="state.queryForm.stage" placeholder="全部" clearable>
|
||||
<el-option v-for="(meta, code) in STAGE_OPTIONS" :key="code" :label="meta.label"
|
||||
:value="code" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="交互方向" prop="direction">
|
||||
<el-select v-model="state.queryForm.direction" placeholder="全部" clearable>
|
||||
<el-option label="我方调厂商" value="1" />
|
||||
<el-option label="厂商调我方" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="本地订单ID" prop="orderId">
|
||||
<el-input v-model="state.queryForm.orderId" clearable />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="false" class="ml10 mr20"
|
||||
style="float: right" @queryTable="getDataList(false)"></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle">
|
||||
<el-table-column type="index" label="#" width="50" align="center" />
|
||||
<el-table-column prop="startTime" label="发生时间" width="170" show-overflow-tooltip />
|
||||
<el-table-column label="阶段" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="stageMeta(row.stage).tagType" size="small" effect="light">
|
||||
{{ stageMeta(row.stage).label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="方向" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.direction === '2' ? 'warning' : 'info'" size="small" effect="plain">
|
||||
{{ row.direction === "2" ? "厂商调我方" : "我方调厂商" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="说明" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="orderNo" label="厂商订单号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="uniqueCode" label="机器号" width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="orderId" label="本地订单ID" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="cost" label="耗时" width="90">
|
||||
<template #default="{ row }">{{ row.cost != null ? row.cost + "ms" : "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="error" label="错误" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-[var(--el-color-danger)]': row.error }">{{ row.error || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="req" label="请求报文" width="100">
|
||||
<template #default="{ row }">
|
||||
<preview-json v-if="row.req" :json="row.req" />
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="res" label="响应报文" width="100">
|
||||
<template #default="{ row }">
|
||||
<preview-json v-if="row.res" :json="row.res" />
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="hardCoffeeLog">
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import * as hardCoffeeApi from "/@/api/hard/hard-coffee"
|
||||
|
||||
/** 阶段筛选与展示选项,编码与后端 HardCoffeeLogStageEnum 保持一致。 */
|
||||
const STAGE_OPTIONS: Record<string, { label: string; tagType: "primary" | "success" | "warning" | "danger" | "info" }> = {
|
||||
GET_ORDER: { label: "扫码拉取厂商订单", tagType: "info" },
|
||||
CREATE_ORDER: { label: "创建本地订单", tagType: "primary" },
|
||||
WALLET_PAY: { label: "饭卡扣款", tagType: "success" },
|
||||
NOTIFY_PAY: { label: "通知厂商支付结果", tagType: "warning" },
|
||||
QUERY: { label: "厂商查单", tagType: "primary" },
|
||||
REFUND_NOTIFY: { label: "通知厂商退款", tagType: "danger" },
|
||||
}
|
||||
|
||||
/** 未识别的阶段编码按原始编码兜底展示。 */
|
||||
function stageMeta(stage?: string) {
|
||||
return STAGE_OPTIONS[String(stage || "")] || { label: stage || "未知阶段", tagType: "info" as const }
|
||||
}
|
||||
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
pageList: hardCoffeeApi.fetchLogPage,
|
||||
})
|
||||
|
||||
// table hook
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state)
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields()
|
||||
getDataList()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row>
|
||||
<SearchPanel v-show="showSearch" ref="queryRef" :model="state.queryForm"
|
||||
:loading="Boolean(state.loading)" :search-text="$t('common.queryBtn')" :reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList(false)" @reset="resetQuery">
|
||||
<el-form-item style="width: 300px" label="厂商订单号" prop="orderNo">
|
||||
<el-input v-model="state.queryForm.orderNo" placeholder="19位数字" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="机器号" prop="machineName">
|
||||
<el-input v-model="state.queryForm.machineName" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="创建时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="datetimerange" />
|
||||
</el-form-item>
|
||||
<template #more>
|
||||
<el-form-item style="width: 300px" label="支付人手机号" prop="phoneNo">
|
||||
<el-input v-model="state.queryForm.phoneNo" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="通知厂商状态" prop="wuliNotifyStatus">
|
||||
<el-select v-model="state.queryForm.wuliNotifyStatus" placeholder="全部" clearable>
|
||||
<el-option label="未通知" value="0" />
|
||||
<el-option label="通知成功" value="1" />
|
||||
<el-option label="通知失败" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="false" class="ml10 mr20"
|
||||
style="float: right" @queryTable="getDataList(false)"></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle">
|
||||
<el-table-column type="index" label="#" width="50" align="center" />
|
||||
<el-table-column prop="createTime" label="下单时间" width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="orderNo" label="厂商订单号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="machineName" label="机器号" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="product" label="商品" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="金额(元)" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatPrice(row.price) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="userName" label="支付人" width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.userName || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phoneNo" label="手机号" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.phoneNo || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="hard_order_status" :value="row.orderStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通知厂商" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.wuliNotifyStatus === '1'" type="success" size="small" effect="light">成功</el-tag>
|
||||
<el-tag v-else-if="row.wuliNotifyStatus === '2'" type="danger" size="small" effect="light">
|
||||
失败({{ row.wuliNotifyCount || 0 }}次)
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="small" effect="plain">未通知</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款同步(元)" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.refundSyncAmount > 0">{{ formatPrice(row.refundSyncAmount) }} /
|
||||
{{ formatPrice(row.refundPrice) }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-auth="'hard_hardCoffeeOrder_view'" icon="View" text type="primary" style="margin: 0"
|
||||
@click="openDetail(row)">
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="detailVisible" title="咖啡机订单详情" size="620px" destroy-on-close>
|
||||
<div v-loading="detailLoading">
|
||||
<template v-if="coffeeDetail.order">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="厂商订单号" :span="2">{{ coffeeDetail.order.orderNo || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="本地订单ID">{{ coffeeDetail.order.orderId || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="机器号">{{ coffeeDetail.order.machineName || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品">
|
||||
{{ coffeeDetail.order.product || "-" }}
|
||||
<el-tag v-if="coffeeDetail.order.isCold === '1'" size="small" type="info" effect="plain"
|
||||
class="ml-1">冷饮</el-tag>
|
||||
<el-tag v-else-if="coffeeDetail.order.isCold === '0'" size="small" type="danger"
|
||||
effect="plain" class="ml-1">热饮</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="金额">{{ formatPrice(coffeeDetail.order.price) }} 元</el-descriptions-item>
|
||||
<el-descriptions-item label="支付人">{{ coffeeDetail.order.userName || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ coffeeDetail.order.phoneNo || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="下单时间">{{ coffeeDetail.order.createTime || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付时间">{{ coffeeDetail.order.payTime || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单状态">
|
||||
<dict-tag :options="hard_order_status" :value="coffeeDetail.order.orderStatus" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="退款">
|
||||
{{ formatPrice(coffeeDetail.order.refundPrice || 0) }} 元(已同步厂商
|
||||
{{ formatPrice(coffeeDetail.order.refundSyncAmount || 0) }} 元)
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-image v-if="coffeeDetail.order.img" :src="coffeeDetail.order.img" :preview-src-list="[coffeeDetail.order.img]"
|
||||
:z-index="3000" fit="contain"
|
||||
class="mt-3 h-[160px] w-full rounded border border-[var(--el-border-color-lighter)]" />
|
||||
<h3 class="mb-1 mt-6 text-sm font-semibold">生命周期时间线</h3>
|
||||
<hard-coffee-log v-auth="'hard_hardCoffeeLog_view'" :order-id="String(coffeeDetail.order.orderId)" />
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="hardCoffeeOrder">
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import * as hardCoffeeApi from "/@/api/hard/hard-coffee"
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
|
||||
/** 咖啡机生命周期时间线组件,详情抽屉内复用。 */
|
||||
const HardCoffeeLog = defineAsyncComponent(() => import("../components/hard-coffee-log.vue"))
|
||||
|
||||
// 定义查询字典
|
||||
const { hard_order_status } = useDict("hard_order_status")
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
// 支持从设备详情页携带设备id进入过滤
|
||||
queryForm: route.query.hardId ? { hardId: String(route.query.hardId) } : {},
|
||||
pageList: hardCoffeeApi.fetchOrderPage,
|
||||
})
|
||||
|
||||
// table hook
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state)
|
||||
|
||||
// 详情抽屉
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const coffeeDetail = reactive<{ order?: Record<string, any> }>({})
|
||||
|
||||
/** 金额单位为分,换算为两位小数元展示。 */
|
||||
function formatPrice(price: unknown): string {
|
||||
if (price === null || price === undefined || price === "") return "-"
|
||||
const fen = Number(price)
|
||||
if (Number.isNaN(fen)) return String(price)
|
||||
return (fen / 100).toFixed(2)
|
||||
}
|
||||
|
||||
async function openDetail(row: any) {
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
coffeeDetail.order = undefined
|
||||
try {
|
||||
const { data } = await hardCoffeeApi.fetchOrderDetail(String(row.id))
|
||||
coffeeDetail.order = data?.order || {}
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 保留从设备详情带入的设备过滤
|
||||
const hardId = state.queryForm.hardId
|
||||
queryRef.value?.resetFields()
|
||||
state.queryForm = hardId ? { hardId } : {}
|
||||
getDataList()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-alert type="info" show-icon :closable="false"
|
||||
title="唯一编号必须与屋里咖啡机器号(machine_name)一致,否则扫码支付会提示设备未接入。" />
|
||||
<el-descriptions class="mt-4" :column="2" border>
|
||||
<el-descriptions-item label="厂商机器号">
|
||||
<span class="break-all">{{ displayValue(uniqueCode, "尚未绑定") }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付方式">饭卡扫码支付(卡号即扣)</el-descriptions-item>
|
||||
<el-descriptions-item label="支付窗口">下单后 5 分钟</el-descriptions-item>
|
||||
<el-descriptions-item label="所属厂商">屋里咖啡</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template v-if="canViewOrders">
|
||||
<div class="mb-2 mt-6 flex items-center justify-between">
|
||||
<h3 class="m-0 text-sm font-semibold">最近交易</h3>
|
||||
<el-button size="small" icon="DataLine" @click="goOrderList">查看全部订单</el-button>
|
||||
</div>
|
||||
<el-table v-loading="orderLoading" :data="orderList" border size="small">
|
||||
<el-table-column prop="createTime" label="下单时间" width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="orderNo" label="厂商订单号" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="product" label="商品" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="金额(元)" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatPrice(row.price) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="userName" label="支付人" width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ displayValue(row.userName, "-") }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="hard_order_status" :value="row.orderStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通知厂商" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.wuliNotifyStatus === '1'" type="success" size="small" effect="light">成功</el-tag>
|
||||
<el-tag v-else-if="row.wuliNotifyStatus === '2'" type="danger" size="small" effect="light">
|
||||
失败({{ row.wuliNotifyCount || 0 }}次)
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="small" effect="plain">未通知</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty :image-size="54" description="该设备暂无咖啡机订单" />
|
||||
</template>
|
||||
</el-table>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="coffeeMachineSection">
|
||||
import * as hardCoffeeApi from "/@/api/hard/hard-coffee"
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { auth } from "/@/utils/authFunction"
|
||||
|
||||
type Props = {
|
||||
/** 咖啡机设备主键(hard_manager.id)。 */
|
||||
deviceId?: string
|
||||
/** 设备唯一编号,即屋里咖啡机器号。 */
|
||||
uniqueCode?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), { deviceId: "", uniqueCode: "" })
|
||||
|
||||
/** 咖啡机订单查看权限。 */
|
||||
const COFFEE_ORDER_VIEW_PERMISSION = "hard_hardCoffeeOrder_view"
|
||||
/** 咖啡机最近交易展示条数。 */
|
||||
const COFFEE_ORDER_PREVIEW_SIZE = 5
|
||||
|
||||
// 订单状态字典
|
||||
const { hard_order_status } = useDict("hard_order_status")
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
/** 是否有咖啡机订单查看权限。 */
|
||||
const canViewOrders = computed(() => auth(COFFEE_ORDER_VIEW_PERMISSION))
|
||||
/** 咖啡机最近交易列表。 */
|
||||
const orderList = ref<Record<string, any>[]>([])
|
||||
/** 咖啡机最近交易加载状态。 */
|
||||
const orderLoading = ref(false)
|
||||
|
||||
/** 将空值统一格式化为明确占位文案。 */
|
||||
function displayValue(value: unknown, emptyText = "--"): string {
|
||||
return value === null || value === undefined || value === "" ? emptyText : String(value)
|
||||
}
|
||||
|
||||
/** 咖啡机订单金额单位为分,展示时换算为两位小数的元。 */
|
||||
function formatPrice(price: unknown): string {
|
||||
if (price === null || price === undefined || price === "") return "-"
|
||||
const fen = Number(price)
|
||||
if (Number.isNaN(fen)) return String(price)
|
||||
return (fen / 100).toFixed(2)
|
||||
}
|
||||
|
||||
/** 加载当前咖啡机设备的最近交易,失败不阻断页面其余分区。 */
|
||||
async function loadOrders(): Promise<void> {
|
||||
if (!props.deviceId || !canViewOrders.value) return
|
||||
orderLoading.value = true
|
||||
orderList.value = []
|
||||
try {
|
||||
const { data } = await hardCoffeeApi.fetchOrderPage({
|
||||
current: 1,
|
||||
size: COFFEE_ORDER_PREVIEW_SIZE,
|
||||
hardId: props.deviceId,
|
||||
})
|
||||
orderList.value = data?.records || []
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "咖啡机订单加载失败")
|
||||
} finally {
|
||||
orderLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转咖啡机订单列表并按当前设备过滤。 */
|
||||
function goOrderList() {
|
||||
router.push({ path: "/hard/coffee-order", query: { hardId: props.deviceId } })
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
() => {
|
||||
loadOrders()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
loadOrders()
|
||||
})
|
||||
</script>
|
||||
@@ -13,6 +13,7 @@
|
||||
effect="light">
|
||||
{{ detail.online ? "在线" : "离线" }}
|
||||
</el-tag>
|
||||
<el-tag v-else-if="isCoffeeMachine" type="info" effect="plain">厂商云管理</el-tag>
|
||||
<el-tag v-else-if="isPrinterAsset" type="info" effect="plain">无屏 USB 外设</el-tag>
|
||||
<el-tag v-else type="info" effect="plain">状态暂不支持</el-tag>
|
||||
</div>
|
||||
@@ -37,20 +38,24 @@
|
||||
|
||||
<div class="status-strip" :class="{ 'has-terminal-logs': supportsTerminalLogs }">
|
||||
<div class="status-item">
|
||||
<span>{{ isPrinterAsset ? "运行方式" : "连接状态" }}</span>
|
||||
<span>{{ isPrinterAsset ? "运行方式" : isCoffeeMachine ? "设备运营" : "连接状态" }}</span>
|
||||
<strong
|
||||
:class="{ 'is-success': detail.online, 'is-danger': isRuntimeSupported && !detail.online }">
|
||||
{{ runtimeStatusText }}
|
||||
</strong>
|
||||
<small>{{ isPrinterAsset ? printerProxyTargetText : detail.lastOnlineTime || "暂无心跳时间" }}</small>
|
||||
<small>{{
|
||||
isPrinterAsset ? printerProxyTargetText : isCoffeeMachine ? "屋里咖啡平台维护在线状态" : detail.lastOnlineTime || "暂无心跳时间"
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>{{ isPrinterAsset ? "客户端归属" : "客户端版本" }}</span>
|
||||
<strong>{{ isPrinterAsset ? "业务终端" : apkVersionText }}</strong>
|
||||
<small>{{
|
||||
<span>{{ isPrinterAsset || isCoffeeMachine ? "客户端归属" : "客户端版本" }}</span>
|
||||
<strong v-if="isCoffeeMachine">屋里咖啡</strong>
|
||||
<strong v-else>{{ isPrinterAsset ? "业务终端" : apkVersionText }}</strong>
|
||||
<small v-if="isCoffeeMachine">固件与出品由厂商负责</small>
|
||||
<small v-else>{{
|
||||
isPrinterAsset ? "不单独安装 APK" : detail.apkVersionCode ? "Build " + detail.apkVersionCode :
|
||||
"暂无内部版本码"
|
||||
}}</small>
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>配置完成度</span>
|
||||
@@ -157,6 +162,9 @@
|
||||
</div>
|
||||
<el-alert v-if="isPrinterAsset" type="info" show-icon :closable="false"
|
||||
title="打印机不单独上报在线状态、心跳或 APK 版本" :description="printerProxyDescription" />
|
||||
<el-alert v-else-if="isCoffeeMachine" type="info" show-icon :closable="false"
|
||||
title="咖啡机由屋里咖啡平台托管,本平台不采集心跳与 APK 版本。"
|
||||
description="用户在机器上选品后扫码打开我方支付页完成饭卡扣款,支付结果由我方回调厂商出餐;厂商也会主动查单核对支付状态。" />
|
||||
<el-alert v-else-if="!isRuntimeSupported" type="info" show-icon :closable="false"
|
||||
title="当前设备类型暂未接入终端心跳和 APK 版本上报。" />
|
||||
<div v-else class="runtime-overview">
|
||||
@@ -618,6 +626,16 @@
|
||||
</el-empty>
|
||||
</section>
|
||||
|
||||
<section v-if="isCoffeeMachine" id="section-coffee" class="detail-section detail-anchor">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>咖啡机接入</h2>
|
||||
<p>屋里咖啡扫码支付接入信息与本机最近交易</p>
|
||||
</div>
|
||||
</div>
|
||||
<coffee-machine-section :device-id="String(detail.id || '')" :unique-code="detail.uniqueCode || ''" />
|
||||
</section>
|
||||
|
||||
<section id="section-maintenance" class="detail-section detail-anchor">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
@@ -669,6 +687,7 @@
|
||||
import {
|
||||
Box,
|
||||
CircleCheck,
|
||||
Coffee,
|
||||
Connection,
|
||||
DataLine,
|
||||
Document,
|
||||
@@ -874,7 +893,7 @@ const runtimeSupportedTypes = ["1", "2"]
|
||||
/** 超市和食堂业务终端,目前也是唯一会上传文件日志的设备类型。 */
|
||||
const businessTerminalTypes = ["1", "2"]
|
||||
/** 不适用 MQTT 和人脸配置的云设备类型编码。 */
|
||||
const terminalConfigurationExcludedTypes = ["9", "10", "11", "12"]
|
||||
const terminalConfigurationExcludedTypes = ["9", "10", "11", "12", "13"]
|
||||
/** 终端日志查看权限。 */
|
||||
const LOG_VIEW_PERMISSION = "hard_hardTerminalLogFile_view"
|
||||
/** 终端日志预览权限。 */
|
||||
@@ -896,6 +915,8 @@ const PrinterDialog = defineAsyncComponent(() => import("./printer-dialog.vue"))
|
||||
const InfoSetting = defineAsyncComponent(() => import("../hard-door-manager/info-setting.vue"))
|
||||
/** 智能柜音量设置弹窗。 */
|
||||
const VolumeSetting = defineAsyncComponent(() => import("../hard-door-manager/volume-setting.vue"))
|
||||
/** 咖啡机接入信息与最近交易分区内容。 */
|
||||
const CoffeeMachineSection = defineAsyncComponent(() => import("./coffee-machine-section.vue"))
|
||||
|
||||
/** 页面内部滚动容器。 */
|
||||
const detailContainerRef = ref<HTMLElement>()
|
||||
@@ -968,6 +989,8 @@ const supportsFace = computed(() => !terminalConfigurationExcludedTypes.includes
|
||||
const isBusinessTerminal = computed(() => businessTerminalTypes.includes(String(detail.type)))
|
||||
/** 当前设备是否为无屏 EY_PRINT 打印机资产。 */
|
||||
const isPrinterAsset = computed(() => String(detail.type) === "12")
|
||||
/** 当前设备是否为屋里咖啡咖啡机。 */
|
||||
const isCoffeeMachine = computed(() => String(detail.type) === "13")
|
||||
/** 当前设备是否提供打印绑定或参数配置。 */
|
||||
const supportsPrinter = computed(() => isBusinessTerminal.value || isPrinterAsset.value)
|
||||
/** 当前设备是否为生鲜智能柜。 */
|
||||
@@ -996,7 +1019,7 @@ const printerConfigured = computed(() =>
|
||||
)
|
||||
/** 当前设备类型适用的可配置能力数量。 */
|
||||
const availableCapabilityCount = computed(
|
||||
() => Number(supportsMq.value) + Number(supportsFace.value) + Number(supportsPrinter.value) + Number(isSmartCabinet.value),
|
||||
() => Number(supportsMq.value) + Number(supportsFace.value) + Number(supportsPrinter.value) + Number(isSmartCabinet.value) + Number(isCoffeeMachine.value),
|
||||
)
|
||||
/** 当前设备已完成关键项的能力数量。 */
|
||||
const configuredCapabilityCount = computed(
|
||||
@@ -1004,7 +1027,8 @@ const configuredCapabilityCount = computed(
|
||||
Number(supportsMq.value && mqConfigured.value) +
|
||||
Number(supportsFace.value && faceConfigured.value) +
|
||||
Number(supportsPrinter.value && printerConfigured.value) +
|
||||
Number(isSmartCabinet.value && Boolean(hardDoorDetail.value?.id)),
|
||||
Number(isSmartCabinet.value && Boolean(hardDoorDetail.value?.id)) +
|
||||
Number(isCoffeeMachine.value && Boolean(detail.uniqueCode)),
|
||||
)
|
||||
/** 当前设备日志文件总数。 */
|
||||
const logFileCount = computed(() => Number(logSummary.value?.fileCount || 0))
|
||||
@@ -1016,6 +1040,7 @@ const apkVersionText = computed(() => {
|
||||
/** 当前运行状态文案。 */
|
||||
const runtimeStatusText = computed(() => {
|
||||
if (isPrinterAsset.value) return "由业务终端代理"
|
||||
if (isCoffeeMachine.value) return "厂商云管理"
|
||||
if (!isRuntimeSupported.value) return "暂不支持"
|
||||
return detail.online ? "在线" : "离线"
|
||||
})
|
||||
@@ -1059,6 +1084,7 @@ const sectionNavItems = computed<SectionNavItem[]>(() => {
|
||||
if (supportsPrinter.value) items.push({ key: "printer", label: "打印配置", targetId: "section-printer", icon: Printer })
|
||||
if (showLogSection.value) items.push({ key: "logs", label: "终端日志", targetId: "section-logs", icon: Document })
|
||||
if (isSmartCabinet.value) items.push({ key: "cabinet", label: "智能柜", targetId: "section-cabinet", icon: Box })
|
||||
if (isCoffeeMachine.value) items.push({ key: "coffee", label: "咖啡机接入", targetId: "section-coffee", icon: Coffee })
|
||||
items.push({ key: "maintenance", label: "维护信息", targetId: "section-maintenance", icon: Setting })
|
||||
return items
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user