checkin-info-动态标签页分析与准备.md 18.9 KB

checkin/info 动态标签页分析与接口接入准备

日期: 2026-02-09 目的: 为动态标签页的 default_title 从接口获取数据做准备


📊 当前实现分析

1. 动态标签页配置(155-177 行)

const tab_configs = ref([
  {
    key: 'introduction',
    title_key: 'introduction_text',    // 接口返回的标题字段
    content_key: 'introduction',        // 接口返回的内容字段
    default_title: '敬老月优惠',        // ❌ 当前写死
    id: 'introduction'
  },
  {
    key: 'story',
    title_key: 'story_text',
    content_key: 'story',
    default_title: '敬老月优惠',        // ❌ 当前写死
    id: 'story'
  },
  {
    key: 'experience',
    title_key: 'experience_text',
    content_key: 'experience',
    default_title: '敬老月优惠',        // ❌ 当前写死
    id: 'experience'
  }
]);

结构说明

  • key: 标签页的唯一标识符
  • title_key: 从接口数据中读取标题的字段名(如 introduction_text
  • content_key: 从接口数据中读取内容的字段名(如 introduction
  • default_title: 当接口未返回标题时的后备值(目前写死为"敬老月优惠"
  • id: DOM 元素 ID,用于内容渲染和事件绑定

2. 标题设置逻辑(186-189 行,279-282 行)

有两处相同的逻辑

// 第 1 处:watch props.info 变化时(186-189 行)
tab_configs.value.forEach(config => {
  page_details.value[config.title_key] = page_details.value[config.title_key] || config.default_title;
});

// 第 2 处:onMounted 时(279-282 行)
tab_configs.value.forEach(config => {
  page_details.value[config.title_key] = page_details.value[config.title_key] || config.default_title;
});

逻辑说明

  • 如果 page_details.value[config.title_key] 有值(如 page_details.value.introduction_text),使用接口返回的值
  • 否则使用 config.default_title(当前写死为"敬老月优惠")

3. 数据来源(263-293 行)

主要数据来源mapAPI

const { data } = await mapAPI({ i: id });
const raw_list = data.list[0].list;                    // 标记点列表
const marker_id = $route.query.marker_id;
const current_marker = raw_list.filter(item => item.id == marker_id)[0];

page_details.value = {
  ...current_marker.details[0],  // 标记点详情
  position: current_marker.position,
  path: current_marker.path,
  // ...
};

当前接口返回的数据结构(推测):

{
  code: 1,
  data: {
    list: [
      {
        id: 1,
        list: [                    // 标记点列表
          {
            id: 123,              // marker_id
            position: [lng, lat],
            path: [],
            details: [            // 标记点详情
              {
                id: 456,
                name: "某某景点",
                banner: [...],
                note: "...",

                // 三个标签页的内容
                introduction: "<p>介绍内容...</p>",
                story: "<p>故事内容...</p>",
                experience: "<p>体验内容...</p>",

                // ❌ 三个标签页的标题(可能不存在)
                introduction_text: undefined,  // 接口可能不返回
                story_text: undefined,
                experience_text: undefined,

                // 其他字段
                show_audio: true,
                experience_audio: [...]
              }
            ]
          }
        ]
      }
    ]
  }
}

4. 标签页渲染(41-64 行)

<van-tabs>
  <template v-for="config in tab_configs" :key="config.key">
    <van-tab
      :title="page_details[config.title_key]"
      v-if="page_details[config.content_key]"
    >
      <!-- 内容 -->
      <div :id="config.id" v-html="page_details[config.content_key]"></div>
    </van-tab>
  </template>
</van-tabs>

渲染逻辑

  • v-if="page_details[config.content_key]":只有当内容存在时才显示该标签页
  • :title="page_details[config.title_key]":标题从 page_details 读取

🎯 问题总结

当前痛点

  1. default_title 写死

    • 所有标签页的 default_title 都是"敬老月优惠"
    • 无法根据不同场景动态设置
  2. 接口数据可能不完整

    • 接口可能不返回 introduction_textstory_textexperience_text
    • 导致所有标签页都使用默认值
  3. 逻辑重复

    • 标题设置逻辑在两处重复(watch 和 onMounted)

🔮 接口数据结构推测

方案 A: 接口直接返回标签页标题(推荐)✅

{
  code: 1,
  data: {
    list: [
      {
        id: 1,
        list: [
          {
            id: 123,
            details: [
              {
                id: 456,
                name: "某某景点",

                // ✅ 标签页内容
                introduction: "<p>介绍内容...</p>",
                story: "<p>故事内容...</p>",
                experience: "<p>体验内容...</p>",

                // ✅ 标签页标题(新增字段)
                introduction_text: "景点介绍",      // 从接口获取
                story_text: "历史故事",             // 从接口获取
                experience_text: "游客体验",        // 从接口获取

                // ✅ 全局默认标题(可选)
                default_tab_title: "默认标签"      // 统一的后备标题
              }
            ]
          }
        ]
      }
    ]
  }
}

方案 B: 接口返回标签页配置对象

{
  code: 1,
  data: {
    list: [
      {
        id: 1,
        list: [
          {
            id: 123,
            details: [
              {
                id: 456,
                name: "某某景点",

                // ✅ 标签页配置对象
                tabs_config: {
                  introduction: {
                    title: "景点介绍",
                    content: "<p>介绍内容...</p>"
                  },
                  story: {
                    title: "历史故事",
                    content: "<p>故事内容...</p>"
                  },
                  experience: {
                    title: "游客体验",
                    content: "<p>体验内容...</p>"
                  }
                }
              }
            ]
          }
        ]
      }
    ]
  }
}

方案 C: 单独的标签页配置接口

// 新增接口:/api/getTabConfig
const tabConfigAPI = (params) => fetch.get('/srv/?a=getTabConfig', params);

// 返回数据
{
  code: 1,
  data: {
    tabs: [
      {
        key: 'introduction',
        title: '景点介绍',
        default_title: '介绍'      // 后备值
      },
      {
        key: 'story',
        title: '历史故事',
        default_title: '故事'
      },
      {
        key: 'experience',
        title: '游客体验',
        default_title: '体验'
      }
    ]
  }
}

🛠️ 接入准备方案

阶段 1: 代码重构准备(不依赖接口)

1.1 提取标签页配置到独立文件

创建文件src/views/checkin/tab-config.js

/**
 * 打卡详情页标签页配置
 *
 * @description 定义标签页的结构和默认配置
 * @module checkin/tab-config
 */

/**
 * 标签页配置数组
 * @type {Array<Object>}
 */
export const TAB_CONFIGS = [
  {
    key: 'introduction',
    title_key: 'introduction_text',
    content_key: 'introduction',
    default_title: '介绍',           // ✅ 修改为通用的默认值
    id: 'introduction'
  },
  {
    key: 'story',
    title_key: 'story_text',
    content_key: 'story',
    default_title: '故事',           // ✅ 修改为通用的默认值
    id: 'story'
  },
  {
    key: 'experience',
    title_key: 'experience_text',
    content_key: 'experience',
    default_title: '体验',           // ✅ 修改为通用的默认值
    id: 'experience'
  }
]

/**
 * 设置标签页标题
 *
 * @description 将接口返回的标题设置到 page_details,如果没有则使用默认值
 * @param {Object} page_details - 页面详情对象
 * @param {Array<Object>} tab_configs - 标签页配置数组
 * @returns {Object} 更新后的 page_details
 */
export function setTabTitles(page_details, tab_configs = TAB_CONFIGS) {
  const updated = { ...page_details }

  tab_configs.forEach(config => {
    // 优先使用接口返回的标题,否则使用默认值
    updated[config.title_key] = updated[config.title_key] || config.default_title
  })

  return updated
}

优势

  • ✅ 配置集中管理
  • ✅ 便于修改和维护
  • ✅ 可以动态加载配置
  • ✅ 提供工具函数,减少重复代码

1.2 修改 info.vue 使用新配置

修改 1:导入配置

// 在 <script setup> 顶部添加
import { TAB_CONFIGS, setTabTitles } from './tab-config'

修改 2:替换 tab_configs 定义(155-177 行)

// ❌ 删除旧代码
// const tab_configs = ref([...])

// ✅ 使用导入的配置
const tab_configs = ref(TAB_CONFIGS)

修改 3:简化标题设置逻辑

// 第 1 处:watch props.info(186-189 行)
watch(
  () => props.info,
  async (newInfo) => {
    if (newInfo && newInfo.details && newInfo.details.length) {
      page_details.value = { ...newInfo.details[0], position: newInfo.position, path: newInfo.path, current_lng: newInfo.current_lng, current_lat: newInfo.current_lat, openid: newInfo.openid };

      // ✅ 使用工具函数设置标题
      page_details.value = setTabTitles(page_details.value, tab_configs.value)

      // 获取浏览器可视范围的高度
      $('.info-page').height(props.height + 'px');
      // 检查打卡状态
      await checkInitialCheckinStatus();
    }
  },
  { immediate: true }
)

// 第 2 处:onMounted(279-282 行)
onMounted(async () => {
  // ...

  // ✅ 使用工具函数设置标题
  page_details.value = setTabTitles(page_details.value, tab_configs.value)

  // ...
})

阶段 2: 接口数据接入(等接口确定后)

2.1 方案 A: 接口直接返回标题字段

修改 tab-config.js

/**
 * 从接口数据获取标签页配置
 *
 * @description 如果接口返回了标签页标题,则更新配置
 * @param {Object} apiData - 接口返回的详情数据
 * @param {Array<Object>} tab_configs - 标签页配置数组
 * @returns {Array<Object>} 更新后的标签页配置
 */
export function updateTabConfigsFromAPI(apiData, tab_configs = TAB_CONFIGS) {
  return tab_configs.map(config => {
    const updated = { ...config }

    // 如果接口返回了该标签页的标题
    if (apiData[config.title_key]) {
      updated.default_title = apiData[config.title_key]
    }

    return updated
  })
}

修改 info.vue

// onMounted 中
onMounted(async () => {
  if (!props.info) {
    let id = $route.query.id;
    const { data } = await mapAPI({ i: id });
    const raw_list = data.list[0].list;
    const marker_id = $route.query.marker_id;
    const current_marker = raw_list.filter(item => item.id == marker_id)[0];

    page_details.value = {
      ...current_marker.details[0],
      position: current_marker.position,
      path: current_marker.path,
      current_lng: current_lng,
      current_lat: current_lat,
      openid: openid
    };

    // ✅ 如果接口返回了全局默认标题,使用它
    const globalDefaultTitle = page_details.value.default_tab_title
    if (globalDefaultTitle) {
      tab_configs.value = tab_configs.value.map(config => ({
        ...config,
        default_title: globalDefaultTitle
      }))
    }

    // ✅ 从接口数据更新标签页配置(如果接口返回了具体标题)
    tab_configs.value = updateTabConfigsFromAPI(page_details.value, tab_configs.value)

    // 设置标题
    page_details.value = setTabTitles(page_details.value, tab_configs.value)

    // ...
  }
})

2.2 方案 B: 接口返回配置对象

修改 tab-config.js

/**
 * 从接口配置对象生成标签页配置
 *
 * @description 如果接口返回了 tabs_config,则使用它
 * @param {Object} tabsConfig - 接口返回的 tabs_config
 * @returns {Array<Object>} 标签页配置数组
 */
export function generateTabConfigsFromAPI(tabsConfig) {
  if (!tabsConfig || typeof tabsConfig !== 'object') {
    return TAB_CONFIGS  // 使用默认配置
  }

  return Object.entries(tabsConfig).map(([key, config]) => ({
    key,
    title_key: `${key}_text`,
    content_key: key,
    default_title: config.title || key,
    id: key
  }))
}

修改 info.vue

// onMounted 中
onMounted(async () => {
  if (!props.info) {
    // ...获取数据

    // ✅ 如果接口返回了 tabs_config,使用它生成标签页配置
    if (page_details.value.tabs_config) {
      tab_configs.value = generateTabConfigsFromAPI(page_details.value.tabs_config)
    }

    // ...
  }
})

2.3 方案 C: 单独的标签页配置接口

创建 APIsrc/api/checkin.js

/**
 * 获取标签页配置
 *
 * @description 从接口获取标签页配置
 * @param {Object} params - 请求参数
 * @returns {Promise<Object>} 接口响应
 */
export const tabConfigAPI = (params) => fn(fetch.get('/srv/?a=getTabConfig', params))

修改 info.vue

import { tabConfigAPI } from '@/api/checkin.js'

// onMounted 中
onMounted(async () => {
  // ...获取地图数据

  try {
    // ✅ 调用标签页配置接口
    const { data: tabConfigData } = await tabConfigAPI({ id: page_details.value.id })

    if (tabConfigData && tabConfigData.tabs) {
      // 更新标签页配置
      tab_configs.value = tabConfigData.tabs.map(tab => ({
        key: tab.key,
        title_key: `${tab.key}_text`,
        content_key: tab.key,
        default_title: tab.default_title || tab.title || tab.key,
        id: tab.key
      }))
    }
  } catch (error) {
    console.error('获取标签页配置失败,使用默认配置:', error)
    // 保持使用默认配置
  }

  // 设置标题
  page_details.value = setTabTitles(page_details.value, tab_configs.value)

  // ...
})

阶段 3: 测试方案

3.1 单元测试(可选)

创建文件src/views/checkin/tab-config.test.js

import { describe, it, expect } from 'vitest'
import { setTabTitles, updateTabConfigsFromAPI, generateTabConfigsFromAPI } from './tab-config'

describe('tab-config', () => {
  describe('setTabTitles', () => {
    it('应该使用接口返回的标题', () => {
      const page_details = {
        introduction_text: '景点介绍',
        story_text: '历史故事'
      }

      const result = setTabTitles(page_details)

      expect(result.introduction_text).toBe('景点介绍')
      expect(result.story_text).toBe('历史故事')
    })

    it('应该使用默认标题当接口未返回', () => {
      const page_details = {}

      const result = setTabTitles(page_details)

      expect(result.introduction_text).toBe('介绍')
      expect(result.story_text).toBe('故事')
    })
  })

  describe('updateTabConfigsFromAPI', () => {
    it('应该从接口数据更新默认标题', () => {
      const apiData = {
        introduction_text: '新介绍',
        story_text: '新故事'
      }

      const result = updateTabConfigsFromAPI(apiData)

      expect(result[0].default_title).toBe('新介绍')
      expect(result[1].default_title).toBe('新故事')
    })
  })
})

3.2 手动测试场景

场景 1: 接口返回所有标题

// 模拟接口数据
const mockData = {
  introduction_text: '景点介绍',
  story_text: '历史故事',
  experience_text: '游客体验'
}

// 预期结果
// ✅ 标签页标题:景点介绍、历史故事、游客体验

场景 2: 接口只返回部分标题

// 模拟接口数据
const mockData = {
  introduction_text: '景点介绍',
  // story_text 未返回
  // experience_text 未返回
}

// 预期结果
// ✅ 标签页标题:景点介绍、故事(默认)、体验(默认)

场景 3: 接口未返回任何标题

// 模拟接口数据
const mockData = {}

// 预期结果
// ✅ 标签页标题:介绍(默认)、故事(默认)、体验(默认)

📋 实施清单

✅ 已完成(准备工作)

  • 分析当前实现
  • 识别问题点
  • 设计重构方案
  • 编写准备文档

🔄 待实施(等接口确定后)

第 1 步:代码重构(不依赖接口)

  • 创建 src/views/checkin/tab-config.js
  • 实现 setTabTitles() 工具函数
  • 修改 info.vue 使用新配置
  • 测试功能是否正常

第 2 步:接口接入(根据接口方案)

  • 确认接口数据结构(方案 A/B/C)
  • 实现 updateTabConfigsFromAPI()generateTabConfigsFromAPI()
  • 修改 info.vue 调用接口或处理数据
  • 处理接口失败的情况

第 3 步:测试验证

  • 单元测试(可选)
  • 手动测试(3 个场景)
  • 真机测试
  • 边界情况测试

🎯 推荐方案

推荐采用:方案 A(接口直接返回标题字段)

理由

  1. 最简单:只需在接口数据中添加 3 个字段
  2. 向后兼容:不影响现有接口结构
  3. 灵活:支持全局默认标题和单个标签页标题
  4. 性能好:无需额外请求

接口需要添加的字段

{
  details: [
    {
      // 新增字段
      introduction_text: "景点介绍",    // 介绍标签页标题
      story_text: "历史故事",          // 故事标签页标题
      experience_text: "游客体验",    // 体验标签页标题

      // 可选:全局默认标题
      default_tab_title: "默认标签"    // 当上面三个字段都不存在时使用
    }
  ]
}

💡 使用指南

当接口确定后

告诉我

  1. 接口返回的数据结构(JSON 示例)
  2. 使用哪个方案(A/B/C)

我会

  1. 根据接口结构调整代码
  2. 实现接口接入逻辑
  3. 添加错误处理
  4. 编写测试用例

快速参考

文件位置

  • 配置文件:src/views/checkin/tab-config.js(待创建)
  • 主要组件:src/views/checkin/info.vue
  • API 文件:src/api/checkin.js

关键函数

  • setTabTitles():设置标签页标题
  • updateTabConfigsFromAPI():从接口更新配置(方案 A)
  • generateTabConfigsFromAPI():从接口生成配置(方案 B)

数据流程

接口返回数据
  ↓
提取标签页标题
  ↓
更新 tab_configs
  ↓
设置 page_details 标题
  ↓
渲染标签页

📞 后续行动

当前状态:准备完成,等待接口确定

需要你提供

  • 接口文档或数据结构示例
  • 采用哪个方案(A/B/C)

我会立即

  1. 根据接口调整代码
  2. 实现完整的接入逻辑
  3. 测试验证功能

文档保存位置docs/checkin-info-动态标签页分析与准备.md