Files
tuanwei-uniapp/pages/tygl/index.vue
T
weicw-dshsa ffd10ee242 fix(tygl): 学院选项补上 organizationName/organizationId 映射
上一次修复(7245008)只把 listColleges() 的接口从 /gxmu/college 换成了
/system/organization?parentId=24,配套的 normalizeCollegeOptions 仍读
gxmu_college 时代的键名 collegeName/collegeId——这两个键在新接口里都不存在,
每项映射成 {label:'', value:''},随即被末尾的 .filter(item => item.label && item.value !== '')
整列表滤空,学院下拉没有任何可选值。

- normalizeCollegeOptions 的 label/rawLabel 补 organizationName,value 补 organizationId
  (保留旧键,兼容本地草稿缓存)
- 该映射此前无人核对:openCollegePicker 无条件弹面板,空选项表现为「没有匹配的选项」,
  静默吞掉了字段不匹配,不像原来的「只有 2 个选项」那样显眼

实测(本机 API):/system/organization?parentId=24 返回 103 条,修复前映射后 0 条,
修复后 103 条;护理学院(id=54) 仍能带出 69 个班级。班级映射未受影响。
2026-10-12 19:11:22 +08:00

1262 lines
41 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-card">
<view class="section-head">
<view class="section-title">基础信息表单</view>
</view>
<view class="field-list">
<view v-for="field in fields" :key="field.key" class="field-item">
<view class="field-label">
{{ field.label }}
<text v-if="field.required" class="field-required">*</text>
</view>
<view
v-if="field.key === 'gender'"
class="field-picker"
@tap="openGenderPicker(field)"
>
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || field.placeholder }}
</text>
</view>
<picker
v-else-if="field.key === 'nation'"
mode="selector"
:range="nationLabels"
:value="nationIndex"
:disabled="!nationOptions.length"
@change="handleNationChange"
>
<view class="field-picker" :class="{ 'field-picker--disabled': !nationOptions.length }">
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || (nationOptions.length ? field.placeholder : '民族加载中') }}
</text>
</view>
</picker>
<view
v-else-if="field.key === 'politics'"
class="field-picker"
@tap="openPoliticsPicker()"
>
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || field.placeholder }}
</text>
</view>
<view
v-else-if="field.key === 'leaguePosition'"
class="field-picker"
@tap="openLeaguePositionPicker(field)"
>
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || field.placeholder }}
</text>
</view>
<view
v-else-if="field.key === 'collegeId'"
class="field-picker"
@tap="openCollegePicker()"
>
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || field.placeholder }}
</text>
</view>
<view
v-else-if="field.key === 'classId'"
class="field-picker"
@tap="openClassPicker()"
>
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || field.placeholder }}
</text>
</view>
<view
v-else-if="field.key === 'branchOrganizationId'"
class="field-picker"
@tap="openOrganizationPicker()"
>
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || field.placeholder }}
</text>
</view>
<view
v-else-if="field.key === 'archiveInCurrentOrg'"
class="field-picker"
@tap="openArchiveInOrgPicker(field)"
>
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || field.placeholder }}
</text>
</view>
<view
v-else-if="field.key === 'joinMonth'"
class="field-picker"
@tap="openJoinMonthPicker(field)"
>
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || field.placeholder }}
</text>
</view>
<picker
v-else-if="field.type === 'select'"
mode="selector"
:range="field.options"
:value="getSelectIndex(field)"
@change="handleSelectChange(field, $event)"
>
<view class="field-picker">
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || field.placeholder }}
</text>
</view>
</picker>
<view v-else-if="field.type === 'readonly' || field.key === 'phone'"
class="field-picker field-picker--readonly">
<text :class="getFieldDisplayValue(field) ? 'field-value' : 'field-placeholder'">
{{ getFieldDisplayValue(field) || field.placeholder }}
</text>
</view>
<uv-input
v-else
class="field-native-input"
:type="field.type === 'phone' ? 'number' : 'text'"
:value="formData[field.key]"
:placeholder="field.placeholder"
placeholder-style="color: #c0c4cc;"
:maxlength="-1"
border="none"
@input="handleTextInput(field, $event)"
/>
</view>
</view>
</view>
<view class="action-card">
<button class="ghost-btn" @click="saveDraft">保存草稿</button>
<button class="primary-btn" @click="submitForm">提交档案</button>
</view>
</scroll-view>
<view v-if="searchPickerVisible" class="option-mask" @tap="closeSearchPicker">
<view class="option-popup" @tap.stop>
<view class="option-popup-head">
<view class="option-popup-btn" @tap="closeSearchPicker">取消</view>
<view class="option-popup-title">{{ searchPickerTitle }}</view>
<view class="option-popup-btn option-popup-btn--primary" @tap="confirmSearchPicker">确定</view>
</view>
<uv-input
:value="searchPickerKeyword"
class="option-search"
:placeholder="`搜索${searchPickerTitle}`"
border="none"
@input="handleSearchPickerInput"
/>
<view v-if="filteredSearchPickerOptions.length" class="option-picker-wrap">
<picker-view class="option-picker-view" :value="[searchPickerIndex]" @change="handleSearchPickerChange">
<picker-view-column>
<view v-for="item in filteredSearchPickerOptions" :key="`${searchPickerMode}-${item.value}`"
class="option-picker-item">
{{ item.label }}
</view>
</picker-view-column>
</picker-view>
</view>
<view v-else class="option-empty">没有匹配的选项</view>
</view>
</view>
<uv-picker
ref="genderPicker"
title="选择性别"
:columns="genderPickerColumns"
:defaultIndex="[genderPickerIndex]"
keyName="text"
@close="closeGenderPicker"
@cancel="closeGenderPicker"
@confirm="confirmGenderPicker"
></uv-picker>
<uv-picker
ref="leaguePositionPicker"
title="选择团内职务"
:columns="leaguePositionPickerColumns"
:defaultIndex="[leaguePositionPickerIndex]"
keyName="text"
@close="closeLeaguePositionPicker"
@cancel="closeLeaguePositionPicker"
@confirm="confirmLeaguePositionPicker"
></uv-picker>
<uv-picker
ref="organizationPicker"
title="选择所属团支部"
:columns="organizationPickerColumns"
:defaultIndex="organizationPickerIndexs"
keyName="text"
@change="handleOrganizationPickerChange"
@close="closeOrganizationPicker"
@cancel="closeOrganizationPicker"
@confirm="confirmOrganizationPicker"
></uv-picker>
<uv-picker
ref="archiveInOrgPicker"
title="选择团籍是否在本组织"
:columns="archiveInOrgPickerColumns"
:defaultIndex="[archiveInOrgPickerIndex]"
keyName="text"
@close="closeArchiveInOrgPicker"
@cancel="closeArchiveInOrgPicker"
@confirm="confirmArchiveInOrgPicker"
></uv-picker>
<uv-picker
ref="joinMonthPicker"
title="选择入团年月"
:columns="joinMonthPickerColumns"
:defaultIndex="joinMonthPickerIndexs"
keyName="text"
@change="handleJoinMonthPickerChange"
@close="closeJoinMonthPicker"
@cancel="closeJoinMonthPicker"
@confirm="confirmJoinMonthPicker"
></uv-picker>
</view>
</template>
<script>
import CommonHero from '../../components/common-hero/common-hero.vue'
import {
getCurrentTyglForm,
getCurrentUser,
getUserProfile,
listClasses,
listColleges,
listDictData,
listOrganizations,
saveTyglForm,
updateTyglForm
} from './service'
const STORAGE_KEY = 'tygl_member_archive_draft'
const POLITICS_DICT_ID = 172
const NATION_DICT_ID = 173
const LEAGUE_POSITION_DICT_ID = 174
const FALLBACK_NATION_OPTIONS = [
'汉族', '蒙古族', '回族', '藏族', '维吾尔族', '苗族', '彝族', '壮族', '布依族', '朝鲜族',
'满族', '侗族', '瑶族', '白族', '土家族', '哈尼族', '哈萨克族', '傣族', '黎族', '傈僳族',
'佤族', '畲族', '高山族', '拉祜族', '水族', '东乡族', '纳西族', '景颇族', '柯尔克孜族', '土族',
'达斡尔族', '仫佬族', '羌族', '布朗族', '撒拉族', '毛南族', '仡佬族', '锡伯族', '阿昌族', '普米族',
'塔吉克族', '怒族', '乌孜别克族', '俄罗斯族', '鄂温克族', '德昂族', '保安族', '裕固族', '京族', '塔塔尔族',
'独龙族', '鄂伦春族', '赫哲族', '门巴族', '珞巴族', '基诺族'
]
const FALLBACK_POLITICS_OPTIONS = ['中共党员', '中共预备党员', '共青团员', '群众', '民盟盟员', '民建会员', '其他']
const FALLBACK_LEAGUE_POSITION_OPTIONS = [
'普通团员',
'团支部书记',
'团支部副书记',
'组织委员',
'宣传委员',
'纪检委员',
'学习委员',
'文体委员',
'班级团支书',
'院级团学干部',
'校级团学干部',
'其他'
]
const fields = [
{key: 'name', label: '姓名', required: true, type: 'text', placeholder: '请输入姓名'},
{key: 'gender', label: '性别', type: 'select', placeholder: '请选择性别'},
{key: 'nation', label: '民族', type: 'search-select', placeholder: '请选择民族'},
{key: 'politics', label: '政治面貌', required: true, type: 'search-select', placeholder: '请选择政治面貌'},
{
key: 'collegeId',
label: '学院',
required: true,
type: 'college',
placeholder: '请选择学院',
displayKey: 'collegeName'
},
// 班级选填:研究生在开放平台体系中本就没有班级(平台 1584 个班级里一个研究生班都没有),
// 强制必填会把 15538 名研究生挡在团员档案之外
{key: 'classId', label: '班级', required: false, type: 'class', placeholder: '请选择班级(研究生可不填)', displayKey: 'className'},
{key: 'phone', label: '手机号码', type: 'readonly', placeholder: '自动读取当前用户手机号'},
{key: 'leaguePosition', label: '团内职务', type: 'text', placeholder: '请选择团内职务'},
{
key: 'branchOrganizationId',
label: '所属团支部',
type: 'organization',
placeholder: '请选择所属团支部',
displayKey: 'branchOrganizationName'
},
{key: 'idCardNo', label: '身份证号', required: true, type: 'text', placeholder: '请输入身份证号'},
{key: 'birthDate', label: '出生日期', type: 'readonly', placeholder: '将根据身份证号自动读取'},
{key: 'archiveInCurrentOrg', label: '团籍是否在本组织', type: 'select', options: ['是', '否'], placeholder: '请选择'},
{key: 'joinMonth', label: '入团年月', type: 'month', placeholder: '请选择入团年月'}
]
const createDefaultFormData = () => ({
name: '',
gender: '',
nation: '',
politics: '',
collegeId: '',
collegeName: '',
college: '',
classId: '',
className: '',
phone: '',
leaguePosition: '',
branchOrganizationId: '',
branchOrganizationName: '',
idCardNo: '',
birthDate: '',
archiveInCurrentOrg: '',
joinMonth: ''
})
const normalizeOptions = (list) =>
(list || []).map((item) => ({
label: item.label || item,
value: item.value || item
}))
export default {
components: {
CommonHero
},
data() {
return {
fields,
formId: null,
formData: createDefaultFormData(),
draftSavedAt: '',
nationOptions: normalizeOptions(FALLBACK_NATION_OPTIONS),
politicsOptions: normalizeOptions(FALLBACK_POLITICS_OPTIONS),
leaguePositionOptions: normalizeOptions(FALLBACK_LEAGUE_POSITION_OPTIONS),
collegeOptions: [],
classOptions: [],
organizationTree: [],
organizationOptions: [],
organizationPickerColumns: [],
organizationPickerIndexs: [],
archiveInOrgFieldKey: 'archiveInCurrentOrg',
archiveInOrgPickerColumns: [[
{text: '是', value: '是'},
{text: '否', value: '否'}
]],
joinMonthFieldKey: 'joinMonth',
joinMonthPickerColumns: [],
joinMonthPickerIndexs: [],
searchPickerVisible: false,
searchPickerTitle: '',
searchPickerMode: '',
searchPickerKeyword: '',
searchPickerOptions: [],
searchPickerIndex: 0,
searchPickerTempOption: null,
genderFieldKey: 'gender',
genderPickerColumns: [[
{text: '男', value: '男'},
{text: '女', value: '女'}
]],
leaguePositionFieldKey: 'leaguePosition',
leaguePositionPickerColumns: [normalizeOptions(FALLBACK_LEAGUE_POSITION_OPTIONS).map((item) => ({
text: item.label,
value: item.value
}))]
}
},
computed: {
nationLabels() {
return this.nationOptions.map((item) => item.label)
},
nationIndex() {
const index = this.nationOptions.findIndex((item) => item.value === this.formData.nation)
return index > -1 ? index : 0
},
filteredSearchPickerOptions() {
const keyword = String(this.searchPickerKeyword || '').trim().toLowerCase()
if (!keyword) {
return this.searchPickerOptions
}
return this.searchPickerOptions.filter((item) => String(item.label || '').toLowerCase().indexOf(keyword) > -1)
},
genderPickerIndex() {
const options = (this.genderPickerColumns[0] || []).map((item) => item.value)
const index = options.indexOf(this.formData[this.genderFieldKey])
return index > -1 ? index : 0
},
leaguePositionPickerIndex() {
const options = (this.leaguePositionPickerColumns[0] || []).map((item) => item.value)
const index = options.indexOf(this.formData[this.leaguePositionFieldKey])
return index > -1 ? index : 0
},
archiveInOrgPickerIndex() {
const options = (this.archiveInOrgPickerColumns[0] || []).map((item) => item.value)
const index = options.indexOf(this.formData[this.archiveInOrgFieldKey])
return index > -1 ? index : 0
}
},
watch: {
'formData.idCardNo'() {
this.syncBirthDateFromIdCard()
}
},
onLoad() {
this.restoreDraft()
this.fillCurrentUserPhone()
this.loadFormOptions()
this.loadCurrentTyglForm()
this.syncBirthDateFromIdCard()
const joinMonthPickerState = this.buildJoinMonthPickerState(this.formData.joinMonth)
this.joinMonthPickerColumns = joinMonthPickerState.columns
this.joinMonthPickerIndexs = joinMonthPickerState.indexs
},
methods: {
async loadCurrentTyglForm() {
try {
const result = await getCurrentTyglForm()
if (!result || !result.id) {
return
}
this.formId = result.id
this.formData = {
...this.formData,
...result,
collegeName: result.collegeName || result.college || this.formData.collegeName,
college: result.college || result.collegeName || this.formData.college,
className: result.className || this.formData.className
}
this.syncBirthDateFromIdCard()
const joinMonthPickerState = this.buildJoinMonthPickerState(this.formData.joinMonth)
this.joinMonthPickerColumns = joinMonthPickerState.columns
this.joinMonthPickerIndexs = joinMonthPickerState.indexs
if (this.formData.collegeId) {
await this.loadClassOptions(this.formData.collegeId)
}
} catch (error) {
}
},
buildSubmitPayload() {
return {
...(this.formId ? {id: this.formId} : {}),
name: this.formData.name,
gender: this.formData.gender,
nation: this.formData.nation,
college: this.formData.collegeName || this.formData.college,
className: this.formData.className,
politics: this.formData.politics,
phone: this.formData.phone,
leaguePosition: this.formData.leaguePosition,
branchOrganizationId: this.formData.branchOrganizationId || null,
branchOrganizationName: this.formData.branchOrganizationName,
idCardNo: this.formData.idCardNo,
birthDate: this.formData.birthDate,
archiveInCurrentOrg: this.formData.archiveInCurrentOrg,
joinMonth: this.formData.joinMonth
}
},
buildJoinMonthPickerState(currentValue) {
const currentDate = new Date()
const currentYear = currentDate.getFullYear()
const years = []
for (let year = 2000; year <= currentYear + 1; year += 1) {
years.push({
text: `${year}年`,
value: String(year)
})
}
const months = []
for (let month = 1; month <= 12; month += 1) {
months.push({
text: `${String(month).padStart(2, '0')}月`,
value: String(month).padStart(2, '0')
})
}
let yearIndex = years.length - 1
let monthIndex = 0
const normalizedValue = String(currentValue || '').trim()
if (/^\d{4}-\d{2}$/.test(normalizedValue)) {
const [yearValue, monthValue] = normalizedValue.split('-')
const nextYearIndex = years.findIndex((item) => item.value === yearValue)
const nextMonthIndex = months.findIndex((item) => item.value === monthValue)
yearIndex = nextYearIndex > -1 ? nextYearIndex : yearIndex
monthIndex = nextMonthIndex > -1 ? nextMonthIndex : monthIndex
}
return {
columns: [years, months],
indexs: [yearIndex, monthIndex]
}
},
async fillCurrentUserPhone() {
const tokenUser = getCurrentUser()
if (tokenUser && tokenUser.phone) {
this.formData.phone = tokenUser.phone
}
try {
const profile = await getUserProfile()
if (profile && profile.phone) {
this.formData.phone = profile.phone
}
} catch (error) {
}
},
showToast(title) {
uni.showToast({
title,
icon: 'none'
})
},
getFieldDisplayValue(field) {
if (field.displayKey) {
return this.formData[field.displayKey]
}
return this.formData[field.key]
},
async loadFormOptions() {
try {
const [politicsList, nationList, leaguePositionList, organizationList, collegeList] = await Promise.all([
listDictData({dictId: POLITICS_DICT_ID}).catch(() => []),
listDictData({dictId: NATION_DICT_ID}).catch(() => []),
listDictData({dictId: LEAGUE_POSITION_DICT_ID}).catch(() => []),
listOrganizations().catch(() => []),
listColleges().catch(() => [])
])
if (Array.isArray(politicsList) && politicsList.length) {
this.politicsOptions = normalizeOptions(
politicsList
.filter((item) => item && item.dictDataName)
.map((item) => ({label: item.dictDataName, value: item.dictDataName}))
)
}
if (Array.isArray(nationList) && nationList.length) {
this.nationOptions = normalizeOptions(
nationList
.filter((item) => item && item.dictDataName)
.map((item) => ({label: item.dictDataName, value: item.dictDataName}))
)
}
if (Array.isArray(leaguePositionList) && leaguePositionList.length) {
this.leaguePositionOptions = normalizeOptions(
leaguePositionList
.filter((item) => item && item.dictDataName)
.map((item) => ({label: item.dictDataName, value: item.dictDataName}))
)
this.leaguePositionPickerColumns = [this.leaguePositionOptions.map((item) => ({
text: item.label,
value: item.value
}))]
}
if (Array.isArray(organizationList) && organizationList.length) {
this.organizationTree = this.normalizeOrganizationTree(organizationList)
this.organizationOptions = this.flattenOrganizations(organizationList)
const organizationPickerState = this.buildOrganizationPickerState(
this.findOrganizationPathByValue(this.organizationTree, this.formData.branchOrganizationId) || []
)
this.organizationPickerColumns = organizationPickerState.columns
this.organizationPickerIndexs = organizationPickerState.indexs
}
if (Array.isArray(collegeList) && collegeList.length) {
this.collegeOptions = this.normalizeCollegeOptions(collegeList)
if (this.formData.collegeId) {
await this.loadClassOptions(this.formData.collegeId)
}
}
} catch (error) {
console.warn('tygl options load failed', error)
}
},
// 学院源是 /system/organization(见 docs/adr/0002),字段名是 organizationName/organizationId,
// 不是 gxmu_college 时代的 collegeName/collegeId:漏了这两个键会让整个列表被下面的 filter 过滤成空。
normalizeCollegeOptions(list) {
return (Array.isArray(list) ? list : [])
.map((item) => ({
label: item.collegeName || item.organizationName || item.name || item.label || '',
value: Number(item.collegeId || item.organizationId || item.id || item.value || 0) || String(item.collegeId || item.organizationId || item.id || item.value || ''),
rawLabel: item.collegeName || item.organizationName || item.name || item.label || ''
}))
.filter((item) => item.label && item.value !== '')
},
normalizeClassOptions(list) {
return (Array.isArray(list) ? list : [])
.map((item) => ({
label: item.className || item.name || item.label || '',
value: Number(item.classId || item.id || item.value || 0) || String(item.classId || item.id || item.value || ''),
rawLabel: item.className || item.name || item.label || ''
}))
.filter((item) => item.label && item.value !== '')
},
normalizeOrganizationTree(list) {
return (Array.isArray(list) ? list : []).map((item) => ({
text: item.organizationName || item.name || item.label || '',
value: Number(item.organizationId || item.id || item.value || 0) || String(item.organizationId || item.id || item.value || ''),
rawLabel: item.organizationName || item.name || item.label || '',
children: this.normalizeOrganizationTree(item.children || [])
})).filter((item) => item.text && item.value !== '')
},
async loadClassOptions(collegeId) {
if (!collegeId) {
this.classOptions = []
return
}
try {
const classList = await listClasses({collegeId})
this.classOptions = this.normalizeClassOptions(classList)
} catch (error) {
this.classOptions = []
}
},
flattenOrganizations(list) {
const source = Array.isArray(list) ? list : []
const hasChildren = source.some((item) => Array.isArray(item.children) && item.children.length)
if (hasChildren) {
return this.flattenOrganizationTree(source)
}
const map = {}
source.forEach((item) => {
map[item.organizationId] = {
...item,
children: []
}
})
const roots = []
source.forEach((item) => {
const current = map[item.organizationId]
const parentId = Number(item.parentId || 0)
if (parentId && map[parentId]) {
map[parentId].children.push(current)
} else {
roots.push(current)
}
})
return this.flattenOrganizationTree(roots)
},
flattenOrganizationTree(list, parentPath = '') {
const result = []
;(list || []).forEach((item) => {
const currentName = item.organizationName || ''
const label = parentPath ? `${parentPath} / ${currentName}` : currentName
if (item.organizationId) {
result.push({
label,
value: Number(item.organizationId),
rawLabel: currentName
})
}
if (Array.isArray(item.children) && item.children.length) {
result.push(...this.flattenOrganizationTree(item.children, label))
}
})
return result
},
restoreDraft() {
const draft = uni.getStorageSync(STORAGE_KEY)
if (!draft || !draft.formData) {
return
}
this.formData = {
...createDefaultFormData(),
...draft.formData
}
if (!this.formData.collegeName && this.formData.college) {
this.formData.collegeName = this.formData.college
}
this.draftSavedAt = draft.savedAt || ''
},
handleSelectChange(field, event) {
const index = Number(event.detail.value)
this.formData[field.key] = field.options[index] || ''
},
handleDateChange(field, event) {
this.formData[field.key] = event.detail.value
},
handleTextInput(field, event) {
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
this.formData[field.key] = value || ''
},
openGenderPicker(field) {
this.genderFieldKey = field.key
this.$nextTick(() => {
this.$refs.genderPicker && this.$refs.genderPicker.open()
})
},
closeGenderPicker() {
return
},
confirmGenderPicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
this.formData[this.genderFieldKey] = value
this.closeGenderPicker()
},
openLeaguePositionPicker(field) {
this.leaguePositionFieldKey = field.key
this.$nextTick(() => {
this.$refs.leaguePositionPicker && this.$refs.leaguePositionPicker.open()
})
},
closeLeaguePositionPicker() {
return
},
confirmLeaguePositionPicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
this.formData[this.leaguePositionFieldKey] = value
this.closeLeaguePositionPicker()
},
openArchiveInOrgPicker(field) {
this.archiveInOrgFieldKey = field.key
this.$nextTick(() => {
this.$refs.archiveInOrgPicker && this.$refs.archiveInOrgPicker.open()
})
},
closeArchiveInOrgPicker() {
return
},
confirmArchiveInOrgPicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
this.formData[this.archiveInOrgFieldKey] = value
this.closeArchiveInOrgPicker()
},
openJoinMonthPicker(field) {
this.joinMonthFieldKey = field.key
const pickerState = this.buildJoinMonthPickerState(this.formData[this.joinMonthFieldKey])
this.joinMonthPickerColumns = pickerState.columns
this.joinMonthPickerIndexs = pickerState.indexs
this.$nextTick(() => {
this.$refs.joinMonthPicker && this.$refs.joinMonthPicker.open()
})
},
closeJoinMonthPicker() {
return
},
handleJoinMonthPickerChange(event) {
this.joinMonthPickerIndexs = (event && event.indexs) || this.joinMonthPickerIndexs
},
confirmJoinMonthPicker(event) {
const values = (event && event.value) || []
const yearValue = (values[0] && values[0].value) || ''
const monthValue = (values[1] && values[1].value) || ''
if (yearValue && monthValue) {
this.formData[this.joinMonthFieldKey] = `${yearValue}-${monthValue}`
}
this.joinMonthPickerIndexs = (event && event.indexs) || this.joinMonthPickerIndexs
this.closeJoinMonthPicker()
},
handleNationChange(event) {
const index = Number(event.detail.value)
const option = this.nationOptions[index]
this.formData.nation = (option && option.value) || ''
},
openPoliticsPicker() {
this.openSearchPicker({
title: '政治面貌',
mode: 'politics',
options: this.politicsOptions,
currentValue: this.formData.politics
})
},
openOrganizationPicker() {
if (!this.organizationTree.length) {
this.showToast('暂无团支部数据')
return
}
const pickerState = this.buildOrganizationPickerState(
this.findOrganizationPathByValue(this.organizationTree, this.formData.branchOrganizationId) || []
)
this.organizationPickerColumns = pickerState.columns
this.organizationPickerIndexs = pickerState.indexs
this.$nextTick(() => {
this.$refs.organizationPicker && this.$refs.organizationPicker.open()
})
},
closeOrganizationPicker() {
return
},
handleOrganizationPickerChange(event) {
const pickerState = this.buildOrganizationPickerState((event && event.indexs) || [])
this.organizationPickerColumns = pickerState.columns
this.organizationPickerIndexs = pickerState.indexs
},
confirmOrganizationPicker(event) {
const pickerState = this.buildOrganizationPickerState((event && event.indexs) || this.organizationPickerIndexs || [])
const pathNodes = pickerState.pathNodes || []
const selectedNode = pathNodes[pathNodes.length - 1]
if (!selectedNode) {
this.closeOrganizationPicker()
return
}
this.formData.branchOrganizationId = selectedNode.value
this.formData.branchOrganizationName = pathNodes.map((item) => item.rawLabel || item.text).filter(Boolean).join(' / ')
this.organizationPickerIndexs = pickerState.indexs
this.closeOrganizationPicker()
},
findOrganizationPathByValue(list, targetValue, path = []) {
const source = Array.isArray(list) ? list : []
const normalizedTarget = String(targetValue || '')
for (let index = 0; index < source.length; index += 1) {
const item = source[index]
const nextPath = path.concat(index)
if (String(item.value) === normalizedTarget) {
return nextPath
}
if (Array.isArray(item.children) && item.children.length) {
const result = this.findOrganizationPathByValue(item.children, targetValue, nextPath)
if (result) {
return result
}
}
}
return null
},
buildOrganizationPickerState(indexs = []) {
const columns = []
const normalizedIndexs = []
const pathNodes = []
let currentList = Array.isArray(this.organizationTree) ? this.organizationTree : []
let depth = 0
while (Array.isArray(currentList) && currentList.length) {
columns.push(currentList.map((item) => ({
text: item.text,
value: item.value,
rawLabel: item.rawLabel
})))
let currentIndex = Number(indexs[depth] || 0)
if (currentIndex < 0 || currentIndex >= currentList.length) {
currentIndex = 0
}
normalizedIndexs.push(currentIndex)
const currentNode = currentList[currentIndex]
if (!currentNode) {
break
}
pathNodes.push(currentNode)
currentList = Array.isArray(currentNode.children) ? currentNode.children : []
depth += 1
}
return {
columns,
indexs: normalizedIndexs,
pathNodes
}
},
openCollegePicker() {
this.openSearchPicker({
title: '学院',
mode: 'college',
options: this.collegeOptions,
currentValue: this.formData.collegeId
})
},
openClassPicker() {
if (!this.formData.collegeId) {
this.showToast('请先选择学院')
return
}
this.openSearchPicker({
title: '班级',
mode: 'class',
options: this.classOptions,
currentValue: this.formData.classId
})
},
openSearchPicker({title, mode, options, currentValue}) {
this.searchPickerTitle = title
this.searchPickerMode = mode
this.searchPickerOptions = Array.isArray(options) ? options : []
this.searchPickerKeyword = ''
this.searchPickerVisible = true
this.$nextTick(() => {
this.syncSearchPickerValue(currentValue)
})
},
closeSearchPicker() {
this.searchPickerVisible = false
},
syncSearchPickerValue(currentValue) {
const list = this.filteredSearchPickerOptions
if (!list.length) {
this.searchPickerIndex = 0
this.searchPickerTempOption = null
return
}
let index = list.findIndex((item) => item.value === currentValue || item.label === currentValue)
if (index < 0) {
index = 0
}
this.searchPickerIndex = index
this.searchPickerTempOption = list[index]
},
handleSearchPickerInput(event) {
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
this.searchPickerKeyword = value || ''
this.$nextTick(() => {
const currentValue = this.searchPickerTempOption ? this.searchPickerTempOption.value : ''
this.syncSearchPickerValue(currentValue)
})
},
handleSearchPickerChange(event) {
const index = Number((event.detail.value || [0])[0] || 0)
this.searchPickerIndex = index
this.searchPickerTempOption = this.filteredSearchPickerOptions[index] || null
},
async confirmSearchPicker() {
const option = this.searchPickerTempOption
if (!option) {
this.closeSearchPicker()
return
}
if (this.searchPickerMode === 'politics') {
this.formData.politics = option.value
} else if (this.searchPickerMode === 'college') {
const nextCollegeId = option.value
const nextCollegeName = option.label
const collegeChanged = String(this.formData.collegeId || '') !== String(nextCollegeId || '')
this.formData.collegeId = nextCollegeId
this.formData.collegeName = nextCollegeName
this.formData.college = nextCollegeName
if (collegeChanged) {
this.formData.classId = ''
this.formData.className = ''
this.classOptions = []
await this.loadClassOptions(nextCollegeId)
}
} else if (this.searchPickerMode === 'class') {
this.formData.classId = option.value
this.formData.className = option.label
}
this.closeSearchPicker()
},
getSelectIndex(field) {
const currentValue = this.formData[field.key]
const index = field.options.indexOf(currentValue)
return index > -1 ? index : 0
},
parseBirthDateFromIdCard(idCardNo) {
const value = String(idCardNo || '').trim().toUpperCase()
let rawBirth = ''
if (/^\d{17}[\dX]$/.test(value)) {
rawBirth = value.slice(6, 14)
} else if (/^\d{15}$/.test(value)) {
rawBirth = `19${value.slice(6, 12)}`
} else {
return ''
}
const year = rawBirth.slice(0, 4)
const month = rawBirth.slice(4, 6)
const day = rawBirth.slice(6, 8)
const date = new Date(`${year}/${month}/${day}`)
if (Number.isNaN(date.getTime())) {
return ''
}
return `${year}-${month}-${day}`
},
syncBirthDateFromIdCard() {
this.formData.birthDate = this.parseBirthDateFromIdCard(this.formData.idCardNo)
},
validateForm() {
for (const field of this.fields) {
if (field.required && !String(this.getFieldDisplayValue(field) || '').trim()) {
this.showToast(`请完善:${field.label}`)
return false
}
}
if (this.formData.phone && !/^1\d{10}$/.test(this.formData.phone)) {
this.showToast('请输入正确的手机号码')
return false
}
if (this.formData.idCardNo && !this.formData.birthDate) {
this.showToast('身份证号格式不正确,无法识别出生日期')
return false
}
// 班级为选填:研究生在开放平台没有班级,此处不再强制要求
return true
},
saveDraft() {
const savedAt = this.formatNow()
uni.setStorageSync(STORAGE_KEY, {
formData: this.formData,
savedAt
})
this.draftSavedAt = savedAt
uni.showToast({
title: '草稿已保存',
icon: 'success'
})
},
async submitForm() {
if (!this.validateForm()) {
return
}
try {
const payload = this.buildSubmitPayload()
const result = this.formId
? await updateTyglForm(payload)
: await saveTyglForm(payload)
if (!this.formId && result && result.id) {
this.formId = result.id
}
const savedAt = this.formatNow()
uni.setStorageSync(STORAGE_KEY, {
formData: this.formData,
savedAt,
status: 'submitted'
})
this.draftSavedAt = savedAt
uni.showModal({
title: '提交成功',
content: this.formId ? '团员档案已更新。' : '团员档案已创建。',
showCancel: false
})
} catch (error) {
this.showToast((error && error.message) || '保存失败')
}
},
formatNow() {
const now = new Date()
const pad = (value) => String(value).padStart(2, '0')
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`
}
}
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: rgba(241, 241, 241, 1);
//radial-gradient(circle at top right, rgba(20, 150, 242, 0.14), transparent 30%),
//linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%);
}
.page-scroll {
height: 100vh;
padding: 0 0 40rpx;
}
.section-card{
border-radius: 30rpx;
background: rgba(255, 255, 255, 1);
}
.action-card {
border-radius: 30rpx;
//background: rgba(255, 255, 255, 0.94);
//border: 1rpx solid rgba(20, 150, 242, 0.08);
//box-shadow: 0 10rpx 28rpx rgba(34, 94, 142, 0.08);
}
.section-card,
.action-card {
margin-top: 24rpx;
padding: 26rpx;
margin-left: 24rpx;
margin-right: 24rpx;
}
.section-head {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 18rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #1f2329;
}
.field-list {
display: flex;
flex-direction: column;
gap: 22rpx;
}
.field-label {
margin-bottom: 12rpx;
font-size: 25rpx;
font-weight: 600;
color: #2b3037;
}
.field-required {
margin-left: 8rpx;
color: #1496f2;
}
.field-picker {
width: 100%;
box-sizing: border-box;
padding: 22rpx 24rpx;
border-radius: 22rpx;
background: #F4FCFF;
border: 1rpx solid rgba(220,220,220,1);
font-size: 24rpx;
color: #2a2e35;
}
.field-picker--readonly {
background: #f9fafb;
}
:deep(.field-native-input.uv-input) {
width: 100%;
height: 88rpx;
padding: 0 24rpx !important;
border-radius: 12rpx;
border: 1px solid #dcdfe6 !important;
background: #ffffff !important;
box-sizing: border-box;
}
:deep(.field-native-input.uv-input .uv-input__content__field-wrapper__field) {
font-size: 28rpx;
line-height: 88rpx;
color: #303133;
}
.field-placeholder {
color: #aa9f98;
}
.field-value {
color: #2a2e35;
}
.action-card {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18rpx;
}
.option-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 99;
display: flex;
align-items: flex-end;
background: rgba(15, 23, 42, 0.34);
}
.option-popup {
width: 100%;
padding: 24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
border-radius: 32rpx 32rpx 0 0;
background: #fff;
}
.option-popup-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.option-popup-title {
font-size: 28rpx;
font-weight: 700;
color: #1f2329;
}
.option-popup-btn {
font-size: 24rpx;
color: #7c736e;
}
.option-popup-btn--primary {
color: #1496f2;
font-weight: 600;
}
:deep(.option-search.uv-input) {
width: 100%;
height: 84rpx;
margin-top: 20rpx;
padding: 0 24rpx !important;
border-radius: 22rpx;
background: #f8f4f1 !important;
border: 1rpx solid #eadfd7 !important;
}
:deep(.option-search.uv-input .uv-input__content__field-wrapper__field) {
font-size: 24rpx;
color: #2a2e35;
}
.option-picker-wrap {
margin-top: 20rpx;
border-radius: 24rpx;
background: #fafcff;
overflow: hidden;
}
.option-picker-view {
width: 100%;
height: 420rpx;
}
.option-picker-item {
display: flex;
align-items: center;
justify-content: center;
height: 84rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #2a2e35;
text-align: center;
}
.option-empty {
padding: 60rpx 0 40rpx;
text-align: center;
font-size: 24rpx;
color: #aa9f98;
}
.ghost-btn,
.primary-btn {
height: 88rpx;
line-height: 88rpx;
border-radius: 22rpx;
font-size: 28rpx;
font-weight: 700;
margin: 0;
}
.ghost-btn {
background: #FFA2DA;
color: #ffffff;
border: none;
border-radius: 999px;
}
.primary-btn {
background: linear-gradient(147.22deg, rgba(0, 180, 255, 1) 37.65%, rgba(88, 206, 255, 1) 80.42%);
color: #fff;
border-radius: 999px;
}
</style>