HTML_MIGRATION_STRATEGY.md 18 KB

纯 HTML 迁移方案分析报告 (更新版)

1. 总体评估

用户希望将基于 UniApp/Vue.js 的现有逻辑(主要涉及 pages/tpl/style3/index.vue, pages/tpl/style3/signup.vue, pages/tpl/style3/rankList.vue, pages/tpl/style3/rankOverview.vue)迁移到一套新的、纯 HTML/JS/CSS 的 UI 设计中。新的 UI 文件位于 pages/tpl/style3/new/ 目录下,包括 index.html (入口页), signup.html (报名页), ranklist.html (排行榜/总览页)。

核心挑战

  1. UniApp API 依赖: 原有业务逻辑文件 (common/api.js, common/tools.js, common/cardfunc.js) 大量使用了 uni.requestuni.showToast 等 UniApp 特有的 API,这些在标准浏览器环境中不可用。
  2. Vue 响应式与指令: 原有 UI 通过 Vue 的数据绑定、指令(v-if, v-for, v-model)实现动态渲染和交互。纯 HTML/JS 环境需要手动操作 DOM。
  3. 模块化: 原有 JS 文件使用 ES Module (export, import) 语法。在纯 HTML 环境中直接使用需要 <script type="module"> 并且要解决模块路径和依赖顺序问题。

结论:迁移完全可行,但需要构建一个“UniApp 兼容层”来适配旧有逻辑,并采用原生 JavaScript 来实现数据与 UI 的绑定及交互。

2. 目录结构规划

为了更好地管理代码,建议在 pages/tpl/style3/new/ 下创建 js/ 目录来存放所有 JavaScript 文件。

pages/tpl/style3/new/
├── index.html          (新UI入口页)
├── signup.html         (新UI报名页)
├── ranklist.html       (新UI排行榜/总览页)
├── bd.png              (新UI图片资源)
├── gd.png              (新UI图片资源)
├── css/                (可选:如果需要将 Tailwind/自定义样式提取到单独文件)
└── js/
    ├── uni-compat.js   (新建:核心兼容层,模拟 uni.xyz API)
    ├── define.js       (移植:常量定义,如 defaultPopUpDataList)
    ├── api.js          (移植:API 接口地址定义)
    ├── tools.js        (移植:工具函数)
    ├── cardfunc.js     (移植:配置加载与处理逻辑)
    ├── logic-index.js  (新建:针对 index.html 的业务逻辑)
    ├── logic-signup.js (新建:针对 signup.html 的业务逻辑)
    └── logic-ranklist.js (新建:针对 ranklist.html 的业务逻辑)

3. 详细实施方案

3.1 构建 UniApp 兼容层 (js/uni-compat.js)

这是一个关键的适配器,用于在标准浏览器环境中模拟 UniApp 的部分 API。

草稿示例 (js/uni-compat.js):

