import { $fetch } from 'ofetch' import { createError, defineEventHandler, getQuery } from 'h3' import { useRuntimeConfig } from '#imports' import { getTenantFromContext } from '../utils/tenant' /** * 获取轮播图列表 * GET /api/banner * * 真实数据结构为「分组(cms_banner_group) + 明细(cms_banner_item)」: * - 明细(item)内嵌在分组的 items[] 中返回,单独 /cms/cms-banner-item/page 接口不存在(404); * - 旧的扁平接口 /cms/cms-banner/page 对本仓库对接的租户无数据(已废弃)。 * * 本接口代理 /cms/cms-banner-group/page,按「启用分组 + 未删除明细」扁平化后, * 映射成前端 CmsBanner 结构返回。支持: * - position 查询参数:只取对应位置的轮播(如 home_slider),缺省返回全部启用分组; * - 时间与状态过滤:分组 status=1、未删除、且在 [startTime,endTime] 窗口内(若设置)才展示。 */ export default defineEventHandler(async (event) => { const config = useRuntimeConfig() const ctx = getTenantFromContext(event, config) const query = getQuery(event) const modulesApiBase = config.public.modulesApiBase as string // position 由本接口自行过滤,不往上游透传(避免 CMS 无此参数导致异常) const position = (query.position as string | undefined) || undefined const { position: _omit, ...upstreamQuery } = query try { const res: any = await $fetch('/cms/cms-banner-group/page', { baseURL: modulesApiBase, headers: { TenantId: ctx.tenantId }, query: { page: 1, limit: 50, ...upstreamQuery } }) const groups: any[] = res?.data?.list ?? res?.list ?? [] const now = Date.now() // 1) 筛选启用分组 const enabledGroups = groups.filter((g) => { if (g.deleted === 1) return false if (g.status !== 1) return false if (position && g.position !== position) return false // 时间窗口:startTime/endTime 任一为空则视为不限;两者都有时需在窗口内 // ⚠️ CMS 时间字符串为「中国时间(Asia/Shanghai, UTC+8)」,必须显式按 +08:00 解析, // 否则 dev/server 在 UTC 时区下会把窗口整体算错(实测曾把在用分组误判为未生效)。 if (g.startTime && g.endTime) { const start = parseCmsTime(g.startTime) // 显式按 UTC+8 解析(中国时间) const end = parseCmsTime(g.endTime) // 显式按 UTC+8 解析(中国时间) if (start !== null && end !== null && (now < start || now > end)) { return false } } return true }) // 2) 扁平化明细并过滤未删除项 const list: any[] = [] for (const g of enabledGroups) { const items: any[] = Array.isArray(g.items) ? g.items : [] for (const it of items) { if (it.deleted === 1) continue list.push({ // 兼容前端 id 字段 id: it.itemId ?? it.id, itemId: it.itemId, groupId: it.groupId ?? g.groupId, // 图片地址(优先 image,兼容其它字段名) image: it.image || it.imageUrl || it.pic || it.url || '', imageUrl: it.imageUrl || it.image, pic: it.pic, url: it.url, // 文案 title: it.title || g.title || '', subtitle: it.subtitle || '', // 跳转(linkType: 0=无 1=外链 2=站内) link: it.linkUrl || '', linkUrl: it.linkUrl || '', linkType: it.linkType ?? 0, linkTargetId: it.linkTargetId ?? null, // 排序(组内 sortNum);分组 sortNum 作为兜底 sortNum: it.sortNum ?? g.sortNum ?? 0, sortOrder: it.sortNum ?? g.sortNum ?? 0, // 状态/删除标记:沿用分组状态,前端按「非 2 即显示、deleted!==1」处理 status: it.status ?? g.status ?? 1, deleted: it.deleted ?? 0 }) } } // 3) 按 sortNum 升序 list.sort((a, b) => (a.sortNum ?? 0) - (b.sortNum ?? 0)) return { code: res?.code ?? 0, message: res?.message ?? '操作成功', data: { list, count: list.length } } } catch (error: any) { throw createError({ statusCode: error?.statusCode || error?.response?.status || 502, statusMessage: error?.statusMessage || 'Failed to fetch banners' }) } }) /** * 将 CMS 时间字符串(如 "2026-07-30 12:14:47",中国时间 UTC+8)解析为毫秒时间戳。 * 显式按 Asia/Shanghai 解析,避免 server 处于 UTC 时区时窗口计算错误。 * 解析失败返回 null。 */ function parseCmsTime(s?: string | null): number | null { if (!s) return null const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/) if (!m) return null const [, Y, Mo, D, h, mi, sec] = m // 视为 UTC+8:先按本地组件构造,再减 8 小时换算成 UTC 时间戳 return Date.UTC(+Y, +Mo - 1, +D, +h - 8, +mi, +sec) }