Compare commits
10
Commits
f4dff79f5a
...
cce677e3ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cce677e3ff | ||
|
|
69a28cce40 | ||
|
|
cb2e95562a | ||
|
|
c8ff18d190 | ||
|
|
07dedf45f9 | ||
|
|
dbe543ae9a | ||
|
|
deefac848b | ||
|
|
75a36d36ad | ||
|
|
f16c1e6eb7 | ||
|
|
18aa169b69 |
No files matched your search
+2
-1
@@ -51,4 +51,5 @@ coverage/
|
||||
.v2c/
|
||||
.video_agent/
|
||||
.mimosa/
|
||||
.zcode/
|
||||
.zcode/
|
||||
.zcodeignore
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -0,0 +1,17 @@
|
||||
import request from '/@/utils/request'
|
||||
|
||||
/** Seata 提交探测 */
|
||||
export function seataProbeCommit() {
|
||||
return request({
|
||||
url: '/admin/seataProbe/commit',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
/** Seata 回滚探测 */
|
||||
export function seataProbeRollback() {
|
||||
return request({
|
||||
url: '/admin/seataProbe/rollback',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
Regular → Executable
File mode changed.
@@ -126,7 +126,7 @@ export interface HardDoorManualSettleItem {
|
||||
/**
|
||||
* 智能柜订单手动结算(第三方识别不可用时,人工核对视频后结算)
|
||||
*/
|
||||
export function manualSettle(data: { orderId: string | number; items: HardDoorManualSettleItem[] }) {
|
||||
export function manualSettle(data: { orderId: string | number; items: HardDoorManualSettleItem[]; reason?: string }) {
|
||||
return request({
|
||||
url: "/hard/hard-order/manual-settle",
|
||||
method: "post",
|
||||
@@ -137,7 +137,7 @@ export function manualSettle(data: { orderId: string | number; items: HardDoorMa
|
||||
/**
|
||||
* 智能柜订单手动取消(用户未拿商品)
|
||||
*/
|
||||
export function manualCancel(data: { orderId: string | number }) {
|
||||
export function manualCancel(data: { orderId: string | number; reason?: string }) {
|
||||
return request({
|
||||
url: "/hard/hard-order/manual-cancel",
|
||||
method: "post",
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
File mode changed.
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span>{{ label }}</span>
|
||||
<el-tooltip placement="top" effect="dark">
|
||||
<template #content>
|
||||
<div class="max-w-[360px] leading-5">{{ tip }}</div>
|
||||
</template>
|
||||
<el-icon class="cursor-help text-[var(--el-color-info)]" tabindex="0" :aria-label="`${label}计算说明`">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="statistics-table-header">
|
||||
import { InfoFilled } from "@element-plus/icons-vue"
|
||||
|
||||
/** 统计表头展示内容和计算口径提示。 */
|
||||
defineProps({
|
||||
/** 统计列展示名称。 */
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
/** 鼠标悬停或键盘聚焦信息图标时展示的计算口径。 */
|
||||
tip: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -19,6 +19,7 @@ import PreviewText from "/@/components/PreviewText/index.vue"
|
||||
import PreviewXml from "/@/components/PreviewXml/index.vue"
|
||||
import DraggableDepTree from "/@/components/DraggableDepTree/index.vue"
|
||||
import DraggableDepMchTree from "/@/components/DraggableDepMchTree/index.vue"
|
||||
import StatisticsTableHeader from "/@/components/StatisticsTableHeader/index.vue"
|
||||
import SearchPanel from "/@/components/SearchPanel/index.vue"
|
||||
import UserInfoCell from "/@/components/UserInfoCell/index.vue"
|
||||
import UserIdentityInfo from "/@/components/UserIdentityInfo/index.vue"
|
||||
@@ -90,6 +91,7 @@ export default {
|
||||
app.component("PreviewXml", PreviewXml)
|
||||
app.component("DraggableDepTree", DraggableDepTree)
|
||||
app.component("DraggableDepMchTree", DraggableDepMchTree)
|
||||
app.component("StatisticsTableHeader", StatisticsTableHeader)
|
||||
app.component("SearchPanel", SearchPanel)
|
||||
app.component("UserInfoCell", UserInfoCell)
|
||||
app.component("UserIdentityInfo", UserIdentityInfo)
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-alert
|
||||
title="用于验证 Seata Server(如升级到 2.1)后全局事务是否正常。请先在业务库执行 db/seata_tx_probe.sql。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb8"
|
||||
/>
|
||||
<div class="mb8">
|
||||
<el-button type="primary" :loading="commitLoading" @click="runCommit" v-auth="'admin_seata_probe'">
|
||||
测试提交(应落库)
|
||||
</el-button>
|
||||
<el-button type="warning" :loading="rollbackLoading" @click="runRollback" v-auth="'admin_seata_probe'">
|
||||
测试回滚(应不落库)
|
||||
</el-button>
|
||||
</div>
|
||||
<el-descriptions v-if="lastResult" title="最近一次结果" :column="1" border>
|
||||
<el-descriptions-item label="是否通过">
|
||||
<el-tag :type="lastResult.pass ? 'success' : 'danger'">{{ lastResult.pass ? '通过' : '未通过' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="说明">{{ lastResult.message }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">{{ lastResult.caseType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="mark">{{ lastResult.mark }}</el-descriptions-item>
|
||||
<el-descriptions-item label="xid">{{ lastResult.xid || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="库中行数">{{ lastResult.rowCount }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-else style="color: #909399">点击上方按钮开始测试</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="adminSeataProbe">
|
||||
import { ref } from 'vue'
|
||||
import { useMessage } from '/@/hooks/message'
|
||||
import { seataProbeCommit, seataProbeRollback } from '/@/api/admin/seataProbe'
|
||||
|
||||
const commitLoading = ref(false)
|
||||
const rollbackLoading = ref(false)
|
||||
const lastResult = ref<any>(null)
|
||||
|
||||
const pickBody = (res: any) => {
|
||||
if (res?.data && typeof res.data === 'object' && ('pass' in res.data || 'mark' in res.data)) {
|
||||
return res.data
|
||||
}
|
||||
return res?.data ?? res
|
||||
}
|
||||
|
||||
const runCommit = async () => {
|
||||
commitLoading.value = true
|
||||
try {
|
||||
const res: any = await seataProbeCommit()
|
||||
const body = pickBody(res)
|
||||
lastResult.value = body
|
||||
if (res?.code === 0 && body?.pass) {
|
||||
useMessage().success(body.message || '提交探测通过')
|
||||
} else {
|
||||
useMessage().error(res?.msg || body?.message || '提交探测失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
useMessage().error(e?.msg || e?.message || '请求失败')
|
||||
} finally {
|
||||
commitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const runRollback = async () => {
|
||||
rollbackLoading.value = true
|
||||
try {
|
||||
const res: any = await seataProbeRollback()
|
||||
const body = pickBody(res)
|
||||
lastResult.value = body
|
||||
if (res?.code === 0 && body?.pass) {
|
||||
useMessage().success(body.message || '回滚探测通过')
|
||||
} else {
|
||||
useMessage().error(res?.msg || body?.message || '回滚探测失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
useMessage().error(e?.msg || e?.message || '请求失败')
|
||||
} finally {
|
||||
rollbackLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2,51 +2,33 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch" class="ml10">
|
||||
<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"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<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" @reset="resetQuery">
|
||||
<el-form-item :label="$t('sysmenu.name')" prop="menuName">
|
||||
<el-input :placeholder="$t('sysmenu.inputNameTip')" clearable style="max-width: 180px" v-model="state.queryForm.menuName" />
|
||||
<el-input :placeholder="$t('sysmenu.inputNameTip')" clearable style="max-width: 180px"
|
||||
v-model="state.queryForm.menuName" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button @click="onOpenAddMenu" class="ml10" icon="folder-add" type="primary" v-auth="'sys_menu_add'">
|
||||
<el-button @click="onOpenAddMenu" class="ml10" icon="folder-add" type="primary"
|
||||
v-auth="'sys_menu_add'">
|
||||
{{ $t("common.addBtn") }}
|
||||
</el-button>
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
class="ml10"
|
||||
style="float: right; margin-right: 20px"
|
||||
@queryTable="query"
|
||||
></right-toolbar>
|
||||
<right-toolbar v-model:showSearch="showSearch" class="ml10" style="float: right; margin-right: 20px"
|
||||
@queryTable="query"></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table
|
||||
class="el-table"
|
||||
ref="tableRef"
|
||||
:data="tableList"
|
||||
row-key="id"
|
||||
style="width: 100%"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:header-cell-style="tableStyle?.headerCellStyle"
|
||||
:expand-row-keys="expandRowKeys"
|
||||
lazy
|
||||
:load="load"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
@expand-change="handleExpandChange"
|
||||
>
|
||||
<el-table-column :label="$t('sysmenu.name')" fixed prop="name" align="left" show-overflow-tooltip min-width="200" />
|
||||
<el-table-column :label="$t('sysmenu.sortOrder')" prop="sortOrder" align="center" show-overflow-tooltip />
|
||||
<el-table class="el-table" ref="tableRef" :data="tableList" row-key="id" style="width: 100%"
|
||||
v-loading="state.loading" border :header-cell-style="tableStyle?.headerCellStyle"
|
||||
:expand-row-keys="expandRowKeys" lazy :load="load"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }" @expand-change="handleExpandChange">
|
||||
<el-table-column :label="$t('sysmenu.name')" fixed prop="name" align="left" show-overflow-tooltip
|
||||
min-width="200" />
|
||||
<el-table-column :label="$t('sysmenu.sortOrder')" prop="sortOrder" align="center"
|
||||
show-overflow-tooltip />
|
||||
<el-table-column :label="$t('sysmenu.icon')" prop="icon" show-overflow-tooltip align="center">
|
||||
<template #default="scope">
|
||||
<SvgIcon :name="scope.row.meta.icon" />
|
||||
@@ -66,26 +48,23 @@
|
||||
<el-tag type="info" v-else>关闭</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('sysmenu.permission')" :show-overflow-tooltip="true" prop="permission" align="center" />
|
||||
<el-table-column :label="$t('sysmenu.permission')" :show-overflow-tooltip="true" prop="permission"
|
||||
align="center" />
|
||||
<el-table-column :label="$t('common.action')" show-overflow-tooltip width="250" align="center">
|
||||
<template #default="scope">
|
||||
<el-button icon="folder-add" @click="onOpenAddMenu('add', scope.row)" text type="primary" v-auth="'sys_menu_add'">
|
||||
<el-button icon="folder-add" @click="onOpenAddMenu('add', scope.row)" text type="primary"
|
||||
v-auth="'sys_menu_add'">
|
||||
{{ $t("common.addBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="edit-pen" @click="onOpenEditMenu('edit', scope.row)" text type="primary" v-auth="'sys_menu_edit'"
|
||||
>{{ $t("common.editBtn") }}
|
||||
<el-button icon="edit-pen" @click="onOpenEditMenu('edit', scope.row)" text type="primary"
|
||||
v-auth="'sys_menu_edit'">{{ $t("common.editBtn") }}
|
||||
</el-button>
|
||||
|
||||
<el-tooltip icon="delete" :content="$t('sysmenu.deleteDisabledTip')" :disabled="!deleteMenuDisabled(scope.row)" placement="top">
|
||||
<el-tooltip icon="delete" :content="$t('sysmenu.deleteDisabledTip')"
|
||||
:disabled="!deleteMenuDisabled(scope.row)" placement="top">
|
||||
<span style="margin-left: 12px">
|
||||
<el-button
|
||||
icon="delete"
|
||||
:disabled="deleteMenuDisabled(scope.row)"
|
||||
@click="handleDelete(scope.row)"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'sys_menu_del'"
|
||||
>
|
||||
<el-button icon="delete" :disabled="deleteMenuDisabled(scope.row)"
|
||||
@click="handleDelete(scope.row)" text type="primary" v-auth="'sys_menu_del'">
|
||||
{{ $t("common.delBtn") }}
|
||||
</el-button>
|
||||
</span>
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="state.queryForm.phone" placeholder="请输入手机号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 180px" label="人脸质量" prop="qualityCategory">
|
||||
<el-select v-model="state.queryForm.qualityCategory" placeholder="全部" clearable>
|
||||
<el-option
|
||||
v-for="option in FACE_QUALITY_FILTER_OPTIONS"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
@@ -83,6 +93,27 @@
|
||||
<preview-image :src="row.faceImg" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="人脸质量" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="getFaceQualityTooltip(row)" placement="top" :show-after="250">
|
||||
<span
|
||||
tabindex="0"
|
||||
role="status"
|
||||
:aria-label="getFaceQualityTooltip(row)"
|
||||
class="inline-flex cursor-help rounded-sm focus:outline-none focus:ring-2 focus:ring-primary-light-5"
|
||||
>
|
||||
<el-tag
|
||||
:type="getFaceQualityTagType(row)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="min-w-[112px] justify-center whitespace-nowrap tabular-nums"
|
||||
>
|
||||
{{ getFaceQualityLabel(row) }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核状态" prop="processStatus" show-overflow-tooltip min-width="100">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="check_status" :value="row.processStatus" />
|
||||
@@ -141,6 +172,104 @@ const processDrawerRef = ref()
|
||||
const batchDealRef = ref()
|
||||
const loading = ref()
|
||||
|
||||
/** PC 当前阶段用于判定低质量人脸的固定阈值,与终端默认在库阈值保持一致。 */
|
||||
const LIBRARY_FACE_QUALITY_THRESHOLD = 0.6
|
||||
|
||||
/** 人脸质量查询分类,与后端分页接口的 qualityCategory 协议值保持一致。 */
|
||||
const FACE_QUALITY_FILTER_OPTIONS = [
|
||||
{ label: "合格", value: "qualified" },
|
||||
{ label: "低质量", value: "low" },
|
||||
{ label: "检测失败", value: "failed" },
|
||||
{ label: "未检测", value: "pending" },
|
||||
] as const
|
||||
|
||||
/** Element Plus 小号状态标签允许使用的语义类型。 */
|
||||
type FaceQualityTagType = "success" | "warning" | "danger" | "info"
|
||||
|
||||
/** 审批列表中与服务端人脸质量字段有关的最小行结构。 */
|
||||
interface UserFaceQualityRow {
|
||||
/** 虹软综合质量分,数据库 decimal 可能由不同网关序列化为数字或文本。 */
|
||||
qualityScore?: number | string | null
|
||||
/** 服务端质量状态;历史未检测记录为空。 */
|
||||
qualityStatus?: string | null
|
||||
/** 终端完成质量检测的时间。 */
|
||||
qualityCheckedAt?: string | null
|
||||
/** 虹软 SDK 或终端插件错误码。 */
|
||||
qualityErrorCode?: number | string | null
|
||||
/** 最后一次成功写入质量结果的终端设备主键。 */
|
||||
qualityDeviceId?: number | string | null
|
||||
}
|
||||
|
||||
/** 人脸质量单元格的紧凑展示模型。 */
|
||||
interface FaceQualityDisplay {
|
||||
/** 标签内可直接理解的状态与分数文案。 */
|
||||
label: string
|
||||
/** 与状态对应的 Element Plus 语义颜色。 */
|
||||
tagType: FaceQualityTagType
|
||||
/** 包含阈值、时间、来源和排查动作的完整辅助说明。 */
|
||||
tooltip: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 将服务端人脸质量字段归一化为不依赖颜色的表格状态。
|
||||
*
|
||||
* @param row 审批列表人脸记录
|
||||
* @returns 标签文案、语义颜色和完整诊断说明
|
||||
*/
|
||||
const resolveFaceQuality = (row: UserFaceQualityRow): FaceQualityDisplay => {
|
||||
const checkedAt = row.qualityCheckedAt || "未知"
|
||||
const deviceId = row.qualityDeviceId ?? "未知"
|
||||
if (row.qualityStatus === "detected") {
|
||||
const score = Number(row.qualityScore)
|
||||
if (row.qualityScore == null || row.qualityScore === "" || !Number.isFinite(score) || score < 0 || score > 1) {
|
||||
return {
|
||||
label: "数据异常",
|
||||
tagType: "danger",
|
||||
tooltip: `服务端质量状态为已检测,但质量分无效。检测时间:${checkedAt};来源设备:${deviceId}`,
|
||||
}
|
||||
}
|
||||
const scoreText = score.toFixed(2)
|
||||
if (score < LIBRARY_FACE_QUALITY_THRESHOLD) {
|
||||
return {
|
||||
label: `低质量 ${scoreText}`,
|
||||
tagType: "warning",
|
||||
tooltip: `质量分 ${scoreText},低于当前阈值 ${LIBRARY_FACE_QUALITY_THRESHOLD.toFixed(
|
||||
2,
|
||||
)}。请排查照片清晰度、光照、人脸角度及人脸区域大小。检测时间:${checkedAt};来源设备:${deviceId}`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: `合格 ${scoreText}`,
|
||||
tagType: "success",
|
||||
tooltip: `质量分 ${scoreText},达到当前阈值 ${LIBRARY_FACE_QUALITY_THRESHOLD.toFixed(
|
||||
2,
|
||||
)}。检测时间:${checkedAt};来源设备:${deviceId}`,
|
||||
}
|
||||
}
|
||||
if (row.qualityStatus === "failed") {
|
||||
const errorCode = row.qualityErrorCode == null ? "未知" : row.qualityErrorCode
|
||||
return {
|
||||
label: "检测失败",
|
||||
tagType: "danger",
|
||||
tooltip: `质量检测失败,错误码:${errorCode}。请确认终端虹软质量授权和本地图片有效性。检测时间:${checkedAt};来源设备:${deviceId}`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: "未检测",
|
||||
tagType: "info",
|
||||
tooltip: "尚未收到终端质量检测结果,可在终端人脸管理中执行存量检测。",
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回人脸质量标签文案。 */
|
||||
const getFaceQualityLabel = (row: UserFaceQualityRow) => resolveFaceQuality(row).label
|
||||
|
||||
/** 返回人脸质量标签语义类型。 */
|
||||
const getFaceQualityTagType = (row: UserFaceQualityRow) => resolveFaceQuality(row).tagType
|
||||
|
||||
/** 返回人脸质量 Tooltip 与无障碍说明。 */
|
||||
const getFaceQualityTooltip = (row: UserFaceQualityRow) => resolveFaceQuality(row).tooltip
|
||||
|
||||
// 多选rows
|
||||
const selectObjs = ref([]) as any
|
||||
const selectIds = ref([]) as any
|
||||
@@ -153,6 +282,7 @@ const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
deptId: "",
|
||||
username: "",
|
||||
phone: "",
|
||||
qualityCategory: "",
|
||||
isDeepDept: false,
|
||||
processStatus: "2",
|
||||
},
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -1,114 +1,115 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<el-descriptions v-if="detail" title="智能柜日志">
|
||||
<el-descriptions-item labelClassName="no-colon">
|
||||
<el-timeline>
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">用户授权</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.createTime }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flow_coll">
|
||||
<span>授权用户:</span>
|
||||
<span>{{ detail.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.requestOpenTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">智能柜申请开门</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.requestOpenTime }}</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>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.openCallbackTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">智能柜开门响应</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.openCallbackTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.videoCallbackTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">智能柜视频回调</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.videoCallbackTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.skuSubmitTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">商品提交识别</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.skuSubmitTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.skuCallbackTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">商品识别响应</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.skuCallbackTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.payTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">订单结算</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.payTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.error && detail.errorDesc">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<el-tag type="danger" style="width: 120px">错误</el-tag>
|
||||
<span class="color-danger ml-2">{{ detail.errorDesc }}</span>
|
||||
<el-button v-if="!detail?.hardAuth?.manualChangeUserId" class="ml-2" link
|
||||
@click="visibleErrorChangeDiaog = true">
|
||||
<!-- 错误节点:人工处理入口 -->
|
||||
<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>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail?.hardAuth?.manualChangeUserId">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<el-tag type="warning" style="width: 120px">手动处理智能柜订单</el-tag>
|
||||
<span class="color-warning ml-2">{{ detail.hardAuth.manualChangeTime }}</span>
|
||||
<span class="color-warning ml-2">{{ detail.hardAuth.manualChangeRemark }}</span>
|
||||
<div class="ml-2" v-if="detail.hardAuth.manualChangePic">
|
||||
<preview-image width="40px" height="40px" :src="detail.hardAuth.manualChangePic" />
|
||||
</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>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="flow-node__payloads">
|
||||
<el-button link type="primary" size="small" @click="openPayload((manualVo.manualCancel ? '手动取消订单' : '手动结算') + ' - 快照报文', manualRaw)">
|
||||
快照报文
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<!-- 手动处理智能柜订单记录 -->
|
||||
<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">
|
||||
@@ -117,8 +118,7 @@
|
||||
</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-input v-model="form.remark" placeholder="请输入处理原因" type="textarea" :autosize="{ minRows: 6 }" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -133,9 +133,12 @@
|
||||
</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, auths, authAll } from '/@/utils/authFunction';
|
||||
import { auth } from "/@/utils/authFunction"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
|
||||
type Props = {
|
||||
orderId?: string
|
||||
@@ -150,6 +153,12 @@ 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: "",
|
||||
@@ -162,11 +171,181 @@ 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()
|
||||
},
|
||||
() => getFlow(),
|
||||
)
|
||||
|
||||
const getFlow = async () => {
|
||||
@@ -193,7 +372,7 @@ const dialogClose = async () => {
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
const valid = await formRef.value.validate().catch(() => { })
|
||||
const valid = await formRef.value.validate().catch(() => {})
|
||||
if (!valid) return false
|
||||
try {
|
||||
formLoading.value = true
|
||||
@@ -213,77 +392,210 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flow_coll {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
.door-log {
|
||||
&__head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
span:nth-child(1) {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
text-align: left;
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
min-width: 50px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
|
||||
// 时间线样式调整
|
||||
:deep(.el-timeline) {
|
||||
margin: 10px 0 0 160px;
|
||||
.flow-node {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.el-timeline-item__wrapper {
|
||||
&:last-of-type .flow-node__rail::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__rail {
|
||||
position: relative;
|
||||
top: -20px;
|
||||
|
||||
.el-timeline-item__timestamp {
|
||||
position: absolute !important;
|
||||
top: 10px;
|
||||
left: -150px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-timeline-right-content {
|
||||
display: flex;
|
||||
flex: 0 0 14px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-height: 30px;
|
||||
padding: 10px;
|
||||
background-color: #b0d8ff;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-top: 16px;
|
||||
|
||||
&::before {
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 13px;
|
||||
border-color: transparent #b0d8ff transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 8px;
|
||||
top: 30px;
|
||||
bottom: -16px;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
content: "";
|
||||
background-color: var(--el-border-color-lighter);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.el-timeline-item__node {
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: -50%;
|
||||
transform: translateY(100%);
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
background: #b0d8ff;
|
||||
&__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%;
|
||||
justify-content: center;
|
||||
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>
|
||||
@@ -1,97 +1,92 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="manual-settlement">
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="第三方商品识别服务不可用,请核对开门视频,人工选择用户拿走的商品后手动结算;若用户未拿商品请取消订单。"
|
||||
/>
|
||||
<el-alert type="warning" :closable="false" show-icon
|
||||
title="第三方商品识别服务不可用,请核对开门视频,人工选择用户拿走的商品后手动结算;若用户未拿商品请取消订单。" />
|
||||
|
||||
<div class="settle-layout">
|
||||
<!-- 左侧:按柜层选择商品 -->
|
||||
<!-- 左侧:柜内商品选择 -->
|
||||
<div class="settle-goods">
|
||||
<div class="settle-block__title">柜内商品({{ cabinetNoLabel }})</div>
|
||||
<template v-if="floors.length">
|
||||
<div v-for="floor in floors" :key="floor.floorNo" class="settle-floor">
|
||||
<div class="settle-floor__title">第 {{ floor.floorNo }} 层</div>
|
||||
<div class="settle-floor__list">
|
||||
<div v-for="product in floor.products" :key="product.productId" class="settle-product">
|
||||
<preview-image width="64px" height="56px" :src="product.pic" />
|
||||
<div class="settle-head">
|
||||
<span class="settle-head__title">柜内商品</span>
|
||||
<el-tag v-if="cabinetNoLabel" size="small" type="info" effect="plain">{{ cabinetNoLabel }}</el-tag>
|
||||
</div>
|
||||
<div class="settle-goods__scroll">
|
||||
<template v-if="floors.length">
|
||||
<div v-for="floor in floors" :key="floor.floorNo" class="settle-floor">
|
||||
<div class="settle-floor__tag">第 {{ floor.floorNo }} 层</div>
|
||||
<div v-for="product in floor.products" :key="product.productId" class="settle-product"
|
||||
:class="{ 'is-selected': Number(cartCount[product.productId] || 0) > 0 }">
|
||||
<preview-image width="56px" height="56px" :src="product.pic" />
|
||||
<div class="settle-product__info">
|
||||
<el-tooltip effect="dark" :content="product.name">
|
||||
<div class="settle-product__name">{{ product.name }}</div>
|
||||
</el-tooltip>
|
||||
<div class="settle-product__meta">
|
||||
<span class="color-primary">{{ displayPrice(product.price) }}</span>
|
||||
<span class="settle-product__price">{{ displayPrice(product.price) }}</span>
|
||||
<span class="settle-product__stock">库存 {{ product.stock }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-input-number
|
||||
v-model="cartCount[product.productId]"
|
||||
:min="0"
|
||||
:max="Math.max(product.stock, 1)"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
/>
|
||||
<el-input-number v-model="cartCount[product.productId]" class="settle-product__stepper"
|
||||
:min="0" controls-position="right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else-if="!loading" description="未找到该柜的商品配置" :image-size="60" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧:已选购物车 + 视频 + 操作 -->
|
||||
<div class="settle-panel">
|
||||
<div class="settle-cart">
|
||||
<div class="settle-block__title">已选商品</div>
|
||||
<template v-if="cartItems.length">
|
||||
<div class="settle-cart__list">
|
||||
<div v-for="item in cartItems" :key="item.productId" class="settle-cart__item">
|
||||
<span class="settle-cart__name">{{ item.name }}</span>
|
||||
<span class="settle-cart__count">× {{ item.count }}</span>
|
||||
<span class="color-primary">{{ displayPrice(item.price * item.count) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settle-cart__total">
|
||||
合计:<span class="color-primary">{{ displayPrice(cartTotalPrice) }}</span>
|
||||
<span class="settle-cart__tip">(金额仅供参考,以实际结算结果为准)</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else description="请从左侧选择用户拿走的商品" :image-size="60" />
|
||||
<el-empty v-else-if="!loading" description="未找到该柜的商品配置" :image-size="60" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settle-video">
|
||||
<div class="settle-video__head">
|
||||
<div class="settle-block__title">开门视频</div>
|
||||
<el-radio-group v-if="videos.length > 1" v-model="currentVideoIndex" size="small">
|
||||
<el-radio-button v-for="(video, index) in videos" :key="index" :label="index">
|
||||
{{ video.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<!-- 右侧:开门视频 + 已选购物车 + 操作 -->
|
||||
<div class="settle-side">
|
||||
<div class="settle-card">
|
||||
<div class="settle-head">
|
||||
<span class="settle-head__title">开门视频</span>
|
||||
<span class="settle-head__extra">共 {{ videos.length }} 路,同步播放</span>
|
||||
</div>
|
||||
<div v-if="videos.length" class="settle-video__grid" :class="{ 'is-single': videos.length === 1 }">
|
||||
<div v-for="(video, index) in videos" :key="index" class="settle-video__item">
|
||||
<el-tag size="small" effect="plain" :type="index === 0 ? 'primary' : 'info'"
|
||||
class="settle-video__label">
|
||||
{{ video.label }}
|
||||
</el-tag>
|
||||
<video-player :src="video.url" width="100%" height="320px" />
|
||||
</div>
|
||||
</div>
|
||||
<video-player v-if="currentVideoUrl" :src="currentVideoUrl" width="100%" height="240px" />
|
||||
<el-empty v-else description="无开门视频(视频未回调或回调失败)" :image-size="60" />
|
||||
</div>
|
||||
|
||||
<div class="settle-actions">
|
||||
<el-button
|
||||
v-auth="'hard_hardwareOrder_manualSettle'"
|
||||
type="primary"
|
||||
:disabled="!cartItems.length"
|
||||
:loading="submitting"
|
||||
@click="onSettle"
|
||||
>
|
||||
手动结算
|
||||
</el-button>
|
||||
<el-button
|
||||
v-auth="'hard_hardwareOrder_manualSettle'"
|
||||
type="danger"
|
||||
plain
|
||||
:loading="cancelling"
|
||||
@click="onCancel"
|
||||
>
|
||||
用户未拿商品,取消订单
|
||||
</el-button>
|
||||
<div class="settle-card">
|
||||
<div class="settle-head">
|
||||
<span class="settle-head__title">已选商品</span>
|
||||
<span v-if="cartItems.length" class="settle-head__extra">共 {{ cartCountTotal }} 件</span>
|
||||
</div>
|
||||
<div v-if="cartItems.length" class="settle-cart">
|
||||
<div class="settle-cart__head">
|
||||
<span>商品</span>
|
||||
<span>数量</span>
|
||||
<span>小计</span>
|
||||
</div>
|
||||
<div v-for="item in cartItems" :key="item.productId" class="settle-cart__row">
|
||||
<span class="settle-cart__name">{{ item.name }}</span>
|
||||
<span class="settle-cart__count">× {{ item.count }}</span>
|
||||
<span class="settle-cart__price">{{ displayPrice(item.price * item.count) }}</span>
|
||||
</div>
|
||||
<div class="settle-cart__total">
|
||||
<span>合计</span>
|
||||
<span class="settle-cart__total-price">{{ displayPrice(cartTotalPrice) }}</span>
|
||||
</div>
|
||||
<div class="settle-cart__tip">金额仅供参考,以实际结算结果为准</div>
|
||||
</div>
|
||||
<el-empty v-else description="请从左侧选择用户拿走的商品" :image-size="60" />
|
||||
<div class="settle-actions">
|
||||
<el-button v-auth="'hard_hardwareOrder_manualSettle'" type="danger" plain :loading="cancelling"
|
||||
@click="onCancel">
|
||||
未拿商品,取消订单
|
||||
</el-button>
|
||||
<el-button v-auth="'hard_hardwareOrder_manualSettle'" type="primary"
|
||||
:disabled="!cartItems.length" :loading="submitting" @click="onSettle">
|
||||
手动结算
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,9 +134,6 @@ const cabinetNoLabel = ref("")
|
||||
/** 商品 id -> 手动选择的数量。 */
|
||||
const cartCount = reactive<Record<string, number>>({})
|
||||
const videos = ref<{ label: string; url: string }[]>([])
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
const currentVideoUrl = computed(() => videos.value[currentVideoIndex.value]?.url || "")
|
||||
|
||||
const cartItems = computed(() => {
|
||||
const items: { productId: string; name: string; price: number; count: number }[] = []
|
||||
@@ -154,20 +146,15 @@ const cartItems = computed(() => {
|
||||
return items
|
||||
})
|
||||
|
||||
const cartTotalPrice = computed(() => cartItems.value.reduce((sum, item) => sum + item.price * item.count, 0))
|
||||
const cartCountTotal = computed(() => cartItems.value.reduce((sum, item) => sum + item.count, 0))
|
||||
|
||||
watch(
|
||||
() => [props.orderId, props.hardId],
|
||||
() => load(),
|
||||
{ immediate: true },
|
||||
)
|
||||
const cartTotalPrice = computed(() => cartItems.value.reduce((sum, item) => sum + item.price * item.count, 0))
|
||||
|
||||
const load = async () => {
|
||||
if (!props.orderId || !props.hardId) return
|
||||
loading.value = true
|
||||
floors.value = []
|
||||
videos.value = []
|
||||
currentVideoIndex.value = 0
|
||||
cabinetNoLabel.value = ""
|
||||
Object.keys(cartCount).forEach((key) => delete cartCount[key])
|
||||
try {
|
||||
@@ -185,6 +172,12 @@ const load = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.orderId, props.hardId],
|
||||
() => load(),
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
/** 把 /hard-spu/get-by-cabinet-no 的层结构转成层数组,同层同商品多槽位合并、库存求和。 */
|
||||
const parseFloors = (cabinetData: any): Floor[] => {
|
||||
if (!Array.isArray(cabinetData)) return []
|
||||
@@ -244,12 +237,17 @@ const onSettle = async () => {
|
||||
h("div", null, `合计金额:${displayPrice(cartTotalPrice.value)}(实际金额以结算结果为准)`),
|
||||
h("div", { style: "color:var(--el-color-danger)" }, "确认后将从用户钱包强制扣款或通过支付宝免密扣款,请谨慎操作。"),
|
||||
])
|
||||
let reason = ""
|
||||
try {
|
||||
await ElMessageBox.confirm(summary, "手动结算确认", {
|
||||
const { value } = await ElMessageBox.prompt(summary, "手动结算确认", {
|
||||
confirmButtonText: "确认结算",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
inputPlaceholder: "操作备注(选填):记录判断依据,如 视频02:35拿走一瓶,便于事后复核",
|
||||
inputPattern: /^[\s\S]{0,200}$/,
|
||||
inputErrorMessage: "备注最多 200 字",
|
||||
})
|
||||
reason = (value || "").trim()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
@@ -258,6 +256,7 @@ const onSettle = async () => {
|
||||
await manualSettle({
|
||||
orderId: props.orderId,
|
||||
items: items.map((item) => ({ productId: item.productId, productCount: item.count })),
|
||||
reason,
|
||||
})
|
||||
useMessage().success("手动结算成功")
|
||||
emit("refresh")
|
||||
@@ -270,18 +269,23 @@ const onSettle = async () => {
|
||||
|
||||
const onCancel = async () => {
|
||||
if (cancelling.value) return
|
||||
let reason = ""
|
||||
try {
|
||||
await ElMessageBox.confirm("确认该订单用户未拿任何商品?取消后订单状态置为已取消,不可恢复。", "取消订单确认", {
|
||||
const { value } = await ElMessageBox.prompt("确认该订单用户未拿任何商品?取消后订单状态置为已取消,不可恢复。", "取消订单确认", {
|
||||
confirmButtonText: "确认取消订单",
|
||||
cancelButtonText: "再想想",
|
||||
type: "warning",
|
||||
inputPlaceholder: "操作备注(选填):记录判断依据,便于事后复核",
|
||||
inputPattern: /^[\s\S]{0,200}$/,
|
||||
inputErrorMessage: "备注最多 200 字",
|
||||
})
|
||||
reason = (value || "").trim()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
cancelling.value = true
|
||||
try {
|
||||
await manualCancel({ orderId: props.orderId })
|
||||
await manualCancel({ orderId: props.orderId, reason })
|
||||
useMessage().success("订单已取消")
|
||||
emit("refresh")
|
||||
} catch (e: any) {
|
||||
@@ -296,132 +300,219 @@ const onCancel = async () => {
|
||||
.manual-settlement {
|
||||
.settle-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
margin-top: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.settle-block__title {
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
.settle-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&__extra {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
// 左侧商品列:适度固定宽,超出内部滚动,避免把视频挤小
|
||||
.settle-goods {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex: 0 0 420px;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
|
||||
&__scroll {
|
||||
flex: 1;
|
||||
max-height: 560px;
|
||||
padding-right: 4px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.settle-floor {
|
||||
&:not(:first-child) {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed var(--el-border-color-lighter);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
&__tag {
|
||||
padding-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.settle-product {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
|
||||
&+.settle-product {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
max-width: 220px;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__price {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__stock {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
// 数量步进器固定紧凑宽度,不随弹性布局拉伸
|
||||
&__stepper {
|
||||
flex-shrink: 0;
|
||||
width: 112px;
|
||||
}
|
||||
}
|
||||
|
||||
.settle-panel {
|
||||
// 右侧视频 + 购物车占满剩余宽度
|
||||
.settle-side {
|
||||
display: flex;
|
||||
flex: 0 0 360px;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settle-card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
// 多路视频同屏并排播放;单路时占满整行
|
||||
.settle-video__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settle-cart {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
.settle-video__item {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
.settle-video__label {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.settle-cart {
|
||||
|
||||
&__head,
|
||||
&__row,
|
||||
&__total {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 72px 96px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__head {
|
||||
padding-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
span:nth-child(2),
|
||||
span:nth-child(3) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&__row {
|
||||
padding: 6px 0;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px dashed var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__count {
|
||||
&__count,
|
||||
&__price {
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__price {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__total {
|
||||
padding-top: 8px;
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
font-weight: 600;
|
||||
|
||||
span:first-child {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
&__total-price {
|
||||
grid-column: 3;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__tip {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.settle-video {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,6 +520,7 @@ const onCancel = async () => {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,8 @@
|
||||
<div class="mb-2 flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-[var(--el-text-color-secondary)]">订单售后详情</span>
|
||||
<dict-tag :options="hard_order_type" :value="globalData.detail.orderType" />
|
||||
<dict-tag :options="hard_order_status" :value="globalData.detail.orderStatus" />
|
||||
<el-tag v-if="isOfflinePaidCompleted" type="success" :disable-transitions="true">已完成</el-tag>
|
||||
<dict-tag v-else :options="hard_order_status" :value="globalData.detail.orderStatus" />
|
||||
<dict-tag :options="hard_pay_type" :value="globalData.detail.payType" />
|
||||
</div>
|
||||
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
@@ -779,7 +780,7 @@
|
||||
</section>
|
||||
|
||||
<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)" />
|
||||
<hard-door-log v-auth="'dzc_hardDoorFlow_view'" :key="cabinetLogKey" :order-id="String(globalData.detail.id)" />
|
||||
</section>
|
||||
|
||||
<section v-if="showManualSettlementSection" id="section-manual-settlement" class="detail-section detail-anchor">
|
||||
@@ -839,6 +840,8 @@ const HARD_DOOR_FREE_PAY_TYPE = "9"
|
||||
const DOOR_ORDER_PENDING_SETTLE_STATUS = "1"
|
||||
/** 消费机支付订单类型编码,后端允许该类型没有购物车明细。 */
|
||||
const SIMPLE_PAYMENT_ORDER_TYPE = "9"
|
||||
/** 线下购买订单类型编码,终端支付完成即履约完成,没有后续核销或配送流程。 */
|
||||
const OFFLINE_ORDER_TYPE = "1"
|
||||
/** 商城订单类型编码,使用发货、收货履约状态。 */
|
||||
const MARKETPLACE_ORDER_TYPE = "7"
|
||||
/** 外卖订单类型编码,使用接单、配送履约状态。 */
|
||||
@@ -1230,6 +1233,10 @@ const { getDataList, tableStyle } = useTable(state)
|
||||
const orderTypeCode = computed(() => String(globalData.detail.orderType || ""))
|
||||
/** 当前订单状态协议编码。 */
|
||||
const orderStatusCode = computed(() => String(globalData.detail.orderStatus || ""))
|
||||
/** 线下订单支付即完成:该类型没有核销流程,状态停在已支付(5/6)即代表订单已完成。 */
|
||||
const isOfflinePaidCompleted = computed(
|
||||
() => orderTypeCode.value === OFFLINE_ORDER_TYPE && ["5", "6"].includes(orderStatusCode.value),
|
||||
)
|
||||
/** 是否为商城订单。 */
|
||||
const isMarketplaceOrder = computed(() => orderTypeCode.value === MARKETPLACE_ORDER_TYPE)
|
||||
/** 是否为外卖订单。 */
|
||||
@@ -1251,6 +1258,13 @@ const showManualSettlementSection = computed(
|
||||
isSmartCabinetOrder.value &&
|
||||
orderStatusCode.value === DOOR_ORDER_PENDING_SETTLE_STATUS,
|
||||
)
|
||||
/** 智能柜日志组件重建键:订单结算/取消(payTime/unfollowTime/updateTime 变化)后强制重新拉取流程,展示手动结算快照。 */
|
||||
const cabinetLogKey = computed(
|
||||
() =>
|
||||
`${globalData.detail.id}-${globalData.detail.payTime || ""}-${globalData.detail.unfollowTime || ""}-${
|
||||
globalData.detail.updateTime || ""
|
||||
}`,
|
||||
)
|
||||
|
||||
/** 当前订单在管理端的业务流程摘要。 */
|
||||
const orderScenarioLabel = computed(() => getOrderScenarioLabel(orderTypeCode.value))
|
||||
@@ -1742,13 +1756,15 @@ const lifecycleStages = computed<LifecycleStage[]>(() => {
|
||||
}
|
||||
|
||||
if (type !== SIMPLE_PAYMENT_ORDER_TYPE) {
|
||||
// 线下订单支付即完成,完成时间回退到支付时间。
|
||||
const offlineCompleted = isOfflinePaidCompleted.value
|
||||
stages.push(
|
||||
createLifecycleStage(
|
||||
"completed",
|
||||
"订单完成",
|
||||
detail.verificationTime || detail.deliveredTime,
|
||||
Boolean(detail.verificationTime || detail.deliveredTime) || status === "20",
|
||||
["5", "6"].includes(status),
|
||||
detail.verificationTime || detail.deliveredTime || (offlineCompleted ? detail.payTime : undefined),
|
||||
Boolean(detail.verificationTime || detail.deliveredTime) || status === "20" || offlineCompleted,
|
||||
!offlineCompleted && ["5", "6"].includes(status),
|
||||
"等待履约完成",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -42,47 +42,71 @@
|
||||
<el-table-column type="index" label="#" width="80" />
|
||||
<el-table-column prop="deptName" min-width="150" label="部门" />
|
||||
<el-table-column prop="walletPrice" label="批量充值金额" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="批量充值金额" :tip="batchRechargeAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.walletPrice != 0 }">{{ displayPrice(row.walletPrice)
|
||||
}}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="walletCount" label="批量充值次数" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="批量充值次数" :tip="batchRechargeCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.walletCount != 0 }">{{ row.walletCount }}次</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="walletOncePrice" label="单次充值金额" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="单次充值金额" :tip="singleRechargeAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.walletOncePrice != 0 }">{{
|
||||
displayPrice(row.walletOncePrice) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="walletOnceCount" label="单次充值次数" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="单次充值次数" :tip="singleRechargeCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.walletOnceCount != 0 }">{{ row.walletOnceCount }}次
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="walletReducePrice" label="批量扣减金额" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="批量扣减金额" :tip="batchDeductionAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.walletReducePrice != 0 }">{{
|
||||
displayPrice(row.walletReducePrice) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="walletReduceCount" label="批量扣减次数" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="批量扣减次数" :tip="batchDeductionCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.walletReduceCount != 0 }">{{ row.walletReduceCount
|
||||
}}次</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="walletOnceReducePrice" label="单次扣减金额" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="单次扣减金额" :tip="singleDeductionAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.walletOnceReducePrice != 0 }">{{
|
||||
displayPrice(row.walletOnceReducePrice) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="walletOnceReduceCount" label="单次扣减次数" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="单次扣减次数" :tip="singleDeductionCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.walletOnceReduceCount != 0 }">{{
|
||||
row.walletOnceReduceCount }}次</div>
|
||||
@@ -91,6 +115,9 @@
|
||||
<!-- start 批量退换食堂 start -->
|
||||
<el-table-column prop="walletReturnCanteenPrice" label="批量退还各自食堂金额" align="center"
|
||||
min-width="170">
|
||||
<template #header>
|
||||
<statistics-table-header label="批量退还各自食堂金额" :tip="returnCanteenAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.walletReturnCanteenPrice != 0 }">{{
|
||||
displayPrice(row.walletReturnCanteenPrice) }}</div>
|
||||
@@ -98,6 +125,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="walletReturnCanteenCount" label="批量退还各自食堂次数" align="center"
|
||||
min-width="170">
|
||||
<template #header>
|
||||
<statistics-table-header label="批量退还各自食堂次数" :tip="returnCanteenCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.walletReturnCanteenCount != 0 }">{{
|
||||
row.walletReturnCanteenCount }}次</div>
|
||||
@@ -105,39 +135,57 @@
|
||||
</el-table-column>
|
||||
<!-- end 批量退换食堂 end -->
|
||||
|
||||
<el-table-column prop="orderPayPrice" label="流水金额" align="center" min-width="90">
|
||||
<el-table-column prop="orderPayPrice" label="流水金额" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="流水金额" :tip="payAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-primary': row.orderPayPrice != 0 }">{{
|
||||
displayPrice(row.orderPayPrice) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderPayCount" label="流水次数" align="center" min-width="90">
|
||||
<el-table-column prop="orderPayCount" label="流水次数" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="流水次数" :tip="payCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-primary': row.orderPayCount != 0 }">{{ row.orderPayCount }}次</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderRefundPrice" label="退款金额" align="center" min-width="90">
|
||||
<el-table-column prop="orderRefundPrice" label="退款金额" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="退款金额" :tip="refundAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-danger': row.orderRefundPrice != 0 }">
|
||||
{{ displayPrice(row.orderRefundPrice) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderRefundCont" label="退款次数" align="center" min-width="90">
|
||||
<el-table-column prop="orderRefundCont" label="退款次数" align="center" min-width="110">
|
||||
<template #header>
|
||||
<statistics-table-header label="退款次数" :tip="refundCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-danger': row.orderRefundCont != 0 }">
|
||||
{{ row.orderRefundCont }}次
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderProfitPrice" label="消费净金额" align="center" min-width="120">
|
||||
<el-table-column prop="orderProfitPrice" label="消费净金额" align="center" min-width="140">
|
||||
<template #header>
|
||||
<statistics-table-header label="消费净金额" :tip="netAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.orderProfitPrice != 0 }">
|
||||
{{ displayPrice(row.orderProfitPrice) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderProfitCont" label="消费净次数" align="center" min-width="120">
|
||||
<el-table-column prop="orderProfitCont" label="消费净次数" align="center" min-width="140">
|
||||
<template #header>
|
||||
<statistics-table-header label="消费净次数" :tip="netCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div :class="{ 'color-success': row.orderProfitCont != 0 }">
|
||||
{{ row.orderProfitCont }}次
|
||||
@@ -167,6 +215,41 @@ const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
const draggableDepTreeRef = ref() // 树组件ref引用
|
||||
|
||||
/** 部门列说明每行数据的统计范围。 */
|
||||
const departmentHeaderTip = "每行显示一个部门,后面的金额和次数都是该部门在查询时间内的数据。"
|
||||
/** 批量充值金额说明批量入账金额的计算方式。 */
|
||||
const batchRechargeAmountHeaderTip = "通过批量充值给该部门人员增加的金额合计。"
|
||||
/** 批量充值次数说明按人员入账记录计次。 */
|
||||
const batchRechargeCountHeaderTip = "批量充值产生的入账记录数。一次批量操作涉及多人时,每个人的入账各算 1 次。"
|
||||
/** 单次充值金额说明逐人充值金额的计算方式。 */
|
||||
const singleRechargeAmountHeaderTip = "通过单次充值给该部门人员增加的金额合计。"
|
||||
/** 单次充值次数说明逐人入账记录的计算方式。 */
|
||||
const singleRechargeCountHeaderTip = "单次充值产生的入账记录数,每次成功增加余额算 1 次。"
|
||||
/** 批量扣减金额说明批量扣款金额的计算方式。 */
|
||||
const batchDeductionAmountHeaderTip = "通过批量扣减从该部门人员账户扣除的金额合计。"
|
||||
/** 批量扣减次数说明按人员扣款记录计次。 */
|
||||
const batchDeductionCountHeaderTip = "批量扣减产生的扣款记录数。一次批量操作涉及多人时,每个人的扣款各算 1 次。"
|
||||
/** 单次扣减金额说明逐人扣款金额的计算方式。 */
|
||||
const singleDeductionAmountHeaderTip = "通过单次扣减从该部门人员账户扣除的金额合计。"
|
||||
/** 单次扣减次数说明逐人扣款记录的计算方式。 */
|
||||
const singleDeductionCountHeaderTip = "单次扣减产生的扣款记录数,每次成功扣减余额算 1 次。"
|
||||
/** 退还食堂金额说明余额退回原食堂的计算方式。 */
|
||||
const returnCanteenAmountHeaderTip = "批量退还余额时,退回该部门人员各自所属食堂的金额合计。"
|
||||
/** 退还食堂次数说明按人员退还记录计次。 */
|
||||
const returnCanteenCountHeaderTip = "批量退还余额产生的记录数;同一次操作涉及多人时,每个人的退还记录各算 1 次。"
|
||||
/** 流水金额说明支付订单原始金额的统计范围。 */
|
||||
const payAmountHeaderTip = "查询时间内创建且已支付的订单金额合计;退款金额另行显示,不会从这里扣除。"
|
||||
/** 流水次数说明已支付订单的计次方式。 */
|
||||
const payCountHeaderTip = "查询时间内创建且已支付的订单数量,同一笔订单算 1 次。"
|
||||
/** 退款金额说明按退款完成时间统计。 */
|
||||
const refundAmountHeaderTip = "查询时间内已经完成退款的金额合计,按退款完成时间统计。"
|
||||
/** 退款次数说明按已完成退款记录计次。 */
|
||||
const refundCountHeaderTip = "查询时间内已经完成的退款记录数;一笔订单有多次退款时,每次完成的退款各算 1 次。"
|
||||
/** 消费净金额说明支付与退款的相减关系。 */
|
||||
const netAmountHeaderTip = "流水金额减去退款金额。订单按创建时间统计,退款按完成时间统计;只选到退款的时间段时,结果可能为负数。"
|
||||
/** 消费净次数说明支付订单与退款记录的相减关系。 */
|
||||
const netCountHeaderTip = "流水次数减去退款次数。退款次数按退款记录计算,一笔订单多次退款会分别扣减。"
|
||||
|
||||
const defaultCreateTimeList = [
|
||||
dayjs().subtract(3, "month").startOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
dayjs().hour(23).minute(23).second(23).format("YYYY-MM-DD HH:mm:ss"),
|
||||
@@ -272,4 +355,4 @@ const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepT
|
||||
.el-table .success-cell {
|
||||
background: var(--el-color-success-light-9);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -7,6 +7,9 @@
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column label="商户" width="200" align="center" prop="merchantName" />
|
||||
<el-table-column label="流水金额" min-width="120" align="center" prop="totalPayPrice">
|
||||
<template #header>
|
||||
<statistics-table-header label="流水金额" :tip="payAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.totalPayPrice) }}
|
||||
@@ -14,6 +17,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="流水次数" min-width="120" align="center" prop="totalPayCount">
|
||||
<template #header>
|
||||
<statistics-table-header label="流水次数" :tip="payCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">
|
||||
{{ row.totalPayCount || 0 }}
|
||||
@@ -21,6 +27,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款金额" min-width="120" align="center" prop="totalRefundPrice">
|
||||
<template #header>
|
||||
<statistics-table-header label="退款金额" :tip="refundAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-danger">
|
||||
{{ displayPrice(row.totalRefundPrice) }}
|
||||
@@ -28,6 +37,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款次数" min-width="120" align="center" prop="totalRefundCount">
|
||||
<template #header>
|
||||
<statistics-table-header label="退款次数" :tip="refundCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-danger">
|
||||
{{ row.totalRefundCount || 0 }}
|
||||
@@ -35,6 +47,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="消费净金额" min-width="120" align="center" prop="totalProfitPrice" min-min-width="120">
|
||||
<template #header>
|
||||
<statistics-table-header label="消费净金额" :tip="netAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.totalProfitPrice) }}
|
||||
@@ -42,6 +57,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="消费净次数" min-width="120" align="center" prop="totalProfitCount" min-min-width="120">
|
||||
<template #header>
|
||||
<statistics-table-header label="消费净次数" :tip="netCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ row.totalProfitCount || 0 }}
|
||||
@@ -81,6 +99,19 @@ import * as hardTotalApi from "/@/api/hard/hard-total"
|
||||
|
||||
const { push } = useRouter()
|
||||
|
||||
/** 流水金额说明已支付订单原始金额的统计方式。 */
|
||||
const payAmountHeaderTip = "所选时间内完成支付的订单金额合计;退款金额另行显示,不会从这里扣除。"
|
||||
/** 流水次数说明已支付订单的计次方式。 */
|
||||
const payCountHeaderTip = "所选时间内完成支付的订单数量,同一笔订单算 1 次。"
|
||||
/** 退款金额说明按退款完成时间统计。 */
|
||||
const refundAmountHeaderTip = "所选时间内已经完成退款的金额合计,按退款完成时间统计。"
|
||||
/** 退款次数说明按已完成退款记录计次。 */
|
||||
const refundCountHeaderTip = "所选时间内已经完成的退款记录数;一笔订单有多次退款时,每次完成的退款各算 1 次。"
|
||||
/** 消费净金额说明支付与退款的相减关系。 */
|
||||
const netAmountHeaderTip = "流水金额减去退款金额。付款和退款分别按完成时间统计;只选到退款的时间段时,结果可能为负数。"
|
||||
/** 消费净次数说明支付订单与退款记录的相减关系。 */
|
||||
const netCountHeaderTip = "流水次数减去退款次数。退款次数按退款记录计算,一笔订单多次退款会分别扣减。"
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
createdIsNeed: false,
|
||||
isPage: false,
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="月份" width="100" align="center" prop="day" />
|
||||
<el-table-column label="流水金额" min-width="120" align="center" prop="totalPayPrice">
|
||||
<template #header>
|
||||
<statistics-table-header label="流水金额" :tip="payAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.totalPayPrice) }}
|
||||
@@ -19,6 +22,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="流水次数" min-width="120" align="center" prop="totalPayCount">
|
||||
<template #header>
|
||||
<statistics-table-header label="流水次数" :tip="payCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">
|
||||
{{ row.totalPayCount || 0 }}
|
||||
@@ -26,6 +32,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款金额" min-width="120" align="center" prop="totalRefundPrice">
|
||||
<template #header>
|
||||
<statistics-table-header label="退款金额" :tip="refundAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-danger">
|
||||
{{ displayPrice(row.totalRefundPrice) }}
|
||||
@@ -33,6 +42,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款次数" min-width="120" align="center" prop="totalRefundCount">
|
||||
<template #header>
|
||||
<statistics-table-header label="退款次数" :tip="refundCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-danger">
|
||||
{{ row.totalRefundCount || 0 }}
|
||||
@@ -40,6 +52,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="消费净金额" min-width="120" align="center" prop="totalProfitPrice" min-min-width="120">
|
||||
<template #header>
|
||||
<statistics-table-header label="消费净金额" :tip="netAmountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.totalProfitPrice) }}
|
||||
@@ -47,6 +62,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="消费净次数" min-width="120" align="center" prop="totalProfitCount" min-min-width="120">
|
||||
<template #header>
|
||||
<statistics-table-header label="消费净次数" :tip="netCountHeaderTip" />
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ row.totalProfitCount || 0 }}
|
||||
@@ -72,6 +90,19 @@ import { displayPrice } from "/@/utils/formatMoney"
|
||||
|
||||
import * as hardTotalApi from "/@/api/hard/hard-total"
|
||||
|
||||
/** 流水金额说明已支付订单原始金额的统计方式。 */
|
||||
const payAmountHeaderTip = "所选月份内完成支付的订单金额合计;退款金额另行显示,不会从这里扣除。"
|
||||
/** 流水次数说明已支付订单的计次方式。 */
|
||||
const payCountHeaderTip = "所选月份内完成支付的订单数量,同一笔订单算 1 次。"
|
||||
/** 退款金额说明按退款完成时间统计。 */
|
||||
const refundAmountHeaderTip = "所选月份内已经完成退款的金额合计,按退款完成时间归属月份。"
|
||||
/** 退款次数说明按已完成退款记录计次。 */
|
||||
const refundCountHeaderTip = "所选月份内已经完成的退款记录数;一笔订单有多次退款时,每次完成的退款各算 1 次。"
|
||||
/** 消费净金额说明支付与退款的相减关系。 */
|
||||
const netAmountHeaderTip = "流水金额减去退款金额。付款按支付月份、退款按退款完成月份统计;只选到退款的月份时,结果可能为负数。"
|
||||
/** 消费净次数说明支付订单与退款记录的相减关系。 */
|
||||
const netCountHeaderTip = "流水次数减去退款次数。退款次数按退款记录计算,一笔订单多次退款会分别扣减。"
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
createdIsNeed: false,
|
||||
queryForm: {},
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -81,11 +81,24 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.types == '2' && form.supType == '20'" :span="24" class="mb20">
|
||||
<el-col v-if="form.types == '2'" :span="12" class="mb20">
|
||||
<el-form-item label="优惠价格:" prop="discountsPrice">
|
||||
<el-input v-model="form.discountsPrice" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.types == '2'" :span="12" class="mb20">
|
||||
<el-form-item prop="discountsRate">
|
||||
<template #label>
|
||||
<span>优惠折扣:</span>
|
||||
<tip content="范围1-99,填10表示1折;折扣越大(数值越小)的手机端排在越前面,留空表示无折扣" />
|
||||
</template>
|
||||
<div class="flex w-full items-center">
|
||||
<el-input-number class="flex-1" v-model="form.discountsRate" :min="1" :max="99"
|
||||
:value-on-clear="null" placeholder="如10表示1折" />
|
||||
<span class="ml5">%</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item prop="phone">
|
||||
<template #label>
|
||||
@@ -174,6 +187,7 @@ const defaultForm = {
|
||||
parentId: "",
|
||||
catgoryId: "",
|
||||
discountsPrice: "",
|
||||
discountsRate: null as number | null,
|
||||
platformRanke: '0',
|
||||
}
|
||||
|
||||
@@ -225,6 +239,7 @@ watch(
|
||||
form.cooperationStatus = "1"
|
||||
form.parentId = ""
|
||||
form.discountsPrice = ""
|
||||
form.discountsRate = null
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -273,6 +288,9 @@ const getmerchantInfoData = async (id: string) => {
|
||||
if (form.platformRanke == undefined) {
|
||||
form.platformRanke = defaultForm.platformRanke
|
||||
}
|
||||
if (form.discountsRate == undefined) {
|
||||
form.discountsRate = null
|
||||
}
|
||||
jobTime.morning = resJobTime[0]?.split("-") ?? []
|
||||
jobTime.noon = resJobTime[1]?.split("-") ?? []
|
||||
jobTime.evening = resJobTime[2]?.split("-") ?? []
|
||||
|
||||
File mode changed.
File mode changed.
+7
-5
@@ -22,11 +22,13 @@
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="40" />
|
||||
<el-table-column prop="apiName" label="接口名称" show-overflow-tooltip/>
|
||||
<el-table-column prop="requestParameter" label="请求参数" show-overflow-tooltip/>
|
||||
<el-table-column prop="requestUrl" label="请求路径" show-overflow-tooltip/>
|
||||
<el-table-column prop="httpStatus" label="状态码" show-overflow-tooltip/>
|
||||
<el-table-column prop="errorCode" label="错误码(如-1)" show-overflow-tooltip/>
|
||||
<el-table-column prop="createTime" label="时间" width="170" show-overflow-tooltip/>
|
||||
<el-table-column prop="apiName" label="接口名称" min-width="180" show-overflow-tooltip/>
|
||||
<el-table-column prop="requestParameter" label="请求参数" min-width="160" show-overflow-tooltip/>
|
||||
<el-table-column prop="requestUrl" label="请求路径" min-width="140" show-overflow-tooltip/>
|
||||
<el-table-column prop="httpStatus" label="状态" width="80" show-overflow-tooltip/>
|
||||
<el-table-column prop="errorCode" label="错误码" width="100" show-overflow-tooltip/>
|
||||
<el-table-column prop="errorMsg" label="错误描述" min-width="120" show-overflow-tooltip/>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button icon="edit-pen" text type="primary" v-auth="'thirdparty_thirdPartyLog_edit'"
|
||||
|
||||
Reference in new issue
Block a user