feat:退款,支付返回

This commit is contained in:
dzc committed 2026-07-11 17:29:55 +08:00
1 parent 41d3b01c24
commit 68349eb8c0
17 files changed
+1092 -491

No files matched your search

+314
View File
@@ -0,0 +1,314 @@
<script setup lang="ts">
import { ref } from "vue"
import { hooks, router } from "@@/index"
import {
hasRoleCode,
MCH_USER,
} from "@@/utils/user-role"
import { filePath } from "@@/utils/file"
import OrderStatusTag from "./order-status-tag.vue"
const emit = defineEmits(['cancel', 'confirmReceipt'])
export type Props = {
// 订单详情
cardetail: any
// 商品信息是否折叠
goodsCollapsed?: boolean
// 是否显示商户信息
visibleMch?: boolean
// 是否显示用户信息
visibleUser?: boolean
// 是否显示底部边框
visibleBottomBorder?: boolean
// 是否是最后一个订单
isLast?: boolean
// 底部操作区域控制。
// 传 true 时,等价于 ['refund', 'qrCodeDetail'],表示两个按钮都允许显示。
// 传数组时,可按需精确控制具体显示哪些按钮。
footerOperate?: boolean | Array<'refund' | 'qrCodeDetail' | 'cancel' | 'expressTracking'>
}
const props = withDefaults(defineProps<Props>(), {
cardetail: {},
goodsCollapsed: true,
visibleMch: true,
visibleUser: false,
visibleBottomBorder: false,
isLast: false,
footerOperate: false,
})
const collapseState = ref<Record<string, boolean>>({})
const getOrderKey = (order: any, index: number) => String(order?.id ?? index)
const hasMultipleGoods = (order: any) => (order?.carList?.length || 0) > 1
const isGoodsCollapsed = (order: any, index: number) => {
if (!props.goodsCollapsed || !hasMultipleGoods(order)) return false
return collapseState.value[getOrderKey(order, index)] ?? true
}
const toggleGoodsCollapse = (order: any, index: number) => {
if (!props.goodsCollapsed || !hasMultipleGoods(order)) return
const key = getOrderKey(order, index)
collapseState.value[key] = !isGoodsCollapsed(order, index)
}
// 判断某个底部操作按钮是否允许显示。
// `footerOperate === true` 时,视为开启全部按钮;
// 未传或传 false 时,不显示任何底部操作按钮;
// 传数组时,仅显示数组中包含的操作项。
const hasFooterOperate = (operate: 'refund' | 'qrCodeDetail' | 'cancel' | 'expressTracking') => {
if (props.footerOperate === true) return true
if (!Array.isArray(props.footerOperate)) return false
return props.footerOperate.includes(operate)
}
// 退款按钮显示条件:
// 1. footerOperate 允许 refund;
// 2. 订单状态为 5、210
// 3. 订单类型为 2、7
const showRefundButton = (order: any) => {
return hasFooterOperate('refund') && ['5', '210'].includes(String(order?.orderStatus)) && ['1', '2', '7'].includes(String(order?.orderType)) && ['2', '3', '4', '5'].includes(String(order?.merchantType))
}
// 显示订单详情按钮条件:
// 1. footerOperate 允许 refund;
// 2. 订单状态为 10、20;
// 3. 订单类型为 2。
// 4. 订单退款为 10,20,30。
const showRefundDetailButton = (order: any) => {
return (
hasFooterOperate('refund') &&
['10', '20'].includes(String(order?.orderStatus)) &&
['1', '2', '7'].includes(String(order?.orderType)) &&
['10', '20', '30'].includes(order?.refundStatus || '')
)
}
// 核销码按钮显示条件:
// 1. footerOperate 允许 qrCodeDetail;
// 2. 订单状态为 5;
// 3. 订单类型为 2 或 7。
const showQrCodeDetailButton = (order: any) => {
return (
hasFooterOperate('qrCodeDetail') &&
String(order?.orderStatus) === '5' &&
['2'].includes(String(order?.orderType))
)
}
// 手动核销按钮显示条件:
// 1. footerOperate 允许 cancel;
// 2. 订单状态为 5;
// 3. 订单类型为 2。
// 4. 用户有'移动端商户角色'
const showCancelButton = (order: any) => {
return (
hasFooterOperate('cancel') &&
String(order?.orderStatus) === '5' &&
['2'].includes(String(order?.orderType)) && hasRoleCode(MCH_USER)
)
}
// 查看订单和核销码
const doToDetail = (v: any) => {
router.jump("/pages/order/detail", { id: v.id })
}
// 申请售后
const doRefund = (v: any) => {
router.jump("/pages/refund/apply", { id: v.id })
}
const doCancel = (v: any) => {
emit('cancel', v)
}
</script>
<template>
<view class="flex-col justify-center pb-20rpx gap-20rpx"
:class="!props.isLast && props.visibleBottomBorder ? 'car-border' : ''">
<!-- start 用户信息 start -->
<view v-if="props.visibleUser" class="flex items-start justify-between gap-16rpx">
<view class="min-w-0 flex-1">
<view class="flex items-center min-w-0">
<text class="sub-text-26 truncate">{{ props.cardetail.username || "未知用户" }}</text>
<text class="mx-10rpx text-18rpx text-[#d0d5dd]">|</text>
<text class="second-text-24 truncate">{{ props.cardetail.userMobile || "-" }}</text>
</view>
<view class="mt-10rpx inline-flex max-w-full items-center rounded-24rpx bg-[#f8fafc] px-14rpx py-8rpx">
<text class="text-20rpx text-[#98a2b3]">下单时间</text>
<text v-if="props.cardetail.createTime" class="ml-10rpx text-20rpx text-[#475467] truncate">
{{ hooks.useTime(props.cardetail.createTime, "{y}-{m}-{d} {h}:{i}:{s}") }}
</text>
<text v-else class="ml-10rpx text-20rpx text-[#1570ef]">正在下单</text>
</view>
</view>
<view class="flex-shrink-0">
<view class="w-100rpx flex-row-center">
<order-status-tag :cardetail="props.cardetail" />
</view>
<view class="w-100rpx flex-row-center second-text-22 mt-10rpx" v-if="props.cardetail.refundStatus">
{{ hooks.useEnum("hardwareRefundStatusEnum", props.cardetail.refundStatus) }}
</view>
</view>
</view>
<!-- end 用户信息 end -->
<!-- start 商户信息 start -->
<view v-if="props.visibleMch" class="flex-row">
<image class="size-90rpx rounded-8rpx bg-[#999] shadow-main overflow-hidden"
:src="filePath(props.cardetail.logoPic) || '/static/logo/zax.png'" mode="aspectFill" />
<view class="flex-1 ml-20rpx">
<view class="flex-row items-center justify-between">
<view class="sub-text-26">{{ props.cardetail.merchantName }}</view>
<view class="w-100rpx flex-row-center">
<order-status-tag :cardetail="props.cardetail" />
</view>
</view>
<view class="flex-row items-center justify-between second-text-22">
<view>
<text>下单时间:</text>
<text v-if="props.cardetail.createTime" class="ml-10rpx">
{{ hooks.useTime(props.cardetail.createTime, "{y}-{m}-{d} {h}:{i}:{s}") }}
</text>
<text v-else class="ml-10rpx color-blue">正在下单</text>
</view>
<view class="w-100rpx flex-row-center mt-10rpx" v-if="props.cardetail.refundStatus">
{{ hooks.useEnum("hardwareRefundStatusEnum", props.cardetail.refundStatus) }}
</view>
</view>
</view>
</view>
<!-- end 商户信息 end -->
<!-- start 商品信息 start -->
<view v-if="props.cardetail?.carList?.length"
class="flex items-center rounded-12rpx bg-[#fafafa] px-20rpx py-16rpx transition-all duration-200"
@tap="toggleGoodsCollapse(props.cardetail, 0)">
<view class="min-w-0 flex-1">
<view v-if="isGoodsCollapsed(props.cardetail, 0)" class="flex items-center">
<image class="h-90rpx w-120rpx flex-shrink-0 rounded-8rpx bg-[#999] shadow-main overflow-hidden"
:src="filePath(props.cardetail.carList?.[0]?.productPic) || '/static/logo/zax.png'"
mode="aspectFill" />
<view class="ml-16rpx min-w-0 flex-1">
<view class="second-text-22 truncate">
{{ props.cardetail.carList?.[0]?.productName || "暂无商品" }}
</view>
<view v-if="hasMultipleGoods(props.cardetail)" class="mt-8rpx text-20rpx text-[#98a2b3]">
共 {{ props.cardetail.carList?.length || 0 }} 件,点击展开
</view>
<view v-else class="mt-8rpx text-20rpx text-[#98a2b3]">共 1 件商品</view>
</view>
</view>
<scroll-view v-if="!isGoodsCollapsed(props.cardetail, 0)" scroll-x
class="overflow-hidden whitespace-nowrap">
<view class="flex items-start">
<view v-for="item in props.cardetail.carList || []" :key="item.id"
class="mr-20rpx w-120rpx flex-shrink-0">
<image class="h-90rpx w-120rpx rounded-8rpx bg-[#999] shadow-main overflow-hidden"
:src="filePath(item.productPic) || '/static/logo/zax.png'" mode="aspectFill" />
<view class="second-text-22 mt-8rpx truncate">{{ item.productName }}</view>
</view>
</view>
</scroll-view>
</view>
<view class="order-car__summary w-120rpx flex-col-center">
<view class="sub-text-28">¥{{ hooks.useMoney(props.cardetail.payPrice) }}</view>
<view class="second-text-20 font-400! line-height-1em!">共 {{ props.cardetail.carList?.length || 0 }} 件
</view>
</view>
<view v-if="hasMultipleGoods(props.cardetail)"
class="ml-12rpx flex-col-center text-[#98a2b3] transition-transform duration-200">
<text class="text-20rpx">{{ isGoodsCollapsed(props.cardetail, 0) ? "展开" : "收起" }}</text>
<text class="iconfont icon-youjiantou mt-8rpx text-20rpx"
:class="isGoodsCollapsed(props.cardetail, 0) ? 'rotate-90' : '-rotate-90'" />
</view>
</view>
<!-- end 商品信息 end -->
<!-- start 简单购买信息(付款方式+支付金额) start -->
<view v-if="!props.cardetail?.carList?.length"
class="flex items-center justify-between rounded-12rpx border-[#eef3f8] bg-[linear-gradient(90deg,#ffffff_0%,#fafafa_100%)] px-22rpx py-12rpx">
<view class="flex-row-center sub-text-24 gap-8rpx">
<text class="iconfont icon-zhifufangshi1 text-24rpx!" />
<text> {{ hooks.useEnum('hardwarePayTypeEnum', props.cardetail.payType) }}</text>
</view>
<view class="order-car__summary w-120rpx flex-col-center">
<view class="sub-text-28">¥{{ hooks.useMoney(props.cardetail.payPrice) }}</view>
</view>
</view>
<!-- end 简单购买信息(付款方式+支付金额) end -->
<!-- start 预约信息 start -->
<view v-if="props.cardetail.apptId && props.cardetail.apptStartTime && props.cardetail.apptEndTime"
class="flex items-center justify-between rounded-12rpx border border-[#eef3f8] bg-[linear-gradient(90deg,#ffffff_0%,#fafafa_100%)] px-22rpx py-12rpx shadow-[0_8rpx_24rpx_rgba(15,23,42,0.04)]">
<view class="min-w-0 flex-1">
<view class="second-text-22 text-[#98a2b3]!">预约时间段</view>
<view class="mt-10rpx sub-text-24">
{{ props.cardetail.apptStartTime }} - {{ props.cardetail.apptEndTime }}
</view>
</view>
<view v-if="props.cardetail.apptTitle"
class="flex items-center gap-4rpx ml-20rpx flex-shrink-0 rounded-full border border-[#d1e9ff] bg-[#eff8ff] px-18rpx py-16rpx text-22rpx line-height-1 text-[#1570ef]">
<text> {{ props.cardetail.apptUseDate }}</text>
<text>{{ props.cardetail.apptTitle }}</text>
</view>
</view>
<!-- end 预约信息 end -->
<!-- start 底部操作区域 start -->
<view
v-if="showRefundButton(props.cardetail) || showRefundDetailButton(props.cardetail) || showQrCodeDetailButton(props.cardetail) || showCancelButton(props.cardetail)"
class="w-full flex-row items-center justify-end my-1">
<view v-if="showRefundButton(props.cardetail)" class="botder_btn" @tap.stop="doRefund(props.cardetail)">
退款
</view>
<view v-if="showRefundDetailButton(props.cardetail)" class="m-l-20rpx botder_btn"
@tap.stop="doToDetail(props.cardetail)">
售后详情
</view>
<view v-if="showQrCodeDetailButton(props.cardetail)" class="m-l-20rpx botder_btn"
@tap.stop="doToDetail(props.cardetail)">
核销码
</view>
<view v-if="showCancelButton(props.cardetail)" class="m-l-20rpx botder_btn"
@tap.stop="doCancel(props.cardetail)">
手动核销
</view>
</view>
<!-- end 底部操作区域 end -->
</view>
</template>
<style scoped lang="scss">
.car-border {
border-bottom: 1px solid #fafafa;
}
.order-car__summary {
position: relative;
padding-left: 18rpx;
}
.order-car__summary::before {
content: "";
position: absolute;
left: -8rpx;
top: 8rpx;
bottom: 8rpx;
width: 20rpx;
pointer-events: none;
background: linear-gradient(90deg, rgba(15, 23, 42, 0.08) 0%, rgba(15, 23, 42, 0.03) 38%, rgba(15, 23, 42, 0) 100%);
filter: blur(8rpx);
}
</style>
@@ -0,0 +1,26 @@
<script setup lang="ts">
export type Props = {
// 订单详情
cardetail: any
}
const props = withDefaults(defineProps<Props>(), {
cardetail: {},
})
</script>
<template>
<slot v-if="props.cardetail.orderStatus == 5">
<tm-tag v-if="props.cardetail.orderType == 2" color="success" size="xs">待使用</tm-tag>
<tm-tag v-else-if="props.cardetail.orderType == 7" color="success" size="xs">待发货</tm-tag>
<tm-tag v-else color="success" size="xs">已支付</tm-tag>
</slot>
<tm-tag v-if="props.cardetail.orderStatus == 200" color="success" size="xs">待发货</tm-tag>
<tm-tag v-if="props.cardetail.orderStatus == 210" color="success" size="xs">待收货</tm-tag>
<tm-tag v-if="props.cardetail.orderStatus == 220" size="xs">已收货</tm-tag>
<tm-tag v-if="props.cardetail.orderStatus == 230" color="success" size="xs">已评价</tm-tag>
<tm-tag v-if="props.cardetail.orderStatus == 10" color="error" size="xs">售后</tm-tag>
<tm-tag v-if="props.cardetail.orderStatus == 20" size="xs">已核销</tm-tag>
<tm-tag v-if="props.cardetail.orderStatus == 30" color="info" size="xs">已取消(智能柜)</tm-tag>
<tm-tag v-if="props.cardetail.orderStatus == 110" color="success" size="xs">配送中</tm-tag>
</template>
+8
View File
@@ -36,6 +36,13 @@ const hardwareRefundStatusEnum = {
30: "售后失败",
}
const hardSaleStatusEnum = {
0: "售后中",
50: "售后完成",
61: "买家取消",
62: "卖家拒绝",
}
const carTypeEnum = {
1: "私家车",
2: "公车",
@@ -98,4 +105,5 @@ export default {
actCouponLimitTypeEnum,
actCouponTypeEnum,
actUserReceiveEnum,
hardSaleStatusEnum,
}
+8 -1
View File
@@ -7,6 +7,7 @@ export function useScanCode(actModalShow) {
const appInfo = await getAppInfo()
if (isDevEnv()) {
let testQrCode = ""
// 开发环境模拟扫码
// const testQrCode = "eyJpZCI6IjE5MDU4ODg4NjQ0NTQ1MzMxMjEiLCJuYW1lIjoi6ICB5Y6/5bqc6aOf5aCCIn0=" // 测试线上-老县府食堂-收款二维码
// const testQrCode = "eyJoYXJkRG9vcklkIjoiMTkxNDIxNTYwODQzNzQxMTg0MSIsImNhYmluZXRObyI6IjEifQ==" // 测试线上-智能柜-收款二维码
@@ -15,7 +16,13 @@ export function useScanCode(actModalShow) {
// const testQrCode = "http://192.168.101.45:8889/h5/#/pages/pay/scanPay?id=1905816751043452930&name=测试新县府超市" // 邓本地-测试环境-新县府超市收款二维码
// const testQrCode = "http://192.168.101.45:8889/h5/#/pages/pay/scanPay?hardDoorId=1913157080418295809&cabinetNo=1" // 测试智能柜柜门二维码
// const testQrCode = "eyJjYW5jZWxJZCI6IjE5ODQyNDA4MzM5NDMyMDc5MzcifQ==" // 测试商户活动核销
const testQrCode = "https://stgl.pj.gov.cn/h5/#/pages/pay/scanPay?id=1931916121241079810"
// const testQrCode = "https://stgl.pj.gov.cn/h5/#/pages/pay/scanPay?id=1931916121241079810"
// 测试环境(自营食堂)-新县府食堂
// testQrCode = "https://stgl.pj.gov.cn/h5/#/pages/pay/scanPay?id=1905816666859577346"
// 测试环境(合作商家)-测试部门超市
testQrCode = "https://stgl.pj.gov.cn/h5/#/pages/pay/scanPay?id=1931916121241079810"
// 测试环境(自营商家)-老县府超市
// testQrCode = "https://stgl.pj.gov.cn/h5/#/pages/pay/scanPay?id=1905816898011865090"
uni.showModal({ content: "开发环境不是浙政钉和诗画环境,扫码仅供测试" })
return Promise.resolve(testQrCode)
}
+111 -3
View File
@@ -1,9 +1,9 @@
@font-face {
font-family: "iconfont";
/* Project id 4740692 */
src: url('//at.alicdn.com/t/c/font_4740692_uwg9mtj0d0p.woff2?t=1770172172940') format('woff2'),
url('//at.alicdn.com/t/c/font_4740692_uwg9mtj0d0p.woff?t=1770172172940') format('woff'),
url('//at.alicdn.com/t/c/font_4740692_uwg9mtj0d0p.ttf?t=1770172172940') format('truetype');
src: url('https://at.alicdn.com/t/c/font_4740692_ol93veips6n.woff2?t=1782372215492') format('woff2'),
url('https://at.alicdn.com/t/c/font_4740692_ol93veips6n.woff?t=1782372215492') format('woff'),
url('https://at.alicdn.com/t/c/font_4740692_ol93veips6n.ttf?t=1782372215492') format('truetype');
}
.iconfont {
@@ -14,6 +14,114 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-jisuwuliu:before {
content: "\e61a";
}
.icon-add:before {
content: "\e79e";
}
.icon-add1:before {
content: "\e6aa";
}
.icon-bianji:before {
content: "\e763";
}
.icon-address1:before {
content: "\e605";
}
.icon-address:before {
content: "\e659";
}
.icon-address-f:before {
content: "\e604";
}
.icon-zhifufangshi:before {
content: "\e759";
}
.icon-zhifufangshi-yuezhifu:before {
content: "\e68c";
}
.icon-zhifufangshi1:before {
content: "\e603";
}
.icon-rili:before {
content: "\e60e";
}
.icon-hexiaoicon:before {
content: "\e602";
}
.icon-hexiao1:before {
content: "\e7fe";
}
.icon-hexiao:before {
content: "\e63f";
}
.icon-fanhui:before {
content: "\e8fb";
}
.icon-tongzhi:before {
content: "\e61b";
}
.icon-dangqianweizhi:before {
content: "\e678";
}
.icon-sousuo:before {
content: "\e619";
}
.icon-yizuofei1:before {
content: "\e60d";
}
.icon-yizuofei:before {
content: "\f2d8";
}
.icon-jilu1:before {
content: "\e631";
}
.icon-jilu:before {
content: "\e664";
}
.icon-kefu:before {
content: "\e677";
}
.icon-kefu1:before {
content: "\e612";
}
.icon-info:before {
content: "\e64a";
}
.icon-setting:before {
content: "\e8d8";
}
.icon-shezhi:before {
content: "\e601";
}
.icon-yiguoqi:before {
content: "\e642";
}
+76
View File
@@ -0,0 +1,76 @@
import { constant } from "@@/index"
import { userApi } from "@@/api"
export const ROLE_ONLINE_CANTEEN_MCH = "ONLINE_CANTEEN_MCH"
export const ROLE_CANTEEN_RIDER = "CANTEEN_RIDER"
// 移动端商户角色(核销订餐二维码)
export const MCH_USER = "MCH_USER"
/** 移动端后台管理入口角色 */
export const ROLE_MOBILE_ADMIN_PC = "MOBILE_ADMIN_PC"
/** 新增用户默认角色 */
export const ROLE_GENERAL_USER = "GENERAL_USER"
export const ROLE_WALLET_AUTO_CREATE = "WALLET_AUTO_CREATE"
const DEFAULT_NEW_USER_ROLES = [
{ roleCode: ROLE_GENERAL_USER, roleId: "2" },
{ roleCode: ROLE_WALLET_AUTO_CREATE, roleId: "1873557615006810113" },
] as const
/** 新增用户时默认勾选的角色 ID(优先按 roleCode 从列表解析) */
export const resolveDefaultNewUserRoleIds = (roleList: { roleId?: string | number; roleCode?: string }[]): string[] =>
DEFAULT_NEW_USER_ROLES.map(({ roleCode, roleId }) => {
const hit = roleList.find((r) => r.roleCode === roleCode)
return hit ? String(hit.roleId) : roleId
})
export const getUserRoleCodes = (): string[] => {
const userInfo: any = uni.getStorageSync(constant.storageConstant.USERINFO) || {}
const codes = userInfo?.roleCodes
if (Array.isArray(codes)) return codes.filter(Boolean)
return []
}
export const hasRoleCode = (code: string) => getUserRoleCodes().includes(code)
/** 刷新用户信息中的 roleCodes(我的页菜单、统计页鉴权用) */
export const refreshUserRoleCodes = async (): Promise<string[]> => {
try {
const res: any = await userApi.userInfo()
if (res?.code === 0 && res.data) {
uni.setStorageSync(constant.storageConstant.USERINFO, res.data)
return Array.isArray(res.data.roleCodes) ? res.data.roleCodes : []
}
} catch (e) {
console.warn("refreshUserRoleCodes failed", e)
}
return getUserRoleCodes()
}
/** 移动端后台管理 - 权限码(与 PC 角色菜单分配一致) */
export const PERM_SYS_USER_ADD = "sys_user_add"
export const PERM_SYS_USER_EDIT = "sys_user_edit"
export const PERM_SYS_USER_DEL = "sys_user_del"
export const PERM_WALLET_MANAGE_VIEW = "wallet_walletManage_view"
export const PERM_WALLET_MANAGE_EDIT = "wallet_walletManage_edit"
export const PERM_WALLET_UPDATE_BALANCE = "wallet_walletTransaction_update_balance"
export const ADMIN_USER_PERMS = [PERM_SYS_USER_ADD, PERM_SYS_USER_EDIT, PERM_SYS_USER_DEL]
export const ADMIN_WALLET_PERMS = [PERM_WALLET_MANAGE_VIEW, PERM_WALLET_MANAGE_EDIT, PERM_WALLET_UPDATE_BALANCE]
export const ADMIN_HUB_PERMS = [...ADMIN_USER_PERMS, ...ADMIN_WALLET_PERMS]
export const getUserPermissions = (): string[] => {
const userInfo: any = uni.getStorageSync(constant.storageConstant.USERINFO) || {}
const perms = userInfo?.permissions
if (Array.isArray(perms)) return perms.filter(Boolean)
return []
}
export const hasPermission = (code: string) => getUserPermissions().includes(code)
export const hasAnyPermission = (codes: string[]) => codes.some((code) => hasPermission(code))
/** 刷新 permissions(顺带刷新 roleCodes) */
export const refreshUserPermissions = async (): Promise<string[]> => {
await refreshUserRoleCodes()
return getUserPermissions()
}