新增达人工作台
This commit is contained in:
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