cloudroam
2025-07-01 a403393c1190994b473e679e1751794d9a1b9502
add: 分享+景点管理
已修改9个文件
已添加8个文件
774 ■■■■■ 文件已修改
components/card/localtion-card.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/share-popup.vue 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/title/section-title.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages.json 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/home/home-main.vue 65 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/home/home.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
static/common/link.png 补丁 | 查看 | 原始文档 | blame | 历史
static/common/parking.png 补丁 | 查看 | 原始文档 | blame | 历史
static/common/visit.png 补丁 | 查看 | 原始文档 | blame | 历史
static/common/wechat-moments.png 补丁 | 查看 | 原始文档 | blame | 历史
static/common/wechat.png 补丁 | 查看 | 原始文档 | blame | 历史
sub-pages/film-list/film-detail.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sub-pages/film-list/film-list.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sub-pages/hot-spot/index.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sub-pages/hot-spot/spot-detail.vue 366 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sub-pages/utils/api.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
types/index.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/card/localtion-card.vue
对比新文件
@@ -0,0 +1,95 @@
<template>
    <view class="card" @click="handleClick(item)">
        <view class="image-wrapper">
            <image :src="item.locationUrl" mode="widthFix" class="card-image" />
        </view>
        <view class="card-title">
            <up-text :lines="2" size="14px" :text="item.locationName" bold></up-text>
        </view>
        <view class="card-footer">
            <view class="user-info">
                <view class="user-text">
                    <text class="nickname">{{ item.address }}</text>
                </view>
            </view>
            <view class="opera-info">
                <up-icon name="heart" size="30rpx" color="#999" />
                <text>{{ item.locationWeight }}</text>
            </view>
        </view>
    </view>
</template>
<script setup lang="ts">
defineProps<{
    item: any
}>()
const emit = defineEmits(['click'])
const handleClick = (item: any) => {
    emit('click', item)
}
</script>
<style scoped lang="scss">
.card {
    border-radius: 10rpx;
    background-color: #ffffff;
    font-size: 14px;
    line-height: 20px;
    color: rgb(51, 51, 51);
    margin: 10rpx;
    .image-wrapper {
        width: 100%;
        position: relative;
        display: inline-block;
        .card-image {
            width: 100%;
            border-radius: inherit;
        }
    }
    .card-title {
        padding: 10rpx;
        font-weight: 500;
    }
    .card-footer {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 10rpx;
    }
    .user-info {
        display: flex;
        align-items: center;
        .user-text {
            font-size: 18rpx;
            line-height: 14px;
            margin-left: 10rpx;
            .nickname {
                font-weight: bold;
                display: block;
                color: #646464;
            }
        }
    }
    .opera-info {
        display: flex;
        align-items: center;
        text {
            margin-left: 10rpx;
            font-size: 12px;
        }
    }
}
</style>
components/share-popup.vue
@@ -4,14 +4,16 @@
    <view class="share-content">
      <view class="share-title">分享到</view>
      <view class="share-options">
        <button class="share-item" open-type="share" @click="handleShare('wechat')">
        <!-- 分享给好友 -->
        <button class="share-item" open-type="share">
          <image src="/static/common/wechat.png" class="share-icon" />
          <text>微信好友</text>
        </button>
        <button class="share-item" open-type="share" @click="handleShare('moments')">
        <!-- 分享到朋友圈 -->
        <view class="share-item" @click="handleShareTimeline">
          <image src="/static/common/wechat-moments.png" class="share-icon" />
          <text>朋友圈</text>
        </button>
        </view>
        <view class="share-item" @click="handleCopyLink">
          <image src="/static/common/link.png" class="share-icon" />
          <text>复制链接</text>
