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

1261 lines
41 KiB
Vue

<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'
},
{key: 'classId', label: '班级', required: true, 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)
}
},
normalizeCollegeOptions(list) {
return (Array.isArray(list) ? list : [])
.map((item) => ({
label: item.collegeName || item.name || item.label || '',
value: Number(item.collegeId || item.id || item.value || 0) || String(item.collegeId || item.id || item.value || ''),
rawLabel: item.collegeName || 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
}
if (this.formData.collegeId && !this.formData.classId) {
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>