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

697
pages/gxmu/index.vue Normal file
View File

@@ -0,0 +1,697 @@
<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>