788 lines
35 KiB
Vue
788 lines
35 KiB
Vue
<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>
|