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

788 lines
35 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="pageTitle" :use-image-bg="true"></common-hero>
<view class="hero-card">
<view class="hero-badge">挑战杯</view>
<view class="hero-title">{{ pageTitle }}</view>
<view class="hero-desc">按后台完整字段拆分为项目申报表项目说明公开展示信息表</view>
<view class="hero-actions">
<view class="hero-action" @tap="openCreate">立即申报</view>
<view class="hero-action hero-action--secondary" @tap="loadData">刷新列表</view>
</view>
</view>
<view v-if="loading" class="state-card">
<view class="state-title">正在加载申报记录...</view>
</view>
<view v-else-if="!list.length" class="state-card">
<view class="state-title">暂无申报记录</view>
<view 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">{{ item.projectName || '未命名项目' }}</view>
<view class="card-subtitle">{{ item.projectType || '-' }} · {{ item.projectGroup || '-' }}</view>
</view>
<view class="card-year">{{ item.year || '-' }}</view>
</view>
<view class="meta-line">学校{{ item.schoolName || '-' }}</view>
<view class="meta-line">负责人{{ getLeaderInfo(item).leader || '-' }} / {{ getLeaderInfo(item).phone || '-' }}</view>
</view>
</view>
</scroll-view>
<view v-if="showEdit" class="modal-mask" @tap="closeEdit">
<view class="modal-panel" @tap.stop>
<view class="modal-head">
<view class="modal-title">{{ current.id ? '编辑挑战杯申报' : '新增挑战杯申报' }}</view>
<view class="modal-close" @tap="closeEdit">关闭</view>
</view>
<scroll-view scroll-y class="modal-body">
<view class="tab-row">
<view class="tab-item" :class="{ 'tab-item--active': activeTab === 'base' }" @tap="activeTab = 'base'">项目申报表</view>
<view class="tab-item" :class="{ 'tab-item--active': activeTab === 'detail' }" @tap="activeTab = 'detail'">项目说明</view>
<view class="tab-item" :class="{ 'tab-item--active': activeTab === 'public' }" @tap="activeTab = 'public'">公开展示信息表</view>
</view>
<view v-if="activeTab === 'base'" class="form-section">
<view class="field-item">
<view class="field-label">年份</view>
<uv-input class="field-input field-input--readonly" :modelValue="current.year" border="none" readonly placeholder="当前申报年份" />
</view>
<view class="field-grid field-grid--2">
<view class="field-item">
<view class="field-label">所在省</view>
<uv-input class="field-input" :modelValue="current.provinceCity" border="none" @input="setField('provinceCity', $event)" placeholder="请输入所在省/市" />
</view>
<view class="field-item">
<view class="field-label">学校名称全称</view>
<uv-input class="field-input" :modelValue="current.schoolName" border="none" @input="setField('schoolName', $event)" placeholder="请输入学校名称" />
</view>
</view>
<view class="field-item">
<view class="field-label">项目名称</view>
<uv-input class="field-input" :modelValue="current.projectName" border="none" @input="setField('projectName', $event)" placeholder="请输入项目名称" />
</view>
<view class="field-item">
<view class="field-label">项目类型</view>
<view class="field-picker" @tap="openFormProjectTypePicker">
<text :class="current.projectType ? 'field-value' : 'field-placeholder'">
{{ getOptionLabel(current.projectType, formProjectTypeOptions) || '请选择项目类型' }}
</text>
</view>
</view>
<view class="field-item">
<view class="field-label">
项目分组
<view class="field-note">仅可选择 1 个组别按后台申报配置提供选项</view>
</view>
<view class="field-picker" @tap="openFormProjectGroupPicker">
<text :class="current.projectGroup ? 'field-value' : 'field-placeholder'">
{{ getOptionLabel(current.projectGroup, formProjectGroupOptions) || '请选择项目分组' }}
</text>
</view>
</view>
<view class="section-header">
<view class="section-subtitle">团队成员</view>
<view class="section-action" @tap="addTeamMember">新增成员</view>
</view>
<view v-if="!current.teamMembers.length" class="empty-inline">暂无团队成员请手动新增</view>
<view v-for="(item, index) in current.teamMembers" :key="`member-${index}`" class="group-card">
<view class="group-head">
<view class="group-title">成员 {{ index + 1 }}</view>
<view class="group-remove" @tap="removeTeamMember(index)">删除</view>
</view>
<view class="field-grid field-grid--2">
<view class="field-item"><view class="field-label">姓名</view><uv-input class="field-input" :modelValue="item.name" border="none" @input="setTeamMemberField(index, 'name', $event)" /></view>
<view class="field-item"><view class="field-label">性别</view><uv-input class="field-input" :modelValue="item.gender" border="none" @input="setTeamMemberField(index, 'gender', $event)" /></view>
<view class="field-item"><view class="field-label">学院</view><uv-input class="field-input" :modelValue="item.college" border="none" @input="setTeamMemberField(index, 'college', $event)" /></view>
<view class="field-item"><view class="field-label">年级专业</view><uv-input class="field-input" :modelValue="item.gradeMajor" border="none" @input="setTeamMemberField(index, 'gradeMajor', $event)" /></view>
<view class="field-item"><view class="field-label">手机</view><uv-input class="field-input" :modelValue="item.phone" border="none" @input="setTeamMemberField(index, 'phone', $event)" /></view>
<view class="field-item"><view class="field-label">备注</view><uv-input class="field-input" :modelValue="item.remark" border="none" @input="setTeamMemberField(index, 'remark', $event)" placeholder="负责人及学历等备注" /></view>
</view>
</view>
<view class="section-header">
<view class="section-subtitle">指导教师</view>
<view class="section-action" @tap="addAdvisor">新增教师</view>
</view>
<view v-if="!current.advisors.length" class="empty-inline">暂无指导教师请手动新增</view>
<view v-for="(item, index) in current.advisors" :key="`advisor-${index}`" class="group-card">
<view class="group-head">
<view class="group-title">指导教师 {{ index + 1 }}</view>
<view class="group-remove" @tap="removeAdvisor(index)">删除</view>
</view>
<view class="field-grid field-grid--2">
<view class="field-item"><view class="field-label">姓名</view><uv-input class="field-input" :modelValue="item.name" border="none" @input="setAdvisorField(index, 'name', $event)" /></view>
<view class="field-item"><view class="field-label">性别</view><uv-input class="field-input" :modelValue="item.gender" border="none" @input="setAdvisorField(index, 'gender', $event)" /></view>
<view class="field-item"><view class="field-label">学院</view><uv-input class="field-input" :modelValue="item.college" border="none" @input="setAdvisorField(index, 'college', $event)" /></view>
<view class="field-item"><view class="field-label">职称</view><uv-input class="field-input" :modelValue="item.title" border="none" @input="setAdvisorField(index, 'title', $event)" /></view>
<view class="field-item"><view class="field-label">职务</view><uv-input class="field-input" :modelValue="item.duty" border="none" @input="setAdvisorField(index, 'duty', $event)" /></view>
<view class="field-item"><view class="field-label">手机</view><uv-input class="field-input" :modelValue="item.phone" border="none" @input="setAdvisorField(index, 'phone', $event)" /></view>
</view>
</view>
<view class="field-item">
<view class="field-label">项目简介500 字以内</view>
<textarea class="field-textarea" :value="current.projectBrief" @input="setField('projectBrief', $event)" placeholder="请输入项目简介" />
</view>
</view>
<view v-else-if="activeTab === 'detail'" class="form-section">
<view class="field-item"><view class="field-label">社会价值500 字以内</view><textarea class="field-textarea" :value="current.socialValue" @input="setField('socialValue', $event)" /></view>
<view class="field-item"><view class="field-label">实践过程500 字以内</view><textarea class="field-textarea" :value="current.practiceProcess" @input="setField('practiceProcess', $event)" /></view>
<view class="field-item"><view class="field-label">创新意义500 字以内</view><textarea class="field-textarea" :value="current.innovationMeaning" @input="setField('innovationMeaning', $event)" /></view>
<view class="field-item"><view class="field-label">发展前景500 字以内</view><textarea class="field-textarea" :value="current.developmentProspect" @input="setField('developmentProspect', $event)" /></view>
<view class="field-item"><view class="field-label">团队协作500 字以内</view><textarea class="field-textarea" :value="current.teamCooperation" @input="setField('teamCooperation', $event)" /></view>
<view class="field-item"><view class="field-label">项目介绍材料</view><textarea class="field-textarea" :value="current.projectMaterials" @input="setField('projectMaterials', $event)" /></view>
<view class="field-item"><view class="field-label">其他相关证明材料</view><textarea class="field-textarea" :value="current.otherProofs" @input="setField('otherProofs', $event)" placeholder="选报,相关证明材料说明" /></view>
</view>
<view v-else class="form-section">
<view class="field-item">
<view class="field-label">项目名称</view>
<uv-input class="field-input" :modelValue="current.projectName" border="none" @input="setField('projectName', $event)" placeholder="请输入项目名称" />
</view>
<view class="field-item">
<view class="field-label">参赛学校</view>
<uv-input class="field-input" :modelValue="current.schoolName" border="none" @input="setField('schoolName', $event)" placeholder="请输入参赛学校" />
</view>
<view class="field-item">
<view class="field-label">项目类型</view>
<view class="field-picker" @tap="openPublicProjectTypePicker">
<text :class="current.publicProjectType ? 'field-value' : 'field-placeholder'">
{{ getOptionLabel(current.publicProjectType, publicProjectTypeOptions) || '请选择项目类型' }}
</text>
</view>
</view>
<view class="field-item">
<view class="field-label">项目分组</view>
<view class="field-picker" @tap="openPublicProjectGroupPicker">
<text :class="current.publicProjectGroup ? 'field-value' : 'field-placeholder'">
{{ getOptionLabel(current.publicProjectGroup, publicProjectGroupOptions) || '请选择项目分组' }}
</text>
</view>
</view>
<view class="field-item">
<view class="field-label">我们的项目</view>
<textarea class="field-textarea" :value="current.projectSummary" @input="setField('projectSummary', $event)" placeholder="150字以内阐述项目的实践来源、社会背景、基本情况等" />
</view>
<view class="field-item">
<view class="field-label">我们的团队</view>
<view class="upload-card">
<image v-if="current.teamIntro" class="upload-image" :src="current.teamIntro" mode="aspectFill" @tap="previewImage(current.teamIntro)" />
<view v-else class="upload-empty">请上传团队图片</view>
<view class="upload-actions">
<view class="upload-btn" @tap="chooseTeamImage">上传图片</view>
<view v-if="current.teamIntro" class="upload-btn upload-btn--ghost" @tap="clearField('teamIntro')">移除</view>
</view>
</view>
</view>
<view class="field-item"><view class="field-label">我们的口号</view><uv-input class="field-input" :modelValue="current.teamSlogan" border="none" @input="setField('teamSlogan', $event)" placeholder="20字以内" /></view>
<view class="field-item">
<view class="field-label">我们的实践日志选填</view>
<view class="upload-card">
<view v-if="current.practiceLog" class="video-box">
<video class="upload-video" :src="current.practiceLog" controls object-fit="cover"></video>
</view>
<view v-else class="upload-empty">请上传实践日志视频</view>
<view class="upload-actions">
<view class="upload-btn" @tap="choosePracticeVideo">上传视频</view>
<view v-if="current.practiceLog" class="upload-btn upload-btn--ghost" @tap="clearField('practiceLog')">移除</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="modal-actions">
<view class="ghost-btn" @tap="closeEdit">取消</view>
<view class="primary-btn" @tap="save">保存</view>
</view>
</view>
</view>
<uv-picker
ref="formProjectTypePicker"
title="选择项目类型"
:columns="formProjectTypePickerColumns"
:defaultIndex="[formProjectTypeIndex]"
keyName="text"
@close="noop"
@cancel="noop"
@confirm="confirmFormProjectTypePicker"
></uv-picker>
<uv-picker
ref="formProjectGroupPicker"
title="选择项目分组"
:columns="formProjectGroupPickerColumns"
:defaultIndex="[formProjectGroupIndex]"
keyName="text"
@close="noop"
@cancel="noop"
@confirm="confirmFormProjectGroupPicker"
></uv-picker>
<uv-picker
ref="publicProjectTypePicker"
title="选择公开项目类型"
:columns="publicProjectTypePickerColumns"
:defaultIndex="[publicProjectTypeIndex]"
keyName="text"
@close="noop"
@cancel="noop"
@confirm="confirmPublicProjectTypePicker"
></uv-picker>
<uv-picker
ref="publicProjectGroupPicker"
title="选择公开项目分组"
:columns="publicProjectGroupPickerColumns"
:defaultIndex="[publicProjectGroupIndex]"
keyName="text"
@close="noop"
@cancel="noop"
@confirm="confirmPublicProjectGroupPicker"
></uv-picker>
</view>
</template>
<script>
import CommonHero from '../../components/common-hero/common-hero.vue'
import { addTzbcyForm, getDeclare, getTzbcyForm, uploadTempFile, userPageTzbcyForm } from './module-form-service'
const FALLBACK_PROJECT_TYPE_OPTIONS = [
{ label: 'I. 普通高校', value: '普通高校' },
{ label: 'II. 职业院校', value: '职业院校' },
{ label: 'III. 东盟留学生专项', value: '东盟留学生专项' }
]
const FALLBACK_PROJECT_GROUP_OPTIONS = [
{ label: 'A. 科技创新和未来产业', value: 'A. 科技创新和未来产业' },
{ label: 'B. 乡村振兴和农业农村现代化', value: 'B. 乡村振兴和农业农村现代化' },
{ label: 'C. 社会治理和公共服务', value: 'C. 社会治理和公共服务' },
{ label: 'D. 生态环保和可持续发展', value: 'D. 生态环保和可持续发展' },
{ label: 'E. 文化创意和区域合作', value: 'E. 文化创意和区域合作' }
]
const createTeamMember = () => ({
name: '',
gender: '',
college: '',
gradeMajor: '',
phone: '',
remark: ''
})
const createAdvisor = () => ({
name: '',
gender: '',
college: '',
title: '',
duty: '',
phone: ''
})
const createDefaultForm = () => ({
id: undefined,
year: '',
provinceCity: '',
schoolName: '广西医科大学',
projectName: '',
projectType: '',
projectGroup: '',
publicProjectType: '',
publicProjectGroup: '',
leader: '',
phone: '',
teamMembers: [],
advisors: [],
projectBrief: '',
socialValue: '',
practiceProcess: '',
innovationMeaning: '',
developmentProspect: '',
teamCooperation: '',
projectMaterials: '',
otherProofs: '',
projectSummary: '',
teamIntro: '',
teamSlogan: '',
practiceLog: ''
})
const parseMultiValueField = (value) => {
if (Array.isArray(value)) {
return value.map((item) => String(item || '').trim()).filter(Boolean)
}
if (typeof value !== 'string') {
return []
}
const trimmed = value.trim()
if (!trimmed) {
return []
}
try {
const parsed = JSON.parse(trimmed)
if (Array.isArray(parsed)) {
return parsed.map((item) => String(item || '').trim()).filter(Boolean)
}
} catch (error) {}
return [trimmed]
}
const buildChoiceOptions = (customValues, fallback, currentValue) => {
const values = (customValues && customValues.length ? customValues : fallback.map((item) => item.value))
.map((item) => String(item || '').trim())
.filter((item, index, arr) => item && arr.indexOf(item) === index)
if (currentValue && String(currentValue).trim() && !values.includes(String(currentValue).trim())) {
values.push(String(currentValue).trim())
}
return values.map((value) => {
const matched = fallback.find((item) => item.value === value)
return {
label: matched && matched.label ? matched.label : value,
value
}
})
}
export default {
components: {
CommonHero
},
data() {
return {
loading: false,
initializingDetail: false,
showEdit: false,
activeTab: 'base',
pageTitle: '挑战杯申报',
fixedYear: '',
declareId: '',
list: [],
current: createDefaultForm(),
declareConfig: null
}
},
computed: {
formProjectTypeOptions() {
const config = this.declareConfig || {}
return buildChoiceOptions(
parseMultiValueField(config.formProjectType || config.projectType),
FALLBACK_PROJECT_TYPE_OPTIONS,
this.current.projectType
)
},
formProjectGroupOptions() {
const config = this.declareConfig || {}
return buildChoiceOptions(
parseMultiValueField(config.formProjectGroup || config.projectGroup),
FALLBACK_PROJECT_GROUP_OPTIONS,
this.current.projectGroup
)
},
publicProjectTypeOptions() {
const config = this.declareConfig || {}
return buildChoiceOptions(
parseMultiValueField(config.publicProjectType || config.projectType),
FALLBACK_PROJECT_TYPE_OPTIONS,
this.current.publicProjectType
)
},
publicProjectGroupOptions() {
const config = this.declareConfig || {}
return buildChoiceOptions(
parseMultiValueField(config.publicProjectGroup || config.projectGroup),
FALLBACK_PROJECT_GROUP_OPTIONS,
this.current.publicProjectGroup
)
},
formProjectTypePickerColumns() {
return [this.formProjectTypeOptions.map((item) => ({ text: item.label, value: item.value }))]
},
formProjectGroupPickerColumns() {
return [this.formProjectGroupOptions.map((item) => ({ text: item.label, value: item.value }))]
},
publicProjectTypePickerColumns() {
return [this.publicProjectTypeOptions.map((item) => ({ text: item.label, value: item.value }))]
},
publicProjectGroupPickerColumns() {
return [this.publicProjectGroupOptions.map((item) => ({ text: item.label, value: item.value }))]
},
formProjectTypeIndex() {
return this.getOptionIndex(this.current.projectType, this.formProjectTypeOptions)
},
formProjectGroupIndex() {
return this.getOptionIndex(this.current.projectGroup, this.formProjectGroupOptions)
},
publicProjectTypeIndex() {
return this.getOptionIndex(this.current.publicProjectType, this.publicProjectTypeOptions)
},
publicProjectGroupIndex() {
return this.getOptionIndex(this.current.publicProjectGroup, this.publicProjectGroupOptions)
}
},
onLoad(options) {
this.fixedYear = String(options.year || '').trim()
this.declareId = String(options.id || '').trim()
this.pageTitle = decodeURIComponent(options.declareTitle || '挑战杯申报').trim() || '挑战杯申报'
this.loadData()
this.loadDeclareConfig()
this.loadInitialDetail(options)
},
methods: {
getOptionIndex(value, options) {
const index = (options || []).findIndex((item) => item.value === value)
return index > -1 ? index : 0
},
getOptionLabel(value, options) {
const matched = (options || []).find((item) => item.value === value)
return matched ? matched.label : value
},
normalizeDetail(detail) {
const defaults = createDefaultForm()
const normalizedProjectType = (detail && detail.projectType) || ''
const normalizedProjectGroup = (detail && detail.projectGroup) || ''
return {
...defaults,
...(detail || {}),
projectType: normalizedProjectType,
projectGroup: normalizedProjectGroup,
publicProjectType: (detail && detail.publicProjectType) || normalizedProjectType,
publicProjectGroup: (detail && detail.publicProjectGroup) || normalizedProjectGroup,
teamMembers: Array.isArray(detail && detail.teamMembers) ? detail.teamMembers.map((item) => ({ ...createTeamMember(), ...(item || {}) })) : [],
advisors: Array.isArray(detail && detail.advisors) ? detail.advisors.map((item) => ({ ...createAdvisor(), ...(item || {}) })) : []
}
},
getLeaderInfo(record) {
const members = Array.isArray(record && record.teamMembers) ? record.teamMembers : []
const firstMember = members.find((item) => String(item && (item.name || item.phone) || '').trim())
return {
leader: (record && record.leader) || (firstMember && firstMember.name) || '',
phone: (record && record.phone) || (firstMember && firstMember.phone) || ''
}
},
async loadDeclareConfig() {
if (!this.declareId) {
return
}
try {
const result = await getDeclare(this.declareId)
this.declareConfig = result || null
const declareYear = String((result && result.year) || '').trim()
if (declareYear) {
this.fixedYear = declareYear
this.current.year = declareYear
}
} catch (error) {
uni.showToast({ title: (error && error.message) || '申报配置加载失败', icon: 'none' })
}
},
async loadInitialDetail(options) {
const id = String((options && options.id) || '').trim()
if (!id) {
return
}
this.initializingDetail = true
try {
const detail = await getTzbcyForm(id)
this.current = {
...this.normalizeDetail(detail),
year: String((detail && detail.year) || this.fixedYear || '').trim()
}
if (this.current.year) {
this.fixedYear = this.current.year
}
// this.showEdit = true
} catch (error) {
uni.showToast({ title: (error && error.message) || '加载详情失败', icon: 'none' })
} finally {
this.initializingDetail = false
}
},
async loadData() {
this.loading = true
try {
const result = await userPageTzbcyForm({
page: 1,
limit: 50,
year: this.fixedYear || undefined
})
this.list = Array.isArray(result && result.list) ? result.list : []
} catch (error) {
uni.showToast({ title: (error && error.message) || '加载失败', icon: 'none' })
} finally {
this.loading = false
}
},
openCreate() {
this.current = {
...createDefaultForm(),
year: this.fixedYear || ''
}
this.activeTab = 'base'
this.showEdit = true
},
async openEdit(item) {
try {
const detail = item && item.id ? await getTzbcyForm(item.id) : item
this.current = {
...this.normalizeDetail(detail),
year: String((detail && detail.year) || this.fixedYear || '').trim()
}
this.activeTab = 'base'
this.showEdit = true
} catch (error) {
uni.showToast({ title: (error && error.message) || '加载详情失败', icon: 'none' })
}
},
closeEdit() {
this.showEdit = false
},
noop() {
return
},
setField(key, event) {
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
this.current[key] = value || ''
},
clearField(key) {
this.current[key] = ''
},
async chooseTeamImage() {
try {
const result = await new Promise((resolve, reject) => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: resolve,
fail: reject
})
})
const file = ((result && result.tempFiles) || [])[0]
if (!file) {
return
}
uni.showLoading({ title: '上传中', mask: true })
const uploaded = await uploadTempFile(file)
this.current.teamIntro = uploaded.url || uploaded.downloadUrl || uploaded.path || ''
uni.showToast({ title: '图片上传成功', icon: 'none' })
} catch (error) {
if (error && error.errMsg && String(error.errMsg).includes('cancel')) {
return
}
uni.showToast({ title: (error && error.message) || '图片上传失败', icon: 'none' })
} finally {
uni.hideLoading()
}
},
async choosePracticeVideo() {
try {
const result = await new Promise((resolve, reject) => {
uni.chooseVideo({
sourceType: ['album', 'camera'],
maxDuration: 120,
compressed: true,
success: resolve,
fail: reject
})
})
if (!result || !result.tempFilePath) {
return
}
const file = {
path: result.tempFilePath,
name: result.name || 'practice-log.mp4',
size: result.size || 0
}
uni.showLoading({ title: '上传中', mask: true })
const uploaded = await uploadTempFile(file)
this.current.practiceLog = uploaded.url || uploaded.downloadUrl || uploaded.path || ''
uni.showToast({ title: '视频上传成功', icon: 'none' })
} catch (error) {
if (error && error.errMsg && String(error.errMsg).includes('cancel')) {
return
}
uni.showToast({ title: (error && error.message) || '视频上传失败', icon: 'none' })
} finally {
uni.hideLoading()
}
},
previewImage(url) {
if (!url) {
return
}
uni.previewImage({
urls: [url],
current: url
})
},
setTeamMemberField(index, key, event) {
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
this.current.teamMembers[index][key] = value || ''
},
addTeamMember() {
this.current.teamMembers = [...this.current.teamMembers, createTeamMember()]
},
removeTeamMember(index) {
this.current.teamMembers = this.current.teamMembers.filter((_, currentIndex) => currentIndex !== index)
},
setAdvisorField(index, key, event) {
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
this.current.advisors[index][key] = value || ''
},
addAdvisor() {
this.current.advisors = [...this.current.advisors, createAdvisor()]
},
removeAdvisor(index) {
this.current.advisors = this.current.advisors.filter((_, currentIndex) => currentIndex !== index)
},
openFormProjectTypePicker() {
this.$nextTick(() => {
this.$refs.formProjectTypePicker && this.$refs.formProjectTypePicker.open()
})
},
openFormProjectGroupPicker() {
this.$nextTick(() => {
this.$refs.formProjectGroupPicker && this.$refs.formProjectGroupPicker.open()
})
},
openPublicProjectTypePicker() {
this.$nextTick(() => {
this.$refs.publicProjectTypePicker && this.$refs.publicProjectTypePicker.open()
})
},
openPublicProjectGroupPicker() {
this.$nextTick(() => {
this.$refs.publicProjectGroupPicker && this.$refs.publicProjectGroupPicker.open()
})
},
confirmFormProjectTypePicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
this.current.projectType = value
},
confirmFormProjectGroupPicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
this.current.projectGroup = value
},
confirmPublicProjectTypePicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
this.current.publicProjectType = value
},
confirmPublicProjectGroupPicker(event) {
const value = (((event || {}).value || [])[0] || {}).value || ''
this.current.publicProjectGroup = value
},
buildSavePayload() {
const leaderInfo = this.getLeaderInfo(this.current)
return {
...this.current,
year: this.fixedYear || this.current.year,
leader: leaderInfo.leader,
phone: leaderInfo.phone
}
},
hasValidTeamMember() {
return (this.current.teamMembers || []).some((item) => {
return [item.name, item.phone, item.college, item.gradeMajor].some((value) => String(value || '').trim())
})
},
hasValidAdvisor() {
return (this.current.advisors || []).some((item) => {
return [item.name, item.phone, item.college, item.title, item.duty].some((value) => String(value || '').trim())
})
},
async save() {
try {
if (!this.hasValidTeamMember()) {
uni.showToast({ title: '请至少填写 1 名团队成员', icon: 'none' })
return
}
if (!this.hasValidAdvisor()) {
uni.showToast({ title: '请至少填写 1 名指导教师', icon: 'none' })
return
}
const payload = this.buildSavePayload()
await addTzbcyForm(payload)
uni.showToast({ title: '保存成功', icon: 'none' })
this.closeEdit()
this.loadData()
} catch (error) {
uni.showToast({ title: (error && error.message) || '保存失败', icon: 'none' })
}
}
}
}
</script>
<style lang="scss" scoped>
.page { min-height: 100vh; background: linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%); }
.page-scroll { height: 100vh; padding: 24rpx; box-sizing: border-box; }
.hero-card,.state-card,.data-card { border-radius: 28rpx; }
.hero-card { padding: 34rpx 36rpx; background: rgba(255,255,255,.5); box-shadow: 0 4rpx 12rpx 0 rgba(174,174,174,.4); backdrop-filter: blur(24rpx); -webkit-backdrop-filter: blur(24rpx); border: 1rpx solid rgba(255,255,255,.5); }
.hero-badge { display: inline-flex; padding: 10rpx 18rpx; border-radius: 999rpx; background: rgba(20,150,242,.1); font-size: 22rpx; color: #1496f2; }
.hero-title { margin-top: 8rpx; padding-left: 10rpx; font-size: 30rpx; font-weight: 700; line-height: 1.35; color: #18a0f7; border-left: 5px solid #0f94ef; }
.hero-desc { margin-top: 20rpx; font-size: 24rpx; line-height: 1.8; color: #7f7f7f; }
.hero-actions { display: flex; gap: 16rpx; margin-top: 24rpx; }
.hero-action { padding: 14rpx 24rpx; border-radius: 999rpx; background: rgba(20,150,242,.12); font-size: 24rpx; color: #1496f2; }
.hero-action--secondary { background: rgba(31,35,41,.08); color: #5f6b76; }
.state-card,.data-card { margin-top: 24rpx; padding: 24rpx; background: rgba(255,255,255,.96); box-shadow: 0 10rpx 28rpx rgba(34,94,142,.08); }
.state-title,.card-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
.state-desc,.card-subtitle,.meta-line { margin-top: 10rpx; font-size: 24rpx; line-height: 1.6; color: #786e69; }
.card-list { margin-top: 24rpx; display: flex; flex-direction: column; gap: 16rpx; }
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16rpx; }
.card-year { padding: 10rpx 16rpx; border-radius: 999rpx; background: #eef5ff; color: #1554ad; font-size: 22rpx; }
.modal-mask { position: fixed; inset: 0; background: rgba(15,23,42,.34); display: flex; align-items: flex-end; z-index: 99; }
.modal-panel { width: 100%; max-height: 92vh; border-radius: 32rpx 32rpx 0 0; background: #fff; display: flex; flex-direction: column; }
.modal-head { display: flex; justify-content: space-between; align-items: center; padding: 24rpx; border-bottom: 1rpx solid #eef1f4; }
.modal-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
.modal-close { font-size: 24rpx; color: #7c8a96; }
.modal-body { max-height: 70vh; padding: 24rpx; box-sizing: border-box; }
.tab-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12rpx; margin-bottom: 20rpx; }
.tab-item { height: 76rpx; line-height: 76rpx; border-radius: 18rpx; background: #eff5ff; color: #1554ad; font-size: 24rpx; text-align: center; }
.tab-item--active { background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%); color: #fff; }
.form-section + .form-section { margin-top: 24rpx; }
.form-section-title,.section-subtitle { font-size: 28rpx; font-weight: 700; color: #1f2329; margin-bottom: 16rpx; }
.section-header { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-top: 24rpx; }
.section-action { flex-shrink: 0; font-size: 22rpx; font-weight: 600; color: #1496f2; }
.section-subtitle { margin-top: 24rpx; }
.empty-inline { margin-top: 12rpx; padding: 20rpx; border-radius: 16rpx; background: #f8fafc; color: #98a2b3; font-size: 24rpx; text-align: center; }
.group-card { margin-top: 16rpx; padding: 20rpx; border-radius: 20rpx; background: #f8fbff; border: 1rpx solid #dce4ec; }
.group-head { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-bottom: 16rpx; }
.group-title { margin-bottom: 16rpx; font-size: 24rpx; font-weight: 700; color: #1554ad; }
.group-remove { flex-shrink: 0; font-size: 22rpx; color: #d92d20; }
.field-grid { display: grid; gap: 16rpx; }
.field-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.field-item + .field-item { margin-top: 16rpx; }
.field-label { margin-bottom: 10rpx; font-size: 24rpx; color: #5f6b7a; }
.field-note { margin-top: 6rpx; font-size: 20rpx; line-height: 1.6; color: #98a2b3; }
.field-textarea { width: 100%; box-sizing: border-box; border-radius: 20rpx; border: 1rpx solid #dce4ec; background: #fff; padding: 20rpx; font-size: 24rpx; color: #1f2329; min-height: 180rpx; }
:deep(.field-input.uv-input) { width: 100%; box-sizing: border-box; border-radius: 20rpx; border: 1rpx solid #dce4ec !important; background: #fff !important; padding: 20rpx !important; }
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) { font-size: 24rpx; color: #1f2329; min-height: 40rpx; }
:deep(.field-input--readonly.uv-input) { background: #f8fafc !important; }
:deep(.field-input--readonly.uv-input .uv-input__content__field-wrapper__field) { color: #667085; }
.field-picker { width: 100%; box-sizing: border-box; padding: 20rpx; border-radius: 20rpx; border: 1rpx solid rgba(220,220,220,1); background: #F4FCFF; font-size: 24rpx; }
.field-placeholder { color: #98a2b3; }
.field-value { color: #1f2329; }
.upload-card { padding: 20rpx; border-radius: 20rpx; border: 1rpx solid #dce4ec; background: #fff; }
.upload-empty { display: flex; align-items: center; justify-content: center; min-height: 220rpx; border-radius: 16rpx; background: #f8fafc; color: #98a2b3; font-size: 24rpx; }
.upload-image { width: 100%; height: 280rpx; border-radius: 16rpx; background: #f3f4f6; }
.video-box { border-radius: 16rpx; overflow: hidden; background: #0f172a; }
.upload-video { width: 100%; height: 320rpx; }
.upload-actions { display: flex; gap: 16rpx; margin-top: 18rpx; }
.upload-btn { flex: 1; height: 76rpx; line-height: 76rpx; border-radius: 18rpx; background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%); color: #fff; text-align: center; font-size: 24rpx; font-weight: 600; }
.upload-btn--ghost { background: #eef5ff; color: #1554ad; }
.modal-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16rpx; padding: 24rpx; border-top: 1rpx solid #eef1f4; }
.ghost-btn,.primary-btn { height: 84rpx; line-height: 84rpx; border-radius: 22rpx; text-align: center; font-size: 26rpx; font-weight: 700; }
.ghost-btn { background: #f6f7fa; color: #50617a; }
.primary-btn { background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%); color: #fff; }
</style>