This commit is contained in:
@@ -55,9 +55,19 @@ class ProjectFileSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from shop.utils import get_current_wechat_user
|
||||||
|
|
||||||
|
request = self.context.get('request')
|
||||||
|
user = get_current_wechat_user(request) if request else None
|
||||||
|
|
||||||
|
print(f"=== File Upload Debug ===")
|
||||||
|
print(f"User: {user}")
|
||||||
|
print(f"Validated data keys: {validated_data.keys()}")
|
||||||
|
|
||||||
file_obj = validated_data.get('file')
|
file_obj = validated_data.get('file')
|
||||||
|
|
||||||
if file_obj:
|
if file_obj:
|
||||||
|
print(f"File name: {file_obj.name}, size: {file_obj.size}")
|
||||||
ext = file_obj.name.split('.')[-1].lower() if '.' in file_obj.name else ''
|
ext = file_obj.name.split('.')[-1].lower() if '.' in file_obj.name else ''
|
||||||
if ext in ['ppt', 'pptx']:
|
if ext in ['ppt', 'pptx']:
|
||||||
validated_data['file_type'] = 'ppt'
|
validated_data['file_type'] = 'ppt'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from rest_framework import viewsets, permissions, status, filters, serializers
|
from rest_framework import viewsets, permissions, status, filters, serializers
|
||||||
from rest_framework.decorators import action, api_view, permission_classes
|
from rest_framework.decorators import action, api_view, permission_classes, csrf_exempt
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@@ -189,6 +189,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
|||||||
return Response({"status": "submitted"})
|
return Response({"status": "submitted"})
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
class ProjectFileViewSet(viewsets.ModelViewSet):
|
class ProjectFileViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
项目附件管理
|
项目附件管理
|
||||||
@@ -200,13 +201,24 @@ class ProjectFileViewSet(viewsets.ModelViewSet):
|
|||||||
return ProjectFile.objects.all()
|
return ProjectFile.objects.all()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
|
from shop.utils import get_current_wechat_user
|
||||||
|
try:
|
||||||
project = serializer.validated_data['project']
|
project = serializer.validated_data['project']
|
||||||
user = get_current_wechat_user(self.request)
|
user = get_current_wechat_user(self.request)
|
||||||
|
|
||||||
|
print(f"=== perform_create debug ===")
|
||||||
|
print(f"User: {user}")
|
||||||
|
print(f"Project: {project}")
|
||||||
|
|
||||||
if not user or project.contestant.user != user:
|
if not user or project.contestant.user != user:
|
||||||
raise serializers.ValidationError("无权上传文件")
|
raise serializers.ValidationError("无权上传文件")
|
||||||
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"=== perform_create ERROR: {e} ===")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class ScoreViewSet(viewsets.ModelViewSet):
|
class ScoreViewSet(viewsets.ModelViewSet):
|
||||||
|
|||||||
@@ -202,10 +202,39 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }
|
|||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="cover_image_url"
|
name="cover_image_url"
|
||||||
label="封面图片链接"
|
label="封面图片"
|
||||||
rules={[{ type: 'url', message: '请输入有效的URL' }]}
|
extra="支持上传本地图片,自动转换为URL"
|
||||||
>
|
>
|
||||||
<Input prefix={<LinkOutlined />} placeholder="https://example.com/image.jpg" />
|
{initialValues ? (
|
||||||
|
<Upload
|
||||||
|
showUploadList={false}
|
||||||
|
accept="image/*"
|
||||||
|
beforeUpload={(file) => {
|
||||||
|
console.log('Cover image selected:', file.name);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
uploadProjectFile(formData)
|
||||||
|
.then(res => {
|
||||||
|
const imageUrl = res.data.file_url_display || res.data.file_url;
|
||||||
|
form.setFieldsValue({ cover_image_url: imageUrl });
|
||||||
|
message.success('封面上传成功');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
message.error(`上传失败: ${err.response?.data?.detail || err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Upload.LIST_IGNORE;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button icon={<CloudUploadOutlined />}>选择图片</Button>
|
||||||
|
</Upload>
|
||||||
|
) : null}
|
||||||
|
<Input
|
||||||
|
placeholder="上传图片或输入URL"
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
onChange={(e) => form.setFieldsValue({ cover_image_url: e.target.value })}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{initialValues?.id && (
|
{initialValues?.id && (
|
||||||
|
|||||||
@@ -94,14 +94,40 @@ export const uploadProjectFile = (filePath: string, projectId: number, fileName?
|
|||||||
'Authorization': `Bearer ${Taro.getStorageSync('token')}`
|
'Authorization': `Bearer ${Taro.getStorageSync('token')}`
|
||||||
}
|
}
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
console.log('Upload response:', res)
|
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
const data = JSON.parse(res.data)
|
const data = JSON.parse(res.data)
|
||||||
console.log('Upload success, data:', data)
|
if (data.error) {
|
||||||
|
const error = new Error(data.error) as any
|
||||||
|
error.response = data
|
||||||
|
throw error
|
||||||
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
console.error('Upload failed:', res)
|
const error = new Error('Upload failed') as any
|
||||||
throw new Error(`Upload failed: ${res.statusCode}`)
|
error.statusCode = res.statusCode
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteProjectFile = (fileId: number) => {
|
||||||
|
const BASE_URL = (typeof process !== 'undefined' && process.env && process.env.TARO_APP_API_URL) || 'https://market.quant-speed.com/api'
|
||||||
|
return Taro.request({
|
||||||
|
url: `${BASE_URL}/competition/files/${fileId}/`,
|
||||||
|
method: 'DELETE',
|
||||||
|
header: {
|
||||||
|
'Authorization': `Bearer ${Taro.getStorageSync('token')}`
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const error = new Error('Delete failed') as any
|
||||||
|
error.statusCode = res.statusCode
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(res.data)
|
||||||
|
error.response = data
|
||||||
|
} catch {}
|
||||||
|
throw error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +146,21 @@ export const uploadMedia = (filePath: string, type: 'image' | 'video') => {
|
|||||||
}
|
}
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
return JSON.parse(res.data)
|
const data = JSON.parse(res.data)
|
||||||
|
if (data.error) {
|
||||||
|
const error = new Error(data.error) as any
|
||||||
|
error.response = data
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Upload failed')
|
return data
|
||||||
|
}
|
||||||
|
const error = new Error('Upload failed') as any
|
||||||
|
error.statusCode = res.statusCode
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(res.data)
|
||||||
|
error.response = data
|
||||||
|
} catch {}
|
||||||
|
throw error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { View, Text, Button, Image, Input, Textarea, Picker } from '@tarojs/components'
|
import { View, Text, Button, Image, Input, Textarea, Picker } from '@tarojs/components'
|
||||||
import Taro, { useLoad, useShareAppMessage, useShareTimeline, useRouter } from '@tarojs/taro'
|
import Taro, { useLoad, useShareAppMessage, useShareTimeline, useRouter } from '@tarojs/taro'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { getProjectDetail, createProject, updateProject, uploadProjectFile, submitProject, uploadMedia, getCompetitions } from '../../api'
|
import { getProjectDetail, createProject, updateProject, uploadProjectFile, submitProject, uploadMedia, getCompetitions, deleteProjectFile } from '../../api'
|
||||||
import './project.scss'
|
import './project.scss'
|
||||||
|
|
||||||
export default function ProjectEdit() {
|
export default function ProjectEdit() {
|
||||||
@@ -89,21 +89,14 @@ export default function ProjectEdit() {
|
|||||||
|
|
||||||
Taro.showLoading({ title: '上传中...' })
|
Taro.showLoading({ title: '上传中...' })
|
||||||
|
|
||||||
console.log('Uploading cover image:', tempFilePaths[0])
|
|
||||||
const res = await uploadMedia(tempFilePaths[0], 'image')
|
const res = await uploadMedia(tempFilePaths[0], 'image')
|
||||||
console.log('Cover upload result:', res)
|
handleInput('cover_image_url', res.file)
|
||||||
|
|
||||||
// res.file is the OSS URL
|
|
||||||
const coverUrl = res.file || res.file_url || res.url
|
|
||||||
console.log('Cover URL:', coverUrl)
|
|
||||||
handleInput('cover_image_url', coverUrl)
|
|
||||||
|
|
||||||
Taro.hideLoading()
|
Taro.hideLoading()
|
||||||
Taro.showToast({ title: '上传成功', icon: 'success' })
|
} catch (e: any) {
|
||||||
} catch (e) {
|
|
||||||
Taro.hideLoading()
|
Taro.hideLoading()
|
||||||
console.error('Cover upload error:', e)
|
const errorMsg = e.response?.error || e.message || '上传失败'
|
||||||
Taro.showToast({ title: '上传失败', icon: 'none' })
|
Taro.showToast({ title: errorMsg, icon: 'none' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,52 +113,25 @@ export default function ProjectEdit() {
|
|||||||
|
|
||||||
Taro.showLoading({ title: '上传中...' })
|
Taro.showLoading({ title: '上传中...' })
|
||||||
const file = tempFiles[0]
|
const file = tempFiles[0]
|
||||||
console.log('Uploading file:', file.name, 'path:', file.path)
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const result = await uploadProjectFile(file.path, project.id, file.name)
|
const result = await uploadProjectFile(file.path, project.id, file.name)
|
||||||
console.log('Upload result:', result)
|
|
||||||
|
|
||||||
// Update file list - use file_url_display or file_url
|
// Update file list
|
||||||
const fileUrl = result.file_url_display || result.file_url || ''
|
|
||||||
setProject(prev => ({
|
setProject(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
files: [...(prev.files || []), {
|
files: [...(prev.files || []), result]
|
||||||
...result,
|
|
||||||
url: fileUrl
|
|
||||||
}]
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
Taro.hideLoading()
|
Taro.hideLoading()
|
||||||
Taro.showToast({ title: '上传成功', icon: 'success' })
|
Taro.showToast({ title: '上传成功', icon: 'success' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Taro.hideLoading()
|
Taro.hideLoading()
|
||||||
console.error('Upload error:', e)
|
console.error(e)
|
||||||
Taro.showToast({ title: '上传失败', icon: 'none' })
|
Taro.showToast({ title: '上传失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePreviewFile = (file: any) => {
|
|
||||||
const url = file.url || file.file_url_display || file.file_url
|
|
||||||
if (!url) {
|
|
||||||
Taro.showToast({ title: '文件链接无效', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Taro.downloadFile({
|
|
||||||
url: url,
|
|
||||||
success: (res: any) => {
|
|
||||||
if (res.statusCode === 200) {
|
|
||||||
Taro.openDocument({ filePath: res.tempFilePath }).catch(() => {
|
|
||||||
Taro.showToast({ title: '无法打开文件', icon: 'none' })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
Taro.showToast({ title: '无法打开文件', icon: 'none' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteFile = (fileId) => {
|
const handleDeleteFile = (fileId) => {
|
||||||
// API call to delete file not implemented yet? Or just remove from list?
|
// 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.
|
// Usually we should call delete API. For now just remove from UI.
|
||||||
@@ -295,10 +261,10 @@ export default function ProjectEdit() {
|
|||||||
<Button size='mini' style={{ margin: 0, fontSize: '12px' }} onClick={handleUploadFile}>上传附件</Button>
|
<Button size='mini' style={{ margin: 0, fontSize: '12px' }} onClick={handleUploadFile}>上传附件</Button>
|
||||||
</View>
|
</View>
|
||||||
<View className='file-list'>
|
<View className='file-list'>
|
||||||
{project.files && project.files.map((file: any, index: number) => (
|
{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' }} onClick={() => handlePreviewFile(file)}>
|
<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='file-name' style={{ flex: 1, fontSize: '14px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{file.name || '未知文件'}</Text>
|
||||||
<Text style={{ color: '#1890ff', fontSize: '12px', marginLeft: '10px' }}>点击查看</Text>
|
{/* <Text className='delete' style={{ color: 'red', marginLeft: '10px' }} onClick={() => handleDeleteFile(file.id)}>删除</Text> */}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
{(!project.files || project.files.length === 0) && <Text style={{ color: '#999', fontSize: '12px' }}>暂无附件 (PDF/PPT/视频)</Text>}
|
{(!project.files || project.files.length === 0) && <Text style={{ color: '#999', fontSize: '12px' }}>暂无附件 (PDF/PPT/视频)</Text>}
|
||||||
|
|||||||
@@ -79,12 +79,15 @@ const CreateTopic = () => {
|
|||||||
setContent(prev => prev + insertText)
|
setContent(prev => prev + insertText)
|
||||||
|
|
||||||
Taro.hideLoading()
|
Taro.hideLoading()
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
Taro.hideLoading()
|
Taro.hideLoading()
|
||||||
// Only toast if it's an error, not cancel
|
|
||||||
if (error.errMsg && error.errMsg.indexOf('cancel') === -1) {
|
if (error.errMsg && error.errMsg.indexOf('cancel') === -1) {
|
||||||
Taro.showToast({ title: '上传失败', icon: 'none' })
|
const errorMsg = error.response?.error || error.message || '上传失败'
|
||||||
|
Taro.showToast({ title: errorMsg, icon: 'none' })
|
||||||
|
} else if (!error.errMsg) {
|
||||||
|
const errorMsg = error.response?.error || error.message || '上传失败'
|
||||||
|
Taro.showToast({ title: errorMsg, icon: 'none' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,10 +170,11 @@ const ForumDetail = () => {
|
|||||||
setReplyContent(prev => prev + insertText)
|
setReplyContent(prev => prev + insertText)
|
||||||
|
|
||||||
Taro.hideLoading()
|
Taro.hideLoading()
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
Taro.hideLoading()
|
Taro.hideLoading()
|
||||||
Taro.showToast({ title: '上传失败', icon: 'none' })
|
const errorMsg = error.response?.error || error.message || '上传失败'
|
||||||
|
Taro.showToast({ title: errorMsg, icon: 'none' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user