联动,定时任务
9
.env.development
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# iOS 控制服务
|
||||||
|
VUE_APP_BASE_LOCAL=http://127.0.0.1:34567/
|
||||||
|
|
||||||
|
# 业务后端(开发用内网地址)
|
||||||
|
VUE_APP_BASE_REMOTE=https://crawlclient.api.yolozs.com
|
||||||
|
# VUE_APP_BASE_REMOTE=http://192.168.1.144:8101/
|
||||||
|
|
||||||
|
# AI 服务
|
||||||
|
VUE_APP_BASE_SPECIAL=http://ai.zhukeping.com
|
||||||
8
.env.production
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# iOS 控制服务(如果生产环境也要用内网可改)
|
||||||
|
VUE_APP_BASE_LOCAL=http://127.0.0.1:34567/
|
||||||
|
|
||||||
|
# 业务后端(正式域名)
|
||||||
|
VUE_APP_BASE_REMOTE=https://crawlclient.api.yolozs.com
|
||||||
|
|
||||||
|
# AI 服务(如支持 HTTPS,最好用 https)
|
||||||
|
VUE_APP_BASE_SPECIAL=http://ai.zhukeping.com
|
||||||
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# tk-page
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
5
babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
||||||
19
jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "esnext",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
14315
package-lock.json
generated
Normal file
47
package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "tk-page",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ffmpeg/core": "^0.12.10",
|
||||||
|
"@ffmpeg/util": "^0.12.2",
|
||||||
|
"@vueuse/core": "^13.1.0",
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
|
"element-plus": "^2.9.7",
|
||||||
|
"ffmpeg-wasm": "^1.0.1",
|
||||||
|
"h264-converter": "^0.1.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"msgpack-lite": "^0.1.26",
|
||||||
|
"pinia": "^3.0.1",
|
||||||
|
"qwebchannel": "^6.2.0",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
|
"vue": "^3.2.13",
|
||||||
|
"vue-i18n": "^11.1.8",
|
||||||
|
"vue-router": "^4.0.3",
|
||||||
|
"vuex": "^4.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~5.0.0",
|
||||||
|
"@vue/cli-plugin-router": "~5.0.0",
|
||||||
|
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||||
|
"@vue/cli-service": "~5.0.0",
|
||||||
|
"less": "^4.2.2",
|
||||||
|
"less-loader": "^12.2.0",
|
||||||
|
"postcss-preset-env": "^10.1.5",
|
||||||
|
"postcss-px-to-viewport": "^1.1.1",
|
||||||
|
"postcss-px-viewport": "^0.0.4",
|
||||||
|
"postcss-viewport-units": "^0.1.6"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead",
|
||||||
|
"not ie 11"
|
||||||
|
]
|
||||||
|
}
|
||||||
13
postcss.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// module.exports = {
|
||||||
|
// plugins: {
|
||||||
|
// 'postcss-px-to-viewport': {
|
||||||
|
// viewportWidth: 1600, // 视窗的宽度,对应设计稿宽度
|
||||||
|
// viewportHeight: 900, // 视窗的高度,对应设计稿高度
|
||||||
|
// unitPrecision: 3, // 指定 px 转换为视窗单位值的小数位数
|
||||||
|
// viewportUnit: 'vw', // 指定需要转换成的视窗单位,vw 或者 vh
|
||||||
|
// selectorBlackList: ['.ignore', '.hairlines'], // 指定不需要转换的类
|
||||||
|
// minPixelValue: 1, // 小于或等于 1 px 不转换为视窗单位
|
||||||
|
// mediaQuery: false // 允许在媒体查询中转换 px
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
32
public/index.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title>
|
||||||
|
<%= webpackConfig.name %>
|
||||||
|
</title>
|
||||||
|
<!-- <script src="qrc:///qtwebchannel/qwebchannel.js"></script> -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it
|
||||||
|
to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
/* width: 1600px;
|
||||||
|
height: 900px; */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</html>
|
||||||
72
src/App.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<router-view v-if="isRouterAlive" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
reload: this.reload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isRouterAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
reload() {
|
||||||
|
// 先将组件隐藏
|
||||||
|
this.isRouterAlive = false
|
||||||
|
|
||||||
|
// 使用nextTick确保DOM更新后再重新显示组件
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.isRouterAlive = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debounce = (fn, delay) => {
|
||||||
|
let timer
|
||||||
|
return (...args) => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
fn(...args)
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _ResizeObserver = window.ResizeObserver
|
||||||
|
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
|
||||||
|
constructor(callback) {
|
||||||
|
callback = debounce(callback, 200)
|
||||||
|
super(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* App.vue */
|
||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #vite-error-overlay { display: none !important; } 隐藏vite错误提示 */
|
||||||
|
.control-area {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
touch-action: none;
|
||||||
|
/* 禁用默认滚动 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
src/api/account.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { getRemote as getAxios, postRemote as postAxios, downFileRemote as downFile } from '@/utils/axios.js'
|
||||||
|
|
||||||
|
|
||||||
|
export function login(data) {
|
||||||
|
return postAxios({ url: '/api/user/aiChat-doLogin', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIdByName(name) {
|
||||||
|
return getAxios({ url: `/api/tenant/get-id-by-name?name=${name}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
//修改主播建联状态
|
||||||
|
export function update(data) {
|
||||||
|
return postAxios({ url: 'api/save_data/update', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取话术
|
||||||
|
export function prologue() {
|
||||||
|
return getAxios({ url: 'api/common/prologue' })
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取评论
|
||||||
|
export function comment() {
|
||||||
|
return getAxios({ url: 'api/common/comment' })
|
||||||
|
}
|
||||||
|
//获取评论
|
||||||
|
export function logout(data) {
|
||||||
|
return postAxios({ url: 'api/user/aiChat-logout', data })
|
||||||
|
}
|
||||||
7
src/api/adb.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { getAxios, postAxios, downFile } from '@/utils/axios.js'
|
||||||
|
export function getPhoneSize() {
|
||||||
|
return getAxios({ url: '/api/router/scrcpy/size' })
|
||||||
|
}
|
||||||
|
export function touchclick(data) {
|
||||||
|
return getAxios({ url: `/api/router/scrcpy/click?x=${data.x}&y=${data.y}&type=${data.type}`, })
|
||||||
|
}
|
||||||
14
src/api/chat.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { getSpecial as getAxios, postSpecial as postAxios } from '@/utils/axios.js'
|
||||||
|
|
||||||
|
|
||||||
|
export function chat(data) {
|
||||||
|
return postAxios({ url: '/chat', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function translationToChinese(data) {
|
||||||
|
return postAxios({ url: '/translationToChinese', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function translation(data) {
|
||||||
|
return postAxios({ url: '/translation', data })
|
||||||
|
}
|
||||||
74
src/api/ios.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { getLocal as getAxios, postLocal as postAxios, downFileLocal as downFile } from '@/utils/axios.js'
|
||||||
|
//获取设别列表
|
||||||
|
export function getDeviceList() {
|
||||||
|
return getAxios({ url: 'deviceList' })
|
||||||
|
}
|
||||||
|
//滑动
|
||||||
|
export function swipeAction(data) {
|
||||||
|
return postAxios({ url: 'swipeAction', data })
|
||||||
|
}
|
||||||
|
//返回主页
|
||||||
|
export function toHome(data) {
|
||||||
|
return postAxios({ url: 'toHome', data })
|
||||||
|
}
|
||||||
|
//点击
|
||||||
|
export function tapAction(data) {
|
||||||
|
return postAxios({ url: 'tapAction', data })
|
||||||
|
}
|
||||||
|
//刷视频
|
||||||
|
export function growAccount(data) {
|
||||||
|
return postAxios({ url: 'growAccount', data })
|
||||||
|
}
|
||||||
|
//停止
|
||||||
|
export function stopScript(data) {
|
||||||
|
return postAxios({ url: 'stopScript', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取app列表
|
||||||
|
export function deviceAppList(data) {
|
||||||
|
return postAxios({ url: 'deviceAppList', data })
|
||||||
|
}
|
||||||
|
//打开指定app
|
||||||
|
export function launchApp(data) {
|
||||||
|
return postAxios({ url: 'launchApp', data })
|
||||||
|
}
|
||||||
|
//看直播
|
||||||
|
export function watchLiveForGrowth(data) {
|
||||||
|
return postAxios({ url: 'watchLiveForGrowth', data })
|
||||||
|
}
|
||||||
|
//传token
|
||||||
|
export function passToken(data) {
|
||||||
|
return postAxios({ url: 'passToken', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//监测回复消息
|
||||||
|
export function monitorMessages(data) {
|
||||||
|
return postAxios({ url: 'replyMessages', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//关注主播
|
||||||
|
export function passAnchorData(data) {
|
||||||
|
return postAxios({ url: 'passAnchorData', data })
|
||||||
|
}
|
||||||
|
//追加主播
|
||||||
|
export function addTempAnchorData(data) {
|
||||||
|
return postAxios({ url: 'addTempAnchorData', data })
|
||||||
|
}
|
||||||
|
//获取聊天记录
|
||||||
|
export function getChatTextInfo(data) {
|
||||||
|
return postAxios({ url: 'getChatTextInfo', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//上传日志
|
||||||
|
export function setLoginInfo(data) {
|
||||||
|
return postAxios({ url: 'setLoginInfo', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取主播列表
|
||||||
|
export function anchorList(data) {
|
||||||
|
return postAxios({ url: 'anchorList', data })
|
||||||
|
}
|
||||||
|
//设置主播列表
|
||||||
|
export function deleteAnchorWithIds(data) {
|
||||||
|
return postAxios({ url: 'deleteAnchorWithIds', data })
|
||||||
|
}
|
||||||
BIN
src/assets/Back.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/Home.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/Overview.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/filter.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src/assets/list.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/listAction.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
src/assets/logo1.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/assets/logoBg.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/logoBg1.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
src/assets/logotext.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
src/assets/logotext1.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/logotext12.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/assets/navAction.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/open.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/assets/password.png
Normal file
|
After Width: | Height: | Size: 806 B |
BIN
src/assets/username.png
Normal file
|
After Width: | Height: | Size: 945 B |
BIN
src/assets/video/chatMes.png
Normal file
|
After Width: | Height: | Size: 932 B |
BIN
src/assets/video/leftBg.png
Normal file
|
After Width: | Height: | Size: 325 KiB |
BIN
src/assets/video/leftBtn1-1.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/video/leftBtn1.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/video/leftBtn2-2.png
Normal file
|
After Width: | Height: | Size: 525 B |
BIN
src/assets/video/leftBtn2.png
Normal file
|
After Width: | Height: | Size: 680 B |
BIN
src/assets/video/leftBtn3-3.png
Normal file
|
After Width: | Height: | Size: 811 B |
BIN
src/assets/video/leftBtn3.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/video/leftBtn4-4.png
Normal file
|
After Width: | Height: | Size: 720 B |
BIN
src/assets/video/leftBtn4.png
Normal file
|
After Width: | Height: | Size: 902 B |
BIN
src/assets/video/leftBtn5-5.png
Normal file
|
After Width: | Height: | Size: 596 B |
BIN
src/assets/video/leftBtn5.png
Normal file
|
After Width: | Height: | Size: 754 B |
BIN
src/assets/video/leftBtn6-6.png
Normal file
|
After Width: | Height: | Size: 799 B |
BIN
src/assets/video/leftBtn6.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/video/leftBtn7-7.png
Normal file
|
After Width: | Height: | Size: 581 B |
BIN
src/assets/video/leftBtn7.png
Normal file
|
After Width: | Height: | Size: 728 B |
BIN
src/assets/video/leftBtn8-8.png
Normal file
|
After Width: | Height: | Size: 714 B |
BIN
src/assets/video/leftBtn8.png
Normal file
|
After Width: | Height: | Size: 900 B |
BIN
src/assets/video/leftBtn9-9.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/video/leftBtn9.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/video/mainBg.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/assets/work.png
Normal file
|
After Width: | Height: | Size: 993 B |
BIN
src/assets/workAction.png
Normal file
|
After Width: | Height: | Size: 993 B |
BIN
src/assets/worklogo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
138
src/components/ChatDialog.vue
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="dialog-overlay" @click.self="close">
|
||||||
|
<div class="dialog-content">
|
||||||
|
<h3 class="text-lg font-bold mb-4">
|
||||||
|
<img style="margin: 0px 15px;" src="@/assets/video/chatMes.png"></img>
|
||||||
|
消息内容
|
||||||
|
</h3>
|
||||||
|
<el-scrollbar class="chat-box">
|
||||||
|
<div v-for="(msg, index) in messages.filter(m => m.type !== 'time')" :key="index"
|
||||||
|
:class="msg.dir === 'in' ? 'left-message' : 'right-message'">
|
||||||
|
<div @click="fallbackCopyTextToClipboard(index, msg.text)"
|
||||||
|
:class="['bubble', msg.dir, { 'active': activeIndex === index }]">{{ msg.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: Boolean,
|
||||||
|
messages: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const close = () => emit('close')
|
||||||
|
|
||||||
|
let activeIndex = ref(null);
|
||||||
|
|
||||||
|
// 兜底方案:传统复制方法
|
||||||
|
function fallbackCopyTextToClipboard(index, text) {
|
||||||
|
activeIndex.value = index;
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-999999px';
|
||||||
|
textArea.style.top = '-999999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const successful = document.execCommand('copy');
|
||||||
|
if (successful) {
|
||||||
|
ElMessage.success('复制成功');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('复制失败1');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('复制失败2');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-overlay {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
background: rgb(246, 246, 246);
|
||||||
|
padding: 0px 20px 20px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-message {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-message {
|
||||||
|
align-self: flex-end;
|
||||||
|
text-align: right;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 18px;
|
||||||
|
max-width: 70%;
|
||||||
|
display: inline-block;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble.active {
|
||||||
|
box-shadow: 0 0 15px 3px rgba(74, 144, 226, 0.7);
|
||||||
|
border: 1px solid #4a90e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble.in {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble.out {
|
||||||
|
background-color: rgb(0, 169, 214);
|
||||||
|
color: white;
|
||||||
|
text-align: left;
|
||||||
|
/* ✅ 关键:强制文字左对齐 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #409EFF;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-justify {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
390
src/components/HostListManagerDialog.vue
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="show" width="70vw" :close-on-click-modal="false" :destroy-on-close="true" @open="onOpen">
|
||||||
|
<template #header>
|
||||||
|
<div class="dlg-title">
|
||||||
|
<span>主播管理</span>
|
||||||
|
<span class="muted">(已选 {{ selectedCount }} / 共 {{ hosts.length }})</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button size="small" @click="selectAll">全选</el-button>
|
||||||
|
<el-button size="small" @click="selectNone">全不选</el-button>
|
||||||
|
<el-button size="small" @click="invertSelect">反选</el-button>
|
||||||
|
<el-button size="small" type="danger" :disabled="!selectedCount" @click="deleteSelected">删除选中</el-button>
|
||||||
|
|
||||||
|
<!-- ✅ 新增:一键删除已处理 -->
|
||||||
|
<!-- <el-button size="small" type="warning" :disabled="!processedCount" @click="deleteProcessed">
|
||||||
|
删除已处理
|
||||||
|
</el-button> -->
|
||||||
|
|
||||||
|
|
||||||
|
<el-tooltip placement="bottom" effect="dark">
|
||||||
|
<template #content>
|
||||||
|
在空白区域按下左键拖拽进行框选<br />
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<el-icon class="hint">i</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 列表区域 -->
|
||||||
|
<div ref="gridRef" class="grid" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp"
|
||||||
|
@mouseleave="onMouseUp" @scroll="recalcRectsSoon">
|
||||||
|
<div v-for="it in hosts" :key="it.anchorId" class="item-card" :class="{ selected: isSelected(it.anchorId) }"
|
||||||
|
:ref="el => setCardRef(it.anchorId, el)" @click.stop="toggleSelect(it.anchorId)">
|
||||||
|
<div class="row top">
|
||||||
|
<span class="id" :title="it.anchorId">{{ it.anchorId }}</span>
|
||||||
|
<button class="x" title="删除此项" @click.stop="deleteOne(it.anchorId)">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="row meta">
|
||||||
|
<span class="country" :title="it.country">{{ it.country || '—' }}</span>
|
||||||
|
<span class="state" :class="{ done: !!it.state }">{{ it.state ? '已处理' : '未处理' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 框选矩形 -->
|
||||||
|
<div v-if="selecting" class="selection-rect" :style="selectionStyle"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="foot">
|
||||||
|
<el-button @click="show = false">关闭</el-button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getHostList, setHostList } from '@/stores/storage'
|
||||||
|
import { anchorList, deleteAnchorWithIds } from '@/api/ios'
|
||||||
|
// v-model:visible 接口
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'save'])
|
||||||
|
const show = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (v) => emit('update:visible', v)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
const hosts = ref([]) // {country, text, state}
|
||||||
|
const selected = reactive(new Set()) // 选中的 text 集合
|
||||||
|
|
||||||
|
// 卡片 DOM 引用与位置缓存
|
||||||
|
const gridRef = ref(null)
|
||||||
|
const cardRefs = reactive({}) // text -> el
|
||||||
|
const rectCache = reactive({}) // text -> DOMRect
|
||||||
|
let rectRecalcTimer = null
|
||||||
|
|
||||||
|
|
||||||
|
const processedCount = computed(() => hosts.value.filter(it => !!it?.state).length)
|
||||||
|
function setCardRef(key, el) {
|
||||||
|
if (el) {
|
||||||
|
cardRefs[key] = el
|
||||||
|
} else {
|
||||||
|
delete cardRefs[key]
|
||||||
|
delete rectCache[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStoredHostList() {
|
||||||
|
const v = await anchorList()
|
||||||
|
console.log("v", v)
|
||||||
|
return v ? v : []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onOpen() {
|
||||||
|
hosts.value = await getStoredHostList()
|
||||||
|
selected.clear()
|
||||||
|
await nextTick()
|
||||||
|
recalcRects()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 选择相关
|
||||||
|
const selectedCount = computed(() => selected.size)
|
||||||
|
function isSelected(id) { return selected.has(id) }
|
||||||
|
function toggleSelect(id) {
|
||||||
|
if (selected.has(id)) selected.delete(id)
|
||||||
|
else selected.add(id)
|
||||||
|
}
|
||||||
|
function selectAll() { selected.clear(); hosts.value.forEach(it => selected.add(it.anchorId)) }
|
||||||
|
function selectNone() { selected.clear() }
|
||||||
|
function invertSelect() {
|
||||||
|
const next = new Set()
|
||||||
|
hosts.value.forEach(it => { if (!selected.has(it.anchorId)) next.add(it.anchorId) })
|
||||||
|
selected.clear(); next.forEach(id => selected.add(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelected() {
|
||||||
|
if (!selected.size) return
|
||||||
|
ElMessageBox.confirm(`确认删除选中的 ${selected.size} 项吗?`, '提示', { type: 'warning' })
|
||||||
|
.then(() => {
|
||||||
|
|
||||||
|
const keep = []
|
||||||
|
const selectHost = []
|
||||||
|
console.log("selected", selected)
|
||||||
|
|
||||||
|
for (const it of hosts.value) {
|
||||||
|
if (selected.has(it.anchorId)) {
|
||||||
|
keep.push(it)
|
||||||
|
} else {
|
||||||
|
selectHost.push(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("keep", keep)
|
||||||
|
|
||||||
|
hosts.value = selectHost
|
||||||
|
selected.clear()
|
||||||
|
deleteAnchorWithIds(keep)
|
||||||
|
recalcRectsSoon()
|
||||||
|
ElMessage.success('已删除选中')
|
||||||
|
})
|
||||||
|
.catch(() => { })
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteOne(id) {
|
||||||
|
const idx = hosts.value.findIndex(it => it.anchorId === id)
|
||||||
|
if (idx !== -1) {
|
||||||
|
hosts.value.splice(idx, 1)
|
||||||
|
selected.delete(id)
|
||||||
|
// setHostList(hosts.value)
|
||||||
|
recalcRectsSoon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// —— 框选逻辑 ——
|
||||||
|
const selecting = ref(false)
|
||||||
|
const anchor = ref({ x: 0, y: 0 })
|
||||||
|
const cursor = ref({ x: 0, y: 0 })
|
||||||
|
const baseSelection = ref(new Set()) // 框选开始时的已有选择(支持累加)
|
||||||
|
|
||||||
|
const selectionStyle = computed(() => {
|
||||||
|
const root = gridRef.value
|
||||||
|
if (!root) return {}
|
||||||
|
// 使用容器坐标系定位矩形
|
||||||
|
const box = root.getBoundingClientRect()
|
||||||
|
const x1 = Math.min(anchor.value.x, cursor.value.x) - box.left + root.scrollLeft
|
||||||
|
const y1 = Math.min(anchor.value.y, cursor.value.y) - box.top + root.scrollTop
|
||||||
|
const x2 = Math.max(anchor.value.x, cursor.value.x) - box.left + root.scrollLeft
|
||||||
|
const y2 = Math.max(anchor.value.y, cursor.value.y) - box.top + root.scrollTop
|
||||||
|
return { left: x1 + 'px', top: y1 + 'px', width: (x2 - x1) + 'px', height: (y2 - y1) + 'px' }
|
||||||
|
})
|
||||||
|
|
||||||
|
function onMouseDown(e) {
|
||||||
|
if (e.button !== 0) return
|
||||||
|
const root = gridRef.value
|
||||||
|
if (!root) return
|
||||||
|
// 只在空白处拖拽,或按着 Alt 任意处拖拽
|
||||||
|
const onItem = e.target && e.target.closest && e.target.closest('.item-card')
|
||||||
|
if (onItem && !e.altKey) return
|
||||||
|
|
||||||
|
// 记录锚点(client 坐标,方便和 DOMRect 比较)
|
||||||
|
anchor.value = { x: e.clientX, y: e.clientY }
|
||||||
|
cursor.value = { x: e.clientX, y: e.clientY }
|
||||||
|
selecting.value = true
|
||||||
|
// 是否保留原选择
|
||||||
|
baseSelection.value = (e.ctrlKey || e.metaKey) ? new Set(Array.from(selected)) : new Set()
|
||||||
|
// 防止选中文本
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e) {
|
||||||
|
if (!selecting.value) return
|
||||||
|
cursor.value = { x: e.clientX, y: e.clientY }
|
||||||
|
updateSelectionByRect()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
if (!selecting.value) return
|
||||||
|
selecting.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectionByRect() {
|
||||||
|
const x1 = Math.min(anchor.value.x, cursor.value.x)
|
||||||
|
const y1 = Math.min(anchor.value.y, cursor.value.y)
|
||||||
|
const x2 = Math.max(anchor.value.x, cursor.value.x)
|
||||||
|
const y2 = Math.max(anchor.value.y, cursor.value.y)
|
||||||
|
|
||||||
|
// 实时选择集合
|
||||||
|
const current = new Set(baseSelection.value)
|
||||||
|
for (const it of hosts.value) {
|
||||||
|
const r = rectCache[it.anchorId]
|
||||||
|
if (!r) continue
|
||||||
|
const hit = !(r.left > x2 || r.right < x1 || r.top > y2 || r.bottom < y1)
|
||||||
|
if (hit) current.add(it.anchorId)
|
||||||
|
}
|
||||||
|
// 覆盖 selected
|
||||||
|
selected.clear(); current.forEach(id => selected.add(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalcRects() {
|
||||||
|
for (const [key, el] of Object.entries(cardRefs)) {
|
||||||
|
try { rectCache[key] = el.getBoundingClientRect() } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalcRectsSoon() {
|
||||||
|
clearTimeout(rectRecalcTimer)
|
||||||
|
rectRecalcTimer = setTimeout(() => nextTick().then(recalcRects), 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function deleteProcessed() {
|
||||||
|
if (!processedCount.value) return
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确认删除所有“已处理”项(${processedCount.value} 个)吗?此操作不可撤销。`,
|
||||||
|
'提示',
|
||||||
|
{ type: 'warning' }
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
// 仅保留未处理
|
||||||
|
const keep = hosts.value.filter(it => !it?.state)
|
||||||
|
hosts.value = keep
|
||||||
|
|
||||||
|
// 清理已不存在的选中项
|
||||||
|
for (const id of Array.from(selected)) {
|
||||||
|
if (!keep.find(it => it.anchorId === id)) selected.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAnchorWithIds(keep) // 同步回缓存
|
||||||
|
recalcRectsSoon() // 重新计算卡片矩形,保证框选正常
|
||||||
|
ElMessage.success('已删除已处理项')
|
||||||
|
})
|
||||||
|
.catch(() => { })
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(hosts, () => nextTick().then(recalcRects))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dlg-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dlg-title .muted {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar .hint {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
position: relative;
|
||||||
|
height: 60vh;
|
||||||
|
overflow: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px;
|
||||||
|
user-select: none;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card {
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow .15s, border-color .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card.selected {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--el-color-primary) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .row.top {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .id {
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .x {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #f56c6c;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .x:hover {
|
||||||
|
color: #f33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .meta {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .state {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .state.done {
|
||||||
|
color: #67c23a;
|
||||||
|
border-color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-rect {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 1px dashed var(--el-color-primary);
|
||||||
|
background: color-mix(in srgb, var(--el-color-primary) 12%, transparent);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
101
src/components/MultiLineInputDialog.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog draggable :title="title" v-model="visibleLocal" width="600px" @closed="onClosed">
|
||||||
|
<el-input type="textarea" v-model="rawText" :rows="10" :placeholder="placeholder" />
|
||||||
|
<template #footer>
|
||||||
|
<span v-if="title === '私信'">
|
||||||
|
自动回复
|
||||||
|
<el-switch style="margin-right: 20px;" v-model="value1" />
|
||||||
|
</span>
|
||||||
|
<el-button v-if="title !== '主播ID'" type="success" @click="exportPrologue(title)">
|
||||||
|
导入{{ title }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="onClickCancel">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { prologue, comment } from '@/api/account';
|
||||||
|
|
||||||
|
let value1 = ref(false);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, required: true },
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
initialText: { type: String, default: '' },
|
||||||
|
type: { type: Number, default: 10 },
|
||||||
|
index: { type: Number, default: 999 },
|
||||||
|
placeholder: { type: String, default: '每行一条,支持粘贴多行后自动拆分' },
|
||||||
|
dedupe: { type: Boolean, default: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'confirm', 'cancel']);
|
||||||
|
|
||||||
|
const rawText = ref(props.initialText);
|
||||||
|
|
||||||
|
// 区分关闭来源:true=通过“确定”关闭;false=取消/遮罩/ESC/右上角关闭
|
||||||
|
const closingByConfirm = ref(false);
|
||||||
|
|
||||||
|
watch(() => props.initialText, (v) => { rawText.value = v; });
|
||||||
|
|
||||||
|
const visibleLocal = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
function parseLines() {
|
||||||
|
const lines = rawText.value
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return props.dedupe ? Array.from(new Set(lines)) : lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
if (!rawText.value) {
|
||||||
|
return; // 空内容直接忽略
|
||||||
|
};
|
||||||
|
const items = parseLines();
|
||||||
|
emit('confirm', items, props.title, props.index, value1.value);
|
||||||
|
closingByConfirm.value = true; // 标记:这次关闭来自“确定”
|
||||||
|
emit('update:visible', false); // 关弹窗
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickCancel() {
|
||||||
|
closingByConfirm.value = false; // 非确认关闭
|
||||||
|
emit('update:visible', false); // 关弹窗
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有关闭后的统一收尾
|
||||||
|
function onClosed() {
|
||||||
|
const byConfirm = closingByConfirm.value;
|
||||||
|
|
||||||
|
// 重置表单状态
|
||||||
|
rawText.value = '';
|
||||||
|
value1.value = false;
|
||||||
|
|
||||||
|
// 只有非“确定”关闭才对外发 cancel
|
||||||
|
if (!byConfirm) emit('cancel');
|
||||||
|
|
||||||
|
// 重置标记
|
||||||
|
closingByConfirm.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportPrologue(title) {
|
||||||
|
if (title === '评论') {
|
||||||
|
comment().then(res => {
|
||||||
|
rawText.value = res.map(item => item).join('\n\n');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
prologue().then(res => {
|
||||||
|
rawText.value = res.map(item => item).join('\n\n');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 根据需要自定义样式 */
|
||||||
|
</style>
|
||||||
24
src/main.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import store from './store'
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
|
|
||||||
|
|
||||||
|
// createApp(App).use(store).use(router).mount('#app')
|
||||||
|
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
app.use(ElementPlus) // 注册 ElementPlus
|
||||||
|
app.use(createPinia()); // 注册 Pinia
|
||||||
|
app.use(store); // 注册 store
|
||||||
|
app.use(router);
|
||||||
|
app.config.globalProperties.Buffer = Buffer; // 注册 Buffer
|
||||||
|
|
||||||
|
// window.addEventListener('unhandledrejection', event => event.preventDefault());
|
||||||
|
// window.addEventListener('error', event => event.preventDefault()); // 阻止错误和未处overlay理的 Promise 拒绝事件冒泡到 window 对象
|
||||||
|
app.mount('#app');
|
||||||
|
|
||||||
24
src/router/index.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
||||||
|
import HomeView from '../views/HomeView.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HomeView
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/Video',
|
||||||
|
name: 'Video',
|
||||||
|
component: () => import(/* webpackChunkName: "about" */ '@/views/VideoStream.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
54
src/services/websocket.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// src/services/websocket.js
|
||||||
|
let ws = null;
|
||||||
|
let reconnectTimer = null;
|
||||||
|
|
||||||
|
export const connectWebSocket = (port = 8000) => {
|
||||||
|
// 关闭已有连接
|
||||||
|
if (ws) ws.close();
|
||||||
|
|
||||||
|
// 创建新连接
|
||||||
|
ws = new WebSocket(`ws://localhost:${port}`);
|
||||||
|
|
||||||
|
// 连接事件处理
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WS connected');
|
||||||
|
clearInterval(reconnectTimer);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WS error:', error);
|
||||||
|
handleReconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (event) => {
|
||||||
|
console.log('WS closed:', event.reason);
|
||||||
|
handleReconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
console.log(12312312, event.data)
|
||||||
|
if (event.data instanceof Blob) {
|
||||||
|
// createImageBitmap(event.data).then(img => {
|
||||||
|
// ctx.drawImage(img, 0, 0);
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return ws;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自动重连机制
|
||||||
|
const handleReconnect = () => {
|
||||||
|
clearInterval(reconnectTimer);
|
||||||
|
// reconnectTimer = setInterval(() => {
|
||||||
|
// connectWebSocket(8000);
|
||||||
|
// }, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送控制指令
|
||||||
|
export const sendControl = (data) => {
|
||||||
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
};
|
||||||
4
src/static/css/app.less
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@bg-color: #022b4e; // 主色
|
||||||
|
@bg-color-light: #022b4eaf; // 浅主色
|
||||||
|
@bg-color-light-light: #022b4e1c; // 浅浅主色
|
||||||
|
@btn-bg-color: #045dac; // 黄色按钮主色
|
||||||
253
src/static/css/video.less
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
body {
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #222;
|
||||||
|
background-image: url('../../assets/video/mainBg.png');
|
||||||
|
background-size: 100% 100%;
|
||||||
|
/* 或其他适合的值,例如contain */
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
background-image: url('../../assets/video/leftBg.png');
|
||||||
|
background-size: 100% 100%;
|
||||||
|
/* 或其他适合的值,例如contain */
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
width: 20vw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: 0px 0px 10px #b3b3b3;
|
||||||
|
border-radius: 0px 40px 40px 0px;
|
||||||
|
|
||||||
|
padding: 53px 31px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 60vw;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(160px, 320px));
|
||||||
|
padding: 0 5vw;
|
||||||
|
overflow: visible;
|
||||||
|
/* 每行 3 个,宽度自适应 */
|
||||||
|
|
||||||
|
align-items: start;
|
||||||
|
/* 关键:grid items 在 block 轴不拉伸 */
|
||||||
|
justify-items: center;
|
||||||
|
/* 可选:让每个卡片水平居中 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
width: 30vw;
|
||||||
|
// background-color: #272727;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
// padding: 10px;
|
||||||
|
overflow: visible;
|
||||||
|
width: auto;
|
||||||
|
/* 取消 100%,避免换行 */
|
||||||
|
display: flex;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
align-self: start;
|
||||||
|
|
||||||
|
/* 只占自身内容高度 */
|
||||||
|
.video-canvas {
|
||||||
|
position: relative;
|
||||||
|
/* 让 overlay 绝对定位于此容器 */
|
||||||
|
line-height: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform .18s ease;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: 20px;
|
||||||
|
border: 2px solid #0400ff;
|
||||||
|
/* 默认较低 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-canvas.active {
|
||||||
|
cursor: default;
|
||||||
|
z-index: 20;
|
||||||
|
/* 足够高,盖住其它缩略卡片 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream {
|
||||||
|
display: block;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
/* 鼠标事件交给上层 canvas */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 盖层画布:与 img 同尺寸,覆盖在上方 */
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: block;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-info {
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
background-color: #424242;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 15px;
|
||||||
|
background: linear-gradient(0deg, #EEFFFF 0%, #FFFFFF 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 4px 3px 14px 2px rgba(174, 174, 174, 0.5);
|
||||||
|
margin-left: 320px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
|
/* 关键:让 video 不拦截鼠标事件 */
|
||||||
|
|
||||||
|
/* 添加动画 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
/* 上对齐 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-row {
|
||||||
|
align-items: flex-end;
|
||||||
|
/* 下对齐 */
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
/* position: relative; */
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
/* object-fit: contain; */
|
||||||
|
/* transform: scale(0.27); */
|
||||||
|
/* 缩小到原始尺寸的50% */
|
||||||
|
/* transform-origin: top left; */
|
||||||
|
/* 控制缩放原点 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 9;
|
||||||
|
/* transform: scale(0.27); */
|
||||||
|
/* 缩小到原始尺寸的50% */
|
||||||
|
/* transform-origin: top left; */
|
||||||
|
/* 控制缩放原点 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.open {
|
||||||
|
background: url(../../assets/open.png) no-repeat center center;
|
||||||
|
background-size: 60% 60%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.Back {
|
||||||
|
background: url(../../assets/Back.png) no-repeat center center;
|
||||||
|
background-size: 60% 60%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.Home {
|
||||||
|
background: url(../../assets/Home.png) no-repeat center center;
|
||||||
|
background-size: 60% 60%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.Overview {
|
||||||
|
background: url(../../assets/Overview.png) no-repeat center center;
|
||||||
|
background-size: 60% 60%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-button {
|
||||||
|
// background-color: darkcyan;
|
||||||
|
// color: white;
|
||||||
|
width: 260px;
|
||||||
|
height: 50px;
|
||||||
|
background: linear-gradient(0deg, #4FCACD 0%, #5FDBDE 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 999;
|
||||||
|
|
||||||
|
|
||||||
|
font-family: Source Han Sans SC;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
margin: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 鼠标按下时效果 */
|
||||||
|
.app-button:active {
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-button {
|
||||||
|
position: relative;
|
||||||
|
z-index: 999;
|
||||||
|
margin-bottom: 19px;
|
||||||
|
width: 100%;
|
||||||
|
height: 72px;
|
||||||
|
// background: #32C9CD;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
font-family: Source Han Sans SC;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #7A8B97;
|
||||||
|
line-height: 16px;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 30px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-button:hover {
|
||||||
|
background: #32C9CD;
|
||||||
|
color: #F9FAFE;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 鼠标按下时效果 */
|
||||||
|
.left-button:active {
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-justify {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
// justify-content: center;
|
||||||
|
}
|
||||||
14
src/store/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createStore } from 'vuex'
|
||||||
|
|
||||||
|
export default createStore({
|
||||||
|
state: {
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
},
|
||||||
|
modules: {
|
||||||
|
}
|
||||||
|
})
|
||||||
12
src/stores/notice.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const noticeStore = defineStore('noticeNum', {
|
||||||
|
state: () => {
|
||||||
|
return { data: { num: 0 } };
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
increment() {
|
||||||
|
this.data.num++;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
101
src/stores/storage.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
export function setToken(token) {
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToken() {
|
||||||
|
return localStorage.getItem('token');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeToken() {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUser(user) {
|
||||||
|
|
||||||
|
localStorage.setItem('user', JSON.stringify(user));
|
||||||
|
}
|
||||||
|
//获取用户信息
|
||||||
|
export function getUser() {
|
||||||
|
return JSON.parse(localStorage.getItem('user'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNumData(numData) {
|
||||||
|
localStorage.setItem('num', JSON.stringify(numData));
|
||||||
|
}
|
||||||
|
export function getNumData() {
|
||||||
|
return JSON.parse(localStorage.getItem('num'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出一个函数,用于设置用户密码
|
||||||
|
export function setUserPass(userdata) {
|
||||||
|
localStorage.setItem('userPass', JSON.stringify(userdata));
|
||||||
|
}
|
||||||
|
// 导出一个函数,用于获取用户密码
|
||||||
|
export function getUserPass() {
|
||||||
|
return JSON.parse(localStorage.getItem('userPass'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于设置设备坐标信息
|
||||||
|
export function setphoneXYinfo(data) {
|
||||||
|
localStorage.setItem('XYinfo', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
// 用于获取设备坐标信息
|
||||||
|
export function getphoneXYinfo() {
|
||||||
|
return JSON.parse(localStorage.getItem('XYinfo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 用于获取主播信息
|
||||||
|
export function getHostList() {
|
||||||
|
return JSON.parse(localStorage.getItem('hostList'));
|
||||||
|
}
|
||||||
|
// 用于设置主播信息
|
||||||
|
export function setHostList(data) {
|
||||||
|
localStorage.setItem('hostList', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
export function clearHostList() {
|
||||||
|
// 清空存储的数组
|
||||||
|
localStorage.setItem('hostList', JSON.stringify([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addToHostList(newItem) {
|
||||||
|
// 获取当前的数组
|
||||||
|
const currentList = JSON.parse(localStorage.getItem('hostList') || '[]');
|
||||||
|
|
||||||
|
// 向数组添加新元素
|
||||||
|
currentList.push(newItem);
|
||||||
|
|
||||||
|
// 更新存储的数组
|
||||||
|
localStorage.setItem('hostList', JSON.stringify(currentList));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 用于获取私信信息
|
||||||
|
export function getContentpriList() {
|
||||||
|
const arr = JSON.parse(localStorage.getItem('Contentpri'))
|
||||||
|
return [arr, arr, arr, arr, arr, arr, arr, arr];
|
||||||
|
}
|
||||||
|
// 用于设置私信信息
|
||||||
|
export function setContentpriList(data) {
|
||||||
|
localStorage.setItem('Contentpri', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于获取评论信息
|
||||||
|
export function getContentList() {
|
||||||
|
const arr = JSON.parse(localStorage.getItem('Content'))
|
||||||
|
return [arr, arr, arr, arr, arr, arr, arr, arr];
|
||||||
|
}
|
||||||
|
// 用于设置评论信息
|
||||||
|
export function setContentList(data) {
|
||||||
|
localStorage.setItem('Content', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
// 用于绘画信息
|
||||||
|
export function setsessionId(data) {
|
||||||
|
sessionStorage.setItem('sessionList', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于获取评论信息
|
||||||
|
export function getsessionId() {
|
||||||
|
return JSON.parse(sessionStorage.getItem('sessionList'));
|
||||||
|
}
|
||||||
17
src/utils/arrUtils
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/** 从 app 列表里找 TikTok 的 bundleId */
|
||||||
|
export function pickTikTokBundleId(apps) {
|
||||||
|
if (!Array.isArray(apps)) return null
|
||||||
|
|
||||||
|
// 1) 首选常见的 TikTok 包名
|
||||||
|
const preferred = apps.find(a => a?.bundleId === 'com.zhiliaoapp.musically')
|
||||||
|
if (preferred) return preferred.bundleId
|
||||||
|
|
||||||
|
// 2) 其次按名字匹配(大小写不敏感)
|
||||||
|
const byName = apps.find(a => String(a?.name).toLowerCase() === 'tiktok')
|
||||||
|
|| apps.find(a => String(a?.name).toLowerCase().includes('tiktok'))
|
||||||
|
if (byName) return byName.bundleId
|
||||||
|
|
||||||
|
// 3) 兜底:bundleId 里包含 musically
|
||||||
|
const fuzzy = apps.find(a => String(a?.bundleId || '').includes('musically'))
|
||||||
|
return fuzzy ? fuzzy.bundleId : null
|
||||||
|
}
|
||||||
140
src/utils/axios.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* axios请求封装(双实例)
|
||||||
|
*/
|
||||||
|
import axios from 'axios'
|
||||||
|
import router from '@/router'
|
||||||
|
import { getToken } from '@/stores/storage'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
// —— 改为从 .env 文件读取 ——
|
||||||
|
const BASE_LOCAL = process.env.VUE_APP_BASE_LOCAL
|
||||||
|
const BASE_REMOTE = process.env.VUE_APP_BASE_REMOTE
|
||||||
|
const BASE_SPECIAL = process.env.VUE_APP_BASE_SPECIAL
|
||||||
|
|
||||||
|
let isStart = true
|
||||||
|
|
||||||
|
// 公共:给某个 axios 实例挂上拦截器
|
||||||
|
function attachInterceptors(instance) {
|
||||||
|
|
||||||
|
//请求拦截器
|
||||||
|
instance.interceptors.request.use((config) => {
|
||||||
|
// 登录/换租户接口可能不需要 token,根据你的需求放行
|
||||||
|
const urlLast = sliceUrl(config.url || '')
|
||||||
|
if ((urlLast === 'prologue' || urlLast === 'comment')) {
|
||||||
|
config.headers['vvtoken'] = getToken()
|
||||||
|
}
|
||||||
|
// 超时 & 通用头
|
||||||
|
config.timeout = 60000
|
||||||
|
if (!config.headers) config.headers = {}
|
||||||
|
// 大多数 POST 走 x-www-form-urlencoded(保持你原来的行为)
|
||||||
|
// console.log(config.method)
|
||||||
|
// if (config.method == 'post') {
|
||||||
|
// config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||||
|
// }
|
||||||
|
// config.headers['Content-type'] = 'application/json'
|
||||||
|
return config
|
||||||
|
}, (error) => Promise.reject(error))
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
const data = response.data
|
||||||
|
if (data?.code === 0 || data?.code === 200) {
|
||||||
|
// 成功:返回业务数据(没有就回传原 data)
|
||||||
|
return (data?.data !== undefined) ? data.data : data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务失败:提示 + reject
|
||||||
|
const msg = `${data?.code ?? ''} ${data?.msg ?? '请求失败'}`
|
||||||
|
ElMessage.error(msg)
|
||||||
|
|
||||||
|
const err = new Error(msg)
|
||||||
|
// @ts-ignore
|
||||||
|
err.code = data?.code
|
||||||
|
// @ts-ignore
|
||||||
|
err.response = response
|
||||||
|
return Promise.reject(err)
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 网络/超时等:提示 + reject
|
||||||
|
if (error.code === 'ERR_NETWORK') {
|
||||||
|
// 你原来的 isStart 逻辑如果要保留,只控制是否弹 toast;但**不要 return**
|
||||||
|
if (!isStart) {
|
||||||
|
ElMessage.error('网络请求失败')
|
||||||
|
}
|
||||||
|
isStart = false
|
||||||
|
} else if (error.code === 'ECONNABORTED') {
|
||||||
|
ElMessage.error('请求超时')
|
||||||
|
}
|
||||||
|
return Promise.reject(error) // 关键:一定要 reject
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建两个独立的实例
|
||||||
|
const remoteAxios = axios.create({ baseURL: BASE_REMOTE })
|
||||||
|
const localAxios = axios.create({ baseURL: BASE_LOCAL })
|
||||||
|
const specialAxios = axios.create({ baseURL: BASE_SPECIAL })
|
||||||
|
|
||||||
|
attachInterceptors(remoteAxios)
|
||||||
|
attachInterceptors(localAxios)
|
||||||
|
attachInterceptors(specialAxios)
|
||||||
|
// —— 导出两套 GET/POST ——
|
||||||
|
// 远端(api.tkpage.yolozs.com)
|
||||||
|
export function getRemote({ url, params }) {
|
||||||
|
return remoteAxios.get(url, { params })
|
||||||
|
}
|
||||||
|
export function postRemote({ url, data }) {
|
||||||
|
return remoteAxios.post(url, data)
|
||||||
|
}
|
||||||
|
export async function downFileRemote(urlstr, data) {
|
||||||
|
const resp = await remoteAxios.post(urlstr, data, { responseType: 'blob' })
|
||||||
|
triggerDownload(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地(192.168.1.218:5000)
|
||||||
|
export function getLocal({ url, params }) {
|
||||||
|
return localAxios.get(url, { params })
|
||||||
|
}
|
||||||
|
export function postLocal({ url, data }) {
|
||||||
|
return localAxios.post(url, data)
|
||||||
|
}
|
||||||
|
export async function downFileLocal(urlstr, data) {
|
||||||
|
const resp = await localAxios.post(urlstr, data, { responseType: 'blob' })
|
||||||
|
triggerDownload(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ai ai翻译等功能的 GET/POST
|
||||||
|
export function getSpecial({ url, params }) {
|
||||||
|
return specialAxios.get(url, { params })
|
||||||
|
}
|
||||||
|
export function postSpecial({ url, data, headers } = {}) {
|
||||||
|
return specialAxios.post(url, data, { headers })
|
||||||
|
}
|
||||||
|
|
||||||
|
// —— 工具函数保留 ——
|
||||||
|
function triggerDownload(response) {
|
||||||
|
const contentDisposition = response.headers['content-disposition']
|
||||||
|
let fileName = 'download'
|
||||||
|
if (contentDisposition) {
|
||||||
|
const m = contentDisposition.match(/filename="(.+)"/)
|
||||||
|
if (m && m[1]) fileName = m[1]
|
||||||
|
}
|
||||||
|
const blob = new Blob([response.data], { type: response.headers['content-type'] })
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = fileName
|
||||||
|
a.click()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sliceUrl(url) {
|
||||||
|
const lastSlash = url.lastIndexOf('/')
|
||||||
|
const questionMark = url.indexOf('?')
|
||||||
|
if (questionMark === -1) return url.slice(lastSlash + 1)
|
||||||
|
return url.slice(lastSlash + 1, questionMark)
|
||||||
|
}
|
||||||
|
|
||||||
|
// export default remoteAxios // 如有需要,默认导出一个实例(可选)
|
||||||
261
src/utils/countryUtil.js
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
// country-utils.js
|
||||||
|
export const CountryCode = {
|
||||||
|
AD: "安道尔",
|
||||||
|
AE: "阿拉伯联合酋长国",
|
||||||
|
AF: "阿富汗",
|
||||||
|
AG: "安提瓜和巴布达",
|
||||||
|
AI: "安圭拉",
|
||||||
|
AL: "阿尔巴尼亚",
|
||||||
|
AM: "亚美尼亚",
|
||||||
|
AO: "安哥拉",
|
||||||
|
AQ: "南极洲",
|
||||||
|
AR: "阿根廷",
|
||||||
|
AS: "美属萨摩亚",
|
||||||
|
AT: "奥地利",
|
||||||
|
AU: "澳大利亚",
|
||||||
|
AU1: "澳大利亚",
|
||||||
|
AW: "阿鲁巴",
|
||||||
|
AX: "奥兰群岛",
|
||||||
|
AZ: "阿塞拜疆",
|
||||||
|
BA: "波斯尼亚和黑塞哥维那",
|
||||||
|
BB: "巴巴多斯",
|
||||||
|
BD: "孟加拉国",
|
||||||
|
BE: "比利时",
|
||||||
|
BF: "布基纳法索",
|
||||||
|
BG: "保加利亚",
|
||||||
|
BH: "巴林",
|
||||||
|
BI: "布隆迪",
|
||||||
|
BJ: "贝宁",
|
||||||
|
BL: "圣巴泰勒米",
|
||||||
|
BM: "百慕大群岛",
|
||||||
|
BN: "文莱达鲁萨兰国",
|
||||||
|
BO: "玻利维亚",
|
||||||
|
BQ: "博奈尔、圣尤斯特歇斯和萨巴",
|
||||||
|
BR: "巴西",
|
||||||
|
BS: "巴哈马",
|
||||||
|
BT: "不丹",
|
||||||
|
BV: "布韦岛",
|
||||||
|
BW: "博茨瓦纳",
|
||||||
|
BY: "白俄罗斯",
|
||||||
|
BZ: "伯利兹",
|
||||||
|
CA: "加拿大",
|
||||||
|
CA1: "加拿大",
|
||||||
|
CC: "科科斯(基林)群岛",
|
||||||
|
CD: "刚果民主共和国",
|
||||||
|
CF: "中非共和国",
|
||||||
|
CG: "刚果共和国",
|
||||||
|
CH: "瑞士",
|
||||||
|
CI: "科特迪瓦",
|
||||||
|
CK: "库克群岛",
|
||||||
|
CL: "智利",
|
||||||
|
CM: "喀麦隆",
|
||||||
|
CN: "中国",
|
||||||
|
CO: "哥伦比亚",
|
||||||
|
CR: "哥斯达黎加",
|
||||||
|
CU: "古巴",
|
||||||
|
CV: "佛得角",
|
||||||
|
CW: "库拉索",
|
||||||
|
CX: "圣诞岛",
|
||||||
|
CY: "塞浦路斯",
|
||||||
|
CZ: "捷克共和国",
|
||||||
|
DE: "德国",
|
||||||
|
DG: "迪戈加西亚岛",
|
||||||
|
DJ: "吉布提",
|
||||||
|
DK: "丹麦",
|
||||||
|
DM: "多米尼克",
|
||||||
|
DO: "多米尼加共和国",
|
||||||
|
DZ: "阿尔及利亚",
|
||||||
|
EC: "厄瓜多尔",
|
||||||
|
EE: "爱沙尼亚",
|
||||||
|
EG: "埃及",
|
||||||
|
EH: "西撒哈拉",
|
||||||
|
ER: "厄立特里亚",
|
||||||
|
ES: "西班牙",
|
||||||
|
ET: "埃塞俄比亚",
|
||||||
|
FI: "芬兰",
|
||||||
|
FJ: "斐济",
|
||||||
|
FK: "福克兰群岛",
|
||||||
|
FM: "密克罗尼西亚",
|
||||||
|
FO: "法罗群岛",
|
||||||
|
FR: "法国",
|
||||||
|
GA: "加蓬",
|
||||||
|
GB: "英国",
|
||||||
|
GD: "格林纳达",
|
||||||
|
GE: "格鲁吉亚",
|
||||||
|
GF: "法属圭亚那",
|
||||||
|
GG: "根西岛",
|
||||||
|
GH: "加纳",
|
||||||
|
GI: "直布罗陀",
|
||||||
|
GL: "格陵兰",
|
||||||
|
GM: "冈比亚",
|
||||||
|
GN: "几内亚",
|
||||||
|
GP: "瓜德罗普",
|
||||||
|
GQ: "赤道几内亚",
|
||||||
|
GR: "希腊",
|
||||||
|
GS: "南乔治亚和南桑德威奇群岛",
|
||||||
|
GT: "危地马拉",
|
||||||
|
GU: "关岛",
|
||||||
|
GW: "几内亚比绍",
|
||||||
|
GY: "圭亚那",
|
||||||
|
HK: "中国香港特别行政区",
|
||||||
|
HM: "赫德岛和麦克唐纳群岛",
|
||||||
|
HN: "洪都拉斯",
|
||||||
|
HR: "克罗地亚",
|
||||||
|
HT: "海地",
|
||||||
|
HU: "匈牙利",
|
||||||
|
ID: "印度尼西亚",
|
||||||
|
IE: "爱尔兰",
|
||||||
|
IL: "以色列",
|
||||||
|
IM: "马恩岛",
|
||||||
|
IN: "印度",
|
||||||
|
IO: "英属印度洋领地",
|
||||||
|
IQ: "伊拉克",
|
||||||
|
IR: "伊朗",
|
||||||
|
IS: "冰岛",
|
||||||
|
IT: "意大利",
|
||||||
|
JE: "泽西岛",
|
||||||
|
JM: "牙买加",
|
||||||
|
JO: "约旦",
|
||||||
|
JP: "日本",
|
||||||
|
JP1: "日本",
|
||||||
|
KE: "肯尼亚",
|
||||||
|
KG: "吉尔吉斯斯坦",
|
||||||
|
KH: "柬埔寨",
|
||||||
|
KI: "基里巴斯",
|
||||||
|
KM: "科摩罗",
|
||||||
|
KN: "圣基茨和尼维斯",
|
||||||
|
KP: "朝鲜",
|
||||||
|
KR: "韩国",
|
||||||
|
KR1: "韩国",
|
||||||
|
KR1_UXWAUDIT: "韩国",
|
||||||
|
KW: "科威特",
|
||||||
|
KY: "开曼群岛",
|
||||||
|
KZ: "哈萨克斯坦",
|
||||||
|
LA: "老挝",
|
||||||
|
LB: "黎巴嫩",
|
||||||
|
LC: "圣卢西亚",
|
||||||
|
LI: "列支敦士登",
|
||||||
|
LK: "斯里兰卡",
|
||||||
|
LR: "利比里亚",
|
||||||
|
LS: "莱索托",
|
||||||
|
LT: "立陶宛",
|
||||||
|
LU: "卢森堡",
|
||||||
|
LV: "拉脱维亚",
|
||||||
|
LY: "利比亚",
|
||||||
|
MA: "摩洛哥",
|
||||||
|
MC: "摩纳哥",
|
||||||
|
MD: "摩尔多瓦",
|
||||||
|
ME: "黑山",
|
||||||
|
MF: "圣马丁",
|
||||||
|
MG: "马达加斯加",
|
||||||
|
MH: "马绍尔群岛",
|
||||||
|
MK: "北马其顿",
|
||||||
|
ML: "马里",
|
||||||
|
MM: "缅甸",
|
||||||
|
MN: "蒙古",
|
||||||
|
MO: "中国澳门特别行政区",
|
||||||
|
MP: "北马里亚纳群岛",
|
||||||
|
MQ: "马提尼克",
|
||||||
|
MR: "毛里塔尼亚",
|
||||||
|
MS: "蒙特塞拉特",
|
||||||
|
MT: "马耳他",
|
||||||
|
MU: "毛里求斯",
|
||||||
|
MV: "马尔代夫",
|
||||||
|
MW: "马拉维",
|
||||||
|
MX: "墨西哥",
|
||||||
|
MY: "马来西亚",
|
||||||
|
MZ: "莫桑比克",
|
||||||
|
NA: "纳米比亚",
|
||||||
|
NC: "新喀里多尼亚",
|
||||||
|
NE: "尼日尔",
|
||||||
|
NF: "诺福克岛",
|
||||||
|
NG: "尼日利亚",
|
||||||
|
NI: "尼加拉瓜",
|
||||||
|
NL: "荷兰",
|
||||||
|
NO: "挪威",
|
||||||
|
NP: "尼泊尔",
|
||||||
|
NR: "瑙鲁",
|
||||||
|
NU: "纽埃",
|
||||||
|
NZ: "新西兰",
|
||||||
|
OM: "阿曼",
|
||||||
|
PA: "巴拿马",
|
||||||
|
PE: "秘鲁",
|
||||||
|
PF: "法属玻利尼西亚",
|
||||||
|
PG: "巴布亚新几内亚",
|
||||||
|
PH: "菲律宾",
|
||||||
|
PK: "巴基斯坦",
|
||||||
|
PL: "波兰",
|
||||||
|
PM: "圣皮埃尔和密克隆群岛",
|
||||||
|
PN: "皮特凯恩群岛",
|
||||||
|
PR: "波多黎各",
|
||||||
|
PS: "巴勒斯坦",
|
||||||
|
PT: "葡萄牙",
|
||||||
|
PW: "帕劳",
|
||||||
|
PY: "巴拉圭",
|
||||||
|
QA: "卡塔尔",
|
||||||
|
RE: "留尼汪",
|
||||||
|
RO: "罗马尼亚",
|
||||||
|
RS: "塞尔维亚",
|
||||||
|
RU: "俄罗斯",
|
||||||
|
RW: "卢旺达",
|
||||||
|
SA: "沙特阿拉伯",
|
||||||
|
SB: "索罗门群岛",
|
||||||
|
SC: "塞舌尔",
|
||||||
|
SD: "苏丹",
|
||||||
|
SE: "瑞典",
|
||||||
|
SG: "新加坡",
|
||||||
|
SI: "斯洛文尼亚",
|
||||||
|
SJ: "斯瓦尔巴和扬马延",
|
||||||
|
SK: "斯洛伐克",
|
||||||
|
SL: "塞拉利昂",
|
||||||
|
SM: "圣马利诺",
|
||||||
|
SN: "塞内加尔",
|
||||||
|
SO: "索马里",
|
||||||
|
SR: "苏里南",
|
||||||
|
SS: "南苏丹",
|
||||||
|
ST: "圣多美和普林西比",
|
||||||
|
SV: "萨尔瓦多",
|
||||||
|
SX: "荷属圣马丁",
|
||||||
|
SY: "叙利亚",
|
||||||
|
SZ: "斯威士兰",
|
||||||
|
TC: "特克斯和凯科斯群岛",
|
||||||
|
TD: "乍得",
|
||||||
|
TF: "法属南部领地",
|
||||||
|
TG: "多哥",
|
||||||
|
TH: "泰国",
|
||||||
|
TJ: "塔吉克斯坦",
|
||||||
|
TK: "托克劳群岛",
|
||||||
|
TL: "东帝汶",
|
||||||
|
TM: "土库曼斯坦",
|
||||||
|
TN: "突尼斯",
|
||||||
|
TO: "汤加",
|
||||||
|
TR: "土耳其",
|
||||||
|
TT: "特立尼达和多巴哥",
|
||||||
|
TV: "图瓦卢",
|
||||||
|
TW: "台湾",
|
||||||
|
TZ: "坦桑尼亚",
|
||||||
|
UA: "乌克兰",
|
||||||
|
UG: "乌干达",
|
||||||
|
UM: "美国本土外小岛屿",
|
||||||
|
US: "美国",
|
||||||
|
UY: "乌拉圭",
|
||||||
|
UZ: "乌兹别克斯坦",
|
||||||
|
VA: "梵蒂冈",
|
||||||
|
VC: "圣文森特",
|
||||||
|
VE: "委内瑞拉",
|
||||||
|
VG: "英属维尔京群岛",
|
||||||
|
VI: "美属维尔京群岛",
|
||||||
|
VN: "越南",
|
||||||
|
VN1: "越南",
|
||||||
|
VU: "瓦努阿图",
|
||||||
|
WS: "萨摩亚",
|
||||||
|
YE: "也门",
|
||||||
|
YT: "马约特岛",
|
||||||
|
ZA: "南非",
|
||||||
|
ZM: "赞比亚",
|
||||||
|
ZW: "津巴布韦"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getCountryName(code) {
|
||||||
|
return CountryCode[code] || null;
|
||||||
|
}
|
||||||
46
src/utils/sseUtils.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//初始化sse连接
|
||||||
|
let eventSource = null
|
||||||
|
//重连延迟
|
||||||
|
let retryTimeout = null
|
||||||
|
export function connectSSE(url, onMessage) {
|
||||||
|
// 如果已有连接,先关闭
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[SSE] 正在连接:', url)
|
||||||
|
|
||||||
|
eventSource = new EventSource(url)
|
||||||
|
|
||||||
|
eventSource.onopen = () => {
|
||||||
|
console.log('[SSE] 连接已建立')
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
console.log('[SSE] 收到消息:', event)
|
||||||
|
try {
|
||||||
|
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
if (onMessage) onMessage(data)
|
||||||
|
} catch (e) {
|
||||||
|
if (onMessage) onMessage(event.data)
|
||||||
|
console.warn('[SSE] 消息解析失败:', event.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource.onerror = (err) => {
|
||||||
|
console.error('[SSE] 连接错误:', err)
|
||||||
|
|
||||||
|
eventSource.close()
|
||||||
|
eventSource = null
|
||||||
|
|
||||||
|
// 避免重复重连
|
||||||
|
if (retryTimeout) clearTimeout(retryTimeout)
|
||||||
|
|
||||||
|
// 3秒后重连
|
||||||
|
retryTimeout = setTimeout(() => {
|
||||||
|
console.log('[SSE] 尝试重新连接...')
|
||||||
|
connectSSE(url, onMessage)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
323
src/views/HomeView.vue
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<div class="container">
|
||||||
|
<div class="right">
|
||||||
|
<img src="../assets/logoBg.png" class="background-video" alt="">
|
||||||
|
<!-- 设置 -->
|
||||||
|
<div class="center-align">
|
||||||
|
<div></div>
|
||||||
|
<div class="setup">
|
||||||
|
<div class="setup-item center-justify">
|
||||||
|
<div></div>
|
||||||
|
<span>
|
||||||
|
网络设置
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="setup-item center-justify">
|
||||||
|
<div></div>
|
||||||
|
<span>
|
||||||
|
简体中文
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="center-line" style="margin-top: 40px;">
|
||||||
|
<!-- logo -->
|
||||||
|
<div class="logo">
|
||||||
|
<div class="center-justify" style="height: 80px; width: 300px;">
|
||||||
|
<!-- <img style="margin-right: 20px;height: 100%;" src="@/assets/logo.png"> -->
|
||||||
|
<img style="height: 100%;" src="@/assets/logotext.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- From -->
|
||||||
|
<div class="from">
|
||||||
|
<div class="from-title center-justify">
|
||||||
|
<div>账号登陆</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="from-input">
|
||||||
|
<el-form label-position="left" label-width="100px" :model="formData">
|
||||||
|
<div class="from-input-item1">
|
||||||
|
<img src="@/assets/username.png" alt="">
|
||||||
|
<el-input style="height: 25px;" v-model="formData.tenantName" placeholder="租户号"
|
||||||
|
clearable @keyup.enter="onSubmit" />
|
||||||
|
</div>
|
||||||
|
<div class="from-input-item1">
|
||||||
|
<img src="@/assets/username.png" alt="">
|
||||||
|
<el-input style="height: 25px;" v-model="formData.userId" placeholder="账号" clearable
|
||||||
|
@keyup.enter="onSubmit" />
|
||||||
|
</div>
|
||||||
|
<div class="from-input-item1">
|
||||||
|
<img src="@/assets/password.png" alt="">
|
||||||
|
<el-input style="height: 25px; " v-model="formData.password" type="password"
|
||||||
|
placeholder="密码" show-password @keyup.enter="onSubmit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="from-input-item">
|
||||||
|
<el-button class="loginButton" color="#8f7ee7" type="primary"
|
||||||
|
@click="onSubmit">登录</el-button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="version center-justify ">版本号:{{ version }}</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { login, getIdByName } from '@/api/account';
|
||||||
|
import { getToken, setToken, setUser, setUserPass, getUserPass } from '@/stores/storage';
|
||||||
|
import { ElLoading, ElMessage } from 'element-plus';
|
||||||
|
import { passToken } from '@/api/ios';
|
||||||
|
|
||||||
|
let version = ref('1.1.1');
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
tenantName: getUserPass() == null ? '' : getUserPass().tenantName,
|
||||||
|
userId: getUserPass() == null ? '' : getUserPass().userId,
|
||||||
|
password: getUserPass() == null ? '' : getUserPass().password,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: 'Loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
});
|
||||||
|
setUserPass(formData.value);
|
||||||
|
getIdByName(formData.value.tenantName).then((tenantId) => {
|
||||||
|
console.log("tenantId", tenantId)
|
||||||
|
login({
|
||||||
|
tenantId: Number(tenantId),
|
||||||
|
username: formData.value.userId,
|
||||||
|
password: formData.value.password,
|
||||||
|
}).then((res) => {
|
||||||
|
loading.close();
|
||||||
|
console.log(res)
|
||||||
|
setToken(res.tokenValue);
|
||||||
|
setUser(res);
|
||||||
|
// setTimeout(() => {
|
||||||
|
// passToken({ token: res.tokenValue })
|
||||||
|
// }, 10000)
|
||||||
|
|
||||||
|
router.push('/Video');
|
||||||
|
}).catch((err) => {
|
||||||
|
loading.close();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.main {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
/* 页面无法选中 */
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
.right {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 20px 40px 20px 50px;
|
||||||
|
border-left: 3px solid #23516e;
|
||||||
|
position: relative;
|
||||||
|
/* 添加 position: relative */
|
||||||
|
overflow: hidden;
|
||||||
|
/* 防止内容溢出 */
|
||||||
|
|
||||||
|
.version {
|
||||||
|
color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
font-size: 20px;
|
||||||
|
bottom: 20px;
|
||||||
|
left: calc(50% - 50px);
|
||||||
|
// box-sizing: border-box;
|
||||||
|
// width: 1600px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.background-video {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
/* 确保视频在内容之下 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup {
|
||||||
|
display: flex;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.setup-item {
|
||||||
|
padding: 10px 6px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
div {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgb(255, 255, 255);
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.from {
|
||||||
|
width: 420px;
|
||||||
|
// height: 320px;
|
||||||
|
color: #022b4e;
|
||||||
|
background-color: #ffffff44;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
padding: 32px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.from-title {
|
||||||
|
font-family: Source Han Sans SC;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #022b4e;
|
||||||
|
line-height: 37px;
|
||||||
|
|
||||||
|
|
||||||
|
div {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
// border-bottom: 4px solid #1db97d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px 0;
|
||||||
|
|
||||||
|
.from-input-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
.from-input-item-title {
|
||||||
|
color: #022b4e;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
width: 80px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton {
|
||||||
|
width: 359px;
|
||||||
|
height: 50px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid #FFFFFF;
|
||||||
|
|
||||||
|
|
||||||
|
font-family: Source Han Sans SC;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #022b4e;
|
||||||
|
line-height: 37px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-input-item1 {
|
||||||
|
display: flex;
|
||||||
|
width: 359px;
|
||||||
|
height: 50px;
|
||||||
|
background: #022b4e1c;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid #FFFFFF;
|
||||||
|
padding: 12px 25px 13px 25px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-justify {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-align {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-flex {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper {
|
||||||
|
--el-input-focus-border-color: rgba(255, 255, 0, 0);
|
||||||
|
--el-menu-hover-bg-color: rgba(255, 255, 0, 0);
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
::v-deep(.el-input__wrapper) {
|
||||||
|
background-color: rgba(255, 0, 0, 0);
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-input__inner) {
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-input__inner::placeholder) {
|
||||||
|
color: #022b4e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
977
src/views/VideoStream.vue
Normal file
@@ -0,0 +1,977 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-scrollbar class="left"> <!-- 左边栏 -->
|
||||||
|
<div class="center-line"> <!-- 左边栏按钮 -->
|
||||||
|
<div v-for="(btn, index) in buttons" :key="index" style="width: 100%;">
|
||||||
|
<div v-if="btn.show?.()" class="left-button" :style="btn.style ? btn.style() : {}" @click="btn.onClick"
|
||||||
|
@mouseenter="hoverIndex = index" @mouseleave="hoverIndex = null">
|
||||||
|
<img :src="hoverIndex === index ? btn.img.hover : btn.img.normal" alt="">
|
||||||
|
{{ btn.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="position: absolute;left: 20px; bottom: 20px;">
|
||||||
|
<el-button @click="showHostDlg = true">执行主播库</el-button>
|
||||||
|
<el-button type="info" @click="uploadLogFile">上传日志</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
<!-- 中间手机区域 -->
|
||||||
|
<div class="content" @click.self="selectedDevice = 999">
|
||||||
|
<div v-if="isImg" class="video-container" v-for="(device, index) in deviceInformation" :key="device.deviceId">
|
||||||
|
<div class="video-canvas" :class="{ active: selectedDevice === index }" :style="imgWH(index)"
|
||||||
|
@click="selectDevice(index)">
|
||||||
|
<img class="stream" :style="imgWH(index)" :src="'http://localhost:' + device.screenPort" />
|
||||||
|
|
||||||
|
<!-- 选中时显示;把down/up都放这里 -->
|
||||||
|
<canvas v-show="selectedDevice === index" class="overlay" :style="imgWH(index)"
|
||||||
|
@mousedown.stop="(e) => onCanvasDown(device.deviceId, e, index)"
|
||||||
|
@mouseup.stop="(e) => onCanvasUp(device.deviceId, e, index)"
|
||||||
|
@mousemove.stop="(e) => onCanvasMove(device.deviceId, e, index)">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
<div class="input-info" v-show="selectedDevice == index">
|
||||||
|
|
||||||
|
<div class="app-button" @click="getMesList(device.deviceId)">获取当前聊天记录</div>
|
||||||
|
<!-- <div class="app-button" @click="mqSend()">mq</div> -->
|
||||||
|
<!-- <div class="app-button"
|
||||||
|
@click="wsActions.clickCopyList(device.deviceId, index); openShowChat = true; istranslate = true">
|
||||||
|
翻译本页对话</div>
|
||||||
|
<div class="app-button" @click="wsActions.clickCopyList(device.deviceId, index); openShowChat = true;">
|
||||||
|
回复消息</div>
|
||||||
|
<div class="app-button" @click="wsActions.test(device.deviceId, index)">打印ui节点树</div>
|
||||||
|
<div class="app-button" @click="wsActions.isOneLive(device.deviceId, index)">判断单人还是双人</div>
|
||||||
|
<div class="app-button" @click="wsActions.slideDown(device.deviceId, index)">下滑</div>
|
||||||
|
<div class="app-button" @click="wsActions.killNow(device.deviceId, index)">关闭当前应用</div>
|
||||||
|
<div class="app-button" @click="chooseFile(device.deviceId, index, 1, wsActions)">安装 APK
|
||||||
|
文件</div>
|
||||||
|
<div class="app-button" @click="chooseFile(device.deviceId, index, 2, wsActions)">
|
||||||
|
传送文件</div> -->
|
||||||
|
<!-- <div style="display: flex;">
|
||||||
|
<input style="border: 1px solid #000;margin:0px 14px;" v-model="textContent[index]" type="text"></input>
|
||||||
|
<div class="app-button" style="margin: 0px;height: 40px;width: 60px;font-size: 14px;"
|
||||||
|
@click="setComText(index)">发送
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right center-justify" @click.self="selectedDevice = 999">
|
||||||
|
<!-- <div style="margin: 30px;"></div> -->
|
||||||
|
<ChatDialog :visible="openShowChat" :messages="chatList" />
|
||||||
|
</div>
|
||||||
|
<MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice"
|
||||||
|
@confirm="onDialogConfirm" @cancel="stopAll" />
|
||||||
|
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 定时调度配置弹窗 -->
|
||||||
|
<el-dialog v-model="showScheduleDlg" title="定时调度(每小时)" width="420px">
|
||||||
|
<div style="display:grid;grid-template-columns: 100px 1fr; gap:12px; align-items:center;">
|
||||||
|
<div>片段 A</div>
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<el-select v-model="schedAKey" style="width:140px;">
|
||||||
|
<el-option label="一键关注" value="follow" />
|
||||||
|
<el-option label="刷视频(养号)" value="like" />
|
||||||
|
<el-option label="刷直播" value="brushLive" />
|
||||||
|
<el-option label="监测消息" value="listen" />
|
||||||
|
</el-select>
|
||||||
|
<el-input-number v-model="schedAMin" :min="1" :max="59" />
|
||||||
|
<span>分钟</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>片段 B</div>
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<el-select v-model="schedBKey" style="width:140px;">
|
||||||
|
<!-- <el-option label="一键关注" value="follow" /> -->
|
||||||
|
<el-option label="刷视频(养号)" value="like" />
|
||||||
|
<el-option label="刷直播" value="brushLive" />
|
||||||
|
<el-option label="监测消息" value="listen" />
|
||||||
|
</el-select>
|
||||||
|
<el-input-number v-model="schedBMin" :min="1" :max="59" />
|
||||||
|
<span>分钟</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>总时长</div>
|
||||||
|
<div><b>{{ schedAMin + schedBMin }}</b> 分钟(必须等于 60)</div>
|
||||||
|
|
||||||
|
<!-- <div>启用调度</div>
|
||||||
|
<div><el-switch v-model="scheduleEnabled" /></div> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showScheduleDlg = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveSchedule">开启</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, onBeforeUnmount, watch, inject } from "vue";
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import {
|
||||||
|
setphoneXYinfo, getphoneXYinfo, getUser,
|
||||||
|
getHostList, setHostList, getContentpriList,
|
||||||
|
setContentpriList, getContentList, setContentList,
|
||||||
|
setsessionId, getsessionId
|
||||||
|
} from '@/stores/storage'
|
||||||
|
import { connectSSE } from '@/utils/sseUtils'
|
||||||
|
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
||||||
|
import { chat, translationToChinese, translation } from "@/api/chat";
|
||||||
|
import HostListManagerDialog from '@/components/HostListManagerDialog.vue'
|
||||||
|
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue'; // 根据实际路径修改
|
||||||
|
import ChatDialog from '@/components/ChatDialog.vue'
|
||||||
|
import { pickTikTokBundleId } from '@/utils/arrUtils'
|
||||||
|
import { logout } from '@/api/account';
|
||||||
|
import {
|
||||||
|
getDeviceList,
|
||||||
|
toHome,
|
||||||
|
swipeAction,
|
||||||
|
tapAction,
|
||||||
|
growAccount,
|
||||||
|
stopScript,
|
||||||
|
watchLiveForGrowth,
|
||||||
|
monitorMessages,
|
||||||
|
passAnchorData,
|
||||||
|
addTempAnchorData,
|
||||||
|
deviceAppList,
|
||||||
|
launchApp,
|
||||||
|
getChatTextInfo,
|
||||||
|
setLoginInfo,
|
||||||
|
|
||||||
|
|
||||||
|
} from '@/api/ios';
|
||||||
|
const router = useRouter();
|
||||||
|
const openShowChat = ref([true])
|
||||||
|
//主播库
|
||||||
|
const showHostDlg = ref(false)
|
||||||
|
|
||||||
|
let hostList = []
|
||||||
|
//查询列表轮询
|
||||||
|
let getListtimer = null;
|
||||||
|
let userdata = getUser();
|
||||||
|
let chatList = ref([])
|
||||||
|
let isImg = ref(true)
|
||||||
|
// 引入刷新方法
|
||||||
|
const reload = inject("reload")
|
||||||
|
const reloadImg = () => {
|
||||||
|
isImg.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
isImg.value = true
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
//start弹窗
|
||||||
|
let isMsgPop = ref(false)
|
||||||
|
//缓存主播联动数组
|
||||||
|
let stroageHost = ref([])
|
||||||
|
let runType = ref('')
|
||||||
|
let isMonitorOn = ref(false)
|
||||||
|
const hoverIndex = ref(null) //选中
|
||||||
|
let showDialog = ref(false);//弹窗是否显示
|
||||||
|
let dialogTitle = ref('');//当前弹窗类型
|
||||||
|
let deviceInformation = ref([])
|
||||||
|
// 你可以用这种方式声明按钮们
|
||||||
|
//联动用作标记
|
||||||
|
let batchMode = ref('init'); // 'init' | 'follow'(仅作标记)
|
||||||
|
|
||||||
|
// 当前是否被其它模式占用(四个互斥按钮专用)
|
||||||
|
const isLocked = (type) => !!runType.value && runType.value !== type
|
||||||
|
|
||||||
|
// 互斥按钮的样式:激活=红,锁定=半透明且禁点
|
||||||
|
const ctrlStyle = (type) => ({
|
||||||
|
backgroundColor: runType.value === type ? 'red' : '',
|
||||||
|
opacity: isLocked(type) ? 0.5 : 1,
|
||||||
|
pointerEvents: isLocked(type) ? 'none' : 'auto',
|
||||||
|
cursor: isLocked(type) ? 'not-allowed' : 'pointer',
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
label: '刷新',
|
||||||
|
onClick: () => reloadImg(),
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '打开tiktok',
|
||||||
|
onClick: () => openTk(),
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn2.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn2-2.png', import.meta.url).href
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '返回主页',
|
||||||
|
onClick: () => {
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
toHome({ udid: item.deviceId })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn3.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn3-3.png', import.meta.url).href
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '刷直播',
|
||||||
|
onClick: () => {
|
||||||
|
if (runType.value == 'brushLive') {
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
stopScript({ udid: item.deviceId })
|
||||||
|
})
|
||||||
|
runType.value = ''
|
||||||
|
return
|
||||||
|
};
|
||||||
|
|
||||||
|
// 若被其它模式占用:直接返回(已在样式层禁点,这里双保险)
|
||||||
|
if (isLocked('brushLive')) return
|
||||||
|
|
||||||
|
runType.value = 'brushLive'
|
||||||
|
deviceInformation.value.forEach((item) => watchLiveForGrowth({ udid: item.deviceId }))
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn4.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn4-4.png', import.meta.url).href
|
||||||
|
},
|
||||||
|
style: () => ctrlStyle('brushLive')
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '刷视频',
|
||||||
|
onClick: () => {
|
||||||
|
if (runType.value == 'like') {
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
stopScript({ udid: item.deviceId })
|
||||||
|
})
|
||||||
|
runType.value = ''
|
||||||
|
return
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLocked('like')) return
|
||||||
|
|
||||||
|
runType.value = 'like'
|
||||||
|
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn5.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn5-5.png', import.meta.url).href
|
||||||
|
},
|
||||||
|
style: () => ctrlStyle('like')
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// label: '一键关注并打招呼',
|
||||||
|
// onClick: () => {
|
||||||
|
|
||||||
|
// if (runType.value == 'follow') {
|
||||||
|
// deviceInformation.value.forEach((item) => {
|
||||||
|
// stopScript({ udid: item.deviceId })
|
||||||
|
// })
|
||||||
|
// runType.value = ''
|
||||||
|
// return
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (isLocked('follow')) return
|
||||||
|
// showDialog.value = true;
|
||||||
|
// dialogTitle.value = '主播ID';
|
||||||
|
|
||||||
|
// },
|
||||||
|
// show: () => true,
|
||||||
|
// img: {
|
||||||
|
// normal: new URL('@/assets/video/leftBtn6.png', import.meta.url).href,
|
||||||
|
// hover: new URL('@/assets/video/leftBtn6-6.png', import.meta.url).href
|
||||||
|
// },
|
||||||
|
// style: () => ctrlStyle('follow')
|
||||||
|
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
label: '启动调度任务',
|
||||||
|
onClick: () => openScheduleDialog(),
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn6.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn6-6.png', import.meta.url).href
|
||||||
|
},
|
||||||
|
style: () => ctrlStyle('follow')
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '监测消息',
|
||||||
|
onClick: () => {
|
||||||
|
if (runType.value == 'lisen') {
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
stopScript({ udid: item.deviceId })
|
||||||
|
})
|
||||||
|
runType.value = ''
|
||||||
|
return
|
||||||
|
};
|
||||||
|
if (isLocked('lisen')) return
|
||||||
|
|
||||||
|
runType.value = 'lisen'
|
||||||
|
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
|
||||||
|
},
|
||||||
|
style: () => ctrlStyle('lisen')
|
||||||
|
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// label: '定时调度',
|
||||||
|
// onClick: () => openScheduleDialog(),
|
||||||
|
// show: () => true,
|
||||||
|
// img: {
|
||||||
|
// normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
|
||||||
|
// hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
label: '全部停止',
|
||||||
|
onClick: () => stopAll(),
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn8.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn8-8.png', import.meta.url).href
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '登出',
|
||||||
|
onClick: () => {
|
||||||
|
logout({ userId: userdata.id, tenantId: userdata.tenantId })
|
||||||
|
router.push('/')
|
||||||
|
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn9.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn9-9.png', import.meta.url).href
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const schedulePlan = [
|
||||||
|
{ key: 'follow', duration: 40 * 60 * 1000 },
|
||||||
|
{ key: 'like', duration: 20 * 60 * 1000 },
|
||||||
|
]
|
||||||
|
// 调度状态(持久化一下,避免刷新丢失)
|
||||||
|
let scheduleState = (() => {
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem('SCHEDULE_STATE') || '{}')
|
||||||
|
if (saved && typeof saved.index === 'number' && typeof saved.startTime === 'number') {
|
||||||
|
return saved
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
return { index: 0, startTime: Date.now() }
|
||||||
|
})()
|
||||||
|
|
||||||
|
let scheduleTimer = null // 轮询定时器句柄
|
||||||
|
const scheduleTickMs = 30_000 // 每 30s 检查一次是否到切换点
|
||||||
|
let scheduleEnabled = ref(true) // 需要时可手动关闭调度(例如“全部停止”)
|
||||||
|
// 弹窗
|
||||||
|
const showScheduleDlg = ref(false)
|
||||||
|
|
||||||
|
// 两个时间片(默认 A=follow 40min,B=like 20min)
|
||||||
|
const schedAKey = ref('follow')
|
||||||
|
const schedAMin = ref(40)
|
||||||
|
const schedBKey = ref('like')
|
||||||
|
const schedBMin = ref(20)
|
||||||
|
// 打开弹窗:把当前 schedulePlan 映射到 UI
|
||||||
|
function openScheduleDialog() {
|
||||||
|
// 把当前计划读出来(只支持两个片段的简易版)
|
||||||
|
if (Array.isArray(schedulePlan) && schedulePlan.length >= 2) {
|
||||||
|
const a = schedulePlan[0], b = schedulePlan[1]
|
||||||
|
schedAKey.value = a?.key || 'follow'
|
||||||
|
schedAMin.value = Math.max(1, Math.round((a?.duration || 40 * 60_000) / 60_000))
|
||||||
|
schedBKey.value = b?.key || 'like'
|
||||||
|
schedBMin.value = Math.max(1, Math.round((b?.duration || 20 * 60_000) / 60_000))
|
||||||
|
}
|
||||||
|
showScheduleDlg.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存:校验=60 分钟 → 更新 schedulePlan → 持久化 → 重启轮询
|
||||||
|
function saveSchedule() {
|
||||||
|
scheduleEnabled.value = true
|
||||||
|
const total = schedAMin.value + schedBMin.value
|
||||||
|
if (total !== 60) {
|
||||||
|
ElMessage.error('两个片段相加必须等于 60 分钟')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schedulePlan.splice(0, schedulePlan.length,
|
||||||
|
{ key: schedAKey.value, duration: schedAMin.value * 60_000 },
|
||||||
|
{ key: schedBKey.value, duration: schedBMin.value * 60_000 },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 存 localStorage
|
||||||
|
localStorage.setItem('SCHEDULE_PLAN', JSON.stringify(schedulePlan))
|
||||||
|
localStorage.setItem('SCHEDULE_ENABLED', JSON.stringify(!!scheduleEnabled.value))
|
||||||
|
|
||||||
|
// 重置时间片起点并立即生效
|
||||||
|
scheduleState.index = 0
|
||||||
|
scheduleState.startTime = Date.now()
|
||||||
|
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
||||||
|
|
||||||
|
// 若启用则重启轮询
|
||||||
|
if (scheduleEnabled.value) startScheduleLoop()
|
||||||
|
|
||||||
|
showScheduleDlg.value = false
|
||||||
|
ElMessage.success('已保存定时调度')
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDevice = ref(null)
|
||||||
|
|
||||||
|
// —— 显示尺寸固定为 320x720;未选中缩略为 THUMB_SCALE 倍 ——
|
||||||
|
const BASE_W = 320
|
||||||
|
const BASE_H = 720
|
||||||
|
const THUMB_SCALE = 0.6
|
||||||
|
|
||||||
|
// 当前选中的卡片:选中=1倍,未选中=缩略比例
|
||||||
|
const imgWH = (index) => {
|
||||||
|
const scale = (selectedDevice.value === index) ? 1 : THUMB_SCALE
|
||||||
|
return {
|
||||||
|
width: `${BASE_W * scale}px`,
|
||||||
|
height: `${BASE_H * scale}px`,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算某索引当前展示宽高(用于坐标换算)
|
||||||
|
const displaySize = (index) => {
|
||||||
|
const scale = (selectedDevice.value === index) ? 1 : THUMB_SCALE
|
||||||
|
return { w: BASE_W * scale, h: BASE_H * scale }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Canvas offset 坐标 → 真实手机分辨率坐标
|
||||||
|
const mapToDeviceXY = (index, offsetX, offsetY) => {
|
||||||
|
const dev = deviceInformation.value[index] || {}
|
||||||
|
const realW = Number(dev.width) || BASE_W // 后端返回的真实分辨率
|
||||||
|
const realH = Number(dev.height) || BASE_H
|
||||||
|
const rotation = Number(dev.rotation || 0) // 若后端有提供旋转角,可用 0/90/180/270
|
||||||
|
|
||||||
|
const { w: dispW, h: dispH } = displaySize(index)
|
||||||
|
|
||||||
|
// 归一化到 0~1
|
||||||
|
let nx = Math.min(Math.max(offsetX / dispW, 0), 1)
|
||||||
|
let ny = Math.min(Math.max(offsetY / dispH, 0), 1)
|
||||||
|
|
||||||
|
// 处理旋转(如果你的服务器坐标基于设备原生朝向)
|
||||||
|
// 0: 直接映射;90: 顺时针;180、270 同理
|
||||||
|
let x, y
|
||||||
|
switch (rotation % 360) {
|
||||||
|
case 90:
|
||||||
|
case -270:
|
||||||
|
x = Math.round(ny * realW)
|
||||||
|
y = Math.round((1 - nx) * realH)
|
||||||
|
break
|
||||||
|
case 180:
|
||||||
|
case -180:
|
||||||
|
x = Math.round((1 - nx) * realW)
|
||||||
|
y = Math.round((1 - ny) * realH)
|
||||||
|
break
|
||||||
|
case 270:
|
||||||
|
case -90:
|
||||||
|
x = Math.round((1 - ny) * realW)
|
||||||
|
y = Math.round(nx * realH)
|
||||||
|
break
|
||||||
|
default: // 0°
|
||||||
|
x = Math.round(nx * realW)
|
||||||
|
y = Math.round(ny * realH)
|
||||||
|
}
|
||||||
|
return { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中:恢复到 320x720 并显示盖层
|
||||||
|
const selectDevice = (index) => {
|
||||||
|
selectedDevice.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— 鼠标交互:按下/移动/抬起 ———
|
||||||
|
const dragState = ref({}) // 以 index 作为 key 保存 {ox, oy, t}
|
||||||
|
|
||||||
|
const onCanvasDown = (udid, e, index) => {
|
||||||
|
// 记录起点(Canvas 内的 offset)
|
||||||
|
dragState.value[index] = { ox: e.offsetX, oy: e.offsetY, t: Date.now(), udid }
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCanvasMove = (udid, e, index) => {
|
||||||
|
// 如需在 overlay 上画指示、十字线等,可在这里使用 e.offsetX/e.offsetY
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCanvasUp = async (udid, e, index) => {
|
||||||
|
const st = dragState.value[index]
|
||||||
|
if (!st) return
|
||||||
|
|
||||||
|
const { ox, oy, t } = st
|
||||||
|
const dx = e.offsetX - ox
|
||||||
|
const dy = e.offsetY - oy
|
||||||
|
const elapsed = Date.now() - t
|
||||||
|
delete dragState.value[index]
|
||||||
|
|
||||||
|
// 映射到真实分辨率
|
||||||
|
const p0 = mapToDeviceXY(index, ox, oy)
|
||||||
|
const p1 = mapToDeviceXY(index, e.offsetX, e.offsetY)
|
||||||
|
console.log(" x, y", p0, p1)
|
||||||
|
// 判断是“点按”还是“滑动”
|
||||||
|
const MOVE_THR = 5 // 像素阈值(可按需调整)
|
||||||
|
const isTap = Math.hypot(dx, dy) < MOVE_THR && elapsed < 500
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isTap) {
|
||||||
|
await tapAction({ udid, x: p1.x, y: p1.y })
|
||||||
|
// console.log('tap', p1)
|
||||||
|
} else {
|
||||||
|
// 只需要方向:1上/2左/3下/4右
|
||||||
|
const rotation = Number((deviceInformation.value[index] || {}).rotation || 0)
|
||||||
|
|
||||||
|
const code = getSwipeCodeWithRotation(dx, dy, rotation) // 必定得到1~4
|
||||||
|
console.log("code", code)
|
||||||
|
await swipeAction({ udid, direction: code })
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 方向码:1=上, 2=左, 3=下, 4=右(不返回0,始终给出一个方向) */
|
||||||
|
function getSwipeCode(dx, dy) {
|
||||||
|
// 哪个轴位移更大就取哪个轴;边界≈45°
|
||||||
|
if (Math.abs(dx) >= Math.abs(dy)) {
|
||||||
|
return dx < 0 ? 2 : 4 // 左/右
|
||||||
|
} else {
|
||||||
|
return dy < 0 ? 1 : 3 // 上/下(DOM坐标里向上是负)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 带设备旋转(0/90/180/270):先把画布向量(dx,dy)旋回设备坐标系再判方向 */
|
||||||
|
function getSwipeCodeWithRotation(dx, dy, rotation = 0) {
|
||||||
|
let dxD = dx, dyD = dy
|
||||||
|
switch ((rotation % 360 + 360) % 360) {
|
||||||
|
case 90: dxD = dy; dyD = -dx; break
|
||||||
|
case 180: dxD = -dx; dyD = -dy; break
|
||||||
|
case 270: dxD = -dy; dyD = dx; break
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
return getSwipeCode(dxD, dyD)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function openTk() {
|
||||||
|
if (!deviceInformation.value?.length) {
|
||||||
|
ElMessage.warning('暂无在线设备')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ElLoading.service({ text: '正在打开 TikTok …', background: 'rgba(0,0,0,.35)' })
|
||||||
|
const results = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 为了稳妥,逐台串行(如果你希望更快,可改 Promise.all 并注意并发数)
|
||||||
|
for (const dev of deviceInformation.value) {
|
||||||
|
const udid = dev.deviceId
|
||||||
|
try {
|
||||||
|
const apps = await deviceAppList({ udid }) // 期望返回示例中的数组
|
||||||
|
const bundleId = pickTikTokBundleId(apps)
|
||||||
|
|
||||||
|
if (!bundleId) {
|
||||||
|
results.push({ udid, ok: false, msg: '未找到 TikTok' })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await launchApp({ udid, bundleId })
|
||||||
|
results.push({ udid, ok: true, msg: `已启动 TikTok (${bundleId})` })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('openTk error', udid, e)
|
||||||
|
results.push({ udid, ok: false, msg: '请求失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 汇总提示(成功/失败各一条)
|
||||||
|
const okCount = results.filter(r => r.ok).length
|
||||||
|
const fail = results.filter(r => !r.ok)
|
||||||
|
if (okCount) ElMessage.success(`已在 ${okCount} 台设备启动 TikTok`)
|
||||||
|
if (fail.length) {
|
||||||
|
const udids = fail.map(f => f.udid).join(', ')
|
||||||
|
ElMessage.error(`以下设备未能启动:${udids}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMesList(deviceId) {
|
||||||
|
getChatTextInfo({ udid: deviceId }).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
chatList.value = res
|
||||||
|
console.log(chatList.value)
|
||||||
|
getTranslation(chatList.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAll() {
|
||||||
|
// if (!runType.value) return
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
stopScript({ udid: item.deviceId }).then((res) => {
|
||||||
|
console.log(`停止成功:${item.deviceId}`, res, printCurrentTime())
|
||||||
|
ElMessage.success(`停止成功:${item.deviceId}`)
|
||||||
|
}).catch((res) => {
|
||||||
|
console.log(`停止失败`, printCurrentTime())
|
||||||
|
ElMessage.error(`脚本已停止`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
scheduleEnabled.value = false
|
||||||
|
runType.value = ''
|
||||||
|
batchMode.value = 'init';
|
||||||
|
}
|
||||||
|
|
||||||
|
//确认多行文本框内容
|
||||||
|
function onDialogConfirm(result, type, index, isMon) {
|
||||||
|
// console.log(type, result, isMon);
|
||||||
|
if (type == '主播ID') {
|
||||||
|
hostList = (result || []).map(id => ({ id, country: '' }))
|
||||||
|
|
||||||
|
dialogTitle.value = '私信';
|
||||||
|
setTimeout(() => {
|
||||||
|
showDialog.value = true;
|
||||||
|
}, 600)
|
||||||
|
} else if (type == '私信') {
|
||||||
|
runType.value = 'follow'
|
||||||
|
setContentpriList(result)
|
||||||
|
passAnchorData(
|
||||||
|
{
|
||||||
|
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||||
|
anchorList: hostList.map(item => ({
|
||||||
|
anchorId: item.id,
|
||||||
|
country: item.country
|
||||||
|
})),
|
||||||
|
prologueList: result,
|
||||||
|
needReply: isMon
|
||||||
|
}
|
||||||
|
).then((res) => {
|
||||||
|
hostList = []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: 'Loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
});
|
||||||
|
getDeviceListFun()
|
||||||
|
window.electronAPI.startMq(userdata.tenantId, userdata.id)
|
||||||
|
// 初始化时获取设备列表
|
||||||
|
getListtimer = setInterval(() => {
|
||||||
|
loading.close();
|
||||||
|
getDeviceListFun()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 批次缓冲(仅用于当前“波”)
|
||||||
|
let batch = []; // [{ country, text }]
|
||||||
|
let flushTimer = null;
|
||||||
|
|
||||||
|
function scheduleFlush(handler, delay = 400) {
|
||||||
|
if (flushTimer) clearTimeout(flushTimer);
|
||||||
|
flushTimer = setTimeout(() => {
|
||||||
|
if (batch.length) {
|
||||||
|
const items = batch.slice(); // 拷贝一份
|
||||||
|
batch.length = 0; // 清空批次
|
||||||
|
try {
|
||||||
|
handler(items);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SSE flush error]', e);
|
||||||
|
// 出错不回灌,避免重复提交;必要时可根据需要 batch.push(...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// —— SSE 接收 ——
|
||||||
|
const es = connectSSE('http://localhost:3312/events', (data) => {
|
||||||
|
console.log('来自服务端:', data);
|
||||||
|
|
||||||
|
if (data === 'start') {
|
||||||
|
// 新一波开始:根据当前状态决定“本波 flush 用谁”
|
||||||
|
if (!isMsgPop.value) {
|
||||||
|
// 还没开始过 -> 首次弹框,确认后使用处理本波
|
||||||
|
isMsgPop.value = true;
|
||||||
|
batchMode.value = 'init';
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'检测到YOLO助手正在爬取主播,是否进行操作?',
|
||||||
|
'消息提醒',
|
||||||
|
{ confirmButtonText: '开始', cancelButtonText: '取消', type: 'success' }
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage({ type: 'success', message: '任务开启成功' });
|
||||||
|
// runType.value = 'follow';
|
||||||
|
batchMode.value = 'follow';
|
||||||
|
|
||||||
|
// 不在这里立刻提交;让后续主播数据先进 batch,再由防抖统一 flush
|
||||||
|
// 不直接发;把这“一波”的主播先塞进 hostList,然后弹出“私信”输入框
|
||||||
|
scheduleFlush((items) => {
|
||||||
|
hostList = (items || []).map(h => ({ id: h.text, country: h.country || '' }))
|
||||||
|
showScheduleDlg.value = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 取消:清理状态,丢弃批次
|
||||||
|
batch.length = 0;
|
||||||
|
isMsgPop.value = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 已经在运行 follow:本波用 addTempAnchorData 追加
|
||||||
|
batchMode.value = 'follow';
|
||||||
|
// 立刻安排一次“尾随防抖”flush,等本波数据齐了再送
|
||||||
|
scheduleFlush((items) => {
|
||||||
|
addTempAnchorData(items.map(h => ({ anchorId: h.text, country: h.country || '' })));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非 start:本波主播数据进入批次
|
||||||
|
const country = data && data.country != null ? data.country : '';
|
||||||
|
const text = data && (data.hostsId != null ? data.hostsId : data.text);
|
||||||
|
if (text == null) {
|
||||||
|
// 数据格式不对,丢弃或打印
|
||||||
|
console.warn('[SSE] 非法数据,缺少 hostsId/text:', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
batch.push({ country, text });
|
||||||
|
|
||||||
|
// 根据当前模式,刷新防抖(让“最后一条到来后”延迟几百毫秒再统一提交)
|
||||||
|
if (batchMode.value === 'init') {
|
||||||
|
// 首次确认前:等用户点“开始”后由上面的 scheduleFlush 执行
|
||||||
|
scheduleFlush((items) => {
|
||||||
|
// 安全起见:只有在 runType 已经 follow 时才真正提交
|
||||||
|
if (runType.value === 'follow') {
|
||||||
|
passAnchorData({
|
||||||
|
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||||
|
anchorList: items.map(h => ({ anchorId: h.text, country: h.country || '' })),
|
||||||
|
needReply: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 还没开始就来了数据:把它们留回批次,等待上面 then 里的 scheduleFlush 再处理
|
||||||
|
batch.push(...items);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 已在关注:走追加逻辑
|
||||||
|
scheduleFlush((items) => {
|
||||||
|
addTempAnchorData(items.map(h => ({ anchorId: h.text, country: h.country || '' })));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(getListtimer)
|
||||||
|
getListtimer = null
|
||||||
|
})
|
||||||
|
const getDeviceListFun = () => {
|
||||||
|
getDeviceList().then((res) => {
|
||||||
|
// console.log('返回', res.length)
|
||||||
|
if (res && res.length > 0 && deviceInformation.value.length !== res.length) {
|
||||||
|
console.log("设备变更")
|
||||||
|
deviceInformation.value = res
|
||||||
|
reloadImg()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.length == 0) {
|
||||||
|
deviceInformation.value = []
|
||||||
|
reloadImg()
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
ElMessage.error(`IOSAI服务错误`)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadLogFile() {
|
||||||
|
let loading = null
|
||||||
|
try {
|
||||||
|
// 先弹出确认框
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定要上传日志文件吗?',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// 如果点了确定,就会走到这里
|
||||||
|
loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: '上传中...',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
})
|
||||||
|
const res = await setLoginInfo({
|
||||||
|
"tenantId": userdata.tenantId,
|
||||||
|
"userId": userdata.id,
|
||||||
|
"token": userdata.tokenValue
|
||||||
|
})
|
||||||
|
loading.close()
|
||||||
|
console.log("上传文件返回", res)
|
||||||
|
if (res) {
|
||||||
|
console.log("✅ 上传成功:", res)
|
||||||
|
ElMessage.success('✅ 上传成功')
|
||||||
|
} else {
|
||||||
|
console.error("❌ 上传失败:", res.msg)
|
||||||
|
ElMessage.error('❌ 上传失败: ' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (loading) {
|
||||||
|
loading.close()
|
||||||
|
}
|
||||||
|
// 如果用户点了取消,会进入这里
|
||||||
|
if (err === 'cancel' || err === 'close') {
|
||||||
|
ElMessage.info('已取消上传')
|
||||||
|
} else {
|
||||||
|
console.error("❌ 上传异常:", err)
|
||||||
|
ElMessage.error('❌ 上传异常: ' + err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function runTask(key) {
|
||||||
|
if (!scheduleEnabled.value) return
|
||||||
|
console.log('[schedule] 切换到任务:', key, printCurrentTime())
|
||||||
|
|
||||||
|
forceActivate(key, () => {
|
||||||
|
if (key === 'follow') {
|
||||||
|
runType.value = 'follow'
|
||||||
|
// 这三行保持你现有的唤起流程(弹“主播ID”输入等)
|
||||||
|
// showDialog.value = true
|
||||||
|
// dialogTitle.value = '主播ID'
|
||||||
|
// selectedDevice.value = 999
|
||||||
|
if (hostList.length <= 0) {
|
||||||
|
dialogTitle.value = '主播ID';
|
||||||
|
} else {
|
||||||
|
dialogTitle.value = '私信';
|
||||||
|
}
|
||||||
|
showDialog.value = true;
|
||||||
|
|
||||||
|
} else if (key === 'like') {
|
||||||
|
|
||||||
|
stopAll()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
runType.value = 'like'
|
||||||
|
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
} else if (key === 'brushLive') {
|
||||||
|
stopAll()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
runType.value = 'brushLive'
|
||||||
|
deviceInformation.value.forEach((item) => watchLiveForGrowth({ udid: item.deviceId }))
|
||||||
|
}, 1000)
|
||||||
|
runType.value = 'brushLive'
|
||||||
|
} else if (key === 'listen') {
|
||||||
|
stopAll()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
runType.value = 'listen'
|
||||||
|
isMonitorOn.value = true
|
||||||
|
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function startScheduleLoop() {
|
||||||
|
// 先按照当前 index 跑一次,保证“即刻对齐”
|
||||||
|
runTask(schedulePlan[scheduleState.index].key)
|
||||||
|
|
||||||
|
// 清理旧轮询,防止重复
|
||||||
|
if (scheduleTimer) clearInterval(scheduleTimer)
|
||||||
|
|
||||||
|
scheduleTimer = setInterval(() => {
|
||||||
|
if (!scheduleEnabled.value) return
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const cur = schedulePlan[scheduleState.index]
|
||||||
|
const elapsed = now - scheduleState.startTime
|
||||||
|
|
||||||
|
if (elapsed >= cur.duration) {
|
||||||
|
// 进入下一个时间片
|
||||||
|
scheduleState.index = (scheduleState.index + 1) % schedulePlan.length
|
||||||
|
scheduleState.startTime = now
|
||||||
|
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
||||||
|
|
||||||
|
runTask(schedulePlan[scheduleState.index].key)
|
||||||
|
}
|
||||||
|
}, scheduleTickMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceActivate(key, runner) {
|
||||||
|
// 跳过互斥逻辑,直接切换
|
||||||
|
runType.value = key;
|
||||||
|
if (typeof runner === 'function') runner();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function mqSend() {
|
||||||
|
window.electronAPI.mqSend("start")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getTranslation(list) {
|
||||||
|
list.forEach((item, index) => {
|
||||||
|
translationToChinese({ msg: item.text }).then(res => {
|
||||||
|
console.log(res);
|
||||||
|
chatList.value[index].text = res
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHostSaved(list) {
|
||||||
|
console.log('保存后的主播id:', list)
|
||||||
|
}
|
||||||
|
// //sse接收爬虫发送的消息
|
||||||
|
// const es = connectSSE(`http://localhost:3311/events`, (data) => {
|
||||||
|
// // connectSSE(`http://192.168.1.155:19665/api/sse/connect/${userdata.tenantId}/${userdata.id}`, (data) => {
|
||||||
|
// // 处理服务端推送的数据
|
||||||
|
// console.log('来自服务端:', data)
|
||||||
|
|
||||||
|
// //接收到start
|
||||||
|
// if (data === 'start') {
|
||||||
|
|
||||||
|
// } else {
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// })
|
||||||
|
|
||||||
|
//当前时间获取
|
||||||
|
function printCurrentTime() {
|
||||||
|
const now = new Date();
|
||||||
|
return now.toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
@import '../static/css/video.less';
|
||||||
|
</style>
|
||||||
949
src/views/dfasd
Normal file
@@ -0,0 +1,949 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-scrollbar class="left"> <!-- 左边栏 -->
|
||||||
|
<div class="center-line"> <!-- 左边栏按钮 -->
|
||||||
|
<div v-for="(btn, index) in buttons" :key="index" style="width: 100%;">
|
||||||
|
<div v-if="btn.show?.()" class="left-button" :style="btn.style ? btn.style() : {}" @click="btn.onClick"
|
||||||
|
@mouseenter="hoverIndex = index" @mouseleave="hoverIndex = null">
|
||||||
|
<img :src="hoverIndex === index ? btn.img.hover : btn.img.normal" alt="">
|
||||||
|
{{ btn.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="position: absolute;left: 20px; bottom: 20px;">
|
||||||
|
<el-button @click="showHostDlg = true">执行主播库</el-button>
|
||||||
|
<el-button type="info" @click="uploadLogFile">上传日志</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
<!-- 中间手机区域 -->
|
||||||
|
<div class="content" @click.self="selectedDevice = 999">
|
||||||
|
<div v-if="isImg" class="video-container" v-for="(device, index) in deviceInformation" :key="device.deviceId">
|
||||||
|
<div class="video-canvas" :class="{ active: selectedDevice === index }" :style="imgWH(index)"
|
||||||
|
@click="selectDevice(index)">
|
||||||
|
<img class="stream" :style="imgWH(index)" :src="'http://localhost:' + device.screenPort" />
|
||||||
|
|
||||||
|
<!-- 选中时显示;把down/up都放这里 -->
|
||||||
|
<canvas v-show="selectedDevice === index" class="overlay" :style="imgWH(index)"
|
||||||
|
@mousedown.stop="(e) => onCanvasDown(device.deviceId, e, index)"
|
||||||
|
@mouseup.stop="(e) => onCanvasUp(device.deviceId, e, index)"
|
||||||
|
@mousemove.stop="(e) => onCanvasMove(device.deviceId, e, index)">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
<div class="input-info" v-show="selectedDevice == index">
|
||||||
|
|
||||||
|
<div class="app-button" @click="getMesList(device.deviceId)">获取当前聊天记录</div>
|
||||||
|
<!-- <div class="app-button" @click="mqSend()">mq</div> -->
|
||||||
|
<!-- <div class="app-button"
|
||||||
|
@click="wsActions.clickCopyList(device.deviceId, index); openShowChat = true; istranslate = true">
|
||||||
|
翻译本页对话</div>
|
||||||
|
<div class="app-button" @click="wsActions.clickCopyList(device.deviceId, index); openShowChat = true;">
|
||||||
|
回复消息</div>
|
||||||
|
<div class="app-button" @click="wsActions.test(device.deviceId, index)">打印ui节点树</div>
|
||||||
|
<div class="app-button" @click="wsActions.isOneLive(device.deviceId, index)">判断单人还是双人</div>
|
||||||
|
<div class="app-button" @click="wsActions.slideDown(device.deviceId, index)">下滑</div>
|
||||||
|
<div class="app-button" @click="wsActions.killNow(device.deviceId, index)">关闭当前应用</div>
|
||||||
|
<div class="app-button" @click="chooseFile(device.deviceId, index, 1, wsActions)">安装 APK
|
||||||
|
文件</div>
|
||||||
|
<div class="app-button" @click="chooseFile(device.deviceId, index, 2, wsActions)">
|
||||||
|
传送文件</div> -->
|
||||||
|
<!-- <div style="display: flex;">
|
||||||
|
<input style="border: 1px solid #000;margin:0px 14px;" v-model="textContent[index]" type="text"></input>
|
||||||
|
<div class="app-button" style="margin: 0px;height: 40px;width: 60px;font-size: 14px;"
|
||||||
|
@click="setComText(index)">发送
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right center-justify" @click.self="selectedDevice = 999">
|
||||||
|
<!-- <div style="margin: 30px;"></div> -->
|
||||||
|
<ChatDialog :visible="openShowChat" :messages="chatList" />
|
||||||
|
</div>
|
||||||
|
<MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice"
|
||||||
|
@confirm="onDialogConfirm" @cancel="stopAll" />
|
||||||
|
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 定时调度配置弹窗 -->
|
||||||
|
<el-dialog v-model="showScheduleDlg" title="定时调度(每小时)" width="420px">
|
||||||
|
<div style="display:grid;grid-template-columns: 100px 1fr; gap:12px; align-items:center;">
|
||||||
|
<div>片段 A</div>
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<el-select v-model="schedAKey" style="width:140px;">
|
||||||
|
<el-option label="一键关注" value="follow" />
|
||||||
|
<el-option label="刷视频(养号)" value="like" />
|
||||||
|
<el-option label="刷直播" value="brushLive" />
|
||||||
|
<el-option label="监测消息" value="listen" />
|
||||||
|
</el-select>
|
||||||
|
<el-input-number v-model="schedAMin" :min="1" :max="59" />
|
||||||
|
<span>分钟</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>片段 B</div>
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<el-select v-model="schedBKey" style="width:140px;">
|
||||||
|
<!-- <el-option label="一键关注" value="follow" /> -->
|
||||||
|
<el-option label="刷视频(养号)" value="like" />
|
||||||
|
<el-option label="刷直播" value="brushLive" />
|
||||||
|
<el-option label="监测消息" value="listen" />
|
||||||
|
</el-select>
|
||||||
|
<el-input-number v-model="schedBMin" :min="1" :max="59" />
|
||||||
|
<span>分钟</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>总时长</div>
|
||||||
|
<div><b>{{ schedAMin + schedBMin }}</b> 分钟(必须等于 60)</div>
|
||||||
|
|
||||||
|
<div>启用调度</div>
|
||||||
|
<div><el-switch v-model="scheduleEnabled" /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showScheduleDlg = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveSchedule">保存并生效</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, onBeforeUnmount, watch, inject } from "vue";
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import {
|
||||||
|
setphoneXYinfo, getphoneXYinfo, getUser,
|
||||||
|
getHostList, setHostList, getContentpriList,
|
||||||
|
setContentpriList, getContentList, setContentList,
|
||||||
|
setsessionId, getsessionId
|
||||||
|
} from '@/stores/storage'
|
||||||
|
import { connectSSE } from '@/utils/sseUtils'
|
||||||
|
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
||||||
|
import { chat, translationToChinese, translation } from "@/api/chat";
|
||||||
|
import HostListManagerDialog from '@/components/HostListManagerDialog.vue'
|
||||||
|
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue'; // 根据实际路径修改
|
||||||
|
import ChatDialog from '@/components/ChatDialog.vue'
|
||||||
|
import { pickTikTokBundleId } from '@/utils/arrUtils'
|
||||||
|
import { logout } from '@/api/account';
|
||||||
|
import {
|
||||||
|
getDeviceList,
|
||||||
|
toHome,
|
||||||
|
swipeAction,
|
||||||
|
tapAction,
|
||||||
|
growAccount,
|
||||||
|
stopScript,
|
||||||
|
watchLiveForGrowth,
|
||||||
|
monitorMessages,
|
||||||
|
passAnchorData,
|
||||||
|
addTempAnchorData,
|
||||||
|
deviceAppList,
|
||||||
|
launchApp,
|
||||||
|
getChatTextInfo,
|
||||||
|
setLoginInfo,
|
||||||
|
|
||||||
|
|
||||||
|
} from '@/api/ios';
|
||||||
|
const router = useRouter();
|
||||||
|
const openShowChat = ref([true])
|
||||||
|
//主播库
|
||||||
|
const showHostDlg = ref(false)
|
||||||
|
|
||||||
|
let hostList = []
|
||||||
|
//查询列表轮询
|
||||||
|
let getListtimer = null;
|
||||||
|
let userdata = getUser();
|
||||||
|
let chatList = ref([])
|
||||||
|
let isImg = ref(true)
|
||||||
|
// 引入刷新方法
|
||||||
|
const reload = inject("reload")
|
||||||
|
const reloadImg = () => {
|
||||||
|
isImg.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
isImg.value = true
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
//start弹窗
|
||||||
|
let isMsgPop = ref(false)
|
||||||
|
//缓存主播联动数组
|
||||||
|
let stroageHost = ref([])
|
||||||
|
let runType = ref('')
|
||||||
|
let isMonitorOn = ref(false)
|
||||||
|
const hoverIndex = ref(null) //选中
|
||||||
|
let showDialog = ref(false);//弹窗是否显示
|
||||||
|
let dialogTitle = ref('');//当前弹窗类型
|
||||||
|
let deviceInformation = ref([])
|
||||||
|
// 你可以用这种方式声明按钮们
|
||||||
|
//联动用作标记
|
||||||
|
let batchMode = ref('init'); // 'init' | 'follow'(仅作标记)
|
||||||
|
|
||||||
|
// 当前是否被其它模式占用(四个互斥按钮专用)
|
||||||
|
const isLocked = (type) => !!runType.value && runType.value !== type
|
||||||
|
|
||||||
|
// 互斥按钮的样式:激活=红,锁定=半透明且禁点
|
||||||
|
const ctrlStyle = (type) => ({
|
||||||
|
backgroundColor: runType.value === type ? 'red' : '',
|
||||||
|
opacity: isLocked(type) ? 0.5 : 1,
|
||||||
|
pointerEvents: isLocked(type) ? 'none' : 'auto',
|
||||||
|
cursor: isLocked(type) ? 'not-allowed' : 'pointer',
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
label: '刷新',
|
||||||
|
onClick: () => reloadImg(),
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '打开tiktok',
|
||||||
|
onClick: () => openTk(),
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn2.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn2-2.png', import.meta.url).href
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '返回主页',
|
||||||
|
onClick: () => {
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
toHome({ udid: item.deviceId })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn3.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn3-3.png', import.meta.url).href
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '打开直播',
|
||||||
|
onClick: () => {
|
||||||
|
if (runType.value == 'brushLive') {
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
stopScript({ udid: item.deviceId })
|
||||||
|
})
|
||||||
|
runType.value = ''
|
||||||
|
return
|
||||||
|
};
|
||||||
|
|
||||||
|
// 若被其它模式占用:直接返回(已在样式层禁点,这里双保险)
|
||||||
|
if (isLocked('brushLive')) return
|
||||||
|
|
||||||
|
runType.value = 'brushLive'
|
||||||
|
deviceInformation.value.forEach((item) => watchLiveForGrowth({ udid: item.deviceId }))
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn4.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn4-4.png', import.meta.url).href
|
||||||
|
},
|
||||||
|
style: () => ctrlStyle('brushLive')
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '一键养号',
|
||||||
|
onClick: () => {
|
||||||
|
if (runType.value == 'like') {
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
stopScript({ udid: item.deviceId })
|
||||||
|
})
|
||||||
|
runType.value = ''
|
||||||
|
return
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLocked('like')) return
|
||||||
|
|
||||||
|
runType.value = 'like'
|
||||||
|
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn5.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn5-5.png', import.meta.url).href
|
||||||
|
},
|
||||||
|
style: () => ctrlStyle('like')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '一键关注并打招呼',
|
||||||
|
onClick: () => {
|
||||||
|
|
||||||
|
if (runType.value == 'follow') {
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
stopScript({ udid: item.deviceId })
|
||||||
|
})
|
||||||
|
runType.value = ''
|
||||||
|
return
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLocked('follow')) return
|
||||||
|
showDialog.value = true;
|
||||||
|
dialogTitle.value = '主播ID';
|
||||||
|
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn6.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn6-6.png', import.meta.url).href
|
||||||
|
},
|
||||||
|
style: () => ctrlStyle('follow')
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '监测消息',
|
||||||
|
onClick: () => {
|
||||||
|
if (runType.value == 'lisen') {
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
stopScript({ udid: item.deviceId })
|
||||||
|
})
|
||||||
|
runType.value = ''
|
||||||
|
return
|
||||||
|
};
|
||||||
|
if (isLocked('lisen')) return
|
||||||
|
|
||||||
|
runType.value = 'lisen'
|
||||||
|
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
|
||||||
|
},
|
||||||
|
style: () => ctrlStyle('lisen')
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '定时调度',
|
||||||
|
onClick: () => openScheduleDialog(),
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '全部停止',
|
||||||
|
onClick: () => stopAll(),
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn8.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn8-8.png', import.meta.url).href
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '登出',
|
||||||
|
onClick: () => {
|
||||||
|
logout({ userId: userdata.id, tenantId: userdata.tenantId })
|
||||||
|
router.push('/')
|
||||||
|
|
||||||
|
},
|
||||||
|
show: () => true,
|
||||||
|
img: {
|
||||||
|
normal: new URL('@/assets/video/leftBtn9.png', import.meta.url).href,
|
||||||
|
hover: new URL('@/assets/video/leftBtn9-9.png', import.meta.url).href
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const schedulePlan = [
|
||||||
|
{ key: 'follow', duration: 40 * 60 * 1000 },
|
||||||
|
{ key: 'like', duration: 20 * 60 * 1000 },
|
||||||
|
]
|
||||||
|
// 调度状态(持久化一下,避免刷新丢失)
|
||||||
|
let scheduleState = (() => {
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem('SCHEDULE_STATE') || '{}')
|
||||||
|
if (saved && typeof saved.index === 'number' && typeof saved.startTime === 'number') {
|
||||||
|
return saved
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
return { index: 0, startTime: Date.now() }
|
||||||
|
})()
|
||||||
|
|
||||||
|
let scheduleTimer = null // 轮询定时器句柄
|
||||||
|
const scheduleTickMs = 30_000 // 每 30s 检查一次是否到切换点
|
||||||
|
let scheduleEnabled = ref(true) // 需要时可手动关闭调度(例如“全部停止”)
|
||||||
|
// 弹窗
|
||||||
|
const showScheduleDlg = ref(false)
|
||||||
|
|
||||||
|
// 两个时间片(默认 A=follow 40min,B=like 20min)
|
||||||
|
const schedAKey = ref('follow')
|
||||||
|
const schedAMin = ref(40)
|
||||||
|
const schedBKey = ref('like')
|
||||||
|
const schedBMin = ref(20)
|
||||||
|
// 打开弹窗:把当前 schedulePlan 映射到 UI
|
||||||
|
function openScheduleDialog() {
|
||||||
|
// 把当前计划读出来(只支持两个片段的简易版)
|
||||||
|
if (Array.isArray(schedulePlan) && schedulePlan.length >= 2) {
|
||||||
|
const a = schedulePlan[0], b = schedulePlan[1]
|
||||||
|
schedAKey.value = a?.key || 'follow'
|
||||||
|
schedAMin.value = Math.max(1, Math.round((a?.duration || 40 * 60_000) / 60_000))
|
||||||
|
schedBKey.value = b?.key || 'like'
|
||||||
|
schedBMin.value = Math.max(1, Math.round((b?.duration || 20 * 60_000) / 60_000))
|
||||||
|
}
|
||||||
|
showScheduleDlg.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存:校验=60 分钟 → 更新 schedulePlan → 持久化 → 重启轮询
|
||||||
|
function saveSchedule() {
|
||||||
|
const total = schedAMin.value + schedBMin.value
|
||||||
|
if (total !== 60) {
|
||||||
|
ElMessage.error('两个片段相加必须等于 60 分钟')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schedulePlan.splice(0, schedulePlan.length,
|
||||||
|
{ key: schedAKey.value, duration: schedAMin.value * 60_000 },
|
||||||
|
{ key: schedBKey.value, duration: schedBMin.value * 60_000 },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 存 localStorage
|
||||||
|
localStorage.setItem('SCHEDULE_PLAN', JSON.stringify(schedulePlan))
|
||||||
|
localStorage.setItem('SCHEDULE_ENABLED', JSON.stringify(!!scheduleEnabled.value))
|
||||||
|
|
||||||
|
// 重置时间片起点并立即生效
|
||||||
|
scheduleState.index = 0
|
||||||
|
scheduleState.startTime = Date.now()
|
||||||
|
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
||||||
|
|
||||||
|
// 若启用则重启轮询
|
||||||
|
if (scheduleEnabled.value) startScheduleLoop()
|
||||||
|
|
||||||
|
showScheduleDlg.value = false
|
||||||
|
ElMessage.success('已保存定时调度')
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDevice = ref(null)
|
||||||
|
|
||||||
|
// —— 显示尺寸固定为 320x720;未选中缩略为 THUMB_SCALE 倍 ——
|
||||||
|
const BASE_W = 320
|
||||||
|
const BASE_H = 720
|
||||||
|
const THUMB_SCALE = 0.6
|
||||||
|
|
||||||
|
// 当前选中的卡片:选中=1倍,未选中=缩略比例
|
||||||
|
const imgWH = (index) => {
|
||||||
|
const scale = (selectedDevice.value === index) ? 1 : THUMB_SCALE
|
||||||
|
return {
|
||||||
|
width: `${BASE_W * scale}px`,
|
||||||
|
height: `${BASE_H * scale}px`,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算某索引当前展示宽高(用于坐标换算)
|
||||||
|
const displaySize = (index) => {
|
||||||
|
const scale = (selectedDevice.value === index) ? 1 : THUMB_SCALE
|
||||||
|
return { w: BASE_W * scale, h: BASE_H * scale }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Canvas offset 坐标 → 真实手机分辨率坐标
|
||||||
|
const mapToDeviceXY = (index, offsetX, offsetY) => {
|
||||||
|
const dev = deviceInformation.value[index] || {}
|
||||||
|
const realW = Number(dev.width) || BASE_W // 后端返回的真实分辨率
|
||||||
|
const realH = Number(dev.height) || BASE_H
|
||||||
|
const rotation = Number(dev.rotation || 0) // 若后端有提供旋转角,可用 0/90/180/270
|
||||||
|
|
||||||
|
const { w: dispW, h: dispH } = displaySize(index)
|
||||||
|
|
||||||
|
// 归一化到 0~1
|
||||||
|
let nx = Math.min(Math.max(offsetX / dispW, 0), 1)
|
||||||
|
let ny = Math.min(Math.max(offsetY / dispH, 0), 1)
|
||||||
|
|
||||||
|
// 处理旋转(如果你的服务器坐标基于设备原生朝向)
|
||||||
|
// 0: 直接映射;90: 顺时针;180、270 同理
|
||||||
|
let x, y
|
||||||
|
switch (rotation % 360) {
|
||||||
|
case 90:
|
||||||
|
case -270:
|
||||||
|
x = Math.round(ny * realW)
|
||||||
|
y = Math.round((1 - nx) * realH)
|
||||||
|
break
|
||||||
|
case 180:
|
||||||
|
case -180:
|
||||||
|
x = Math.round((1 - nx) * realW)
|
||||||
|
y = Math.round((1 - ny) * realH)
|
||||||
|
break
|
||||||
|
case 270:
|
||||||
|
case -90:
|
||||||
|
x = Math.round((1 - ny) * realW)
|
||||||
|
y = Math.round(nx * realH)
|
||||||
|
break
|
||||||
|
default: // 0°
|
||||||
|
x = Math.round(nx * realW)
|
||||||
|
y = Math.round(ny * realH)
|
||||||
|
}
|
||||||
|
return { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中:恢复到 320x720 并显示盖层
|
||||||
|
const selectDevice = (index) => {
|
||||||
|
selectedDevice.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— 鼠标交互:按下/移动/抬起 ———
|
||||||
|
const dragState = ref({}) // 以 index 作为 key 保存 {ox, oy, t}
|
||||||
|
|
||||||
|
const onCanvasDown = (udid, e, index) => {
|
||||||
|
// 记录起点(Canvas 内的 offset)
|
||||||
|
dragState.value[index] = { ox: e.offsetX, oy: e.offsetY, t: Date.now(), udid }
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCanvasMove = (udid, e, index) => {
|
||||||
|
// 如需在 overlay 上画指示、十字线等,可在这里使用 e.offsetX/e.offsetY
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCanvasUp = async (udid, e, index) => {
|
||||||
|
const st = dragState.value[index]
|
||||||
|
if (!st) return
|
||||||
|
|
||||||
|
const { ox, oy, t } = st
|
||||||
|
const dx = e.offsetX - ox
|
||||||
|
const dy = e.offsetY - oy
|
||||||
|
const elapsed = Date.now() - t
|
||||||
|
delete dragState.value[index]
|
||||||
|
|
||||||
|
// 映射到真实分辨率
|
||||||
|
const p0 = mapToDeviceXY(index, ox, oy)
|
||||||
|
const p1 = mapToDeviceXY(index, e.offsetX, e.offsetY)
|
||||||
|
console.log(" x, y", p0, p1)
|
||||||
|
// 判断是“点按”还是“滑动”
|
||||||
|
const MOVE_THR = 5 // 像素阈值(可按需调整)
|
||||||
|
const isTap = Math.hypot(dx, dy) < MOVE_THR && elapsed < 500
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isTap) {
|
||||||
|
await tapAction({ udid, x: p1.x, y: p1.y })
|
||||||
|
// console.log('tap', p1)
|
||||||
|
} else {
|
||||||
|
// 只需要方向:1上/2左/3下/4右
|
||||||
|
const rotation = Number((deviceInformation.value[index] || {}).rotation || 0)
|
||||||
|
|
||||||
|
const code = getSwipeCodeWithRotation(dx, dy, rotation) // 必定得到1~4
|
||||||
|
console.log("code", code)
|
||||||
|
await swipeAction({ udid, direction: code })
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 方向码:1=上, 2=左, 3=下, 4=右(不返回0,始终给出一个方向) */
|
||||||
|
function getSwipeCode(dx, dy) {
|
||||||
|
// 哪个轴位移更大就取哪个轴;边界≈45°
|
||||||
|
if (Math.abs(dx) >= Math.abs(dy)) {
|
||||||
|
return dx < 0 ? 2 : 4 // 左/右
|
||||||
|
} else {
|
||||||
|
return dy < 0 ? 1 : 3 // 上/下(DOM坐标里向上是负)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 带设备旋转(0/90/180/270):先把画布向量(dx,dy)旋回设备坐标系再判方向 */
|
||||||
|
function getSwipeCodeWithRotation(dx, dy, rotation = 0) {
|
||||||
|
let dxD = dx, dyD = dy
|
||||||
|
switch ((rotation % 360 + 360) % 360) {
|
||||||
|
case 90: dxD = dy; dyD = -dx; break
|
||||||
|
case 180: dxD = -dx; dyD = -dy; break
|
||||||
|
case 270: dxD = -dy; dyD = dx; break
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
return getSwipeCode(dxD, dyD)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function openTk() {
|
||||||
|
if (!deviceInformation.value?.length) {
|
||||||
|
ElMessage.warning('暂无在线设备')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ElLoading.service({ text: '正在打开 TikTok …', background: 'rgba(0,0,0,.35)' })
|
||||||
|
const results = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 为了稳妥,逐台串行(如果你希望更快,可改 Promise.all 并注意并发数)
|
||||||
|
for (const dev of deviceInformation.value) {
|
||||||
|
const udid = dev.deviceId
|
||||||
|
try {
|
||||||
|
const apps = await deviceAppList({ udid }) // 期望返回示例中的数组
|
||||||
|
const bundleId = pickTikTokBundleId(apps)
|
||||||
|
|
||||||
|
if (!bundleId) {
|
||||||
|
results.push({ udid, ok: false, msg: '未找到 TikTok' })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await launchApp({ udid, bundleId })
|
||||||
|
results.push({ udid, ok: true, msg: `已启动 TikTok (${bundleId})` })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('openTk error', udid, e)
|
||||||
|
results.push({ udid, ok: false, msg: '请求失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 汇总提示(成功/失败各一条)
|
||||||
|
const okCount = results.filter(r => r.ok).length
|
||||||
|
const fail = results.filter(r => !r.ok)
|
||||||
|
if (okCount) ElMessage.success(`已在 ${okCount} 台设备启动 TikTok`)
|
||||||
|
if (fail.length) {
|
||||||
|
const udids = fail.map(f => f.udid).join(', ')
|
||||||
|
ElMessage.error(`以下设备未能启动:${udids}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMesList(deviceId) {
|
||||||
|
getChatTextInfo({ udid: deviceId }).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
chatList.value = res
|
||||||
|
console.log(chatList.value)
|
||||||
|
getTranslation(chatList.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAll() {
|
||||||
|
if (!runType.value) return
|
||||||
|
deviceInformation.value.forEach((item) => {
|
||||||
|
stopScript({ udid: item.deviceId }).then((res) => {
|
||||||
|
ElMessage.success(`停止成功:${item.deviceId}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
scheduleEnabled.value = false
|
||||||
|
runType.value = ''
|
||||||
|
batchMode.value = 'init';
|
||||||
|
}
|
||||||
|
|
||||||
|
//确认多行文本框内容
|
||||||
|
function onDialogConfirm(result, type, index, isMon) {
|
||||||
|
// console.log(type, result, isMon);
|
||||||
|
if (type == '主播ID') {
|
||||||
|
hostList = result
|
||||||
|
|
||||||
|
dialogTitle.value = '私信';
|
||||||
|
setTimeout(() => {
|
||||||
|
showDialog.value = true;
|
||||||
|
}, 600)
|
||||||
|
} else if (type == '私信') {
|
||||||
|
runType.value = 'follow'
|
||||||
|
passAnchorData(
|
||||||
|
{
|
||||||
|
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||||
|
anchorList: hostList.map(id => ({
|
||||||
|
anchorId: id,
|
||||||
|
country: ""
|
||||||
|
})),
|
||||||
|
prologueList: result,
|
||||||
|
needReply: isMon
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: 'Loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
});
|
||||||
|
getDeviceListFun()
|
||||||
|
window.electronAPI.startMq(userdata.tenantId, userdata.id)
|
||||||
|
// 初始化时获取设备列表
|
||||||
|
getListtimer = setInterval(() => {
|
||||||
|
loading.close();
|
||||||
|
getDeviceListFun()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 批次缓冲(仅用于当前“波”)
|
||||||
|
let batch = []; // [{ country, text }]
|
||||||
|
let flushTimer = null;
|
||||||
|
|
||||||
|
function scheduleFlush(handler, delay = 400) {
|
||||||
|
if (flushTimer) clearTimeout(flushTimer);
|
||||||
|
flushTimer = setTimeout(() => {
|
||||||
|
if (batch.length) {
|
||||||
|
const items = batch.slice(); // 拷贝一份
|
||||||
|
batch.length = 0; // 清空批次
|
||||||
|
try {
|
||||||
|
handler(items);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SSE flush error]', e);
|
||||||
|
// 出错不回灌,避免重复提交;必要时可根据需要 batch.push(...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// —— SSE 接收 ——
|
||||||
|
const es = connectSSE('http://localhost:3312/events', (data) => {
|
||||||
|
console.log('来自服务端:', data);
|
||||||
|
|
||||||
|
if (data === 'start') {
|
||||||
|
// 新一波开始:根据当前状态决定“本波 flush 用谁”
|
||||||
|
if (!isMsgPop.value) {
|
||||||
|
// 还没开始过 -> 首次弹框,确认后使用 passAnchorData 处理本波
|
||||||
|
isMsgPop.value = true;
|
||||||
|
batchMode.value = 'init';
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'检测到YOLO助手正在爬取主播,是否进行操作?',
|
||||||
|
'消息提醒',
|
||||||
|
{ confirmButtonText: '开始', cancelButtonText: '取消', type: 'success' }
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage({ type: 'success', message: '任务开启成功' });
|
||||||
|
runType.value = 'follow';
|
||||||
|
batchMode.value = 'follow';
|
||||||
|
|
||||||
|
// 不在这里立刻提交;让后续主播数据先进 batch,再由防抖统一 flush
|
||||||
|
// 若这一波的主播在点击之前已到达,也已经在 batch 等待
|
||||||
|
scheduleFlush((items) => {
|
||||||
|
passAnchorData({
|
||||||
|
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||||
|
anchorList: items.map(h => ({ anchorId: h.text, country: h.country || '' })),
|
||||||
|
needReply: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 取消:清理状态,丢弃批次
|
||||||
|
batch.length = 0;
|
||||||
|
isMsgPop.value = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 已经在运行 follow:本波用 addTempAnchorData 追加
|
||||||
|
batchMode.value = 'follow';
|
||||||
|
// 立刻安排一次“尾随防抖”flush,等本波数据齐了再送
|
||||||
|
scheduleFlush((items) => {
|
||||||
|
addTempAnchorData(items.map(h => ({ anchorId: h.text, country: h.country || '' })));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非 start:本波主播数据进入批次
|
||||||
|
const country = data && data.country != null ? data.country : '';
|
||||||
|
const text = data && (data.hostsId != null ? data.hostsId : data.text);
|
||||||
|
if (text == null) {
|
||||||
|
// 数据格式不对,丢弃或打印
|
||||||
|
console.warn('[SSE] 非法数据,缺少 hostsId/text:', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
batch.push({ country, text });
|
||||||
|
|
||||||
|
// 根据当前模式,刷新防抖(让“最后一条到来后”延迟几百毫秒再统一提交)
|
||||||
|
if (batchMode.value === 'init') {
|
||||||
|
// 首次确认前:等用户点“开始”后由上面的 scheduleFlush 执行 passAnchorData
|
||||||
|
scheduleFlush((items) => {
|
||||||
|
// 安全起见:只有在 runType 已经 follow 时才真正提交
|
||||||
|
if (runType.value === 'follow') {
|
||||||
|
passAnchorData({
|
||||||
|
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||||
|
anchorList: items.map(h => ({ anchorId: h.text, country: h.country || '' })),
|
||||||
|
needReply: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 还没开始就来了数据:把它们留回批次,等待上面 then 里的 scheduleFlush 再处理
|
||||||
|
batch.push(...items);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 已在关注:走追加逻辑
|
||||||
|
scheduleFlush((items) => {
|
||||||
|
addTempAnchorData(items.map(h => ({ anchorId: h.text, country: h.country || '' })));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(getListtimer)
|
||||||
|
getListtimer = null
|
||||||
|
logout({ userId: userdata.id, tenantId: userdata.tenantId })
|
||||||
|
})
|
||||||
|
const getDeviceListFun = () => {
|
||||||
|
getDeviceList().then((res) => {
|
||||||
|
// console.log('返回', res.length)
|
||||||
|
if (res && res.length > 0 && deviceInformation.value.length !== res.length) {
|
||||||
|
console.log("设备变更")
|
||||||
|
deviceInformation.value = res
|
||||||
|
reloadImg()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.length == 0) {
|
||||||
|
deviceInformation.value = []
|
||||||
|
reloadImg()
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
ElMessage.error(`IOSAI服务错误`)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadLogFile() {
|
||||||
|
let loading = null
|
||||||
|
try {
|
||||||
|
// 先弹出确认框
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定要上传日志文件吗?',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// 如果点了确定,就会走到这里
|
||||||
|
loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: '上传中...',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
})
|
||||||
|
const res = await setLoginInfo({
|
||||||
|
"tenantId": userdata.tenantId,
|
||||||
|
"userId": userdata.id,
|
||||||
|
"token": userdata.tokenValue
|
||||||
|
})
|
||||||
|
loading.close()
|
||||||
|
console.log("上传文件返回", res)
|
||||||
|
if (res) {
|
||||||
|
console.log("✅ 上传成功:", res)
|
||||||
|
ElMessage.success('✅ 上传成功')
|
||||||
|
} else {
|
||||||
|
console.error("❌ 上传失败:", res.msg)
|
||||||
|
ElMessage.error('❌ 上传失败: ' + (res.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (loading) {
|
||||||
|
loading.close()
|
||||||
|
}
|
||||||
|
// 如果用户点了取消,会进入这里
|
||||||
|
if (err === 'cancel' || err === 'close') {
|
||||||
|
ElMessage.info('已取消上传')
|
||||||
|
} else {
|
||||||
|
console.error("❌ 上传异常:", err)
|
||||||
|
ElMessage.error('❌ 上传异常: ' + err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function runTask(key) {
|
||||||
|
if (!scheduleEnabled.value) return
|
||||||
|
console.log('[schedule] 切换到任务:', key)
|
||||||
|
|
||||||
|
forceActivate(key, () => {
|
||||||
|
if (key === 'follow') {
|
||||||
|
runType.value = 'follow'
|
||||||
|
// 这三行保持你现有的唤起流程(弹“主播ID”输入等)
|
||||||
|
// showDialog.value = true
|
||||||
|
// dialogTitle.value = '主播ID'
|
||||||
|
// selectedDevice.value = 999
|
||||||
|
showDialog.value = true;
|
||||||
|
dialogTitle.value = '主播ID';
|
||||||
|
} else if (key === 'like') {
|
||||||
|
runType.value = 'like'
|
||||||
|
stopAll()
|
||||||
|
setTimeout(() => {
|
||||||
|
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
} else if (key === 'brushLive') {
|
||||||
|
stopAll()
|
||||||
|
setTimeout(() => {
|
||||||
|
deviceInformation.value.forEach((item) => watchLiveForGrowth({ udid: item.deviceId }))
|
||||||
|
}, 1000)
|
||||||
|
runType.value = 'brushLive'
|
||||||
|
} else if (key === 'listen') {
|
||||||
|
stopAll()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
runType.value = 'listen'
|
||||||
|
isMonitorOn.value = true
|
||||||
|
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function startScheduleLoop() {
|
||||||
|
// 先按照当前 index 跑一次,保证“即刻对齐”
|
||||||
|
runTask(schedulePlan[scheduleState.index].key)
|
||||||
|
|
||||||
|
// 清理旧轮询,防止重复
|
||||||
|
if (scheduleTimer) clearInterval(scheduleTimer)
|
||||||
|
|
||||||
|
scheduleTimer = setInterval(() => {
|
||||||
|
if (!scheduleEnabled.value) return
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const cur = schedulePlan[scheduleState.index]
|
||||||
|
const elapsed = now - scheduleState.startTime
|
||||||
|
|
||||||
|
if (elapsed >= cur.duration) {
|
||||||
|
// 进入下一个时间片
|
||||||
|
scheduleState.index = (scheduleState.index + 1) % schedulePlan.length
|
||||||
|
scheduleState.startTime = now
|
||||||
|
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
||||||
|
|
||||||
|
runTask(schedulePlan[scheduleState.index].key)
|
||||||
|
}
|
||||||
|
}, scheduleTickMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceActivate(key, runner) {
|
||||||
|
// 跳过互斥逻辑,直接切换
|
||||||
|
runType.value = key;
|
||||||
|
if (typeof runner === 'function') runner();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function mqSend() {
|
||||||
|
window.electronAPI.mqSend("start")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getTranslation(list) {
|
||||||
|
list.forEach((item, index) => {
|
||||||
|
translationToChinese({ msg: item.text }).then(res => {
|
||||||
|
console.log(res);
|
||||||
|
chatList.value[index].text = res
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHostSaved(list) {
|
||||||
|
console.log('保存后的主播id:', list)
|
||||||
|
}
|
||||||
|
// //sse接收爬虫发送的消息
|
||||||
|
// const es = connectSSE(`http://localhost:3311/events`, (data) => {
|
||||||
|
// // connectSSE(`http://192.168.1.155:19665/api/sse/connect/${userdata.tenantId}/${userdata.id}`, (data) => {
|
||||||
|
// // 处理服务端推送的数据
|
||||||
|
// console.log('来自服务端:', data)
|
||||||
|
|
||||||
|
// //接收到start
|
||||||
|
// if (data === 'start') {
|
||||||
|
|
||||||
|
// } else {
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// })
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
@import '../static/css/video.less';
|
||||||
|
</style>
|
||||||
27
vue.config.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const { defineConfig } = require('@vue/cli-service');
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
devServer: {
|
||||||
|
// client: {
|
||||||
|
// overlay: false, // 重点:新版写法
|
||||||
|
|
||||||
|
// },
|
||||||
|
historyApiFallback: true,
|
||||||
|
},
|
||||||
|
transpileDependencies: true,
|
||||||
|
publicPath: './', // 必须是相对路径,否则打包后图片路径会出错
|
||||||
|
css: {
|
||||||
|
loaderOptions: {
|
||||||
|
postcss: {
|
||||||
|
postcssOptions: {
|
||||||
|
plugins: [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
less: {
|
||||||
|
additionalData: `@import "@/static/css/app.less";` // 注入全局变量文件
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||