gateway.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. /**
  2. * features/travel/gateway.js
  3. *
  4. * 旅游攻略首页接口封装。
  5. *
  6. * 接口套用参考项目 e:\NewLive\MLLive\pages\newhome.vue(同一套后端框架:
  7. * FastAdmin + wanlshop 插件),路径前缀分两类:
  8. *
  9. * A. FastAdmin 主框架原生控制器(不带 /wanlshop 前缀):
  10. * - /appbanner/list 首页 banner
  11. * - /appvideolabel/list 圆形分类标签
  12. * ↑ 直接用裸路径,让拦截器拼上 appurl(= https://backend.amazewayhk.com/api)
  13. *
  14. * B. wanlshop 插件路由(带 /wanlshop 前缀):
  15. * - /wanlshop/find/find/getList 内容流(热点推荐)
  16. * - /wanlshop/live/livelist 直播列表(已被 features/live/gateway.js 用过)
  17. * ↑ 走 buildRestPath,由 restPathPrefix=/wanlshop 自动拼
  18. *
  19. * 后端没有对应接口时调用者负责 catch + 默认数据兜底(不抛)。
  20. */
  21. import { get } from '@/core/http/http-client'
  22. import { buildRestPath } from '@/core/config/api-paths'
  23. import appSettings from '@/core/config/app-settings'
  24. import { fetchVideoFeedPage } from '@/features/feed/feed-gateway'
  25. import { stripHtml } from '@/features/feed/clip-normalize'
  26. /** 把后端返回的相对路径补全为完整 CDN URL */
  27. function cdn(path) {
  28. if (!path) return ''
  29. if (/^https?:\/\//.test(path)) return path
  30. if (path.startsWith('//')) return `https:${path}`
  31. const base = (appSettings.cdnurl || '').replace(/\/$/, '')
  32. return `${base}${path.startsWith('/') ? path : `/${path}`}`
  33. }
  34. /**
  35. * 兼容后端返回的多种 envelope:
  36. * - 直接数组: [{...}, {...}]
  37. * - {data: [...]}: { data: [...] }
  38. * - {list: [...]}: { list: [...] }
  39. * - 二级 data.data: { data: { data: [...] } }
  40. */
  41. function unwrapList(payload) {
  42. if (!payload) return []
  43. if (Array.isArray(payload)) return payload
  44. if (Array.isArray(payload.data)) return payload.data
  45. if (Array.isArray(payload.list)) return payload.list
  46. if (payload.data && Array.isArray(payload.data.data)) return payload.data.data
  47. if (payload.data && Array.isArray(payload.data.list)) return payload.data.list
  48. return []
  49. }
  50. /**
  51. * 首页 banner(show_type=10 与参考项目一致)
  52. * 参考 newhome.vue#getBannerData
  53. */
  54. export async function pullHomeBanners() {
  55. try {
  56. const { data, raw } = await get('/appbanner/list', { show_type: '10' })
  57. const rows = unwrapList(data).length ? unwrapList(data) : unwrapList(raw && raw.res)
  58. return rows.map((row, idx) => ({
  59. id: row.id || row.banner_id || idx,
  60. image: cdn(row.image || row.cover),
  61. jump_url: row.jump_url || row.link || row.url || '',
  62. link: row.jump_url || row.link || row.url || '',
  63. show_type: row.show_type,
  64. title: row.title || ''
  65. }))
  66. } catch (_) {
  67. return []
  68. }
  69. }
  70. /**
  71. * 5 圆形分类(视频标签)
  72. * 参考 newhome.vue#getMenuData
  73. *
  74. * 字段说明(来自参考项目):
  75. * row.id 标签 id
  76. * row.name 标签名("热门景区/游记分享/水上项目/亲子出游/美食特产")
  77. * row.image 圆形背景图
  78. * row.icon 图标
  79. * row.desc 描述/人数
  80. */
  81. export async function pullHomeCategories() {
  82. try {
  83. const { data, raw } = await get('/appvideolabel/list')
  84. const rows = unwrapList(data).length ? unwrapList(data) : unwrapList(raw && raw.res)
  85. return rows.map((row, idx) => ({
  86. id: row.id || row.label_id || idx,
  87. name: row.name || row.title || '',
  88. icon: cdn(row.icon),
  89. bg: cdn(row.image),
  90. desc: row.desc || ''
  91. }))
  92. } catch (_) {
  93. return []
  94. }
  95. }
  96. /**
  97. * 热点推荐瀑布流(视频内容流)
  98. * 参考 newhome.vue#getStreamersData
  99. *
  100. * @param {object} opts
  101. * @param {number} opts.page 页码(默认 1)
  102. * @param {number} opts.pageSize 截取条数(默认 20)
  103. * @param {string} opts.orderType 排序:'1'=最多点赞,'2'=最佳人气
  104. * @param {number|string} [opts.isHotDian] 是否热门推荐:0=否,1=是(接口字段 is_hot_dian)
  105. */
  106. function resolveFeedItemId(item) {
  107. if (!item) return ''
  108. const id = item.id != null && item.id !== '' ? item.id : item.find_id
  109. return id != null && id !== '' ? id : ''
  110. }
  111. function resolveFeedItemTitle(item) {
  112. if (!item) return ''
  113. const text = stripHtml(item.content)
  114. if (text) return text
  115. const fields = [
  116. item.title,
  117. item.name,
  118. item.label_name,
  119. item.labelname,
  120. item.jingdian_name,
  121. item.jingdian,
  122. item.video && item.video.title,
  123. item.shop && item.shop.shopname
  124. ]
  125. for (let i = 0; i < fields.length; i++) {
  126. const value = fields[i]
  127. if (value != null && String(value).trim()) return String(value).trim()
  128. }
  129. const label = item.label
  130. if (label && typeof label === 'object' && label.name) {
  131. return String(label.name).trim()
  132. }
  133. return ''
  134. }
  135. function mapVideoFeedCard(item) {
  136. const video = (item && item.video) || {}
  137. const shop = (item && item.shop) || {}
  138. const user = (item && item.user) || {}
  139. const images = Array.isArray(item.images) ? item.images : []
  140. const cover = video.cover_url || images[0] || item.cover || item.image || ''
  141. return {
  142. id: resolveFeedItemId(item),
  143. title: resolveFeedItemTitle(item),
  144. cover: cdn(cover),
  145. avatar: cdn(shop.avatar || user.avatar || ''),
  146. user: shop.shopname || user.nickname || item.username || '游客',
  147. likes: Number(item.views || item.likes || 0),
  148. type: item.type || 'video',
  149. label_id: item.label_id || '',
  150. raw: item
  151. }
  152. }
  153. /**
  154. * 按标签拉取视频列表(圆形分类 → 列表页)
  155. * 参考 newhome.vue#loadTabData:POST/GET find/find/getList,type=video,label_id
  156. *
  157. * @param {object} opts
  158. * @param {string|number} opts.labelId 标签 id(0 表示全部)
  159. * @param {number} opts.page
  160. */
  161. export async function pullLabelVideoPage({ labelId, page = 1 } = {}) {
  162. const lid = labelId === '' || labelId == null ? 0 : labelId
  163. const pack = await fetchVideoFeedPage(page, { label_id: lid })
  164. return {
  165. rows: pack.rows.map(mapVideoFeedCard),
  166. currentPage: pack.currentPage,
  167. lastPage: pack.lastPage
  168. }
  169. }
  170. export async function pullRecommendVideoPage({ page = 1, orderType = '1', isHotDian, isHotJingdian } = {}) {
  171. try {
  172. const filters = { order_type: orderType }
  173. if (isHotDian != null && isHotDian !== '') {
  174. filters.is_hot_dian = isHotDian
  175. }
  176. if (isHotJingdian != null && isHotJingdian !== '') {
  177. filters.is_hot_jingdian = isHotJingdian
  178. }
  179. const pack = await fetchVideoFeedPage(page, filters)
  180. return {
  181. rows: pack.rows
  182. .map(mapVideoFeedCard)
  183. .filter((row) => row.id != null && row.id !== '')
  184. .filter((row) => {
  185. const url = row.raw && row.raw.video && row.raw.video.url
  186. return !!(url && String(url).trim())
  187. }),
  188. currentPage: pack.currentPage,
  189. lastPage: pack.lastPage
  190. }
  191. } catch (_) {
  192. return { rows: [], currentPage: 1, lastPage: 1 }
  193. }
  194. }
  195. export async function pullRecommendations({ page = 1, pageSize = 20, orderType = '1', isHotDian } = {}) {
  196. const pack = await pullRecommendVideoPage({ page, orderType, isHotDian })
  197. return pack.rows.slice(0, pageSize)
  198. }
  199. /**
  200. * 首页服务入口(酒店/机票/货币换算等)
  201. * GET /other/menuData → { data: [{ name, jump_url, image, type }] }
  202. * type: 0=系统浏览器, 1=应用内页面(jump_url), 2=应用内 WebView
  203. */
  204. export async function pullHomeServices() {
  205. try {
  206. const { data, raw } = await get('/other/menuData')
  207. const rows = unwrapList(data).length ? unwrapList(data) : unwrapList(raw && raw.res)
  208. return rows.map((row, idx) => ({
  209. id: row.id || `menu-${idx}`,
  210. name: row.name || '',
  211. image: cdn(row.image || row.icon),
  212. link: row.jump_url || row.link || row.url || '',
  213. type: row.type != null ? row.type : ''
  214. }))
  215. } catch (_) {
  216. return []
  217. }
  218. }
  219. /**
  220. * 热门景点横向卡片(与热点推荐同接口 find/find/getList,is_hot_jingdian=1)
  221. */
  222. export async function pullHotAttractions({ page = 1, pageSize = 12 } = {}) {
  223. try {
  224. const pack = await pullRecommendVideoPage({ page, isHotJingdian: 1 })
  225. return pack.rows
  226. .slice(0, pageSize)
  227. .filter((row) => row.id != null && row.id !== '')
  228. .map((row) => ({
  229. id: row.id,
  230. name: row.title || resolveFeedItemTitle(row.raw) || '',
  231. image: row.cover,
  232. label_id: row.label_id || '',
  233. listPage: page,
  234. raw: row.raw
  235. }))
  236. } catch (_) {
  237. return []
  238. }
  239. }
  240. export { cdn as resolveTravelCdn }