酒店
build / task (16.x, npm, macOS-latest) (push) Canceled after 0s
build / task (16.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, npm, windows-latest) (push) Canceled after 0s
build / task (16.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (16.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (16.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (16.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, yarn, windows-latest) (push) Canceled after 0s
build / task (18.x, npm, macOS-latest) (push) Canceled after 0s
build / task (18.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, npm, windows-latest) (push) Canceled after 0s
build / task (18.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (18.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (18.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (18.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, yarn, windows-latest) (push) Canceled after 0s
build / task (20.x, npm, macOS-latest) (push) Canceled after 0s
build / task (20.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, npm, windows-latest) (push) Canceled after 0s
build / task (20.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (20.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (20.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (20.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, yarn, windows-latest) (push) Canceled after 0s

This commit is contained in:
zjf committed 2026-10-28 15:45:15 +08:00
1 parent e84e7c64a6
commit f207ba9459
3 files changed
+273 -48

No files matched your search

@@ -34,6 +34,8 @@ daily-days: 15
resource-push-join-schedule: true
# 推送流水:fail_only=只记失败;sample=失败+成功抽样;all=全记
# 说明:房型对不上/计划失败等「部分未推上」会单独落 push_partial(不受 fail_only 限制),
# 在 PC「下游推送日志」筛动作「部分未推上」或结果「跳过/部分」,点详情看 unmatchedRooms / planErrors
push-log-mode: fail_only
# sample 模式下,单次任务最多再记多少条成功样例
@@ -211,26 +211,30 @@
</el-form-item>
</template>
<el-form-item v-if="form.direction === 'downstream' && form.scopeType === 'hotel'" label="指定酒店" prop="hotelIdList">
<el-select
v-model="form.hotelIdList"
multiple
filterable
remote
reserve-keyword
collapse-tags
collapse-tags-tooltip
:remote-method="searchHotels"
:loading="hotelSearching"
placeholder="输入名称/编码搜索后多选"
class="w-full"
>
<el-option
v-for="h in hotelOptions"
:key="h.id"
:label="hotelOptionLabel(h)"
:value="String(h.id)"
/>
</el-select>
<div class="w-full">
<div class="flex flex-wrap items-center gap-2 mb-2">
<el-button type="primary" @click="openHotelPicker">选择酒店</el-button>
<span class="text-gray-500 text-sm">已选 {{ form.hotelIdList.length }} 家(弹窗内可筛选、多选)</span>
<el-button v-if="form.hotelIdList.length" link type="danger" @click="clearSelectedHotels">
清空
</el-button>
</div>
<div
v-if="form.hotelIdList.length"
class="flex flex-wrap gap-1 max-h-32 overflow-auto p-2 border border-solid border-[var(--el-border-color)] rounded"
>
<el-tag
v-for="id in form.hotelIdList"
:key="id"
closable
class="mb-1"
@close="removeSelectedHotel(id)"
>
{{ selectedHotelLabel(id) }}
</el-tag>
</div>
<div v-else class="text-gray-400 text-xs">请点击「选择酒店」打开列表勾选</div>
</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
@@ -248,6 +252,112 @@
</template>
</el-dialog>
<!-- 指定酒店:弹窗筛选 + 多选 -->
<el-dialog
v-model="hotelPickerVisible"
title="选择授权酒店"
width="960px"
append-to-body
destroy-on-close
@opened="onHotelPickerOpened"
>
<el-form :inline="true" class="mb-2" @keyup.enter="loadHotelPicker(1)">
<el-form-item label="名称">
<el-input
v-model="hotelPickerQuery.hotelName"
clearable
placeholder="酒店名称"
style="width: 160px"
/>
</el-form-item>
<el-form-item label="编码">
<el-input
v-model="hotelPickerQuery.hotelCode"
clearable
placeholder="hotel_code"
style="width: 140px"
/>
</el-form-item>
<el-form-item label="来源">
<el-select
v-model="hotelPickerQuery.sheetName"
clearable
placeholder="全部"
style="width: 140px"
>
<el-option
v-for="item in sm_hotel_data_source"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="上架">
<el-select
v-model="hotelPickerQuery.upstreamStatus"
clearable
placeholder="全部"
style="width: 110px"
>
<el-option label="上架" value="ACTIVE" />
<el-option label="下架" value="OFFLINE" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" :loading="hotelPickerLoading" @click="loadHotelPicker(1)">
查询
</el-button>
<el-button icon="Refresh" @click="resetHotelPickerQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="mb-2 text-sm text-gray-500">
本页勾选会并入已选;已选 {{ hotelPickerDraftIds.length }} 家
(打开弹窗时带入表单已选 {{ form.hotelIdList.length }} 家)
</div>
<el-table
ref="hotelPickerTableRef"
:data="hotelPickerRows"
border
height="420"
row-key="id"
v-loading="hotelPickerLoading"
@selection-change="onHotelPickerSelectionChange"
>
<el-table-column type="selection" width="45" reserve-selection />
<el-table-column prop="hotelName" label="酒店名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="hotelCode" label="酒店编码" min-width="120" show-overflow-tooltip />
<el-table-column prop="sheetName" label="数据来源" width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ dataSourceLabel(row.sheetName) }}
</template>
</el-table-column>
<el-table-column prop="upstreamStatus" label="上架" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.upstreamStatus === 'OFFLINE' ? 'info' : 'success'" size="small">
{{ row.upstreamStatus === "OFFLINE" ? "下架" : "上架" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="district" label="区域" min-width="120" show-overflow-tooltip />
</el-table>
<div class="mt-3 flex justify-end">
<el-pagination
v-model:current-page="hotelPickerPage"
v-model:page-size="hotelPickerSize"
:total="hotelPickerTotal"
:page-sizes="[20, 50, 100]"
layout="total, sizes, prev, pager, next"
@current-change="(p: number) => loadHotelPicker(p)"
@size-change="() => loadHotelPicker(1)"
/>
</div>
<template #footer>
<el-button @click="hotelPickerVisible = false">取消</el-button>
<el-button type="primary" @click="confirmHotelPicker">确定(已选 {{ hotelPickerDraftIds.length }})</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -278,9 +388,26 @@ const saving = ref(false)
const districtPath = ref("")
const districtCascaderKey = ref(0)
const downstreamPartnerOptions = ref<any[]>([])
/** 已选酒店展示缓存 id -> hotel */
const hotelOptions = ref<any[]>([])
const hotelSearching = ref(false)
const pushingPartner = ref<string | null>(null)
const hotelPickerVisible = ref(false)
const hotelPickerLoading = ref(false)
const hotelPickerTableRef = ref()
const hotelPickerRows = ref<any[]>([])
const hotelPickerPage = ref(1)
const hotelPickerSize = ref(20)
const hotelPickerTotal = ref(0)
const hotelPickerDraftIds = ref<string[]>([])
const hotelPickerDraftMap = ref<Map<string, any>>(new Map())
const hotelPickerQuery = reactive({
hotelName: "",
hotelCode: "",
sheetName: "",
upstreamStatus: "",
})
/** 避免 selection-change 在程序勾选时清空草稿 */
const hotelPickerSyncing = ref(false)
const form = reactive<any>({
id: undefined,
direction: "upstream",
@@ -324,6 +451,121 @@ const hotelOptionLabel = (h: any) => {
return `${name}(${code})`
}
const dataSourceLabel = (v?: string) => {
const hit = (sm_hotel_data_source.value || []).find((d: any) => String(d.value) === String(v))
return hit?.label || v || "-"
}
const selectedHotelLabel = (id: string) => {
const h = hotelOptions.value.find((x) => String(x.id) === String(id))
return h ? hotelOptionLabel(h) : String(id)
}
const removeSelectedHotel = (id: string) => {
form.hotelIdList = (form.hotelIdList || []).filter((x: string) => String(x) !== String(id))
formRef.value?.validateField?.("hotelIdList")
}
const clearSelectedHotels = () => {
form.hotelIdList = []
formRef.value?.validateField?.("hotelIdList")
}
const openHotelPicker = () => {
hotelPickerDraftIds.value = [...(form.hotelIdList || []).map(String)]
hotelPickerDraftMap.value = new Map(
hotelOptions.value
.filter((h) => hotelPickerDraftIds.value.includes(String(h.id)))
.map((h) => [String(h.id), h]),
)
hotelPickerQuery.hotelName = ""
hotelPickerQuery.hotelCode = ""
hotelPickerQuery.sheetName = ""
hotelPickerQuery.upstreamStatus = ""
hotelPickerPage.value = 1
hotelPickerVisible.value = true
}
const onHotelPickerOpened = () => {
loadHotelPicker(1)
}
const resetHotelPickerQuery = () => {
hotelPickerQuery.hotelName = ""
hotelPickerQuery.hotelCode = ""
hotelPickerQuery.sheetName = ""
hotelPickerQuery.upstreamStatus = ""
loadHotelPicker(1)
}
const loadHotelPicker = async (page = 1) => {
hotelPickerLoading.value = true
hotelPickerPage.value = page
try {
const res: any = await fetchHotelList({
current: page,
size: hotelPickerSize.value,
hotelName: hotelPickerQuery.hotelName || undefined,
hotelCode: hotelPickerQuery.hotelCode || undefined,
sheetName: hotelPickerQuery.sheetName || undefined,
upstreamStatus: hotelPickerQuery.upstreamStatus || undefined,
})
hotelPickerRows.value = res?.data?.records || []
hotelPickerTotal.value = Number(res?.data?.total || 0)
await nextTick()
syncHotelPickerSelection()
} catch (e: any) {
hotelPickerRows.value = []
hotelPickerTotal.value = 0
useMessage().error(e?.msg || e?.message || "加载酒店失败")
} finally {
hotelPickerLoading.value = false
}
}
const syncHotelPickerSelection = () => {
const table = hotelPickerTableRef.value
if (!table) return
hotelPickerSyncing.value = true
try {
table.clearSelection?.()
for (const row of hotelPickerRows.value) {
if (row?.id != null && hotelPickerDraftIds.value.includes(String(row.id))) {
table.toggleRowSelection?.(row, true)
}
}
} finally {
nextTick(() => {
hotelPickerSyncing.value = false
})
}
}
const onHotelPickerSelectionChange = (rows: any[]) => {
if (hotelPickerSyncing.value) return
const pageIds = new Set(hotelPickerRows.value.map((r) => String(r.id)))
// 先去掉本页未勾选的,再并入本页勾选的(跨页保留)
hotelPickerDraftIds.value = hotelPickerDraftIds.value.filter((id) => !pageIds.has(id))
for (const row of rows || []) {
const id = String(row.id)
if (!hotelPickerDraftIds.value.includes(id)) {
hotelPickerDraftIds.value.push(id)
}
hotelPickerDraftMap.value.set(id, row)
}
}
const confirmHotelPicker = () => {
form.hotelIdList = [...hotelPickerDraftIds.value]
const map = new Map<string, any>()
;[...hotelOptions.value, ...hotelPickerDraftMap.value.values()].forEach((h) => {
if (h?.id != null) map.set(String(h.id), h)
})
hotelOptions.value = form.hotelIdList.map((id: string) => map.get(String(id)) || { id, hotelName: id })
hotelPickerVisible.value = false
formRef.value?.validateField?.("hotelIdList")
}
const loadDownstreamPartners = async () => {
try {
const [cfgRes, appRes]: any[] = await Promise.all([
@@ -395,27 +637,6 @@ const handlePushScope = async (row: any) => {
}
}
const searchHotels = async (keyword: string) => {
hotelSearching.value = true
try {
const res: any = await fetchHotelList({
current: 1,
size: 30,
hotelName: keyword || undefined,
})
const records = res?.data?.records || []
const map = new Map<string, any>()
;[...hotelOptions.value, ...records].forEach((h) => {
if (h?.id != null) map.set(String(h.id), h)
})
hotelOptions.value = Array.from(map.values())
} catch {
// keep existing options
} finally {
hotelSearching.value = false
}
}
const ensureHotelOptions = async (ids: string[]) => {
const missing = ids.filter((id) => !hotelOptions.value.some((h) => String(h.id) === id))
for (const id of missing) {
@@ -570,7 +791,7 @@ const openForm = async (row?: any) => {
if (form.hotelIdList.length) {
await ensureHotelOptions(form.hotelIdList)
} else {
await searchHotels("")
hotelOptions.value = []
}
if (form.direction === "downstream" && form.scopeType === "dept" && form.districtAdcode != null) {
try {
@@ -6,7 +6,7 @@
:closable="false"
show-icon
class="mb-3"
title="资源推送默认只落失败(YAML:push-log-mode=fail_only)。慧订/环启打进我方的校验、下单、取消会全部落库,动作用「入站-」筛选,点详情看请求/响应。下架/无产品/无日历记「预检跳过」。「重试」只适用于资源推送。"
title="资源推送默认只落失败(YAML:push-log-mode=fail_only)。圣诺房型对不上/计划失败会记「部分未推上」(push_partial),点详情看缺口清单。下架/无产品/无日历记「预检跳过」。「重试」只适用于资源推送。"
/>
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
@@ -32,15 +32,16 @@
<el-option label="产品同步" value="product_sync" />
<el-option label="日历同步" value="daily_sync" />
<el-option label="预检跳过" value="push_skip" />
<el-option label="部分未推上" value="push_partial" />
<el-option label="订单查询" value="order_query" />
<el-option label="订单详情" value="order_detail" />
</el-select>
</el-form-item>
<el-form-item label="结果" prop="success">
<el-select v-model="state.queryForm.success" clearable placeholder="全部" style="width: 120px">
<el-select v-model="state.queryForm.success" clearable placeholder="全部" style="width: 140px">
<el-option label="成功" value="1" />
<el-option label="失败" value="0" />
<el-option label="跳过" value="2" />
<el-option label="跳过/部分" value="2" />
</el-select>
</el-form-item>
<el-form-item>
@@ -291,6 +292,7 @@ const actionLabel = (a?: string) => {
product_sync: "产品同步",
daily_sync: "日历同步",
push_skip: "预检跳过",
push_partial: "部分未推上",
order_query: "订单查询",
order_detail: "订单详情",
inbound_check: "入站-校验",
@@ -305,13 +307,13 @@ const actionLabel = (a?: string) => {
const successLabel = (s?: string) => {
if (s === "1") return "成功"
if (s === "2") return "跳过"
if (s === "2") return "跳过/部分"
return "失败"
}
const successTagType = (s?: string) => {
if (s === "1") return "success"
if (s === "2") return "info"
if (s === "2") return "warning"
return "danger"
}