新增达人工作台
This commit is contained in:
@@ -27,6 +27,11 @@ export function tkbigdata(data) {
|
|||||||
return postAxios({ url: '/api/big-brother/page', data })
|
return postAxios({ url: '/api/big-brother/page', data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//查询达人列表
|
||||||
|
export function expertInfoPage(data) {
|
||||||
|
return postAxios({ url: '/api/expert-info/page', data })
|
||||||
|
}
|
||||||
|
|
||||||
//获取到期时间
|
//获取到期时间
|
||||||
export function getExpiredTime(tenantId) {
|
export function getExpiredTime(tenantId) {
|
||||||
return getAxios({ url: '/api/tenant/get-expired-time?tenantId=' + tenantId })
|
return getAxios({ url: '/api/tenant/get-expired-time?tenantId=' + tenantId })
|
||||||
|
|||||||
@@ -49,6 +49,14 @@
|
|||||||
<span class="menu-label">大哥工作台</span>
|
<span class="menu-label">大哥工作台</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button @click="currentView = 'expert_workbench'"
|
||||||
|
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||||
|
:class="currentView === 'expert_workbench' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'"
|
||||||
|
title="达人工作台">
|
||||||
|
<img :src="currentView === 'expert_workbench' ? nav44 : nav4" class="w-9 h-9 object-contain flex-shrink-0" />
|
||||||
|
<span class="menu-label">达人工作台</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button @click="currentView = 'pk_mini'"
|
<button @click="currentView = 'pk_mini'"
|
||||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||||
:class="currentView === 'pk_mini' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'"
|
:class="currentView === 'pk_mini' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'"
|
||||||
@@ -126,6 +134,13 @@
|
|||||||
</PermissionMask>
|
</PermissionMask>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-show="currentView === 'expert_workbench'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden">
|
||||||
|
<PermissionMask permission-key="crawl" title="达人工作台未开通" description="您当前没有使用达人工作台功能的权限"
|
||||||
|
:placeholder-image="placeholderHosts" :contacts="serviceContacts">
|
||||||
|
<ExpertWorkbench />
|
||||||
|
</PermissionMask>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-show="currentView === 'pk_mini'" class="absolute inset-0 z-20 h-full overflow-hidden">
|
<div v-show="currentView === 'pk_mini'" class="absolute inset-0 z-20 h-full overflow-hidden">
|
||||||
<PkMiniWorkbench />
|
<PkMiniWorkbench />
|
||||||
</div>
|
</div>
|
||||||
@@ -151,6 +166,7 @@ import TkWorkbenches from '@/views/tk/Workbenches.vue'
|
|||||||
import HostsList from '@/views/tk/HostsList.vue'
|
import HostsList from '@/views/tk/HostsList.vue'
|
||||||
import ConfigPage from '@/pages/ConfigPage.vue'
|
import ConfigPage from '@/pages/ConfigPage.vue'
|
||||||
import FanWorkbench from '@/views/tk/FanWorkbench.vue'
|
import FanWorkbench from '@/views/tk/FanWorkbench.vue'
|
||||||
|
import ExpertWorkbench from '@/views/tk/ExpertWorkbench.vue'
|
||||||
import PkMiniWorkbench from '@/views/pk-mini/PkMiniWorkbench.vue'
|
import PkMiniWorkbench from '@/views/pk-mini/PkMiniWorkbench.vue'
|
||||||
import AutoDmTkWorkbench from '@/views/auto-dm/AutoDmTkWorkbench.vue'
|
import AutoDmTkWorkbench from '@/views/auto-dm/AutoDmTkWorkbench.vue'
|
||||||
import PermissionMask from '@/components/PermissionMask.vue'
|
import PermissionMask from '@/components/PermissionMask.vue'
|
||||||
|
|||||||
309
src/views/tk/ExpertWorkbench.vue
Normal file
309
src/views/tk/ExpertWorkbench.vue
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full w-full overflow-y-auto bg-gray-50 p-6">
|
||||||
|
<div class="bg-white rounded-3xl shadow-sm border border-slate-100 p-6 h-full flex flex-col">
|
||||||
|
<div class="mb-6 grid grid-cols-1 xl:grid-cols-[1.2fr,0.8fr] gap-4">
|
||||||
|
<section class="rounded-2xl border border-slate-100 bg-slate-50/70 p-5">
|
||||||
|
<div class="mb-4 flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-base font-semibold text-slate-800">爬虫配置</div>
|
||||||
|
<p class="mt-1 text-sm text-slate-500">粉丝区间仅用于启动达人爬虫任务,不参与列表查询。</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<el-button v-show="!isRunning" type="success" @click="handleStart"
|
||||||
|
class="!rounded-xl !font-semibold shadow-lg shadow-emerald-500/20">
|
||||||
|
开始
|
||||||
|
</el-button>
|
||||||
|
<el-button v-show="isRunning" type="danger" @click="handleStop"
|
||||||
|
class="!rounded-xl !font-semibold shadow-lg shadow-red-500/20">
|
||||||
|
结束
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-end gap-4">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">最小粉丝数</label>
|
||||||
|
<el-input v-model="crawlForm.fansMin" placeholder="最小值" style="width: 150px" type="number"
|
||||||
|
:disabled="isRunning" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">最大粉丝数</label>
|
||||||
|
<el-input v-model="crawlForm.fansMax" placeholder="最大值" style="width: 150px" type="number"
|
||||||
|
:disabled="isRunning" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
|
||||||
|
当前仓库里还没有达人爬虫的后端启动接口,这里先保留前端配置和任务态,等后端接口接入后再把参数真正传下去。
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-slate-100 bg-white p-5">
|
||||||
|
<div class="text-base font-semibold text-slate-800">任务状态</div>
|
||||||
|
<div class="mt-4 grid grid-cols-2 gap-3">
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-4 py-3">
|
||||||
|
<div class="text-xs text-slate-500">运行状态</div>
|
||||||
|
<div class="mt-2 text-sm font-semibold" :class="isRunning ? 'text-emerald-600' : 'text-slate-700'">
|
||||||
|
{{ isRunning ? '运行中' : '未启动' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-4 py-3">
|
||||||
|
<div class="text-xs text-slate-500">运行时间</div>
|
||||||
|
<div class="mt-2 font-mono text-sm font-semibold text-slate-700">{{ formattedElapsed }}</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="rounded-xl border border-slate-100 bg-slate-50 px-4 py-3">
|
||||||
|
<div class="text-xs text-slate-500">tenantId</div>
|
||||||
|
<div class="mt-2 text-sm font-semibold text-slate-700">{{ tenantId || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-4 py-3">
|
||||||
|
<div class="text-xs text-slate-500">数据来源</div>
|
||||||
|
<div class="mt-2 text-sm font-semibold text-slate-700">api/expert-info/page</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6 rounded-2xl border border-slate-100 bg-slate-50/50 p-5">
|
||||||
|
<div class="mb-4 flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-base font-semibold text-slate-800">列表查询</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-white/80 border border-slate-200 rounded-full px-5 py-2 flex items-center gap-4 text-sm shadow-sm">
|
||||||
|
<span class="font-medium text-slate-500">总数:</span>
|
||||||
|
<span class="font-bold text-slate-700">{{ stats.total }}</span>
|
||||||
|
<div class="w-px h-4 bg-slate-200"></div>
|
||||||
|
<span class="font-medium text-slate-500">当前页:</span>
|
||||||
|
<span class="font-bold text-primary">{{ stats.pageCount }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-end gap-4">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">查询日期</label>
|
||||||
|
<el-date-picker v-model="queryDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期"
|
||||||
|
style="width: 180px" />
|
||||||
|
</div>
|
||||||
|
<el-button @click="handleSearch" type="primary"
|
||||||
|
class="!rounded-xl !font-semibold shadow-lg shadow-blue-500/20">
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset" class="!rounded-xl !font-semibold">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-hidden border border-slate-100 rounded-xl">
|
||||||
|
<el-table :data="tableData" stripe v-loading="loading" height="100%">
|
||||||
|
<el-table-column prop="displayId" label="达人ID" min-width="150" />
|
||||||
|
<el-table-column prop="nickname" label="昵称" min-width="180" />
|
||||||
|
<el-table-column prop="region" label="地区" min-width="120" />
|
||||||
|
<el-table-column prop="followerCount" label="粉丝数" min-width="120" />
|
||||||
|
<el-table-column prop="awemeCount" label="作品数" min-width="120" />
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="180" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex justify-end items-center">
|
||||||
|
<el-pagination v-model:current-page="page" background
|
||||||
|
layout="prev, pager, next" :total="total" :page-size="pageSize"
|
||||||
|
@current-change="handleCurrentChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { getUser } from '@/utils/storage';
|
||||||
|
import { expertInfoPage } from '@/api/account';
|
||||||
|
|
||||||
|
const userInfo = ref({});
|
||||||
|
const tenantId = ref('');
|
||||||
|
const crawlForm = ref({
|
||||||
|
fansMin: '',
|
||||||
|
fansMax: '',
|
||||||
|
});
|
||||||
|
const queryDate = ref('');
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = 20;
|
||||||
|
const total = ref(0);
|
||||||
|
const loading = ref(false);
|
||||||
|
const tableData = ref([]);
|
||||||
|
const isRunning = ref(false);
|
||||||
|
const elapsedSeconds = ref(0);
|
||||||
|
const elapsedTimerId = ref(null);
|
||||||
|
|
||||||
|
const stats = computed(() => ({
|
||||||
|
total: total.value,
|
||||||
|
pageCount: tableData.value.length,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const formattedElapsed = computed(() => {
|
||||||
|
const hours = Math.floor(elapsedSeconds.value / 3600);
|
||||||
|
const minutes = Math.floor((elapsedSeconds.value % 3600) / 60);
|
||||||
|
const seconds = elapsedSeconds.value % 60;
|
||||||
|
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
userInfo.value = getUser() || {};
|
||||||
|
tenantId.value = String(userInfo.value?.tenantId || '');
|
||||||
|
queryDate.value = buildTodayText();
|
||||||
|
void loadList();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopElapsedTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildTodayText() {
|
||||||
|
const date = new Date();
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNumber(value) {
|
||||||
|
if (value === '' || value === null || value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapExpertRecord(item) {
|
||||||
|
return {
|
||||||
|
displayId: item?.displayId || item?.expertId || item?.awemeId || item?.id || '-',
|
||||||
|
nickname: item?.nickname || item?.nickName || item?.name || '-',
|
||||||
|
region: item?.region || item?.country || item?.countryEng || '-',
|
||||||
|
followerCount: item?.followerCount ?? item?.fansCount ?? item?.fans ?? 0,
|
||||||
|
awemeCount: item?.awemeCount ?? item?.videoCount ?? item?.worksCount ?? 0,
|
||||||
|
createTime: formatDateTime(item?.createTime || item?.gmtCreate || item?.createdAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQueryPayload() {
|
||||||
|
const selectedDate = queryDate.value || buildTodayText();
|
||||||
|
return {
|
||||||
|
current: page.value,
|
||||||
|
pageSize,
|
||||||
|
tenantId: Number(tenantId.value || 0),
|
||||||
|
createTime: selectedDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCrawlRange() {
|
||||||
|
const min = normalizeNumber(crawlForm.value.fansMin);
|
||||||
|
const max = normalizeNumber(crawlForm.value.fansMax);
|
||||||
|
|
||||||
|
if (min !== undefined && max !== undefined && min > max) {
|
||||||
|
ElMessage.error('粉丝区间输入不正确');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
if (!tenantId.value) {
|
||||||
|
tableData.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await expertInfoPage(buildQueryPayload());
|
||||||
|
const records = Array.isArray(res?.records) ? res.records : [];
|
||||||
|
total.value = Number(res?.total || 0);
|
||||||
|
tableData.value = records.map(mapExpertRecord);
|
||||||
|
} catch (error) {
|
||||||
|
tableData.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
const message = error?.message || '达人列表加载失败';
|
||||||
|
ElMessage.error(message);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startElapsedTimer() {
|
||||||
|
stopElapsedTimer();
|
||||||
|
elapsedSeconds.value = 0;
|
||||||
|
elapsedTimerId.value = setInterval(() => {
|
||||||
|
elapsedSeconds.value += 1;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopElapsedTimer() {
|
||||||
|
if (elapsedTimerId.value) {
|
||||||
|
clearInterval(elapsedTimerId.value);
|
||||||
|
elapsedTimerId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStart() {
|
||||||
|
if (!tenantId.value) {
|
||||||
|
ElMessage.error('缺少 tenantId,无法启动达人工作台');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateCrawlRange()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning.value = true;
|
||||||
|
startElapsedTimer();
|
||||||
|
ElMessage.success('达人工作台已进入运行态');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStop() {
|
||||||
|
isRunning.value = false;
|
||||||
|
stopElapsedTimer();
|
||||||
|
ElMessage.success('达人工作台已停止');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearch() {
|
||||||
|
page.value = 1;
|
||||||
|
await loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReset() {
|
||||||
|
queryDate.value = buildTodayText();
|
||||||
|
page.value = 1;
|
||||||
|
await loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCurrentChange(current) {
|
||||||
|
page.value = current;
|
||||||
|
await loadList();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user