// pages/tpl/style3/new/js/uni-compat.js
(function() {
    window.uni = window.uni || {}; // 确保全局uni对象存在

    // 1. 模拟网络请求 (核心)
    uni.request = function(options) {
        let headers = options.header || {};
        if (options.method === 'POST' && !headers['Content-Type']) {
            headers['Content-Type'] = 'application/json'; // 默认JSON
            // 如果是 x-www-form-urlencoded,需要转换data
            if (options.header && options.header['Content-Type'] === 'application/x-www-form-urlencoded') {
                headers['Content-Type'] = 'application/x-www-form-urlencoded';
                options.body = Object.keys(options.data).map(key => `${key}=${encodeURIComponent(options.data[key])}`).join('&');
            } else if (options.data && typeof options.data === 'object') {
                options.body = JSON.stringify(options.data);
            }
        } else if (options.method === 'GET' && options.data) {
            const queryString = Object.keys(options.data).map(key => `${key}=${encodeURIComponent(options.data[key])}`).join('&');
            options.url = `${options.url}?${queryString}`;
        }
        
        fetch(options.url, {
            method: options.method || 'GET',
            headers: headers,
            body: options.body || undefined
        })
        .then(response => response.json())
        .then(data => {
            if (options.success) {
                options.success({ statusCode: 200, data: data }); // 适配 uni.request 的回调格式
            }
        })
        .catch(err => {
            console.error('uni.request error:', err);
            if (options.fail) options.fail(err);
        });
    };

    // 2. 模拟本地存储
    uni.setStorageSync = function(key, data) {
        localStorage.setItem(key, JSON.stringify(data));
    };
    uni.getStorageSync = function(key) {
        const val = localStorage.getItem(key);
        try { return JSON.parse(val); } catch(e) { return val; }
    };
    uni.getStorage = function(obj) { // 模拟异步版本
        try {
            const data = uni.getStorageSync(obj.key);
            if(obj.success) obj.success({ data: data });
        } catch (e) {
            if(obj.fail) obj.fail(e);
        }
    };
    uni.setStorage = function(obj) { // 模拟异步版本
        try {
            uni.setStorageSync(obj.key, obj.data);
            if(obj.success) obj.success();
        } catch (e) {
            if(obj.fail) obj.fail(e);
        }
    };

    // 3. 模拟交互反馈
    uni.showToast = function(options) {
        // 可替换为更美观的自定义 Toast 实现
        alert(`Toast: ${options.title}`); // 简单实现
        // console.log('Toast:', options.title);
    };
    
    // 4. 模拟系统信息
    uni.getSystemInfoSync = function() {
        return { appVersion: '1.0.0' }; // 默认值
    };

    // 5. 模拟导航
    uni.navigateTo = function(options) {
        window.location.href = options.url;
    };

    // 6. 模拟 getApp() 全局对象 (用于 cardfunc.js)
    window.getApp = function() {
        return {
            globalData: { defaultMatchLogo: '' }, // 需根据实际情况填充
            $cardconfigType: 'remote' // 'remote' 或 'local',根据需要调整
        };
    };

    // 7. 模拟原生AppAction (由 tools.js 调用)
    window.appAction = function(url, actType = "") {
        if (url.startsWith('action://')) {
            console.log(`Simulating native app action: ${url}`);
            // 在纯HTML中,action://通常无法直接处理。
            // 可以在这里做一些兼容性处理,比如弹出提示或者跳转到特定H5页面。
            // 例如,如果 action://to_login/,可以跳到H5登录页
            if (url.includes('to_login/')) {
                // window.location.href = '/login.html'; // 假设有H5登录页
                alert('请登录');
            } else if (url.includes('to_home/')) {
                window.location.href = 'index.html'; // 假设首页
            } else if (url.includes('to_detail/')) {
                // window.location.href = '/game_detail.html?id=' + url.split('id=')[1].split('&')[0];
                alert(`进入比赛详情: ${url}`);
            }
        } else if (url.startsWith('http') || url.startsWith('/')) {
            window.location.href = url;
        } else {
            console.warn(`Unknown app action URL: ${url}`);
        }
    };

})();

3.2 移植公共模块 (js/)

将原 common/ 目录下的 JS 文件内容复制到 pages/tpl/style3/new/js/ 相应文件,并进行改造以适应浏览器环境。

  • js/define.js:

    • 移除 export const
    • 直接定义全局变量或使用 window.define = {...} 挂载。
    // pages/tpl/style3/new/js/define.js
    window.tplStyleList = [];
    tplStyleList[0] = 'blue';
    // ... 其他 tplStyleList 的定义
    
    window.teamName = [];
    teamName[0] = [];
    // ... 其他 teamName 的定义
    
    window.defaultPopUpDataList = [...];
    window.defaultPopUpDataList2 = [...];
    window.defaultPopUpDataList3 = [...];
    
    • js/api.js:
    • 移除 export const
    • process.env.OSS_URLprocess.env.API_BASE_URL 需硬编码。
    • token 变量可以设为初始空字符串,或从 URL 参数获取。
    • checkResCode, checkToken 函数可以直接定义在全局。
    // pages/tpl/style3/new/js/api.js
    const API_BASE_URL = 'YOUR_ACTUAL_API_SERVER_URL/'; // !!! 替换为实际的 API 地址
    const OSS_URL = 'YOUR_ACTUAL_OSS_URL/'; // !!! 替换为实际的 OSS 地址
    
    window.ossUrl = OSS_URL;
    window.apiServer = API_BASE_URL;
    
    window.token = ''; // 初始为空,会从 URL 参数中获取或动态设置
    
    // 所有 API 接口路径
    window.apiCardBaseQuery = API_BASE_URL + 'CardBaseQuery';
    // ... 其他 apiXXXQuery 定义
    
    // 辅助函数直接定义
    window.checkResCode = function(res, failLabel='') {
        // ... 原 common/api.js 中的 checkResCode 逻辑,将 uni.showToast 替换为 window.uni.showToast
        if (res.data.code == 0) {
            return true;
        } else if (res.statusCode == 401) {
            window.uni.showToast({ title: `您尚未登录`, icon: 'none' });
            window.appAction(`action://to_login/`);
            return false;
        } else {
            window.uni.showToast({ title: `${failLabel}${res.data.message}`, icon: 'none' });
            return false;
        }
    };
    
    window.checkToken = function(token) {
        // ... 原 common/api.js 中的 checkToken 逻辑
        const regex = /^[0-9A-Za-z]{32}$/;
        if (regex.test(token)) {
            return true;
        } else {
            window.uni.showToast({ title: `您尚未登录`, icon: 'none' });
            window.appAction(`action://to_login/`);
            return false;
        }
    };
    
  • js/tools.js:

    • 移除 import tools from '/common/tools';export default tools;
    • tools 对象直接定义在 window 上。
    • 内部对 uni 的调用会通过 uni-compat.js 提供的 window.uni 对象。
    • 内部对 appAction 的调用需要改为 window.appAction (或者确保 appAction 在全局)。
    // pages/tpl/style3/new/js/tools.js
    // 假设 window.uni 和 window.appAction 已经存在 (由 uni-compat.js 提供)
    window.tools = {
        // ... 原 common/tools.js 中的所有方法
        // 注意:内部对 uni.xyz 的调用会自动映射到 window.uni.xyz
        // appAction 方法会调用到 window.appAction (由 uni-compat.js 提供模拟)
    };
    
    • js/cardfunc.js:
    • 移除 import ... 语句。
    • cardfunc 对象直接定义在 window 上。
    • 确保 window.tools, window.uni, window.checkResCode, window.apiXXX 等变量在加载 cardfunc.js 之前已定义。
    // pages/tpl/style3/new/js/cardfunc.js
    // 假设 window.tools, window.uni, window.apiXXX, window.checkResCode, window.defaultPopUpDataList 等都已定义
    window.cardfunc = {
        caller: null, // 在纯HTML中,caller的概念可能不再适用,或者需要重新定义
        token: "",
        ecId: 0,
        isNewUser: false,
        cardConfigData: { /* ... */ },
        userConfigData: { /* ... */ },
    
        init(token, ecId) { // 调整 init 参数,去除 caller
            this.token = token;
            this.ecId = ecId;
            // this.removeCss(); // 如果纯HTML没有uni.css,则可能不需要
        },
        // ... 其他 cardfunc 中的所有方法
        // 内部对 uni.request 会使用 window.uni.request
        // 内部对 tools.xxx 会使用 window.tools.xxx
        // 内部对 apiXXX 会使用 window.apiXXX
        // 内部对 checkResCode 会使用 window.checkResCode
        // 内部对 getApp().xxx 会使用 window.getApp().xxx
    };
    

