提交 73afdff3 authored 作者: 张伊明's avatar 张伊明

Merge branch 'master' of http://8.140.26.4:10003/caijian/risk-monitor into zym-dev

...@@ -170,3 +170,99 @@ export function getPostMemberList() { ...@@ -170,3 +170,99 @@ export function getPostMemberList() {
url: `/api/BillDict/member`, url: `/api/BillDict/member`,
}) })
} }
/**
* 获取筛选项配置 - 行业列表
* GET /api/billImpactAnalysis/industry/hylyList
*/
export async function getIndustryKeyList() {
return request('/api/billImpactAnalysis/industry/hylyList', {
method: 'GET',
})
}
/**
* 获取进度阶段配置
* GET /api/commonDict/bill/stage
*/
export async function getBillStageConfig() {
return request('/api/commonDict/bill/stage', {
method: 'GET',
})
}
/**
* 获取法案列表
* GET /api/personHomepage/historyBill/{personId}
* @param {string} personId - 人物ID
* @param {Object} params - 查询参数(不包含分页参数)
*/
export async function getHistoryBillList(personId, params = {}) {
const queryString = Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => {
if (Array.isArray(value)) {
return value.map(v => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`).join('&')
}
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
})
.join('&')
const url = queryString
? `/api/personHomepage/historyBill/${personId}?${queryString}`
: `/api/personHomepage/historyBill/${personId}`
return request(url, {
method: 'GET',
})
}
/**
* 获取法案列表(含阶段字典)
* @param {string} personId - 人物ID
* @param {Object} params - 查询参数
*/
export async function getHistoryBillListWithStage(personId, params = {}) {
const stageRes = await getBillStageConfig()
const billRes = await getHistoryBillList(personId, params)
return {
stageConfig: stageRes,
billList: billRes,
}
}
export function getSortOptions() {
return Promise.resolve({
code: 200,
data: [
{ value: 'latestMotionTimeDesc', label: '最新修议时间倒序' },
{ value: 'latestMotionTimeAsc', label: '最新修议时间正序' },
],
})
}
export async function getPotentialNewsList(personId, params = {}) {
const queryString = Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
const url = queryString
? `/api/personHomepage/historyBill/clause/${personId}?${queryString}`
: `/api/personHomepage/historyBill/clause/${personId}`
return request(url, {
method: 'GET',
})
}
export async function getPotentialNewsKeywords(personId) {
return request(`/api/personHomepage/historyBill/clauseKeyword/${personId}`, {
method: 'GET',
})
}
\ No newline at end of file
...@@ -99,7 +99,7 @@ export function getCharacterResume(params) { ...@@ -99,7 +99,7 @@ export function getCharacterResume(params) {
export function getCharacterView(params) { export function getCharacterView(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/personHomepage/option/${params.personId}/${params.year}`, url: `/api/personHomepage/option/`,
params, params,
}) })
} }
...@@ -112,7 +112,7 @@ export function getCharacterView(params) { ...@@ -112,7 +112,7 @@ export function getCharacterView(params) {
export function getCharacterFundSource(params) { export function getCharacterFundSource(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/personHomepage/personFunds/${params.personId}/${params.year}`, url: `/api/personHomepage/personFunds/`,
params, params,
}) })
} }
...@@ -142,3 +142,61 @@ export function getCharacterRelatedEntity(params) { ...@@ -142,3 +142,61 @@ export function getCharacterRelatedEntity(params) {
params, params,
}) })
} }
// 获取人物教育履历
/**
* @param {personId}
* @header token
*/
export function getCharacterReducationResume(params) {
return request({
method: 'GET',
url: `/api/personHomepage/educationResume/${params.personId}`,
params,
})
}
export async function getFindingsReport(personId, params = {}) {
const queryParts = []
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') return
if (Array.isArray(value)) {
value.forEach(v => {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`)
})
} else {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
}
})
const queryString = queryParts.join('&')
const url = queryString
? `/api/personHomepage/findingsReport/${personId}?${queryString}`
: `/api/personHomepage/findingsReport/${personId}`
return request(url, { method: 'GET' })
}
/**
* 获取创新主体列表(大学/实验室/企业)
* @param {Object} params - 请求参数
* @param {string} params.arealist - 科技领域ID
* @param {number} params.currentPage - 当前页码(从0开始)
* @param {string} params.keywords - 搜索关键词
* @param {number} params.pageSize - 每页条数
* @param {string} params.subjectTypeId - 主体类型ID
* @returns {Promise} 返回列表数据
*/
export async function getSubjectList(params) {
return request('/api/innovateSubject/findListBySubjectTypeId', {
method: 'GET',
params
})
}
export function getareaType(params) {
return request({
method: 'GET',
url: `/api/commonDict/areaType`,
params
})
}
\ No newline at end of file
...@@ -2,10 +2,11 @@ import request from "@/api/request.js"; ...@@ -2,10 +2,11 @@ import request from "@/api/request.js";
// 概览页-------------------------------------------------------------------- // 概览页--------------------------------------------------------------------
// 概览统计 // 概览统计
export function getStatCount() { export function getStatCount(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/marketsearchHome/statCount` url: `/api/marketsearchHome/statCount`,
params
}) })
} }
......
...@@ -55,3 +55,10 @@ export function getAreaList() { ...@@ -55,3 +55,10 @@ export function getAreaList() {
url: `/api/commonDict/areaType`, url: `/api/commonDict/areaType`,
}) })
} }
export function getNewsDetail(params) {
return request({
method: 'GET',
url: `/api/news/findById/${params.newsId}`,
params
})
}
const getGraphChart = (nodes, links, layoutType) => {
const option = {
// title: {
// text: '企业关系网络',
// subtext: '节点图标表示企业,箭头表示关联方向',
// top: 'top',
// left: 'center',
// textStyle: {
// fontSize: 20,
// color: '#2c3e50'
// }
// },
// tooltip: {
// formatter: function (params) {
// if (params.dataType === 'node') {
// return `<div style="font-weight:bold;margin-bottom:5px">${params.data.name}</div>
// <div>类别: ${categories[params.data.category].name}</div>
// <div>关联度: ${params.data.value}</div>`;
// } else {
// return `<div>${nodes[params.data.source].name} → ${nodes[params.data.target].name}</div>
// <div>关联强度: ${params.data.value}</div>`;
// }
// }
// },
legend: {
// data: categories.map(c => c.name),
show: false,
top: 40,
textStyle: {
fontSize: 12
}
},
animation: true,
animationDuration: 1000,
animationEasing: 'cubicOut',
series: [{
type: 'graph',
itemStyle: {
color: '#73C0DE'
},
layout: layoutType,
data: nodes,
links: links,
// categories: categories,
roam: true,
label: {
show: true,
position: 'bottom',
formatter: '{b}',
fontSize: 12,
fontWeight: 'bold',
// backgroundColor: 'rgba(255,255,255,0.8)',
padding: [4, 6],
borderRadius: 4
},
lineStyle: {
color: 'source',
curveness: 0,
width: 2,
type: 'dashed',
color: '#AED6FF'
},
edgeSymbol: ['none', 'arrow'],
edgeSymbolSize: [0, 10],
emphasis: {
focus: 'adjacency',
lineStyle: {
width: 4
},
label: {
show: true,
fontSize: 14
}
},
force: {
repulsion: 300,
gravity: 0,
edgeLength: 300
},
edgeLabel: {
show: true,
position: 'middle',
fontSize: 14,
color: '#333',
backgroundColor: 'rgba(255,255,255,0.9)',
borderColor: '#bbb',
borderWidth: 1,
borderRadius: 4,
padding: [4, 8],
formatter: params => params.data.label ? params.data.label.formatter : ''
}
// edgeLabel: {
// show: true,
// position: 'middle',
// // fontSize: 14,
// // backgroundColor: 'rgba(255,255,255,0.9)',
// // borderColor: '#bbb',
// borderWidth: 1,
// borderRadius: 4,
// padding: [4, 8],
// // 核心:使用 formatter 函数
// formatter: function (params) {
// console.log('完整 params:', params);
// console.log('label 数据:', params.data.label);
// // 获取标签的文本内容,它存储在 params.data.label.formatter 里
// const labelText = params.data.label?.formatter || '';
// // 定义一个颜色映射
// const colorMap = {
// '合作': '#52c41a', // 绿色
// '持股': '#faad14', // 橙色
// '从属': '#f5222d' // 红色
// };
// // 根据文本内容获取对应的颜色,如果没有定义则使用默认的灰色
// const color = colorMap[labelText] || '#666';
// // 返回一个带内联样式的 HTML 字符串
// return `<span style="color: ${color};">${labelText}</span>`;
// }
// }
}],
// color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de']
};
return option
}
export default getGraphChart
\ No newline at end of file
<template>
<div class="graph-chart-wrapper" id="graph">
</div>
</template>
<script setup>
import { onMounted, nextTick } from 'vue';
import setChart from '@/utils/setChart';
import getGraphChart from './graphChart';
const props = defineProps({
nodes: {
type: Array,
default: []
},
links: {
type: Array,
default: []
},
layoutType: {
type: String,
default: 'force'
},
width: {
type: String,
default: 'force'
},
height: {
type: String,
default: 'force'
}
})
onMounted(() => {
const graph = getGraphChart(props.nodes, props.links, props.layoutType)
setChart(graph, 'graph')
})
</script>
<style lang="scss" scoped>
.graph-chart-wrapper {
width: 100%;
height: 100%;
}
</style>
\ No newline at end of file
<template>
<div class="search-wrapper">
<div class="search-main" :class="{ 'search-main-with-tabs': enableBillTypeSwitch }">
<input v-model="inputValue" :placeholder="placeholder" @keyup.enter="handleSearch" class="search-input" />
<div class="search-btn" @click="handleSearch">
<img src="@/assets/icons/search-icon.png" alt />
搜索
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
searchText: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
})
const emit = defineEmits(['search', 'update:searchText'])
const inputValue = computed({
get: () => props.searchText,
set: (value) => emit('update:searchText', value)
})
const handleSearch = () => {
emit('search')
}
</script>
<style lang="scss" scoped>
.search-wrapper {
width: 960px;
height: 48px;
.search-main {
display: flex;
padding-right: 3px;
align-items: center;
justify-content: space-between;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
width: 960px;
height: 48px;
background-color: rgba(255, 255, 255, 0.65);
border-radius: 10px;
border: 1px solid #fff;
&:hover {
border: 1px solid var(--color-main-active);
}
.search-input {
border: none;
outline: none;
width: 838px;
height: 48px;
background-color: transparent;
font-size: 14px;
padding: 12px 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 22px;
color: rgba(59, 65, 75, 1);
&::placeholder {
color: #a8abb2;
}
}
.search-btn {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 120px;
height: 46px;
margin-right: -3px;
border-radius: 8px;
background-color: rgb(5, 95, 194);
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 22px;
color: #fff;
img {
width: 18px;
height: 18px;
margin-right: 8px;
}
}
}
}
</style>
\ No newline at end of file
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="url(#clipPath_0)">
<defs>
<clipPath id="clipPath_0">
<rect width="16.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="容器 767" width="16.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" />
<g id="组合 1059">
<path id="矩形 350" d="M2.50019 7.57031L12.4998 7.57048C12.7759 7.57048 12.9992 7.79409 12.9995 8.07012L13 8.07012C13.0001 8.34632 12.776 8.57031 12.4998 8.57031L2.50018 8.57015C2.22419 8.57015 2.00069 8.34649 2.00054 8.07051L2 8.07051C1.9997 7.79434 2.22403 7.57031 2.50019 7.57031Z" fill="rgb(206,79,81)" fill-rule="evenodd" />
<path id="矢量 610" d="M8 3L13 8L8 13" stroke="rgb(206,79,81)" stroke-linecap="round" stroke-width="1.000000" />
</g>
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="url(#clipPath_0)">
<defs>
<clipPath id="clipPath_0">
<rect width="16.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="容器 767" width="16.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" />
<g id="组合 1059">
<path id="矩形 350" d="M2.50019 7.57031L12.4998 7.57048C12.7759 7.57048 12.9992 7.79409 12.9995 8.07012L13 8.07012C13.0001 8.34632 12.776 8.57031 12.4998 8.57031L2.50018 8.57015C2.22419 8.57015 2.00069 8.34649 2.00054 8.07051L2 8.07051C1.9997 7.79434 2.22403 7.57031 2.50019 7.57031Z" fill="rgb(255, 149, 77)" fill-rule="evenodd" />
<path id="矢量 610" d="M8 3L13 8L8 13" stroke="rgb(255, 149, 77)" stroke-linecap="round" stroke-width="1.000000" />
</g>
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="url(#clipPath_0)">
<defs>
<clipPath id="clipPath_0">
<rect width="16.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="容器 767" width="16.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" />
<g id="组合 1059">
<path id="矩形 350" d="M2.50019 7.57031L12.4998 7.57048C12.7759 7.57048 12.9992 7.79409 12.9995 8.07012L13 8.07012C13.0001 8.34632 12.776 8.57031 12.4998 8.57031L2.50018 8.57015C2.22419 8.57015 2.00069 8.34649 2.00054 8.07051L2 8.07051C1.9997 7.79434 2.22403 7.57031 2.50019 7.57031Z" fill="rgb(232, 189, 11)" fill-rule="evenodd" />
<path id="矢量 610" d="M8 3L13 8L8 13" stroke="rgb(232, 189, 11)" stroke-linecap="round" stroke-width="1.000000" />
</g>
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="url(#clipPath_0)">
<defs>
<clipPath id="clipPath_0">
<rect width="16.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="容器 767" width="16.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" />
<g id="组合 1059">
<path id="矩形 350" d="M2.50019 7.57031L12.4998 7.57048C12.7759 7.57048 12.9992 7.79409 12.9995 8.07012L13 8.07012C13.0001 8.34632 12.776 8.57031 12.4998 8.57031L2.50018 8.57015C2.22419 8.57015 2.00069 8.34649 2.00054 8.07051L2 8.07051C1.9997 7.79434 2.22403 7.57031 2.50019 7.57031Z" fill="rgb(33, 129, 57)" fill-rule="evenodd" />
<path id="矢量 610" d="M8 3L13 8L8 13" stroke="rgb(33, 129, 57)" stroke-linecap="round" stroke-width="1.000000" />
</g>
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="url(#clipPath_0)">
<defs>
<clipPath id="clipPath_0">
<rect width="16.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="容器 767" width="16.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" />
<g id="组合 1059">
<path id="矩形 350" d="M2.50019 7.57031L12.4998 7.57048C12.7759 7.57048 12.9992 7.79409 12.9995 8.07012L13 8.07012C13.0001 8.34632 12.776 8.57031 12.4998 8.57031L2.50018 8.57015C2.22419 8.57015 2.00069 8.34649 2.00054 8.07051L2 8.07051C1.9997 7.79434 2.22403 7.57031 2.50019 7.57031Z" fill="rgb(5, 95, 194)" fill-rule="evenodd" />
<path id="矢量 610" d="M8 3L13 8L8 13" stroke="rgb(5, 95, 194)" stroke-linecap="round" stroke-width="1.000000" />
</g>
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="#000000">
<rect id="容器 1922" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="减去顶层" d="M15.8612 13.4253L8.86371 1.38047C8.47821 0.716925 7.51986 0.716925 7.13436 1.38047L0.136829 13.4253C-0.250475 14.0919 0.230498 14.9276 1.0015 14.9276L14.9966 14.9276C15.7676 14.9276 16.2485 14.0919 15.8612 13.4253ZM8.73076 9.86062C8.71726 10.2202 8.42184 10.5047 8.06201 10.5047C7.7017 10.5047 7.40606 10.2195 7.39322 9.85936L7.25891 6.0939C7.24261 5.63713 7.60849 5.25797 8.06555 5.25797C8.5232 5.25797 8.8893 5.63808 8.87213 6.09541L8.73076 9.86062ZM7.21926 11.7574C7.26412 11.6666 7.32814 11.5851 7.41131 11.513C7.46859 11.4633 7.52999 11.4215 7.59553 11.3876C7.73661 11.3146 7.89683 11.278 8.07618 11.278C8.26676 11.278 8.43415 11.3187 8.57836 11.3999C8.6354 11.4321 8.68881 11.4706 8.73859 11.5154C8.81988 11.5886 8.88236 11.6708 8.92603 11.7618C8.97673 11.8675 9.00208 11.9853 9.00208 12.115C9.00208 12.2317 8.981 12.3396 8.93885 12.4386C8.89617 12.5388 8.83188 12.63 8.74598 12.7121C8.66424 12.7902 8.5746 12.8496 8.47706 12.8903C8.37086 12.9346 8.25529 12.9568 8.13036 12.9568C7.86357 12.9568 7.64481 12.8932 7.47407 12.7659C7.45149 12.7491 7.42975 12.7311 7.40885 12.7121C7.31197 12.6238 7.24131 12.5249 7.19688 12.4155C7.15926 12.3229 7.14044 12.2227 7.14044 12.115C7.14044 11.983 7.16671 11.8638 7.21926 11.7574Z" fill="rgb(255,255,255)" fill-rule="evenodd" />
</svg>
<template>
<div class="warnning-pane-wrapper" :style="{ width: width ? width : '1600px', height: height ? height : '116px' }"
:class="{
level1: warnningLevel === '特别重大风险',
level2: warnningLevel === '重大风险',
level3: warnningLevel === '较大风险',
level4: warnningLevel === '一般风险',
}" @click="handleClickPane">
<div class="warnning-pane-header">
<div class="header-left" :class="{
'header-left-level1': warnningLevel === '特别重大风险',
'header-left-level2': warnningLevel === '重大风险',
'header-left-level3': warnningLevel === '较大风险',
'header-left-level4': warnningLevel === '一般风险',
}">
<div class="warnning-icon">
<img src="./icons/warnning-icon.svg" alt="">
</div>
<div class="warnning-text text-title-3-bold">{{ warnningLevel }}</div>
</div>
<div class="header-right">
<img v-if="warnningLevel === '特别重大风险'" src="./icons/level1.svg" alt="">
<img v-else-if="warnningLevel === '重大风险'" src="./icons/level2.svg" alt="">
<img v-else-if="warnningLevel === '较大风险'" src="./icons/level3.svg" alt="">
<img v-else-if="warnningLevel === '一般风险'" src="./icons/level4.svg" alt="">
<img v-else src="./icons/level5.svg" alt="">
</div>
</div>
<div class="warnning-pane-content text-regular">
{{ warnningContent }}
</div>
</div>
</template>
<script setup>
import { ElMessage } from 'element-plus'
const props = defineProps(
{
warnningContent: {
type: String,
default: ''
},
warnningLevel: {
type: String,
default: '低风险'
}
}
)
const emit = defineEmits(['clickPane'])
const handleClickPane = () => {
ElMessage.success('点击了预警面板')
emit('clickPane')
}
</script>
<style lang="scss" scoped>
.warnning-pane-wrapper {
border-radius: 10px;
border: 1px solid var(--color-primary-100);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
cursor: pointer;
}
.level1 {
border: 1px solid var(--color-red-100) !important;
}
.level2 {
border: 1px solid var(--color-orange-100) !important;
}
.level3 {
border: 1px solid var(--color-yellow-100) !important;
}
.level4 {
border: 1px solid var(--color-green-100) !important;
}
.warnning-pane-header {
height: 44px;
display: flex;
justify-content: space-between;
}
.header-left {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
width: 164px;
height: 32px;
background: var(--color-main-active);
border-radius: 10px 0 10px 0;
}
.header-left-level1 {
background: var(--color-red-100) !important;
}
.header-left-level2 {
background: var(--color-orange-100) !important;
}
.header-left-level3 {
background: var(--color-yellow-100) !important;
}
.header-left-level4 {
background: var(--color-green-100) !important;
}
.warnning-icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.warnning-text {
color: #fff;
}
.header-right {
width: 16px;
height: 16px;
margin-top: 17px;
margin-right: 16px;
img {
width: 100%;
height: 100%;
}
}
.warnning-pane-content{
width: calc(100% - 40px);
margin: 0 auto;
height: 60px;
display: -webkit-box;
/* 2. 设置内部布局方向为垂直 */
-webkit-box-orient: vertical;
/* 3. 限制显示的行数为 2 行 */
-webkit-line-clamp: 2;
/* 4. 隐藏超出部分 */
overflow: hidden;
/* 5. 设置文本溢出显示省略号 */
text-overflow: ellipsis;
}
</style>
\ No newline at end of file
...@@ -139,7 +139,7 @@ const emit = defineEmits(['save', 'download', 'collect']) ...@@ -139,7 +139,7 @@ const emit = defineEmits(['save', 'download', 'collect'])
.header-btn1 { .header-btn1 {
position: absolute; position: absolute;
top: 14px; top: 14px;
right: 104px; right: 116px;
} }
.header-right { .header-right {
......
...@@ -21,9 +21,10 @@ const props = defineProps({ ...@@ -21,9 +21,10 @@ const props = defineProps({
// SVG颜色 // SVG颜色
color: { color: {
type: String, type: String,
default: '#000' default: null
} }
, size: { ,
size: {
type: Number, type: Number,
default: null default: null
} }
...@@ -43,6 +44,7 @@ const processedSvgContent = computed(() => { ...@@ -43,6 +44,7 @@ const processedSvgContent = computed(() => {
// 替换SVG中的颜色 // 替换SVG中的颜色
let processed = svgContent.value; let processed = svgContent.value;
if (props.color) {
// 替换fill属性 // 替换fill属性
processed = processed.replace(/fill="([^"]*)"/g, (match, p1) => { processed = processed.replace(/fill="([^"]*)"/g, (match, p1) => {
...@@ -61,7 +63,9 @@ const processedSvgContent = computed(() => { ...@@ -61,7 +63,9 @@ const processedSvgContent = computed(() => {
} }
return `stroke="${props.color}"`; return `stroke="${props.color}"`;
}); });
}
if (props.size) {
// 替换width属性 // 替换width属性
processed = processed.replace(/width="([^"]*)"/g, (match, p1) => { processed = processed.replace(/width="([^"]*)"/g, (match, p1) => {
if (props.size !== null) { if (props.size !== null) {
...@@ -77,6 +81,7 @@ const processedSvgContent = computed(() => { ...@@ -77,6 +81,7 @@ const processedSvgContent = computed(() => {
} }
return match; return match;
}); });
}
console.log(processed) console.log(processed)
return processed; return processed;
...@@ -131,10 +136,14 @@ onMounted(() => { ...@@ -131,10 +136,14 @@ onMounted(() => {
<style scoped> <style scoped>
.color-svg { .color-svg {
display: inline-block; display: inline-block;
/* svg垂直居中 */
vertical-align: middle;
} }
.svg-container { .svg-container {
display: inline-block; display: flex;
align-items: center;
justify-content: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
......
...@@ -10,11 +10,10 @@ ...@@ -10,11 +10,10 @@
</div> </div>
<!-- <div class="more" @click="handleToMoreNews">{{ "更多 +" }}</div> --> <!-- <div class="more" @click="handleToMoreNews">{{ "更多 +" }}</div> -->
</div> </div>
<div class="msg-bubble-main"> <div class="msg-bubble-main" ref="scrollContainer">
<div class="message-bubble" v-for="(item, index) in messageList" :key="index" @click="handleClickPerson(item)"> <div class="message-bubble" v-for="(item, index) in displayList" :key="index" @click="handleClickPerson(item)">
<div class="avatar-container"> <div class="avatar-container">
<img :src="item[props.imageUrl] || avatarUser" :alt="item[props.name]" class="avatar" /> <img :src="item[props.imageUrl] || avatarUser" :alt="item[props.name]" class="avatar" />
<div class="avatar-containerOne" v-if="isRepublicanParty"> <div class="avatar-containerOne" v-if="isRepublicanParty">
<img src="./image2.png" alt="" class="avatar-imageOne" /> <img src="./image2.png" alt="" class="avatar-imageOne" />
</div> </div>
...@@ -35,27 +34,12 @@ ...@@ -35,27 +34,12 @@
</div> </div>
</div> </div>
</div> </div>
<!-- <MessageBubble v-for="(item, index) in messageList" @click="handleClickPsserson(item)"
@info-click="handleMediaClick(item)" :key="index" :avatar="item.img ? item.img : DefaultIcon1" :name="item.name"
:time="item.time" :source="item.source" :content="item.content" /> -->
<!-- <div class="msg-bubble-main-item" v-for="(item, index) in messageList" :key="index">
<div class="left" @click="handleClickPerson(item)">
<img :src="item.img ? item.img : DefaultIcon1" alt="" />
</div>
<div class="right">
<div class="right-top">
<div class="name">{{ item.name }}</div>
<div class="time">{{ item.time }}</div>
</div>
<div class="content">{{ item.content }}</div>
</div>
</div> -->
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed } from "vue"; import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import avatarUser from "@/assets/images/avatar_user.png"; import avatarUser from "@/assets/images/avatar_user.png";
const emit = defineEmits(["click", "info-click"]); const emit = defineEmits(["click", "info-click"]);
...@@ -118,6 +102,66 @@ const handleInfoClick = item => { ...@@ -118,6 +102,66 @@ const handleInfoClick = item => {
const handleToMoreNews = item => { const handleToMoreNews = item => {
emit("more-click", item); emit("more-click", item);
}; };
const scrollContainer = ref(null)
let timer = null
const currentIndex = ref(0)
// const itemHeight = ref(80) // 每条消息的高度,需要根据实际调整
// 计算当前显示的消息列表(只显示固定数量的消息)
const displayList = computed(() => {
if(props.messageList.length < 4) {
return props.messageList
}
// 确保 messageList 存在且有数据
if (!props.messageList || !Array.isArray(props.messageList) || props.messageList.length === 0) {
return []
}
const list = []
const totalLength = props.messageList.length
for (let i = 0; i < 3; i++) {
// 计算当前索引,确保不会超出数组长度
const index = (currentIndex.value + i) % totalLength
const item = props.messageList[index]
// 确保 item 存在再添加到列表
if (item) {
list.push(item)
}
}
return list
})
// 开始滚动
const startScroll = () => {
if (timer) clearInterval(timer)
timer = setInterval(() => {
currentIndex.value = (currentIndex.value + 1) % props.messageList.length
}, 2000) // 每秒滚动一条
}
// 停止滚动
const stopScroll = () => {
if (timer) {
clearInterval(timer)
timer = null
}
}
onMounted(() => {
if (props.messageList.length > 3) {
startScroll()
}
})
onBeforeUnmount(() => {
stopScroll()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -180,7 +224,7 @@ const handleToMoreNews = item => { ...@@ -180,7 +224,7 @@ const handleToMoreNews = item => {
.msg-bubble-main { .msg-bubble-main {
height: 402px; height: 402px;
overflow-y: auto; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
padding-bottom: 8px; padding-bottom: 8px;
padding-left: 21px; padding-left: 21px;
...@@ -190,6 +234,8 @@ const handleToMoreNews = item => { ...@@ -190,6 +234,8 @@ const handleToMoreNews = item => {
display: flex; display: flex;
max-width: 740px; max-width: 740px;
margin-bottom: 15px; margin-bottom: 15px;
transition: transform 2s ease;
/* 可选:添加平滑动画 */
.avatar-container { .avatar-container {
flex-shrink: 0; flex-shrink: 0;
...@@ -317,6 +363,18 @@ const handleToMoreNews = item => { ...@@ -317,6 +363,18 @@ const handleToMoreNews = item => {
} }
} }
// .msg-bubble-main {
// height: 400px; /* 设置固定高度 */
// overflow: hidden;
// position: relative;
// }
// .message-bubble {
// transition: transform 0.3s ease; /* 可选:添加平滑动画 */
// height: 80px; /* 固定每条消息高度 */
// margin-bottom: 10px; /* 消息之间的间距 */
// }
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.message-bubble { .message-bubble {
......
...@@ -9,14 +9,9 @@ ...@@ -9,14 +9,9 @@
<SearchBar v-show="isShowSearchBar" /> <SearchBar v-show="isShowSearchBar" />
<div class="title-box" v-show="!isShowSearchBar"> <div class="title-box" v-show="!isShowSearchBar">
<!-- <div class="title-box" v-if="false"> --> <!-- <div class="title-box" v-if="false"> -->
<div <div class="title" v-for="(item, index) in homeTitleList" :key="index"
class="title" @mouseenter="handleShowMenu(index, true)" @mouseleave="handleShowMenu(index, false)"
v-for="(item, index) in homeTitleList" @click="handleClickTitle(item)">
:key="index"
@mouseenter="handleShowMenu(index, true)"
@mouseleave="handleShowMenu(index, false)"
@click="handleClickTitle(item)"
>
<div class="text" :class="{ textActive: homeActiveTitleIndex === index }"> <div class="text" :class="{ textActive: homeActiveTitleIndex === index }">
{{ item.name }} {{ item.name }}
</div> </div>
...@@ -36,7 +31,8 @@ ...@@ -36,7 +31,8 @@
<div class="name">{{ "管理员" }}</div> <div class="name">{{ "管理员" }}</div>
</div> </div>
</div> </div>
<div class="menu-box" v-show="isShowMenu" @mouseenter="handleHoverMenu(true)" @mouseleave="handleHoverMenu(false)"> <div class="menu-box" v-show="isShowMenu" @mouseenter="handleHoverMenu(true)"
@mouseleave="handleHoverMenu(false)">
<div class="menu-content"> <div class="menu-content">
<div class="menu-item" v-for="(item, index) in menuList" :key="index" @click="handleToModule(item)"> <div class="menu-item" v-for="(item, index) in menuList" :key="index" @click="handleToModule(item)">
<div class="icon"> <div class="icon">
...@@ -90,7 +86,7 @@ const handleGetPersonType = async () => { ...@@ -90,7 +86,7 @@ const handleGetPersonType = async () => {
personTypeList.value = []; personTypeList.value = [];
} }
window.sessionStorage.setItem("personTypeList", JSON.stringify(personTypeList.value)); window.sessionStorage.setItem("personTypeList", JSON.stringify(personTypeList.value));
} catch (error) {} } catch (error) { }
}; };
// 概览页标题列表 // 概览页标题列表
...@@ -230,6 +226,7 @@ onMounted(() => { ...@@ -230,6 +226,7 @@ onMounted(() => {
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: linear-gradient(180deg, rgba(246, 250, 255, 0.8) 0%, rgba(255, 255, 255, 0.8) 100%); background: linear-gradient(180deg, rgba(246, 250, 255, 0.8) 0%, rgba(255, 255, 255, 0.8) 100%);
padding: 12px 0; padding: 12px 0;
.nav-content { .nav-content {
width: 1600px; width: 1600px;
margin: 0 auto; margin: 0 auto;
...@@ -237,11 +234,14 @@ onMounted(() => { ...@@ -237,11 +234,14 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
position: relative; position: relative;
align-items: flex-start; align-items: flex-start;
.nav-left { .nav-left {
display: flex; display: flex;
align-items: center; align-items: center;
&.flex-start { &.flex-start {
align-items: flex-start; align-items: flex-start;
.icon { .icon {
flex-shrink: 0; flex-shrink: 0;
width: 48px; width: 48px;
...@@ -251,16 +251,19 @@ onMounted(() => { ...@@ -251,16 +251,19 @@ onMounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
img { img {
width: 29px; width: 29px;
height: 30px; height: 30px;
} }
} }
} }
.icon { .icon {
width: 29px; width: 29px;
height: 30px; height: 30px;
margin-right: 17px; margin-right: 17px;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
...@@ -274,6 +277,7 @@ onMounted(() => { ...@@ -274,6 +277,7 @@ onMounted(() => {
.title { .title {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
&:hover { &:hover {
.text { .text {
color: var(--color-main-active); color: var(--color-main-active);
...@@ -299,6 +303,7 @@ onMounted(() => { ...@@ -299,6 +303,7 @@ onMounted(() => {
width: 90%; width: 90%;
height: 20px; height: 20px;
margin-top: 9px; margin-top: 9px;
&::after { &::after {
display: block; display: block;
content: ""; content: "";
...@@ -365,23 +370,35 @@ onMounted(() => { ...@@ -365,23 +370,35 @@ onMounted(() => {
} }
.menu-box { .menu-box {
// position: absolute;
// z-index: 999999999;
// width: 713px;
// height: 413px;
// top: 52px;
// left: 0;
// box-sizing: border-box;
// border-radius: 10px;
// backdrop-filter: blur(10px);
// -webkit-backdrop-filter: blur(10px);
// box-shadow: 0px 8px 32px 0px rgba(31, 38, 135, 0.15);
// background: rgba(255, 255, 255, 0.25);
// backdrop-filter: blur(10px);
// -webkit-backdrop-filter: blur(10px);
// border: 1px solid rgba(255, 255, 255, 0.3);
// background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
// box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.2);
position: absolute; position: absolute;
z-index: 999999999; z-index: 999999;
width: 713px; width: 713px;
height: 413px; height: 413px;
top: 52px; top: 52px;
left: 0; left: -2px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px; border-radius: 10px;
backdrop-filter: blur(10px); backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(10px); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
box-shadow: 0px 8px 32px 0px rgba(31, 38, 135, 0.15); background: rgba(255, 255, 255, 0.8);
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.2);
.menu-content { .menu-content {
width: 562px; width: 562px;
......
<template>
<div class="news-item">
<el-space direction="vertical" class="flex-fill" alignment='flex-start'>
<common-text :lineLimit="1" class="text-bold" color="var(--text-primary-80-color)">{{
props.title
}}</common-text>
<common-text class="text-tip-2" color="var(--text-primary-65-color)">
{{ props.from }}
</common-text>
<el-space v-if="props.aeraTags">
<area-tag v-for="(tag, index) in props.aeraTags" :key="index" :tagName="tag" />
</el-space>
</el-space>
<img style="width: 122px; height: 82px" :src="props.img">
<!-- <img v-else style="width: 122px; height: 82px" :src="DefaultIconNews"> -->
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElSpace } from 'element-plus'
import AreaTag from '@/components/base/AreaTag/index.vue'
import DefaultIconNews from "@/assets/icons/default-icon-news.png";
import CommonText from '../texts/CommonText.vue';
const props = defineProps({
img: {
type: String,
default: 'img'
},
title: {
type: String,
default: "title"
},
from: {
type: String,
default: "from"
},
aeraTags: {
type: Array,
default: []
},
content: {
type: String,
default: "content"
},
});
const emit = defineEmits(['item-click', 'more-click']);
</script>
<style lang="scss" scoped>
@use '@/styles/common.scss';
.news-item {
display: flex;
margin-top: 10px;
}
</style>
\ No newline at end of file
<script setup>
</script>
<template> <template>
<div class="flex-display box"> <div class="flex-display box">
<div class="img"></div> <div class="img">
<img src="@/assets/icons/box-footer-left-icon.png" alt="">
</div>
<div class="flex-fill txt text-tip-1"> <div class="flex-fill txt text-tip-1">
<slot></slot> <slot></slot>
</div> </div>
<div class="arrow"><span></span></div> <div class="arrow">
<img src="@/assets/icons/box-footer-right-icon.png" alt="">
</div>
</div> </div>
</template> </template>
<script setup>
</script>
<style scoped lang="scss"> <style scoped lang="scss">
@use '@/styles/common.scss'; @use '@/styles/common.scss';
...@@ -29,7 +33,11 @@ ...@@ -29,7 +33,11 @@
.img { .img {
width: 19px; width: 19px;
height: 20px; height: 20px;
background-image: url("@/assets/icons/model.png");
img {
width: 100%;
height: 100%;
}
} }
.txt { .txt {
...@@ -42,21 +50,12 @@ ...@@ -42,21 +50,12 @@
} }
.arrow { .arrow {
border-radius: 50%;
min-width: 24px;
width: 24px; width: 24px;
height: 24px; height: 24px;
background: var(--color-primary-10);
display: flex;
align-items: center;
justify-content: center;
span { img {
font-size: 22px; width: 100%;
font-weight: bold; height: 100%;
position: relative;
top: -3px;
/* 向上偏移2px */
} }
} }
</style> </style>
\ No newline at end of file
<template> <template>
<el-space :size="16" class="text-tip-1-bold box"> <el-space :size="16" class="text-tip-1-bold box-color-prefix">
<div class="color-prefix"></div> <div class="color-prefix"></div>
<slot></slot> <slot></slot>
</el-space> </el-space>
...@@ -11,6 +11,10 @@ const props = defineProps({ ...@@ -11,6 +11,10 @@ const props = defineProps({
color: { color: {
type: String, type: String,
default: 'var(--color-primary-100)' default: 'var(--color-primary-100)'
},
height: {
type: String,
default: '16px'
} }
}) })
</script> </script>
...@@ -18,11 +22,11 @@ const props = defineProps({ ...@@ -18,11 +22,11 @@ const props = defineProps({
<style lang="scss" scoped> <style lang="scss" scoped>
.color-prefix { .color-prefix {
width: 8px; width: 8px;
height: 16px; height: v-bind(height);
background-color: v-bind(color); background-color: v-bind(color);
} }
.box { .box-color-prefix {
color: v-bind(color); color: v-bind(color);
} }
</style> </style>
\ No newline at end of file
<template>
<div class="common-text">
<slot></slot>
</div>
</template>
<script setup lang="js">
const props = defineProps({
color: {
type: String,
default: "#000"
},
lineLimit: {
type: Number,
default: null
}
});
</script>
<style lang="scss" scoped>
@use '@/styles/common.scss';
.common-text {
color: v-bind(color);
@if('v-bind(lineLimit) !==null') {
@include common.text-ellipsis(v-bind(lineLimit));
}
}
</style>
import { useRouter } from "vue-router";
export function useGotoPage() {
const router = useRouter();
return (path, data, isNewTabs = true) => {
console.log('path', path);
if (isNewTabs) {
// 打开新页面
const url = new URL(window.location.origin + path);
if (data) {
Object.entries(data).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
window.open(url.toString(), '_blank');
} else {
// 当前页面打开
router.push({ path, query: data });
}
}
}
\ No newline at end of file
const newsBrief = () => import('@/views/newsBrief/index.vue') const newsBrief = () => import('@/views/newsBrief/index.vue')
const ModeuleNews = () => import('@/views/newsBrief/ModeuleNews.vue')
const NewsDetial = () => import('@/views/newsBrief/NewsDetial.vue')
const NewsAnalysis = () => import('@/views/newsAnalysis/index.vue') const NewsAnalysis = () => import('@/views/newsAnalysis/index.vue')
const newsRoutes = [ const newsRoutes = [
...@@ -11,6 +13,25 @@ const newsRoutes = [ ...@@ -11,6 +13,25 @@ const newsRoutes = [
title: "新闻速览" title: "新闻速览"
} }
}, },
//新闻模块页面路由
{
path: "/newsModeule/:id",
name: "newsModeule",
component: ModeuleNews,
meta: {
title: "新闻模块"
}
},
//新闻详情页面路由
{
path: "/newsDetail/:id",
name: "newsDetail",
component: NewsDetial,
meta: {
title: "新闻详情"
}
},
// 新闻事件分析 // 新闻事件分析
{ {
...@@ -22,5 +43,21 @@ const newsRoutes = [ ...@@ -22,5 +43,21 @@ const newsRoutes = [
} }
} }
]; ];
import { useGotoPage } from "../common.js";
export function useGotoNewsBrief() {
const gotoPage = useGotoPage();
return (isNewTabs = true) => gotoPage("/newsBrief/", {}, isNewTabs)
}
export function useGotoNewsDetail() {
const gotoPage = useGotoPage();
return (id, isNewTabs = true) => gotoPage("/newsDetail/" + id, {}, isNewTabs)
}
export function useGotoNewsModule() {
const gotoPage = useGotoPage();
return (id, name, isNewTabs = true) =>
gotoPage("/newsModeule/" + id, { name }, isNewTabs)
}
export default newsRoutes; export default newsRoutes;
...@@ -11,19 +11,19 @@ ...@@ -11,19 +11,19 @@
display: flex; display: flex;
} }
.flex-display-center{ .flex-display-center {
@extend .flex-display; @extend .flex-display;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.flex-display-end{ .flex-display-end {
@extend .flex-display; @extend .flex-display;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
} }
.flex-display-start{ .flex-display-start {
@extend .flex-display; @extend .flex-display;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
...@@ -37,6 +37,13 @@ ...@@ -37,6 +37,13 @@
width: 100%; width: 100%;
} }
.mouse-hover:hover {
cursor: pointer;
box-shadow: 0px 0px 1px 1px rgba(0, 0, 0, 0.082);
// margin-top: 1px;
// margin-left: 1px;
}
// 文本超出指定行数省略号显示 // 文本超出指定行数省略号显示
@mixin text-ellipsis($line-clamp) { @mixin text-ellipsis($line-clamp) {
overflow: hidden; overflow: hidden;
......
...@@ -14,11 +14,29 @@ const span = 12 ...@@ -14,11 +14,29 @@ const span = 12
</template> </template>
`}} `}}
</pre> </pre>
<div class="common-padding">
<div class="background-as-card"> <div class="background-as-card">
<div v-for="item in [1, 2, 3, 4]" :key="item"> <div v-for="item in [1, 2, 3, 4]" :key="item">
{{ item }} {{ item }}
</div> </div>
</div> </div>
</div>
</el-col>
<el-col :span="span">
<pre>{{ `import '@/styles/container.scss';\n<template>
<div class="common-page"></div>
</template>
`}}
</pre>
通用的页面容器,限定了页面宽度为1600px,居中显示
</el-col>
<el-col :span="span">
<pre>{{ `import '@/styles/common.scss';\n<template>
<div class="mouse-hover"></div>
</template>
`}}
</pre>
<div class="mouse-hover">鼠标悬停</div>
</el-col> </el-col>
</el-row> </el-row>
</template> </template>
......
<template>
<el-row class="wrapper layout-grid-line">
<el-col :span="span">
<pre>
{{
`import GraphChart from '@/components/base/GraphChart/index.vue';
<template>
<GraphChart :nodes="nodes" :links="links" layoutType="none">
</GraphChart>
</template>
`}}
</pre>
<div class="chart-box">
<GraphChart :nodes="nodes" :links="links" layoutType="none">
</GraphChart>
</div>
</el-col>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
import GraphChart from '@/components/base/GraphChart/index.vue'
import CompanyImg from "./symbol.png"
const span = 24
const nodes = ref([
{
id: 0,
name: "泰丰先行",
// category: 0,
symbolSize: 30,
value: 8,
symbol: `image://${CompanyImg}`,
x: 50,
y: 10
},
{
id: 1,
name: "国轩高科",
// category: 0,
symbolSize: 30,
value: 9,
symbol: `image://${CompanyImg}`,
x: 150,
y: 10
},
{
id: 2,
name: "智方纳米",
// category: 2,
symbolSize: 30,
value: 7,
symbol: `image://${CompanyImg}`,
x: 250,
y: 10
},
{
id: 3,
name: "香百科技",
// category: 1,
symbolSize: 30,
value: 6,
symbol: `image://${CompanyImg}`,
x: 350,
y: 10
},
{
id: 4,
name: "格林滨",
// category: 2,
symbolSize: 30,
value: 6,
symbol: `image://${CompanyImg}`,
x: 450,
y: 10
},
{
id: 5,
name: "江西紫宸",
// category: 2,
symbolSize: 30,
value: 7,
symbol: `image://${CompanyImg}`,
x: 550,
y: 10
},
{
id: 6,
name: "紫江企业",
// category: 4,
symbolSize: 30,
value: 6,
symbol: `image://${CompanyImg}`,
x: 650,
y: 10
},
{
id: 7,
name: "大而美法案",
// category: 4,
symbolSize: 50,
value: 5,
symbol: `image://${CompanyImg}`,
x: 300,
y: 200
},
{
id: 8,
name: "比亚迪",
// category: 0,
symbolSize: 30,
value: 10,
symbol: `image://${CompanyImg}`,
x: 50,
y: 400
},
{
id: 9,
name: "铜陵有色",
// category: 3,
symbolSize: 30,
value: 8,
symbol: `image://${CompanyImg}`,
x: 150,
y: 400
},
{
id: 10,
name: "长盛精密",
// category: 1,
symbolSize: 30,
value: 7,
symbol: `image://${CompanyImg}`,
x: 250,
y: 400
},
{
id: 11,
name: "天合光能",
// category: 0,
symbolSize: 30,
value: 8,
symbol: `image://${CompanyImg}`,
x: 350,
y: 400
},
{
id: 12,
name: "昆仑化学",
// category: 2,
symbolSize: 30,
value: 6,
symbol: `image://${CompanyImg}`,
x: 250,
y: 400
},
{
id: 13,
name: "嘉源科技",
// category: 1,
symbolSize: 30,
value: 6,
symbol: `image://${CompanyImg}`,
x: 450,
y: 400
},
{
id: 14,
name: "华阳集团",
// category: 4,
symbolSize: 30,
value: 7,
symbol: `image://${CompanyImg}`,
x: 550,
y: 400
},
{
id: 15,
name: "海辰智能",
// category: 1,
symbolSize: 30,
value: 7,
symbol: `image://${CompanyImg}`,
x: 650,
y: 400
},
]);
const links = ref([
{ source: 1, target: 7, label: { show: true, formatter: '合作' } },
{ source: 2, target: 7, label: { show: true, formatter: '持股' } },
{ source: 3, target: 7, label: { show: true, formatter: '合作' } },
{ source: 4, target: 7, lineStyle: { type: 'dashed', color: '#d32f2f' }, label: { show: true, formatter: '从属' } },
{ source: 5, target: 7, label: { show: true, formatter: '合作' } },
{ source: 6, target: 7, label: { show: true, formatter: '持股' } },
{ source: 0, target: 7, label: { show: true, formatter: '持股' } },
{ source: 8, target: 7, label: { show: true, formatter: '合作' } },
{ source: 9, target: 7, lineStyle: { type: 'dashed', color: '#d32f2f' }, label: { show: true, formatter: '从属' } },
{ source: 10, target: 7, lineStyle: { type: 'dashed', color: '#d32f2f' }, label: { show: true, formatter: '合作' } },
{ source: 11, target: 7, label: { show: true, formatter: '合作' } },
{ source: 12, target: 7, label: { show: true, formatter: '合作' } },
{ source: 13, target: 7, label: { show: true, formatter: '合作' } },
{ source: 14, target: 7, label: { show: true, formatter: '合作' } },
{ source: 15, target: 7, label: { show: true, formatter: '合作', color: 'red', borderColor: 'red' } },
]);
</script>
<style lang="scss" scoped>
.wrapper {
width: 100%;
}
.chart-box {
width: 800px;
height: 500px;
}
</style>
\ No newline at end of file
<template>
<el-row class="wrapper layout-grid-line">
<el-col :span="span">
<pre>
{{
`import GraphChart from '@/components/base/GraphChart/index.vue';
<template>
<GraphChart :nodes="nodes" :links="links" layoutType="force" >
</GraphChart>
</template>
`}}
</pre>
<div class="chart-box">
<GraphChart :nodes="nodes" :links="links">
</GraphChart>
</div>
</el-col>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
import GraphChart from '@/components/base/GraphChart/index.vue'
import CompanyImg from "./symbol.png"
const span = 24
const nodes = ref([
{
id: 1,
name: "泰丰先行",
// category: 0,
symbolSize: 30,
value: 8,
symbol: `image://${CompanyImg}`,
},
{
id: 2,
name: "国轩高科",
// category: 0,
symbolSize: 30,
value: 9,
symbol: `image://${CompanyImg}`,
},
{
id: 3,
name: "智方纳米",
// category: 2,
symbolSize: 30,
value: 7,
symbol: `image://${CompanyImg}`,
},
{
id: 4,
name: "香百科技",
// category: 1,
symbolSize: 30,
value: 6,
symbol: `image://${CompanyImg}`,
},
{
id: 5,
name: "格林滨",
// category: 2,
symbolSize: 30,
value: 6,
symbol: `image://${CompanyImg}`,
},
{
id: 6,
name: "江西紫宸",
// category: 2,
symbolSize: 30,
value: 7,
symbol: `image://${CompanyImg}`,
},
{
id: 7,
name: "紫江企业",
// category: 4,
symbolSize: 30,
value: 6,
symbol: `image://${CompanyImg}`,
},
{
id: 8,
name: "大而美法案",
// category: 4,
symbolSize: 50,
value: 5,
symbol: `image://${CompanyImg}`,
},
{
id: 9,
name: "比亚迪",
// category: 0,
symbolSize: 30,
value: 10,
symbol: `image://${CompanyImg}`,
},
{
id: 10,
name: "铜陵有色",
// category: 3,
symbolSize: 30,
value: 8,
symbol: `image://${CompanyImg}`,
},
{
id: 11,
name: "长盛精密",
// category: 1,
symbolSize: 30,
value: 7,
symbol: `image://${CompanyImg}`,
},
{
id: 12,
name: "天合光能",
// category: 0,
symbolSize: 30,
value: 8,
symbol: `image://${CompanyImg}`,
},
{
id: 13,
name: "昆仑化学",
// category: 2,
symbolSize: 30,
value: 6,
symbol: `image://${CompanyImg}`,
},
{
id: 14,
name: "嘉源科技",
// category: 1,
symbolSize: 30,
value: 6,
symbol: `image://${CompanyImg}`,
},
{
id: 15,
name: "华阳集团",
// category: 4,
symbolSize: 30,
value: 7,
symbol: `image://${CompanyImg}`,
},
{
id: 16,
name: "海辰智能",
// category: 1,
symbolSize: 30,
value: 7,
symbol: `image://${CompanyImg}`,
},
]);
const links = ref([
{ source: 1, target: 7, label: { show: true, formatter: '合作' } },
{ source: 2, target: 7, label: { show: true, formatter: '持股' } },
{ source: 3, target: 7, label: { show: true, formatter: '合作' } },
{ source: 4, target: 7, lineStyle: { type: 'dashed', color: '#d32f2f' }, label: { show: true, formatter: '从属' } },
{ source: 5, target: 7, label: { show: true, formatter: '合作' } },
{ source: 6, target: 7, label: { show: true, formatter: '持股' } },
{ source: 8, target: 7, label: { show: true, formatter: '持股' } },
{ source: 9, target: 7, label: { show: true, formatter: '合作' } },
{ source: 10, target: 7, lineStyle: { type: 'dashed', color: '#d32f2f' }, label: { show: true, formatter: '从属' } },
{ source: 11, target: 7, lineStyle: { type: 'dashed', color: '#d32f2f' }, label: { show: true, formatter: '合作' } },
{ source: 12, target: 7, label: { show: true, formatter: '合作' } },
{ source: 13, target: 7, label: { show: true, formatter: '合作' } },
{ source: 14, target: 7, label: { show: true, formatter: '合作' } },
{ source: 15, target: 7, label: { show: true, formatter: '合作' } },
{ source: 16, target: 7, label: { show: true, formatter: '合作', color: 'red', borderColor: 'red' } },
]);
</script>
<style lang="scss" scoped>
.wrapper {
width: 100%;
}
.chart-box {
width: 1600px;
height: 800px;
}
</style>
\ No newline at end of file
...@@ -24,6 +24,19 @@ const span = 12 ...@@ -24,6 +24,19 @@ const span = 12
<el-radio-button :value="3">选项3</el-radio-button> <el-radio-button :value="3">选项3</el-radio-button>
</el-radio-group> </el-radio-group>
</el-col> </el-col>
<el-col :span="span">
<pre>{{ `import '@/styles/radio.scss';\n <template>
<el-radio-group class="radio-group-as-radius-btn">
</el-radio-group>
</template>
`}}
</pre>
<el-radio-group v-model="radio" class="radio-group-as-radius-btn">
<el-space>
<el-radio-button v-for="item in 3" :key="item" :value="item">选项{{ item }} ></el-radio-button>
</el-space>
</el-radio-group>
</el-col>
</el-row> </el-row>
</template> </template>
......
...@@ -3,6 +3,7 @@ import { ElSpace, ElRow, ElCol } from 'element-plus'; ...@@ -3,6 +3,7 @@ import { ElSpace, ElRow, ElCol } from 'element-plus';
import '@/styles/tabs.scss' import '@/styles/tabs.scss'
import ColorPrefixTitle from '@/components/base/texts/ColorPrefixTitle.vue'; import ColorPrefixTitle from '@/components/base/texts/ColorPrefixTitle.vue';
import AiTipPane from '@/components/base/panes/AiTipPane.vue'; import AiTipPane from '@/components/base/panes/AiTipPane.vue';
import CommonText from '@/components/base/texts/CommonText.vue';
const span = 12 const span = 12
</script> </script>
...@@ -10,20 +11,37 @@ const span = 12 ...@@ -10,20 +11,37 @@ const span = 12
<el-row class="layout-grid-line"> <el-row class="layout-grid-line">
<el-col :span="span"> <el-col :span="span">
<pre> <pre>
{{ `import ColorPrefixTitle from '@/components/base/texts/ColorPrefixTitle.vue'; {{ `import CommonText from '@/components/base/texts/CommonText.vue';
<template> <template>
<common-text>科技领域</common-text>
<common-text class="text-title-1-show" color="var(--color-yellow-100)">科技领域</common-text>
<common-text color="red" :lineLimit="1">科技领域</common-text>
</template>
`}}
</pre>
<el-space direction="vertical">
<common-text>科技领域</common-text>
<common-text class="text-title-1-show" color="var(--color-yellow-100)">科技领域</common-text>
<common-text color="red"
:lineLimit="1">超出行数则省略号。超出行数则省略号。超出行数则省略号。超出行数则省略号。超出行数则省略号。超出行数则省略号。超出行数则省略号。超出行数则省略号。超出行数则省略号。</common-text>
</el-space>
</el-col>
<el-col :span="span">
<pre>{{ `import ColorPrefixTitle from '@/components/base/texts/ColorPrefixTitle.vue';\n <template>
<color-prefix-title>科技领域</color-prefix-title> <color-prefix-title>科技领域</color-prefix-title>
<color-prefix-title color="var(--color-yellow-100)">科技领域</color-prefix-title> <color-prefix-title color="var(--color-yellow-100)">科技领域</color-prefix-title>
<color-prefix-title color="red">科技领域</color-prefix-title> <color-prefix-title color="red">科技领域</color-prefix-title>
</template> </template>
`}} `}}
</pre> </pre>
<el-space direction="vertical"> <el-space direction="vertical">
<color-prefix-title>科技领域</color-prefix-title> <color-prefix-title>科技领域</color-prefix-title>
<color-prefix-title color="var(--color-yellow-100)">科技领域</color-prefix-title> <color-prefix-title color="var(--color-yellow-100)">科技领域</color-prefix-title>
<color-prefix-title color="red">科技领域</color-prefix-title> <color-prefix-title color="red">科技领域</color-prefix-title>
</el-space> </el-space>
</el-col> </el-col>
<el-col :span="span"> <el-col :span="span">
<pre>{{ `import AiTipPane from '@/components/base/panes/AiTipPane.vue';\n<template> <pre>{{ `import AiTipPane from '@/components/base/panes/AiTipPane.vue';\n<template>
<ai-tip-pane>huidadadadadasda</ai-tip-pane> <ai-tip-pane>huidadadadadasda</ai-tip-pane>
......
<script setup>
import { ElRow, ElCol } from 'element-plus';
import '@/styles/common.scss'
import WarnningPane from '@/components/base/WarningPane/index.vue'
const span = 12
</script>
<template>
<el-row class="wrapper layout-grid-line">
<el-col :span="span">
<pre>
{{
`import WarnningPane from '@/components/base/WarningPane/index.vue';
<template>
<WarnningPane warnningLevel="特别重大风险" warnningContent="我是特别重大风险内容文字我是特别重大风险内容文字">
</WarnningPane>
</template>
`}}
</pre>
<WarnningPane warnningLevel="特别重大风险" warnningContent="我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字我是特别重大风险内容文字">
</WarnningPane>
</el-col>
</el-row>
</template>
<style lang="scss" scoped>
.person-avatar {
width: 200px;
}
</style>
\ No newline at end of file
...@@ -28,6 +28,15 @@ ...@@ -28,6 +28,15 @@
<el-tab-pane label="人物" lazy> <el-tab-pane label="人物" lazy>
<people-page /> <people-page />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="预警面板" lazy>
<WarnningPane />
</el-tab-pane>
<el-tab-pane label="层级关系图" lazy>
<GraphChart />
</el-tab-pane>
<el-tab-pane label="引力关系图" lazy>
<GraphTreeChart />
</el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</el-space> </el-space>
...@@ -47,6 +56,9 @@ import CommonPage from './CommonPage/index.vue'; ...@@ -47,6 +56,9 @@ import CommonPage from './CommonPage/index.vue';
import TextPage from './TextPage/index.vue'; import TextPage from './TextPage/index.vue';
import ImagesPage from './Images/index.vue'; import ImagesPage from './Images/index.vue';
import PeoplePage from './People/index.vue'; import PeoplePage from './People/index.vue';
import WarnningPane from './WarnningPane/index.vue'
import GraphChart from './GraphChart/index.vue'
import GraphTreeChart from './GraphTreeChart/index.vue'
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
...@@ -6,3 +6,14 @@ ...@@ -6,3 +6,14 @@
// width: 1600px; // width: 1600px;
align-items: center; align-items: center;
} }
//水平居中
.h-center {
//水平居中
margin: 0 auto;
display: block; // 强制设置为块级元素
}
.common-padding {
padding: 20px 24px;
}
\ No newline at end of file
...@@ -17,3 +17,34 @@ ...@@ -17,3 +17,34 @@
border-radius: 4px; border-radius: 4px;
} }
} }
.radio-group-as-radius-btn {
.el-radio-button {
--el-radio-button-checked-bg-color: transparent;
--el-radio-button-checked-border-color: transparent;
.el-radio-button__inner {
border-radius: 32px !important;
// height: 40px;
@extend .text-regular;
background-color: var(--bg-white-65);
border-color: var(--bg-white-100);
border-width: 1px;
color: var(--text-primary-65-color);
//垂直居中
// display: flex;
// justify-content: center;
}
}
.el-radio-button.is-active {
--el-radio-button-checked-text-color: var(--color-primary-100);
.el-radio-button__inner {
background-color: var(--color-primary-10) !important;
border-color: var(--color-primary-35) !important;
}
}
}
\ No newline at end of file
// 绘制echarts图表 // 绘制echarts图表
import * as echarts from 'echarts' import * as echarts from 'echarts'
import 'echarts-wordcloud';
const setChart = (option, chartId) => { const setChart = (option, chartId) => {
let chartDom = document.getElementById(chartId); let chartDom = document.getElementById(chartId);
if (!chartDom) { if (!chartDom) {
......
...@@ -69,8 +69,11 @@ ...@@ -69,8 +69,11 @@
<div <div
class="item-left" class="item-left"
:class="{ :class="{
itemLeftStatus1: item.signalLevel === '特别重大', 'item-status-1': item.signalLevel === '特别重大',
itemLeftStatus2: item.signalLevel === '重大风险' 'item-status-2': item.signalLevel === '重大风险',
'item-status-3': item.signalLevel === '较大风险',
'item-status-4': item.signalLevel === '一般风险',
'item-status-5': item.signalLevel === '低风险'
}" }"
> >
{{ item.signalLevel ? item.signalLevel : "一般风险" }} {{ item.signalLevel ? item.signalLevel : "一般风险" }}
...@@ -1225,14 +1228,29 @@ onUnmounted(() => { ...@@ -1225,14 +1228,29 @@ onUnmounted(() => {
// } // }
// } // }
.itemLeftStatus1 { .item-status-1 {
color: rgba(245, 34, 45, 1) !important; color: var(--color-red-100) !important;
background: rgba(255, 241, 240) !important; background: var(--color-red-10) !important;
} }
.itemLeftStatus2 { .item-status-2 {
color: rgba(250, 140, 22, 1) !important; color: var(--color-orange-100) !important;
background: rgba(255, 247, 230, 1) !important; background: var(--color-orange-10) !important;
}
.item-status-3 {
color: var(--color-yellow-100) !important;
background: var(--color-yellow-10) !important;
}
.item-status-4 {
color: var(--color-green-100) !important;
background: var(--color-green-10) !important;
}
.item-status-5 {
color: var(--color-primary-100) !important;
background: var(--color-primary-10) !important;
} }
.item-left { .item-left {
...@@ -1303,12 +1321,12 @@ onUnmounted(() => { ...@@ -1303,12 +1321,12 @@ onUnmounted(() => {
// background: var(--color-bg-hover); // background: var(--color-bg-hover);
// } // }
.itemLeftStatus1 { .item-status-1 {
color: rgba(245, 34, 45, 1) !important; color: rgba(245, 34, 45, 1) !important;
background: rgba(255, 241, 240) !important; background: rgba(255, 241, 240) !important;
} }
.itemLeftStatus2 { .item-status-2 {
color: rgba(250, 140, 22, 1) !important; color: rgba(250, 140, 22, 1) !important;
background: rgba(255, 247, 230, 1) !important; background: rgba(255, 247, 230, 1) !important;
} }
......
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32.000000" height="32.000000" fill="none" customFrame="#000000">
<g id="组合 275">
<rect id="矩形 264" width="32.000000" height="32.000000" x="0.000000" y="0.000000" opacity="0" fill="rgb(255,0,0)" fill-opacity="0.319999993" />
<path id="合并" d="M9.49992 8.50003L9.4999 2.36022e-06L8.49991 0L8.49993 8.50003L0 8.50003L0 9.50003L8.49993 9.50003L8.49995 17.9999L9.49995 17.9999L9.49993 9.50003L18 9.50003L18 8.50003L9.49992 8.50003Z" fill="rgb(59,65,75)" fill-rule="evenodd" transform="matrix(0.707107,0.707107,-0.707107,0.707107,15.9996,3.27197)" />
</g>
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="icon-圆形布局 1" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 423" d="M13.81 6.6C13.67 5.97 13.39 5.41 13.04 4.85C13.18 4.64 13.25 4.36 13.25 4.15C13.25 3.38 12.62 2.75 11.85 2.75C11.57 2.75 11.36 2.82 11.15 2.96C10.59 2.61 10.03 2.4 9.4 2.19C9.33 1.49 8.7 1 8 1C7.3 1 6.74 1.56 6.6 2.19C5.97 2.4 5.41 2.61 4.85 2.96C4.64 2.82 4.36 2.75 4.15 2.75C3.38 2.75 2.75 3.38 2.75 4.15C2.75 4.43 2.82 4.64 2.96 4.85C2.61 5.41 2.4 5.97 2.19 6.6C1.56 6.74 1 7.3 1 8C1 8.7 1.56 9.26 2.19 9.4C2.33 10.03 2.61 10.59 2.96 11.15C2.82 11.36 2.75 11.64 2.75 11.85C2.75 12.62 3.38 13.25 4.15 13.25C4.43 13.25 4.64 13.18 4.85 13.04C5.41 13.39 5.97 13.6 6.6 13.81C6.67 14.51 7.3 15 8 15C8.7 15 9.26 14.44 9.4 13.81C10.03 13.67 10.59 13.39 11.15 13.04C11.36 13.18 11.64 13.25 11.85 13.25C12.62 13.25 13.25 12.62 13.25 11.85C13.25 11.57 13.18 11.36 13.04 11.15C13.39 10.59 13.6 10.03 13.81 9.4C14.51 9.33 15 8.7 15 8C15 7.3 14.44 6.74 13.81 6.6L13.81 6.6ZM12.55 10.66C12.34 10.52 12.13 10.45 11.85 10.45C11.08 10.45 10.45 11.08 10.45 11.85C10.45 12.13 10.52 12.34 10.66 12.55C10.24 12.76 9.82 12.97 9.33 13.11C9.12 12.62 8.63 12.2 8 12.2C7.44 12.2 6.88 12.55 6.67 13.11C6.18 12.97 5.76 12.83 5.34 12.55C5.48 12.34 5.55 12.13 5.55 11.85C5.55 11.08 4.92 10.45 4.15 10.45C3.87 10.45 3.66 10.52 3.45 10.66C3.24 10.24 3.03 9.82 2.89 9.33C3.45 9.12 3.8 8.56 3.8 8C3.8 7.44 3.45 6.88 2.96 6.74C3.1 6.25 3.24 5.83 3.52 5.41C3.73 5.55 3.94 5.62 4.22 5.62C4.99 5.62 5.62 4.99 5.62 4.22C5.62 3.94 5.55 3.73 5.41 3.52C5.83 3.31 6.25 3.1 6.74 2.96C6.88 3.45 7.44 3.8 8 3.8C8.56 3.8 9.12 3.45 9.33 2.89C9.82 3.03 10.24 3.17 10.66 3.45C10.52 3.66 10.45 3.87 10.45 4.15C10.45 4.92 11.08 5.55 11.85 5.55C12.13 5.55 12.34 5.48 12.55 5.34C12.76 5.76 12.97 6.18 13.11 6.67C12.55 6.88 12.2 7.44 12.2 8C12.2 8.56 12.55 9.12 13.04 9.26C12.97 9.75 12.76 10.24 12.55 10.66L12.55 10.66Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none">
<defs>
<clipPath id="clipPath_1">
<rect width="18.000000" height="15.000000" x="5.000000" y="5.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="导出数据" width="28.000000" height="28.000000" x="0.000000" y="0.000000" />
<g id="容器 742" customFrame="url(#clipPath_1)">
<rect id="容器 742" width="18.000000" height="15.000000" x="5.000000" y="5.000000" />
<rect id="矩形 347" width="2.000000" height="6.000000" x="13.000000" y="5.000000" fill="rgb(132,136,142)" />
<path id="矢量 600" d="M18 11L10 11L14 16L18 11Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
<path id="矢量 601" d="M22 19.9996L22.9999 15.0012L19.9999 12.0011L18.9999 12L21.0003 15.001L17.9999 15.0015L16.9998 17.9987L14 17.9996L11.0001 17.9997L9.99998 15.002L7.00017 15.0028L8.99996 12.0008L8 12.0004L5 15.0023L6.00016 20L22 19.9996Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</g>
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none">
<defs>
<clipPath id="clipPath_0">
<rect width="20.000000" height="20.000000" x="4.000000" y="4.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="数据源" width="28.000000" height="28.000000" x="0.000000" y="0.000000" />
<g id="数据库 1" clip-path="url(#clipPath_0)" customFrame="url(#clipPath_0)">
<rect id="数据库 1" width="20.000000" height="20.000000" x="4.000000" y="4.000000" />
<path id="合并" d="M10.6426 6.48828C11.6719 6.28906 12.791 6.1875 14 6.1875C15.209 6.1875 16.3281 6.28906 17.3574 6.48828C18.3867 6.6875 19.2012 6.96094 19.7988 7.30469C20.3965 7.64844 20.6973 8.01953 20.6973 8.42188L20.6973 9.53906C20.6973 9.94141 20.3984 10.3125 19.7988 10.6563C19.1992 11 18.3867 11.2715 17.3574 11.4727C16.3281 11.6699 15.209 11.7695 14 11.7695C12.791 11.7695 11.6719 11.668 10.6426 11.4688C9.61328 11.2695 8.79883 10.9961 8.20117 10.6523C7.60156 10.3086 7.30273 9.9375 7.30273 9.53516L7.30273 8.41797C7.30273 8.01563 7.60156 7.64453 8.20117 7.30078C8.80078 6.96094 9.61328 6.68945 10.6426 6.48828ZM10.1387 12.5078C11.3359 12.7578 12.623 12.8828 14 12.8828C15.377 12.8828 16.6641 12.7578 17.8613 12.5078C19.0586 12.2578 20.0039 11.8887 20.6953 11.4004L20.6953 12.8828C20.6953 13.2852 20.3965 13.6563 19.7969 14C19.1973 14.3438 18.3848 14.6152 17.3555 14.8164C16.3281 15.0156 15.209 15.1152 14 15.1152C12.791 15.1152 11.6719 15.0137 10.6426 14.8145C9.61328 14.6152 8.79883 14.3418 8.20117 13.998C7.60156 13.6543 7.30273 13.2832 7.30273 12.8809L7.30273 11.3984C7.99609 11.8906 8.94141 12.2598 10.1387 12.5078ZM10.1387 15.8574C11.3359 16.1074 12.623 16.2324 14 16.2324C14.6624 16.2324 15.3041 16.2035 15.9249 16.1456C14.2088 16.4715 12.8443 17.3161 12.2805 18.3935C11.7114 18.3432 11.1654 18.2672 10.6426 18.166C9.61328 17.9668 8.80078 17.6934 8.20117 17.3496C7.60156 17.0059 7.30273 16.6348 7.30273 16.2324L7.30273 14.75C7.9961 15.2383 8.94141 15.6074 10.1387 15.8574ZM17.5 16C17.3281 16 17.1581 16.005 16.9902 16.0148C17.2857 15.9695 17.5761 15.917 17.8613 15.8574C19.0586 15.6074 20.0039 15.2383 20.6953 14.75L20.6953 16.2324C20.6953 16.3614 20.6646 16.4872 20.6031 16.6099C19.7199 16.2251 18.6512 16 17.5 16ZM13 19.5C13 18.1193 15.0147 17 17.5 17C19.9853 17 22 18.1193 22 19.5C22 20.8807 19.9853 22 17.5 22C15.0147 22 13 20.8807 13 19.5ZM17.5 18C18.3284 18 19 18.6716 19 19.5C19 20.3284 18.3284 21 17.5 21C16.6716 21 16 20.3284 16 19.5C16 18.6716 16.6716 18 17.5 18ZM12 19.5L12 19.4861C11.3572 19.4236 10.7367 19.33 10.1387 19.2051C8.94141 18.9551 7.99609 18.5859 7.30273 18.0957L7.30273 19.5781C7.30273 19.9805 7.60156 20.3516 8.20117 20.6953C8.79883 21.0391 9.61328 21.3125 10.6426 21.5117C11.4872 21.6752 12.3923 21.7729 13.3579 21.8027C12.5123 21.1873 12 20.3817 12 19.5Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</g>
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="拓扑 1" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 422" d="M12.7727 11.142C12.4273 11.142 12.107 11.2495 11.8433 11.4327L9.42154 9.6837C9.65586 9.26852 9.78974 8.7891 9.78974 8.27836C9.78974 8.18728 9.7853 8.09723 9.77697 8.00833L12.257 6.54325C12.6396 6.77576 13.1451 6.72689 13.4758 6.39622C13.8641 6.00791 13.8641 5.37835 13.4758 4.99004C13.0875 4.60173 12.4579 4.60173 12.0696 4.99004C11.7835 5.27612 11.7085 5.69309 11.844 6.04812L9.63655 7.35217C9.40869 6.68522 8.9416 6.12922 8.33762 5.78637L8.77881 3.58047C8.81046 3.5835 8.84253 3.58519 8.87497 3.58519C9.42413 3.58519 9.8693 3.14002 9.8693 2.59087C9.8693 2.04173 9.42413 1.59656 8.87497 1.59656C8.32582 1.59656 7.88065 2.04173 7.88065 2.59087C7.88065 2.87182 7.99736 3.12535 8.18469 3.30617L7.73951 5.53213C7.48164 5.45587 7.2087 5.41473 6.9261 5.41473C6.388 5.41473 5.88464 5.56327 5.45458 5.8214L4.08134 4.61294C4.32918 4.12086 4.24801 3.50548 3.83714 3.09461C3.32457 2.58205 2.49355 2.58205 1.98098 3.09461C1.46841 3.60717 1.46842 4.4382 1.98098 4.95078C2.43973 5.40952 3.15341 5.45735 3.66574 5.09492L4.93964 6.21596C4.39895 6.73684 4.06247 7.46826 4.06247 8.27836C4.06247 9.15881 4.45994 9.94631 5.0851 10.4716L3.95324 11.6792C3.74531 11.5408 3.49572 11.4602 3.22724 11.4602C2.50236 11.4602 1.91474 12.0478 1.91474 12.7727C1.91474 13.4975 2.50236 14.0852 3.22724 14.0852C3.95212 14.0852 4.53974 13.4975 4.53974 12.7727C4.53974 12.5477 4.48308 12.3359 4.38332 12.1508L5.62251 10.8288C6.01351 11.029 6.45662 11.142 6.9261 11.142C7.76819 11.142 8.52534 10.7785 9.04931 10.1999L11.3976 11.8958C11.2358 12.149 11.142 12.4499 11.142 12.7727C11.142 13.6733 11.8721 14.4034 12.7727 14.4034C13.6733 14.4034 14.4034 13.6733 14.4034 12.7727C14.4034 11.8721 13.6733 11.142 12.7727 11.142L12.7727 11.142Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none">
<rect id="收藏" width="28.000000" height="28.000000" x="0.000000" y="0.000000" />
<path id="星形 2" d="M15.9534 11.0113C15.9936 11.1349 16.1088 11.2186 16.2388 11.2186L21.6363 11.2188C21.9269 11.2188 22.0478 11.5907 21.8127 11.7615L17.446 14.9343C17.3409 15.0107 17.2969 15.1461 17.3371 15.2697L19.0048 20.4031C19.0946 20.6795 18.7783 20.9094 18.5432 20.7385L14.1763 17.5661C14.0712 17.4897 13.9288 17.4897 13.8237 17.5661L9.45683 20.7385C9.22171 20.9094 8.90539 20.6795 8.99518 20.4031L10.6629 15.2697C10.7031 15.1461 10.6591 15.0107 10.554 14.9343L6.18734 11.7615C5.95224 11.5907 6.07307 11.2188 6.36368 11.2188L11.7612 11.2186C11.8912 11.2186 12.0064 11.1349 12.0466 11.0113L13.7147 5.87799C13.8045 5.60161 14.1955 5.60161 14.2853 5.87799L15.9534 11.0113Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="绿洲_拓扑图 1" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 422" d="M15.0147 9.88449L14.1867 9.88449L14.1867 8.65516C14.1867 7.53912 13.312 6.63116 12.2368 6.63116L8.37334 6.63116L8.37334 5.08449L9.2317 5.08449C9.42605 5.08449 9.58222 4.94394 9.58222 4.74957L9.58222 2.34115C9.58222 2.11642 9.40148 1.92004 9.17669 1.92004L6.76829 1.92004C6.57396 1.92004 6.41778 2.09179 6.41778 2.28616L6.41778 4.69459C6.41778 4.91931 6.59853 5.08449 6.82329 5.08449L7.62667 5.08449L7.62667 6.63116L3.59268 6.63116C2.62816 6.63116 1.81334 7.45731 1.81334 8.43519L1.81334 9.88449L0.930314 9.88449C0.735986 9.88449 0.58667 10.0572 0.58667 10.2515L0.58667 12.66C0.58667 12.8847 0.760554 13.0489 0.985319 13.0489L3.39371 13.0489C3.58806 13.0489 3.75111 12.9093 3.75111 12.7149L3.75111 10.3066C3.75111 10.0818 3.56351 9.88451 3.33874 9.88451L2.52445 9.88451L2.52445 8.43521C2.52445 7.84276 3.01364 7.34227 3.59268 7.34227L7.62667 7.34227L7.62667 9.88449L6.76829 9.88449C6.57396 9.88449 6.41778 10.0572 6.41778 10.2515L6.41778 12.66C6.41778 12.8847 6.59853 13.0489 6.82329 13.0489L9.2317 13.0489C9.42605 13.0489 9.58222 12.9093 9.58222 12.7149L9.58222 10.3066C9.58222 10.0818 9.40148 9.88451 9.1767 9.88451L8.37334 9.88451L8.37334 7.34227L12.2368 7.34227C12.9199 7.34227 13.4756 7.93123 13.4756 8.65516L13.4756 9.88449L12.6063 9.88449C12.4119 9.88449 12.2489 10.0572 12.2489 10.2515L12.2489 12.66C12.2489 12.8847 12.4365 13.0489 12.6613 13.0489L15.0697 13.0489C15.264 13.0489 15.4133 12.9093 15.4133 12.7149L15.4133 10.3066C15.4133 10.0818 15.2395 9.88449 15.0147 9.88449Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</svg>
/**
* Mock API - 法案数据模拟接口
* 所有数据集中管理,方便后续替换为真实 API
*/
// ===== 筛选项配置数据 =====
const DOMAIN_OPTIONS = [
{ id: 'all', label: '全部领域' },
{ id: 'ai', label: '人工智能' },
{ id: 'ic', label: '集成电路' },
{ id: 'quantum', label: '量子科技' },
{ id: 'network', label: '新一代通信网络' },
{ id: 'biotech', label: '生物技术' },
{ id: 'ocean', label: '海洋' },
{ id: 'deep-sea', label: '深海' },
{ id: 'polar', label: '极地' },
{ id: 'nuclear', label: '核' },
{ id: 'other', label: '其他' },
]
const TIME_OPTIONS = [
{ id: 'all', label: '全部时间' },
{ id: '2025', label: '2025年' },
{ id: '2024', label: '2024年' },
{ id: '2023', label: '2023年' },
{ id: '2022', label: '2022年' },
{ id: '2021', label: '2021年' },
{ id: 'earlier', label: '更早' },
]
// ===== 进度阶段配置 =====
const PROGRESS_STAGES = ['提出', '众议院通过', '参议院通过', '分歧协调', '提交总统', '法案通过']
// ===== 法案模拟数据 =====
const MOCK_BILLS = [
{
id: 1,
billNumber: 'H.R.1',
billSerial: '第1234',
title: '国家人工智能安全法案',
titleEn: 'One Big Beautiful Bill Act',
importance: '特别重大',
proposer: { name: '乔迪', avatar: null },
committee: '众议院科技委员会',
relatedDomains: ['人工智能', '集成电路'],
latestMotion: '2025.07.04',
latestMotionResult: '成',
currentStage: 5,
congressSession: '119th',
},
{
id: 2,
billNumber: 'H.R.2648',
billSerial: '第2648',
title: '半导体供应链安全法',
titleEn: 'CHIPS and Science Act Extension',
importance: '重大',
proposer: { name: '佩洛西', avatar: null },
committee: '参议院商务委员会',
relatedDomains: ['集成电路'],
latestMotion: '2025.06.18',
latestMotionResult: '通过',
currentStage: 3,
congressSession: '119th',
},
{
id: 3,
billNumber: 'S.1052',
billSerial: '第1052',
title: '量子计算出口管制法',
titleEn: 'Quantum Computing Export Control Act',
importance: '关注',
proposer: { name: '沃纳', avatar: null },
committee: '参议院情报委员会',
relatedDomains: ['量子科技', '人工智能'],
latestMotion: '2025.05.22',
latestMotionResult: '通过',
currentStage: 1,
congressSession: '119th',
},
{
id: 4,
billNumber: 'H.R.4217',
billSerial: '第4217',
title: '清洁能源核技术推进法案',
titleEn: 'Clean Nuclear Energy Advancement Act',
importance: '一般',
proposer: { name: '格兰姆', avatar: null },
committee: '众议院能源委员会',
relatedDomains: ['核', '生物技术'],
latestMotion: '2025.04.11',
latestMotionResult: '提出',
currentStage: 0,
congressSession: '119th',
},
{
id: 5,
billNumber: 'S.3301',
billSerial: '第3301',
title: '深海战略资源保护法',
titleEn: 'Deep Sea Strategic Resources Protection Act',
importance: '特别重大',
proposer: { name: '卢比奥', avatar: null },
committee: '参议院外交委员会',
relatedDomains: ['深海', '海洋'],
latestMotion: '2025.07.01',
latestMotionResult: '通过',
currentStage: 4,
congressSession: '119th',
},
]
// ===== 潜在新闻模拟数据 =====
const MOCK_POTENTIAL_NEWS = [
{ id: 1, title: '我国新一代人工智能大模型正式开放公众体验。', tags: [{ label: '人工智能', color: 'blue' }, { label: '科技突破', color: 'green' }] },
{ id: 2, title: '2026 年全国春运客流总量再创新高', tags: [{ label: '春运', color: 'blue' }] },
{ id: 3, title: '我国新一代人工智能大模型正式开放公众体验。', tags: [{ label: '人工智能', color: 'blue' }, { label: '科技突破', color: 'green' }] },
{ id: 4, title: '2026 年全国春运客流总量再创新高', tags: [{ label: '人工智能', color: 'blue' }, { label: '科技突破', color: 'green' }] },
{ id: 5, title: '我国新一代人工智能大模型正式开放公众体验。', tags: [{ label: '人工智能', color: 'blue' }, { label: '科技突破', color: 'green' }] },
{ id: 6, title: '2026 年全国春运客流总量再创新高', tags: [{ label: '人工智能', color: 'blue' }, { label: '科技突破', color: 'green' }] },
{ id: 7, title: '我国新一代人工智能大模型正式开放公众体验。', tags: [{ label: '人工智能', color: 'blue' }, { label: '科技突破', color: 'green' }] },
{ id: 8, title: '2026 年全国春运客流总量再创新高', tags: [{ label: '人工智能', color: 'blue' }, { label: '科技突破', color: 'green' }] },
{ id: 9, title: '我国新一代人工智能大模型正式开放公众体验。', tags: [{ label: '人工智能', color: 'blue' }, { label: '科技突破', color: 'green' }] },
{ id: 10, title: '2026 年全国春运客流总量再创新高', tags: [{ label: '人工智能', color: 'blue' }, { label: '科技突破', color: 'green' }] },
]
const MOCK_KEYWORDS = [
{ label: '财政责任', color: 'red', value: 48 },
{ label: '地球化改造', color: 'red', value: 36 },
{ label: '双边合作', color: 'cyan', value: 32 },
{ label: 'Neuralink+Grok', color: 'blue', value: 44 },
{ label: '能源政策', color: 'blue', value: 40 },
{ label: '保守派财政立场', color: 'red', value: 28 },
{ label: '农场法案倡导者', color: 'gold', value: 30 },
{ label: '环境议题', color: 'cyan', value: 34 },
{ label: '外交政策立场', color: 'gold', value: 26 },
{ label: '农业委员会成员', color: 'red', value: 22 },
{ label: '教育政策', color: 'cyan', value: 24 },
{ label: '工作可选项', color: 'gold', value: 20 },
{ label: '保守派价值观', color: 'blue', value: 38 },
{ label: '可持续能源', color: 'volcano', value: 42 },
{ label: '农业经济引擎', color: 'red', value: 18 },
{ label: '第一性原理', color: 'gold', value: 26 },
{ label: '多智能体协作', color: 'cyan', value: 28 },
{ label: '可持续富足', color: 'gold', value: 22 },
{ label: '为后代负责', color: 'gold', value: 32 },
]
// ===== 模拟网络延迟 =====
function delay(ms = 300) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// ===== API 接口函数 =====
/**
* 获取筛选项配置
*/
export async function fetchFilterOptions() {
await delay(100)
return {
code: 200,
data: {
domains: DOMAIN_OPTIONS,
timeRanges: TIME_OPTIONS,
},
}
}
/**
* 获取进度阶段配置
*/
export async function fetchProgressStages() {
await delay(50)
return {
code: 200,
data: PROGRESS_STAGES,
}
}
/**
* 获取法案列表
* @param {Object} params - 查询参数
* @param {string} params.keyword - 搜索关键词
* @param {string[]} params.domains - 选中的领域
* @param {string[]} params.timeRanges - 选中的时间范围
* @param {boolean} params.chinaRelatedOnly - 只看涉华法案
* @param {string} params.sortBy - 排序方式
*/
export async function fetchBills(params = {}) {
await delay(400)
let result = [...MOCK_BILLS]
// 模拟关键词过滤
if (params.keyword) {
const kw = params.keyword.toLowerCase()
result = result.filter(
(b) =>
b.title.toLowerCase().includes(kw) ||
b.titleEn.toLowerCase().includes(kw) ||
b.billNumber.toLowerCase().includes(kw)
)
}
// 模拟领域过滤
if (params.domains && params.domains.length > 0 && !params.domains.includes('all')) {
result = result.filter((b) =>
b.relatedDomains.some((d) => params.domains.includes(d))
)
}
// 模拟总量 1149
const simulatedTotal = 1149
const page = params.page || 1
const pageSize = params.pageSize || 10
// 循环填充 mock 数据以模拟分页
const pagedList = []
for (let i = 0; i < pageSize; i++) {
const src = result[i % result.length]
pagedList.push({
...src,
id: (page - 1) * pageSize + i + 1,
})
}
return {
code: 200,
data: {
list: pagedList,
total: simulatedTotal,
},
}
}
/**
* 获取潜在新闻列表
*/
export async function fetchPotentialNews(params = {}) {
await delay(300)
const page = params.page || 1
const pageSize = params.pageSize || 10
// simulate 96 total items
const totalItems = 96
const list = MOCK_POTENTIAL_NEWS.map((item, i) => ({
...item,
id: (page - 1) * pageSize + i + 1,
}))
return {
code: 200,
data: {
list,
total: totalItems,
},
}
}
/**
* 获取潜在新闻分析关键词
*/
export async function fetchPotentialNewsKeywords() {
await delay(200)
return {
code: 200,
data: MOCK_KEYWORDS,
}
}
/**
* 获取排序选项
*/
export async function fetchSortOptions() {
await delay(50)
return {
code: 200,
data: [
{ value: 'publishTimeDesc', label: '发布时间倒序' },
{ value: 'publishTimeAsc', label: '发布时间正序' },
],
}
}
<template>
<div class="bill-card">
<div class="bill-card-inner">
<div class="bill-card-preview">
<DocumentPreview :bill-number="bill.billId" :bill-serial="bill.name" />
</div>
<div class="bill-card-detail">
<div class="bill-card-upper">
<div class="bill-card-title-row">
<h3 class="bill-card-title">{{ bill.name }}</h3>
</div>
<p class="bill-card-desc">{{ bill.ename }}</p>
</div>
<div class="bill-card-divider" />
<div class="bill-card-meta">
<div class="meta-row">
<span class="meta-label">提案人:</span>
<span class="meta-value sponsor-name" :title="allSponsorNames">
{{ firstSponsorName }}
</span>
</div>
<div class="meta-row">
<span class="meta-label">相关领域:</span>
<div class="meta-tags">
<TagBadge v-for="item in bill.industryList" :key="item.industryName" :label="item.industryName" tag-class="tag3" />
</div>
</div>
<div class="meta-row">
<span class="meta-label">最新动议:</span>
<span class="meta-value">{{ latestActionText }}</span>
</div>
<div class="meta-row meta-row-progress">
<ProgressBar :stages="stageNames" :current="currentStageIndex" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import DocumentPreview from './DocumentPreview.vue'
import TagBadge from './TagBadge.vue'
import ProgressBar from './ProgressBar.vue'
const props = defineProps({
bill: { type: Object, required: true },
progressStages: { type: Array, default: () => [] },
})
// 提案人:只展示第一个,悬浮 title 显示全部
const firstSponsorName = computed(() => {
if (!props.bill.sponsorList || props.bill.sponsorList.length === 0) return ''
return props.bill.sponsorList[0] || ''
})
const allSponsorNames = computed(() => {
if (!props.bill.sponsorList || props.bill.sponsorList.length === 0) return ''
return props.bill.sponsorList.filter(Boolean).join('\n')
})
// 最新动议:null 替换为至今
const latestActionText = computed(() => {
if (!props.bill.latestAction) return ''
// 将 "null" 字符串替换为 "至今"
return props.bill.latestAction.replace(/\bnull\b/gi, '至今').trim()
})
// 法案进展:stageList 是对象数组 [{id, name}],提取 name 列表
const stageNames = computed(() => {
if (!props.progressStages || props.progressStages.length === 0) return []
// progressStages 是全量阶段字典 [{id, name}],提取 name
return props.progressStages.map(s => s.name || s)
})
// 当前阶段:找到 stageList 最后一个阶段在全量字典中的位置
const currentStageIndex = computed(() => {
if (!props.bill.stageList || props.bill.stageList.length === 0) return 0
if (!props.progressStages || props.progressStages.length === 0) return 0
// stageList 是已走过的阶段对象数组,取最后一个
const lastStage = props.bill.stageList[props.bill.stageList.length - 1]
const lastStageName = lastStage?.name || lastStage
const idx = props.progressStages.findIndex(s => (s.name || s) === lastStageName)
return idx > -1 ? idx : 0
})
</script>
<style scoped>
.bill-card {
width: 1224px;
max-width: 100%;
height: 320px;
background: #fff;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
box-sizing: border-box;
padding: 12px 16px;
cursor: pointer;
overflow: hidden;
transition: box-shadow 0.2s;
}
.bill-card:hover {
box-shadow: 0px 0px 24px 0px rgba(25, 69, 130, 0.16);
}
.bill-card-inner {
display: flex;
flex-direction: row;
gap: 13px;
align-items: center;
height: 100%;
}
.bill-card-preview {
flex-shrink: 0;
width: 240px;
height: 296px;
}
.bill-card-detail {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-start;
height: 100%;
}
.bill-card-upper {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.bill-card-title-row {
display: flex;
align-items: flex-start;
gap: 20px;
width: 100%;
}
.bill-card-title {
font-size: 20px;
font-weight: 700;
color: rgba(59, 65, 75, 1);
margin: 0;
line-height: 26px;
flex: 1;
min-width: 0;
}
.bill-card-desc {
font-size: 16px;
font-weight: 400;
color: rgba(59, 65, 75, 1);
margin: 0;
line-height: 24px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.bill-card-divider {
width: 100%;
height: 1px;
background: rgba(234, 236, 238, 1);
flex-shrink: 0;
}
.bill-card-meta {
width: 100%;
flex: 1;
position: relative;
min-height: 0;
}
.meta-row {
display: flex;
align-items: center;
position: absolute;
left: 0;
width: 100%;
}
.meta-row:nth-child(1) { top: 0; }
.meta-row:nth-child(2) { top: 36px; }
.meta-row:nth-child(3) { top: 72px; }
.meta-row:nth-child(4) { top: 108px; }
.meta-row:nth-child(5) { top: 144px; }
.meta-label {
font-size: 16px;
font-weight: 700;
color: rgba(59, 65, 75, 1);
letter-spacing: 1px;
line-height: 24px;
white-space: nowrap;
flex-shrink: 0;
width: 100px;
}
.meta-value {
font-size: 16px;
font-weight: 400;
color: rgba(95, 101, 108, 1);
line-height: 24px;
}
.sponsor-name {
cursor: default;
white-space: nowrap;
}
.meta-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.meta-row-progress {
left: 0;
right: 0;
width: 100%;
}
.meta-row-progress :deep(.progress-bar) {
width: 100%;
}
.meta-progress {
flex: 1;
min-width: 0;
}
</style>
<template>
<div class="bill-list">
<div v-if="loading" class="bill-list-loading">
<div v-for="i in 3" :key="i" class="bill-list-skeleton">
<div class="skeleton-left" />
<div class="skeleton-right">
<div class="skeleton-line w30" />
<div class="skeleton-line w50" />
<div class="skeleton-divider" />
<div class="skeleton-line w60" />
<div class="skeleton-line w40" />
</div>
</div>
</div>
<div v-else-if="bills.length === 0" class="bill-list-empty">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#bfbfbf" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p>暂无相关法案</p>
</div>
<template v-else>
<BillCard
v-for="bill in bills"
:key="bill.id"
:bill="bill"
:progress-stages="progressStages"
/>
<div v-if="total > 0" class="bill-pagination">
<span class="bill-pagination-total">{{ '\u5171' + total + '\u9879\u6cd5\u6848' }}</span>
<el-pagination
:current-page="currentPage"
:page-size="pageSize"
:total="total"
:pager-count="5"
layout="prev, pager, next"
background
@current-change="$emit('page-change', $event)"
/>
</div>
</template>
</div>
</template>
<script setup>
import BillCard from './BillCard.vue'
defineProps({
bills: { type: Array, required: true },
loading: { type: Boolean, default: false },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 },
pageSize: { type: Number, default: 10 },
progressStages: { type: Array, default: () => ['提出', '众议院通过', '参议院通过', '分歧协调', '提交总统', '法案通过'] },
})
defineEmits(['page-change'])
</script>
<style scoped>
.bill-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.bill-list-loading {
display: flex;
flex-direction: column;
gap: 16px;
}
.bill-list-skeleton {
display: flex;
gap: 24px;
background: #fff;
border: 1px solid #ebedf0;
border-radius: 8px;
padding: 24px;
}
.skeleton-left {
width: 120px;
height: 190px;
background: #f0f0f0;
border-radius: 4px;
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-right {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.skeleton-line {
height: 16px;
background: #f0f0f0;
border-radius: 4px;
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-line.w30 { width: 30%; }
.skeleton-line.w40 { width: 40%; }
.skeleton-line.w50 { width: 50%; }
.skeleton-line.w60 { width: 60%; }
.skeleton-divider {
height: 1px;
background: #ebedf0;
}
.bill-list-empty {
background: #fff;
border: 1px solid #ebedf0;
border-radius: 8px;
padding: 48px;
text-align: center;
color: #bfbfbf;
font-size: 13px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Pagination */
.bill-pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0 0;
}
.bill-pagination-total {
font-size: 14px;
font-family: 'Microsoft YaHei', sans-serif;
font-weight: 400;
color: rgba(95, 101, 108, 1);
white-space: nowrap;
}
/* Override el-pagination to match design */
.bill-pagination :deep(.el-pagination) {
--el-pagination-bg-color: #fff;
--el-pagination-hover-color: rgba(5, 95, 194, 1);
padding: 0;
}
.bill-pagination :deep(.el-pagination.is-background .btn-prev),
.bill-pagination :deep(.el-pagination.is-background .btn-next),
.bill-pagination :deep(.el-pagination.is-background .el-pager li) {
min-width: 32px;
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid rgba(234, 236, 238, 1);
background: #fff;
color: rgba(59, 65, 75, 1);
font-size: 14px;
font-family: 'Microsoft YaHei', sans-serif;
margin: 0 3px;
}
.bill-pagination :deep(.el-pagination.is-background .el-pager li:not(.is-disabled).is-active) {
background: #fff;
color: rgba(5, 95, 194, 1);
border-color: rgba(5, 95, 194, 1);
}
.bill-pagination :deep(.el-pagination.is-background .el-pager li:not(.is-disabled):hover) {
color: rgba(5, 95, 194, 1);
}
.bill-pagination :deep(.el-pagination.is-background .btn-prev:hover),
.bill-pagination :deep(.el-pagination.is-background .btn-next:hover) {
color: rgba(5, 95, 194, 1);
}
</style>
<template>
<div class="bill-tracker-root" ref="trackerRoot">
<div class="bt-topbar">
<TopBar
:keyword="filters.searchText"
:china-related-only="filters.isCN"
:sort-by="filters.sortBy"
:sort-options="sortOptions"
@update:keyword="updateKeyword"
@toggle-china-related="updateChinaRelated(!filters.isCN)"
@update:sort="updateSort"
/>
</div>
<div class="bt-main">
<div class="bt-sidebar">
<SidebarFilter
:domains="filterOptions.domains"
:time-ranges="filterOptions.timeRanges"
:selected-domains="filters.selectedDomains"
:selected-time-ranges="filters.selectedTimeRanges"
@toggle-domain="toggleDomain"
@toggle-time="toggleTimeRange"
/>
</div>
<div class="bt-list-area">
<BillList
:bills="bills"
:loading="loading"
:total="total"
:current-page="currentPage"
:page-size="pageSize"
:progress-stages="stageConfig"
@page-change="onPageChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import TopBar from './TopBar.vue'
import SidebarFilter from './SidebarFilter.vue'
import BillList from './BillList.vue'
import { useBills } from '../composables/useBills.js'
const trackerRoot = ref(null)
const {
bills,
stageConfig,
total,
loading,
currentPage,
pageSize,
filters,
filterOptions,
sortOptions,
init,
updateKeyword,
toggleDomain,
toggleTimeRange,
updateSort,
updateChinaRelated,
handlePageChange,
} = useBills()
const onPageChange = async (page) => {
await handlePageChange(page)
// 数据加载完成后滚动到顶部
if (trackerRoot.value) {
trackerRoot.value.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
onMounted(async () => {
try {
await init()
} catch (err) {
console.error('初始化失败:', err)
}
})
</script>
<style scoped>
.bill-tracker-root {
width: 100%;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
color: rgba(59, 65, 75, 1);
}
.bt-topbar {
padding: 12px 0;
}
.bt-main {
display: flex;
gap: 16px;
}
.bt-sidebar {
flex-shrink: 0;
}
.bt-list-area {
flex: 1;
min-width: 0;
}
</style>
<template>
<div class="doc-preview">
<div class="doc-image">
<div class="doc-frame">
<div class="doc-seal">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="#999" stroke-width="0.8" fill="none"/>
<circle cx="8" cy="8" r="4" stroke="#999" stroke-width="0.5" fill="none"/>
<line x1="8" y1="1" x2="8" y2="3" stroke="#999" stroke-width="0.5"/>
<line x1="8" y1="13" x2="8" y2="15" stroke="#999" stroke-width="0.5"/>
<line x1="1" y1="8" x2="3" y2="8" stroke="#999" stroke-width="0.5"/>
<line x1="13" y1="8" x2="15" y2="8" stroke="#999" stroke-width="0.5"/>
</svg>
</div>
<div class="doc-text">
<div class="doc-line-italic">One Hundred Nineteenth Congress</div>
<div class="doc-line-small">of the</div>
<div class="doc-line-italic">United States of America</div>
<div class="doc-body-lines">
<div class="doc-body-line" style="width: 100%" />
<div class="doc-body-line" style="width: 83%" />
<div class="doc-body-line" style="width: 92%" />
<div class="doc-body-line" style="width: 75%" />
<div class="doc-body-line" style="width: 100%" />
<div class="doc-body-line" style="width: 67%" />
</div>
</div>
</div>
</div>
<div class="doc-info">
<div class="doc-number">{{ billNumber }}</div>
<div class="doc-serial">{{ billSerial }}</div>
</div>
</div>
</template>
<script setup>
defineProps({
billNumber: { type: String, required: true },
billSerial: { type: String, required: true },
})
</script>
<style scoped>
.doc-preview {
width: 240px;
height: 296px;
position: relative;
}
.doc-image {
width: 222px;
height: 296px;
position: absolute;
left: 13px;
top: 0;
}
.doc-frame {
width: 100%;
height: 100%;
background: #fff;
border: 1px solid #ebedf0;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 12px 12px;
overflow: hidden;
box-sizing: border-box;
}
.doc-seal {
position: absolute;
top: 8px;
left: 8px;
}
.doc-text {
margin-top: 20px;
width: 100%;
display: flex;
flex-direction: column;
gap: 3px;
padding: 0 4px;
}
.doc-line-italic {
font-size: 7px;
color: rgba(95, 101, 108, 1);
text-align: center;
font-style: italic;
line-height: 1.4;
}
.doc-line-small {
font-size: 6px;
color: rgba(95, 101, 108, 1);
text-align: center;
line-height: 1.4;
}
.doc-body-lines {
margin-top: 10px;
border-top: 1px solid #ebedf0;
padding-top: 8px;
display: flex;
flex-direction: column;
gap: 5px;
}
.doc-body-line {
height: 2.5px;
background: #e0e0e0;
border-radius: 1px;
margin: 0 auto;
}
.doc-info {
position: absolute;
left: 0;
bottom: 0;
width: 240px;
height: 78px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding-bottom: 4px;
background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,0.95) 40%);
border-radius: 0 0 4px 4px;
}
.doc-number {
font-size: 20px;
font-weight: 700;
color: rgba(59, 65, 75, 1);
line-height: 36px;
text-align: center;
}
.doc-serial {
font-size: 16px;
font-weight: 400;
color: rgba(95, 101, 108, 1);
line-height: 24px;
text-align: center;
}
</style>
<template>
<span class="importance-badge" :class="'importance-' + level">
<span class="importance-dot"></span>
{{ label }}
</span>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
label: { type: String, required: true },
})
const level = computed(() => {
if (props.label.includes('特别重大') || props.label.includes('高')) return 'high'
if (props.label.includes('重大') || props.label.includes('中') || props.label.includes('橙')) return 'medium'
if (props.label.includes('关注') || props.label.includes('黄')) return 'warning'
return 'low'
})
</script>
<style scoped>
.importance-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 8px;
border-radius: 20px;
font-size: 16px;
font-weight: 400;
white-space: nowrap;
flex-shrink: 0;
line-height: 24px;
}
.importance-dot {
display: inline-block;
width: 4px;
height: 4px;
border-radius: 50%;
flex-shrink: 0;
}
.importance-high {
background: rgba(206, 79, 81, 0.1);
color: rgba(206, 79, 81, 1);
}
.importance-high .importance-dot {
background: rgba(206, 79, 81, 1);
}
.importance-medium {
background: rgba(255, 149, 77, 0.1);
color: rgba(255, 149, 77, 1);
}
.importance-medium .importance-dot {
background: rgba(255, 149, 77, 1);
}
.importance-warning {
background: rgba(232, 189, 11, 0.1);
color: rgba(232, 189, 11, 1);
}
.importance-warning .importance-dot {
background: rgba(232, 189, 11, 1);
}
.importance-low {
background: rgba(33, 129, 57, 0.1);
color: rgba(33, 129, 57, 1);
}
.importance-low .importance-dot {
background: rgba(33, 129, 57, 1);
}
</style>
<template>
<div class="potential-news-root">
<div class="pn-analysis">
<PotentialNewsAnalysis :keywords="keywords" />
</div>
<div class="pn-list">
<PotentialNewsList
:news-list="newsList"
:total="total"
:current-page="currentPage"
:page-size="10"
@page-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import PotentialNewsList from './PotentialNewsList.vue'
import PotentialNewsAnalysis from './PotentialNewsAnalysis.vue'
import { getPotentialNewsList, getPotentialNewsKeywords } from '@/api/bill/billHome.js'
const route = useRoute()
const personId = computed(() => route.params.personId || route.query.personId || '')
const newsList = ref([])
const total = ref(0)
const currentPage = ref(1)
const keywords = ref([])
const COLORS = ['red', 'cyan', 'gold', 'blue', 'volcano']
async function loadNews(page = 1) {
if (!personId.value) return
currentPage.value = page
const res = await getPotentialNewsList(personId.value, {
currentPage: page - 1,
pageSize: 10,
})
if (res.code === 200 && res.data) {
newsList.value = res.data.content || []
total.value = res.data.totalElements || 0
} else {
newsList.value = []
total.value = 0
}
}
async function loadKeywords() {
if (!personId.value) return
const res = await getPotentialNewsKeywords(personId.value)
if (res.code === 200 && res.data) {
keywords.value = res.data.map((item, index) => ({
label: item.clause,
value: item.count,
color: COLORS[index % COLORS.length],
}))
}
}
async function handlePageChange(page) {
await loadNews(page)
}
onMounted(async () => {
if (!personId.value) return
await Promise.all([loadNews(), loadKeywords()])
})
</script>
<style scoped>
.potential-news-root {
display: flex;
gap: 16px;
width: 100%;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
color: rgba(59, 65, 75, 1);
}
.pn-analysis {
width: 430px;
flex-shrink: 0;
}
.pn-list {
flex: 1;
min-width: 0;
}
</style>
<template>
<div class="pna-container">
<!-- Header -->
<div class="pna-header">
<div class="pna-header-left">
<svg class="pna-header-icon" width="8" height="20" viewBox="0 0 8 20" fill="none">
<rect width="8" height="20" rx="2" fill="rgba(5,95,194,1)" />
</svg>
<span class="pna-header-title">潜在提案主题分析</span>
</div>
<div class="pna-header-actions">
<!-- 下载图标 (9_1305): 竖线 + 向下箭头 + 底座 -->
<svg class="pna-action-icon" width="28" height="28" viewBox="0 0 28 28" fill="none">
<rect x="13" y="7" width="2" height="6" rx="0.5" fill="rgba(132,136,142,1)" />
<path d="M10 13l4 4 4-4" stroke="rgba(132,136,142,1)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
<path d="M7 19h14" stroke="rgba(132,136,142,1)" stroke-width="1.5" stroke-linecap="round" />
</svg>
<!-- 收藏/星标图标 (9_1303): 五角星 -->
<svg class="pna-action-icon" width="28" height="28" viewBox="0 0 28 28" fill="none">
<path d="M14 6l2.47 5.01L22 11.76l-4 3.9.94 5.5L14 18.51l-4.94 2.65.94-5.5-4-3.9 5.53-.75L14 6z" stroke="rgba(132,136,142,1)" stroke-width="1.5" stroke-linejoin="round" fill="none" />
</svg>
</div>
</div>
<!-- ECharts word cloud -->
<div ref="chartRef" class="pna-cloud"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import * as echarts from 'echarts'
import 'echarts-wordcloud'
const props = defineProps({
keywords: { type: Array, default: () => [] },
})
const chartRef = ref(null)
let chartInstance = null
// 设计稿色板
const COLOR_MAP = {
red: 'rgba(206, 79, 81, 1)',
cyan: 'rgba(19, 168, 168, 1)',
gold: 'rgba(250, 173, 20, 1)',
blue: 'rgba(22, 119, 255, 1)',
volcano: 'rgba(255, 122, 69, 1)',
}
function buildOption(data) {
const seriesData = data.map((item) => ({
name: item.label,
value: item.value || item.size || 18,
textStyle: {
color: COLOR_MAP[item.color] || COLOR_MAP.blue,
},
}))
return {
series: [
{
type: 'wordCloud',
shape: 'circle',
left: 'center',
top: 'center',
width: '90%',
height: '90%',
sizeRange: [12, 28],
rotationRange: [-45, 45],
rotationStep: 15,
gridSize: 8,
drawOutOfBound: false,
layoutAnimation: true,
textStyle: {
fontFamily: 'Microsoft YaHei, Source Han Sans CN, Noto Sans SC, sans-serif',
fontWeight: 400,
},
emphasis: {
textStyle: {
fontWeight: 700,
},
},
data: seriesData,
},
],
}
}
function initChart() {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
if (props.keywords.length > 0) {
chartInstance.setOption(buildOption(props.keywords))
}
}
function handleResize() {
chartInstance?.resize()
}
watch(
() => props.keywords,
(val) => {
if (chartInstance && val.length > 0) {
chartInstance.setOption(buildOption(val))
}
},
{ deep: true }
)
onMounted(() => {
nextTick(() => {
initChart()
window.addEventListener('resize', handleResize)
})
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
chartInstance?.dispose()
chartInstance = null
})
</script>
<style scoped>
.pna-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 10px;
border: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
overflow: hidden;
}
.pna-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
flex-shrink: 0;
}
.pna-header-left {
display: flex;
align-items: center;
gap: 12px;
}
.pna-header-title {
font-size: 20px;
font-weight: 700;
line-height: 26px;
color: rgba(5, 95, 194, 1);
font-family: 'Source Han Sans CN', 'Noto Sans SC', sans-serif;
}
.pna-header-actions {
display: flex;
align-items: center;
gap: 4px;
}
.pna-action-icon {
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.pna-action-icon:hover {
opacity: 1;
}
.pna-cloud {
flex: 1;
min-height: 400px;
}
</style>
<template>
<div class="pn-list-container">
<div v-if="loading" class="pn-loading-mask">
<div class="pn-spinner"></div>
</div>
<div class="pn-header">
<div class="pn-header-left">
<svg class="pn-header-icon" width="8" height="20" viewBox="0 0 8 20" fill="none">
<rect width="8" height="20" rx="2" fill="rgba(5,95,194,1)" />
</svg>
<span class="pn-header-title">潜在提案举措分析</span>
</div>
<div class="pn-header-actions">
<svg class="pn-action-icon" width="28" height="28" viewBox="0 0 28 28" fill="none">
<rect x="13" y="7" width="2" height="6" rx="0.5" fill="rgba(132,136,142,1)" />
<path d="M10 13l4 4 4-4" stroke="rgba(132,136,142,1)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
<path d="M7 19h14" stroke="rgba(132,136,142,1)" stroke-width="1.5" stroke-linecap="round" />
</svg>
<svg class="pn-action-icon" width="28" height="28" viewBox="0 0 28 28" fill="none">
<path d="M14 6l2.47 5.01L22 11.76l-4 3.9.94 5.5L14 18.51l-4.94 2.65.94-5.5-4-3.9 5.53-.75L14 6z" stroke="rgba(132,136,142,1)" stroke-width="1.5" stroke-linejoin="round" fill="none" />
</svg>
</div>
</div>
<div class="pn-search-wrapper">
<div class="pn-search">
<span class="pn-search-placeholder">{{ keyword || '搜索新闻' }}</span>
<svg class="pn-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" fill="none" />
<path d="M11 11L14 14" stroke="rgba(132,136,142,1)" stroke-width="1.2" stroke-linecap="round" />
</svg>
</div>
</div>
<div class="pn-rows" :class="{ 'pn-rows-loading': loading }">
<div v-if="loading" class="pn-rows-spinner">
<div class="pn-spinner-icon"></div>
<span class="pn-spinner-text">加载中...</span>
</div>
<div
v-for="(item, index) in newsList"
v-show="!loading"
:key="item.tkid"
:class="['pn-row', { 'pn-row-alt': index % 2 === 0 }]"
>
<div class="pn-row-num">
<span class="pn-num-circle"></span>
<span class="pn-num-text">{{ index + 1 + (currentPage - 1) * pageSize }}</span>
</div>
<p class="pn-row-title">{{ item.fynr }}</p>
</div>
</div>
<div class="pn-footer">
<span class="pn-footer-total">{{ total }}条关键新闻</span>
<div class="pn-pagination">
<button
class="pn-page-btn pn-page-prev"
:disabled="currentPage <= 1"
@click="$emit('page-change', currentPage - 1)"
>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
<path d="M6.5 1.5L3.5 5L6.5 8.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<button
v-for="page in displayedPages"
:key="page"
:class="['pn-page-num', { active: page === currentPage }]"
@click="$emit('page-change', page)"
>{{ page }}</button>
<button
class="pn-page-btn pn-page-next"
:disabled="currentPage >= totalPages"
@click="$emit('page-change', currentPage + 1)"
>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
<path d="M3.5 1.5L6.5 5L3.5 8.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import TagBadge from './TagBadge.vue'
const tagColorMap = {
blue: 'tag6',
green: '',
}
const props = defineProps({
newsList: { type: Array, default: () => [] },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 },
pageSize: { type: Number, default: 10 },
keyword: { type: String, default: '' },
loading: { type: Boolean, default: false },
})
defineEmits(['page-change'])
const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)))
const displayedPages = computed(() => {
const pages = []
const tp = totalPages.value
const cp = props.currentPage
const start = Math.max(1, cp - 2)
const end = Math.min(tp, start + 4)
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
})
</script>
<style scoped>
.pn-list-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 10px;
border: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
overflow: hidden;
}
.pn-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
flex-shrink: 0;
}
.pn-header-left {
display: flex;
align-items: center;
gap: 12px;
}
.pn-header-title {
font-size: 20px;
font-weight: 700;
line-height: 26px;
color: rgba(5, 95, 194, 1);
font-family: 'Source Han Sans CN', 'Noto Sans SC', sans-serif;
}
.pn-header-actions {
display: flex;
align-items: center;
gap: 4px;
}
.pn-action-icon {
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.pn-action-icon:hover {
opacity: 1;
}
.pn-search-wrapper {
padding: 0 24px 0 24px;
flex-shrink: 0;
}
.pn-search {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
padding: 0 12px;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
background: #fff;
}
.pn-search-placeholder {
font-size: 16px;
line-height: 24px;
color: rgba(132, 136, 142, 1);
font-family: 'Source Han Sans CN', 'Noto Sans SC', sans-serif;
}
.pn-search-icon {
flex-shrink: 0;
}
.pn-rows {
flex: 1;
overflow-y: auto;
margin-top: 12px;
position: relative;
}
.pn-rows-loading {
display: flex;
align-items: center;
justify-content: center;
}
.pn-rows-spinner {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.pn-spinner-icon {
width: 40px;
height: 40px;
border: 4px solid rgba(22, 119, 255, 0.2);
border-top-color: rgba(22, 119, 255, 1);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.pn-spinner-text {
font-size: 14px;
color: rgba(132, 136, 142, 1);
}
.pn-row {
position: relative;
height: 60px;
display: flex;
align-items: center;
padding: 0 24px;
border-top: 1px solid rgba(234, 236, 238, 1);
background: #fff;
}
.pn-row-alt {
background: rgba(247, 248, 249, 1);
}
.pn-row-num {
position: relative;
width: 24px;
height: 24px;
flex-shrink: 0;
margin-right: 12px;
}
.pn-num-circle {
position: absolute;
inset: 0;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, rgba(5, 95, 194, 0.06) 0%, rgba(5, 95, 194, 0.12) 100%);
border: 1px solid rgba(5, 95, 194, 0.15);
}
.pn-num-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 400;
line-height: 12px;
color: rgba(5, 95, 194, 1);
font-family: 'Microsoft YaHei', sans-serif;
}
.pn-row-title {
flex: 1;
min-width: 0;
font-size: 16px;
font-weight: 700;
line-height: 30px;
color: rgba(59, 65, 75, 1);
font-family: 'Source Han Sans CN', 'Noto Sans SC', sans-serif;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pn-row-tags {
display: flex;
gap: 8px;
flex-shrink: 0;
margin-left: 24px;
}
.pn-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 24px;
border-top: 1px solid rgba(234, 236, 238, 1);
flex-shrink: 0;
}
.pn-footer-total {
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: rgba(132, 136, 142, 1);
font-family: 'Source Han Sans CN', 'Noto Sans SC', sans-serif;
}
.pn-pagination {
display: flex;
align-items: center;
gap: 4px;
}
.pn-page-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
background: #fff;
cursor: pointer;
color: rgba(0, 0, 0, 0.65);
transition: border-color 0.2s, color 0.2s;
}
.pn-page-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pn-page-num {
min-width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
background: #fff;
cursor: pointer;
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: rgba(0, 0, 0, 0.88);
font-family: 'Microsoft YaHei', sans-serif;
padding: 0 4px;
transition: border-color 0.2s, color 0.2s;
}
.pn-page-num.active {
border-color: rgba(22, 119, 255, 1);
color: rgba(22, 119, 255, 1);
}
.pn-page-num:hover:not(.active) {
border-color: rgba(22, 119, 255, 0.5);
color: rgba(22, 119, 255, 0.8);
}
.pn-loading-mask {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
backdrop-filter: blur(2px);
}
.pn-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(22, 119, 255, 0.2);
border-top-color: rgba(22, 119, 255, 1);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
<template>
<div class="progress-bar">
<span class="progress-title">法案进展:</span>
<div class="progress-steps">
<div
v-for="(stage, idx) in [...visibleStages].reverse()"
:key="idx"
class="tag"
:style="{ zIndex: visibleStages.length - idx }"
>
{{ stage }}
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
stages: { type: Array, required: true },
current: { type: Number, default: 0 },
})
/**
* 只渲染到当前步骤为止的阶段
* @returns {Array} 可见的阶段列表
*/
const visibleStages = computed(() => {
return props.stages.slice(0, props.current + 1)
})
</script>
<style scoped>
.progress-bar {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
height: 26px;
}
.progress-title {
font-size: 16px;
font-weight: 700;
color: rgba(59, 65, 75, 1);
letter-spacing: 1px;
line-height: 24px;
white-space: nowrap;
flex-shrink: 0;
}
.progress-steps {
display: flex;
align-items: center;
}
.tag {
height: 24px;
line-height: 22px;
padding: 0 10px 0 30px;
background: rgba(255, 255, 255, 1);
color: rgb(95, 101, 108);
border-top: 1px solid rgb(234, 236, 238);
border-bottom: 1px solid rgb(234, 236, 238);
position: relative;
margin-left: -10px;
font-family: 'Microsoft YaHei', sans-serif;
font-size: 14px;
font-weight: 400;
white-space: nowrap;
box-sizing: border-box;
}
.tag::after {
content: '';
position: absolute;
top: 50%;
right: -8.485px;
width: 16.97px;
height: 16.97px;
background: inherit;
border-top: 1px solid rgb(234, 236, 238);
border-right: 1px solid rgb(234, 236, 238);
transform: translateY(-50%) rotate(45deg);
z-index: 1;
box-shadow: 2px -2px 2px rgba(0, 0, 0, 0.05);
box-sizing: border-box;
}
.tag:first-child {
margin-left: 0;
padding-left: 10px;
border-left: 1px solid rgb(234, 236, 238);
border-radius: 4px 0 0 4px;
}
.tag:last-child {
background: rgb(59, 65, 75);
color: rgba(255, 255, 255, 1);
border-color: rgb(59, 65, 75);
padding-right: 10px;
border-radius: 0;
border-right: none;
}
.tag:last-child::after {
display: block;
border-color: rgb(59, 65, 75);
box-shadow: none;
}
.tag:first-child:last-child {
margin-left: 0;
padding: 0 10px;
border-radius: 4px 0 0 4px;
background: rgb(59, 65, 75);
color: rgba(255, 255, 255, 1);
border: 1px solid rgb(59, 65, 75);
border-right: none;
}
</style>
<template>
<aside class="sidebar-filter">
<div class="sf-section">
<div class="sf-title-row">
<span class="sf-title-bar"></span>
<span class="sf-title-text">科技领域</span>
</div>
<div class="sf-options">
<label v-for="opt in domains" :key="opt.id" class="sf-label">
<span
class="sf-checkbox-icon"
:class="{ 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="#055FC2"/>
<path d="M3.5 7L6 9.5L10.5 5" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span v-else class="sf-checkbox-empty"></span>
</span>
<span class="sf-text" @click="$emit('toggle-domain', opt.id)">{{ opt.label }}</span>
</label>
</div>
</div>
<div class="sf-section">
<div class="sf-title-row">
<span class="sf-title-bar"></span>
<span class="sf-title-text">发布时间</span>
</div>
<div class="sf-options">
<label v-for="opt in timeRanges" :key="opt.id" class="sf-label">
<span
class="sf-checkbox-icon"
:class="{ checked: selectedTimeRanges.includes(opt.id) }"
@click.prevent="$emit('toggle-time', opt.id)"
>
<svg v-if="selectedTimeRanges.includes(opt.id)" width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect width="14" height="14" rx="4" fill="#055FC2"/>
<path d="M3.5 7L6 9.5L10.5 5" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span v-else class="sf-checkbox-empty"></span>
</span>
<span class="sf-text" @click="$emit('toggle-time', opt.id)">{{ opt.label }}</span>
</label>
</div>
</div>
</aside>
</template>
<script setup>
defineProps({
domains: { type: Array, default: () => [] },
timeRanges: { type: Array, default: () => [] },
selectedDomains: { type: Array, default: () => ['all'] },
selectedTimeRanges: { type: Array, default: () => ['all'] },
})
defineEmits(['toggle-domain', 'toggle-time'])
</script>
<style scoped>
.sidebar-filter {
width: 360px;
background: #fff;
border-radius: 10px;
border: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
box-sizing: border-box;
padding: 16px 0 24px 0;
display: flex;
flex-direction: column;
gap: 16px;
align-items: flex-start;
overflow: hidden;
}
.sf-section {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.sf-title-row {
display: flex;
align-items: center;
gap: 17px;
width: 100%;
padding: 0 0 0 0;
box-sizing: border-box;
}
.sf-title-bar {
display: inline-block;
width: 8px;
height: 16px;
background: rgba(5, 95, 194, 1);
border-radius: 0 2px 2px 0;
flex-shrink: 0;
}
.sf-title-text {
font-size: 16px;
font-weight: 700;
font-family: 'Source Han Sans CN', 'Noto Sans SC', sans-serif;
color: rgba(5, 95, 194, 1);
letter-spacing: 1px;
line-height: 24px;
}
.sf-options {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px 4px;
align-content: flex-start;
align-items: flex-start;
padding: 0 0 0 24px;
width: 100%;
box-sizing: border-box;
}
.sf-label {
width: 160px;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
flex-shrink: 0;
}
.sf-checkbox-icon {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
flex-shrink: 0;
cursor: pointer;
}
.sf-checkbox-empty {
display: block;
width: 14px;
height: 14px;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
box-sizing: border-box;
background: #fff;
}
.sf-text {
font-size: 16px;
font-weight: 400;
font-family: 'Source Han Sans CN', 'Noto Sans SC', sans-serif;
color: rgba(95, 101, 108, 1);
line-height: 24px;
cursor: pointer;
white-space: nowrap;
}
.sf-label:hover .sf-text {
color: rgba(59, 65, 75, 1);
}
</style>
<template>
<AreaTag :tag-name="label" :class="tagClass" />
</template>
<script setup>
import { h, defineComponent } from 'vue'
const AreaTag = defineComponent({
name: 'AreaTag',
props: {
tagName: { type: String, default: '标签名称' },
},
setup(props) {
return () => h('div', { class: 'tag-wrapper' }, props.tagName)
},
})
defineProps({
label: { type: String, required: true },
tagClass: { type: String, default: 'tag3' },
})
</script>
<style scoped>
.tag-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
height: 24px;
padding: 0 8px;
line-height: 24px;
text-align: center;
font-family: Microsoft YaHei, sans-serif;
font-size: 14px;
font-weight: 400;
letter-spacing: 0px;
box-sizing: border-box;
border-radius: 4px;
border: 1px solid rgba(183, 235, 143, 1);
background: rgba(246, 255, 237, 1);
color: rgba(82, 196, 26, 1);
}
.tag3 .tag-wrapper,
:deep(.tag3) {
border: 1px solid rgba(174, 214, 255, 1);
background: rgba(246, 250, 255, 1);
color: rgba(5, 95, 194, 1);
}
.tag1 .tag-wrapper,
:deep(.tag1) {
border: 1px solid rgba(255, 163, 158, 1);
background: rgba(255, 241, 240, 1);
color: rgba(245, 34, 45, 1);
}
.tag6 .tag-wrapper,
:deep(.tag6) {
border: 1px solid rgba(145, 202, 255, 1);
background: rgba(230, 244, 255, 1);
color: rgba(22, 119, 255, 1);
}
.tag8 .tag-wrapper,
:deep(.tag8) {
border: 1px solid rgba(255, 229, 143, 1);
background: rgba(255, 251, 230, 1);
color: rgba(250, 173, 20, 1);
}
.tag9 .tag-wrapper,
:deep(.tag9) {
border: 1px solid rgba(255, 213, 145, 1);
background: rgba(255, 247, 230, 1);
color: rgba(250, 140, 22, 1);
}
.tag4 .tag-wrapper,
:deep(.tag4) {
border: 1px solid rgba(211, 173, 247, 1);
background: rgba(249, 240, 255, 1);
color: rgba(114, 46, 209, 1);
}
</style>
<template>
<header class="topbar">
<div class="topbar-search">
<input
type="text"
:value="keyword"
@input="$emit('update:keyword', $event.target.value)"
placeholder="搜索法案"
class="topbar-search-input"
/>
<svg class="topbar-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
</div>
<div class="topbar-spacer"></div>
<div class="topbar-sort" ref="sortDropdownRef">
<button class="topbar-sort-btn" @click="showSortDropdown = !showSortDropdown">
<svg class="topbar-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="topbar-sort-label">{{ currentSortLabel }}</span>
<svg class="topbar-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="topbar-sort-dropdown">
<button
v-for="opt in sortOptions"
:key="opt.value"
@click="selectSort(opt.value)"
class="topbar-sort-option"
:class="{ active: sortBy === opt.value }"
>
{{ opt.label }}
</button>
</div>
</div>
<label class="topbar-checkbox-label">
<input
type="checkbox"
:checked="chinaRelatedOnly"
@change="$emit('toggle-china-related')"
class="topbar-checkbox"
/>
<span>只看涉华法案</span>
</label>
</header>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({
keyword: { type: String, default: '' },
chinaRelatedOnly: { type: Boolean, default: true },
sortBy: { type: String, default: 'publishTimeDesc' },
sortOptions: { type: Array, default: () => [] },
})
const emit = defineEmits(['update:keyword', 'toggle-china-related', '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>
.topbar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.topbar-search {
position: relative;
flex: 1;
min-width: 180px;
max-width: 400px;
}
.topbar-search-input {
width: 100%;
height: 32px;
padding: 0 32px 0 12px;
font-size: 14px;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
outline: none;
color: rgba(59, 65, 75, 1);
background: #fff;
box-sizing: border-box;
line-height: 22px;
}
.topbar-search-input::placeholder {
color: rgba(132, 136, 142, 1);
}
.topbar-search-input:focus {
border-color: rgba(5, 95, 194, 1);
box-shadow: 0 0 0 2px rgba(5, 95, 194, 0.1);
}
.topbar-search-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: rgba(132, 136, 142, 1);
pointer-events: none;
}
.topbar-spacer {
flex: 1;
}
.topbar-sort {
position: relative;
flex-shrink: 0;
}
.topbar-sort-btn {
width: 170px;
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;
}
.topbar-sort-btn:hover {
border-color: rgba(5, 95, 194, 0.4);
}
.topbar-sort-icon {
flex-shrink: 0;
margin-right: 6px;
}
.topbar-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;
}
.topbar-sort-arrow {
flex-shrink: 0;
transition: transform 0.2s;
}
.topbar-sort-arrow.open {
transform: rotate(180deg);
}
.topbar-sort-dropdown {
position: absolute;
right: 0;
top: 100%;
margin-top: 4px;
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;
min-width: 140px;
}
.topbar-sort-option {
display: block;
width: 100%;
text-align: left;
padding: 8px 12px;
font-size: 14px;
background: none;
border: none;
cursor: pointer;
color: rgba(95, 101, 108, 1);
}
.topbar-sort-option:hover {
background: rgba(247, 248, 249, 1);
}
.topbar-sort-option.active {
color: rgba(5, 95, 194, 1);
font-weight: 500;
}
.topbar-checkbox-label {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 400;
color: rgba(132, 136, 142, 1);
white-space: nowrap;
line-height: 24px;
}
.topbar-checkbox {
width: 16px;
height: 16px;
accent-color: rgba(5, 95, 194, 1);
cursor: pointer;
}
</style>
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论