<fix>修复无法接收礼物信息问题

- 更新协议版本,使用较新协议(webcast100)

- 增加登录相关 Cookie 配置入口

 - 目前需登录才可正常接收礼物信息

 - 未登录只能获取其它信息,无法获取礼物信息

 - 配置操作可查看界面说明 => ?

- 更新 emoji 列表,支持更多 emoji

- 对界面做一些其它优化
This commit is contained in:
skmcj committed 2026-04-12 16:05:41 +08:00
1 parent 4216ce84d5
commit dc3ab9ad46
22 files changed
+2796 -432

No files matched your search

+2
View File
@@ -28,3 +28,5 @@ coverage
*.sw?
*.tsbuildinfo
test/
+7 -5
View File
@@ -25,7 +25,7 @@
- 主要为解析提取后的弹幕,通过序列化`json`的格式
- 分类展示直播间弹幕
- 聊天弹幕(包含文本、普通表情、会员表情、合并表情等)
- 礼物弹幕
- 礼物弹幕(需登录 cookie)
- 关注弹幕
- 点赞弹幕(包含点赞数量)
- 进入弹幕
@@ -82,6 +82,8 @@ export interface DyMessage {
method?: CastMethod;
// 用户信息
user?: CastUser;
// 送给用户
toUser?: CastUser;
// 礼物信息(当类型为礼物弹幕时有值)
gift?: CastGift;
// 弹幕文本
@@ -194,7 +196,7 @@ export enum CastMethod {
- 项目运行后,具体界面展示如下
![主界面](https://static.ltgcm.top/md/20250428180514.png)
![主界面](https://static.ltgcm.top/md/20260412153737.png)
- 整体界面为三栏布局:左侧为直播间信息及连接状态展示;中间为主要弹幕展示;右侧为输入及其它信息展示
- 右侧主要包含两个输入框,第一个为房间号输入框,第二个为转发地址输入框;输入带有格式验证,格式不正确无法连接
@@ -202,7 +204,7 @@ export enum CastMethod {
- 在右侧房间号输入框输入房间号后,点击**连接**,等待几秒后,会在左下方状态信息展示连接结果,有时可能出现网络拥堵情况,稍后再连接即可,正常连接成功/失败均会有相应的消息通知提示,也可以看控制台输出。连接成功后,大致展示如下:
![结果](https://static.ltgcm.top/md/20250428181510.png)
![结果](https://static.ltgcm.top/md/20260412153804.png)
- 此时,用户可在转发信息框填入自己的`WebSocket`服务端地址,点击**转发**,即可建立连接,将弹幕信息实时传送到所设置后端
@@ -294,8 +296,8 @@ export enum CastMethod {
# Nginx 不区分 ws / wss 协议
# WebSocket 实际上是通过 HTTP 升级实现的
# 故使用 https:// 非 wss://
proxy_pass https://webcast5-ws-web-lf.douyin.com/;
proxy_pass https://webcast100-ws-web-lq.douyin.com/;
# WebSocket 关键配置
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dycast",
"version": "1.0.0",
"version": "1.1.0",
"private": true,
"type": "module",
"scripts": {
Binary file not shown.
+17 -2
View File
@@ -1,7 +1,6 @@
/* 抖音美好体 */
@font-face {
font-family: 'dymht';
;
src: url('./DouyinSansBold.otf') format('opentype');
}
@@ -14,7 +13,7 @@
/* 图标 */
@font-face {
font-family: "castfont";
src: url('castfont.ttf') format('truetype');
src: url('./castfont.ttf') format('truetype');
}
[class*=' ice-'],
@@ -34,6 +33,22 @@
-moz-osx-font-smoothing: grayscale;
}
.ice-keya:before {
content: "\e6b4";
}
.ice-key:before {
content: "\e66b";
}
.ice-logout:before {
content: "\e66c";
}
.ice-del:before {
content: "\e66d";
}
.ice-fullscreen:before {
content: "\e658";
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

+54 -20
View File
@@ -15,7 +15,8 @@
<label class="nickname">[{{ user?.name ? user.name : 'unknown' }}]:</label>
<template v-for="item in doms">
<span v-if="item.node === 'text'" class="text">{{ item.text }}</span>
<span v-if="item.node === 'user'" class="touser">{{ item.text }}</span>
<span v-if="item.node === 'user'" class="atuser">{{ item.text }}</span>
<span v-if="item.node === 'touser'" class="touser">{{ item.text }}</span>
<img v-if="item.node === 'icon'" class="icon" :title="item.text" :src="item.url" :alt="item.text" />
<img v-if="item.node === 'emoji'" class="emoji" alt="会员表情" :src="item.url" />
</template>
@@ -26,10 +27,10 @@
<script setup lang="ts">
import { CastMethod, CastRtfContentType, type CastGift, type CastRtfContent, type CastUser } from '@/core/dycast';
import { emojis } from '@/core/emoji';
import { computed, ref } from 'vue';
import { computed } from 'vue';
interface CastContentDOM {
node: 'text' | 'icon' | 'emoji' | 'user';
node: 'text' | 'icon' | 'emoji' | 'user' | 'touser';
url?: string;
text?: string;
}
@@ -37,6 +38,7 @@ interface CastContentDOM {
interface CastItemProps {
method?: CastMethod;
user?: CastUser;
toUser?: CastUser;
gift?: CastGift;
content?: string;
rtfContent?: CastRtfContent[];
@@ -109,21 +111,43 @@ const doms = computed(() => {
break;
case CastMethod.GIFT:
if (props.gift) {
list = [
{
node: 'text',
text: '送出了'
},
{
node: 'icon',
text: props.gift.name,
url: props.gift.icon
},
{
node: 'text',
text: `× ${props.gift.count}`
}
];
if (props.toUser) {
list = [
{
node: 'text',
text: '送给'
},
{
node: 'touser',
text: props.toUser?.name
},
{
node: 'text',
text: `${props.gift.count} 个`
},
{
node: 'icon',
text: props.gift.name,
url: props.gift.icon
}
];
} else {
list = [
{
node: 'text',
text: '送出了'
},
{
node: 'icon',
text: props.gift.name,
url: props.gift.icon
},
{
node: 'text',
text: `× ${props.gift.count}`
}
];
}
} else {
list = [
{
@@ -158,13 +182,15 @@ const doms = computed(() => {
$prefixColor: #38b48b;
$nameColor: #9079ad;
$textColor: #6b798e;
$toUserColor: #e95464;
$atUserColor: #e95464;
$toUserColor: #3271ae;
$prefixDarkColor: #38b48b;
$nameDarkColor: #83ccd2;
$textDarkColor: #f7fcfe;
$toUserDarkColor: #e83929;
$atUserDarkColor: #e83929;
$toUserDarkColor: #2ca9e1;
$giftText: #eba825;
@@ -189,14 +215,19 @@ $giftText: #eba825;
flex-shrink: 0;
}
.text,
.atuser,
.touser {
color: $textColor;
// line-height: 1rem;
word-break: break-all;
white-space: normal;
}
.atuser {
color: $atUserColor;
}
.touser {
color: $toUserColor;
margin: 0 3px;
}
.icon {
width: 1.5rem;
@@ -245,6 +276,9 @@ $giftText: #eba825;
.text {
color: $textDarkColor;
}
.atuser {
color: $atUserDarkColor;
}
.touser {
color: $toUserDarkColor;
}
+15 -4
View File
@@ -34,6 +34,7 @@
<CastItem
:method="item.method"
:user="item.user"
:to-user="item.toUser"
:gift="item.gift"
:content="item.content"
:rtf-content="item.rtfContent" />
@@ -249,7 +250,10 @@ $darkText: #a5a5a5;
box-sizing: border-box;
width: 100%;
height: 100%;
box-shadow: 0px 5px 10px 0px $shadowB3, 0px 3px 6px 0px $shadowB15, 0 0 1px 0 $lightBd;
box-shadow:
0px 5px 10px 0px $shadowB3,
0px 3px 6px 0px $shadowB15,
0 0 1px 0 $lightBd;
border-radius: 0.5rem;
background-color: $lightBg;
overflow: hidden;
@@ -273,10 +277,15 @@ $darkText: #a5a5a5;
&.theme-dark {
background-color: $darkBg;
border: 0.5px solid $shadowW2;
box-shadow: 0px 6px 10px 0px $shadowB2, 0px 3px 6px 0px $shadowB12, inset 0px 0.5px 0.5px 0px $shadowW6;
box-shadow:
0px 6px 10px 0px $shadowB2,
0px 3px 6px 0px $shadowB12,
inset 0px 0.5px 0.5px 0px $shadowW6;
.cast-list-header {
background-color: $darkBarBg;
box-shadow: 0px 1px 0px 0px $shadowB3, 0px 0.5px 0px 0px $shadowB0;
box-shadow:
0px 1px 0px 0px $shadowB3,
0px 0.5px 0px 0px $shadowB0;
}
.title {
color: $darkText;
@@ -314,7 +323,9 @@ $darkText: #a5a5a5;
padding: 3px 8px;
display: flex;
align-items: center;
box-shadow: 0px 1px 0px 0px $shadowB05, 0px 0.5px 0px 0px $shadowB1;
box-shadow:
0px 1px 0px 0px $shadowB05,
0px 0.5px 0px 0px $shadowB1;
.title {
width: 100%;
display: flex;
+185
View File
@@ -0,0 +1,185 @@
<template>
<Transition name="opin" :duration="500">
<div class="feed-dialog" v-show="visible">
<div className="feed-dialog-close" @click="hideFeedDialog">
<i className="ice-close-b icon"></i>
</div>
<div class="feed-dialog-main">
<div className="coin">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="128" height="128">
<path
d="M512 0c282.760533 0 512 229.239467 512 512 0 282.760533-229.239467 512-512 512-282.760533 0-512-229.239467-512-512C0 229.239467 229.239467 0 512 0z"
fill="#FCE815"></path>
<path
d="M951.569067 249.309867L65.024 761.890133a510.498133 510.498133 0 0 1-13.687467-26.2144L935.662933 224.392533c5.5296 8.192 10.8544 16.4864 15.906134 24.917334z m-33.314134-48.9472L39.1168 708.608c-11.6736-28.023467-20.821333-57.002667-27.306667-86.664533l845.0048-488.448c22.391467 20.411733 42.973867 42.8032 61.44 66.8672z"
fill="#FFDE92"></path>
<path
d="M512 34.133333C248.081067 34.133333 34.133333 248.081067 34.133333 512s213.947733 477.866667 477.866667 477.866667 477.866667-213.947733 477.866667-477.866667S775.918933 34.133333 512 34.133333z"
fill="#FCE815"></path>
<path
d="M906.001067 241.527467c5.597867 8.123733 10.9568 16.418133 16.042666 24.917333L94.549333 744.789333c-4.778667-8.6016-9.352533-17.339733-13.653333-26.248533L906.0352 241.527467zM825.685333 151.483733a480.733867 480.733867 0 0 1 62.805334 66.116267l-819.541334 473.770667a474.248533 474.248533 0 0 1-25.941333-87.381334L825.685333 151.483733z"
fill="#FFF9BC"></path>
<path
d="M512 136.533333C304.64 136.533333 136.533333 304.64 136.533333 512s168.106667 375.466667 375.466667 375.466667 375.466667-168.106667 375.466667-375.466667S719.36 136.533333 512 136.533333z"
fill="#FFC91A"></path>
<path
d="M730.385067 206.5408A375.022933 375.022933 0 0 1 887.466667 512c0 207.36-168.106667 375.466667-375.466667 375.466667-194.901333 0-355.157333-148.548267-373.691733-338.602667L730.385067 206.506667z"
fill="#FFB50D"></path>
<path
d="M409.6 307.2m65.8432 0l73.1136 0q65.8432 0 65.8432 65.8432l0 277.9136q0 65.8432-65.8432 65.8432l-73.1136 0q-65.8432 0-65.8432-65.8432l0-277.9136q0-65.8432 65.8432-65.8432Z"
fill="#FFF48A"></path>
</svg>
</div>
<div className="qr-box">
<img className="qr" alt="QR" :src="qrCover" />
</div>
<div className="tip">
<span>微信扫码,可投喂UP ٩( 'ω' )و</span>
</div>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import qrCover from '@/assets/images/feed.png';
import { isBoolean } from '@/utils/typeUtil';
interface FeedDialogProps {
modelValue?: boolean;
}
const props = withDefaults(defineProps<FeedDialogProps>(), {
modelValue: false
});
const visible = ref(false);
const emits = defineEmits({ 'update:modelValue': (value: boolean) => isBoolean(value) });
const hideFeedDialog = () => {
visible.value = false;
emits('update:modelValue', false);
};
watch(
() => props.modelValue,
val => {
if (val) {
visible.value = true;
} else {
if (visible.value) visible.value = false;
}
}
);
</script>
<style lang="scss" scoped>
$bg: #f7f6f5;
.opin-enter-active,
.opin-leave-active {
transition: opacity 0.5s ease-in-out;
}
.opin-enter-from,
.opin-leave-to {
opacity: 0;
}
.feed-dialog {
z-index: 9999;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: $bg;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.feed-dialog-close {
position: absolute;
right: 3rem;
top: 3rem;
cursor: pointer;
color: #e94829;
will-change: transform;
transition: transform 0.2s ease-in-out;
transform: rotateZ(-180deg);
.icon {
font-size: 2rem;
}
&:active {
transform: rotateZ(0deg);
transition-duration: 0s;
}
}
.feed-dialog-main {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
.qr-box {
width: 16rem;
height: 16rem;
border-radius: 5rem;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 1rem;
background: #f7f6f5;
box-shadow:
9px 9px 12px #e1e0df,
-9px -9px 12px #ffffff;
.qr {
width: 100%;
height: 100%;
border-radius: 20%;
object-fit: cover;
}
}
.tip {
user-select: none;
margin-top: 24px;
font-size: 1.5rem;
font-family: 'dymht';
color: #9aa7b1;
text-shadow:
1px 1px 1px #333,
-1px -1px 1px #fff;
}
.coin {
position: absolute;
top: 0;
z-index: -1;
animation: putCoin 3s ease infinite;
svg {
width: 2rem;
height: 2rem;
}
}
}
@keyframes putCoin {
0% {
transform: translateY(0%);
}
24% {
transform: translateY(-200%) rotateY(360deg) scale(1.2);
}
50% {
transform: translateY(0%);
}
100% {
transform: translateY(0%);
}
}
</style>
+1
View File
@@ -100,6 +100,7 @@ $errorText: #e94829;
gap: 12px;
box-sizing: border-box;
padding: 24px 18px;
padding-bottom: 0;
.live-info-cover {
width: 100%;
display: flex;
+710
View File
@@ -0,0 +1,710 @@
<template>
<Transition name="opac" mode="out-in">
<div v-if="!hasSSid" class="sid-tool" :class="{ active: isKeyActive, hover: isKeyHover }">
<!-- tooltip -->
<div class="sid-key-tooltip">
<span class="key-tooltip_text">点击导入凭证</span>
<span class="key-tooltip_arrow"></span>
</div>
<div class="sid-tool-main">
<!-- 输入框 -->
<input ref="input" class="sid-tool-input" type="text" placeholder="请输入sessionid" :disabled="inputDisabled" />
<!-- 图标 -->
<div class="sid-tool-key" @click="handleBtnClick" @mouseenter="handleKeyHover" @mouseleave="handleKeyNotHover">
<i class="ice-keya icon"></i>
</div>
</div>
<!-- 保存按钮 -->
<div class="sid-save" title="保存" @click="handleSave">
<span class="back"></span>
<span class="front">
<i class="ice-checkmark icon"></i>
</span>
</div>
<div class="sid-help" title="help" @click="handleHelpOpen">
<i class="ice-help icon"></i>
</div>
<!-- 加载条 -->
<div class="sid-loading" v-show="inputDisabled">
<div class="sid-loading-mask"></div>
<section class="dots-container">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</section>
</div>
<!-- 帮助弹窗 -->
<SidToolHelp v-model="helpVisible" />
</div>
<div v-else class="sid-user">
<div class="sid-user-card">
<div class="sid-user-avatar">
<img class="user-avatar" alt="头像" :src="uerAvatar" />
</div>
<div class="sid-user-info">
<a class="user-name" target="_blank" :href="userLink">{{ userName }}</a>
<div class="user-fs">
<div class="user-fsi user-follow">
<div class="name">关注</div>
<div class="value">{{ userFollowing }}</div>
</div>
<div class="user-fsi user-fans">
<div class="name">粉丝</div>
<div class="value">{{ userFollower }}</div>
</div>
</div>
<div class="user-sign">{{ userSign }}</div>
</div>
<div class="sid-user-logout" title="退出" @click="delSidCookie">
<i class="ice-logout icon"></i>
</div>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { onBeforeMount, ref, useTemplateRef } from 'vue';
import SidToolHelp from './SidToolHelp.vue';
import { getMeInfo } from '@/core/request';
import SkMessage from '@/components/Message';
import { CLog } from '@/utils/logUtil';
import { hasCookie, removeCookie, setCookie } from '@/utils/cookieUtil';
// 输入框Ref
const inputRef = useTemplateRef('input');
// 是否激活
const isKeyActive = ref(false);
// 是否悬停
const isKeyHover = ref(false);
let keyActive = false;
let keyHover = false;
// 输入框禁用
const inputDisabled = ref(false);
// 帮助弹窗
const helpVisible = ref(false);
// 是否已有登录凭证
const hasSSid = ref(false);
// 用户信息
// 头像
const uerAvatar = ref('');
// 用户名
const userName = ref('');
// 粉丝
const userFollower = ref(0);
// 关注
const userFollowing = ref(0);
// 签名
const userSign = ref('');
// 用户主页链接
const userLink = ref('');
// 秘钥按钮点击
const handleBtnClick = function () {
if (keyActive) {
keyActive = false;
isKeyActive.value = false;
isKeyHover.value = true;
keyHover = true;
inputRef.value?.blur();
} else {
keyActive = true;
isKeyActive.value = true;
isKeyHover.value = false;
keyHover = false;
inputRef.value?.focus();
}
};
// 悬停
const handleKeyHover = function () {
if (keyActive) return;
isKeyHover.value = true;
keyHover = true;
};
// 非悬停
const handleKeyNotHover = function () {
if (keyHover) {
keyHover = false;
isKeyHover.value = false;
}
};
/** 帮助弹窗点击 */
const handleHelpOpen = function () {
helpVisible.value = true;
};
/**
* 请求用户信息
*/
const requestMeInfo = function () {
getMeInfo()
.then(res => {
if (res.code !== 0) {
// 获取失败
if (res.code === 20003) {
// 用户未登录
CLog.warn(`Request Me Info Error: (${res.code})${res.msg}`);
SkMessage.warning('用户未登录,请先登录');
} else {
// 其它错误
CLog.error(`Request Me Info Error: (${res.code})${res.msg}`);
SkMessage.error('请求失败,请检查sessionid是否有误');
}
} else {
// 获取成功
if (res.data) {
const info = res.data;
// 设置信息
uerAvatar.value = info.avatar;
userName.value = info.nickname;
userFollower.value = info.follower;
userFollowing.value = info.following;
userSign.value = info.sign || '';
userLink.value = `https://www.douyin.com/user/${info.uid}`;
hasSSid.value = true;
} else {
CLog.info(`用户信息为空: (${res.code})${res.msg}`);
SkMessage.warning('用户信息为空,请检查sessionid是否有误');
}
}
})
.catch(err => {
CLog.error(err);
SkMessage.error('请求失败,请检查sessionid是否有误');
})
.finally(() => {
inputDisabled.value = false;
});
};
/** 保存cookie */
const setSidCookie = function (sid: string) {
// 保存cookie
// 官网实际会添加多个相同值的cookie,但经测试有这一个就够用了
setCookie({
name: 'sessionid',
value: sid,
path: '/',
maxAge: 604800,
secure: true
});
// document.cookie = `sid_tt=${sid}; Path=/; Max-Age=604800; Secure`;
// document.cookie = `sessionid_ss=${sid}; Path=/; Max-Age=604800; Secure`;
// 一个自定义,标识作用的cookie
setCookie({
name: '__lg',
value: 'true',
path: '/',
maxAge: 604800,
secure: true
});
};
/** 删除cookie */
const delSidCookie = function () {
removeCookie('sessionid');
removeCookie('__lg');
hasSSid.value = false;
};
/** 保存 */
const handleSave = function () {
if (!keyActive) return;
const ssid = inputRef.value?.value.trim();
if (!ssid) return;
// 保存cookie
setSidCookie(ssid);
// 请求用户信息
// - 禁用输入框
inputDisabled.value = true;
requestMeInfo();
};
// 组件挂载前
onBeforeMount(() => {
// 验证是否存在登录凭证 cookie
if (hasCookie('__lg')) {
inputDisabled.value = true;
// 获取登录用户信息
requestMeInfo();
}
});
</script>
<style lang="scss" scoped>
@use 'sass:math';
$bg: #f7f6f5;
$sidToolH: 32px;
$iconColor: #9aa7b1;
$iconColorH: #f39800;
$iconColorA: #38b48b;
$btnSize: $sidToolH;
$tooltipColor: #303133;
$phColor: #b4c3cf;
$inputColor: #6b798e;
$inputDisabledColor: #a5acb1;
$saveBtnW: 24px;
$saveBtnH: 20px;
$saveBtnRgb: rgb(0, 253, 186);
$saveBtnSRgb: rgb(0, 153, 121);
$helpSize: 16px;
$helpColor: #9aa7b1;
$helpColorH: #fcc800;
$loadingDotC: rgb(156, 219, 183);
$loadingDotB: #65db98;
$loadingDotSize: 15px;
// 用户信息卡片
$avatarSize: 52px;
$nameColor: #5f766a;
$signColor: #ac9f8a;
$fsiNColor: #bec2bc;
$fsiVColor: #9d9d82;
$fsiHrColor: #a8b092;
$lgoutColor: #a7aaa1;
$lgoutHColor: #e94829;
.opac-enter-active,
.opac-leave-active {
transition: opacity 0.3s ease-in-out;
}
.opac-enter-from,
.opac-leave-to {
opacity: 0;
}
.sid-tool {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: $sidToolH + 12px;
box-sizing: border-box;
padding: 6px 12px;
gap: 6px;
&.active {
.sid-tool-main {
box-shadow:
inset -2px -2px 3px 0 #ffffff,
inset 2px 2px 3px 0 #c7c8c8,
inset -0.5px -0.5px 0 0 #ffffff,
inset 0.5px 0.5px 0 0 #a0a0a0;
// width: 100%;
flex-grow: 1;
transition:
box-shadow 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275),
flex-grow 0.5s cubic-bezier(0.23, 1, 0.32, 1);
}
.sid-tool-key {
color: $iconColorA;
.icon {
transform: rotateZ(0deg);
transition: transform 0.3s ease-in-out;
}
}
.sid-tool-input {
width: 100%;
}
.sid-save {
opacity: 1;
transform: translateY(0);
transition:
0.5s transform 0.3s ease-in,
0.5s opacity 0.3s ease-in;
}
}
&.hover {
.sid-key-tooltip {
opacity: 1;
transform: translate(6px, -130%);
}
.sid-tool-key {
color: $iconColorH;
}
}
}
.sid-tool-main {
display: inline-flex;
align-items: center;
position: relative;
width: $sidToolH;
height: $sidToolH;
border-radius: math.div($btnSize, 2);
box-shadow:
inset 2px 2px 3px 0 #ffffff,
inset -2px -2px 3px 0 #c7c8c8,
inset -0.5px -0.5px 0 0 #ffffff,
inset 0.5px 0.5px 0 0 #a0a0a0,
2px 2px 3px 0 rgba(0, 0, 0, 0.15),
-2px -2px 3px 0 #ffffff;
transition:
box-shadow 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275),
0.3s flex-grow 0.5s cubic-bezier(0.23, 1, 0.32, 1);
&:hover {
box-shadow:
inset 2px 2px 3px 0 #ffffff,
inset -2px -2px 3px 0 #c7c8c8,
inset -0.5px -0.5px 0 0 #ffffff,
inset 0.5px 0.5px 0 0 #a0a0a0,
1px 1px 1px 0 rgba(0, 0, 0, 0.15),
-1px -1px 1px 0 #ffffff;
}
&:active {
box-shadow:
inset -2px -2px 3px 0 #ffffff,
inset 2px 2px 3px 0 #c7c8c8,
inset -0.5px -0.5px 0 0 #ffffff,
inset 0.5px 0.5px 0 0 #a0a0a0;
}
}
.sid-tool-key {
display: inline-flex;
align-items: center;
justify-content: center;
width: $btnSize;
height: $btnSize;
position: absolute;
top: 0;
left: 0;
color: $iconColor;
transition: color 0.3s ease-in-out;
.icon {
display: inline-flex;
font-size: 24px;
transform: rotateZ(135deg);
transition: 0.3s transform 0.3s ease-in-out;
}
}
.sid-key-tooltip {
opacity: 0;
user-select: none;
position: absolute;
top: 0;
left: 0;
transform: translate(6px, -100%);
background-color: $tooltipColor;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
transition:
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
.key-tooltip_arrow {
position: absolute;
z-index: -1;
width: 10px;
height: 10px;
bottom: -5px;
left: 12px;
&::before {
position: absolute;
right: 0;
width: 10px;
height: 10px;
z-index: -1;
content: ' ';
transform: rotate(45deg);
background: $tooltipColor;
box-sizing: border-box;
border: 1px solid $tooltipColor;
border-bottom-right-radius: 2px;
border-top-color: transparent;
border-left-color: transparent;
}
}
}
.sid-tool-input {
// 清除样式
outline: none;
padding: 0;
margin: 0;
border: none;
background: none;
width: $btnSize;
height: $sidToolH;
padding-left: $btnSize;
box-sizing: border-box;
font-family: 'dymht';
font-size: 14px;
color: $inputColor;
&::placeholder {
color: $phColor;
}
&:disabled {
color: $inputDisabledColor;
}
}
.sid-save {
opacity: 0;
transform: translateY(100%);
width: $saveBtnW;
height: $saveBtnH;
border: none;
outline: none;
cursor: pointer;
user-select: none;
touch-action: manipulation;
outline: 5px solid rgba($saveBtnRgb, 0.5);
margin: 0 5px;
border-radius: 100%;
position: relative;
transition:
transform 0.3s ease-in,
opacity 0.3s ease-in;
.back {
background: $saveBtnSRgb;
border-radius: 100%;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.front {
background: linear-gradient(0deg, rgba($saveBtnRgb, 0.6) 20%, $saveBtnRgb 50%);
box-shadow: 0 6px 5px -2px rgba($saveBtnSRgb, 0.5);
border-radius: 100%;
position: absolute;
border: 1px solid $saveBtnSRgb;
box-sizing: border-box;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.2rem;
font-weight: 600;
font-family: inherit;
transform: translateY(-15%);
transition: transform 0.15s ease-in;
color: #fff;
.icon {
font-size: 12px;
}
}
&:active {
.front {
transform: translateY(0%);
}
}
}
.sid-help {
margin-left: auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: $helpSize;
height: $helpSize;
color: $helpColor;
transition: color 0.2s ease;
cursor: pointer;
&:hover {
color: $helpColorH;
}
.icon {
font-size: $helpSize;
}
}
.sid-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 3px 6px;
.sid-loading-mask {
width: 100%;
height: 100%;
box-sizing: border-box;
border-radius: 6px;
background-color: rgba(0, 0, 0, 0.1);
}
.dots-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
box-sizing: border-box;
padding: 6px 12px;
}
.dot {
height: $loadingDotSize;
width: $loadingDotSize;
margin-right: math.div($loadingDotSize, 2);
border-radius: math.div($loadingDotSize, 2);
background-color: $loadingDotC;
animation: pulse 1.5s infinite ease-in-out;
}
.dot:last-child {
margin-right: 0;
}
.dot:nth-child(1) {
animation-delay: -0.3s;
}
.dot:nth-child(2) {
animation-delay: -0.1s;
}
.dot:nth-child(3) {
animation-delay: 0.1s;
}
}
.sid-user {
width: 100%;
box-sizing: border-box;
padding: 6px 12px;
position: relative;
}
.sid-user-card {
display: flex;
align-items: center;
box-sizing: border-box;
border-radius: 0 12px 12px 0;
padding: 8px 12px;
background: linear-gradient(120deg, #f7f6f5, #ece9e6, #ffffff);
border: 1px solid rgba(0, 0, 0, 0.15);
border-left: none;
box-shadow: 5px 0 3px 0 rgba(0, 0, 0, 0.15);
}
.sid-user-avatar {
width: $avatarSize;
height: $avatarSize;
border-radius: math.div($avatarSize, 2);
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
.user-avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.sid-user-info {
flex-grow: 1;
margin-left: 8px;
display: flex;
justify-content: center;
flex-direction: column;
font-family: 'dymht';
.user-name {
display: block;
font-size: 16px;
font-weight: 500;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 6px;
color: $nameColor;
-webkit-app-region: no-drag;
outline: none;
text-decoration: none;
}
.user-fs {
user-select: none;
display: flex;
align-items: center;
}
.user-fsi {
display: inline-flex;
align-items: center;
font-size: 14px;
font-weight: 400;
& + .user-fsi::before {
content: '';
display: block;
width: 0;
height: 16px;
border-left: 1px solid $fsiHrColor;
margin: 0 8px;
}
.name {
color: $fsiNColor;
}
.value {
margin-left: 4px;
color: $fsiVColor;
max-width: 80px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.user-sign {
user-select: none;
margin-top: 5px;
font-size: 12px;
color: $signColor;
line-height: 1.2;
font-weight: 400;
}
}
.sid-user-logout {
margin-left: 8px;
color: $lgoutColor;
transition: color 0.2s ease;
.icon {
font-size: 18px;
}
&:hover {
color: $lgoutHColor;
}
}
@keyframes pulse {
0% {
transform: scale(0.8);
background-color: $loadingDotC;
box-shadow: 0 0 0 0 rgba($loadingDotC, 0.7);
}
50% {
transform: scale(1.2);
background-color: $loadingDotB;
box-shadow: 0 0 0 10px rgba($loadingDotC, 0);
}
100% {
transform: scale(0.8);
background-color: $loadingDotC;
box-shadow: 0 0 0 0 rgba($loadingDotC, 0.7);
}
}
</style>
+226
View File
@@ -0,0 +1,226 @@
<template>
<Transition name="fade" @after-leave="handleTransLeave" :duration="500">
<div class="sid-tool-help" v-show="visible">
<!-- 遮罩 -->
<div class="sid-tool-help_mask" @click="handleClose"></div>
<!-- 内容 -->
<div role="dialog" class="sid-tool-help_main">
<h3 class="help-title">一则说明</h3>
<div class="help-article">
<div class="article-content">
<p class="article-tip">
由于官方做了某些更新,目前,未登录的话是无法获取到礼物信息的,只有登录后,才能正常获取礼物信息
</p>
<h4 class="article-h">怎么办?</h4>
<p class="article-p">
现在,如果想获取礼物信息,需要导入一个登录凭证(cookie),具体操作可参考下侧;如果不想,无需任何操作
</p>
<h4 class="article-h">参考操作</h4>
<p class="article-p">
首先,<a class="article-a" href="https://live.douyin.com/" target="_blank">打开官网</a
>;同时,确保自己是已登录状态,未登录则先自行登录
</p>
<p class="article-p">
接着,在官网界面打开浏览器开发者工具(可按<span class="article-code">F12</span>),具体操作可参考下图
</p>
<img class="article-img" alt="控制台" :src="cover" />
<p class="article-p">复制官网cookie中sessionid的值</p>
<p class="article-p">最后将复制的值粘贴到输入框内保存即可</p>
<p class="article-p">
如果使用过程中礼物信息接收不到就尝试更新下sessionid的值,保证它与你官网登录账户的sessionid一致
</p>
</div>
</div>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import cover from '@/assets/images/help-article.jpg';
import { isBoolean } from '@/utils/typeUtil';
interface SidToolHelpProp {
modelValue?: boolean;
}
const props = withDefaults(defineProps<SidToolHelpProp>(), {
modelValue: false
});
// 是否可见
const visible = ref(false);
const emits = defineEmits({ 'update:modelValue': (value: boolean) => isBoolean(value) });
/** 关闭弹窗 */
const handleClose = function () {
close();
};
const handleTransLeave = function () {
emits('update:modelValue', false);
};
const open = function () {
visible.value = true;
};
const close = function () {
visible.value = false;
};
onMounted(() => {
if (props.modelValue) {
open();
}
});
watch(
() => props.modelValue,
val => {
if (val) {
// 打开
open();
} else {
// 关闭
if (visible.value) {
close();
}
}
}
);
</script>
<style lang="scss" scoped>
$titleC: #d08635;
$textC: #6b798e;
$tipC: #98d98e;
$hC: #354e6b;
$linkC: #6ca8af;
.fade-enter-active,
.fade-leave-active {
.sid-tool-help_mask {
transition: opacity 0.3s cubic-bezier(0.55, 0, 0.1, 1);
}
.sid-tool-help_main {
transition:
opacity 0.3s cubic-bezier(1, 0.5, 0.8, 1),
transform 0.5s cubic-bezier(1, 0.5, 0.8, 1);
}
}
// 进入初 & 离开末
.fade-enter-from,
.fade-leave-to {
.sid-tool-help_mask {
opacity: 0;
}
.sid-tool-help_main {
opacity: 0;
transform: translateY(100%);
}
}
.sid-tool-help {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.sid-tool-help_mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: -1;
}
.sid-tool-help_main {
max-width: 750px;
width: 60%;
height: 80%;
background-color: #fff;
border-radius: 12px;
box-sizing: border-box;
box-shadow: 5px 5px 10px 0 rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
flex-direction: column;
padding: 8px 12px;
.help-title {
font-family: 'dymht';
font-size: 21px;
font-weight: bold;
color: $titleC;
margin: 0;
}
.help-article {
width: 100%;
height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
}
.article-content {
margin-top: 18px;
font-family: 'mkwxy';
display: flex;
flex-direction: column;
color: $textC;
gap: 8px;
padding-bottom: 36px;
}
.article-p {
color: $textC;
margin: 0;
font-size: 16px;
margin-left: 5px;
line-height: 1.5;
text-indent: 2em;
}
.article-tip {
margin: 0;
color: $tipC;
font-size: 14px;
line-height: 1.2;
}
.article-h {
font-size: 18px;
font-weight: bold;
color: $hC;
margin: 0;
}
.article-a {
color: $linkC;
}
.article-code {
border: 1px solid #ecd452;
border-radius: 3px;
box-sizing: border-box;
padding: 2px 5px;
margin: 0 3px;
background-color: rgba(236, 212, 82, 0.7);
color: #d452ec;
}
.article-img {
width: 70%;
object-fit: cover;
overflow: hidden;
border-radius: 12px;
margin: 0 auto;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.15);
}
}
</style>
+551
View File
@@ -0,0 +1,551 @@
/**
* 生成 a_bougs 参数值
* @param {string} query 请求的 query 参数
* @param {string} ua 浏览器 User-Agent
* @returns
*/
export const getAbogus = function (query, ua) {
function enc_sum(n_str) {
function ir(t) {
return (
(ir =
'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator
? function (t) {
return typeof t;
}
: function (t) {
return t && 'function' == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype
? 'symbol'
: typeof t;
}),
ir(t)
);
}
function ur(t, r) {
for (let e = 0; e < r.length; e++) {
const n = r[e];
((n.enumerable = n.enumerable || !1),
(n.configurable = !0),
'value' in n && (n.writable = !0),
Object.defineProperty(t, sr(n.key), n));
}
}
function sr(t) {
const r = (function (t, r) {
if ('object' != ir(t) || !t) return t;
const e = t[Symbol.toPrimitive];
if (void 0 !== e) {
const n = e.call(t, r || 'default');
if ('object' != ir(n)) return n;
throw new TypeError('@@toPrimitive must return a primitive value.');
}
return ('string' === r ? String : Number)(t);
})(t, 'string');
return 'symbol' == ir(r) ? r : r + '';
}
const gr = (function () {
function t() {
if (
((function (t, r) {
if (!(t instanceof r)) throw new TypeError('Cannot call a class as a function');
})(this, t),
!(this instanceof t))
)
return new t();
((this.reg = new Array(8)), (this.chunk = []), (this.size = 0), this.reset());
}
return (
(function (t, r, e) {
(r && ur(t.prototype, r),
e && ur(t, e),
Object.defineProperty(t, 'prototype', {
writable: !1
}));
})(t, [
{
key: 'reset',
value: function () {
((this.reg[0] = 1937774191),
(this.reg[1] = 1226093241),
(this.reg[2] = 388252375),
(this.reg[3] = 3666478592),
(this.reg[4] = 2842636476),
(this.reg[5] = 372324522),
(this.reg[6] = 3817729613),
(this.reg[7] = 2969243214),
(this.chunk = []),
(this.size = 0));
}
},
{
key: 'write',
value: function (t) {
const r =
'string' == typeof t
? (function (t) {
const r = encodeURIComponent(t).replace(/%([0-9A-F]{2})/g, function (t, r) {
return String.fromCharCode('0x' + r);
}),
e = new Array(r.length);
return (
Array.prototype.forEach.call(r, function (t, r) {
e[r] = t.charCodeAt(0);
}),
e
);
})(t)
: t;
this.size += r.length;
let e = 64 - this.chunk.length;
if (r.length < e) this.chunk = this.chunk.concat(r);
else
for (this.chunk = this.chunk.concat(r.slice(0, e)); this.chunk.length >= 64; )
(this._compress(this.chunk),
e < r.length ? (this.chunk = r.slice(e, Math.min(e + 64, r.length))) : (this.chunk = []),
(e += 64));
}
},
{
key: 'sum',
value: function (t, r) {
(t && (this.reset(), this.write(t)), this._fill());
let e = 0;
for (; e < this.chunk.length; e += 64) this._compress(this.chunk.slice(e, e + 64));
let n,
o,
i,
u = null;
if ('hex' == r) {
u = '';
for (e = 0; e < 8; e++)
u +=
((n = this.reg[e].toString(16)),
(o = 8),
(i = '0'),
n.length >= o ? n : i.repeat(o - n.length) + n);
} else
for (u = new Array(32), e = 0; e < 8; e++) {
let s = this.reg[e];
((u[4 * e + 3] = (255 & s) >>> 0),
(s >>>= 8),
(u[4 * e + 2] = (255 & s) >>> 0),
(s >>>= 8),
(u[4 * e + 1] = (255 & s) >>> 0),
(s >>>= 8),
(u[4 * e] = (255 & s) >>> 0));
}
return (this.reset(), u);
}
},
{
key: '_compress',
value: function (t) {
if (t < 64) console.error('compress error: not enough data');
else {
let r = (function (t) {
const r = new Array(132);
for (let e = 0; e < 16; e++) {
r[e] = t[4 * e] << 24;
r[e] |= t[4 * e + 1] << 16;
r[e] |= t[4 * e + 2] << 8;
r[e] |= t[4 * e + 3];
r[e] >>>= 0;
}
for (let n = 16; n < 68; n++) {
let o = r[n - 16] ^ r[n - 9] ^ dr(r[n - 3], 15);
o = o ^ dr(o, 15) ^ dr(o, 23);
r[n] = (o ^ dr(r[n - 13], 7) ^ r[n - 6]) >>> 0;
}
for (let n = 0; n < 64; n++) r[n + 68] = (r[n] ^ r[n + 4]) >>> 0;
return r;
})(t);
let e = this.reg.slice(0);
for (let n = 0; n < 64; n++) {
let o = dr(e[0], 12) + e[4] + dr(yr(n), n),
i = ((o = dr((o = (4294967295 & o) >>> 0), 7)) ^ dr(e[0], 12)) >>> 0,
u = br(n, e[0], e[1], e[2]);
u = (4294967295 & (u = u + e[3] + i + r[n + 68])) >>> 0;
let s = mr(n, e[4], e[5], e[6]);
s = (4294967295 & (s = s + e[7] + o + r[n])) >>> 0;
e[3] = e[2];
e[2] = dr(e[1], 9);
e[1] = e[0];
e[0] = u;
e[7] = e[6];
e[6] = dr(e[5], 19);
e[5] = e[4];
e[4] = (s ^ dr(s, 9) ^ dr(s, 17)) >>> 0;
}
for (let c = 0; c < 8; c++) this.reg[c] = (this.reg[c] ^ e[c]) >>> 0;
}
}
},
{
key: '_fill',
value: function () {
let t = 8 * this.size,
r = this.chunk.push(128) % 64;
for (64 - r < 8 && (r -= 64); r < 56; r++) this.chunk.push(0);
for (let e = 0; e < 4; e++) {
let n = Math.floor(t / 4294967296);
this.chunk.push((n >>> (8 * (3 - e))) & 255);
}
for (let e = 0; e < 4; e++) this.chunk.push((t >>> (8 * (3 - e))) & 255);
}
}
]),
t
);
})();
function dr(t, r) {
return ((t << (r %= 32)) | (t >>> (32 - r))) >>> 0;
}
function yr(t) {
return 0 <= t && t < 16
? 2043430169
: 16 <= t && t < 64
? 2055708042
: void console.error('invalid j for constant Tj');
}
function br(t, r, e, n) {
return 0 <= t && t < 16
? (r ^ e ^ n) >>> 0
: 16 <= t && t < 64
? ((r & e) | (r & n) | (e & n)) >>> 0
: (console.error('invalid j for bool function FF'), 0);
}
function mr(t, r, e, n) {
return 0 <= t && t < 16
? (r ^ e ^ n) >>> 0
: 16 <= t && t < 64
? ((r & e) | (~r & n)) >>> 0
: (console.error('invalid j for bool function GG'), 0);
}
const enc_ = new gr();
return enc_.sum(n_str);
}
function generate_lm_g_EP(uat = ua) {
function get_sz256f() {
let r = [],
k = 0,
y = [0, 1, 0];
for (let i = 255; i >= 0; i--) {
r.push(i);
}
for (let i = 0; i < r.length; i++) {
let a = r[i];
k = (k * a + k + y[i % 3]) % 256;
let b = r[k];
((r[i] = b), (r[k] = a));
}
return r;
}
const sz256f = [
233, 5, 1, 249, 162, 140, 57, 143, 19, 203, 254, 236, 99, 248, 93, 213, 79, 149, 216, 50, 145, 123, 240, 92, 23,
113, 130, 53, 235, 220, 201, 136, 223, 155, 190, 242, 243, 42, 52, 214, 151, 232, 97, 187, 163, 222, 30, 78, 47,
71, 49, 170, 247, 196, 25, 156, 183, 182, 217, 180, 147, 124, 208, 69, 215, 200, 161, 154, 91, 60, 133, 224, 119,
164, 221, 45, 98, 40, 186, 120, 51, 167, 38, 90, 194, 212, 129, 56, 87, 195, 144, 44, 75, 84, 81, 13, 197, 245,
36, 250, 115, 100, 105, 252, 206, 103, 112, 202, 114, 138, 192, 21, 116, 173, 181, 29, 82, 125, 141, 16, 211, 131,
225, 118, 31, 101, 77, 146, 135, 150, 62, 66, 67, 176, 0, 41, 46, 59, 107, 178, 43, 26, 189, 128, 8, 207, 166,
110, 3, 229, 85, 54, 63, 11, 32, 4, 234, 142, 72, 58, 33, 231, 12, 230, 102, 86, 70, 159, 226, 65, 237, 34, 244,
76, 132, 122, 111, 95, 179, 152, 175, 18, 177, 6, 126, 193, 219, 74, 134, 2, 61, 251, 191, 168, 209, 241, 137,
165, 88, 238, 160, 174, 153, 157, 199, 48, 22, 64, 246, 7, 139, 55, 27, 188, 148, 204, 127, 171, 89, 37, 172, 205,
121, 20, 28, 17, 169, 15, 227, 117, 80, 218, 198, 10, 106, 9, 39, 210, 104, 83, 109, 24, 108, 228, 184, 96, 185,
158, 14, 255, 239, 68, 94, 35, 73, 253
];
let k = 0,
s = '';
for (let i = 0; i < uat.length; i++) {
let t = (i + 1) % 256;
let a = sz256f[t];
k = (k + a) % 256;
let c = sz256f[k];
sz256f[t] = c;
sz256f[k] = a;
s += String.fromCharCode(uat.charCodeAt(i) ^ sz256f[(a + c) % 256]);
}
return s;
}
function get_str_chr_list(one_str) {
const r = [];
for (let i = 0; i < one_str.length; i++) {
r.push(one_str.charCodeAt(i));
}
return r;
}
function generate_szenc_head8p1() {
let z = Math.random() * 65535;
let a = z & 255;
let b = (z >> 8) & 255,
d = [];
d.push((a & 170) | 1);
d.push((a & 85) | 0);
d.push((b & 170) | 0);
d.push((b & 85) | 0);
return d;
}
function generate_szenc_head8p2() {
let a = ((Math.random() * 240) >> 0) + 1;
let b = ((Math.random() * 255) >> 0) & 77,
c = [1, 4, 5, 7],
d = [];
for (let i = 0; i < c.length; i++) {
b = b | (1 << c[i]);
}
d.push((a & 170) | 1);
d.push((a & 85) | 0);
d.push((b & 170) | 0);
d.push((b & 85) | 0);
return d;
}
function get_szenc_tail(sz96) {
const zKeys = [145, 110, 66, 189, 44, 211];
const a = [];
for (let i = 0; i < 94; i += 3) {
let b = sz96[i];
let c = sz96[i + 1];
let d = sz96[i + 2];
let e = (Math.random() * 1000) & 255;
a.push((e & zKeys[0]) | (b & zKeys[1]));
a.push((e & zKeys[2]) | (c & zKeys[3]));
a.push((e & zKeys[4]) | (d & zKeys[5]));
a.push((b & zKeys[0]) | (c & zKeys[2]) | (d & zKeys[4]));
}
return a;
}
function generate_lm_g_ab_head4() {
let s = '';
const a = (Math.random() * 65535) & 255,
b = (Math.random() * 40) >> 0;
s += String.fromCharCode((a & 170) | 1);
s += String.fromCharCode((a & 85) | 2);
s += String.fromCharCode((b & 170) | 80);
s += String.fromCharCode((b & 85) | 2);
return s;
}
function get_list_str(one_list) {
let s = '';
for (let i = 0; i < one_list.length; i++) {
s += String.fromCharCode(one_list[i]);
}
return s;
}
function get_lm_g_ab(lm_g_lm_n) {
function getSZ256() {
const raw = [];
let z = 0;
for (let i = 255; i >= 0; i--) {
raw.push(i);
}
for (let i = 0; i < raw.length; i++) {
z += 211;
let a = z % 256;
let b = raw[i];
let c = raw[a];
raw[a] = b;
raw[i] = c;
z = raw[i + 1] * a + a;
}
return raw;
}
const fixedSZ256 = [
194, 249, 255, 165, 114, 67, 251, 187, 174, 231, 164, 237, 124, 235, 68, 83, 206, 79, 142, 167, 30, 77, 0, 93,
118, 29, 32, 161, 2, 171, 243, 179, 42, 170, 223, 119, 98, 222, 219, 57, 245, 135, 197, 13, 186, 202, 88, 184,
214, 12, 76, 185, 116, 74, 54, 53, 104, 208, 158, 163, 82, 173, 253, 240, 172, 63, 191, 207, 25, 15, 201, 203,
215, 236, 183, 233, 145, 127, 72, 6, 16, 10, 228, 35, 232, 159, 66, 168, 108, 71, 217, 75, 33, 155, 112, 128, 36,
24, 138, 50, 211, 23, 107, 14, 247, 137, 175, 242, 234, 157, 199, 49, 139, 85, 81, 17, 180, 86, 120, 78, 51, 205,
169, 148, 181, 3, 94, 106, 252, 220, 150, 47, 151, 84, 212, 18, 149, 182, 100, 123, 121, 156, 154, 152, 126, 204,
60, 133, 132, 248, 7, 91, 58, 59, 20, 97, 113, 117, 131, 46, 250, 224, 21, 73, 146, 31, 193, 69, 140, 125, 9, 39,
89, 5, 65, 141, 218, 80, 1, 70, 64, 166, 87, 189, 55, 147, 22, 26, 143, 61, 144, 99, 92, 44, 129, 130, 227, 103,
90, 192, 198, 244, 136, 101, 246, 153, 56, 38, 4, 178, 221, 162, 134, 37, 111, 28, 216, 96, 102, 210, 254, 196,
195, 230, 241, 62, 11, 122, 52, 40, 41, 229, 226, 225, 48, 45, 160, 105, 8, 115, 34, 43, 209, 95, 239, 190, 188,
109, 27, 19, 176, 213, 200, 238, 177, 110
];
// const fixedSZ256 = getSZ256();
let z = 0;
let st = '';
for (let i = 0; i < lm_g_lm_n.length; i++) {
let a = (i + 1) % 256;
let c = fixedSZ256[a];
z = (z + c) % 256;
let e = fixedSZ256[z];
fixedSZ256[a] = e;
fixedSZ256[z] = c;
let g = (e + c) % 256;
let h = lm_g_lm_n.charCodeAt(i);
let j = fixedSZ256[g];
let k = h ^ j;
let l = String.fromCharCode(k);
st += l;
}
return st;
}
function get_raw_ab(lm_get_ab_n, key_str = info_dic.s4) {
let s = '',
bw = 0;
for (let i = 0; i < lm_get_ab_n.length; i += 3) {
let cl = 16;
let tcz = 0;
let sof = 16515072;
for (let j = i; j < i + 3; j++) {
if (j < lm_get_ab_n.length) {
let tlcy = lm_get_ab_n.charCodeAt(j) & 255;
tcz = tcz | (tlcy << cl);
cl -= 8;
} else {
bw += 1;
}
}
for (let h = 18; h >= 6 * bw; h -= 6) {
let tsz = tcz & sof;
s += key_str[tsz >> h];
sof = sof / 64;
}
s += '='.repeat(bw);
}
return s;
}
function get_random_number(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const info_dic = {
s0: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
s1: 'Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=',
s2: 'Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=',
s3: 'ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe',
s4: 'Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe'
};
const t1 = Date.now();
const s = [];
const t2 = Date.now() - 1 + get_random_number(1, 3);
const EP = get_raw_ab(generate_lm_g_EP(ua), info_dic.s3);
const eEP = enc_sum(EP);
s.push('env_fx_list', 'dpf_ua_dic', 1, 0, 8, 'dpf', '', 'ua', 6241, 6383, '1.0.1.19-fix.01', 'ink', 3, '0X21_dic'); // 固定即可
const t3 = Date.now() + get_random_number(4, 15);
const eedp = enc_sum(enc_sum(query + 'dhzx'));
s.push(t3, 'reg_dic', 1, 0, eedp, 'eedh', EP, eEP, t2, [3, 82], 41, [1, 0, 1, 0, 1]);
const t4 = Date.now() + get_random_number(100, 1000);
const s1 = ((t4 - 1721836800000) / 1000 / 60 / 60 / 24 / 14) >> 0,
szenc_o95_tail41 = [
49, 52, 52, 49, 124, 56, 51, 56, 124, 49, 52, 52, 49, 124, 57, 49, 51, 124, 49, 52, 52, 49, 124, 57, 49, 51, 124,
49, 52, 52, 49, 124, 57, 54, 49, 124, 87, 105, 110, 51, 50
];
s.push(
s1,
6,
(t3 - t1 + 3) & 255,
t3 & 255,
(t3 >> 8) & 255,
(t3 >> 16) & 255,
(t3 >> 24) & 255,
(t3 / 256 / 256 / 256 / 256) & 255
);
const s2 = (t3 / 256 / 256 / 256 / 256 / 256) & 255;
s.push(
s2,
(s2 % 256) & 255,
(s2 / 256) & 255,
[211, 2, 5, 1, 129],
129,
0,
211,
2,
5,
1,
0,
0,
0,
0,
eedp[9],
eedp[18],
3,
eedp[3],
82,
177,
4,
44,
eEP[11],
eEP[21],
5,
eEP[5],
t2 & 255,
(t2 >> 8) & 255,
(t2 >> 16) & 255,
(t2 >> 24) & 255,
(t2 / 256 / 256 / 256 / 256) & 255,
(t2 / 256 / 256 / 256 / 256 / 256) & 255,
3,
97,
24,
0,
0,
239,
24,
0,
0,
'screec_dic',
'screen_str',
szenc_o95_tail41,
41,
41,
0
);
const s3 = ((t3 + 3) & 255) + ',',
s4 = get_str_chr_list(s3);
s.push(s3, s4, s4.length, s4.length & 255, (s4.length >> 8) & 255);
const szenc_head8_p1 = generate_szenc_head8p1(),
szenc_head8_p2 = generate_szenc_head8p2(),
szenc_head8 = szenc_head8_p1.concat(szenc_head8_p2),
s5 = [],
s6 = [
24, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 51, 52, 53, 55,
56, 57, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 79, 80, 84, 85
];
for (let i = 0; i < s6.length; i++) {
s5.push(s[s6[i]]);
}
s.push(szenc_head8);
const s7 = szenc_head8.concat(s5);
let s8 = s7[0];
for (let i = 1; i < s7.length; i++) {
s8 = s8 ^ s7[i];
}
s.push(s8);
const enc_s_i = [
34, 44, 56, 61, 73, 29, 70, 45, 35, 49, 38, 66, 51, 68, 28, 48, 64, 47, 30, 71, 26, 55, 31, 69, 59, 40, 62, 63,
27, 72, 41, 74, 57, 52, 42, 39, 33, 67, 53, 43, 65, 46, 36, 24, 60, 32, 79, 80, 84, 85
],
szenc_o95_head50 = [];
for (let i = 0; i < enc_s_i.length; i++) {
szenc_o95_head50.push(s[enc_s_i[i]]);
}
let szenc_o95 = [];
szenc_o95 = szenc_o95.concat(szenc_o95_head50, szenc_o95_tail41, s4, [s8]);
const szenc_tail = get_szenc_tail(szenc_o95),
szenc = szenc_head8.concat(szenc_tail),
lm_get_ab_head4 = generate_lm_g_ab_head4();
const lm_get_lm = get_list_str(szenc);
const lm_get_ab_tail = get_lm_g_ab(lm_get_lm);
const lm_get_ab = lm_get_ab_head4 + lm_get_ab_tail;
const ab = get_raw_ab(lm_get_ab);
return ab;
};
+16 -12
View File
@@ -24,9 +24,9 @@ import type {
Text,
User
} from './model';
import { getImInfo, getLiveInfo } from './request';
import { fetchUser, getImInfo, getLiveInfo } from './request';
import { getSignature } from './signature';
import { logUserCast } from '@/utils/debugUtil';
// import { logUserCast } from '@/utils/debugUtil';
/**
* 连接状态
@@ -140,6 +140,7 @@ export interface DyMessage {
id?: string;
method?: CastMethod;
user?: CastUser;
toUser?: CastUser;
gift?: CastGift;
content?: string;
rtfContent?: CastRtfContent[];
@@ -308,11 +309,12 @@ enum PayloadType {
}
/** API */
// const BASE_URL = 'wss://webcast5-ws-web-lf.douyin.com/webcast/im/push/v2/';
// wss://webcast5-ws-web-lf.douyin.com/webcast/im/push/v2/ => version: 1.0.14-beta.0
// wss://webcast100-ws-web-lq.douyin.com/webcast/im/push/v2/ => version: 1.0.15
const BASE_URL = `${location.origin.replace(/^http/, 'ws')}/socket/webcast/im/push/v2/`;
/** SDK 版本 */
const VERSION = '1.0.14-beta.0';
export const VERSION = '1.0.15';
/**
* 默认配置
@@ -766,6 +768,7 @@ export class DyCast {
message = decodeGiftMessage(payload);
data.method = CastMethod.GIFT;
data.user = this._getCastUser(message.user);
data.toUser = this._getCastUser(message.toUser);
data.gift = this._getCastGift(message.gift, message.repeatCount || message.comboCount, message.repeatEnd);
break;
case CastMethod.LIKE:
@@ -934,11 +937,11 @@ export class DyCast {
});
} else if (pieces[i].userValue) {
// 艾特用户
let toUser = pieces[i].userValue?.user;
let atUser = pieces[i].userValue?.user;
list.push({
type: CastRtfContentType.USER,
text: `@${toUser?.nickname}`,
user: this._getCastUser(toUser)
text: `@${atUser?.nickname}`,
user: this._getCastUser(atUser)
});
} else {
// 假定为普通文本类型
@@ -992,7 +995,7 @@ export class DyCast {
private _ping() {
return encodePushFrame({
payloadType: PayloadType.Hb
});
}) as Uint8Array<ArrayBuffer>;
}
/**
@@ -1008,9 +1011,9 @@ export class DyCast {
index < 128
? arr.push(index)
: index < 2048
? (arr.push(192 + (index >> 6)), arr.push(128 + (63 & index)))
: index < 65536 &&
(arr.push(224 + (index >> 12)), arr.push(128 + ((index >> 6) & 63)), arr.push(128 + (63 & index)));
? (arr.push(192 + (index >> 6)), arr.push(128 + (63 & index)))
: index < 65536 &&
(arr.push(224 + (index >> 12)), arr.push(128 + ((index >> 6) & 63)), arr.push(128 + (63 & index)));
}
return new Uint8Array(arr);
};
@@ -1018,7 +1021,7 @@ export class DyCast {
payloadType: PayloadType.Ack,
payload: getPayload(ext),
logId
});
}) as Uint8Array<ArrayBuffer>;
}
/** 关闭后 */
@@ -1080,6 +1083,7 @@ export class DyCast {
const info = await getLiveInfo(roomNum);
this.info = info;
this.status = info.status;
await fetchUser();
const res = await getImInfo(info.roomId, info.uniqueId);
this.imInfo = res;
} catch (err) {
+609 -365
View File
File diff suppressed because it is too large. Load diff
+138 -1
View File
@@ -1,3 +1,4 @@
import { getAbogus } from './abogus';
import type { DyImInfo } from './dycast';
import { decodeResponse } from './model';
import { getMsToken } from './signature';
@@ -39,6 +40,24 @@ export const getLiveInfo = async function (id: string) {
}
};
/**
* 用户请求
* - 用于增加cookie
*/
export const fetchUser = async function () {
try {
await fetch(`/dylive/webcast/user/`, {
method: 'HEAD',
headers: {
'X-Secsdk-Csrf-Request': '1',
'X-Secsdk-Csrf-Version': '1.2.22'
}
});
} catch (err) {
return Promise.reject(Error('Fetch Webcast User Error'));
}
};
const USER_AGENT =
navigator.userAgent ||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36';
@@ -100,8 +119,16 @@ export const fetchImInfo = async function (roomId: string, uniqueId: string) {
room_id: roomId,
user_unique_id: uniqueId
});
const paramStr = makeUrlParams(
Object.assign({}, defaultIMFetchParams, {
room_id: roomId,
user_unique_id: uniqueId,
live_pc: roomId
})
);
// 一个加密参数,须通过上侧 params 参数计算,感兴趣自己去逆向,这里不解析,不一定验证
const aBogus = '00000000';
// const aBogus = '00000000';
const aBogus = getAbogus(paramStr, USER_AGENT);
Object.assign(params, {
live_pc: roomId,
a_bogus: aBogus
@@ -148,3 +175,113 @@ export const getImInfo = async function (roomId: string, uniqueId: string): Prom
};
}
};
/** 默认请求参数 */
const defaultMeFetchParam = {
aid: '6383',
app_name: 'douyin_web',
browser_language: 'zh-CN',
browser_name: 'Edge',
browser_platform: 'Win32',
browser_version: '146.0.0.0',
cookie_enabled: 'true',
device_platform: 'web',
enter_from: 'web_live',
language: 'zh-CN',
live_id: '1',
os_name: 'Windows',
os_version: '10',
room_id: '0',
screen_height: '1080',
screen_width: '1920'
};
/**
* 获取当前登录用户信息
* @returns
*/
export const fetchMeInfo = async function () {
try {
const params = Object.assign({}, defaultMeFetchParam);
const paramStr = makeUrlParams(params);
const msToken = getMsToken(184);
const abogus = getAbogus(paramStr, USER_AGENT);
Object.assign(params, {
msToken,
a_bogus: abogus
});
const url = `/dylive/webcast/user/me/?${makeUrlParams(params)}`;
const res = await fetch(url).then(res => res.json());
if (res) return res;
else return Promise.reject(`Fetch Me Info Fail`);
if (res && res['status_code'] !== 0) {
const msg = res?.data?.message;
return Promise.reject(`Fetch Me Info Fail: status => ${res['status_code']}, msg => ${msg}`);
} else return res;
} catch (err) {
return Promise.reject(`Fetch Me Info Error: ${err}`);
}
};
interface DyResult<T> {
/** 状态码 */
code: number;
/** 状态描述 */
msg: string;
/** 数据 */
data: T;
}
interface DyMeInfo {
/** sec_uid */
uid: string;
/** display_id : 抖音号 */
did: string;
/** 昵称 */
nickname: string;
/** 头像 */
avatar: string;
/** follower_count : 粉丝数 */
follower: number;
/** following_count : 关注数 */
following: number;
/** signature : 个性签名 */
sign?: string;
}
/**
* 获取当前登录用户信息
*/
export const getMeInfo = async function (): Promise<DyResult<DyMeInfo | null>> {
try {
const res = await fetchMeInfo();
let code = 0;
let msg = '';
let info: DyMeInfo | null = null;
if (res['status_code'] !== 0) {
// 获取失败
const data = res?.data || {};
code = res['status_code'];
msg = data?.prompts || data?.message || 'get user info fail';
} else {
// 获取成功
const data = res.data || {};
info = {
uid: data.sec_uid || '',
did: data.display_id || '',
sign: data.signature || '',
nickname: data.nickname || '',
avatar: data.avatar_medium?.url_list?.[0] || '',
follower: data.follow_info?.follower_count || 0,
following: data.follow_info?.following_count || 0
};
}
return {
code,
msg,
data: info
};
} catch (err) {
return Promise.reject(err);
}
};
+8 -6
View File
@@ -1,3 +1,5 @@
import { VERSION } from './dycast';
const stringToBytes = function (t) {
t = decodeURIComponent(encodeURIComponent(t));
let n = [];
@@ -21,7 +23,7 @@ const stringToBytes = function (t) {
},
bytesToHex = function (t) {
let n = [];
for (let e = 0; e < t.length; e++) n.push((t[e] >>> 4).toString(16)), n.push((15 & t[e]).toString(16));
for (let e = 0; e < t.length; e++) (n.push((t[e] >>> 4).toString(16)), n.push((15 & t[e]).toString(16)));
return n.join('');
},
_ff = function (t, n, e, p, r, o, i) {
@@ -58,7 +60,7 @@ function un(t, n) {
l = 271733878;
for (let t = 0; t < e.length; t++)
e[t] = (16711935 & ((e[t] << 8) | (e[t] >>> 24))) | (4278255360 & ((e[t] << 24) | (e[t] >>> 8)));
(e[r >>> 5] |= 128 << r % 32), (e[14 + (((r + 64) >>> 9) << 4)] = r);
((e[r >>> 5] |= 128 << (r % 32)), (e[14 + (((r + 64) >>> 9) << 4)] = r));
let f = _ff,
s = _gg,
c = _hh,
@@ -68,7 +70,7 @@ function un(t, n) {
_ = i,
h = u,
a = l;
(o = d(
((o = d(
(o = c(
(o = c(
(o = c(
@@ -321,7 +323,7 @@ function un(t, n) {
(o = (o + g) >>> 0),
(i = (i + _) >>> 0),
(u = (u + h) >>> 0),
(l = (l + a) >>> 0);
(l = (l + a) >>> 0));
}
return endian([o, i, u, l]);
}
@@ -337,7 +339,7 @@ function getSTUB(t) {
* @return {string} signature
*/
export const getSignature = function (roomId, uniqueId) {
const sdkVersion = '1.0.14-beta.0';
const sdkVersion = VERSION;
const e = getSTUB(
`live_id=1,aid=6383,version_code=180800,webcast_sdk_version=${sdkVersion},room_id=${roomId},sub_room_id=,sub_channel_id=,did_rule=3,user_unique_id=${uniqueId},device_platform=web,device_type=,ac=,identity=audience`
);
@@ -356,7 +358,7 @@ export const getSignature = function (roomId, uniqueId) {
* @returns
*/
export const getMsToken = function (length = 182) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
+188
View File
@@ -0,0 +1,188 @@
/**
* 简单封装的 Cookie 操作工具
* - 只能获取非 HttpOnly 的 Cookie 项(前端限制)
* - 同样的,也不能设置 HttpOnly 的 Cookie 项
*/
type SameSite = 'strict' | 'lax' | 'none';
/**
* Cookie 结构
*/
interface CustomCookie {
name: string;
value: string;
path?: string;
// null 表示可能为会话 cookie
expires?: string | number | null;
domain?: string;
maxAge?: number;
secure?: boolean;
sameSite?: SameSite;
}
/**
* CookieStore 项结构
* - CookieStore.get 返回 Promise 兑现的对象
*/
interface CookieStoreItem {
/** 记录 cookie 名称的字符串 */
name: string;
/** 记录 cookie 的值的字符串 */
value: string;
/** 记录 cookie 域名的字符串 */
domain: string | null;
/** 记录 cookie 的到期日期的 Unix 时间戳(以毫秒为单位表示)的数字 */
expires: number | null;
/**
* 表示 cookie 是否是分区 cookie(true)或(false)
* - 更多信息请参阅具有独立分区状态的 Cookie(CHIPS)
*/
partitioned: boolean;
/** 记录 cookie 路径的字符串 */
path: string;
/**
* 以下 SameSite 的值之一
* - strict: Cookie 只会在第一方上下文中发送,不会与第三方网站发起的请求一起发送
* - lax: Cookie 不会在正常的跨站点子请求(例如将图像或框架加载到第三方站点)中发送,而是在用户在原始站点内导航时(即点击链接时)发送
* - none: Cookie 将会在所有上下文中发送
*/
sameSite: SameSite;
/** 表示 cookie 是否仅在安全上下文中使用(true)或(false) */
secure: boolean;
}
type SameSiteMap = {
[K in SameSite]: string;
};
const SAMESITE_MAP: SameSiteMap = {
strict: 'Strict',
lax: 'Lax',
none: 'None'
};
/**
* 获取某个 Cookie 项
* - 通过 document.cookie,只能获取其值
* @param name cookie 名称
*/
export const getCookie = function (name: string) {
let cookie: CustomCookie | null = null;
// 用正则匹配对应cookie项,包括path、expires等信息
const reg = new RegExp(`(^|; )${name}=([^;]*)`);
const match = document.cookie.match(reg);
if (match && match.length > 2) {
cookie = {
name,
value: decodeURIComponent(match[2])
};
}
return cookie;
};
/**
* 异步获取某个 Cookie 项
* - 通过 CookieStore,能获取其所有信息,如:path、expires 等
* - 如浏览器不支持则通过 document.cookie 获取
* @param name cookie 名称
*/
export const getCookieAsync = async function (name: string) {
// 用正则匹配对应cookie项,包括path、expires等信息
try {
let cookie: CustomCookie | null = null;
if (window.cookieStore && window.cookieStore instanceof CookieStore) {
const ck = (await window.cookieStore.get(name)) as CookieStoreItem;
if (ck) {
cookie = {
name: ck.name || name,
value: ck.value || '',
path: ck.path || '/',
expires: ck.expires || null,
secure: ck.secure || false,
sameSite: ck.sameSite || 'lax'
};
}
} else {
cookie = getCookie(name);
}
return cookie;
} catch (err) {
return Promise.reject(`Get Cookie ${name} failed: ${err}`);
}
};
/**
* 设置 Cookie 项
* @param cookie
*/
export const setCookie = function (cookie: CustomCookie) {
// 处理
if (cookie['maxAge']) {
// Max-Age 优先于 Expires
cookie['expires'] = void 0;
}
// cookie 组成表
const cks: string[] = [];
// 先构造键值对
cks.push(`${cookie.name}=${decodeURIComponent(cookie.value)}`);
// 构造 cookie 其它属性
for (const [key, value] of Object.entries(cookie)) {
if (!value) continue;
switch (key) {
case 'path':
cks.push(`Path=${value}`);
break;
case 'domain':
cks.push(`Domain=${value}`);
break;
case 'maxAge':
cks.push(`Max-Age=${value}`);
break;
case 'expires':
cks.push(`Expires=${value}`);
break;
case 'secure':
cks.push(`Secure`);
break;
case 'sameSite':
cks.push(`SameSite=${SAMESITE_MAP[value as SameSite]}`);
break;
}
}
// 合并所有属性
document.cookie = cks.join('; ');
return true;
};
/**
* 删除某个 Cookie 项
* @param name
*/
export const removeCookie = function (name: string) {
// 将 cookie 设置为过期时间已过期
document.cookie = `${name}=; Max-Age=0;`;
return true;
};
/**
* 判断是否存在某个 Cookie 项
* @param name
*/
export const hasCookie = function (name: string) {
const regex = new RegExp(`(^|; )${name}=`);
return regex.test(document.cookie);
};
/** 获取 Cookie 的键值列表 */
export const getCookieKeys = function () {
const cookies = document.cookie.split(';');
const keys = [];
for (const cookie of cookies) {
const [key] = cookie.trim().split('=');
if (key) keys.push(key);
}
return keys;
};
+1
View File
@@ -2,6 +2,7 @@ export const objectToString = Object.prototype.toString;
export const toTypeString = (value: unknown): string => objectToString.call(value);
export const isNumber = (val: any): val is number => typeof val === 'number';
export const isBoolean = (val: any): val is boolean => typeof val === 'boolean';
export const isArray = Array.isArray;
export const isMap = (val: unknown): val is Map<any, any> => toTypeString(val) === '[object Map]';
export const isSet = (val: unknown): val is Set<any> => toTypeString(val) === '[object Set]';
+65 -15
View File
@@ -11,9 +11,13 @@
:user-count="userCount"
:like-count="likeCount" />
<div class="view-left-bottom">
<SidTool />
<div class="view-left-tools">
<div class="view-left-tool" title="保存弹幕" @click.stop="saveCastToFile">
<i class="ice-save"></i>
<div class="view-left-tool feed-btn" title="投喂" @click.stop="openFeedDialog">
<i class="ice-coin icon"></i>
</div>
<div class="view-left-tool cm-btn" title="保存弹幕" @click.stop="saveCastToFile">
<i class="ice-save icon"></i>
</div>
</div>
<hr class="hr" />
@@ -50,6 +54,8 @@
<CastList ref="otherEl" :types="['social', 'like', 'member']" pos="left" no-prefix theme="dark" />
</div>
</div>
<!-- 投喂弹窗 -->
<FeedDialog v-model="fdVisible" />
</div>
</template>
@@ -58,6 +64,8 @@ import ConnectInput from '@/components/ConnectInput.vue';
import LiveInfo from '@/components/LiveInfo.vue';
import LiveStatusPanel from '@/components/LiveStatusPanel.vue';
import CastList from '@/components/CastList.vue';
import SidTool from '@/components/SidTool/SidTool.vue';
import FeedDialog from '@/components/FeedDialog.vue';
import {
CastMethod,
DyCast,
@@ -91,6 +99,9 @@ const relayInputRef = useTemplateRef('relayInput');
// 状态面板
const statusPanelRef = useTemplateRef('panel');
// 投喂弹窗可见性
const fdVisible = ref(false);
/** 直播间信息 */
const cover = ref<string>('');
const title = ref<string>('*****');
@@ -184,8 +195,9 @@ const handleMessages = function (msgs: DyMessage[]) {
try {
for (const msg of msgs) {
if (!msg.id) continue;
if (castSet.has(msg.id)) continue;
castSet.add(msg.id);
const msgId = `${msg.method}-${msg.id}`;
if (castSet.has(msgId)) continue;
castSet.add(msgId);
switch (msg.method) {
case CastMethod.CHAT:
newCasts.push(msg);
@@ -428,6 +440,13 @@ const saveCastToFile = function () {
CLog.error('弹幕保存出错了 =>', err);
});
};
/**
* 打开投喂弹窗
*/
const openFeedDialog = function () {
fdVisible.value = true;
};
</script>
<style lang="scss" scoped>
@@ -435,6 +454,7 @@ $bg: #f7f6f5;
$bd: #b2bfc3;
$theme: #68be8d;
$tool: #8b968d;
$gold: #e6b422;
.index-view {
position: relative;
@@ -486,19 +506,34 @@ $tool: #8b968d;
align-items: center;
justify-content: center;
color: $tool;
transition: color 0.2s ease-in-out, background-color 0.3s ease-in-out, opacity 0.2s ease;
background-color: transparent;
border-radius: 0.4em;
i {
&.cm-btn {
transition:
color 0.2s ease-in-out,
background-color 0.3s ease-in-out,
opacity 0.2s ease;
background-color: transparent;
border-radius: 0.4em;
&:hover {
color: #fff;
background-color: $theme;
}
&:active {
opacity: 0.8;
}
}
&.feed-btn {
animation: coinAni 1s ease-in-out;
transition: color 0.2s ease-in-out;
&:hover {
color: $gold;
}
&:active {
animation: none;
}
}
.icon {
font-size: 1em;
}
&:hover {
color: #fff;
background-color: $theme;
}
&:active {
opacity: 0.8;
}
}
.view-center {
flex-grow: 4.5;
@@ -560,4 +595,19 @@ $tool: #8b968d;
}
}
}
@keyframes coinAni {
0% {
color: $gold;
transform: translateY(0%);
}
50% {
color: $gold;
transform: translateY(-120%) rotateY(360deg) scale(1.2);
}
100% {
color: $gold;
transform: translateY(0%);
}
}
</style>
+2 -1
View File
@@ -58,7 +58,8 @@ export default defineConfig({
}
},
'/socket': {
target: 'wss://webcast5-ws-web-lf.douyin.com',
// target: 'wss://webcast5-ws-web-lf.douyin.com',
target: 'wss://webcast100-ws-web-lq.douyin.com',
changeOrigin: true, // 保持原始 Host,利于服务端识别 Cookie
secure: true,
ws: true, // 启用 WebSocket 代理