Files
lingji-work-fe/src/views/components/dataEngine/keyWord.vue
2025-07-07 10:42:38 +08:00

727 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- eslint-disable vue/no-duplicate-attributes -->
<template>
<view>
<topHeader ref="topHeaderRef" @search="search"></topHeader>
<!-- 关键词热度榜 -->
<a-space
direction="vertical"
class="bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid w-100% py-0 px-20px mb-20px"
>
<div class="title-row">
<span class="title mr-4px">关键词热度榜</span>
<a-tooltip>
<template #content>基于该行业用户内容中提及频率较高的关键词按热度进行排序反映近期关注焦点</template>
<icon-question-circle size="16" class="!color-#737478" />
</a-tooltip>
</div>
<a-table
:columns="columns"
:data="dataList"
:filter-icon-align-left="alignLeft"
:scroll="true"
:pagination="false"
@change="handleChange"
>
<template #empty>
<NoData />
</template>
<template #heatLevel>
<a-space>
<span>热度指数</span>
<a-tooltip>
<template #content>综合话题出现频次互动数据如点赞收藏评论加权计算的热度得分</template>
<icon-question-circle size="14" class="!color-#737478" />
</a-tooltip>
</a-space>
</template>
<template #trendTitle>
<a-space>
<span>变化幅度</span>
<a-tooltip>
<template #content>仅基于关键词出现频次</template>
<icon-question-circle size="14" class="!color-#737478" />
</a-tooltip>
</a-space>
</template>
<template #rank="{ record }">
<img v-if="record.rank == 1" :src="topImages[0]" style="width: 25px; height: 17px" />
<img v-else-if="record.rank == 2" :src="topImages[1]" style="width: 25px; height: 17px" />
<img v-else-if="record.rank == 3" :src="topImages[2]" style="width: 25px; height: 17px" />
<span v-else>{{ record.rank }}</span>
</template>
<template #keywords="{ record }">
<a-tag v-for="item in record.keywords" :key="item" style="margin-right: 5px">{{ item }}</a-tag>
</template>
<template #hot="{ record }">
<img v-for="i in record.hot" :key="i" :src="starImages[i - 1]" style="width: 16px; height: 16px" />
</template>
<template #sentiment="{ record }">
<img v-if="record.felling == '2'" src="@/assets/img/hottranslation/good.png" class="w-24px h-24px" />
<img v-else-if="record.felling == '1'" src="@/assets/img/hottranslation/normal.png" class="w-24px h-24px" />
<img v-else-if="record.felling == '0'" src="@/assets/img/hottranslation/poor.png" class="w-24px h-24px" />
</template>
<template #tred="{ record }">
<div class="flex items-center" :class="record.trend > 0 ? 'color-#F64B31' : 'color-#25C883'">
<icon-arrow-up v-if="record.trend > 0" size="16" />
<icon-arrow-down v-else size="16" />
{{ `${(record.trend * 100).toFixed(2)}%` }}
</div>
</template>
</a-table>
</a-space>
<!-- 行业情绪 -->
<a-space
direction="vertical"
class="bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid w-100% py-0 px-20px mb-24px"
>
<div class="title-row">
<span class="title mr-4px">行业情绪</span>
<a-tooltip>
<template #content
>对该行业下用户内容进行情绪分析按情绪类别统计占比提取占比最高者作为行业情绪代表</template
>
<icon-question-circle size="16" class="!color-#737478" />
</a-tooltip>
</div>
<div class="flex items-center w-100%">
<div v-show="fellingRate.length > 0" class="w-500px flex items-center">
<div id="container" class="w-300px h-300px"></div>
<div class="flex flex-col">
<div class="mb-12px flex items-center">
<div class="w-12px h-12px mr-10px bg-#25C883 rounded-50%"></div>
<span
>正面情绪 <span class="num">{{ getFormatter(fellingRate[0] * 100) }}</span>
</span>
</div>
<div class="flex items-center">
<div class="w-12px h-12px mr-10px bg-#F64B31 rounded-50%"></div>
<span>负面情绪 </span>
<span style="width: 40px">{{ getFormatter(fellingRate[1] * 100) }}</span>
</div>
</div>
</div>
<a-table
class="flex-1"
:columns="columns2"
:data="sortedRowData"
:span-method="spanMethod"
:filter-icon-align-left="alignLeft"
:scroll="true"
:pagination="false"
@change="handleChange"
>
<template #empty>
<NoData />
</template>
<template #felling="{ record }">
<div class="flex items-center">
<img :src="fellingStatus[record.felling].icon" class="w-20px h-20px mr-4px" />
<span>{{ fellingStatus[record.felling].label }}</span>
</div>
</template>
</a-table>
</div>
</a-space>
<!-- 新兴关键词 -->
<a-space
direction="vertical"
class="bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid w-100% py-0 px-20px"
>
<div class="title-row">
<span class="title mr-4px">新兴关键词</span>
<a-tooltip>
<template #content
>指当前周期中首次出现或相较上一周期词频显著增长的关键词反映近期出现的新关注点</template
>
<icon-question-circle size="16" class="!color-#737478" />
</a-tooltip>
</div>
<a-table
:columns="columns3"
:data="keywordList"
:filter-icon-align-left="alignLeft"
:scroll="true"
:pagination="false"
@change="handleChange"
>
<template #empty>
<NoData />
</template>
<template #rank="{ record }">
<img v-if="record.rank == 1" :src="topImages[0]" style="width: 25px; height: 17px" />
<img v-else-if="record.rank == 2" :src="topImages[1]" style="width: 25px; height: 17px" />
<img v-else-if="record.rank == 3" :src="topImages[2]" style="width: 25px; height: 17px" />
<span v-else>{{ record.rank }}</span>
</template>
<template #felling="{ record }">
<img
v-if="record.felling == '2'"
src="@/assets/img/hottranslation/good.png"
style="width: 16px; height: 16px"
/>
<img
v-else-if="record.felling == '1'"
src="@/assets/img/hottranslation/normal.png"
style="width: 16px; height: 16px"
/>
<img
v-else-if="record.felling == '0'"
src="@/assets/img/hottranslation/poor.png"
style="width: 16px; height: 16px"
/>
</template>
<template #first_appeared_at="{ record }">
{{ formatTimestamp(record.first_appeared_at) }}
</template>
<template #hot="{ record }">
<img v-for="i in record.hot" :key="i" :src="starImages[i - 1]" style="width: 16px; height: 16px" />
</template>
<template #hotTitle="{ record }">
<a-space>
<span>当前热度指数</span>
<a-tooltip>
<template #content>综合关键词出现频次互动表现如点赞收藏评论加权计算的热度得分</template>
<icon-question-circle size="16" class="!color-#737478" />
</a-tooltip>
</a-space>
</template>
<template #trendTitle="{ record }">
<a-space>
<span>变化幅度</span>
<a-tooltip>
<template #content>仅基于关键词出现频次</template>
<icon-question-circle size="16" class="!color-#737478" />
</a-tooltip>
</a-space>
</template>
<template #tred="{ record }">
<div class="flex items-center" :class="record.trend > 0 ? 'color-#F64B31' : 'color-#25C883'">
<icon-arrow-up v-if="record.trend > 0" size="16" />
<icon-arrow-down v-else size="16" />
{{ `${(record.trend * 100).toFixed(2)}%` }}
</div>
</template>
<template #optional="{ record }">
<a-button type="outline" @click="gotoDetail(record)">详情</a-button>
</template>
</a-table>
</a-space>
<!-- modal -->
<a-modal
:visible="visible"
modal-class="keyword-modal"
unmountOnClose
width="640px"
@ok="handleOk"
@cancel="handleCancel"
>
<template #title>
<span style="text-align: left; width: 100%">新兴关键词</span>
</template>
<div>
<a-space direction="vertical">
<div class="mb-12px flex items-center">
<p class="cts !mr-16px flex-shrink-0 w-83px">话题名称</p>
<span class="cts">{{ topicInfo.name }}</span>
</div>
<div class="mb-12px flex items-center">
<p class="cts !mr-16px flex-shrink-0 w-83px">最大规模出现</p>
<span class="cts">{{ formatTimestamp(topicInfo.first_appeared_at) }}</span>
</div>
<div class="mb-12px flex items-center">
<p class="cts !mr-16px flex-shrink-0 w-83px">变化幅度</p>
<div class="flex items-center" :class="topicInfo.trend > 0 ? 'color-#F64B31' : 'color-#25C883'">
<icon-arrow-up v-if="topicInfo.trend > 0" size="16" />
<icon-arrow-down v-else size="16" />
{{ `${topicInfo.trend * 100}%` }}
</div>
</div>
<div class="mb-12px flex items-center">
<p class="cts !mr-16px flex-shrink-0 w-83px">热度指数</p>
<img v-for="i in topicInfo.hot" :key="i" :src="starImages[i - 1]" style="width: 16px; height: 16px" />
</div>
<div class="flex items-start">
<p class="!mr-16px w-83px cts relative top-2px">原始来源</p>
<div class="flex flex-col">
<div v-for="item in topicInfo.industry_new_keyword_sources" :key="item" class="mb-18px flex items-center">
<a-link style="background-color: initial" :href="item.link" target="_blank" class="!text-12px">{{
item.title
}}</a-link>
<img src="@/assets/img/hottranslation/xhs.png" width="16" height="16" />
</div>
</div>
</div>
</a-space>
</div>
<template #footer>
<a-button size="large" class="cancel-btn" @click="handleCancel">取消</a-button>
<a-button type="primary" size="large" class="rounded-4px" @click="handleOk"> 确定 </a-button>
</template>
</a-modal>
</view>
</template>
<script setup>
import topHeader from './topHeader.vue';
import {
fetchKeywordTrendsList,
fetchIndustryEmotions,
fetchNewKeywordList,
fetchNewKeywordDetail,
} from '@/api/all/index';
import { ref, onMounted, onBeforeUnmount, watchEffect, computed } from 'vue';
import * as echarts from 'echarts';
import star1 from '@/assets/img/hottranslation/star-fill1.png';
import star2 from '@/assets/img/hottranslation/star-fill2.png';
import star3 from '@/assets/img/hottranslation/star-fill3.png';
import star4 from '@/assets/img/hottranslation/star-fill4.png';
import star5 from '@/assets/img/hottranslation/star-fill5.png';
import top1 from '@/assets/img/captcha/top1.svg';
import top2 from '@/assets/img/captcha/top2.svg';
import top3 from '@/assets/img/captcha/top3.svg';
import icon1 from '@/assets/img/hottranslation/good.png';
import icon2 from '@/assets/img/hottranslation/normal.png';
import icon3 from '@/assets/img/hottranslation/poor.png';
const fellingStatus = {
2: {
icon: icon1,
label: '正面情绪',
},
1: {
icon: icon2,
label: '中性情绪',
},
0: {
icon: icon3,
label: '负面情绪',
},
};
const starImages = [star1, star2, star3, star4, star5];
const topImages = [top1, top2, top3];
const chartRef = (ref < HTMLElement) | (null > null);
const topHeaderRef = ref();
// 从topHeader获取统一的状态
const selectedIndustry = computed(() => topHeaderRef.value?.selectedIndustry);
const selectedSubCategory = computed(() => topHeaderRef.value?.selectedSubCategory);
const selectedTimePeriod = computed(() => topHeaderRef.value?.selectedTimePeriod);
const dataList = ref([]);
const rowData = ref([]);
const keywordList = ref([]);
const fellingRate = ref([]);
const visible = ref(false);
const topicInfo = ref({});
const columns = [
{
title: '排名',
dataIndex: 'rank',
slotName: 'rank',
width: 100,
},
{
title: '关键词名称',
dataIndex: 'name',
width: 300,
},
{
titleSlotName: 'heatLevel',
title: '热度指数',
dataIndex: 'hot',
sortable: {
sortDirections: ['ascend', 'descend'],
},
slotName: 'hot',
width: 220,
},
{
titleSlotName: 'trendTitle',
title: '变化幅度',
sortable: {
sortDirections: ['ascend', 'descend'],
},
dataIndex: 'tred',
slotName: 'tred',
width: 220,
},
{
title: '情感倾向',
dataIndex: 'sentiment',
slotName: 'sentiment',
width: 220,
},
];
const columns2 = [
{
title: '情绪分布',
dataIndex: 'felling',
slotName: 'felling',
width: 160,
// minWidth: 120,
},
{
title: '主要观点',
dataIndex: 'content',
},
];
const sortedRowData = computed(() => {
return [...rowData.value].sort((a, b) => {
return b.felling - a.felling;
});
});
const spanMethod = ({ record, columnIndex, rowIndex }) => {
if (columnIndex === 0) {
const currentFelling = record.felling;
let rowspan = 1;
for (let i = rowIndex + 1; i < sortedRowData.value.length; i++) {
if (sortedRowData.value[i].felling === currentFelling) {
rowspan++;
} else {
break;
}
}
// eslint-disable-next-line no-unreachable-loop
for (let i = rowIndex - 1; i >= 0; i--) {
if (sortedRowData.value[i].felling === currentFelling) {
return {
rowspan: 0,
colspan: 0,
};
} else {
break;
}
}
return {
rowspan,
colspan: 1,
};
}
// 其他列不合并
return {
rowspan: 1,
colspan: 1,
};
};
const columns3 = [
{
title: '排名',
dataIndex: 'rank',
slotName: 'rank',
width: 180,
minWidth: 180,
},
{
title: '新兴关键词名称',
dataIndex: 'name',
},
{
title: '首次大规模出现',
dataIndex: 'first_appeared_at',
slotName: 'first_appeared_at',
},
{
titleSlotName: 'hotTitle',
title: '当前热度指数',
dataIndex: 'hot',
slotName: 'hot',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
titleSlotName: 'trendTitle',
title: '变化幅度',
sortable: {
sortDirections: ['ascend', 'descend'],
},
dataIndex: 'tred',
slotName: 'tred',
width: 180,
minWidth: 180,
},
{
title: '操作',
slotName: 'optional',
width: 120,
minWidth: 120,
},
];
const getFormatter = (value) => {
const formattedValue = Number.isInteger(value) ? value.toString() : value.toFixed(2);
return formattedValue + '%';
};
const getIndustryEmotions = async () => {
const params = {
industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value,
};
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) {
params['industry_id'] = selectedSubCategory.value;
}
const res = await fetchIndustryEmotions(params);
if (res.code == 200) {
let data = res['data'];
fellingRate.value = [];
fellingRate.value.push(data['good_felling_rate']);
fellingRate.value.push(data['bad_felling_rate']);
drawChart();
rowData.value = data['industry_emotion_view_points'];
let items = groupedData();
}
};
// 详情
const gotoDetail = async (record) => {
const res = await fetchNewKeywordDetail(record.id);
if (res.code === 200) {
visible.value = true;
topicInfo.value = res.data;
}
};
const groupedData = () => {
const groups = {
negative: { name: '负面', items: [], color: '#F64B31' },
neutral: { name: '中性', items: [], color: '#FFAA16' },
positive: { name: '正面', items: [], color: '#25C883' },
};
rowData.value.forEach((item) => {
if (item.felling === 0) groups.negative.items.push(item);
else if (item.felling === 1) groups.neutral.items.push(item);
else if (item.felling === 2) groups.positive.items.push(item);
});
return groups;
};
// 弹窗的取消
const handleCancel = () => {
visible.value = false;
};
// 弹窗的确定
const handleOk = () => {
visible.value = false;
};
const getKeywordTrendsList = async () => {
const params = {
industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value,
};
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) {
params['industry_id'] = selectedSubCategory.value;
}
const res = await fetchKeywordTrendsList(params);
if (res.code === 200) {
dataList.value = res.data;
}
};
const formatTimestamp = (timestamp) => {
if (!timestamp) return '未记录';
try {
return dayjs.unix(timestamp).format('YYYY-MM-DD HH:mm');
} catch (e) {
return '格式错误';
}
};
const getNewKeywordList = async () => {
const params = {
industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value,
};
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0 && selectedSubCategory.value != undefined) {
params['industry_id'] = selectedSubCategory.value;
}
const res = await fetchNewKeywordList(params);
if (res.code == 200) {
// 这里需要根据API返回的数据结构处理成tagRows需要的格式
keywordList.value = res.data;
}
};
const drawChart = () => {
let dom = document.getElementById('container');
let myChart = echarts.init(dom, null, {
renderer: 'canvas',
useDirtyRect: false,
});
let option;
option = {
color: ['#25C883', '#F64B31'],
series: [
{
type: 'pie',
avoidLabelOverlap: false,
data: fellingRate.value,
labelLine: {
show: false, // 不显示引导线
},
radius: ['50%', '70%'], // 调整半径范围
},
],
};
if (option && typeof option === 'object') {
myChart.setOption(option);
}
};
const search = () => {
getKeywordTrendsList();
getIndustryEmotions();
getNewKeywordList();
};
// 监听筛选条件变化
watch([selectedTimePeriod, selectedSubCategory], () => {
getKeywordTrendsList();
getIndustryEmotions();
getNewKeywordList();
});
watch([selectedIndustry], () => {
selectedSubCategory.value = 0;
getKeywordTrendsList();
getIndustryEmotions();
getNewKeywordList();
});
onMounted(() => {
getKeywordTrendsList();
getIndustryEmotions();
getNewKeywordList();
});
</script>
<style scoped lang="scss">
/* 自定义样式 */
:deep(.arco-table-th) {
background-color: var(--color-fill-2);
}
:deep(.arco-table-tr):hover {
background-color: var(--color-fill-1);
}
:deep(.arco-statistic-content .arco-statistic-value-integer) {
font-size: 14px;
}
.pop-btn {
background: #fff !important;
border-color: #fff !important;
color: #737478 !important;
margin-left: -8px;
}
.pop-btn2 {
background: transparent !important;
border-color: transparent !important;
color: #737478 !important;
margin-left: -8px;
}
.title-row {
display: flex;
height: 64px;
padding: 10px 0 2px 0;
align-items: center;
.title {
color: var(--Text-1, #211f24);
font-family: 'PuHuiTi-Medium';
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px; /* 150% */
}
}
.cts {
color: var(--Text-2, #3c4043);
font-family: 'PuHuiTi-Medium';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
&.num {
font-family: 'HarmonyOS Sans SC';
}
}
</style>
<style lang="scss">
.keyword-modal {
.arco-modal-header {
border-bottom: none;
height: 56px;
padding: 0 20px;
.arco-modal-title {
justify-content: flex-start;
}
}
.arco-modal-body {
padding: 12px 20px 0;
.cts {
color: var(--Text-2, #3c4043);
font-family: 'PuHuiTi-Regular';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
}
p {
margin: 0;
padding: 0;
}
}
.arco-modal-footer {
display: flex;
height: 64px;
padding: 0px 20px;
justify-content: flex-end;
align-items: center;
border-top: 1px solid var(--Border-1, #d7d7d9);
.cancel-btn {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
&:hover {
border: 1px solid var(--BG-500, #b1b2b5);
}
}
}
}
</style>