教研成果调整
This commit is contained in:
parent
ce5c2f05ba
commit
f0c85a60f9
45
src/api/base/jycgApi.ts
Normal file
45
src/api/base/jycgApi.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { get, post } from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 获取教研成果列表
|
||||
*/
|
||||
export function jycgFindPageApi(params: any) {
|
||||
return get('/api/jycg/findPage', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取教研成果详情
|
||||
*/
|
||||
export function jycgFindByIdApi(params: any) {
|
||||
return get('/api/jycg/findById', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存教研成果(新增/修改)
|
||||
*/
|
||||
export function jycgSaveApi(params: any) {
|
||||
return post('/api/jycg/save', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除教研成果
|
||||
*/
|
||||
export function jycgDeleteApi(params: any) {
|
||||
return post('/api/jycg/delete', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除教研成果
|
||||
*/
|
||||
export function jycgBatchDeleteApi(params: any) {
|
||||
return post('/api/jycg/batchDelete', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
export function jycgUploadFileApi(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return post('/api/jycg/upload', formData);
|
||||
}
|
||||
@ -72,7 +72,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="video-info">
|
||||
<text class="video-name">{{ video.name }}</text>
|
||||
<text class="video-name">{{ video.originalName || video.name }}</text>
|
||||
<text class="video-size" v-if="video.size">{{ formatFileSize(video.size) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@ -103,10 +103,14 @@
|
||||
>
|
||||
<view class="file-preview" @click="previewFile(index)">
|
||||
<view class="file-icon">
|
||||
<uni-icons :type="getFileIcon(file.extension || '')" size="32" color="#666"></uni-icons>
|
||||
<image
|
||||
:src="getFileIcon(file.extension || '')"
|
||||
class="icon-image"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view class="file-info">
|
||||
<text class="file-name">{{ file.name }}</text>
|
||||
<text class="file-name">{{ file.originalName || file.name }}</text>
|
||||
<text class="file-size" v-if="file.size">{{ formatFileSize(file.size) }}</text>
|
||||
<text class="file-type">{{ file.extension?.toUpperCase() || 'FILE' }}</text>
|
||||
</view>
|
||||
@ -160,6 +164,7 @@ interface FileItem {
|
||||
tempPath?: string
|
||||
url?: string
|
||||
name?: string
|
||||
originalName?: string
|
||||
type?: string
|
||||
size?: number
|
||||
extension?: string
|
||||
@ -433,40 +438,43 @@ const checkVideoSize = async (filePath: string): Promise<boolean> => {
|
||||
|
||||
// 选择图片
|
||||
const chooseImage = () => {
|
||||
// 使用 chooseImage 支持拍照和相册选择
|
||||
uni.chooseImage({
|
||||
count: props.maxImageCount - imageList.value.length,
|
||||
sizeType: ['original'],
|
||||
sourceType: ['album', 'camera'],
|
||||
sourceType: ['camera', 'album'], // 支持拍照和相册
|
||||
sizeType: ['original', 'compressed'], // 可以选择原图或压缩图
|
||||
success: async (res) => {
|
||||
const tempFilePaths = res.tempFilePaths as string[]
|
||||
|
||||
showLoading('压缩图片中...')
|
||||
|
||||
try {
|
||||
const compressedImages = []
|
||||
const tempFilePaths = res.tempFilePaths
|
||||
if (Array.isArray(tempFilePaths) && tempFilePaths.length > 0) {
|
||||
showLoading('压缩图片中...')
|
||||
|
||||
// 并行压缩所有图片
|
||||
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
|
||||
try {
|
||||
const compressedImages = []
|
||||
|
||||
// 并行压缩所有图片
|
||||
const compressPromises = tempFilePaths.map(async (filePath: string, index) => {
|
||||
try {
|
||||
showLoading(`压缩图片中... (${index + 1}/${tempFilePaths.length})`)
|
||||
|
||||
const compressedPath = await smartCompressImage(filePath)
|
||||
|
||||
return {
|
||||
tempPath: compressedPath,
|
||||
name: filePath.split('/').pop() || 'image.jpg',
|
||||
originalName: filePath.split('/').pop() || 'image.jpg',
|
||||
originalPath: filePath,
|
||||
isCompressed: true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`图片 ${filePath} 压缩失败:`, error)
|
||||
return {
|
||||
tempPath: filePath,
|
||||
name: filePath.split('/').pop() || 'image.jpg',
|
||||
originalName: filePath.split('/').pop() || 'image.jpg',
|
||||
isCompressed: false
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
@ -496,8 +504,9 @@ const chooseImage = () => {
|
||||
showToast({ title: '图片处理失败', icon: 'none' })
|
||||
console.error('图片处理失败:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
fail: (error: any) => {
|
||||
console.error('选择图片失败:', error)
|
||||
showToast({ title: '选择图片失败', icon: 'none' })
|
||||
}
|
||||
@ -534,6 +543,7 @@ const chooseVideo = () => {
|
||||
const newVideo = {
|
||||
tempPath: tempFilePath,
|
||||
name: tempFilePath.split('/').pop() || 'video.mp4',
|
||||
originalName: res.name || 'video.mp4', // 保存原始文件名
|
||||
duration: res.duration,
|
||||
size: res.size
|
||||
}
|
||||
@ -584,18 +594,29 @@ const chooseFile = () => {
|
||||
|
||||
// 确定文件类型
|
||||
let fileType = 'document'
|
||||
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(fileExtension)) {
|
||||
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'tiff', 'ico'].includes(fileExtension)) {
|
||||
fileType = 'image'
|
||||
} else if (['mp4', 'mov', 'avi', 'wmv', 'flv'].includes(fileExtension)) {
|
||||
} else if (['mp4', 'mov', 'avi', 'wmv', 'flv', 'mkv', 'webm', '3gp', 'm4v'].includes(fileExtension)) {
|
||||
fileType = 'video'
|
||||
} else if (['mp3', 'wav', 'aac', 'ogg'].includes(fileExtension)) {
|
||||
} else if (['mp3', 'wav', 'aac', 'ogg', 'flac', 'm4a', 'wma'].includes(fileExtension)) {
|
||||
fileType = 'audio'
|
||||
} else if (['pdf', 'doc', 'docx', 'txt', 'rtf', 'wps'].includes(fileExtension)) {
|
||||
fileType = 'document'
|
||||
} else if (['xls', 'xlsx', 'csv', 'et'].includes(fileExtension)) {
|
||||
fileType = 'spreadsheet'
|
||||
} else if (['ppt', 'pptx', 'dps'].includes(fileExtension)) {
|
||||
fileType = 'presentation'
|
||||
} else if (['zip', 'rar', '7z', 'tar', 'gz'].includes(fileExtension)) {
|
||||
fileType = 'archive'
|
||||
} else if (['html', 'htm', 'css', 'js', 'json', 'xml'].includes(fileExtension)) {
|
||||
fileType = 'code'
|
||||
}
|
||||
|
||||
// 创建文件项
|
||||
const fileItem: FileItem = {
|
||||
tempPath: fileInfo.path,
|
||||
name: fileName,
|
||||
originalName: fileName, // 保存原始文件名
|
||||
type: fileType,
|
||||
size: fileInfo.size,
|
||||
extension: fileExtension,
|
||||
@ -634,15 +655,29 @@ const uploadFile = async (fileItem: FileItem, index: number) => {
|
||||
if (!props.uploadApi || !fileItem.tempPath) return
|
||||
|
||||
try {
|
||||
// 触发上传开始事件
|
||||
emit('upload-progress', 'file', 1, 1)
|
||||
|
||||
const result = await props.uploadApi(fileItem.tempPath)
|
||||
|
||||
if (result && result.resultCode === 1 && result.result && result.result.length > 0) {
|
||||
const uploadedFile = result.result[0]
|
||||
|
||||
// 更新文件项
|
||||
fileItem.url = imagUrl(uploadedFile.filePath)
|
||||
// 更新文件项 - 直接使用相对路径,不添加域名前缀
|
||||
fileItem.url = uploadedFile.filePath
|
||||
// 如果服务器返回了文件名,更新name字段,但保留originalName
|
||||
if (uploadedFile.fileName) {
|
||||
fileItem.name = uploadedFile.fileName
|
||||
}
|
||||
fileItem.tempPath = undefined // 清除临时路径
|
||||
|
||||
// 同步更新 fileList 中的对应项
|
||||
if (fileList.value[index]) {
|
||||
fileList.value[index].url = fileItem.url
|
||||
fileList.value[index].name = fileItem.name
|
||||
fileList.value[index].tempPath = undefined
|
||||
}
|
||||
|
||||
// 触发成功事件
|
||||
emit('file-upload-success', fileItem, index)
|
||||
|
||||
@ -703,32 +738,92 @@ const removeFile = (index: number) => {
|
||||
|
||||
// 获取文件图标
|
||||
const getFileIcon = (extension: string): string => {
|
||||
const iconMap: { [key: string]: string } = {
|
||||
// 文档
|
||||
'pdf': 'book',
|
||||
'doc': 'book',
|
||||
'docx': 'book',
|
||||
'txt': 'book',
|
||||
// 表格
|
||||
'xls': 'table',
|
||||
'xlsx': 'table',
|
||||
// 演示文稿
|
||||
'ppt': 'slideshow',
|
||||
'pptx': 'slideshow',
|
||||
// 音频
|
||||
'mp3': 'mic',
|
||||
'wav': 'mic',
|
||||
'aac': 'mic',
|
||||
'ogg': 'mic',
|
||||
const type = extension.toLowerCase();
|
||||
switch (type) {
|
||||
// PDF文档
|
||||
case 'pdf':
|
||||
return '/static/base/view/pdf.png';
|
||||
|
||||
// Word文档
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
case 'rtf':
|
||||
case 'wps': // WPS文字
|
||||
return '/static/base/view/word.png';
|
||||
|
||||
// Excel表格
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
case 'csv':
|
||||
case 'et': // WPS表格
|
||||
return '/static/base/view/excel.png';
|
||||
|
||||
// PowerPoint演示
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
case 'dps': // WPS演示
|
||||
return '/static/base/view/ppt.png';
|
||||
|
||||
// 压缩文件
|
||||
'zip': 'folder',
|
||||
'rar': 'folder',
|
||||
'7z': 'folder',
|
||||
// 其他
|
||||
'default': 'paperclip'
|
||||
case 'zip':
|
||||
case 'rar':
|
||||
case '7z':
|
||||
case 'tar':
|
||||
case 'gz':
|
||||
return '/static/base/view/zip.png';
|
||||
|
||||
// 图片文件
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'png':
|
||||
case 'gif':
|
||||
case 'bmp':
|
||||
case 'webp':
|
||||
case 'svg':
|
||||
case 'tiff':
|
||||
case 'ico':
|
||||
return '/static/base/view/image.png';
|
||||
|
||||
// 视频文件
|
||||
case 'mp4':
|
||||
case 'avi':
|
||||
case 'mov':
|
||||
case 'wmv':
|
||||
case 'flv':
|
||||
case 'mkv':
|
||||
case 'webm':
|
||||
case '3gp':
|
||||
case 'm4v':
|
||||
return '/static/base/view/video.png';
|
||||
|
||||
// 音频文件
|
||||
case 'mp3':
|
||||
case 'wav':
|
||||
case 'aac':
|
||||
case 'ogg':
|
||||
case 'flac':
|
||||
case 'm4a':
|
||||
case 'wma':
|
||||
return '/static/base/view/audio.png';
|
||||
|
||||
// 文本文件
|
||||
case 'txt':
|
||||
case 'md':
|
||||
return '/static/base/view/text.png';
|
||||
|
||||
// 代码文件
|
||||
case 'html':
|
||||
case 'htm':
|
||||
case 'css':
|
||||
case 'js':
|
||||
case 'json':
|
||||
case 'xml':
|
||||
return '/static/base/view/code.png';
|
||||
|
||||
// 默认文件
|
||||
default:
|
||||
return '/static/base/view/more.png';
|
||||
}
|
||||
|
||||
return iconMap[extension.toLowerCase()] || iconMap['default']
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
@ -753,10 +848,14 @@ const uploadImages = async (images: ImageItem[]) => {
|
||||
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 = imagUrl(serverPath)
|
||||
imageList.value[index].url = serverPath
|
||||
// 如果服务器返回了文件名,更新name字段,但保留originalName
|
||||
if (uploadResult.result[0].fileName) {
|
||||
imageList.value[index].name = uploadResult.result[0].fileName
|
||||
}
|
||||
delete imageList.value[index].tempPath
|
||||
delete imageList.value[index].originalPath
|
||||
}
|
||||
@ -821,10 +920,14 @@ const uploadVideos = async (videos: VideoItem[]) => {
|
||||
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 = imagUrl(serverPath)
|
||||
videoList.value[index].url = serverPath
|
||||
// 如果服务器返回了文件名,更新name字段,但保留originalName
|
||||
if (uploadResult.result[0].fileName) {
|
||||
videoList.value[index].name = uploadResult.result[0].fileName
|
||||
}
|
||||
delete videoList.value[index].tempPath
|
||||
}
|
||||
|
||||
@ -941,7 +1044,7 @@ defineExpose({
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.image-video-upload {
|
||||
width: 100%;
|
||||
}
|
||||
@ -1128,6 +1231,11 @@ defineExpose({
|
||||
justify-content: center;
|
||||
margin-right: 16rpx;
|
||||
border: 1rpx solid #dee2e6;
|
||||
|
||||
.icon-image {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
export interface ImageItem {
|
||||
tempPath?: string // 临时路径(用于预览)
|
||||
url?: string // 服务器路径(上传成功后)
|
||||
name?: string // 文件名
|
||||
name?: string // 文件名(临时文件名或服务器文件名)
|
||||
originalName?: string // 原始文件名(用户选择的真实文件名)
|
||||
originalPath?: string // 原始路径(用于调试)
|
||||
isCompressed?: boolean // 是否已压缩
|
||||
}
|
||||
@ -11,7 +12,8 @@ export interface ImageItem {
|
||||
export interface VideoItem {
|
||||
tempPath?: string // 临时路径(用于预览)
|
||||
url?: string // 服务器路径(上传成功后)
|
||||
name?: string // 文件名
|
||||
name?: string // 文件名(临时文件名或服务器文件名)
|
||||
originalName?: string // 原始文件名(用户选择的真实文件名)
|
||||
duration?: number // 视频时长(秒)
|
||||
size?: number // 文件大小(字节)
|
||||
thumbnail?: string // 缩略图路径
|
||||
@ -21,7 +23,8 @@ export interface VideoItem {
|
||||
export interface FileItem {
|
||||
tempPath?: string // 临时路径(用于预览)
|
||||
url?: string // 服务器路径(上传成功后)
|
||||
name?: string // 文件名
|
||||
name?: string // 文件名(临时文件名或服务器文件名)
|
||||
originalName?: string // 原始文件名(用户选择的真实文件名)
|
||||
type?: string // 文件类型(image/video/audio/document等)
|
||||
size?: number // 文件大小(字节)
|
||||
extension?: string // 文件扩展名
|
||||
|
||||
@ -325,6 +325,20 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/jiaoyan/cg/achievementList",
|
||||
"style": {
|
||||
"navigationBarTitleText": "教研成果",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/jiaoyan/cg/achievementForm",
|
||||
"style": {
|
||||
"navigationBarTitleText": "新增成果",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/rw/index",
|
||||
"style": {
|
||||
|
||||
@ -363,6 +363,14 @@ const sections = reactive<Section[]>([
|
||||
permissionKey: "xkjy-jltl",
|
||||
path: "/pages/view/routine/lt/index?ltlx=xkjy",
|
||||
},
|
||||
{
|
||||
id: "gnyy15",
|
||||
icon: "jycg",
|
||||
text: "教研成果",
|
||||
show: true,
|
||||
permissionKey: "xkjy-jycg",
|
||||
path: "/pages/view/routine/jiaoyan/cg/achievementList",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@ -258,7 +258,7 @@ const goBack = () => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.add-member-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);
|
||||
|
||||
882
src/pages/view/routine/jiaoyan/cg/achievementForm.vue
Normal file
882
src/pages/view/routine/jiaoyan/cg/achievementForm.vue
Normal file
@ -0,0 +1,882 @@
|
||||
<template>
|
||||
<view class="achievement-form-page">
|
||||
<!-- 表单内容 -->
|
||||
<scroll-view scroll-y class="form-scroll-view">
|
||||
<view class="form-container">
|
||||
|
||||
<!-- 成果名称 -->
|
||||
<view class="form-section">
|
||||
<view class="section-label">
|
||||
<text class="label-text">成果名称</text>
|
||||
<text class="required-star">*</text>
|
||||
</view>
|
||||
<view class="simple-text-wrapper">
|
||||
<textarea
|
||||
v-model="formData.jycgmc"
|
||||
placeholder="请输入成果名称..."
|
||||
class="simple-textarea"
|
||||
:maxlength="200"
|
||||
auto-height
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 解决的主要问题 -->
|
||||
<view class="form-section">
|
||||
<view class="section-label">
|
||||
<text class="label-text">解决的主要问题</text>
|
||||
<text class="required-star">*</text>
|
||||
</view>
|
||||
<view class="simple-text-wrapper">
|
||||
<textarea
|
||||
v-model="formData.jjdywt"
|
||||
placeholder="请输入解决的主要问题..."
|
||||
class="simple-textarea"
|
||||
:maxlength="2000"
|
||||
auto-height
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最终成果 -->
|
||||
<view class="form-section">
|
||||
<view class="section-label">
|
||||
<text class="label-text">最终成果</text>
|
||||
<text class="required-star">*</text>
|
||||
</view>
|
||||
<view class="simple-text-wrapper">
|
||||
<textarea
|
||||
v-model="formData.zzcg"
|
||||
placeholder="请输入最终成果内容..."
|
||||
class="simple-textarea"
|
||||
:maxlength="5000"
|
||||
auto-height
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加附件 -->
|
||||
<view class="form-section">
|
||||
<view class="section-label">
|
||||
<text class="label-text">添加附件</text>
|
||||
</view>
|
||||
<view class="file-upload-wrapper">
|
||||
<ImageVideoUpload
|
||||
v-model:file-list="fileList"
|
||||
:max-file-count="30"
|
||||
:enable-image="false"
|
||||
:enable-video="false"
|
||||
:enable-file="true"
|
||||
:allowed-file-types="allowedFileTypes"
|
||||
:upload-api="customUploadApi"
|
||||
@file-upload-success="onFileUploadSuccess"
|
||||
@upload-progress="onUploadProgress"
|
||||
@file-upload-error="onFileUploadError"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交人 -->
|
||||
<view class="form-section">
|
||||
<view class="section-label">
|
||||
<text class="label-text">提交人</text>
|
||||
</view>
|
||||
<view class="readonly-input-wrapper">
|
||||
<text class="readonly-text">{{ formData.jsxm }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交时间 -->
|
||||
<view class="form-section">
|
||||
<view class="section-label">
|
||||
<text class="label-text">提交时间</text>
|
||||
</view>
|
||||
<view class="readonly-input-wrapper">
|
||||
<text class="readonly-text">{{ formData.createdTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部固定操作按钮 -->
|
||||
<view class="bottom-actions">
|
||||
<view class="action-btn cancel-btn" @click="cancelForm">
|
||||
<text class="btn-text">取消</text>
|
||||
</view>
|
||||
<view
|
||||
class="action-btn submit-btn"
|
||||
:class="{ 'disabled': isUploading }"
|
||||
@click="submitAchievement"
|
||||
>
|
||||
<text class="btn-text">{{ isEdit ? '保存' : '提交' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 文件上传遮罩层 -->
|
||||
<view v-if="isUploading" class="upload-mask">
|
||||
<view class="upload-content">
|
||||
<view class="upload-icon">
|
||||
<text class="icon-text">📤</text>
|
||||
</view>
|
||||
<view class="upload-title">文件上传中...</view>
|
||||
<view class="upload-status">{{ uploadStatus }}</view>
|
||||
<view class="upload-progress">
|
||||
<view class="progress-bar">
|
||||
<view
|
||||
class="progress-fill"
|
||||
:style="{ width: uploadProgress + '%' }"
|
||||
></view>
|
||||
</view>
|
||||
<text class="progress-text">{{ uploadProgress }}%</text>
|
||||
</view>
|
||||
<view class="upload-tip">请等待文件上传完成后再提交</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { jycgSaveApi, jycgFindByIdApi, jycgUploadFileApi } from "@/api/base/jycgApi";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { ImageVideoUpload, type FileItem } from "@/components/ImageVideoUpload";
|
||||
import { attachmentUpload } from "@/api/system/upload";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { getJs, getUser } = userStore;
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = ref(false);
|
||||
const isLoading = ref(false);
|
||||
|
||||
// 文件上传状态管理
|
||||
const isUploading = ref(false);
|
||||
const uploadProgress = ref(0);
|
||||
const uploadStatus = ref('');
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: '', // 成果ID(编辑时使用)
|
||||
jycgmc: '', // 成果名称
|
||||
jjdywt: '', // 解决的主要问题
|
||||
jjgc: '', // 解决过程
|
||||
zzcg: '', // 最终成果
|
||||
jsId: '', // 教师ID
|
||||
jsxm: '', // 教师姓名(提交人)
|
||||
jyjbId: '', // 教研组ID
|
||||
createdTime: '', // 创建时间(提交时间)
|
||||
fileName: '', // 文件名
|
||||
fileUrl: '', // 文件路径
|
||||
fileFormat: '', // 文件格式
|
||||
remark: '' // 备注
|
||||
});
|
||||
|
||||
// 文件上传数据
|
||||
const fileList = ref<FileItem[]>([]);
|
||||
|
||||
// 允许的文件类型(支持所有格式)
|
||||
const allowedFileTypes = [
|
||||
// 图片格式
|
||||
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'tiff', 'ico',
|
||||
// 视频格式
|
||||
'mp4', 'mov', 'avi', 'wmv', 'flv', 'mkv', 'webm', '3gp', 'm4v',
|
||||
// 音频格式
|
||||
'mp3', 'wav', 'aac', 'ogg', 'flac', 'm4a', 'wma',
|
||||
// 文档格式
|
||||
'pdf', 'doc', 'docx', 'txt', 'rtf', 'wps',
|
||||
// 表格格式
|
||||
'xls', 'xlsx', 'csv', 'et',
|
||||
// 演示格式
|
||||
'ppt', 'pptx', 'dps',
|
||||
// 压缩格式
|
||||
'zip', 'rar', '7z', 'tar', 'gz',
|
||||
// 代码格式
|
||||
'html', 'htm', 'css', 'js', 'json', 'xml'
|
||||
];
|
||||
|
||||
|
||||
|
||||
// 自定义上传API,保持原始文件名
|
||||
const customUploadApi = async (filePath: string) => {
|
||||
try {
|
||||
const result: any = await attachmentUpload(filePath as any);
|
||||
|
||||
console.log('上传接口返回结果:', result);
|
||||
|
||||
// 判断上传是否成功:resultCode === 1 且 success === true
|
||||
if (result && result.resultCode === 1 && result.success === true && result.result && result.result.length > 0) {
|
||||
const uploadedFile = result.result[0];
|
||||
|
||||
// 从文件路径中提取原始文件名(去掉UUID前缀)
|
||||
const originalFileName = filePath.split('/').pop() || 'unknown';
|
||||
|
||||
console.log('文件上传成功,准备返回结果');
|
||||
|
||||
// 返回包含原始文件名的结果
|
||||
return {
|
||||
...result,
|
||||
result: [{
|
||||
...uploadedFile,
|
||||
originalFileName: originalFileName,
|
||||
fileName: originalFileName
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
console.error('上传失败,接口返回:', result);
|
||||
throw new Error(result?.message || '文件上传失败');
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 文件上传成功回调
|
||||
const onFileUploadSuccess = (file: FileItem, index: number) => {
|
||||
console.log('=== 文件上传成功回调 ===');
|
||||
console.log('接收到的文件对象:', file);
|
||||
console.log('文件索引:', index);
|
||||
console.log('当前fileList长度:', fileList.value.length);
|
||||
console.log('当前fileList内容:', fileList.value);
|
||||
|
||||
// 从服务器返回的URL中提取真实文件名
|
||||
if (file.url) {
|
||||
const urlParts = file.url.split('/');
|
||||
const serverFileName = urlParts[urlParts.length - 1];
|
||||
|
||||
console.log('解析的文件信息:', {
|
||||
url: file.url,
|
||||
serverFileName,
|
||||
originalName: file.originalName
|
||||
});
|
||||
|
||||
// 更新文件信息
|
||||
if (fileList.value[index]) {
|
||||
console.log('更新前的文件项:', fileList.value[index]);
|
||||
|
||||
fileList.value[index].url = file.url; // 确保URL被正确设置
|
||||
fileList.value[index].name = serverFileName;
|
||||
// 保持原始文件名用于显示和保存
|
||||
fileList.value[index].originalName = file.originalName || serverFileName;
|
||||
|
||||
console.log('更新后的文件项:', fileList.value[index]);
|
||||
} else {
|
||||
console.error('文件索引超出范围或文件不存在:', index, fileList.value);
|
||||
}
|
||||
|
||||
// 立即测试文件信息收集
|
||||
console.log('=== 文件信息收集测试 ===');
|
||||
console.log('当前文件列表:', fileList.value);
|
||||
console.log('收集的文件URLs:', getFileUrls());
|
||||
console.log('收集的文件名:', getFileNames());
|
||||
console.log('收集的文件格式:', getFileFormats());
|
||||
|
||||
// 检查是否所有文件都已上传完成(真正的上传成功)
|
||||
const allFilesUploaded = fileList.value.every(f => f.url);
|
||||
console.log('所有文件是否已上传:', allFilesUploaded);
|
||||
|
||||
if (allFilesUploaded) {
|
||||
// 延迟关闭遮罩层,让用户看到上传完成的提示
|
||||
setTimeout(() => {
|
||||
isUploading.value = false;
|
||||
uploadProgress.value = 0;
|
||||
uploadStatus.value = '';
|
||||
console.log('所有文件上传成功,关闭遮罩层');
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
console.error('文件URL为空:', file);
|
||||
}
|
||||
};
|
||||
|
||||
// 文件上传进度回调
|
||||
const onUploadProgress = (type: string, current: number, total: number) => {
|
||||
console.log('上传进度:', { type, current, total });
|
||||
isUploading.value = true;
|
||||
uploadProgress.value = Math.round((current / total) * 100);
|
||||
uploadStatus.value = `正在上传文件 ${current}/${total}`;
|
||||
|
||||
// 注意:不在这里关闭遮罩层,因为进度100%不代表接口返回成功
|
||||
// 遮罩层的关闭在 onFileUploadSuccess 回调中处理
|
||||
};
|
||||
|
||||
// 文件上传错误回调
|
||||
const onFileUploadError = (error: any, index: number) => {
|
||||
console.error('文件上传失败:', error, index);
|
||||
uploadStatus.value = '文件上传失败,请重试';
|
||||
|
||||
// 延迟隐藏遮罩层
|
||||
setTimeout(() => {
|
||||
isUploading.value = false;
|
||||
uploadProgress.value = 0;
|
||||
uploadStatus.value = '';
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// 获取文件URL列表
|
||||
const getFileUrls = () => {
|
||||
const urls = fileList.value
|
||||
.filter(file => file.url)
|
||||
.map(file => file.url)
|
||||
.filter((url): url is string => !!url);
|
||||
return urls.length > 0 ? urls.join(',') : '';
|
||||
};
|
||||
|
||||
// 获取文件名列表(不包含扩展名)
|
||||
const getFileNames = () => {
|
||||
const names = fileList.value
|
||||
.filter(file => file.originalName || file.name)
|
||||
.map(file => {
|
||||
const fullName = file.originalName || file.name || '';
|
||||
// 去除扩展名,只保留文件名
|
||||
return fullName.replace(/\.[^/.]+$/, '');
|
||||
})
|
||||
.filter((name): name is string => !!name);
|
||||
return names.length > 0 ? names.join(',') : '';
|
||||
};
|
||||
|
||||
// 获取文件格式列表
|
||||
const getFileFormats = () => {
|
||||
const formats = fileList.value
|
||||
.filter(file => file.originalName || file.name)
|
||||
.map(file => (file.originalName || file.name)?.split('.').pop()?.toLowerCase() || '')
|
||||
.filter(format => !!format);
|
||||
return formats.length > 0 ? formats.join(',') : '';
|
||||
};
|
||||
|
||||
|
||||
// 取消表单
|
||||
const cancelForm = () => {
|
||||
uni.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定要取消并返回吗?未保存的内容将丢失',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateBack();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 提交成果
|
||||
const submitAchievement = async () => {
|
||||
// 检查文件上传状态
|
||||
if (isUploading.value) {
|
||||
uni.showToast({
|
||||
title: '文件正在上传中,请稍候...',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证必填项
|
||||
if (!formData.jycgmc) {
|
||||
uni.showToast({
|
||||
title: '请输入成果名称',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.jjdywt) {
|
||||
uni.showToast({
|
||||
title: '请输入解决的主要问题',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.zzcg) {
|
||||
uni.showToast({
|
||||
title: '请输入最终成果',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: isEdit.value ? '保存中...' : '提交中...' });
|
||||
|
||||
// 收集所有文件URL和文件名
|
||||
const fileUrls = getFileUrls();
|
||||
const fileNames = getFileNames();
|
||||
const fileFormats = getFileFormats();
|
||||
|
||||
console.log('=== 提交前文件信息收集 ===');
|
||||
console.log('当前文件列表:', fileList.value);
|
||||
console.log('文件列表长度:', fileList.value.length);
|
||||
console.log('文件列表详情:', fileList.value.map((file, index) => ({
|
||||
index,
|
||||
url: file.url,
|
||||
name: file.name,
|
||||
originalName: file.originalName,
|
||||
hasUrl: !!file.url,
|
||||
hasOriginalName: !!file.originalName
|
||||
})));
|
||||
|
||||
console.log('收集的文件信息:', {
|
||||
fileUrls,
|
||||
fileNames,
|
||||
fileFormats
|
||||
});
|
||||
console.log('fileName字段(不含扩展名):', fileNames);
|
||||
console.log('fileFormat字段(仅扩展名):', fileFormats);
|
||||
console.log('fileUrl字段(文件路径):', fileUrls);
|
||||
|
||||
// 检查每个文件的信息收集过程
|
||||
fileList.value.forEach((file, index) => {
|
||||
console.log(`文件${index + 1}信息收集:`, {
|
||||
url: file.url,
|
||||
name: file.name,
|
||||
originalName: file.originalName,
|
||||
getFileUrls: file.url ? file.url : '无URL',
|
||||
getFileNames: (file.originalName || file.name) ? (file.originalName || file.name || '').replace(/\.[^/.]+$/, '') : '无名称',
|
||||
getFileFormats: (file.originalName || file.name) ? (file.originalName || file.name || '').split('.').pop()?.toLowerCase() : '无格式'
|
||||
});
|
||||
});
|
||||
|
||||
const params = {
|
||||
...formData,
|
||||
fileUrl: fileUrls,
|
||||
fileName: fileNames,
|
||||
fileFormat: fileFormats, // 添加文件格式
|
||||
status: 'A' // 已发布状态
|
||||
};
|
||||
|
||||
console.log(`${isEdit.value ? '更新' : '新增'}成果参数:`, params);
|
||||
|
||||
await jycgSaveApi(params);
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: isEdit.value ? '保存成功' : '提交成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 返回列表页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error(`${isEdit.value ? '保存' : '提交'}失败:`, error);
|
||||
uni.showToast({
|
||||
title: isEdit.value ? '保存失败' : '提交失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载时接收路由参数
|
||||
onLoad(async (options: any) => {
|
||||
console.log('页面加载参数:', options);
|
||||
|
||||
// 接收教研组ID
|
||||
if (options && options.jyjbId) {
|
||||
formData.jyjbId = options.jyjbId;
|
||||
console.log('接收到的教研组ID:', formData.jyjbId);
|
||||
}
|
||||
|
||||
// 接收成果ID(编辑模式)
|
||||
if (options && options.id) {
|
||||
isEdit.value = true;
|
||||
formData.id = options.id;
|
||||
console.log('编辑模式,成果ID:', formData.id);
|
||||
|
||||
// 加载成果详情
|
||||
await loadAchievementDetail(options.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载成果详情
|
||||
const loadAchievementDetail = async (id: string) => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
uni.showLoading({ title: '加载中...' });
|
||||
|
||||
const response: any = await jycgFindByIdApi({ id });
|
||||
|
||||
if (response && response.result) {
|
||||
const detail = response.result;
|
||||
|
||||
console.log('从后端获取的详情数据:', detail);
|
||||
console.log('文件相关字段:', {
|
||||
fileUrl: detail.fileUrl,
|
||||
fileName: detail.fileName,
|
||||
fileFormat: detail.fileFormat
|
||||
});
|
||||
|
||||
// 回显基本信息
|
||||
formData.id = detail.id || '';
|
||||
formData.jycgmc = detail.jycgmc || '';
|
||||
formData.jjdywt = detail.jjdywt || '';
|
||||
formData.jjgc = detail.jjgc || '';
|
||||
formData.zzcg = detail.zzcg || '';
|
||||
formData.jsId = detail.jsId || '';
|
||||
formData.jsxm = detail.jsxm || '';
|
||||
formData.jyjbId = detail.jyjbId || '';
|
||||
|
||||
// 处理时间格式(保持完整时间戳)
|
||||
if (detail.createdTime) {
|
||||
formData.createdTime = detail.createdTime; // 保持完整时间戳
|
||||
} else {
|
||||
formData.createdTime = '';
|
||||
}
|
||||
|
||||
formData.remark = detail.remark || '';
|
||||
|
||||
// 回显附件
|
||||
console.log('开始回显附件,fileUrl:', detail.fileUrl);
|
||||
if (detail.fileUrl) {
|
||||
const urls = detail.fileUrl.split(',').filter(Boolean);
|
||||
const names = detail.fileName ? detail.fileName.split(',').filter(Boolean) : [];
|
||||
const formats = detail.fileFormat ? detail.fileFormat.split(',').filter(Boolean) : [];
|
||||
|
||||
console.log('解析的文件数据:', {
|
||||
urls,
|
||||
names,
|
||||
formats
|
||||
});
|
||||
|
||||
// 清空现有文件列表
|
||||
fileList.value = [];
|
||||
|
||||
// 回显所有文件
|
||||
urls.forEach((url: string, index: number) => {
|
||||
const fileName = names[index] || url.split('/').pop() || '';
|
||||
const fileFormat = formats[index] || fileName.split('.').pop()?.toLowerCase() || '';
|
||||
// 组合完整的文件名用于显示
|
||||
const fullFileName = fileFormat ? `${fileName}.${fileFormat}` : fileName;
|
||||
|
||||
console.log(`文件${index + 1}:`, {
|
||||
url: url.trim(),
|
||||
fileName,
|
||||
fileFormat,
|
||||
fullFileName
|
||||
});
|
||||
|
||||
fileList.value.push({
|
||||
url: url.trim(),
|
||||
name: fullFileName,
|
||||
originalName: fullFileName, // 回显时使用完整文件名
|
||||
type: 'document', // 默认为文档类型
|
||||
extension: fileFormat
|
||||
});
|
||||
});
|
||||
|
||||
console.log('回显后的文件列表:', fileList.value);
|
||||
} else {
|
||||
console.log('没有文件需要回显');
|
||||
fileList.value = [];
|
||||
}
|
||||
|
||||
console.log('成果详情加载成功:', formData);
|
||||
console.log('附件回显:', { fileList: fileList.value });
|
||||
}
|
||||
|
||||
uni.hideLoading();
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('加载成果详情失败:', error);
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
|
||||
// 加载失败返回列表页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载时初始化
|
||||
onMounted(() => {
|
||||
// 编辑模式下不需要设置默认值(会从详情中获取)
|
||||
if (isEdit.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 新增模式:设置默认日期为今天
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
formData.createdTime = `${year}-${month}-${day}`;
|
||||
|
||||
// 新增模式:设置提交人信息
|
||||
console.log('教师信息 getJs:', getJs);
|
||||
formData.jsId = getJs.id || '';
|
||||
formData.jsxm = getJs.jsxm || '';
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.achievement-form-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
// 表单滚动区域
|
||||
.form-scroll-view {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
padding: 20px 16px 100px 16px; // 底部留出空间给固定按钮
|
||||
}
|
||||
|
||||
// 表单区域
|
||||
.form-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.section-label {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.label-text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.required-star {
|
||||
font-size: 16px;
|
||||
color: #ef4444;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 日期输入
|
||||
.date-input-wrapper {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 12px 16px;
|
||||
|
||||
.date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 只读输入框
|
||||
.readonly-input-wrapper {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 12px 16px;
|
||||
|
||||
.readonly-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
// 简单文本区域
|
||||
.simple-text-wrapper {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
|
||||
.simple-textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #2d3748;
|
||||
background-color: #ffffff;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传
|
||||
.file-upload-wrapper {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
// 底部固定操作按钮
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #e8ecf1;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.btn-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #6b7280;
|
||||
|
||||
&:active {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #10b981;
|
||||
|
||||
&:active {
|
||||
background-color: #059669;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深度选择器样式
|
||||
:deep(.uni-datetime-picker) {
|
||||
width: 100%;
|
||||
|
||||
.uni-date {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.uni-date__x-input {
|
||||
font-size: 14px;
|
||||
color: #2d3748;
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传遮罩层样式
|
||||
.upload-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
|
||||
.upload-content {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 40px 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 280px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.upload-icon {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.icon-text {
|
||||
font-size: 48px;
|
||||
animation: bounce 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-status {
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: #e2e8f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #0ea5e9 0%, #0284c7 100%);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: #0ea5e9;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #a0aec0;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交按钮禁用状态
|
||||
.action-btn.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// 弹跳动画
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
832
src/pages/view/routine/jiaoyan/cg/achievementList.vue
Normal file
832
src/pages/view/routine/jiaoyan/cg/achievementList.vue
Normal file
@ -0,0 +1,832 @@
|
||||
<template>
|
||||
<view class="jycg-page">
|
||||
<!-- 页面标题横幅 -->
|
||||
<view class="page-banner">
|
||||
<view class="banner-content">
|
||||
<view class="banner-title-wrapper">
|
||||
<image class="banner-icon" src="/static/base/view/rgzn.png" mode="aspectFit"></image>
|
||||
<view class="banner-text">
|
||||
<text class="banner-title">教研成果</text>
|
||||
<text class="banner-subtitle">记录和展示您的教研成果</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="research-count">
|
||||
<text class="count-number">{{ achievementList.length }}</text>
|
||||
<text class="count-label">个成果</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计和筛选 -->
|
||||
<view class="stats-bar">
|
||||
<view class="stats-left">
|
||||
<image class="stats-icon" src="/static/base/view/rgzn.png" mode="aspectFit"></image>
|
||||
<text class="stats-text">所有成果</text>
|
||||
<text class="stats-count">{{ achievementList.length }}个</text>
|
||||
</view>
|
||||
<view class="stats-right">
|
||||
<view class="select-wrapper" :class="{ 'select-animate': showSelectHint }">
|
||||
<uni-data-select
|
||||
v-model="searchForm.jyjbId"
|
||||
:localdata="jyjbList"
|
||||
placeholder="选择教研组"
|
||||
@change="handleJyjbChange"
|
||||
@click="handleSelectClick"
|
||||
class="jyjb-select"
|
||||
></uni-data-select>
|
||||
<view class="select-hint" v-if="showSelectHint">
|
||||
<text class="hint-icon">👆</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表组件 -->
|
||||
<view class="list-component">
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="list-scroll-view"
|
||||
@scrolltolower="loadMore"
|
||||
lower-threshold="100"
|
||||
>
|
||||
<view v-if="isLoading && achievementList.length === 0" class="loading-indicator">
|
||||
<text class="loading-icon">⏳</text>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<template v-else-if="achievementList.length > 0">
|
||||
<view v-for="achievement in achievementList" :key="achievement.id" class="achievement-card" @click="viewAchievementDetail(achievement)">
|
||||
<view class="card-badge">
|
||||
<text class="badge-text">教研成果</text>
|
||||
</view>
|
||||
|
||||
<view class="card-header">
|
||||
<view class="achievement-title-wrapper">
|
||||
<text class="achievement-title">{{ achievement.jycgmc }}</text>
|
||||
</view>
|
||||
<view class="arrow-icon">
|
||||
<text class="arrow-text">→</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<view class="achievement-info">
|
||||
<view class="info-item" v-if="achievement.jsxm">
|
||||
<text class="info-icon">👨🏫</text>
|
||||
<text class="info-label">创建教师:</text>
|
||||
<text class="info-value">{{ achievement.jsxm }}</text>
|
||||
</view>
|
||||
<view class="info-item" v-if="achievement.createdTime">
|
||||
<text class="info-icon">📅</text>
|
||||
<text class="info-label">创建时间:</text>
|
||||
<text class="info-value">{{ formatTime(achievement.createdTime) }}</text>
|
||||
</view>
|
||||
<view class="info-item" v-if="achievement.fileName">
|
||||
<text class="info-icon">📎</text>
|
||||
<text class="info-label">附件:</text>
|
||||
<text class="info-value">{{ achievement.fileName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view v-else class="empty-state">
|
||||
<image class="empty-icon" src="/static/base/view/rgzn.png" mode="aspectFit"></image>
|
||||
<text class="empty-text">暂无教研成果</text>
|
||||
<text class="empty-hint">当前没有符合条件的教研成果</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="isLoading && achievementList.length > 0" class="loading-more">
|
||||
<text class="loading-more-icon">⏳</text>
|
||||
<text class="loading-more-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 没有更多数据 -->
|
||||
<view v-if="!hasMore && achievementList.length > 0" class="no-more">
|
||||
<text class="no-more-icon">✓</text>
|
||||
<text class="no-more-text">已加载全部成果</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部固定操作按钮 -->
|
||||
<view class="bottom-actions">
|
||||
<view
|
||||
class="action-button add-achievement-button"
|
||||
@click="addAchievement"
|
||||
>
|
||||
<text class="action-icon">+</text>
|
||||
<text class="action-text">新增成果</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { jycgFindPageApi } from "@/api/base/jycgApi";
|
||||
import { jyjbFindPageApi } from "@/api/base/jyjbApi";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
const { getJs, getUser } = useUserStore();
|
||||
|
||||
interface AchievementItem {
|
||||
id: string;
|
||||
jycgmc: string; // 成果名称
|
||||
jjdywt: string; // 解决的主要问题
|
||||
jjgc: string; // 解决过程
|
||||
zzcg: string; // 最终成果
|
||||
jsId: string; // 创建教师ID
|
||||
jsxm: string; // 教师姓名
|
||||
fileUrl?: string; // 文件路径
|
||||
fileName?: string; // 文件名
|
||||
fileFormat?: string; // 文件格式
|
||||
createdTime: string; // 创建时间
|
||||
status: string; // 状态
|
||||
}
|
||||
|
||||
interface JyjbItem {
|
||||
value: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
jyjbId: '' // 教研组ID
|
||||
});
|
||||
|
||||
// 教研组列表
|
||||
const jyjbList = ref<JyjbItem[]>([]);
|
||||
|
||||
// 数据列表
|
||||
const achievementList = ref<AchievementItem[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const hasMore = ref(true);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
|
||||
// 选择提示动画
|
||||
const showSelectHint = ref(false);
|
||||
|
||||
// 获取教研组列表
|
||||
const loadJyjbList = async () => {
|
||||
try {
|
||||
const response: any = await jyjbFindPageApi({
|
||||
page: 1,
|
||||
rows: 1000
|
||||
});
|
||||
const data = response.rows || [];
|
||||
jyjbList.value = data.map((item: any) => ({
|
||||
value: item.id,
|
||||
text: item.jymc
|
||||
}));
|
||||
|
||||
// 默认选择第一条
|
||||
if (jyjbList.value.length > 0) {
|
||||
searchForm.jyjbId = jyjbList.value[0].value;
|
||||
getAchievementList(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取教研组列表失败:', error);
|
||||
jyjbList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 处理教研组选择变化
|
||||
const handleJyjbChange = (value: string) => {
|
||||
console.log('选择的教研组ID:', value);
|
||||
showSelectHint.value = false; // 选择后隐藏提示
|
||||
getAchievementList(false);
|
||||
};
|
||||
|
||||
// 处理选择框点击
|
||||
const handleSelectClick = () => {
|
||||
showSelectHint.value = false; // 点击时隐藏提示
|
||||
};
|
||||
|
||||
// 获取教研成果列表
|
||||
const getAchievementList = async (isLoadMore = false) => {
|
||||
if (isLoading.value) return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const params: any = {
|
||||
page: isLoadMore ? currentPage.value + 1 : 1,
|
||||
rows: pageSize.value,
|
||||
jyjbId: searchForm.jyjbId
|
||||
};
|
||||
|
||||
// 如果不是 admin 用户,则传递当前教师ID进行过滤
|
||||
const empCode = getUser?.empCode || '';
|
||||
if (empCode !== 'admin') {
|
||||
params.jsId = getJs.id;
|
||||
}
|
||||
|
||||
console.log('API请求参数:', params);
|
||||
const response = await jycgFindPageApi(params);
|
||||
const newData = response.rows || [];
|
||||
|
||||
if (isLoadMore) {
|
||||
achievementList.value.push(...newData);
|
||||
currentPage.value++;
|
||||
} else {
|
||||
achievementList.value = newData;
|
||||
currentPage.value = 1;
|
||||
}
|
||||
|
||||
hasMore.value = newData.length === pageSize.value;
|
||||
} catch (error) {
|
||||
console.error('获取教研成果列表失败:', error);
|
||||
achievementList.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '未设置';
|
||||
return new Date(time).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// 查看/编辑成果详情
|
||||
const viewAchievementDetail = (achievement: AchievementItem) => {
|
||||
console.log('编辑成果:', achievement);
|
||||
console.log('成果文件信息:', {
|
||||
fileUrl: achievement.fileUrl,
|
||||
fileName: achievement.fileName,
|
||||
fileFormat: achievement.fileFormat
|
||||
});
|
||||
// 跳转到表单页面,传递成果ID和教研组ID
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/jiaoyan/cg/achievementForm?id=${achievement.id}&jyjbId=${searchForm.jyjbId}`
|
||||
});
|
||||
};
|
||||
|
||||
// 添加成果
|
||||
const addAchievement = () => {
|
||||
if (!searchForm.jyjbId) {
|
||||
uni.showToast({
|
||||
title: '请先选择教研组',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 将教研组ID传递到表单页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/view/routine/jiaoyan/cg/achievementForm?jyjbId=${searchForm.jyjbId}`
|
||||
});
|
||||
};
|
||||
|
||||
// 加载更多数据
|
||||
const loadMore = () => {
|
||||
if (!isLoading.value && hasMore.value) {
|
||||
getAchievementList(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 页面显示时刷新数据
|
||||
onShow(() => {
|
||||
if (searchForm.jyjbId) {
|
||||
getAchievementList(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载时调用
|
||||
onMounted(() => {
|
||||
loadJyjbList();
|
||||
|
||||
// 延迟显示选择提示动画(无论是否已选择都显示引导)
|
||||
setTimeout(() => {
|
||||
showSelectHint.value = true;
|
||||
console.log('显示选择提示动画');
|
||||
// 3秒后自动隐藏提示
|
||||
setTimeout(() => {
|
||||
showSelectHint.value = false;
|
||||
console.log('隐藏选择提示动画');
|
||||
}, 3000);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.jycg-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #f0f9ff 0%, #f5f7fa 100%);
|
||||
}
|
||||
|
||||
// 页面横幅样式
|
||||
.page-banner {
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
padding: 20px 16px;
|
||||
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
|
||||
|
||||
.banner-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.banner-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.banner-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.banner-subtitle {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.research-count {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 10px 16px;
|
||||
border-radius: 12px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.count-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.count-label {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计和筛选样式
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
gap: 12px;
|
||||
|
||||
.stats-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.stats-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.stats-text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.stats-count {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #0ea5e9;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-right {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
position: relative;
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&.select-animate {
|
||||
animation: selectPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.jyjb-select {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.select-hint {
|
||||
position: absolute;
|
||||
top: 50px; /* 调整到下拉框下方 */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
color: white;
|
||||
padding: 4px 8px; /* 稍微缩小 */
|
||||
border-radius: 12px; /* 稍微缩小 */
|
||||
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
animation: hintBounce 1.5s ease-in-out infinite;
|
||||
|
||||
.hint-icon {
|
||||
font-size: 18px;
|
||||
animation: pointUp 1s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 列表组件样式
|
||||
.list-component {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list-scroll-view {
|
||||
flex: 1;
|
||||
padding: 12px 16px 80px 16px; /* 为底部按钮留出空间 */
|
||||
box-sizing: border-box;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
|
||||
.loading-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #0ea5e9;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 选择框脉冲动画
|
||||
@keyframes selectPulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 0 rgba(14, 165, 233, 0.4);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 8px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 提示气泡弹跳动画
|
||||
@keyframes hintBounce {
|
||||
0%, 100% {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-50%) translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
// 手指向上指动画
|
||||
@keyframes pointUp {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
// 成果卡片样式
|
||||
.achievement-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
|
||||
border-radius: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow:
|
||||
0 2px 16px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(14, 165, 233, 0.1);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
padding: 16px;
|
||||
|
||||
&:active {
|
||||
transform: translateY(2px) scale(0.98);
|
||||
box-shadow:
|
||||
0 1px 8px rgba(0, 0, 0, 0.12),
|
||||
0 0 0 1px rgba(14, 165, 233, 0.15);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 5px;
|
||||
background: linear-gradient(180deg, #0ea5e9 0%, #0284c7 100%);
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
|
||||
.card-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.3);
|
||||
|
||||
.badge-text {
|
||||
font-size: 11px;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.achievement-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.achievement-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
|
||||
border-radius: 8px;
|
||||
|
||||
.arrow-text {
|
||||
font-size: 16px;
|
||||
color: #0ea5e9;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
.achievement-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: linear-gradient(135deg, #f7f9fc 0%, #ffffff 100%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(14, 165, 233, 0.1);
|
||||
|
||||
.info-icon {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 13px;
|
||||
color: #718096;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 13px;
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
|
||||
.empty-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #4a5568;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 13px;
|
||||
color: #a0aec0;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多状态
|
||||
.loading-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 20px;
|
||||
|
||||
.loading-more-icon {
|
||||
font-size: 18px;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-more-text {
|
||||
color: #0ea5e9;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 无更多数据状态
|
||||
.no-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 24px 20px;
|
||||
margin-top: 8px;
|
||||
background: linear-gradient(135deg, #f7f9fc 0%, #e8f0fe 100%);
|
||||
border-radius: 12px;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
|
||||
.no-more-icon {
|
||||
font-size: 16px;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.no-more-text {
|
||||
color: #718096;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部固定操作按钮
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #e8ecf1;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.add-achievement-button {
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 3px 12px rgba(14, 165, 233, 0.35);
|
||||
}
|
||||
|
||||
.add-achievement-button:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.4);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
// 深度选择器用于第三方组件样式调整
|
||||
:deep(.stats-right .uni-data-select) {
|
||||
width: 100%;
|
||||
|
||||
.uni-select {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.uni-select__input-text {
|
||||
color: #212529;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.uni-select__input-placeholder {
|
||||
color: #adb5bd;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.uni-select__selector {
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -655,7 +655,7 @@ const deleteMember = (member: MemberItem) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<view class="page-banner">
|
||||
<view class="banner-content">
|
||||
<view class="banner-title-wrapper">
|
||||
<text class="banner-icon">🔬</text>
|
||||
<image class="banner-icon" src="/static/base/view/rgzn.png" mode="aspectFit"></image>
|
||||
<view class="banner-text">
|
||||
<text class="banner-title">学科教研</text>
|
||||
<text class="banner-subtitle">教学探索 · 实践创新</text>
|
||||
@ -20,7 +20,7 @@
|
||||
<!-- 统计和学期选择 -->
|
||||
<view class="stats-bar">
|
||||
<view class="stats-left">
|
||||
<text class="stats-icon">🔬</text>
|
||||
<image class="stats-icon" src="/static/base/view/rgzn.png" mode="aspectFit"></image>
|
||||
<text class="stats-text">所有教研组</text>
|
||||
<text class="stats-count">{{ researchList.length }}个</text>
|
||||
</view>
|
||||
@ -84,7 +84,7 @@
|
||||
</view>
|
||||
</template>
|
||||
<view v-else class="empty-state">
|
||||
<text class="empty-icon">🔬</text>
|
||||
<image class="empty-icon" src="/static/base/view/rgzn.png" mode="aspectFit"></image>
|
||||
<text class="empty-text">暂无教研组数据</text>
|
||||
<text class="empty-hint">当前没有符合条件的教研组</text>
|
||||
</view>
|
||||
@ -312,8 +312,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.banner-text {
|
||||
@ -378,7 +378,8 @@ onMounted(() => {
|
||||
flex-shrink: 0;
|
||||
|
||||
.stats-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.stats-text {
|
||||
@ -612,7 +613,8 @@ onMounted(() => {
|
||||
padding: 80px 20px;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@ -215,8 +215,8 @@ const checkItems = ref<any[]>([]);
|
||||
// 导入类型和配置
|
||||
import { type ImageItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
|
||||
|
||||
// 压缩配置
|
||||
const compressConfig = ref(COMPRESS_PRESETS.medium)
|
||||
// 压缩配置 - 使用高质量预设,避免过度压缩
|
||||
const compressConfig = ref(COMPRESS_PRESETS.high)
|
||||
|
||||
const imageList = ref<ImageItem[]>([]);
|
||||
const videoList = ref<VideoItem[]>([]);
|
||||
|
||||
@ -340,8 +340,8 @@ const checkItems = ref<any[]>([]);
|
||||
// 导入类型和配置
|
||||
import { type ImageItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
|
||||
|
||||
// 压缩配置
|
||||
const compressConfig = ref(COMPRESS_PRESETS.medium)
|
||||
// 压缩配置 - 使用高质量预设,避免过度压缩
|
||||
const compressConfig = ref(COMPRESS_PRESETS.high)
|
||||
|
||||
const imageList = ref<ImageItem[]>([]);
|
||||
const videoList = ref<VideoItem[]>([]);
|
||||
|
||||
@ -283,7 +283,7 @@ const goBack = () => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.add-member-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);
|
||||
|
||||
@ -696,7 +696,7 @@ const deleteMember = (member: MemberItem) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);
|
||||
|
||||
BIN
src/static/base/home/jycg.png
Normal file
BIN
src/static/base/home/jycg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
BIN
src/static/base/view/image.png
Normal file
BIN
src/static/base/view/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/static/base/view/rgzn.png
Normal file
BIN
src/static/base/view/rgzn.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Loading…
x
Reference in New Issue
Block a user