新增达人工作台

This commit is contained in:
2026-04-28 16:22:58 +08:00
parent f0d5b2872e
commit 54402b0dd4
3 changed files with 330 additions and 0 deletions

View 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>