|
|
@@ -42,6 +42,9 @@ const OrderDetail: React.FC = () => {
|
|
|
const router = Taro.useRouter()
|
|
|
const orderIdParam = router.params.id ? parseInt(router.params.id) : null
|
|
|
const orderId = orderIdParam && !Number.isNaN(orderIdParam) ? orderIdParam : null
|
|
|
+ const [showCheckinCalendar, setShowCheckinCalendar] = useState(false)
|
|
|
+ const [filterStartDate, setFilterStartDate] = useState('')
|
|
|
+ const [filterEndDate, setFilterEndDate] = useState('')
|
|
|
|
|
|
|
|
|
// 获取订单详情查询函数
|
|
|
@@ -359,11 +362,217 @@ const OrderDetail: React.FC = () => {
|
|
|
|
|
|
|
|
|
|
|
|
+ const handleViewCheckinDetails = () => {
|
|
|
+ setShowCheckinCalendar(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取打卡日历数据
|
|
|
+ const getCheckinCalendarData = () => {
|
|
|
+ if (!videos || videos.length === 0) {
|
|
|
+ return {
|
|
|
+ checkinDays: [],
|
|
|
+ totalDays: 31,
|
|
|
+ checkinCount: 0,
|
|
|
+ missedCount: 0,
|
|
|
+ attendanceRate: 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 筛选打卡视频
|
|
|
+ const checkinVideos = videos.filter(video => video.type === 'checkin_video')
|
|
|
+
|
|
|
+ // 提取日期(假设uploadTime格式为YYYY-MM-DD)
|
|
|
+ const checkinDates = checkinVideos.map(video => {
|
|
|
+ const dateStr = video.uploadTime
|
|
|
+ return dateStr.split(' ')[0] // 取日期部分
|
|
|
+ })
|
|
|
+
|
|
|
+ // 去重
|
|
|
+ const uniqueDates = [...new Set(checkinDates)]
|
|
|
+
|
|
|
+ // 计算本月数据(模拟)
|
|
|
+ const currentMonth = '2025-12'
|
|
|
+ const daysInMonth = 31
|
|
|
+
|
|
|
+ // 筛选本月日期
|
|
|
+ const monthDates = uniqueDates.filter(date => date.startsWith(currentMonth))
|
|
|
+
|
|
|
+ // 计算打卡日期
|
|
|
+ const checkinDays = monthDates.map(date => parseInt(date.split('-')[2]))
|
|
|
+
|
|
|
+ return {
|
|
|
+ checkinDays,
|
|
|
+ totalDays: daysInMonth,
|
|
|
+ checkinCount: checkinDays.length,
|
|
|
+ missedCount: daysInMonth - checkinDays.length,
|
|
|
+ attendanceRate: Math.round((checkinDays.length / daysInMonth) * 100)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
const handleDownloadReport = () => {
|
|
|
console.log('下载报告')
|
|
|
// TODO: 下载订单报告
|
|
|
}
|
|
|
|
|
|
+ // 导出打卡数据
|
|
|
+ const handleExportCheckinData = async () => {
|
|
|
+ try {
|
|
|
+ // 获取打卡数据
|
|
|
+ const checkinData = getCheckinCalendarData()
|
|
|
+ const checkinVideos = videos.filter(video => video.type === 'checkin_video')
|
|
|
+
|
|
|
+ // 创建CSV内容
|
|
|
+ const headers = ['日期', '视频名称', '视频类型', '大小', '上传时间']
|
|
|
+ const rows = checkinVideos.map(video => [
|
|
|
+ video.uploadTime.split(' ')[0],
|
|
|
+ video.name,
|
|
|
+ video.type === 'checkin_video' ? '打卡视频' : '其他',
|
|
|
+ video.size,
|
|
|
+ video.uploadTime
|
|
|
+ ])
|
|
|
+
|
|
|
+ // 添加统计信息
|
|
|
+ rows.push([])
|
|
|
+ rows.push(['统计信息', '', '', '', ''])
|
|
|
+ rows.push(['总打卡天数', checkinData.checkinCount.toString(), '', '', ''])
|
|
|
+ rows.push(['未打卡天数', checkinData.missedCount.toString(), '', '', ''])
|
|
|
+ rows.push(['出勤率', `${checkinData.attendanceRate}%`, '', '', ''])
|
|
|
+
|
|
|
+ const csvContent = [headers, ...rows].map(row => row.join(',')).join('\n')
|
|
|
+
|
|
|
+ // 在小程序中,可以使用Taro.saveFile或Taro.downloadFile
|
|
|
+ // 这里先显示提示,实际项目中需要实现文件保存逻辑
|
|
|
+ Taro.showToast({
|
|
|
+ title: `已生成${checkinVideos.length}条打卡记录`,
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+
|
|
|
+ console.log('打卡数据CSV:', csvContent)
|
|
|
+ // TODO: 实际文件保存逻辑
|
|
|
+ // Taro.downloadFile({
|
|
|
+ // url: 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent),
|
|
|
+ // success: function (res) {
|
|
|
+ // Taro.saveFile({
|
|
|
+ // tempFilePath: res.tempFilePath,
|
|
|
+ // success: function (saveRes) {
|
|
|
+ // Taro.showToast({ title: '导出成功', icon: 'success' })
|
|
|
+ // }
|
|
|
+ // })
|
|
|
+ // }
|
|
|
+ // })
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('导出打卡数据失败:', error)
|
|
|
+ Taro.showToast({
|
|
|
+ title: '导出失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 播放视频
|
|
|
+ const handlePlayVideo = (videoId: number, videoName: string) => {
|
|
|
+ console.log('播放视频:', videoId, videoName)
|
|
|
+ // TODO: 实现视频播放逻辑
|
|
|
+ // 在小程序中,可能需要使用Taro.openVideo或Taro.previewMedia
|
|
|
+ // 首先需要获取视频URL
|
|
|
+ Taro.showToast({
|
|
|
+ title: `播放视频: ${videoName}`,
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 下载视频
|
|
|
+ const handleDownloadVideo = async (videoId: number, videoName: string) => {
|
|
|
+ try {
|
|
|
+ console.log('下载视频:', videoId, videoName)
|
|
|
+ Taro.showToast({
|
|
|
+ title: `开始下载: ${videoName}`,
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+
|
|
|
+ // TODO: 实现视频下载逻辑
|
|
|
+ // 1. 获取视频URL
|
|
|
+ // 2. 使用Taro.downloadFile下载
|
|
|
+ // 3. 使用Taro.saveFile保存到本地
|
|
|
+
|
|
|
+ // 模拟下载延迟
|
|
|
+ setTimeout(() => {
|
|
|
+ Taro.showToast({
|
|
|
+ title: `已下载: ${videoName}`,
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+ }, 1000)
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('下载视频失败:', error)
|
|
|
+ Taro.showToast({
|
|
|
+ title: '下载失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 分享视频
|
|
|
+ const handleShareVideo = (videoId: number, videoName: string) => {
|
|
|
+ console.log('分享视频:', videoId, videoName)
|
|
|
+ // 小程序分享功能
|
|
|
+ Taro.showShareMenu({
|
|
|
+ withShareTicket: true
|
|
|
+ })
|
|
|
+ Taro.showToast({
|
|
|
+ title: `分享视频: ${videoName}`,
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量下载视频
|
|
|
+ const handleBatchDownload = async () => {
|
|
|
+ try {
|
|
|
+ if (!videos || videos.length === 0) {
|
|
|
+ Taro.showToast({
|
|
|
+ title: '没有可下载的视频',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ Taro.showToast({
|
|
|
+ title: `开始批量下载${videos.length}个视频`,
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+
|
|
|
+ // 在实际项目中,这里需要调用批量下载API
|
|
|
+ // const response = await enterpriseOrderClient['batch-download'].$post({
|
|
|
+ // body: {
|
|
|
+ // downloadScope: 'company',
|
|
|
+ // companyId: getEnterpriseUserInfo()?.companyId || 0,
|
|
|
+ // assetTypes: ['checkin_video', 'salary_video', 'tax_video']
|
|
|
+ // }
|
|
|
+ // })
|
|
|
+
|
|
|
+ // 模拟批量下载过程
|
|
|
+ let downloadedCount = 0
|
|
|
+ for (const video of videos) {
|
|
|
+ console.log(`下载视频: ${video.name}`)
|
|
|
+ downloadedCount++
|
|
|
+ // 模拟每个视频下载延迟
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 500))
|
|
|
+ }
|
|
|
+
|
|
|
+ Taro.showToast({
|
|
|
+ title: `批量下载完成: ${downloadedCount}/${videos.length}`,
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('批量下载视频失败:', error)
|
|
|
+ Taro.showToast({
|
|
|
+ title: '批量下载失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
@@ -489,7 +698,7 @@ const OrderDetail: React.FC = () => {
|
|
|
<Text className="text-xs text-gray-500">{statistics?.taxVideoStats.percentage || 0}%</Text>
|
|
|
</View>
|
|
|
</View>
|
|
|
- <View className="flex items-center text-blue-500 text-sm" onClick={() => console.log('查看详细打卡记录')}>
|
|
|
+ <View className="flex items-center text-blue-500 text-sm" onClick={handleViewCheckinDetails}>
|
|
|
<Text>查看详细打卡记录</Text>
|
|
|
</View>
|
|
|
</View>
|
|
|
@@ -525,7 +734,7 @@ const OrderDetail: React.FC = () => {
|
|
|
<View className="card bg-white p-4 mb-4">
|
|
|
<View className="flex justify-between items-center mb-3">
|
|
|
<Text className="font-semibold text-gray-700">视频资料</Text>
|
|
|
- <View className="flex items-center text-blue-500 text-xs" onClick={() => console.log('批量下载')}>
|
|
|
+ <View className="flex items-center text-blue-500 text-xs" onClick={handleBatchDownload}>
|
|
|
<Text>批量下载</Text>
|
|
|
</View>
|
|
|
</View>
|
|
|
@@ -540,12 +749,15 @@ const OrderDetail: React.FC = () => {
|
|
|
</Text>
|
|
|
</View>
|
|
|
<View className="flex space-x-2">
|
|
|
- <View className="flex items-center text-blue-500 text-xs" onClick={() => console.log('播放视频', video.id)}>
|
|
|
+ <View className="flex items-center text-blue-500 text-xs" onClick={() => handlePlayVideo(video.id, video.name)}>
|
|
|
<Text>播放</Text>
|
|
|
</View>
|
|
|
- <View className="flex items-center text-gray-500 text-xs" onClick={() => console.log('下载视频', video.id)}>
|
|
|
+ <View className="flex items-center text-gray-500 text-xs" onClick={() => handleDownloadVideo(video.id, video.name)}>
|
|
|
<Text>下载</Text>
|
|
|
</View>
|
|
|
+ <View className="flex items-center text-green-500 text-xs" onClick={() => handleShareVideo(video.id, video.name)}>
|
|
|
+ <Text>分享</Text>
|
|
|
+ </View>
|
|
|
</View>
|
|
|
</View>
|
|
|
))}
|
|
|
@@ -573,6 +785,113 @@ const OrderDetail: React.FC = () => {
|
|
|
</View>
|
|
|
</>)}
|
|
|
</ScrollView>
|
|
|
+ {/* 打卡日历模态框 */}
|
|
|
+ {showCheckinCalendar && (
|
|
|
+ <View className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
|
+ <View className="bg-white rounded-lg w-11/12 max-w-md">
|
|
|
+ {/* 模态框头部 */}
|
|
|
+ <View className="flex justify-between items-center p-4 border-b border-gray-200">
|
|
|
+ <Text className="font-semibold text-gray-700">打卡日历</Text>
|
|
|
+ <View
|
|
|
+ className="text-gray-500 text-lg"
|
|
|
+ onClick={() => setShowCheckinCalendar(false)}
|
|
|
+ >
|
|
|
+ <Text>×</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ {/* 模态框内容 */}
|
|
|
+ <View className="p-4">
|
|
|
+ {/* 时间范围筛选 */}
|
|
|
+ <View className="flex space-x-2 mb-4">
|
|
|
+ <View className="flex-1">
|
|
|
+ <Text className="text-xs text-gray-500 mb-1">开始日期</Text>
|
|
|
+ <Input
|
|
|
+ className="border border-gray-300 rounded px-2 py-1 text-sm"
|
|
|
+ placeholder="选择开始日期"
|
|
|
+ type="date"
|
|
|
+ value={filterStartDate}
|
|
|
+ onInput={(e) => setFilterStartDate(e.detail.value)}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+ <View className="flex-1">
|
|
|
+ <Text className="text-xs text-gray-500 mb-1">结束日期</Text>
|
|
|
+ <Input
|
|
|
+ className="border border-gray-300 rounded px-2 py-1 text-sm"
|
|
|
+ placeholder="选择结束日期"
|
|
|
+ type="date"
|
|
|
+ value={filterEndDate}
|
|
|
+ onInput={(e) => setFilterEndDate(e.detail.value)}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {/* 简单的日历视图 */}
|
|
|
+ <View className="mb-4">
|
|
|
+ <Text className="font-medium text-gray-700 mb-2">2025年12月</Text>
|
|
|
+ <View className="grid grid-cols-7 gap-1 text-center">
|
|
|
+ {/* 星期标题 */}
|
|
|
+ {['日', '一', '二', '三', '四', '五', '六'].map((day) => (
|
|
|
+ <Text key={day} className="text-xs text-gray-500 py-1">{day}</Text>
|
|
|
+ ))}
|
|
|
+ {/* 日期单元格 */}
|
|
|
+ {Array.from({ length: 31 }, (_, i) => i + 1).map((day) => {
|
|
|
+ const checkinData = getCheckinCalendarData()
|
|
|
+ const isCheckedIn = checkinData.checkinDays.includes(day)
|
|
|
+ return (
|
|
|
+ <View
|
|
|
+ key={day}
|
|
|
+ className={`p-2 rounded ${isCheckedIn ? 'bg-green-100' : 'bg-gray-100'}`}
|
|
|
+ >
|
|
|
+ <Text className={`text-sm ${isCheckedIn ? 'text-green-800' : 'text-gray-500'}`}>
|
|
|
+ {day}
|
|
|
+ </Text>
|
|
|
+ {isCheckedIn && (
|
|
|
+ <Text className="text-xs text-green-600">已打卡</Text>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+ })}
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {/* 打卡统计 */}
|
|
|
+ <View className="bg-blue-50 rounded-lg p-3 mb-4">
|
|
|
+ <Text className="font-medium text-blue-700 mb-1">本月打卡统计</Text>
|
|
|
+ <View className="flex justify-between">
|
|
|
+ <View>
|
|
|
+ <Text className="text-xs text-blue-600">已打卡</Text>
|
|
|
+ <Text className="text-lg font-bold text-blue-800">{getCheckinCalendarData().checkinCount}天</Text>
|
|
|
+ </View>
|
|
|
+ <View>
|
|
|
+ <Text className="text-xs text-blue-600">未打卡</Text>
|
|
|
+ <Text className="text-lg font-bold text-blue-800">{getCheckinCalendarData().missedCount}天</Text>
|
|
|
+ </View>
|
|
|
+ <View>
|
|
|
+ <Text className="text-xs text-blue-600">出勤率</Text>
|
|
|
+ <Text className="text-lg font-bold text-blue-800">{getCheckinCalendarData().attendanceRate}%</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {/* 操作按钮 */}
|
|
|
+ <View className="flex space-x-2">
|
|
|
+ <View
|
|
|
+ className="flex-1 flex items-center justify-center bg-blue-500 text-white text-sm px-4 py-2 rounded-lg"
|
|
|
+ onClick={handleExportCheckinData}
|
|
|
+ >
|
|
|
+ <Text>导出打卡数据</Text>
|
|
|
+ </View>
|
|
|
+ <View
|
|
|
+ className="flex-1 flex items-center justify-center border border-gray-300 text-gray-700 text-sm px-4 py-2 rounded-lg"
|
|
|
+ onClick={() => setShowCheckinCalendar(false)}
|
|
|
+ >
|
|
|
+ <Text>关闭</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
</>
|
|
|
)
|
|
|
}
|