240 lines
7.7 KiB
Vue
240 lines
7.7 KiB
Vue
|
|
<!-- LiveRecordDialog.vue -->
|
|||
|
|
<template>
|
|||
|
|
<el-dialog v-model="visible" title="直播记录" width="86vw" 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>
|
|||
|
|
</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-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>
|
|||
|
|
<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>
|
|||
|
|
|
|||
|
|
<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";
|
|||
|
|
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: boolean;
|
|||
|
|
rows: Row[];
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
(e: "update:modelValue", v: boolean): void;
|
|||
|
|
(e: "select", row: Row): void;
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const visible = computed({
|
|||
|
|
get: () => props.modelValue,
|
|||
|
|
set: (v) => emit("update:modelValue", v),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const kw = ref("");
|
|||
|
|
const sortKey = ref<keyof Row>("startTimeFormatted");
|
|||
|
|
const sortOrder = ref<"asc" | "desc">("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
|
|||
|
|
if (!s) return 0;
|
|||
|
|
return new Date(s.replace(" ", "T")).getTime() || 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function durationSeconds(durationFormatted?: string) {
|
|||
|
|
// 例:"2小时49分钟55秒" / "18分钟31秒" / "25秒"
|
|||
|
|
if (!durationFormatted) return 0;
|
|||
|
|
const h = Number((durationFormatted.match(/(\d+)\s*小时/) || [])[1] || 0);
|
|||
|
|
const m = Number((durationFormatted.match(/(\d+)\s*分钟/) || [])[1] || 0);
|
|||
|
|
const s = Number((durationFormatted.match(/(\d+)\s*秒/) || [])[1] || 0);
|
|||
|
|
return h * 3600 + m * 60 + s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const filteredRows = computed(() => {
|
|||
|
|
const k = kw.value.trim().toLowerCase();
|
|||
|
|
|
|||
|
|
let arr = (props.rows || []).filter((r) => {
|
|||
|
|
if (!k) return true;
|
|||
|
|
const hay = `${r.id} ${r.userId} ${r.hostsId} ${r.tenantId}`.toLowerCase();
|
|||
|
|
return hay.includes(k);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (onlyAbnormal.value) {
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
// 时间字段特殊处理
|
|||
|
|
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));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const zeroLikeCount = computed(() => filteredRows.value.filter((r) => r.likeCount === 0).length);
|
|||
|
|
|
|||
|
|
function reset() {
|
|||
|
|
kw.value = "";
|
|||
|
|
sortKey.value = "startTimeFormatted";
|
|||
|
|
sortOrder.value = "desc";
|
|||
|
|
onlyAbnormal.value = false;
|
|||
|
|
page.value = 1;
|
|||
|
|
pageSize.value = 20;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function copyRow(row: Row) {
|
|||
|
|
try {
|
|||
|
|
await navigator.clipboard.writeText(JSON.stringify(row, null, 2));
|
|||
|
|
ElMessage.success("已复制该行 JSON");
|
|||
|
|
} catch {
|
|||
|
|
ElMessage.warning("复制失败:浏览器不支持或无权限");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function emitSelect(row: Row) {
|
|||
|
|
emit("select", row);
|
|||
|
|
ElMessage.success(`已选中:ID ${row.id}`);
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.toolbar {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 12px;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toolbar .left {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
align-items: center;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toolbar .right {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
align-items: center;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
margin-top: 12px;
|
|||
|
|
}
|
|||
|
|
</style>
|