提交 94274ead authored 作者: hsx's avatar hsx

feat:新闻详情页

上级 7505facb
...@@ -10,6 +10,7 @@ lerna-debug.log* ...@@ -10,6 +10,7 @@ lerna-debug.log*
*.rar *.rar
*.zip *.zip
*.7z *.7z
*.rest
# Dependencies # Dependencies
node_modules node_modules
......
import request from "@/api/request.js";
const INTELLECTUAL_API = "/intelligent-api";
export class IntelligentResultWrapper<T> {
result: T;
status: string;
}
export class TextEntity {
text_span: string;
type: string;
}
// 智能化:提取文本实体
export function extractTextEntity(text: string): Promise<IntelligentResultWrapper<TextEntity[]>> {
return request({
url: `${INTELLECTUAL_API}/extract-entity`,
method: "POST",
data: {
text
}
});
}
...@@ -35,7 +35,7 @@ export function getHotNews() { ...@@ -35,7 +35,7 @@ export function getHotNews() {
export function getHotNewsByArea(params) { export function getHotNewsByArea(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/news/hotNews`, url: `/api/news/hotAreaNews/${params.moduleId}`,
params params
}) })
} }
......
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { size } from 'lodash';
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
// 组件属性 // 组件属性
...@@ -83,7 +82,7 @@ const processedSvgContent = computed(() => { ...@@ -83,7 +82,7 @@ const processedSvgContent = computed(() => {
}); });
} }
console.log(processed) // console.log(processed)
return processed; return processed;
}); });
......
<template> <template>
<div class="box3-item" @click="handleToNewsAnalysis(news)"> <el-space alignment="flex-start" :size="10">
<div class="left"> <img :width="64" :height="52" :src="news ?? DefaultIconNews" alt="" />
<img :src="news[props.img] ? news[props.img] : DefaultIconNews" alt="" /> <el-space direction="vertical" alignment="flex-start" :size="0">
</div> <common-text :line-limit="1" class="text-regular text-hover" color="var(--text-primary-80-color)">
<div class="right"> {{ title }}
<div class="right-top"> </common-text>
<div class="title"><span class="text-inner">{{ news[props.title] }}</span></div> <common-text :line-limit="1" class="text-tip-1" color="var(--text-primary-65-color)">
<div class="time">{{ news[props.from] }}</div> {{ from }}
</div> </common-text>
<div class="right-footer">{{ news[props.content] }}</div> </el-space>
</div> </el-space>
</div>
</template> </template>
<script setup> <script setup>
import DefaultIconNews from "@/assets/icons/default-icon-news.png"; import DefaultIconNews from "@/assets/icons/default-icon-news.png";
const props = defineProps({ import { ElSpace } from "element-plus";
import CommonText from "../texts/CommonText.vue";
// 新闻列表数据
news: {
type: Object,
default: () => { }
},
const props = defineProps({
img: { img: {
type: String, type: String,
default: 'img' default: ''
}, },
title: { title: {
type: String, type: String,
default: "title" default: ""
}, },
from: { from: {
type: String, type: String,
default: "from" default: ""
}, },
content: { content: {
type: String, type: String,
default: "content" default: ""
}, },
}); });
...@@ -49,72 +44,4 @@ const handleToNewsAnalysis = (item, index) => { ...@@ -49,72 +44,4 @@ const handleToNewsAnalysis = (item, index) => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@use '@/styles/common.scss'; @use '@/styles/common.scss';
.box3-item {
display: flex;
align-items: center;
height: 78px;
margin: 0px 21px;
cursor: pointer;
&:hover {
.right-top .title {
color: rgb(5, 95, 194) !important;
font-weight: 700;
}
.right-top .text-inner {
border-bottom-color: rgb(5, 95, 194) !important;
}
}
}
.left {
width: 97px;
// flex-shrink: 0;
height: 72px;
img {
width: 100%;
height: 100%;
border-radius: 4px;
}
}
.right {
flex: 1;
min-width: 0;
margin-left: 20px;
.right-top {
display: flex;
justify-content: space-between;
.title {
// width: 500px;
@extend .text-title-3-bold;
color: var(--text-primary-80-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.text-inner {
border-bottom: 1px solid transparent;
}
}
.time {
text-align: right;
@extend .text-tip-2;
color: var(--text-primary-65-color);
}
}
.right-footer {
@extend .text-compact;
color: var(--text-primary-65-color);
@include common.text-ellipsis(2);
}
}
</style> </style>
\ No newline at end of file
<template> <template>
<div class="common-text"> <div class="common-text-box-hudf">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
...@@ -18,8 +18,8 @@ const props = defineProps({ ...@@ -18,8 +18,8 @@ const props = defineProps({
<style lang="scss" scoped> <style lang="scss" scoped>
@use '@/styles/common.scss'; @use '@/styles/common.scss';
.common-text { .common-text-box-hudf {
color: v-bind(color); color: v-bind(color) !important;
@if('v-bind(lineLimit) !==null') { @if('v-bind(lineLimit) !==null') {
@include common.text-ellipsis(v-bind(lineLimit)); @include common.text-ellipsis(v-bind(lineLimit));
......
<template>
<p class="p-regular-rereg">
<span class="text-regular" v-for="(segment, index) in processedText" :key="index">
<a v-if="segment.isEntity" :href="`https://cn.bing.com/search?q=${segment.entity?.text_span}`"
class="entity-link" target="_blank" rel="noopener noreferrer">
{{ segment.entity?.text_span }}
<img :src="SearchIcon" :width="10" :height="10" alt="search" />
</a>
<span v-else>
{{ segment.text }}
</span>
</span>
</p>
</template>
<script lang="ts" setup>
import { TextEntity } from '@/api/intelligent';
import { ref, watch, onMounted } from 'vue';
import SearchIcon from './images/search.png'
export interface ProcessedTextSegment {
text: string
isEntity: boolean
entity?: TextEntity
}
const props = defineProps({
text: {
type: String,
default: ''
}
, entities: {
type: Array<TextEntity>,
default: () => []
}
})
// 处理后的文本段
const processedText = ref<ProcessedTextSegment[]>([])
// 处理文本,识别并替换实体
const processText = () => {
if (!props.text || !props.entities) {
console.log('props.text', props.entities.length)
processedText.value = [{ text: '', isEntity: false }]
return
}
const result = []
let currentPosition = 0
// 按实体文本长度排序,优先匹配长文本
const sortedEntities = [...props.entities].sort((a, b) =>
b.text_span.length - a.text_span.length
)
while (currentPosition < props.text.length) {
let matched = false
for (const entity of sortedEntities) {
const entityText = entity.text_span
const endPosition = currentPosition + entityText.length
if (props.text.substring(currentPosition, endPosition) === entityText) {
// 如果当前位置是实体,添加到结果
result.push({
isEntity: true,
entity: { ...entity }
})
currentPosition = endPosition
matched = true
break
}
}
if (!matched) {
// 如果不是实体,收集普通文本
let nextEntityStart = props.text.length
for (const entity of sortedEntities) {
const pos = props.text.indexOf(entity.text_span, currentPosition)
if (pos !== -1 && pos < nextEntityStart) {
nextEntityStart = pos
}
}
if (nextEntityStart > currentPosition) {
const plainText = props.text.substring(currentPosition, nextEntityStart)
result.push({
text: plainText,
isEntity: false
})
currentPosition = nextEntityStart
} else {
// 没有更多实体,添加剩余文本
const remainingText = props.text.substring(currentPosition)
if (remainingText) {
result.push({
text: remainingText,
isEntity: false
})
}
currentPosition = props.text.length
}
}
}
processedText.value = result
}
// 监听文本和实体变化
watch(() => props.text, processText)
watch(() => props.entities, processText, { deep: true })
// 初始化处理
onMounted(processText)
</script>
<style lang="scss" scoped>
@use '@/styles/common.scss';
.entity-link {
color: var(--color-primary-100);
}
.p-regular-rereg {
text-indent: 2em;
}
</style>
\ No newline at end of file
...@@ -71,6 +71,13 @@ ...@@ -71,6 +71,13 @@
color: var(--text-primary-80); color: var(--text-primary-80);
} }
.text-hover {
&:hover {
color: rgb(5, 95, 194) !important;
font-weight: 700;
}
}
//0级标题 //0级标题
.text-title-0 { .text-title-0 {
@extend .text-base; @extend .text-base;
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div class="text-title-1-show">文字样式</div> <div class="text-title-1-show">文字样式</div>
<TextStyle /> <TextStyle />
<div class="text-title-1-show">通用样式/组件</div> <div class="text-title-1-show">通用样式/组件</div>
<div style="position: relative; min-height: 300px;"> <div style="position: relative; min-height: 700px;">
<el-tabs tabPosition="left" class="tabs-nav-no-wrap left-float-nav-tabs"> <el-tabs tabPosition="left" class="tabs-nav-no-wrap left-float-nav-tabs">
<el-tab-pane label="通用" lazy> <el-tab-pane label="通用" lazy>
<common-page /> <common-page />
......
...@@ -56,8 +56,11 @@ onMounted(async () => { ...@@ -56,8 +56,11 @@ onMounted(async () => {
async function changeArea(id) { async function changeArea(id) {
const { data } = await getHotNewsByArea({ const { data } = await getHotNewsByArea({
moduleId: moduleId.value, moduleId: moduleId.value,
industryId: id, industryId: id ? id : null,
}); });
data?.forEach(item => {
item.newsImage = item.coverUrl ?? ""
})
NewsData.value = data ?? []; NewsData.value = data ?? [];
} }
......
...@@ -11,9 +11,9 @@ ...@@ -11,9 +11,9 @@
{{ newsDetail.title }} {{ newsDetail.title }}
</common-text> </common-text>
<common-text class="text-tip-1-bold" color="var(--text-primary-65-color)"> <common-text class="text-tip-1-bold" color="var(--text-primary-65-color)">
{{ `${newsDetail.publishedTime} · ${newsDetail.source}` }} {{ `${newsDetail.publishedTime} · ${newsDetail.source} ` }}
<el-space> <el-space>
<area-tag :area-name="newsDetail.areaName" /> <area-tag v-for="item in newsDetail.domainList" :key="item" :tag-name="item.industryName" />
</el-space> </el-space>
</common-text> </common-text>
</el-space> </el-space>
...@@ -28,11 +28,12 @@ ...@@ -28,11 +28,12 @@
</div> </div>
<div class="flex-display common-page content-box-news-detail"> <div class="flex-display common-page content-box-news-detail">
<el-space direction="vertical" class="background-as-card flex-fill" fill alignment="flex-start"> <el-space direction="vertical" class="background-as-card flex-fill" fill alignment="flex-start">
<div style="margin-top: 10px; margin-right: 24px;" class="flex-display"> <div style="margin-top: 10px; margin-right: 24px;gap:10px" class="flex-display">
<color-prefix-title height="20px"> <color-prefix-title height="20px">
<div class="text-title-2-bold">新闻内容</div> <div class="text-title-2-bold">新闻内容</div>
</color-prefix-title> </color-prefix-title>
<div class="flex-fill"></div> <div class="flex-fill"></div>
<el-switch v-model="isHightLightEntity" active-text="高亮实体" @change="handleHighlightEntity" />
<el-button v-if="hasTranslation" :type="isOpenTranslation ? 'primary' : ''" plain <el-button v-if="hasTranslation" :type="isOpenTranslation ? 'primary' : ''" plain
@click="handleTranslation"> @click="handleTranslation">
<color-svg :svg-url="TranslationSvg" color="var(--color-primary-100)" :size="18" <color-svg :svg-url="TranslationSvg" color="var(--color-primary-100)" :size="18"
...@@ -42,11 +43,12 @@ ...@@ -42,11 +43,12 @@
</div> </div>
<div class="common-padding"> <div class="common-padding">
<div class="flex-display" style="align-items: center;"> <div class="flex-display" style="align-items: center;">
<common-text class="text-title-3-bold" color="var(--text-primary-80-color)">中文</common-text> <common-text class="text-title-3-bold" color="var(--text-primary-80-color)">{{ isOpenTranslation
? '中文' : '原文' }}</common-text>
<div class="flex-fill" style="margin: 0 10px;"> <div class="flex-fill" style="margin: 0 10px;">
<el-divider></el-divider> <el-divider></el-divider>
</div> </div>
<el-button @click="() => showMore = !showMore"> <el-button v-if="zhEnTexts.length > 6" @click="() => showMore = !showMore">
{{ showMore ? '收起' : '展开' }} {{ showMore ? '收起' : '展开' }}
<el-icon> <el-icon>
<arrow-down v-if="showMore" /> <arrow-down v-if="showMore" />
...@@ -57,7 +59,9 @@ ...@@ -57,7 +59,9 @@
<el-row :gutter="32"> <el-row :gutter="32">
<el-col :span="znEnColSpan" <el-col :span="znEnColSpan"
v-for="(item, index) in showMore ? zhEnTexts : zhEnTexts.slice(0, 6)" :key="index"> v-for="(item, index) in showMore ? zhEnTexts : zhEnTexts.slice(0, 6)" :key="index">
<p class="p-news-content"> {{ item }}</p> <!-- <p class="p-news-content"> {{ item }}</p> -->
<intelligent-entity-text :text="item"
:entities="isHightLightEntity ? textEntities : []"></intelligent-entity-text>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
...@@ -68,8 +72,10 @@ ...@@ -68,8 +72,10 @@
<div class="text-title-2-bold">相关新闻</div> <div class="text-title-2-bold">相关新闻</div>
</color-prefix-title> </color-prefix-title>
</el-space> </el-space>
<el-space style="margin-top: 10px;"> <el-space direction="vertical" fill class="common-padding">
<news-item v-for="item in relationNews" :key="item.newsId" :news-item="item" /> <news-item v-for="item in relationNews" :key="item.newsId" :news="item" :img="item.newsImage"
:title="item.newsTitle" :from="`${item.newsDate} · ${item.newsOrg}`"
@click="gotoNewsDetail(item.newsId)" />
</el-space> </el-space>
</el-space> </el-space>
</div> </div>
...@@ -81,7 +87,7 @@ import '@/styles/container.scss'; ...@@ -81,7 +87,7 @@ import '@/styles/container.scss';
import '@/styles/common.scss'; import '@/styles/common.scss';
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { ElSpace, ElImage, ElButton, ElIcon, ElScrollbar, ElRow, ElCol, ElDivider } from "element-plus"; import { ElSpace, ElImage, ElButton, ElIcon, ElScrollbar, ElRow, ElCol, ElDivider, ElSwitch } from "element-plus";
import CommonText from "@/components/base/texts/CommonText.vue"; import CommonText from "@/components/base/texts/CommonText.vue";
import AreaTag from "@/components/base/AreaTag/index.vue"; import AreaTag from "@/components/base/AreaTag/index.vue";
import ColorPrefixTitle from '@/components/base/texts/ColorPrefixTitle.vue'; import ColorPrefixTitle from '@/components/base/texts/ColorPrefixTitle.vue';
...@@ -90,14 +96,22 @@ import NewsItem from "@/components/base/newsList/NewsItem.vue"; ...@@ -90,14 +96,22 @@ import NewsItem from "@/components/base/newsList/NewsItem.vue";
import ColorSvg from "@/components/base/images/ColorSvg.vue"; import ColorSvg from "@/components/base/images/ColorSvg.vue";
import TranslationSvg from './assets/images/翻译 1.svg'; import TranslationSvg from './assets/images/翻译 1.svg';
import NewsLogo from './assets/images/组合 293.svg'; import NewsLogo from './assets/images/组合 293.svg';
import { extractTextEntity } from "@/api/intelligent";
import { useGotoNewsDetail } from "@/router/modules/news";
import IntelligentEntityText from "@/components/base/texts/IntelligentEntityText.vue";
const newsDetail = ref({}); const newsDetail = ref({});
const relationNews = ref([]); const relationNews = ref([]);
const zhEnTexts = ref([]); const zhEnTexts = ref([]);
const znEnColSpan = ref(12); const znEnColSpan = ref(12);
const hasTranslation = ref(false); const hasTranslation = ref(false);
const isOpenTranslation = ref(true); const isOpenTranslation = ref(true);
const isHightLightEntity = ref(true);
const textEntities = ref([]);
const showMore = ref(false); const showMore = ref(false);
const route = useRoute(); const route = useRoute();
const gotoNewsDetail = useGotoNewsDetail();
onMounted(async () => { onMounted(async () => {
const params = { const params = {
newsId: route.params.id newsId: route.params.id
...@@ -107,9 +121,23 @@ onMounted(async () => { ...@@ -107,9 +121,23 @@ onMounted(async () => {
const { data: relationNewsData } = await getRelationNews(params); const { data: relationNewsData } = await getRelationNews(params);
relationNews.value = relationNewsData ?? []; relationNews.value = relationNewsData ?? [];
console.log(relationNews.value)
updateText(); updateText();
await handleHighlightEntity();
}); });
async function handleHighlightEntity() {
if (textEntities.value.length > 0) return
const { result: entityDataZh } = await extractTextEntity(newsDetail.value.contentZh);
textEntities.value = [...entityDataZh ?? []]
if (newsDetail.value.contentZh !== newsDetail.value.content) {
const { result: entityData } = await extractTextEntity(newsDetail.value.content);
textEntities.value = [...textEntities.value, ...entityData ?? []]
}
}
function handleTranslation() { function handleTranslation() {
isOpenTranslation.value = !isOpenTranslation.value; isOpenTranslation.value = !isOpenTranslation.value;
updateText(); updateText();
......
...@@ -15,31 +15,15 @@ ...@@ -15,31 +15,15 @@
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import '@/styles/container.scss'; import '@/styles/container.scss';
import { useRoute } from "vue-router";
import Headlines from "./Headlines.vue";
import Subject from "./Subject.vue";
import { ElInput, ElSpace, ElImage, ElDivider, ElCol, ElRow } from "element-plus"; import { ElInput, ElSpace, ElImage, ElDivider, ElCol, ElRow } from "element-plus";
import AreaTag from '@/components/base/AreaTag/index.vue'
import CommonText from "@/components/base/texts/CommonText.vue";
import NewsMain from "./NewsMain.vue"; import NewsMain from "./NewsMain.vue";
import SearchBox from "@/components/base/SearchBox/index.vue"; import SearchBox from "@/components/base/SearchBox/index.vue";
//顶部数据搜索 //顶部数据搜索
const searchInput = ref(""); const searchInput = ref("");
//当前页面显示 //当前页面显示
const showPage = ref("home"); const showPage = ref("home");
const changePage = page => {
console.log(page);
showPage.value = page;
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
...@@ -104,6 +104,11 @@ export default defineConfig({ ...@@ -104,6 +104,11 @@ export default defineConfig({
proxyReq.setHeader('Cache-Control', 'no-cache'); proxyReq.setHeader('Cache-Control', 'no-cache');
}); });
} }
},
'/intelligent-api': {
target: 'http://8.140.26.4:10029/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/intelligent-api/, '')
} }
} }
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论