Browse Source

商户端团餐订单相关

baijunde 3 weeks ago
parent
commit
39e2e87686

+ 14 - 0
api/merchantDish.js

@@ -189,6 +189,20 @@ const api = {
 		return http.post('/groupmeal/gmMerchantApplicationRecord/isApply', {})
 	},
 
+	/** 商户团餐订单详情 */
+	packageOrderDetail(hostOrderId) {
+		return http.get('/groupmeal/merchantPackageOrder/detail', {
+			params: { hostOrderId }
+		})
+	},
+
+	/** 商户端直接退款 */
+	packageOrderDirectRefund(userOrderId) {
+		return http.post('/groupmeal/merchantPackageOrder/directRefund', {}, {
+			params: { userOrderId }
+		})
+	},
+
 	/** 餐盒分页 */
 	mealBoxList(params = {}) {
 		return http.get(`${MEALBOX_BASE}/list`, {

+ 21 - 1
common/router/modules/routes.js

@@ -926,5 +926,25 @@ const routes = [{
 		name: 'merchantDishOrders',
 		meta: { title: '团餐订单' },
 	},
+	{
+		path: '/pages/merchantDish/orders/order-blist',
+		name: 'merchantDishOrderBList',
+		meta: { title: '订单明细' },
+	},
+	{
+		path: '/pages/merchantDish/orders/order-details',
+		name: 'merchantDishOrderDetails',
+		meta: { title: '订单明细' },
+	},
+	{
+		path: '/pages/merchantDish/orders/refund-details',
+		name: 'merchantDishRefundDetails',
+		meta: { title: '退款详情' },
+	},
+	{
+		path: '/pages/merchantDish/orders/refund-record',
+		name: 'merchantDishRefundRecord',
+		meta: { title: '协商记录' },
+	},
 ]
-export default routes
+export default routes

+ 17 - 6
components/list-scroll-view/index.vue

@@ -54,27 +54,38 @@ export default {
 			type: [Number, String],
 			default: 10
 		},
+		downEnabled: {
+			type: Boolean,
+			default: true
+		},
+		upTextVisible: {
+			type: Boolean,
+			default: true
+		},
 	},
 	data() {
 		return {
 			params: {},
 			dataList: [],
-			// 下拉刷新的配置
-			downOption: {
-				
-			},
 			// 是否滚动
 			isScrolling: false,
       		scrollTimer: null
 		};
 	},
 	computed: {
+		downOption() {
+			return {
+				use: this.downEnabled
+			};
+		},
 		// 上拉加载的配置
 		upOption() {
 			let obj = {
 				// noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于5条才显示无更多数据;
 				onScroll: true,
-				textNoMore: '-- 暂无更多数据 --', 
+				showLoadMoreText: this.upTextVisible,
+				textLoading: this.upTextVisible ? '加载中 ...' : '',
+				textNoMore: this.upTextVisible ? '-- 暂无更多数据 --' : '',
 				page: {
 					size: this.pageSize // 每页数据的数量,默认10
 				},
@@ -184,4 +195,4 @@ export default {
 		min-height: 100%;
 	}
 }
-</style>
+</style>

+ 1 - 1
components/mescroll-uni/mescroll-uni.vue

@@ -20,7 +20,7 @@
 				<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
 				<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
 				<view v-if="mescroll.optUp.use && !isDownLoading && !isShowEmpty" class="mescroll-upwarp" :style="{'background-color':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
-          <view v-show="upLoadType===0">
+					<view v-show="upLoadType===0 && mescroll.optUp.showLoadMoreText !== false">
 						<view class="upwarp-tip">上拉加载更多...</view>
 					</view>
 					<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->

+ 25 - 1
pages.json

@@ -696,6 +696,30 @@
 			"style": {
 				"navigationBarTitleText": "团餐订单"
 			}
+		},
+		{
+			"path": "pages/merchantDish/orders/order-blist",
+			"style": {
+				"navigationBarTitleText": "订单明细"
+			}
+		},
+		{
+			"path": "pages/merchantDish/orders/order-details",
+			"style": {
+				"navigationBarTitleText": "订单明细"
+			}
+		},
+		{
+			"path": "pages/merchantDish/orders/refund-details",
+			"style": {
+				"navigationBarTitleText": "退款详情"
+			}
+		},
+		{
+			"path": "pages/merchantDish/orders/refund-record",
+			"style": {
+				"navigationBarTitleText": "协商记录"
+			}
 		}
 	],
 	"globalStyle": {
@@ -757,4 +781,4 @@
 		}]
 	}
 
-}
+}

+ 514 - 19
pages/merchantDish/orders/index.vue

@@ -1,41 +1,536 @@
 <template>
 	<view class="page">
-		<u-navbar title="团餐订单" :autoBack="true" :placeholder="true"></u-navbar>
-		<view class="placeholder">
-			<u-empty text="团餐订单" mode="order"></u-empty>
+		<u-navbar title="团餐订单" :autoBack="true" :placeholder="true" :border-bottom="false">
+			<view slot="right" class="navbar-right" @click="goToDetail">订单明细</view>
+		</u-navbar>
+
+		<view class="search-bar">
+			<u-search
+				v-model="keyword"
+				placeholder="请输入商品名称"
+				bg-color="#f7f8fc"
+				search-icon-color="#9aa1ab"
+				:show-action="true"
+				action-text="搜索"
+				:action-style="actionStyle"
+				@search="onSearch"
+				@custom="onSearch"
+			></u-search>
+		</view>
+
+		<view class="status-tabs">
+			<view
+				v-for="(tab, index) in orderTabs"
+				:key="tab.name"
+				:class="['status-tab', { active: currentTab === index }]"
+				@click="onTabChange(index)"
+			>
+				<text>{{ tab.name }}</text>
+				<image
+					v-if="currentTab === index"
+					class="arrow-down"
+					src="/static/merchantDish/select-tab.png"
+					mode="aspectFit"
+				></image>
+			</view>
+		</view>
+
+		<view class="filter-row" v-if="currentTab !== 2">
+			<view class="filter-item" v-if="currentTab === 0" @click="openDatePicker">
+				<text>{{ dateLabel }}</text>
+				<u-icon name="arrow-down" size="22" color="#8b929c"></u-icon>
+			</view>
+			<view class="filter-item meal-filter" @click="openMealPicker">
+				<text>{{ mealTypeLabel }}</text>
+				<u-icon name="arrow-down" size="22" color="#8b929c"></u-icon>
+			</view>
+			<view class="total">合计:<text>{{ total }}</text>份</view>
+		</view>
+
+		<view class="list-wrap">
+			<ListScrollView
+				ref="listScroll"
+				:api="api"
+				:init="initParams"
+				:page-size="20"
+				:down-enabled="false"
+				:up-text-visible="false"
+				empty-text="暂无订单"
+				@afterFetch="afterFetch"
+			>
+				<template v-slot:list="{ data }">
+					<view class="order-list">
+						<view class="order-item" v-for="item in data" :key="item.hostOrderId || item.id" @click="openOrder(item)">
+							<view class="order-summary">
+								<view class="summary-row date-row">
+									<view class="summary-label">
+										<u-icon name="clock" color="#8b929c" size="27"></u-icon>
+										<text>预约日期:{{ getReserveDate(item) }}</text>
+									</view>
+									<text :class="['meal-tag', `meal-tag-${getMealCategory(item)}`]">{{ getMealLabel(item) }}</text>
+								</view>
+								<view class="summary-row leader-row">
+									<view class="summary-label">
+										<u-icon name="account" color="#8b929c" size="27"></u-icon>
+										<text>{{ item.headName || item.leaderName || '团长姓名' }}</text>
+									</view>
+									<view class="order-count">合计:<text>{{ item.buyNumber || 0 }}</text>份</view>
+								</view>
+								<view class="summary-label address-line">
+									<u-icon name="map" color="#8b929c" size="27"></u-icon>
+									<text>{{ item.address || '暂无地址' }}</text>
+								</view>
+								<view class="region">{{ item.region || '' }}</view>
+							</view>
+							<view class="product-list">
+								<view
+									class="product-item"
+									v-for="(product, productIndex) in (item.merchantPackageOrderDTOS || [])"
+									:key="product.merchantOrderIds || product.id || productIndex"
+								>
+									<u-image
+										width="80rpx"
+										height="80rpx"
+										border-radius="8rpx"
+										:src="getProductImage(product.productImage)"
+										mode="aspectFill"
+									></u-image>
+									<view class="product-info">
+										<text class="product-name">{{ product.packageName  || '' }}</text>
+										<text class="product-desc">{{ product.boxName || '默认餐盒'}}</text>
+									</view>
+									<text class="product-quantity">{{ product.numberUserCount || product.packageNumber || 0 }}份</text>
+								</view>
+							</view>
+						</view>
+					</view>
+				</template>
+			</ListScrollView>
 		</view>
+
+		<u-action-sheet
+			:list="mealTypeOptions"
+			v-model="mealPickerVisible"
+			@click="onMealPick"
+		></u-action-sheet>
+		<rangTime
+			ref="rangeTime"
+			:mode="1"
+			title="选择日期"
+			:default-start-date="startDate"
+			:default-end-date="endDate"
+			@onSubmit="onDateRangeConfirm"
+		/>
 	</view>
 </template>
 
 <script>
