酒店
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-15 17:12:47 +08:00
1 parent 1976afa5e2
commit 04e1d3bdab
5 files changed
+283 -59

No files matched your search

+8
View File
@@ -22,6 +22,14 @@ export function retryTask(id: string | number) {
})
}
/** 把滞留待处理任务重新推入 Redis 消费队列 */
export function requeuePendingTasks() {
return request({
url: "/supermarket/smHotelCallback/requeuePending",
method: "post",
})
}
/** 删除单条 */
export function delTask(id: string | number) {
return request({
+228 -53
View File
@@ -1,59 +1,99 @@
<template>
<el-drawer v-model="visible" :title="title" size="72%" destroy-on-close @closed="reset">
<div v-loading="loading">
<el-descriptions v-if="hotel" :column="2" border class="mb-4">
<el-descriptions-item label="酒店名称">{{ hotel.hotelName || "-" }}</el-descriptions-item>
<el-descriptions-item label="酒店编码">{{ hotel.hotelCode || "-" }}</el-descriptions-item>
<el-descriptions-item label="数据来源">
<dict-tag :options="sm_hotel_data_source" :value="hotel.sheetName" />
</el-descriptions-item>
<el-descriptions-item label="上架状态">
<el-tag :type="hotel.upstreamStatus === 'OFFLINE' ? 'info' : 'success'" size="small">
{{ hotel.upstreamStatus === "OFFLINE" ? "下架" : "上架" }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ hotel.phone || "-" }}</el-descriptions-item>
<template v-if="isLocal">
<el-descriptions-item label="自动滚加">
<el-tag :type="hotel.localAutoRoll === 1 ? 'success' : 'info'" size="small">
{{ hotel.localAutoRoll === 1 ? "已开启" : "未开启" }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="滚动天数">
{{ hotel.localAutoRoll === 1 ? `${hotel.localRollHorizonDays || 30} 天` : "-" }}
</el-descriptions-item>
</template>
<el-descriptions-item label="详细地址" :span="2">{{ hotel.address || "-" }}</el-descriptions-item>
<el-descriptions-item label="位置区域">
{{ hotel.district || "-" }}
<span v-if="hotel.districtAdcode" class="text-gray-400">({{ hotel.districtAdcode }})</span>
</el-descriptions-item>
<el-descriptions-item label="商圈地标">{{ hotel.bizArea || "-" }}</el-descriptions-item>
<el-descriptions-item label="星级档次">{{ starLabel(hotel.starLevel) }}</el-descriptions-item>
<el-descriptions-item label="评分">{{ hotel.score != null ? hotel.score : "-" }}</el-descriptions-item>
<el-descriptions-item label="设施标签" :span="2">{{ hotel.facilityTags || "-" }}</el-descriptions-item>
<el-descriptions-item label="坐标">
<span v-if="hotel.longitude != null && hotel.latitude != null">
{{ hotel.longitude }}, {{ hotel.latitude }}
</span>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="主图">
<el-image
v-if="hotel.coverImage"
:src="resolveFileUrl(hotel.coverImage)"
style="width: 72px; height: 72px"
fit="cover"
:preview-src-list="[resolveFileUrl(hotel.coverImage)]"
/>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="简介" :span="2">
<div v-if="hotel.intro" class="intro-html" v-html="hotel.intro" />
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ hotel.remark || "-" }}</el-descriptions-item>
</el-descriptions>
<!-- 顶部概览:主图+名称+状态一眼可见;不用 border descriptions(标签易被挤成竖排) -->
<div v-if="hotel" class="hotel-overview mb-4">
<div class="hotel-overview__hero">
<div class="hotel-overview__cover">
<el-image
v-if="hotel.coverImage"
:src="resolveFileUrl(hotel.coverImage)"
fit="cover"
class="hotel-overview__cover-img"
:preview-src-list="[resolveFileUrl(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">
<dict-tag :options="sm_hotel_data_source" :value="hotel.sheetName" />
<el-tag :type="hotel.upstreamStatus === 'OFFLINE' ? 'info' : 'success'" effect="dark" size="small">
{{ hotel.upstreamStatus === "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>
<template v-if="isLocal">
<el-tag :type="hotel.localAutoRoll === 1 ? 'success' : 'info'" effect="plain" size="small">
自动滚加 {{ hotel.localAutoRoll === 1 ? "已开启" : "未开启" }}
</el-tag>
<el-tag v-if="hotel.localAutoRoll === 1" effect="plain" size="small">
滚动 {{ hotel.localRollHorizonDays || 30 }} 天
</el-tag>
</template>
</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-1 mb-1"
>
{{ 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 v-if="hotel.remark" class="hotel-overview__cell hotel-overview__cell--full">
<span class="hotel-overview__label">备注</span>
<span class="hotel-overview__value">{{ hotel.remark }}</span>
</div>
</div>
</div>
<el-alert
v-if="isLocal"
@@ -530,6 +570,14 @@ const orderForm = reactive({
const title = computed(() => (hotel.value?.hotelName ? `酒店详情 - ${hotel.value.hotelName}` : "酒店详情"))
const isLocal = computed(() => String(hotel.value?.sheetName || "").toLowerCase() === "local")
const facilityTagList = computed(() => {
const raw = String(hotel.value?.facilityTags || "").trim()
if (!raw) return [] as string[]
return raw
.split(/[,,;;|、\n]+/)
.map((s) => s.trim())
.filter(Boolean)
})
const resolveFileUrl = (url?: string) => {
const u = String(url || "").trim()
if (!u) return ""
@@ -1055,6 +1103,117 @@ defineExpose({ open })
</script>
<style scoped>
.hotel-overview {
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
overflow: hidden;
background: var(--el-bg-color);
}
.hotel-overview__hero {
display: flex;
gap: 16px;
padding: 16px;
background: linear-gradient(135deg, var(--el-fill-color-light) 0%, var(--el-bg-color) 55%);
border-bottom: 1px solid var(--el-border-color-lighter);
}
.hotel-overview__cover {
flex: 0 0 120px;
width: 120px;
height: 120px;
border-radius: 8px;
overflow: hidden;
background: var(--el-fill-color);
border: 1px solid var(--el-border-color-lighter);
}
.hotel-overview__cover-img {
width: 100%;
height: 100%;
display: block;
}
.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__main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
gap: 8px;
}
.hotel-overview__name {
font-size: 20px;
font-weight: 600;
line-height: 1.35;
color: var(--el-text-color-primary);
word-break: break-word;
}
.hotel-overview__code {
font-size: 13px;
color: var(--el-text-color-secondary);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.hotel-overview__tags {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.hotel-overview__addr {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
line-height: 1.5;
color: var(--el-text-color-regular);
}
.hotel-overview__addr-label {
flex: 0 0 auto;
color: var(--el-text-color-secondary);
}
.hotel-overview__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0;
}
.hotel-overview__cell {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--el-border-color-extra-light);
border-right: 1px solid var(--el-border-color-extra-light);
min-height: 44px;
}
.hotel-overview__cell:nth-child(2n) {
border-right: none;
}
.hotel-overview__cell--full {
grid-column: 1 / -1;
border-right: none;
}
.hotel-overview__label {
flex: 0 0 72px;
width: 72px;
font-size: 13px;
line-height: 22px;
color: var(--el-text-color-secondary);
white-space: nowrap;
}
.hotel-overview__value {
flex: 1;
min-width: 0;
font-size: 13px;
line-height: 22px;
color: var(--el-text-color-primary);
word-break: break-word;
}
.intro-html {
line-height: 1.6;
word-break: break-word;
@@ -1062,4 +1221,20 @@ defineExpose({ open })
.intro-html :deep(img) {
max-width: 100%;
}
@media (max-width: 900px) {
.hotel-overview__hero {
flex-direction: column;
}
.hotel-overview__cover {
width: 100%;
height: 160px;
flex-basis: auto;
}
.hotel-overview__grid {
grid-template-columns: 1fr;
}
.hotel-overview__cell {
border-right: none;
}
}
</style>
-1
View File
@@ -150,7 +150,6 @@
详情
</el-button>
<el-button
v-if="isLocalHotel(scope.row)"
icon="edit-pen"
text
type="primary"
@@ -45,6 +45,16 @@
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button
type="primary"
plain
icon="RefreshRight"
v-auth="'supermarket_smHotelCallback_retry'"
:loading="requeuing"
@click="handleRequeuePending"
>
重新入队待处理
</el-button>
<el-button
type="danger"
plain
@@ -100,7 +110,7 @@
<template #default="scope">
<el-button text type="primary" v-auth="'supermarket_smHotelCallback_view'" @click="openDetail(scope.row)">详情</el-button>
<el-button
v-if="scope.row.status === 'FAILED' || scope.row.status === 'SKIPPED' || scope.row.status === 'SUCCESS'"
v-if="scope.row.status !== 'RUNNING'"
text
type="warning"
v-auth="'supermarket_smHotelCallback_retry'"
@@ -182,7 +192,15 @@
<script setup lang="ts" name="systemSmHotelCallback">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { fetchList, getObj, retryTask, delTask, delTaskByRange, clearAllTasks } from "/@/api/supermarket/smHotelCallback"
import {
fetchList,
getObj,
retryTask,
requeuePendingTasks,
delTask,
delTaskByRange,
clearAllTasks,
} from "/@/api/supermarket/smHotelCallback"
import { useMessage, useMessageBox } from "/@/hooks/message"
const queryRef = ref()
@@ -190,6 +208,7 @@ const showSearch = ref(true)
const detailVisible = ref(false)
const detail = ref<any>(null)
const clearing = ref(false)
const requeuing = ref(false)
const rangeVisible = ref(false)
const rangeDeleting = ref(false)
const rangeBegin = ref("")
@@ -238,7 +257,7 @@ const openDetail = async (row: any) => {
const handleRetry = async (row: any) => {
try {
await useMessageBox().confirm(`确认重试任务 ${row.id}?将重新拉取上游房价房量。`)
await useMessageBox().confirm(`确认重试任务 ${row.id}?将重新推入消费队列并拉取上游。`)
} catch {
return
}
@@ -251,6 +270,26 @@ const handleRetry = async (row: any) => {
}
}
const handleRequeuePending = async () => {
try {
await useMessageBox().confirm(
"确认把当前滞留的「待处理」任务重新推入 Redis 队列?需先保证渠道配置里 callback-queue-enabled=true,且 supermarket 服务已启动。",
)
} catch {
return
}
requeuing.value = true
try {
const res: any = await requeuePendingTasks()
useMessage().success(res?.msg || `已重新入队 ${res?.data ?? 0} 条`)
getDataList(false)
} catch (err: any) {
useMessage().error(err?.msg || err?.message || "重新入队失败")
} finally {
requeuing.value = false
}
}
const handleDeleteOne = async (row: any) => {
try {
await useMessageBox().confirm(`确认删除任务 ${row.id}?(软删除,不影响本地已落库房价)`)
@@ -72,5 +72,8 @@ push-log-retain-days: 15
# order-pull-schedule-enabled: true # false=整机关掉定时(手动仍可用)
# push-log-cleanup-cron: 0 40 4 * * ? # 推送流水清理
#
# 环启侧配置我们的接收地址(B→A 推送,与拉取独立):
# {网关}/supermarket/third/party/api/order/push
# 环启侧需配置/联调的地址(B→A,验签同 app-key):
# 下单前预检(我方补充,文档无;请环启售前调用):
# POST {网关}/supermarket/third/party/api/order/check
# 订单推送接收:
# POST {网关}/supermarket/third/party/api/order/push