Compare commits

...
10 Commits
Author SHA1 Message Date
dzc cce677e3ff 重新推送
build / task (16.x, npm, macOS-latest) (push) Canceled after 0s
build / task (16.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, npm, windows-latest) (push) Canceled after 0s
build / task (16.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (16.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (16.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (16.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, yarn, windows-latest) (push) Canceled after 0s
build / task (18.x, npm, macOS-latest) (push) Canceled after 0s
build / task (18.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, npm, windows-latest) (push) Canceled after 0s
build / task (18.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (18.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (18.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (18.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, yarn, windows-latest) (push) Canceled after 0s
build / task (20.x, npm, macOS-latest) (push) Canceled after 0s
build / task (20.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, npm, windows-latest) (push) Canceled after 0s
build / task (20.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (20.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (20.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (20.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, yarn, windows-latest) (push) Canceled after 0s
2026-10-15 14:12:05 +08:00
dzc 69a28cce40 feat:对账以wallet_transaction表为准 2026-09-30 16:36:04 +08:00
dzc cb2e95562a feat:店铺折扣 2026-09-30 15:07:33 +08:00
dzc c8ff18d190 Merge branch 'master' of https://gitea.zianxin.com/zax/biz-pc-cloud 2026-09-29 20:00:29 +08:00
dzc 07dedf45f9 feat:优化 2026-09-29 20:00:22 +08:00
zjf dbe543ae9a Merge remote-tracking branch 'origin/master' 2026-09-19 23:57:45 +08:00
zjf deefac848b seata测试 2026-09-19 23:57:33 +08:00
dzc 75a36d36ad 111 2026-09-10 18:51:01 +08:00
dzc f16c1e6eb7 feat:人脸质量 2026-09-10 15:34:23 +08:00
dzc 18aa169b69 123123 2026-09-09 20:22:46 +08:00
37 changed files with 1193 additions and 363 deletions

No files matched your search

+2 -1
View File
@@ -51,4 +51,5 @@ coverage/
.v2c/
.video_agent/
.mimosa/
.zcode/
.zcode/
.zcodeignore
Regular → Executable
View File
File mode changed.
Regular → Executable
View File
File mode changed.
+17
View File
@@ -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
View File
File mode changed.
+2 -2
View File
@@ -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",
View File
File mode changed.
View File
File mode changed.
Regular → Executable
View File
File mode changed.
View File
File mode changed.
Vendored Regular → Executable
View File
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>
+2
View File
@@ -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)
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
+85
View File
@@ -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>
+27 -48
View File
@@ -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",
},
View File
File mode changed.
View File
File mode changed.
+473 -161
View File
@@ -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>
+225 -133
View File
@@ -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),
"等待履约完成",
),
)
+90 -7
View File
@@ -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: {},
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
+19 -1
View File
@@ -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("-") ?? []
Vendored Regular → Executable
View File
File mode changed.
Vendored Regular → Executable
View File
File mode changed.
+7 -5
View File
@@ -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'"