diff --git a/.env.development b/.env.development index 61676a9..2fc6189 100644 --- a/.env.development +++ b/.env.development @@ -6,3 +6,4 @@ VITE_BUILD_VCONSOLE = false # 是否开启调试工具 vconsole VITE_PARAMETER_ENCRYPTION = 0 # 是否开启参数加密 0关闭 1开启 # VITE_BASE_URL= ["http://172.104.39.32:25508"] # api选线域名,这里必须为数组 VITE_BASE_URL= ["https://d3gc46ktrfda7z.cloudfront.net","https://d2bvxgoqsmyuy4.cloudfront.net"] # api选线域名,这里必须为数组 +VITE_DNS_DOMAIN = 91fans.mfcg.it.com # DNS 兜底域名:上面线路全挂时解析它的 TXT 记录拿线路 diff --git a/.env.production b/.env.production index eeb315e..35e7f3b 100644 --- a/.env.production +++ b/.env.production @@ -5,3 +5,4 @@ VITE_BUILD_DROP_CONSOLE = true # 是否在打包时删除 console 代码 VITE_BUILD_VCONSOLE = false # 是否开启调试工具 vconsole VITE_PARAMETER_ENCRYPTION = 1 # 是否开启参数加密 0关闭 1开启 VITE_BASE_URL= ["https://d3gc46ktrfda7z.cloudfront.net","https://d2bvxgoqsmyuy4.cloudfront.net"] # api选线域名,这里必须为数组 +VITE_DNS_DOMAIN = 91fans.mfcg.it.com # DNS 兜底域名:上面线路全挂时解析它的 TXT 记录拿线路 diff --git a/.env.test b/.env.test index c0f6f07..c65f391 100644 --- a/.env.test +++ b/.env.test @@ -5,3 +5,4 @@ VITE_BUILD_DROP_CONSOLE = true # 是否在打包时删除 console 代码 VITE_BUILD_VCONSOLE = false # 是否开启调试工具 vconsole VITE_PARAMETER_ENCRYPTION = 0 # 是否开启参数加密 0关闭 1开启 VITE_BASE_URL= ["https://d3gc46ktrfda7z.cloudfront.net","https://d2bvxgoqsmyuy4.cloudfront.net"] # api选线域名,这里必须为数组 +VITE_DNS_DOMAIN = 91fans.mfcg.it.com # DNS 兜底域名:上面线路全挂时解析它的 TXT 记录拿线路 diff --git a/package-lock.json b/package-lock.json index c662709..a0571c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "redapricot", + "name": "91fans", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "redapricot", + "name": "91fans", "version": "1.0.0", "dependencies": { "autoprefixer": "^10.4.13", diff --git a/src/assets/images/webp/splash.webp b/src/assets/images/webp/splash.webp index 4b39285..7d503b3 100644 Binary files a/src/assets/images/webp/splash.webp and b/src/assets/images/webp/splash.webp differ diff --git a/src/components/HeaderSearch/index.vue b/src/components/HeaderSearch/index.vue index aa51b6d..0a3979c 100644 --- a/src/components/HeaderSearch/index.vue +++ b/src/components/HeaderSearch/index.vue @@ -76,6 +76,9 @@ const toSearchPage = () => { flex: 1; margin: 0 12px; height: 32px; + // 无热词时是纯文本,父级定高不给行高会贴着顶部排,这里压回垂直居中; + // 有热词走 van-notice-bar,它自带 flex 居中和自己的 line-height,不受这里影响 + line-height: 32px; border-radius: 999px; background: rgba(255, 255, 255, 0.05); color: rgba(255, 255, 255, 0.35); diff --git a/src/components/HorizontalVideoBox/index.vue b/src/components/HorizontalVideoBox/index.vue index d3e2d62..86201a3 100644 --- a/src/components/HorizontalVideoBox/index.vue +++ b/src/components/HorizontalVideoBox/index.vue @@ -20,30 +20,8 @@ :src="videoInfo?.cover || (acgHorizontal ? videoInfo?.horizontalCover : videoInfo?.verticalCover)" /> +
免费试看
- -
@@ -319,11 +297,6 @@ const del = async (info: any) => { color: rgba(20, 20, 20, 1); background: linear-gradient(135deg, #35DEBC 0%, #22BB9C 100%); } - // 金币 - &.coins_icon { - color: #8b3e00; - background: linear-gradient(135deg, #ffe580 0%, #facc15 100%); - } } .videoInfo { diff --git a/src/components/MainItem/components/VerticalTwoSlid.vue b/src/components/MainItem/components/VerticalTwoSlid.vue index ea80f43..68b13d8 100644 --- a/src/components/MainItem/components/VerticalTwoSlid.vue +++ b/src/components/MainItem/components/VerticalTwoSlid.vue @@ -4,28 +4,12 @@
- - - - + +
{{ item.title }}

@@ -45,9 +29,6 @@ import { Swiper, SwiperSlide } from 'swiper/vue'; import 'swiper/swiper.min.css'; import { jumpAdv, isLongFormVideo } from '@/utils'; import { useVideoStore } from '@/stores/video'; -import { useConfigStore } from '@/stores/config'; -const configStore = useConfigStore(); - const videoStore = useVideoStore(); const props = defineProps<{ section: any; @@ -159,31 +140,6 @@ const jumpPage = (videoInfo: any) => { overflow: hidden; flex-shrink: 0; position: relative; - .vip_icon { - position: absolute; - top: 4px; - right: 4px; - width: 27px; - height: 16px; - line-height: 16px; - text-align: center; - font-size: 10px; - border-radius: 3px; - font-family: 'PingFang SC'; - color: #292929; - background: linear-gradient(135deg, #f8f3ae 0%, #ddac44 100%); - // 免费 - &.free_icon { - width: 32px; - color: #fff; - background: linear-gradient(135deg, #35debc 0%, #22bb9c 100%); - } - // 金币 - &.coins_icon { - color: #8b3e00; - background: linear-gradient(135deg, #ffe580 0%, #facc15 100%); - } - } } .infoBox { padding-left: 12px; diff --git a/src/components/TabBar/index.vue b/src/components/TabBar/index.vue index bdea3d3..1d1c933 100644 --- a/src/components/TabBar/index.vue +++ b/src/components/TabBar/index.vue @@ -23,7 +23,7 @@ - 短剧 + 抖音 diff --git a/src/router/index.ts b/src/router/index.ts index 1faf49c..b3331a2 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -850,7 +850,7 @@ const routes: RouteRecordRaw[] = [ path: '/main', name: 'main', component: () => import('@/views/MainView.vue'), - redirect: '/shortVideo', + redirect: '/home', //启动固定落在主页,同安卓 meta: { pageKey: 'main', pageName: '主页', diff --git a/src/utils/detectLine.ts b/src/utils/detectLine.ts new file mode 100644 index 0000000..7f713f3 --- /dev/null +++ b/src/utils/detectLine.ts @@ -0,0 +1,121 @@ +import axios from 'axios'; + +/** + * 选线:本地缓存线路 → 内置兜底线路 → DNS 解析,同安卓 DetectLineManager。 + * + * 内置线路全挂时,去解析 VITE_DNS_DOMAIN 的 TXT 记录拿线路(运维提供,多条用 _ 分隔), + * 解析出来的线路存本地,下次启动优先用它们。 + */ + +/** DNS 解析出来的线路缓存 key(同安卓 StoreKeys.DETECT_LINE) */ +const LINE_CACHE_KEY = 'detect_line'; + +/** 单次 ping / DNS 请求超时 */ +const TIMEOUT = 1000 * 10; + +/** DoH 服务商,按顺序试:阿里(安卓用的也是这家)→ Cloudflare 兜底 */ +const DNS_PROVIDERS = ['https://dns.alidns.com/resolve', 'https://cloudflare-dns.com/dns-query']; + +/** 内置兜底线路(.env 的 VITE_BASE_URL) */ +const builtinLines = (): string[] => { + try { + const lines = JSON.parse(process.env.VITE_BASE_URL || '[]'); + return Array.isArray(lines) ? lines.filter(Boolean) : []; + } catch { + return []; + } +}; + +/** 上次 DNS 解析出来的线路 */ +const cachedLines = (): string[] => { + try { + const lines = JSON.parse(localStorage.getItem(LINE_CACHE_KEY) || '[]'); + return Array.isArray(lines) ? lines.filter(Boolean) : []; + } catch { + return []; + } +}; + +const saveCachedLines = (lines: string[]): void => { + try { + localStorage.setItem(LINE_CACHE_KEY, JSON.stringify(lines)); + } catch { + // 无痕模式等写不进去,不影响本次选线 + } +}; + +/** ping 单条线路,不抛错:通了返回 true */ +const pingLine = async (line: string): Promise => { + try { + const { data } = await axios.get(line + '/api/app/ping/check', { timeout: TIMEOUT }); + return data?.code == 200; + } catch { + return false; + } +}; + +/** 并发 ping 一批线路:谁先成功用谁;全失败返回 '' */ +const pingBatch = (lines: string[]): Promise => + new Promise((resolve) => { + if (!lines.length) return resolve(''); + let failCount = 0; + let settled = false; + lines.forEach((line) => { + void pingLine(line).then((ok) => { + if (settled) return; + if (ok) { + settled = true; + resolve(line); + return; + } + if (++failCount >= lines.length) { + settled = true; + resolve(''); + } + }); + }); + }); + +/** + * 解析 DNS TXT 记录拿线路。 + * TXT 值形如 "https://a.com_https://b.com",带引号,按 _ 拆成多条 + */ +const dnsSolve = async (): Promise => { + const domain = process.env.VITE_DNS_DOMAIN; + if (!domain) return []; + for (const provider of DNS_PROVIDERS) { + try { + const { data } = await axios.get(provider, { + params: { name: domain, type: 'TXT' }, + headers: { accept: 'application/dns-json' }, + timeout: TIMEOUT, + }); + const lines: string[] = (data?.Answer || []) + .flatMap((record: any) => String(record?.data || '').split('_')) + .map((line: string) => line.replace(/"/g, '').trim()) + .filter(Boolean); + if (lines.length) return lines; + } catch { + // 这家解析不出来就换下一家 + } + } + return []; +}; + +/** + * 完整选线:缓存线路(没有就用内置线路)全挂时回退 DNS 解析。 + * 返回通了的线路,全都不通返回 '' + */ +export const detectLine = async (): Promise => { + const cached = cachedLines(); + const line = await pingBatch(cached.length ? cached : builtinLines()); + if (line) return line; + + // 本地线路全部失败,回退 DNS 解析 + const dnsLines = await dnsSolve(); + if (!dnsLines.length) return ''; + saveCachedLines(dnsLines); + return await pingBatch(dnsLines); +}; + +export default detectLine; diff --git a/src/utils/launchAdPopup.ts b/src/utils/launchAdPopup.ts index cdb56fe..39657f4 100644 --- a/src/utils/launchAdPopup.ts +++ b/src/utils/launchAdPopup.ts @@ -5,7 +5,7 @@ export type LaunchAdPopupKey = 'nineGrid' | 'homeAdv' | 'announcement'; /** * 当前正在展示的启动广告弹窗,'' 表示整条链已走完。 - * 弹窗挂在 MainView,跟着启动落地页弹(不固定在首页,默认落在短视频 tab 也会弹); + * 弹窗挂在 MainView,跟着启动落地页弹(不依赖首页被构建,落在哪个 tab 都会弹); * 首页 HomeAdv 的弹窗队列须等这条链走完再开始,避免叠在一起。 */ export const launchAdPopupKey = ref(''); diff --git a/src/utils/ping.ts b/src/utils/ping.ts index 23185cf..1b61cd5 100644 --- a/src/utils/ping.ts +++ b/src/utils/ping.ts @@ -1,57 +1,13 @@ import { useConfigStore } from './../stores/config'; -import axios from 'axios'; +import { detectLine } from './detectLine'; /** - * 测试域名 - * @param url - * @returns - */ -async function testPing(url: string): Promise { - try { - const ret = await axios({ - method: 'GET', - url: url + '/api/app/ping/check', - timeout: 1000 * 10, // 10秒超时 - }); - - if (ret.data.code == 200) { - return true; - } - return false; - } catch (error) { - return false; - } -} - -/** - * 将数组乱序 - * @param arr - * @returns - */ -function shuffle(arr: Array): Array { - let m = arr.length; - while (m > 1) { - const index = Math.floor(Math.random() * m--); - [arr[m], arr[index]] = [arr[index], arr[m]]; - } - return arr; -} - -/** - * 选线 - * @returns + * 选线:本地线路 → 全挂则 DNS 解析兜底(实现见 utils/detectLine) + * @returns 是否选到可用线路 */ export default async function pingCheck(): Promise { - const baseUrls: string = process.env.VITE_BASE_URL || '[]'; - const hosts = JSON.parse(baseUrls); - const allLine = shuffle(hosts); - for (const line of allLine) { - const isOk = await testPing(line); - if (isOk) { - //选线成功 - useConfigStore().saveBaseUrl(line); - return true; - } - } - return false; + const line = await detectLine(); + if (!line) return false; + useConfigStore().saveBaseUrl(line); + return true; } diff --git a/src/views/MainView.vue b/src/views/MainView.vue index b6fb897..59ecc9f 100644 --- a/src/views/MainView.vue +++ b/src/views/MainView.vue @@ -19,7 +19,8 @@ @close="closeVipContentGuide" /> + 挂在这里跟着启动落地页弹,不依赖首页被构建(落在哪个 tab 都会弹); + 每次启动只弹一次,逐个关闭后再弹下一个 --> null, //检测图片是否加载成功定时器 checkTime: 3, //图片加载成功检测时间 isResLock: false, // 请求结果锁 - lineFailCount: 0, // 已失败的线路数 loading: true, // 选线loading isUpgrade: true, //是否升级 }); @@ -142,8 +141,8 @@ function adControl() { const goApp = () => { // 仅启动后首次进首页允许弹老用户吸底;首页消费该标记 markHomeOldUserGuidePendingFromSplash(); - // 同安卓:启动固定落在短剧(默认 AI短剧) - router.replace('/shortVideo'); + // 同安卓:启动固定落在主页 + router.replace('/home'); let createAt = new Date(userStore.userInfo.createdAt).getTime(); let now = Date.now(); if (now - createAt > configStore.appConfig?.newbieSaleTime * 60 * 60 * 1000 && now - createAt > 3600000 * 24) { @@ -349,16 +348,6 @@ const getConfig = async (): Promise => { return false; } }; -/** - * @description 选线失败:同安卓,失败不做处理(不弹重试/线路异常弹窗)。 - * 全部线路都不通就收起「选线中...」,启动流程停在启动页 - */ -const onLineCheckFail = (hosts: any) => { - state.lineFailCount++; - if (state.lineFailCount >= hosts.length && !state.isResLock) { - state.loading = false; - } -}; /** * 初始化 */ @@ -367,30 +356,18 @@ const init = async () => { route.query.isAppStore ? localStorage.setItem('isAppStore', route.query.isAppStore.toString()) : localStorage.setItem('isAppStore', ''); // 保存是否来自商店包,用于判断是否跳转外链 - const baseUrls: string = process.env.VITE_BASE_URL || '[]'; - const hosts = JSON.parse(baseUrls); - hosts.forEach((url: any) => { - axios({ - method: 'GET', - url: url + '/api/app/ping/check', - timeout: 1000 * 10, // 10秒超时 - }) - .then(async (res) => { - if (res.data.code == 200) { - if (!state.isResLock) { - state.isResLock = true; - //选线成功 - useConfigStore().saveBaseUrl(url); - await ensureAuthAndConfig(); - } - } else { - onLineCheckFail(hosts); - } - }) - .catch(() => { - onLineCheckFail(hosts); - }); - }); + // 选线:本地线路 → 全挂则 DNS 解析兜底,同安卓 DetectLineManager + const line = await detectLine(); + // 选线失败:同安卓,失败不做处理(不弹重试/线路异常弹窗), + // 收起「选线中...」,启动流程停在启动页 + if (!line) { + state.loading = false; + return; + } + if (state.isResLock) return; + state.isResLock = true; + configStore.saveBaseUrl(line); + await ensureAuthAndConfig(); }; /** diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index ce8d1eb..90d4102 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -5,6 +5,7 @@ interface ImportMetaEnv { readonly VITE_BUILD_SOURCEMAP: string; readonly VITE_BUILD_DROP_CONSOLE: string; readonly VITE_BASE_URL: string; + readonly VITE_DNS_DOMAIN: string; readonly VITE_PARAMETER_ENCRYPTION: string; }