提交 de9b61c0 authored 作者: 刘宇琪's avatar 刘宇琪

刘宇琪 更新智库人物页面

上级 592d75b3
<template>
<div class="resume-card">
<div class="resume-card__header">
<div class="resume-card__indicator"></div>
<div class="resume-card__title">{{ title }}</div>
<div class="resume-card__tabs" v-if="tabs.length > 0">
<div
v-for="(tab, i) in tabs"
:key="tab.key"
class="resume-card__tab"
:class="{ 'resume-card__tab--active': activeTab === tab.key }"
@click="switchTab(tab.key)"
>{{ tab.label }}</div>
</div>
<div class="resume-card__actions">
<img
:src="downloadIcon"
alt="下载"
class="resume-card__action-btn"
@click="$emit('download')"
/>
<img
:src="collectIcon"
alt="收藏"
class="resume-card__action-btn"
@click="$emit('collect')"
/>
</div>
</div>
<div class="resume-card__body">
<div v-for="(item, index) in currentList" :key="index" class="resume-item">
<img :src="timelineIcon" alt="" class="resume-item__icon" />
<div class="resume-item__time">{{ item.startTime + '-' + item.endTime }}</div>
<div class="resume-item__title">{{ item.orgName + '|' + item.jobName }}</div>
<div class="resume-item__content">{{ item.content }}</div>
<div class="resume-item__door" v-if="item.door">
<img :src="doorIcon" alt="" />
<span>{{ item.door }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import defaultDownloadIcon from "../assets/下载按钮.png";
import defaultCollectIcon from "../assets/收藏按钮.png";
import defaultTimelineIcon from "../assets/icon01.png";
import defaultDoorIcon from "../assets/icon02.png";
export default {
name: "ResumeCard",
props: {
/** 卡片标题 */
title: {
type: String,
default: "生涯履历",
},
/**
* Tab 配置数组,示例:
* [{ key: "career", label: "职业履历" }, { key: "edu", label: "教育履历" }]
* 不传则无 tab,直接显示 list 数据
*/
tabs: {
type: Array,
default: () => [],
},
/**
* 无 tab 时的数据数组;有 tab 时忽略此 prop,使用 dataMap
*/
list: {
type: Array,
default: () => [],
},
/**
* 有 tab 时的数据映射,key 对应 tabs[].key,示例:
* { career: [...], edu: [...] }
*/
dataMap: {
type: Object,
default: () => ({}),
},
/** 自定义图标路径(可选) */
downloadIcon: {
type: String,
default: defaultDownloadIcon,
},
collectIcon: {
type: String,
default: defaultCollectIcon,
},
timelineIcon: {
type: String,
default: defaultTimelineIcon,
},
doorIcon: {
type: String,
default: defaultDoorIcon,
},
},
emits: ["download", "collect", "tab-change"],
data() {
return {
activeTab: this.tabs.length > 0 ? this.tabs[0].key : "",
};
},
computed: {
/** 当前展示的列表数据 */
currentList() {
if (this.tabs.length > 0 && this.activeTab) {
return this.dataMap[this.activeTab] || [];
}
return this.list;
},
},
methods: {
switchTab(key) {
this.activeTab = key;
this.$emit("tab-change", key);
},
},
};
</script>
<style scoped lang="scss">
.resume-card {
width: 520px;
min-height: 200px;
background-color: rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1);
}
.resume-card__header {
width: 100%;
height: 80px;
display: flex;
align-items: center;
padding: 14px 12px 40px 0;
}
.resume-card__indicator {
width: 8px;
height: 20px;
background-color: rgb(5, 95, 194);
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
margin-right: 14px;
}
.resume-card__title {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(5, 95, 194);
}
.resume-card__tabs {
display: flex;
align-items: center;
gap: 12px;
margin-left: 20px;
}
.resume-card__tab {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
padding: 2px 12px;
border-radius: 4px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.2s;
&:hover {
color: rgb(5, 95, 194);
}
&--active {
color: rgb(5, 95, 194);
font-weight: 700;
border-color: rgb(5, 95, 194);
background-color: rgba(5, 95, 194, 0.05);
}
}
.resume-card__actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
}
.resume-card__action-btn {
width: 28px;
height: 28px;
cursor: pointer;
}
.resume-card__body {
width: 480px;
margin-left: 16px;
padding-bottom: 20px;
}
.resume-item {
width: 454px;
// margin-bottom: 60px;
margin-left: 26px;
position: relative;
}
.resume-item__icon {
width: 14px;
height: 12.13px;
position: absolute;
top: 8px;
left: -26px;
}
.resume-item__time {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 30px;
color: rgb(5, 95, 194);
margin-bottom: 8px;
}
.resume-item__title {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 30px;
color: rgb(59, 65, 75);
margin-bottom: 8px;
}
.resume-item__content {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
margin-bottom: 8px;
}
.resume-item__door {
width: 300px;
height: 32px;
display: flex;
align-items: center;
padding: 4px 0 4px 11px;
border-radius: 4px;
background-color: rgba(255, 246, 240, 1);
border: 1px solid rgba(250, 140, 22, 0.4);
img {
width: 20px;
height: 24px;
margin-right: 10px;
}
span {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgba(255, 149, 77, 1);
}
}
</style>
...@@ -118,7 +118,7 @@ const getCharacterGlobalInfoFn = async () => { ...@@ -118,7 +118,7 @@ const getCharacterGlobalInfoFn = async () => {
const res = await getCharacterGlobalInfo(params); const res = await getCharacterGlobalInfo(params);
if (res.code === 200) { if (res.code === 200) {
console.log("人物全局信息", res); console.log("人物全局信息4", res);
if (res.data) { if (res.data) {
characterInfo.value = res.data; characterInfo.value = res.data;
} }
......
/**
* 新闻动态模块 Mock API
*/
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const NEWS_TITLES = [
'解决春节回家难的问题',
'全国新能源汽车产销双增长',
'人工智能大模型发展新趋势',
'量子计算芯片突破性进展',
'深海资源开发战略规划出台',
'航空航天技术再创新高',
'生物医药产业集群加速形成',
'新材料研发助力制造升级',
'半导体产业链自主可控提速',
'清洁能源装机容量再创纪录',
'5G通信网络覆盖全面铺开',
'极地科考取得重要发现',
]
const NEWS_SOURCES = ['河南新闻', '新华社', '人民日报', '中新网', '科技日报', '央视新闻']
/**
* 获取新闻列表
*/
export async function fetchNewsList(params = {}) {
await delay(300)
const page = params.page || 1
const pageSize = params.pageSize || 12
const total = 1059
const list = Array.from({ length: pageSize }, (_, i) => ({
id: (page - 1) * pageSize + i + 1,
title: NEWS_TITLES[i % NEWS_TITLES.length],
date: '2025年6月26日',
source: NEWS_SOURCES[i % NEWS_SOURCES.length],
image: null,
}))
return { code: 200, data: { list, total } }
}
<template>
<div class="news-card">
<div class="news-card-image">
<img v-if="item.imageUrl" :src="item.imageUrl" :alt="item.name" />
<div v-else class="news-card-placeholder">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
<rect width="48" height="48" rx="6" fill="rgba(5,95,194,0.06)" />
<path d="M16 32l6-8 4 5 6-8 6 11H16z" fill="rgba(5,95,194,0.15)" />
<circle cx="20" cy="20" r="3" fill="rgba(5,95,194,0.15)" />
</svg>
</div>
</div>
<div class="news-card-content">
<p class="news-card-title">{{ item.name }}</p>
<div class="news-card-meta">
<span class="news-card-date">{{ item.time }}</span>
<span class="news-card-source">
{{item.sourceName}}
</span>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
item: {
type: Object,
required: true,
},
})
</script>
<style scoped>
.news-card {
width: 398px;
height: 300px;
border-radius: 10px;
background: rgba(255, 255, 255, 1);
border: 1px solid rgba(230, 231, 232, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
overflow: hidden;
position: relative;
cursor: pointer;
transition: box-shadow 0.2s;
}
.news-card:hover {
box-shadow: 0px 0px 24px 0px rgba(25, 69, 130, 0.16);
}
.news-card-image {
position: absolute;
left: 17px;
right: 17px;
top: 15px;
bottom: 105px;
border-radius: 6px;
overflow: hidden;
background: rgba(247, 248, 249, 1);
}
.news-card-image img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.news-card-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(5, 95, 194, 0.04) 0%, rgba(5, 95, 194, 0.08) 100%);
}
.news-card-content {
position: absolute;
left: 21px;
right: 17px;
bottom: 15px;
height: 78px;
}
.news-card-source{
position: absolute;
right: 0;
right: 0;
top: 0;
font-size: 18px;
font-family: 'Source Han Sans CN', sans-serif;
font-weight: 700;
line-height: 24px;
color: rgba(59, 65, 75, 1);
text-align: justify;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
}
.news-card-title {
position: absolute;
left: 0;
right: 0;
top: 0;
font-size: 18px;
font-family: 'Source Han Sans CN', sans-serif;
font-weight: 700;
line-height: 24px;
color: rgba(59, 65, 75, 1);
text-align: justify;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
}
.news-card-meta {
position: absolute;
left: 0;
right: 28px;
bottom: 0;
display: flex;
justify-content: space-between;
}
.news-card-date {
font-size: 14px;
font-family: 'Source Han Sans CN', sans-serif;
font-weight: 400;
line-height: 22px;
color: rgba(95, 101, 108, 1);
}
.news-card-source {
font-size: 14px;
font-family: 'Source Han Sans CN', sans-serif;
font-weight: 400;
line-height: 22px;
color: rgba(95, 101, 108, 1);
position: absolute;
right: -28px;
bottom: 0;
}
</style>
<template>
<div class="news-pagination-bar">
<span class="news-pagination-total">{{ total }}篇新闻报告</span>
<div class="news-pagination">
<button class="pg-btn" :disabled="currentPage <= 1" @click="$emit('update:currentPage', currentPage - 1)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M10 3l-5 5 5 5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<template v-for="p in pageList" :key="p">
<span v-if="p === '...'" class="pg-ellipsis">...</span>
<button
v-else
:class="['pg-num', { active: p === currentPage }]"
@click="$emit('update:currentPage', p)"
>{{ p }}</button>
</template>
<button class="pg-btn" :disabled="currentPage >= totalPages" @click="$emit('update:currentPage', currentPage + 1)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 3l5 5-5 5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 },
pageSize: { type: Number, default: 12 },
})
defineEmits(['update:currentPage'])
const totalPages = computed(() => Math.ceil(props.total / props.pageSize) || 1)
const pageList = computed(() => {
const tp = totalPages.value
const cp = props.currentPage
if (tp <= 7) return Array.from({ length: tp }, (_, i) => i + 1)
const pages = []
pages.push(1)
if (cp > 3) pages.push('...')
const start = Math.max(2, cp - 1)
const end = Math.min(tp - 1, cp + 1)
for (let i = start; i <= end; i++) pages.push(i)
if (cp < tp - 2) pages.push('...')
pages.push(tp)
return pages
})
</script>
<style scoped>
.news-pagination-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0 0 0;
}
.news-pagination-total {
font-size: 14px;
font-family: 'Microsoft YaHei', sans-serif;
font-weight: 400;
color: rgba(132, 136, 142, 1);
}
.news-pagination {
display: flex;
align-items: center;
gap: 8px;
}
.pg-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
color: rgba(95, 101, 108, 1);
cursor: pointer;
transition: all 0.15s;
}
.pg-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pg-num {
min-width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
font-size: 14px;
font-family: 'Microsoft YaHei', sans-serif;
font-weight: 400;
color: rgba(59, 65, 75, 1);
cursor: pointer;
transition: all 0.15s;
padding: 0 4px;
}
.pg-num.active {
background: rgba(5, 95, 194, 1);
border-color: rgba(5, 95, 194, 1);
color: #fff;
}
.pg-ellipsis {
font-size: 14px;
color: rgba(132, 136, 142, 1);
width: 32px;
text-align: center;
}
</style>
<template>
<div class="news-sidebar">
<!-- Section: 科技领域 -->
<div class="ns-section">
<div class="ns-section-header">
<span class="ns-header-bar"></span>
<span class="ns-header-title">科技领域</span>
</div>
<div class="ns-options">
<label
v-for="opt in domainOptions"
:key="opt.id"
class="ns-option"
>
<span
:class="['ns-checkbox', { checked: selectedDomains.includes(opt.id) }]"
@click.prevent="$emit('toggle-domain', opt.id)"
>
<svg v-if="selectedDomains.includes(opt.id)" width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect width="14" height="14" rx="4" fill="rgba(5,95,194,1)" />
<path d="M3.5 7l2.5 2.5 4.5-5" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
</svg>
</span>
<span class="ns-option-label" @click="$emit('toggle-domain', opt.id)">{{ opt.label }}</span>
</label>
</div>
</div>
<div class="ns-section">
<div class="ns-section-header">
<span class="ns-header-bar"></span>
<span class="ns-header-title">发布时间</span>
</div>
<div class="ns-options">
<label
v-for="opt in timeOptions"
:key="opt.id"
class="ns-option"
>
<span
:class="['ns-checkbox', { checked: selectedTimes.includes(opt.id) }]"
@click.prevent="$emit('toggle-time', opt.id)"
>
<svg v-if="selectedTimes.includes(opt.id)" width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect width="14" height="14" rx="4" fill="rgba(5,95,194,1)" />
<path d="M3.5 7l2.5 2.5 4.5-5" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
</svg>
</span>
<span class="ns-option-label" @click="$emit('toggle-time', opt.id)">{{ opt.label }}</span>
</label>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
domainOptions: { type: Array, default: () => [] },
timeOptions: { type: Array, default: () => [] },
selectedDomains: { type: Array, default: () => ['all'] },
selectedTimes: { type: Array, default: () => ['all'] },
})
defineEmits(['toggle-domain', 'toggle-time'])
</script>
<style scoped>
.news-sidebar {
width: 360px;
flex-shrink: 0;
background: rgba(255, 255, 255, 1);
border-radius: 10px;
border: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
padding: 19px 0 24px 0;
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
}
.ns-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.ns-section-header {
display: flex;
flex-direction: row;
gap: 17px;
align-items: center;
}
.ns-header-bar {
width: 8px;
height: 16px;
background: rgba(5, 95, 194, 1);
border-radius: 0 4px 4px 0;
flex-shrink: 0;
}
.ns-header-title {
font-size: 16px;
font-family: 'Source Han Sans CN', sans-serif;
font-weight: 700;
letter-spacing: 1px;
line-height: 24px;
color: rgba(5, 95, 194, 1);
}
.ns-options {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px 4px;
padding: 0 0 0 24px;
}
.ns-option {
width: 160px;
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
cursor: pointer;
}
.ns-checkbox {
width: 14px;
height: 14px;
flex-shrink: 0;
border-radius: 4px;
border: 1px solid rgba(230, 231, 232, 1);
display: flex;
align-items: center;
justify-content: center;
background: transparent;
}
.ns-checkbox.checked {
border: none;
}
.ns-checkbox.checked svg {
display: block;
}
.ns-option-label {
font-size: 16px;
font-family: 'Source Han Sans CN', sans-serif;
font-weight: 400;
line-height: 24px;
color: rgba(95, 101, 108, 1);
user-select: none;
}
</style>
<template>
<div class="news-topbar">
<div class="news-search-wrapper">
<div class="news-search-box">
<input
type="text"
class="news-search-input"
:placeholder="'搜索新闻动态'"
:value="searchText"
@input="$emit('update:searchText', $event.target.value)"
/>
<svg class="news-search-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="7" cy="7" r="5.5" stroke="rgba(132,136,142,1)" stroke-width="1.2" />
<line x1="11" y1="11" x2="14" y2="14" stroke="rgba(132,136,142,1)" stroke-width="1.2" stroke-linecap="round" />
</svg>
</div>
</div>
<div class="news-tabs">
<button
:class="['news-tab', { active: activeTab === 'local' }]"
@click="$emit('update:activeTab', 'local')"
>智库报告</button>
<button
:class="['news-tab', { active: activeTab === 'capital' }]"
@click="$emit('update:activeTab', 'capital')"
>调查项目</button>
</div>
<div class="news-sort" ref="sortDropdownRef">
<button class="news-sort-btn" @click="showSortDropdown = !showSortDropdown">
<svg class="news-sort-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="4" y="3" width="2" height="10" rx="1" fill="rgba(95,101,108,1)" />
<rect x="10" y="6" width="2" height="7" rx="1" fill="rgba(95,101,108,1)" />
</svg>
<span class="news-sort-label">{{ currentSortLabel }}</span>
<svg class="news-sort-arrow" :class="{ open: showSortDropdown }" width="11" height="6" viewBox="0 0 11 6" fill="none">
<path d="M1 1l4.5 4L10 1" stroke="rgba(95,101,108,1)" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<div v-if="showSortDropdown" class="news-sort-dropdown">
<button
v-for="opt in sortOptions"
:key="opt.value"
@click="selectSort(opt.value)"
class="news-sort-option"
:class="{ active: sortBy === opt.value }"
>{{ opt.label }}</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({
searchText: { type: String, default: '' },
activeTab: { type: String, default: 'local' },
sortBy: { type: String, default: 'publishTimeDesc' },
sortOptions: {
type: Array,
default: () => [
{ value: 'publishTimeDesc', label: '发布时间倒序' },
{ value: 'publishTimeAsc', label: '发布时间正序' },
],
},
})
const emit = defineEmits(['update:searchText', 'update:activeTab', 'update:sort'])
const showSortDropdown = ref(false)
const sortDropdownRef = ref(null)
const currentSortLabel = computed(() => {
const opt = props.sortOptions.find((o) => o.value === props.sortBy)
return opt ? opt.label : '发布时间倒序'
})
function selectSort(value) {
emit('update:sort', value)
showSortDropdown.value = false
}
function handleClickOutside(e) {
if (sortDropdownRef.value && !sortDropdownRef.value.contains(e.target)) {
showSortDropdown.value = false
}
}
onMounted(() => document.addEventListener('click', handleClickOutside))
onUnmounted(() => document.removeEventListener('click', handleClickOutside))
</script>
<style scoped>
.news-topbar {
display: flex;
align-items: center;
gap: 16px;
width: 100%;
height: 32px;
margin-bottom: 16px;
}
.news-search-wrapper {
width: 360px;
height: 32px;
flex-shrink: 0;
}
.news-search-box {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
padding: 5px 8px 5px 12px;
border-radius: 4px;
border: 1px solid rgba(230, 231, 232, 1);
background: rgba(255, 255, 255, 1);
}
.news-search-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 14px;
font-family: 'Microsoft YaHei', sans-serif;
font-weight: 400;
line-height: 22px;
color: rgba(59, 65, 75, 1);
}
.news-search-input::placeholder {
color: rgba(132, 136, 142, 1);
}
.news-search-icon {
flex-shrink: 0;
}
.news-tabs {
display: flex;
flex-direction: row;
gap: 8px;
justify-content: center;
align-items: flex-start;
}
.news-tab {
width: 120px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
padding: 4px 24px;
border-radius: 21px;
font-size: 16px;
font-family: 'Source Han Sans CN', sans-serif;
font-weight: 400;
line-height: 24px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid rgba(234, 236, 238, 1);
background: rgba(255, 255, 255, 1);
color: rgba(95, 101, 108, 1);
}
.news-tab.active {
background: rgba(5, 95, 194, 1);
border-color: rgba(5, 95, 194, 1);
color: rgba(255, 255, 255, 1);
font-weight: 700;
}
.news-sort {
margin-left: auto;
position: relative;
flex-shrink: 0;
}
.news-sort-btn {
width: 120px;
height: 32px;
display: flex;
align-items: center;
padding: 0 12px 0 7px;
gap: 0;
border-radius: 4px;
border: 1px solid rgba(230, 231, 232, 1);
background: rgba(255, 255, 255, 1);
cursor: pointer;
box-sizing: border-box;
}
.news-sort-btn:hover {
border-color: rgba(5, 95, 194, 0.4);
}
.news-sort-icon {
flex-shrink: 0;
margin-right: 6px;
}
.news-sort-label {
flex: 1;
font-size: 14px;
font-family: 'Microsoft YaHei', sans-serif;
font-weight: 400;
color: rgba(95, 101, 108, 1);
white-space: nowrap;
}
.news-sort-arrow {
flex-shrink: 0;
transition: transform 0.2s;
}
.news-sort-arrow.open {
transform: rotate(180deg);
}
.news-sort-dropdown {
position: absolute;
right: 0;
top: 100%;
margin-top: 4px;
min-width: 120px;
background: #fff;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 8px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
padding: 4px 0;
z-index: 20;
}
.news-sort-option {
display: block;
width: 100%;
text-align: left;
padding: 8px 12px;
font-size: 14px;
font-family: 'Microsoft YaHei', sans-serif;
background: none;
border: none;
cursor: pointer;
color: rgba(95, 101, 108, 1);
}
.news-sort-option:hover {
background: rgba(247, 248, 249, 1);
}
.news-sort-option.active {
color: rgba(5, 95, 194, 1);
font-weight: 500;
}
</style>
<template>
<div class="news-tracker">
<NewsTopBar
v-model:searchText="searchText"
v-model:activeTab="activeTab"
:sort-by="sortBy"
@update:sort="updateSort"
/>
<div class="news-body">
<NewsSidebar
:domain-options="domainOptions"
:time-options="timeOptions"
:selected-domains="selectedDomains"
:selected-times="selectedTimes"
@toggle-domain="toggleDomain"
@toggle-time="toggleTime"
/>
<div class="news-main">
<div class="news-grid">
<NewsCard
v-for="item in newsList"
:key="item.reportId"
:item="item"
/>
</div>
<NewsPagination
:total="totalNews"
v-model:current-page="currentPage"
:page-size="pageSize"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import NewsTopBar from './NewsTopBar.vue'
import NewsSidebar from './NewsSidebar.vue'
import NewsCard from './NewsCard.vue'
import NewsPagination from './NewsPagination.vue'
import { getIndustryKeyList } from '@/api/bill/billHome.js'
import { getFindingsReport } from '@/api/characterPage/characterPage.js'
const route = useRoute()
const personId = computed(() => route.params.personId || route.query.personId || '')
const searchText = ref('')
const activeTab = ref('local')
const currentPage = ref(1)
const pageSize = 12
const totalNews = ref(0)
const newsList = ref([])
const loading = ref(false)
const sortBy = ref('publishTimeDesc')
const domainOptions = ref([])
const timeOptions = ref([])
const selectedDomains = ref(['all'])
const selectedTimes = ref(['all'])
async function loadFilterOptions() {
const res = await getIndustryKeyList()
if (res.code === 200 && res.data) {
domainOptions.value = [
{ id: 'all', label: '全部领域' },
...res.data.map(item => ({ id: item.id, label: item.name })),
]
}
const currentYear = new Date().getFullYear()
timeOptions.value = [
{ id: 'all', label: '全部时间' },
...Array.from({ length: 5 }, (_, i) => {
const year = currentYear - i
return { id: String(year), label: `${year}年` }
}),
]
}
function buildParams() {
const params = {
currentPage: currentPage.value - 1,
pageSize,
sortFun: sortBy.value === 'publishTimeAsc',
}
if (!selectedDomains.value.includes('all')) {
params.industryIds = selectedDomains.value
}
if (!selectedTimes.value.includes('all')) {
params.years = selectedTimes.value
}
return params
}
function updateSort(value) {
sortBy.value = value
currentPage.value = 1
loadNews()
}
async function loadNews() {
if (!personId.value) return
loading.value = true
try {
const params = buildParams()
const res = await getFindingsReport(personId.value, params)
if (res.code === 200 && res.data) {
newsList.value = res.data.content || []
totalNews.value = res.data.totalElements || 0
} else {
newsList.value = []
totalNews.value = 0
}
} finally {
loading.value = false
}
}
function toggleDomain(id) {
if (id === 'all') {
selectedDomains.value = ['all']
} else {
const without = selectedDomains.value.filter(d => d !== 'all')
const idx = without.indexOf(id)
if (idx > -1) without.splice(idx, 1)
else without.push(id)
selectedDomains.value = without.length > 0 ? without : ['all']
}
}
function toggleTime(id) {
if (id === 'all') {
selectedTimes.value = ['all']
} else {
const without = selectedTimes.value.filter(d => d !== 'all')
const idx = without.indexOf(id)
if (idx > -1) without.splice(idx, 1)
else without.push(id)
selectedTimes.value = without.length > 0 ? without : ['all']
}
}
watch(
() => [selectedDomains.value, selectedTimes.value],
() => {
currentPage.value = 1
loadNews()
},
{ deep: true }
)
watch(currentPage, () => {
loadNews()
})
onMounted(async () => {
await loadFilterOptions()
await loadNews()
})
</script>
<style scoped>
.news-tracker {
width: 100%;
background: rgba(247, 248, 249, 1);
padding: 0;
}
.news-body {
display: flex;
flex-direction: row;
gap: 14px;
align-items: flex-start;
}
.news-main {
flex: 1;
min-width: 0;
}
.news-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
align-content: flex-start;
align-items: flex-start;
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论