酒店
This commit is contained in:
1 parent
3f7a980683
commit
3143da3f16
6 files changed
+1930
No files matched your search
@@ -0,0 +1,219 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface HotelZaxRoomVO {
|
||||
id?: number
|
||||
hotelId?: number
|
||||
hotelCode?: string
|
||||
roomTypeCode?: string
|
||||
roomTypeName?: string
|
||||
bedType?: string
|
||||
capacity?: number
|
||||
roomNumber?: number
|
||||
}
|
||||
|
||||
export interface HotelZaxProductVO {
|
||||
id?: number
|
||||
hotelId?: number
|
||||
hotelCode?: string
|
||||
roomTypeCode?: string
|
||||
productCode?: string
|
||||
productName?: string
|
||||
priceCode?: string
|
||||
cancelCode?: string
|
||||
mealType?: number
|
||||
mealCount?: number
|
||||
sellPrice?: number
|
||||
stock?: number
|
||||
}
|
||||
|
||||
export interface HotelZaxVO {
|
||||
id?: number
|
||||
hotelCode?: string
|
||||
hotelName?: string
|
||||
groupCode?: string
|
||||
coverImage?: string
|
||||
carouselImages?: string
|
||||
address?: string
|
||||
district?: string
|
||||
districtAdcode?: number
|
||||
bizArea?: string
|
||||
starLevel?: number
|
||||
score?: number
|
||||
facilityTags?: string
|
||||
longitude?: number
|
||||
latitude?: number
|
||||
phone?: string
|
||||
minSellPrice?: number
|
||||
status?: string
|
||||
supplierUpdateTime?: string
|
||||
syncedAt?: string
|
||||
intro?: string
|
||||
roomCount?: number
|
||||
productCount?: number
|
||||
rooms?: HotelZaxRoomVO[]
|
||||
products?: HotelZaxProductVO[]
|
||||
}
|
||||
|
||||
export interface HotelZaxPageReq {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
hotelName?: string
|
||||
hotelCode?: string
|
||||
status?: string
|
||||
district?: string
|
||||
districtAdcode?: number
|
||||
}
|
||||
|
||||
export const getHotelZaxPage = (params: HotelZaxPageReq) => {
|
||||
return request.get({ url: '/product/hotel-zax/page', params })
|
||||
}
|
||||
|
||||
export const getHotelZax = (id: number) => {
|
||||
return request.get({ url: '/product/hotel-zax/get', params: { id } })
|
||||
}
|
||||
|
||||
export const updateHotelZax = (data: HotelZaxVO) => {
|
||||
return request.put({ url: '/product/hotel-zax/update', data })
|
||||
}
|
||||
|
||||
export const updateHotelZaxStatus = (id: number, status: string) => {
|
||||
return request.put({ url: '/product/hotel-zax/update-status', data: { id, status } })
|
||||
}
|
||||
|
||||
export const saveHotelZaxRoom = (data: HotelZaxRoomVO) => {
|
||||
return request.post({ url: '/product/hotel-zax/room/save', data })
|
||||
}
|
||||
|
||||
export const deleteHotelZaxRoom = (hotelId: number, roomId: number) => {
|
||||
return request.delete({ url: '/product/hotel-zax/room/delete', params: { hotelId, roomId } })
|
||||
}
|
||||
|
||||
export const saveHotelZaxProduct = (data: HotelZaxProductVO) => {
|
||||
return request.post({ url: '/product/hotel-zax/product/save', data })
|
||||
}
|
||||
|
||||
export const deleteHotelZaxProduct = (hotelId: number, productId: number) => {
|
||||
return request.delete({ url: '/product/hotel-zax/product/delete', params: { hotelId, productId } })
|
||||
}
|
||||
|
||||
export interface HotelZaxDailyVO {
|
||||
id?: number
|
||||
hotelId?: number
|
||||
hotelCode?: string
|
||||
productId?: number
|
||||
productCode?: string
|
||||
roomTypeCode?: string
|
||||
stayDate?: string
|
||||
costPrice?: number
|
||||
sellPrice?: number
|
||||
stock?: number
|
||||
sold?: number
|
||||
remainStock?: number
|
||||
dailyStatus?: number
|
||||
}
|
||||
|
||||
export interface HotelZaxDailyPageReq {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
hotelId: number
|
||||
productCode?: string
|
||||
stayFrom?: string
|
||||
stayTo?: string
|
||||
}
|
||||
|
||||
export const getHotelZaxDailyPage = (params: HotelZaxDailyPageReq) => {
|
||||
return request.get({ url: '/product/hotel-zax/daily/page', params })
|
||||
}
|
||||
|
||||
export const saveHotelZaxDaily = (data: HotelZaxDailyVO) => {
|
||||
return request.post({ url: '/product/hotel-zax/daily/save', data })
|
||||
}
|
||||
|
||||
export const batchHotelZaxDaily = (data: {
|
||||
hotelId: number
|
||||
mode: 'FILL' | 'ADJUST'
|
||||
startDate: string
|
||||
endDate: string
|
||||
productCodes?: string[]
|
||||
costPrice?: number
|
||||
sellPrice?: number
|
||||
stock?: number
|
||||
dailyStatus?: number
|
||||
fillMissing?: boolean
|
||||
}) => {
|
||||
return request.post({ url: '/product/hotel-zax/daily/batch', data })
|
||||
}
|
||||
|
||||
export const deleteHotelZaxDaily = (hotelId: number, dailyId: number) => {
|
||||
return request.delete({ url: '/product/hotel-zax/daily/delete', params: { hotelId, dailyId } })
|
||||
}
|
||||
|
||||
export const syncHotelZaxDaily = (hotelId: number) => {
|
||||
return request.post({ url: '/product/hotel-zax/daily/sync', params: { hotelId } })
|
||||
}
|
||||
|
||||
export const getHotelZaxSyncCursor = () => {
|
||||
return request.get({ url: '/product/hotel-zax/sync-cursor' })
|
||||
}
|
||||
|
||||
/** full=true:按当前资安心授权范围全量拉(改授权后必须全量,否则增量水位会显示 0) */
|
||||
export const syncHotelZax = (full = true) => {
|
||||
return request.post({ url: '/product/hotel-zax/sync', params: { full } })
|
||||
}
|
||||
|
||||
export const resetHotelZaxCursor = () => {
|
||||
return request.post({ url: '/product/hotel-zax/reset-cursor' })
|
||||
}
|
||||
|
||||
export interface HotelZaxOrderVO {
|
||||
id?: number
|
||||
userId?: number
|
||||
channelNo?: string
|
||||
orderNo?: string
|
||||
platformNo?: string
|
||||
orderStatus?: string
|
||||
bookStatus?: string
|
||||
hotelCode?: string
|
||||
hotelName?: string
|
||||
productCode?: string
|
||||
productName?: string
|
||||
roomTypeCode?: string
|
||||
checkInDate?: string
|
||||
checkOutDate?: string
|
||||
nights?: number
|
||||
roomCount?: number
|
||||
contactName?: string
|
||||
contactPhone?: string
|
||||
guestJson?: string
|
||||
payOrderId?: number
|
||||
payStatus?: boolean
|
||||
payTime?: string
|
||||
sellAmount?: number
|
||||
failMsg?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
export interface HotelZaxOrderPageReq {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
hotelName?: string
|
||||
hotelCode?: string
|
||||
channelNo?: string
|
||||
orderNo?: string
|
||||
orderStatus?: string
|
||||
payStatus?: boolean
|
||||
contactPhone?: string
|
||||
userId?: number
|
||||
}
|
||||
|
||||
export const getHotelZaxOrderPage = (params: HotelZaxOrderPageReq) => {
|
||||
return request.get({ url: '/product/hotel-zax/order/page', params })
|
||||
}
|
||||
|
||||
export const getHotelZaxOrder = (id: number) => {
|
||||
return request.get({ url: '/product/hotel-zax/order/get', params: { id } })
|
||||
}
|
||||
|
||||
export const cancelHotelZaxOrder = (id: number, reason?: string) => {
|
||||
return request.post({ url: '/product/hotel-zax/order/cancel', params: { id, reason } })
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="title" size="72%" destroy-on-close @closed="reset">
|
||||
<div v-loading="loading">
|
||||
<div v-if="hotel" class="hotel-overview mb-16px">
|
||||
<div class="hotel-overview__hero">
|
||||
<div class="hotel-overview__cover">
|
||||
<el-image
|
||||
v-if="hotel.coverImage"
|
||||
:src="hotel.coverImage"
|
||||
fit="cover"
|
||||
class="hotel-overview__cover-img"
|
||||
:preview-src-list="[hotel.coverImage]"
|
||||
/>
|
||||
<div v-else class="hotel-overview__cover-empty">暂无主图</div>
|
||||
</div>
|
||||
<div class="hotel-overview__main">
|
||||
<div class="hotel-overview__name">{{ hotel.hotelName || '-' }}</div>
|
||||
<div class="hotel-overview__code">编码 {{ hotel.hotelCode || '-' }}</div>
|
||||
<div class="hotel-overview__tags">
|
||||
<el-tag
|
||||
:type="hotel.status === 'ACTIVE' ? 'success' : hotel.status === 'OUT_OF_SCOPE' ? 'warning' : 'info'"
|
||||
effect="dark"
|
||||
size="small"
|
||||
>
|
||||
{{
|
||||
hotel.status === 'ACTIVE'
|
||||
? '上架'
|
||||
: hotel.status === 'OUT_OF_SCOPE'
|
||||
? '未授权'
|
||||
: hotel.status === 'LOCAL_OFFLINE'
|
||||
? '本地下架'
|
||||
: '下架'
|
||||
}}
|
||||
</el-tag>
|
||||
<el-tag v-if="starLabel(hotel.starLevel) !== '-'" type="warning" effect="plain" size="small">
|
||||
{{ starLabel(hotel.starLevel) }}
|
||||
</el-tag>
|
||||
<el-tag v-if="hotel.score != null" type="danger" effect="plain" size="small">
|
||||
评分 {{ hotel.score }}
|
||||
</el-tag>
|
||||
<el-tag v-if="hotel.minSellPrice != null" effect="plain" size="small">
|
||||
起价 ¥{{ hotel.minSellPrice }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="hotel-overview__addr">
|
||||
<span class="hotel-overview__addr-label">地址</span>
|
||||
<span>{{ hotel.address || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hotel-overview__grid">
|
||||
<div class="hotel-overview__cell">
|
||||
<span class="hotel-overview__label">联系电话</span>
|
||||
<span class="hotel-overview__value">{{ hotel.phone || '-' }}</span>
|
||||
</div>
|
||||
<div class="hotel-overview__cell">
|
||||
<span class="hotel-overview__label">位置区域</span>
|
||||
<span class="hotel-overview__value">
|
||||
{{ hotel.district || '-' }}
|
||||
<span v-if="hotel.districtAdcode" class="text-gray-400">({{ hotel.districtAdcode }})</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="hotel-overview__cell">
|
||||
<span class="hotel-overview__label">商圈地标</span>
|
||||
<span class="hotel-overview__value">{{ hotel.bizArea || '-' }}</span>
|
||||
</div>
|
||||
<div class="hotel-overview__cell">
|
||||
<span class="hotel-overview__label">坐标</span>
|
||||
<span class="hotel-overview__value">
|
||||
<template v-if="hotel.longitude != null && hotel.latitude != null">
|
||||
{{ hotel.longitude }}, {{ hotel.latitude }}
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="hotel-overview__cell hotel-overview__cell--full">
|
||||
<span class="hotel-overview__label">设施标签</span>
|
||||
<span class="hotel-overview__value">
|
||||
<template v-if="facilityTagList.length">
|
||||
<el-tag
|
||||
v-for="(tag, idx) in facilityTagList"
|
||||
:key="`${tag}-${idx}`"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="mr-4px mb-4px"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="hotel.intro" class="hotel-overview__cell hotel-overview__cell--full">
|
||||
<span class="hotel-overview__label">简介</span>
|
||||
<div class="hotel-overview__value intro-html" v-html="hotel.intro"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HotelZaxInventory v-if="hotel?.id" :hotel-id="hotel.id" @changed="emit('changed')" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as HotelZaxApi from '@/api/mall/product/hotelZax'
|
||||
import HotelZaxInventory from './HotelZaxInventory.vue'
|
||||
|
||||
defineOptions({ name: 'HotelZaxDetail' })
|
||||
|
||||
const emit = defineEmits<{ (e: 'changed'): void }>()
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const hotel = ref<HotelZaxApi.HotelZaxVO | null>(null)
|
||||
|
||||
const title = computed(() => (hotel.value?.hotelName ? `酒店详情 - ${hotel.value.hotelName}` : '酒店详情'))
|
||||
const facilityTagList = computed(() =>
|
||||
(hotel.value?.facilityTags || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
const starLabel = (v?: number) => {
|
||||
const map: Record<number, string> = { 5: '五星', 4: '高档', 3: '舒适', 2: '经济', 1: '民宿' }
|
||||
return v ? map[v] || String(v) : '-'
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
hotel.value = null
|
||||
}
|
||||
|
||||
const load = async (id: number) => {
|
||||
loading.value = true
|
||||
try {
|
||||
hotel.value = await HotelZaxApi.getHotelZax(id)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const open = async (id: number) => {
|
||||
visible.value = true
|
||||
await load(id)
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hotel-overview__hero {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.hotel-overview__cover {
|
||||
width: 140px;
|
||||
height: 105px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
.hotel-overview__cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.hotel-overview__cover-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 13px;
|
||||
}
|
||||
.hotel-overview__name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.hotel-overview__code {
|
||||
margin-top: 4px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.hotel-overview__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.hotel-overview__addr {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.hotel-overview__addr-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
.hotel-overview__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px 20px;
|
||||
padding: 14px 16px;
|
||||
background: var(--el-fill-color-blank);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.hotel-overview__cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
.hotel-overview__cell--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.hotel-overview__label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.hotel-overview__value {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
.intro-html {
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.intro-html :deep(img) {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
:title="'编辑酒店'"
|
||||
size="80%"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@closed="reset"
|
||||
>
|
||||
<div v-loading="loading" class="pr-12px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="酒店编码">
|
||||
<el-input v-model="form.hotelCode" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="酒店名称" prop="hotelName">
|
||||
<el-input v-model="form.hotelName" maxlength="256" placeholder="酒店名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="上架状态">
|
||||
<el-switch
|
||||
v-model="form.status"
|
||||
active-value="ACTIVE"
|
||||
inactive-value="LOCAL_OFFLINE"
|
||||
inline-prompt
|
||||
active-text="上架"
|
||||
inactive-text="下架"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="form.phone" maxlength="32" placeholder="联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="酒店主图">
|
||||
<UploadImg v-model="form.coverImage" :limit="1" height="80px" width="80px" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="轮播图">
|
||||
<UploadImgs v-model="carouselList" :limit="9" />
|
||||
<div class="text-12px text-gray-400 mt-4px">多图上传,保存为逗号分隔 URL</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="位置区域">
|
||||
<el-input v-model="form.district" placeholder="如:南湖区" maxlength="128" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="区域编码">
|
||||
<el-input-number
|
||||
v-model="form.districtAdcode"
|
||||
class="!w-full"
|
||||
:controls="false"
|
||||
placeholder="adcode"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商圈地标">
|
||||
<el-input v-model="form.bizArea" maxlength="128" placeholder="如:嘉兴南站附近" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="起价(元)">
|
||||
<el-input-number
|
||||
v-model="form.minSellPrice"
|
||||
class="!w-full"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="经度">
|
||||
<el-input-number v-model="form.longitude" class="!w-full" :precision="6" :controls="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="纬度">
|
||||
<el-input-number v-model="form.latitude" class="!w-full" :precision="6" :controls="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="详细地址">
|
||||
<el-input v-model="form.address" type="textarea" :rows="2" maxlength="512" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="星级档次">
|
||||
<el-select v-model="form.starLevel" clearable placeholder="请选择" class="!w-full">
|
||||
<el-option :value="5" label="五星" />
|
||||
<el-option :value="4" label="高档" />
|
||||
<el-option :value="3" label="舒适" />
|
||||
<el-option :value="2" label="经济" />
|
||||
<el-option :value="1" label="民宿" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="评分">
|
||||
<el-input-number
|
||||
v-model="form.score"
|
||||
class="!w-full"
|
||||
:min="0"
|
||||
:max="5"
|
||||
:precision="1"
|
||||
:step="0.1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="设施标签">
|
||||
<el-select
|
||||
v-model="facilityTagList"
|
||||
class="!w-full"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="含早餐 / 免费取消 / 免费停车 等"
|
||||
>
|
||||
<el-option v-for="t in facilityOptions" :key="t" :label="t" :value="t" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="酒店简介">
|
||||
<Editor v-if="visible" v-model="form.intro" height="280px" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<el-divider v-if="form.id" content-position="left">房型 / 产品 / 日历</el-divider>
|
||||
<HotelZaxInventory v-if="form.id" :hotel-id="form.id" @changed="emit('success')" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保 存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Editor } from '@/components/Editor'
|
||||
import { UploadImg, UploadImgs } from '@/components/UploadFile'
|
||||
import * as HotelZaxApi from '@/api/mall/product/hotelZax'
|
||||
import HotelZaxInventory from './HotelZaxInventory.vue'
|
||||
|
||||
defineOptions({ name: 'HotelZaxForm' })
|
||||
|
||||
const message = useMessage()
|
||||
const emit = defineEmits<{ (e: 'success'): void }>()
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref()
|
||||
const carouselList = ref<string[]>([])
|
||||
const facilityTagList = ref<string[]>([])
|
||||
const facilityOptions = ['含早餐', '免费取消', '免费停车', '近地铁', '接送机', '健身房', '游泳池']
|
||||
|
||||
const form = reactive<HotelZaxApi.HotelZaxVO>({
|
||||
id: undefined,
|
||||
hotelCode: '',
|
||||
hotelName: '',
|
||||
status: 'ACTIVE',
|
||||
phone: '',
|
||||
coverImage: '',
|
||||
carouselImages: '',
|
||||
district: '',
|
||||
districtAdcode: undefined,
|
||||
bizArea: '',
|
||||
address: '',
|
||||
starLevel: undefined,
|
||||
score: undefined,
|
||||
facilityTags: '',
|
||||
longitude: undefined,
|
||||
latitude: undefined,
|
||||
minSellPrice: undefined,
|
||||
intro: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
hotelName: [{ required: true, message: '请输入酒店名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
formRef.value?.resetFields?.()
|
||||
carouselList.value = []
|
||||
facilityTagList.value = []
|
||||
}
|
||||
|
||||
const open = async (id: number) => {
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await HotelZaxApi.getHotelZax(id)
|
||||
Object.assign(form, {
|
||||
id: data.id,
|
||||
hotelCode: data.hotelCode,
|
||||
hotelName: data.hotelName,
|
||||
status: data.status || 'ACTIVE',
|
||||
phone: data.phone || '',
|
||||
coverImage: data.coverImage || '',
|
||||
carouselImages: data.carouselImages || '',
|
||||
district: data.district || '',
|
||||
districtAdcode: data.districtAdcode,
|
||||
bizArea: data.bizArea || '',
|
||||
address: data.address || '',
|
||||
starLevel: data.starLevel,
|
||||
score: data.score,
|
||||
facilityTags: data.facilityTags || '',
|
||||
longitude: data.longitude,
|
||||
latitude: data.latitude,
|
||||
minSellPrice: data.minSellPrice,
|
||||
intro: data.intro || ''
|
||||
})
|
||||
carouselList.value = (data.carouselImages || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
facilityTagList.value = (data.facilityTags || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
await formRef.value?.validate?.()
|
||||
saving.value = true
|
||||
try {
|
||||
await HotelZaxApi.updateHotelZax({
|
||||
...form,
|
||||
carouselImages: carouselList.value.filter(Boolean).join(','),
|
||||
facilityTags: facilityTagList.value.filter(Boolean).join(',')
|
||||
})
|
||||
message.success('保存成功')
|
||||
visible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -0,0 +1,675 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-12px"
|
||||
title="房型、产品、日历在本页维护。同步资安心会拉房型/产品和近 90 天日历;也可在日历 Tab 单独「从资安心拉取」。本地改过的日历再同步时会更新价态/配额,已售保留。"
|
||||
/>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane :label="`房型(${rooms.length})`" name="room">
|
||||
<div class="mb-8px">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
v-hasPermi="['product:hotel-zax:update']"
|
||||
@click="openRoomDialog()"
|
||||
>
|
||||
新增房型
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="rooms" border size="small" max-height="420">
|
||||
<el-table-column prop="roomTypeCode" label="房型编码" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="roomTypeName" label="房型名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="bedType" label="床型" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="capacity" label="可住" width="70" />
|
||||
<el-table-column prop="roomNumber" label="房间数" width="80" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" v-hasPermi="['product:hotel-zax:update']" @click="openRoomDialog(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" v-hasPermi="['product:hotel-zax:update']" @click="handleRemoveRoom(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="`产品(${products.length})`" name="product">
|
||||
<div class="mb-8px">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
v-hasPermi="['product:hotel-zax:update']"
|
||||
@click="openProductDialog()"
|
||||
>
|
||||
新增产品
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="products" border size="small" max-height="420">
|
||||
<el-table-column prop="productCode" label="产品编码" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="roomTypeCode" label="所属房型" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="mealCount" label="早餐份数" width="90" />
|
||||
<el-table-column prop="sellPrice" label="卖价(元)" width="100" />
|
||||
<el-table-column prop="stock" label="库存" width="80" />
|
||||
<el-table-column prop="priceCode" label="价格码" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
v-hasPermi="['product:hotel-zax:update']"
|
||||
@click="openBatchDailyDialog('FILL', row.productCode)"
|
||||
>
|
||||
生成日历
|
||||
</el-button>
|
||||
<el-button link type="primary" v-hasPermi="['product:hotel-zax:update']" @click="openProductDialog(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
v-hasPermi="['product:hotel-zax:update']"
|
||||
@click="handleRemoveProduct(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="日历" name="daily">
|
||||
<div class="mb-8px flex flex-wrap gap-8px items-center">
|
||||
<el-select
|
||||
v-model="dailyProductCode"
|
||||
clearable
|
||||
placeholder="按产品筛选"
|
||||
class="!w-220px"
|
||||
@change="loadDaily"
|
||||
>
|
||||
<el-option
|
||||
v-for="p in products"
|
||||
:key="p.productCode"
|
||||
:label="p.productName || p.productCode"
|
||||
:value="p.productCode!"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="dailySyncing"
|
||||
v-hasPermi="['product:hotel-zax:sync']"
|
||||
@click="handleSyncDaily"
|
||||
>
|
||||
从资安心拉取
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
v-hasPermi="['product:hotel-zax:update']"
|
||||
@click="openBatchDailyDialog('FILL')"
|
||||
>
|
||||
批量生成
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
size="small"
|
||||
v-hasPermi="['product:hotel-zax:update']"
|
||||
@click="openBatchDailyDialog('ADJUST')"
|
||||
>
|
||||
批量调价
|
||||
</el-button>
|
||||
<el-button size="small" v-hasPermi="['product:hotel-zax:update']" @click="openDailyDialog()">
|
||||
单日编辑
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table v-loading="dailyLoading" :data="dailyList" border size="small" max-height="420">
|
||||
<el-table-column label="日期" width="120">
|
||||
<template #default="{ row }">{{ formatStayDate(row.stayDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="productCode" label="产品编码" min-width="120" />
|
||||
<el-table-column prop="roomTypeCode" label="房型" min-width="100" />
|
||||
<el-table-column prop="costPrice" label="成本价(元)" width="100" />
|
||||
<el-table-column prop="sellPrice" label="卖价(元)" width="100" />
|
||||
<el-table-column prop="stock" label="配额" width="70" />
|
||||
<el-table-column prop="sold" label="已售" width="70" />
|
||||
<el-table-column label="可售" width="70">
|
||||
<template #default="{ row }">
|
||||
{{ row.remainStock != null ? row.remainStock : Math.max(0, (row.stock || 0) - (row.sold || 0)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dailyStatus" label="房态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.dailyStatus === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.dailyStatus === 1 ? '开房' : '关房' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" v-hasPermi="['product:hotel-zax:update']" @click="openDailyDialog(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" v-hasPermi="['product:hotel-zax:update']" @click="handleRemoveDaily(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt-12px flex justify-end">
|
||||
<el-pagination
|
||||
v-model:current-page="dailyPage.pageNo"
|
||||
v-model:page-size="dailyPage.pageSize"
|
||||
:total="dailyPage.total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="loadDaily"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="roomVisible" :title="roomForm.id ? '编辑房型' : '新增房型'" width="520px" destroy-on-close>
|
||||
<el-form :model="roomForm" label-width="90px">
|
||||
<el-form-item label="房型编码" required>
|
||||
<el-input v-model="roomForm.roomTypeCode" :disabled="!!roomForm.id" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="房型名称" required>
|
||||
<el-input v-model="roomForm.roomTypeName" maxlength="128" />
|
||||
</el-form-item>
|
||||
<el-form-item label="床型">
|
||||
<el-input v-model="roomForm.bedType" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="可住人数">
|
||||
<el-input-number v-model="roomForm.capacity" :min="1" :max="20" />
|
||||
</el-form-item>
|
||||
<el-form-item label="房间数">
|
||||
<el-input-number v-model="roomForm.roomNumber" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="roomVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="subSaving" @click="submitRoom">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="productVisible" :title="productForm.id ? '编辑产品' : '新增产品'" width="560px" destroy-on-close>
|
||||
<el-form :model="productForm" label-width="90px">
|
||||
<el-form-item label="产品编码" required>
|
||||
<el-input v-model="productForm.productCode" :disabled="!!productForm.id" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称" required>
|
||||
<el-input v-model="productForm.productName" maxlength="128" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属房型" required>
|
||||
<el-select v-model="productForm.roomTypeCode" class="!w-full" filterable>
|
||||
<el-option
|
||||
v-for="r in rooms"
|
||||
:key="r.roomTypeCode"
|
||||
:label="`${r.roomTypeName || r.roomTypeCode}(${r.roomTypeCode})`"
|
||||
:value="r.roomTypeCode!"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="早餐份数">
|
||||
<el-input-number v-model="productForm.mealCount" :min="0" :max="20" />
|
||||
</el-form-item>
|
||||
<el-form-item label="卖价(元)">
|
||||
<el-input-number v-model="productForm.sellPrice" :min="0" :precision="2" />
|
||||
</el-form-item>
|
||||
<el-form-item label="库存">
|
||||
<el-input-number v-model="productForm.stock" :min="0" :max="99999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="价格码">
|
||||
<el-input v-model="productForm.priceCode" maxlength="64" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="productVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="subSaving" @click="submitProduct">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="dailyVisible" :title="dailyForm.id ? '编辑日历' : '新增日历'" width="520px" destroy-on-close>
|
||||
<el-form :model="dailyForm" label-width="100px">
|
||||
<el-form-item label="产品" required>
|
||||
<el-select v-model="dailyForm.productCode" class="!w-full" @change="onDailyProductChange">
|
||||
<el-option
|
||||
v-for="p in products"
|
||||
:key="p.productCode"
|
||||
:label="p.productName || p.productCode"
|
||||
:value="p.productCode!"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="日期" required>
|
||||
<el-date-picker v-model="dailyForm.stayDate" type="date" value-format="YYYY-MM-DD" class="!w-full" />
|
||||
</el-form-item>
|
||||
<el-form-item label="成本价">
|
||||
<el-input-number v-model="dailyForm.costPrice" :min="0" :precision="2" class="!w-full" />
|
||||
</el-form-item>
|
||||
<el-form-item label="卖价" required>
|
||||
<el-input-number v-model="dailyForm.sellPrice" :min="0" :precision="2" class="!w-full" />
|
||||
</el-form-item>
|
||||
<el-form-item label="当日配额" required>
|
||||
<el-input-number v-model="dailyForm.stock" :min="0" class="!w-full" />
|
||||
<div class="text-12px text-gray-400 mt-4px">可售 = 配额 − 已售;已售由下单累加,不可手工改</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dailyForm.id" label="已售">
|
||||
<span>{{ dailyForm.sold ?? 0 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="房态" required>
|
||||
<el-select v-model="dailyForm.dailyStatus" class="!w-full">
|
||||
<el-option label="开房" :value="1" />
|
||||
<el-option label="关房" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dailyVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="subSaving" @click="submitDaily">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="batchVisible"
|
||||
:title="batchForm.mode === 'ADJUST' ? '批量调价' : '批量生成日历'"
|
||||
width="560px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-12px"
|
||||
:type="batchForm.mode === 'ADJUST' ? 'warning' : 'info'"
|
||||
:title="
|
||||
batchForm.mode === 'ADJUST'
|
||||
? '对区间内已有日历改价/配额/房态,已售不动;未填的字段不改。'
|
||||
: '按产品默认卖价/配额补齐缺失日期,已有日期自动跳过。单次最长 366 天。'
|
||||
"
|
||||
/>
|
||||
<el-form :model="batchForm" label-width="110px">
|
||||
<el-form-item label="日期区间" required>
|
||||
<el-date-picker
|
||||
v-model="batchRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品">
|
||||
<el-select
|
||||
v-model="batchForm.productCodes"
|
||||
multiple
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="空=全部产品"
|
||||
class="!w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="p in products"
|
||||
:key="p.productCode"
|
||||
:label="p.productName || p.productCode"
|
||||
:value="p.productCode!"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="batchForm.mode === 'ADJUST' ? '卖价(可选)' : '卖价覆盖'">
|
||||
<el-input-number v-model="batchForm.sellPrice" :min="0" :precision="2" class="!w-full" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="batchForm.mode === 'ADJUST' ? '成本价(可选)' : '成本覆盖'">
|
||||
<el-input-number v-model="batchForm.costPrice" :min="0" :precision="2" class="!w-full" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="batchForm.mode === 'ADJUST' ? '配额(可选)' : '配额覆盖'">
|
||||
<el-input-number v-model="batchForm.stock" :min="0" class="!w-full" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="batchForm.mode === 'ADJUST' ? '房态(可选)' : '房态'">
|
||||
<el-select v-model="batchForm.dailyStatus" clearable placeholder="不改 / 默认开房" class="!w-full">
|
||||
<el-option label="开房" :value="1" />
|
||||
<el-option label="关房" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="batchForm.mode === 'ADJUST'" label="同时补齐">
|
||||
<el-switch v-model="batchForm.fillMissing" />
|
||||
<span class="ml-8px text-12px text-gray-500">勾选后,区间内没有的日期也会按产品默认生成</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="batchVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="batchSaving" @click="submitBatchDaily">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as HotelZaxApi from '@/api/mall/product/hotelZax'
|
||||
|
||||
defineOptions({ name: 'HotelZaxInventory' })
|
||||
|
||||
const props = defineProps<{ hotelId: number }>()
|
||||
const emit = defineEmits<{ (e: 'changed'): void }>()
|
||||
const message = useMessage()
|
||||
|
||||
const activeTab = ref('room')
|
||||
const rooms = ref<HotelZaxApi.HotelZaxRoomVO[]>([])
|
||||
const products = ref<HotelZaxApi.HotelZaxProductVO[]>([])
|
||||
const dailyList = ref<HotelZaxApi.HotelZaxDailyVO[]>([])
|
||||
const dailyLoading = ref(false)
|
||||
const dailySyncing = ref(false)
|
||||
const dailyProductCode = ref('')
|
||||
const dailyPage = reactive({ pageNo: 1, pageSize: 10, total: 0 })
|
||||
const subSaving = ref(false)
|
||||
const batchSaving = ref(false)
|
||||
|
||||
/** LocalDate 被序列成 [y,m,d] 时表格会显示成 2026,11,2 */
|
||||
const formatStayDate = (raw: unknown) => {
|
||||
if (raw == null || raw === '') return '-'
|
||||
if (Array.isArray(raw) && raw.length >= 3) {
|
||||
const y = Number(raw[0])
|
||||
const m = Number(raw[1])
|
||||
const d = Number(raw[2])
|
||||
if (y > 0 && m > 0 && d > 0) {
|
||||
return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
const s = String(raw).trim()
|
||||
const comma = s.match(/^(\d{4}),(\d{1,2}),(\d{1,2})/)
|
||||
if (comma) {
|
||||
return `${comma[1]}-${comma[2].padStart(2, '0')}-${comma[3].padStart(2, '0')}`
|
||||
}
|
||||
return s.length >= 10 ? s.slice(0, 10) : s
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
if (!props.hotelId) return
|
||||
const data = await HotelZaxApi.getHotelZax(props.hotelId)
|
||||
rooms.value = data.rooms || []
|
||||
products.value = data.products || []
|
||||
await loadDaily()
|
||||
}
|
||||
|
||||
const handleSyncDaily = async () => {
|
||||
dailySyncing.value = true
|
||||
try {
|
||||
const res = await HotelZaxApi.syncHotelZaxDaily(props.hotelId)
|
||||
message.success(`已拉取日历 ${res?.upsert ?? 0} 条`)
|
||||
dailyPage.pageNo = 1
|
||||
await loadDaily()
|
||||
emit('changed')
|
||||
} finally {
|
||||
dailySyncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadDaily = async () => {
|
||||
if (!props.hotelId) return
|
||||
dailyLoading.value = true
|
||||
try {
|
||||
const data = await HotelZaxApi.getHotelZaxDailyPage({
|
||||
pageNo: dailyPage.pageNo,
|
||||
pageSize: dailyPage.pageSize,
|
||||
hotelId: props.hotelId,
|
||||
productCode: dailyProductCode.value || undefined
|
||||
})
|
||||
dailyList.value = data.list || []
|
||||
dailyPage.total = data.total || 0
|
||||
} finally {
|
||||
dailyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.hotelId,
|
||||
() => {
|
||||
activeTab.value = 'room'
|
||||
dailyProductCode.value = ''
|
||||
dailyPage.pageNo = 1
|
||||
load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const roomVisible = ref(false)
|
||||
const roomForm = reactive<HotelZaxApi.HotelZaxRoomVO>({
|
||||
id: undefined,
|
||||
hotelId: undefined,
|
||||
roomTypeCode: '',
|
||||
roomTypeName: '',
|
||||
bedType: '',
|
||||
capacity: 2,
|
||||
roomNumber: 1
|
||||
})
|
||||
|
||||
const openRoomDialog = (row?: HotelZaxApi.HotelZaxRoomVO) => {
|
||||
Object.assign(roomForm, {
|
||||
id: row?.id,
|
||||
hotelId: props.hotelId,
|
||||
roomTypeCode: row?.roomTypeCode || '',
|
||||
roomTypeName: row?.roomTypeName || '',
|
||||
bedType: row?.bedType || '',
|
||||
capacity: row?.capacity ?? 2,
|
||||
roomNumber: row?.roomNumber ?? 1
|
||||
})
|
||||
roomVisible.value = true
|
||||
}
|
||||
|
||||
const submitRoom = async () => {
|
||||
if (!roomForm.roomTypeCode || !roomForm.roomTypeName) {
|
||||
message.warning('请填写房型编码和名称')
|
||||
return
|
||||
}
|
||||
subSaving.value = true
|
||||
try {
|
||||
await HotelZaxApi.saveHotelZaxRoom(roomForm)
|
||||
message.success('保存成功')
|
||||
roomVisible.value = false
|
||||
await load()
|
||||
emit('changed')
|
||||
} finally {
|
||||
subSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveRoom = async (row: HotelZaxApi.HotelZaxRoomVO) => {
|
||||
if (!row.id) return
|
||||
try {
|
||||
await message.confirm(`确认删除房型「${row.roomTypeName || row.roomTypeCode}」?`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await HotelZaxApi.deleteHotelZaxRoom(props.hotelId, row.id)
|
||||
message.success('已删除')
|
||||
await load()
|
||||
emit('changed')
|
||||
}
|
||||
|
||||
const productVisible = ref(false)
|
||||
const productForm = reactive<HotelZaxApi.HotelZaxProductVO>({
|
||||
id: undefined,
|
||||
hotelId: undefined,
|
||||
productCode: '',
|
||||
productName: '',
|
||||
roomTypeCode: '',
|
||||
mealCount: 0,
|
||||
sellPrice: undefined,
|
||||
stock: 0,
|
||||
priceCode: ''
|
||||
})
|
||||
|
||||
const openProductDialog = (row?: HotelZaxApi.HotelZaxProductVO) => {
|
||||
Object.assign(productForm, {
|
||||
id: row?.id,
|
||||
hotelId: props.hotelId,
|
||||
productCode: row?.productCode || '',
|
||||
productName: row?.productName || '',
|
||||
roomTypeCode: row?.roomTypeCode || rooms.value[0]?.roomTypeCode || '',
|
||||
mealCount: row?.mealCount ?? 0,
|
||||
sellPrice: row?.sellPrice,
|
||||
stock: row?.stock ?? 0,
|
||||
priceCode: row?.priceCode || ''
|
||||
})
|
||||
productVisible.value = true
|
||||
}
|
||||
|
||||
const submitProduct = async () => {
|
||||
if (!productForm.productCode || !productForm.productName || !productForm.roomTypeCode) {
|
||||
message.warning('请填写产品编码、名称和所属房型')
|
||||
return
|
||||
}
|
||||
subSaving.value = true
|
||||
try {
|
||||
await HotelZaxApi.saveHotelZaxProduct(productForm)
|
||||
message.success('保存成功')
|
||||
productVisible.value = false
|
||||
await load()
|
||||
emit('changed')
|
||||
} finally {
|
||||
subSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveProduct = async (row: HotelZaxApi.HotelZaxProductVO) => {
|
||||
if (!row.id) return
|
||||
try {
|
||||
await message.confirm(`确认删除产品「${row.productName || row.productCode}」?其日历也会一并删除。`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await HotelZaxApi.deleteHotelZaxProduct(props.hotelId, row.id)
|
||||
message.success('已删除')
|
||||
await load()
|
||||
emit('changed')
|
||||
}
|
||||
|
||||
const dailyVisible = ref(false)
|
||||
const dailyForm = reactive<HotelZaxApi.HotelZaxDailyVO>({
|
||||
id: undefined,
|
||||
hotelId: undefined,
|
||||
productCode: '',
|
||||
stayDate: '',
|
||||
costPrice: 0,
|
||||
sellPrice: undefined,
|
||||
stock: 0,
|
||||
sold: 0,
|
||||
dailyStatus: 1
|
||||
})
|
||||
|
||||
const onDailyProductChange = (code?: string) => {
|
||||
const p = products.value.find((x) => x.productCode === code)
|
||||
if (!p) return
|
||||
if (dailyForm.sellPrice == null) dailyForm.sellPrice = p.sellPrice
|
||||
if (dailyForm.stock == null || dailyForm.stock === 0) dailyForm.stock = p.stock ?? 0
|
||||
}
|
||||
|
||||
const openDailyDialog = (row?: HotelZaxApi.HotelZaxDailyVO) => {
|
||||
const first = products.value[0]
|
||||
Object.assign(dailyForm, {
|
||||
id: row?.id,
|
||||
hotelId: props.hotelId,
|
||||
productCode: row?.productCode || first?.productCode || '',
|
||||
stayDate: row?.stayDate || '',
|
||||
costPrice: row?.costPrice ?? 0,
|
||||
sellPrice: row?.sellPrice ?? first?.sellPrice,
|
||||
stock: row?.stock ?? first?.stock ?? 0,
|
||||
sold: row?.sold ?? 0,
|
||||
dailyStatus: row?.dailyStatus ?? 1
|
||||
})
|
||||
dailyVisible.value = true
|
||||
}
|
||||
|
||||
const submitDaily = async () => {
|
||||
if (!dailyForm.productCode || !dailyForm.stayDate || dailyForm.sellPrice == null || dailyForm.stock == null
|
||||
|| dailyForm.dailyStatus == null) {
|
||||
message.warning('请填写产品、日期、卖价、配额和房态')
|
||||
return
|
||||
}
|
||||
subSaving.value = true
|
||||
try {
|
||||
await HotelZaxApi.saveHotelZaxDaily(dailyForm)
|
||||
message.success('保存成功')
|
||||
dailyVisible.value = false
|
||||
await loadDaily()
|
||||
emit('changed')
|
||||
} finally {
|
||||
subSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveDaily = async (row: HotelZaxApi.HotelZaxDailyVO) => {
|
||||
if (!row.id) return
|
||||
try {
|
||||
await message.confirm(`确认删除 ${row.stayDate} / ${row.productCode} 的日历?`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await HotelZaxApi.deleteHotelZaxDaily(props.hotelId, row.id)
|
||||
message.success('已删除')
|
||||
await loadDaily()
|
||||
emit('changed')
|
||||
}
|
||||
|
||||
const batchVisible = ref(false)
|
||||
const batchRange = ref<string[]>([])
|
||||
const batchForm = reactive({
|
||||
mode: 'FILL' as 'FILL' | 'ADJUST',
|
||||
productCodes: [] as string[],
|
||||
sellPrice: undefined as number | undefined,
|
||||
costPrice: undefined as number | undefined,
|
||||
stock: undefined as number | undefined,
|
||||
dailyStatus: undefined as number | undefined,
|
||||
fillMissing: false
|
||||
})
|
||||
|
||||
const openBatchDailyDialog = (mode: 'FILL' | 'ADJUST', productCode?: string) => {
|
||||
batchForm.mode = mode
|
||||
batchForm.productCodes = productCode ? [productCode] : []
|
||||
batchForm.sellPrice = undefined
|
||||
batchForm.costPrice = undefined
|
||||
batchForm.stock = undefined
|
||||
batchForm.dailyStatus = mode === 'FILL' ? 1 : undefined
|
||||
batchForm.fillMissing = false
|
||||
const today = new Date()
|
||||
const end = new Date()
|
||||
end.setDate(today.getDate() + 29)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
const fmt = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||
batchRange.value = [fmt(today), fmt(end)]
|
||||
batchVisible.value = true
|
||||
}
|
||||
|
||||
const submitBatchDaily = async () => {
|
||||
if (!batchRange.value || batchRange.value.length !== 2) {
|
||||
message.warning('请选择日期区间')
|
||||
return
|
||||
}
|
||||
batchSaving.value = true
|
||||
try {
|
||||
const res = await HotelZaxApi.batchHotelZaxDaily({
|
||||
hotelId: props.hotelId,
|
||||
mode: batchForm.mode,
|
||||
startDate: batchRange.value[0],
|
||||
endDate: batchRange.value[1],
|
||||
productCodes: batchForm.productCodes.length ? batchForm.productCodes : undefined,
|
||||
sellPrice: batchForm.sellPrice,
|
||||
costPrice: batchForm.costPrice,
|
||||
stock: batchForm.stock,
|
||||
dailyStatus: batchForm.dailyStatus,
|
||||
fillMissing: batchForm.fillMissing
|
||||
})
|
||||
message.success(`完成:新增 ${res?.inserted ?? 0},更新 ${res?.updated ?? 0}`)
|
||||
batchVisible.value = false
|
||||
activeTab.value = 'daily'
|
||||
await loadDaily()
|
||||
emit('changed')
|
||||
} finally {
|
||||
batchSaving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-alert
|
||||
class="mb-15px"
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="资安心酒店本地库:列表/详情读本地;可编辑展示信息、房型、产品与日历。同步可能覆盖房型/产品;已生成的本地日历优先于供应方价态。"
|
||||
/>
|
||||
<el-descriptions v-if="cursor" :column="4" border class="mb-15px" size="small">
|
||||
<el-descriptions-item label="本地总数">{{ cursor.total ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上架数">{{ cursor.active ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="同步水位">{{ cursor.updatedSince || '(全量起点)' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上次同步">{{
|
||||
cursor.lastSyncAt ? formatDate(new Date(cursor.lastSyncAt)) : '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="上次结果" :span="4">{{ cursor.lastMsg || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="酒店名称" prop="hotelName">
|
||||
<el-input
|
||||
v-model="queryParams.hotelName"
|
||||
placeholder="酒店名称"
|
||||
clearable
|
||||
class="!w-200px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="酒店编码" prop="hotelCode">
|
||||
<el-input
|
||||
v-model="queryParams.hotelCode"
|
||||
placeholder="hotelCode"
|
||||
clearable
|
||||
class="!w-180px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="上架状态" prop="status">
|
||||
<el-select v-model="queryParams.status" clearable placeholder="全部" class="!w-140px">
|
||||
<el-option label="上架" value="ACTIVE" />
|
||||
<el-option label="本地下架" value="LOCAL_OFFLINE" />
|
||||
<el-option label="供应方下架" value="OFFLINE" />
|
||||
<el-option label="未授权" value="OUT_OF_SCOPE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="区域" prop="district">
|
||||
<el-input
|
||||
v-model="queryParams.district"
|
||||
placeholder="区域名"
|
||||
clearable
|
||||
class="!w-180px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="syncing"
|
||||
v-hasPermi="['product:hotel-zax:sync']"
|
||||
@click="handleSync"
|
||||
>
|
||||
<Icon icon="ep:refresh" class="mr-5px" />同步资安心
|
||||
</el-button>
|
||||
<el-button plain v-hasPermi="['product:hotel-zax:sync']" @click="handleResetCursor">
|
||||
重置水位
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" border :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="封面" width="72" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.coverImage"
|
||||
:src="row.coverImage"
|
||||
class="h-40px w-40px rounded-4px"
|
||||
fit="cover"
|
||||
:preview-src-list="[row.coverImage]"
|
||||
preview-teleported
|
||||
/>
|
||||
<span v-else class="text-gray-400">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="酒店编码" prop="hotelCode" min-width="120" />
|
||||
<el-table-column label="酒店名称" prop="hotelName" min-width="180" />
|
||||
<el-table-column label="位置区域" prop="district" min-width="140">
|
||||
<template #default="{ row }">{{ row.district || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商圈" prop="bizArea" width="120">
|
||||
<template #default="{ row }">{{ cleanText(row.bizArea) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上架状态" prop="status" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-if="checkPermi(['product:hotel-zax:update'])"
|
||||
:model-value="row.status === 'ACTIVE'"
|
||||
inline-prompt
|
||||
active-text="上架"
|
||||
inactive-text="下架"
|
||||
:disabled="listingId === row.id"
|
||||
@change="(on: boolean) => handleToggleListing(row, on)"
|
||||
/>
|
||||
<el-tag v-else-if="row.status === 'ACTIVE'" type="success" size="small">上架</el-tag>
|
||||
<el-tag v-else-if="row.status === 'OUT_OF_SCOPE'" type="warning" size="small">未授权</el-tag>
|
||||
<el-tag v-else type="info" size="small">下架</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="房型数" prop="roomCount" width="80" align="center">
|
||||
<template #default="{ row }">{{ row.roomCount ?? 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品数" prop="productCount" width="80" align="center">
|
||||
<template #default="{ row }">{{ row.productCount ?? 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="起价" prop="minSellPrice" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.minSellPrice != null ? `¥${row.minSellPrice}` : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="同步时间"
|
||||
prop="syncedAt"
|
||||
width="170"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" v-hasPermi="['product:hotel-zax:query']" @click="openDetail(row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button link type="primary" v-hasPermi="['product:hotel-zax:update']" @click="openEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<HotelZaxForm ref="formRef" @success="onChanged" />
|
||||
<HotelZaxDetail ref="detailRef" @changed="onChanged" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dateFormatter, formatDate } from '@/utils/formatTime'
|
||||
import { checkPermi } from '@/utils/permission'
|
||||
import * as HotelZaxApi from '@/api/mall/product/hotelZax'
|
||||
import HotelZaxForm from './HotelZaxForm.vue'
|
||||
import HotelZaxDetail from './HotelZaxDetail.vue'
|
||||
|
||||
defineOptions({ name: 'ProductHotelZax' })
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const loading = ref(false)
|
||||
const syncing = ref(false)
|
||||
const listingId = ref<number>()
|
||||
const total = ref(0)
|
||||
const list = ref<HotelZaxApi.HotelZaxVO[]>([])
|
||||
const cursor = ref<any>(null)
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref<InstanceType<typeof HotelZaxForm>>()
|
||||
const detailRef = ref<InstanceType<typeof HotelZaxDetail>>()
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
hotelName: undefined as string | undefined,
|
||||
hotelCode: undefined as string | undefined,
|
||||
status: undefined as string | undefined,
|
||||
district: undefined as string | undefined
|
||||
})
|
||||
|
||||
const cleanText = (v?: string) => {
|
||||
if (!v) return '-'
|
||||
const s = v.trim()
|
||||
if (s === '[]' || s === '{}' || s === '()') return '-'
|
||||
return s
|
||||
}
|
||||
|
||||
const loadCursor = async () => {
|
||||
try {
|
||||
cursor.value = await HotelZaxApi.getHotelZaxSyncCursor()
|
||||
} catch {
|
||||
cursor.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await HotelZaxApi.getHotelZaxPage(queryParams)
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const onChanged = async () => {
|
||||
await loadCursor()
|
||||
await getList()
|
||||
}
|
||||
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
await message.confirm(
|
||||
'将按资安心当前授权范围全量同步到本地(改授权后也请用此按钮)。不在授权内的本地酒店会标为下架。确认继续?'
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
syncing.value = true
|
||||
try {
|
||||
const res = await HotelZaxApi.syncHotelZax(true)
|
||||
const offline = res?.offlineOutOfScope ?? 0
|
||||
const dailyFail = res?.dailyFail ?? 0
|
||||
message.success(
|
||||
`同步完成:写入 ${res?.upsert ?? 0},详情 ${res?.detailOk ?? 0},日历 ${res?.dailyRows ?? 0} 条` +
|
||||
(dailyFail > 0 ? `,日历失败 ${dailyFail} 家` : '') +
|
||||
(offline > 0 ? `,下架授权外 ${offline}` : '')
|
||||
)
|
||||
await onChanged()
|
||||
} catch (e: any) {
|
||||
message.error(e?.msg || e?.message || '同步失败')
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetCursor = async () => {
|
||||
try {
|
||||
await message.confirm('重置水位后,下次同步会从最早变更重新拉(仍按授权范围)。确认?')
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await HotelZaxApi.resetHotelZaxCursor()
|
||||
message.success('已重置')
|
||||
await loadCursor()
|
||||
}
|
||||
|
||||
const handleToggleListing = async (row: HotelZaxApi.HotelZaxVO, on: boolean) => {
|
||||
if (!row.id) return
|
||||
listingId.value = row.id
|
||||
try {
|
||||
// 后台下架用 LOCAL_OFFLINE,避免与供应方下架/授权外混淆;同步不会冲掉
|
||||
await HotelZaxApi.updateHotelZaxStatus(row.id, on ? 'ACTIVE' : 'LOCAL_OFFLINE')
|
||||
row.status = on ? 'ACTIVE' : 'LOCAL_OFFLINE'
|
||||
message.success(on ? '已上架' : '已下架')
|
||||
await loadCursor()
|
||||
} catch (e: any) {
|
||||
message.error(e?.msg || e?.message || '操作失败')
|
||||
} finally {
|
||||
listingId.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const openDetail = (row: HotelZaxApi.HotelZaxVO) => {
|
||||
if (!row.id) return
|
||||
detailRef.value?.open(row.id)
|
||||
}
|
||||
|
||||
const openEdit = (row: HotelZaxApi.HotelZaxVO) => {
|
||||
if (!row.id) return
|
||||
formRef.value?.open(row.id)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCursor()
|
||||
await getList()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-alert
|
||||
class="mb-15px"
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="小如酒店订单:用户先微信支付,支付成功后再向资安心预定。待支付不会出现在资安心后台。"
|
||||
/>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="88px"
|
||||
>
|
||||
<el-form-item label="酒店名称" prop="hotelName">
|
||||
<el-input
|
||||
v-model="queryParams.hotelName"
|
||||
placeholder="酒店名称"
|
||||
clearable
|
||||
class="!w-180px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="渠道单号" prop="channelNo">
|
||||
<el-input
|
||||
v-model="queryParams.channelNo"
|
||||
placeholder="channelNo"
|
||||
clearable
|
||||
class="!w-180px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="供应单号" prop="orderNo">
|
||||
<el-input
|
||||
v-model="queryParams.orderNo"
|
||||
placeholder="资安心单号"
|
||||
clearable
|
||||
class="!w-180px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="orderStatus">
|
||||
<el-select v-model="queryParams.orderStatus" clearable placeholder="全部" class="!w-150px">
|
||||
<el-option label="待支付" value="UNPAID" />
|
||||
<el-option label="已支付待预定" value="PAID" />
|
||||
<el-option label="已提交供应方" value="CREATED" />
|
||||
<el-option label="预定失败" value="BOOK_FAIL" />
|
||||
<el-option label="已取消" value="CANCELLED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付" prop="payStatus">
|
||||
<el-select v-model="queryParams.payStatus" clearable placeholder="全部" class="!w-120px">
|
||||
<el-option label="已支付" :value="true" />
|
||||
<el-option label="未支付" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="contactPhone">
|
||||
<el-input
|
||||
v-model="queryParams.contactPhone"
|
||||
placeholder="联系人手机"
|
||||
clearable
|
||||
class="!w-150px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" border :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="ID" prop="id" width="80" />
|
||||
<el-table-column label="酒店" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.hotelName || '-' }}</div>
|
||||
<div class="text-12px text-gray-400">{{ row.hotelCode }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品" min-width="140">
|
||||
<template #default="{ row }">{{ row.productName || row.productCode || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入住" width="200">
|
||||
<template #default="{ row }">
|
||||
{{ row.checkInDate || '-' }} → {{ row.checkOutDate || '-' }}
|
||||
<span v-if="row.nights">({{ row.nights }}晚×{{ row.roomCount || 1 }}间)</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" prop="sellAmount" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.sellAmount != null ? `¥${row.sellAmount}` : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.payStatus ? 'success' : 'warning'" size="small">
|
||||
{{ row.payStatus ? '已支付' : '未支付' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.orderStatus)" size="small">{{ statusText(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="渠道单号" prop="channelNo" min-width="160" />
|
||||
<el-table-column label="供应单号" prop="orderNo" min-width="140">
|
||||
<template #default="{ row }">{{ row.orderNo || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系人" width="140">
|
||||
<template #default="{ row }">{{ row.contactName || '-' }} {{ row.contactPhone || '' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失败原因" prop="failMsg" min-width="160">
|
||||
<template #default="{ row }">{{ row.failMsg || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="下单时间"
|
||||
prop="createTime"
|
||||
width="170"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="操作" width="110" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="canCancel(row)"
|
||||
link
|
||||
type="danger"
|
||||
:loading="cancellingId === row.id"
|
||||
@click="handleCancel(row)"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as HotelZaxApi from '@/api/mall/product/hotelZax'
|
||||
|
||||
defineOptions({ name: 'ProductHotelZaxOrder' })
|
||||
|
||||
const message = useMessage()
|
||||
const loading = ref(false)
|
||||
const cancellingId = ref<number>()
|
||||
const total = ref(0)
|
||||
const list = ref<HotelZaxApi.HotelZaxOrderVO[]>([])
|
||||
const queryFormRef = ref()
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
hotelName: undefined as string | undefined,
|
||||
channelNo: undefined as string | undefined,
|
||||
orderNo: undefined as string | undefined,
|
||||
orderStatus: undefined as string | undefined,
|
||||
payStatus: undefined as boolean | undefined,
|
||||
contactPhone: undefined as string | undefined
|
||||
})
|
||||
|
||||
const statusText = (row: HotelZaxApi.HotelZaxOrderVO) => {
|
||||
const s = row.orderStatus || ''
|
||||
if (s === 'UNPAID') return '待支付'
|
||||
if (s === 'PAID') return '已支付待预定'
|
||||
if (s === 'CREATED' || s === 'UPSTREAM_OK') return '已提交供应方'
|
||||
if (s === 'BOOK_FAIL' || s === 'UPSTREAM_FAIL') return '预定失败'
|
||||
if (s === 'CANCELLED' || s === 'CANCELED') return '已取消'
|
||||
return s || '-'
|
||||
}
|
||||
|
||||
const statusType = (s?: string) => {
|
||||
if (s === 'UNPAID') return 'warning'
|
||||
if (s === 'PAID') return 'info'
|
||||
if (s === 'CREATED' || s === 'UPSTREAM_OK') return 'success'
|
||||
if (s === 'BOOK_FAIL' || s === 'UPSTREAM_FAIL') return 'danger'
|
||||
if (s === 'CANCELLED' || s === 'CANCELED') return 'info'
|
||||
return ''
|
||||
}
|
||||
|
||||
const canCancel = (row: HotelZaxApi.HotelZaxOrderVO) => {
|
||||
const s = row.orderStatus || ''
|
||||
return s !== 'CANCELLED' && s !== 'CANCELED' && s !== 'COMPLETED'
|
||||
}
|
||||
|
||||
const handleCancel = async (row: HotelZaxApi.HotelZaxOrderVO) => {
|
||||
if (!row.id) return
|
||||
const paid = !!row.payStatus
|
||||
try {
|
||||
await message.confirm(
|
||||
paid
|
||||
? `确认取消订单 ${row.channelNo || row.id}?将原路退款给用户,并通知资安心取消预订。`
|
||||
: `确认取消未支付订单 ${row.channelNo || row.id}?`
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
cancellingId.value = row.id
|
||||
try {
|
||||
await HotelZaxApi.cancelHotelZaxOrder(row.id, paid ? '后台取消退款' : '后台取消')
|
||||
message.success(paid ? '已取消并退款' : '已取消')
|
||||
await getList()
|
||||
} catch (e: any) {
|
||||
message.error(e?.msg || e?.message || '取消失败')
|
||||
} finally {
|
||||
cancellingId.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await HotelZaxApi.getHotelZaxOrderPage(queryParams)
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
Reference in new issue
Block a user