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

432
pages/gxmu/qmgc.vue Normal file
View File

@@ -0,0 +1,432 @@
<template>
<view class="page">
<scroll-view scroll-y class="page-scroll">
<common-hero :title="pageTitle" :use-image-bg="true"></common-hero>
<view class="hero-card">
<!-- <view class="hero-badge">GXMU · 青马工程</view>-->
<view class="hero-title">{{ pageTitle }}</view>
<!-- <view class="hero-desc">按照后台青马工程登记表字段组织包含基础信息奖惩情况培养意向等内容</view>-->
<view class="hero-actions">
<view class="hero-action" @tap="openCreate">立即申报</view>
<view class="hero-action hero-action--secondary" @tap="loadData">刷新列表</view>
</view>
</view>
<view v-if="loading" class="state-card"><view class="state-title">正在加载申报记录...</view></view>
<view v-else-if="!list.length" class="state-card"><view class="state-title">暂无申报记录</view></view>
<view v-else class="card-list">
<view v-for="item in list" :key="item.id" class="data-card" @tap="openEdit(item)">
<view class="card-top">
<view><view class="card-title">{{ item.name || '未命名学员' }}</view><view class="card-subtitle">{{ item.schoolInfo || '-' }}</view></view>
<view class="card-year">{{ item.year || '-' }}</view>
</view>
<view class="meta-line">手机号{{ item.phone || '-' }}</view>
<view class="meta-line">团学职务{{ item.leaguePosition || '-' }}</view>
</view>
</view>
</scroll-view>
<view v-if="showEdit" class="modal-mask" @tap="closeEdit">
<view class="modal-panel" @tap.stop>
<view class="modal-head"><view class="modal-title">{{ current.id ? '编辑青马工程申报' : '新增青马工程申报' }}</view><view class="modal-close" @tap="closeEdit">关闭</view></view>
<scroll-view scroll-y class="modal-body">
<view class="form-section">
<view class="form-section-title">登记表</view>
<view class="field-item"><view class="field-label">年份</view><uv-input class="field-input" :modelValue="current.year" border="none" @input="setField('year', $event)" /></view>
<view class="field-item"><view class="field-label">姓名</view><uv-input class="field-input" :modelValue="current.name" border="none" @input="setField('name', $event)" /></view>
<view class="field-item">
<view class="field-label">性别</view>
<view class="field-picker" @tap="openGenderPicker">
{{ current.gender || '请选择性别' }}
</view>
</view>
<view class="field-item">
<view class="field-label">民族</view>
<picker mode="selector" :range="nationLabels" :value="nationIndex" :disabled="!nationOptions.length" @change="handleNationChange">
<view class="field-picker" :class="{ 'field-picker--disabled': !nationOptions.length }">
{{ current.nation || (nationOptions.length ? '请选择民族' : '民族加载中') }}
</view>
</picker>
</view>
<view class="field-item">
<view class="field-label">出生年月</view>
<view class="field-picker" @tap="openBirthMonthPicker">
{{ current.birthMonth || '请选择出生年月' }}
</view>
</view>
<view class="field-item">
<view class="field-label">政治面貌</view>
<view class="field-picker" :class="{ 'field-picker--disabled': !politicsOptions.length }" @tap="openPoliticsPicker">
{{ current.politics || (politicsOptions.length ? '请选择政治面貌' : '政治面貌加载中') }}
</view>
</view>
<view class="field-item"><view class="field-label">籍贯</view><uv-input class="field-input" :modelValue="current.nativePlace" border="none" @input="setField('nativePlace', $event)" /></view>
<view class="field-item"><view class="field-label">手机号码</view><uv-input class="field-input" :modelValue="current.phone" border="none" @input="setField('phone', $event)" /></view>
<view class="field-item"><view class="field-label">微信号</view><uv-input class="field-input" :modelValue="current.wechat" border="none" @input="setField('wechat', $event)" /></view>
<view class="field-item"><view class="field-label">电子邮箱</view><uv-input class="field-input" :modelValue="current.email" border="none" @input="setField('email', $event)" /></view>
<view class="field-item"><view class="field-label">QQ号</view><uv-input class="field-input" :modelValue="current.qq" border="none" @input="setField('qq', $event)" /></view>
<view class="field-item"><view class="field-label">身份证号</view><uv-input class="field-input" :modelValue="current.idCardNo" border="none" @input="setField('idCardNo', $event)" /></view>
<view class="field-item"><view class="field-label">爱好特长</view><uv-input class="field-input" :modelValue="current.hobby" border="none" @input="setField('hobby', $event)" /></view>
<view class="field-item">
<view class="field-label">是否有志到广西基层工作</view>
<view class="field-picker" @tap="openWillingPicker">
{{ current.willingToWorkInGuangxi || '请选择' }}
</view>
</view>
<view class="field-item"><view class="field-label">学校院系年级专业</view><uv-input class="field-input" :modelValue="current.schoolInfo" border="none" @input="setField('schoolInfo', $event)" /></view>
<view class="field-item"><view class="field-label">担任团学职务情况</view><uv-input class="field-input" :modelValue="current.leaguePosition" border="none" @input="setField('leaguePosition', $event)" /></view>
<view class="field-item"><view class="field-label">个人简历</view><textarea class="field-textarea" :value="current.resume" @input="setField('resume', $event)" /></view>
<view class="field-item"><view class="field-label">奖惩情况</view><textarea class="field-textarea" :value="current.awards" @input="setField('awards', $event)" /></view>
<view class="field-item"><view class="field-label">综合成绩情况</view><textarea class="field-textarea" :value="current.academicPerformance" @input="setField('academicPerformance', $event)" /></view>
</view>
</scroll-view>
<view class="modal-actions"><view class="ghost-btn" @tap="closeEdit">取消</view><view class="primary-btn" @tap="save">保存</view></view>
</view>
</view>
<uv-picker
ref="willingPicker"
keyName="text"
:columns="willingPickerColumns"
:defaultIndex="[willingPickerIndex]"
closeOnClickOverlay
@confirm="confirmWillingPicker"
></uv-picker>
<uv-picker
ref="genderPicker"
title="选择性别"
keyName="text"
:columns="genderPickerColumns"
:defaultIndex="[genderPickerIndex]"
closeOnClickOverlay
@confirm="confirmGenderPicker"
></uv-picker>
<uv-picker
ref="politicsPicker"
title="选择政治面貌"
keyName="text"
:columns="politicsPickerColumns"
:defaultIndex="[politicsPickerIndex]"
closeOnClickOverlay
@confirm="confirmPoliticsPicker"
></uv-picker>
<uv-picker
ref="birthMonthPicker"
title="选择出生年月"
keyName="text"
:columns="birthMonthPickerColumns"
:defaultIndex="birthMonthPickerIndexs"
closeOnClickOverlay
@change="handleBirthMonthPickerChange"
@confirm="confirmBirthMonthPicker"
></uv-picker>
</view>
</template>
<script>
import CommonHero from '../../components/common-hero/common-hero.vue'
import { addQmgcForm, getQmgcForm, listDictData, updateQmgcForm, userPageQmgcForm } from './module-form-service'
const POLITICS_DICT_ID = 172
const NATION_DICT_ID = 173
const createDefaultForm = () => ({
id: undefined,
year: '',
name: '',
gender: '',
nation: '',
photo: '',
birthMonth: '',
politics: '',
nativePlace: '',
phone: '',
wechat: '',
email: '',
qq: '',
idCardNo: '',
hobby: '',
willingToWorkInGuangxi: '',
schoolInfo: '',
leaguePosition: '',
resume: '',
awards: '',
academicPerformance: '',
secondaryLeagueOpinion: '',
secondaryLeagueOpinionDate: '',
secondaryPartyOpinion: '',
secondaryPartyOpinionDate: '',
schoolLeagueOpinion: '',
schoolLeagueOpinionDate: ''
})
const normalizeDictOptions = (list) =>
(Array.isArray(list) ? list : [])
.map((item) => {
const label = String((item && (item.dictDataName || item.label || item.name || item.value)) || '').trim()
return {
label,
value: label
}
})
.filter((item) => item.label)
export default {
components: {
CommonHero
},
data() {
return {
loading: false,
showEdit: false,
pageTitle: '青马工程申报',
fixedYear: '',
list: [],
current: createDefaultForm(),
nationOptions: [],
politicsOptions: [],
willingPickerColumns: [[
{ text: '是', value: '是' },
{ text: '否', value: '否' }
]],
genderPickerColumns: [[
{ text: '男', value: '男' },
{ text: '女', value: '女' }
]],
birthMonthPickerColumns: [[]],
birthMonthPickerIndexs: [0, 0]
}
},
computed: {
nationLabels() {
return this.nationOptions.map((item) => item.label)
},
politicsLabels() {
return this.politicsOptions.map((item) => item.label)
},
nationIndex() {
const index = this.nationOptions.findIndex((item) => item.value === this.current.nation)
return index > -1 ? index : 0
},
politicsIndex() {
const index = this.politicsOptions.findIndex((item) => item.value === this.current.politics)
return index > -1 ? index : 0
},
genderPickerIndex() {
const options = (this.genderPickerColumns[0] || []).map((item) => item.value)
const index = options.indexOf(this.current.gender)
return index > -1 ? index : 0
},
politicsPickerColumns() {
return [
this.politicsOptions.map((item) => ({
text: item.label,
value: item.value
}))
]
},
politicsPickerIndex() {
const options = (this.politicsPickerColumns[0] || []).map((item) => item.value)
const index = options.indexOf(this.current.politics)
return index > -1 ? index : 0
},
willingPickerIndex() {
const options = (this.willingPickerColumns[0] || []).map((item) => item.value)
const index = options.indexOf(this.current.willingToWorkInGuangxi)
return index > -1 ? index : 0
}
},
onLoad(options) {
this.fixedYear = String(options.year || '').trim()
this.pageTitle = decodeURIComponent(options.declareTitle || '青马工程申报').trim() || '青马工程申报'
this.loadDictOptions()
this.loadData()
},
methods: {
async loadDictOptions() {
try {
const [nationList, politicsList] = await Promise.all([
listDictData({ dictId: NATION_DICT_ID }),
listDictData({ dictId: POLITICS_DICT_ID })
])
this.nationOptions = normalizeDictOptions(nationList)
this.politicsOptions = normalizeDictOptions(politicsList)
} catch (error) {
uni.showToast({ title: (error && error.message) || '字典加载失败', icon: 'none' })
}
},
async loadData() {
this.loading = true
try {
const result = await userPageQmgcForm({
page: 1,
limit: 50,
year: this.fixedYear || undefined
})
this.list = Array.isArray(result && result.list) ? result.list : []
} catch (error) {
uni.showToast({ title: (error && error.message) || '加载失败', icon: 'none' })
} finally {
this.loading = false
}
},
openCreate() {
this.current = { ...createDefaultForm(), year: this.fixedYear || '' }
this.showEdit = true
},
async openEdit(item) {
try {
const detail = item && item.id ? await getQmgcForm(item.id) : item
this.current = { ...createDefaultForm(), ...(detail || {}), year: (detail && detail.year) || this.fixedYear || '' }
this.showEdit = true
} catch (error) {
uni.showToast({ title: (error && error.message) || '加载详情失败', icon: 'none' })
}
},
closeEdit() { this.showEdit = false },
setField(key, event) {
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
this.current[key] = value || ''
},
handleNationChange(event) {
const index = Number((event.detail && event.detail.value) || 0)
const option = this.nationOptions[index]
this.current.nation = (option && option.value) || ''
},
openGenderPicker() {
this.$nextTick(() => {
this.$refs.genderPicker && this.$refs.genderPicker.open()
})
},
confirmGenderPicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
this.current.gender = value
},
openPoliticsPicker() {
if (!this.politicsOptions.length) {
return
}
this.$nextTick(() => {
this.$refs.politicsPicker && this.$refs.politicsPicker.open()
})
},
confirmPoliticsPicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
this.current.politics = value
},
buildBirthMonthPickerState(value, nextIndexs = []) {
const nowYear = new Date().getFullYear()
const years = []
for (let year = nowYear; year >= 1950; year -= 1) {
years.push({ text: `${year}`, value: `${year}` })
}
const months = Array.from({ length: 12 }, (_, index) => {
const month = String(index + 1).padStart(2, '0')
return { text: month, value: month }
})
let selectedYear = String(this.current.birthMonth || '').slice(0, 4) || `${nowYear}`
let selectedMonth = String(this.current.birthMonth || '').slice(5, 7) || '01'
if (typeof nextIndexs[0] === 'number' && years[nextIndexs[0]]) {
selectedYear = years[nextIndexs[0]].value
}
if (typeof nextIndexs[1] === 'number' && months[nextIndexs[1]]) {
selectedMonth = months[nextIndexs[1]].value
}
const yearIndex = Math.max(0, years.findIndex((item) => item.value === selectedYear))
const monthIndex = Math.max(0, months.findIndex((item) => item.value === selectedMonth))
return {
columns: [years, months],
indexs: [yearIndex, monthIndex]
}
},
openBirthMonthPicker() {
const pickerState = this.buildBirthMonthPickerState(this.current.birthMonth)
this.birthMonthPickerColumns = pickerState.columns
this.birthMonthPickerIndexs = pickerState.indexs
this.$nextTick(() => {
this.$refs.birthMonthPicker && this.$refs.birthMonthPicker.open()
})
},
handleBirthMonthPickerChange(event) {
const pickerState = this.buildBirthMonthPickerState(this.current.birthMonth, (event && event.indexs) || [])
this.birthMonthPickerColumns = pickerState.columns
this.birthMonthPickerIndexs = pickerState.indexs
},
confirmBirthMonthPicker(event) {
const values = (event && event.value) || []
const year = (values[0] && values[0].value) || ''
const month = (values[1] && values[1].value) || ''
if (year && month) {
this.current.birthMonth = `${year}-${month}`
}
this.birthMonthPickerIndexs = (event && event.indexs) || this.birthMonthPickerIndexs
},
openWillingPicker() {
this.$nextTick(() => {
this.$refs.willingPicker && this.$refs.willingPicker.open()
})
},
confirmWillingPicker(event) {
const values = (event && event.value) || []
const option = values[0] || null
this.current.willingToWorkInGuangxi = (option && option.value) || ''
},
async save() {
try {
const payload = { ...this.current, year: this.fixedYear || this.current.year }
if (payload.id) {
await updateQmgcForm(payload)
} else {
await addQmgcForm(payload)
}
uni.showToast({ title: '保存成功', icon: 'none' })
this.closeEdit()
this.loadData()
} catch (error) {
uni.showToast({ title: (error && error.message) || '保存失败', icon: 'none' })
}
}
}
}
</script>
<style lang="scss" scoped>
.page { min-height: 100vh; background: linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%); }
.page-scroll { height: 100vh; padding: 24rpx; box-sizing: border-box; }
.hero-card,.state-card,.data-card { border-radius: 28rpx; }
.hero-card { padding: 34rpx 36rpx; background: rgba(255,255,255,.5); box-shadow: 0 4rpx 12rpx 0 rgba(174,174,174,.4); backdrop-filter: blur(24rpx); -webkit-backdrop-filter: blur(24rpx); border: 1rpx solid rgba(255,255,255,.5); }
.hero-badge { display: inline-flex; padding: 10rpx 18rpx; border-radius: 999rpx; background: rgba(20,150,242,.1); font-size: 22rpx; color: #1496f2; }
.hero-title { margin-top: 8rpx; padding-left: 10rpx; font-size: 30rpx; font-weight: 700; line-height: 1.35; color: #18a0f7; border-left: 5px solid #0f94ef; }
.hero-desc { margin-top: 20rpx; font-size: 24rpx; line-height: 1.8; color: #7f7f7f; }
.hero-actions { display: flex; gap: 16rpx; margin-top: 24rpx; }
.hero-action { padding: 14rpx 24rpx; border-radius: 999rpx; background: rgba(20,150,242,.12); font-size: 24rpx; color: #1496f2; }
.hero-action--secondary { background: rgba(31,35,41,.08); color: #5f6b76; }
.state-card,.data-card { margin-top: 24rpx; padding: 24rpx; background: rgba(255,255,255,.96); box-shadow: 0 10rpx 28rpx rgba(34,94,142,.08); }
.state-title,.card-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
.card-subtitle,.meta-line { margin-top: 10rpx; font-size: 24rpx; line-height: 1.6; color: #786e69; }
.card-list { margin-top: 24rpx; display: flex; flex-direction: column; gap: 16rpx; }
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16rpx; }
.card-year { padding: 10rpx 16rpx; border-radius: 999rpx; background: #eef5ff; color: #1554ad; font-size: 22rpx; }
.modal-mask { position: fixed; inset: 0; background: rgba(15,23,42,.34); display: flex; align-items: flex-end; z-index: 99; }
.modal-panel { width: 100%; max-height: 92vh; border-radius: 32rpx 32rpx 0 0; background: #fff; display: flex; flex-direction: column; }
.modal-head { display: flex; justify-content: space-between; align-items: center; padding: 24rpx; border-bottom: 1rpx solid #eef1f4; }
.modal-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
.modal-close { font-size: 24rpx; color: #7c8a96; }
.modal-body { max-height: 70vh; padding: 24rpx; box-sizing: border-box; }
.form-section-title { font-size: 28rpx; font-weight: 700; color: #1f2329; margin-bottom: 16rpx; }
.field-item + .field-item { margin-top: 16rpx; }
.field-label { margin-bottom: 10rpx; font-size: 24rpx; color: #5f6b7a; }
.field-picker { width: 100%; box-sizing: border-box; min-height: 84rpx; padding: 20rpx; border-radius: 20rpx; border: 1rpx solid rgba(220,220,220,1); background: #F4FCFF; font-size: 24rpx; color: #1f2329; }
.field-picker--disabled { color: #a8b3bf; background: #f7f9fb; }
.field-textarea { width: 100%; box-sizing: border-box; border-radius: 20rpx; border: 1rpx solid #dce4ec; background: #fff; padding: 20rpx; font-size: 24rpx; color: #1f2329; }
:deep(.field-input.uv-input) { width: 100%; box-sizing: border-box; border-radius: 20rpx; border: 1rpx solid #dce4ec !important; background: #fff !important; padding: 20rpx !important; }
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) { font-size: 24rpx; color: #1f2329; min-height: 40rpx; }
.field-textarea { min-height: 180rpx; }
.modal-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16rpx; padding: 24rpx; border-top: 1rpx solid #eef1f4; }
.ghost-btn,.primary-btn { height: 84rpx; line-height: 84rpx; border-radius: 22rpx; text-align: center; font-size: 26rpx; font-weight: 700; }
.ghost-btn { background: #f6f7fa; color: #50617a; }
.primary-btn { background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%); color: #fff; }
</style>