import axios from 'axios'; import { isLofter } from 'nw-detect'; import type { MissionItem, } from '../classes/MissionItem'; import { isSafeUrl } from '../utils/url'; export interface BrowseStoreData { type: 'browse'; missionCode: string; activityCode: string; missionActivityCode: string; token: string; browseTime: string | number; url: string; } export interface MissionBrush { /** 任务校验token */ token: string; /** 需要浏览时间 */ browseTime: number; /** 浏览任务链接 */ url: string; } export interface MissionDetailResponseData { /** 状态(200:成功;500:内部错误;) */ code: number; /** 错误描述 */ msg: string; data: MissionBrush; } export interface BrowseCompleteResponseData { /** 状态(200:成功;500:内部错误;) */ code: number; /** 错误描述 */ msg: string; /** 业务数据 */ data: any; } export interface BrowseCompleteRequestData { /** 任务code */ missionCode: string; /** 跳转url */ actionLinkUrl: string; /** 防刷 */ token: string; /** 活动code */ activityCode: string; /** 任务方案code */ missionActivityCode: string; } const detailUrl = '/spread/activity/mission/preCheck' const completeUrl = '/spread/activity/mission/browse' const browseStorePrefix = 'lofter_card_browseMission_' function getDetail(data: { missionCode: string; }): Promise { return axios.post(detailUrl,data).then(res=>{ return res.data }) } function completeMission(data: BrowseCompleteRequestData): Promise { return axios.post(completeUrl, data).then(res=>{ return res.data }) } /** * 处理浏览任务 * @param missionInfo 任务信息 * @param activityCode 活动code */ export async function handle({ missionItem }: { missionItem: MissionItem; }): Promise { const activityCode = missionItem.context?.activityCode; if (!activityCode) { console.error('Browse mission missing activityCode', missionItem); return; } const missionActivityCode = missionItem.context?.missionActivityCode; if (!missionActivityCode) { console.error('Browse mission missing missionActivityCode', missionItem); return; } const missionInfo = missionItem.info; try { const { data } = await getDetail({ missionCode: missionInfo.code, }) const { token, url, browseTime } = data; if (url && token && browseTime !== null && browseTime !== undefined) { let targetUrl = url if (browseTime > 0) { const storeKey = `${browseStorePrefix}${missionInfo.code}`; const storeData: BrowseStoreData = { type: 'browse', missionCode: missionInfo.code, activityCode, missionActivityCode, token, browseTime, url, } localStorage.setItem(storeKey, JSON.stringify(storeData)) targetUrl = `${url}${url.indexOf('?') >= 0 ? '&' : '?'}type=browse&missionCode=${missionInfo.code}` } else { await complete({ missionCode: missionInfo.code, actionLinkUrl: url, token, activityCode, missionActivityCode, }) } if (isLofter()) { const inside = isSafeUrl(url) ? 'true' : 'false'; location.href = `lofter://webview?url=${encodeURIComponent(targetUrl)}&inside=${inside}`; } else { location.href = targetUrl; } } else { console.error('Browse mission data missing', missionInfo); } } catch (error) { console.error('Get browse mission detail failed', missionInfo, error); } } export function complete ( params: BrowseCompleteRequestData ) { return completeMission(params).then(res => { if (res.code === 200) { return res; } return Promise.reject(res); }) } /** * 响应浏览任务(在url中type=browse及有token和browseTime的页面中生效) */ export function effect({ missionCode, }: { missionCode: string; }): void { const storeKey = `${browseStorePrefix}${missionCode}`; const storeData = localStorage.getItem(storeKey); if (!storeData) { console.warn('No matching mission store data.'); return; } const parsedStoreData: BrowseStoreData = JSON.parse(storeData); const { token, activityCode, url, missionActivityCode } = parsedStoreData; let { browseTime } = parsedStoreData; browseTime = typeof browseTime === 'string' ? parseInt(browseTime, 10) : browseTime; if (!token || browseTime === undefined || !activityCode) { console.error('Missing browse effect arguments') return; } if (browseTime < 0) { console.error('Invalid browseTime.') return; } const getSize = (number: number): string => { // 商品详情页缩放规则特殊,单独处理 if (location.pathname === '/market/fe/html/product.html') { return `${number}px` } return `${number/2}px` } const icon = document.createElement('div'); icon.style.cssText = ` position: fixed; right: ${getSize(44)}; bottom: ${getSize(324)}; width: ${getSize(130)}; z-index: 102; text-align: center; ` const clock = document.createElement('img'); clock.style.cssText = ` display: block; width: ${getSize(92)}; margin: 0 auto; ` clock.src = 'https://lofter.lf127.net/1646114448597/clock.svg'; const pointer = document.createElement('img'); pointer.src = 'https://lofter.lf127.net/1646116288354/pointer.svg'; pointer.style.cssText = ` position: absolute; width: ${getSize(46)}; top: ${getSize(6)}; left: ${getSize(42)}; transform-origin: center ${getSize(40)}; ` const animationStyle = document.createElement('style'); animationStyle.innerText = ` @-webkit-keyframes tik { to { transform: rotate(360deg); } } @keyframes tik { to { transform: rotate(360deg); } } ` document.head.appendChild(animationStyle); const second = document.createElement('div'); second.style.cssText = ` margin-top: ${getSize(6)}; line-height: ${getSize(34)}; font-size: ${getSize(22)}; font-weight: 600; color: #FFF7EA; background: #C6BA88; border-radius: ${getSize(20)}; border: 1px solid #FFF7EA; ` let timer: null | NodeJS.Timeout = null second.innerText = '滑动开始'; icon.appendChild(clock); icon.appendChild(pointer); icon.appendChild(second); document.body.appendChild(icon); const secondsNum = Math.ceil(browseTime/1000); const startTik = () => { pointer.style.cssText += ` -webkit-animation: tik ${secondsNum}s linear; animation tik ${secondsNum}s linear; ` } const countDown = (time: number) => { second.innerText = time.toString()+'s'; if (time > 0) { timer = setTimeout(() => { countDown(time - 1) }, 1000); } else { complete({ activityCode, missionActivityCode, token, missionCode, actionLinkUrl: url, }).then(() => { second.innerText = '已完成'; window.history.replaceState(null,'',url) }) } } document.addEventListener('touchstart',()=>{ if(!timer){ startTik(); countDown(secondsNum); } }) }