feat: 搜索页开发

This commit is contained in:
liulujian 2026-04-22 19:11:03 +08:00
parent 78a531f7d1
commit f584a9fec2
13 changed files with 1069 additions and 148 deletions

View File

@ -26,7 +26,8 @@
"Bash(sed -i 's/com\\\\.css\\\\.txw\\\\.gxzx\\\\.pojo\\\\.vo\\\\.lsjr/com.css.txw.mhzc.pojo.vo.gxzx.lsjr/g' \"E:/00项目/T_碳信网/code/txw/txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/gxzx/lsjr/ProductApplyVO.java\")", "Bash(sed -i 's/com\\\\.css\\\\.txw\\\\.gxzx\\\\.pojo\\\\.vo\\\\.lsjr/com.css.txw.mhzc.pojo.vo.gxzx.lsjr/g' \"E:/00项目/T_碳信网/code/txw/txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/gxzx/lsjr/ProductApplyVO.java\")",
"Bash(\"/d/Program Files/apache-maven/apache-maven-3.6.3/bin/mvn\" compile -pl txw-mhzc-service-biz -am)", "Bash(\"/d/Program Files/apache-maven/apache-maven-3.6.3/bin/mvn\" compile -pl txw-mhzc-service-biz -am)",
"Bash(\"/d/Program Files/apache-maven/apache-maven-3.6.3/bin/mvn\" clean package -pl txw-mhzc-service-biz -am -DskipTests)", "Bash(\"/d/Program Files/apache-maven/apache-maven-3.6.3/bin/mvn\" clean package -pl txw-mhzc-service-biz -am -DskipTests)",
"Bash(\"/d/Program Files/jdk8/bin/java\" -Xms256m -Xmx512m -Duser.timezone=Asia/Shanghai -jar target/txw-mhzc-service-biz.jar --spring.profiles.active=local)" "Bash(\"/d/Program Files/jdk8/bin/java\" -Xms256m -Xmx512m -Duser.timezone=Asia/Shanghai -jar target/txw-mhzc-service-biz.jar --spring.profiles.active=local)",
"Bash(mvn compile *)"
] ]
} }
} }

View File

