| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- <template>
- <view class="achievement-container">
- <!-- 顶部固定 Tab 区域 -->
- <view class="header">
- <view class="search-section">
- <view class="search-bar">
- <uv-input
- v-model="searchText"
- placeholder="搜索名称"
- prefixIcon="search"
- prefixIconStyle="color: #94a3b8; font-size: 32rpx;"
- shape="circle"
- border="none"
- customStyle="background-color: #f1f5f9; padding: 12rpx 24rpx;"
- clearable
- @input="onInput"
- @confirm="onSearch"
- @clear="onSearch"
- ></uv-input>
- </view>
- </view>
-
- <view class="tab-section">
- <uv-tabs
- :list="tabList"
- :current="current"
- @change="onTabChange"
- activeStyle="{ color: '#3b82f6', fontWeight: 'bold', fontSize: '30rpx' }"
- inactiveStyle="{ color: '#64748b', fontSize: '28rpx' }"
- lineColor="#3b82f6"
- lineWidth="40rpx"
- lineHeight="6rpx"
- :scrollable="true"
- itemStyle="padding-left: 30rpx; padding-right: 30rpx; height: 88rpx;"
- ></uv-tabs>
- </view>
- </view>
- <!-- 列表区域 -->
- <view class="list-wrapper">
- <scroll-view
- scroll-y
- class="scroll-view"
- :scroll-top="scrollTopValue"
- scroll-with-animation
- refresher-enabled
- refresher-background="#f8fafc"
- :refresher-triggered="isTriggered"
- @refresherrefresh="onRefresh"
- @scrolltolower="onLoadMore"
- @scroll="onScroll"
- >
- <view class="list-inner">
- <!-- 初始加载骨架屏或 Loading -->
- <view v-if="currentLoading && !currentList.length" class="loading-state">
- <uv-loading-icon text="正在加载科研成果..." color="#3b82f6"></uv-loading-icon>
- </view>
-
- <uv-empty v-else-if="!currentList.length" mode="list" text="暂无相关科研成果" icon="https://cdn.uviewui.com/uview/empty/list.png"></uv-empty>
-
- <view v-else class="card-list">
- <AchievementCard
- v-for="(item, index) in currentList"
- :key="item.id || index"
- :item="item"
- :type="currentType"
- class="animate-fade-in"
- :style="{ animationDelay: `${(index % 10) * 0.05}s` }"
- @click="goDetail(item)"
- />
- </view>
- <view class="load-more-box" v-if="currentList.length > 0">
- <uv-load-more :status="loadStatus" color="#94a3b8" />
- </view>
- </view>
- </scroll-view>
- <!-- 返回顶部 -->
- <uv-back-top :scrollTop="currentScrollTop" :bottom="100" :right="30" @click="backToTop"></uv-back-top>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- import { ref, computed, onMounted, nextTick } from 'vue';
- import { useAchievementStore } from '@/store/modules/achievement';
- import { storeToRefs } from 'pinia';
- import { debounce } from 'lodash-es';
- import AchievementCard from './components/AchievementCard.vue';
- const achievementStore = useAchievementStore();
- const {
- patentList, softwareList, otherList, standardList, decisionList, workList, awardsList, paperList, conferenceList, specialActivityList,
- patentPagination, softwarePagination, otherPagination, standardPagination, decisionPagination, workPagination, awardsPagination, paperPagination, conferencePagination, specialActivityPagination,
- patentLoading, softwareLoading, otherLoading, standardLoading, decisionLoading, workLoading, awardsLoading, paperLoading, conferenceLoading, specialActivityLoading
- } = storeToRefs(achievementStore);
- const tabList = [
- { name: '专利', type: 'patent' },
- { name: '软著', type: 'software' },
- { name: '其他成果', type: 'other' },
- { name: '标准', type: 'standard' },
- { name: '决策咨询', type: 'decision' },
- { name: '学术著作', type: 'work' },
- { name: '奖项荣誉', type: 'awards' },
- { name: '学术论文', type: 'paper' },
- { name: '学术会议', type: 'conference' },
- { name: '科技专项活动', type: 'special_activity' }
- ];
- const current = ref(0);
- const searchText = ref('');
- const isTriggered = ref(false);
- const scrollTopValue = ref(0);
- const currentScrollTop = ref(0);
- const currentType = computed(() => tabList[current.value].type);
- const currentList = computed<any[]>(() => {
- const typeMap: Record<string, any[]> = {
- patent: patentList.value,
- software: softwareList.value,
- other: otherList.value,
- standard: standardList.value,
- decision: decisionList.value,
- work: workList.value,
- awards: awardsList.value,
- paper: paperList.value,
- conference: conferenceList.value,
- special_activity: specialActivityList.value,
- };
- return typeMap[currentType.value] || [];
- });
- const currentPagination = computed(() => {
- const typeMap: any = {
- patent: patentPagination.value,
- software: softwarePagination.value,
- other: otherPagination.value,
- standard: standardPagination.value,
- decision: decisionPagination.value,
- work: workPagination.value,
- awards: awardsPagination.value,
- paper: paperPagination.value,
- conference: conferencePagination.value,
- special_activity: specialActivityPagination.value,
- };
- return typeMap[currentType.value];
- });
- const currentLoading = computed(() => {
- const typeMap: any = {
- patent: patentLoading.value,
- software: softwareLoading.value,
- other: otherLoading.value,
- standard: standardLoading.value,
- decision: decisionLoading.value,
- work: workLoading.value,
- awards: awardsLoading.value,
- paper: paperLoading.value,
- conference: conferenceLoading.value,
- special_activity: specialActivityLoading.value,
- };
- return typeMap[currentType.value];
- });
- const loadStatus = computed(() => {
- if (currentLoading.value) return 'loading';
- if (currentList.value.length >= (currentPagination.value?.total || 0)) return 'nomore';
- return 'loadmore';
- });
- const getQuery = () => {
- const query: any = {};
- const type = currentType.value;
- if (searchText.value) {
- if (type === 'patent') query.patentName = searchText.value;
- else if (type === 'software') query.softwareName = searchText.value;
- else if (type === 'other') query.achievementName = searchText.value;
- else if (type === 'standard') query.standardName = searchText.value;
- else if (type === 'decision') query.reportTitle = searchText.value;
- else if (type === 'work') query.workName = searchText.value;
- else if (type === 'awards') query.awardName = searchText.value;
- else if (type === 'paper') query.paperName = searchText.value;
- else if (type === 'conference') query.codTitle = searchText.value;
- else if (type === 'special_activity') query.activityName = searchText.value;
- }
- return query;
- };
- const onTabChange = (e: any) => {
- current.value = e.index;
- // 切换 tab 时清空搜索或保留?通常清空比较清晰
- searchText.value = '';
- initData();
- };
- const onInput = () => {
- debouncedSearch();
- };
- const onSearch = () => {
- initData();
- };
- const debouncedSearch = debounce(() => {
- initData();
- }, 500);
- const initData = async () => {
- await achievementStore.fetchList(currentType.value, getQuery(), true);
- };
- const onRefresh = async () => {
- isTriggered.value = true;
- await initData();
- isTriggered.value = false;
- };
- const onLoadMore = () => {
- if (loadStatus.value === 'loadmore') {
- achievementStore.loadMore(currentType.value, getQuery());
- }
- };
- const goDetail = (item: any) => {
- uni.navigateTo({
- url: `/pages/achievement/detail?type=${currentType.value}&id=${item.id}&code=${item.patentCode || item.workCode || item.awardCode || item.paperCode || item.standardCode || ''}`
- });
- };
- const onScroll = (e: any) => {
- currentScrollTop.value = e.detail.scrollTop;
- };
- const backToTop = () => {
- // 先同步 scrollTopValue 为当前位置,再置为 0 触发滚动
- scrollTopValue.value = currentScrollTop.value;
- nextTick(() => {
- scrollTopValue.value = 0;
- });
- };
- onMounted(() => {
- initData();
- });
- </script>
- <style lang="scss" scoped>
- .achievement-container {
- height: 100vh;
- display: flex;
- flex-direction: column;
- background-color: #f8fafc;
- }
- .header {
- background-color: #ffffff;
- z-index: 100;
- box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
-
- .search-section {
- padding: 24rpx 30rpx 12rpx;
-
- .search-bar {
- border-radius: 100rpx;
- overflow: hidden;
- transition: all 0.3s ease;
-
- &:focus-within {
- transform: scale(1.02);
- }
- }
- }
-
- .tab-section {
- border-bottom: 1rpx solid #f1f5f9;
- }
- }
- .list-wrapper {
- flex: 1;
- overflow: hidden;
- }
- .scroll-view {
- height: 100%;
- }
- .list-inner {
- padding: 30rpx;
- padding-bottom: env(safe-area-inset-bottom);
- }
- .loading-state {
- display: flex;
- justify-content: center;
- align-items: center;
- padding-top: 100rpx;
- }
- .card-list {
- display: flex;
- flex-direction: column;
- }
- .load-more-box {
- padding: 20rpx 0 40rpx;
- }
- .animate-fade-in {
- animation: fadeIn 0.5s ease both;
- }
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(20rpx);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
- </style>
|