3.3 针对每个页面的业务逻辑 (js/logic-*.js)

每个 HTML 页面将有一个专属的 logic-*.js 文件来处理其特定的业务逻辑。

3.3.1 pages/tpl/style3/new/js/logic-index.js (对应 index.html)

逻辑来源: pages/tpl/style3/index.vue

  1. 全局状态: 定义页面所需变量 (token, ecId, beginSecond, endSecond, mcState, isJoin 等)。
  2. initPage() 函数: 页面加载时执行。
    • 解析 URL 参数: 从 window.location.search 获取 token, id (即 ecId)。
    • 初始化 cardfunc: window.cardfunc.init(state.token, state.ecId);
    • 加载配置: window.cardfunc.getCardConfig(loadConfigCallback);
    • 数据请求:
      • 调用 apiCardBaseQuery 获取赛事基本信息(beginSecond, endSecond)。
      • 调用 apiUserJoinCardQuery 获取用户报名状态 (isJoin)。
    • 启动倒计时/更新 UI: 根据 beginSecond, endSecond, isJoin 更新 action-btn 的文本、样式,以及倒计时显示。
    • 绑定事件: 为 action-btn 绑定点击事件。
  3. updateUI() 函数: 根据 mcStateisJoin 动态修改 action-btntimer-container 的内容和样式。
  4. btnClick() 事件处理:
    • 根据 mcStateisJoin 决定跳转到 signup.htmlranklist.html
    • 使用 window.uni.navigateTo({ url: '...' });window.appAction('...');

3.3.2 pages/tpl/style3/new/js/logic-signup.js (对应 signup.html)

逻辑来源: pages/tpl/style3/signup.vue

  1. 全局状态: token, ecId, mcName, beginSecond, endSecond, coiRs (组织列表), nickName, coiId 等。
  2. initPage() 函数:
    • 解析 URL 参数: token, id, from
    • 初始化 cardfunc: window.cardfunc.init(state.token, state.ecId);
    • 加载配置: window.cardfunc.getCardConfig(loadConfigCallback);
    • 数据请求:
      • apiCardDetailQuery 获取赛事详情和用户已填信息。
      • apiOnlineMcSignUpDetail 获取组织列表 (coiRs),填充到“选择战队”的下拉菜单。
    • 更新 UI: 填充“昵称”输入框,更新“比赛时间”显示,动态生成下拉菜单选项。
    • 绑定事件: 为“立即报名”按钮、下拉菜单、返回按钮、说明按钮等绑定事件。
  3. btnSignup() 函数: 验证输入,弹出确认框,然后调用 apiOnlineMcSignUp
  4. selectOption() 函数: 更新下拉菜单选中项的显示和隐藏 coiId 值。
  5. 导航: 报名成功后跳转到 ranklist.html

