This commit is contained in:
2026-07-20 11:56:02 +08:00
commit 2dcae4165b
176 changed files with 34633 additions and 0 deletions

View File

@@ -0,0 +1,680 @@
<template>
<view class="page">
<scroll-view
scroll-y
class="page-scroll"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="handleRefresh"
@scrolltolower="loadMore"
>
<common-hero title="挑战杯人才库" :use-image-bg="true"></common-hero>
<view class="hero-card hero-card--green">
<view class="hero-title">创新竞赛历史人才数据库</view>
<view class="hero-desc">
按姓名学校人才类型参赛项目和比赛级别查看历届挑战杯人才线索支持复制详情链接用于进一步跟踪
</view>
<view class="hero-meta">
<view class="meta-item">
<view class="meta-value">{{ list.length }}</view>
<view class="meta-label">当前已加载</view>
</view>
<view class="meta-item">
<view class="meta-value">{{ levelOptions.length }}</view>
<view class="meta-label">级别筛选</view>
</view>
<view class="meta-item">
<view class="meta-value">{{ talentTypeCount }}</view>
<view class="meta-label">人才类型</view>
</view>
</view>
</view>
<view class="filter-card">
<view class="search-box">
<uv-input
v-model.trim="filters.keywords"
class="search-input"
placeholder="姓名/项目/学校/奖项"
confirm-type="search"
@confirm="applyFilters"
:maxlength="-1"
border="none"
/>
<view class="search-btn" @tap="applyFilters">查询</view>
</view>
<view class="field-row">
<uv-input
v-model.trim="filters.schoolName"
class="field-input"
placeholder="参赛学校"
confirm-type="search"
@confirm="applyFilters"
:maxlength="-1"
border="none"
/>
</view>
<view class="field-row">
<uv-input
v-model.trim="filters.talentType"
class="field-input"
placeholder="人才类型"
confirm-type="search"
@confirm="applyFilters"
:maxlength="-1"
border="none"
/>
</view>
<view class="picker-row picker-row--single">
<picker class="picker-item" mode="selector" :range="levelLabels" :value="levelIndex"
@change="handleLevelChange">
<view class="picker-trigger">{{ filters.matchLevel || '比赛级别' }}</view>
</picker>
</view>
<view class="action-row">
<view class="action-btn action-btn--primary action-btn--green" @tap="applyFilters">查询</view>
<view class="action-btn" @tap="resetFilters">重置</view>
</view>
</view>
<view v-if="loading && !list.length" class="state-card">
<view class="state-title">正在加载人才库...</view>
<view class="state-desc">请稍候正在同步挑战杯历史人才数据</view>
</view>
<view v-else-if="loadError && !list.length" class="state-card state-card--error">
<view class="state-title">加载失败</view>
<view class="state-desc">{{ loadError }}</view>
<view class="state-action state-action--green" @tap="loadData(true)">重新加载</view>
</view>
<view v-else-if="!list.length" class="state-card">
<view class="state-title">暂无匹配人才</view>
<view class="state-desc">请调整搜索条件后重新查询</view>
</view>
<view v-else class="card-list">
<view v-for="item in list" :key="item.id || item.sourceId" class="data-card">
<view class="card-top">
<view class="avatar-cover" :style="getCoverStyle(item.coverImage)">
<view class="avatar-text">{{ getAvatarText(item.personName) }}</view>
</view>
<view class="card-main">
<view class="card-title-row">
<view class="card-title">{{ item.personName || '未命名人才' }}</view>
<view class="tag-chip tag-chip--green">{{ item.talentType || '未分类' }}</view>
</view>
<view class="card-school">{{ item.schoolName || '未知学校' }}</view>
<view class="card-project">{{ item.projectName || '未录入项目名称' }}</view>
<view class="tag-row">
<view class="tag-chip">{{ item.awardName || '未录入奖项' }}</view>
<view class="tag-chip tag-chip--blue">{{ item.matchLevel || '未知级别' }}</view>
<view class="tag-chip tag-chip--soft">{{ item.matchTerm || '未知届次' }}</view>
</view>
</view>
</view>
<view class="info-grid">
<view class="info-item">
<view class="info-label">来源ID</view>
<view class="info-value">{{ item.sourceId || '-' }}</view>
</view>
<view class="info-item">
<view class="info-label">来源页码</view>
<view class="info-value">{{ item.pageNo || '-' }}</view>
</view>
<view class="info-item">
<view class="info-label">同步时间</view>
<view class="info-value">{{ formatDateTime(item.lastSyncTime) }}</view>
</view>
</view>
<view class="card-footer">
<view class="footer-link footer-link--green" @tap="copyLink(item.detailUrl)">复制详情链接</view>
</view>
</view>
</view>
<view v-if="list.length" class="load-more">
<view v-if="loadingMore" class="load-more-text">正在加载更多...</view>
<view v-else-if="hasMore" class="load-more-text" @tap="loadMore">点击加载更多</view>
<view v-else class="load-more-text load-more-text--end">没有更多数据了</view>
</view>
</scroll-view>
</view>
</template>
<script>
import CommonHero from '../../components/common-hero/common-hero.vue'
import {pageTzbTalentList} from './tzb-database-service'
const DEFAULT_FILTERS = () => ({
keywords: '',
schoolName: '',
talentType: '',
matchLevel: ''
})
export default {
components: {
CommonHero
},
data() {
return {
loading: false,
loadingMore: false,
refreshing: false,
loadError: '',
page: 1,
limit: 10,
total: 0,
hasMore: true,
list: [],
filters: DEFAULT_FILTERS(),
levelOptions: ['国赛', '省赛', '校赛']
}
},
computed: {
levelLabels() {
return ['全部级别', ...this.levelOptions]
},
levelIndex() {
if (!this.filters.matchLevel) {
return 0
}
const index = this.levelOptions.findIndex((item) => item === this.filters.matchLevel)
return index >= 0 ? index + 1 : 0
},
talentTypeCount() {
const set = new Set(this.list.map((item) => item.talentType).filter(Boolean))
return set.size || '-'
}
},
onLoad() {
this.loadData(true)
},
methods: {
showToast(title) {
uni.showToast({
title,
icon: 'none'
})
},
formatDateTime(value) {
if (!value) {
return '-'
}
const date = new Date(String(value).replace(/-/g, '/'))
if (Number.isNaN(date.getTime())) {
return value
}
const pad = (item) => String(item).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
},
getCoverStyle(url) {
if (url) {
return `background-image: linear-gradient(rgba(15, 23, 42, 0.12), rgba(15, 23, 42, 0.24)), url(${url});`
}
return 'background: linear-gradient(135deg, #34d399 0%, #10b981 100%);'
},
getAvatarText(name) {
return String(name || '人才').slice(-2)
},
handleLevelChange(event) {
const index = Number(event.detail.value || 0)
this.filters.matchLevel = index === 0 ? '' : this.levelOptions[index - 1]
},
handleRefresh() {
this.refreshing = true
this.loadData(true)
},
applyFilters() {
this.loadData(true)
},
resetFilters() {
this.filters = DEFAULT_FILTERS()
this.loadData(true)
},
async loadData(reset = false) {
if (this.loading || this.loadingMore) {
if (reset) {
this.refreshing = false
}
return
}
const nextPage = reset ? 1 : this.page
if (!reset && !this.hasMore) {
return
}
if (reset) {
this.loading = true
this.loadError = ''
} else {
this.loadingMore = true
}
try {
const params = {
page: nextPage,
limit: this.limit
}
if (this.filters.keywords) params.keywords = this.filters.keywords
if (this.filters.schoolName) params.schoolName = this.filters.schoolName
if (this.filters.talentType) params.talentType = this.filters.talentType
if (this.filters.matchLevel) params.matchLevel = this.filters.matchLevel
const result = await pageTzbTalentList(params)
const nextList = (result && result.list) || []
const count = Number((result && result.count) || 0)
this.total = count
this.page = nextPage + 1
this.list = reset ? nextList : this.list.concat(nextList)
this.hasMore = this.list.length < count
} catch (error) {
this.loadError = (error && error.message) || '人才库加载失败'
this.showToast(this.loadError)
} finally {
this.loading = false
this.loadingMore = false
this.refreshing = false
}
},
loadMore() {
if (!this.loading && !this.loadingMore && this.hasMore) {
this.loadData(false)
}
},
copyLink(url) {
if (!url) {
this.showToast('暂无详情链接')
return
}
uni.setClipboardData({
data: url,
success: () => {
this.showToast('详情链接已复制')
}
})
}
}
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: radial-gradient(circle at top left, rgba(16, 185, 129, 0.14), transparent 28%),
linear-gradient(180deg, #f5fffb 0%, #fffdf8 100%);
}
.page-scroll {
height: 100vh;
//padding: 28rpx 24rpx 40rpx;
box-sizing: border-box;
}
.hero-card,
.filter-card,
.state-card,
.data-card {
border-radius: 30rpx;
box-shadow: 0 14rpx 34rpx rgba(15, 23, 42, 0.06);
}
.hero-card {
padding: 34rpx 36rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.5);
box-shadow: 0 4rpx 12rpx 0 rgba(174, 174, 174, 0.4);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
border: 1rpx solid rgba(255, 255, 255, 0.5);
}
.hero-card--green .hero-badge {
background: rgba(20,150,242,.1);
}
.hero-badge {
display: inline-flex;
padding: 10rpx 18rpx;
border-radius: 999rpx;
font-size: 22rpx;
color: #1496f2;
}
.hero-title {
margin-top: 8rpx;
padding-left: 10rpx;
font-size: 30rpx;
line-height: 1.35;
font-weight: 700;
color: #18a0f7;
border-left: 5px solid #0f94ef;
}
.hero-desc {
margin-top: 20rpx;
font-size: 24rpx;
line-height: 1.8;
color: #7f7f7f;
}
.hero-meta {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16rpx;
margin-top: 26rpx;
}
.meta-item {
padding: 18rpx 16rpx;
border-radius: 22rpx;
background: rgba(255, 255, 255, 0.12);
}
.meta-value {
font-size: 32rpx;
font-weight: 700;
}
.meta-label {
margin-top: 6rpx;
font-size: 22rpx;
color: rgba(247, 255, 251, 0.84);
}
.filter-card {
margin-top: 22rpx;
padding: 24rpx;
background: rgba(255, 255, 255, 0.94);
}
.search-box,
.field-input,
.picker-trigger {
background: #f7fffb;
border: 1rpx solid rgba(16, 185, 129, 0.12);
}
.search-box {
display: flex;
align-items: center;
gap: 16rpx;
padding: 0 18rpx;
height: 84rpx;
border-radius: 999rpx;
}
:deep(.search-input.uv-input) {
flex: 1;
height: 100%;
padding: 0 !important;
background: transparent !important;
border: 0 !important;
}
:deep(.search-input.uv-input .uv-input__content__field-wrapper__field) {
font-size: 24rpx;
color: #1f2937;
}
.search-btn {
flex-shrink: 0;
font-size: 24rpx;
font-weight: 600;
color: #0f9f71;
}
.field-row,
.picker-row,
.action-row {
margin-top: 16rpx;
}
:deep(.field-input.uv-input) {
width: 100%;
height: 84rpx;
padding: 0 20rpx !important;
border-radius: 22rpx;
box-sizing: border-box;
background: #f7fffb !important;
border: 1rpx solid rgba(16, 185, 129, 0.12) !important;
}
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) {
font-size: 24rpx;
color: #1f2937;
}
.picker-row--single {
display: block;
}
.picker-item {
display: block;
}
.picker-trigger {
display: flex;
align-items: center;
justify-content: center;
height: 84rpx;
border-radius: 22rpx;
font-size: 24rpx;
color: #1f2937;
}
.action-row {
display: flex;
gap: 14rpx;
}
.action-btn {
flex: 1;
height: 82rpx;
border-radius: 22rpx;
background: #ecfdf5;
color: #0f9f71;
font-size: 24rpx;
font-weight: 600;
line-height: 82rpx;
text-align: center;
}
.action-btn--primary {
color: #fff;
}
.action-btn--green {
background: linear-gradient(135deg, #34d399, #10b981);
}
.state-card {
margin-top: 22rpx;
padding: 32rpx 28rpx;
background: rgba(255, 255, 255, 0.94);
}
.state-card--error {
background: rgba(247, 255, 251, 0.98);
border: 1rpx solid rgba(16, 185, 129, 0.16);
}
.state-title {
font-size: 30rpx;
font-weight: 700;
color: #1f2329;
}
.state-desc {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.7;
color: #6c7480;
}
.state-action {
margin-top: 18rpx;
font-size: 24rpx;
font-weight: 600;
color: #2563eb;
}
.state-action--green {
color: #0f9f71;
}
.card-list {
display: flex;
flex-direction: column;
gap: 18rpx;
margin-top: 22rpx;
}
.data-card {
padding: 24rpx;
background: rgba(255, 255, 255, 0.95);
}
.card-top {
display: flex;
gap: 18rpx;
}
.avatar-cover {
flex-shrink: 0;
width: 144rpx;
height: 144rpx;
border-radius: 50%;
background-size: cover;
background-position: center;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-text {
font-size: 34rpx;
font-weight: 700;
color: #fff;
}
.card-main {
flex: 1;
min-width: 0;
}
.card-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
.card-title {
font-size: 30rpx;
font-weight: 700;
line-height: 1.5;
color: #0f172a;
}
.card-school,
.card-project {
margin-top: 8rpx;
font-size: 24rpx;
line-height: 1.6;
color: #64748b;
}
.card-project {
color: #1f2937;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin-top: 14rpx;
}
.tag-chip {
padding: 8rpx 14rpx;
border-radius: 999rpx;
background: #fff4e6;
color: #c76b11;
font-size: 20rpx;
}
.tag-chip--green {
background: #e9fbf5;
color: #0f9f71;
}
.tag-chip--blue {
background: #eef5ff;
color: #2563eb;
}
.tag-chip--soft {
background: #f3f4f6;
color: #6b7280;
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14rpx;
margin-top: 18rpx;
}
.info-item {
padding: 18rpx;
border-radius: 22rpx;
background: #f7fffb;
}
.info-label {
font-size: 20rpx;
color: #8b94a3;
}
.info-value {
margin-top: 8rpx;
font-size: 23rpx;
line-height: 1.6;
color: #1f2937;
word-break: break-all;
}
.card-footer {
display: flex;
justify-content: flex-end;
margin-top: 18rpx;
}
.footer-link {
font-size: 24rpx;
font-weight: 600;
}
.footer-link--green {
color: #0f9f71;
}
.load-more {
padding: 28rpx 0 12rpx;
text-align: center;
}
.load-more-text {
font-size: 22rpx;
color: #64748b;
}
.load-more-text--end {
color: #94a3b8;
}
</style>