mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 更新依赖和移除不必要的文件, 更新歌单管理列表在小屏幕上的显示效果, 修复自定义配置文件加载
- 在 package.json 中移除不再使用的依赖项,并更新部分依赖版本 - 删除多个不再使用的组件和文件,包括 CheckInTemplateHelper.vue、CommonConfigItems.vue、GlobalSettingsConfig.vue 等 - 更新 bun.lockb 文件以反映依赖变更
This commit is contained in:
37
package.json
37
package.json
@@ -10,7 +10,6 @@
|
|||||||
"knip": "knip"
|
"knip": "knip"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@antfu/ni": "^24.3.0",
|
|
||||||
"@guolao/vue-monaco-editor": "^1.5.5",
|
"@guolao/vue-monaco-editor": "^1.5.5",
|
||||||
"@hyperdx/browser": "^0.21.2",
|
"@hyperdx/browser": "^0.21.2",
|
||||||
"@hyperdx/cli": "^0.1.0",
|
"@hyperdx/cli": "^0.1.0",
|
||||||
@@ -30,55 +29,44 @@
|
|||||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/md5": "^2.3.5",
|
"@types/md5": "^2.3.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
|
||||||
"@vicons/fluent": "^0.13.0",
|
"@vicons/fluent": "^0.13.0",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"@vue/cli": "^5.0.8",
|
|
||||||
"@vueuse/core": "^13.1.0",
|
"@vueuse/core": "^13.1.0",
|
||||||
"@vueuse/integrations": "^13.1.0",
|
"@vueuse/integrations": "^13.1.0",
|
||||||
"@vueuse/router": "^13.1.0",
|
"@vueuse/router": "^13.1.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"bilibili-live-ws": "^6.3.1",
|
"bilibili-live-ws": "^6.3.1",
|
||||||
"brotli-compress": "^1.3.3",
|
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"easy-speech": "^2.4.0",
|
"easy-speech": "^2.4.0",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-oxlint": "^0.16.8",
|
||||||
"eslint-plugin-oxlint": "^0.16.7",
|
|
||||||
"eslint-plugin-prettier": "^5.2.6",
|
|
||||||
"fast-xml-parser": "^5.2.1",
|
"fast-xml-parser": "^5.2.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"grapheme-splitter": "^1.0.4",
|
"grapheme-splitter": "^1.0.4",
|
||||||
"hammerjs": "^2.0.8",
|
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"linqts": "^2.0.0",
|
"linqts": "^2.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"music-metadata-browser": "^2.5.11",
|
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"oxlint": "^0.16.7",
|
|
||||||
"peerjs": "^1.5.4",
|
"peerjs": "^1.5.4",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"qrcode.vue": "^3.6.0",
|
"qrcode.vue": "^3.6.0",
|
||||||
"queue-typescript": "^1.0.1",
|
|
||||||
"tui-image-editor": "^3.15.3",
|
|
||||||
"unplugin-auto-import": "^19.1.2",
|
"unplugin-auto-import": "^19.1.2",
|
||||||
"unplugin-vue-components": "^28.5.0",
|
"unplugin-vue-components": "^28.5.0",
|
||||||
"unplugin-vue-markdown": "^28.3.1",
|
"unplugin-vue-markdown": "^28.3.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vite": "6.3.3",
|
"vite": "6.3.4",
|
||||||
"vite-plugin-oxlint": "^1.3.1",
|
"vite-plugin-oxlint": "^1.3.1",
|
||||||
"vite-svg-loader": "^5.1.0",
|
"vite-svg-loader": "^5.1.0",
|
||||||
"vue": "3.5.13",
|
"vue": "3.5.13",
|
||||||
"vue-echarts": "^7.0.3",
|
"vue-echarts": "^7.0.3",
|
||||||
"vue-request": "^2.0.4",
|
"vue-request": "^2.0.4",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.1",
|
||||||
"vue-turnstile": "^1.0.11",
|
"vue-turnstile": "^1.0.11",
|
||||||
"vue3-aplayer": "^1.7.3",
|
"vue3-aplayer": "^1.7.3",
|
||||||
"vue3-marquee": "^4.2.2",
|
"vue3-marquee": "^4.2.2",
|
||||||
@@ -87,24 +75,15 @@
|
|||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@types/bun": "^1.2.11",
|
||||||
"@types/bun": "^1.2.10",
|
|
||||||
"@types/eslint": "^9.6.1",
|
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/obs-studio": "^2.17.2",
|
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/parser": "^8.31.0",
|
|
||||||
"@vicons/ionicons5": "^0.13.0",
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||||
"@vue-vine/eslint-config": "^0.2.19",
|
"@vue-vine/eslint-config": "^0.2.19",
|
||||||
"@vue/eslint-config-typescript": "^14.5.0",
|
"eslint": "^9.26.0",
|
||||||
"eslint": "^9.25.1",
|
"eslint-plugin-vue": "^10.1.0",
|
||||||
"eslint-config-prettier": "^10.1.2",
|
|
||||||
"eslint-plugin-vue": "^10.0.0",
|
|
||||||
"knip": "^5.50.5",
|
|
||||||
"naive-ui": "^2.41.0",
|
|
||||||
"stylus": "^0.64.0",
|
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vue-vine": "^0.3.21"
|
"vue-vine": "^0.4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
// src/index.ts
|
|
||||||
import chalk from 'chalk'
|
|
||||||
import { spawn } from 'child_process'
|
|
||||||
|
|
||||||
// src/utils.ts
|
|
||||||
import { execSync } from 'child_process'
|
|
||||||
function validateCaddyIsInstalled() {
|
|
||||||
let caddyInstalled = false
|
|
||||||
try {
|
|
||||||
execSync('caddy version')
|
|
||||||
caddyInstalled = true
|
|
||||||
} catch {
|
|
||||||
caddyInstalled = false
|
|
||||||
console.error('caddy cli is not installed')
|
|
||||||
}
|
|
||||||
return caddyInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/index.ts
|
|
||||||
function viteCaddyTlsPlugin(url?:string) {
|
|
||||||
return {
|
|
||||||
name: 'vite:caddy-tls',
|
|
||||||
async configResolved({ command }) {
|
|
||||||
if (command !== 'serve') return
|
|
||||||
console.log('starting caddy plugin...')
|
|
||||||
validateCaddyIsInstalled()
|
|
||||||
const handle = spawn(
|
|
||||||
`caddy reverse-proxy ${url ? `--from ${url}` : ''} --to http://localhost:5173`,
|
|
||||||
{
|
|
||||||
shell: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
handle.stdout.on('data', (data) => {
|
|
||||||
console.log(`stdout: ${data}`)
|
|
||||||
})
|
|
||||||
handle.stderr.on('data', () => {})
|
|
||||||
//const servers = parseNamesFromCaddyFile(`${cwd}/Caddyfile`);
|
|
||||||
console.log()
|
|
||||||
console.log(
|
|
||||||
chalk.green('\u{1F512} Caddy is running to proxy your traffic on https')
|
|
||||||
)
|
|
||||||
console.log()
|
|
||||||
console.log(`\u{1F517} Access your local server `)
|
|
||||||
console.log(chalk.blue(`\u{1F30D} https://${url ?? 'localhost'}`))
|
|
||||||
console.log()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export { viteCaddyTlsPlugin as default }
|
|
||||||
@@ -202,7 +202,7 @@ export async function DownloadConfig<T>(name: string, id?: number): Promise<
|
|||||||
}
|
}
|
||||||
> {
|
> {
|
||||||
try {
|
try {
|
||||||
const resp = await QueryGetAPI<string>(USER_CONFIG_API_URL + (id ? 'user-get' : 'get'), {
|
const resp = await QueryGetAPI<string>(USER_CONFIG_API_URL + (id ? 'get-user' : 'get'), {
|
||||||
name: name,
|
name: name,
|
||||||
id: id
|
id: id
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="checkin-template-helper">
|
|
||||||
<TemplateHelper :placeholders="checkInPlaceholders" />
|
|
||||||
<NAlert
|
|
||||||
type="info"
|
|
||||||
:show-icon="false"
|
|
||||||
style="margin-top: 8px;"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="alert-header">
|
|
||||||
<NIcon
|
|
||||||
:component="Info24Filled"
|
|
||||||
style="margin-right: 4px;"
|
|
||||||
/>
|
|
||||||
签到模板可用变量列表
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<NDivider style="margin: 6px 0;" />
|
|
||||||
<div class="placeholder-groups">
|
|
||||||
<div class="placeholder-group">
|
|
||||||
<div class="group-title">
|
|
||||||
用户信息
|
|
||||||
</div> <div class="placeholder-item">
|
|
||||||
<code>{{user.name}}</code> - 用户名称
|
|
||||||
</div>
|
|
||||||
<div class="placeholder-item">
|
|
||||||
<code>{{user.uid}}</code> - 用户ID
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="placeholder-group">
|
|
||||||
<div class="group-title">
|
|
||||||
签到信息
|
|
||||||
</div> <div class="placeholder-item">
|
|
||||||
<code>{{checkin.points}}</code> - 基础签到积分
|
|
||||||
</div>
|
|
||||||
<div class="placeholder-item">
|
|
||||||
<code>{{checkin.bonusPoints}}</code> - 早鸟额外积分 (普通签到为0)
|
|
||||||
</div>
|
|
||||||
<div class="placeholder-item">
|
|
||||||
<code>{{checkin.totalPoints}}</code> - 总获得积分
|
|
||||||
</div>
|
|
||||||
<div class="placeholder-item">
|
|
||||||
<code>{{checkin.isEarlyBird}}</code> - 是否是早鸟签到 (true/false)
|
|
||||||
</div>
|
|
||||||
<div class="placeholder-item">
|
|
||||||
<code>{{checkin.cooldownSeconds}}</code> - 签到冷却时间(秒)
|
|
||||||
</div>
|
|
||||||
<div class="placeholder-item">
|
|
||||||
<code>{{checkin.time}}</code> - 签到时间对象
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<NDivider style="margin: 6px 0;" />
|
|
||||||
<div class="placeholder-example">
|
|
||||||
<div class="example-title">
|
|
||||||
示例模板:
|
|
||||||
</div> <div class="example-item">
|
|
||||||
普通签到: <code>{{user.name}} 签到成功!获得 {{checkin.totalPoints}} 积分。</code>
|
|
||||||
</div>
|
|
||||||
<div class="example-item">
|
|
||||||
早鸟签到: <code>恭喜 {{user.name}} 完成早鸟签到!额外获得 {{checkin.bonusPoints}} 积分,共获得 {{checkin.totalPoints}} 积分!</code>
|
|
||||||
</div>
|
|
||||||
<div class="example-item">
|
|
||||||
条件表达式: <code>{{js: checkin.isEarlyBird ? `恭喜 ${user.name} 获得早鸟奖励!` : `${user.name} 签到成功!`}} 获得 {{checkin.totalPoints}} 积分。</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NAlert>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { NAlert, NDivider, NIcon } from 'naive-ui';
|
|
||||||
import { Info24Filled } from '@vicons/fluent';
|
|
||||||
import TemplateHelper from './TemplateHelper.vue';
|
|
||||||
|
|
||||||
// 签到模板的特定占位符
|
|
||||||
const checkInPlaceholders = [
|
|
||||||
{ name: '{{user.name}}', description: '用户名称' },
|
|
||||||
{ name: '{{user.uid}}', description: '用户ID' },
|
|
||||||
{ name: '{{checkin.points}}', description: '基础签到积分' },
|
|
||||||
{ name: '{{checkin.bonusPoints}}', description: '早鸟额外积分 (普通签到为0)' },
|
|
||||||
{ name: '{{checkin.totalPoints}}', description: '总获得积分' },
|
|
||||||
{ name: '{{checkin.isEarlyBird}}', description: '是否是早鸟签到 (true/false)' },
|
|
||||||
{ name: '{{checkin.cooldownSeconds}}', description: '签到冷却时间(秒)' },
|
|
||||||
{ name: '{{checkin.time}}', description: '签到时间对象' }
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.checkin-template-helper {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-groups {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-group {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-item {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-item code {
|
|
||||||
padding: 1px 4px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-example {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-item {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-item code {
|
|
||||||
display: block;
|
|
||||||
padding: 4px 8px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-top: 2px;
|
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { NSpace, NSwitch, NInputNumber, NSelect, NCheckbox, NDivider } from 'naive-ui';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
config: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
showLiveOnly: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showDelay: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
showUserFilter: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
showTianXuan: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="common-config-section">
|
|
||||||
<NSpace
|
|
||||||
vertical
|
|
||||||
size="medium"
|
|
||||||
>
|
|
||||||
<NSpace
|
|
||||||
align="center"
|
|
||||||
justify="space-between"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<span>启用功能:</span>
|
|
||||||
<NSwitch v-model:value="config.enabled" />
|
|
||||||
</NSpace>
|
|
||||||
|
|
||||||
<NSpace
|
|
||||||
v-if="showLiveOnly"
|
|
||||||
align="center"
|
|
||||||
justify="space-between"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<span>仅直播中开启:</span>
|
|
||||||
<NSwitch v-model:value="config.onlyDuringLive" />
|
|
||||||
</NSpace>
|
|
||||||
|
|
||||||
<NSpace
|
|
||||||
v-if="showDelay"
|
|
||||||
align="center"
|
|
||||||
justify="space-between"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<span>延迟时间 (秒):</span>
|
|
||||||
<NInputNumber
|
|
||||||
v-model:value="config.delaySeconds"
|
|
||||||
:min="0"
|
|
||||||
:max="300"
|
|
||||||
style="width: 120px"
|
|
||||||
/>
|
|
||||||
</NSpace>
|
|
||||||
|
|
||||||
<NSpace
|
|
||||||
v-if="showTianXuan"
|
|
||||||
align="center"
|
|
||||||
justify="space-between"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<span>屏蔽天选时刻:</span>
|
|
||||||
<NSwitch v-model:value="config.ignoreTianXuan" />
|
|
||||||
</NSpace>
|
|
||||||
|
|
||||||
<template v-if="showUserFilter">
|
|
||||||
<NDivider title-placement="left">
|
|
||||||
用户过滤设置
|
|
||||||
</NDivider>
|
|
||||||
|
|
||||||
<NSpace
|
|
||||||
align="center"
|
|
||||||
justify="space-between"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<span>启用用户过滤:</span>
|
|
||||||
<NSwitch v-model:value="config.userFilterEnabled" />
|
|
||||||
</NSpace>
|
|
||||||
|
|
||||||
<NSpace
|
|
||||||
v-if="config.userFilterEnabled"
|
|
||||||
align="center"
|
|
||||||
justify="space-between"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<span>要求本房间勋章:</span>
|
|
||||||
<NSwitch v-model:value="config.requireMedal" />
|
|
||||||
</NSpace>
|
|
||||||
|
|
||||||
<NSpace
|
|
||||||
v-if="config.userFilterEnabled"
|
|
||||||
align="center"
|
|
||||||
justify="space-between"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<span>要求任意舰长:</span>
|
|
||||||
<NSwitch v-model:value="config.requireCaptain" />
|
|
||||||
</NSpace>
|
|
||||||
</template>
|
|
||||||
</NSpace>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.common-config-section {
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="template-tester">
|
|
||||||
<NSpace vertical>
|
|
||||||
<NInput
|
|
||||||
v-model:value="template"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="输入包含表达式的模板"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NSpace>
|
|
||||||
<NButton
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="testTemplate"
|
|
||||||
>
|
|
||||||
测试模板
|
|
||||||
</NButton>
|
|
||||||
<NButton
|
|
||||||
size="small"
|
|
||||||
@click="resetTemplate"
|
|
||||||
>
|
|
||||||
重置
|
|
||||||
</NButton>
|
|
||||||
</NSpace>
|
|
||||||
|
|
||||||
<template
|
|
||||||
v-if="hasResult"
|
|
||||||
>
|
|
||||||
<NDivider style="margin: 5px;" />
|
|
||||||
<NCard
|
|
||||||
title="结果预览"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<NInput
|
|
||||||
type="textarea"
|
|
||||||
:value="result"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</NCard>
|
|
||||||
</template>
|
|
||||||
</NSpace>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
import { NSpace, NInput, NInputGroup, NInputGroupLabel, NButton, useMessage, NDivider } from 'naive-ui';
|
|
||||||
import { evaluateTemplateExpressions } from '@/client/store/autoAction/expressionEvaluator';
|
|
||||||
import { EventModel } from '@/api/api-models';
|
|
||||||
import { TriggerType } from '@/client/store/autoAction/types';
|
|
||||||
import { buildExecutionContext } from '@/client/store/autoAction/utils';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
defaultTemplate: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = ref(props.defaultTemplate || '');
|
|
||||||
const result = ref('');
|
|
||||||
const hasResult = computed(() => result.value !== '');
|
|
||||||
const message = useMessage();
|
|
||||||
|
|
||||||
function evaluateTemplateForUI(template: string, contextObj: Record<string, any>): string {
|
|
||||||
const tempContext = buildExecutionContext(contextObj, undefined, TriggerType.DANMAKU);
|
|
||||||
return evaluateTemplateExpressions(template, tempContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
function testTemplate() {
|
|
||||||
try {
|
|
||||||
result.value = evaluateTemplateForUI(template.value, props.context);
|
|
||||||
} catch (error) {
|
|
||||||
message.error(`表达式求值错误: ${(error as Error).message}`);
|
|
||||||
result.value = `[错误] ${(error as Error).message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetTemplate() {
|
|
||||||
template.value = props.defaultTemplate;
|
|
||||||
result.value = '';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.template-tester {
|
|
||||||
margin-top: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-container {
|
|
||||||
margin-top: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-content {
|
|
||||||
padding: 8px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px dashed #d9d9d9;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { EventDataTypes, EventModel } from '@/api/api-models';
|
|
||||||
import { DanmakuWindowSettings, useDanmakuWindow } from '../../store/useDanmakuWindow';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { AVATAR_URL } from '@/data/constants';
|
|
||||||
import { GetGuardColor } from '@/Utils';
|
|
||||||
|
|
||||||
export interface BaseDanmakuItemProps {
|
|
||||||
item: EventModel & { randomId: string; isNew?: boolean; disappearAt?: number; };
|
|
||||||
setting: DanmakuWindowSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<BaseDanmakuItemProps>();
|
|
||||||
|
|
||||||
const emojiData = useDanmakuWindow().emojiData;
|
|
||||||
|
|
||||||
// 检查弹幕是否将要消失
|
|
||||||
const isDisappearing = computed(() => {
|
|
||||||
return props.item.disappearAt && Date.now() > props.item.disappearAt - 300; // 提前300ms进入消失动画
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算SC弹幕的颜色类
|
|
||||||
const scColorClass = computed(() => {
|
|
||||||
if (props.item.type === EventDataTypes.SC) {
|
|
||||||
const price = props.item?.price || 0;
|
|
||||||
if (price === 0) return 'sc-0';
|
|
||||||
if (price > 0 && price < 50) return 'sc-50';
|
|
||||||
if (price >= 50 && price < 100) return 'sc-100';
|
|
||||||
if (price >= 100 && price < 500) return 'sc-500';
|
|
||||||
if (price >= 500 && price < 1000) return 'sc-1000';
|
|
||||||
if (price >= 1000 && price < 2000) return 'sc-2000';
|
|
||||||
if (price >= 2000) return 'sc-max';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 根据类型计算样式
|
|
||||||
const typeClass = computed(() => {
|
|
||||||
switch (props.item.type) {
|
|
||||||
case EventDataTypes.Message: return 'message-item';
|
|
||||||
case EventDataTypes.Gift: return 'gift-item';
|
|
||||||
case EventDataTypes.SC: return `sc-item ${scColorClass.value}`;
|
|
||||||
case EventDataTypes.Guard: return 'guard-item';
|
|
||||||
case EventDataTypes.Enter: return 'enter-item';
|
|
||||||
default: return '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取舰长颜色
|
|
||||||
const guardColor = computed(() => GetGuardColor(props.item.guard_level));
|
|
||||||
|
|
||||||
// 舰长样式类
|
|
||||||
const guardLevelClass = computed(() => {
|
|
||||||
if (props.item.type === EventDataTypes.Guard) {
|
|
||||||
return `guard-level-${props.item.guard_level || 0}`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查是否需要显示头像
|
|
||||||
const showAvatar = computed(() => props.setting.showAvatar);
|
|
||||||
|
|
||||||
// 解析包含内联表情的消息
|
|
||||||
const parsedMessage = computed<{ type: 'text' | 'emoji'; content?: string; url?: string; name?: string; }[]>(() => {
|
|
||||||
// 仅处理非纯表情的普通消息
|
|
||||||
if (props.item.type !== EventDataTypes.Message || props.item.emoji || !props.item.msg) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments: { type: 'text' | 'emoji'; content?: string; url?: string; name?: string; }[] = [];
|
|
||||||
let lastIndex = 0;
|
|
||||||
const regex = /\[([^\]]+)\]/g; // 匹配 [表情名]
|
|
||||||
let match;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const availableEmojis = emojiData.data || {}; // 确保 emojiData 已加载
|
|
||||||
|
|
||||||
while ((match = regex.exec(props.item.msg)) !== null) {
|
|
||||||
// 添加表情前的文本部分
|
|
||||||
if (match.index > lastIndex) {
|
|
||||||
segments.push({ type: 'text', content: props.item.msg.substring(lastIndex, match.index) });
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojiFullName = match[0]; // 完整匹配,例如 "[哈哈]"
|
|
||||||
const emojiInfo = availableEmojis.inline[emojiFullName] || availableEmojis.plain[emojiFullName];
|
|
||||||
|
|
||||||
if (emojiInfo) {
|
|
||||||
// 找到了表情
|
|
||||||
segments.push({ type: 'emoji', url: emojiInfo, name: emojiFullName });
|
|
||||||
} else {
|
|
||||||
// 未找到表情,当作普通文本处理
|
|
||||||
segments.push({ type: 'text', content: emojiFullName });
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIndex = regex.lastIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加最后一个表情后的文本部分
|
|
||||||
if (lastIndex < props.item.msg.length) {
|
|
||||||
segments.push({ type: 'text', content: props.item.msg.substring(lastIndex) });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error parsing message for emojis:", error);
|
|
||||||
// 解析出错时,返回原始文本
|
|
||||||
return [{ type: 'text', content: props.item.msg }];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果解析后为空(例如,消息只包含无法识别的[]),则返回原始文本
|
|
||||||
if (segments.length === 0 && props.item.msg) {
|
|
||||||
return [{ type: 'text', content: props.item.msg }];
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取不同类型消息的显示标签
|
|
||||||
const typeLabel = computed(() => {
|
|
||||||
switch (props.item.type) {
|
|
||||||
case EventDataTypes.Message: return ''; // 普通消息不需要标签
|
|
||||||
case EventDataTypes.Gift: return '【礼物】';
|
|
||||||
case EventDataTypes.SC: return '【SC】';
|
|
||||||
case EventDataTypes.Guard: return '【舰长】';
|
|
||||||
case EventDataTypes.Enter: return '【进场】';
|
|
||||||
default: return '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取礼物或SC的价格文本
|
|
||||||
const priceText = computed(() => {
|
|
||||||
if (props.item.type === EventDataTypes.SC ||
|
|
||||||
(props.item.type === EventDataTypes.Gift && props.item.price > 0)) {
|
|
||||||
return `¥${props.item.price || 0}`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取用户名显示
|
|
||||||
const displayName = computed(() => {
|
|
||||||
return props.item.uname || '匿名用户';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取消息显示内容
|
|
||||||
const displayContent = computed(() => {
|
|
||||||
switch (props.item.type) {
|
|
||||||
case EventDataTypes.Message:
|
|
||||||
return props.item.msg || '';
|
|
||||||
case EventDataTypes.Gift:
|
|
||||||
return `${props.item.num || 1} × ${props.item.msg}`;
|
|
||||||
case EventDataTypes.SC:
|
|
||||||
return props.item.msg || '';
|
|
||||||
case EventDataTypes.Guard:
|
|
||||||
return props.item.msg || '开通了舰长';
|
|
||||||
case EventDataTypes.Enter:
|
|
||||||
return '进入了直播间';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 根据风格及类型获取文本颜色
|
|
||||||
const textModeColor = computed(() => {
|
|
||||||
if (props.item.type === EventDataTypes.SC) {
|
|
||||||
return '#FFD700'; // SC消息金色
|
|
||||||
} else if (props.item.type === EventDataTypes.Gift) {
|
|
||||||
return '#FF69B4'; // 礼物消息粉色
|
|
||||||
} else if (props.item.type === EventDataTypes.Guard) {
|
|
||||||
return guardColor.value; // 舰长消息使用舰长颜色
|
|
||||||
} else if (props.item.type === EventDataTypes.Enter) {
|
|
||||||
return '#67C23A'; // 入场消息绿色
|
|
||||||
}
|
|
||||||
return undefined; // 普通消息使用默认颜色
|
|
||||||
});
|
|
||||||
|
|
||||||
// 向外导出所有计算属性
|
|
||||||
defineExpose({
|
|
||||||
isDisappearing,
|
|
||||||
scColorClass,
|
|
||||||
typeClass,
|
|
||||||
guardColor,
|
|
||||||
guardLevelClass,
|
|
||||||
showAvatar,
|
|
||||||
parsedMessage,
|
|
||||||
typeLabel,
|
|
||||||
priceText,
|
|
||||||
displayName,
|
|
||||||
displayContent,
|
|
||||||
textModeColor
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<slot />
|
|
||||||
</template>
|
|
||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
NTag,
|
NTag,
|
||||||
NText,
|
NText,
|
||||||
NTooltip,
|
NTooltip,
|
||||||
|
NSwitch,
|
||||||
useMessage, // Naive UI 组件
|
useMessage, // Naive UI 组件
|
||||||
} from 'naive-ui';
|
} from 'naive-ui';
|
||||||
import { VNodeChild, computed, h, onMounted, ref, watch } from 'vue'; // Vue 核心 API
|
import { VNodeChild, computed, h, onMounted, ref, watch } from 'vue'; // Vue 核心 API
|
||||||
@@ -58,6 +59,8 @@ const props = defineProps<{
|
|||||||
// --- 响应式状态 ---
|
// --- 响应式状态 ---
|
||||||
const message = useMessage() // Naive UI 消息提示
|
const message = useMessage() // Naive UI 消息提示
|
||||||
const volume = useLocalStorage('Settings.AplayerVolume', 0.8) // 播放器音量,持久化存储
|
const volume = useLocalStorage('Settings.AplayerVolume', 0.8) // 播放器音量,持久化存储
|
||||||
|
const showListenButton = useLocalStorage('SongList.ShowListenButton', true) // 是否显示试听按钮
|
||||||
|
const showLinkButton = useLocalStorage('SongList.ShowLinkButton', true) // 是否显示跳转按钮
|
||||||
const songsInternal = ref<SongsInfo[]>([]) // 内部维护的歌曲列表,避免直接修改 props
|
const songsInternal = ref<SongsInfo[]>([]) // 内部维护的歌曲列表,避免直接修改 props
|
||||||
const playingSong = ref<SongsInfo>() // 当前正在试听的歌曲
|
const playingSong = ref<SongsInfo>() // 当前正在试听的歌曲
|
||||||
const isLrcLoading = ref<string>() // 歌词加载状态(存储歌曲 key)
|
const isLrcLoading = ref<string>() // 歌词加载状态(存储歌曲 key)
|
||||||
@@ -116,6 +119,32 @@ defineExpose({
|
|||||||
|
|
||||||
// --- 计算属性 ---
|
// --- 计算属性 ---
|
||||||
|
|
||||||
|
// 计算操作列的预定义宽度
|
||||||
|
const actionColumnWidth = computed(() => {
|
||||||
|
const baseSelfWidth = 85; // 基础宽度 (isSelf=true, 编辑+删除)
|
||||||
|
const basePublicWidth = 40; // 基础宽度 (isSelf=false)
|
||||||
|
const listenButtonWidth = 40;
|
||||||
|
const linkButtonWidth = 40;
|
||||||
|
const extraButtonWidth = 40; // 假设的额外按钮宽度
|
||||||
|
|
||||||
|
let width = props.isSelf ? baseSelfWidth : basePublicWidth;
|
||||||
|
|
||||||
|
if (showListenButton.value) {
|
||||||
|
width += listenButtonWidth;
|
||||||
|
}
|
||||||
|
if (showLinkButton.value) {
|
||||||
|
width += linkButtonWidth;
|
||||||
|
}
|
||||||
|
if (props.extraButton) {
|
||||||
|
width += extraButtonWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回一个合理的宽度值,例如,可以设定几个档位
|
||||||
|
// 这里用之前的计算逻辑,但可以替换为固定档位如 80, 120, 160, 200, 240
|
||||||
|
// 为了精确,我们还是用计算值,但它是响应式的
|
||||||
|
return width;
|
||||||
|
});
|
||||||
|
|
||||||
// 筛选后的歌曲列表
|
// 筛选后的歌曲列表
|
||||||
const songsComputed = computed(() => {
|
const songsComputed = computed(() => {
|
||||||
let filteredSongs = songsInternal.value;
|
let filteredSongs = songsInternal.value;
|
||||||
@@ -340,18 +369,19 @@ function createColumns(): DataTableColumns<SongsInfo> {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'manage',
|
key: 'manage',
|
||||||
width: props.isSelf ? 170 : 120, // 根据是否自己的歌单调整宽度
|
|
||||||
fixed: 'right', // 固定操作列在右侧
|
fixed: 'right', // 固定操作列在右侧
|
||||||
render(data) {
|
render(data) {
|
||||||
const buttons: VNodeChild[] = [];
|
const buttons: VNodeChild[] = [];
|
||||||
|
|
||||||
// 1. 获取播放/信息按钮 (来自 Utils)
|
// 1. 获取播放/信息按钮 (来自 Utils)
|
||||||
const playButton = GetPlayButton(data);
|
if (showLinkButton.value) { // 添加条件
|
||||||
if (playButton) buttons.push(playButton);
|
const playButton = GetPlayButton(data);
|
||||||
|
if (playButton) buttons.push(playButton);
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 试听按钮 (仅对音频文件显示)
|
// 2. 试听按钮 (仅对音频文件显示)
|
||||||
const isAudio = /\.(mp3|flac|ogg|wav|m4a)$/i.test(data.url ?? ''); // 正则判断音频后缀
|
const isAudio = /\.(mp3|flac|ogg|wav|m4a)$/i.test(data.url ?? ''); // 正则判断音频后缀
|
||||||
if (isAudio) {
|
if (showListenButton.value && isAudio) { // 添加条件
|
||||||
buttons.push(
|
buttons.push(
|
||||||
h(NTooltip, null, {
|
h(NTooltip, null, {
|
||||||
trigger: () =>
|
trigger: () =>
|
||||||
@@ -422,6 +452,17 @@ function createColumns(): DataTableColumns<SongsInfo> {
|
|||||||
// 使用 NSpace 渲染所有按钮
|
// 使用 NSpace 渲染所有按钮
|
||||||
return h(NSpace, { justify: 'end', size: 8, wrap: false }, () => buttons); // 增加间距,禁止换行
|
return h(NSpace, { justify: 'end', size: 8, wrap: false }, () => buttons); // 增加间距,禁止换行
|
||||||
},
|
},
|
||||||
|
// --- 动态计算宽度 --- START
|
||||||
|
/* width: (() => {
|
||||||
|
let calculatedWidth = 20; // 基础内边距
|
||||||
|
if (showLinkButton.value) calculatedWidth += 40; // 链接按钮宽度
|
||||||
|
if (showListenButton.value) calculatedWidth += 40; // 试听按钮宽度
|
||||||
|
if (props.isSelf) calculatedWidth += 80; // 编辑 + 删除按钮宽度
|
||||||
|
if (props.extraButton) calculatedWidth += 40; // 额外按钮预估宽度
|
||||||
|
return Math.max(calculatedWidth, props.isSelf ? 160 : 80); // 设置最小宽度防止太窄
|
||||||
|
})(), */
|
||||||
|
width: actionColumnWidth.value, // 使用计算属性
|
||||||
|
// --- 动态计算宽度 --- END
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -452,6 +493,12 @@ watch(
|
|||||||
{ deep: true } // 深度监听,如果 songs 数组内部对象变化也触发
|
{ deep: true } // 深度监听,如果 songs 数组内部对象变化也触发
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 监听按钮显示状态变化,重新计算列定义以更新宽度
|
||||||
|
watch([showListenButton, showLinkButton], () => {
|
||||||
|
console.log('Button visibility changed, recalculating columns.');
|
||||||
|
columns.value = createColumns();
|
||||||
|
});
|
||||||
|
|
||||||
// 更新单首歌曲信息
|
// 更新单首歌曲信息
|
||||||
async function updateSong() {
|
async function updateSong() {
|
||||||
try {
|
try {
|
||||||
@@ -711,16 +758,26 @@ onMounted(() => {
|
|||||||
style="min-width: 180px; flex-grow: 1;"
|
style="min-width: 180px; flex-grow: 1;"
|
||||||
max-tag-count="responsive"
|
max-tag-count="responsive"
|
||||||
/>
|
/>
|
||||||
<!-- 清除作者列筛选按钮 (当顶部选择器清除时,列筛选也应清除,但保留按钮以防万一) -->
|
<!-- 显示控制开关 -->
|
||||||
<!-- <NButton
|
<NSpace
|
||||||
v-if="authorColumn.filterOptionValue"
|
item-style="display: flex; align-items: center;"
|
||||||
type="warning"
|
|
||||||
size="small"
|
size="small"
|
||||||
ghost
|
|
||||||
@click="onAuthorClick(authorColumn.filterOptionValue as string)"
|
|
||||||
>
|
>
|
||||||
清除歌手列筛选
|
<NSwitch
|
||||||
</NButton> -->
|
v-model:value="showListenButton"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<NText style="font-size: 12px;">
|
||||||
|
试听
|
||||||
|
</NText>
|
||||||
|
<NSwitch
|
||||||
|
v-model:value="showLinkButton"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<NText style="font-size: 12px;">
|
||||||
|
链接
|
||||||
|
</NText>
|
||||||
|
</NSpace>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
@@ -770,6 +827,7 @@ onMounted(() => {
|
|||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="songsComputed"
|
:data="songsComputed"
|
||||||
size="small"
|
size="small"
|
||||||
|
:scroll-x="800"
|
||||||
:pagination="{
|
:pagination="{
|
||||||
itemCount: songsInternal.length,
|
itemCount: songsInternal.length,
|
||||||
defaultPageSize: pageSize,
|
defaultPageSize: pageSize,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
import EasySpeech from 'easy-speech'
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import ChatClientOfficialBase, * as base from './ChatClientOfficialBase'
|
|
||||||
import { processAvatarUrl } from './models'
|
|
||||||
|
|
||||||
export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
|
||||||
constructor(authInfo) {
|
|
||||||
super()
|
|
||||||
this.CMD_CALLBACK_MAP = CMD_CALLBACK_MAP
|
|
||||||
|
|
||||||
this.auth = authInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
super.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
async initRoom() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async onBeforeWsConnect() {
|
|
||||||
return super.onBeforeWsConnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
getWsUrl() {
|
|
||||||
return this.auth.websocket_info.wss_link[this.retryCount % this.auth.websocket_info.wss_link.length]
|
|
||||||
}
|
|
||||||
|
|
||||||
sendAuth() {
|
|
||||||
this.websocket.send(this.makePacket(this.auth.websocket_info.auth_body, base.OP_AUTH))
|
|
||||||
}
|
|
||||||
|
|
||||||
async dmCallback(command) {
|
|
||||||
if (!this.onAddText) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let data = command.data
|
|
||||||
|
|
||||||
let authorType
|
|
||||||
if (data.uid === this.roomOwnerUid) {
|
|
||||||
authorType = 3
|
|
||||||
} else if (data.guard_level !== 0) {
|
|
||||||
authorType = 1
|
|
||||||
} else {
|
|
||||||
authorType = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
let emoticon = null
|
|
||||||
if (data.dm_type === 1) {
|
|
||||||
emoticon = data.emoji_img_url
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
avatarUrl: processAvatarUrl(data.uface),
|
|
||||||
timestamp: data.timestamp,
|
|
||||||
authorName: data.uname,
|
|
||||||
authorType: authorType,
|
|
||||||
content: data.msg,
|
|
||||||
privilegeType: data.guard_level,
|
|
||||||
isGiftDanmaku: false,
|
|
||||||
authorLevel: 1,
|
|
||||||
isNewbie: false,
|
|
||||||
isMobileVerified: true,
|
|
||||||
medalLevel: data.fans_medal_wearing_status ? data.fans_medal_level : 0,
|
|
||||||
id: data.msg_id,
|
|
||||||
translation: '',
|
|
||||||
emoticon: emoticon,
|
|
||||||
}
|
|
||||||
this.onAddText(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendGiftCallback(command) {
|
|
||||||
if (!this.onAddGift) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let data = command.data
|
|
||||||
if (!data.paid) {
|
|
||||||
// 丢人
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
id: data.msg_id,
|
|
||||||
avatarUrl: processAvatarUrl(data.uface),
|
|
||||||
timestamp: data.timestamp,
|
|
||||||
authorName: data.uname,
|
|
||||||
totalCoin: data.price,
|
|
||||||
giftName: data.gift_name,
|
|
||||||
num: data.gift_num,
|
|
||||||
}
|
|
||||||
this.onAddGift(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
async guardCallback(command) {
|
|
||||||
if (!this.onAddMember) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = command.data
|
|
||||||
data = {
|
|
||||||
id: data.msg_id,
|
|
||||||
avatarUrl: processAvatarUrl(data.user_info.uface),
|
|
||||||
timestamp: data.timestamp,
|
|
||||||
authorName: data.user_info.uname,
|
|
||||||
privilegeType: data.guard_level,
|
|
||||||
}
|
|
||||||
this.onAddMember(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
superChatCallback(command) {
|
|
||||||
if (!this.onAddSuperChat) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = command.data
|
|
||||||
data = {
|
|
||||||
id: data.message_id.toString(),
|
|
||||||
avatarUrl: processAvatarUrl(data.uface),
|
|
||||||
timestamp: data.start_time,
|
|
||||||
authorName: data.uname,
|
|
||||||
price: data.rmb,
|
|
||||||
content: data.message,
|
|
||||||
translation: '',
|
|
||||||
}
|
|
||||||
this.onAddSuperChat(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
superChatDelCallback(command) {
|
|
||||||
if (!this.onDelSuperChat) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const ids = []
|
|
||||||
for (const id of command.data.message_ids) {
|
|
||||||
ids.push(id.toString())
|
|
||||||
}
|
|
||||||
this.onDelSuperChat({ ids })
|
|
||||||
}
|
|
||||||
|
|
||||||
rawMessageCallback(command) {
|
|
||||||
if (!this.onRawMessage) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.onRawMessage(command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CMD_CALLBACK_MAP = {
|
|
||||||
LIVE_OPEN_PLATFORM_DM: ChatClientDirectOpenLive.prototype.dmCallback,
|
|
||||||
LIVE_OPEN_PLATFORM_SEND_GIFT: ChatClientDirectOpenLive.prototype.sendGiftCallback,
|
|
||||||
LIVE_OPEN_PLATFORM_GUARD: ChatClientDirectOpenLive.prototype.guardCallback,
|
|
||||||
LIVE_OPEN_PLATFORM_SUPER_CHAT: ChatClientDirectOpenLive.prototype.superChatCallback,
|
|
||||||
LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL: ChatClientDirectOpenLive.prototype.superChatDelCallback,
|
|
||||||
RAW_MESSAGE: ChatClientDirectOpenLive.prototype.rawMessageCallback,
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
import * as chat from './ChatClientOfficialBase'
|
|
||||||
import * as chatModels from './models.js'
|
|
||||||
import * as base from './ChatClientOfficialBase'
|
|
||||||
import ChatClientOfficialBase from './ChatClientOfficialBase'
|
|
||||||
|
|
||||||
export default class ChatClientDirectWeb extends ChatClientOfficialBase {
|
|
||||||
constructor(roomId) {
|
|
||||||
super()
|
|
||||||
this.CMD_CALLBACK_MAP = CMD_CALLBACK_MAP
|
|
||||||
|
|
||||||
// 调用initRoom后初始化,如果失败,使用这里的默认值
|
|
||||||
this.roomId = roomId
|
|
||||||
this.roomOwnerUid = -1
|
|
||||||
this.hostServerList = [
|
|
||||||
{
|
|
||||||
host: 'broadcastlv.chat.bilibili.com',
|
|
||||||
port: 2243,
|
|
||||||
wss_port: 443,
|
|
||||||
ws_port: 2244
|
|
||||||
}
|
|
||||||
]
|
|
||||||
this.hostServerToken = null
|
|
||||||
this.buvid = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async initRoom() {
|
|
||||||
let res
|
|
||||||
try {
|
|
||||||
res = await (
|
|
||||||
await fetch('/api/room_info?room_id=' + this.roomId, { method: 'GET' })
|
|
||||||
).json()
|
|
||||||
} catch {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
this.roomId = res.roomId
|
|
||||||
this.roomOwnerUid = res.ownerUid
|
|
||||||
if (res.hostServerList.length !== 0) {
|
|
||||||
this.hostServerList = res.hostServerList
|
|
||||||
}
|
|
||||||
this.hostServerToken = res.hostServerToken
|
|
||||||
this.buvid = res.buvid
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async onBeforeWsConnect() {
|
|
||||||
// 重连次数太多则重新init_room,保险
|
|
||||||
let reinitPeriod = Math.max(3, (this.hostServerList || []).length)
|
|
||||||
if (this.retryCount > 0 && this.retryCount % reinitPeriod === 0) {
|
|
||||||
this.needInitRoom = true
|
|
||||||
}
|
|
||||||
return super.onBeforeWsConnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
getWsUrl() {
|
|
||||||
let hostServer =
|
|
||||||
this.hostServerList[this.retryCount % this.hostServerList.length]
|
|
||||||
return `wss://${hostServer.host}:${hostServer.wss_port}/sub`
|
|
||||||
}
|
|
||||||
|
|
||||||
sendAuth() {
|
|
||||||
let authParams = {
|
|
||||||
uid: 0,
|
|
||||||
roomid: this.roomId,
|
|
||||||
protover: 3,
|
|
||||||
platform: 'web',
|
|
||||||
type: 2,
|
|
||||||
buvid: this.buvid
|
|
||||||
}
|
|
||||||
if (this.hostServerToken !== null) {
|
|
||||||
authParams.key = this.hostServerToken
|
|
||||||
}
|
|
||||||
this.websocket.send(this.makePacket(authParams, base.OP_AUTH))
|
|
||||||
}
|
|
||||||
|
|
||||||
async danmuMsgCallback(command) {
|
|
||||||
let info = command.info
|
|
||||||
|
|
||||||
let roomId, medalLevel
|
|
||||||
if (info[3]) {
|
|
||||||
roomId = info[3][3]
|
|
||||||
medalLevel = info[3][0]
|
|
||||||
} else {
|
|
||||||
roomId = medalLevel = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
let uid = info[2][0]
|
|
||||||
let isAdmin = info[2][2]
|
|
||||||
let privilegeType = info[7]
|
|
||||||
let authorType
|
|
||||||
if (uid === this.roomOwnerUid) {
|
|
||||||
authorType = 3
|
|
||||||
} else if (isAdmin) {
|
|
||||||
authorType = 2
|
|
||||||
} else if (privilegeType !== 0) {
|
|
||||||
authorType = 1
|
|
||||||
} else {
|
|
||||||
authorType = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
let authorName = info[2][1]
|
|
||||||
let content = info[1]
|
|
||||||
let data = new chatModels.AddTextMsg({
|
|
||||||
avatarUrl: await chat.getAvatarUrl(uid, authorName),
|
|
||||||
timestamp: info[0][4] / 1000,
|
|
||||||
authorName: authorName,
|
|
||||||
authorType: authorType,
|
|
||||||
content: content,
|
|
||||||
privilegeType: privilegeType,
|
|
||||||
isGiftDanmaku:
|
|
||||||
Boolean(info[0][9]) || chat.isGiftDanmakuByContent(content),
|
|
||||||
authorLevel: info[4][0],
|
|
||||||
isNewbie: info[2][5] < 10000,
|
|
||||||
isMobileVerified: Boolean(info[2][6]),
|
|
||||||
medalLevel: roomId === this.roomId ? medalLevel : 0,
|
|
||||||
emoticon: info[0][13].url || null
|
|
||||||
})
|
|
||||||
this.msgHandler.onAddText(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendGiftCallback(command) {
|
|
||||||
let data = command.data
|
|
||||||
let isPaidGift = data.coin_type === 'gold'
|
|
||||||
data = new chatModels.AddGiftMsg({
|
|
||||||
avatarUrl: chat.processAvatarUrl(data.face),
|
|
||||||
timestamp: data.timestamp,
|
|
||||||
authorName: data.uname,
|
|
||||||
totalCoin: isPaidGift ? data.total_coin : 0,
|
|
||||||
totalFreeCoin: !isPaidGift ? data.total_coin : 0,
|
|
||||||
giftName: data.giftName,
|
|
||||||
num: data.num
|
|
||||||
})
|
|
||||||
this.msgHandler.onAddGift(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
async guardBuyCallback(command) {
|
|
||||||
let data = command.data
|
|
||||||
data = new chatModels.AddMemberMsg({
|
|
||||||
avatarUrl: await chat.getAvatarUrl(data.uid, data.username),
|
|
||||||
timestamp: data.start_time,
|
|
||||||
authorName: data.username,
|
|
||||||
privilegeType: data.guard_level
|
|
||||||
})
|
|
||||||
this.msgHandler.onAddMember(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
superChatMessageCallback(command) {
|
|
||||||
let data = command.data
|
|
||||||
data = new chatModels.AddSuperChatMsg({
|
|
||||||
id: data.id.toString(),
|
|
||||||
avatarUrl: chat.processAvatarUrl(data.user_info.face),
|
|
||||||
timestamp: data.start_time,
|
|
||||||
authorName: data.user_info.uname,
|
|
||||||
price: data.price,
|
|
||||||
content: data.message
|
|
||||||
})
|
|
||||||
this.msgHandler.onAddSuperChat(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
superChatMessageDeleteCallback(command) {
|
|
||||||
let ids = []
|
|
||||||
for (let id of command.data.ids) {
|
|
||||||
ids.push(id.toString())
|
|
||||||
}
|
|
||||||
let data = new chatModels.DelSuperChatMsg({ ids })
|
|
||||||
this.msgHandler.onDelSuperChat(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CMD_CALLBACK_MAP = {
|
|
||||||
DANMU_MSG: ChatClientDirectWeb.prototype.danmuMsgCallback,
|
|
||||||
SEND_GIFT: ChatClientDirectWeb.prototype.sendGiftCallback,
|
|
||||||
GUARD_BUY: ChatClientDirectWeb.prototype.guardBuyCallback,
|
|
||||||
SUPER_CHAT_MESSAGE: ChatClientDirectWeb.prototype.superChatMessageCallback,
|
|
||||||
SUPER_CHAT_MESSAGE_DELETE:
|
|
||||||
ChatClientDirectWeb.prototype.superChatMessageDeleteCallback
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,320 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
// @ts-nocheck
|
|
||||||
import { BrotliDecode } from './brotli_decode'
|
|
||||||
import { setInterval, clearInterval, setTimeout, clearTimeout } from 'worker-timers'
|
|
||||||
|
|
||||||
const HEADER_SIZE = 16
|
|
||||||
|
|
||||||
export const WS_BODY_PROTOCOL_VERSION_NORMAL = 0
|
|
||||||
export const WS_BODY_PROTOCOL_VERSION_HEARTBEAT = 1
|
|
||||||
export const WS_BODY_PROTOCOL_VERSION_DEFLATE = 2
|
|
||||||
export const WS_BODY_PROTOCOL_VERSION_BROTLI = 3
|
|
||||||
|
|
||||||
export const OP_HANDSHAKE = 0
|
|
||||||
export const OP_HANDSHAKE_REPLY = 1
|
|
||||||
export const OP_HEARTBEAT = 2
|
|
||||||
export const OP_HEARTBEAT_REPLY = 3
|
|
||||||
export const OP_SEND_MSG = 4
|
|
||||||
export const OP_SEND_MSG_REPLY = 5
|
|
||||||
export const OP_DISCONNECT_REPLY = 6
|
|
||||||
export const OP_AUTH = 7
|
|
||||||
export const OP_AUTH_REPLY = 8
|
|
||||||
export const OP_RAW = 9
|
|
||||||
export const OP_PROTO_READY = 10
|
|
||||||
export const OP_PROTO_FINISH = 11
|
|
||||||
export const OP_CHANGE_ROOM = 12
|
|
||||||
export const OP_CHANGE_ROOM_REPLY = 13
|
|
||||||
export const OP_REGISTER = 14
|
|
||||||
export const OP_REGISTER_REPLY = 15
|
|
||||||
export const OP_UNREGISTER = 16
|
|
||||||
export const OP_UNREGISTER_REPLY = 17
|
|
||||||
// B站业务自定义OP
|
|
||||||
// export const MinBusinessOp = 1000
|
|
||||||
// export const MaxBusinessOp = 10000
|
|
||||||
|
|
||||||
export const AUTH_REPLY_CODE_OK = 0
|
|
||||||
export const AUTH_REPLY_CODE_TOKEN_ERROR = -101
|
|
||||||
|
|
||||||
const HEARTBEAT_INTERVAL = 10 * 1000
|
|
||||||
const RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + 5 * 1000
|
|
||||||
|
|
||||||
const textEncoder = new TextEncoder()
|
|
||||||
const textDecoder = new TextDecoder()
|
|
||||||
|
|
||||||
export default class ChatClientOfficialBase {
|
|
||||||
constructor() {
|
|
||||||
this.CMD_CALLBACK_MAP = {}
|
|
||||||
|
|
||||||
this.onAddText = null
|
|
||||||
this.onAddGift = null
|
|
||||||
this.onAddMember = null
|
|
||||||
this.onAddSuperChat = null
|
|
||||||
this.onDelSuperChat = null
|
|
||||||
this.onUpdateTranslation = null
|
|
||||||
|
|
||||||
this.onFatalError = null
|
|
||||||
|
|
||||||
this.needInitRoom = true
|
|
||||||
this.websocket = null
|
|
||||||
this.retryCount = 0
|
|
||||||
this.isDestroying = false
|
|
||||||
this.heartbeatTimerId = null
|
|
||||||
this.receiveTimeoutTimerId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
this.wsConnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.isDestroying = true
|
|
||||||
if (this.websocket) {
|
|
||||||
this.websocket.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initRoom() {
|
|
||||||
throw Error('Not implemented')
|
|
||||||
}
|
|
||||||
|
|
||||||
makePacket(data, operation) {
|
|
||||||
let body
|
|
||||||
if (typeof data === 'object') {
|
|
||||||
body = textEncoder.encode(JSON.stringify(data))
|
|
||||||
} else {
|
|
||||||
// string
|
|
||||||
body = textEncoder.encode(data)
|
|
||||||
}
|
|
||||||
const header = new ArrayBuffer(HEADER_SIZE)
|
|
||||||
const headerView = new DataView(header)
|
|
||||||
headerView.setUint32(0, HEADER_SIZE + body.byteLength) // pack_len
|
|
||||||
headerView.setUint16(4, HEADER_SIZE) // raw_header_size
|
|
||||||
headerView.setUint16(6, 1) // ver
|
|
||||||
headerView.setUint32(8, operation) // operation
|
|
||||||
headerView.setUint32(12, 1) // seq_id
|
|
||||||
return new Blob([header, body])
|
|
||||||
}
|
|
||||||
|
|
||||||
sendAuth() {
|
|
||||||
throw Error('Not implemented')
|
|
||||||
}
|
|
||||||
|
|
||||||
async wsConnect() {
|
|
||||||
if (this.isDestroying) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.onBeforeWsConnect()
|
|
||||||
if (this.isDestroying) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.websocket = new WebSocket(this.getWsUrl())
|
|
||||||
this.websocket.binaryType = 'arraybuffer'
|
|
||||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
|
||||||
this.websocket.onclose = this.onWsClose.bind(this)
|
|
||||||
this.websocket.onmessage = this.onWsMessage.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
async onBeforeWsConnect() {
|
|
||||||
if (!this.needInitRoom) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let res
|
|
||||||
try {
|
|
||||||
res = await this.initRoom()
|
|
||||||
} catch (e) {
|
|
||||||
res = false
|
|
||||||
console.error('initRoom exception:', e)
|
|
||||||
if (this.onFatalError) {
|
|
||||||
this.onFatalError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
this.onWsClose()
|
|
||||||
throw Error('initRoom failed')
|
|
||||||
}
|
|
||||||
this.needInitRoom = false
|
|
||||||
}
|
|
||||||
|
|
||||||
getWsUrl() {
|
|
||||||
throw Error('Not implemented')
|
|
||||||
}
|
|
||||||
|
|
||||||
onWsOpen() {
|
|
||||||
this.sendAuth()
|
|
||||||
this.heartbeatTimerId = setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
|
|
||||||
this.refreshReceiveTimeoutTimer()
|
|
||||||
//console.log('ws 已连接')
|
|
||||||
}
|
|
||||||
|
|
||||||
sendHeartbeat() {
|
|
||||||
this.websocket.send(this.makePacket({}, OP_HEARTBEAT))
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshReceiveTimeoutTimer() {
|
|
||||||
if (this.receiveTimeoutTimerId) {
|
|
||||||
clearTimeout(this.receiveTimeoutTimerId)
|
|
||||||
}
|
|
||||||
this.receiveTimeoutTimerId = setTimeout(this.onReceiveTimeout.bind(this), RECEIVE_TIMEOUT)
|
|
||||||
}
|
|
||||||
|
|
||||||
onReceiveTimeout() {
|
|
||||||
console.warn('接收消息超时')
|
|
||||||
this.discardWebsocket()
|
|
||||||
}
|
|
||||||
|
|
||||||
discardWebsocket() {
|
|
||||||
if (this.receiveTimeoutTimerId) {
|
|
||||||
clearTimeout(this.receiveTimeoutTimerId)
|
|
||||||
this.receiveTimeoutTimerId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接丢弃阻塞的websocket,不等onclose回调了
|
|
||||||
this.websocket.onopen = this.websocket.onclose = this.websocket.onmessage = null
|
|
||||||
this.websocket.close()
|
|
||||||
this.onWsClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
onWsClose() {
|
|
||||||
this.websocket = null
|
|
||||||
if (this.heartbeatTimerId) {
|
|
||||||
clearInterval(this.heartbeatTimerId)
|
|
||||||
this.heartbeatTimerId = null
|
|
||||||
}
|
|
||||||
if (this.receiveTimeoutTimerId) {
|
|
||||||
clearTimeout(this.receiveTimeoutTimerId)
|
|
||||||
this.receiveTimeoutTimerId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isDestroying) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.retryCount++
|
|
||||||
console.warn('心跳超时, 重连中', this.retryCount)
|
|
||||||
setTimeout(this.wsConnect.bind(this), 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
onWsMessage(event) {
|
|
||||||
if (!(event.data instanceof ArrayBuffer)) {
|
|
||||||
console.warn('未知的websocket消息类型,data=', event.data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = new Uint8Array(event.data)
|
|
||||||
this.parseWsMessage(data)
|
|
||||||
|
|
||||||
// 至少成功处理1条消息
|
|
||||||
this.retryCount = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
parseWsMessage(data) {
|
|
||||||
let offset = 0
|
|
||||||
let dataView = new DataView(data.buffer)
|
|
||||||
let packLen = dataView.getUint32(0)
|
|
||||||
let rawHeaderSize = dataView.getUint16(4)
|
|
||||||
// let ver = dataView.getUint16(6)
|
|
||||||
const operation = dataView.getUint32(8)
|
|
||||||
// let seqId = dataView.getUint32(12)
|
|
||||||
|
|
||||||
switch (operation) {
|
|
||||||
case OP_AUTH_REPLY:
|
|
||||||
case OP_SEND_MSG_REPLY: {
|
|
||||||
// 业务消息,可能有多个包一起发,需要分包
|
|
||||||
while (true) {
|
|
||||||
// eslint-disable-line no-constant-condition
|
|
||||||
const body = new Uint8Array(data.buffer, offset + rawHeaderSize, packLen - rawHeaderSize)
|
|
||||||
this.parseBusinessMessage(dataView, body)
|
|
||||||
|
|
||||||
offset += packLen
|
|
||||||
if (offset >= data.byteLength) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
dataView = new DataView(data.buffer, offset)
|
|
||||||
packLen = dataView.getUint32(0)
|
|
||||||
rawHeaderSize = dataView.getUint16(4)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case OP_HEARTBEAT_REPLY: {
|
|
||||||
// 服务器心跳包,包含人气值,这里没用
|
|
||||||
this.refreshReceiveTimeoutTimer()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
// 未知消息
|
|
||||||
const body = new Uint8Array(data.buffer, offset + rawHeaderSize, packLen - rawHeaderSize)
|
|
||||||
console.warn('未知包类型,operation=', operation, dataView, body)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseBusinessMessage(dataView, body) {
|
|
||||||
const ver = dataView.getUint16(6)
|
|
||||||
const operation = dataView.getUint32(8)
|
|
||||||
|
|
||||||
switch (operation) {
|
|
||||||
case OP_SEND_MSG_REPLY: {
|
|
||||||
// 业务消息
|
|
||||||
if (ver == WS_BODY_PROTOCOL_VERSION_BROTLI) {
|
|
||||||
// 压缩过的先解压
|
|
||||||
body = BrotliDecode(body)
|
|
||||||
this.parseWsMessage(body)
|
|
||||||
} /*else if (ver == WS_BODY_PROTOCOL_VERSION_DEFLATE) {
|
|
||||||
// web端已经不用zlib压缩了,但是开放平台会用
|
|
||||||
body = inflate(body)
|
|
||||||
this.parseWsMessage(body)
|
|
||||||
}*/ else {
|
|
||||||
// 没压缩过的直接反序列化
|
|
||||||
if (body.length !== 0) {
|
|
||||||
try {
|
|
||||||
const text = textDecoder.decode(body)
|
|
||||||
this.onRawMessage(text)
|
|
||||||
this.CMD_CALLBACK_MAP['RAW_MESSAGE']?.call(this, text)
|
|
||||||
body = JSON.parse(text)
|
|
||||||
this.handlerCommand(body)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('body=', body)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case OP_AUTH_REPLY: {
|
|
||||||
// 认证响应
|
|
||||||
body = JSON.parse(textDecoder.decode(body))
|
|
||||||
if (body.code !== AUTH_REPLY_CODE_OK) {
|
|
||||||
console.error('认证响应错误,body=', body)
|
|
||||||
this.needInitRoom = true
|
|
||||||
this.discardWebsocket()
|
|
||||||
throw new Error('认证响应错误')
|
|
||||||
}
|
|
||||||
this.sendHeartbeat()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
// 未知消息
|
|
||||||
console.warn('未知包类型,operation=', operation, dataView, body)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onRawMessage(command) {}
|
|
||||||
|
|
||||||
handlerCommand(command) {
|
|
||||||
let cmd = command.cmd || ''
|
|
||||||
const pos = cmd.indexOf(':')
|
|
||||||
if (pos != -1) {
|
|
||||||
cmd = cmd.substr(0, pos)
|
|
||||||
}
|
|
||||||
const callback = this.CMD_CALLBACK_MAP[cmd]
|
|
||||||
if (callback) {
|
|
||||||
callback.call(this, command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { GetSelfAccount, useAccount } from '@/api/account'
|
|
||||||
import { QueryGetAPI } from '@/api/query'
|
|
||||||
import { BILI_API_URL } from '@/data/constants'
|
|
||||||
import {
|
|
||||||
NAlert,
|
|
||||||
NButton,
|
|
||||||
NCard,
|
|
||||||
NCountdown,
|
|
||||||
NInput,
|
|
||||||
NInputGroup,
|
|
||||||
NInputNumber,
|
|
||||||
NSpace,
|
|
||||||
NSpin,
|
|
||||||
NText,
|
|
||||||
useMessage,
|
|
||||||
} from 'naive-ui'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const accountInfo = useAccount()
|
|
||||||
const isStart = ref(false)
|
|
||||||
const timeLeft = ref(0)
|
|
||||||
const timeOut = ref(false)
|
|
||||||
|
|
||||||
const uId = ref()
|
|
||||||
const roomId = ref()
|
|
||||||
const timer = ref()
|
|
||||||
|
|
||||||
function onStartVerify() {
|
|
||||||
QueryGetAPI(BILI_API_URL + 'verify', {
|
|
||||||
uId: uId.value,
|
|
||||||
}).then((data) => {
|
|
||||||
if (data.code == 200) {
|
|
||||||
message.info('已开始认证流程, 请前往直播间发送认证码')
|
|
||||||
checkStatus()
|
|
||||||
isStart.value = true
|
|
||||||
timer.value = setInterval(checkStatus, 2500)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
async function checkStatus() {
|
|
||||||
const data = await QueryGetAPI<{
|
|
||||||
uId: number
|
|
||||||
roomId: number
|
|
||||||
endTime: number
|
|
||||||
}>(BILI_API_URL + 'status')
|
|
||||||
if (data.code == 200) {
|
|
||||||
//正在进行认证
|
|
||||||
roomId.value ??= data.data.roomId
|
|
||||||
timeLeft.value = data.data.endTime
|
|
||||||
return true
|
|
||||||
} else if (data.code == 201) {
|
|
||||||
clearInterval(timer.value)
|
|
||||||
message.success('认证成功')
|
|
||||||
setTimeout(() => {
|
|
||||||
GetSelfAccount()
|
|
||||||
}, 1)
|
|
||||||
return true
|
|
||||||
} else if (data.code == 400 && isStart.value) {
|
|
||||||
timeOut.value = true
|
|
||||||
clearInterval(timer.value)
|
|
||||||
message.error('认证超时')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
function copyCode() {
|
|
||||||
if (navigator.clipboard) {
|
|
||||||
navigator.clipboard.writeText(accountInfo.value?.biliVerifyCode ?? '')
|
|
||||||
message.success('已复制认证码到剪切板')
|
|
||||||
} else {
|
|
||||||
message.warning('当前环境不支持自动复制, 请手动选择并复制')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (accountInfo.value && !accountInfo.value.isBiliVerified) {
|
|
||||||
if (await checkStatus()) {
|
|
||||||
isStart.value = true
|
|
||||||
timer.value = setInterval(checkStatus, 5000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NAlert
|
|
||||||
v-if="accountInfo?.isBiliVerified"
|
|
||||||
type="success"
|
|
||||||
>
|
|
||||||
你已通过验证
|
|
||||||
</NAlert>
|
|
||||||
<NAlert v-else-if="!accountInfo">
|
|
||||||
尚未登录
|
|
||||||
</NAlert>
|
|
||||||
<NCard
|
|
||||||
v-else
|
|
||||||
embedded
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
Bilibili 身份验证
|
|
||||||
</template>
|
|
||||||
<template v-if="isStart">
|
|
||||||
<NSpace
|
|
||||||
vertical
|
|
||||||
justify="center"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<template v-if="!timeOut">
|
|
||||||
<NSpin />
|
|
||||||
<span> 剩余 <NCountdown :duration="timeLeft - Date.now()" /> </span>
|
|
||||||
</template>
|
|
||||||
<NAlert
|
|
||||||
v-else
|
|
||||||
type="error"
|
|
||||||
>
|
|
||||||
认证超时
|
|
||||||
<NButton
|
|
||||||
type="info"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
isStart = false
|
|
||||||
timeOut = false
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
重新开始
|
|
||||||
</NButton>
|
|
||||||
</NAlert>
|
|
||||||
<NInputGroup>
|
|
||||||
<NInput
|
|
||||||
v-model:value="accountInfo.biliVerifyCode"
|
|
||||||
:allow-input="() => false"
|
|
||||||
/>
|
|
||||||
<NButton @click="copyCode">
|
|
||||||
复制认证码
|
|
||||||
</NButton>
|
|
||||||
</NInputGroup>
|
|
||||||
<NButton
|
|
||||||
v-if="roomId"
|
|
||||||
type="primary"
|
|
||||||
tag="a"
|
|
||||||
:href="'https://live.bilibili.com/' + roomId"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
前往直播间
|
|
||||||
</NButton>
|
|
||||||
</NSpace>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<NSpace
|
|
||||||
vertical
|
|
||||||
justify="center"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<NAlert type="info">
|
|
||||||
<NText>
|
|
||||||
请在点击
|
|
||||||
<NText
|
|
||||||
type="primary"
|
|
||||||
strong
|
|
||||||
>
|
|
||||||
开始认证
|
|
||||||
</NText>
|
|
||||||
后2分钟之内使用
|
|
||||||
<NText
|
|
||||||
strong
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
需要认证的账户
|
|
||||||
</NText>
|
|
||||||
在自己的直播间内发送
|
|
||||||
<NButton
|
|
||||||
type="info"
|
|
||||||
text
|
|
||||||
@click="copyCode"
|
|
||||||
>
|
|
||||||
{{ accountInfo?.biliVerifyCode }}
|
|
||||||
</NButton>
|
|
||||||
</NText>
|
|
||||||
</NAlert>
|
|
||||||
<NInputNumber
|
|
||||||
v-model:value="uId"
|
|
||||||
size="small"
|
|
||||||
placeholder="输入用户UId"
|
|
||||||
:min="1"
|
|
||||||
:show-button="false"
|
|
||||||
/>
|
|
||||||
<NButton
|
|
||||||
size="large"
|
|
||||||
type="primary"
|
|
||||||
@click="onStartVerify"
|
|
||||||
>
|
|
||||||
开始认证
|
|
||||||
</NButton>
|
|
||||||
</NSpace>
|
|
||||||
</template>
|
|
||||||
</NCard>
|
|
||||||
</template>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useAccount } from '@/api/account'
|
|
||||||
import { useDanmakuClient } from '@/store/useDanmakuClient'
|
|
||||||
import { NAlert } from 'naive-ui'
|
|
||||||
import OpenLottery from '../open_live/OpenLottery.vue'
|
|
||||||
|
|
||||||
const accountInfo = useAccount()
|
|
||||||
const client = await useDanmakuClient().initOpenlive()
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NAlert
|
|
||||||
v-if="accountInfo?.isBiliVerified != true"
|
|
||||||
type="info"
|
|
||||||
>
|
|
||||||
尚未进行Bilibili认证
|
|
||||||
</NAlert>
|
|
||||||
<OpenLottery
|
|
||||||
v-else
|
|
||||||
:room-info="client.authInfo!"
|
|
||||||
:code="accountInfo?.biliAuthCode"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useAccount } from '@/api/account'
|
|
||||||
import { useDanmakuClient } from '@/store/useDanmakuClient'
|
|
||||||
import { NAlert } from 'naive-ui'
|
|
||||||
import MusicRequest from '../open_live/MusicRequest.vue'
|
|
||||||
|
|
||||||
const accountInfo = useAccount()
|
|
||||||
const client = await useDanmakuClient().initOpenlive()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NAlert
|
|
||||||
v-if="accountInfo?.isBiliVerified != true"
|
|
||||||
type="info"
|
|
||||||
>
|
|
||||||
尚未进行Bilibili认证
|
|
||||||
</NAlert>
|
|
||||||
<MusicRequest
|
|
||||||
v-else
|
|
||||||
:client="client"
|
|
||||||
:room-info="client.authInfo!"
|
|
||||||
:code="accountInfo?.biliAuthCode"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
// 组件属性定义
|
|
||||||
const props = defineProps<{
|
|
||||||
// 可以根据需要添加属性
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 事件定义
|
|
||||||
const emit = defineEmits<{
|
|
||||||
// 可以根据需要添加事件
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 组件挂载时的初始化
|
|
||||||
onMounted(() => {
|
|
||||||
// 初始化逻辑
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="point-sub-item-manage">
|
|
||||||
<!-- 组件内容 -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.point-sub-item-manage {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -22,7 +22,9 @@ import {
|
|||||||
NFlex,
|
NFlex,
|
||||||
NForm,
|
NForm,
|
||||||
NFormItem,
|
NFormItem,
|
||||||
NInput, // 引入 NInput
|
NGrid,
|
||||||
|
NGi,
|
||||||
|
NInput,
|
||||||
NInputNumber,
|
NInputNumber,
|
||||||
NModal,
|
NModal,
|
||||||
NSelect,
|
NSelect,
|
||||||
@@ -556,42 +558,50 @@ onMounted(async () => {
|
|||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
</NEmpty>
|
</NEmpty>
|
||||||
<div
|
<NGrid
|
||||||
v-else
|
v-else
|
||||||
class="goods-grid"
|
cols="1 500:2 750:3 1000:4 1300:5"
|
||||||
|
:x-gap="12"
|
||||||
|
:y-gap="12"
|
||||||
|
class="goods-list"
|
||||||
|
style="justify-items: center;"
|
||||||
>
|
>
|
||||||
<PointGoodsItem
|
<NGi
|
||||||
v-for="item in selectedItems"
|
v-for="item in selectedItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:goods="item"
|
style="width: 100%;"
|
||||||
content-style="max-width: 300px; min-width: 250px; height: 380px;"
|
|
||||||
class="goods-item"
|
|
||||||
:class="{ 'pinned-item': item.isPinned }"
|
|
||||||
>
|
>
|
||||||
<template #footer>
|
<PointGoodsItem
|
||||||
<NFlex
|
:goods="item"
|
||||||
justify="space-between"
|
content-style="max-width: 300px; min-width: 250px; height: 380px;"
|
||||||
align="center"
|
class="goods-item"
|
||||||
class="goods-footer"
|
:class="{ 'pinned-item': item.isPinned }"
|
||||||
>
|
>
|
||||||
<NTooltip placement="bottom">
|
<template #footer>
|
||||||
<template #trigger>
|
<NFlex
|
||||||
<NButton
|
justify="space-between"
|
||||||
:disabled="getTooltip(item) !== '开始兑换'"
|
align="center"
|
||||||
size="small"
|
class="goods-footer"
|
||||||
type="primary"
|
>
|
||||||
class="exchange-btn"
|
<NTooltip placement="bottom">
|
||||||
@click="onBuyClick(item)"
|
<template #trigger>
|
||||||
>
|
<NButton
|
||||||
{{ item.isPinned ? '🔥 兑换' : '兑换' }}
|
:disabled="getTooltip(item) !== '开始兑换'"
|
||||||
</NButton>
|
size="small"
|
||||||
</template>
|
type="primary"
|
||||||
{{ getTooltip(item) }}
|
class="exchange-btn"
|
||||||
</NTooltip>
|
@click="onBuyClick(item)"
|
||||||
</NFlex>
|
>
|
||||||
</template>
|
{{ item.isPinned ? '🔥 兑换' : '兑换' }}
|
||||||
</PointGoodsItem>
|
</NButton>
|
||||||
</div>
|
</template>
|
||||||
|
{{ getTooltip(item) }}
|
||||||
|
</NTooltip>
|
||||||
|
</NFlex>
|
||||||
|
</template>
|
||||||
|
</PointGoodsItem>
|
||||||
|
</NGi>
|
||||||
|
</NGrid>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
|
|
||||||
<!-- 兑换确认模态框 -->
|
<!-- 兑换确认模态框 -->
|
||||||
@@ -712,7 +722,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.point-goods-container {
|
.point-goods-container {
|
||||||
max-width: 1200px;
|
max-width: 1300px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
@@ -799,11 +809,9 @@ onMounted(async () => {
|
|||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-grid {
|
.goods-list {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
justify-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-item {
|
.goods-item {
|
||||||
@@ -815,6 +823,7 @@ onMounted(async () => {
|
|||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-item:hover {
|
.goods-item:hover {
|
||||||
@@ -943,10 +952,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.goods-grid {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-text {
|
.price-text {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
function TestVineComponent() {
|
|
||||||
return vine`
|
|
||||||
<div>
|
|
||||||
<h1>Test Vine</h1>
|
|
||||||
<p>This is a test vine component.</p>
|
|
||||||
<p>Vine is a new way to build web applications.</p>
|
|
||||||
<p>Enjoy building with Vine!</p>
|
|
||||||
<footer>Footer content goes here.</footer>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TestVineComponent
|
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="checkin-ranking-view">
|
<div class="checkin-ranking-view">
|
||||||
<NSpace vertical>
|
<NSpace vertical>
|
||||||
<NCard title="签到排行榜">
|
<NCard
|
||||||
|
class="ranking-card"
|
||||||
|
title="签到排行榜"
|
||||||
|
>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<NSpace>
|
<NSpace
|
||||||
|
:wrap="true"
|
||||||
|
:size="8"
|
||||||
|
>
|
||||||
<NSelect
|
<NSelect
|
||||||
v-model:value="timeRange"
|
v-model:value="timeRange"
|
||||||
style="width: 180px"
|
style="min-width: 120px; width: auto"
|
||||||
:options="timeRangeOptions"
|
:options="timeRangeOptions"
|
||||||
@update:value="loadCheckInRanking"
|
@update:value="loadCheckInRanking"
|
||||||
/>
|
/>
|
||||||
@@ -14,7 +20,7 @@
|
|||||||
v-model:value="userFilter"
|
v-model:value="userFilter"
|
||||||
placeholder="搜索用户"
|
placeholder="搜索用户"
|
||||||
clearable
|
clearable
|
||||||
style="width: 150px"
|
style="min-width: 120px; width: auto"
|
||||||
/>
|
/>
|
||||||
<NButton
|
<NButton
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -38,125 +44,129 @@
|
|||||||
<!-- 自定义排行榜表格 -->
|
<!-- 自定义排行榜表格 -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="custom-ranking-table"
|
class="ranking-table-wrapper"
|
||||||
>
|
>
|
||||||
<!-- 排行榜头部 -->
|
<div
|
||||||
<div class="ranking-header">
|
class="custom-ranking-table"
|
||||||
<div class="ranking-row">
|
>
|
||||||
<div class="col-rank">
|
<!-- 排行榜头部 -->
|
||||||
排名
|
<div class="ranking-header">
|
||||||
</div>
|
<div class="ranking-row">
|
||||||
<div class="col-user">
|
<div class="col-rank">
|
||||||
用户
|
排名
|
||||||
</div>
|
</div>
|
||||||
<div class="col-days">
|
<div class="col-user">
|
||||||
连续签到
|
用户
|
||||||
</div>
|
</div>
|
||||||
<div class="col-monthly">
|
<div class="col-days">
|
||||||
本月签到
|
连续签到
|
||||||
</div>
|
</div>
|
||||||
<div class="col-total">
|
<div class="col-monthly">
|
||||||
总签到
|
本月签到
|
||||||
</div>
|
</div>
|
||||||
<div class="col-time">
|
<div class="col-total">
|
||||||
最近签到时间
|
总签到
|
||||||
|
</div>
|
||||||
|
<div class="col-time">
|
||||||
|
最近签到时间
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 排行榜内容 -->
|
<!-- 排行榜内容 -->
|
||||||
<div class="ranking-body">
|
<div class="ranking-body">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in pagedData"
|
v-for="(item, index) in pagedData"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="ranking-row"
|
class="ranking-row"
|
||||||
:class="{'top-three': index < 3}"
|
:class="{'top-three': index < 3}"
|
||||||
>
|
>
|
||||||
<!-- 排名列 -->
|
<!-- 排名列 -->
|
||||||
<div class="col-rank">
|
<div class="col-rank">
|
||||||
<div
|
<div
|
||||||
class="rank-number"
|
class="rank-number"
|
||||||
:class="{
|
:class="{
|
||||||
'rank-1': index === 0,
|
'rank-1': index === 0,
|
||||||
'rank-2': index === 1,
|
'rank-2': index === 1,
|
||||||
'rank-3': index === 2
|
'rank-3': index === 2
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ index + 1 + (pagination.page - 1) * pagination.pageSize }}
|
{{ index + 1 + (pagination.page - 1) * pagination.pageSize }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 用户列 -->
|
<!-- 用户列 -->
|
||||||
<div class="col-user">
|
<div class="col-user">
|
||||||
<div class="user-name">
|
<div class="user-name">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="item.isAuthed"
|
||||||
|
class="user-authed"
|
||||||
|
>
|
||||||
|
已认证
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="item.isAuthed"
|
|
||||||
class="user-authed"
|
|
||||||
>
|
|
||||||
已认证
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 连续签到列 -->
|
<!-- 连续签到列 -->
|
||||||
<div class="col-days">
|
<div class="col-days">
|
||||||
<div class="days-count">
|
<div class="days-count">
|
||||||
{{ item.consecutiveDays }}
|
{{ item.consecutiveDays }}
|
||||||
|
</div>
|
||||||
|
<div class="days-text">
|
||||||
|
天
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="days-text">
|
|
||||||
天
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 本月签到列 -->
|
<!-- 本月签到列 -->
|
||||||
<div class="col-monthly">
|
<div class="col-monthly">
|
||||||
<div class="count-value">
|
<div class="count-value">
|
||||||
{{ item.monthlyCheckInCount || 0 }}
|
{{ item.monthlyCheckInCount || 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="count-text">
|
||||||
|
次
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="count-text">
|
|
||||||
次
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 总签到列 -->
|
<!-- 总签到列 -->
|
||||||
<div class="col-total">
|
<div class="col-total">
|
||||||
<div class="count-value">
|
<div class="count-value">
|
||||||
{{ item.totalCheckInCount || 0 }}
|
{{ item.totalCheckInCount || 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="count-text">
|
||||||
|
次
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="count-text">
|
|
||||||
次
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 签到时间列 -->
|
<!-- 签到时间列 -->
|
||||||
<div class="col-time">
|
<div class="col-time">
|
||||||
<NTooltip>
|
<NTooltip>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NTime
|
<NTime
|
||||||
:time="item.lastCheckInTime"
|
:time="item.lastCheckInTime"
|
||||||
type="relative"
|
type="relative"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<NTime
|
<NTime
|
||||||
:time="item.lastCheckInTime"
|
:time="item.lastCheckInTime"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页控制 -->
|
<!-- 分页控制 -->
|
||||||
<div class="ranking-footer">
|
<div class="ranking-footer">
|
||||||
<NPagination
|
<NPagination
|
||||||
v-model:page="pagination.page"
|
v-model:page="pagination.page"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
:item-count="filteredRankingData.length"
|
:item-count="filteredRankingData.length"
|
||||||
:page-sizes="[10, 20, 50]"
|
:page-sizes="[10, 20, 50]"
|
||||||
show-size-picker
|
show-size-picker
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
@@ -353,29 +363,27 @@ onMounted(() => {
|
|||||||
.custom-ranking-table {
|
.custom-ranking-table {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* 使用官方阴影变量 */
|
|
||||||
box-shadow: var(--box-shadow-1);
|
box-shadow: var(--box-shadow-1);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ranking-header {
|
.ranking-header {
|
||||||
/* 使用官方背景色变量 */
|
|
||||||
background-color: var(--table-header-color);
|
background-color: var(--table-header-color);
|
||||||
font-weight: var(--font-weight-strong);
|
font-weight: var(--font-weight-strong);
|
||||||
color: var(--text-color-2);
|
color: var(--text-color-2);
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ranking-row {
|
.ranking-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
/* 使用官方分割线变量 */
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
transition: background-color 0.3s var(--cubic-bezier-ease-in-out);
|
transition: background-color 0.3s var(--cubic-bezier-ease-in-out);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ranking-body .ranking-row:hover {
|
.ranking-body .ranking-row:hover {
|
||||||
/* 使用官方悬停背景色变量 */
|
|
||||||
background-color: var(--hover-color);
|
background-color: var(--hover-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +392,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.top-three {
|
.top-three {
|
||||||
/* 使用官方条纹背景色变量 */
|
|
||||||
background-color: var(--table-color-striped);
|
background-color: var(--table-color-striped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,12 +430,10 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-weight: var(--font-weight-strong);
|
font-weight: var(--font-weight-strong);
|
||||||
/* 使用官方文本和背景色变量 */
|
|
||||||
color: var(--text-color-2);
|
color: var(--text-color-2);
|
||||||
background-color: var(--action-color);
|
background-color: var(--action-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 保持奖牌颜色在暗色模式下也清晰可见 */
|
|
||||||
.rank-1 {
|
.rank-1 {
|
||||||
background: linear-gradient(135deg, #ffe259, #ffa751);
|
background: linear-gradient(135deg, #ffe259, #ffa751);
|
||||||
color: white !important;
|
color: white !important;
|
||||||
@@ -475,7 +480,64 @@ onMounted(() => {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/* 使用官方背景色变量 */
|
|
||||||
background-color: var(--table-header-color);
|
background-color: var(--table-header-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 增强响应式样式 */
|
||||||
|
.ranking-card :deep(.n-card-header__main) {
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ranking-card :deep(.n-card-header) {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-card :deep(.n-card-header__extra) {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-rank {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-days,
|
||||||
|
.col-monthly,
|
||||||
|
.col-total {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-time {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-ranking-table {
|
||||||
|
min-width: 550px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.col-user {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-days,
|
||||||
|
.col-monthly,
|
||||||
|
.col-total {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-time {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -821,6 +821,7 @@ onUnmounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表单卡片样式 */
|
/* 表单卡片样式 */
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { UserInfo } from '@/api/api-models'
|
|
||||||
import { TemplateConfig } from '@/data/VTsuruConfigTypes'
|
|
||||||
import { h } from 'vue'
|
|
||||||
|
|
||||||
const width = window.innerWidth
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
userInfo: UserInfo | undefined
|
|
||||||
biliInfo: any | undefined
|
|
||||||
currentData?: any
|
|
||||||
}>()
|
|
||||||
function navigate(url: string) {
|
|
||||||
window.open(url, '_blank')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export type ConfigType = {
|
|
||||||
cover?: string
|
|
||||||
}
|
|
||||||
export const Config: TemplateConfig<ConfigType> = {
|
|
||||||
name: 'Template.Index.Simple',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: '封面',
|
|
||||||
type: 'image',
|
|
||||||
imageLimit: 1,
|
|
||||||
key: 'cover',
|
|
||||||
onUploaded: (url, config) => {
|
|
||||||
config.cover = url[0]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'test',
|
|
||||||
key: 'test',
|
|
||||||
type: 'render',
|
|
||||||
render: (config) => h('div', '1'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>1</template>
|
|
||||||
Reference in New Issue
Block a user