فهرست منبع

酒店订房与订房记录改动

wengan01 6 روز پیش
والد
کامیت
a30ae439cc
4فایلهای تغییر یافته به همراه228 افزوده شده و 22 حذف شده
  1. 16 0
      src/api/content/bookingRecord.js
  2. 9 0
      src/api/content/hotel.js
  3. 83 11
      src/views/content/bookingRecord/index.vue
  4. 120 11
      src/views/content/hotelBooking/index.vue

+ 16 - 0
src/api/content/bookingRecord.js

@@ -16,3 +16,19 @@ export function getBooking(id) {
     method: 'get'
   });
 }
+
+// 手动退房(后台强制退订)
+export function forceCancelBooking(id) {
+  return request({
+    url: '/content/hotelBooking/forceCancel/' + id,
+    method: 'put'
+  });
+}
+
+// 差价退回
+export function refundExtraBooking(id) {
+  return request({
+    url: '/content/hotelBooking/refundExtra/' + id,
+    method: 'put'
+  });
+}

+ 9 - 0
src/api/content/hotel.js

@@ -42,3 +42,12 @@ export function delHotel(ids) {
     method: 'delete'
   });
 }
+
+// 切换酒店APP展示/隐藏状态(isShow:0隐藏 1展示)
+export function changeHotelShow(id, isShow) {
+  return request({
+    url: '/content/hotel/changeShow',
+    method: 'put',
+    params: { id, isShow }
+  });
+}

+ 83 - 11
src/views/content/bookingRecord/index.vue

