280 lines
9.8 KiB
TypeScript
280 lines
9.8 KiB
TypeScript
import { View, Text, Button, Image, Input, Textarea, Picker } from '@tarojs/components'
|
|
import Taro, { useLoad, useShareAppMessage, useShareTimeline, useRouter } from '@tarojs/taro'
|
|
import { useState } from 'react'
|
|
import { getProjectDetail, createProject, updateProject, uploadProjectFile, submitProject, uploadMedia, getCompetitions } from '../../api'
|
|
import './project.scss'
|
|
|
|
export default function ProjectEdit() {
|
|
const [project, setProject] = useState<any>({
|
|
title: '',
|
|
description: '',
|
|
team_info: '',
|
|
files: []
|
|
})
|
|
const [competitionId, setCompetitionId] = useState<string>('')
|
|
const [competitions, setCompetitions] = useState<any[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [isEdit, setIsEdit] = useState(false)
|
|
const router = useRouter()
|
|
|
|
useLoad((options) => {
|
|
fetchCompetitions()
|
|
const { id, competitionId } = options
|
|
if (id) {
|
|
setIsEdit(true)
|
|
fetchProject(id)
|
|
} else if (competitionId) {
|
|
setCompetitionId(competitionId)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 配置并监听分享给朋友的功能
|
|
*/
|
|
useShareAppMessage(() => {
|
|
const id = project?.id || router.params.id || ''
|
|
const compId = competitionId || router.params.competitionId || ''
|
|
return {
|
|
title: project?.title || '提交作品',
|
|
path: `/pages/competition/project?id=${id}&competitionId=${compId}`,
|
|
imageUrl: project?.cover_image_url || project?.display_cover_image || ''
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 配置并监听分享到朋友圈的功能
|
|
*/
|
|
useShareTimeline(() => {
|
|
const id = project?.id || router.params.id || ''
|
|
const compId = competitionId || router.params.competitionId || ''
|
|
return {
|
|
title: project?.title || '提交作品',
|
|
query: `id=${id}&competitionId=${compId}`,
|
|
imageUrl: project?.cover_image_url || project?.display_cover_image || ''
|
|
}
|
|
})
|
|
|
|
const fetchCompetitions = async () => {
|
|
try {
|
|
const res = await getCompetitions()
|
|
if (res && res.results) {
|
|
setCompetitions(res.results)
|
|
}
|
|
} catch (e) {
|
|
console.error('获取比赛列表失败', e)
|
|
}
|
|
}
|
|
|
|
const fetchProject = async (id) => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await getProjectDetail(id)
|
|
setProject(res)
|
|
setCompetitionId(res.competition)
|
|
} catch (e) {
|
|
Taro.showToast({ title: '加载项目失败', icon: 'none' })
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleInput = (key, value) => {
|
|
setProject(prev => ({ ...prev, [key]: value }))
|
|
}
|
|
|
|
const handleUploadCover = async () => {
|
|
try {
|
|
const { tempFilePaths } = await Taro.chooseImage({ count: 1 })
|
|
if (!tempFilePaths.length) return
|
|
|
|
Taro.showLoading({ title: '上传中...' })
|
|
|
|
const res = await uploadMedia(tempFilePaths[0], 'image')
|
|
handleInput('cover_image_url', res.file) // 假设返回 { file: 'url...' }
|
|
|
|
Taro.hideLoading()
|
|
} catch (e) {
|
|
Taro.hideLoading()
|
|
Taro.showToast({ title: '上传失败', icon: 'none' })
|
|
}
|
|
}
|
|
|
|
const handleUploadFile = async () => {
|
|
if (!project.id) {
|
|
Taro.showToast({ title: '请先保存草稿再上传附件', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
try {
|
|
const res = await Taro.chooseMessageFile({ count: 1, type: 'file' })
|
|
const tempFiles = res.tempFiles
|
|
if (!tempFiles.length) return
|
|
|
|
Taro.showLoading({ title: '上传中...' })
|
|
const file = tempFiles[0]
|
|
|
|
// @ts-ignore
|
|
const result = await uploadProjectFile(file.path, project.id, file.name)
|
|
|
|
// Update file list
|
|
setProject(prev => ({
|
|
...prev,
|
|
files: [...(prev.files || []), result]
|
|
}))
|
|
|
|
Taro.hideLoading()
|
|
Taro.showToast({ title: '上传成功', icon: 'success' })
|
|
} catch (e) {
|
|
Taro.hideLoading()
|
|
console.error(e)
|
|
Taro.showToast({ title: '上传失败', icon: 'none' })
|
|
}
|
|
}
|
|
|
|
const handleDeleteFile = (fileId) => {
|
|
// API call to delete file not implemented yet? Or just remove from list?
|
|
// Usually we should call delete API. For now just remove from UI.
|
|
// Ideally we should have deleteProjectFile API.
|
|
// But user only asked to "optimize upload".
|
|
setProject(prev => ({
|
|
...prev,
|
|
files: prev.files.filter(f => f.id !== fileId)
|
|
}))
|
|
}
|
|
|
|
const handleSave = async (submit = false) => {
|
|
if (!project.title) {
|
|
Taro.showToast({ title: '请输入项目标题', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
setLoading(true)
|
|
try {
|
|
const data = {
|
|
competition: competitionId,
|
|
title: project.title,
|
|
description: project.description,
|
|
team_info: project.team_info,
|
|
cover_image_url: project.cover_image_url
|
|
}
|
|
|
|
let res
|
|
if (isEdit) {
|
|
res = await updateProject(project.id, data)
|
|
} else {
|
|
res = await createProject(data)
|
|
}
|
|
|
|
if (submit) {
|
|
await submitProject(res.id)
|
|
Taro.showToast({ title: '提交成功', icon: 'success' })
|
|
setTimeout(() => Taro.navigateBack(), 1500)
|
|
} else {
|
|
Taro.showToast({ title: '保存成功', icon: 'success' })
|
|
if (!isEdit) {
|
|
// 创建变编辑
|
|
setIsEdit(true)
|
|
setProject(res)
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Taro.showToast({ title: e.message || '操作失败', icon: 'none' })
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
if (loading && !project.id && isEdit) return <View className='loading'>加载中...</View>
|
|
|
|
return (
|
|
<View className='project-edit'>
|
|
<View className='form-item'>
|
|
<Text className='label'>所属比赛</Text>
|
|
<Picker
|
|
mode='selector'
|
|
range={competitions}
|
|
rangeKey='title'
|
|
onChange={e => {
|
|
const idx = Number(e.detail.value)
|
|
const selected = competitions[idx]
|
|
if (selected) {
|
|
setCompetitionId(String(selected.id))
|
|
}
|
|
}}
|
|
>
|
|
<View className='picker'>
|
|
{competitions.find(c => String(c.id) === String(competitionId))?.title || '请选择比赛'}
|
|
</View>
|
|
</Picker>
|
|
</View>
|
|
|
|
<View className='form-item'>
|
|
<Text className='label'>项目标题</Text>
|
|
<Input
|
|
className='input'
|
|
placeholder='请输入项目标题'
|
|
value={project.title}
|
|
onInput={e => handleInput('title', e.detail.value)}
|
|
/>
|
|
</View>
|
|
|
|
<View className='form-item'>
|
|
<Text className='label'>封面图</Text>
|
|
<View className='upload-box' onClick={handleUploadCover}>
|
|
{project.cover_image_url || project.display_cover_image ? (
|
|
<Image
|
|
className='preview'
|
|
mode='aspectFill'
|
|
src={project.cover_image_url || project.display_cover_image}
|
|
/>
|
|
) : (
|
|
<Text className='placeholder'>点击上传封面</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
<View className='form-item'>
|
|
<Text className='label'>项目介绍</Text>
|
|
<Textarea
|
|
className='textarea'
|
|
placeholder='请输入项目详细介绍'
|
|
value={project.description}
|
|
onInput={e => handleInput('description', e.detail.value)}
|
|
maxlength={2000}
|
|
/>
|
|
</View>
|
|
|
|
<View className='form-item'>
|
|
<Text className='label'>团队介绍</Text>
|
|
<Textarea
|
|
className='textarea small'
|
|
placeholder='请输入团队成员信息'
|
|
value={project.team_info}
|
|
onInput={e => handleInput('team_info', e.detail.value)}
|
|
/>
|
|
</View>
|
|
|
|
<View className='form-item'>
|
|
<View className='label-row' style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
|
|
<Text className='label' style={{ marginBottom: 0 }}>项目附件</Text>
|
|
<Button size='mini' style={{ margin: 0, fontSize: '12px' }} onClick={handleUploadFile}>上传附件</Button>
|
|
</View>
|
|
<View className='file-list'>
|
|
{project.files && project.files.map((file, index) => (
|
|
<View key={index} className='file-item' style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px', background: '#f8f8f8', marginBottom: '8px', borderRadius: '4px' }}>
|
|
<Text className='file-name' style={{ flex: 1, fontSize: '14px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{file.name || '未知文件'}</Text>
|
|
{/* <Text className='delete' style={{ color: 'red', marginLeft: '10px' }} onClick={() => handleDeleteFile(file.id)}>删除</Text> */}
|
|
</View>
|
|
))}
|
|
{(!project.files || project.files.length === 0) && <Text style={{ color: '#999', fontSize: '12px' }}>暂无附件 (PDF/PPT/视频)</Text>}
|
|
</View>
|
|
</View>
|
|
|
|
<View className='footer-btns'>
|
|
<Button className='btn save' onClick={() => handleSave(false)}>保存草稿</Button>
|
|
<Button className='btn submit' onClick={() => handleSave(true)}>提交作品</Button>
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|