直播场次
This commit is contained in:
@@ -1,103 +1,55 @@
|
||||
<!-- LiveRecordDialog.vue -->
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="直播记录" width="86vw" top="6vh" :close-on-click-modal="false" destroy-on-close>
|
||||
<!-- 顶部工具栏 -->
|
||||
<el-dialog v-model="visible" title="直播记录" width="80vw" top="6vh" :close-on-click-modal="false" destroy-on-close>
|
||||
<div class="toolbar">
|
||||
<div class="left">
|
||||
<el-input v-model="kw" placeholder="搜索:hostsId / userId / tenantId / id" clearable
|
||||
style="width: 320px" />
|
||||
<el-select v-model="sortKey" style="width: 170px" placeholder="排序字段">
|
||||
<el-option label="开始时间" value="startTimeFormatted" />
|
||||
<el-option label="结束时间" value="endTimeFormatted" />
|
||||
<el-option label="点赞数" value="likeCount" />
|
||||
<el-option label="粉丝团" value="fansClubCount" />
|
||||
</el-select>
|
||||
<el-select v-model="sortOrder" style="width: 140px" placeholder="排序方式">
|
||||
<el-option label="降序" value="desc" />
|
||||
<el-option label="升序" value="asc" />
|
||||
</el-select>
|
||||
|
||||
<el-checkbox v-model="onlyAbnormal">只看异常</el-checkbox>
|
||||
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<el-tag type="info">总条数:{{ filteredRows.length }}</el-tag>
|
||||
<el-tag type="success">点赞合计:{{ totalLikes }}</el-tag>
|
||||
<el-tag type="warning">like=0:{{ zeroLikeCount }}</el-tag>
|
||||
<el-tag type="warning">无点赞:{{ zeroLikeCount }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="pagedRows" border height="62vh" style="width: 100%"
|
||||
:default-sort="{ prop: 'startTimeFormatted', order: 'descending' }">
|
||||
<el-table-column prop="id" label="ID" width="90" />
|
||||
<el-table-column prop="userId" label="userId" width="90" />
|
||||
<el-table-column prop="hostsId" label="hostsId" width="130" />
|
||||
<el-table-column prop="tenantId" label="tenantId" width="100" />
|
||||
<el-table :data="filteredRows" border height="62vh" style="width: 100%"
|
||||
:default-sort="{ prop: 'startTimeFormatted', order: 'descending' }" table-layout="auto">
|
||||
<el-table-column prop="hostsId" label="主播id" />
|
||||
<el-table-column prop="startTimeFormatted" label="开始时间" />
|
||||
<el-table-column prop="endTimeFormatted" label="结束时间" />
|
||||
<el-table-column prop="durationFormatted" label="时长" />
|
||||
|
||||
<el-table-column prop="fansClubCount" label="粉丝团" width="90" />
|
||||
<el-table-column prop="lightedVsTotalGifts" label="点亮/礼物" width="110" />
|
||||
|
||||
<el-table-column prop="startTimeFormatted" label="开始时间" width="170" />
|
||||
<el-table-column prop="endTimeFormatted" label="结束时间" width="170" />
|
||||
<el-table-column prop="durationFormatted" label="时长" width="120" />
|
||||
|
||||
<el-table-column prop="likeCount" label="点赞" width="110" sortable>
|
||||
<el-table-column prop="likeCount" label="点赞" sortable>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.likeCount === 0" type="danger">0</el-tag>
|
||||
<span v-else>{{ row.likeCount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="createTime" label="入库时间" width="170" />
|
||||
|
||||
<el-table-column label="操作" fixed="right" width="160">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="copyRow(row)">复制</el-button>
|
||||
<el-button size="small" type="primary" @click="emitSelect(row)">选中</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="入库时间" />
|
||||
</el-table>
|
||||
|
||||
<div class="footer">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="filteredRows.length" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
<script setup lang="js">
|
||||
import { computed, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
type Row = {
|
||||
id: number;
|
||||
userId: number;
|
||||
hostsId: string;
|
||||
tenantId: number;
|
||||
fansClubCount: number;
|
||||
lightedVsTotalGifts: string;
|
||||
startTimeFormatted: string;
|
||||
endTimeFormatted: string;
|
||||
likeCount: number;
|
||||
durationFormatted: string;
|
||||
createTime: string;
|
||||
};
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
rows: Row[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", v: boolean): void;
|
||||
(e: "select", row: Row): void;
|
||||
}>();
|
||||
const emit = defineEmits(["update:modelValue", "select"]);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
@@ -105,25 +57,16 @@ const visible = computed({
|
||||
});
|
||||
|
||||
const kw = ref("");
|
||||
const sortKey = ref<keyof Row>("startTimeFormatted");
|
||||
const sortOrder = ref<"asc" | "desc">("desc");
|
||||
const sortKey = ref("startTimeFormatted");
|
||||
const sortOrder = ref("desc");
|
||||
const onlyAbnormal = ref(false);
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
|
||||
watch([kw, sortKey, sortOrder, onlyAbnormal], () => {
|
||||
page.value = 1;
|
||||
});
|
||||
|
||||
function parseTime(s?: string) {
|
||||
// "2025-12-18 13:12:11" -> Date
|
||||
function parseTime(s) {
|
||||
if (!s) return 0;
|
||||
return new Date(s.replace(" ", "T")).getTime() || 0;
|
||||
}
|
||||
|
||||
function durationSeconds(durationFormatted?: string) {
|
||||
// 例:"2小时49分钟55秒" / "18分钟31秒" / "25秒"
|
||||
function durationSeconds(durationFormatted) {
|
||||
if (!durationFormatted) return 0;
|
||||
const h = Number((durationFormatted.match(/(\d+)\s*小时/) || [])[1] || 0);
|
||||
const m = Number((durationFormatted.match(/(\d+)\s*分钟/) || [])[1] || 0);
|
||||
@@ -144,27 +87,23 @@ const filteredRows = computed(() => {
|
||||
arr = arr.filter((r) => r.likeCount === 0 || durationSeconds(r.durationFormatted) < 60);
|
||||
}
|
||||
|
||||
// 排序
|
||||
const key = sortKey.value;
|
||||
const order = sortOrder.value;
|
||||
|
||||
arr = [...arr].sort((a, b) => {
|
||||
const av = a[key] as any;
|
||||
const bv = b[key] as any;
|
||||
const av = a[key];
|
||||
const bv = b[key];
|
||||
|
||||
// 时间字段特殊处理
|
||||
if (key === "startTimeFormatted" || key === "endTimeFormatted" || key === "createTime") {
|
||||
const aa = parseTime(String(av));
|
||||
const bb = parseTime(String(bv));
|
||||
return order === "desc" ? bb - aa : aa - bb;
|
||||
}
|
||||
|
||||
// 数字
|
||||
if (typeof av === "number" && typeof bv === "number") {
|
||||
return order === "desc" ? bv - av : av - bv;
|
||||
}
|
||||
|
||||
// 字符串
|
||||
return order === "desc"
|
||||
? String(bv).localeCompare(String(av))
|
||||
: String(av).localeCompare(String(bv));
|
||||
@@ -173,11 +112,6 @@ const filteredRows = computed(() => {
|
||||
return arr;
|
||||
});
|
||||
|
||||
const pagedRows = computed(() => {
|
||||
const start = (page.value - 1) * pageSize.value;
|
||||
return filteredRows.value.slice(start, start + pageSize.value);
|
||||
});
|
||||
|
||||
const totalLikes = computed(() =>
|
||||
filteredRows.value.reduce((sum, r) => sum + (Number(r.likeCount) || 0), 0)
|
||||
);
|
||||
@@ -189,11 +123,9 @@ function reset() {
|
||||
sortKey.value = "startTimeFormatted";
|
||||
sortOrder.value = "desc";
|
||||
onlyAbnormal.value = false;
|
||||
page.value = 1;
|
||||
pageSize.value = 20;
|
||||
}
|
||||
|
||||
async function copyRow(row: Row) {
|
||||
async function copyRow(row) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(JSON.stringify(row, null, 2));
|
||||
ElMessage.success("已复制该行 JSON");
|
||||
@@ -202,7 +134,7 @@ async function copyRow(row: Row) {
|
||||
}
|
||||
}
|
||||
|
||||
function emitSelect(row: Row) {
|
||||
function emitSelect(row) {
|
||||
emit("select", row);
|
||||
ElMessage.success(`已选中:ID ${row.id}`);
|
||||
}
|
||||
@@ -231,10 +163,4 @@ function emitSelect(row: Row) {
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -17,8 +17,8 @@ let baseURL = ''
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 生产环境
|
||||
// baseURL = "https://api.tkpage.yolozs.com"
|
||||
baseURL = "http://192.168.2.21:8101"
|
||||
// baseURL = "https://crawlclient.api.yolozs.com"
|
||||
// baseURL = "http://192.168.2.21:8101"
|
||||
baseURL = "https://crawlclient.api.yolozs.com"
|
||||
} else {
|
||||
// 测试环境
|
||||
// baseURL = "http://120.26.251.180:8085/"
|
||||
|
||||
@@ -1,121 +1,134 @@
|
||||
// pythonBridge.js
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const bridge = ref(null);
|
||||
|
||||
// 统一安全调用,确保 Qt 响应有回调可执行
|
||||
const callBridge = (method, ...args) => {
|
||||
if (!bridge.value || typeof bridge.value[method] !== 'function') return;
|
||||
const last = args[args.length - 1];
|
||||
const hasCallback = typeof last === 'function';
|
||||
const callback = hasCallback ? args.pop() : () => { };
|
||||
bridge.value[method](...args, callback);
|
||||
};
|
||||
|
||||
// 防御:若 Qt 返回了未知 id,忽略以免 execCallbacks 报错
|
||||
const patchQWebChannel = () => {
|
||||
if (!window.QWebChannel || QWebChannel.__patchedIgnoreMissing) return;
|
||||
const originalHandleResponse = QWebChannel.prototype.handleResponse;
|
||||
QWebChannel.__patchedIgnoreMissing = true;
|
||||
QWebChannel.prototype.handleResponse = function (message) {
|
||||
const cb = this.execCallbacks && this.execCallbacks[message.id];
|
||||
if (message.id && typeof cb !== 'function') {
|
||||
console.warn('忽略未知的 WebChannel 响应', message);
|
||||
return;
|
||||
}
|
||||
return originalHandleResponse.call(this, message);
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化 QWebChannel
|
||||
const initBridge = () => {
|
||||
if (/localhost/.test(window.location.href)) return
|
||||
if (/localhost/.test(window.location.href)) return;
|
||||
patchQWebChannel();
|
||||
new QWebChannel(qt.webChannelTransport, (channel) => {
|
||||
// 兜底:任何缺失的回调都返回空函数,避免 execCallbacks 报错
|
||||
channel.execCallbacks = new Proxy(channel.execCallbacks || {}, {
|
||||
get(target, prop) {
|
||||
const val = target[prop];
|
||||
if (typeof val === 'function') return val;
|
||||
// 返回空函数,确保 handleResponse 可调用
|
||||
return () => {};
|
||||
},
|
||||
set(target, prop, value) {
|
||||
target[prop] = value;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
bridge.value = channel.objects.bridge;
|
||||
});
|
||||
};
|
||||
|
||||
export function usePythonBridge() {
|
||||
|
||||
|
||||
|
||||
|
||||
// 调用 Python 方法
|
||||
const fetchDataConfig = (data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (bridge.value) {
|
||||
bridge.value.fetchDataConfig(data, function (result) {
|
||||
return new Promise((resolve) => {
|
||||
if (!bridge.value) return resolve(null);
|
||||
callBridge('fetchDataConfig', data, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
// 查询获取主播的数据
|
||||
const fetchDataCount = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (bridge.value) {
|
||||
bridge.value.fetchDataCount(function (result) {
|
||||
return new Promise((resolve) => {
|
||||
if (!bridge.value) return resolve(null);
|
||||
callBridge('fetchDataCount', (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 打开 tk 后台
|
||||
const loginTikTok = () => {
|
||||
if (bridge.value) {
|
||||
bridge.value.loginTikTok(function (result) {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
callBridge('loginTikTok');
|
||||
};
|
||||
|
||||
// 登录 tk 后台
|
||||
const loginBackStage = (data) => {
|
||||
if (bridge.value) {
|
||||
if (data.index == 0) {
|
||||
bridge.value.loginBackStage(JSON.stringify(data));
|
||||
callBridge('loginBackStage', JSON.stringify(data));
|
||||
} else if (data.index == 1) {
|
||||
bridge.value.loginBackStageCopy(JSON.stringify(data));
|
||||
}
|
||||
|
||||
callBridge('loginBackStageCopy', JSON.stringify(data));
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到主播页面
|
||||
const givePyAnchorId = (id) => {
|
||||
|
||||
if (bridge.value) {
|
||||
bridge.value.givePyAnchorId(id, function (result) {
|
||||
|
||||
});
|
||||
}
|
||||
callBridge('givePyAnchorId', id);
|
||||
};
|
||||
|
||||
// 查询登录状态
|
||||
const backStageloginStatus = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (bridge.value) {
|
||||
bridge.value.backStageloginStatus(function (result) {
|
||||
return new Promise((resolve) => {
|
||||
if (!bridge.value) return resolve(null);
|
||||
callBridge('backStageloginStatus', (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
//查询登录状态
|
||||
|
||||
// 查询登录状态(副账号)
|
||||
const backStageloginStatusCopy = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (bridge.value) {
|
||||
bridge.value.backStageloginStatusCopy(function (result) {
|
||||
return new Promise((resolve) => {
|
||||
if (!bridge.value) return resolve(null);
|
||||
callBridge('backStageloginStatusCopy', (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// 导出表格
|
||||
const exportToExcel = (data) => {
|
||||
if (bridge.value) {
|
||||
bridge.value.exportToExcel(JSON.stringify(data));
|
||||
}
|
||||
|
||||
callBridge('exportToExcel', JSON.stringify(data));
|
||||
};
|
||||
const stopScript = () => {
|
||||
if (bridge.value) {
|
||||
bridge.value.stopScript();
|
||||
}
|
||||
|
||||
const stopScript = () => {
|
||||
callBridge('stopScript');
|
||||
};
|
||||
|
||||
// 获取版本号
|
||||
const getVersion = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (bridge.value) {
|
||||
bridge.value.currentVersion(function (result) {
|
||||
return new Promise((resolve) => {
|
||||
if (!bridge.value) return resolve(null);
|
||||
callBridge('currentVersion', (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 在组件挂载时初始化桥接
|
||||
onMounted(initBridge);
|
||||
|
||||
@@ -129,6 +142,6 @@ export function usePythonBridge() {
|
||||
backStageloginStatusCopy,
|
||||
exportToExcel,
|
||||
stopScript,
|
||||
getVersion
|
||||
getVersion,
|
||||
};
|
||||
}
|
||||
@@ -46,12 +46,20 @@
|
||||
<el-table-column prop="invitationType" :label="$t('hostList.invitationType')" width="80">
|
||||
<template #default="scope">
|
||||
|
||||
<el-tag :type="scope.row.invitationType == 1 ? 'success' : 'warning'" @click="getliveHost">
|
||||
<el-tag :type="scope.row.invitationType == 1 ? 'success' : 'warning'">
|
||||
{{ scope.row.invitationType == 1 ? $t('hostList.invitationType1') : $t('hostList.invitationType2') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="直播场次" width="120">
|
||||
<template #default="scope">
|
||||
<el-button class="live-btn" size="small" @click="getliveHost(scope.row.hostId)">
|
||||
查看场次
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-for="label in labelList" :key="label.paramCode" :prop="label.paramCode"
|
||||
:label="label.paramCodeMeaning" width="120">
|
||||
<template v-if="label.paramCode != 'createDt'" #default="scope">
|
||||
@@ -139,6 +147,9 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<LiveRecordDialog v-model:modelValue="liveDetailDialogVisible" :rows="liveDetailRecords"
|
||||
@select="handleLiveSelect" />
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
@@ -154,6 +165,7 @@ import { ref, reactive, onMounted } from 'vue';
|
||||
// import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
// import { color } from 'echarts';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import LiveRecordDialog from '@/components/LiveRecordDialog.vue'
|
||||
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -224,6 +236,9 @@ let staffId = ref({})
|
||||
let commentInfo = ref('')
|
||||
//备注信息主播
|
||||
let commentHost = ref('')
|
||||
let liveDetailDialogVisible = ref(false)
|
||||
let liveDetailRecords = ref([])
|
||||
let liveDetailLoading = ref(false)
|
||||
//分页
|
||||
let pageSize = ref(10)
|
||||
let page = ref(1)
|
||||
@@ -359,17 +374,29 @@ function handleClose(done) {
|
||||
}
|
||||
|
||||
|
||||
function getliveHost() {
|
||||
function getliveHost(hostId) {
|
||||
liveDetailLoading.value = true
|
||||
liveHostDetail(
|
||||
{
|
||||
"hostsId": "1296peahh",
|
||||
"hostsId": hostId,
|
||||
"tenantId": userInfo.value.tenantId
|
||||
}
|
||||
).then(res => {
|
||||
console.log("直播间信息列表", JSON.stringify(res))
|
||||
const detailList = Array.isArray(res) ? res : (res?.records || [])
|
||||
liveDetailRecords.value = detailList
|
||||
liveDetailDialogVisible.value = true
|
||||
}).catch(err => {
|
||||
console.log('liveHostDetail error', err)
|
||||
}).finally(() => {
|
||||
liveDetailLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleLiveSelect(row) {
|
||||
console.log('selected live row', row)
|
||||
liveDetailDialogVisible.value = false
|
||||
}
|
||||
|
||||
//修改主播维护状态
|
||||
// function handleSelectChange(event, data) {
|
||||
|
||||
@@ -518,6 +545,8 @@ function openHTML(id) {
|
||||
box-sizing: border-box;
|
||||
// height: 100vh;
|
||||
padding: 40px;
|
||||
background: linear-gradient(135deg, #f7fbff 0%, #f2f9f8 100%);
|
||||
border-radius: 16px;
|
||||
|
||||
/* 页面无法选中 */
|
||||
// -webkit-user-select: none;
|
||||
@@ -527,12 +556,38 @@ function openHTML(id) {
|
||||
|
||||
.hostTable {
|
||||
width: 100%;
|
||||
padding: 40px 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
|
||||
.hostIdText {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.live-btn {
|
||||
background: linear-gradient(90deg, #45a1ff, #5ad9ff);
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.live-btn:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
width: 100% !important;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper),
|
||||
:deep(.el-table__body-wrapper) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.serch-button {
|
||||
|
||||
Reference in New Issue
Block a user