@@ -32,20 +32,20 @@
           {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
         </template>
       </el-table-column>
-      <el-table-column label="用户ID" align="center" prop="userId" min-width="100" show-overflow-tooltip />
-      <el-table-column label="选手姓名" align="center" prop="contactName" min-width="100" show-overflow-tooltip>
+      <el-table-column label="下单用户ID" align="center" prop="userId" min-width="100" show-overflow-tooltip />
+      <el-table-column label="下单人姓名" align="center" prop="contactName" min-width="100" show-overflow-tooltip>
         <template #default="scope">
           {{ scope.row.contactName || '-' }}
         </template>
       </el-table-column>
-      <el-table-column label="选手身份证号码" align="center" prop="idCard" min-width="160" show-overflow-tooltip>
+      <el-table-column label="入住人姓名" align="center" prop="guestName" min-width="100" show-overflow-tooltip>
         <template #default="scope">
-          {{ scope.row.idCard || '-' }}
+          {{ scope.row.guestName || '-' }}
         </template>
       </el-table-column>
-      <el-table-column label="手机号码" align="center" prop="contactPhone" min-width="120" show-overflow-tooltip>
+      <el-table-column label="入住人手机号码" align="center" prop="guestPhone" min-width="120" show-overflow-tooltip>
         <template #default="scope">
-          {{ scope.row.contactPhone || '-' }}
+          {{ scope.row.guestPhone || '-' }}
         </template>
       </el-table-column>
       <el-table-column label="选择酒店" align="center" prop="hotelName" min-width="120" show-overflow-tooltip />
@@ -58,12 +58,43 @@
           <span v-else>-</span>
         </template>
       </el-table-column>
-      <el-table-column label="状态" align="center" prop="status" width="90">
+      <el-table-column label="是否补差" align="center" min-width="120">
+        <template #default="scope">
+          <span v-if="scope.row.isExtraPay === '1'"
+            >是 <span class="cell-extra-amount">金额:{{ scope.row.extraAmount || 0 }}</span></span
+          >
+          <span v-else>否</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="订房时间" align="center" prop="bookingTime" min-width="160" show-overflow-tooltip />
+      <el-table-column label="状态" align="center" prop="status" width="100">
         <template #default="scope">
           <el-tag :type="statusTagType[scope.row.status] || 'info'">{{ statusMap[scope.row.status] || '未知' }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="订房时间" align="center" prop="createTime" min-width="160" show-overflow-tooltip />
+      <el-table-column label="操作" align="center" width="150" fixed="right">
+        <template #default="scope">
+          <el-button
+            v-if="scope.row.status === '0' || scope.row.status === '1'"
+            v-hasPermi="['content:bookingRecord:cancel']"
+            link
+            type="danger"
+            @click="handleForceCancel(scope.row)"
+          >
+            手动退房
+          </el-button>
+          <el-button
+            v-if="scope.row.isExtraPay === '1' && scope.row.refundFlag !== '1'"
+            v-hasPermi="['content:bookingRecord:refund']"
+            link
+            type="warning"
+            @click="handleRefundExtra(scope.row)"
+          >
+            差价退回
+          </el-button>
+          <span v-if="scope.row.isExtraPay === '1' && scope.row.refundFlag === '1'" class="cell-refunded">差价已退回</span>
+        </template>
+      </el-table-column>
     </el-table>
 
     <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
@@ -71,7 +102,8 @@
 </template>
 
 <script setup name="BookingRecord">
-import { listBooking } from '@/api/content/bookingRecord';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { listBooking, forceCancelBooking, refundExtraBooking } from '@/api/content/bookingRecord';
 
 const { proxy } = getCurrentInstance();
 
@@ -89,8 +121,8 @@ const queryParams = reactive({
 });
 const queryRef = ref(null);
 
-const statusMap = { 0: '已预订', 1: '已取消', 2: '已完成' };
-const statusTagType = { 0: 'success', 1: 'info', 2: 'primary' };
+const statusMap = { 0: '预约成功', 1: '修改信息', 2: '退订房间' };
+const statusTagType = { 0: 'success', 1: 'warning', 2: 'info' };
 
 // 入住天数(含首尾两天)
 function stayDays(row) {
@@ -125,5 +157,45 @@ function handleExport() {
   proxy?.download('content/hotelBooking/export', { ...queryParams }, `booking_${new Date().getTime()}.xlsx`);
 }
 
+// 手动退房(后台强制退订):二次确认,退订后恢复房间库存
+function handleForceCancel(row) {
+  ElMessageBox.confirm(
+    `确定要手动退房吗?将强制退订订单「${row.bookingNo || ''}」(${row.hotelName || ''} ${row.roomName || ''}),并恢复该时段剩余房间`,
+    '系统提示',
+    { type: 'warning', confirmButtonText: '确认退房' }
+  )
+    .then(async () => {
+      await forceCancelBooking(row.id);
+      ElMessage.success('退房成功');
+      getList();
+    })
+    .catch(() => {});
+}
+
+// 差价退回:仅补差为“是”可操作,二次确认(登记退回,不走真实支付)
+function handleRefundExtra(row) {
+  ElMessageBox.confirm(`确定要将订单「${row.bookingNo || ''}」的补差金额 ${row.extraAmount || 0} 原路退回吗?`, '系统提示', {
+    type: 'warning',
+    confirmButtonText: '确认退回'
+  })
+    .then(async () => {
+      await refundExtraBooking(row.id);
+      ElMessage.success('差价已标记退回');
+      getList();
+    })
+    .catch(() => {});
+}
+
 getList();
 </script>
+
+<style scoped>
+.cell-extra-amount {
+  color: #e6a23c;
+}
+
+.cell-refunded {
+  font-size: 12px;
+  color: #909399;
+}
+</style>

+ 120 - 11
src/views/content/hotelBooking/index.vue

@@ -26,13 +26,13 @@
           {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
         </template>
       </el-table-column>
-      <el-table-column label="酒店名称" align="center" prop="hotelName" min-width="120" show-overflow-tooltip />
-      <el-table-column label="酒店简介" align="center" prop="introduction" min-width="140" show-overflow-tooltip>
+      <el-table-column label="酒店名称" align="center" prop="hotelName" min-width="100" show-overflow-tooltip />
+      <el-table-column label="酒店简介" align="center" prop="introduction" min-width="110" show-overflow-tooltip>
         <template #default="scope">
           {{ scope.row.introduction || '-' }}
         </template>
       </el-table-column>
-      <el-table-column label="宣传图片" align="center" width="180">
+      <el-table-column label="宣传图片" align="center" width="140">
         <template #default="scope">
           <div v-if="scope.row.imageList && scope.row.imageList.length > 0" class="cell-images">
             <el-image
@@ -50,11 +50,18 @@
           <span v-else class="cell-empty">暂无</span>
         </template>
       </el-table-column>
-      <el-table-column label="房型与时段" align="center" min-width="220">
+      <el-table-column label="关联道具" align="center" min-width="100" show-overflow-tooltip>
+        <template #default="scope">
+          {{ itemNameById(scope.row.itemId) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="房型与时段" align="center" min-width="180">
         <template #default="scope">
           <div v-if="scope.row.roomList && scope.row.roomList.length > 0" class="cell-room-block">
             <div v-for="(room, ri) in scope.row.roomList" :key="room.id || ri" class="cell-room-item">
-              <div class="cell-room-title">{{ ri + 1 }}.{{ room.roomName }}:</div>
+              <div class="cell-room-title">
+                {{ ri + 1 }}.{{ room.roomName }}:<span v-if="room.roomValue != null" class="cell-room-value">(价值:{{ room.roomValue }})</span>
+              </div>
               <div v-for="(p, pi) in room.periodList || []" :key="p.id || pi" class="cell-room-line">
                 时间段{{ pi + 1 }}:{{ p.startDate }}-{{ p.endDate }}
               </div>
@@ -64,7 +71,7 @@
           <span v-else class="cell-empty">暂无</span>
         </template>
       </el-table-column>
-      <el-table-column label="剩余数量" align="center" min-width="150">
+      <el-table-column label="剩余数量" align="center" min-width="120">
         <template #default="scope">
           <div v-if="scope.row.roomList && scope.row.roomList.length > 0" class="cell-room-block">
             <div v-for="(room, ri) in scope.row.roomList" :key="room.id || ri" class="cell-room-item">
@@ -76,7 +83,7 @@
           <span v-else class="cell-empty">暂无</span>
         </template>
       </el-table-column>
-      <el-table-column label="房间介绍" align="center" min-width="140">
+      <el-table-column label="房间介绍" align="center" min-width="120">
         <template #default="scope">
           <div v-if="scope.row.roomList && scope.row.roomList.length > 0" class="cell-room-block">
             <div v-for="(room, ri) in scope.row.roomList" :key="room.id || ri" class="cell-room-line cell-room-item" style="white-space: pre-line">
@@ -86,7 +93,7 @@
           <span v-else class="cell-empty">暂无</span>
         </template>
       </el-table-column>
-      <el-table-column label="房间图片" align="center" width="180">
+      <el-table-column label="房间图片" align="center" width="140">
         <template #default="scope">
           <div v-if="scope.row.roomList && scope.row.roomList.length > 0" class="cell-room-block">
             <div v-for="(room, ri) in scope.row.roomList" :key="room.id || ri" class="cell-room-images-row cell-room-item">
@@ -109,12 +116,23 @@
           <span v-else class="cell-empty">暂无</span>
         </template>
       </el-table-column>
+      <el-table-column label="酒店时效" align="center" min-width="130">
+        <template #default="scope">
+          <span v-if="scope.row.effectStartDate || scope.row.effectEndDate">
+            {{ scope.row.effectStartDate || '不限' }} 至 {{ scope.row.effectEndDate || '不限' }}
+          </span>
+          <span v-else class="cell-empty">不限</span>
+        </template>
+      </el-table-column>
       <el-table-column label="置顶排序" align="center" prop="sortOrder" width="90" />
-      <el-table-column label="操作" align="center" width="140" fixed="right">
+      <el-table-column label="操作" align="center" width="160" fixed="right">
         <template #default="scope">
           <div class="action-buttons">
             <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
             <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
+            <el-button link :type="scope.row.isShow === '1' ? 'warning' : 'success'" @click="handleChangeShow(scope.row)">
+              {{ scope.row.isShow === '1' ? '隐藏' : '展示' }}
+            </el-button>
           </div>
         </template>
       </el-table-column>
@@ -128,6 +146,25 @@
         <el-form-item label="酒店" prop="hotelName">
           <el-input v-model="form.hotelName" placeholder="请输入酒店名称" />
         </el-form-item>
+        <el-form-item label="道具关联">
+          <el-select v-model="form.itemId" placeholder="请选择关联的服务券-酒店道具" clearable filterable style="width: 300px">
+            <el-option v-for="item in itemOptions" :key="item.id" :label="item.name" :value="item.id" />
+          </el-select>
+          <span class="form-inline-tip">用于APP判定订单是否需补差价</span>
+        </el-form-item>
+        <el-form-item label="酒店时效">
+          <el-date-picker
+            :model-value="form.effectStartDate && form.effectEndDate ? [form.effectStartDate, form.effectEndDate] : null"
+            @update:model-value="(v) => setEffectRange(v)"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="YYYY-MM-DD"
+            style="width: 300px"
+          />
+          <span class="form-inline-tip">仅时效区间内APP列表展示,留空不限制</span>
+        </el-form-item>
         <el-form-item label="酒店简介" prop="introduction">
           <el-input v-model="form.introduction" placeholder="请输入酒店简介" />
         </el-form-item>
@@ -197,6 +234,10 @@
               <el-form-item label="房间" :prop="'rooms.' + ri + '.roomName'" :rules="rules.roomName" label-width="80px">
                 <el-input v-model="room.roomName" placeholder="请输入房间名称" style="width: 260px" />
               </el-form-item>
+              <el-form-item label="房间价值" label-width="80px">
+                <el-input-number v-model="room.roomValue" :min="0" :controls="false" placeholder="积分值" style="width: 160px" />
+                <span class="form-inline-tip">高于关联道具价值时APP显示补差价</span>
+              </el-form-item>
               <el-form-item label="款式名" label-width="80px">
                 <el-input v-model="room.styleName" placeholder="APP房型标签" style="width: 260px" />
               </el-form-item>
@@ -316,7 +357,8 @@
 </template>
 
 <script setup name="HotelBooking">
-import { listHotel, getHotel, addHotel, updateHotel, delHotel } from '@/api/content/hotel';
+import { listHotel, getHotel, addHotel, updateHotel, delHotel, changeHotelShow } from '@/api/content/hotel';
+import { selectServiceItemsSelList } from '@/api/system/business/items';
 import { uploadImageOnly } from '@/api/content/competitionZoneImage';
 import { ElMessage, ElMessageBox } from 'element-plus';
 
@@ -332,6 +374,15 @@ const queryParams = reactive({
 });
 const queryRef = ref(null);
 
+// 服务券-酒店道具选项(用于“道具关联”下拉与列表“关联道具”列名称回显)
+const itemOptions = ref([]);
+// 根据 itemId 取道具名称(列表展示用,未匹配到则显示ID)
+function itemNameById(itemId) {
+  if (!itemId) return '-';
+  const hit = itemOptions.value.find((i) => String(i.id) === String(itemId));
+  return hit ? hit.name : 'ID:' + itemId;
+}
+
 // ========== 弹窗表单 ==========
 const dialogVisible = ref(false);
 const dialogTitle = ref('');
@@ -343,7 +394,16 @@ const MAX_IMAGES = 5;
 
 // 时段:只含当天,剩余间数创建后可二次修改
 const createPeriod = () => ({ id: null, startDate: null, endDate: null, remainingCount: null });
-const createRoom = () => ({ id: null, roomName: '', styleName: '', coverImage: '', roomIntro: '', roomImages: [], periods: [createPeriod()] });
+const createRoom = () => ({
+  id: null,
+  roomName: '',
+  styleName: '',
+  coverImage: '',
+  roomIntro: '',
+  roomValue: null,
+  roomImages: [],
+  periods: [createPeriod()]
+});
 
 // 空白时段行/空白房型块:保存时过滤掉,不提交给后端
 function isPeriodEmpty(p) {
@@ -361,6 +421,11 @@ const initForm = {
   hotelName: '',
   introduction: '',
   location: '',
+  // 关联道具(服务券-酒店类型 items.id),用于APP判定是否补差价
+  itemId: null,
+  // 酒店时效(天到天),为空表示不限制;APP列表仅今天处于时效区间内才展示
+  effectStartDate: null,
+  effectEndDate: null,
   // 留空由后端自动分配为「当前最大值+1」,避免默认值与已有记录撞车导致新增失败
   sortOrder: null,
   promoImages: [],
@@ -480,6 +545,10 @@ async function handleUpdate(row) {
       hotelName: data.hotelName || '',
       introduction: data.introduction || '',
       location: data.location || '',
+      itemId: data.itemId ?? null,
+      effectStartDate: data.effectStartDate || null,
+      effectEndDate: data.effectEndDate || null,
+      isShow: data.isShow || '0',
       // 库里若为 0/NULL 这类无效值,保持为空强制用户重填,不能兜底成 0(后端会拒绝保存)
       sortOrder: data.sortOrder && data.sortOrder >= 1 ? data.sortOrder : null,
       promoImages: (data.imageList || []).map((img) => img.imageUrl),
@@ -489,6 +558,7 @@ async function handleUpdate(row) {
         styleName: room.styleName || '',
         coverImage: room.coverImage || '',
         roomIntro: room.roomIntro || '',
+        roomValue: room.roomValue ?? null,
         roomImages: (room.imageList || []).map((img) => img.imageUrl),
         periods: (room.periodList || []).map((p) => ({
           id: p.id,
@@ -587,6 +657,12 @@ function setPeriodRange(period, value) {
   period.endDate = value && value.length === 2 ? value[1] : null;
 }
 
+// 酒店时效日期范围:选两个日期回填到 effectStartDate/effectEndDate,清空则均为 null(不限制)
+function setEffectRange(value) {
+  form.value.effectStartDate = value && value.length === 2 ? value[0] : null;
+  form.value.effectEndDate = value && value.length === 2 ? value[1] : null;
+}
+
 // ========== 图片上传(仅上传OSS,不存图库) ==========
 async function uploadFileToUrl(rawFile) {
   if (!rawFile.type.startsWith('image/')) {
@@ -660,7 +736,35 @@ function moveImageDown(arr, index) {
   [arr[index], arr[index + 1]] = [arr[index + 1], arr[index]];
 }
 
+// ========== 展示/隐藏切换 ==========
+function handleChangeShow(row) {
+  // 当前为展示(1)则本次切为隐藏(0),反之切为展示
+  const nextShow = row.isShow === '1' ? '0' : '1';
+  const actionText = nextShow === '1' ? '展示' : '隐藏';
+  ElMessageBox.confirm(`确定${actionText}酒店「${row.hotelName}」在APP端的显示吗?`, '系统提示', {
+    type: 'warning',
+    confirmButtonText: '确认'
+  })
+    .then(async () => {
+      await changeHotelShow(row.id, nextShow);
+      ElMessage.success(`${actionText}成功`);
+      getList();
+    })
+    .catch(() => {});
+}
+
+// 加载服务券道具选项(供“道具关联”下拉与列表“关联道具”列名称回显)
+async function loadItemOptions() {
+  try {
+    const res = await selectServiceItemsSelList();
+    itemOptions.value = res.data || [];
+  } catch (err) {
+    console.error('加载道具列表失败:', err);
+  }
+}
+
 getList();
+loadItemOptions();
 </script>
 
 <style scoped>
@@ -714,6 +818,11 @@ getList();
   color: #303133;
 }
 
+.cell-room-value {
+  font-weight: normal;
+  color: #e6a23c;
+}
+
 .cell-room-line {
   color: #606266;
   padding-left: 8px;