@@ -48,10 +50,40 @@
  emit('update:show', false)
}
// 处理分享
const handleShare = (type: 'wechat' | 'moments') => {
  // 小程序分享通过页面配置和按钮的 open-type="share" 实现
  // 分享内容在页面的 onShareAppMessage 中配置
// 处理分享到朋友圈
const handleShareTimeline = () => {
  // #ifdef MP-WEIXIN
  // 使用微信原生朋友圈分享
  wx.openChannelsActivity({
    finderUserName: '', // 视频号用户名,可选
    success: (res) => {
      console.log('打开朋友圈成功', res)
      uni.showToast({
        title: '已打开朋友圈',
        icon: 'success'
      })
    },
    fail: (err) => {
      console.error('打开朋友圈失败', err)
      // 如果打开失败,尝试使用小程序分享
      uni.showModal({
        title: '提示',
        content: '无法直接打开朋友圈,请点击右上角分享到朋友圈',
        showCancel: false
      })
    }
  })
  // #endif
  // #ifndef MP-WEIXIN
  // 非微信小程序环境
  uni.showToast({
    title: '请点击右上角分享到朋友圈',
    icon: 'none',
    duration: 2000
  })
  // #endif
  closePopup()
}
components/title/section-title.vue
@@ -53,6 +53,7 @@
})
function go(url: string) {
  console.log("url",url)
    if (url) {
        uni.navigateTo({
            url
pages.json
@@ -52,7 +52,9 @@
                    "path": "film-detail",
                    "style": {
                        "navigationBarTitleText": "",
                        "enablePullDownRefresh": true
                        "enablePullDownRefresh": true,
                        "enableShareTimeline": true,
                        "enableShareAppMessage": true
                    }
                }
                ,{
@@ -122,6 +124,28 @@
                }
            ]
        }
        ,{
            "root": "sub-pages/hot-spot",
            "pages": [
                {
                    "path": "index",
                    "style": {
                        "navigationBarTitleText": "场景博物馆",
                        "enablePullDownRefresh": true
                    }
                }
                ,{
                    "path": "spot-detail",
                    "style": {
                        "navigationBarTitleText": "",
                        "enablePullDownRefresh": true,
                        "enableShareTimeline": true,
                        "enableShareAppMessage": true
                    }
                }
            ]
        }
    ],
    "globalStyle": {
        "navigationBarTextStyle": "black",
pages/home/home-main.vue
@@ -2,23 +2,26 @@
    <view>
      <!-- <view class="card" v-if="!showVideo"> -->
        <view class="card">
        <view class="main-title">每一帧画面,都藏着一个等待探索的世界</view>
        <view class="main-title">{{ config.mainTitle }}</view>
        <view class="sub-title">
          从经典场景到幕后故事,开启你的专属影视朝圣之旅
          {{ config.subTitle }}
        </view>
  
        <view class="btn-group">
          <view class="custom-btn explore-btn" @click="startExplore">
            <text class="btn-text">开始探索</text>
            <up-icon name="play-right-fill" size="34rpx" color="black" class="btn-icon" />
            <text class="btn-text">{{ config.btnText1 }}</text>
            <up-icon
                :name="config.btnIcon1"
                :color="config.iconColor1"
                size="34rpx" class="btn-icon" />
          </view>
  
          <view class="custom-btn route-btn" @click="hotRoute">
            <text class="btn-text">热门路线</text>
            <text class="btn-text">{{ config.btnText2 }}</text>
            <up-icon
              name="/static/common/road-map-fill.png"
                :name="config.btnIcon2"
                :color="config.iconColor2"
              size="34rpx"
              color="white"
              class="btn-icon"
            />
          </view>
@@ -54,7 +57,9 @@
  
  <script setup lang="ts">
  import { ref, computed } from 'vue'
  import { onLoad } from '@dcloudio/uni-app'
  import { useGlobal } from '@/composables/useGlobal'
  const { $http, $message, $store } = useGlobal()
  const showVideo = ref(false)
  const currentIndex = ref(0)
  
@@ -70,7 +75,17 @@
  ]
  
  const currentVideo = computed(() => videoList[currentIndex.value])
  // 添加配置响应式对象
  const config = ref({
    mainTitle: '每一帧画面,都藏着一个等待探索的世界',
    subTitle: '从经典场景到幕后故事,开启你的专属影视朝圣之旅',
    btnText1: '开始探索',
    btnText2: '热门路线',
    btnIcon1: 'play-right-fill',
    btnIcon2: '/static/common/road-map-fill.png',
    iconColor1: 'black',
    iconColor2: 'white'
  })
  function startExplore() {
    // 跳转到具体页面
    uni.navigateTo({
@@ -89,6 +104,38 @@
      icon: 'none',
    })
  }
  onLoad((options: any) => {
    getHomeConfig()
  })
  // 获取首页配置信息
  const getHomeConfig = async () => {
    try {
      const { code, data } = await $http.request(
          'get',
          '/api/home/homeConfig/info',
          {}
      )
      if (code == 0) {
        console.log("接口返回数据:", data);
        // 只更新接口返回的有效字段
        Object.keys(config.value).forEach(key => {
          const newValue = data[key];
          console.log(`字段 ${key}: 接口值=${newValue}, 本地值=${config.value[key]}`);
          if (newValue !== undefined && newValue !== null && newValue !== '') {
            config.value[key] = newValue;
          }
        });
        console.log("首页配置加载成功", config.value)
      } else {
      }
    } catch (error) {
      console.error('配置请求失败', error)
    }
  }
  </script>
  <style scoped>
  .card {
pages/home/home.vue
@@ -35,7 +35,7 @@
      <SectionTitle title="全球影视地标" optitle="查看全部" goUrl="/pages/home/home-more" />
      <GlobalGeo />
      <SectionTitle title="场景博物馆" optitle="查看全部" goUrl="/pages/home/home-more" />
      <SectionTitle title="场景博物馆" optitle="查看全部" goUrl="/sub-pages/hot-spot/index" />
      <SceneMuseumCard v-for="(item, index) in cardList" :key="index" :image="item.image" :title="item.title"
        :subtitle="item.subtitle" :readTime="item.readTime" />
static/common/link.png
static/common/parking.png
static/common/visit.png
static/common/wechat-moments.png
static/common/wechat.png
sub-pages/film-list/film-detail.vue
@@ -183,7 +183,8 @@
    title: filmInfo.value?.coverTitle || '分享内容',
    desc: filmInfo.value?.filmContent?.substring(0, 50) || '',
    image: filmPictureList.value[0] || '',
    url: `https://您的域名/sub-pages/film-list/film-detail?id=${filmInfo.value?.id}`
    // url: `http://您的域名/sub-pages/film-list/film-detail?id=${filmInfo.value?.id}`
    url: `/sub-pages/film-list/film-detail?id=${filmInfo.value?.id}`
  }
  showSharePopup.value = true
}
@@ -193,7 +194,9 @@
const copyLink = () => {
  sharePopupShow.value = false
  uni.setClipboardData({
    data: `https://你的域名/sub-pages/film-list/film-detail?id=${filmInfo.value?.id}`,
    // data: `http://14.103.144.28/sub-pages/film-list/film-detail?id=${filmInfo.value?.id}`,
    // 此处应该是一个小程序的外链
    data: `http://14.103.144.28/sub-pages/film-list/film-detail?id=${filmInfo.value?.id}`,
    success: () => $message.showToast('链接已复制')
  })
}
@@ -426,7 +429,8 @@
    return {
      title: filmInfo.value?.coverTitle || '分享内容',
      query: `id=${filmInfo.value?.id}`,
      imageUrl: filmPictureList.value[0] || ''
      imageUrl: filmPictureList.value[0] || '',
      desc: filmInfo.value?.filmContent?.substring(0, 50) || ''
    }
  }
})
sub-pages/film-list/film-list.vue
@@ -119,6 +119,7 @@
    type: '',
    current: filmPage.value,
    size: filmSize,
    status: 'published',
    keywords: keywords.value // 关键修改
  };
  const records = await getFilmWorksBase(query)
