选课调整
This commit is contained in:
parent
f51548a299
commit
bf543d3929
@ -158,3 +158,9 @@ const setDefaultValue = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -16,17 +16,17 @@
|
||||
<view class="selector-popup">
|
||||
<!-- 头部 -->
|
||||
<view class="popup-header">
|
||||
<text class="cancel-btn" @click="closeSelector">取消</text>
|
||||
<text class="popup-title">{{ title }}</text>
|
||||
<view class="popup-actions">
|
||||
<text class="action-btn cancel-btn" @click="closeSelector">取消</text>
|
||||
<text class="action-btn confirm-btn" @click="confirmSelection">确定</text>
|
||||
</view>
|
||||
<text class="confirm-btn" @click="confirmSelection">确定</text>
|
||||
</view>
|
||||
|
||||
<!-- 树形选择器内容 -->
|
||||
<view class="popup-content">
|
||||
<scroll-view scroll-y class="tree-scroll" :style="{ height: 'calc(80vh - 80px)' }">
|
||||
<checkbox-group v-if="multiple" @change="onGradeCheckboxChange">
|
||||
<scroll-view scroll-y class="tree-scroll">
|
||||
<view v-if="isLoading" class="loading-indicator">加载中...</view>
|
||||
|
||||
<checkbox-group v-else-if="multiple" @change="onGradeCheckboxChange">
|
||||
<view class="tree-container">
|
||||
<view
|
||||
v-for="(nj, njIndex) in njList"
|
||||
@ -49,17 +49,10 @@
|
||||
</view>
|
||||
<text class="grade-text">{{ nj.title }}</text>
|
||||
<checkbox
|
||||
v-if="multiple"
|
||||
:value="nj.key"
|
||||
:checked="isGradeSelected(nj)"
|
||||
class="grade-checkbox"
|
||||
/>
|
||||
<radio
|
||||
v-else
|
||||
:value="nj.key"
|
||||
:checked="isGradeSelected(nj)"
|
||||
class="grade-radio"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -68,7 +61,7 @@
|
||||
v-if="nj.expanded"
|
||||
class="class-children"
|
||||
>
|
||||
<checkbox-group v-if="multiple" @change="(event: any) => onClassCheckboxChange(nj, event)">
|
||||
<checkbox-group @change="(event: any) => onClassCheckboxChange(nj, event)">
|
||||
<view
|
||||
v-for="(bj, bjIndex) in nj.children"
|
||||
:key="bj.key"
|
||||
@ -146,6 +139,8 @@
|
||||
</view>
|
||||
</view>
|
||||
</radio-group>
|
||||
|
||||
<view v-if="!isLoading && njList.length === 0" class="empty-state">暂无数据</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
@ -258,6 +253,14 @@ const displayText = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
// 获取选中数量
|
||||
const getSelectedCount = () => {
|
||||
if (!props.modelValue) return 0
|
||||
const gradeCount = props.modelValue.selectedGrades?.length || 0
|
||||
const classCount = props.modelValue.selectedClasses?.length || 0
|
||||
return gradeCount + classCount
|
||||
}
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal) {
|
||||
@ -522,36 +525,31 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px 20px;
|
||||
padding: 10px 15px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.cancel-btn {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
.action-btn {
|
||||
font-size: 14px;
|
||||
padding: 4px 8px;
|
||||
|
||||
&.cancel-btn {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&.confirm-btn {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
font-size: 14px;
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -564,7 +562,8 @@ onMounted(() => {
|
||||
.tree-scroll {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
|
||||
.tree-container {
|
||||
padding: 0;
|
||||
@ -572,6 +571,10 @@ onMounted(() => {
|
||||
|
||||
.tree-node {
|
||||
.tree-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
@ -587,13 +590,13 @@ onMounted(() => {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
.tree-item-content {
|
||||
padding: 5px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
flex-grow: 1;
|
||||
margin-right: 15px;
|
||||
|
||||
.expand-icon {
|
||||
margin-right: 5px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -603,13 +606,13 @@ onMounted(() => {
|
||||
|
||||
.grade-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.grade-checkbox {
|
||||
margin-left: 5px;
|
||||
.grade-checkbox,
|
||||
.grade-radio {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -618,26 +621,27 @@ onMounted(() => {
|
||||
background-color: #fff;
|
||||
|
||||
.tree-item-content {
|
||||
padding: 3px 12px 3px 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
flex-grow: 1;
|
||||
margin-right: 15px;
|
||||
|
||||
.class-indent {
|
||||
width: 10px;
|
||||
height: 1px;
|
||||
background-color: #e0e0e0;
|
||||
margin-right: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.class-text {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.class-checkbox {
|
||||
margin-left: 5px;
|
||||
.class-checkbox,
|
||||
.class-radio {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -659,8 +663,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.tree-item-content {
|
||||
padding: 2px 12px 2px 26px;
|
||||
min-height: 24px;
|
||||
padding: 8px 15px 8px 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -668,6 +671,15 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 30px 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
845
src/components/ImageVideoUpload/ImageVideoUpload.vue
Normal file
845
src/components/ImageVideoUpload/ImageVideoUpload.vue
Normal file
@ -0,0 +1,845 @@
|
||||
<template>
|
||||
<view class="image-video-upload">
|
||||
<!-- 图片上传区域 -->
|
||||
<view class="upload-section" v-if="enableImage">
|
||||
<view class="section-title">
|
||||
<text class="title-text">图片</text>
|
||||
<text class="count-text">({{ imageList.length }}/{{ maxImageCount }})</text>
|
||||
</view>
|
||||
|
||||
<view class="image-list">
|
||||
<view
|
||||
class="image-item"
|
||||
v-for="(image, index) in imageList"
|
||||
:key="index"
|
||||
>
|
||||
<image
|
||||
:src="image.url || image.tempPath"
|
||||
class="image-preview"
|
||||
@click="previewImage(index)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="image-actions">
|
||||
<view class="delete-btn" @click="removeImage(index)">
|
||||
<uni-icons type="close" size="16" color="#fff"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="image-status" v-if="image.isCompressed">
|
||||
<text class="compressed-tag">已压缩</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="add-btn"
|
||||
v-if="imageList.length < maxImageCount"
|
||||
@click="chooseImage"
|
||||
>
|
||||
<uni-icons type="camera" size="24" color="#999"></uni-icons>
|
||||
<text class="add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 视频上传区域 -->
|
||||
<view class="upload-section" v-if="enableVideo">
|
||||
<view class="section-title">
|
||||
<text class="title-text">视频</text>
|
||||
<text class="count-text">({{ videoList.length }}/{{ maxVideoCount }})</text>
|
||||
</view>
|
||||
|
||||
<view class="video-list">
|
||||
<view
|
||||
class="video-item"
|
||||
v-for="(video, index) in videoList"
|
||||
:key="index"
|
||||
>
|
||||
<view class="video-preview" @click="previewVideo(index)">
|
||||
<image
|
||||
:src="video.thumbnail || '/static/icons/video-placeholder.png'"
|
||||
class="video-thumbnail"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="play-icon">
|
||||
<uni-icons type="play-filled" size="24" color="#fff"></uni-icons>
|
||||
</view>
|
||||
<view class="video-duration" v-if="video.duration">
|
||||
{{ formatDuration(video.duration) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="video-actions">
|
||||
<view class="delete-btn" @click="removeVideo(index)">
|
||||
<uni-icons type="close" size="16" color="#fff"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="video-info">
|
||||
<text class="video-name">{{ video.name }}</text>
|
||||
<text class="video-size" v-if="video.size">{{ formatFileSize(video.size) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="add-btn"
|
||||
v-if="videoList.length < maxVideoCount"
|
||||
@click="chooseVideo"
|
||||
>
|
||||
<uni-icons type="videocam" size="24" color="#999"></uni-icons>
|
||||
<text class="add-text">添加视频</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
// 接口定义
|
||||
interface ImageItem {
|
||||
tempPath?: string
|
||||
url?: string
|
||||
name?: string
|
||||
originalPath?: string
|
||||
isCompressed?: boolean
|
||||
}
|
||||
|
||||
interface VideoItem {
|
||||
tempPath?: string
|
||||
url?: string
|
||||
name?: string
|
||||
duration?: number
|
||||
size?: number
|
||||
thumbnail?: string
|
||||
}
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
// 图片相关
|
||||
enableImage?: boolean
|
||||
maxImageCount?: number
|
||||
imageList?: ImageItem[]
|
||||
|
||||
// 视频相关
|
||||
enableVideo?: boolean
|
||||
maxVideoCount?: number
|
||||
videoList?: VideoItem[]
|
||||
|
||||
// 压缩配置
|
||||
compressConfig?: {
|
||||
image: {
|
||||
quality: number
|
||||
maxWidth: number
|
||||
maxHeight: number
|
||||
maxSize: number
|
||||
minQuality: number
|
||||
}
|
||||
video: {
|
||||
maxDuration: number
|
||||
maxSize: number
|
||||
quality: string
|
||||
}
|
||||
}
|
||||
|
||||
// 上传配置
|
||||
autoUpload?: boolean
|
||||
uploadApi?: (file: any) => Promise<any>
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
enableImage: true,
|
||||
maxImageCount: 5,
|
||||
imageList: () => [],
|
||||
|
||||
enableVideo: true,
|
||||
maxVideoCount: 3,
|
||||
videoList: () => [],
|
||||
|
||||
compressConfig: () => ({
|
||||
image: {
|
||||
quality: 60,
|
||||
maxWidth: 1280,
|
||||
maxHeight: 720,
|
||||
maxSize: 200 * 1024, // 200KB
|
||||
minQuality: 20
|
||||
},
|
||||
video: {
|
||||
maxDuration: 60,
|
||||
maxSize: 10 * 1024 * 1024, // 10MB
|
||||
quality: 'medium'
|
||||
}
|
||||
}),
|
||||
|
||||
autoUpload: true,
|
||||
uploadApi: undefined
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:imageList': [images: ImageItem[]]
|
||||
'update:videoList': [videos: VideoItem[]]
|
||||
'image-upload-success': [image: ImageItem, index: number]
|
||||
'image-upload-error': [error: any, index: number]
|
||||
'video-upload-success': [video: VideoItem, index: number]
|
||||
'video-upload-error': [error: any, index: number]
|
||||
'upload-progress': [type: 'image' | 'video', current: number, total: number]
|
||||
}>()
|
||||
|
||||
// 响应式数据
|
||||
const imageList = ref<ImageItem[]>([...props.imageList])
|
||||
const videoList = ref<VideoItem[]>([...props.videoList])
|
||||
|
||||
// 监听props变化
|
||||
watch(() => props.imageList, (newList) => {
|
||||
imageList.value = [...newList]
|
||||
}, { deep: true })
|
||||
|
||||
watch(() => props.videoList, (newList) => {
|
||||
videoList.value = [...newList]
|
||||
}, { deep: true })
|
||||
|
||||
// 压缩配置
|
||||
const COMPRESS_CONFIG = computed(() => props.compressConfig)
|
||||
|
||||
// 获取文件信息
|
||||
const getFileInfo = (filePath: string): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getFileInfo({
|
||||
filePath: filePath,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 图片压缩函数 - 支持H5和APP
|
||||
const compressImage = (src: string, quality: number = COMPRESS_CONFIG.value.image.quality): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 检查是否在H5环境
|
||||
// @ts-ignore
|
||||
if (typeof window !== 'undefined' && window.uni && window.uni.compressImage) {
|
||||
// APP环境使用uni.compressImage
|
||||
uni.compressImage({
|
||||
src: src,
|
||||
quality: quality,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// H5环境使用Canvas压缩
|
||||
compressImageWithCanvas(src, quality).then(resolve).catch(reject)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// H5环境下的Canvas压缩函数
|
||||
const compressImageWithCanvas = (src: string, quality: number): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 计算压缩后的尺寸
|
||||
let { width, height } = calculateCompressedSize(img.width, img.height)
|
||||
|
||||
// 创建Canvas
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
if (!ctx) {
|
||||
reject(new Error('无法获取Canvas上下文'))
|
||||
return
|
||||
}
|
||||
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
// 绘制压缩后的图片
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
|
||||
// 转换为Blob
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
// 创建临时URL
|
||||
const compressedUrl = URL.createObjectURL(blob)
|
||||
resolve(compressedUrl)
|
||||
} else {
|
||||
reject(new Error('Canvas压缩失败'))
|
||||
}
|
||||
}, 'image/jpeg', quality / 100)
|
||||
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('图片加载失败'))
|
||||
}
|
||||
|
||||
img.src = src
|
||||
})
|
||||
}
|
||||
|
||||
// 计算压缩后的尺寸
|
||||
const calculateCompressedSize = (originalWidth: number, originalHeight: number): { width: number, height: number } => {
|
||||
const maxWidth = COMPRESS_CONFIG.value.image.maxWidth
|
||||
const maxHeight = COMPRESS_CONFIG.value.image.maxHeight
|
||||
|
||||
let width = originalWidth
|
||||
let height = originalHeight
|
||||
|
||||
// 按比例缩放
|
||||
if (width > maxWidth || height > maxHeight) {
|
||||
const ratio = Math.min(maxWidth / width, maxHeight / height)
|
||||
width = Math.floor(width * ratio)
|
||||
height = Math.floor(height * ratio)
|
||||
}
|
||||
|
||||
return { width, height }
|
||||
}
|
||||
|
||||
// 智能图片压缩
|
||||
const smartCompressImage = async (filePath: string): Promise<string> => {
|
||||
try {
|
||||
const fileInfo = await getFileInfo(filePath)
|
||||
const originalSize = fileInfo.size
|
||||
|
||||
// 如果文件已经很小,直接返回
|
||||
if (originalSize <= COMPRESS_CONFIG.value.image.maxSize) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
// 根据文件大小计算压缩质量
|
||||
let quality = COMPRESS_CONFIG.value.image.quality
|
||||
if (originalSize > COMPRESS_CONFIG.value.image.maxSize * 10) {
|
||||
quality = 15
|
||||
} else if (originalSize > COMPRESS_CONFIG.value.image.maxSize * 5) {
|
||||
quality = 25
|
||||
} else if (originalSize > COMPRESS_CONFIG.value.image.maxSize * 3) {
|
||||
quality = 35
|
||||
} else if (originalSize > COMPRESS_CONFIG.value.image.maxSize * 2) {
|
||||
quality = 45
|
||||
} else if (originalSize > COMPRESS_CONFIG.value.image.maxSize) {
|
||||
quality = 55
|
||||
}
|
||||
|
||||
// 执行压缩
|
||||
const compressedPath = await compressImage(filePath, quality)
|
||||
|
||||
// 检查压缩后的文件大小
|
||||
const compressedInfo = await getFileInfo(compressedPath)
|
||||
|
||||
// 如果还是太大,进一步压缩
|
||||
if (compressedInfo.size > COMPRESS_CONFIG.value.image.maxSize && quality > COMPRESS_CONFIG.value.image.minQuality) {
|
||||
const newQuality = Math.max(quality - 10, COMPRESS_CONFIG.value.image.minQuality)
|
||||
return await smartCompressImage(compressedPath)
|
||||
}
|
||||
|
||||
return compressedPath
|
||||
} catch (error) {
|
||||
console.error('智能压缩失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 检查视频文件大小
|
||||
const checkVideoSize = async (filePath: string): Promise<boolean> => {
|
||||
try {
|
||||
const fileInfo = await getFileInfo(filePath)
|
||||
return fileInfo.size <= COMPRESS_CONFIG.value.video.maxSize
|
||||
} catch (error) {
|
||||
console.error('获取视频文件信息失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择图片
|
||||
const chooseImage = () => {
|
||||
uni.chooseImage({
|
||||
count: props.maxImageCount - imageList.value.length,
|
||||
sizeType: ['original'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempFilePaths = res.tempFilePaths as string[]
|
||||
|
||||
showLoading('压缩图片中...')
|
||||
|
||||
try {
|
||||
const compressedImages = []
|
||||
|
||||
// 并行压缩所有图片
|
||||
const compressPromises = tempFilePaths.map(async (path, index) => {
|
||||
try {
|
||||
showLoading(`压缩图片中... (${index + 1}/${tempFilePaths.length})`)
|
||||
|
||||
const compressedPath = await smartCompressImage(path)
|
||||
|
||||
return {
|
||||
tempPath: compressedPath,
|
||||
name: path.split('/').pop() || 'image.jpg',
|
||||
originalPath: path,
|
||||
isCompressed: true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`图片 ${path} 压缩失败:`, error)
|
||||
return {
|
||||
tempPath: path,
|
||||
name: path.split('/').pop() || 'image.jpg',
|
||||
isCompressed: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all(compressPromises)
|
||||
compressedImages.push(...results)
|
||||
|
||||
// 添加到列表
|
||||
imageList.value = [...imageList.value, ...compressedImages]
|
||||
emit('update:imageList', imageList.value)
|
||||
|
||||
hideLoading()
|
||||
|
||||
// 显示压缩结果统计
|
||||
const compressedCount = results.filter(r => r.isCompressed).length
|
||||
if (compressedCount > 0) {
|
||||
showToast({
|
||||
title: `已压缩 ${compressedCount} 张图片`,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
// 自动上传
|
||||
if (props.autoUpload && props.uploadApi) {
|
||||
await uploadImages(compressedImages)
|
||||
}
|
||||
} catch (error) {
|
||||
hideLoading()
|
||||
showToast({ title: '图片处理失败', icon: 'none' })
|
||||
console.error('图片处理失败:', error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('选择图片失败:', error)
|
||||
showToast({ title: '选择图片失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 选择视频
|
||||
const chooseVideo = () => {
|
||||
uni.chooseVideo({
|
||||
sourceType: ['album', 'camera'],
|
||||
maxDuration: COMPRESS_CONFIG.value.video.maxDuration,
|
||||
camera: 'back',
|
||||
success: async (res) => {
|
||||
const tempFilePath = res.tempFilePath
|
||||
|
||||
try {
|
||||
// 检查视频文件大小
|
||||
const isValidSize = await checkVideoSize(tempFilePath)
|
||||
|
||||
if (!isValidSize) {
|
||||
const fileInfo = await getFileInfo(tempFilePath)
|
||||
const fileSizeMB = (fileInfo.size / 1024 / 1024).toFixed(1)
|
||||
|
||||
uni.showModal({
|
||||
title: '视频文件过大',
|
||||
content: `视频文件大小为 ${fileSizeMB}MB,超过限制的 ${(COMPRESS_CONFIG.value.video.maxSize / 1024 / 1024).toFixed(1)}MB,请选择较小的视频文件。`,
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 文件大小合适,添加到列表
|
||||
const newVideo = {
|
||||
tempPath: tempFilePath,
|
||||
name: tempFilePath.split('/').pop() || 'video.mp4',
|
||||
duration: res.duration,
|
||||
size: res.size
|
||||
}
|
||||
|
||||
videoList.value = [...videoList.value, newVideo]
|
||||
emit('update:videoList', videoList.value)
|
||||
|
||||
// 自动上传
|
||||
if (props.autoUpload && props.uploadApi) {
|
||||
await uploadVideos([newVideo])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('视频处理失败:', error)
|
||||
showToast({ title: '视频处理失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('选择视频失败:', error)
|
||||
showToast({ title: '选择视频失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
const uploadImages = async (images: ImageItem[]) => {
|
||||
if (!props.uploadApi) return
|
||||
|
||||
try {
|
||||
showLoading('上传图片中...')
|
||||
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const image = images[i]
|
||||
if (image.tempPath) {
|
||||
try {
|
||||
showLoading(`上传图片中... (${i + 1}/${images.length})`)
|
||||
emit('upload-progress', 'image', i + 1, images.length)
|
||||
|
||||
const uploadResult = await props.uploadApi(image.tempPath)
|
||||
|
||||
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
|
||||
const serverPath = uploadResult.result[0].filePath
|
||||
|
||||
// 更新图片对象
|
||||
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath)
|
||||
if (index !== -1) {
|
||||
imageList.value[index].url = serverPath
|
||||
delete imageList.value[index].tempPath
|
||||
delete imageList.value[index].originalPath
|
||||
}
|
||||
|
||||
emit('image-upload-success', imageList.value[index], index)
|
||||
successCount++
|
||||
} else {
|
||||
throw new Error('上传响应格式异常')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('图片上传失败:', error)
|
||||
emit('image-upload-error', error, i)
|
||||
|
||||
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath)
|
||||
if (index !== -1) {
|
||||
imageList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit('update:imageList', imageList.value)
|
||||
hideLoading()
|
||||
|
||||
if (failCount === 0) {
|
||||
showToast({ title: `成功上传 ${successCount} 张图片`, icon: 'success' })
|
||||
} else {
|
||||
showToast({
|
||||
title: `上传完成:成功 ${successCount} 张,失败 ${failCount} 张`,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
hideLoading()
|
||||
console.error('批量上传图片失败:', error)
|
||||
showToast({ title: '图片上传失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 上传视频
|
||||
const uploadVideos = async (videos: VideoItem[]) => {
|
||||
if (!props.uploadApi) return
|
||||
|
||||
try {
|
||||
showLoading('上传视频中...')
|
||||
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
const video = videos[i]
|
||||
if (video.tempPath) {
|
||||
try {
|
||||
showLoading(`上传视频中... (${i + 1}/${videos.length})`)
|
||||
emit('upload-progress', 'video', i + 1, videos.length)
|
||||
|
||||
const uploadResult = await props.uploadApi(video.tempPath)
|
||||
|
||||
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
|
||||
const serverPath = uploadResult.result[0].filePath
|
||||
|
||||
// 更新视频对象
|
||||
const index = videoList.value.findIndex(v => v.tempPath === video.tempPath)
|
||||
if (index !== -1) {
|
||||
videoList.value[index].url = serverPath
|
||||
delete videoList.value[index].tempPath
|
||||
}
|
||||
|
||||
emit('video-upload-success', videoList.value[index], index)
|
||||
successCount++
|
||||
} else {
|
||||
throw new Error('上传响应格式异常')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('视频上传失败:', error)
|
||||
emit('video-upload-error', error, i)
|
||||
|
||||
const index = videoList.value.findIndex(v => v.tempPath === video.tempPath)
|
||||
if (index !== -1) {
|
||||
videoList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit('update:videoList', videoList.value)
|
||||
hideLoading()
|
||||
|
||||
if (failCount === 0) {
|
||||
showToast({ title: `成功上传 ${successCount} 个视频`, icon: 'success' })
|
||||
} else {
|
||||
showToast({
|
||||
title: `上传完成:成功 ${successCount} 个,失败 ${failCount} 个`,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
hideLoading()
|
||||
console.error('批量上传视频失败:', error)
|
||||
showToast({ title: '视频上传失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (index: number) => {
|
||||
const urls = imageList.value.map(img => img.url || img.tempPath).filter(Boolean)
|
||||
uni.previewImage({
|
||||
urls: urls,
|
||||
current: index
|
||||
})
|
||||
}
|
||||
|
||||
// 预览视频
|
||||
const previewVideo = (index: number) => {
|
||||
const video = videoList.value[index]
|
||||
if (video.url || video.tempPath) {
|
||||
uni.previewVideo({
|
||||
sources: [{
|
||||
src: video.url || video.tempPath,
|
||||
type: 'mp4'
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
const removeImage = (index: number) => {
|
||||
imageList.value.splice(index, 1)
|
||||
emit('update:imageList', imageList.value)
|
||||
}
|
||||
|
||||
// 删除视频
|
||||
const removeVideo = (index: number) => {
|
||||
videoList.value.splice(index, 1)
|
||||
emit('update:videoList', videoList.value)
|
||||
}
|
||||
|
||||
// 格式化时长
|
||||
const formatDuration = (seconds: number): string => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = Math.floor(seconds % 60)
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + 'B'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'
|
||||
return (bytes / 1024 / 1024).toFixed(1) + 'MB'
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const showLoading = (title: string) => {
|
||||
uni.showLoading({ title })
|
||||
}
|
||||
|
||||
const hideLoading = () => {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
const showToast = (options: { title: string; icon?: string; duration?: number }) => {
|
||||
uni.showToast(options)
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
chooseImage,
|
||||
chooseVideo,
|
||||
uploadImages,
|
||||
uploadVideos,
|
||||
removeImage,
|
||||
removeVideo
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-video-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.count-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.image-list, .video-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.image-item, .video-item {
|
||||
position: relative;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.video-duration {
|
||||
position: absolute;
|
||||
bottom: 4rpx;
|
||||
right: 4rpx;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 6rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.image-actions, .video-actions {
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
right: 4rpx;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-status {
|
||||
position: absolute;
|
||||
bottom: 4rpx;
|
||||
left: 4rpx;
|
||||
}
|
||||
|
||||
.compressed-tag {
|
||||
background: #4CAF50;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 6rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.video-name, .video-size {
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.video-size {
|
||||
margin-top: 2rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border: 2rpx dashed #ddd;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
</style>
|
||||
223
src/components/ImageVideoUpload/README.md
Normal file
223
src/components/ImageVideoUpload/README.md
Normal file
@ -0,0 +1,223 @@
|
||||
# ImageVideoUpload 图片视频上传组件
|
||||
|
||||
一个功能完整的图片和视频上传组件,支持压缩、预览、删除等功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 图片和视频上传
|
||||
- ✅ 智能图片压缩(支持H5和APP环境)
|
||||
- ✅ 文件大小和时长限制
|
||||
- ✅ 预览功能
|
||||
- ✅ 删除功能
|
||||
- ✅ 上传进度显示
|
||||
- ✅ 自定义压缩配置
|
||||
- ✅ 自动上传或手动上传
|
||||
- ✅ 完整的事件回调
|
||||
|
||||
## 基本使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="container">
|
||||
<ImageVideoUpload
|
||||
v-model:image-list="imageList"
|
||||
v-model:video-list="videoList"
|
||||
:upload-api="uploadApi"
|
||||
:compress-config="compressConfig"
|
||||
@image-upload-success="onImageUploadSuccess"
|
||||
@video-upload-success="onVideoUploadSuccess"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ImageVideoUpload, type ImageItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
|
||||
import { attachmentUpload } from '@/api/system/upload'
|
||||
|
||||
// 数据
|
||||
const imageList = ref<ImageItem[]>([])
|
||||
const videoList = ref<VideoItem[]>([])
|
||||
|
||||
// 压缩配置
|
||||
const compressConfig = ref(COMPRESS_PRESETS.medium)
|
||||
|
||||
// 上传API
|
||||
const uploadApi = attachmentUpload
|
||||
|
||||
// 事件处理
|
||||
const onImageUploadSuccess = (image: ImageItem, index: number) => {
|
||||
console.log('图片上传成功:', image, index)
|
||||
}
|
||||
|
||||
const onVideoUploadSuccess = (video: VideoItem, index: number) => {
|
||||
console.log('视频上传成功:', video, index)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Props 属性
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| enableImage | boolean | true | 是否启用图片上传 |
|
||||
| enableVideo | boolean | true | 是否启用视频上传 |
|
||||
| maxImageCount | number | 5 | 最大图片数量 |
|
||||
| maxVideoCount | number | 3 | 最大视频数量 |
|
||||
| imageList | ImageItem[] | [] | 图片列表 |
|
||||
| videoList | VideoItem[] | [] | 视频列表 |
|
||||
| compressConfig | CompressConfig | 默认配置 | 压缩配置 |
|
||||
| autoUpload | boolean | true | 是否自动上传 |
|
||||
| uploadApi | Function | - | 上传API函数 |
|
||||
|
||||
## Events 事件
|
||||
|
||||
| 事件名 | 参数 | 说明 |
|
||||
|--------|------|------|
|
||||
| update:imageList | images: ImageItem[] | 图片列表更新 |
|
||||
| update:videoList | videos: VideoItem[] | 视频列表更新 |
|
||||
| image-upload-success | image: ImageItem, index: number | 图片上传成功 |
|
||||
| image-upload-error | error: any, index: number | 图片上传失败 |
|
||||
| video-upload-success | video: VideoItem, index: number | 视频上传成功 |
|
||||
| video-upload-error | error: any, index: number | 视频上传失败 |
|
||||
| upload-progress | type: 'image' \| 'video', current: number, total: number | 上传进度 |
|
||||
|
||||
## 压缩配置
|
||||
|
||||
### 使用预设配置
|
||||
|
||||
```javascript
|
||||
import { COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
|
||||
|
||||
// 高质量配置
|
||||
const highQualityConfig = COMPRESS_PRESETS.high
|
||||
|
||||
// 平衡配置(默认)
|
||||
const mediumConfig = COMPRESS_PRESETS.medium
|
||||
|
||||
// 高压缩配置
|
||||
const lowQualityConfig = COMPRESS_PRESETS.low
|
||||
```
|
||||
|
||||
### 自定义配置
|
||||
|
||||
```javascript
|
||||
const customConfig = {
|
||||
image: {
|
||||
quality: 70, // 压缩质量 (1-100)
|
||||
maxWidth: 1600, // 最大宽度
|
||||
maxHeight: 900, // 最大高度
|
||||
maxSize: 300 * 1024, // 最大文件大小 300KB
|
||||
minQuality: 25 // 最低压缩质量
|
||||
},
|
||||
video: {
|
||||
maxDuration: 45, // 最大时长45秒
|
||||
maxSize: 8 * 1024 * 1024, // 最大文件大小 8MB
|
||||
quality: 'medium' // 视频质量
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 手动上传
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<ImageVideoUpload
|
||||
ref="uploadRef"
|
||||
v-model:image-list="imageList"
|
||||
v-model:video-list="videoList"
|
||||
:auto-upload="false"
|
||||
:upload-api="uploadApi"
|
||||
/>
|
||||
<button @click="handleUpload">手动上传</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const uploadRef = ref()
|
||||
|
||||
const handleUpload = async () => {
|
||||
// 上传所有图片
|
||||
await uploadRef.value.uploadImages(imageList.value)
|
||||
// 上传所有视频
|
||||
await uploadRef.value.uploadVideos(videoList.value)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 监听上传进度
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<ImageVideoUpload
|
||||
v-model:image-list="imageList"
|
||||
v-model:video-list="videoList"
|
||||
:upload-api="uploadApi"
|
||||
@upload-progress="onUploadProgress"
|
||||
/>
|
||||
<view v-if="uploading" class="progress">
|
||||
上传进度: {{ currentProgress }}/{{ totalProgress }}
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const uploading = ref(false)
|
||||
const currentProgress = ref(0)
|
||||
const totalProgress = ref(0)
|
||||
|
||||
const onUploadProgress = (type: 'image' | 'video', current: number, total: number) => {
|
||||
uploading.value = true
|
||||
currentProgress.value = current
|
||||
totalProgress.value = total
|
||||
|
||||
if (current === total) {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 样式自定义
|
||||
|
||||
组件使用 scoped 样式,如需自定义样式,可以通过以下方式:
|
||||
|
||||
```vue
|
||||
<style>
|
||||
/* 全局样式覆盖 */
|
||||
.image-video-upload .add-btn {
|
||||
border-color: #007aff;
|
||||
}
|
||||
|
||||
.image-video-upload .add-btn .add-text {
|
||||
color: #007aff;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **H5环境**:使用Canvas进行图片压缩
|
||||
2. **APP环境**:使用uni.compressImage进行压缩
|
||||
3. **文件大小**:建议根据实际需求调整压缩配置
|
||||
4. **上传API**:需要返回标准的响应格式
|
||||
5. **内存管理**:上传成功后会自动清理临时文件
|
||||
|
||||
## 响应格式要求
|
||||
|
||||
上传API需要返回以下格式的响应:
|
||||
|
||||
```javascript
|
||||
{
|
||||
resultCode: 1, // 1表示成功
|
||||
result: [
|
||||
{
|
||||
filePath: "服务器文件路径"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
65
src/components/ImageVideoUpload/example.vue
Normal file
65
src/components/ImageVideoUpload/example.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<view class="example-page">
|
||||
<text class="title">图片视频上传组件示例</text>
|
||||
|
||||
<ImageVideoUpload
|
||||
v-model:image-list="imageList"
|
||||
v-model:video-list="videoList"
|
||||
:max-image-count="5"
|
||||
:max-video-count="3"
|
||||
:compress-config="compressConfig"
|
||||
:upload-api="uploadApi"
|
||||
@image-upload-success="onImageUploadSuccess"
|
||||
@video-upload-success="onVideoUploadSuccess"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ImageVideoUpload, type ImageItem, type VideoItem, COMPRESS_PRESETS } from './index'
|
||||
|
||||
// 数据
|
||||
const imageList = ref<ImageItem[]>([])
|
||||
const videoList = ref<VideoItem[]>([])
|
||||
|
||||
// 压缩配置
|
||||
const compressConfig = ref(COMPRESS_PRESETS.medium)
|
||||
|
||||
// 模拟上传API
|
||||
const uploadApi = async (file: any) => {
|
||||
console.log('上传文件:', file)
|
||||
// 模拟上传延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 模拟成功响应
|
||||
return {
|
||||
resultCode: 1,
|
||||
result: [{
|
||||
filePath: `https://example.com/uploads/${Date.now()}.jpg`
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
const onImageUploadSuccess = (image: ImageItem, index: number) => {
|
||||
console.log('图片上传成功:', image, index)
|
||||
}
|
||||
|
||||
const onVideoUploadSuccess = (video: VideoItem, index: number) => {
|
||||
console.log('视频上传成功:', video, index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.example-page {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
6
src/components/ImageVideoUpload/index.ts
Normal file
6
src/components/ImageVideoUpload/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// 导出组件和类型
|
||||
export { default as ImageVideoUpload } from './ImageVideoUpload.vue'
|
||||
export * from './types'
|
||||
|
||||
// 默认导出
|
||||
export { default } from './ImageVideoUpload.vue'
|
||||
132
src/components/ImageVideoUpload/types.ts
Normal file
132
src/components/ImageVideoUpload/types.ts
Normal file
@ -0,0 +1,132 @@
|
||||
// 图片项接口
|
||||
export interface ImageItem {
|
||||
tempPath?: string // 临时路径(用于预览)
|
||||
url?: string // 服务器路径(上传成功后)
|
||||
name?: string // 文件名
|
||||
originalPath?: string // 原始路径(用于调试)
|
||||
isCompressed?: boolean // 是否已压缩
|
||||
}
|
||||
|
||||
// 视频项接口
|
||||
export interface VideoItem {
|
||||
tempPath?: string // 临时路径(用于预览)
|
||||
url?: string // 服务器路径(上传成功后)
|
||||
name?: string // 文件名
|
||||
duration?: number // 视频时长(秒)
|
||||
size?: number // 文件大小(字节)
|
||||
thumbnail?: string // 缩略图路径
|
||||
}
|
||||
|
||||
// 压缩配置接口
|
||||
export interface CompressConfig {
|
||||
image: {
|
||||
quality: number // 压缩质量 (1-100)
|
||||
maxWidth: number // 最大宽度
|
||||
maxHeight: number // 最大高度
|
||||
maxSize: number // 最大文件大小(字节)
|
||||
minQuality: number // 最低压缩质量
|
||||
}
|
||||
video: {
|
||||
maxDuration: number // 最大时长(秒)
|
||||
maxSize: number // 最大文件大小(字节)
|
||||
quality: string // 视频质量
|
||||
}
|
||||
}
|
||||
|
||||
// 组件Props接口
|
||||
export interface ImageVideoUploadProps {
|
||||
// 图片相关
|
||||
enableImage?: boolean
|
||||
maxImageCount?: number
|
||||
imageList?: ImageItem[]
|
||||
|
||||
// 视频相关
|
||||
enableVideo?: boolean
|
||||
maxVideoCount?: number
|
||||
videoList?: VideoItem[]
|
||||
|
||||
// 压缩配置
|
||||
compressConfig?: CompressConfig
|
||||
|
||||
// 上传配置
|
||||
autoUpload?: boolean
|
||||
uploadApi?: (file: any) => Promise<any>
|
||||
}
|
||||
|
||||
// 组件Emits接口
|
||||
export interface ImageVideoUploadEmits {
|
||||
'update:imageList': [images: ImageItem[]]
|
||||
'update:videoList': [videos: VideoItem[]]
|
||||
'image-upload-success': [image: ImageItem, index: number]
|
||||
'image-upload-error': [error: any, index: number]
|
||||
'video-upload-success': [video: VideoItem, index: number]
|
||||
'video-upload-error': [error: any, index: number]
|
||||
'upload-progress': [type: 'image' | 'video', current: number, total: number]
|
||||
}
|
||||
|
||||
// 默认压缩配置
|
||||
export const DEFAULT_COMPRESS_CONFIG: CompressConfig = {
|
||||
image: {
|
||||
quality: 60,
|
||||
maxWidth: 1280,
|
||||
maxHeight: 720,
|
||||
maxSize: 200 * 1024, // 200KB
|
||||
minQuality: 20
|
||||
},
|
||||
video: {
|
||||
maxDuration: 60,
|
||||
maxSize: 10 * 1024 * 1024, // 10MB
|
||||
quality: 'medium'
|
||||
}
|
||||
}
|
||||
|
||||
// 预设压缩配置
|
||||
export const COMPRESS_PRESETS = {
|
||||
// 高质量配置
|
||||
high: {
|
||||
image: {
|
||||
quality: 80,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080,
|
||||
maxSize: 500 * 1024, // 500KB
|
||||
minQuality: 40
|
||||
},
|
||||
video: {
|
||||
maxDuration: 60,
|
||||
maxSize: 20 * 1024 * 1024, // 20MB
|
||||
quality: 'high'
|
||||
}
|
||||
},
|
||||
|
||||
// 平衡配置
|
||||
medium: {
|
||||
image: {
|
||||
quality: 60,
|
||||
maxWidth: 1280,
|
||||
maxHeight: 720,
|
||||
maxSize: 200 * 1024, // 200KB
|
||||
minQuality: 20
|
||||
},
|
||||
video: {
|
||||
maxDuration: 60,
|
||||
maxSize: 10 * 1024 * 1024, // 10MB
|
||||
quality: 'medium'
|
||||
}
|
||||
},
|
||||
|
||||
// 高压缩配置
|
||||
low: {
|
||||
image: {
|
||||
quality: 40,
|
||||
maxWidth: 960,
|
||||
maxHeight: 540,
|
||||
maxSize: 100 * 1024, // 100KB
|
||||
minQuality: 15
|
||||
},
|
||||
video: {
|
||||
maxDuration: 30,
|
||||
maxSize: 5 * 1024 * 1024, // 5MB
|
||||
quality: 'low'
|
||||
}
|
||||
}
|
||||
}
|
||||
204
src/components/ImageVideoUpload/使用说明.md
Normal file
204
src/components/ImageVideoUpload/使用说明.md
Normal file
@ -0,0 +1,204 @@
|
||||
# ImageVideoUpload 图片视频上传组件
|
||||
|
||||
## 组件位置
|
||||
`D:\code\zhxy-jsd\src\components\ImageVideoUpload\`
|
||||
|
||||
## 文件结构
|
||||
```
|
||||
ImageVideoUpload/
|
||||
├── ImageVideoUpload.vue # 主组件文件
|
||||
├── types.ts # 类型定义
|
||||
├── index.ts # 导出文件
|
||||
├── example.vue # 使用示例
|
||||
├── README.md # 详细文档
|
||||
└── 使用说明.md # 中文说明
|
||||
```
|
||||
|
||||
## 快速使用
|
||||
|
||||
### 1. 基本用法
|
||||
```vue
|
||||
<template>
|
||||
<ImageVideoUpload
|
||||
v-model:image-list="imageList"
|
||||
v-model:video-list="videoList"
|
||||
:upload-api="uploadApi"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ImageVideoUpload } from '@/components/ImageVideoUpload'
|
||||
|
||||
const imageList = ref([])
|
||||
const videoList = ref([])
|
||||
|
||||
const uploadApi = async (file) => {
|
||||
// 你的上传逻辑
|
||||
return { resultCode: 1, result: [{ filePath: '服务器路径' }] }
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 完整配置
|
||||
```vue
|
||||
<template>
|
||||
<ImageVideoUpload
|
||||
v-model:image-list="imageList"
|
||||
v-model:video-list="videoList"
|
||||
:max-image-count="5"
|
||||
:max-video-count="3"
|
||||
:compress-config="compressConfig"
|
||||
:upload-api="uploadApi"
|
||||
:auto-upload="true"
|
||||
@image-upload-success="onImageUploadSuccess"
|
||||
@video-upload-success="onVideoUploadSuccess"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ImageVideoUpload, COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
|
||||
|
||||
const imageList = ref([])
|
||||
const videoList = ref([])
|
||||
const compressConfig = ref(COMPRESS_PRESETS.medium)
|
||||
|
||||
const uploadApi = async (file) => {
|
||||
// 上传逻辑
|
||||
}
|
||||
|
||||
const onImageUploadSuccess = (image, index) => {
|
||||
console.log('图片上传成功', image)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 主要功能
|
||||
|
||||
### ✅ 图片功能
|
||||
- 拍照/相册选择
|
||||
- 智能压缩(H5/APP自适应)
|
||||
- 预览功能
|
||||
- 删除功能
|
||||
- 压缩状态显示
|
||||
|
||||
### ✅ 视频功能
|
||||
- 拍摄/相册选择
|
||||
- 文件大小检查
|
||||
- 时长限制
|
||||
- 预览播放
|
||||
- 删除功能
|
||||
|
||||
### ✅ 压缩功能
|
||||
- 尺寸压缩(降低分辨率)
|
||||
- 质量压缩(调整JPEG质量)
|
||||
- 智能压缩(根据文件大小自动调整)
|
||||
- 多级压缩(确保达到目标大小)
|
||||
|
||||
## 压缩配置
|
||||
|
||||
### 预设配置
|
||||
```javascript
|
||||
import { COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
|
||||
|
||||
// 高质量(文件较大)
|
||||
const highConfig = COMPRESS_PRESETS.high
|
||||
|
||||
// 平衡(推荐)
|
||||
const mediumConfig = COMPRESS_PRESETS.medium
|
||||
|
||||
// 高压缩(文件很小)
|
||||
const lowConfig = COMPRESS_PRESETS.low
|
||||
```
|
||||
|
||||
### 自定义配置
|
||||
```javascript
|
||||
const customConfig = {
|
||||
image: {
|
||||
quality: 70, // 压缩质量 1-100
|
||||
maxWidth: 1600, // 最大宽度
|
||||
maxHeight: 900, // 最大高度
|
||||
maxSize: 300 * 1024, // 最大文件大小(字节)
|
||||
minQuality: 25 // 最低压缩质量
|
||||
},
|
||||
video: {
|
||||
maxDuration: 45, // 最大时长(秒)
|
||||
maxSize: 8 * 1024 * 1024, // 最大文件大小(字节)
|
||||
quality: 'medium' // 视频质量
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件回调
|
||||
|
||||
```javascript
|
||||
// 图片上传成功
|
||||
@image-upload-success="(image, index) => {}"
|
||||
|
||||
// 图片上传失败
|
||||
@image-upload-error="(error, index) => {}"
|
||||
|
||||
// 视频上传成功
|
||||
@video-upload-success="(video, index) => {}"
|
||||
|
||||
// 视频上传失败
|
||||
@video-upload-error="(error, index) => {}"
|
||||
|
||||
// 上传进度
|
||||
@upload-progress="(type, current, total) => {}"
|
||||
```
|
||||
|
||||
## 在 xcXkkcDetail.vue 中的使用
|
||||
|
||||
原来的代码已经替换为:
|
||||
|
||||
```vue
|
||||
<ImageVideoUpload
|
||||
v-model:image-list="imageList"
|
||||
v-model:video-list="videoList"
|
||||
:max-image-count="5"
|
||||
:max-video-count="3"
|
||||
:compress-config="compressConfig"
|
||||
:upload-api="attachmentUpload"
|
||||
@image-upload-success="onImageUploadSuccess"
|
||||
@video-upload-success="onVideoUploadSuccess"
|
||||
/>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **环境兼容**:自动检测H5/APP环境,使用不同的压缩方案
|
||||
2. **文件大小**:建议根据实际需求调整压缩配置
|
||||
3. **上传API**:需要返回标准格式的响应
|
||||
4. **内存管理**:上传成功后自动清理临时文件
|
||||
5. **错误处理**:完善的错误处理和用户提示
|
||||
|
||||
## 响应格式要求
|
||||
|
||||
上传API需要返回:
|
||||
```javascript
|
||||
{
|
||||
resultCode: 1, // 1表示成功
|
||||
result: [
|
||||
{
|
||||
filePath: "服务器文件路径"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 样式自定义
|
||||
|
||||
组件使用scoped样式,如需自定义:
|
||||
|
||||
```css
|
||||
/* 全局样式覆盖 */
|
||||
.image-video-upload .add-btn {
|
||||
border-color: #007aff;
|
||||
}
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
可以运行 `example.vue` 来测试组件功能。
|
||||
@ -1,7 +1,9 @@
|
||||
const ip: string = "127.0.0.1:8897";
|
||||
// const ip: string = "yufangzc.com";
|
||||
// const ip: string = "lzcxsx.cn";
|
||||
const fwqip: string = "lzcxsx.cn";
|
||||
const fwqip: string = "127.0.0.1:8897";
|
||||
//const ip: string = "lzcxsx.cn";
|
||||
//const fwqip: string = "lzcxsx.cn";
|
||||
//const ip: string = "zhxy.yufangzc.com";//
|
||||
//const fwqip: string = "zhxy.yufangzc.com";
|
||||
//打包服务器接口代理标识
|
||||
const SERVERAGENT: string = "/jsd-api";
|
||||
//本地代理url地址,配置了就启动代理,没配置就不启动代理
|
||||
@ -15,7 +17,7 @@ export const BASE_WS_URL: string = `wss://${ip}`;
|
||||
// export const BASE_IMAGE_URL: string = process.env.NODE_ENV == "development" ? `https://${ip}` : `https://${fwqip}`;
|
||||
export const BASE_IMAGE_URL = `https://${fwqip}`;
|
||||
// kkFileView预览服务地址
|
||||
export const KK_FILE_VIEW_URL: string = `http://${fwqip}:8891`;
|
||||
export const KK_FILE_VIEW_URL: string = `https://${fwqip}/kkpro`;
|
||||
//存token的key
|
||||
export const AUTH_KEY: string = "satoken";
|
||||
//token过期返回状态码
|
||||
|
||||
@ -241,6 +241,13 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/ShiTangXunCha/edit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "食堂巡查修改",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/JiFenPingJia/detail",
|
||||
"style": {
|
||||
|
||||
@ -242,7 +242,7 @@ const sections = reactive<Section[]>([
|
||||
{
|
||||
id: "r6",
|
||||
icon: "kfxc",
|
||||
text: "课服巡查",
|
||||
text: "教学巡查",
|
||||
show: true,
|
||||
permissionKey: "routine-kfxc", // 课服巡查权限编码
|
||||
path: "/pages/view/routine/kefuxuncha/xcPbList",
|
||||
|
||||
@ -85,8 +85,12 @@ let bjId = '';
|
||||
// 改变了年级班级
|
||||
const changeNjBj = (nj: any, bj: any) => {
|
||||
bjId = bj.key;
|
||||
selectDay(curRqIndex.value);
|
||||
console.log(nj, bj);
|
||||
console.log("班级选择变更:", { nj, bj, bjId });
|
||||
|
||||
// 重新加载当前日期的课表数据
|
||||
if (rqList.value.length > 0) {
|
||||
selectDay(curRqIndex.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 这是当年的第几周
|
||||
@ -153,7 +157,8 @@ const selectWeek = (zc: any) => {
|
||||
// 关闭弹窗
|
||||
closeWeekPicker();
|
||||
|
||||
// 选中第一天
|
||||
// 重置为第一天并加载数据
|
||||
curRqIndex.value = 0;
|
||||
selectDay(0);
|
||||
};
|
||||
|
||||
@ -163,9 +168,18 @@ const selectDay = (index: number) => {
|
||||
if (!rqList.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("查询课表参数:", {
|
||||
jsId: "",
|
||||
bjId: bjId || "",
|
||||
xqId: xqId,
|
||||
rq: rqList.value[index].rq,
|
||||
zj: rqList.value[index].zj
|
||||
});
|
||||
|
||||
drpkkbApi({
|
||||
jsId: "", // getJs.id,
|
||||
bjId: bjId,
|
||||
bjId: bjId || "", // 如果没有选择班级,传空字符串
|
||||
xqId: xqId,
|
||||
rq: rqList.value[index].rq, // 用于查询代课信息
|
||||
zj: rqList.value[index].zj
|
||||
@ -173,14 +187,17 @@ const selectDay = (index: number) => {
|
||||
// 根据接口返回的result判断是否已报名
|
||||
if (res && res.resultCode === 1) {
|
||||
sjList.value = res.result;
|
||||
console.log("课表数据加载成功:", res.result);
|
||||
} else {
|
||||
// 接口调用成功但返回错误
|
||||
console.warn("检查报名状态接口返回错误:", res);
|
||||
console.warn("课表接口返回错误:", res);
|
||||
sjList.value = [];
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// 接口调用失败
|
||||
console.error("调用检查报名状态接口失败:", error);
|
||||
console.error("调用课表接口失败:", error);
|
||||
sjList.value = [];
|
||||
});
|
||||
};
|
||||
|
||||
@ -194,17 +211,38 @@ const getCourseColorClass = (subject: string | undefined): string => {
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await dqpkApi();
|
||||
const result = res.result;
|
||||
dqZc = res.result.dqZc;
|
||||
xqId = res.result.dqXq.id;
|
||||
zcList.value = result.zcList;
|
||||
sjList.value = result.sjList;
|
||||
let zc = zcList.value.find((item:any) => item.djz === dqZc);
|
||||
if (!zc) {
|
||||
zc = zcList.value[0];
|
||||
try {
|
||||
const res = await dqpkApi();
|
||||
const result = res.result;
|
||||
dqZc = res.result.dqZc;
|
||||
xqId = res.result.dqXq.id;
|
||||
zcList.value = result.zcList;
|
||||
sjList.value = result.sjList;
|
||||
|
||||
// 1. 默认定位到当前周
|
||||
let zc = zcList.value.find((item:any) => item.djz === dqZc);
|
||||
if (!zc) {
|
||||
zc = zcList.value[0];
|
||||
}
|
||||
|
||||
// 设置周次和日期
|
||||
Object.assign(curZc.value, zc);
|
||||
Object.assign(rqList.value, curZc.value.drList);
|
||||
|
||||
// 2. 默认定位到当前周几(周三)
|
||||
const currentDayOfWeek = dayjs().day(); // 0=周日, 1=周一, ..., 6=周六
|
||||
const targetDayIndex = currentDayOfWeek === 0 ? 6 : currentDayOfWeek - 1; // 转换为1=周一, 7=周日的格式
|
||||
|
||||
// 确保索引在有效范围内
|
||||
const validDayIndex = Math.min(Math.max(targetDayIndex, 0), rqList.value.length - 1);
|
||||
curRqIndex.value = validDayIndex;
|
||||
|
||||
|
||||
// 自动加载当前日期的课表数据
|
||||
selectDay(validDayIndex);
|
||||
} catch (error) {
|
||||
console.error("初始化失败:", error);
|
||||
}
|
||||
selectWeek(zc);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -380,6 +418,7 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { hcFindByIdApi } from "@/api/base/hcApi";
|
||||
import { hcFindPageApi } from "@/api/base/hcApi";
|
||||
import { imagUrl } from "@/utils";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
@ -101,9 +101,13 @@ onLoad((options) => {
|
||||
// 加载详情数据
|
||||
const loadDetail = async (id: string) => {
|
||||
try {
|
||||
const res = await hcFindByIdApi({ id });
|
||||
if (res && res.result) {
|
||||
detailData.value = res.result;
|
||||
const res = await hcFindPageApi({
|
||||
id: id,
|
||||
page: 1,
|
||||
rows: 1
|
||||
});
|
||||
if (res && res.rows && res.rows.length > 0) {
|
||||
detailData.value = res.rows[0];
|
||||
|
||||
// 处理图片
|
||||
if (detailData.value.tjfj) {
|
||||
|
||||
552
src/pages/view/routine/ShiTangXunCha/edit.vue
Normal file
552
src/pages/view/routine/ShiTangXunCha/edit.vue
Normal file
@ -0,0 +1,552 @@
|
||||
<!-- src/pages/view/routine/ShiTangXunCha/edit.vue -->
|
||||
<template>
|
||||
<view class="edit-page">
|
||||
<view class="form-container">
|
||||
<view class="form-title">修改食堂巡查</view>
|
||||
|
||||
<!-- 工作名称 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">工作名称 *</text>
|
||||
<input
|
||||
v-model="formData.gzmc"
|
||||
class="form-input"
|
||||
placeholder="请输入工作名称"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 工作描述 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">工作描述</text>
|
||||
<textarea
|
||||
v-model="formData.gzms"
|
||||
class="form-textarea"
|
||||
placeholder="请输入工作描述"
|
||||
maxlength="500"
|
||||
:show-count="true"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 上传照片 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">上传照片</text>
|
||||
<view class="upload-section">
|
||||
<view class="upload-list">
|
||||
<view
|
||||
v-for="(image, index) in imageList"
|
||||
:key="index"
|
||||
class="upload-item"
|
||||
>
|
||||
<image
|
||||
:src="image.url ? imagUrl(image.url) : image.tempPath"
|
||||
mode="aspectFill"
|
||||
class="upload-image"
|
||||
@click="previewImage(index)"
|
||||
/>
|
||||
<view class="upload-delete" @click="deleteImage(index)">
|
||||
<text class="delete-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="imageList.length < 9"
|
||||
class="upload-add"
|
||||
@click="chooseImage"
|
||||
>
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交人 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">提交人</text>
|
||||
<input
|
||||
:value="formData.jsxm"
|
||||
class="form-input"
|
||||
disabled
|
||||
placeholder="自动获取当前用户"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 提交时间 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">提交时间</text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="formData.tjtime"
|
||||
@change="onTimeChange"
|
||||
class="form-picker"
|
||||
>
|
||||
<view class="picker-display">
|
||||
{{ formData.tjtime || '请选择提交时间' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<u-button
|
||||
text="提交"
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="handleSubmit"
|
||||
class="submit-btn"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { hcSaveApi, hcFindPageApi } from "@/api/base/hcApi";
|
||||
import { attachmentUpload } from "@/api/system/upload";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { showLoading, hideLoading, showToast } from "@/utils/uniapp";
|
||||
import { imagUrl } from "@/utils";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
const { getJs } = useUserStore();
|
||||
|
||||
// 获取页面参数
|
||||
let hcId = '';
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
id: '', // 记录ID
|
||||
gzmc: '', // 工作名称
|
||||
gzms: '', // 工作描述
|
||||
jsId: '', // 提交老师ID
|
||||
jsxm: '', // 提交老师姓名
|
||||
tjtime: '', // 提交时间
|
||||
tjfj: '' // 提交附件
|
||||
});
|
||||
|
||||
// 图片列表 - 修改为包含临时路径和服务器路径的对象
|
||||
interface ImageItem {
|
||||
tempPath?: string; // 临时路径(用于预览)
|
||||
url?: string; // 服务器路径(上传成功后)
|
||||
name?: string; // 文件名
|
||||
}
|
||||
|
||||
const imageList = ref<ImageItem[]>([]);
|
||||
|
||||
// 提交状态
|
||||
const submitting = ref(false);
|
||||
|
||||
// 页面加载时获取数据
|
||||
onLoad((options: any) => {
|
||||
if (options.id) {
|
||||
hcId = options.id;
|
||||
loadData();
|
||||
} else {
|
||||
showToast({ title: '参数错误', icon: 'none' });
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
showLoading('加载中...');
|
||||
console.log('正在加载数据,ID:', hcId);
|
||||
|
||||
const res = await hcFindPageApi({
|
||||
id: hcId,
|
||||
page: 1,
|
||||
rows: 1
|
||||
});
|
||||
console.log('API响应:', res);
|
||||
|
||||
if (res && res.rows && res.rows.length > 0) {
|
||||
const data = res.rows[0];
|
||||
|
||||
// 填充表单数据
|
||||
formData.value = {
|
||||
id: data.id,
|
||||
gzmc: data.gzmc || '',
|
||||
gzms: data.gzms || '',
|
||||
jsId: data.jsId || '',
|
||||
jsxm: data.jsxm || '',
|
||||
tjtime: data.tjtime ? data.tjtime.split(' ')[0] : '', // 只取日期部分
|
||||
tjfj: data.tjfj || ''
|
||||
};
|
||||
|
||||
// 处理图片数据
|
||||
if (data.tjfj) {
|
||||
const imageUrls = data.tjfj.split(',').filter((url: string) => url.trim());
|
||||
imageList.value = imageUrls.map((url: string) => ({
|
||||
url: url.trim(),
|
||||
name: url.split('/').pop() || 'image.jpg'
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
throw new Error('获取数据失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
showToast({ title: '加载数据失败', icon: 'none' });
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化数据
|
||||
onMounted(() => {
|
||||
const js = getJs;
|
||||
formData.value.jsId = js.id;
|
||||
formData.value.jsxm = js.jsxm;
|
||||
});
|
||||
|
||||
// 选择图片
|
||||
const chooseImage = () => {
|
||||
uni.chooseImage({
|
||||
count: 9 - imageList.value.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
// 添加临时图片到列表
|
||||
const tempFilePaths = res.tempFilePaths as string[];
|
||||
const newImages = tempFilePaths.map((path: string) => ({
|
||||
tempPath: path,
|
||||
name: path.split('/').pop() || 'image.jpg'
|
||||
}));
|
||||
imageList.value = [...imageList.value, ...newImages];
|
||||
// 自动上传图片
|
||||
await uploadImages(newImages);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 上传图片到服务器
|
||||
const uploadImages = async (images: ImageItem[]) => {
|
||||
try {
|
||||
showLoading('上传图片中...');
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const image = images[i];
|
||||
if (image.tempPath) {
|
||||
try {
|
||||
// 调用上传接口
|
||||
const uploadResult: any = await attachmentUpload(image.tempPath as any);
|
||||
|
||||
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
|
||||
// 保存服务器返回的文件路径
|
||||
const serverPath = uploadResult.result[0].filePath;
|
||||
|
||||
// 更新图片对象,保存服务器路径
|
||||
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
|
||||
if (index !== -1) {
|
||||
imageList.value[index].url = serverPath;
|
||||
// 可以删除临时路径以节省内存
|
||||
delete imageList.value[index].tempPath;
|
||||
}
|
||||
} else {
|
||||
throw new Error('上传响应格式异常');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('图片上传失败:', error);
|
||||
showToast({ title: `${image.name || '图片'}上传失败`, icon: 'none' });
|
||||
|
||||
// 从列表中移除上传失败的图片
|
||||
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
|
||||
if (index !== -1) {
|
||||
imageList.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
showToast({ title: '图片上传完成', icon: 'success' });
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
console.error('批量上传图片失败:', error);
|
||||
showToast({ title: '图片上传失败,请重试', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (index: number) => {
|
||||
const urls = imageList.value.map(img =>
|
||||
img.url ? imagUrl(img.url) : img.tempPath
|
||||
).filter((url): url is string => !!url);
|
||||
|
||||
uni.previewImage({
|
||||
urls: urls,
|
||||
current: index
|
||||
});
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const deleteImage = (index: number) => {
|
||||
imageList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 时间选择
|
||||
const onTimeChange = (e: any) => {
|
||||
formData.value.tjtime = e.detail.value;
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
// 验证必填字段
|
||||
if (!formData.value.gzmc.trim()) {
|
||||
showToast({ title: '请输入工作名称', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.value.tjtime) {
|
||||
showToast({ title: '请选择提交时间', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有图片正在上传
|
||||
const hasUploadingImages = imageList.value.some(img => img.tempPath && !img.url);
|
||||
if (hasUploadingImages) {
|
||||
showToast({ title: '请等待图片上传完成', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
showLoading('提交中...');
|
||||
|
||||
try {
|
||||
// 处理图片路径
|
||||
let tjfj = '';
|
||||
const uploadedImages = imageList.value.filter(img => img.url);
|
||||
if (uploadedImages.length > 0) {
|
||||
// 使用服务器路径,用逗号分隔,过滤掉 undefined 值
|
||||
const imageUrls = uploadedImages.map(img => img.url).filter((url): url is string => !!url);
|
||||
tjfj = imageUrls.join(',');
|
||||
}
|
||||
|
||||
const submitData = {
|
||||
...formData.value,
|
||||
tjfj
|
||||
};
|
||||
|
||||
await hcSaveApi(submitData);
|
||||
|
||||
hideLoading();
|
||||
showToast({ title: '提交成功', icon: 'success' });
|
||||
|
||||
// 返回列表页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
showToast({ title: '提交失败,请重试', icon: 'none' });
|
||||
console.error('提交失败:', error);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.edit-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border-color: #28a745;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #f8f9fa;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
border-color: #28a745;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.form-picker {
|
||||
.picker-display {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
.upload-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e9ecef;
|
||||
|
||||
.upload-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.upload-delete {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.delete-icon {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-add {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 2px dashed #e9ecef;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f8f9fa;
|
||||
cursor: pointer;
|
||||
|
||||
.add-icon {
|
||||
font-size: 24px;
|
||||
color: #6c757d;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
padding: 0 16px;
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 16px rgba(40, 167, 69, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式优化
|
||||
@media (max-width: 375px) {
|
||||
.edit-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -55,16 +55,30 @@
|
||||
<view class="card-body">
|
||||
<rich-text class="hc-excerpt" :nodes="data.gzms"></rich-text>
|
||||
<view v-if="data.tjfj" class="image-preview">
|
||||
<image
|
||||
:src="getImageUrl(data.tjfj)"
|
||||
mode="aspectFill"
|
||||
class="preview-image"
|
||||
></image>
|
||||
<view
|
||||
v-for="(imageUrl, index) in getImageList(data.tjfj)"
|
||||
:key="index"
|
||||
class="preview-image-wrapper"
|
||||
>
|
||||
<image
|
||||
:src="imageUrl"
|
||||
mode="aspectFill"
|
||||
class="preview-image"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<text class="footer-item">提交人: {{ data.jsxm || '未知' }}</text>
|
||||
<text class="footer-item">提交时间: {{ formatTime(data.tjtime) }}</text>
|
||||
<view class="footer-actions">
|
||||
<view
|
||||
@click.stop="goToEdit(data.id)"
|
||||
class="edit-btn"
|
||||
>
|
||||
修改
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@ -170,6 +184,7 @@ const getHcList = async () => {
|
||||
|
||||
// 跳转到详情页
|
||||
const goToDetail = (hcId: string) => {
|
||||
console.log('点击卡片,跳转详情页,ID:', hcId);
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/ShiTangXunCha/detail?id=${hcId}`,
|
||||
});
|
||||
@ -182,6 +197,14 @@ const goToAdd = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到修改页
|
||||
const goToEdit = (hcId: string) => {
|
||||
console.log('点击修改按钮,ID:', hcId);
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/ShiTangXunCha/edit?id=${hcId}`,
|
||||
});
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string) => {
|
||||
if (!timeStr) return "";
|
||||
@ -200,6 +223,14 @@ const getImageUrl = (imagePath: string) => {
|
||||
return imagUrl(firstImage);
|
||||
};
|
||||
|
||||
// 获取图片列表(最多3张)
|
||||
const getImageList = (imagePath: string) => {
|
||||
if (!imagePath) return [];
|
||||
// 按逗号分割图片路径,最多取3张
|
||||
const imagePaths = imagePath.split(',').slice(0, 3);
|
||||
return imagePaths.map(path => imagUrl(path.trim()));
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
getHcList();
|
||||
});
|
||||
@ -371,12 +402,23 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
|
||||
.preview-image-wrapper {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 80px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -409,6 +451,36 @@ onMounted(() => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
|
||||
.edit-btn {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 4px 12px;
|
||||
min-width: 50px;
|
||||
height: 28px;
|
||||
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.3);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 3px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增按钮 - 固定在底部
|
||||
|
||||
@ -3,22 +3,10 @@
|
||||
<!-- 班级选择器 -->
|
||||
<view class="section">
|
||||
<text class="section-title">选择班级</text>
|
||||
<BasicNjBjSelect
|
||||
:custom-style="{
|
||||
borderRadius: '12rpx',
|
||||
background: 'background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)',
|
||||
padding: '0rpx 30rpx',
|
||||
border: '1rpx solid #e9ecef',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
}"
|
||||
v-model="selectedNjBj"
|
||||
placeholder="请选择年级班级"
|
||||
:only-confirm-change-flag="true"
|
||||
:auto-selected-first="false"
|
||||
:bj-required="true"
|
||||
@change="changeNjBj"
|
||||
/>
|
||||
<view class="class-selector" @click="showClassTree">
|
||||
<text :class="{ placeholder: !selectedClassText }">{{ selectedClassText || "请选择班级" }}</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
<!-- 班级选择提示 -->
|
||||
<view v-if="!curBj" class="class-tip">
|
||||
<text class="tip-icon">ℹ️</text>
|
||||
@ -38,16 +26,18 @@
|
||||
ref="dmXsRef"
|
||||
/>
|
||||
|
||||
<!-- 拍照视频组件 -->
|
||||
<!-- 图片视频上传组件 -->
|
||||
<view class="section" v-if="curBj">
|
||||
<DmPsComponent
|
||||
v-model="mediaData"
|
||||
:photo-title="'就餐现场拍照'"
|
||||
:video-title="'就餐现场视频'"
|
||||
:max-photo-count="9"
|
||||
<ImageVideoUpload
|
||||
v-model:image-list="imageList"
|
||||
v-model:video-list="videoList"
|
||||
:max-image-count="9"
|
||||
:max-video-count="3"
|
||||
:max-video-duration="60"
|
||||
ref="dmPsRef"
|
||||
:compress-config="compressConfig"
|
||||
:upload-api="attachmentUpload"
|
||||
@image-upload-success="onImageUploadSuccess"
|
||||
@video-upload-success="onVideoUploadSuccess"
|
||||
ref="imageVideoUploadRef"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@ -69,15 +59,30 @@
|
||||
提交点名
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 班级选择树 -->
|
||||
<BasicTree
|
||||
ref="treeRef"
|
||||
:range="treeData"
|
||||
idKey="key"
|
||||
rangeKey="title"
|
||||
title="选择班级"
|
||||
:multiple="false"
|
||||
:selectParent="false"
|
||||
@confirm="onTreeConfirm"
|
||||
@cancel="onTreeCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import BasicNjBjSelect from '@/components/BasicNjBjSelect/index.vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import BasicTree from '@/components/BasicTree/Tree.vue'
|
||||
import DmJs from './dmJs.vue'
|
||||
import DmXs from './dmXs.vue'
|
||||
import DmPsComponent from '@/pages/components/dmPs/index.vue'
|
||||
import { ImageVideoUpload, type ImageItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
|
||||
import { submitJcDmDataApi } from '@/api/base/jcApi'
|
||||
import { findAllNjBjTree } from '@/api/base/server'
|
||||
import { attachmentUpload } from '@/api/system/upload'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useDataStore } from '@/store/modules/data'
|
||||
import { useDebounce } from "@/utils/debounce";
|
||||
@ -87,15 +92,9 @@ const { getJcBz } = useDataStore();
|
||||
// 替换 isSubmitting 状态为 useDebounce
|
||||
const { isProcessing: isSubmitting, debounce } = useDebounce(2000);
|
||||
|
||||
const selectedNjBj = ref<any>(null);
|
||||
/**
|
||||
* 就餐点名组件
|
||||
* 功能:
|
||||
* 1. 选择班级,获取学生列表
|
||||
* 2. 将学生分为两类:已缴费(可切换正常/请假/缺勤)和未缴费/未报名(状态为未缴费/未报名)
|
||||
* 3. 选择陪餐教师
|
||||
* 4. 批量提交点名记录,包含jsId和dmPcId字段
|
||||
*/
|
||||
// 树形数据
|
||||
const treeData = ref<any[]>([]);
|
||||
const treeRef = ref();
|
||||
|
||||
// 接收外部传入属性
|
||||
const props = withDefaults(defineProps<{
|
||||
@ -110,24 +109,86 @@ const isLoading = ref(false);
|
||||
const curNj = ref<any>(null);
|
||||
const curBj = ref<any>(null);
|
||||
|
||||
// 计算属性:显示选中的班级文本
|
||||
const selectedClassText = computed(() => {
|
||||
if (curBj.value && curNj.value) {
|
||||
return `${curNj.value.title} ${curBj.value.title}`;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const dmXsRef = ref<any>(null);
|
||||
const dmJsRef = ref<any>(null);
|
||||
|
||||
// 媒体数据
|
||||
const mediaData = ref({
|
||||
photoList: [],
|
||||
videoList: []
|
||||
});
|
||||
const dmPsRef = ref<any>(null);
|
||||
// 压缩配置
|
||||
const compressConfig = ref(COMPRESS_PRESETS.medium)
|
||||
|
||||
// 改变了年级班级
|
||||
const changeNjBj = (val: any) => {
|
||||
const njList = val.selectedGrades || [];
|
||||
const nj = njList[0] || {};
|
||||
const bjList = val.selectedClasses || [];
|
||||
const bj = bjList[0] || {};
|
||||
curNj.value = nj
|
||||
curBj.value = bj
|
||||
// 媒体数据
|
||||
const imageList = ref<ImageItem[]>([]);
|
||||
const videoList = ref<VideoItem[]>([]);
|
||||
const imageVideoUploadRef = ref<any>(null);
|
||||
|
||||
// 上传成功回调
|
||||
const onImageUploadSuccess = (image: ImageItem, index: number) => {
|
||||
console.log('图片上传成功:', image, index);
|
||||
};
|
||||
|
||||
const onVideoUploadSuccess = (video: VideoItem, index: number) => {
|
||||
console.log('视频上传成功:', video, index);
|
||||
};
|
||||
|
||||
// 加载树形数据
|
||||
const loadTreeData = async () => {
|
||||
try {
|
||||
const res = await findAllNjBjTree();
|
||||
if (res.resultCode === 1 && res.result) {
|
||||
// 递归转换数据格式以适配 BasicTree 组件
|
||||
const convertTreeData = (items: any[]): any[] => {
|
||||
return items.map((item: any) => ({
|
||||
key: item.key,
|
||||
title: item.title,
|
||||
njmcId: item.njmcId,
|
||||
children: item.children ? convertTreeData(item.children) : [],
|
||||
}));
|
||||
};
|
||||
treeData.value = convertTreeData(res.result);
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "加载班级数据失败", icon: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
// 显示班级选择树
|
||||
const showClassTree = () => {
|
||||
if (treeRef.value) {
|
||||
treeRef.value._show();
|
||||
}
|
||||
};
|
||||
|
||||
// 树形选择确认
|
||||
const onTreeConfirm = (selectedItems: any[]) => {
|
||||
if (selectedItems.length > 0) {
|
||||
const selectedItem = selectedItems[0]; // 单选模式,取第一个
|
||||
|
||||
// 如果选择的是班级(有parents表示是班级)
|
||||
if (selectedItem.parents && selectedItem.parents.length > 0) {
|
||||
const parent = selectedItem.parents[0]; // 年级信息
|
||||
const nj = parent; // 年级信息
|
||||
const bj = selectedItem; // 班级信息
|
||||
|
||||
curNj.value = nj;
|
||||
curBj.value = bj;
|
||||
} else {
|
||||
// 如果选择的是年级,清空班级选择
|
||||
curNj.value = selectedItem;
|
||||
curBj.value = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 树形选择取消
|
||||
const onTreeCancel = () => {
|
||||
// 取消选择,不做任何操作
|
||||
};
|
||||
|
||||
const tjDm = debounce(async () => {
|
||||
@ -141,26 +202,16 @@ const tjDm = debounce(async () => {
|
||||
try {
|
||||
const dmXsList = dmXsRef.value.getDmXsList();
|
||||
const dmJsList = dmJsRef.value.getDmJsList();
|
||||
// 先上传媒体文件
|
||||
let photoUrls = '';
|
||||
let videoUrls = '';
|
||||
// 获取媒体文件URL
|
||||
const photoUrls = imageList.value
|
||||
.filter(img => img.url)
|
||||
.map(img => img.url)
|
||||
.join(',');
|
||||
|
||||
if (mediaData.value.photoList.length > 0 || mediaData.value.videoList.length > 0) {
|
||||
try {
|
||||
if (dmPsRef.value) {
|
||||
const uploadResult = await dmPsRef.value.uploadMedia();
|
||||
photoUrls = uploadResult.photoUrls;
|
||||
videoUrls = uploadResult.videoUrls;
|
||||
}
|
||||
} catch (uploadError) {
|
||||
console.error('媒体文件上传失败:', uploadError);
|
||||
uni.showToast({
|
||||
title: '媒体文件上传失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
const videoUrls = videoList.value
|
||||
.filter(video => video.url)
|
||||
.map(video => video.url)
|
||||
.join(',');
|
||||
let dmData: any = {
|
||||
jcTime: new Date(),
|
||||
bjId: curBj.value.key,
|
||||
@ -191,10 +242,8 @@ const tjDm = debounce(async () => {
|
||||
// 重置表单
|
||||
curNj.value = null
|
||||
curBj.value = null
|
||||
mediaData.value = {
|
||||
photoList: [],
|
||||
videoList: []
|
||||
};
|
||||
imageList.value = []
|
||||
videoList.value = []
|
||||
// 返回上一页
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
@ -209,6 +258,11 @@ const tjDm = debounce(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadTreeData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -235,6 +289,32 @@ const tjDm = debounce(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
.class-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 30rpx;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border: 1rpx solid #e9ecef;
|
||||
border-radius: 12rpx;
|
||||
margin-top: 20rpx;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
|
||||
&.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.class-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -7,27 +7,27 @@
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<view class="stats-container">
|
||||
<view class="stat-item">
|
||||
<view class="stat-item clickable" @click="showStudentDrawer('all')">
|
||||
<text class="stat-number">{{ rsData.zrs }}</text>
|
||||
<text class="stat-label">总人数</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-item clickable" @click="showStudentDrawer('registered')">
|
||||
<text class="stat-number unregistered">{{ rsData.bmRs }}</text>
|
||||
<text class="stat-label">报名就餐</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-item clickable" @click="showStudentDrawer('unregistered')">
|
||||
<text class="stat-number unregistered">{{ rsData.unBmRs }}</text>
|
||||
<text class="stat-label">未报名就餐</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-item clickable" @click="showStudentDrawer('normal')">
|
||||
<text class="stat-number normal">{{ hqZtSl('A') }}</text>
|
||||
<text class="stat-label">正常</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-item clickable" @click="showStudentDrawer('leave')">
|
||||
<text class="stat-number leave">{{ hqZtSl('B') }}</text>
|
||||
<text class="stat-label">请假</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-item clickable" @click="showStudentDrawer('absent')">
|
||||
<text class="stat-number absent">{{ hqZtSl('C') }}</text>
|
||||
<text class="stat-label">缺勤</text>
|
||||
</view>
|
||||
@ -122,6 +122,42 @@
|
||||
@confirm="onChangeZt"
|
||||
@cancel="ztVisible = false"
|
||||
></u-picker>
|
||||
|
||||
<!-- 学生列表抽屉 -->
|
||||
<uni-popup ref="studentPopup" type="bottom" @change="popupChange">
|
||||
<view class="student-drawer">
|
||||
<view class="drawer-header">
|
||||
<text class="drawer-title">{{ drawerTitle }}</text>
|
||||
<text class="drawer-close" @click="closeStudentDrawer">✕</text>
|
||||
</view>
|
||||
<view class="drawer-content">
|
||||
<view v-if="filteredStudentList.length > 0" class="student-list">
|
||||
<view
|
||||
v-for="xs in filteredStudentList"
|
||||
:key="xs.id"
|
||||
class="student-item"
|
||||
>
|
||||
<view class="student-info">
|
||||
<image
|
||||
class="student-avatar"
|
||||
:src="imagUrl(xs.xstx) || '/static/images/default-avatar.png'"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="student-details">
|
||||
<text class="student-name">{{ xs.xm }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="student-tag" :class="getStatusClass(xs.jcZt)">
|
||||
{{ hqZtWz(xs.jcZt) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-drawer">
|
||||
<text class="empty-text">{{ emptyText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -165,6 +201,69 @@ const ztList = ref<Array<{ text: string, value: string }>>([
|
||||
const dqXs = ref<any>(null) // 当前学生
|
||||
const mqXz = ref<any>([0]) // 默认选择
|
||||
|
||||
// 学生列表抽屉相关
|
||||
const studentPopup = ref<any>(null)
|
||||
const drawerTitle = ref('')
|
||||
const emptyText = ref('')
|
||||
const filteredStudentList = ref<any>([])
|
||||
|
||||
const showStudentDrawer = (type: string) => {
|
||||
let students: any[] = []
|
||||
let title = ''
|
||||
let empty = ''
|
||||
|
||||
switch (type) {
|
||||
case 'all':
|
||||
students = [...bmXsList.value, ...unBmXsList.value]
|
||||
title = '全部学生'
|
||||
empty = '暂无学生数据'
|
||||
break
|
||||
case 'registered':
|
||||
students = bmXsList.value
|
||||
title = '报名就餐学生'
|
||||
empty = '暂无报名学生'
|
||||
break
|
||||
case 'unregistered':
|
||||
students = unBmXsList.value
|
||||
title = '未报名就餐学生'
|
||||
empty = '暂无未报名学生'
|
||||
break
|
||||
case 'normal':
|
||||
students = bmXsList.value.filter((s: any) => s.jcZt === 'A')
|
||||
title = '正常学生'
|
||||
empty = '暂无正常学生'
|
||||
break
|
||||
case 'leave':
|
||||
students = bmXsList.value.filter((s: any) => s.jcZt === 'B')
|
||||
title = '请假学生'
|
||||
empty = '暂无请假学生'
|
||||
break
|
||||
case 'absent':
|
||||
students = bmXsList.value.filter((s: any) => s.jcZt === 'C')
|
||||
title = '缺勤学生'
|
||||
empty = '暂无缺勤学生'
|
||||
break
|
||||
}
|
||||
|
||||
drawerTitle.value = title
|
||||
emptyText.value = empty
|
||||
filteredStudentList.value = students
|
||||
|
||||
if (studentPopup.value) {
|
||||
studentPopup.value.open('bottom')
|
||||
}
|
||||
}
|
||||
|
||||
const closeStudentDrawer = () => {
|
||||
if (studentPopup.value) {
|
||||
studentPopup.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
const popupChange = (e: { show: boolean }) => {
|
||||
// 抽屉状态变化处理
|
||||
}
|
||||
|
||||
// 加载学生列表
|
||||
const loadXsList = async () => {
|
||||
if (!props.bzId || !props.bjId) return
|
||||
@ -364,6 +463,45 @@ defineExpose({
|
||||
align-items: center;
|
||||
min-width: 120rpx;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: 0 4rpx 12rpx rgba(235, 47, 150, 0.2);
|
||||
background-color: rgba(235, 47, 150, 0.05);
|
||||
border-radius: 12rpx;
|
||||
padding: 8rpx;
|
||||
margin: -8rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2rpx 8rpx rgba(235, 47, 150, 0.3);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -10rpx;
|
||||
transform: translateY(-50%);
|
||||
width: 6rpx;
|
||||
height: 6rpx;
|
||||
background-color: #eb2f96;
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
@ -395,6 +533,7 @@ defineExpose({
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -585,4 +724,128 @@ defineExpose({
|
||||
font-size: 28rpx;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
/* 学生列表抽屉样式 */
|
||||
.student-drawer {
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx 40rpx 20rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
z-index: 10;
|
||||
|
||||
.drawer-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.drawer-close {
|
||||
font-size: 36rpx;
|
||||
color: #999;
|
||||
padding: 10rpx;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
padding: 20rpx 40rpx 40rpx;
|
||||
max-height: calc(80vh - 100rpx);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.student-list {
|
||||
.student-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.student-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.student-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 24rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.student-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.student-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.student-tag {
|
||||
font-size: 20rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-normal {
|
||||
background-color: #e6f7ff;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.status-leave {
|
||||
background-color: #fff7e6;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
&.status-absent {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.status-unpaid {
|
||||
background-color: #f9f0ff;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
&.status-unregistered {
|
||||
background-color: #fff0f6;
|
||||
color: #eb2f96;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-drawer {
|
||||
text-align: center;
|
||||
padding: 80rpx 0;
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -7,10 +7,18 @@
|
||||
style="position: absolute"
|
||||
>
|
||||
<template v-slot="{ data, index }">
|
||||
<view class="inspection-record bg-white r-md p-15 mb-15">
|
||||
<view class="inspection-record bg-white r-md p-15 mb-15 timeline-item">
|
||||
<!-- 时间轴连接线 -->
|
||||
<view class="timeline-line" v-if="index < 10"></view>
|
||||
|
||||
<!-- 时间轴节点 -->
|
||||
<view class="timeline-dot">
|
||||
<view class="dot-inner"></view>
|
||||
</view>
|
||||
|
||||
<view class="record-header">
|
||||
<view class="record-time">
|
||||
<u-icon name="clock" color="#666" size="14"></u-icon>
|
||||
<u-icon name="clock" color="#4080ff" size="16"></u-icon>
|
||||
<text class="time-text">{{ formatTime(data.xctime) }}</text>
|
||||
</view>
|
||||
<view class="record-status">
|
||||
@ -18,10 +26,18 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<view class="content-item" v-if="data.njbj">
|
||||
<text class="item-label">年级班级:</text>
|
||||
<text class="item-value">{{ data.njbj }}</text>
|
||||
</view>
|
||||
<view class="content-item">
|
||||
<text class="item-label">巡查教师:</text>
|
||||
<text class="item-value">{{ data.jsxm }}</text>
|
||||
</view>
|
||||
<view class="content-item" v-if="data.rkjsxm">
|
||||
<text class="item-label">任课老师:</text>
|
||||
<text class="item-value">{{ data.rkjsxm }}</text>
|
||||
</view>
|
||||
<view class="content-item flex-col">
|
||||
<text class="item-label" style="flex: 0 0 25px"
|
||||
>巡查项目:</text
|
||||
@ -36,20 +52,25 @@
|
||||
style="margin-bottom: 4px"
|
||||
>
|
||||
<view>
|
||||
<text>{{ idx + 1 }}、{{ xm.xcMc }}</text>
|
||||
<view
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 4px 0;
|
||||
"
|
||||
>
|
||||
<text>分值:{{ xm.xmFz }}分</text>
|
||||
<text
|
||||
>巡查结果:{{
|
||||
xm.xcJg === "A" ? "有" : "无"
|
||||
}}</text
|
||||
>
|
||||
<view style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
|
||||
<view style="display: flex; align-items: center; flex: 1;">
|
||||
<text style="margin-right: 8px;">{{ idx + 1 }}、{{ xm.xcMc }}</text>
|
||||
<view style="display: flex; align-items: center;">
|
||||
<u-icon
|
||||
:name="xm.xcJg === 'B' ? 'checkmark-circle-fill' : 'close-circle-fill'"
|
||||
:color="xm.xcJg === 'B' ? '#67c23a' : '#f56c6c'"
|
||||
size="18"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view style="font-size: 12px; color: #666;">
|
||||
<text v-if="xm.xcJg === 'A'" style="color: #f56c6c;">
|
||||
扣分:-{{ xm.xmFz }}分
|
||||
</text>
|
||||
<text v-else style="color: #67c23a;">
|
||||
不扣分
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -173,12 +194,15 @@ const xkkc = computed(() => getData);
|
||||
let inspectionParams = ref({
|
||||
rows: 10,
|
||||
kyXcId: xkkc.value.id,
|
||||
jsId: js.value.id,
|
||||
njId: xkkc.value.njId,
|
||||
bjId: xkkc.value.bjId,
|
||||
njmcId: xkkc.value.njmcId,
|
||||
// 移除时间参数,不传时间
|
||||
});
|
||||
|
||||
// 添加调试信息
|
||||
console.log('课业辅导巡查记录查询参数:', inspectionParams.value);
|
||||
|
||||
// 巡查记录列表
|
||||
const [registerInspection, { reload }] = useLayout({
|
||||
api: getKyXcPageApi,
|
||||
@ -189,9 +213,10 @@ const [registerInspection, { reload }] = useLayout({
|
||||
// 图片预览
|
||||
const handlePreviewImage = (img: string, images: string[]) => {
|
||||
// 兼容uni-app的图片预览API
|
||||
const processedImages = images.map(image => imagUrl(image));
|
||||
uni.previewImage({
|
||||
current: img,
|
||||
urls: images,
|
||||
current: imagUrl(img),
|
||||
urls: processedImages,
|
||||
});
|
||||
};
|
||||
|
||||
@ -202,7 +227,7 @@ const handlePreviewVideo = (videos: string[], index: number) => {
|
||||
uni.previewMedia({
|
||||
current: index,
|
||||
sources: videos.map((url) => ({
|
||||
url,
|
||||
url: imagUrl(url),
|
||||
type: "video",
|
||||
})),
|
||||
});
|
||||
@ -210,7 +235,10 @@ const handlePreviewVideo = (videos: string[], index: number) => {
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp: string) => {
|
||||
return dayjs(timestamp).format("YYYY-MM-DD HH:mm");
|
||||
const date = dayjs(timestamp);
|
||||
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
const weekDay = weekDays[date.day()];
|
||||
return `${weekDay} ${date.format("YYYY-MM-DD HH:mm")}`;
|
||||
};
|
||||
|
||||
// 将逗号分隔的字符串转换为数组
|
||||
@ -235,52 +263,170 @@ onMounted(() => {
|
||||
.inspection-list {
|
||||
position: relative;
|
||||
height: calc(100vh - 50px);
|
||||
padding-left: 12px; // 为时间轴留出空间
|
||||
|
||||
.inspection-record {
|
||||
position: relative;
|
||||
margin-left: 0; // 去掉左边距
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e8f4fd;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||
border-color: #4080ff;
|
||||
}
|
||||
|
||||
// 时间轴连接线
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #4080ff 0%, #e8f4fd 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// 时间轴节点
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: -18px;
|
||||
top: 20px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #4080ff;
|
||||
border-radius: 50%;
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 0 4px #ffffff, 0 0 0 6px #e8f4fd;
|
||||
|
||||
.dot-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #4080ff 0%, #66b3ff 100%);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 15px;
|
||||
padding: 12px 8px;
|
||||
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4fd 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
border-bottom: 1px solid #d1e7ff;
|
||||
|
||||
.record-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
color: #2c5aa0;
|
||||
font-weight: 600;
|
||||
|
||||
.time-text {
|
||||
margin-left: 5px;
|
||||
margin-left: 8px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.record-status {
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #4080ff;
|
||||
display: inline-flex;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, #4080ff 0%, #66b3ff 100%);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
color: #4080ff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px rgba(64, 128, 255, 0.3);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.record-content {
|
||||
padding: 0 8px 12px 8px;
|
||||
|
||||
.content-item {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
padding: 6px 8px;
|
||||
background: #fafbfc;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #e8f4fd;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f0f7ff;
|
||||
border-left-color: #4080ff;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-weight: bold;
|
||||
flex: 0 0 80px;
|
||||
font-weight: 600;
|
||||
flex: 0 0 70px;
|
||||
color: #2c5aa0;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
color: #4a5568;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第一个时间轴节点特殊样式
|
||||
.inspection-record:first-child {
|
||||
.timeline-dot {
|
||||
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
||||
box-shadow: 0 0 0 4px #ffffff, 0 0 0 6px #f0f9ff;
|
||||
}
|
||||
}
|
||||
|
||||
// 最后一个时间轴节点
|
||||
.inspection-record:last-child {
|
||||
.timeline-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 渐入动画
|
||||
.timeline-item {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .zp-loading-fixed {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<view class="header-content">
|
||||
<view class="title-section" @click="clickShowXkSelector">
|
||||
<view class="title">
|
||||
<text v-if="xkData && xkData.xkmc">{{ xkData.xkmc }}</text>
|
||||
<text v-if="pbData && pbData.xcbt">{{ pbData.xcbt }}</text>
|
||||
<text v-else>巡查选课</text>
|
||||
</view>
|
||||
<view class="switch-btn" v-if="xkList.length > 1">切换</view>
|
||||
@ -22,14 +22,14 @@
|
||||
:key="xkkc.id || index"
|
||||
class="course-item"
|
||||
>
|
||||
<view class="course-name">{{ xkkc.kcmc }}</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">年级:</view>
|
||||
<view class="info-data">{{ xkkc.gradeName || '暂无' }}</view>
|
||||
<!-- 巡查状态标识 -->
|
||||
<view class="course-status" :class="xkkc.sfxc === '是' ? 'status-done' : 'status-pending'">
|
||||
{{ xkkc.sfxc === '是' ? '已巡查' : '待巡查' }}
|
||||
</view>
|
||||
<view class="course-info-item" v-if="xkkc.bjmc">
|
||||
<view class="info-label">班级:</view>
|
||||
<view class="info-data">{{ xkkc.bjmc || '暂无' }}</view>
|
||||
<view class="course-name">{{ getCourseDisplayName(xkkc) }}</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">巡查时间:</view>
|
||||
<view class="info-data">{{ getInspectionTime(xkkc) }}</view>
|
||||
</view>
|
||||
<view class="separator-line"></view>
|
||||
<view class="course-btn-group">
|
||||
@ -95,6 +95,7 @@ import { getKyXcCourseListApi } from "@/api/base/pbApi";
|
||||
import { useDataStore } from "@/store/modules/data";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { getJs } = useUserStore();
|
||||
const dataStore = useDataStore();
|
||||
@ -108,25 +109,105 @@ const xkData = ref();
|
||||
// 课程列表数据
|
||||
const xkkcList = ref<any[]>([]);
|
||||
|
||||
// 排班数据
|
||||
const pbData = ref<any>(null);
|
||||
|
||||
const xcBeforeMinute = ref<number>(0);
|
||||
|
||||
// 星期名称列表
|
||||
const wdNameList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
||||
|
||||
// 检查当前时间是否符合课程时间安排
|
||||
const isCurrentTimeMatch = (xkkc: any) => {
|
||||
const now = dayjs();
|
||||
let wDay = now.day();
|
||||
if (wDay === 0) {
|
||||
wDay = 7; // 将周日从0改为7
|
||||
}
|
||||
let mDay = now.date(); // 当前日期(1-31)
|
||||
|
||||
// 判断周期类型
|
||||
switch (xkkc.skzqlx) {
|
||||
case '每天':
|
||||
return true; // 每天都可以巡查
|
||||
case '每周':
|
||||
if (!xkkc.skzq) return false;
|
||||
const daysOfWeek = xkkc.skzq.split(',').map(Number);
|
||||
return daysOfWeek.includes(wDay);
|
||||
case '每月':
|
||||
if (!xkkc.skzq) return false;
|
||||
const daysOfMonth = xkkc.skzq.split(',').map(Number);
|
||||
return daysOfMonth.includes(mDay);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取课程显示名称(只显示年级和班级信息)
|
||||
const getCourseDisplayName = (xkkc: any) => {
|
||||
// 构建年级班级信息
|
||||
let gradeClassInfo = '';
|
||||
|
||||
if (xkkc.gradeName) {
|
||||
gradeClassInfo = xkkc.gradeName;
|
||||
}
|
||||
|
||||
if (xkkc.bjmc && xkkc.bjmc !== '暂无') {
|
||||
if (gradeClassInfo) {
|
||||
gradeClassInfo += `-${xkkc.bjmc}`;
|
||||
} else {
|
||||
gradeClassInfo = xkkc.bjmc;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有年级班级信息,显示默认文本
|
||||
return gradeClassInfo || '暂无信息';
|
||||
};
|
||||
|
||||
// 获取巡查时间显示文本
|
||||
const getInspectionTime = (xkkc: any) => {
|
||||
if (!xkkc.skzqlx || !xkkc.skzq) {
|
||||
return '暂无';
|
||||
}
|
||||
|
||||
// 判断周期类型
|
||||
switch (xkkc.skzqlx) {
|
||||
case '每天':
|
||||
return '每天';
|
||||
case '每周':
|
||||
if (!xkkc.skzq) return '每周';
|
||||
const daysOfWeek = xkkc.skzq.split(',').map(Number);
|
||||
const weekDays = daysOfWeek.map((day: number) => wdNameList[day - 1]).join('、');
|
||||
return `每周 ${weekDays}`;
|
||||
case '每月':
|
||||
if (!xkkc.skzq) return '每月';
|
||||
const daysOfMonth = xkkc.skzq.split(',').map(Number);
|
||||
const monthDays = daysOfMonth.map((day: number) => day + '号').join('、');
|
||||
return `每月 ${monthDays}`;
|
||||
default:
|
||||
return '暂无';
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
uni.showLoading({
|
||||
title: "加载中...",
|
||||
});
|
||||
|
||||
// 优先从global获取排班数据,如果没有则从data获取
|
||||
let pbData = dataStore.getGlobal;
|
||||
// 优先从data获取排班数据(xcPbList.vue传递的数据)
|
||||
let tempPbData = dataStore.getData;
|
||||
|
||||
if (!pbData || !pbData.xcbt ) {
|
||||
pbData = dataStore.getData;
|
||||
// 添加调试信息
|
||||
console.log('初始获取的data数据:', tempPbData);
|
||||
|
||||
// 如果data没有数据,再从global获取
|
||||
if (!tempPbData || !tempPbData.xcbt) {
|
||||
tempPbData = dataStore.getGlobal;
|
||||
console.log('从global获取的数据:', tempPbData);
|
||||
}
|
||||
|
||||
// 检查数据是否有效
|
||||
if (!pbData || !pbData.xcbt ) {
|
||||
if (!tempPbData || !tempPbData.xcbt) {
|
||||
uni.showToast({
|
||||
title: '数据异常,请重新选择排班',
|
||||
icon: 'none'
|
||||
@ -134,41 +215,154 @@ onMounted(async () => {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是课程数据(包含kcmc字段)
|
||||
if (pbData.kcmc) {
|
||||
// 这是课程数据,需要重建排班数据
|
||||
// 从课程数据中提取排班相关字段
|
||||
const reconstructedPbData = {
|
||||
id: pbData.pbId, // 使用pbId作为排班ID
|
||||
status: 'A',
|
||||
njIds: pbData.njIds || '',
|
||||
bjIds: pbData.bjIds || '',
|
||||
xcbt: pbData.xcbt,
|
||||
xclx: pbData.xclx,
|
||||
xqId: pbData.xqId,
|
||||
xqmc: pbData.xqmc,
|
||||
xcstime: pbData.xcstime,
|
||||
xcjstime: pbData.xcjstime,
|
||||
pk: pbData.pk
|
||||
};
|
||||
dataStore.setGlobal(reconstructedPbData);
|
||||
pbData = reconstructedPbData;
|
||||
} else {
|
||||
// 这是排班数据,直接保存
|
||||
dataStore.setGlobal(pbData);
|
||||
|
||||
// 验证是否为排班数据(不是课程数据)
|
||||
if (tempPbData.kcmc) {
|
||||
console.warn('检测到课程数据,尝试从global获取排班数据');
|
||||
tempPbData = dataStore.getGlobal;
|
||||
if (!tempPbData || !tempPbData.xcbt) {
|
||||
uni.showToast({
|
||||
title: '排班数据丢失,请重新选择排班',
|
||||
icon: 'none'
|
||||
});
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await loadXcCourseList(pbData);
|
||||
// 检查是否是课程数据(包含kcmc字段)
|
||||
if (tempPbData.kcmc) {
|
||||
// 这是课程数据,需要重建排班数据
|
||||
// 从课程数据中提取排班相关字段
|
||||
// 确保使用正确的排班ID,优先使用pbId,如果不存在则使用id
|
||||
const pbId = tempPbData.pbId || tempPbData.id;
|
||||
const reconstructedPbData = {
|
||||
pbId: pbId, // 排班ID(统一使用pbId字段)
|
||||
status: 'A',
|
||||
njIds: tempPbData.njIds || '',
|
||||
bjIds: tempPbData.bjIds || '',
|
||||
xcbt: tempPbData.xcbt,
|
||||
xclx: tempPbData.xclx,
|
||||
xqId: tempPbData.xqId,
|
||||
xqmc: tempPbData.xqmc,
|
||||
xcstime: tempPbData.xcstime,
|
||||
xcjstime: tempPbData.xcjstime,
|
||||
pk: tempPbData.pk,
|
||||
pbLxId: tempPbData.pbLxId || '1165059210334048256' // 添加排班类型ID,如果不存在则使用默认值
|
||||
};
|
||||
dataStore.setGlobal(reconstructedPbData);
|
||||
pbData.value = reconstructedPbData;
|
||||
tempPbData = reconstructedPbData;
|
||||
} else {
|
||||
// 这是排班数据,需要添加pbId字段用于API调用
|
||||
// 确保有pbId字段,如果没有则从id字段创建
|
||||
const pbId = tempPbData.pbId || tempPbData.id;
|
||||
const pbDataWithPbId = {
|
||||
...tempPbData,
|
||||
pbId: pbId, // 使用pbId字段
|
||||
pbLxId: tempPbData.pbLxId || '1165059210334048256' // 添加排班类型ID,如果不存在则使用默认值
|
||||
};
|
||||
// 移除原始的id字段,只保留pbId
|
||||
delete pbDataWithPbId.id;
|
||||
dataStore.setGlobal(pbDataWithPbId);
|
||||
pbData.value = pbDataWithPbId;
|
||||
tempPbData = pbDataWithPbId;
|
||||
}
|
||||
|
||||
// 添加调试信息
|
||||
console.log('排班数据检查:', {
|
||||
tempPbData,
|
||||
pbId: tempPbData.pbId,
|
||||
pbLxId: tempPbData.pbLxId,
|
||||
hasKcmc: !!tempPbData.kcmc,
|
||||
allKeys: Object.keys(tempPbData),
|
||||
pbIdField: tempPbData.pbId,
|
||||
isFromXcPbList: !tempPbData.kcmc && tempPbData.xcbt // 判断是否来自排班列表
|
||||
});
|
||||
|
||||
await loadXcCourseList(tempPbData);
|
||||
|
||||
// 监听刷新事件
|
||||
uni.$on('refreshCourseList', async () => {
|
||||
console.log('收到刷新事件,重新加载课业辅导巡查课程列表');
|
||||
await refreshCourseList();
|
||||
});
|
||||
|
||||
// 监听静默刷新事件
|
||||
uni.$on('silentRefreshCourseList', async () => {
|
||||
console.log('收到静默刷新事件,重新加载课业辅导巡查课程列表');
|
||||
await silentRefreshCourseList();
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
});
|
||||
|
||||
// 刷新课程列表
|
||||
const refreshCourseList = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: "刷新中...",
|
||||
});
|
||||
|
||||
// 检查是否有排班数据
|
||||
const pbData = dataStore.getGlobal;
|
||||
|
||||
// 添加调试信息
|
||||
console.log('刷新时的排班数据:', {
|
||||
pbData,
|
||||
pbId: pbData?.pbId,
|
||||
pbIdField: pbData?.pbId
|
||||
});
|
||||
|
||||
if (pbData && pbData.pbId) {
|
||||
// 重新加载巡查课程列表
|
||||
await loadXcCourseList(pbData);
|
||||
}
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: "刷新成功",
|
||||
icon: "success",
|
||||
duration: 1000
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('刷新课程列表失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: "刷新失败",
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 静默刷新课程列表(不显示加载提示和成功提示)
|
||||
const silentRefreshCourseList = async () => {
|
||||
try {
|
||||
// 检查是否有排班数据
|
||||
const pbData = dataStore.getGlobal;
|
||||
|
||||
// 添加调试信息
|
||||
console.log('静默刷新时的排班数据:', {
|
||||
pbData,
|
||||
pbId: pbData?.pbId,
|
||||
pbIdField: pbData?.pbId
|
||||
});
|
||||
|
||||
if (pbData && pbData.pbId) {
|
||||
// 重新加载巡查课程列表,不显示加载提示
|
||||
await loadXcCourseList(pbData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('静默刷新课程列表失败:', error);
|
||||
// 静默刷新失败也不显示错误提示,避免打扰用户
|
||||
}
|
||||
};
|
||||
|
||||
// 加载巡查课程列表
|
||||
const loadXcCourseList = async (pbData: any) => {
|
||||
try {
|
||||
// 检查pbData是否有效
|
||||
if (!pbData || !pbData.id) {
|
||||
if (!pbData || !pbData.pbId) {
|
||||
uni.showToast({
|
||||
title: '排班数据无效,请重新选择',
|
||||
icon: 'none'
|
||||
@ -176,24 +370,47 @@ const loadXcCourseList = async (pbData: any) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取排班ID(使用pbId字段)
|
||||
const pbId = pbData.pbId;
|
||||
|
||||
// 添加调试信息
|
||||
console.log('API调用参数:', {
|
||||
jsId: getJs.id,
|
||||
pbId: pbId,
|
||||
pbData: pbData
|
||||
});
|
||||
|
||||
const res = await getKyXcCourseListApi({
|
||||
jsId: getJs.id,
|
||||
pbId: pbData.id
|
||||
pbId: pbId
|
||||
});
|
||||
|
||||
if (res && res.resultCode == 1) {
|
||||
const list = res.result || [];
|
||||
xkkcList.value = list.map((item: any) => ({
|
||||
|
||||
// 先映射数据,然后进行时间过滤
|
||||
let mappedList = list.map((item: any) => ({
|
||||
id: item.id, // 课程记录ID
|
||||
pbId: item.pbId || pbData.id, // 排班ID
|
||||
pbId: pbId, // 排班ID
|
||||
kcmc: item.kmmc || '课业辅导',
|
||||
gradeName: item.bc && item.njmc ? `${item.bc}(${item.njmc})` : (item.njmc || '暂无'),
|
||||
bjmc: item.bjmc || '暂无',
|
||||
// 添加年级和班级ID字段
|
||||
njId: item.njId || '',
|
||||
njmcId: item.njmcId || '',
|
||||
bjId: item.bjId || ''
|
||||
bjId: item.bjId || '',
|
||||
// 添加巡查时间字段
|
||||
skzqlx: item.skzqlx || '',
|
||||
skzq: item.skzq || '',
|
||||
// 添加巡查状态字段
|
||||
sfxc: item.sfxc || '否',
|
||||
// 添加巡查开始和结束时间
|
||||
xcstime: item.xcstime || '',
|
||||
xcjstime: item.xcjstime || ''
|
||||
}));
|
||||
|
||||
// 过滤出当前时间符合的课程
|
||||
xkkcList.value = mappedList.filter((xkkc: any) => isCurrentTimeMatch(xkkc));
|
||||
} else {
|
||||
xkkcList.value = [];
|
||||
uni.showToast({
|
||||
@ -252,8 +469,12 @@ function switchXk(xk: any) {
|
||||
|
||||
// 跳转到巡查
|
||||
const goXc = (xkkc: any) => {
|
||||
// 直接从global获取排班数据
|
||||
const pbData = dataStore.getGlobal;
|
||||
// 优先从global获取排班数据,如果没有则从data获取
|
||||
let pbData = dataStore.getGlobal;
|
||||
|
||||
if (!pbData || !pbData.xcbt ) {
|
||||
pbData = dataStore.getData;
|
||||
}
|
||||
|
||||
// 检查排班数据是否有效
|
||||
if (!pbData || !pbData.xcbt ) {
|
||||
@ -267,17 +488,29 @@ const goXc = (xkkc: any) => {
|
||||
// 合并排班数据和课程数据
|
||||
const combinedData = {
|
||||
...xkkc,
|
||||
id: xkkc.id, // 课程记录ID
|
||||
pbId: pbData.id, // 排班ID - 确保使用正确的排班ID
|
||||
pbJsId: xkkc.id, // 将课程记录ID作为pbJsId传递
|
||||
pbId: pbData.pbId, // 排班ID - 优先使用pb_id字段
|
||||
pbNjBjId: xkkc.id, // 将课程记录ID作为pbNjBjId传递
|
||||
pbLxId: pbData.pbLxId || '', // 传递排班类型ID
|
||||
xclx: pbData.xclx,
|
||||
xcbt: pbData.xcbt,
|
||||
xqmc: pbData.xqmc,
|
||||
// 确保传递年级和班级ID
|
||||
njId: xkkc.njId || '',
|
||||
njmcId: xkkc.njmcId || '',
|
||||
bjId: xkkc.bjId || ''
|
||||
bjId: xkkc.bjId || '',
|
||||
// 传递巡查时间
|
||||
xcstime: xkkc.xcstime || '',
|
||||
xcjstime: xkkc.xcjstime || ''
|
||||
};
|
||||
|
||||
// 添加调试信息
|
||||
console.log('点击巡查,传递数据:', {
|
||||
pbLxId: combinedData.pbLxId,
|
||||
pbId: combinedData.pbId,
|
||||
pbNjBjId: combinedData.pbNjBjId,
|
||||
pbData: pbData,
|
||||
xkkc: xkkc
|
||||
});
|
||||
|
||||
// 使用不同的存储键来避免覆盖排班数据
|
||||
dataStore.setData(combinedData); // 存储课程数据
|
||||
@ -285,7 +518,7 @@ const goXc = (xkkc: any) => {
|
||||
// 确保保存到global的是原始排班数据,而不是课程数据
|
||||
// 从combinedData中提取排班相关字段
|
||||
const originalPbData = {
|
||||
id: pbData.id, // 排班ID
|
||||
pbId: pbData.pbId, // 排班ID(统一使用pbId字段)
|
||||
status: pbData.status,
|
||||
njIds: pbData.njIds,
|
||||
bjIds: pbData.bjIds,
|
||||
@ -295,7 +528,8 @@ const goXc = (xkkc: any) => {
|
||||
xqmc: pbData.xqmc,
|
||||
xcstime: pbData.xcstime,
|
||||
xcjstime: pbData.xcjstime,
|
||||
pk: pbData.pk
|
||||
pk: pbData.pk,
|
||||
pbLxId: pbData.pbLxId || '' // 添加排班类型ID
|
||||
};
|
||||
dataStore.setGlobal(originalPbData); // 保存原始排班数据到global
|
||||
|
||||
@ -306,8 +540,12 @@ const goXc = (xkkc: any) => {
|
||||
|
||||
// 跳转到巡查记录
|
||||
const goRecord = (xkkc: any) => {
|
||||
// 直接从global获取排班数据
|
||||
const pbData = dataStore.getGlobal;
|
||||
// 优先从global获取排班数据,如果没有则从data获取
|
||||
let pbData = dataStore.getGlobal;
|
||||
|
||||
if (!pbData || !pbData.xcbt ) {
|
||||
pbData = dataStore.getData;
|
||||
}
|
||||
|
||||
// 检查排班数据是否有效
|
||||
if (!pbData || !pbData.xcbt ) {
|
||||
@ -321,8 +559,8 @@ const goRecord = (xkkc: any) => {
|
||||
// 合并排班数据和课程数据
|
||||
const combinedData = {
|
||||
...xkkc,
|
||||
id: xkkc.id, // 课程记录ID
|
||||
pbId: pbData.id, // 排班ID - 确保使用正确的排班ID
|
||||
pbId: pbData.pbId, // 排班ID - 优先使用pb_id字段
|
||||
pbLxId: pbData.pbLxId || '', // 传递排班类型ID
|
||||
xclx: pbData.xclx,
|
||||
xcbt: pbData.xcbt,
|
||||
xqmc: pbData.xqmc,
|
||||
@ -331,6 +569,24 @@ const goRecord = (xkkc: any) => {
|
||||
njmcId: xkkc.njmcId || '',
|
||||
bjId: xkkc.bjId || ''
|
||||
};
|
||||
|
||||
// 确保保存到global的是原始排班数据,而不是课程数据
|
||||
const originalPbData = {
|
||||
pbId: pbData.pbId, // 排班ID(统一使用pbId字段)
|
||||
status: pbData.status,
|
||||
njIds: pbData.njIds,
|
||||
bjIds: pbData.bjIds,
|
||||
xcbt: pbData.xcbt,
|
||||
xclx: pbData.xclx,
|
||||
xqId: pbData.xqId,
|
||||
xqmc: pbData.xqmc,
|
||||
xcstime: pbData.xcstime,
|
||||
xcjstime: pbData.xcjstime,
|
||||
pk: pbData.pk,
|
||||
pbLxId: pbData.pbLxId || '' // 添加排班类型ID
|
||||
};
|
||||
dataStore.setGlobal(originalPbData); // 保存原始排班数据到global
|
||||
|
||||
dataStore.setData(combinedData);
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/kefuxuncha/kyRecord`,
|
||||
@ -338,8 +594,12 @@ const goRecord = (xkkc: any) => {
|
||||
};
|
||||
|
||||
|
||||
// 页面卸载前清除定时器
|
||||
onBeforeUnmount(() => {});
|
||||
// 页面卸载前清除定时器和事件监听器
|
||||
onBeforeUnmount(() => {
|
||||
// 移除事件监听器
|
||||
uni.$off('refreshCourseList');
|
||||
uni.$off('silentRefreshCourseList');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -450,6 +710,30 @@ onBeforeUnmount(() => {});
|
||||
box-shadow: 0 4px 20px rgba(63, 191, 114, 0.15);
|
||||
}
|
||||
|
||||
.course-status {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
z-index: 2;
|
||||
animation: fadeInRight 0.5s ease-out 0.2s both;
|
||||
|
||||
&.status-done {
|
||||
background: linear-gradient(135deg, #67c23a, #85ce61);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(103, 194, 58, 0.3);
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
background: linear-gradient(135deg, #e6a23c, #f0c78a);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(230, 162, 60, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.course-name {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
@ -457,6 +741,7 @@ onBeforeUnmount(() => {});
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.4;
|
||||
animation: fadeInLeft 0.5s ease-out 0.1s both;
|
||||
padding-right: 80px; // 为状态标识留出空间
|
||||
}
|
||||
|
||||
.course-btn-group {
|
||||
@ -521,19 +806,21 @@ onBeforeUnmount(() => {});
|
||||
display: flex;
|
||||
margin-bottom: 14px;
|
||||
font-size: 13px;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
animation: fadeInUp 0.5s ease-out 0.15s both;
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
flex: 0 0 100px;
|
||||
flex: 0 0 auto;
|
||||
font-weight: 500;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.info-data {
|
||||
flex: 1 0 1px;
|
||||
color: #333;
|
||||
font-weight: 400;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,36 +8,37 @@
|
||||
<view class="course-icon flex-center mr-10">
|
||||
<u-icon name="calendar" color="#4080ff" size="20"></u-icon>
|
||||
</view>
|
||||
<text class="font-16 font-bold">{{ xkkc.kcmc }}</text>
|
||||
<text class="font-16 font-bold">{{ getGradeClassDisplay() }}</text>
|
||||
<text class="font-14 cor-999 ml-10"
|
||||
>{{ todayInfo.date }} ({{ todayInfo.weekName }})</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- 课业辅导信息 -->
|
||||
<view class="course-time-info">
|
||||
<view class="time-item">
|
||||
<view class="time-label">年级:</view>
|
||||
<view class="time-value">{{ xkkc.gradeName || '暂无' }}</view>
|
||||
</view>
|
||||
<view class="time-item" v-if="xkkc.bjmc">
|
||||
<view class="time-label">班级:</view>
|
||||
<view class="time-value">{{ xkkc.bjmc || '暂无' }}</view>
|
||||
</view>
|
||||
<view class="time-item">
|
||||
<view class="time-label">巡查类型:</view>
|
||||
<view class="time-value">{{ xkkc.xclx === 'B' ? '课业辅导巡查' : '课程巡查' }}</view>
|
||||
</view>
|
||||
<view class="time-item">
|
||||
<view class="time-label">排班标题:</view>
|
||||
<view class="time-value">{{ xkkc.xcbt || '暂无' }}</view>
|
||||
</view>
|
||||
<view class="time-item">
|
||||
<view class="time-label">学期:</view>
|
||||
<view class="time-value">{{ xkkc.xqmc || '暂无' }}</view>
|
||||
<!-- 任课老师选择 -->
|
||||
<view class="teacher-selection-info">
|
||||
<view class="teacher-item">
|
||||
<view class="teacher-label">任课老师:</view>
|
||||
<view class="teacher-picker-container">
|
||||
<BasicJsPicker
|
||||
:defaultValue="selectedTeacherId"
|
||||
:multiple="false"
|
||||
@change="onTeacherChange"
|
||||
placeholder="请选择任课老师"
|
||||
ref="teacherPickerRef"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 巡查时间信息 -->
|
||||
<view class="inspection-time-info" v-if="xkkc.xcstime && xkkc.xcjstime">
|
||||
<view class="time-item">
|
||||
<u-icon name="clock" color="#4080ff" size="16"></u-icon>
|
||||
<text class="time-label">巡查时间:</text>
|
||||
<text class="time-value">{{ xkkc.xcstime }} - {{ xkkc.xcjstime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 巡查时间状态 -->
|
||||
<view class="inspection-status" v-if="!canInspect">
|
||||
<u-icon name="clock" color="#ff9900" size="16"></u-icon>
|
||||
@ -112,94 +113,29 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拍照上传 -->
|
||||
<view class="section mx-15 mb-15">
|
||||
<view class="section-title-bar">
|
||||
<view class="decorator"></view>
|
||||
<text class="title-text">拍照上传</text>
|
||||
</view>
|
||||
<view class="upload-card bg-white r-md p-15">
|
||||
<view class="upload-section">
|
||||
<view class="upload-list">
|
||||
<view
|
||||
v-for="(image, index) in imageList"
|
||||
:key="index"
|
||||
class="upload-item"
|
||||
>
|
||||
<image
|
||||
:src="image.url ? imagUrl(image.url) : image.tempPath"
|
||||
mode="aspectFill"
|
||||
class="upload-image"
|
||||
@click="previewImage(index)"
|
||||
/>
|
||||
<view class="upload-delete" @click="deleteImage(index)">
|
||||
<text class="delete-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="imageList.length < 5"
|
||||
class="upload-add"
|
||||
@click="chooseImage"
|
||||
>
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拍视频上传 -->
|
||||
<!-- 图片视频上传组件 -->
|
||||
<view class="section mx-15 mb-30">
|
||||
<view class="section-title-bar">
|
||||
<view class="decorator"></view>
|
||||
<text class="title-text">拍视频上传</text>
|
||||
<text class="title-text">图片视频上传</text>
|
||||
</view>
|
||||
<view class="upload-card bg-white r-md p-15">
|
||||
<view class="upload-section">
|
||||
<view class="upload-list">
|
||||
<view
|
||||
v-for="(video, index) in videoList"
|
||||
:key="index"
|
||||
class="upload-item"
|
||||
>
|
||||
<video
|
||||
:src="video.url ? video.url : video.tempPath"
|
||||
class="upload-video"
|
||||
:controls="false"
|
||||
:show-center-play-btn="false"
|
||||
:show-play-btn="false"
|
||||
:show-fullscreen-btn="false"
|
||||
:show-progress="false"
|
||||
:show-mute-btn="false"
|
||||
:enable-progress-gesture="false"
|
||||
:enable-play-gesture="false"
|
||||
:loop="false"
|
||||
:muted="true"
|
||||
:poster="''"
|
||||
></video>
|
||||
<view class="video-play-icon">
|
||||
<u-icon name="play-right-fill" color="#fff" size="20"></u-icon>
|
||||
</view>
|
||||
<view class="upload-delete" @click="deleteVideo(index)">
|
||||
<text class="delete-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="videoList.length < 3"
|
||||
class="upload-add"
|
||||
@click="chooseVideo"
|
||||
>
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加视频</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<ImageVideoUpload
|
||||
v-model:image-list="imageList"
|
||||
v-model:video-list="videoList"
|
||||
:max-image-count="5"
|
||||
:max-video-count="3"
|
||||
:compress-config="compressConfig"
|
||||
:upload-api="attachmentUpload"
|
||||
@image-upload-success="onImageUploadSuccess"
|
||||
@video-upload-success="onVideoUploadSuccess"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<template #bottom>
|
||||
<view
|
||||
v-if="canInspect"
|
||||
@ -223,6 +159,8 @@ import { xcXmFindByXcLxApi } from "@/api/base/xcXmApi";
|
||||
import { kyXcSaveApi } from "@/api/base/kyXcApi";
|
||||
import { attachmentUpload } from "@/api/system/upload";
|
||||
import BasicLayout from "@/components/BasicLayout/Layout.vue";
|
||||
import BasicJsPicker from "@/components/BasicJsPicker/Picker.vue";
|
||||
import { ImageVideoUpload } from "@/components/ImageVideoUpload";
|
||||
import { useDataStore } from "@/store/modules/data";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
@ -236,6 +174,30 @@ const { getData } = useDataStore();
|
||||
const js = computed(() => getJs);
|
||||
const xkkc = computed(() => getData);
|
||||
|
||||
// 获取年级班级显示文本
|
||||
const getGradeClassDisplay = () => {
|
||||
const gradeName = xkkc.value.gradeName || '';
|
||||
const bjmc = xkkc.value.bjmc || '';
|
||||
|
||||
// 构建年级班级信息
|
||||
let displayText = '';
|
||||
|
||||
if (gradeName) {
|
||||
displayText = gradeName;
|
||||
}
|
||||
|
||||
if (bjmc && bjmc !== '暂无') {
|
||||
if (displayText) {
|
||||
displayText += bjmc;
|
||||
} else {
|
||||
displayText = bjmc;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有年级班级信息,显示默认文本
|
||||
return displayText || '暂无信息';
|
||||
};
|
||||
|
||||
const now = dayjs();
|
||||
let wDay = now.day();
|
||||
if (wDay === 0) {
|
||||
@ -250,19 +212,11 @@ const todayInfo = ref({
|
||||
// 巡查项目
|
||||
const checkItems = ref<any[]>([]);
|
||||
|
||||
// 图片列表 - 修改为包含临时路径和服务器路径的对象
|
||||
interface ImageItem {
|
||||
tempPath?: string; // 临时路径(用于预览)
|
||||
url?: string; // 服务器路径(上传成功后)
|
||||
name?: string; // 文件名
|
||||
}
|
||||
// 导入类型和配置
|
||||
import { type ImageItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
|
||||
|
||||
// 视频列表 - 修改为包含临时路径和服务器路径的对象
|
||||
interface VideoItem {
|
||||
tempPath?: string; // 临时路径(用于预览)
|
||||
url?: string; // 服务器路径(上传成功后)
|
||||
name?: string; // 文件名
|
||||
}
|
||||
// 压缩配置
|
||||
const compressConfig = ref(COMPRESS_PRESETS.medium)
|
||||
|
||||
const imageList = ref<ImageItem[]>([]);
|
||||
const videoList = ref<VideoItem[]>([]);
|
||||
@ -274,6 +228,11 @@ const canInspect = ref(true); // 是否可以巡查
|
||||
// 提交状态控制
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
// 任课老师选择相关
|
||||
const selectedTeacher = ref<any>(null);
|
||||
const selectedTeacherId = ref<string>('');
|
||||
const teacherPickerRef = ref();
|
||||
|
||||
// 加载巡查项目
|
||||
const loadCheckItems = async () => {
|
||||
try {
|
||||
@ -302,140 +261,13 @@ const onCheckItemChange = (e: any, item: any) => {
|
||||
item.checked = e.detail.value === "A";
|
||||
};
|
||||
|
||||
// 选择图片
|
||||
const chooseImage = () => {
|
||||
uni.chooseImage({
|
||||
count: 5 - imageList.value.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
// 添加临时图片到列表
|
||||
const tempFilePaths = res.tempFilePaths as string[];
|
||||
const newImages = tempFilePaths.map((path: string) => ({
|
||||
tempPath: path,
|
||||
name: path.split('/').pop() || 'image.jpg'
|
||||
}));
|
||||
imageList.value = [...imageList.value, ...newImages];
|
||||
// 自动上传图片
|
||||
await uploadImages(newImages);
|
||||
}
|
||||
});
|
||||
// 上传成功回调
|
||||
const onImageUploadSuccess = (image: ImageItem, index: number) => {
|
||||
console.log('图片上传成功:', image, index);
|
||||
};
|
||||
|
||||
// 选择视频
|
||||
const chooseVideo = () => {
|
||||
uni.chooseVideo({
|
||||
sourceType: ['album', 'camera'],
|
||||
maxDuration: 60,
|
||||
camera: 'back',
|
||||
success: async (res) => {
|
||||
// 添加临时视频到列表
|
||||
const tempFilePath = res.tempFilePath;
|
||||
const newVideo = {
|
||||
tempPath: tempFilePath,
|
||||
name: tempFilePath.split('/').pop() || 'video.mp4'
|
||||
};
|
||||
videoList.value = [...videoList.value, newVideo];
|
||||
// 自动上传视频
|
||||
await uploadVideos([newVideo]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 上传图片到服务器
|
||||
const uploadImages = async (images: ImageItem[]) => {
|
||||
try {
|
||||
showLoading('上传图片中...');
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const image = images[i];
|
||||
if (image.tempPath) {
|
||||
try {
|
||||
// 调用上传接口
|
||||
const uploadResult: any = await attachmentUpload(image.tempPath as any);
|
||||
|
||||
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
|
||||
// 保存服务器返回的文件路径
|
||||
const serverPath = uploadResult.result[0].filePath;
|
||||
|
||||
// 更新图片对象,保存服务器路径
|
||||
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
|
||||
if (index !== -1) {
|
||||
imageList.value[index].url = serverPath;
|
||||
// 可以删除临时路径以节省内存
|
||||
delete imageList.value[index].tempPath;
|
||||
}
|
||||
} else {
|
||||
throw new Error('上传响应格式异常');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('图片上传失败:', error);
|
||||
showToast({ title: `${image.name || '图片'}上传失败`, icon: 'none' });
|
||||
|
||||
// 从列表中移除上传失败的图片
|
||||
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
|
||||
if (index !== -1) {
|
||||
imageList.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
showToast({ title: '图片上传完成', icon: 'success' });
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
console.error('批量上传图片失败:', error);
|
||||
showToast({ title: '图片上传失败,请重试', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 上传视频到服务器
|
||||
const uploadVideos = async (videos: VideoItem[]) => {
|
||||
try {
|
||||
showLoading('上传视频中...');
|
||||
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
const video = videos[i];
|
||||
if (video.tempPath) {
|
||||
try {
|
||||
// 调用上传接口
|
||||
const uploadResult: any = await attachmentUpload(video.tempPath as any);
|
||||
|
||||
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
|
||||
// 保存服务器返回的文件路径
|
||||
const serverPath = uploadResult.result[0].filePath;
|
||||
|
||||
// 更新视频对象,保存服务器路径
|
||||
const index = videoList.value.findIndex(v => v.tempPath === video.tempPath);
|
||||
if (index !== -1) {
|
||||
videoList.value[index].url = serverPath;
|
||||
// 可以删除临时路径以节省内存
|
||||
delete videoList.value[index].tempPath;
|
||||
}
|
||||
} else {
|
||||
throw new Error('上传响应格式异常');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('视频上传失败:', error);
|
||||
showToast({ title: `${video.name || '视频'}上传失败`, icon: 'none' });
|
||||
|
||||
// 从列表中移除上传失败的视频
|
||||
const index = videoList.value.findIndex(v => v.tempPath === video.tempPath);
|
||||
if (index !== -1) {
|
||||
videoList.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
showToast({ title: '视频上传完成', icon: 'success' });
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
console.error('批量上传视频失败:', error);
|
||||
showToast({ title: '视频上传失败,请重试', icon: 'none' });
|
||||
}
|
||||
const onVideoUploadSuccess = (video: VideoItem, index: number) => {
|
||||
console.log('视频上传成功:', video, index);
|
||||
};
|
||||
|
||||
// 预览图片
|
||||
@ -450,21 +282,71 @@ const previewImage = (index: number) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const deleteImage = (index: number) => {
|
||||
imageList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 删除视频
|
||||
const deleteVideo = (index: number) => {
|
||||
videoList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 检查巡查时间 - 课业辅导巡查不需要时间验证
|
||||
// 检查巡查时间 - 验证当前时间是否在巡查时间范围内
|
||||
const checkInspectionTime = () => {
|
||||
// 课业辅导巡查直接允许巡查,不需要时间验证
|
||||
canInspect.value = true;
|
||||
inspectionStatusText.value = "可以巡查";
|
||||
const xcstime = xkkc.value.xcstime;
|
||||
const xcjstime = xkkc.value.xcjstime;
|
||||
|
||||
// 如果没有设置巡查时间,直接允许巡查
|
||||
if (!xcstime || !xcjstime) {
|
||||
canInspect.value = true;
|
||||
inspectionStatusText.value = "可以巡查";
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前时间
|
||||
const now = dayjs();
|
||||
const currentTime = now.format("HH:mm:ss");
|
||||
|
||||
// 将时间字符串转换为可比较的格式
|
||||
const currentTimeMinutes = timeToMinutes(currentTime);
|
||||
const startTimeMinutes = timeToMinutes(xcstime);
|
||||
const endTimeMinutes = timeToMinutes(xcjstime);
|
||||
|
||||
// 检查当前时间是否在巡查时间范围内
|
||||
if (currentTimeMinutes < startTimeMinutes) {
|
||||
canInspect.value = false;
|
||||
inspectionStatusText.value = `还未到巡查时间,无法巡查(巡查时间:${xcstime} - ${xcjstime})`;
|
||||
} else if (currentTimeMinutes > endTimeMinutes) {
|
||||
canInspect.value = false;
|
||||
inspectionStatusText.value = `巡查时间已结束,无法巡查(巡查时间:${xcstime} - ${xcjstime})`;
|
||||
} else {
|
||||
canInspect.value = true;
|
||||
inspectionStatusText.value = `可以巡查(巡查时间:${xcstime} - ${xcjstime})`;
|
||||
}
|
||||
};
|
||||
|
||||
// 将时间字符串转换为分钟数,便于比较
|
||||
const timeToMinutes = (timeStr: string) => {
|
||||
if (!timeStr || !timeStr.includes(':')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const parts = timeStr.split(':');
|
||||
if (parts.length !== 3) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const hours = parseInt(parts[0], 10) || 0;
|
||||
const minutes = parseInt(parts[1], 10) || 0;
|
||||
const seconds = parseInt(parts[2], 10) || 0;
|
||||
|
||||
return hours * 60 + minutes + seconds / 60;
|
||||
};
|
||||
|
||||
// 任课老师选择变化回调
|
||||
const onTeacherChange = (teacher: any) => {
|
||||
console.log('任课老师选择变化:', teacher);
|
||||
if (teacher) {
|
||||
selectedTeacher.value = teacher;
|
||||
selectedTeacherId.value = teacher.id || teacher.value;
|
||||
console.log('选中的老师:', selectedTeacher.value);
|
||||
console.log('选中的老师ID:', selectedTeacherId.value);
|
||||
} else {
|
||||
selectedTeacher.value = null;
|
||||
selectedTeacherId.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 提交数据
|
||||
@ -479,6 +361,9 @@ const submit = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 重新检查巡查时间(防止时间在页面加载后发生变化)
|
||||
checkInspectionTime();
|
||||
|
||||
if (!canInspect.value) {
|
||||
uni.showToast({
|
||||
title: inspectionStatusText.value,
|
||||
@ -499,6 +384,20 @@ const submit = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证任课老师是否已选择
|
||||
console.log('提交时检查任课老师:', {
|
||||
selectedTeacher: selectedTeacher.value,
|
||||
selectedTeacherId: selectedTeacherId.value
|
||||
});
|
||||
if (!selectedTeacher.value && !selectedTeacherId.value) {
|
||||
uni.showToast({
|
||||
title: "请选择任课老师",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置提交状态
|
||||
isSubmitting.value = true;
|
||||
|
||||
@ -521,13 +420,22 @@ const submit = async () => {
|
||||
njId: xkkc.value.njId || '', // 确保年级ID不为空
|
||||
njmcId: xkkc.value.njmcId || '', // 确保年级名称ID不为空
|
||||
bjId: xkkc.value.bjId || '', // 确保班级ID不为空
|
||||
pbJsId: xkkc.value.pbJsId || xkkc.value.id, // 使用传递的pbJsId或课程记录ID
|
||||
pbNjBjId: xkkc.value.pbNjBjId || xkkc.value.id, // 使用传递的pbNjBjId或课程记录ID
|
||||
pbLxId: xkkc.value.pbLxId || '', // 添加排班类型ID
|
||||
rkjsId: selectedTeacherId.value, // 添加任课老师ID
|
||||
xctime: now.format("YYYY-MM-DD HH:mm:ss"),
|
||||
zp: getImageUrls(),
|
||||
sp: getVideoUrls(),
|
||||
kyXcXmList: xkXcXmList, // 添加巡查项目列表
|
||||
};
|
||||
|
||||
// 添加调试信息
|
||||
console.log('提交巡查数据:', {
|
||||
pbLxId: submitData.pbLxId,
|
||||
pbNjBjId: submitData.pbNjBjId,
|
||||
xkkcData: xkkc.value
|
||||
});
|
||||
|
||||
const res = await kyXcSaveApi(submitData);
|
||||
|
||||
if (res && res.resultCode === 1) {
|
||||
@ -536,7 +444,12 @@ const submit = async () => {
|
||||
icon: "success",
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
uni.navigateBack({
|
||||
success: () => {
|
||||
// 发送静默更新事件给kyXkList页面,不显示刷新提示
|
||||
uni.$emit('silentRefreshCourseList');
|
||||
}
|
||||
});
|
||||
}, 1500);
|
||||
} else {
|
||||
uni.showToast({
|
||||
@ -611,27 +524,53 @@ onMounted(async () => {
|
||||
background-color: rgba(64, 128, 255, 0.1);
|
||||
}
|
||||
|
||||
.course-time-info {
|
||||
.teacher-selection-info {
|
||||
margin-top: 15px;
|
||||
padding: 10px 15px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #eee;
|
||||
|
||||
.time-item {
|
||||
.teacher-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
align-items: center;
|
||||
|
||||
.teacher-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.teacher-picker-container {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inspection-time-info {
|
||||
margin-top: 15px;
|
||||
padding: 10px 15px;
|
||||
background-color: #f0f8ff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d6e4ff;
|
||||
|
||||
.time-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.time-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
color: #4080ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<view class="title-section">
|
||||
<view class="title">
|
||||
<text v-if="pbList && pbList.length > 0">
|
||||
{{ getCurrentSemesterName() }} - 选课排班
|
||||
{{ getCurrentSemesterName() }} - 教学巡查
|
||||
</text>
|
||||
<text v-else>暂无排班数据</text>
|
||||
</view>
|
||||
@ -15,7 +15,15 @@
|
||||
</view>
|
||||
|
||||
<!-- 可滚动的内容区域 -->
|
||||
<view class="scrollable-content">
|
||||
<scroll-view
|
||||
class="scrollable-content"
|
||||
scroll-y
|
||||
@scrolltolower="onScrollToLower"
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
refresher-background="#f8fafc"
|
||||
>
|
||||
<!-- 排班列表 -->
|
||||
<view class="course-list" v-if="pbList && pbList.length > 0">
|
||||
<view v-for="(pb, index) in pbList" :key="pb.id || index" class="course-item">
|
||||
@ -49,13 +57,18 @@
|
||||
</view>
|
||||
<view class="empty-text">暂无排班数据</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" v-if="hasMore && pbList.length > 0">
|
||||
<u-loading-icon v-if="loading" mode="spinner" size="20"></u-loading-icon>
|
||||
<text v-else @click="loadMore">加载更多</text>
|
||||
</view>
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" v-if="hasMore && pbList.length > 0">
|
||||
<u-loading-icon v-if="loading" mode="spinner" size="20"></u-loading-icon>
|
||||
<text v-else>加载更多</text>
|
||||
</view>
|
||||
|
||||
<!-- 没有更多数据提示 -->
|
||||
<view class="no-more" v-if="!hasMore && pbList.length > 0">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
@ -79,6 +92,7 @@ const { getData, setData } = useDataStore();
|
||||
const pbList = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const hasMore = ref(true);
|
||||
const refreshing = ref(false);
|
||||
|
||||
// 分页参数
|
||||
const pagination = reactive({
|
||||
@ -150,6 +164,7 @@ const loadPbList = async (isRefresh = false) => {
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
refreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -161,8 +176,17 @@ const loadMore = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 滚动到底部时加载更多
|
||||
const onScrollToLower = () => {
|
||||
if (hasMore.value && !loading.value) {
|
||||
pagination.page++;
|
||||
loadPbList();
|
||||
}
|
||||
};
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = () => {
|
||||
refreshing.value = true;
|
||||
loadPbList(true);
|
||||
};
|
||||
|
||||
@ -222,7 +246,14 @@ const goXc = (pb: any) => {
|
||||
console.log('pb.id:', pb?.id);
|
||||
console.log('pb的所有属性:', Object.keys(pb || {}));
|
||||
|
||||
setData(pb);
|
||||
// 确保数据一致性,只使用pbId字段
|
||||
const pbData = {
|
||||
...pb,
|
||||
pbId: pb.id, // 将id字段作为pbId字段
|
||||
};
|
||||
delete pbData.id; // 移除原始的id字段
|
||||
|
||||
setData(pbData);
|
||||
if (pb.xclx === 'A') {
|
||||
// 课程巡查,跳转到课程巡查列表
|
||||
uni.navigateTo({
|
||||
@ -448,6 +479,20 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
// 没有更多数据样式
|
||||
.no-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
|
||||
text {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加动画关键帧
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
|
||||
@ -7,10 +7,18 @@
|
||||
style="position: absolute"
|
||||
>
|
||||
<template v-slot="{ data, index }">
|
||||
<view class="inspection-record bg-white r-md p-15 mb-15">
|
||||
<view class="inspection-record bg-white r-md p-15 mb-15 timeline-item">
|
||||
<!-- 时间轴连接线 -->
|
||||
<view class="timeline-line" v-if="index < 10"></view>
|
||||
|
||||
<!-- 时间轴节点 -->
|
||||
<view class="timeline-dot">
|
||||
<view class="dot-inner"></view>
|
||||
</view>
|
||||
|
||||
<view class="record-header">
|
||||
<view class="record-time">
|
||||
<u-icon name="clock" color="#666" size="14"></u-icon>
|
||||
<u-icon name="clock" color="#4080ff" size="16"></u-icon>
|
||||
<text class="time-text">{{ formatTime(data.xctime) }}</text>
|
||||
</view>
|
||||
<view class="record-status">
|
||||
@ -21,6 +29,12 @@
|
||||
<view class="content-item">
|
||||
<text class="item-label">巡查教师:</text>
|
||||
<text class="item-value">{{ data.jsxm }}</text>
|
||||
<text class="item-label" style="margin-left: 20px;">代课教师:</text>
|
||||
<text class="item-value">{{ data.dkjsxm || '无' }}</text>
|
||||
</view>
|
||||
<view class="content-item">
|
||||
<text class="item-label">点名时间:</text>
|
||||
<text class="item-value">{{ data.dmTime ? formatTime(data.dmTime) : '无' }}</text>
|
||||
</view>
|
||||
<view class="content-item flex-col">
|
||||
<text class="item-label" style="flex: 0 0 25px"
|
||||
@ -36,20 +50,25 @@
|
||||
style="margin-bottom: 4px"
|
||||
>
|
||||
<view>
|
||||
<text>{{ idx + 1 }}、{{ xm.xcMc }}</text>
|
||||
<view
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 4px 0;
|
||||
"
|
||||
>
|
||||
<text>分值:{{ xm.xmFz }}分</text>
|
||||
<text
|
||||
>巡查结果:{{
|
||||
xm.xcJg === "A" ? "有" : "无"
|
||||
}}</text
|
||||
>
|
||||
<view style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
|
||||
<view style="display: flex; align-items: center; flex: 1;">
|
||||
<text style="margin-right: 8px;">{{ idx + 1 }}、{{ xm.xcMc }}</text>
|
||||
<view style="display: flex; align-items: center;">
|
||||
<u-icon
|
||||
:name="xm.xcJg === 'B' ? 'checkmark-circle-fill' : 'close-circle-fill'"
|
||||
:color="xm.xcJg === 'B' ? '#67c23a' : '#f56c6c'"
|
||||
size="18"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view style="font-size: 12px; color: #666;">
|
||||
<text v-if="xm.xcJg === 'A'" style="color: #f56c6c;">
|
||||
扣分:-{{ xm.xmFz }}分
|
||||
</text>
|
||||
<text v-else style="color: #67c23a;">
|
||||
不扣分
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -173,9 +192,11 @@ const xkkc = computed(() => getData);
|
||||
let inspectionParams = ref({
|
||||
rows: 10,
|
||||
xkkcId: xkkc.value.id,
|
||||
jsId: js.value.id,
|
||||
});
|
||||
|
||||
// 添加调试信息
|
||||
console.log('巡查记录查询参数:', inspectionParams.value);
|
||||
|
||||
// 巡查记录列表
|
||||
const [registerInspection, { reload }] = useLayout({
|
||||
api: xkXcFindPageApi,
|
||||
@ -186,9 +207,10 @@ const [registerInspection, { reload }] = useLayout({
|
||||
// 图片预览
|
||||
const handlePreviewImage = (img: string, images: string[]) => {
|
||||
// 兼容uni-app的图片预览API
|
||||
const processedImages = images.map(image => imagUrl(image));
|
||||
uni.previewImage({
|
||||
current: img,
|
||||
urls: images,
|
||||
current: imagUrl(img),
|
||||
urls: processedImages,
|
||||
});
|
||||
};
|
||||
|
||||
@ -199,7 +221,7 @@ const handlePreviewVideo = (videos: string[], index: number) => {
|
||||
uni.previewMedia({
|
||||
current: index,
|
||||
sources: videos.map((url) => ({
|
||||
url,
|
||||
url: imagUrl(url),
|
||||
type: "video",
|
||||
})),
|
||||
});
|
||||
@ -207,7 +229,10 @@ const handlePreviewVideo = (videos: string[], index: number) => {
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp: string) => {
|
||||
return dayjs(timestamp).format("YYYY-MM-DD HH:mm");
|
||||
const date = dayjs(timestamp);
|
||||
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
const weekDay = weekDays[date.day()];
|
||||
return `${weekDay} ${date.format("YYYY-MM-DD HH:mm")}`;
|
||||
};
|
||||
|
||||
// 将逗号分隔的字符串转换为数组
|
||||
@ -232,52 +257,170 @@ onMounted(() => {
|
||||
.inspection-list {
|
||||
position: relative;
|
||||
height: calc(100vh - 50px);
|
||||
padding-left: 12px; // 为时间轴留出空间
|
||||
|
||||
.inspection-record {
|
||||
position: relative;
|
||||
margin-left: 0; // 去掉左边距
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e8f4fd;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||
border-color: #4080ff;
|
||||
}
|
||||
|
||||
// 时间轴连接线
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #4080ff 0%, #e8f4fd 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// 时间轴节点
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: -18px;
|
||||
top: 20px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #4080ff;
|
||||
border-radius: 50%;
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 0 4px #ffffff, 0 0 0 6px #e8f4fd;
|
||||
|
||||
.dot-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #4080ff 0%, #66b3ff 100%);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 15px;
|
||||
padding: 12px 8px;
|
||||
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4fd 100%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
border-bottom: 1px solid #d1e7ff;
|
||||
|
||||
.record-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
color: #2c5aa0;
|
||||
font-weight: 600;
|
||||
|
||||
.time-text {
|
||||
margin-left: 5px;
|
||||
margin-left: 8px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.record-status {
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #4080ff;
|
||||
display: inline-flex;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, #4080ff 0%, #66b3ff 100%);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
color: #4080ff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px rgba(64, 128, 255, 0.3);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.record-content {
|
||||
padding: 0 8px 12px 8px;
|
||||
|
||||
.content-item {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
padding: 6px 8px;
|
||||
background: #fafbfc;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #e8f4fd;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f0f7ff;
|
||||
border-left-color: #4080ff;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-weight: bold;
|
||||
flex: 0 0 80px;
|
||||
font-weight: 600;
|
||||
flex: 0 0 70px;
|
||||
color: #2c5aa0;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
color: #4a5568;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第一个时间轴节点特殊样式
|
||||
.inspection-record:first-child {
|
||||
.timeline-dot {
|
||||
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
||||
box-shadow: 0 0 0 4px #ffffff, 0 0 0 6px #f0f9ff;
|
||||
}
|
||||
}
|
||||
|
||||
// 最后一个时间轴节点
|
||||
.inspection-record:last-child {
|
||||
.timeline-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 渐入动画
|
||||
.timeline-item {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .zp-loading-fixed {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@ -22,26 +22,33 @@
|
||||
:key="xkkc.id || index"
|
||||
class="course-item"
|
||||
>
|
||||
<!-- 巡查状态标识 -->
|
||||
<view class="course-status" :class="xkkc.sfxc === '是' ? 'status-done' : 'status-pending'">
|
||||
{{ xkkc.sfxc === '是' ? '已巡查' : '待巡查' }}
|
||||
</view>
|
||||
<view class="course-name">{{ xkkc.kcmc }}</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">上课周期:</view>
|
||||
<view class="info-data">{{ xkkc.skzqmc }}</view>
|
||||
<!-- 第一行:上课周期、开课年级 -->
|
||||
<view class="course-info-row">
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">上课周期:</view>
|
||||
<view class="info-data">{{ xkkc.skzqmc }}</view>
|
||||
</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">开课年级:</view>
|
||||
<view class="info-data">{{ xkkc.njname || '暂无' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">上课时间:</view>
|
||||
<view class="info-data">{{ formatClassTime(xkkc.skkstime, xkkc.skjstime) }}</view>
|
||||
</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">开课地点:</view>
|
||||
<view class="info-data">{{ xkkc.kcdd }}</view>
|
||||
</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">授课教师:</view>
|
||||
<view class="info-data">{{ xkkc.jsName || '暂无' }}</view>
|
||||
</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">开课年级:</view>
|
||||
<view class="info-data">{{ xkkc.njname || '暂无' }}</view>
|
||||
|
||||
<!-- 第二行:授课教师、开课地点 -->
|
||||
<view class="course-info-row">
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">授课教师:</view>
|
||||
<view class="info-data">{{ xkkc.jsName || '暂无' }}</view>
|
||||
</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">开课地点:</view>
|
||||
<view class="info-data">{{ xkkc.kcdd }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="separator-line"></view>
|
||||
<view class="course-btn-group">
|
||||
@ -114,6 +121,32 @@ const dataStore = useDataStore();
|
||||
|
||||
const wdNameList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
||||
|
||||
// 检查当前时间是否符合课程时间安排
|
||||
const isCurrentTimeMatch = (xkkc: any) => {
|
||||
const now = dayjs();
|
||||
let wDay = now.day();
|
||||
if (wDay === 0) {
|
||||
wDay = 7; // 将周日从0改为7
|
||||
}
|
||||
let mDay = now.date(); // 当前日期(1-31)
|
||||
|
||||
// 判断周期类型
|
||||
switch (xkkc.skzqlx) {
|
||||
case '每天':
|
||||
return true; // 每天都可以巡查
|
||||
case '每周':
|
||||
if (!xkkc.skzq) return false;
|
||||
const daysOfWeek = xkkc.skzq.split(',').map(Number);
|
||||
return daysOfWeek.includes(wDay);
|
||||
case '每月':
|
||||
if (!xkkc.skzq) return false;
|
||||
const daysOfMonth = xkkc.skzq.split(',').map(Number);
|
||||
return daysOfMonth.includes(mDay);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 控制选择器显示状态
|
||||
const showXkFlag = ref(false);
|
||||
|
||||
@ -133,12 +166,12 @@ onMounted(async () => {
|
||||
// 检查是否从排班页面跳转过来
|
||||
const pbData = dataStore.getData;
|
||||
|
||||
if (pbData && pbData.id) {
|
||||
if (pbData && pbData.pbId) {
|
||||
// 检查是否是课程数据(包含kcmc字段)
|
||||
if (pbData.kcmc) {
|
||||
// 这是课程数据,需要重建排班数据
|
||||
const reconstructedPbData = {
|
||||
id: pbData.pbId, // 使用pbId作为排班ID
|
||||
pbId: pbData.pbId, // 使用pbId作为排班ID
|
||||
status: 'A',
|
||||
njIds: pbData.njIds || '',
|
||||
bjIds: pbData.bjIds || '',
|
||||
@ -153,15 +186,26 @@ onMounted(async () => {
|
||||
dataStore.setGlobal(reconstructedPbData);
|
||||
await loadXcCourseList(reconstructedPbData);
|
||||
} else {
|
||||
// 这是排班数据,直接使用
|
||||
dataStore.setGlobal(pbData);
|
||||
await loadXcCourseList(pbData);
|
||||
// 这是排班数据,确保有pbId字段
|
||||
const pbDataWithPbId = {
|
||||
...pbData,
|
||||
pbId: pbData.pbId || pbData.id
|
||||
};
|
||||
delete pbDataWithPbId.id; // 移除原始的id字段
|
||||
dataStore.setGlobal(pbDataWithPbId);
|
||||
await loadXcCourseList(pbDataWithPbId);
|
||||
}
|
||||
} else {
|
||||
// 正常加载课程列表
|
||||
await loadCourseList();
|
||||
}
|
||||
|
||||
// 监听刷新事件
|
||||
uni.$on('refreshCourseList', async () => {
|
||||
console.log('收到刷新事件,重新加载课程列表');
|
||||
await refreshCourseList();
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
});
|
||||
|
||||
@ -182,21 +226,58 @@ const loadCourseList = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新课程列表
|
||||
const refreshCourseList = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: "刷新中...",
|
||||
});
|
||||
|
||||
// 检查是否有排班数据
|
||||
const pbData = dataStore.getGlobal;
|
||||
|
||||
if (pbData && pbData.pbId) {
|
||||
// 重新加载巡查课程列表
|
||||
await loadXcCourseList(pbData);
|
||||
} else {
|
||||
// 重新加载普通课程列表
|
||||
await loadCourseList();
|
||||
}
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: "刷新成功",
|
||||
icon: "success",
|
||||
duration: 1000
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('刷新课程列表失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: "刷新失败",
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 加载巡查课程列表
|
||||
const loadXcCourseList = async (pbData: any) => {
|
||||
try {
|
||||
const res = await getXcCourseListApi({
|
||||
jsId: getJs.id,
|
||||
pbId: pbData.id,
|
||||
pbId: pbData.pbId,
|
||||
xclx: pbData.xclx
|
||||
});
|
||||
|
||||
if (res && res.resultCode == 1) {
|
||||
const list = res.result || [];
|
||||
|
||||
// 先映射数据,然后进行时间过滤
|
||||
let mappedList = [];
|
||||
|
||||
if (pbData.xclx === 'A') {
|
||||
// 类型A:直接使用返回的课程数据
|
||||
xkkcList.value = list.map((item: any) => ({
|
||||
mappedList = list.map((item: any) => ({
|
||||
id: item.id,
|
||||
kcmc: item.kcmc,
|
||||
skzqmc: item.skzqmc || '每周',
|
||||
@ -209,12 +290,15 @@ const loadXcCourseList = async (pbData: any) => {
|
||||
skzqlx: item.skzqlx,
|
||||
skzq: item.skzq,
|
||||
jsName: item.jsName,
|
||||
kcjsId: item.kcjsId, // 添加开课教师ID字段
|
||||
jxjh: item.jxjh, // 添加教学计划字段
|
||||
jxll: item.jxll // 添加教学理论字段
|
||||
jxll: item.jxll, // 添加教学理论字段
|
||||
pbLxId: item.pbLxId, // 添加排班类型ID字段
|
||||
sfxc: item.sfxc || '否' // 添加是否巡查字段
|
||||
}));
|
||||
} else if (pbData.xclx === 'B') {
|
||||
// 类型B:直接使用返回的课程数据
|
||||
xkkcList.value = list.map((item: any) => ({
|
||||
mappedList = list.map((item: any) => ({
|
||||
id: item.id,
|
||||
kcmc: item.kcmc,
|
||||
skzqmc: item.skzqmc || '每周',
|
||||
@ -227,11 +311,17 @@ const loadXcCourseList = async (pbData: any) => {
|
||||
skzqlx: item.skzqlx,
|
||||
skzq: item.skzq,
|
||||
jsName: item.jsName,
|
||||
kcjsId: item.kcjsId || item.jsId, // 添加开课教师ID字段
|
||||
jxjh: item.jxjh, // 添加教学计划字段
|
||||
jxll: item.jxll // 添加教学理论字段
|
||||
jxll: item.jxll, // 添加教学理论字段
|
||||
pbLxId: item.pbLxId, // 添加排班类型ID字段
|
||||
sfxc: item.sfxc || '否' // 添加是否巡查字段
|
||||
}));
|
||||
}
|
||||
|
||||
// 过滤出当前时间符合的课程
|
||||
xkkcList.value = mappedList.filter((xkkc: any) => isCurrentTimeMatch(xkkc));
|
||||
|
||||
// 处理课程周期显示
|
||||
for (let i = 0; i < xkkcList.value.length; i++) {
|
||||
let xkkc = xkkcList.value[i];
|
||||
@ -313,7 +403,7 @@ const goXc = (xkkc: any) => {
|
||||
const pbData = dataStore.getGlobal;
|
||||
|
||||
// 检查排班数据是否有效
|
||||
if (!pbData || !pbData.id || !pbData.xcbt) {
|
||||
if (!pbData || !pbData.pbId || !pbData.xcbt) {
|
||||
console.log('排班数据检查失败:', pbData);
|
||||
uni.showToast({
|
||||
title: '数据异常,请重新选择排班',
|
||||
@ -326,12 +416,15 @@ const goXc = (xkkc: any) => {
|
||||
const combinedData = {
|
||||
...xkkc,
|
||||
id: xkkc.id, // 课程记录ID
|
||||
pbId: pbData.id, // 排班ID - 确保使用正确的排班ID
|
||||
pbId: pbData.pbId, // 排班ID - 确保使用正确的排班ID
|
||||
pbLxId: xkkc.pbLxId || pbData.pbLxId, // 传递 pbLxId,优先使用课程中的 pbLxId
|
||||
kcjsId: xkkc.kcjsId , // 传递开课教师ID
|
||||
xclx: pbData.xclx,
|
||||
xcbt: pbData.xcbt,
|
||||
xqmc: pbData.xqmc
|
||||
};
|
||||
|
||||
|
||||
dataStore.setData(combinedData);
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/kefuxuncha/xcXkkcDetail`,
|
||||
@ -344,7 +437,7 @@ const goRecord = (xkkc: any) => {
|
||||
const pbData = dataStore.getGlobal;
|
||||
|
||||
// 检查排班数据是否有效
|
||||
if (!pbData || !pbData.id || !pbData.xcbt) {
|
||||
if (!pbData || !pbData.pbId || !pbData.xcbt) {
|
||||
console.log('排班数据检查失败:', pbData);
|
||||
uni.showToast({
|
||||
title: '数据异常,请重新选择排班',
|
||||
@ -357,7 +450,7 @@ const goRecord = (xkkc: any) => {
|
||||
const combinedData = {
|
||||
...xkkc,
|
||||
id: xkkc.id, // 课程记录ID
|
||||
pbId: pbData.id, // 排班ID - 确保使用正确的排班ID
|
||||
pbId: pbData.pbId, // 排班ID - 确保使用正确的排班ID
|
||||
xclx: pbData.xclx,
|
||||
xcbt: pbData.xcbt,
|
||||
xqmc: pbData.xqmc
|
||||
@ -405,8 +498,11 @@ const formatClassTime = (startTime: string, endTime: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 页面卸载前清除定时器
|
||||
onBeforeUnmount(() => {});
|
||||
// 页面卸载前清除定时器和事件监听器
|
||||
onBeforeUnmount(() => {
|
||||
// 移除事件监听器
|
||||
uni.$off('refreshCourseList');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -517,6 +613,30 @@ onBeforeUnmount(() => {});
|
||||
box-shadow: 0 4px 20px rgba(63, 191, 114, 0.15);
|
||||
}
|
||||
|
||||
.course-status {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
z-index: 2;
|
||||
animation: fadeInRight 0.5s ease-out 0.2s both;
|
||||
|
||||
&.status-done {
|
||||
background: linear-gradient(135deg, #67c23a, #85ce61);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(103, 194, 58, 0.3);
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
background: linear-gradient(135deg, #e6a23c, #f0c78a);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(230, 162, 60, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.course-name {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
@ -524,6 +644,7 @@ onBeforeUnmount(() => {});
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.4;
|
||||
animation: fadeInLeft 0.5s ease-out 0.1s both;
|
||||
padding-right: 80px; // 为状态标识留出空间
|
||||
}
|
||||
|
||||
.course-btn-group {
|
||||
@ -584,23 +705,31 @@ onBeforeUnmount(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
.course-info-item {
|
||||
.course-info-row {
|
||||
display: flex;
|
||||
margin-bottom: 14px;
|
||||
gap: 20px;
|
||||
animation: fadeInUp 0.5s ease-out 0.15s both;
|
||||
}
|
||||
|
||||
.course-info-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
align-items: center;
|
||||
animation: fadeInUp 0.5s ease-out 0.15s both;
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
flex: 0 0 100px;
|
||||
flex: 0 0 70px;
|
||||
font-weight: 500;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.info-data {
|
||||
flex: 1 0 1px;
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-weight: 400;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,38 +9,34 @@
|
||||
<u-icon name="calendar" color="#4080ff" size="20"></u-icon>
|
||||
</view>
|
||||
<text class="font-16 font-bold">{{ xkkc.kcmc }}</text>
|
||||
<text class="font-14 cor-999 ml-10"
|
||||
>{{ todayInfo.date }} ({{ todayInfo.weekName }})</text
|
||||
>
|
||||
<text class="font-14 cor-999 ml-10">{{ todayInfo.weekName }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 上课时间信息 -->
|
||||
<!-- 课程详细信息 -->
|
||||
<view class="course-time-info">
|
||||
<view class="time-item">
|
||||
<view class="time-label">上课周期类型:</view>
|
||||
<view class="time-value">{{ xkkc.skzqlx }}</view>
|
||||
</view>
|
||||
<view class="time-item">
|
||||
<view class="time-label">上课周期:</view>
|
||||
<view class="time-value">{{ xkkc.skzqmc }}</view>
|
||||
</view>
|
||||
<view class="time-item">
|
||||
<view class="time-label">上课时间:</view>
|
||||
<view class="time-value"
|
||||
>{{ xkkc.skkstime }} - {{ xkkc.skjstime }}</view
|
||||
>
|
||||
<view class="time-label">授课老师:</view>
|
||||
<view class="time-value">{{ xkkc.jsName || '暂无' }}</view>
|
||||
</view>
|
||||
<view class="time-item">
|
||||
<view class="time-label">上课地点:</view>
|
||||
<view class="time-value">{{ xkkc.kcdd }}</view>
|
||||
</view>
|
||||
<view class="time-item">
|
||||
<view class="time-label">开课年级:</view>
|
||||
<view class="time-value">{{ xkkc.njname || '暂无' }}</view>
|
||||
</view>
|
||||
<view class="time-item">
|
||||
<view class="time-label">上课人数:</view>
|
||||
<view class="time-value">{{ xkkc.hasNum || 0 }} | {{ xkkc.maxNum || 0 }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 代课老师选择 -->
|
||||
<view class="teacher-selection-info">
|
||||
<view class="teacher-item">
|
||||
<view class="teacher-label">代课老师:</view>
|
||||
<view class="teacher-picker-container">
|
||||
<BasicJsPicker
|
||||
:defaultValue="selectedSubstituteTeacherId"
|
||||
:multiple="false"
|
||||
@change="onSubstituteTeacherChange"
|
||||
placeholder="请选择代课老师"
|
||||
ref="substituteTeacherPickerRef"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -195,91 +191,25 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拍照上传 -->
|
||||
<view class="section mx-15 mb-15">
|
||||
<view class="section-title-bar">
|
||||
<view class="decorator"></view>
|
||||
<text class="title-text">拍照上传</text>
|
||||
</view>
|
||||
<view class="upload-card bg-white r-md p-15">
|
||||
<view class="upload-section">
|
||||
<view class="upload-list">
|
||||
<view
|
||||
v-for="(image, index) in imageList"
|
||||
:key="index"
|
||||
class="upload-item"
|
||||
>
|
||||
<image
|
||||
:src="image.url ? imagUrl(image.url) : image.tempPath"
|
||||
mode="aspectFill"
|
||||
class="upload-image"
|
||||
@click="previewImage(index)"
|
||||
/>
|
||||
<view class="upload-delete" @click="deleteImage(index)">
|
||||
<text class="delete-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="imageList.length < 5"
|
||||
class="upload-add"
|
||||
@click="chooseImage"
|
||||
>
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拍视频上传 -->
|
||||
<!-- 图片视频上传组件 -->
|
||||
<view class="section mx-15 mb-30">
|
||||
<view class="section-title-bar">
|
||||
<view class="decorator"></view>
|
||||
<text class="title-text">拍视频上传</text>
|
||||
<text class="title-text">图片视频上传</text>
|
||||
</view>
|
||||
<view class="upload-card bg-white r-md p-15">
|
||||
<view class="upload-section">
|
||||
<view class="upload-list">
|
||||
<view
|
||||
v-for="(video, index) in videoList"
|
||||
:key="index"
|
||||
class="upload-item"
|
||||
>
|
||||
<video
|
||||
:src="video.url ? video.url : video.tempPath"
|
||||
class="upload-video"
|
||||
:controls="false"
|
||||
:show-center-play-btn="false"
|
||||
:show-play-btn="false"
|
||||
:show-fullscreen-btn="false"
|
||||
:show-progress="false"
|
||||
:show-mute-btn="false"
|
||||
:enable-progress-gesture="false"
|
||||
:enable-play-gesture="false"
|
||||
:loop="false"
|
||||
:muted="true"
|
||||
:poster="''"
|
||||
></video>
|
||||
<view class="video-play-icon">
|
||||
<u-icon name="play-right-fill" color="#fff" size="20"></u-icon>
|
||||
</view>
|
||||
<view class="upload-delete" @click="deleteVideo(index)">
|
||||
<text class="delete-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="videoList.length < 3"
|
||||
class="upload-add"
|
||||
@click="chooseVideo"
|
||||
>
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加视频</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<ImageVideoUpload
|
||||
v-model:image-list="imageList"
|
||||
v-model:video-list="videoList"
|
||||
:max-image-count="5"
|
||||
:max-video-count="3"
|
||||
:compress-config="compressConfig"
|
||||
:upload-api="attachmentUpload"
|
||||
@image-upload-success="onImageUploadSuccess"
|
||||
@video-upload-success="onVideoUploadSuccess"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -304,6 +234,8 @@ import { xkXcSaveApi } from "@/api/base/xkXcApi";
|
||||
import { getXkDmPageApi } from "@/api/base/xkApi";
|
||||
import { attachmentUpload } from "@/api/system/upload";
|
||||
import BasicLayout from "@/components/BasicLayout/Layout.vue";
|
||||
import BasicJsPicker from "@/components/BasicJsPicker/Picker.vue";
|
||||
import { ImageVideoUpload } from "@/components/ImageVideoUpload";
|
||||
import { useDataStore } from "@/store/modules/data";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
@ -317,6 +249,7 @@ const { getData } = useDataStore();
|
||||
const js = computed(() => getJs);
|
||||
const xkkc = computed(() => getData);
|
||||
|
||||
|
||||
// 解析教学计划数据
|
||||
const parsedTeachingPlan = computed(() => {
|
||||
console.log('教学计划原始数据:', xkkc.value.jxjh);
|
||||
@ -386,19 +319,11 @@ const todayInfo = ref({
|
||||
// 巡查项目
|
||||
const checkItems = ref<any[]>([]);
|
||||
|
||||
// 图片列表 - 修改为包含临时路径和服务器路径的对象
|
||||
interface ImageItem {
|
||||
tempPath?: string; // 临时路径(用于预览)
|
||||
url?: string; // 服务器路径(上传成功后)
|
||||
name?: string; // 文件名
|
||||
}
|
||||
// 导入类型和配置
|
||||
import { type ImageItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
|
||||
|
||||
// 视频列表 - 修改为包含临时路径和服务器路径的对象
|
||||
interface VideoItem {
|
||||
tempPath?: string; // 临时路径(用于预览)
|
||||
url?: string; // 服务器路径(上传成功后)
|
||||
name?: string; // 文件名
|
||||
}
|
||||
// 压缩配置
|
||||
const compressConfig = ref(COMPRESS_PRESETS.medium)
|
||||
|
||||
const imageList = ref<ImageItem[]>([]);
|
||||
const videoList = ref<VideoItem[]>([]);
|
||||
@ -426,9 +351,28 @@ const toggleTeachingPlan = () => {
|
||||
teachingPlanExpanded.value = !teachingPlanExpanded.value;
|
||||
};
|
||||
|
||||
// 代课老师选择变化回调
|
||||
const onSubstituteTeacherChange = (teacher: any) => {
|
||||
console.log('代课老师选择变化:', teacher);
|
||||
if (teacher) {
|
||||
selectedSubstituteTeacher.value = teacher;
|
||||
selectedSubstituteTeacherId.value = teacher.id || teacher.value;
|
||||
console.log('选中的代课老师:', selectedSubstituteTeacher.value);
|
||||
console.log('选中的代课老师ID:', selectedSubstituteTeacherId.value);
|
||||
} else {
|
||||
selectedSubstituteTeacher.value = null;
|
||||
selectedSubstituteTeacherId.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 提交状态控制
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
// 代课老师选择相关
|
||||
const selectedSubstituteTeacher = ref<any>(null);
|
||||
const selectedSubstituteTeacherId = ref<string>('');
|
||||
const substituteTeacherPickerRef = ref();
|
||||
|
||||
// 加载点名数据
|
||||
const loadRollCallData = async () => {
|
||||
try {
|
||||
@ -436,6 +380,14 @@ const loadRollCallData = async () => {
|
||||
const startTime = `${today} 00:00:00`;
|
||||
const endTime = `${today} 23:59:59`;
|
||||
|
||||
// 添加调试信息
|
||||
console.log('点名数据加载参数:', {
|
||||
xkkcId: xkkc.value.id,
|
||||
kcjsId: xkkc.value.kcjsId,
|
||||
jsId: js.value.id,
|
||||
courseData: xkkc.value
|
||||
});
|
||||
|
||||
const res = await getXkDmPageApi({
|
||||
rows: 10,
|
||||
page: 1,
|
||||
@ -443,7 +395,7 @@ const loadRollCallData = async () => {
|
||||
endTime,
|
||||
pageNo: 1,
|
||||
xkkcId: xkkc.value.id,
|
||||
jsId: js.value.id,
|
||||
jsId: xkkc.value.kcjsId, // 使用开课教师ID,如果没有则使用当前教师ID
|
||||
sidx: 'dmTime',
|
||||
sord: 'desc'
|
||||
});
|
||||
@ -514,140 +466,13 @@ const onCheckItemChange = (e: any, item: any) => {
|
||||
item.checked = e.detail.value === "A";
|
||||
};
|
||||
|
||||
// 选择图片
|
||||
const chooseImage = () => {
|
||||
uni.chooseImage({
|
||||
count: 5 - imageList.value.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
// 添加临时图片到列表
|
||||
const tempFilePaths = res.tempFilePaths as string[];
|
||||
const newImages = tempFilePaths.map((path: string) => ({
|
||||
tempPath: path,
|
||||
name: path.split('/').pop() || 'image.jpg'
|
||||
}));
|
||||
imageList.value = [...imageList.value, ...newImages];
|
||||
// 自动上传图片
|
||||
await uploadImages(newImages);
|
||||
}
|
||||
});
|
||||
// 上传成功回调
|
||||
const onImageUploadSuccess = (image: ImageItem, index: number) => {
|
||||
console.log('图片上传成功:', image, index);
|
||||
};
|
||||
|
||||
// 选择视频
|
||||
const chooseVideo = () => {
|
||||
uni.chooseVideo({
|
||||
sourceType: ['album', 'camera'],
|
||||
maxDuration: 60,
|
||||
camera: 'back',
|
||||
success: async (res) => {
|
||||
// 添加临时视频到列表
|
||||
const tempFilePath = res.tempFilePath;
|
||||
const newVideo = {
|
||||
tempPath: tempFilePath,
|
||||
name: tempFilePath.split('/').pop() || 'video.mp4'
|
||||
};
|
||||
videoList.value = [...videoList.value, newVideo];
|
||||
// 自动上传视频
|
||||
await uploadVideos([newVideo]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 上传图片到服务器
|
||||
const uploadImages = async (images: ImageItem[]) => {
|
||||
try {
|
||||
showLoading('上传图片中...');
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const image = images[i];
|
||||
if (image.tempPath) {
|
||||
try {
|
||||
// 调用上传接口
|
||||
const uploadResult: any = await attachmentUpload(image.tempPath as any);
|
||||
|
||||
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
|
||||
// 保存服务器返回的文件路径
|
||||
const serverPath = uploadResult.result[0].filePath;
|
||||
|
||||
// 更新图片对象,保存服务器路径
|
||||
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
|
||||
if (index !== -1) {
|
||||
imageList.value[index].url = serverPath;
|
||||
// 可以删除临时路径以节省内存
|
||||
delete imageList.value[index].tempPath;
|
||||
}
|
||||
} else {
|
||||
throw new Error('上传响应格式异常');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('图片上传失败:', error);
|
||||
showToast({ title: `${image.name || '图片'}上传失败`, icon: 'none' });
|
||||
|
||||
// 从列表中移除上传失败的图片
|
||||
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
|
||||
if (index !== -1) {
|
||||
imageList.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
showToast({ title: '图片上传完成', icon: 'success' });
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
console.error('批量上传图片失败:', error);
|
||||
showToast({ title: '图片上传失败,请重试', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 上传视频到服务器
|
||||
const uploadVideos = async (videos: VideoItem[]) => {
|
||||
try {
|
||||
showLoading('上传视频中...');
|
||||
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
const video = videos[i];
|
||||
if (video.tempPath) {
|
||||
try {
|
||||
// 调用上传接口
|
||||
const uploadResult: any = await attachmentUpload(video.tempPath as any);
|
||||
|
||||
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
|
||||
// 保存服务器返回的文件路径
|
||||
const serverPath = uploadResult.result[0].filePath;
|
||||
|
||||
// 更新视频对象,保存服务器路径
|
||||
const index = videoList.value.findIndex(v => v.tempPath === video.tempPath);
|
||||
if (index !== -1) {
|
||||
videoList.value[index].url = serverPath;
|
||||
// 可以删除临时路径以节省内存
|
||||
delete videoList.value[index].tempPath;
|
||||
}
|
||||
} else {
|
||||
throw new Error('上传响应格式异常');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('视频上传失败:', error);
|
||||
showToast({ title: `${video.name || '视频'}上传失败`, icon: 'none' });
|
||||
|
||||
// 从列表中移除上传失败的视频
|
||||
const index = videoList.value.findIndex(v => v.tempPath === video.tempPath);
|
||||
if (index !== -1) {
|
||||
videoList.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
showToast({ title: '视频上传完成', icon: 'success' });
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
console.error('批量上传视频失败:', error);
|
||||
showToast({ title: '视频上传失败,请重试', icon: 'none' });
|
||||
}
|
||||
const onVideoUploadSuccess = (video: VideoItem, index: number) => {
|
||||
console.log('视频上传成功:', video, index);
|
||||
};
|
||||
|
||||
// 预览图片
|
||||
@ -662,16 +487,6 @@ const previewImage = (index: number) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const deleteImage = (index: number) => {
|
||||
imageList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 删除视频
|
||||
const deleteVideo = (index: number) => {
|
||||
videoList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 检查巡查时间
|
||||
const checkInspectionTime = () => {
|
||||
const currentTime = now;
|
||||
@ -758,11 +573,14 @@ const submit = async () => {
|
||||
jsId: js.value.id,
|
||||
jsxm: js.value.xm || js.value.jsxm, // 添加教师姓名
|
||||
xkkcId: xkkc.value.id,
|
||||
pbLxId: xkkc.value.pbLxId, // 添加排班类型ID
|
||||
dkjsId: selectedSubstituteTeacherId.value, // 添加代课老师ID
|
||||
xctime: now.format("YYYY-MM-DD HH:mm:ss"),
|
||||
zp: getImageUrls(),
|
||||
sp: getVideoUrls(),
|
||||
xkXcXmList: xkXcXmList, // 添加巡查项目列表
|
||||
};
|
||||
|
||||
|
||||
const res = await xkXcSaveApi(submitData);
|
||||
|
||||
@ -772,7 +590,13 @@ const submit = async () => {
|
||||
icon: "success",
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
// 返回到课程列表页面并刷新
|
||||
uni.navigateBack({
|
||||
success: () => {
|
||||
// 通过事件总线通知列表页面刷新
|
||||
uni.$emit('refreshCourseList');
|
||||
}
|
||||
});
|
||||
}, 1500);
|
||||
} else {
|
||||
uni.showToast({
|
||||
@ -872,6 +696,31 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
.teacher-selection-info {
|
||||
margin-top: 15px;
|
||||
padding: 10px 15px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #eee;
|
||||
|
||||
.teacher-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.teacher-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.teacher-picker-container {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inspection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user