You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

435 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="wrap">
<u-navbar
title="退款详情"
:is-back="true"
back-icon-color="#fff"
:background="{ background: '#1479ff' }"
title-color="#fff"
:border-bottom="false"
:custom-back="goBack"
></u-navbar>
<view class="b-border"></view>
<view v-if="loading" class="state-tip">加载中…</view>
<view v-else-if="!refundDetail.id" class="state-tip">未找到退款记录</view>
<scroll-view v-else scroll-y class="scroll" :style="{ paddingBottom: actionBarPad }">
<view class="order-info">
<view class="section-title">退款信息</view>
<view class="info-item">
<text class="label">退款编号</text>
<text class="value">{{ refundDetail.no }}</text>
</view>
<view class="info-item">
<text class="label">状态</text>
<text class="value">{{ refundStatusText(refundDetail.status) }}</text>
</view>
<view class="info-item" v-if="refundDetail.apply_reason">
<text class="label">退款理由</text>
<text class="value">{{ refundDetail.apply_reason }}</text>
</view>
<view class="info-item" v-if="showRefundResult">
<text class="label">处理说明</text>
<text class="value">{{ displayRefundResult }}</text>
</view>
<view class="info-item" v-if="refundDetail.created_at">
<text class="label">申请时间</text>
<text class="value">{{ refundDetail.created_at }}</text>
</view>
</view>
<view class="order-info" v-if="orderRef && orderRef.id">
<view class="section-title">关联订单</view>
<view class="info-item">
<text class="label">订单号</text>
<text class="value">{{ orderRef.no }}</text>
</view>
<view class="info-item">
<text class="label">服务项目</text>
<text class="value">{{ orderRef.accompany_product ? orderRef.accompany_product.name : '' }}</text>
</view>
<view class="info-item">
<text class="label">服务时间</text>
<text class="value">{{ orderRef.time }}</text>
</view>
<view class="info-item" v-if="Number(orderRef.type) === 2">
<text class="label">服务地址</text>
<text class="value">{{ homeCareServiceAddress(orderRef) }}</text>
</view>
<view class="info-item" v-else>
<text class="label">就诊医院</text>
<text class="value">{{ orderRef.hospital ? orderRef.hospital.name : '' }}</text>
</view>
<view class="info-item">
<text class="label">被服务人</text>
<text class="value">{{ orderRef.user_archive ? orderRef.user_archive.name : '' }}</text>
</view>
<view class="info-item">
<text class="label">订单金额</text>
<text class="value">¥{{ orderRef.price }}</text>
</view>
<view class="info-item" v-if="orderRef.paid_at">
<text class="label">支付时间</text>
<text class="value">{{ orderRef.paid_at }}</text>
</view>
</view>
<view class="order-info" v-if="orderLogs.length > 0">
<view class="section-title">订单日志</view>
<view class="log-entry" v-for="(log, idx) in orderLogs" :key="log.id != null ? log.id : idx">
<text class="log-meta">{{ formatLogTime(log) }}</text>
<text class="log-operator">{{ formatLogOperator(log) }}</text>
<text class="log-text">{{ log.remark || '—' }}</text>
</view>
</view>
</scroll-view>
<view v-if="showActionBar" class="action-bar">
<view class="bar-btn-wrap">
<u-button type="primary" shape="circle" :custom-style="approveStyle" @click="onApprove">同意退款</u-button>
</view>
<view class="bar-btn-wrap">
<u-button shape="circle" type="default" :custom-style="rejectStyle" @click="onReject"></u-button>
</view>
</view>
</view>
</template>
<script>
import { homeCareServiceAddress } from '@/common/homeCareAddress.js'
import { parseApiRecord, refundStatusText } from '@/common/refundApply.js'
export default {
data() {
return {
refundId: '',
detailSource: 'staff',
loading: true,
refundDetail: {},
approveStyle: {
background: '#1479ff',
color: '#fff',
fontSize: '30rpx',
height: '88rpx',
width: '100%'
},
rejectStyle: {
background: '#969da7',
color: '#fff',
fontSize: '30rpx',
height: '88rpx',
width: '100%'
}
}
},
computed: {
orderRef() {
return this.refundDetail.order || null
},
orderLogs() {
const o = this.orderRef
if (!o || typeof o !== 'object') return []
const raw = o.accompany_order_log || o.accompanyOrderLog
if (!Array.isArray(raw)) return []
return [...raw].sort((a, b) => {
const ta = new Date(a.created_at || 0).getTime()
const tb = new Date(b.created_at || 0).getTime()
return tb - ta
})
},
showActionBar() {
return !this.loading && Number(this.refundDetail.status) === 0
},
actionBarPad() {
return this.showActionBar ? '180rpx' : '40rpx'
},
showRefundResult() {
const st = Number(this.refundDetail.status)
if (st === 1 || !this.refundDetail.result) return false
return st === 2
},
displayRefundResult() {
const raw = String(this.refundDetail.result || '').trim()
if (!raw) return ''
if (raw.startsWith('{')) {
try {
const obj = JSON.parse(raw)
if (obj.summary) return String(obj.summary)
} catch (e) {}
return '退款失败,请联系管理员'
}
return raw
}
},
onLoad(options) {
this.refundId = options.id || ''
this.detailSource = options.source === 'operator' ? 'operator' : 'staff'
if (this.refundId) {
this.fetchDetail()
} else {
this.loading = false
}
},
methods: {
homeCareServiceAddress,
goBack() {
uni.navigateBack({ delta: 1 })
},
normalizePayload(res) {
let payload = res
if (
payload &&
typeof payload.data !== 'undefined' &&
payload.data !== null &&
payload.errcode === undefined
) {
payload = payload.data
}
return payload
},
async fetchDetail() {
this.loading = true
try {
let res = null
if (this.detailSource === 'operator') {
res = await this.$u.api.operatorAccompanyRefundShow({
id: this.refundId,
'show_relation[0]': 'order',
'show_relation[1]': 'order.userArchive',
'show_relation[2]': 'order.accompanyProduct',
'show_relation[3]': 'order.hospital',
'show_relation[4]': 'order.accompanyOrderLog'
})
} else {
res = await this.$u.api.staffRefundShow({
id: this.refundId,
'show_relation[0]': 'order',
'show_relation[1]': 'order.userArchive',
'show_relation[2]': 'order.accompanyProduct',
'show_relation[3]': 'order.hospital',
'show_relation[4]': 'order.accompanyOrderLog'
})
}
if (res === false) {
this.refundDetail = {}
this.loading = false
return
}
this.refundDetail = this.normalizePayload(res) || {}
} catch (e) {
this.refundDetail = {}
uni.showToast({ title: this.apiErrorMessage(e, '加载失败'), icon: 'none' })
} finally {
this.loading = false
}
},
apiErrorMessage(e, fallback) {
const m = e && (e.msg || e.message || (e.data && (e.data.msg || e.data.message)))
if (m && String(m).trim()) return String(m).slice(0, 60)
return fallback || '操作失败'
},
refundStatusText(s) {
return refundStatusText(s)
},
formatLogTime(log) {
if (!log) return ''
return log.created_at || log.updated_at || ''
},
formatLogOperator(log) {
if (!log) return '操作人:—'
const name = log.operator_name && String(log.operator_name).trim()
? String(log.operator_name).trim()
: ''
if (!name) return '操作人:—'
const typeLabels = {
worker: '工作人员',
nurse: '护工',
admin: '运营',
user: '用户',
system: '系统'
}
let t = log.operator_type ? (typeLabels[log.operator_type] || log.operator_type) : ''
if (log.operator_type === 'system' && name === '微信支付') {
t = '微信支付'
}
return t ? `操作人:${name}${t}` : `操作人:${name}`
},
onApprove() {
uni.showModal({
title: '确认同意退款?',
content: '将按规则发起微信退款',
success: async (r) => {
if (!r.confirm) return
try {
let res = null
if (this.detailSource === 'operator') {
res = await this.$u.api.operatorAccompanyRefundSave({ id: this.refundDetail.id, status: 1 })
} else {
res = await this.$u.api.staffRefundProcess({ id: this.refundDetail.id, action: 'approve' })
}
if (res === false || parseApiRecord(res) == null) {
return
}
await this.fetchDetail()
const st = Number(this.refundDetail.status)
if (st === 1) {
uni.showToast({ title: '退款成功', icon: 'success' })
} else if (st === 2) {
uni.showToast({ title: '退款失败,请查看处理说明', icon: 'none' })
} else {
uni.showToast({ title: '处理未完成,请稍后重试', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: this.apiErrorMessage(e, '操作失败'), icon: 'none' })
}
}
})
},
onReject() {
uni.showModal({
title: '确认驳回该退款?',
content: '驳回后用户可重新沟通或再次申请',
success: async (r) => {
if (!r.confirm) return
try {
let res = null
if (this.detailSource === 'operator') {
res = await this.$u.api.operatorAccompanyRefundSave({ id: this.refundDetail.id, status: 2 })
} else {
res = await this.$u.api.staffRefundProcess({ id: this.refundDetail.id, action: 'reject' })
}
if (res === false || parseApiRecord(res) == null) {
return
}
await this.fetchDetail()
if (Number(this.refundDetail.status) === 2) {
uni.showToast({ title: '已驳回', icon: 'success' })
}
} catch (e) {
uni.showToast({ title: this.apiErrorMessage(e, '操作失败'), icon: 'none' })
}
}
})
}
}
}
</script>
<style lang="scss" scoped>
.wrap {
min-height: 100vh;
background: #f5f5f5;
}
.b-border {
width: 100%;
height: 30rpx;
border-radius: 0 0 120rpx 120rpx;
background-color: #1479ff;
}
.state-tip {
text-align: center;
color: #999;
padding: 80rpx 30rpx;
font-size: 28rpx;
}
.scroll {
height: calc(100vh - 200rpx);
box-sizing: border-box;
}
.order-info {
background: #fff;
margin: 24rpx 24rpx 0;
border-radius: 16rpx;
padding: 24rpx 28rpx 32rpx;
box-shadow: 0 4rpx 16rpx #e6eaf1;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.info-item {
display: flex;
padding: 16rpx 0;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
&:last-child {
border-bottom: none;
}
.label {
width: 200rpx;
flex-shrink: 0;
font-size: 28rpx;
color: #666;
}
.value {
flex: 1;
font-size: 28rpx;
color: #333;
word-break: break-all;
}
}
.log-entry {
padding: 24rpx 0;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
&:last-child {
border-bottom: none;
}
}
.log-meta {
display: block;
font-size: 24rpx;
color: #999;
margin-bottom: 12rpx;
}
.log-operator {
display: block;
font-size: 24rpx;
color: #666;
margin-bottom: 12rpx;
}
.log-text {
display: block;
font-size: 28rpx;
color: #333;
line-height: 1.5;
}
.action-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 24rpx;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
display: flex;
gap: 24rpx;
align-items: center;
z-index: 100;
.bar-btn-wrap {
flex: 1;
min-width: 0;
}
}
</style>