sub-pages/hot-spot/index.vue
对比新文件
@@ -0,0 +1,100 @@
<template>
  <view :class="['app', theme]">
    <view style="padding-top: 10rpx;">
      <up-search height="70rpx" placeholder="搜索" search-icon-size="40rpx" margin="0rpx 30rpx" v-model="search"
                 :show-action="true" @search="onSearch" />
    </view>
    <view class="list-view">
      <up-waterfall v-model="films">
        <template #left="{ leftList }">
          <LocaltionCard v-for="(item, index) in leftList" :key="index" :item="item" @click="handleDetailClick(item)" />
        </template>
        <template #right="{ rightList }">
          <LocaltionCard v-for="(item, index) in rightList" :key="index" :item="item" @click="handleDetailClick(item)" />
        </template>
      </up-waterfall>
      <up-loadmore :status="filmStatus" :line="true" />
    </view>
  </view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad, onShow, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { FilmLocationVO, FilmLocationQueryDTO } from '@/types/index'
import { useGlobal } from '@/composables/useGlobal'
const { $http, $message } = useGlobal()
import { useNavigator } from '@/composables/useNavigator'
const { navigateTo } = useNavigator()
import { getFilmLocationBase } from '@/sub-pages/utils/api'
import LocaltionCard from "../../components/card/localtion-card.vue";
const theme = ref('light')
const search = ref('')
const films = ref<FilmLocationVO[]>([])
const filmPage = ref(1)
const filmSize = 10
const filmStatus = ref('loading')
const onSearch = () => {
  filmPage.value = 1
  films.value = []
  filmStatus.value = 'loadmore'
  getFilmLocation(search.value)
}
const handleDetailClick = (item: FilmLocationVO) => {
  const url = `/sub-pages/hot-spot/spot-detail?id=${item.id}`
  navigateTo(url)
}
onLoad(() => {
  const storedTheme = uni.getStorageSync('theme') || 'light'
  theme.value = storedTheme
})
onShow(() => {
  getFilmLocation()
})
onPullDownRefresh(async () => {
  filmPage.value = 1
  getFilmLocation()
  uni.stopPullDownRefresh()
})
onReachBottom(() => {
  getFilmLocation()
})
const getFilmLocation = async (keyword = '') => {
  if (filmStatus.value === 'nomore') return
  filmStatus.value = 'loading'
  try {
  const query: FilmLocationQueryDTO = {
    locationName: keyword,
    current: filmPage.value,
    size: filmSize,
    isEnabled: 1
  }
  const records = await getFilmLocationBase(query)
  if (records && records.length > 0) {
    const existingIds = new Set(films.value.map(item => item.id))
    const uniqueRecords = records.filter(item => !existingIds.has(item.id))
    films.value = [...films.value, ...uniqueRecords]
    if (records.length < filmSize) {
      filmStatus.value = 'noMore'
    }
    filmPage.value++
  } else {
    filmStatus.value = 'noMore'
  }
  } catch (e) {
    filmStatus.value = 'loadmore' // 重置状态允许重试
    $message.error('加载失败,请重试')
  }
}
</script>
<style lang="scss" scoped></style>
sub-pages/hot-spot/spot-detail.vue
对比新文件
@@ -0,0 +1,366 @@
<template>
  <view class="spot-detail-page">
    <!-- 顶部信息 -->
    <view class="header">
      <view class="header-info">
        <view class="geo-row">
          <view class="geo-path">{{ geoPath }}</view>
          <image src="/static/common/earth.png" class="earth-icon" />
        </view>
        <view class="title-row">
          <view class="title">{{ location?.locationName || '景点详情' }}</view>
          <up-icon name="heart" size="40rpx" :color="collected ? '#FF4D4F' : '#ccc'" @click="toggleCollect" />
        </view>
      </view>
    </view>
    <!-- 主图 -->
    <view class="main-image">
      <image :src="mainImage" mode="aspectFill" class="main-img" />
    </view>
    <!-- 地址、类型、电话、身份准入等 -->
    <view class="info-list">
      <view class="info-item">{{ location?.address }}</view>
      <view class="info-item"><up-icon name="/static/common/marker.png" size="32rpx" /> {{  location?.sceneType || '类型未知' }}</view>
      <view class="info-item"><up-icon name="/static/common/parking.png" size="32rpx" /> {{ location?.parkingInfo || '无电话' }}</view>
      <view class="info-item"><up-icon name="/static/common/visit.png" size="32rpx" />  {{ location?.isOpenVisitStr === '是' ? '允许参观' : '不允许参观' }}</view>
    </view>
    <!-- 介绍描述 -->
    <view class="divider-line"></view>
    <view class="desc-block">
      <text>{{ location?.landmarkDesc || location?.visitInfo || '暂无介绍' }}</text>
    </view>
    <!-- 地图 -->
    <view class="map-block" v-if="showMap">
      <!-- 腾讯地图小程序组件(uni-app中需配置腾讯地图key) -->
      <map
          :latitude="location?.gpsLat"
          :longitude="location?.gpsLng"
          :markers="mapMarkers"
          style="width: 100%; height: 300rpx;"
          v-if="location?.gpsLat && location?.gpsLng"
      />
      <image v-else src="/static/common/geo-earth.png" class="map-placeholder" />
    </view>
    <!-- 实景图 -->
    <view class="scene-block" v-if="sceneImages.length">
      <view class="scene-title">实景图 <text class="scene-count">{{ sceneImages.length }}</text></view>
      <view class="scene-list">
        <image
            v-for="(img, idx) in sceneImages"
            :key="idx"
            :src="img"
            mode="aspectFill"
            class="scene-image"
            @tap="previewScene(idx)"
        />
      </view>
    </view>
    <!-- 关联影视作品 -->
    <view class="film-block" v-if="relatedFilms.length">
      <view class="film-title">关联影视作品</view>
      <view class="film-list">
        <view class="film-item" v-for="film in relatedFilms" :key="film.id">
          <image :src="film.coverUrl" class="film-cover" />
          <view class="film-info">
            <view class="film-name">{{ film.nameCn }}</view>
            <view class="film-en">{{ film.nameEn }}</view>
            <view class="film-meta">
              <text>{{ film.typeStr }}</text>
              <text v-if="film.country"> / {{ film.country }}</text>
              <text v-if="film.releaseYear"> / {{ film.releaseYear }}</text>
            </view>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useGlobal } from '@/composables/useGlobal'