+import ListScrollView from '@/components/list-scroll-view/index.vue'
+import rangTime from '@/components/dengrq-datetime-picker/dateSelector/index.vue'
+
+const MEAL_TYPE_OPTIONS = [
+	{ text: '全部', value: '' },
+	{ text: '早餐', value: 1 },
+	{ text: '午餐', value: 2 },
+	{ text: '晚餐', value: 3 }
+]
+
 export default {
+	components: {
+		ListScrollView,
+		rangTime
+	},
 	data() {
 		return {
-			
+			api: {
+				url: '/groupmeal/merchantPackageOrder/pageList',
+				method: 'POST'
+			},
+			keyword: '',
+			currentTab: 1,
+			orderTabs: [
+				{ name: '预约单' },
+				{ name: '待出餐' },
+				{ name: '已出餐' }
+			],
+			mealType: '',
+			startDate: '',
+			endDate: '',
+			total: 0,
+			mealPickerVisible: false,
+			mealTypeOptions: MEAL_TYPE_OPTIONS,
+			actionStyle: {
+				width: 'auto',
+				color: '#333',
+				padding: '0 20rpx',
+				borderLeft: '1rpx solid #e5e7eb'
+			}
 		}
 	},
-	onShow() {
-		
+	computed: {
+		initParams() {
+			const params = {
+				orderStatus: this.currentTab
+			}
+			if (this.currentTab === 0) params.isReservation = 1
+			if (this.keyword) params.queryName = this.keyword
+			if (this.mealType) params.category = this.mealType
+			if (this.currentTab === 0 && this.startDate && this.endDate) {
+				params.reservationDateStart = this.startDate
+				params.reservationDateEnd = this.endDate
+			}
+			return params
+		},
+		mealTypeLabel() {
+			return (MEAL_TYPE_OPTIONS.find((item) => item.value === this.mealType) || MEAL_TYPE_OPTIONS[0]).text
+		},
+		dateLabel() {
+			if (!this.startDate || !this.endDate) return '选择日期'
+			return `${this.formatDateLabel(this.startDate)}-${this.formatDateLabel(this.endDate)}`
+		},
 	},
 	methods: {
-		
-	},
-};
+		goToDetail() {
+			uni.navigateTo({ url: '/pages/merchantDish/orders/order-blist' })
+		},
+		onSearch(value) {
+			this.keyword = typeof value === 'string' ? value : this.keyword
+			this.reloadList()
+		},
+		onTabChange(index) {
+			this.currentTab = index
+			this.reloadList()
+		},
+		openMealPicker() {
+			this.mealPickerVisible = true
+		},
+		onMealPick(event) {
+			const value = event && typeof event === 'object'
+				? event.value
+				: (MEAL_TYPE_OPTIONS[event] || {}).value
+			if (value === undefined) return
+			this.mealType = value
+			this.reloadList()
+		},
+		openDatePicker() {
+			const picker = this.$refs.rangeTime
+			if (!picker) return
+			picker.startDate = this.startDate
+			picker.endDate = this.endDate
+			picker.open()
+		},
+		onDateRangeConfirm(value) {
+			this.startDate = value.startDate
+			this.endDate = value.endDate
+			this.reloadList()
+		},
+		formatDateLabel(value) {
+			const parts = String(value || '').slice(0, 10).split('-')
+			if (parts.length !== 3) return value
+			return `${Number(parts[1])}.${Number(parts[2])}`
+		},
+		getOrderId(item) {
+			return item && (item.hostOrderId || item.id || item.orderId)
+		},
+		openOrder(item) {
+			const orderId = this.getOrderId(item)
+			if (!orderId) return
+			uni.navigateTo({
+				url: `/pages/merchantDish/orders/order-details?hostOrderId=${encodeURIComponent(orderId)}`
+			})
+		},
+		getReserveDate(item) {
+			const value = item.reserveDate || item.appointmentDate || item.mealDate || item.deliveryDate || item.createTime || ''
+			return String(value).slice(0, 10).replace(/-/g, '.') || '--'
+		},
+		getMealLabel(item) {
+			const products = item.merchantPackageOrderDTOS || []
+			const firstProduct = products[0] || {}
+			const text = item.mealTypeName || item.categoryName || item.mealName || firstProduct.mealTypeName || firstProduct.categoryName
+			if (text) return text
+			return ({ 1: '早餐', 2: '午餐', 3: '晚餐' })[this.getMealCategory(item)] || '团餐'
+		},
+		getMealCategory(item) {
+			const products = item.merchantPackageOrderDTOS || []
+			const firstProduct = products[0] || {}
+			return Number(item.category || item.mealType || firstProduct.category || firstProduct.mealType) || 0
+		},
+		getProductImage(value) {
+			return String(value || '').split(',').filter(Boolean)[0] || ''
+		},
+		reloadList() {
+			this.$nextTick(() => {
+				const list = this.$refs.listScroll
+				if (list && list.mescroll) list.reloadList(this.initParams)
+			})
+		},
+		afterFetch(result) {
+			if (result && result.totalBuyNumber != null) {
+				this.total = Number(result.totalBuyNumber) || 0
+				return
+			}
+			const summary = (result && result.summary) || {}
+			const summaryTotal = summary.buyNumber || summary.copies || summary.totalCopies
+			if (summaryTotal != null) {
+				this.total = Number(summaryTotal) || 0
+				return
+			}
+			this.$nextTick(() => {
+				const list = this.$refs.listScroll
+				const records = (list && list.dataList) || []
+				this.total = records.reduce((sum, item) => sum + (Number(item.buyNumber) || 0), 0)
+			})
+		}
+	}
+}
 </script>
 
 <style lang="scss" scoped>
 .page {
-	min-height: 100vh;
+	display: flex;
+	flex-direction: column;
+	height: 100vh;
+	box-sizing: border-box;
+	background: #fff;
+	padding: 0 32rpx;
+	overflow: hidden;
+}
+
+.search-bar {
+	width: 100%;
+	height: 64rpx;
+	box-sizing: border-box;
+	padding: 12rpx 20rpx;
+	background: #F7F8FC;
+	margin-bottom: 12rpx;
+}
+
+.navbar-right {
+	padding: 0 24rpx;
+	color: #333;
+	font-size: 28rpx;
+}
+
+.filter-row {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	height: 76rpx;
 	background: #f7f8fc;
+	color: #7d858f;
+	font-size: 26rpx;
 }
