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

893 lines
29 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 v-if="formConfig" class="page">
<scroll-view scroll-y class="page-scroll">
<common-hero :title="pageTitle" :use-image-bg="true"></common-hero>
<view class="hero-wrap">
<view class="hero-card">
<view class="hero-title">{{ pageTitle }}</view>
<view class="hero-desc">{{ moduleMeta && moduleMeta.desc ? moduleMeta.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>
<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 class="state-desc">点击上方立即申报开始填写当前模块</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">{{ getRecordTitle(item) }}</view>
<view class="card-subtitle">{{ getRecordSubtitle(item) }}</view>
</view>
<view class="card-year">{{ item.year || fixedYear || '-' }}</view>
</view>
<view class="meta-line">{{ getRecordMeta(item) }}</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">{{ formData.id ? `编辑${pageTitle}` : `新增${pageTitle}` }}</view>
<view class="modal-close" @tap="closeEdit">关闭</view>
</view>
<scroll-view scroll-y class="modal-body">
<view class="hero-meta hero-meta--form">
<view class="meta-item meta-item--form">
<view class="meta-label meta-label--form">申报年度</view>
<view class="meta-value meta-value--form">{{ fixedYear || formData.year || '-' }}</view>
</view>
<view class="meta-item meta-item--form">
<view class="meta-label meta-label--form">草稿状态</view>
<view class="meta-value meta-value--form">{{ draftSavedAt ? '已保存' : '未保存' }}</view>
</view>
</view>
<view v-for="section in formConfig.sections" :key="section.title" class="section-card">
<view class="section-head">
<view class="section-title">{{ section.title }}</view>
</view>
<view class="field-list">
<view v-for="field in section.fields" :key="field.key" class="field-item">
<view class="field-label">
{{ field.label }}
<text v-if="field.required" class="field-required">*</text>
</view>
<textarea
v-if="field.type === 'textarea'"
class="field-textarea"
:placeholder="field.placeholder"
:maxlength="-1"
:value="formData[field.key]"
@input="handleInput(field, $event)"
/>
<view
v-else-if="isGenderField(field)"
class="field-picker"
@tap="openGenderPicker(field)"
>
<text :class="formData[field.key] ? 'field-value' : 'field-placeholder'">
{{ formData[field.key] || field.placeholder }}
</text>
</view>
<view
v-else-if="isNationField(field)"
class="field-item-picker-wrap"
>
<picker
mode="selector"
:range="nationLabels"
:value="getNationIndex(field)"
:disabled="!nationOptions.length"
@change="handleNationPickerChange(field, $event)"
>
<view class="field-picker" :class="{ 'field-picker--disabled': !nationOptions.length }">
<text :class="formData[field.key] ? 'field-value' : 'field-placeholder'">
{{ formData[field.key] || (nationOptions.length ? '请选择民族' : '民族加载中') }}
</text>
</view>
</picker>
</view>
<view
v-else-if="isUvPickerField(field)"
class="field-picker"
:class="{ 'field-picker--disabled': isPickerFieldDisabled(field) }"
@tap="openSelectPicker(field)"
>
<text :class="formData[field.key] ? 'field-value' : 'field-placeholder'">
{{ getPickerDisplayText(field) }}
</text>
</view>
<view
v-else-if="isDateField(field)"
class="field-picker"
@tap="openDatePicker(field)"
>
<text :class="formData[field.key] ? 'field-value' : 'field-placeholder'">
{{ formData[field.key] || getDatePlaceholder(field) }}
</text>
</view>
<uv-input
v-else
class="field-input"
:modelValue="formData[field.key]"
:type="getInputType(field)"
:placeholder="field.placeholder"
placeholder-style="color: #c0c4cc;"
:maxlength="-1"
@input="setFieldValue(field.key, $event)"
/>
</view>
</view>
</view>
</scroll-view>
<view class="modal-actions">
<view class="ghost-btn" @tap="saveDraft">保存草稿</view>
<view class="primary-btn" @tap="submitForm">提交申报</view>
</view>
</view>
</view>
</view>
<view v-else class="empty-page">
<common-hero :title="pageTitle || '申报表'" :use-image-bg="true"></common-hero>
<view class="empty-body">
<view class="empty-title">未找到对应表单</view>
<view class="empty-desc">请返回上一页重新选择申报模块</view>
</view>
</view>
<uv-picker
ref="genderPicker"
title="选择性别"
:columns="genderPickerColumns"
:defaultIndex="[genderPickerIndex]"
keyName="text"
@close="noop"
@cancel="noop"
@confirm="confirmGenderPicker"
></uv-picker>
<uv-picker
ref="selectPicker"
:title="selectPickerTitle"
:columns="selectPickerColumns"
:defaultIndex="[selectPickerIndex]"
keyName="text"
@close="noop"
@cancel="noop"
@confirm="confirmSelectPicker"
></uv-picker>
<uv-picker
ref="datePicker"
:title="datePickerTitle"
:columns="datePickerColumns"
:defaultIndex="datePickerIndexs"
keyName="text"
@change="handleDatePickerChange"
@cancel="noop"
@confirm="confirmDatePicker"
></uv-picker>
</template>
<script>
import CommonHero from '../../components/common-hero/common-hero.vue'
import { createInitialFormData, getFormConfig, getModuleMeta } from '../../utils/gxmu/config'
import {
addSjqnForm,
addSjtbzbsjForm,
addWshqtwForm,
addWshqtzbForm,
addYxgqtdgbForm,
addYxgqtyForm,
getSjqnForm,
getSjtbzbsjForm,
getWshqtwForm,
getWshqtzbForm,
getYxgqtdgbForm,
getYxgqtyForm,
listDictData,
updateSjqnForm,
updateSjtbzbsjForm,
updateWshqtwForm,
updateWshqtzbForm,
updateYxgqtdgbForm,
updateYxgqtyForm,
userPageSjqnForm,
userPageSjtbzbsjForm,
userPageWshqtwForm,
userPageWshqtzbForm,
userPageYxgqtdgbForm,
userPageYxgqtyForm
} from './module-form-service'
const STORAGE_PREFIX = 'gxmu_form_draft_'
const POLITICS_DICT_ID = 172
const NATION_DICT_ID = 173
const MODULE_API_MAP = {
gxmu_sjqn: {
get: getSjqnForm,
add: addSjqnForm,
update: updateSjqnForm,
page: userPageSjqnForm
},
gxmu_sjtbzbsj: {
get: getSjtbzbsjForm,
add: addSjtbzbsjForm,
update: updateSjtbzbsjForm,
page: userPageSjtbzbsjForm
},
gxmu_wshqtw: {
get: getWshqtwForm,
add: addWshqtwForm,
update: updateWshqtwForm,
page: userPageWshqtwForm
},
gxmu_wshqtzb: {
get: getWshqtzbForm,
add: addWshqtzbForm,
update: updateWshqtzbForm,
page: userPageWshqtzbForm
},
gxmu_yxgqtdgb: {
get: getYxgqtdgbForm,
add: addYxgqtdgbForm,
update: updateYxgqtdgbForm,
page: userPageYxgqtdgbForm
},
gxmu_yxgqty: {
get: getYxgqtyForm,
add: addYxgqtyForm,
update: updateYxgqtyForm,
page: userPageYxgqtyForm
}
}
export default {
components: {
CommonHero
},
data() {
return {
moduleCode: '',
moduleMeta: null,
formConfig: null,
formData: {},
draftSavedAt: '',
fixedYear: '',
declareTitle: '',
nationOptions: [],
politicsOptions: [],
recordId: '',
loading: false,
showEdit: false,
list: [],
genderFieldKey: '',
genderPickerColumns: [[]],
selectFieldKey: '',
selectPickerTitle: '',
selectPickerColumns: [[]],
dateFieldKey: '',
datePickerTitle: '',
datePickerColumns: [[]],
datePickerIndexs: []
}
},
computed: {
pageTitle() {
return this.declareTitle || (this.formConfig ? this.formConfig.title : '申报表')
},
nationLabels() {
return this.nationOptions
},
genderPickerIndex() {
const options = (this.genderPickerColumns[0] || []).map((item) => item.value)
const index = options.indexOf(this.formData[this.genderFieldKey])
return index > -1 ? index : 0
},
selectPickerIndex() {
const options = (this.selectPickerColumns[0] || []).map((item) => item.value)
const index = options.indexOf(this.formData[this.selectFieldKey])
return index > -1 ? index : 0
}
},
onLoad(options) {
const code = options.code || ''
const config = getFormConfig(code)
const meta = getModuleMeta(code)
const fixedYear = String(options.year || '').trim()
const declareTitle = decodeURIComponent(options.declareTitle || '').trim()
const recordId = String(options.id || '').trim()
console.log('this.moduleMeta', meta)
this.moduleCode = code
this.formConfig = config || null
this.moduleMeta = meta || null
this.formData = createInitialFormData(code)
this.fixedYear = fixedYear
this.declareTitle = declareTitle
this.recordId = recordId
if (config) {
uni.setNavigationBarTitle({
title: declareTitle || (meta ? meta.title : '申报表')
})
this.loadDictOptions()
this.setupPickerState()
this.loadData()
this.loadInitialDetail()
}
},
methods: {
getApiConfig() {
return MODULE_API_MAP[this.moduleCode] || null
},
createFreshFormData() {
const data = createInitialFormData(this.moduleCode)
data.year = this.fixedYear || data.year || String(new Date().getFullYear())
return data
},
async loadInitialDetail() {
if (!this.recordId) {
return
}
try {
await this.loadRemoteRecord(this.recordId)
this.showEdit = true
} catch (error) {
uni.showToast({
title: (error && error.message) || '表单详情加载失败',
icon: 'none'
})
}
},
async loadRemoteRecord(id) {
const api = this.getApiConfig()
if (!api || !api.get || !id) {
return
}
const detail = await api.get(id)
if (!detail) {
return
}
this.formData = {
...this.createFreshFormData(),
...detail
}
if (this.fixedYear) {
this.formData.year = this.fixedYear
}
this.setupPickerState()
},
async loadData() {
const api = this.getApiConfig()
if (!api || !api.page) {
this.list = []
return
}
this.loading = true
try {
const result = await api.page({
page: 1,
limit: 50,
year: this.fixedYear || undefined
})
this.list = Array.isArray(result && result.list) ? result.list : []
} catch (error) {
this.list = []
uni.showToast({
title: (error && error.message) || '加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
openCreate() {
this.formData = this.createFreshFormData()
this.draftSavedAt = ''
this.restoreDraft()
this.setupPickerState()
this.showEdit = true
},
async openEdit(item) {
try {
const id = item && item.id ? item.id : ''
if (id) {
await this.loadRemoteRecord(id)
} else {
this.formData = {
...this.createFreshFormData(),
...(item || {})
}
}
this.showEdit = true
} catch (error) {
uni.showToast({
title: (error && error.message) || '加载详情失败',
icon: 'none'
})
}
},
closeEdit() {
this.showEdit = false
},
getRecordTitle(item) {
if (!item) {
return this.pageTitle
}
const fields = ['name', 'title', 'orgName', 'branchName', 'applyType']
for (const key of fields) {
const value = String(item[key] || '').trim()
if (value) {
return value
}
}
return this.pageTitle
},
getRecordSubtitle(item) {
const fields = ['applyType', 'unit', 'collegeClass', 'organization', 'secondOrg', 'branch', 'position']
const parts = fields
.map((key) => String((item && item[key]) || '').trim())
.filter(Boolean)
.slice(0, 1)
return parts.length ? parts.join(' · ') : (this.moduleMeta && this.moduleMeta.group) || '申报记录'
},
getRecordMeta(item) {
const pairs = [
['政治面貌', item && item.politics],
['联系方式', item && (item.contact || item.phone)],
['民族', item && item.nation]
]
const matched = pairs.find((entry) => String(entry[1] || '').trim())
if (matched) {
return `${matched[0]}${matched[1]}`
}
return `记录编号:${(item && item.id) || '-'}`
},
noop() {
return
},
setupPickerState() {
this.genderPickerColumns = [[
{ text: '男', value: '男' },
{ text: '女', value: '女' }
]]
},
async loadDictOptions() {
try {
const [nationResult, politicsResult] = await Promise.all([
listDictData({ dictId: NATION_DICT_ID }),
listDictData({ dictId: POLITICS_DICT_ID })
])
const normalize = (result) =>
(Array.isArray(result) ? result : [])
.map((item) => String(item && (item.dictDataName || item.name || item.label || item.value) || '').trim())
.filter(Boolean)
this.nationOptions = normalize(nationResult)
this.politicsOptions = normalize(politicsResult)
} catch (error) {
this.nationOptions = []
this.politicsOptions = []
}
},
getStorageKey() {
return `${STORAGE_PREFIX}${this.moduleCode}_${this.fixedYear || 'default'}`
},
getLegacyStorageKey() {
return `${STORAGE_PREFIX}${this.moduleCode}`
},
restoreDraft() {
const draft =
uni.getStorageSync(this.getStorageKey()) ||
uni.getStorageSync(this.getLegacyStorageKey())
if (!draft || !draft.formData) {
return
}
this.formData = {
...this.formData,
...draft.formData
}
if (this.fixedYear) {
this.formData.year = this.fixedYear
}
this.draftSavedAt = draft.savedAt || ''
},
handleInput(field, event) {
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
this.formData[field.key] = value
},
setFieldValue(key, event) {
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
this.formData[key] = value
},
findFieldByKey(key) {
if (!this.formConfig || !Array.isArray(this.formConfig.sections)) {
return null
}
for (const section of this.formConfig.sections) {
const matched = (section.fields || []).find((field) => field.key === key)
if (matched) {
return matched
}
}
return null
},
isGenderField(field) {
return field && field.key === 'gender'
},
isPoliticsField(field) {
return field && field.key === 'politics'
},
isNationField(field) {
return field && field.key === 'nation'
},
isUvPickerField(field) {
return field && (field.type === 'select' || this.isPoliticsField(field) || this.isNationField(field))
},
isPickerFieldDisabled(field) {
return (this.isPoliticsField(field) || this.isNationField(field)) && !this.getSelectOptions(field).length
},
getPickerDisplayText(field) {
if (this.formData[field.key]) {
return this.formData[field.key]
}
if (this.isPoliticsField(field)) {
return this.getSelectOptions(field).length ? '请选择政治面貌' : '政治面貌加载中'
}
if (this.isNationField(field)) {
return this.getSelectOptions(field).length ? '请选择民族' : '民族加载中'
}
return field.placeholder
},
getSelectOptions(field) {
if (this.isGenderField(field)) {
return field.options || ['男', '女']
}
if (this.isPoliticsField(field)) {
return this.politicsOptions.length ? this.politicsOptions : (field.options || [])
}
return field.options || []
},
getNationIndex(field) {
const index = this.nationOptions.findIndex((item) => item === this.formData[field.key])
return index > -1 ? index : 0
},
handleNationPickerChange(field, event) {
const index = Number((event && event.detail && event.detail.value) || 0)
this.formData[field.key] = this.nationOptions[index] || ''
},
openSelectPicker(field) {
const options = this.getSelectOptions(field)
if (!options.length) {
return
}
this.selectFieldKey = field.key
this.selectPickerTitle = `选择${field.label}`
this.selectPickerColumns = [
options.map((item) => ({ text: item, value: item }))
]
this.$nextTick(() => {
this.$refs.selectPicker && this.$refs.selectPicker.open()
})
},
confirmSelectPicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
if (this.selectFieldKey) {
this.formData[this.selectFieldKey] = value
}
},
openGenderPicker(field) {
this.genderFieldKey = field.key
this.genderPickerColumns = [
this.getSelectOptions(field).map((item) => ({ text: item, value: item }))
]
this.$nextTick(() => {
this.$refs.genderPicker && this.$refs.genderPicker.open()
})
},
confirmGenderPicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
if (this.genderFieldKey) {
this.formData[this.genderFieldKey] = value
}
},
isDateField(field) {
return ['year', 'month', 'date'].includes(field.type)
},
getDateRange(field) {
const start = String(field.start || '2000-01-01').trim()
const end = String(field.end || '2099-12-31').trim()
return {
startYear: Number(start.slice(0, 4)) || 2000,
endYear: Number(end.slice(0, 4)) || 2099
}
},
getDaysInMonth(year, month) {
return new Date(year, month, 0).getDate()
},
buildDatePickerState(field, value, nextIndexs = []) {
const { startYear, endYear } = this.getDateRange(field)
const years = []
for (let year = startYear; year <= endYear; 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 }
})
const safeValue = String(value || '').trim()
let selectedYear = safeValue.slice(0, 4) || `${startYear}`
let selectedMonth = safeValue.slice(5, 7) || '01'
let selectedDay = safeValue.slice(8, 10) || '01'
if (Array.isArray(nextIndexs) && nextIndexs.length) {
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 dayTotal = this.getDaysInMonth(Number(selectedYear), Number(selectedMonth))
const days = Array.from({ length: dayTotal }, (_, index) => {
const day = String(index + 1).padStart(2, '0')
return { text: day, value: day }
})
if (Array.isArray(nextIndexs) && nextIndexs.length > 2 && typeof nextIndexs[2] === 'number' && days[nextIndexs[2]]) {
selectedDay = days[nextIndexs[2]].value
}
const yearIndex = Math.max(0, years.findIndex((item) => item.value === selectedYear))
const monthIndex = Math.max(0, months.findIndex((item) => item.value === selectedMonth))
const dayIndex = Math.max(0, days.findIndex((item) => item.value === selectedDay))
if (field.type === 'year') {
return {
columns: [years],
indexs: [yearIndex]
}
}
if (field.type === 'month') {
return {
columns: [years, months],
indexs: [yearIndex, monthIndex]
}
}
return {
columns: [years, months, days],
indexs: [yearIndex, monthIndex, dayIndex]
}
},
openDatePicker(field) {
this.dateFieldKey = field.key
this.datePickerTitle = `选择${field.label}`
const pickerState = this.buildDatePickerState(field, this.formData[field.key])
this.datePickerColumns = pickerState.columns
this.datePickerIndexs = pickerState.indexs
this.$nextTick(() => {
this.$refs.datePicker && this.$refs.datePicker.open()
})
},
handleDatePickerChange(event) {
const field = this.findFieldByKey(this.dateFieldKey)
if (!field || field.type !== 'date') {
this.datePickerIndexs = (event && event.indexs) || this.datePickerIndexs
return
}
const nextIndexs = (event && event.indexs) || []
const pickerState = this.buildDatePickerState(field, this.formData[field.key], nextIndexs)
this.datePickerColumns = pickerState.columns
this.datePickerIndexs = pickerState.indexs
},
formatDatePickerValue(values, field) {
const year = (values[0] && values[0].value) || ''
if (field.type === 'year') {
return year
}
const month = (values[1] && values[1].value) || '01'
if (field.type === 'month') {
return `${year}-${month}`
}
const day = (values[2] && values[2].value) || '01'
return `${year}-${month}-${day}`
},
confirmDatePicker(event) {
const field = this.findFieldByKey(this.dateFieldKey)
if (!field) {
return
}
this.formData[field.key] = this.formatDatePickerValue((event && event.value) || [], field)
this.datePickerIndexs = (event && event.indexs) || this.datePickerIndexs
},
getDatePlaceholder(field) {
if (field.type === 'year') {
return '请选择年份'
}
if (field.type === 'month') {
return '请选择年月'
}
return '请选择日期'
},
getInputType(field) {
if (field.type === 'number' || field.type === 'phone') {
return 'number'
}
return 'text'
},
validateForm() {
const requiredFields = []
this.formConfig.sections.forEach((section) => {
section.fields.forEach((field) => {
if (field.required && !String(this.formData[field.key] || '').trim()) {
requiredFields.push(field.label)
}
})
})
if (!requiredFields.length) {
return true
}
uni.showToast({
title: `请完善:${requiredFields[0]}`,
icon: 'none'
})
return false
},
saveDraft() {
const savedAt = this.formatNow()
uni.setStorageSync(this.getStorageKey(), {
moduleCode: this.moduleCode,
formData: this.formData,
savedAt
})
this.draftSavedAt = savedAt
uni.showToast({
title: '草稿已保存',
icon: 'success'
})
},
clearDraftStorage() {
uni.removeStorageSync(this.getStorageKey())
uni.removeStorageSync(this.getLegacyStorageKey())
this.draftSavedAt = ''
},
async submitForm() {
if (!this.validateForm()) {
return
}
const api = this.getApiConfig()
if (!api) {
this.clearDraftStorage()
uni.showModal({
title: '提交成功',
content: `${this.moduleMeta ? this.moduleMeta.title : '当前模块'}表单已提交,后续可继续接入后台接口。`,
showCancel: false
})
return
}
try {
const payload = {
...this.formData,
year: this.fixedYear || this.formData.year
}
const request = payload.id ? api.update : api.add
await request({
...payload,
id: payload.id || undefined
})
this.clearDraftStorage()
uni.showToast({
title: '提交成功',
icon: 'none'
})
this.closeEdit()
this.loadData()
} catch (error) {
uni.showToast({
title: (error && error.message) || '提交失败',
icon: 'none'
})
}
},
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: linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%); }
.page-scroll { height: 100vh; box-sizing: border-box; }
.hero-wrap{ padding:24rpx 24rpx 0; background: url("@/static/indexBg.jpg") no-repeat cover}
.hero-card,.state-card,.data-card { border-radius: 28rpx; }
.hero-card {
padding: 34rpx 36rpx;
background: rgba(255, 255, 255, 0.5);
box-shadow: 0 4rpx 12rpx 0 rgba(174, 174, 174, 0.4);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
border: 1rpx solid rgba(255, 255, 255, 0.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 {
position: relative;
margin-top: 8rpx;
padding-left: 10rpx;
font-size: 30rpx;
font-weight: 700;
line-height: 1.35;
color: #18a0f7;
border-left: 5px solid #0f94ef;
}
//.hero-title::before {
// content: '';
// position: absolute;
// left: 0;
// top: 8rpx;
// width: 8rpx;
// height: 52rpx;
// border-radius: 999rpx;
// background: linear-gradient(180deg, #0f94ef 0%, #22b4ff 100%);
//}
.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; }
.state-desc,.card-subtitle,.meta-line { margin-top: 10rpx; font-size: 24rpx; line-height: 1.6; color: #786e69; }
.card-list { padding: 0 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; }
.hero-meta--form { grid-template-columns: repeat(2, minmax(0, 1fr)); margin-bottom: 20rpx; }
.meta-item--form { background: #f3f7fd; }
.meta-label--form { color: #6b7785; }
.meta-value--form { color: #1f2329; }
.section-card { margin-top: 24rpx; padding: 26rpx; border-radius: 30rpx; background: rgba(255,255,255,.94); border: 1rpx solid rgba(20,150,242,.08); box-shadow: 0 10rpx 28rpx rgba(34,94,142,.08); }
.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-textarea,.field-picker { width: 100%; box-sizing: border-box; padding: 22rpx 24rpx; border-radius: 22rpx; font-size: 24rpx; color: #2a2e35; }
.field-textarea { background: #f8f4f1; border: 1rpx solid #eadfd7; }
.field-picker { background: #F4FCFF; border: 1rpx solid rgba(220,220,220,1); }
.field-picker--disabled { background: #f7f9fb; color: #aaafb7; }
.field-textarea { min-height: 180rpx; }
:deep(.field-input.uv-input) { width: 100%; min-height: 88rpx; padding: 0 24rpx; border-radius: 12rpx; border: 1px solid #dcdfe6; background: #ffffff; box-sizing: border-box; transition: border-color 0.2s ease; }
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) { height: 88rpx; min-height: 88rpx; font-size: 28rpx; line-height: 88rpx; color: #303133; }
.field-placeholder { color: #aa9f98; }
.field-value { color: #2a2e35; }
.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: #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; }
.empty-page { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48rpx; text-align: center; }
.empty-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
.empty-desc { margin-top: 12rpx; font-size: 24rpx; line-height: 1.7; color: #7b8694; }
</style>