加上兜底域名策略
This commit is contained in:
@@ -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 记录拿线路
|
||||
|
||||
@@ -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 记录拿线路
|
||||
|
||||
@@ -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 记录拿线路
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 117 KiB |
@@ -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);
|
||||
|
||||
@@ -20,30 +20,8 @@
|
||||
:src="videoInfo?.cover || (acgHorizontal ? videoInfo?.horizontalCover : videoInfo?.verticalCover)"
|
||||
/>
|
||||
<!-- 角标独立于 lazyImg:避免封面加载失败/未完成时 isLoad=false 导致 slot 不渲染 -->
|
||||
<!-- VIP/金币 角标已去掉,只保留「免费试看」 -->
|
||||
<div class="vip_icon free_trial_icon" v-if="showFreeTrialIcon">免费试看</div>
|
||||
<template v-else-if="['video', 'image', 'text'].includes(videoInfo.mediaType)">
|
||||
<div class="vip_icon" v-if="configStore.appConfig.vipMark && videoInfo.permission == 0">VIP</div>
|
||||
<div
|
||||
class="vip_icon coins_icon"
|
||||
v-if="configStore.appConfig.coinMark && videoInfo.permission === 1 && videoInfo.price"
|
||||
>
|
||||
金币
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
class="vip_icon"
|
||||
v-if="configStore.appConfig.vipMark && videoInfo?.originCoins == 0 && !videoInfo?.freeArea"
|
||||
>
|
||||
VIP
|
||||
</div>
|
||||
<div
|
||||
class="vip_icon coins_icon"
|
||||
v-if="configStore.appConfig.coinMark && videoInfo?.originCoins != 0 && !videoInfo?.freeArea"
|
||||
>
|
||||
金币
|
||||
</div>
|
||||
</template>
|
||||
<div class="videoInfo flex-center-between" v-if="!videoInfo.type && showTimeAndComment">
|
||||
<div class="playCount flex-vcenter">
|
||||
<SvgIcon name="playCountIcon" color="#fff" />
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,28 +4,12 @@
|
||||
<swiper class="swiper" :loop="true" :effect="'creative'" :slidesPerView="'auto'">
|
||||
<swiper-slide class="swiperSlide" v-for="(arr, index) in state.newArr" :key="index">
|
||||
<div class="actorBox" v-for="item in arr" :key="item.id" @click="jumpPage(item)">
|
||||
<lazyImg class="actorAvatar" :src="item.cover ?? item.verticalCover" :key="item.cover ?? item.verticalCover">
|
||||
<template v-if="['video', 'image', 'text'].includes(item.mediaType)">
|
||||
<div class="vip_icon" v-if="configStore.appConfig.vipMark && item.permission == 0">VIP</div>
|
||||
<div
|
||||
class="vip_icon coins_icon"
|
||||
v-if="configStore.appConfig.coinMark && item.permission === 1 && item.price"
|
||||
>
|
||||
金币
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="vip_icon" v-if="configStore.appConfig.vipMark && item?.originCoins == 0 && !item?.freeArea">
|
||||
VIP
|
||||
</div>
|
||||
<div
|
||||
class="vip_icon coins_icon"
|
||||
v-if="configStore.appConfig.coinMark && item?.originCoins != 0 && !item?.freeArea"
|
||||
>
|
||||
金币
|
||||
</div>
|
||||
</template>
|
||||
</lazyImg>
|
||||
<!-- VIP/金币 角标已去掉 -->
|
||||
<lazyImg
|
||||
class="actorAvatar"
|
||||
:src="item.cover ?? item.verticalCover"
|
||||
:key="item.cover ?? item.verticalCover"
|
||||
/>
|
||||
<div class="infoBox">
|
||||
<div class="actorName ellipsis2">{{ item.title }}</div>
|
||||
<p class="desc" v-if="item.mediaType">
|
||||
@@ -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;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item to="/shortVideo" name="shortVideo" replace>
|
||||
<span class="tabbar-item-text">短剧</span>
|
||||
<span class="tabbar-item-text">抖音</span>
|
||||
<template #icon="props">
|
||||
<div class="tabbar-item" :class="[props.active ? 'liveActive' : 'live']"></div>
|
||||
</template>
|
||||
|
||||
+1
-1
@@ -850,7 +850,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/main',
|
||||
name: 'main',
|
||||
component: () => import('@/views/MainView.vue'),
|
||||
redirect: '/shortVideo',
|
||||
redirect: '/home', //启动固定落在主页,同安卓
|
||||
meta: {
|
||||
pageKey: 'main',
|
||||
pageName: '主页',
|
||||
|
||||
@@ -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<boolean> => {
|
||||
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<string> =>
|
||||
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<string[]> => {
|
||||
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<string> => {
|
||||
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;
|
||||
@@ -5,7 +5,7 @@ export type LaunchAdPopupKey = 'nineGrid' | 'homeAdv' | 'announcement';
|
||||
|
||||
/**
|
||||
* 当前正在展示的启动广告弹窗,'' 表示整条链已走完。
|
||||
* 弹窗挂在 MainView,跟着启动落地页弹(不固定在首页,默认落在短视频 tab 也会弹);
|
||||
* 弹窗挂在 MainView,跟着启动落地页弹(不依赖首页被构建,落在哪个 tab 都会弹);
|
||||
* 首页 HomeAdv 的弹窗队列须等这条链走完再开始,避免叠在一起。
|
||||
*/
|
||||
export const launchAdPopupKey = ref<LaunchAdPopupKey | ''>('');
|
||||
|
||||
+7
-51
@@ -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<boolean> {
|
||||
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<string>): Array<string> {
|
||||
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<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
@close="closeVipContentGuide"
|
||||
/>
|
||||
<!-- 启动广告弹窗链:九宫格 → 弹窗广告 → APP公告。
|
||||
跟着启动落地页弹,不固定在首页;每次启动只弹一次,逐个关闭后再弹下一个 -->
|
||||
挂在这里跟着启动落地页弹,不依赖首页被构建(落在哪个 tab 都会弹);
|
||||
每次启动只弹一次,逐个关闭后再弹下一个 -->
|
||||
<NineGridPopup v-if="launchAdPopupKey === 'nineGrid'" :is-open="true" @close="showNextLaunchAd" />
|
||||
<HomePopupAdv
|
||||
v-else-if="launchAdPopupKey === 'homeAdv'"
|
||||
|
||||
+15
-38
@@ -33,9 +33,9 @@ import { reactive } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import axios from 'axios';
|
||||
import setTheme from '@/assets/theme/index';
|
||||
import { markHomeOldUserGuidePendingFromSplash } from '@/utils/paymentGuide';
|
||||
import { detectLine } from '@/utils/detectLine';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -50,7 +50,6 @@ const state = reactive({
|
||||
checkTimer: <any>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<boolean> => {
|
||||
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();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user