260720
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user