-.placeholder {
-	padding-top: 200rpx;
-	.desc {
-		text-align: center;
-		color: #999;
-		font-size: 24rpx;
-		margin-top: 20rpx;
-		padding: 0 60rpx;
-	}
+
+.filter-item {
+	display: flex;
+	align-items: center;
+	gap: 6rpx;
 }
+
+.meal-filter {
+	justify-content: center;
+}
+
+.total {
+	white-space: nowrap;
+}
+
+.total text {
+	color: #1677ff;
+}
+
+.list-wrap {
+	flex: 1;
+	height: 0;
+	margin: 0 -32rpx;
+	// padding: 20rpx 24rpx 0;
+	background: #f7f8fc;
+	overflow: hidden;
+}
+
+.order-list {
+	padding-bottom: 24rpx;
+}
+
+.order-item {
+	margin-bottom: 20rpx;
+	background: #fff;
+	border-radius: 8rpx;
+	overflow: hidden;
+	padding: 24rpx;
+	padding-top: 0;
+}
+
+.order-summary {
+	position: relative;
+	padding: 20rpx 24rpx 16rpx;
+	color: #7f8792;
+	font-size: 26rpx;
+	border-bottom: 1px solid #dddddd;
+}
+
+.summary-row {
+	display: flex;
+	align-items: center;
+}
+
+.date-row {
+	padding-right: 92rpx;
+}
+
+.leader-row {
+	gap: 50rpx;
+	margin-top: 10rpx;
+}
+
+.summary-label {
+	font-size: 28rpx;
+	display: flex;
+	align-items: center;
+	gap: 8rpx;
+	min-width: 0;
+}
+
+.summary-label text {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.meal-tag {
+	position: absolute;
+	top: 0;
+	right: 0;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 88rpx;
+	height: 36rpx;
+	box-sizing: border-box;
+	border-radius: 0 12rpx 0 12rpx;
+	background: linear-gradient(89deg, #1aeeae 0%, #0bda84 100%);
+	color: #fff;
+	font-size: 22rpx;
+	text-align: center;
+}
+
+.meal-tag-2 {
+	background: linear-gradient(89deg, #ffbf1e 0%, #ff781f 100%);
+}
+
+.meal-tag-3 {
+	background: linear-gradient(90deg, #10abfe 0%, #2260f6 100%);
+}
+
+.order-count {
+	flex-shrink: 0;
+	color: #7f8792;
+	font-size: 24rpx;
+}
+
+.order-count text {
+	color: #1677ff;
+}
+
+.address-line {
+	margin-top: 10rpx;
+}
+
+.region {
+	margin-top: 8rpx;
+	color: #8a8f98;
+	font-size: 24rpx;
+}
+
+.product-list {
+	margin: 0 24rpx;
+	border-top: 1rpx solid #e5e7eb;
+}
+
+.product-item {
+	display: flex;
+	align-items: end;
+	gap: 18rpx;
+	min-height: 80rpx;
+	box-sizing: border-box;
+	border-bottom: 1rpx solid #f0f1f3;
+	margin-top: 20rpx;
+}
+
+.product-item:last-child {
+	border-bottom: 0;
+}
+
+.product-info {
+	min-height: 80rpx;
+	height: 100%;
+	display: flex;
+	flex: 1;
+	min-width: 0;
+	flex-direction: column;
+	justify-content: space-between;
+	gap: 8rpx;
+}
+
+.product-name,
+.product-desc {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.product-name {
+	color: #303133;
+	font-size: 26rpx;
+	font-weight: 600;
+}
+
+.product-desc {
+	color: #8a91a8;
+	font-size: 22rpx;
+}
+
+.product-quantity {
+	flex-shrink: 0;
+	align-self: flex-end;
+	margin-bottom: 4rpx;
+	color: #333;
+	font-size: 24rpx;
+}
+
+.status-tabs {
+	display: flex;
+	justify-content: space-between;
+	height: 92rpx;
+	border-bottom: 1rpx solid #f0f1f3;
+}
+
+.status-tab {
+	position: relative;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 140rpx;
+	color: #8a8f98;
+	font-size: 28rpx;
+}
+
+.status-tab.active {
+	color: #1677ff;
+	font-weight: 600;
+}
+
+.arrow-down {
+	position: absolute;
+	bottom: 8rpx;
+	left: 50%;
+	width: 48rpx;
+	height: 8rpx;
+	transform: translateX(-50%);
+}
+
+::v-deep .u-search {
+	border-radius: 4rpx;
+}
+
 </style>

+ 781 - 0
pages/merchantDish/orders/order-blist.vue

@@ -0,0 +1,781 @@
+<template>
+	<view class="page">
+		<u-navbar title="订单明细" :autoBack="true" :placeholder="true" :border-bottom="true"></u-navbar>
+
+		<view class="search-bar">
+			<u-search
+				v-model="keyword"
+				placeholder="请输入商品名称"
+				bg-color="#f7f8fc"
+				search-icon-color="#c9cdd4"
+				placeholder-color="#c9cdd4"
+				:show-action="true"
+				action-text="搜索"
+				:action-style="actionStyle"
+				@search="onSearch"
+				@custom="onSearch"
+			></u-search>
+		</view>
+
+		<view class="status-tabs">
+			<view
+				v-for="(tab, index) in statusTabs"
+				:key="tab.value"
+				:class="['status-tab', { active: currentTab === index }]"
+				@click="onTabChange(index)"
+			>
+				<text>{{ tab.name }}</text>
+				<image
+					v-if="currentTab === index"
+					class="arrow-down"
+					src="/static/merchantDish/select-tab.png"
+					mode="aspectFit"
+				></image>
+			</view>
+		</view>
+
+		<view class="date-filter">
+			<view class="date-trigger" @click="openDatePicker">
+				<text>{{ dateLabel }}</text>
+				<u-icon name="arrow-down" size="20" color="#86909c"></u-icon>
+			</view>
+			<view v-if="startDate && endDate" class="date-clear" @click.stop="clearDate">
+				<u-icon name="close-circle" size="24" color="#86909c"></u-icon>
+			</view>
+		</view>
+
+		<view class="list-wrap">
+			<ListScrollView
+				ref="listScroll"
+				:api="api"
+				:init="initParams"
+				:page-size="10"
+				:down-enabled="false"
+				:up-text-visible="false"
+				empty-text="暂无订单明细"
+			>
+				<template v-slot:list="{ data }">
+					<view class="detail-list">
+						<view v-if="currentTab === 2" class="after-sale-list">
+							<view
+								v-for="entry in getAfterSaleEntries(data)"
+								:key="entry.key"
+								class="detail-card after-sale-card"
+								@click="openAfterSaleOrder(entry.group, entry.item)"
+							>
+								<view class="after-sale-header">
+									<view class="address-title">
+										<u-icon name="map" color="#86909c" size="30"></u-icon>
+										<text>{{ entry.group.deliveryAddress || entry.group.address || entry.item.deliveryAddress || entry.item.address || '暂无地址' }}</text>
+									</view>
+									<text :class="['after-sale-status', getAfterSaleStatusClass(entry)]">
+										{{ getAfterSaleStatusText(entry) }}
+									</text>
+								</view>
+								<text class="region">{{ entry.group.region || entry.item.region || '' }}</text>
+								<view class="order-separator"></view>
+
+								<view class="pickup-row after-sale-pickup">
+									<text class="pickup-number">取餐号:{{ getMealNumber(entry.item) }}</text>
+									<view class="customer-info">
+										<text>{{ getCustomerName(entry.item, entry.group) }}</text>
+										<text>{{ getCustomerPhone(entry.item, entry.group) }}</text>
+									</view>
+								</view>
+
+								<view class="product-row after-sale-product">
+									<u-image
+										width="120rpx"
+										height="120rpx"
+										border-radius="8rpx"
+										:src="getProductImage(entry.item.productImage)"
+										mode="aspectFill"
+									></u-image>
+									<view class="product-info">
+										<text class="product-name">{{ entry.item.packageName || entry.item.dishName || '团餐套餐' }}</text>
+										<text class="product-desc">{{ getDishDescription(entry.item) }}</text>
+										<view class="product-bottom">
+											<text class="quantity">{{ getItemCopies(entry.item) }}份</text>
+											<view class="price-wrap after-sale-price">
+												<text class="price-label">成团价:</text>
+												<text class="price-unit">¥</text>
+												<text class="price">{{ formatMoney(getItemPrice(entry.item)) }}</text>
+											</view>
+										</view>
+									</view>
+								</view>
+							</view>
+						</view>
+
+						<template v-else>
+						<view
+							v-for="(group, groupIndex) in data"
+							:key="group.hostOrderId || group.id || groupIndex"
+							class="detail-card"
+						>
+							<view :class="['group-header', { 'completed-group-header': currentTab === 1 }]">
+								<view v-if="currentTab === 0" class="group-time">
+									<u-icon name="clock" color="#4e5969" size="28"></u-icon>
+									<text>成团时间:{{ getGroupTime(group) }}</text>
+								</view>
+								<text class="copies">合计:<text>{{ getGroupCopies(group) }}</text>份</text>
+							</view>
+
+							<view class="address-row">
+								<view class="address-title">
+									<u-icon name="map" color="#4e5969" size="30"></u-icon>
+									<text>{{ group.deliveryAddress || group.address || '暂无地址' }}</text>
+								</view>
+							</view>
+							<text class="region">{{ group.region || '' }}</text>
+
+							<view class="order-separator"></view>
+
+							<view
+								v-for="(item, itemIndex) in getGroupItems(group)"
+								:key="getItemKey(group, item, itemIndex, groupIndex)"
+								class="detail-item"
+								@click="openOrder(group, item)"
+							>
+								<view class="pickup-row">
+									<text class="pickup-number">取餐号:{{ getMealNumber(item) }}</text>
+									<text class="item-status">{{ getItemStatusText(item) }}</text>
+								</view>
+
+								<view class="product-row">
+									<u-image
+										width="120rpx"
+										height="120rpx"
+										border-radius="8rpx"
+										:src="getProductImage(item.productImage)"
+										mode="aspectFill"
+									></u-image>
+									<view class="product-info">
+										<text class="product-name">{{ item.packageName || item.dishName || '团餐套餐' }}</text>
+										<text class="box-fee">餐盒费:{{ formatMoney(getItemContainerFee(item)) }}元</text>
+										<view class="product-bottom">
+											<text class="quantity">{{ getItemCopies(item) }}份</text>
+											<view class="price-wrap">
+												<text class="price-label">成团价:</text>
+												<text class="price-unit">¥</text>
+												<text class="price">{{ formatMoney(getItemPrice(item)) }}</text>
+											</view>
+										</view>
+									</view>
+								</view>
+							</view>
+
+							<view class="card-footer">
+								<view class="footer-amount">
+									<text>合计餐盒费:</text>
+									<text class="amount-unit">¥</text>
+									<text class="amount-value box-total">{{ formatMoney(getGroupContainerFee(group)) }}</text>
+								</view>
+								<view class="footer-amount">
+									<text>合计金额:</text>
+									<text class="amount-unit">¥</text>
+									<text class="amount-value total-amount">{{ formatMoney(getGroupAmount(group)) }}</text>
+								</view>
+							</view>
+						</view>
+						</template>
+					</view>
+				</template>
+			</ListScrollView>
+		</view>
+
+		<rangTime
+			ref="rangeTime"
+			:mode="1"
+			title="选择日期"
+			:default-start-date="startDate"
+			:default-end-date="endDate"
+			@onSubmit="onDateRangeConfirm"
+		/>
+	</view>
+</template>
+
+<script>
+import ListScrollView from '@/components/list-scroll-view/index.vue'
+import rangTime from '@/components/dengrq-datetime-picker/dateSelector/index.vue'
+
+export default {
+	components: {
+		ListScrollView,
+		rangTime
+	},
+	data() {
+		return {
+			api: {
+				url: '/groupmeal/merchantPackageOrder/getOrderDteails',
+				method: 'POST'
+			},
+			keyword: '',
+			currentTab: 0,
+			statusTabs: [
+				{ name: '待出餐', value: 1 },
+				{ name: '已出餐', value: 2 },
+				{ name: '售后', value: 3 }
+			],
+			startDate: '',
+			endDate: '',
+			listReady: false,
+			actionStyle: {
+				width: 'auto',
+				color: '#333',
+				fontSize: '26rpx',
+				padding: '0 20rpx',
+				borderLeft: '1rpx solid #e5e7eb'
+			}
+		}
+	},
+	computed: {
+		initParams() {
+			const params = {
+				orderStatus: this.statusTabs[this.currentTab].value
+			}
+			if (this.keyword) params.queryName = this.keyword
+			if (this.startDate && this.endDate) {
+				params.completeStartDate = this.startDate
+				params.completeEndDate = this.endDate
+			}
+			return params
+		},
+		dateLabel() {
+			if (!this.startDate || !this.endDate) return '选择日期'
+			return `${this.formatDateLabel(this.startDate)}-${this.formatDateLabel(this.endDate)}`
+		}
+	},
+	onShow() {
+		if (this.listReady) {
+			this.reloadList()
+		} else {
+			this.listReady = true
+		}
+	},
+	methods: {
+		onSearch(value) {
+			this.keyword = typeof value === 'string' ? value.trim() : this.keyword.trim()
+			this.reloadList()
+		},
+		onTabChange(index) {
+			if (this.currentTab === index) return
+			this.currentTab = index
+			this.reloadList()
+		},
+		openDatePicker() {
+			const picker = this.$refs.rangeTime
+			if (!picker) return
+			picker.startDate = this.startDate
+			picker.endDate = this.endDate
+			picker.open()
+		},
+		onDateRangeConfirm(value) {
+			this.startDate = value.startDate
+			this.endDate = value.endDate
+			this.reloadList()
+		},
+		clearDate() {
+			this.startDate = ''
+			this.endDate = ''
+			this.reloadList()
+		},
+		reloadList() {
+			this.$nextTick(() => {
+				const list = this.$refs.listScroll
+				if (list) list.reloadList(this.initParams)
+			})
+		},
+		getGroupItems(group) {
+			return group.orderChildren || group.merchantPackageOrderDTOS || group.items || []
+		},
+		getAfterSaleEntries(data) {
+			return (data || []).reduce((entries, group, groupIndex) => {
+				const items = this.getGroupItems(group)
+				if (!items.length) {
+					entries.push({
+						group,
+						item: group,
+						key: this.getItemKey(group, group, 0, groupIndex)
+					})
+					return entries
+				}
+				items.forEach((item, itemIndex) => {
+					entries.push({
+						group,
+						item,
+						key: this.getItemKey(group, item, itemIndex, groupIndex)
+					})
+				})
+				return entries
+			}, [])
+		},
+		getGroupKey(group, groupIndex) {
+			return group.hostOrderId || group.id || `group-${groupIndex}`
+		},
+		getItemKey(group, item, itemIndex, groupIndex) {
+			return `${this.getGroupKey(group, groupIndex)}-${item.userOrderId || item.merchantOrderPackageId || item.id || itemIndex}`
+		},
+		getGroupTime(group) {
+			const value = group.groupTime || group.groupMealTime || group.groupSuccessTime || group.groupingTime || group.clusterTime || group.createTime || group.orderTime || group.payTime || ''
+			if (!value) return '--'
+			const text = String(value).trim()
+			const match = text.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})(?:[ T](\d{1,2}):(\d{2}))?/)
+			if (!match) return text.length > 16 ? text.slice(0, 16) : text
+			const date = `${match[1]}.${Number(match[2])}.${Number(match[3])}`
+			return match[4] ? `${date} ${String(match[4]).padStart(2, '0')}:${match[5]}` : date
+		},
+		getMealNumber(item) {
+			return item.mealNumber || item.pickupNumber || item.takeMealNumber || item.mealCode || '--'
+		},
+		getCustomerName(item, group = {}) {
+			return item.linkName || item.userName || item.customerName || item.consignee || group.linkName || group.userName || '--'
+		},
+		getCustomerPhone(item, group = {}) {
+			return item.userPhone || group.userPhone || '--'
+		},
+		getDishDescription(item) {
+			return item.dishName || item.packageContent || item.dishNames || item.boxName || '暂无菜品信息'
+		},
+		getGroupCopies(group) {
+			return group.totalCopies || group.copies || group.buyNumber || this.getGroupItems(group).reduce((sum, item) => sum + this.getItemCopies(item), 0)
+		},
+		getItemCopies(item) {
+			return Number(item.packageNumber || item.numberUserCount || item.copies || 0)
+		},
+		getItemPrice(item) {
+			if (item.groupPrice != null) return item.groupPrice
+			if (item.price != null) return item.price
+			return item.packagePrice || 0
+		},
+		getItemContainerFee(item) {
+			if (item.containerFee != null) return item.containerFee
+			if (item.boxFee != null) return item.boxFee
+			return Number(item.boxPrice || 0) * this.getItemCopies(item)
+		},
+		getGroupContainerFee(group) {
+			if (group.containerFeeTotal != null) return group.containerFeeTotal
+			if (group.boxFeeTotal != null) return group.boxFeeTotal
+			return this.getGroupItems(group).reduce((sum, item) => sum + Number(this.getItemContainerFee(item) || 0), 0)
+		},
+		getGroupAmount(group) {
+			if (group.amountTotal != null) return group.amountTotal
+			if (group.copiesAmount != null) return group.copiesAmount
+			return group.totalAmount != null ? group.totalAmount : 0
+		},
+		getItemStatusText(item) {
+			const text = item.orderStatusName || item.statusName || ''
+			if (text.includes('已出餐')) return '已出餐'
+			if (text.includes('未出餐') || text.includes('待出餐') || text.includes('成团')) return '未出餐'
+			const status = Number(item.orderStatus)
+			if (Number.isFinite(status)) return status >= 3 && status !== 6 ? '已出餐' : '未出餐'
+			return this.currentTab === 1 ? '已出餐' : '未出餐'
+		},
+		getAfterSaleStatusText(entry) {
+			const item = entry.item || {}
+			const group = entry.group || {}
+			const text = item.refundStatusName || item.refundTypeName || item.afterSaleStatusName || group.refundStatusName || group.refundTypeName || group.afterSaleStatusName
+			if (text) {
+				if (text.includes('成功')) return '退款成功'
+				if (text.includes('拒绝')) return '拒绝退款'
+				if (text.includes('失败')) return '退款失败'
+				if (text.includes('申请') || text.includes('处理中') || text.includes('退款中')) return '售后申请中'
+				return text
+			}
+			const type = item.refundType != null
+				? item.refundType
+				: (item.refundStatus != null ? item.refundStatus : (group.refundType != null ? group.refundType : group.refundStatus))
+			return ({ 0: '售后申请中', 1: '退款成功', 2: '售后申请中', 3: '退款失败', 4: '拒绝退款' })[type] || '售后申请中'
+		},
+		getAfterSaleStatusClass(entry) {
+			const text = this.getAfterSaleStatusText(entry)
+			if (text.includes('成功')) return 'success'
+			if (text.includes('拒绝') || text.includes('失败')) return 'danger'
+			return 'pending'
+		},
+		formatDateLabel(value) {
+			const parts = String(value || '').slice(0, 10).split('-')
+			if (parts.length !== 3) return value
+			return `${Number(parts[1])}.${Number(parts[2])}`
+		},
+		formatMoney(value) {
+			const number = Number(value)
+			if (!Number.isFinite(number)) return '0'
+			return number.toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1')
+		},
+		getProductImage(value) {
+			const image = String(value || '').split(',').filter(Boolean)[0] || ''
+			if (!image || /^https?:\/\//.test(image)) return image
+			return this.$config && this.$config.apiUrl ? `${this.$config.apiUrl}/${image}` : image
+		},
+		openOrder(group, item) {
+			const hostOrderId = group.hostOrderId || item.hostOrderId
+			if (!hostOrderId) return
+			uni.navigateTo({
+				url: `/pages/merchantDish/orders/order-details?hostOrderId=${encodeURIComponent(hostOrderId)}`
+			})
+		},
+		openAfterSaleOrder(group, item) {
+			const params = {
+				packageId: item.packageId || group.packageId,
+				userOrderId: item.userOrderId || group.userOrderId,
+				hostOrderId: item.hostOrderId || group.hostOrderId
+			}
+			if (!params.hostOrderId) {
+				uni.showToast({ title: '缺少订单信息', icon: 'none' })
+				return
+			}
+			const query = this.$u.queryParams(params)
+			uni.navigateTo({
+				url: `/pages/merchantDish/orders/refund-details${query}`
+			})
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+	display: flex;
+	flex-direction: column;
+	height: 100vh;
+	box-sizing: border-box;
+	background: #f7f8fc;
+	overflow: hidden;
+}
+
+.search-bar {
+	height: 88rpx;
+	box-sizing: border-box;
+	padding: 10rpx 32rpx;
+	background: #fff;
+}
+
+.status-tabs {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	height: 88rpx;
+	box-sizing: border-box;
+	padding: 0 40rpx;
+	background: #fff;
+}
+
+.status-tab {
+	position: relative;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 120rpx;
+	height: 100%;
+	color: #86909c;
+	font-size: 30rpx;
+}
+
+.status-tab.active {
+	color: #1c7cf9;
+	font-weight: 500;
+}
+
+.arrow-down {
+	position: absolute;
+	bottom: 8rpx;
+	left: 50%;
+	width: 48rpx;
+	height: 8rpx;
+	transform: translateX(-50%);
+}
+
+.date-filter {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	height: 80rpx;
+	color: #86909c;
+	font-size: 28rpx;
+}
+
+.date-trigger {
+	display: flex;
+	align-items: center;
+	gap: 6rpx;
+}
+
+.date-clear {
+	display: flex;
+	align-items: center;
+	padding: 12rpx 4rpx 12rpx 10rpx;
+}
+
+.list-wrap {
+	flex: 1;
+	height: 0;
+	overflow: hidden;
+}
+
+.detail-list {
+	box-sizing: border-box;
+	padding: 0 32rpx 32rpx;
+}
+
+.detail-card {
+	box-sizing: border-box;
+	margin-bottom: 20rpx;
+	padding: 24rpx;
+	border-radius: 16rpx;
+	background: #fff;
+}
+
+.group-header,
+.after-sale-header,
+.address-row,
+.pickup-row,
+.product-row,
+.product-bottom,
+.card-footer,
+.footer-amount {
+	display: flex;
+	align-items: center;
+}
+
+.group-header {
+	justify-content: space-between;
+	min-width: 0;
+}
+
+.completed-group-header {
+	justify-content: flex-end;
+}
+
+.after-sale-header {
+	justify-content: space-between;
+	gap: 16rpx;
+	min-width: 0;
+}
+
+.after-sale-header .address-title {
+	flex: 1;
+}
+
+.after-sale-status {
+	flex-shrink: 0;
+	font-size: 28rpx;
+	line-height: 28rpx;
+	white-space: nowrap;
+}
+
+.after-sale-status.pending {
+	color: #f8b328;
+}
+
+.after-sale-status.success {
+	color: #39cf34;
+}
+
+.after-sale-status.danger {
+	color: #f53f3f;
+}
+
+.group-time {
+	display: flex;
+	align-items: center;
+	gap: 8rpx;
+	min-width: 0;
+	color: #4e5969;
+	font-size: 28rpx;
+}
+
+.group-time text {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.copies {
+	flex-shrink: 0;
+	margin-left: 12rpx;
+	color: #999;
+	font-size: 26rpx;
+}
+
+.copies text {
+	color: #388bf9;
+}
+
+.address-row {
+	justify-content: space-between;
+	margin-top: 10rpx;
+}
+
+.address-title {
+	display: flex;
+	align-items: center;
+	gap: 10rpx;
+	min-width: 0;
+	color: #1d2129;
+	font-size: 30rpx;
+	font-weight: 600;
+}
+
+.address-title text {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.region {
+	display: block;
+	margin-top: 10rpx;
+	color: #666;
+	font-size: 26rpx;
+}
+
+.order-separator {
+	height: 1rpx;
+	margin-top: 20rpx;
+	background: #eee;
+}
+
+.detail-item {
+	padding: 16rpx 0 24rpx;
+	border-bottom: 1rpx solid #eee;
+}
+
+.pickup-row {
+	justify-content: space-between;
+	box-sizing: border-box;
+	height: 64rpx;
+	padding: 10rpx;
+	border-radius: 8rpx;
+	background: #eff6ff;
+}
+
+.after-sale-pickup {
+	margin-top: 16rpx;
+}
+
+.pickup-number {
+	color: #333;
+	font-size: 30rpx;
+}
+
+.item-status {
+	color: #86909c;
+	font-size: 28rpx;
+}
+
+.customer-info {
+	display: flex;
+	align-items: center;
+	gap: 16rpx;
+	min-width: 0;
+	color: #333;
+	font-size: 30rpx;
+}
+
+.customer-info text {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.product-row {
+	align-items: stretch;
+	gap: 12rpx;
+	margin-top: 16rpx;
+}
+
+.product-info {
+	display: flex;
+	flex: 1;
+	min-width: 0;
+	flex-direction: column;
+	justify-content: space-between;
+}
+
+.product-name {
+	overflow: hidden;
+	color: #1d2129;
+	font-size: 28rpx;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.box-fee,
+.product-desc,
+.quantity {
+	color: #86909c;
+	font-size: 24rpx;
+}
+
+.product-desc {
+	display: block;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.product-bottom {
+	justify-content: space-between;
+}
+
+.price-wrap {
+	display: flex;
+	align-items: baseline;
+	color: #1d2129;
+}
+
+.price-label {
+	margin-right: 4rpx;
+	color: #4e5969;
+	font-size: 24rpx;
+}
+
+.price-unit {
+	font-size: 24rpx;
+}
+
+.price {
+	font-size: 32rpx;
+}
+
+.after-sale-price .price-unit,
+.after-sale-price .price {
+	color: #ff004e;
+}
+
+.card-footer {
+	justify-content: space-between;
+	padding-top: 20rpx;
+	color: #86909c;
+	font-size: 26rpx;
+}
+
+.footer-amount {
+	align-items: baseline;
+	white-space: nowrap;
+}
+
+.amount-unit,
+.amount-value {
+	color: #ff004e;
+}
+
+.amount-unit {
+	margin-left: 8rpx;
+	font-size: 24rpx;
+}
+
+.amount-value {
+	font-size: 32rpx;
+}
+
+.total-amount {
+	font-size: 40rpx;
+}
+
+::v-deep .u-search {
+	height: 64rpx;
+	border-radius: 8rpx;
+}
+</style>

+ 564 - 0
pages/merchantDish/orders/order-details.vue

@@ -0,0 +1,564 @@
+<template>
+	<view class="page">
+		<u-navbar title="订单明细" :autoBack="true" :placeholder="true" :border-bottom="false"></u-navbar>
+
+		<view v-if="loading" class="loading">加载中...</view>
+		<view v-else-if="!detail.hostOrderId" class="empty">暂无订单详情</view>
+		<scroll-view v-else class="content" scroll-y>
+			<view class="detail-card">
+				<view class="location-info">
+					<view class="location-top">
+						<view class="location-title">
+							<u-icon name="map" color="#1d2129" size="30"></u-icon>
+							<text>{{ detail.deliveryAddress || '暂无地址' }}</text>
+						</view>
+						<text v-if="statusText" class="status-badge">{{ statusText }}</text>
+						<text class="copies">合计:<text class="copies-number">{{ detail.totalCopies || 0 }}</text>份</text>
+					</view>
+					<text class="location-region">{{ detail.region || '' }}</text>
+				</view>
+
+				<view
+					v-for="(item, index) in items"
+					:key="item.merchantOrderPackageId || index"
+					class="detail-item"
+				>
+					<view
+						class="select-circle"
+						:class="{ checked: isSelected(item, index) }"
+						@click.stop="toggleItem(item, index)"
+					>
+						<u-icon v-if="isSelected(item, index)" name="checkmark" color="#fff" size="22"></u-icon>
+					</view>
+					<view class="item-content">
+						<view class="contact-row">
+							<view class="contact-info">
+								<text>{{ item.linkName || '--' }}</text>
+								<text>{{ item.maskedPhone || '--' }}</text>
+							</view>
+							<image
+								class="call-icon"
+								src="/static/merchantDish/call.png"
+								mode="aspectFit"
+								@click.stop="makePhoneCall(item.maskedPhone)"
+							></image>
+						</view>
+						<view class="product-row">
+							<u-image
+								width="120rpx"
+								height="120rpx"
+								border-radius="12rpx"
+								:src="getProductImage(item.productImage)"
+								mode="aspectFill"
+							></u-image>
+							<view class="product-info">
+								<text class="product-name">{{ item.packageName || '团餐套餐' }}</text>
+								<text class="product-desc">{{ item.dishName || '暂无菜品信息' }}</text>
+								<view class="product-meta">
+									<text>{{ getItemCopies(item) }}份</text>
+									<text>餐盒费:{{ formatMoney(item.containerFee) }}元</text>
+									<text>成团价:<text class="price">¥{{ formatMoney(item.groupPrice) }}</text></text>
+								</view>
+							</view>
+						</view>
+					</view>
+				</view>
+
+				<view v-if="!items.length" class="no-items">暂无套餐明细</view>
+				<view class="card-footer">
+					<text>合计餐盒费: <text class="amount">¥{{ formatMoney(detail.containerFeeTotal) }}</text></text>
+					<text>合计金额: <text class="amount total-amount">¥{{ formatMoney(detail.amountTotal) }}</text></text>
+				</view>
+			</view>
+		</scroll-view>
+
+		<view v-if="!loading && detail.hostOrderId" class="footer-bar">
+			<view class="select-all" @click="toggleAll">
+				<view class="select-circle" :class="{ checked: isAllSelected }">
+					<u-icon v-if="isAllSelected" name="checkmark" color="#fff" size="22"></u-icon>
+				</view>
+				<text>全选</text>
+			</view>
+			<view class="refund-button" @click="handleRefund">退款</view>
+		</view>
+
+		<view v-if="refundVisible" class="refund-mask" @click.self="closeRefundModal">
+			<view class="refund-dialog" @click.stop>
+				<text class="refund-title">提示</text>
+				<text class="refund-message">是否要发起退款</text>
+				<view class="refund-actions">
+					<view class="refund-action cancel" @click="closeRefundModal">取消</view>
+					<view class="refund-action confirm" @click="confirmRefund">确定</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import merchantDishApi from '@/api/merchantDish.js'
+
+export default {
+	data() {
+		return {
+			hostOrderId: '',
+			detail: {},
+			loading: true,
+			selectedKeys: [],
+			refundVisible: false,
+			refundTarget: null,
+			refundSubmitting: false
+		}
+	},
+	computed: {
+		items() {
+			return Array.isArray(this.detail.items) ? this.detail.items : []
+		},
+		statusText() {
+			const status = this.detail.orderStatusName || this.detail.statusName || this.detail.orderStatus
+			if (typeof status === 'string' && status && Number.isNaN(Number(status))) return status
+			return ({ 0: '待支付', 1: '待成团', 2: '已成团', 3: '已出餐', 4: '已送达', 5: '已取消', 6: '售后' })[status] || ''
+		},
+		isAllSelected() {
+			return this.items.length > 0 && this.items.every((item, index) => this.isSelected(item, index))
+		}
+	},
+	onLoad(query) {
+		this.hostOrderId = (query && query.hostOrderId) || ''
+		this.loadDetail()
+	},
+	methods: {
+		loadDetail() {
+			if (!this.hostOrderId) {
+				this.loading = false
+				uni.showToast({ title: '缺少主订单号', icon: 'none' })
+				return
+			}
+
+			merchantDishApi.packageOrderDetail(this.hostOrderId).then((res) => {
+				if (res.data.code !== 200) {
+					uni.showToast({ title: res.data.message || '详情加载失败', icon: 'none' })
+					return
+				}
+				this.detail = res.data.result || {}
+			}).catch(() => {
+				uni.showToast({ title: '详情加载失败', icon: 'none' })
+			}).finally(() => {
+				this.loading = false
+			})
+		},
+		getItemKey(item, index) {
+			return item.merchantOrderPackageId || item.userOrderId || item.packageId || `item-${index}`
+		},
+		isSelected(item, index) {
+			return this.selectedKeys.includes(this.getItemKey(item, index))
+		},
+		toggleItem(item, index) {
+			const key = this.getItemKey(item, index)
+			const selectedIndex = this.selectedKeys.indexOf(key)
+			if (selectedIndex >= 0) this.selectedKeys.splice(selectedIndex, 1)
+			else this.selectedKeys.push(key)
+		},
+		toggleAll() {
+			if (this.isAllSelected) {
+				this.selectedKeys = []
+				return
+			}
+			this.selectedKeys = this.items.map((item, index) => this.getItemKey(item, index))
+		},
+		handleRefund() {
+			const selected = this.items.filter((item, index) => this.isSelected(item, index))
+			if (selected.length !== 1) {
+				uni.showToast({ title: selected.length ? '一次只能选择一条套餐退款' : '请选择要退款的套餐', icon: 'none' })
+				return
+			}
+			if (!selected[0].userOrderId) {
+				uni.showToast({ title: '退款缺少用户子订单号', icon: 'none' })
+				return
+			}
+			this.refundTarget = selected[0]
+			this.refundVisible = true
+		},
+		closeRefundModal() {
+			if (this.refundSubmitting) return
+			this.refundVisible = false
+			this.refundTarget = null
+		},
+		confirmRefund() {
+			if (!this.refundTarget || !this.refundTarget.userOrderId || this.refundSubmitting) return
+			this.refundSubmitting = true
+			merchantDishApi.packageOrderDirectRefund(this.refundTarget.userOrderId).then((res) => {
+				if (res.data.code !== 200) {
+					uni.showToast({ title: res.data.message || '退款失败', icon: 'none' })
+					return
+				}
+				this.refundVisible = false
+				this.refundTarget = null
+				this.selectedKeys = []
+				uni.showToast({ title: res.data.message || '退款成功', icon: 'success' })
+				this.loadDetail()
+			}).catch(() => {
+				uni.showToast({ title: '退款失败', icon: 'none' })
+			}).finally(() => {
+				this.refundSubmitting = false
+			})
+		},
+		getItemCopies(item) {
+			return item.packageNumber || 0
+		},
+		makePhoneCall(phone) {
+			const phoneNumber = String(phone || '')
+			if (!phoneNumber || phoneNumber.includes('*')) {
+				uni.showToast({ title: '手机号暂不可拨打', icon: 'none' })
+				return
+			}
+			uni.makePhoneCall({
+				phoneNumber
+			})
+		},
+		formatMoney(value) {
+			if (value == null || value === '') return '0.00'
+			const number = Number(value)
+			return Number.isFinite(number) ? number.toFixed(2) : '0.00'
+		},
+		getProductImage(value) {
+			const image = String(value || '').split(',').filter(Boolean)[0] || ''
+			if (!image || /^https?:\/\//.test(image)) return image
+			return this.$config && this.$config.apiUrl ? `${this.$config.apiUrl}/${image}` : image
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+	display: flex;
+	flex-direction: column;
+	height: 100vh;
+	background: #f7f8fc;
+}
+
+.content {
+	flex: 1;
+	height: 0;
+	box-sizing: border-box;
+	padding: 20rpx 32rpx 140rpx;
+}
+
+.loading,
+.empty {
+	padding: 80rpx 24rpx;
+	color: #999;
+	text-align: center;
+}
+
+.detail-card {
+	padding: 24rpx;
+	border-radius: 16rpx;
+	background: #fff;
+}
+
+.location-info {
+	padding-bottom: 20rpx;
+	border-bottom: 1rpx solid #eee;
+}
+
+.location-top {
+	display: flex;
+	align-items: center;
+	min-height: 34rpx;
+}
+
+.location-title {
+	display: flex;
+	flex: 1;
+	align-items: center;
+	min-width: 0;
+	gap: 10rpx;
+	color: #1d2129;
+	font-size: 30rpx;
+	font-weight: 600;
+}
+
+.location-title text {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.status-badge {
+	flex-shrink: 0;
+	margin: 0 10rpx;
+	padding: 4rpx 10rpx;
+	border-radius: 4rpx;
+	background: #43c28c;
+	color: #fff;
+	font-size: 24rpx;
+}
+
+.copies {
+	flex-shrink: 0;
+	color: #999;
+	font-size: 26rpx;
+}
+
+.copies-number {
+	color: #388bf9;
+}
+
+.location-region {
+	display: block;
+	margin-top: 10rpx;
+	color: #666;
+	font-size: 26rpx;
+}
+
+.detail-item {
+	display: flex;
+	align-items: flex-start;
+	gap: 20rpx;
+	padding-top: 24rpx;
+}
+
+.select-circle {
+	display: flex;
+	flex-shrink: 0;
+	align-items: center;
+	justify-content: center;
+	width: 32rpx;
+	height: 32rpx;
+	margin-top: 8rpx;
+	box-sizing: border-box;
+	border: 1rpx solid #d9d9d9;
+	border-radius: 50%;
+	background: #fff;
+}
+
+.select-circle.checked {
+	border-color: #388bf9;
+	background: #388bf9;
+}
+
+.item-content {
+	flex: 1;
+	min-width: 0;
+}
+
+.contact-row {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	box-sizing: border-box;
+	min-height: 48rpx;
+	padding: 10rpx;
+	border-radius: 8rpx;
+	background: #f8f8f8;
+	image{
+		width: 40rpx;
+		height: 40rpx;
+	}
+}
+
+.contact-info {
+	display: flex;
+	align-items: center;
+	gap: 24rpx;
+	min-width: 0;
+	color: #333;
+	font-size: 30rpx;
+}
+
+.contact-info text {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.call-icon {
+	flex-shrink: 0;
+	width: 40rpx;
+	height: 40rpx;
+}
+
+.product-row {
+	display: flex;
+	align-items: stretch;
+	gap: 12rpx;
+	margin-top: 16rpx;
+}
+
+.product-info {
+	display: flex;
+	flex: 1;
+	min-width: 0;
+	flex-direction: column;
+	justify-content: space-between;
+}
+
+.product-name,
+.product-desc {
+	display: block;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.product-name {
+	color: #1d2129;
+	font-size: 28rpx;
+}
+
+.product-desc {
+	margin-top: 6rpx;
+	color: #86909c;
+	font-size: 24rpx;
+	line-height: 32rpx;
+}
+
+.product-meta {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	gap: 8rpx;
+	margin-top: 8rpx;
+	color: #4e5969;
+	font-size: 24rpx;
+	white-space: nowrap;
+}
+
+.price,
+.amount {
+	color: #ff004e;
+}
+
+.price {
+	font-size: 32rpx;
+}
+
+.card-footer {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	margin-top: 24rpx;
+	padding-top: 20rpx;
+	border-top: 1rpx solid #eee;
+	color: #86909c;
+	font-size: 26rpx;
+}
+
+.total-amount {
+	font-size: 40rpx;
+}
+
+.no-items {
+	padding: 30rpx 0;
+	color: #999;
+	text-align: center;
+}
+
+.footer-bar {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 10;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	box-sizing: border-box;
+	min-height: 88rpx;
+	padding: 24rpx 48rpx calc(24rpx + env(safe-area-inset-bottom));
+	background: rgba(255, 255, 255, 0.96);
+	box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.06);
+}
+
+.select-all {
+	display: flex;
+	align-items: center;
+	gap: 16rpx;
+	color: #1d2129;
+	font-size: 28rpx;
+}
+
+.select-all .select-circle {
+	margin-top: 0;
+}
+
+.refund-button {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 240rpx;
+	height: 80rpx;
+	border-radius: 80rpx;
+	background: linear-gradient(93deg, #10abfe 0%, #2260f6 100%);
+	color: #fff;
+	font-size: 32rpx;
+}
+
+.refund-mask {
+	position: fixed;
+	left: 0;
+	right: 0;
+	top: 0;
+	bottom: 0;
+	z-index: 20;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	background: rgba(0, 0, 0, 0.6);
+}
+
+.refund-dialog {
+	display: flex;
+	width: 590rpx;
+	flex-direction: column;
+	align-items: center;
+	border-radius: 16rpx;
+	background: #fff;
+	overflow: hidden;
+}
+
+.refund-title {
+	padding-top: 32rpx;
+	color: #1d2129;
+	font-size: 32rpx;
+	font-weight: 600;
+	line-height: 44rpx;
+	margin-bottom: 32rpx;
+}
+
+.refund-message {
+	color: #4e5969;
+	font-size: 30rpx;
+	line-height: 42rpx;
+	text-align: center;
+	margin-bottom: 32rpx;
+}
+
+.refund-actions {
+	display: flex;
+	width: 100%;
+	border-top: 1rpx solid #e7e7e7;
+}
+
+.refund-action {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 50%;
+	height: 90rpx;
+	box-sizing: border-box;
+	font-size: 30rpx;
+	line-height: 42rpx;
+}
+
+.refund-action.cancel {
+	border-right: 1rpx solid #e7e7e7;
+	color: #86909c;
+}
+
+.refund-action.confirm {
+	color: #1d2129;
+}
+</style>

+ 471 - 0
pages/merchantDish/orders/refund-details.vue

@@ -0,0 +1,471 @@
+<template>
+	<view class="page">
+		<u-navbar title="退款详情" :autoBack="true" :placeholder="true" :border-bottom="false"></u-navbar>
+
+		<view v-if="loading" class="state-tip">加载中...</view>
+		<view v-else-if="!hasDetail" class="state-tip">暂无退款详情</view>
+		<scroll-view v-else class="content" scroll-y>
+			<view class="status-card">
+				<text class="status-title">{{ product.orderStatusName || '--' }}</text>
+				<text class="status-time">{{ statusTimeLabel }}:{{ formatApplyTime(refundTime) }}</text>
+			</view>
+
+			<view class="card product-card">
+				<view class="product-row">
+					<u-image
+						width="120rpx"
+						height="120rpx"
+						border-radius="12rpx"
+						:src="productImageUrl"
+						mode="aspectFill"
+					></u-image>
+					<view class="product-info">
+						<view class="product-title-row">
+							<text class="product-name">{{ product.packageName || product.productName || '团餐套餐' }}</text>
+							<text class="delivery-status">{{ deliveryStatusText }}</text>
+						</view>
+						<view class="product-desc-row">
+							<text class="product-desc">{{ dishDescription }}</text>
+							<text class="quantity">x{{ quantity }}</text>
+						</view>
+						<view class="item-price">
+							<text class="currency">¥</text>
+							<text>{{ formatMoney(itemPrice) }}</text>
+						</view>
+					</view>
+				</view>
+
+				<view class="product-summary">
+					<text>餐盒费:{{ formatMoney(containerFee) }}元</text>
+					<view class="refund-amount">
+						<text class="refund-label">退款金额</text>
+						<text class="refund-currency">¥</text>
+						<text class="refund-value">{{ formatMoney(refundAmount) }}</text>
+						<text class="refund-note">不包含餐盒费</text>
+					</view>
+				</view>
+			</view>
+
+			<view class="card negotiate-card" @click="goRecord">
+				<text class="card-title">协商历史</text>
+				<view class="negotiate-link">
+					<text>查看详情</text>
+					<u-icon name="arrow-right" size="22" color="#4e5969"></u-icon>
+				</view>
+			</view>
+
+			<view class="card info-card">
+				<text class="card-title">订单信息</text>
+				<view class="info-row">
+					<text class="info-label">订单号</text>
+					<text class="info-value">{{ product.orderId || product.hostOrderId || '--' }}</text>
+				</view>
+				<view class="info-row">
+					<text class="info-label">支付流水号</text>
+					<text class="info-value">{{ product.tradeId || product.paymentNo || '--' }}</text>
+				</view>
+				<view class="info-row">
+					<text class="info-label">用户姓名</text>
+					<text class="info-value">{{ product.linkName || product.userName || '--' }}</text>
+				</view>
+				<view class="info-row">
+					<text class="info-label">联系电话</text>
+					<view class="phone-value" @click.stop="makePhoneCall(userPhone)">
+						<text>{{ userPhone || '--' }}</text>
+						<image v-if="userPhone" class="phone-icon" src="/static/merchantDish/call.png" mode="aspectFit"></image>
+					</view>
+				</view>
+				<view class="info-row">
+					<text class="info-label">下单时间</text>
+					<text class="info-value">{{ formatDateTime(product.orderDate || product.createTime) }}</text>
+				</view>
+				<view class="info-row">
+					<text class="info-label">付款时间</text>
+					<text class="info-value">{{ formatDateTime(product.paymentTime || product.payTime) }}</text>
+				</view>
+				<view v-if="product.completeTime" class="info-row">
+					<text class="info-label">成团时间</text>
+					<text class="info-value">{{ formatDateTime(product.completeTime) }}</text>
+				</view>
+				<view v-if="product.sendTime" class="info-row">
+					<text class="info-label">送达时间</text>
+					<text class="info-value">{{ formatDateTime(product.sendTime) }}</text>
+				</view>
+			</view>
+
+			<view class="card info-card delivery-card">
+				<text class="card-title">配送信息</text>
+				<view class="info-row">
+					<text class="info-label">配送团长</text>
+					<text class="info-value">{{ product.nickName || product.headName || product.leaderName || '--' }}</text>
+				</view>
+				<view class="info-row">
+					<text class="info-label">联系方式</text>
+					<view class="phone-value" @click.stop="makePhoneCall(deliveryPhone)">
+						<text>{{ deliveryPhone || '--' }}</text>
+						<image v-if="deliveryPhone" class="phone-icon" src="/static/merchantDish/call.png" mode="aspectFit"></image>
+					</view>
+				</view>
+			</view>
+		</scroll-view>
+	</view>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			query: {},
+			product: {},
+			loading: true
+		}
+	},
+	computed: {
+		hasDetail() {
+			return Object.keys(this.product || {}).length > 0
+		},
+		productImageUrl() {
+			const image = String(this.product.productImage || '').split(',').filter(Boolean)[0] || ''
+			if (!image || /^https?:\/\//.test(image)) return image
+			return this.$config && this.$config.apiUrl ? `${this.$config.apiUrl}/${image}` : image
+		},
+		statusTimeLabel() {
+			return Number(this.product.fundStatus) === 1 ? '退款时间' : '申请时间'
+		},
+		refundTime() {
+			return this.product.applyTime || this.product.refundApplyTime || this.product.refundTime || this.product.createTime
+		},
+		deliveryStatusText() {
+			const statusMap = {
+				0: '未成团',
+				1: '已成团',
+				2: '已出餐',
+				3: '已送达',
+				4: '成团失败',
+				9: '已到达',
+				10: '已取餐'
+			}
+			const orderStatus = this.product.orderStatus
+			if (orderStatus !== '' && orderStatus != null && Object.prototype.hasOwnProperty.call(statusMap, Number(orderStatus))) {
+				return statusMap[Number(orderStatus)]
+			}
+			const text = this.product.deliveryStatusName || this.product.packageOrderStatusName || this.product.merchantOrderStatusName || this.product.statusName
+			if (text) return text
+			const orderText = String(this.product.orderStatusName || '')
+			return orderText && !orderText.includes('退款') && !orderText.includes('售后') ? orderText : '待送达'
+		},
+		dishDescription() {
+			return this.product.dishName || this.product.packageContent || this.product.dishNames || '暂无菜品信息'
+		},
+		quantity() {
+			return Number(this.product.packageNumber || this.product.buyNumber || this.product.quantity || 1)
+		},
+		itemPrice() {
+			if (this.product.priceEach != null) return this.product.priceEach
+			if (this.product.groupPrice != null) return this.product.groupPrice
+			return this.product.price || 0
+		},
+		refundAmount() {
+			if (this.product.refundAmount != null) return this.product.refundAmount
+			if (this.product.refundPrice != null) return this.product.refundPrice
+			return this.product.price || 0
+		},
+		containerFee() {
+			if (this.product.containerFee != null) return this.product.containerFee
+			if (this.product.boxFee != null) return this.product.boxFee
+			return Number(this.product.boxPrice || 0) * this.quantity
+		},
+		userPhone() {
+			return this.product.phone || this.product.maskedPhone || this.product.mobile || ''
+		},
+		deliveryPhone() {
+			return this.product.nickPhone || this.product.leaderPhone || this.product.headPhone || ''
+		}
+	},
+	onLoad(query) {
+		this.query = query || {}
+	},
+	onShow() {
+		this.getDetail()
+	},
+	methods: {
+		getDetail() {
+			if (!this.query.hostOrderId) {
+				this.loading = false
+				uni.showToast({ title: '缺少订单信息', icon: 'none' })
+				return
+			}
+			this.loading = true
+			this.$http.post('/groupmeal/merchantPackageOrder/refundDetails', this.query).then((res) => {
+				if (res.data.code !== 200) {
+					uni.showToast({ title: res.data.message || '退款详情加载失败', icon: 'none' })
+					return
+				}
+				this.product = res.data.result || {}
+			}).catch(() => {
+				uni.showToast({ title: '退款详情加载失败', icon: 'none' })
+			}).finally(() => {
+				this.loading = false
+			})
+		},
+		goRecord() {
+			const query = this.$u.queryParams(this.query)
+			uni.navigateTo({
+				url: `/pages/merchantDish/orders/refund-record${query}`
+			})
+		},
+		makePhoneCall(phone) {
+			const phoneNumber = String(phone || '')
+			if (!phoneNumber || phoneNumber.includes('*')) {
+				uni.showToast({ title: '手机号暂不可拨打', icon: 'none' })
+				return
+			}
+			uni.makePhoneCall({ phoneNumber })
+		},
+		formatMoney(value) {
+			const number = Number(value)
+			if (!Number.isFinite(number)) return '0'
+			return number.toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1')
+		},
+		formatDateTime(value) {
+			if (!value) return '--'
+			return String(value).replace(/-/g, '.')
+		},
+		formatApplyTime(value) {
+			if (!value) return '--'
+			const text = String(value).trim()
+			const match = text.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})(.*)$/)
+			return match ? `${match[1]}.${Number(match[2])}.${Number(match[3])}${match[4]}` : text
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+	display: flex;
+	flex-direction: column;
+	height: 100vh;
+	background: #f7f8fa;
+	overflow: hidden;
+}
+
+.content {
+	flex: 1;
+	height: 0;
+	box-sizing: border-box;
+	padding: 32rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
+}
+
+.state-tip {
+	padding: 100rpx 24rpx;
+	color: #86909c;
+	font-size: 28rpx;
+	text-align: center;
+}
+
+.card,
+.status-card {
+	box-sizing: border-box;
+	margin-bottom: 16rpx;
+	border-radius: 16rpx;
+	background: #fff;
+}
+
+.status-card {
+	display: flex;
+	min-height: 164rpx;
+	padding: 32rpx;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	gap: 8rpx;
+}
+
+.status-title {
+	color: #1d2129;
+	font-size: 40rpx;
+	font-weight: 600;
+}
+
+.status-time {
+	color: #86909c;
+	font-size: 26rpx;
+}
+
+.card {
+	padding: 32rpx;
+}
+
+.product-row {
+	display: flex;
+	align-items: stretch;
+	gap: 20rpx;
+}
+
+.product-info {
+	display: flex;
+	flex: 1;
+	min-width: 0;
+	flex-direction: column;
+	justify-content: space-between;
+}
+
+.product-title-row,
+.product-desc-row,
+.product-summary,
+.refund-amount,
+.negotiate-card,
+.negotiate-link,
+.info-row,
+.phone-value {
+	display: flex;
+	align-items: center;
+}
+
+.product-title-row,
+.product-desc-row,
+.product-summary,
+.negotiate-card,
+.info-row {
+	justify-content: space-between;
+}
+
+.product-name {
+	flex: 1;
+	min-width: 0;
+	overflow: hidden;
+	color: #1d2129;
+	font-size: 30rpx;
+	font-weight: 600;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.delivery-status {
+	flex-shrink: 0;
+	margin-left: 16rpx;
+	color: #86909c;
+	font-size: 28rpx;
+}
+
+.product-desc {
+	flex: 1;
+	min-width: 0;
+	overflow: hidden;
+	color: #86909c;
+	font-size: 24rpx;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.quantity {
+	flex-shrink: 0;
+	margin-left: 16rpx;
+	color: #86909c;
+	font-size: 24rpx;
+}
+
+.item-price {
+	color: #1d2129;
+	font-size: 28rpx;
+	font-weight: 600;
+}
+
+.currency {
+	font-size: 20rpx;
+}
+
+.product-summary {
+	margin-top: 16rpx;
+	color: #333;
+	font-size: 24rpx;
+}
+
+.refund-amount {
+	align-items: baseline;
+	white-space: nowrap;
+}
+
+.refund-label {
+	margin-right: 6rpx;
+}
+
+.refund-currency {
+	color: #1d2129;
+	font-size: 26rpx;
+	font-weight: 600;
+}
+
+.refund-value {
+	color: #1d2129;
+	font-size: 36rpx;
+	font-weight: 600;
+}
+
+.refund-note {
+	margin-left: 8rpx;
+	color: #1c7cf9;
+	font-size: 20rpx;
+}
+
+.negotiate-card {
+	height: 108rpx;
+}
+
+.card-title {
+	color: #1d2129;
+	font-size: 32rpx;
+	font-weight: 500;
+}
+
+.negotiate-link {
+	color: #4e5969;
+	font-size: 28rpx;
+}
+
+.info-card {
+	display: flex;
+	flex-direction: column;
+	gap: 24rpx;
+}
+
+.info-row {
+	gap: 24rpx;
+	min-height: 32rpx;
+	font-size: 28rpx;
+}
+
+.info-label {
+	flex-shrink: 0;
+	color: #4e5969;
+}
+
+.info-value,
+.phone-value {
+	min-width: 0;
+	color: #1d2129;
+	text-align: right;
+}
+
+.info-value {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.phone-value {
+	justify-content: flex-end;
+	gap: 12rpx;
+}
+
+.phone-icon {
+	flex-shrink: 0;
+	width: 32rpx;
+	height: 32rpx;
+}
+
+.delivery-card {
+	margin-bottom: 0;
+}
+</style>

+ 700 - 0
pages/merchantDish/orders/refund-record.vue

@@ -0,0 +1,700 @@
+<template>
+	<view class="page">
+		<u-navbar title="协商记录" :autoBack="true" :placeholder="true" :border-bottom="false"></u-navbar>
+
+		<view v-if="loading" class="state-tip">加载中...</view>
+		<view v-else-if="!hasDetail" class="state-tip">暂无协商记录</view>
+		<scroll-view v-else class="content" scroll-y>
+			<view class="card product-card">
+				<view class="product-row">
+					<u-image
+						width="120rpx"
+						height="120rpx"
+						border-radius="12rpx"
+						:src="productImageUrl"
+						mode="aspectFill"
+					></u-image>
+					<view class="product-info">
+						<view class="product-title-row">
+							<text class="product-name">{{ data.packageName || data.productName || '团餐套餐' }}</text>
+							<text class="delivery-status">{{ deliveryStatusText }}</text>
+						</view>
+						<view class="product-desc-row">
+							<text class="product-desc">{{ dishDescription }}</text>
+							<text class="quantity">x{{ quantity }}</text>
+						</view>
+						<view class="item-price">
+							<text class="currency">¥</text>
+							<text>{{ formatMoney(itemPrice) }}</text>
+						</view>
+					</view>
+				</view>
+
+				<view class="product-summary">
+					<text>餐盒费:{{ formatMoney(containerFee) }}元</text>
+					<view class="refund-amount">
+						<text class="refund-label">退款金额</text>
+						<text class="refund-currency">¥</text>
+						<text class="refund-value">{{ formatMoney(refundAmount) }}</text>
+						<text class="refund-note">不包含餐盒费</text>
+					</view>
+				</view>
+			</view>
+
+			<view class="card history-card">
+				<text class="card-title">协商历史</text>
+				<text class="history-status">{{ data.orderStatusName || '--' }}</text>
+			</view>
+
+			<view class="card info-card">
+				<text class="card-title">订单信息</text>
+				<view class="info-row avatar-row">
+					<text class="info-label">用户昵称</text>
+					<u-image
+						width="60rpx"
+						height="60rpx"
+						border-radius="8rpx"
+						:src="avatarUrl"
+						mode="aspectFill"
+					></u-image>
+				</view>
+				<view class="info-row">
+					<text class="info-label">退货数量</text>
+					<text class="info-value">{{ quantity }}份</text>
+				</view>
+				<view class="info-row">
+					<text class="info-label">退货金额</text>
+					<text class="info-value">¥{{ formatMoney(refundAmount) }}</text>
+				</view>
+				<view class="info-row">
+					<text class="info-label">联系人</text>
+					<text class="info-value">{{ data.linkName || data.userName || '--' }}</text>
+				</view>
+				<view class="info-row">
+					<text class="info-label">联系方式</text>
+					<view class="phone-value" @click.stop="makePhoneCall(userPhone)">
+						<text>{{ userPhone || '--' }}</text>
+						<image v-if="userPhone" class="phone-icon" src="/static/merchantDish/call.png" mode="aspectFit"></image>
+					</view>
+				</view>
+				<view class="info-row">
+					<text class="info-label">申请原因</text>
+					<text class="info-value reason-value">{{ data.reason || data.refundReason || '--' }}</text>
+				</view>
+
+				<view v-if="showActions" class="actions">
+					<view class="action-button reject-button" @click="onSubmit(0)">拒绝</view>
+					<view class="action-button agree-button" @click="onSubmit(1)">同意</view>
+				</view>
+			</view>
+
+			<view v-if="data.refuseReason !== null && data.refuseReason !== undefined && data.refuseReason !== ''" class="card refused-card">
+				<view class="refused-header">
+					<text class="card-title">商家拒绝退款</text>
+					<text class="refused-time">{{ formatDateTime(data.refundTime) }}</text>
+				</view>
+				<view class="refused-row">拒绝原因:{{ getRefuseReasonText(data.refuseReason) }}</view>
+				<view v-if="data.refuseReasonExplain" class="refused-row">补充说明:{{ data.refuseReasonExplain }}</view>
+			</view>
+		</scroll-view>
+
+		<u-popup v-model="isRefuse" mode="bottom" :border-radius="32" :safe-area-inset-bottom="true">
+			<view class="refuse-popup">
+				<view class="popup-header">
+					<text class="popup-title">拒绝原因</text>
+					<u-icon class="popup-close" name="close" size="30" color="#4e5969" @click="onClose"></u-icon>
+				</view>
+				<view class="popup-content">
+					<view
+						v-for="(item, index) in reasonList"
+						:key="item.value || index"
+						class="reason-item"
+						@click="onSelect(item, index)"
+					>
+						<text>{{ item.text }}</text>
+						<view :class="['radio', { checked: item.check }]">
+							<text v-if="item.check" class="radio-check">✓</text>
+						</view>
+					</view>
+
+					<view class="explain-section">
+						<text class="explain-title">补充说明(选填)</text>
+						<view class="explain-box">
+							<textarea
+								v-model="rejectData.refuseReasonExplain"
+								class="explain-input"
+								:maxlength="50"
+								placeholder="输入退款说明"
+							></textarea>
+							<text class="word-count">{{ rejectData.refuseReasonExplain.length }}/50</text>
+						</view>
+					</view>
+				</view>
+
+				<view class="popup-actions">
+					<view class="popup-button cancel-button" @click="onClose">取消</view>
+					<view class="popup-button confirm-button" @click="onRefund">确定</view>
+				</view>
+			</view>
+		</u-popup>
+	</view>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			data: {},
+			query: {},
+			reasonList: [],
+			isRefuse: false,
+			loading: true,
+			rejectData: {
+				refuseReason: '',
+				refuseReasonExplain: ''
+			}
+		}
+	},
+	computed: {
+		hasDetail() {
+			return Object.keys(this.data || {}).length > 0
+		},
+		productImageUrl() {
+			return this.resolveImage(this.data.productImage)
+		},
+		avatarUrl() {
+			return this.resolveImage(this.data.userAvatar || this.data.avatar)
+		},
+		deliveryStatusText() {
+			return this.data.deliveryStatusName || this.data.packageOrderStatusName || this.data.merchantOrderStatusName || this.data.statusName || '待送达'
+		},
+		dishDescription() {
+			return this.data.dishName || this.data.packageContent || this.data.dishNames || '暂无菜品信息'
+		},
+		quantity() {
+			return Number(this.data.packageNumber || this.data.buyNumber || this.data.quantity || 1)
+		},
+		itemPrice() {
+			if (this.data.priceEach != null) return this.data.priceEach
+			if (this.data.groupPrice != null) return this.data.groupPrice
+			return this.data.price || 0
+		},
+		refundAmount() {
+			if (this.data.refundAmount != null) return this.data.refundAmount
+			if (this.data.refundPrice != null) return this.data.refundPrice
+			return this.data.price || 0
+		},
+		containerFee() {
+			if (this.data.containerFee != null) return this.data.containerFee
+			if (this.data.boxFee != null) return this.data.boxFee
+			return Number(this.data.boxPrice || 0) * this.quantity
+		},
+		userPhone() {
+			return this.data.phone || this.data.maskedPhone || this.data.mobile || ''
+		},
+		showActions() {
+			return this.data.orderStatusName === '售后申请中' || Number(this.data.fundStatus) === 2
+		}
+	},
+	onLoad(query) {
+		this.query = query || {}
+		this.getData()
+		this.getCodeList()
+	},
+	methods: {
+		getData() {
+			if (!this.query.hostOrderId) {
+				this.loading = false
+				uni.showToast({ title: '缺少订单信息', icon: 'none' })
+				return
+			}
+			this.loading = true
+			this.$http.post('/groupmeal/merchantPackageOrder/refundDetails', this.query).then((res) => {
+				if (res.data.code !== 200) {
+					uni.showToast({ title: res.data.message || '协商记录加载失败', icon: 'none' })
+					return
+				}
+				this.data = res.data.result || {}
+			}).catch(() => {
+				uni.showToast({ title: '协商记录加载失败', icon: 'none' })
+			}).finally(() => {
+				this.loading = false
+			})
+		},
+		getCodeList() {
+			this.$http.get('/sys/api/getManyDictItems?dictCodeList=reason_refusal').then((res) => {
+				const list = res.data && res.data.reason_refusal
+				this.reasonList = Array.isArray(list) ? list.map((item) => ({ ...item, check: false })) : []
+			})
+		},
+		onSelect(item, index) {
+			this.reasonList.forEach((reason) => {
+				reason.check = false
+			})
+			this.reasonList[index].check = true
+			this.rejectData.refuseReason = item.value
+		},
+		onRefund() {
+			if (this.rejectData.refuseReason === '') {
+				uni.showToast({ title: '请选择拒绝原因', icon: 'none' })
+				return
+			}
+			this.$http.post('/groupmeal/merchantPackageOrder/refundRefuse', {
+				...this.query,
+				...this.rejectData
+			}).then((res) => {
+				if (res.data.code !== 200) {
+					uni.showToast({ title: res.data.message || '拒绝失败', icon: 'none' })
+					return
+				}
+				uni.showToast({ title: res.data.message || '拒绝成功', icon: 'success' })
+				this.resetRejectData()
+				this.onClose()
+				this.getData()
+			})
+		},
+		onSubmit(type) {
+			if (type === 0) {
+				this.isRefuse = true
+				return
+			}
+			uni.showModal({
+				title: '提示',
+				content: '是否同意退款?',
+				success: (modalRes) => {
+					if (!modalRes.confirm) return
+					this.$http.post('/groupmeal/merchantPackageOrder/refundAgree', { ...this.query }).then((res) => {
+						if (res.data.code !== 200) {
+							uni.showToast({ title: res.data.message || '操作失败', icon: 'none' })
+							return
+						}
+						uni.showToast({ title: res.data.message || '商家已同意退款', icon: 'success' })
+						this.getData()
+					})
+				}
+			})
+		},
+		onClose() {
+			this.isRefuse = false
+		},
+		resetRejectData() {
+			this.rejectData = {
+				refuseReason: '',
+				refuseReasonExplain: ''
+			}
+			this.reasonList.forEach((reason) => {
+				reason.check = false
+			})
+		},
+		getRefuseReasonText(value) {
+			const selected = this.reasonList.find((item) => String(item.value) === String(value))
+			return selected ? selected.text : value
+		},
+		makePhoneCall(phone) {
+			const phoneNumber = String(phone || '')
+			if (!phoneNumber || phoneNumber.includes('*')) {
+				uni.showToast({ title: '手机号暂不可拨打', icon: 'none' })
+				return
+			}
+			uni.makePhoneCall({ phoneNumber })
+		},
+		resolveImage(value) {
+			const image = String(value || '').split(',').filter(Boolean)[0] || ''
+			if (!image || /^https?:\/\//.test(image)) return image
+			return this.$config && this.$config.apiUrl ? `${this.$config.apiUrl}/${image}` : image
+		},
+		formatMoney(value) {
+			const number = Number(value)
+			if (!Number.isFinite(number)) return '0'
+			return number.toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1')
+		},
+		formatDateTime(value) {
+			return value ? String(value).replace(/-/g, '.') : '--'
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+	display: flex;
+	flex-direction: column;
+	height: 100vh;
+	background: #f7f8fa;
+	overflow: hidden;
+}
+
+.content {
+	flex: 1;
+	height: 0;
+	box-sizing: border-box;
+	padding: 32rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
+}
+
+.state-tip {
+	padding: 100rpx 24rpx;
+	color: #86909c;
+	font-size: 28rpx;
+	text-align: center;
+}
+
+.card {
+	box-sizing: border-box;
+	margin-bottom: 16rpx;
+	padding: 32rpx;
+	border-radius: 16rpx;
+	background: #fff;
+}
+
+.product-row,
+.product-title-row,
+.product-desc-row,
+.product-summary,
+.refund-amount,
+.history-card,
+.info-row,
+.phone-value,
+.actions,
+.refused-header,
+.popup-header,
+.reason-item,
+.popup-actions {
+	display: flex;
+	align-items: center;
+}
+
+.product-row {
+	align-items: stretch;
+	gap: 20rpx;
+}
+
+.product-info {
+	display: flex;
+	flex: 1;
+	min-width: 0;
+	flex-direction: column;
+	justify-content: space-between;
+}
+
+.product-title-row,
+.product-desc-row,
+.product-summary,
+.history-card,
+.info-row,
+.refused-header,
+.popup-header,
+.reason-item {
+	justify-content: space-between;
+}
+
+.product-name {
+	flex: 1;
+	min-width: 0;
+	overflow: hidden;
+	color: #1d2129;
+	font-size: 30rpx;
+	font-weight: 600;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.delivery-status {
+	flex-shrink: 0;
+	margin-left: 16rpx;
+	color: #86909c;
+	font-size: 28rpx;
+}
+
+.product-desc {
+	flex: 1;
+	min-width: 0;
+	overflow: hidden;
+	color: #86909c;
+	font-size: 24rpx;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.quantity {
+	flex-shrink: 0;
+	margin-left: 16rpx;
+	color: #86909c;
+	font-size: 24rpx;
+}
+
+.item-price {
+	color: #1d2129;
+	font-size: 28rpx;
+	font-weight: 600;
+}
+
+.currency {
+	font-size: 20rpx;
+}
+
+.product-summary {
+	margin-top: 16rpx;
+	color: #333;
+	font-size: 24rpx;
+}
+
+.refund-amount {
+	align-items: baseline;
+	white-space: nowrap;
+}
+
+.refund-label {
+	margin-right: 6rpx;
+}
+
+.refund-currency {
+	font-size: 26rpx;
+	font-weight: 600;
+}
+
+.refund-value {
+	font-size: 36rpx;
+	font-weight: 600;
+}
+
+.refund-note {
+	margin-left: 8rpx;
+	color: #1c7cf9;
+	font-size: 20rpx;
+}
+
+.history-card {
+	height: 108rpx;
+}
+
+.card-title {
+	color: #1d2129;
+	font-size: 32rpx;
+	font-weight: 500;
+}
+
+.history-status {
+	color: #f8b328;
+	font-size: 28rpx;
+}
+
+.info-card {
+	display: flex;
+	flex-direction: column;
+	gap: 24rpx;
+}
+
+.info-row {
+	gap: 24rpx;
+	min-height: 32rpx;
+	font-size: 28rpx;
+}
+
+.avatar-row {
+	min-height: 60rpx;
+}
+
+.info-label {
+	flex-shrink: 0;
+	color: #4e5969;
+}
+
+.info-value,
+.phone-value {
+	min-width: 0;
+	color: #1d2129;
+	text-align: right;
+}
+
+.reason-value {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.phone-value {
+	justify-content: flex-end;
+	gap: 12rpx;
+}
+
+.phone-icon {
+	width: 32rpx;
+	height: 32rpx;
+}
+
+.actions {
+	justify-content: flex-end;
+	gap: 24rpx;
+}
+
+.action-button {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 152rpx;
+	height: 64rpx;
+	border-radius: 64rpx;
+	font-size: 26rpx;
+}
+
+.reject-button {
+	background: rgba(51, 51, 53, 0.05);
+	color: #1d2129;
+}
+
+.agree-button {
+	background: linear-gradient(92deg, #10abfe 0%, #2260f6 100%);
+	color: #fff;
+}
+
+.refused-card {
+	display: flex;
+	flex-direction: column;
+	gap: 16rpx;
+}
+
+.refused-time {
+	color: #86909c;
+	font-size: 24rpx;
+}
+
+.refused-row {
+	color: #4e5969;
+	font-size: 26rpx;
+}
+
+.refuse-popup {
+	display: flex;
+	flex-direction: column;
+	gap: 24rpx;
+	box-sizing: border-box;
+	background: #fff;
+}
+
+.popup-header {
+	position: relative;
+	height: 100rpx;
+	box-sizing: border-box;
+	justify-content: center;
+	padding: 25rpx 32rpx;
+	border-bottom: 1rpx solid #e7e7e7;
+}
+
+.popup-title {
+	color: #333;
+	font-size: 36rpx;
+	font-weight: 500;
+	line-height: 50rpx;
+}
+
+.popup-close {
+	position: absolute;
+	right: 32rpx;
+	top: 35rpx;
+}
+
+.popup-content {
+	display: flex;
+	flex-direction: column;
+	gap: 32rpx;
+	box-sizing: border-box;
+	padding: 16rpx 32rpx 0;
+}
+
+.reason-item {
+	height: 32rpx;
+	color: #4e5969;
+	font-size: 28rpx;
+	line-height: 32rpx;
+}
+
+.radio {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 32rpx;
+	height: 32rpx;
+	box-sizing: border-box;
+	border: 1rpx solid #c9cdd4;
+	border-radius: 50%;
+}
+
+.radio.checked {
+	border-color: #168cff;
+	background: #168cff;
+}
+
+.radio-check {
+	color: #fff;
+	font-size: 22rpx;
+	font-weight: 600;
+	line-height: 32rpx;
+}
+
+.explain-section {
+	display: flex;
+	flex-direction: column;
+	gap: 16rpx;
+}
+
+.explain-title {
+	color: #1d2129;
+	font-size: 28rpx;
+	font-weight: 500;
+	line-height: 40rpx;
+}
+
+.explain-box {
+	position: relative;
+	box-sizing: border-box;
+	height: 160rpx;
+	padding: 16rpx;
+	border-radius: 8rpx;
+	background: #f8f8f8;
+}
+
+.explain-input {
+	width: 100%;
+	height: 88rpx;
+	color: #1d2129;
+	font-size: 24rpx;
+	line-height: 34rpx;
+}
+
+.word-count {
+	position: absolute;
+	right: 16rpx;
+	bottom: 16rpx;
+	color: #86909c;
+	font-size: 24rpx;
+	line-height: 34rpx;
+	text-align: right;
+}
+
+.popup-actions {
+	gap: 32rpx;
+	box-sizing: border-box;
+	padding: 24rpx 48rpx;
+	background: #fff;
+}
+
+.popup-button {
+	display: flex;
+	flex: 1;
+	height: 88rpx;
+	align-items: center;
+	justify-content: center;
+	border-radius: 140rpx;
+	font-size: 32rpx;
+}
+
+.cancel-button {
+	background: rgba(51, 51, 53, 0.05);
+	color: #1d2129;
+}
+
+.confirm-button {
+	background: linear-gradient(92deg, #10abfe 0%, #2260f6 100%);
+	color: #fff;
+}
+</style>

BIN
static/merchantDish/call.png


BIN
static/merchantDish/select-tab.png