3.3.3 pages/tpl/style3/new/js/logic-ranklist.js (对应 ranklist.html)

逻辑来源: pages/tpl/style3/rankList.vuepages/tpl/style3/rankOverview.vue

  1. 全局状态: token, ecId, mcId, mcName, currentTab, currentMetric, rankList (所有榜单数据), userInfo (个人信息), mapList (地图列表), ocaId (当前地图ID) 等。
  2. initPage() 函数:
    • 解析 URL 参数: token, id
    • 初始化 cardfunc: window.cardfunc.init(state.token, state.ecId);
    • 加载配置: window.cardfunc.getCardConfig(loadConfigCallback);
    • 数据请求:
      • apiMatchRsDetailQuery 获取赛事和个人概览数据。
      • apiCompStatisticQuery 获取全局统计数据(用于跑马灯)。
      • apiMapListQuery 获取地图列表。
      • apiCardRankDetailQuery 获取榜单数据。
    • 更新 UI: 填充个人信息卡片、跑马灯、默认榜单列表。
    • 绑定事件: Tabs 切换、维度切换、底部“进入比赛”按钮、编辑资料、说明模态框等。
  3. renderLeaderboard() 函数: 负责根据 currentTab, currentMetricrankList 数据渲染排行榜列表。
  4. switchMainTab(), switchMetric() 函数: 切换 Tab 时更新 state.currentTab/state.currentMetric,然后调用 renderLeaderboard()
  5. openDrawer(), closeDrawer() 函数: 控制底部抽屉的显示与隐藏。
  6. editProfile(), saveProfile() 函数: 弹出/关闭编辑资料模态框,保存时调用 apiOnlineMcSignUp 更新。
  7. 导航: 底部“进入比赛”按钮跳转到 action://to_detail 或其他 H5 页面。返回按钮跳转到 index.html

3.4 HTML 文件集成

每个 HTML 文件的底部需要引入其所需的 JS 文件,注意顺序:

<!-- 在所有 HTML 文件中 (例如 index.html, signup.html, ranklist.html) -->
<!-- 1. Tailwind CSS 和 FontAwesome (已存在) -->
<!-- 2. 兼容层 -->
<script src="js/uni-compat.js"></script>
<!-- 3. 基础定义 -->
<script src="js/define.js"></script>
<script src="js/api.js"></script>
<script src="js/tools.js"></script>
<!-- 4. 业务模块 -->
<script src="js/cardfunc.js"></script>
<!-- 5. 页面特定逻辑 (每个页面引入自己的) -->
<!-- index.html --> <script src="js/logic-index.js"></script>
<!-- signup.html --> <script src="js/logic-signup.js"></script>
<!-- ranklist.html --> <script src="js/logic-ranklist.js"></script>

<script>
    // 在每个页面的 script 标签底部,调用各自的初始化函数
    // 例如对于 index.html:
    document.addEventListener('DOMContentLoaded', function() {
        initIndexPage(); // logic-index.js 中定义的入口函数
    });
</script>

4. 潜在风险与注意事项

  1. 样式冲突与 Tailwind: 新 UI 使用 Tailwind CSS。如果未来与 UniApp 项目的其他部分集成,可能存在样式冲突。建议将新 UI 的 Tailwind 类转换为静态 CSS 文件,或使用 Tailwind 的 @apply 规则来集成。
  2. API 域名: api.js 中的 API_BASE_URLOSS_URL 必须替换为真实的服务器地址。
  3. 跨域问题 (CORS): 如果这些 HTML 文件部署在与 API 服务器不同的域,浏览器会遇到 CORS 问题。需要确保服务器端已正确配置 CORS 头部。
  4. 图片资源路径: HTML 中引用的图片资源 (src="https://orienteering.beswell.com/card/nanning/...", ./gd.png, ./bd.png) 在部署时需要确保路径正确。
  5. 原生 App 交互 (action://): uni-compat.js 中的 window.appAction 只是一个模拟。在实际的混合应用 (Hybrid App) 中,需要由原生 App 拦截并处理 action:// 协议的 URL。
  6. cardfunc.initcaller 参数: 在 Vue 组件中 this 就是 caller。在纯 HTML 中,caller 的概念不再直接适用。cardfunc.init(caller, token, ecId) 可以简化为 cardfunc.init(token, ecId),或者 caller 可以是当前页面的一个全局对象或空对象。

这个方案提供了将现有 UniApp/Vue 逻辑迁移到纯 HTML/JS 环境的详细路线图,并考虑了 UniApp 特有 API 的兼容性问题。