@ -43,6 +43,7 @@ export default {
methods: { methods: {
handleClick() { handleClick() {
if (this.result.url) { if (this.result.url) {
// URLid
window.location.href = this.result.url; window.location.href = this.result.url;
} }
}, },

View File

@ -94,6 +94,7 @@
<div <div
v-for="(card, index) in cardList" v-for="(card, index) in cardList"
:key="card.gxUuid" :key="card.gxUuid"
:data-gx-uuid="card.gxUuid"
class="service-card" class="service-card"
> >
<!-- 卡片头部 --> <!-- 卡片头部 -->
@ -292,6 +293,12 @@ export default {
if (this.$route.query.publish === '1') { if (this.$route.query.publish === '1') {
this.handlePublish(); this.handlePublish();
} }
// URLid
if (this.$route.query.id) {
this.$nextTick(() => {
this.scrollToItem(this.$route.query.id);
});
}
}, },
methods: { methods: {
// //
@ -419,11 +426,24 @@ export default {
this.page.pageNo = 1; this.page.pageNo = 1;
this.searchList(); this.searchList();
}, },
// //
onPageChange(pageInfo) { scrollToItem(gxUuid) {
this.page.pageNo = pageInfo.current; this.$nextTick(() => {
this.page.pageSize = pageInfo.pageSize; const targetCard = this.cardList.find(card => card.gxUuid === gxUuid);
this.searchList(); if (targetCard) {
//
const cardIndex = this.cardList.indexOf(targetCard);
const cardElement = document.querySelector(`[data-gx-uuid="${gxUuid}"]`);
if (cardElement) {
cardElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
//
cardElement.classList.add('highlight-card');
setTimeout(() => {
cardElement.classList.remove('highlight-card');
}, 3000);
}
}
});
}, },
// //
handlePublish() { handlePublish() {
@ -804,6 +824,10 @@ export default {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease; transition: all 0.3s ease;
&.highlight-card {
animation: highlight-pulse 3s ease-out;
}
&::before { &::before {
position: absolute; position: absolute;
top: 0; top: 0;
@ -1124,4 +1148,16 @@ export default {
font-size: 18px; font-size: 18px;
} }
} }
@keyframes highlight-pulse {
0% {
box-shadow: 0 0 0 0 rgba(0, 154, 41, 0.4);
}
50% {
box-shadow: 0 0 20px 10px rgba(0, 154, 41, 0.2);
}
100% {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
}
</style> </style>

View File

@ -86,6 +86,7 @@
<div <div
v-for="card in cardList" v-for="card in cardList"
:key="card.gxUuid" :key="card.gxUuid"
:data-gx-uuid="card.gxUuid"
class="demand-card" class="demand-card"
> >
<!-- 卡片头部 --> <!-- 卡片头部 -->
@ -261,6 +262,12 @@ export default {
if (this.$route.query.publish === '1') { if (this.$route.query.publish === '1') {
this.handlePublish(); this.handlePublish();
} }
// URLid
if (this.$route.query.id) {
this.$nextTick(() => {
this.scrollToItem(this.$route.query.id);
});
}
}, },
methods: { methods: {
// //
@ -345,11 +352,21 @@ export default {
this.page.pageNo = 1; this.page.pageNo = 1;
this.searchList(); this.searchList();
}, },
// //
onPageChange(pageInfo) { scrollToItem(gxUuid) {
this.page.pageNo = pageInfo.current; this.$nextTick(() => {
this.page.pageSize = pageInfo.pageSize; const targetCard = this.cardList.find(card => card.gxUuid === gxUuid);
this.searchList(); if (targetCard) {
const cardElement = document.querySelector(`[data-gx-uuid="${gxUuid}"]`);
if (cardElement) {
cardElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
cardElement.classList.add('highlight-card');
setTimeout(() => {
cardElement.classList.remove('highlight-card');
}, 3000);
}
}
});
}, },
// //
handlePublish() { handlePublish() {
@ -705,6 +722,10 @@ export default {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease; transition: all 0.3s ease;
&.highlight-card {
animation: highlight-pulse 3s ease-out;
}
&::before { &::before {
position: absolute; position: absolute;
top: 0; top: 0;
@ -1028,4 +1049,16 @@ export default {
font-size: 18px; font-size: 18px;
} }
} }
@keyframes highlight-pulse {
0% {
box-shadow: 0 0 0 0 rgba(0, 154, 41, 0.4);
}
50% {
box-shadow: 0 0 20px 10px rgba(0, 154, 41, 0.2);
}
100% {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
}
</style> </style>

View File

@ -48,16 +48,12 @@
<div class="top-search-box"> <div class="top-search-box">
<div class="search-box"> <div class="search-box">
<input type="text" v-model="inputValue" placeholder="搜索碳资产、企业、服务" /> <input type="text" v-model="inputValue" placeholder="搜索碳资产、企业、服务" />
<div class="search-btn"> <div class="search-btn" @click="handleSearch">
搜索 搜索
</div> </div>
</div> </div>
<div class="top-search-hot"> <div class="top-search-hot">
<div class="hot-tag">电厂配额</div> <div class="hot-tag" v-for="(tag, index) in hotSearchTags" :key="index" @click="handleHotSearch(tag)">{{ tag }}</div>
<div class="hot-tag">林业碳汇开发</div>
<div class="hot-tag">CBAM 报告</div>
<div class="hot-tag">2025年度碳报告</div>
<div class="hot-tag">光伏发展</div>
</div> </div>
</div> </div>
@ -359,7 +355,7 @@ export default {
icon: require('@/pages/index/views/home2/assets/closed-loop@2x.png') icon: require('@/pages/index/views/home2/assets/closed-loop@2x.png')
} }
], ],
inputValue: "", hotSearchTags: ["碳核查", "ESG", "碳资产管理", "ISO 14067"],
// //
overseas2List: [ overseas2List: [
{ name: '电池法案', btnName: "申请服务", desc: "欧盟电池法案管控电池全生命周期,涉及回收、碳足迹等要求。", icon: require('@/pages/index/assets/home-dcfa-icon.png') }, { name: '电池法案', btnName: "申请服务", desc: "欧盟电池法案管控电池全生命周期,涉及回收、碳足迹等要求。", icon: require('@/pages/index/assets/home-dcfa-icon.png') },
@ -514,14 +510,12 @@ export default {
// //
handleSearch() { handleSearch() {
if (this.inputValue) { if (this.inputValue) {
console.log('搜索:', this.inputValue); this.$router.push({ path: '/search', query: { keyword: this.inputValue } });
// TODO:
} }
}, },
// //
handleHotSearch(keyword) { handleHotSearch(keyword) {
this.inputValue = keyword; this.$router.push({ path: '/search', query: { keyword: keyword } });
this.handleSearch();
}, },
// //
handleOverseasClick(item) { handleOverseasClick(item) {
@ -799,6 +793,14 @@ export default {
padding: 4px 12px; padding: 4px 12px;
color: #333; color: #333;
font-size: 14px; font-size: 14px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: rgba(0, 185, 107, 0.4);
transform: translateY(-2px);
color: #fff;
}
} }
.top-box-bottom-over { .top-box-bottom-over {

View File

@ -228,6 +228,7 @@ export default {
} }
let params={ let params={
captchaVerification:this.loginForm.captchaVerification, captchaVerification:this.loginForm.captchaVerification,
captchaCode:this.loginForm.captchaCode,
sjhm1:this.loginForm.dlzh, sjhm1:this.loginForm.dlzh,
} }
sendMsg(params).then((res) => { sendMsg(params).then((res) => {

View File

@ -20,19 +20,6 @@
</template> </template>
</t-input> </t-input>
</t-form-item> </t-form-item>
<t-form-item class="verification-code" name="sms">
<t-input v-model="loginForm.sms" size="large" placeholder="请输入短信验证码" key="verifyCode" />
<t-button :disabled="countDown > 0" @click="handleCounter">
{{ countDown === 0 ? '发送验证码' : `${countDown}秒后可重发` }}
</t-button>
</t-form-item>
<!-- 滑块验证区域已注释保留以便回滚 -->
<!--
<t-form-item name="captchaVerification" user-select:none>
<div class="drag" ref="dragDiv" v-if="iscxhk">...</div>
<div class="drag" ref="dragDiv" v-else>...</div>
</t-form-item>
-->
<!-- 新增图形验证码 --> <!-- 新增图形验证码 -->
<t-form-item name="captchaCode"> <t-form-item name="captchaCode">
@ -52,6 +39,20 @@
/> />
</div> </div>
</t-form-item> </t-form-item>
<t-form-item class="verification-code" name="sms">
<t-input v-model="loginForm.sms" size="large" placeholder="请输入短信验证码" key="verifyCode" />
<t-button :disabled="countDown > 0" @click="handleCounter">
{{ countDown === 0 ? '发送验证码' : `${countDown}秒后可重发` }}
</t-button>
</t-form-item>
<!-- 滑块验证区域已注释保留以便回滚 -->
<!--
<t-form-item name="captchaVerification" user-select:none>
<div class="drag" ref="dragDiv" v-if="iscxhk">...</div>
<div class="drag" ref="dragDiv" v-else>...</div>
</t-form-item>
-->
</div> </div>
<t-form-item class="btn-container"> <t-form-item class="btn-container">
<t-button class="btn-container" block size="large" type="submit"> 登录</t-button> <t-button class="btn-container" block size="large" type="submit"> 登录</t-button>
@ -176,6 +177,7 @@ handleCounter() {
this.startCountDown(); this.startCountDown();
let params = { let params = {
captchaVerification: this.loginForm.captchaVerification, captchaVerification: this.loginForm.captchaVerification,
captchaCode: this.loginForm.captchaCode,
sjhm1: this.loginForm.sjhm, sjhm1: this.loginForm.sjhm,
} }
sendMsg(params).then((res) => { sendMsg(params).then((res) => {

View File

@ -1,63 +1,191 @@
<template> <template>
<div class="search-page"> <div class="search-page">
<!-- 顶部导航 (复用现有组件) --> <!-- 搜索区域背景 -->
<Nav :isyhxx="false" /> <div class="search-hero">
<div class="hero-bg-overlay"></div>
<div class="hero-content">
<h1 class="hero-title">搜索碳资产企业服务</h1>
<p class="hero-subtitle">一站式碳信息搜索平台快速定位所需内容</p>
<!-- 搜索区域 --> <!-- 搜索框 -->
<SearchArea @search="handleSearch" /> <div class="search-box">
<div class="search-input-wrapper">
<!-- 搜索历史 --> <t-select
<SearchHistoryBar v-model="selectedCategory"
:historyList="searchHistory" :options="categoryOptions"
@select="handleHistorySelect" placeholder="全部分类"
@clear="handleClearHistory" @change="handleCategoryChange"
class="category-select"
/> />
<t-input
v-model="keyword"
placeholder="输入关键词搜索..."
@enter="handleSearch"
@input="handleInput"
class="keyword-input"
>
<template #suffix-icon>
<t-icon name="search" class="search-icon" />
</template>
</t-input>
</div>
<t-button theme="primary" class="search-btn" @click="handleSearch">
搜索
</t-button>
</div>
<!-- 热门搜索 -->
<div class="hot-search" v-if="hotSearchList.length > 0">
<span class="hot-label">热门</span>
<t-tag
v-for="(item, index) in hotSearchList"
:key="index"
class="hot-item"
@click="handleHotClick(item)"
>
{{ item }}
</t-tag>
</div>
</div>
</div>
<!-- 搜索结果区域 -->
<div class="search-results-wrapper">
<!-- 搜索历史 -->
<div class="history-section" v-if="searchHistory.length > 0 && !keyword">
<div class="section-header">
<span class="section-title">
<i class="title-icon"></i>
搜索历史
</span>
<span class="clear-btn" @click="handleClearHistory">清空</span>
</div>
<div class="history-tags">
<t-tag
v-for="(item, index) in searchHistory"
:key="index"
class="history-tag"
variant="light"
@click="handleHistorySelect(item)"
>
<template #icon>
<t-icon name="history" class="tag-icon" />
</template>
{{ item }}
</t-tag>
</div>
</div>
<!-- 分类 Tab --> <!-- 分类 Tab -->
<SearchCategoryTabs <div class="category-tabs" v-if="keyword || resultList.length > 0">
:currentTab="currentCategory" <div class="tabs-header">
:categoryCount="categoryCount" <span class="tabs-title">
@change="handleCategoryChange" <i class="title-icon"></i>
/> 搜索结果
<span class="result-count" v-if="total > 0"> {{ total }} </span>
</span>
<div class="tabs-wrapper">
<div
v-for="tab in categoryTabs"
:key="tab.value"
:class="['tab-item', { active: currentCategory === tab.value }]"
@click="handleCategoryChange(tab.value)"
>
<span class="tab-name">{{ tab.label }}</span>
<span class="tab-count" v-if="categoryCount[tab.value]">{{ categoryCount[tab.value] }}</span>
</div>
</div>
</div>
</div>
<!-- 搜索结果列表 --> <!-- 搜索结果列表 -->
<SearchResultList <div class="results-container" v-if="keyword">
:list="resultList" <!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<t-loading size="large" text="搜索中..." />
</div>
<!-- 空状态 -->
<div v-else-if="resultList.length === 0" class="empty-state">
<div class="empty-icon">
<t-icon name="search" />
</div>
<p class="empty-text">未找到相关结果</p>
<p class="empty-hint">换个关键词试试或浏览热门内容</p>
</div>
<!-- 结果列表 -->
<div v-else class="result-list">
<div
v-for="item in resultList"
:key="item.id"
class="result-item"
@click="handleResultClick(item)"
>
<div class="result-header">
<t-tag :style="getCategoryStyle(item.categoryType)" size="small">
{{ item.category }}
</t-tag>
<span class="result-date" v-if="item.publishTime">{{ formatDate(item.publishTime) }}</span>
</div>
<h3 class="result-title" v-html="item.title"></h3>
<p class="result-summary" v-html="item.summary"></p>
<div class="result-footer">
<span class="result-source" v-if="item.source">
<t-icon name="enterprise" class="source-icon" />
{{ item.source }}
</span>
<t-link theme="primary" class="result-link" @click="handleResultClick(item)">
查看详情
<template #suffixIcon>
<t-icon name="arrow-right" class="link-icon" />
</template>
</t-link>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="total > pageSize">
<t-pagination
v-model="currentPage"
:total="total" :total="total"
:pageSize="pageSize" :page-size="pageSize"
:loading="loading" @change="handlePageChange"
:keyword="keyword"
@page-change="handlePageChange"
/> />
</div>
</div>
</div>
<!-- 底部版权 --> <!-- 底部版权 -->
<div class="footer"> <Footer />
<p>版权信息等</p>
</div>
</div> </div>
</template> </template>
<script> <script>
import Nav from '@/pages/index/components/nav/index.vue'; import Footer from '@/pages/index/components/footer/index.vue';
import SearchArea from '@/pages/index/components/search/SearchArea.vue';
import SearchHistoryBar from '@/pages/index/components/search/SearchHistoryBar.vue';
import SearchCategoryTabs from '@/pages/index/components/search/SearchCategoryTabs.vue';
import SearchResultList from '@/pages/index/components/search/SearchResultList.vue';
import searchApi from '@/pages/index/api/search.js'; import searchApi from '@/pages/index/api/search.js';
export default { export default {
name: 'SearchPage', name: 'SearchPage',
components: { components: {
Nav, Footer,
SearchArea,
SearchHistoryBar,
SearchCategoryTabs,
SearchResultList,
}, },
data() { data() {
return { return {
keyword: '', keyword: '',
currentCategory: 'all', selectedCategory: 'all',
categoryOptions: [
{ label: '全部分类', value: 'all' },
{ label: '碳证中心', value: 'carbon_cert' },
{ label: '服务中心', value: 'service' },
{ label: '行业专题', value: 'news' },
],
categoryTabs: [
{ label: '全部', value: 'all' },
{ label: '行业专题', value: 'news' },
{ label: '服务中心', value: 'service' },
],
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,
resultList: [], resultList: [],
@ -65,35 +193,67 @@ export default {
categoryCount: {}, categoryCount: {},
loading: false, loading: false,
searchHistory: [], searchHistory: [],
hotSearchList: [],
suggestions: [],
showSuggestion: false,
inputTimer: null,
}; };
}, },
mounted() { mounted() {
this.keyword = this.$route.query.keyword || ''; this.keyword = this.$route.query.keyword || '';
this.currentCategory = this.$route.query.category || 'all'; this.currentCategory = this.$route.query.category || 'all';
this.loadSearchHistory(); this.loadSearchHistory();
this.loadHotSearch();
if (this.keyword) { if (this.keyword) {
this.doSearch(); this.doSearch();
} }
}, },
methods: { methods: {
handleSearch(params) { handleSearch() {
this.keyword = params.keyword; if (!this.keyword.trim()) {
this.currentCategory = params.categoryType; this.$message.warning('请输入搜索关键词');
return;
}
this.showSuggestion = false;
this.currentCategory = this.selectedCategory;
this.currentPage = 1; this.currentPage = 1;
this.$router.push({ this.$router.push({
query: { query: {
keyword: this.keyword, keyword: this.keyword,
category: this.currentCategory, category: this.currentCategory,
}, },
}); });
this.saveSearchHistory(this.keyword); this.saveSearchHistory(this.keyword);
this.doSearch(); this.doSearch();
}, },
handleInput(value) {
if (this.inputTimer) {
clearTimeout(this.inputTimer);
}
if (!value.trim()) {
this.showSuggestion = false;
return;
}
this.inputTimer = setTimeout(() => {
this.loadSuggestions(value);
}, 300);
},
handleCategoryChange(value) {
this.currentCategory = value || this.selectedCategory;
this.currentPage = 1;
this.$router.push({
query: {
keyword: this.keyword,
category: this.currentCategory,
},
});
this.doSearch();
},
handlePageChange(pageInfo) {
this.currentPage = pageInfo.current;
this.doSearch();
window.scrollTo(0, 0);
},
handleHistorySelect(keyword) { handleHistorySelect(keyword) {
this.keyword = keyword; this.keyword = keyword;
this.doSearch(); this.doSearch();
@ -102,23 +262,14 @@ export default {
this.searchHistory = []; this.searchHistory = [];
searchApi.clearSearchHistory(); searchApi.clearSearchHistory();
}, },
handleCategoryChange(category) { handleHotClick(keyword) {
this.currentCategory = category; this.keyword = keyword;
this.currentPage = 1; this.handleSearch();
this.$router.push({
query: {
keyword: this.keyword,
category: this.currentCategory,
}, },
}); handleResultClick(item) {
if (item.url) {
this.doSearch(); window.location.href = item.url;
}, }
handlePageChange(page) {
this.currentPage = page;
this.doSearch();
window.scrollTo(0, 0);
}, },
async doSearch() { async doSearch() {
this.loading = true; this.loading = true;
@ -129,7 +280,6 @@ export default {
page: this.currentPage, page: this.currentPage,
pageSize: this.pageSize, pageSize: this.pageSize,
}); });
if (res && res.data) { if (res && res.data) {
this.resultList = res.data.list || []; this.resultList = res.data.list || [];
this.total = res.data.total || 0; this.total = res.data.total || 0;
@ -155,21 +305,55 @@ export default {
}, },
saveSearchHistory(keyword) { saveSearchHistory(keyword) {
if (!keyword) return; if (!keyword) return;
this.searchHistory = this.searchHistory.filter((k) => k !== keyword); this.searchHistory = this.searchHistory.filter((k) => k !== keyword);
this.searchHistory.unshift(keyword); this.searchHistory.unshift(keyword);
if (this.searchHistory.length > 10) { if (this.searchHistory.length > 10) {
this.searchHistory = this.searchHistory.slice(0, 10); this.searchHistory = this.searchHistory.slice(0, 10);
} }
try { try {
localStorage.setItem('searchHistory', JSON.stringify(this.searchHistory)); localStorage.setItem('searchHistory', JSON.stringify(this.searchHistory));
} catch (e) { } catch (e) {
console.error('保存搜索历史失败', e); console.error('保存搜索历史失败', e);
} }
}, },
async loadHotSearch() {
try {
const res = await searchApi.getHotSearch();
if (res && res.data) {
this.hotSearchList = res.data;
}
} catch (error) {
console.error('加载热门搜索失败', error);
}
},
async loadSuggestions(keyword) {
try {
const res = await searchApi.getSuggest(keyword);
if (res && res.data && res.data.suggestions) {
this.suggestions = res.data.suggestions;
this.showSuggestion = this.suggestions.length > 0;
}
} catch (error) {
console.error('加载搜索建议失败', error);
}
},
getCategoryStyle(type) {
if (type === 'news') {
return { theme: 'primary', variant: 'light' };
} else if (type === 'service') {
return { theme: 'success', variant: 'light' };
}
return { theme: 'default', variant: 'light' };
},
formatDate(dateStr) {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
} catch {
return dateStr;
}
},
}, },
}; };
</script> </script>
@ -177,19 +361,515 @@ export default {
<style scoped lang="less"> <style scoped lang="less">
.search-page { .search-page {
min-height: 100vh; min-height: 100vh;
background-color: #fff; background-color: #f5f7fa;
} }
.footer { //
background-color: #f5f5f5; .search-hero {
padding: 24px 0; position: relative;
text-align: center; background: linear-gradient(135deg, #007242 0%, #009a29 50%, #00b96b 100%);
margin-top: 40px; padding: 80px 20px 60px;
overflow: hidden;
p { &::before {
margin: 0; content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('~@/pages/index/assets/home-top-bg1.jpg') center/cover no-repeat;
opacity: 0.3;
}
}
.hero-bg-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(0, 114, 66, 0.9) 0%, rgba(0, 154, 41, 0.85) 100%);
}
.hero-content {
position: relative;
z-index: 1;
max-width: 800px;
margin: 0 auto;
text-align: center;
}
.hero-title {
font-size: 36px;
font-weight: 600;
color: #ffffff;
margin: 0 0 12px;
line-height: 1.4;
}
.hero-subtitle {
font-size: 16px;
color: rgba(255, 255, 255, 0.85);
margin: 0 0 32px;
}
//
.search-box {
display: flex;
align-items: stretch;
gap: 0;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.search-input-wrapper {
display: flex;
flex: 1;
gap: 0;
border-radius: 6px;
overflow: hidden;
}
.category-select {
width: 130px;
flex-shrink: 0;
border: none !important;
border-radius: 6px 0 0 6px !important;
background: #f5f7fa !important;
::v-deep .t-input__inner {
text-align: center !important;
background-color: none !important;
}
::v-deep .t-select__wrap {
border: none !important;
border-radius: 6px 0 0 6px !important;
background: transparent !important;
}
::v-deep .t-input {
border: none !important;
border-radius: 6px 0 0 6px !important;
background: #f5f7fa !important;
text-align: center !important;
}
::v-deep .t-input__value {
text-align: center !important;
justify-content: center !important;
}
}
.keyword-input {
flex: 1;
border: none !important;
border-radius: 0 !important;
background: #fff !important;
::v-deep .t-input {
border: none !important;
border-radius: 0 !important;
background: transparent !important;
height: 100%;
}
}
.search-btn {
flex-shrink: 0;
border-radius: 6px !important;
font-size: 15px;
font-weight: 500;
}
//
.hot-search {
margin-top: 16px;
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
.hot-label {
margin-right: 4px;
opacity: 0.9;
}
.hot-item {
cursor: pointer;
transition: all 0.3s;
background: rgba(255, 255, 255, 0.15) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
color: #fff !important;
&:hover {
background: rgba(0, 185, 107, 0.4) !important;
border-color: rgba(0, 185, 107, 0.6) !important;
transform: translateY(-2px);
}
}
}
//
.search-results-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 24px 20px;
}
//
.history-section {
background: #fff;
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #333;
.title-icon {
width: 4px;
height: 16px;
background: linear-gradient(180deg, #00b42a, #00d468);
border-radius: 2px;
}
}
.clear-btn {
font-size: 13px;
color: #999;
cursor: pointer;
&:hover {
color: #666;
}
}
.history-tags {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.history-tag {
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #e8f5e9 !important;
color: #009a29 !important;
border-color: #009a29 !important;
}
.tag-icon {
font-size: 14px;
opacity: 0.6;
}
}
//
.category-tabs {
background: #fff;
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.tabs-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
}
.tabs-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #333;
.title-icon {
width: 4px;
height: 16px;
background: linear-gradient(180deg, #00b42a, #00d468);
border-radius: 2px;
}
.result-count {
font-size: 13px;
font-weight: 400;
color: #999;
margin-left: 8px;
}
}
.tabs-wrapper {
display: flex;
gap: 8px;
}
.tab-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 20px;
background: #f5f7fa;
border-radius: 20px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #e8f5e9;
color: #009a29;
}
&.active {
background: linear-gradient(135deg, #009a29, #00b96b);
color: #fff;
box-shadow: 0 4px 12px rgba(0, 154, 41, 0.3);
}
.tab-count {
padding: 2px 8px;
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
font-size: 12px;
}
&.active .tab-count {
background: rgba(255, 255, 255, 0.25);
}
}
//
.results-container {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.loading-state {
padding: 60px 0;
text-align: center;
}
.empty-state {
padding: 60px 0;
text-align: center;
.empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 16px;
background: #f5f7fa;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.t-icon {
font-size: 36px;
color: #ccc;
}
}
.empty-text {
font-size: 18px;
font-weight: 500;
color: #333;
margin: 0 0 8px;
}
.empty-hint {
font-size: 14px; font-size: 14px;
color: #999; color: #999;
margin: 0;
}
}
.result-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.result-item {
padding: 20px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #00b42a;
box-shadow: 0 4px 16px rgba(0, 154, 41, 0.12);
transform: translateY(-2px);
}
}
.result-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.result-category {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.result-date {
font-size: 13px;
color: #999;
}
.result-title {
font-size: 18px;
font-weight: 600;
color: #222;
margin: 0 0 12px;
line-height: 1.5;
::v-deep em {
color: #00b42a;
font-style: normal;
}
}
.result-summary {
font-size: 14px;
color: #666;
line-height: 1.8;
margin: 0 0 16px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
::v-deep em {
color: #00b42a;
font-style: normal;
}
}
.result-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.result-source {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #999;
.source-icon {
font-size: 14px;
}
}
.result-link {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
.link-icon {
font-size: 12px;
transition: transform 0.3s;
}
}
.result-item:hover .result-link .link-icon {
transform: translateX(4px);
}
//
.pagination-wrapper {
margin-top: 32px;
display: flex;
justify-content: center;
}
//
@media (max-width: 768px) {
.search-hero {
padding: 60px 16px 40px;
}
.hero-title {
font-size: 24px;
}
.hero-subtitle {
font-size: 14px;
}
.search-box {
flex-direction: column;
gap: 8px;
}
.search-input-wrapper {
flex-direction: column;
}
.category-select {
width: 100%;
}
.search-btn {
width: 100%;
}
.tabs-wrapper {
overflow-x: auto;
padding-bottom: 8px;
}
.tab-item {
flex-shrink: 0;
}
.result-item {
padding: 16px;
}
.result-title {
font-size: 16px;
} }
} }
</style> </style>

View File

@ -30,16 +30,11 @@ public class SearchController {
@Operation(summary = "搜索", description = "全站搜索") @Operation(summary = "搜索", description = "全站搜索")
public CommonResult<Map<String, Object>> search(@Valid SearchReqVO reqVO) { public CommonResult<Map<String, Object>> search(@Valid SearchReqVO reqVO) {
List<SearchResultVO> list = searchService.search(reqVO); List<SearchResultVO> list = searchService.search(reqVO);
Map<String, Integer> categoryCount = searchService.getCategoryCount(reqVO.getKeyword());
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("total", list.size()); result.put("total", list.size());
result.put("list", list); result.put("list", list);
Map<String, Integer> categoryCount = new HashMap<>();
categoryCount.put("all", list.size());
categoryCount.put("carbon_cert", 0);
categoryCount.put("service", 0);
categoryCount.put("news", list.size());
result.put("categoryCount", categoryCount); result.put("categoryCount", categoryCount);
return CommonResult.success(result); return CommonResult.success(result);

View File

@ -3,6 +3,7 @@ package com.css.txw.mhzc.service;
import com.css.txw.mhzc.pojo.req.SearchReqVO; import com.css.txw.mhzc.pojo.req.SearchReqVO;
import com.css.txw.mhzc.pojo.vo.SearchResultVO; import com.css.txw.mhzc.pojo.vo.SearchResultVO;
import java.util.List; import java.util.List;
import java.util.Map;
public interface SearchService { public interface SearchService {
@ -11,6 +12,11 @@ public interface SearchService {
*/ */
List<SearchResultVO> search(SearchReqVO reqVO); List<SearchResultVO> search(SearchReqVO reqVO);
/**
* 获取搜索结果分类统计
*/
Map<String, Integer> getCategoryCount(String keyword);
/** /**
* 获取热门搜索词 * 获取热门搜索词
*/ */

View File

@ -1,6 +1,10 @@
package com.css.txw.mhzc.service.impl; package com.css.txw.mhzc.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.css.txw.mhzc.mapper.TxwGxzxGxxxbMapper;
import com.css.txw.mhzc.pojo.domain.TxwGxzxGxxxbDO;
import com.css.txw.mhzc.pojo.req.SearchReqVO; import com.css.txw.mhzc.pojo.req.SearchReqVO;
import com.css.txw.mhzc.pojo.vo.GxxxVO;
import com.css.txw.mhzc.pojo.vo.SearchResultVO; import com.css.txw.mhzc.pojo.vo.SearchResultVO;
import com.css.txw.mhzc.pojo.vo.SyzxxxVO; import com.css.txw.mhzc.pojo.vo.SyzxxxVO;
import com.css.txw.mhzc.service.SearchService; import com.css.txw.mhzc.service.SearchService;
@ -12,6 +16,7 @@ import org.springframework.util.StringUtils;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -23,15 +28,23 @@ public class SearchServiceImpl implements SearchService {
@Resource @Resource
private TxwMhzcZxxxbService zxxxbService; private TxwMhzcZxxxbService zxxxbService;
@Resource
private TxwGxzxGxxxbMapper gxxxbMapper;
private static final String HIGHLIGHT_START = "<em>"; private static final String HIGHLIGHT_START = "<em>";
private static final String HIGHLIGHT_END = "</em>"; private static final String HIGHLIGHT_END = "</em>";
private static final String VIEW_MHZC_PREFIX = "/view/mhzc";
private static final String URL_PARAM_KEYWORD = "keyword";
@Override @Override
public List<SearchResultVO> search(SearchReqVO reqVO) { public List<SearchResultVO> search(SearchReqVO reqVO) {
Map<String, List<SyzxxxVO>> data = zxxxbService.zxxx();
List<SearchResultVO> resultList = new ArrayList<>(); List<SearchResultVO> resultList = new ArrayList<>();
String categoryType = reqVO.getCategoryType(); String categoryType = reqVO.getCategoryType();
String keyword = reqVO.getKeyword();
// 搜索资讯信息
if (!"service".equals(categoryType)) {
Map<String, List<SyzxxxVO>> data = zxxxbService.zxxx();
if (data != null) { if (data != null) {
data.forEach((category, list) -> { data.forEach((category, list) -> {
String currentCategoryType = getCategoryType(category); String currentCategoryType = getCategoryType(category);
@ -40,26 +53,38 @@ public class SearchServiceImpl implements SearchService {
} }
list.forEach(zxxx -> { list.forEach(zxxx -> {
if (matchKeyword(zxxx.getBt1(), reqVO.getKeyword()) || if (matchKeyword(zxxx.getBt1(), keyword) ||
matchKeyword(zxxx.getZxNr(), reqVO.getKeyword())) { matchKeyword(zxxx.getZxNr(), keyword)) {
SearchResultVO vo = new SearchResultVO(); SearchResultVO vo = new SearchResultVO();
vo.setId(zxxx.getUuid()); vo.setId(zxxx.getUuid());
vo.setTitle(highlightKeyword(zxxx.getBt1(), reqVO.getKeyword())); vo.setTitle(highlightKeyword(zxxx.getBt1(), keyword));
vo.setSummary(highlightKeyword(truncate(zxxx.getZxNr(), 200), reqVO.getKeyword())); vo.setSummary(highlightKeyword(truncate(zxxx.getZxNr(), 200), keyword));
vo.setCategory(category); vo.setCategory(category);
vo.setCategoryType(currentCategoryType); vo.setCategoryType(currentCategoryType);
vo.setSource("碳信网"); vo.setSource("碳信网");
vo.setSourceType("news"); vo.setSourceType("news");
vo.setPublishTime(zxxx.getFbsj() != null ? zxxx.getFbsj().toString() : null); vo.setPublishTime(zxxx.getFbsj() != null ? zxxx.getFbsj().toString() : null);
vo.setUrl("/mhzc/news/" + zxxx.getUuid()); vo.setUrl(VIEW_MHZC_PREFIX + "/zx?id=" + zxxx.getUuid());
resultList.add(vo); resultList.add(vo);
} }
}); });
}); });
} }
}
// 搜索服务中心(供需信息)
if (!"news".equals(categoryType) && !"carbon_cert".equals(categoryType)) {
List<SearchResultVO> serviceResults = searchServiceInfo(keyword);
if (!"all".equals(categoryType)) {
resultList.addAll(serviceResults);
} else {
resultList.addAll(serviceResults);
}
}
// 分页处理
int start = (reqVO.getPage() - 1) * reqVO.getPageSize(); int start = (reqVO.getPage() - 1) * reqVO.getPageSize();
int end = Math.min(start + reqVO.getPageSize(), resultList.size()); int end = Math.min(start + reqVO.getPageSize(), resultList.size());
@ -72,7 +97,47 @@ public class SearchServiceImpl implements SearchService {
@Override @Override
public List<String> getHotSearchKeywords() { public List<String> getHotSearchKeywords() {
return Arrays.asList("江苏电厂配额", "林业碳汇开发", "CBAM 报告", "零碳展会"); return Arrays.asList("碳核查", "ESG", "碳资产管理", "ISO 14067");
}
@Override
public Map<String, Integer> getCategoryCount(String keyword) {
Map<String, Integer> countMap = new HashMap<>();
countMap.put("all", 0);
countMap.put("news", 0);
countMap.put("service", 0);
countMap.put("carbon_cert", 0);
// 统计资讯数量
Map<String, List<SyzxxxVO>> data = zxxxbService.zxxx();
if (data != null) {
data.forEach((category, list) -> {
String categoryType = getCategoryType(category);
long count = list.stream()
.filter(zxxx -> matchKeyword(zxxx.getBt1(), keyword) || matchKeyword(zxxx.getZxNr(), keyword))
.count();
countMap.put(categoryType, (int) count);
if (!"carbon_cert".equals(categoryType)) {
countMap.put("all", countMap.get("all") + (int) count);
}
});
}
// 统计供需信息数量
if (StringUtils.hasText(keyword)) {
QueryWrapper<TxwGxzxGxxxbDO> wrapper = new QueryWrapper<>();
wrapper.lambda()
.and(w -> w.like(TxwGxzxGxxxbDO::getBt1, keyword)
.or().like(TxwGxzxGxxxbDO::getFwnr, keyword)
.or().like(TxwGxzxGxxxbDO::getQymc, keyword))
.eq(TxwGxzxGxxxbDO::getSjzt, "Y")
.eq(TxwGxzxGxxxbDO::getZt, "3");
Long serviceCount = gxxxbMapper.selectCount(wrapper);
countMap.put("service", serviceCount.intValue());
countMap.put("all", countMap.get("all") + serviceCount.intValue());
}
return countMap;
} }
@Override @Override
@ -84,10 +149,42 @@ public class SearchServiceImpl implements SearchService {
List<String> suggestions = new ArrayList<>(); List<String> suggestions = new ArrayList<>();
String lowerKeyword = keyword.toLowerCase(); String lowerKeyword = keyword.toLowerCase();
suggestions.add(keyword); // 从资讯中提取建议
Map<String, List<SyzxxxVO>> data = zxxxbService.zxxx();
if (data != null) {
data.forEach((category, list) -> {
list.forEach(zxxx -> {
if (matchKeyword(zxxx.getBt1(), keyword)) {
suggestions.add(zxxx.getBt1());
}
});
});
}
// 从供需信息中提取建议
QueryWrapper<TxwGxzxGxxxbDO> wrapper = new QueryWrapper<>();
wrapper.lambda()
.and(w -> w.like(TxwGxzxGxxxbDO::getBt1, keyword)
.or().like(TxwGxzxGxxxbDO::getFwnr, keyword)
.or().like(TxwGxzxGxxxbDO::getQymc, keyword))
.eq(TxwGxzxGxxxbDO::getSjzt, "Y")
.eq(TxwGxzxGxxxbDO::getZt, "3")
.select(TxwGxzxGxxxbDO::getBt1, TxwGxzxGxxxbDO::getQymc)
.last("LIMIT 20");
List<TxwGxzxGxxxbDO> gxxxList = gxxxbMapper.selectList(wrapper);
if (gxxxList != null) {
gxxxList.forEach(gxxx -> {
if (StringUtils.hasText(gxxx.getBt1())) {
suggestions.add(gxxx.getBt1());
}
});
}
// 去重并返回包含关键词的建议
return suggestions.stream() return suggestions.stream()
.filter(s -> s.toLowerCase().contains(lowerKeyword)) .filter(s -> s != null && s.toLowerCase().contains(lowerKeyword))
.distinct()
.limit(10) .limit(10)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ -124,6 +221,69 @@ public class SearchServiceImpl implements SearchService {
return "all"; return "all";
} }
private List<SearchResultVO> searchServiceInfo(String keyword) {
List<SearchResultVO> resultList = new ArrayList<>();
if (!StringUtils.hasText(keyword)) {
return resultList;
}
QueryWrapper<TxwGxzxGxxxbDO> wrapper = new QueryWrapper<>();
wrapper.lambda()
.and(w -> w.like(TxwGxzxGxxxbDO::getBt1, keyword)
.or().like(TxwGxzxGxxxbDO::getFwnr, keyword)
.or().like(TxwGxzxGxxxbDO::getQymc, keyword))
.eq(TxwGxzxGxxxbDO::getSjzt, "Y")
.eq(TxwGxzxGxxxbDO::getZt, "3")
.orderByDesc(TxwGxzxGxxxbDO::getLrrq);
List<TxwGxzxGxxxbDO> list = gxxxbMapper.selectList(wrapper);
if (list != null) {
for (TxwGxzxGxxxbDO gxxx : list) {
String ywlxMc = getYwlxMc(gxxx.getYwlxDm());
String ywlxPath = getYwlxPath(gxxx.getYwlxDm());
SearchResultVO vo = new SearchResultVO();
vo.setId(gxxx.getGxUuid());
vo.setTitle(highlightKeyword(gxxx.getBt1(), keyword));
vo.setSummary(highlightKeyword(truncate(gxxx.getFwnr(), 200), keyword));
vo.setCategory(ywlxMc);
vo.setCategoryType("service");
vo.setSource(gxxx.getQymc());
vo.setSourceType("service");
vo.setPublishTime(gxxx.getLrrq() != null ? gxxx.getLrrq().toString() : null);
vo.setUrl(VIEW_MHZC_PREFIX + ywlxPath + "?id=" + gxxx.getGxUuid());
resultList.add(vo);
}
}
return resultList;
}
private String getYwlxMc(String ywlxDm) {
if ("01".equals(ywlxDm)) {
return "碳服务市场";
} else if ("02".equals(ywlxDm)) {
return "碳需求市场";
} else if ("03".equals(ywlxDm)) {
return "碳金融市场";
} else if ("04".equals(ywlxDm)) {
return "碳数据市场";
}
return "服务中心";
}
private String getYwlxPath(String ywlxDm) {
if ("01".equals(ywlxDm)) {
return "/tfwsc";
} else if ("02".equals(ywlxDm)) {
return "/txqsc";
} else if ("03".equals(ywlxDm)) {
return "/tjrsc";
} else if ("04".equals(ywlxDm)) {
return "/tsjsc";
}
return "/tfwsc";
}
@Override @Override
public List<String> getSearchHistory() { public List<String> getSearchHistory() {
return new ArrayList<>(); return new ArrayList<>();

View File

@ -17,8 +17,12 @@ public class SendMsgReqVO {
@NotEmpty(message = "手机号码不能为空") @NotEmpty(message = "手机号码不能为空")
private String sjhm1; private String sjhm1;
@Schema(description = "验证码,验证码开启时,需要传递", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "图形验证码uuid", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "验证码不能为空", groups = AuthLoginReqVO.CodeEnableGroup.class) @NotEmpty(message = "验证码不能为空", groups = AuthLoginReqVO.CodeEnableGroup.class)
private String captchaVerification; private String captchaVerification;
@Schema(description = "图形验证码", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "验证码不能为空", groups = AuthLoginReqVO.CodeEnableGroup.class)
private String captchaCode;
} }

View File

@ -158,7 +158,7 @@ public class AuthServiceImpl implements AuthService {
@Override @Override
public Integer sendMsg(SendMsgReqVO reqVO) throws Exception{ public Integer sendMsg(SendMsgReqVO reqVO) throws Exception{
final Boolean checked = verifyService.checkVerifyToken(reqVO.getCaptchaVerification()); final Boolean checked = verifyService.checkCaptcha(reqVO.getCaptchaVerification(), reqVO.getCaptchaCode());
if (!checked) { if (!checked) {
throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR); throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR);
} }