feat:咖啡机

This commit is contained in:
dzc committed 2026-09-18 14:38:38 +08:00
1 parent 4dac32d89c
commit 11e46b06b3
7 files changed
+751 -12

No files matched your search

+39
View File
@@ -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
})
+131
View File
@@ -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>
+188
View File
@@ -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>
+35 -9
View File
@@ -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
})