const { $http, $message } = useGlobal()
import { FilmLocationVO, FilmWorks } from '@/types/index'
const location = ref<FilmLocationVO>()
const collected = ref(false)
const geoPath = ref('亚洲-中国-海南省-海口市')
const mainImage = ref('')
const sceneImages = ref<string[]>([])
const relatedFilms = ref<FilmWorks[]>([])
const showMap = ref(true)
onLoad(async (options: any) => {
  const id = options.id
  if (id) {
    await getLocationDetail(id)
    await getRelatedFilms(id)
  }
})
const getLocationDetail = async (id: string) => {
  const { code, data } = await $http.request('get', '/api/filmLocation/list/view', {
    params: { id }
  })
  if (code === 0) {
    location.value = data
    // 主图优先locationUrl,其次visitorPhotos
    if (data.locationUrl) {
      mainImage.value = data.locationUrl
      console.log(data.locationUrl)
      console.log( mainImage.value)
    } else if (data.visitorPhotos) {
      try {
        const arr = JSON.parse(data.visitorPhotos)
        mainImage.value = arr[0]?.url || ''
      } catch {
        mainImage.value = ''
      }
    }
    // 实景图
    if (data.visitorPhotos) {
      try {
        const arr = JSON.parse(data.visitorPhotos)
        sceneImages.value = arr.map((item: any) => item.url)
      } catch {
        sceneImages.value = []
      }
    } else {
      sceneImages.value = []
    }
    // 拼接地理层级
    geoPath.value = [
      '亚洲',
      '中国',
      data.province,
      data.city
    ].filter(Boolean).join('-')
  } else {
    $message.showToast('获取景点信息失败')
  }
}
// 地图标记
const mapMarkers = ref([
  {
    id: 1,
    latitude: location.value?.gpsLat,
    longitude: location.value?.gpsLng,
    iconPath: '/static/common/marker.png',
    width: 40,
    height: 40
  }
])
const previewScene = (idx: number) => {
  uni.previewImage({
    current: sceneImages.value[idx],
    urls: sceneImages.value
  })
}
// 影视作品接口示例
const getRelatedFilms = async (locationId: string) => {
  // 假设接口 /api/filmWorks/related?locationId=xxx
  const { code, data } = await $http.request('get', '/api/filmLocation/related', {
    params: { locationId }
  })
  if (code === 0 && Array.isArray(data)) {
    relatedFilms.value = data
  } else {
    relatedFilms.value = []
  }
}
const toggleCollect = () => {
  collected.value = !collected.value
  $message.showToast(collected.value ? '已收藏' : '已取消收藏')
}
</script>
<style scoped>
.spot-detail-page {
  background: #fff;
  min-height: 100vh;
}
.header {
  display: flex;
  align-items: flex-start;
  padding: 30rpx 20rpx 10rpx 20rpx;
}
.header-info {
  flex: 1;
  margin-left: 20rpx;
}
.geo-path {
  color: #6c8fc5;
  font-size: 22rpx;
  margin-bottom: 10rpx;
}
.title-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 30rpx; /* 增加名称与上方的间距 */
}
.title {
  font-size: 36rpx;
  font-weight: bold;
  margin-right: 16rpx;
}
.earth-icon {
  width: 144rpx; /* 放大三倍 */
  height: 144rpx;
}
.main-image {
  width: 100%;
  height: 320rpx;
  margin-top: 40rpx; /* 增加名称和图片之间的间隔 */
  margin-bottom: 40rpx;
}
.main-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 10rpx;
}
/*.info-list {*/
/*  padding: 0 20rpx;*/
/*  margin-bottom: 20rpx;*/
/*}*/
/*.info-item {*/
/*  font-size: 28rpx;*/
/*  color: #333;*/
/*  margin-bottom: 16rpx;*/
/*  display: flex;*/
/*  align-items: center;*/
/*}*/
/*.desc-block {*/
/*  padding: 0 20rpx 30rpx 20rpx;*/
/*  color: #666;*/
/*  font-size: 26rpx;*/
/*}*/
.info-list {
  padding: 20rpx; /* 增加内边距 */
  margin: 20rpx 0; /* 调整上下外边距 */
  background-color: #f9f9f9;
  border-radius: 12rpx;
}
.info-item {
  font-size: 28rpx;
  color: #333;
  margin-bottom: 20rpx; /* 调整间距 */
  display: flex;
  align-items: center;
  line-height: 1.6;
}
.info-item:last-child {
  margin-bottom: 0;
}
/* 添加分割线样式 */
.divider-line {
  height: 1px;
  background-color: #eee;
  margin: 30rpx 20rpx 20rpx 20rpx;
}
/* 调整描述区块的间距 */
.desc-block {
  padding: 0 20rpx 30rpx 20rpx;
  color: #666;
  font-size: 26rpx;
  line-height: 1.8;
  margin-top: 20rpx;
}
.map-block {
  margin: 20rpx 0;
}
.map-placeholder {
  width: 100%;
  height: 300rpx;
  object-fit: cover;
  border-radius: 10rpx;
}
.scene-block {
  padding: 0 20rpx 30rpx 20rpx;
}
.scene-title {
  font-size: 28rpx;
  font-weight: bold;
  margin-bottom: 10rpx;
}
.scene-count {
  color: #888;
  font-size: 22rpx;
  margin-left: 10rpx;
}
.scene-list {
  display: flex;
  gap: 20rpx;
  flex-wrap: wrap;
}
.scene-image {
  width: 45vw;
  height: 140rpx;
  object-fit: cover;
  border-radius: 8rpx;
  margin-bottom: 10rpx;
}
.film-block {
  padding: 0 20rpx 30rpx 20rpx;
}
.film-title {
  font-size: 28rpx;
  font-weight: bold;
  margin-bottom: 10rpx;
}
.film-list {
  display: flex;
  flex-direction: column;
  gap: 20rpx;
}
.film-item {
  display: flex;
  align-items: flex-start;
  gap: 20rpx;
}
.film-cover {
  width: 120rpx;
  height: 160rpx;
  object-fit: cover;
  border-radius: 8rpx;
}
.film-info {
  flex: 1;
}
.film-name {
  font-size: 28rpx;
  font-weight: bold;
}
.film-en {
  font-size: 22rpx;
  color: #888;
  margin-bottom: 6rpx;
}
.film-meta {
  font-size: 22rpx;
  color: #888;
}
.geo-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 15rpx;
}
</style>
sub-pages/utils/api.ts
@@ -84,3 +84,16 @@
      return null;
    }
  }
