This commit is contained in:
2026-07-20 11:50:47 +08:00
parent 95d43746f6
commit 019ed28d37
401 changed files with 61622 additions and 23869 deletions
@@ -0,0 +1,344 @@
<template>
<div class="topic-search-page">
<a-card :bordered="false" class="hero-card">
<div class="hero-wrap">
<div>
<div class="hero-kicker">PROJECT TOPIC RETRIEVAL</div>
<div class="hero-title">项目主题语义检索</div>
<div class="hero-desc">
输入主题关键字后,从未来学术之星项目库中检索课题名称、课题类型和立论依据相关内容,并以卡片方式展示。
</div>
</div>
<div class="hero-search">
<a-select
v-model:value="selectedModule"
class="module-select"
size="large"
:options="moduleOptions"
@change="handleModuleChange"
/>
<a-input-search
v-model:value="keyword"
size="large"
allow-clear
placeholder="请输入项目主题关键字,例如:乡村振兴、医学科普、心理健康"
enter-button="搜索项目"
:loading="loading"
@search="handleSearch"
/>
</div>
</div>
</a-card>
<a-card :bordered="false" class="result-card">
<template #title>
<div class="result-head">
<span>检索结果</span>
<a-tag color="processing">
{{ hasSearched ? `共 ${results.length} 条` : '待搜索' }}
</a-tag>
</div>
</template>
<template #extra>
<a-button :loading="loading" @click="handleSearch">刷新</a-button>
</template>
<a-spin :spinning="loading">
<a-empty
v-if="!hasSearched && !loading"
description="输入关键字后开始检索项目主题"
/>
<a-empty
v-else-if="hasSearched && !results.length && !loading"
description="未检索到相关项目"
/>
<div v-else class="card-grid">
<div
v-for="item in results"
:key="item.id"
class="topic-card"
@click="openDetail(item)"
>
<div class="topic-card-head">
<div class="topic-tags">
<a-tag color="geekblue">{{ item.moduleName || getDeclareModuleLabel(item.module) }}</a-tag>
<a-tag color="blue">{{ item.topicType || '未分类' }}</a-tag>
</div>
<span class="score-text">相关度 {{ item.score ?? 0 }}</span>
</div>
<div class="topic-title">{{ item.topicName || '未命名项目' }}</div>
<div class="topic-snippet">{{ item.snippet || '暂无摘要' }}</div>
<div class="topic-action">点击查看详情</div>
</div>
</div>
</a-spin>
</a-card>
<a-modal
:visible="showDetail"
width="760px"
title="项目详情"
:footer="null"
@update:visible="updateDetailVisible"
>
<template v-if="current">
<div class="detail-meta">
<a-tag color="geekblue">{{ current.moduleName || getDeclareModuleLabel(current.module) }}</a-tag>
<a-tag color="blue">{{ current.topicType || '未分类' }}</a-tag>
<a-tag color="geekblue">相关度 {{ current.score ?? 0 }}</a-tag>
</div>
<div class="detail-title">{{ current.topicName || '未命名项目' }}</div>
<div class="detail-section">
<div class="detail-label">立论依据</div>
<div class="detail-content">{{ current.rationale || '暂无立论依据' }}</div>
</div>
</template>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { searchProjectTopics } from '@/api/gxmu/project-topic-semantic-search';
import type { WxxzxTopicSemanticSearchItem } from '@/api/gxmu/project-topic-semantic-search/model';
import {
DECLARE_MODULE_OPTIONS,
getDeclareModuleLabel
} from '@/views/gxmu/declare/moduleOptions';
const keyword = ref('');
const selectedModule = ref('gxmu_wxxzx_project');
const loading = ref(false);
const hasSearched = ref(false);
const results = ref<WxxzxTopicSemanticSearchItem[]>([]);
const showDetail = ref(false);
const current = ref<WxxzxTopicSemanticSearchItem>();
const moduleOptions = DECLARE_MODULE_OPTIONS.map((item) => ({
label: item.label,
value: item.value
}));
const handleSearch = async () => {
const searchValue = keyword.value.trim();
hasSearched.value = true;
if (!searchValue) {
results.value = [];
message.warning('请输入检索关键字');
return;
}
loading.value = true;
try {
results.value = await searchProjectTopics(searchValue, selectedModule.value);
} catch (e: any) {
message.error(e.message || '检索失败');
} finally {
loading.value = false;
}
};
const handleModuleChange = () => {
if (keyword.value.trim()) {
handleSearch();
} else if (hasSearched.value) {
results.value = [];
}
};
const openDetail = (item: WxxzxTopicSemanticSearchItem) => {
current.value = item;
showDetail.value = true;
};
const updateDetailVisible = (value: boolean) => {
showDetail.value = value;
};
</script>
<script lang="ts">
export default {
name: 'ProjectTopicSemanticSearch'
};
</script>
<style lang="less" scoped>
.topic-search-page {
padding: 0;
}
.hero-card,
.result-card {
border-radius: 20px;
overflow: hidden;
}
.hero-card {
margin-bottom: 20px;
background:
radial-gradient(circle at top left, rgba(22, 119, 255, 0.18), transparent 34%),
linear-gradient(135deg, #f3f8ff 0%, #ffffff 52%, #f7fbf7 100%);
}
.hero-wrap {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 520px);
gap: 24px;
align-items: center;
}
.hero-kicker {
font-size: 12px;
letter-spacing: 0.24em;
color: #1677ff;
font-weight: 700;
}
.hero-title {
margin-top: 12px;
font-size: 34px;
line-height: 1.2;
font-weight: 800;
color: #1f2937;
}
.hero-desc {
margin-top: 12px;
max-width: 760px;
line-height: 1.85;
color: rgba(31, 41, 55, 0.78);
}
.hero-search {
display: grid;
gap: 12px;
padding: 18px;
border: 1px solid rgba(22, 119, 255, 0.12);
border-radius: 18px;
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 18px 44px rgba(31, 41, 55, 0.08);
}
.module-select {
width: 100%;
}
.result-head {
display: flex;
align-items: center;
gap: 10px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.topic-card {
padding: 18px;
border: 1px solid #e5edf7;
border-radius: 18px;
background:
linear-gradient(180deg, rgba(243, 248, 255, 0.92), #ffffff 72%),
#ffffff;
cursor: pointer;
transition:
transform 0.2s ease,
box-shadow 0.2s ease,
border-color 0.2s ease;
}
.topic-card:hover {
transform: translateY(-4px);
border-color: rgba(22, 119, 255, 0.28);
box-shadow: 0 16px 32px rgba(22, 119, 255, 0.12);
}
.topic-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.topic-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-width: 0;
}
.score-text {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.topic-title {
margin-top: 14px;
font-size: 18px;
line-height: 1.5;
font-weight: 700;
color: #111827;
}
.topic-snippet {
margin-top: 12px;
min-height: 72px;
line-height: 1.8;
color: rgba(17, 24, 39, 0.74);
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.topic-action {
margin-top: 16px;
color: #1677ff;
font-weight: 600;
}
.detail-meta {
margin-bottom: 12px;
}
.detail-title {
font-size: 24px;
font-weight: 700;
line-height: 1.5;
color: #111827;
}
.detail-section {
margin-top: 20px;
padding: 18px;
border-radius: 16px;
background: #f8fafc;
}
.detail-label {
margin-bottom: 10px;
font-size: 14px;
font-weight: 700;
color: #1677ff;
}
.detail-content {
line-height: 1.9;
color: rgba(17, 24, 39, 0.82);
white-space: pre-wrap;
word-break: break-word;
}
@media (max-width: 991px) {
.hero-wrap {
grid-template-columns: 1fr;
}
.hero-title {
font-size: 28px;
}
}
</style>