This commit is contained in:
jeremygan2021
2026-03-10 14:05:37 +08:00
parent 3d74ccc04f
commit 880192c358
9 changed files with 254 additions and 45 deletions

View File

@@ -79,12 +79,17 @@ export const getProjectDetail = (id: number) => request({ url: `/competition/pro
export const createProject = (data: any) => request({ url: '/competition/projects/', method: 'POST', data })
export const updateProject = (id: number, data: any) => request({ url: `/competition/projects/${id}/`, method: 'PATCH', data })
export const submitProject = (id: number) => request({ url: `/competition/projects/${id}/submit/`, method: 'POST' })
export const uploadProjectFile = (filePath: string) => {
export const getComments = (params: any) => request({ url: '/competition/comments/', data: params })
export const uploadProjectFile = (filePath: string, projectId: number, fileName?: string) => {
const BASE_URL = (typeof process !== 'undefined' && process.env && process.env.TARO_APP_API_URL) || 'https://market.quant-speed.com/api'
return Taro.uploadFile({
url: `${BASE_URL}/competition/files/`,
filePath,
name: 'file',
formData: {
project: projectId,
name: fileName || ''
},
header: {
'Authorization': `Bearer ${Taro.getStorageSync('token')}`
}

View File

@@ -1,7 +1,7 @@
import { View, Text, Button, Image, ScrollView } from '@tarojs/components'
import { View, Text, Button, Image, ScrollView, PageContainer } from '@tarojs/components'
import Taro, { useLoad, useDidShow } from '@tarojs/taro'
import { useState, useEffect } from 'react'
import { getCompetitionDetail, enrollCompetition, getMyCompetitionEnrollment, getProjects } from '../../api'
import { getCompetitionDetail, enrollCompetition, getMyCompetitionEnrollment, getProjects, getComments } from '../../api'
import MarkdownReader from '../../components/MarkdownReader'
import './detail.scss'
@@ -12,6 +12,8 @@ export default function CompetitionDetail() {
const [myProject, setMyProject] = useState<any>(null)
const [activeTab, setActiveTab] = useState(0)
const [loading, setLoading] = useState(false)
const [showComments, setShowComments] = useState(false)
const [comments, setComments] = useState<any[]>([])
useLoad((options) => {
const { id } = options
@@ -145,6 +147,20 @@ export default function CompetitionDetail() {
} catch (e) {}
}
const fetchComments = async (projectId) => {
Taro.showLoading({ title: '加载中' })
try {
const res = await getComments({ project: projectId })
const list = res.results || res.data || res || []
setComments(Array.isArray(list) ? list : [])
setShowComments(true)
} catch (e) {
Taro.showToast({ title: '获取评语失败', icon: 'none' })
} finally {
Taro.hideLoading()
}
}
const handleEnroll = async () => {
if (!detail) return
try {
@@ -231,6 +247,10 @@ export default function CompetitionDetail() {
</View>
{project.final_score > 0 && <Text className='score'>{project.final_score}</Text>}
</View>
<Button size='mini' style={{ marginTop: '8px', fontSize: '12px', background: 'transparent', color: '#666', border: '1px solid #ddd' }} onClick={(e) => {
e.stopPropagation()
fetchComments(project.id)
}}></Button>
</View>
</View>
))}
@@ -264,12 +284,22 @@ export default function CompetitionDetail() {
<View className='footer-action'>
{enrollment ? (
myProject ? (
<Button
className='btn enrolled'
onClick={() => Taro.navigateTo({ url: `/pages/competition/project?id=${myProject.id}` })}
>
({myProject.status === 'submitted' ? '已提交' : '草稿'})
</Button>
<View style={{ display: 'flex', width: '100%', gap: '10px' }}>
<Button
className='btn enrolled'
style={{ flex: 1 }}
onClick={() => Taro.navigateTo({ url: `/pages/competition/project?id=${myProject.id}` })}
>
({myProject.status === 'submitted' ? '已提交' : '草稿'})
</Button>
<Button
className='btn'
style={{ width: '80px', background: '#fff', color: '#333', border: '1px solid #ddd', fontSize: '12px', padding: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
onClick={() => fetchComments(myProject.id)}
>
</Button>
</View>
) : (
enrollment.status === 'approved' ? (
<Button
@@ -294,6 +324,24 @@ export default function CompetitionDetail() {
</Button>
)}
</View>
<PageContainer show={showComments} onClickOverlay={() => setShowComments(false)} position='bottom' round>
<View className='comments-container' style={{ padding: '20px', maxHeight: '60vh', background: '#fff', borderTopLeftRadius: '16px', borderTopRightRadius: '16px' }}>
<Text style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '16px', display: 'block', textAlign: 'center' }}></Text>
<ScrollView scrollY style={{ height: '300px' }}>
{comments.length > 0 ? comments.map((c: any) => (
<View key={c.id} style={{ marginBottom: '16px', borderBottom: '1px solid #eee', paddingBottom: '8px' }}>
<View style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
<Text style={{ fontWeight: 'bold', fontSize: '14px' }}>{c.judge_name || '评委'}</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>{c.created_at?.substring(0, 16)}</Text>
</View>
<Text style={{ display: 'block', color: '#333', fontSize: '14px', lineHeight: '1.5' }}>{c.content}</Text>
</View>
)) : <Text style={{ color: '#999', textAlign: 'center', display: 'block', marginTop: '20px' }}></Text>}
</ScrollView>
<Button onClick={() => setShowComments(false)} style={{ marginTop: '16px' }}></Button>
</View>
</PageContainer>
</ScrollView>
)
}

View File

@@ -16,7 +16,7 @@
color: #ccc;
}
.input, .textarea {
.input, .textarea, .picker {
background: #1f1f1f;
border-radius: 8px;
padding: 12px;

View File

@@ -1,7 +1,7 @@
import { View, Text, Button, Image, Input, Textarea } from '@tarojs/components'
import { View, Text, Button, Image, Input, Textarea, Picker } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro'
import { useState } from 'react'
import { getProjectDetail, createProject, updateProject, uploadProjectFile, submitProject, uploadMedia } from '../../api'
import { getProjectDetail, createProject, updateProject, uploadProjectFile, submitProject, uploadMedia, getCompetitions } from '../../api'
import './project.scss'
export default function ProjectEdit() {
@@ -12,10 +12,12 @@ export default function ProjectEdit() {
files: []
})
const [competitionId, setCompetitionId] = useState<string>('')
const [competitions, setCompetitions] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const [isEdit, setIsEdit] = useState(false)
useLoad((options) => {
fetchCompetitions()
const { id, competitionId } = options
if (id) {
setIsEdit(true)
@@ -25,6 +27,17 @@ export default function ProjectEdit() {
}
})
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 {
@@ -59,6 +72,49 @@ export default function ProjectEdit() {
}
}
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' })
@@ -105,6 +161,26 @@ export default function ProjectEdit() {
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
@@ -151,7 +227,21 @@ export default function ProjectEdit() {
/>
</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>