export const getFilmLocationBase = async (query: FilmWorksQueryDTO) => {
    const { code, data } = await http.request('get', '/api/filmLocation/list', {
        params: query
    });
    if (code === 0) {
        return data.records;
    } else {
        message.showToast('系统异常,无法获取数据');
        return null;
    }
}
types/index.ts
@@ -305,3 +305,52 @@
    url: string;
    status: string;
  }
export interface FilmLocationVO {
    address?: string,
    arEntry?: string,
    checkinCount?: number,
    city?: string,
    classicScene?: string,
    endDate?: string,
    filmId?: number,
    gpsLat?: number,
    gpsLng?: number,
    id?: number,
    isEnabled?: boolean,
    isOpenVisit?: string,
    isOpenVisitStr?: string,
    landmarkDesc?: string,
    locationName?: string,
    locationUrl?: string,
    locationWeight?: number,
    operationWeight?: number,
    parkingInfo?: string,
    province?: string,
    region?: string,
    sceneType?: string,
    startDate?: string,
    surroundingFacilities?: string,
    transportGuide?: string,
    visitInfo?: string,
    visitorPhotos?: string
}
export interface FilmWorksQueryDTO extends PaginationQuery {
    /** 拍摄地点名称 */
    locationName?: string;
    /** 所在省 */
    province?: string;
    /** 所在市 */
    city?: string;
    /** 所在区 */
    region?: string;
    /** 启用/禁用(USER_ENABLED_OR_DISABLED) */
    isEnabled?: boolean;
}