txw/txw-mhzc-web/src/pages/index/views/fwsc/fwsc.vue

1137 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="fwsc-container">
<!-- 面包屑导航 -->
<!-- <BreadcrumbNav currentPage="碳服务市场" /> -->
<!-- 二级菜单 -->
<div class="secondary-nav">
<div class="secondary-nav-content">
<div class="nav-tabs">
<button v-for="tab in navTabs" :key="tab.path" :class="['nav-tab', { active: isActiveTab(tab.path) }]"
@click="goToTab(tab)">
{{ tab.label }}
</button>
</div>
<div class="nav-right">
<span class="list-count">共 {{ page.total }} 条服务</span>
<button class="publish-btn" @click="handlePublish">免费发布服务</button>
</div>
</div>
</div>
<main class="fwsc-main">
<div class="content-area">
<!-- 左侧服务卡片列表 -->
<div class="card-list">
<div class="service-grid">
<div v-for="(card, index) in cardList" :key="card.gxUuid" :data-gx-uuid="card.gxUuid" class="service-card">
<!-- 卡片头部 -->
<div class="card-header">
<div class="card-title-box">
<div class="card-title-text">
<div class="card-title-row">
<div class="card-title-main">{{ card.bt1 }}</div>
<!-- 收藏按钮 -->
<div class="card-collect" @click="handleCollect(card)">
<img v-if="card.scbz === 'Y'" src="../../assets/fwsc/ysc.svg" />
<img v-else src="../../assets/fwsc/wsc.svg" />
</div>
</div>
<div class="card-title-sub">
<div class="card-company">
<img src="../../assets/fwsc/city.svg" />
<span class="company-name">{{ card.qymc }}</span>
</div>
<div class="location" v-if="card.fwfw">
<img src="../../assets/fwsc/map.svg" />
<span class="company-name">{{ card.fwfw }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 卡片内容 -->
<div class="card-content">
<p class="card-desc">{{ card.fwnr }}</p>
<div class="card-tags">
<span v-for="(tag, i) in card.fwlxbqList" :key="i" class="tag">{{ tag }}</span>
</div>
</div>
<!-- 卡片底部 -->
<div class="card-footer">
<div class="card-price-info">
<span class="price-value" v-if="card.gjjg">¥{{ card.gjjg }}</span>
<span class="price-value" v-else>面议</span>
<!-- <span class="price-unit" v-if="card.gjdwDm">/{{ card.gjdwDm }}</span> -->
</div>
<div class="card-actions">
<span @click="handleContact(card)">立即咨询</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="cardList.length === 0 && !loading" class="empty-state">
<p>暂无服务信息</p>
</div>
</div>
<!-- 右侧筛选栏 -->
<aside class="filter-sidebar">
<div class="filter-toggle" @click="filterCollapsed = !filterCollapsed">
<span class="toggle-text">筛选</span>
<span class="toggle-icon">{{ filterCollapsed ? '▼' : '▲' }}</span>
</div>
<div :class="['filter-sidebar-content', { collapsed: filterCollapsed }]">
<!-- 内容搜索 -->
<div class="filter-section">
<div class="filter-title">内容搜索</div>
<t-input v-model="filter.nr" placeholder="请输入关键词" @enter="onSearch">
<template #suffix-icon>
<SearchIcon />
</template>
</t-input>
<!-- 只展示收藏项 -->
<div class="filter-section">
<t-checkbox v-model="filter.zzsscx" @change="() => onSearch()">只展示收藏项</t-checkbox>
</div>
</div>
<!-- 服务类型 -->
<div class="filter-section">
<div class="filter-title">服务类型</div>
<div class="filter-options">
<t-checkbox-group v-model="filter.fwlxjh" :options="fwlxOptions" @change="onFwlxChange" />
</div>
</div>
<!-- 服务企业 -->
<div class="filter-section">
<div class="filter-title">服务企业</div>
<t-input v-model="qySearchKeyword" placeholder="搜索企业名称" clearable class="qy-search-input"
@change="filterQyOptions">
<template #prefix-icon>
<SearchIcon />
</template>
</t-input>
<div class="filter-options enterprise-options" v-if="qyFilteredOptions.length > 0">
<t-checkbox-group v-model="filter.qyuuids" :options="qyFilteredOptions" @change="onSearch" />
</div>
<div class="Qy-options-empty" v-else>
<span>未找到匹配企业</span>
</div>
</div>
</div>
</aside>
</div>
<!-- 分页 -->
<div class="pagination-box" v-if="page.total > 0">
<div class="pagination-total">共 {{ page.total }} 条数据</div>
<t-pagination v-model="page.pageNo" :total="page.total" :page-size.sync="page.pageSize" :totalContent='false'
:page-size-options="[10, 20, 30, 50]" @change="onPageChange" align="center" />
</div>
</main>
<Footer />
<!-- 发布服务抽屉 -->
<FwscPublish :visible.sync="publishVisible" @success="onPublishSuccess" />
<!-- 联系服务弹窗 -->
<t-dialog :closeOnOverlayClick="false" header="联系服务" :visible.sync="contactVisible" @confirm="onContactConfirm"
:onClose="onContactClose" class="global-dialog" attach="body">
<div class="dialog-line">
<div class="dialog-line-title">联系人:</div>
<div class="dialog-line-text">{{ contactData.lxr }}</div>
</div>
<div class="dialog-line">
<div class="dialog-line-title">联系电话:</div>
<div class="dialog-line-text">{{ contactData.lxdh }}</div>
</div>
<div class="dialog-line">
<div class="dialog-line-title">电子邮箱:</div>
<div class="dialog-line-text">{{ contactData.email }}</div>
</div>
</t-dialog>
<!-- 提示弹窗 -->
<t-dialog :closeOnOverlayClick="false" header="提示" body="请先进行企业入驻" :visible.sync="rzVisible" @confirm="onRzConfirm"
:onClose="onRzClose" :cancelBtn="null" class="global-dialog" />
<!-- 发布成功弹窗 -->
<t-dialog :closeOnOverlayClick="false" body="发布申请成功,请等待审核,是否继续发布?" :visible.sync="publishSuccessVisible"
@confirm="onPublishSuccessConfirm" @cancel="onPublishSuccessCancel" :onClose="onPublishSuccessClose"
cancelBtn="返回" confirmBtn="继续发布" class="global-dialog" />
</div>
</template>
<script>
import { LocationIcon, SearchIcon } from 'tdesign-icons-vue';
import NewNav from '@/pages/index/components/new-nav/index.vue';
import Footer from '@/pages/index/components/footer/index.vue';
import BreadcrumbNav from '@/pages/index/components/breadcrumb/index.vue';
import FwscPublish from './components/FwscPublish.vue';
import api from '@/pages/index/api/fwsc/index.js';
export default {
name: 'FwscPage',
components: {
NewNav,
Footer,
BreadcrumbNav,
FwscPublish,
LocationIcon,
SearchIcon,
},
data() {
return {
navTabs: [
{ label: '碳服务市场', path: '/tfwsc' },
{ label: '碳需求市场', path: '/txqsc' },
{ label: '碳数据市场', path: '/tsjsc' },
{ label: '碳金融市场', path: '/tjrsc', disable: true },
],
// 筛选条件
filter: {
fwlxjh: [],
qyuuids: [],
nr: '',
zzsscx: false,
},
// 服务类型选项
fwlxOptions: [],
// 企业选项(根据服务类型动态过滤)
qyOptions: [],
// 全部企业列表
allQyOptions: [],
// 企业搜索关键词
qySearchKeyword: '',
// 过滤后的企业选项
qyFilteredOptions: [],
// 卡片列表
cardList: [],
// 分页
page: {
pageNo: 1,
pageSize: 10,
total: 0,
},
// 加载状态
loading: false,
// 用户信息
yhxx: {},
// 发布抽屉
publishVisible: false,
// 联系弹窗
contactVisible: false,
contactData: {},
// 入驻提示弹窗
rzVisible: false,
// 发布成功弹窗
publishSuccessVisible: false,
// 移动端筛选折叠状态
filterCollapsed: true,
};
},
mounted() {
this.initUser();
this.loadDmList();
this.searchList();
// 检查是否需要直接打开发布面板
if (this.$route.query.publish === '1') {
this.handlePublish();
}
},
methods: {
// 初始化用户信息
async initUser() {
try {
const { data } = await api.init();
this.yhxx = data || {};
window.sessionStorage.setItem('yhxx', JSON.stringify(data));
} catch (error) {
console.error('获取用户信息失败', error);
}
},
// 加载代码表
loadDmList() {
this.fwlxoptionsSearch();
this.hqqylist();
},
async fwlxoptionsSearch() {
try {
const res = await api.dms2mc('fwlx', {});
this.fwlxOptions = res.data || [];
} catch (error) {
this.fwlxOptions = [];
}
},
// 获取已入驻企业列表
async hqqylist() {
try {
const res = await api.getQyxxListByYwlx();
const options = [];
(res.data || []).map((item) => {
options.push({ value: item.qyuuid, label: item.qymc });
});
this.allQyOptions = options || [];
this.qyOptions = options || [];
this.qyFilteredOptions = options || [];
} catch (error) {
this.allQyOptions = [];
this.qyOptions = [];
this.qyFilteredOptions = [];
}
},
// 服务类型变化时,动态过滤企业
async onFwlxChange() {
this.qySearchKeyword = '';
if (this.filter.fwlxjh.length > 0) {
try {
const res = await api.getQyuuidsByBq({ fwlxjh: this.filter.fwlxjh });
const selectedCodes = res.data || [];
this.qyOptions = this.allQyOptions.filter((q) => selectedCodes.includes(q.value));
this.qyFilteredOptions = this.qyOptions;
} catch (error) {
this.qyOptions = this.allQyOptions;
this.qyFilteredOptions = this.allQyOptions;
}
} else {
this.qyOptions = this.allQyOptions;
this.qyFilteredOptions = this.allQyOptions;
}
this.onSearch();
},
// 过滤企业选项
filterQyOptions() {
console.log('this.qySearchKeyword', this.qySearchKeyword);
if (!this.qySearchKeyword) {
this.qyFilteredOptions = this.qyOptions;
} else {
const keyword = this.qySearchKeyword.toLowerCase();
this.qyFilteredOptions = this.qyOptions.filter((q) =>
q.label.toLowerCase().includes(keyword)
);
}
},
// 搜索列表
async searchList() {
this.loading = true;
this.cardList = [];
const targetId = this.$route.query.id || '';
try {
const prame = {
ywlxDm: '01',
...this.filter,
...this.page,
};
// 只展示收藏项Y=是, N=否
prame.scbz = this.filter.zzsscx ? 'Y' : 'N';
// URL携带id按id精确查询单条
if (targetId) {
prame.gxUuid = targetId;
}
const { data } = await api.gxxxList(prame);
if (data.records) {
data.records.map((item) => {
if (item.bqjh) {
item.bqjh = item.bqjh.split(',');
}
if (item.fwlxjh) {
item.fwlxbqList = item.fwlxjh.split(',');
} else {
item.fwlxbqList = [];
}
});
}
this.cardList = data.records || [];
this.page.total = Number(data.total || 0);
// 数据渲染后定位到对应项
if (targetId) {
this.scrollToItem(targetId);
}
} catch (error) {
this.cardList = [];
this.page.total = 0;
console.error('获取服务列表失败', error);
} finally {
this.loading = false;
}
},
// 重置筛选
onReset() {
this.filter = {
fwlxjh: [],
qyuuids: [],
nr: '',
zzsscx: false,
};
this.qySearchKeyword = '';
this.qyOptions = this.allQyOptions;
this.qyFilteredOptions = this.allQyOptions;
this.page.pageNo = 1;
this.searchList();
},
// 搜索
onSearch() {
this.page.pageNo = 1;
this.searchList();
},
// 滚动到指定项并高亮
scrollToItem(gxUuid) {
this.$nextTick(() => {
const targetCard = this.cardList.find(card => card.gxUuid === gxUuid);
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() {
const yhxx = window.sessionStorage.getItem('yhxx');
if (yhxx) {
const yhxxData = JSON.parse(yhxx);
if (yhxxData.gxdtRzbz === 'Y') {
this.publishVisible = true;
} else {
this.rzVisible = true;
}
} else {
this.rzVisible = true;
}
},
// 收藏/取消收藏
async handleCollect(card) {
try {
const type = card.scbz === 'Y' ? 'remove' : 'add';
await api.gxsc({ gxUuid: card.gxUuid, type });
card.scbz = card.scbz === 'Y' ? 'N' : 'Y';
} catch (error) {
console.error('收藏失败', error);
}
},
// 联系服务
handleContact(card) {
this.contactData = {
lxr: card.lxr || '',
lxdh: card.lxdh || '',
email: card.email || '',
};
this.contactVisible = true;
},
// 发布成功回调
onPublishSuccess() {
this.publishSuccessVisible = true;
},
// 导航相关
isActiveTab(path) {
return this.$route.path === path;
},
goToTab({path, disable}) {
if (disable) {
this.$message.info('敬请期待');
return
}
this.$router.push(path);
},
// 弹窗回调
onContactConfirm() {
this.contactVisible = false;
},
onContactClose() {
this.contactVisible = false;
},
onRzConfirm() {
this.rzVisible = false;
},
onRzClose() {
this.rzVisible = false;
},
onPublishSuccessConfirm() {
this.publishSuccessVisible = false;
this.publishVisible = true;
},
onPublishSuccessCancel() {
this.publishSuccessVisible = false;
this.$router.push('/tfwsc');
},
onPublishSuccessClose() {
this.publishSuccessVisible = false;
},
// 分页变化
onPageChange() {
this.searchList();
},
},
};
</script>
<style lang="less" scoped>
.fwsc-container {
min-height: 100vh;
background: #f5f5f5;
}
.fwsc-main {
width: 100%;
max-width: 1400px;
padding: 20px;
margin: 0 auto;
}
// 二级菜单
.secondary-nav {
border-bottom: none;
}
.secondary-nav-content {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1400px;
padding: 20px 20px 0;
margin: 0 auto;
}
.nav-tabs {
display: flex;
gap: 40px;
width: 596px;
height: 42px;
}
.nav-right {
display: flex;
align-items: center;
gap: 20px;
}
.nav-tab {
position: relative;
min-width: max-content;
height: 42px;
padding: 8px 16px;
font-size: 18px;
font-weight: 500;
color: #003B1A;
cursor: pointer;
background: transparent;
border-radius: 32px;
transition: all 0.3s;
&:hover {
color: #009a29;
}
&.active {
background: #8CFFCE;
box-shadow: inset 0 0 0 1px #00B96B;
}
}
.publish-btn {
width: 220px;
height: 42px;
line-height: 26px;
padding: 8px 16px;
font-size: 18px;
font-weight: 400;
color: #fff;
cursor: pointer;
background: linear-gradient(135deg, #009a29 0%, #48C666 100%);
border: none;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 154, 41, 0.25);
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 154, 41, 0.35);
}
&:active {
transform: translateY(0);
}
}
.content-area {
display: flex;
gap: 20px;
align-items: stretch;
}
// 左侧筛选栏
.filter-sidebar {
position: sticky;
top: 104px;
width: 220px;
flex-shrink: 0;
}
// 移动端筛选栏折叠
.filter-toggle {
display: none;
}
@media (max-width: 768px) {
.filter-toggle {
display: flex;
padding: 12px;
margin-bottom: 8px;
cursor: pointer;
background: #fff;
border-radius: 8px;
justify-content: space-between;
align-items: center;
.toggle-text {
font-size: 14px;
font-weight: 600;
color: #333;
}
.toggle-icon {
font-size: 18px;
color: #666;
transition: transform 0.3s;
}
&.collapsed .toggle-icon {
transform: rotate(-90deg);
}
}
.filter-sidebar-content {
max-height: 1000px;
padding: 20px;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.filter-sidebar-content.collapsed {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
}
}
.filter-sidebar-content {
padding: 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.filter-section {
// padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px dashed #e0e0e0;
&:last-child {
padding: 10px 0;
margin-bottom: 0;
border-bottom: none;
}
}
.filter-title {
display: flex;
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
color: #333;
align-items: center;
gap: 6px;
&::before {
width: 4px;
height: 14px;
background: linear-gradient(180deg, #009a29, #48C666);
border-radius: 2px;
content: '';
}
}
.filter-options {
/deep/.t-checkbox__label {
font-size: 14px;
color: #666;
}
&.enterprise-options {
max-height: 200px;
overflow-y: auto;
}
}
.qy-search-input {
margin-bottom: 12px;
}
.Qy-options-empty {
padding: 16px 0;
font-size: 14px;
color: #999;
text-align: center;
}
// 右侧卡片列表
.card-list {
flex: 1;
min-width: 0;
}
.list-count {
font-size: 14px;
font-weight: 400;
color: #6B8575;
}
.service-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.service-card {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
&.highlight-card {
animation: highlight-pulse 3s ease-out;
}
// &::before {
// position: absolute;
// top: 0;
// left: 0;
// width: 100%;
// height: 3px;
// background: linear-gradient(90deg, #009a29, #48C666);
// content: '';
// transform: scaleX(0);
// transform-origin: left;
// transition: transform 0.3s ease;
// }
// &:hover {
// transform: translateY(-4px);
// box-shadow: 0 8px 24px rgba(0, 154, 41, 0.15);
// &::before {
// transform: scaleX(1);
// }
// }
}
.card-header {
position: relative;
padding: 16px;
height: 70px;
&::after {
position: absolute;
right: 16px;
bottom: -6px;
left: 16px;
height: 1px;
background: #E8F0EC;
content: '';
}
}
.card-title-box {
display: flex;
justify-content: space-between;
}
.card-title-text {
flex: 1;
min-width: 0;
}
.card-title-main {
flex: 1;
overflow: hidden;
font-size: 16px;
font-weight: 500;
line-height: 24px;
color: #003B1A;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.card-company {
display: flex;
align-items: center;
gap: 8px;
}
.card-title-sub {
display: flex;
gap: 12px;
justify-content: space-between;
}
.company-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
color: #6B8575;
font-weight: 400;
}
.location {
display: flex;
align-items: center;
gap: 8px;
}
.card-collect {
flex-shrink: 0;
margin-left: 8px;
cursor: pointer;
img {
width: 20px;
height: 20px;
}
}
.card-content {
padding: 16px;
}
.card-desc {
display: block;
height: 70px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #666;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
padding: 2px 8px;
font-size: 13px;
font-weight: 400;
color: #00B96B;
background: #EEFAE2;
border-radius: 4px;
}
.card-footer {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
height: 64px;
&::before {
position: absolute;
top: 0;
right: 16px;
left: 16px;
height: 1px;
background: #E8F0EC;
content: '';
}
}
.card-price-info {
display: flex;
align-items: baseline;
}
.card-actions {
display: flex;
height: 32px;
padding: 6px 12px;
border-radius: 4px;
border: 1px solid #00b96b;
color: #00b96b;
font-size: 14px;
font-weight: 400;
&:hover {
background: rgba(0, 154, 41, 0.1);
cursor: pointer;
}
}
.price-value {
font-size: 20px;
font-weight: 600;
color: #FF4D4F;
}
.price-unit {
margin-left: 2px;
font-size: 14px;
color: #666;
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
min-height: 300px;
padding: 40px;
background: #fff;
border-radius: 12px;
animation: fadeIn 0.5s ease;
.empty-icon {
margin-bottom: 16px;
font-size: 48px;
color: #d0d0d0;
}
p {
font-size: 14px;
color: #999;
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// 分页
.pagination-total {
font-size: 16px;
color: #666;
}
.pagination-box {
display: flex;
align-items: center;
justify-content: center;
padding-top: 32px;
gap: 10px;
::v-deep .t-pagination {
display: flex !important;
justify-content: center;
flex-wrap: wrap;
width: initial;
.t-input.t-is-readonly {
width: 110px;
}
}
}
// 弹窗样式
.dialog-line {
display: flex;
margin-bottom: 16px;
}
.dialog-line-title {
flex-shrink: 0;
width: 80px;
color: #333;
}
.dialog-line-text {
color: #666;
}
@media (max-width: 1200px) {
.service-card {
width: calc((100% - 20px) / 2);
}
}
@media (max-width: 768px) {
.fwsc-main {
padding: 12px;
}
.secondary-nav-content {
flex-direction: column;
padding: 12px 16px;
gap: 12px;
}
.nav-tabs {
width: 100%;
padding-bottom: 4px;
overflow-x: auto;
gap: 4px;
}
.nav-tab {
padding: 10px 12px;
font-size: 13px;
white-space: nowrap;
}
.publish-btn {
width: 100%;
padding: 10px;
}
.content-area {
flex-direction: column;
}
.filter-sidebar {
position: relative;
top: 0;
width: 100%;
}
.card-list {
width: 100%;
}
.list-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.service-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.service-card {
width: 100%;
}
.card-header {
padding: 12px;
}
.card-content {
padding: 12px;
}
.card-footer {
padding: 12px;
flex-direction: column;
gap: 12px;
}
.card-actions {
width: 100%
}
.pagination-box {
padding-top: 20px;
}
}
@media (max-width: 480px) {
.fwsc-main {
padding: 8px;
}
.secondary-nav-content {
padding: 8px 12px;
}
.nav-tab {
padding: 8px 10px;
font-size: 12px;
}
.publish-btn {
padding: 8px;
font-size: 13px;
}
.filter-sidebar-content {
padding: 12px;
}
.list-title-box .list-title {
font-size: 18px;
}
.card-title-main {
font-size: 14px;
}
.card-desc {
height: 60px;
font-size: 13px;
}
.price-value {
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>