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

刘宇琪科技暂存当前分支的修改:科技人物功能开发中

上级 a1df7e9b
差异被折叠。
......@@ -41,7 +41,10 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"autoprefixer": "^10.4.27",
"postcss": "^8.5.8",
"sass": "^1.93.3",
"tailwindcss": "^3.4.17",
"unplugin-auto-import": "^0.17.0",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.0"
......
......@@ -78,3 +78,6 @@
letter-spacing: 0px;
text-align: left;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
/**
* 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',
},
]
// ===== 模拟网络延迟 =====
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))
)
}
return {
code: 200,
data: {
list: result,
total: result.length,
},
}
}
/**
* 获取排序选项
*/
export async function fetchSortOptions() {
await delay(50)
return {
code: 200,
data: [
{ value: 'publishTime', label: '按发布时间' },
{ value: 'importance', label: '按重要程度' },
{ value: 'progress', label: '按进展阶段' },
],
}
}
<template>
<div class="bill-card">
<div class="bill-card-inner">
<!-- Left: Document preview -->
<div class="bill-card-preview">
<DocumentPreview :bill-number="bill.billNumber" :bill-serial="bill.billSerial" />
</div>
<!-- Right: Details -->
<div class="bill-card-detail">
<!-- Upper: title + tag + description -->
<div class="bill-card-upper">
<!-- Title row -->
<div class="bill-card-title-row">
<h3 class="bill-card-title">{{ bill.title }}</h3>
<ImportanceBadge :label="bill.importance" />
</div>
<!-- Description -->
<p class="bill-card-desc">{{ bill.titleEn }}</p>
</div>
<!-- Divider -->
<div class="bill-card-divider"></div>
<!-- Meta grid area -->
<div class="bill-card-meta">
<div class="meta-row">
<span class="meta-label">提案人:</span>
<span class="meta-value">
<UserAvatar :name="bill.proposer.name" :avatar="bill.proposer.avatar" />
</span>
</div>
<div class="meta-row">
<span class="meta-label">委员会:</span>
<span class="meta-value">{{ bill.committee }}</span>
</div>
<div class="meta-row">
<span class="meta-label">相关领域:</span>
<div class="meta-tags">
<TagBadge v-for="domain in bill.relatedDomains" :key="domain" variant="blue">{{ domain }}</TagBadge>
<TagBadge v-if="bill.chinaRelated" variant="red">涉华</TagBadge>
</div>
</div>
<div class="meta-row">
<span class="meta-label">最新动议:</span>
<span class="meta-value">{{ bill.latestMotion }}</span>
</div>
<div class="meta-row meta-row-progress">
<ProgressBar :stages="progressStages" :current="bill.currentStage" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import DocumentPreview from './DocumentPreview.vue'
import ImportanceBadge from './ImportanceBadge.vue'
import UserAvatar from './UserAvatar.vue'
import TagBadge from './TagBadge.vue'
import ProgressBar from './ProgressBar.vue'
defineProps({
bill: { type: Object, required: true },
progressStages: { type: Array, default: () => ['提出', '众议院通过', '参议院通过', '分歧协调', '提交总统', '法案通过'] },
})
</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;
}
.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">
<!-- Loading -->
<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>
<div class="skeleton-right">
<div class="skeleton-line w30"></div>
<div class="skeleton-line w50"></div>
<div class="skeleton-divider"></div>
<div class="skeleton-line w60"></div>
<div class="skeleton-line w40"></div>
</div>
</div>
</div>
<!-- Empty -->
<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>
<!-- Cards -->
<template v-else>
<BillCard
v-for="bill in bills"
:key="bill.id"
:bill="bill"
:progress-stages="progressStages"
/>
</template>
</div>
</template>
<script setup>
import BillCard from './BillCard.vue'
defineProps({
bills: { type: Array, required: true },
loading: { type: Boolean, default: false },
progressStages: { type: Array, default: () => ['提出', '众议院通过', '参议院通过', '分歧协调', '提交总统', '法案通过'] },
})
</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; }
}
</style>
<template>
<div class="bill-tracker-root">
<!-- Top bar -->
<div class="bt-topbar">
<TopBar
:keyword="filters.keyword"
:china-related-only="filters.chinaRelatedOnly"
:sort-by="filters.sortBy"
:sort-options="sortOptions"
@update:keyword="updateKeyword"
@toggle-china-related="toggleChinaRelated"
@update:sort="updateSort"
/>
</div>
<!-- Main content area -->
<div class="bt-main">
<!-- Sidebar filter panel -->
<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>
<!-- Bill list -->
<div class="bt-list-area">
<!-- Total count + pagination header -->
<div class="bt-list-header">
<span class="bt-total">{{ bills.length }}项法案</span>
</div>
<BillList
:bills="bills"
:loading="loading"
:progress-stages="progressStages"
/>
</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'
import { fetchProgressStages } from '../api/bills.js'
const {
bills,
loading,
filters,
filterOptions,
sortOptions,
init,
updateKeyword,
toggleDomain,
toggleTimeRange,
toggleChinaRelated,
updateSort,
} = useBills()
const progressStages = ref(['提出', '众议院通过', '参议院通过', '分歧协调', '提交总统', '法案通过'])
onMounted(async () => {
const stagesRes = await fetchProgressStages()
if (stagesRes.code === 200) {
progressStages.value = stagesRes.data
}
await init()
})
</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);
background: rgba(247, 248, 249, 1);
min-height: 100vh;
}
.bt-topbar {
padding: 12px 24px;
background: #fff;
border-bottom: 1px solid rgba(234, 236, 238, 1);
}
.bt-main {
display: flex;
padding: 16px 24px;
gap: 16px;
}
.bt-sidebar {
flex-shrink: 0;
}
.bt-list-area {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.bt-list-header {
display: flex;
align-items: center;
}
.bt-total {
font-size: 16px;
font-weight: 400;
color: rgba(59, 65, 75, 1);
line-height: 24px;
}
</style>
<template>
<div class="doc-preview">
<!-- Document image area -->
<div class="doc-image">
<div class="doc-frame">
<!-- Seal icon -->
<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>
<!-- Simulated text -->
<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>
<div class="doc-body-line" style="width: 83%"></div>
<div class="doc-body-line" style="width: 92%"></div>
<div class="doc-body-line" style="width: 75%"></div>
<div class="doc-body-line" style="width: 100%"></div>
<div class="doc-body-line" style="width: 67%"></div>
</div>
</div>
</div>
</div>
<!-- Overlay info -->
<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;
}
/* High importance - Red */
.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);
}
/* Medium importance - Orange */
.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);
}
/* Warning importance - Yellow */
.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);
}
/* Low importance - Green */
.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="progress-bar">
<span class="progress-title">法案进展:</span>
<div class="progress-steps">
<div
v-for="(stage, index) in stages"
:key="stage"
class="progress-step"
:class="{
active: index <= current,
last: index === stages.length - 1,
'last-active': index === stages.length - 1 && index <= current,
}"
>
<svg
v-if="index < stages.length - 1"
class="step-shape"
:viewBox="'0 0 120 26'"
preserveAspectRatio="none"
>
<path
:d="index === 0 ? 'M4,0 L108,0 L120,13 L108,26 L4,26 C1.8,26 0,24.2 0,22 L0,4 C0,1.8 1.8,0 4,0 Z' : 'M0,0 L108,0 L120,13 L108,26 L0,26 L12,13 Z'"
:fill="index <= current ? '#E6E7E8' : '#F7F8F9'"
:stroke="index <= current ? '#D0D1D2' : '#E6E7E8'"
stroke-width="0.5"
/>
</svg>
<div
v-else
class="step-pill"
:class="{ filled: index <= current }"
></div>
<span class="step-label" :class="{ 'label-white': index === stages.length - 1 && index <= current }">
{{ stage }}
</span>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
stages: { type: Array, required: true },
current: { type: Number, default: 0 },
})
</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;
height: 26px;
flex: 1;
min-width: 0;
}
.progress-step {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 26px;
flex: 1;
min-width: 0;
}
.progress-step.last {
flex: 0 0 auto;
min-width: 90px;
}
.step-shape {
display: block;
width: 100%;
height: 26px;
}
.step-label {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 13px;
font-weight: 400;
color: rgba(132, 136, 142, 1);
white-space: nowrap;
pointer-events: none;
line-height: 1;
}
.progress-step.active .step-label {
color: rgba(59, 65, 75, 1);
}
.step-label.label-white {
color: #fff;
}
.step-pill {
width: 100%;
min-width: 90px;
height: 26px;
border-radius: 13px;
background: #E6E7E8;
}
.step-pill.filled {
background: rgba(59, 65, 75, 1);
}
.progress-step.last .step-label {
color: rgba(132, 136, 142, 1);
}
.progress-step.last-active .step-label {
color: #fff;
}
</style>
<template>
<aside class="sidebar-filter">
<!-- Domain 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>
<!-- Time range 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 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>
<span class="tag-badge" :class="'tag-' + variant">
<slot />
</span>
</template>
<script setup>
defineProps({
variant: {
type: String,
default: 'blue',
validator: (v) => ['blue', 'red'].includes(v),
},
})
</script>
<style scoped>
.tag-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 14px;
font-weight: 400;
line-height: 14px;
white-space: nowrap;
}
/* Blue variant - matching design token 业务系统主色10 */
.tag-blue {
background: rgba(231, 243, 255, 1);
color: rgba(5, 95, 194, 1);
}
/* Red variant - matching design token 业务系统红色10 */
.tag-red {
background: rgba(206, 79, 81, 0.1);
color: rgba(206, 79, 81, 1);
}
</style>
<template>
<header class="topbar">
<!-- Search input -->
<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>
<!-- Spacer -->
<div class="topbar-spacer"></div>
<!-- Sort dropdown -->
<div class="topbar-sort" ref="sortDropdownRef">
<button class="topbar-sort-btn" @click="showSortDropdown = !showSortDropdown">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
</svg>
{{ currentSortLabel }}
</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>
<!-- China-related checkbox -->
<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: 'publishTime' },
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 {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 14px;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
background: #fff;
color: rgba(95, 101, 108, 1);
cursor: pointer;
white-space: nowrap;
}
.topbar-sort-btn:hover {
border-color: rgba(5, 95, 194, 1);
}
.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>
<template>
<div class="user-avatar">
<div class="user-avatar-circle" :style="{ backgroundColor: avatarColor }">
{{ initial }}
</div>
<span class="user-avatar-name">{{ name }}</span>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
name: { type: String, required: true },
avatar: { type: String, default: null },
})
const COLORS = ['#CE4F51', '#055FC2', '#E8BD0B', '#218139', '#FF954D', '#3B414B']
const avatarColor = computed(() => {
let hash = 0
for (let i = 0; i < props.name.length; i++) {
hash = props.name.charCodeAt(i) + ((hash << 5) - hash)
}
return COLORS[Math.abs(hash) % COLORS.length]
})
const initial = computed(() => props.name.charAt(0))
</script>
<style scoped>
.user-avatar {
display: inline-flex;
align-items: center;
gap: 8px;
}
.user-avatar-circle {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 10px;
font-weight: 500;
flex-shrink: 0;
}
.user-avatar-name {
font-size: 16px;
font-weight: 400;
color: rgba(59, 65, 75, 1);
line-height: 24px;
}
</style>
import { ref, reactive, watch } from 'vue'
import { fetchBills, fetchFilterOptions, fetchSortOptions } from '../api/bills.js'
/**
* 法案数据管理 composable
* 封装所有与法案数据相关的状态与请求逻辑
*/
export function useBills() {
// ===== 状态 =====
const bills = ref([])
const total = ref(0)
const loading = ref(false)
const filterOptions = reactive({
domains: [],
timeRanges: [],
})
const sortOptions = ref([])
const filters = reactive({
keyword: '',
selectedDomains: ['all'],
selectedTimeRanges: ['all'],
chinaRelatedOnly: true,
sortBy: 'publishTime',
})
// ===== 方法 =====
async function loadFilterOptions() {
const res = await fetchFilterOptions()
if (res.code === 200) {
filterOptions.domains = res.data.domains
filterOptions.timeRanges = res.data.timeRanges
}
}
async function loadSortOptions() {
const res = await fetchSortOptions()
if (res.code === 200) {
sortOptions.value = res.data
}
}
async function loadBills() {
loading.value = true
try {
const res = await fetchBills({
keyword: filters.keyword,
domains: filters.selectedDomains,
timeRanges: filters.selectedTimeRanges,
chinaRelatedOnly: filters.chinaRelatedOnly,
sortBy: filters.sortBy,
})
if (res.code === 200) {
bills.value = res.data.list
total.value = res.data.total
}
} finally {
loading.value = false
}
}
function updateKeyword(keyword) {
filters.keyword = keyword
}
function toggleDomain(domainId) {
if (domainId === 'all') {
filters.selectedDomains = ['all']
} else {
const idx = filters.selectedDomains.indexOf(domainId)
// Remove 'all' if selecting specific
filters.selectedDomains = filters.selectedDomains.filter((d) => d !== 'all')
if (idx > -1) {
filters.selectedDomains.splice(idx, 1)
} else {
filters.selectedDomains.push(domainId)
}
if (filters.selectedDomains.length === 0) {
filters.selectedDomains = ['all']
}
}
}
function toggleTimeRange(timeId) {
if (timeId === 'all') {
filters.selectedTimeRanges = ['all']
} else {
const idx = filters.selectedTimeRanges.indexOf(timeId)
filters.selectedTimeRanges = filters.selectedTimeRanges.filter((d) => d !== 'all')
if (idx > -1) {
filters.selectedTimeRanges.splice(idx, 1)
} else {
filters.selectedTimeRanges.push(timeId)
}
if (filters.selectedTimeRanges.length === 0) {
filters.selectedTimeRanges = ['all']
}
}
}
function toggleChinaRelated() {
filters.chinaRelatedOnly = !filters.chinaRelatedOnly
}
function updateSort(sortBy) {
filters.sortBy = sortBy
}
// 监听筛选变化自动重新获取
watch(
() => ({
domains: [...filters.selectedDomains],
timeRanges: [...filters.selectedTimeRanges],
chinaRelatedOnly: filters.chinaRelatedOnly,
sortBy: filters.sortBy,
keyword: filters.keyword,
}),
() => {
loadBills()
},
{ deep: true }
)
// 初始化
async function init() {
await Promise.all([loadFilterOptions(), loadSortOptions()])
await loadBills()
}
return {
bills,
total,
loading,
filters,
filterOptions,
sortOptions,
init,
loadBills,
updateKeyword,
toggleDomain,
toggleTimeRange,
toggleChinaRelated,
updateSort,
}
}
<template>
<div class="resume-card">
<div class="resume-card__header">
<div class="resume-card__indicator"></div>
<div class="resume-card__title">{{ title }}</div>
<div class="resume-card__tabs" v-if="tabs.length > 0">
<div
v-for="(tab, i) in tabs"
:key="tab.key"
class="resume-card__tab"
:class="{ 'resume-card__tab--active': activeTab === tab.key }"
@click="switchTab(tab.key)"
>{{ tab.label }}</div>
</div>
<div class="resume-card__actions">
<img
:src="downloadIcon"
alt="下载"
class="resume-card__action-btn"
@click="$emit('download')"
/>
<img
:src="collectIcon"
alt="收藏"
class="resume-card__action-btn"
@click="$emit('collect')"
/>
</div>
</div>
<div class="resume-card__body">
<div v-for="(item, index) in currentList" :key="index" class="resume-item">
<img :src="timelineIcon" alt="" class="resume-item__icon" />
<div class="resume-item__time">{{ item.startTime + '-' + item.endTime }}</div>
<div class="resume-item__title">{{ item.orgName + '|' + item.jobName }}</div>
<div class="resume-item__content">{{ item.content }}</div>
<div class="resume-item__door" v-if="item.door">
<img :src="doorIcon" alt="" />
<span>{{ item.door }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import defaultDownloadIcon from "../assets/下载按钮.png";
import defaultCollectIcon from "../assets/收藏按钮.png";
import defaultTimelineIcon from "../assets/icon01.png";
import defaultDoorIcon from "../assets/icon02.png";
export default {
name: "ResumeCard",
props: {
/** 卡片标题 */
title: {
type: String,
default: "生涯履历",
},
/**
* Tab 配置数组,示例:
* [{ key: "career", label: "职业履历" }, { key: "edu", label: "教育履历" }]
* 不传则无 tab,直接显示 list 数据
*/
tabs: {
type: Array,
default: () => [],
},
/**
* 无 tab 时的数据数组;有 tab 时忽略此 prop,使用 dataMap
*/
list: {
type: Array,
default: () => [],
},
/**
* 有 tab 时的数据映射,key 对应 tabs[].key,示例:
* { career: [...], edu: [...] }
*/
dataMap: {
type: Object,
default: () => ({}),
},
/** 自定义图标路径(可选) */
downloadIcon: {
type: String,
default: defaultDownloadIcon,
},
collectIcon: {
type: String,
default: defaultCollectIcon,
},
timelineIcon: {
type: String,
default: defaultTimelineIcon,
},
doorIcon: {
type: String,
default: defaultDoorIcon,
},
},
emits: ["download", "collect", "tab-change"],
data() {
return {
activeTab: this.tabs.length > 0 ? this.tabs[0].key : "",
};
},
computed: {
/** 当前展示的列表数据 */
currentList() {
if (this.tabs.length > 0 && this.activeTab) {
return this.dataMap[this.activeTab] || [];
}
return this.list;
},
},
methods: {
switchTab(key) {
this.activeTab = key;
this.$emit("tab-change", key);
},
},
};
</script>
<style scoped lang="scss">
.resume-card {
width: 520px;
min-height: 200px;
background-color: rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1);
}
.resume-card__header {
width: 100%;
height: 80px;
display: flex;
align-items: center;
padding: 14px 12px 40px 0;
}
.resume-card__indicator {
width: 8px;
height: 20px;
background-color: rgb(5, 95, 194);
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
margin-right: 14px;
}
.resume-card__title {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(5, 95, 194);
}
.resume-card__tabs {
display: flex;
align-items: center;
gap: 12px;
margin-left: 20px;
}
.resume-card__tab {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
padding: 2px 12px;
border-radius: 4px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.2s;
&:hover {
color: rgb(5, 95, 194);
}
&--active {
color: rgb(5, 95, 194);
font-weight: 700;
border-color: rgb(5, 95, 194);
background-color: rgba(5, 95, 194, 0.05);
}
}
.resume-card__actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
}
.resume-card__action-btn {
width: 28px;
height: 28px;
cursor: pointer;
}
.resume-card__body {
width: 480px;
margin-left: 16px;
padding-bottom: 20px;
}
.resume-item {
width: 454px;
// margin-bottom: 60px;
margin-left: 26px;
position: relative;
}
.resume-item__icon {
width: 14px;
height: 12.13px;
position: absolute;
top: 8px;
left: -26px;
}
.resume-item__time {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 30px;
color: rgb(5, 95, 194);
margin-bottom: 8px;
}
.resume-item__title {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 30px;
color: rgb(59, 65, 75);
margin-bottom: 8px;
}
.resume-item__content {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
margin-bottom: 8px;
}
.resume-item__door {
width: 300px;
height: 32px;
display: flex;
align-items: center;
padding: 4px 0 4px 11px;
border-radius: 4px;
background-color: rgba(255, 246, 240, 1);
border: 1px solid rgba(250, 140, 22, 0.4);
img {
width: 20px;
height: 24px;
margin-right: 10px;
}
span {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgba(255, 149, 77, 1);
}
}
</style>
......@@ -205,28 +205,12 @@
</div>
</div>
</div>
<div class="right-bottom">
<div class="title">
<div class="box"></div>
<div class="text">政治履历</div>
<div class="btn">
<img src="./assets/下载按钮.png" alt="" />
<img src="./assets/收藏按钮.png" alt="" />
</div>
</div>
<div class="content-main">
<div v-for="item in CharacterResume" class="content-item">
<img src="./assets/icon01.png" alt="" class="image01" />
<div class="content-item-time">{{ item.startTime +'-' + item.endTime}}</div>
<div class="content-item-title">{{ item.orgName +'|' + item.jobName}}</div>
<div class="content-item-content">{{ item.content }}</div>
<div class="content-item-door" v-if="item.door">
<img src="./assets/icon02.png" alt="" />
<span>{{ item.door }}</span>
</div>
</div>
</div>
</div>
<ResumeCard
title="政治履历"
:list="CharacterResume"
@download="handleDownload"
@collect="handleCollect"
/>
</div>
</div>
<!-- 成果报告 -->
......@@ -281,7 +265,7 @@ import { getCharacterGlobalInfo,
import Musk from "./assets/Musk.png";
import cp1 from "./assets/cp1.png";
import cp2 from "./assets/cp2.png";
import ResumeCard from "./components/Resume.vue";
import img1 from "./assets/img1.png";
import img2 from "./assets/img2.png";
......@@ -1433,112 +1417,7 @@ const dialogData = ref([
}
}
}
.right-bottom {
width: 520px;
height: 1292px;
background-color: rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1);
.title {
width: 100%;
height: 80px;
display: flex;
align-items: center;
padding: 14px 12px 40px 0;
.box {
width: 8px;
height: 20px;
background-color: rgb(5, 95, 194);
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
margin-right: 14px;
}
.text {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(5, 95, 194);
}
.btn {
width: 60px;
height: 28px;
margin-left: auto;
img {
width: 28px;
height: 28px;
cursor: pointer;
}
img:first-child {
margin-right: 4px;
}
}
}
.content-main {
width: 480px;
height: 1020px;
margin-left: 16px;
.content-item {
width: 454px;
margin-bottom: 60px;
margin-left: 26px;
position: relative;
.image01 {
width: 14px;
height: 12.13px;
position: absolute;
top: 8px;
left: -26px;
}
.content-item-time {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 30px;
color: rgb(5, 95, 194);
margin-bottom: 8px;
}
.content-item-title {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 30px;
color: rgb(59, 65, 75);
margin-bottom: 8px;
}
.content-item-content {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
margin-bottom: 8px;
}
.content-item-door {
width: 300px;
height: 32px;
display: flex;
align-items: center;
padding: 4px 0 4px 11px;
border-radius: 4px;
background-color: rgba(255, 246, 240, 1);
border: 1px solid rgba(250, 140, 22, 0.4);
img {
width: 20px;
height: 24px;
margin-right: 10px;
}
span {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgba(255, 149, 77, 1);
}
}
}
}
}
}
}
}
......
......@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="person-info">
<div class="person-info" @click="handleClickToCharacter(item.personId)">
<div class="person-name">{{ item.name }}</div>
<div class="person-position">{{ item.position }}</div>
</div>
......@@ -49,7 +49,8 @@
import { ref,onMounted,defineProps,watch } from "vue";
import personData from "../json/personData.json"; // 引入JSON数据
import {getMainCharactersView } from "@/api/technologyFigures/technologyFigures";
import { useCharacterNav } from "../utils/useCharacterNav";
const { handleClickToCharacter } = useCharacterNav();
const props = defineProps({
persontypeid: {
type: String,
......@@ -86,7 +87,8 @@ const handlegetMainCharactersViewFn = async () => {
position: item.positionTitle,
tags: ["1", "2"],
chinaRelatedCount: item.remarksCount,
mediaQuoteCount: item.remarksCount
mediaQuoteCount: item.remarksCount,
personId:item.personId
}
});
......
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论