| | |
| | | <div class="invite-box"> |
| | | <div class="invite-label">邀请码</div> |
| | | <div class="invite-code" id="inviteCodeValue">--</div> |
| | | <div class="copy-tip">安装完成后请返回本页,点击“打开 App 继续注册”完成绑定。</div> |
| | | <div class="copy-tip" id="flowTip">点击下方按钮,已安装将直接打开 APP;未安装会自动下载。</div> |
| | | </div> |
| | | |
| | | <div class="btn-group"> |
| | | <button class="download-btn" id="androidDownloadBtn" type="button">下载 Android 版</button> |
| | | <button class="download-btn secondary" id="iosDownloadBtn" type="button">下载 iOS 版</button> |
| | | <button class="download-btn secondary" id="openAppBtn" type="button">打开 App 继续注册</button> |
| | | <button class="download-btn" id="primaryActionBtn" type="button">打开 APP 继续注册</button> |
| | | <button class="copy-btn" id="copyInviteBtn" type="button">复制邀请码</button> |
| | | </div> |
| | | |
| | |
| | | <div class="section"> |
| | | <div class="section-title">安装说明</div> |
| | | <ol class="notice"> |
| | | <li>点击对应平台下载按钮,下载安装 APP。</li> |
| | | <li>安装完成后返回本页,点击“打开 App 继续注册”。</li> |
| | | <li>点击“打开 APP 继续注册”,页面会优先尝试打开 APP。</li> |
| | | <li>若尚未安装,将自动下载对应平台安装包;安装完成后返回本页会继续打开 APP。</li> |
| | | <li>普通用户注册成功后,将自动关联邀请医生所属诊所。</li> |
| | | </ol> |
| | | </div> |
| | |
| | | iosDownloadUrl: '', |
| | | wechatBrowser: false, |
| | | openAppBaseUrl: '', |
| | | bindTicketLoading: false |
| | | bindTicketLoading: false, |
| | | openAppUrl: '', |
| | | autoOpenTimer: 0, |
| | | openingApp: false, |
| | | pendingResumeOpen: false |
| | | } |
| | | |
| | | const PENDING_OPEN_STORAGE_KEY = 'appInvitePendingOpen' |
| | | const AUTO_DOWNLOAD_DELAY_MS = 1600 |
| | | const RESUME_AUTO_OPEN_DELAY_MS = 500 |
| | | |
| | | /** |
| | | * 页面初始化。 |
| | |
| | | bindActions() |
| | | detectWechatEnvironment() |
| | | bindWechatEnvironmentListeners() |
| | | bindResumeAutoOpenListeners() |
| | | const inviteCode = normalizeInviteCode(readQueryParam('inviteCode')) |
| | | state.inviteCode = inviteCode |
| | | renderInviteCode(inviteCode) |
| | |
| | | document.getElementById('updateNoticeText').textContent = '当前已提供最新安装包下载。' |
| | | } |
| | | updateButtonState() |
| | | scheduleResumeAutoOpen() |
| | | } |
| | | |
| | | /** |
| | |
| | | document.getElementById('clinicTitle').textContent = '欢迎下载中医培训 APP' |
| | | document.getElementById('inviteCodeValue').textContent = inviteCode || '--' |
| | | document.getElementById('updateNoticeText').textContent = '如需绑定邀请关系,请复制邀请码后在注册页填写。' |
| | | document.getElementById('flowTip').textContent = '邀请信息加载失败时,可复制邀请码后在 APP 注册页手动填写。' |
| | | updateButtonState() |
| | | } |
| | | |
| | |
| | | * 绑定页面按钮事件。 |
| | | */ |
| | | function bindActions() { |
| | | document.getElementById('androidDownloadBtn').addEventListener('click', function () { |
| | | handleDownload(state.androidDownloadUrl, 'ANDROID') |
| | | }) |
| | | document.getElementById('iosDownloadBtn').addEventListener('click', function () { |
| | | handleDownload(state.iosDownloadUrl, 'IOS') |
| | | document.getElementById('primaryActionBtn').addEventListener('click', function () { |
| | | void handlePrimaryAction(false) |
| | | }) |
| | | document.getElementById('copyInviteBtn').addEventListener('click', function () { |
| | | if (!state.inviteCode) { |
| | |
| | | } |
| | | copyInviteCode(state.inviteCode, true) |
| | | }) |
| | | document.getElementById('openAppBtn').addEventListener('click', function () { |
| | | void handleOpenApp() |
| | | }) |
| | | document.getElementById('wechatGuideClose').addEventListener('click', function () { |
| | | hideWechatGuide() |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | * 处理下载动作,仅负责下载安装包。 |
| | | * 处理主按钮动作,优先拉起 APP,未安装时自动下载。 |
| | | */ |
| | | function handleDownload(url, platform) { |
| | | async function handlePrimaryAction(fromResume) { |
| | | if (shouldBlockDirectDownload()) { |
| | | showWechatGuide() |
| | | showStatus('请点击右上角,选择在浏览器中打开后再下载') |
| | | return |
| | | } |
| | | const normalizedUrl = normalizeOptionalText(url) |
| | | if (!normalizedUrl) { |
| | | showStatus(platform === 'ANDROID' ? 'Android 安装包暂未配置' : 'iOS 安装包暂未配置') |
| | | return |
| | | } |
| | | window.location.href = normalizedUrl |
| | | } |
| | | |
| | | /** |
| | | * 生成绑定票据并通过深链打开 App 继续注册。 |
| | | */ |
| | | async function handleOpenApp() { |
| | | if (state.bindTicketLoading) { |
| | | showStatus('请点击右上角,选择在浏览器中打开后继续') |
| | | return |
| | | } |
| | | if (!state.inviteCode) { |
| | | showStatus('当前二维码未包含邀请码,请改用手动填写邀请码') |
| | | showStatus('当前二维码未包含邀请码,请复制邀请码后手动注册') |
| | | return |
| | | } |
| | | const openAppUrl = await ensureOpenAppUrl() |
| | | if (!openAppUrl) { |
| | | return |
| | | } |
| | | persistPendingOpen() |
| | | tryOpenApp(openAppUrl) |
| | | if (fromResume) { |
| | | clearPendingOpen() |
| | | showStatus('正在尝试打开 APP,如未打开请点击按钮重试') |
| | | return |
| | | } |
| | | scheduleDownloadFallback() |
| | | } |
| | | |
| | | /** |
| | | * 确保已拿到可打开 APP 的深链地址。 |
| | | */ |
| | | async function ensureOpenAppUrl() { |
| | | if (state.openAppUrl) { |
| | | return state.openAppUrl |
| | | } |
| | | if (state.bindTicketLoading) { |
| | | return '' |
| | | } |
| | | if (!state.openAppBaseUrl) { |
| | | showStatus('App 打开地址暂未配置') |
| | | return |
| | | return '' |
| | | } |
| | | state.bindTicketLoading = true |
| | | updateButtonState() |
| | | try { |
| | | const response = await fetch('/api/open/app-invite/bind-ticket', { |
| | | method: 'POST', |
| | |
| | | const payload = await response.json() |
| | | if (!payload || payload.code !== 0 || !payload.data || !payload.data.openAppUrl) { |
| | | showStatus(payload && (payload.msg || payload.message) ? payload.msg || payload.message : '绑定凭证生成失败,请稍后重试') |
| | | return |
| | | return '' |
| | | } |
| | | window.location.href = payload.data.openAppUrl |
| | | showStatus('若未自动打开 App,请安装后返回本页再次点击') |
| | | state.openAppUrl = normalizeOptionalText(payload.data.openAppUrl) |
| | | return state.openAppUrl |
| | | } catch (error) { |
| | | showStatus('打开 App 失败,请安装后返回本页再次点击') |
| | | showStatus('绑定凭证生成失败,请稍后重试') |
| | | return '' |
| | | } finally { |
| | | state.bindTicketLoading = false |
| | | updateButtonState() |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 通过 Scheme 拉起 APP。 |
| | | */ |
| | | function tryOpenApp(openAppUrl) { |
| | | state.openingApp = true |
| | | showStatus('正在打开 APP,请稍候...') |
| | | window.location.href = openAppUrl |
| | | window.setTimeout(function () { |
| | | state.openingApp = false |
| | | }, AUTO_DOWNLOAD_DELAY_MS) |
| | | } |
| | | |
| | | /** |
| | | * 拉起失败时按当前平台下载 APP。 |
| | | */ |
| | | function scheduleDownloadFallback() { |
| | | clearAutoOpenTimer() |
| | | state.autoOpenTimer = window.setTimeout(function () { |
| | | if (document.hidden) { |
| | | return |
| | | } |
| | | const downloadUrl = resolveCurrentPlatformDownloadUrl() |
| | | if (!downloadUrl) { |
| | | showStatus('当前平台安装包暂未配置,请复制邀请码后联系工作人员') |
| | | return |
| | | } |
| | | document.getElementById('flowTip').textContent = '安装完成后返回本页,页面会自动打开 APP 继续注册。' |
| | | document.getElementById('primaryActionBtn').textContent = '安装完成后自动打开中...' |
| | | showStatus('未检测到 APP,正在为您下载对应安装包') |
| | | window.location.href = downloadUrl |
| | | }, AUTO_DOWNLOAD_DELAY_MS) |
| | | } |
| | | |
| | | /** |
| | | * 根据浏览器平台选择安装包地址。 |
| | | */ |
| | | function resolveCurrentPlatformDownloadUrl() { |
| | | if (isIosBrowser()) { |
| | | return state.iosDownloadUrl |
| | | } |
| | | if (isAndroidBrowser()) { |
| | | return state.androidDownloadUrl |
| | | } |
| | | return state.androidDownloadUrl || state.iosDownloadUrl |
| | | } |
| | | |
| | | /** |
| | | * 判断当前访问环境是否为 iOS 浏览器。 |
| | | */ |
| | | function isIosBrowser() { |
| | | const userAgent = String(window.navigator.userAgent || '').toLowerCase() |
| | | return /iphone|ipad|ipod/.test(userAgent) |
| | | } |
| | | |
| | | /** |
| | | * 判断当前访问环境是否为 Android 浏览器。 |
| | | */ |
| | | function isAndroidBrowser() { |
| | | const userAgent = String(window.navigator.userAgent || '').toLowerCase() |
| | | return userAgent.indexOf('android') >= 0 || userAgent.indexOf('adr') >= 0 |
| | | } |
| | | |
| | | /** |
| | | * 记录安装后返回页面需要继续打开 APP。 |
| | | */ |
| | | function persistPendingOpen() { |
| | | try { |
| | | window.localStorage.setItem(PENDING_OPEN_STORAGE_KEY, JSON.stringify({ |
| | | inviteCode: state.inviteCode, |
| | | openAppUrl: state.openAppUrl, |
| | | createdAt: Date.now() |
| | | })) |
| | | } catch (error) { |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据本地待继续记录恢复自动打开 APP。 |
| | | */ |
| | | function scheduleResumeAutoOpen() { |
| | | const pendingOpen = readPendingOpen() |
| | | if (!pendingOpen || pendingOpen.inviteCode !== state.inviteCode || state.pendingResumeOpen) { |
| | | return |
| | | } |
| | | state.pendingResumeOpen = true |
| | | if (pendingOpen.openAppUrl) { |
| | | state.openAppUrl = pendingOpen.openAppUrl |
| | | } |
| | | window.setTimeout(function () { |
| | | if (!document.hidden) { |
| | | void handlePrimaryAction(true) |
| | | } |
| | | state.pendingResumeOpen = false |
| | | }, RESUME_AUTO_OPEN_DELAY_MS) |
| | | } |
| | | |
| | | /** |
| | | * 绑定页面返回前台时的自动继续动作。 |
| | | */ |
| | | function bindResumeAutoOpenListeners() { |
| | | window.addEventListener('pageshow', function () { |
| | | scheduleResumeAutoOpen() |
| | | }) |
| | | document.addEventListener('visibilitychange', function () { |
| | | if (!document.hidden && !state.openingApp) { |
| | | scheduleResumeAutoOpen() |
| | | } |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | * 读取待继续打开 APP 的本地记录。 |
| | | */ |
| | | function readPendingOpen() { |
| | | try { |
| | | const rawValue = window.localStorage.getItem(PENDING_OPEN_STORAGE_KEY) |
| | | if (!rawValue) { |
| | | return null |
| | | } |
| | | const parsedValue = JSON.parse(rawValue) |
| | | if (!parsedValue || Date.now() - Number(parsedValue.createdAt || 0) > 24 * 60 * 60 * 1000) { |
| | | window.localStorage.removeItem(PENDING_OPEN_STORAGE_KEY) |
| | | return null |
| | | } |
| | | return parsedValue |
| | | } catch (error) { |
| | | return null |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 清理已经消费的待继续打开记录。 |
| | | */ |
| | | function clearPendingOpen() { |
| | | try { |
| | | window.localStorage.removeItem(PENDING_OPEN_STORAGE_KEY) |
| | | } catch (error) { |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 清理当前延迟打开或下载任务。 |
| | | */ |
| | | function clearAutoOpenTimer() { |
| | | if (state.autoOpenTimer) { |
| | | window.clearTimeout(state.autoOpenTimer) |
| | | state.autoOpenTimer = 0 |
| | | } |
| | | } |
| | | |
| | |
| | | * 根据当前接口数据刷新下载按钮状态。 |
| | | */ |
| | | function updateButtonState() { |
| | | document.getElementById('androidDownloadBtn').disabled = !state.androidDownloadUrl |
| | | document.getElementById('iosDownloadBtn').disabled = !state.iosDownloadUrl |
| | | const primaryActionBtn = document.getElementById('primaryActionBtn') |
| | | primaryActionBtn.disabled = state.bindTicketLoading || (!state.androidDownloadUrl && !state.iosDownloadUrl) |
| | | primaryActionBtn.textContent = state.bindTicketLoading ? '正在准备注册信息...' : '打开 APP 继续注册' |
| | | } |
| | | |
| | | /** |