提交 15c01871 authored 作者: caijian's avatar caijian

Merge branch 'cj_dev'

<template> <template>
<div class="policy-list"> <div class="policy-list">
<div v-for="item in props.policyList" :key="item.id" class="policy-item"> <div
<el-image :src="$withFallbackImage(item.imageUrl, item.content) " class="item-cover" fit="cover"/> v-for="(item, index) in policyList"
<div class="item-details"> :key="index"
<h3 class="item-title">{{ item.name }}</h3> class="policy-item"
<div class="item-content"> {{ item.times }} · {{ item.content }} <el-icon><Link /></el-icon></div> >
<div class="item-tags"> <div class="item-left">
<el-tag v-for="tag in item.tags" :key="tag" class="custom-tag">{{ tag }}</el-tag> <div class="report-cover">
<img :src="$withFallbackImage(item.imageUrl, index)" alt="Report Cover" />
</div> </div>
</div>
<div
v-if="item.relatedBill" <div class="item-right">
class="related-bill-box" <h3 class="item-title">
:class="`status-bg-${item.status}`" {{ item.content }}
> </h3>
<span>{{ item.relatedBill.text }}</span>
<div class="status-badge" :class="`status-color-${item.status}`"> <div class="item-meta">
<span class="badge-dot"></span> <span class="meta-date">{{ formatDate(item.times) }}</span>
{{ getStatusInfo(item.status).text }} <span class="meta-divider">·</span>
</div> <span class="meta-source">
{{ item.name }}
<el-icon class="link-icon"><TopRight /></el-icon>
</span>
</div>
<div class="item-tags" v-if="item.tags && item.tags.length">
<span v-for="(tag, tIndex) in item.tags" :key="tIndex" class="tag-pill">
{{ tag }}
</span>
</div> </div>
<div v-else class="related-bill-box status-bg-unimplemented"> <div class="item-actions" v-if="item.statusRaw">
<span>不存在相关提案。</span> <div
<div class="status-badge status-color-unimplemented"> v-for="(statusItem, sIndex) in parseStatus(item.statusRaw)"
<span class="badge-dot"></span> :key="sIndex"
{{ item.status }} class="status-link"
>
<span class="status-type">{{ statusItem.type }}</span>
<span class="status-year">{{ statusItem.year }}</span>
<span class="status-name">{{ statusItem.name }}</span>
<el-icon class="arrow-icon"><Right /></el-icon>
</div> </div>
</div> </div>
</div> </div>
...@@ -33,35 +48,53 @@ ...@@ -33,35 +48,53 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { ref } from 'vue' import { TopRight, Right } from '@element-plus/icons-vue'
import { Link } from '@element-plus/icons-vue'
interface PolicyItem {
const props = defineProps({ content: string;
policyList: { statusRaw: string; // 原始的长字符串
type: Array, name: string;
default: () => [] times: string;
} tags?: string[];
}) coverUrl?: string;
}
// --- Status Styling Helper ---
const getStatusInfo = (status) => { const props = defineProps<{
switch (status) { policyList: PolicyItem[]
case 'implemented': }>()
return { text: '已实施', color: '#e66657', bgColor: '#fdeeed' };
case 'partial':
return { text: '部分实施', color: '#d38f24', bgColor: '#fcf3e4' };
case 'unimplemented':
return { text: '未实施', color: '#409eff', bgColor: '#ecf5ff' };
default:
return { text: '未知', color: '#909399', bgColor: '#f4f4f5' };
}
};
// 格式化日期:2025-06-26 -> 2025年6月26日
const formatDate = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`;
}
// 解析状态字符串
// 输入: "法案 2024 《芯片科学法案》; 政令 2025 《推动美国...》"
// 输出: 数组对象
const parseStatus = (raw: string) => {
if (!raw) return [];
// 按分号分割多个条目
const items = raw.split(/[;;]/).map(s => s.trim()).filter(s => s);
return items.map(itemStr => {
// 简单正则匹配: "类型 年份 《名称》"
// 注意:这里假设数据格式比较规范,实际需根据后端数据调整
// 尝试移除书名号进行提取
const cleanStr = itemStr.replace(/[《》]/g, '');
const parts = cleanStr.split(' ');
return {
type: parts[0] || '政策',
year: parts[1] || '',
name: parts.slice(2).join(' ') || cleanStr // 剩余部分作为名称
}
});
}
</script> </script>
<style scoped> <style scoped>
/* --- Policy List Styles --- */
.policy-list { .policy-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -70,139 +103,143 @@ const getStatusInfo = (status) => { ...@@ -70,139 +103,143 @@ const getStatusInfo = (status) => {
.policy-item { .policy-item {
display: flex; display: flex;
padding: 20px 0; padding: 20px 0;
border-bottom: 1px solid #e4e7ed; border-bottom: 1px solid #ebeef5;
gap: 16px;
transition: background-color 0.2s;
} }
.policy-item:last-child { .policy-item:last-child {
border-bottom: none; border-bottom: none;
} }
.item-cover { /* 左侧封面 */
width: 80px; .item-left {
height: 80px;
margin-right: 20px;
flex-shrink: 0; flex-shrink: 0;
border-radius: 4px;
border: 1px solid #ebeef5;
} }
.item-details { .report-cover {
flex-grow: 1; width: 60px;
height: 80px;
background-color: #f2f3f5;
border: 1px solid #e4e7ed;
border-radius: 2px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.report-cover img {
width: 100%;
height: 100%;
object-fit: cover;
} }
/* 右侧内容 */
.item-right {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* 1. 标题 */
.item-title { .item-title {
margin: 0 0 6px 0;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 700;
color: #303133; color: #1a1a1a;
margin: 0 0 8px;
cursor: pointer;
line-height: 1.4; line-height: 1.4;
cursor: pointer;
} }
.item-title:hover { .item-title:hover {
color: #409eff; color: #409EFF;
} }
.item-content { /* 2. 元数据 */
color: #909399; .item-meta {
font-size: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 13px;
color: #606266;
margin-bottom: 8px;
} }
.item-tags { .meta-divider {
margin-bottom: 12px; margin: 0 8px;
} font-weight: bold;
.custom-tag {
margin-right: 8px;
background-color: #f0f2f5;
color: #606266;
border-color: #e4e7ed;
font-size: 12px;
} }
.related-bill-box { .meta-source {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
padding: 8px 12px; gap: 4px;
border-radius: 6px; cursor: pointer;
font-size: 14px; }
line-height: 1.4;
.meta-source:hover {
color: #409EFF;
} }
.status-badge { .link-icon {
font-size: 12px;
}
/* 3. 标签 */
.item-tags {
display: flex; display: flex;
align-items: center; gap: 8px;
font-size: 13px; margin-bottom: 10px;
padding: 4px 8px;
border-radius: 4px;
white-space: nowrap;
font-weight: 500;
} }
.badge-dot { .tag-pill {
width: 6px; background-color: #f2f3f5;
height: 6px; color: #5e6d82;
border-radius: 50%; font-size: 12px;
background-color: currentColor; padding: 2px 8px;
margin-right: 6px; border-radius: 4px;
} }
/* Dynamic status colors */ /* 4. 底部状态链接 */
.status-bg-implemented { .item-actions {
background-color: #fdeeed; display: flex;
flex-wrap: wrap;
gap: 10px;
} }
.status-color-implemented { .status-link {
color: #e66657; display: inline-flex;
align-items: center;
background-color: #ecf5ff; /* 浅蓝色背景 */
color: #409EFF; /* 蓝色文字 */
padding: 4px 12px;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
} }
.status-bg-partial { .status-link:hover {
background-color: #fcf3e4; background-color: #d9ecff;
} }
.status-color-partial { .status-type {
color: #d38f24; font-weight: bold;
margin-right: 4px;
} }
.status-bg-unimplemented { .status-year {
background-color: #ecf5ff; margin-right: 4px;
} }
.status-color-unimplemented { .status-name {
color: #409eff; margin-right: 4px;
} }
/* 响应式设计 */ .arrow-icon {
@media (max-width: 768px) { margin-left: 4px;
.policy-item { font-size: 12px;
flex-direction: column;
gap: 12px;
padding: 16px 0;
}
.item-cover {
width: 100%;
height: 120px;
margin-right: 0;
margin-bottom: 12px;
}
.item-title {
font-size: 15px;
}
.related-bill-box {
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 12px;
}
.status-badge {
align-self: flex-end;
}
} }
</style> </style>
\ No newline at end of file
...@@ -82,7 +82,7 @@ import PolicyList from './PolicyList.vue'; ...@@ -82,7 +82,7 @@ import PolicyList from './PolicyList.vue';
import CardTitle from './CardTitle.vue'; import CardTitle from './CardTitle.vue';
import { getOverviewPolicy } from '@/api' import { getOverviewPolicy } from '@/api'
import PolicyOverview from '@/views/thinkTank/components/PolicyOverview.vue' import PolicyOverview from '@/views/thinkTank/components/PolicyOverview.vue'
import { mockPolicyList } from '@/views/thinkTank/mockData';
const props = defineProps({ const props = defineProps({
showSearch: { showSearch: {
type: Boolean, type: Boolean,
...@@ -120,7 +120,8 @@ const getPolicies = async () => { ...@@ -120,7 +120,8 @@ const getPolicies = async () => {
researchTypeIds: activeTechField.value, researchTypeIds: activeTechField.value,
statusList: activeStatus.value, statusList: activeStatus.value,
}) })
policies.value = data // policies.value = data
policies.value = mockPolicyList
} }
......
<template>
<div class="funding-source-container">
<div class="chart-header">
<CardTitle title="经费来源" />
<div class="header-icons">
<el-icon><Coin /></el-icon>
<el-icon><Download /></el-icon>
<el-icon><Star /></el-icon>
</div>
</div>
<div class="chart-body">
<div class="stats-panel">
<div class="stat-card total-card">
<div class="label">总计</div>
<div class="value">4.358亿美元</div>
</div>
<div class="stat-card govt-card">
<div class="label">政府部门</div>
<div class="value">3.271亿美元</div>
</div>
<div class="stat-card other-card">
<div class="label">其他机构</div>
<div class="value">1.087亿美元</div>
</div>
</div>
<div class="chart-panel" ref="chartRef"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts';
import { Coin, Download, Star } from '@element-plus/icons-vue';
import CardTitle from '@/components/CardTitle.vue';
// 1. 模拟数据
// 注意:为了还原图表,这里的数据是凑出来的近似值,确保比例看起来像截图
const chartData = [
// 右侧数据 (通常从12点顺时针开始)
{ value: 7830, name: '美国国土安全部', percent: '21%' },
{ value: 7290, name: '美国办公室国防部长和...', percent: '21%' },
{ value: 6740, name: '美国卫生与公众服务部...', percent: '18%' },
{ value: 4840, name: '美国空军', percent: '18%' },
{ value: 3880, name: '美国陆军', percent: '16%' },
{ value: 3520, name: '捐款', percent: '16%' },
// 左侧数据
{ value: 3110, name: '基金', percent: '14%' },
{ value: 2905, name: '大学', percent: '12%' },
{ value: 2840, name: '私营部门', percent: '12%' },
{ value: 2400, name: '州和地方政府机构', percent: '12%' },
{ value: 2130, name: '其他非营利组织', percent: '11%' },
{ value: 2060, name: '非美国政府机构和国际...', percent: '8%' },
{ value: 1850, name: '其他联邦机构', percent: '8%' },
{ value: 1200, name: '其他', percent: '8%' },
];
// 颜色盘 (从截图吸取的近似色)
const colorPalette = [
'#8cbbf1', // 浅蓝
'#a5d67d', // 浅绿
'#f6c469', // 橙黄
'#fdf27e', // 黄色
'#94e6d6', // 青绿
'#6b85ef', // 深蓝紫
'#d3d7fd', // 极浅蓝
'#d9f3b2', // 极浅绿
'#eb7d7d', // 红
'#a28ee3', // 紫
'#f4a678', // 橙
'#6ba7f5', // 蓝
'#f5a8a8', // 浅红
];
const chartRef = ref(null);
let myChart = null;
const initChart = () => {
if (!chartRef.value) return;
myChart = echarts.init(chartRef.value);
const option = {
color: colorPalette,
tooltip: {
trigger: 'item',
formatter: '{b}: {c}万 ({d}%)'
},
series: [
{
name: '经费来源',
type: 'pie',
radius: ['45%', '60%'], // 环形图半径
center: ['50%', '50%'], // 居中
data: chartData,
// 标签配置
label: {
show: true,
position: 'outside',
formatter: function (params) {
// 这里的逻辑是为了模仿截图:右边的文字名字在右侧,左边的文字名字在左侧
// 简单的判断逻辑:基于 ECharts 内部计算的 label 角度,或者根据数据索引
// 这里我们构建一个富文本结构
return `{name|${params.name}}\n{val|${params.value}万} {pct|${params.data.percent}}`;
},
// 关键配置:使用 edge 对齐方式让标签像表格一样排列在两侧
alignTo: 'edge',
edgeDistance: 10, // 距离容器边缘的距离
minMargin: 5,
lineHeight: 20,
rich: {
name: {
fontSize: 13,
fontWeight: 'bold',
color: '#333',
padding: [0, 5]
},
val: {
fontSize: 12,
color: '#666'
},
pct: {
fontSize: 12,
color: '#666',
padding: [0, 5]
}
}
},
// 引导线配置
labelLine: {
length: 15,
length2: 60, // 第二段线长一点,以便连接到边缘
maxSurfaceAngle: 80
},
// 每一项的样式
itemStyle: {
borderColor: '#fff',
borderWidth: 2
}
}
]
};
myChart.setOption(option);
};
// 响应式处理
const resizeHandler = () => {
if (myChart) {
myChart.resize();
}
};
onMounted(() => {
nextTick(() => {
initChart();
window.addEventListener('resize', resizeHandler);
});
});
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
if (myChart) {
myChart.dispose();
}
});
</script>
<style lang="scss" scoped>
.funding-source-container {
width: 100%;
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
// 头部样式
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.title-wrapper {
display: flex;
align-items: center;
.blue-bar {
width: 4px;
height: 18px;
background-color: #409eff; // Element Plus Primary Blue
margin-right: 8px;
border-radius: 2px;
}
.title-text {
font-size: 18px;
font-weight: 700;
color: #303133;
}
}
.header-icons {
display: flex;
gap: 15px;
color: #909399;
cursor: pointer;
.el-icon {
font-size: 18px;
&:hover {
color: #409eff;
}
}
}
}
// 主体布局
.chart-body {
display: flex;
flex-direction: row;
height: 450px; // 固定一个高度给图表展示
// 左侧统计面板
.stats-panel {
width: 200px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 20px;
flex-shrink: 0;
.stat-card {
padding: 15px;
border-radius: 6px;
display: flex;
flex-direction: column;
gap: 8px;
.label {
font-size: 14px;
}
.value {
font-size: 20px;
font-weight: 800;
}
// 不同卡片的特定样式
&.total-card {
background-color: #eef6ff; // 浅蓝背景
.label { color: #409eff; }
.value { color: #185ebd; }
}
&.govt-card {
background-color: #fff2f2; // 浅红背景
.label { color: #f56c6c; }
.value { color: #c43e3e; }
}
&.other-card {
background-color: #f0f9eb; // 浅绿背景
.label { color: #67c23a; }
.value { color: #3a8e1e; }
}
}
}
// 右侧图表区域
.chart-panel {
flex: 1;
min-width: 0; // 防止 flex 子项溢出
height: 100%;
}
}
}
// 移动端适配微调
@media (max-width: 768px) {
.chart-body {
flex-direction: column !important;
height: auto !important;
.stats-panel {
width: 100% !important;
flex-direction: row !important;
overflow-x: auto;
padding-bottom: 10px;
}
.chart-panel {
height: 400px !important;
}
}
}
</style>
\ No newline at end of file
...@@ -72,6 +72,7 @@ import { onMounted, ref } from 'vue'; ...@@ -72,6 +72,7 @@ import { onMounted, ref } from 'vue';
import { Search } from '@element-plus/icons-vue'; import { Search } from '@element-plus/icons-vue';
import { getThinkTankReport } from '@/api'; import { getThinkTankReport } from '@/api';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { mockReportList } from '../mockData';
const route = useRoute(); const route = useRoute();
const props = defineProps({ const props = defineProps({
...@@ -102,7 +103,8 @@ const reportList = ref([ ...@@ -102,7 +103,8 @@ const reportList = ref([
onMounted(() => { onMounted(() => {
getThinkTankReport({ id: route.params.id }).then(res => { getThinkTankReport({ id: route.params.id }).then(res => {
reportList.value = res.data; // reportList.value = res.data;
reportList.value = mockReportList;
}); });
}) })
</script> </script>
...@@ -145,8 +147,11 @@ onMounted(() => { ...@@ -145,8 +147,11 @@ onMounted(() => {
.report-card { .report-card {
cursor: pointer; cursor: pointer;
border-radius: 10px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
border: 1px solid #e4e7ed; border: 1px solid #e4e7ed;
padding: 10px;
} }
.report-card:hover { .report-card:hover {
...@@ -173,9 +178,9 @@ onMounted(() => { ...@@ -173,9 +178,9 @@ onMounted(() => {
/* overflow: hidden; */ /* overflow: hidden; */
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 1;
overflow: hidden;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
min-height: 42px;
} }
.card-meta { .card-meta {
......
...@@ -50,7 +50,7 @@ import PolicyTab from '@/components/PolicyTab.vue'; ...@@ -50,7 +50,7 @@ import PolicyTab from '@/components/PolicyTab.vue';
import DescTab from './DescTab.vue'; import DescTab from './DescTab.vue';
import { getThinkTankSummary } from '@/api'; import { getThinkTankSummary } from '@/api';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { mockThinkTankList } from '../mockData';
// --- Component State --- // --- Component State ---
const activeTab = ref('reports'); const activeTab = ref('reports');
const route = useRoute(); const route = useRoute();
...@@ -77,13 +77,16 @@ const currentComponent = computed(() => { ...@@ -77,13 +77,16 @@ const currentComponent = computed(() => {
onMounted(() => { onMounted(() => {
getThinkTankSummary({ id: route.params.id }).then(res => { getThinkTankSummary({ id: route.params.id }).then(res => {
console.log(res.data); console.log(res.data);
summary.value = res.data || {}; // summary.value = res.data || {};
summary.value = mockThinkTankList[0];
}); });
}); });
</script> </script>
<style scoped> <style scoped>
/* 变量 1200px - 定义在组件根元素上 */
.page-container { .page-container {
--max-width: 1650px;
background-color: #f5f7fa; background-color: #f5f7fa;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
min-height: 100vh; min-height: 100vh;
...@@ -99,7 +102,7 @@ onMounted(() => { ...@@ -99,7 +102,7 @@ onMounted(() => {
} }
.header-container { .header-container {
max-width: 1200px; max-width: var(--max-width);
margin: 0 auto; margin: 0 auto;
padding: 24px 24px 0; padding: 24px 24px 0;
display: flex; display: flex;
...@@ -116,7 +119,7 @@ onMounted(() => { ...@@ -116,7 +119,7 @@ onMounted(() => {
} }
.tabs-container { .tabs-container {
max-width: 1200px; max-width: var(--max-width);
margin: 0 auto; margin: 0 auto;
padding: 0 24px; padding: 0 24px;
} }
...@@ -212,7 +215,7 @@ onMounted(() => { ...@@ -212,7 +215,7 @@ onMounted(() => {
} }
.content-container { .content-container {
max-width: 1200px; max-width: var(--max-width);
width: 100%; width: 100%;
background-color: #fff; background-color: #fff;
padding: 24px; padding: 24px;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论