Files
tuanwei-uniapp/pages/gxmu/index.vue
2026-07-20 11:56:02 +08:00

698 lines
16 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>
<view class="page">
<scroll-view scroll-y class="page-scroll">
<common-hero title="五四评优" :use-image-bg="true"></common-hero>
<view class="section">
<view class="section-head">
<view>
<view class="section-title">项目列表</view>
<view class="section-subtitle">当前仅展示可申报项目</view>
</view>
<view class="refresh-btn" :class="{ 'refresh-btn--loading': loading }" @tap="loadData()">
{{ loading ? '刷新中' : '刷新' }}
</view>
</view>
<view v-if="loading" class="state-card">
<view class="state-title">正在加载申报项目...</view>
<view class="state-desc">请稍候正在同步后台开放的申报配置</view>
</view>
<view v-else-if="loadError" class="state-card state-card--error">
<view class="state-title">加载失败</view>
<view class="state-desc">{{ loadError }}</view>
<view class="state-action" @tap="loadData()">重新加载</view>
</view>
<view v-else-if="!declareList.length" class="state-card">
<view class="state-title">当前暂无可申报项目</view>
<view class="state-desc">后台还没有开放可申报数据稍后再来查看</view>
</view>
<view v-else>
<view class="filter-bar">
<picker class="filter-item" mode="selector" :range="yearLabels" :value="yearIndex" @change="handleYearChange">
<view class="filter-trigger">
<text class="filter-text">{{ selectedYear ? `${selectedYear}` : '全部年份' }}</text>
<text class="filter-arrow"></text>
</view>
</picker>
<view class="filter-item" @tap="openModulePicker">
<view class="filter-trigger">
<text class="filter-text">{{ selectedModule ? getDeclareModuleLabel(selectedModule) : '全部模块' }}</text>
<text class="filter-arrow"></text>
</view>
</view>
</view>
<view v-if="filteredDeclareList.length" class="declare-list">
<view
v-for="item in filteredDeclareList"
:key="item.id || `${item.module}-${item.year}`"
class="declare-card"
:class="{ 'declare-card--disabled': !isModuleSupported(item.module) }"
@tap="goApply(item)"
>
<view class="card-top">
<view class="card-main">
<view class="card-title">{{ getDeclareTitle(item) }}</view>
<view class="card-subtitle">{{ getDeclareModuleLabel(item.module) }}</view>
</view>
<view
class="card-status"
:class="isModuleSupported(item.module) ? 'card-status--active' : 'card-status--disabled'"
>
{{ isModuleSupported(item.module) ? '可申报' : '待支持' }}
</view>
</view>
<view class="card-year">
{{ item.year ? `${item.year}` : '未设置年度' }}
</view>
<view class="card-desc">{{ getDeclareDescription(item.module) }}</view>
<view class="card-meta">
<view class="meta-row">
<view class="meta-row-label">开始时间</view>
<view class="meta-row-value">{{ formatDateTime(item.startTime) }}</view>
</view>
<view class="meta-row">
<view class="meta-row-label">结束时间</view>
<view class="meta-row-value">{{ formatDateTime(item.endTime) }}</view>
</view>
</view>
<view class="card-footer">
<view class="card-tag">{{ getDeclareGroup(item.module) }}</view>
<view class="card-action">
{{ isModuleSupported(item.module) ? '进入申报' : '暂未开放' }}
</view>
</view>
</view>
</view>
<view v-else class="state-card">
<view class="state-title">暂无匹配项目</view>
<view class="state-desc">请调整年份或模块筛选条件后重试</view>
</view>
</view>
</view>
</scroll-view>
<uv-picker
ref="modulePicker"
title="选择模块"
:columns="modulePickerColumns"
:defaultIndex="[moduleIndex]"
keyName="text"
@close="noop"
@cancel="noop"
@confirm="confirmModulePicker"
></uv-picker>
</view>
</template>
<script>
import CommonHero from '../../components/common-hero/common-hero.vue'
import { getFormConfig, getModuleMeta } from '../../utils/gxmu/config'
import { listDeclare } from '../../utils/gxmu/declare-service'
export default {
components: {
CommonHero
},
data() {
return {
loading: false,
loadError: '',
declareList: [],
selectedYear: '',
selectedModule: ''
}
},
computed: {
supportedCount() {
return this.declareList.filter((item) => this.isModuleSupported(item.module)).length
},
latestYear() {
const year = this.declareList.reduce((result, item) => {
const current = Number(item.year || 0)
return current > result ? current : result
}, 0)
return year || new Date().getFullYear()
},
yearOptions() {
const set = new Set(
this.declareList
.map((item) => String(item.year || '').trim())
.filter(Boolean)
)
return [...set].sort((left, right) => Number(right) - Number(left))
},
moduleOptions() {
const set = new Set(
this.declareList
.map((item) => String(item.module || '').trim())
.filter(Boolean)
)
return [...set]
},
yearLabels() {
return ['全部年份', ...this.yearOptions.map((item) => `${item}`)]
},
moduleLabels() {
return ['全部模块', ...this.moduleOptions.map((item) => this.getDeclareModuleLabel(item))]
},
modulePickerColumns() {
return [[
{ text: '全部模块', value: '' },
...this.moduleOptions.map((item) => ({
text: this.getDeclareModuleLabel(item),
value: item
}))
]]
},
yearIndex() {
if (!this.selectedYear) {
return 0
}
const index = this.yearOptions.findIndex((item) => item === this.selectedYear)
return index > -1 ? index + 1 : 0
},
moduleIndex() {
if (!this.selectedModule) {
return 0
}
const index = this.moduleOptions.findIndex((item) => item === this.selectedModule)
return index > -1 ? index + 1 : 0
},
filteredDeclareList() {
return this.declareList.filter((item) => {
const yearMatched = !this.selectedYear || String(item.year || '').trim() === this.selectedYear
const moduleMatched = !this.selectedModule || String(item.module || '').trim() === this.selectedModule
return yearMatched && moduleMatched
})
}
},
onLoad() {
this.loadData()
},
onPullDownRefresh() {
this.loadData(true)
},
methods: {
showToast(title) {
uni.showToast({
title,
icon: 'none'
})
},
getModuleMetaSafe(code) {
return getModuleMeta(code) || {
title: code || '未命名模块',
icon: (code || '申').slice(0, 1),
group: '申报项目',
desc: '当前申报项目已开放,点击可查看申报内容。'
}
},
getDeclareTitle(item) {
const title = String(item.title || '').trim()
if (title) {
return title
}
return this.getDeclareModuleLabel(item.module)
},
getDeclareModuleLabel(module) {
return this.getModuleMetaSafe(module).title
},
getDeclareDescription(module) {
return this.getModuleMetaSafe(module).desc
},
getDeclareGroup(module) {
return this.getModuleMetaSafe(module).group
},
isModuleSupported(module) {
return !!getFormConfig(module)
},
getTimestamp(value) {
if (!value) {
return 0
}
const result = new Date(String(value).replace(/-/g, '/')).getTime()
return Number.isNaN(result) ? 0 : result
},
formatDateTime(value) {
if (!value) {
return '-'
}
const date = new Date(String(value).replace(/-/g, '/'))
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())}`
},
handleYearChange(event) {
const index = Number((event.detail && event.detail.value) || 0)
this.selectedYear = index === 0 ? '' : (this.yearOptions[index - 1] || '')
},
handleModuleChange(event) {
const index = Number((event.detail && event.detail.value) || 0)
this.selectedModule = index === 0 ? '' : (this.moduleOptions[index - 1] || '')
},
openModulePicker() {
this.$nextTick(() => {
this.$refs.modulePicker && this.$refs.modulePicker.open()
})
},
confirmModulePicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
this.selectedModule = value
},
noop() {
return
},
syncFilterState() {
if (this.selectedYear && !this.yearOptions.includes(this.selectedYear)) {
this.selectedYear = ''
}
if (this.selectedModule && !this.moduleOptions.includes(this.selectedModule)) {
this.selectedModule = ''
}
},
async loadData(fromPullDown = false) {
if (this.loading) {
return
}
this.loading = true
this.loadError = ''
try {
const result = await listDeclare({ usable: true })
const list = Array.isArray(result) ? result : []
this.declareList = [...list].sort((left, right) => {
const yearDiff = Number(right.year || 0) - Number(left.year || 0)
if (yearDiff !== 0) {
return yearDiff
}
return this.getTimestamp(right.startTime || right.createTime) - this.getTimestamp(left.startTime || left.createTime)
})
this.syncFilterState()
} catch (error) {
this.loadError = (error && error.message) || '申报项目加载失败'
this.showToast(this.loadError)
} finally {
this.loading = false
if (fromPullDown) {
uni.stopPullDownRefresh()
}
}
},
goApply(item) {
if (!item.module) {
this.showToast('当前申报项目缺少模块标识')
return
}
if (!this.isModuleSupported(item.module)) {
this.showToast('当前申报项目在小程序端暂未配置')
return
}
const moduleRouteMap = {
gxmu_tzbcy_form: '/pages/gxmu/tzbcy',
gxmu_qmgc_form: '/pages/gxmu/qmgc',
gxmu_wxxzx_project: '/pages/gxmu/wxxzx'
}
const moduleRoute = moduleRouteMap[item.module]
if (moduleRoute) {
const query = [
`code=${encodeURIComponent(item.module)}`,
`year=${encodeURIComponent(item.year || '')}`,
`declareTitle=${encodeURIComponent(this.getDeclareTitle(item))}`,
`id=${encodeURIComponent(item.id)}`
]
uni.navigateTo({
url: `${moduleRoute}?${query.join('&')}`
})
return
}
const query = [
`code=${encodeURIComponent(item.module)}`,
`year=${encodeURIComponent(item.year || '')}`,
`declareTitle=${encodeURIComponent(this.getDeclareTitle(item))}`
]
uni.navigateTo({
url: `/pages/gxmu/form?${query.join('&')}`
})
},
goStatsCenter() {
uni.navigateTo({
url: '/pages/gxmu/stats'
})
},
goCrossSchoolBoard() {
uni.navigateTo({
url: '/pages/gxmu/cross-school-board'
})
},
goTzbDatabase() {
uni.navigateTo({
url: '/pages/gxmu/tzb-database'
})
}
}
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(20, 150, 242, 0.14), transparent 30%),
linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%);
}
.page-scroll {
height: 100vh;
padding: 28rpx 24rpx 40rpx;
}
.hero-card {
padding: 34rpx 30rpx;
border-radius: 32rpx;
background: linear-gradient(145deg, #0d6fba 0%, #1496f2 56%, #5bc2ff 100%);
box-shadow: 0 18rpx 44rpx rgba(20, 118, 194, 0.2);
color: #f7fcff;
}
.hero-badge {
display: inline-flex;
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.16);
font-size: 22rpx;
letter-spacing: 1rpx;
}
.hero-title {
margin-top: 22rpx;
font-size: 42rpx;
line-height: 1.35;
font-weight: 700;
}
.hero-desc {
margin-top: 16rpx;
font-size: 25rpx;
line-height: 1.7;
color: rgba(255, 247, 244, 0.86);
}
.hero-meta {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16rpx;
margin-top: 28rpx;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 22rpx;
}
.hero-action {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 16rpx 26rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.18);
font-size: 24rpx;
font-weight: 600;
color: #f7fcff;
}
.hero-action--secondary {
background: rgba(13, 36, 61, 0.18);
}
.hero-action--tertiary {
background: rgba(8, 74, 61, 0.24);
}
.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(255, 247, 244, 0.8);
}
.section {
margin-top: 30rpx;
}
.section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
padding: 0 6rpx;
}
.section-title {
font-size: 34rpx;
font-weight: 700;
color: #1f2329;
}
.section-subtitle {
margin-top: 8rpx;
font-size: 22rpx;
color: #8b827d;
}
.refresh-btn {
flex-shrink: 0;
padding: 12rpx 24rpx;
border-radius: 999rpx;
background: rgba(20, 150, 242, 0.08);
color: #1496f2;
font-size: 22rpx;
}
.refresh-btn--loading {
opacity: 0.7;
}
.state-card {
margin-top: 18rpx;
padding: 30rpx 26rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.9);
border: 1rpx solid rgba(20, 150, 242, 0.08);
box-shadow: 0 10rpx 28rpx rgba(34, 94, 142, 0.08);
}
.state-card--error {
border-color: rgba(20, 150, 242, 0.18);
background: rgba(240, 248, 255, 0.96);
}
.state-title {
font-size: 30rpx;
font-weight: 700;
color: #1f2329;
}
.state-desc {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.7;
color: #786e69;
}
.state-action {
margin-top: 18rpx;
font-size: 24rpx;
font-weight: 600;
color: #1496f2;
}
.filter-bar {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 18rpx;
}
.filter-item {
display: block;
}
.filter-trigger {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 84rpx;
padding: 0 20rpx;
border-radius: 22rpx;
background: rgba(255, 255, 255, 0.92);
border: 1rpx solid rgba(21, 84, 173, 0.08);
font-size: 24rpx;
color: #1f2329;
text-align: center;
}
.filter-text {
flex: 1;
min-width: 0;
text-align: center;
}
.filter-arrow {
flex-shrink: 0;
margin-left: 12rpx;
font-size: 20rpx;
color: #7b8ba1;
}
.declare-list {
display: flex;
flex-direction: column;
gap: 18rpx;
margin-top: 18rpx;
}
.declare-card {
padding: 26rpx;
border-radius: 28rpx;
background:
linear-gradient(180deg, rgba(240, 247, 255, 0.95) 0%, #ffffff 44%),
#ffffff;
border: 1rpx solid rgba(21, 84, 173, 0.08);
box-shadow: 0 10rpx 28rpx rgba(76, 49, 35, 0.08);
}
.declare-card--disabled {
opacity: 0.72;
}
.card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
}
.card-main {
flex: 1;
min-width: 0;
}
.card-title {
font-size: 31rpx;
font-weight: 700;
line-height: 1.45;
color: #1f2329;
}
.card-subtitle {
margin-top: 8rpx;
font-size: 22rpx;
line-height: 1.6;
color: #5f6b7a;
}
.card-status {
flex-shrink: 0;
padding: 10rpx 16rpx;
border-radius: 999rpx;
font-size: 20rpx;
font-weight: 600;
}
.card-status--active {
background: #edf5ff;
color: #1554ad;
}
.card-status--disabled {
background: #f5f1ee;
color: #8f837d;
}
.card-year {
display: inline-flex;
align-items: center;
padding: 10rpx 18rpx;
margin-top: 18rpx;
border-radius: 999rpx;
background: #eef5ff;
color: #1554ad;
font-size: 22rpx;
font-weight: 600;
}
.card-desc {
margin-top: 16rpx;
font-size: 24rpx;
line-height: 1.7;
color: #716863;
}
.card-meta {
display: flex;
flex-direction: column;
gap: 12rpx;
margin-top: 18rpx;
}
.meta-row {
padding: 18rpx 20rpx;
border-radius: 22rpx;
background: rgba(246, 248, 250, 0.92);
}
.meta-row-label {
font-size: 20rpx;
color: #8b827d;
}
.meta-row-value {
margin-top: 8rpx;
font-size: 24rpx;
line-height: 1.6;
color: #1f2329;
word-break: break-all;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin-top: 22rpx;
}
.card-tag {
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: #f8f1ea;
color: #8a5a22;
font-size: 22rpx;
}
.card-action {
font-size: 24rpx;
font-weight: 600;
color: #1496f2;
}
</style>