教研成果调整

This commit is contained in:
hebo 2025-10-20 11:51:15 +08:00
parent ce5c2f05ba
commit f0c85a60f9
17 changed files with 1979 additions and 85 deletions

45
src/api/base/jycgApi.ts Normal file
View 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);
}

View File

@ -72,7 +72,7 @@
</view> </view>
</view> </view>
<view class="video-info"> <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> <text class="video-size" v-if="video.size">{{ formatFileSize(video.size) }}</text>
</view> </view>
</view> </view>
@ -103,10 +103,14 @@
> >
<view class="file-preview" @click="previewFile(index)"> <view class="file-preview" @click="previewFile(index)">
<view class="file-icon"> <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>
<view class="file-info"> <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-size" v-if="file.size">{{ formatFileSize(file.size) }}</text>
<text class="file-type">{{ file.extension?.toUpperCase() || 'FILE' }}</text> <text class="file-type">{{ file.extension?.toUpperCase() || 'FILE' }}</text>
</view> </view>
@ -160,6 +164,7 @@ interface FileItem {
tempPath?: string tempPath?: string
url?: string url?: string
name?: string name?: string
originalName?: string
type?: string type?: string
size?: number size?: number
extension?: string extension?: string
@ -433,40 +438,43 @@ const checkVideoSize = async (filePath: string): Promise<boolean> => {
// //
const chooseImage = () => { const chooseImage = () => {
// 使 chooseImage
uni.chooseImage({ uni.chooseImage({
count: props.maxImageCount - imageList.value.length, count: props.maxImageCount - imageList.value.length,
sizeType: ['original'], sourceType: ['camera', 'album'], //
sourceType: ['album', 'camera'], sizeType: ['original', 'compressed'], //
success: async (res) => { success: async (res) => {
const tempFilePaths = res.tempFilePaths as string[] const tempFilePaths = res.tempFilePaths
if (Array.isArray(tempFilePaths) && tempFilePaths.length > 0) {
showLoading('压缩图片中...') showLoading('压缩图片中...')
try {
const compressedImages = []
// try {
const compressPromises = tempFilePaths.map(async (path, index) => { const compressedImages = []
try {
showLoading(`压缩图片中... (${index + 1}/${tempFilePaths.length})`) //
const compressPromises = tempFilePaths.map(async (filePath: string, index) => {
const compressedPath = await smartCompressImage(path) try {
showLoading(`压缩图片中... (${index + 1}/${tempFilePaths.length})`)
return {
tempPath: compressedPath, const compressedPath = await smartCompressImage(filePath)
name: path.split('/').pop() || 'image.jpg',
originalPath: path, return {
isCompressed: true 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) const results = await Promise.all(compressPromises)
compressedImages.push(...results) compressedImages.push(...results)
@ -496,8 +504,9 @@ const chooseImage = () => {
showToast({ title: '图片处理失败', icon: 'none' }) showToast({ title: '图片处理失败', icon: 'none' })
console.error('图片处理失败:', error) console.error('图片处理失败:', error)
} }
}
}, },
fail: (error) => { fail: (error: any) => {
console.error('选择图片失败:', error) console.error('选择图片失败:', error)
showToast({ title: '选择图片失败', icon: 'none' }) showToast({ title: '选择图片失败', icon: 'none' })
} }
@ -534,6 +543,7 @@ const chooseVideo = () => {
const newVideo = { const newVideo = {
tempPath: tempFilePath, tempPath: tempFilePath,
name: tempFilePath.split('/').pop() || 'video.mp4', name: tempFilePath.split('/').pop() || 'video.mp4',
originalName: res.name || 'video.mp4', //
duration: res.duration, duration: res.duration,
size: res.size size: res.size
} }
@ -584,18 +594,29 @@ const chooseFile = () => {
// //
let fileType = 'document' 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' 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' fileType = 'video'
} else if (['mp3', 'wav', 'aac', 'ogg'].includes(fileExtension)) { } else if (['mp3', 'wav', 'aac', 'ogg', 'flac', 'm4a', 'wma'].includes(fileExtension)) {
fileType = 'audio' 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 = { const fileItem: FileItem = {
tempPath: fileInfo.path, tempPath: fileInfo.path,
name: fileName, name: fileName,
originalName: fileName, //
type: fileType, type: fileType,
size: fileInfo.size, size: fileInfo.size,
extension: fileExtension, extension: fileExtension,
@ -634,15 +655,29 @@ const uploadFile = async (fileItem: FileItem, index: number) => {
if (!props.uploadApi || !fileItem.tempPath) return if (!props.uploadApi || !fileItem.tempPath) return
try { try {
//
emit('upload-progress', 'file', 1, 1)
const result = await props.uploadApi(fileItem.tempPath) const result = await props.uploadApi(fileItem.tempPath)
if (result && result.resultCode === 1 && result.result && result.result.length > 0) { if (result && result.resultCode === 1 && result.result && result.result.length > 0) {
const uploadedFile = result.result[0] const uploadedFile = result.result[0]
// // - 使
fileItem.url = imagUrl(uploadedFile.filePath) fileItem.url = uploadedFile.filePath
// nameoriginalName
if (uploadedFile.fileName) {
fileItem.name = uploadedFile.fileName
}
fileItem.tempPath = undefined // 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) emit('file-upload-success', fileItem, index)
@ -703,32 +738,92 @@ const removeFile = (index: number) => {
// //
const getFileIcon = (extension: string): string => { const getFileIcon = (extension: string): string => {
const iconMap: { [key: string]: string } = { const type = extension.toLowerCase();
// switch (type) {
'pdf': 'book', // PDF
'doc': 'book', case 'pdf':
'docx': 'book', return '/static/base/view/pdf.png';
'txt': 'book',
// // Word
'xls': 'table', case 'doc':
'xlsx': 'table', case 'docx':
// 稿 case 'rtf':
'ppt': 'slideshow', case 'wps': // WPS
'pptx': 'slideshow', return '/static/base/view/word.png';
//
'mp3': 'mic', // Excel
'wav': 'mic', case 'xls':
'aac': 'mic', case 'xlsx':
'ogg': 'mic', 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', case 'zip':
'rar': 'folder', case 'rar':
'7z': 'folder', case '7z':
// case 'tar':
'default': 'paperclip' 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) { if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
const serverPath = uploadResult.result[0].filePath const serverPath = uploadResult.result[0].filePath
// // - 使
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath) const index = imageList.value.findIndex(img => img.tempPath === image.tempPath)
if (index !== -1) { if (index !== -1) {
imageList.value[index].url = imagUrl(serverPath) imageList.value[index].url = serverPath
// nameoriginalName
if (uploadResult.result[0].fileName) {
imageList.value[index].name = uploadResult.result[0].fileName
}
delete imageList.value[index].tempPath delete imageList.value[index].tempPath
delete imageList.value[index].originalPath 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) { if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
const serverPath = uploadResult.result[0].filePath const serverPath = uploadResult.result[0].filePath
// // - 使
const index = videoList.value.findIndex(v => v.tempPath === video.tempPath) const index = videoList.value.findIndex(v => v.tempPath === video.tempPath)
if (index !== -1) { if (index !== -1) {
videoList.value[index].url = imagUrl(serverPath) videoList.value[index].url = serverPath
// nameoriginalName
if (uploadResult.result[0].fileName) {
videoList.value[index].name = uploadResult.result[0].fileName
}
delete videoList.value[index].tempPath delete videoList.value[index].tempPath
} }
@ -941,7 +1044,7 @@ defineExpose({
}) })
</script> </script>
<style scoped> <style lang="scss" scoped>
.image-video-upload { .image-video-upload {
width: 100%; width: 100%;
} }
@ -1128,6 +1231,11 @@ defineExpose({
justify-content: center; justify-content: center;
margin-right: 16rpx; margin-right: 16rpx;
border: 1rpx solid #dee2e6; border: 1rpx solid #dee2e6;
.icon-image {
width: 40rpx;
height: 40rpx;
}
} }
.file-info { .file-info {

View File

@ -2,7 +2,8 @@
export interface ImageItem { export interface ImageItem {
tempPath?: string // 临时路径(用于预览) tempPath?: string // 临时路径(用于预览)
url?: string // 服务器路径(上传成功后) url?: string // 服务器路径(上传成功后)
name?: string // 文件名 name?: string // 文件名(临时文件名或服务器文件名)
originalName?: string // 原始文件名(用户选择的真实文件名)
originalPath?: string // 原始路径(用于调试) originalPath?: string // 原始路径(用于调试)
isCompressed?: boolean // 是否已压缩 isCompressed?: boolean // 是否已压缩
} }
@ -11,7 +12,8 @@ export interface ImageItem {
export interface VideoItem { export interface VideoItem {
tempPath?: string // 临时路径(用于预览) tempPath?: string // 临时路径(用于预览)
url?: string // 服务器路径(上传成功后) url?: string // 服务器路径(上传成功后)
name?: string // 文件名 name?: string // 文件名(临时文件名或服务器文件名)
originalName?: string // 原始文件名(用户选择的真实文件名)
duration?: number // 视频时长(秒) duration?: number // 视频时长(秒)
size?: number // 文件大小(字节) size?: number // 文件大小(字节)
thumbnail?: string // 缩略图路径 thumbnail?: string // 缩略图路径
@ -21,7 +23,8 @@ export interface VideoItem {
export interface FileItem { export interface FileItem {
tempPath?: string // 临时路径(用于预览) tempPath?: string // 临时路径(用于预览)
url?: string // 服务器路径(上传成功后) url?: string // 服务器路径(上传成功后)
name?: string // 文件名 name?: string // 文件名(临时文件名或服务器文件名)
originalName?: string // 原始文件名(用户选择的真实文件名)
type?: string // 文件类型image/video/audio/document等 type?: string // 文件类型image/video/audio/document等
size?: number // 文件大小(字节) size?: number // 文件大小(字节)
extension?: string // 文件扩展名 extension?: string // 文件扩展名

View File

@ -325,6 +325,20 @@
"enablePullDownRefresh": false "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", "path": "pages/view/rw/index",
"style": { "style": {

View File

@ -363,6 +363,14 @@ const sections = reactive<Section[]>([
permissionKey: "xkjy-jltl", permissionKey: "xkjy-jltl",
path: "/pages/view/routine/lt/index?ltlx=xkjy", 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",
},
], ],
}, },
], ],

View File

@ -258,7 +258,7 @@ const goBack = () => {
}; };
</script> </script>
<style scoped> <style lang="scss" scoped>
.add-member-page { .add-member-page {
min-height: 100vh; min-height: 100vh;
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%); background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);

View 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>

View 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
});
// IDID
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>

View File

@ -655,7 +655,7 @@ const deleteMember = (member: MemberItem) => {
}; };
</script> </script>
<style scoped> <style lang="scss" scoped>
.detail-page { .detail-page {
min-height: 100vh; min-height: 100vh;
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%); background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);

View File

@ -4,7 +4,7 @@
<view class="page-banner"> <view class="page-banner">
<view class="banner-content"> <view class="banner-content">
<view class="banner-title-wrapper"> <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"> <view class="banner-text">
<text class="banner-title">学科教研</text> <text class="banner-title">学科教研</text>
<text class="banner-subtitle">教学探索 · 实践创新</text> <text class="banner-subtitle">教学探索 · 实践创新</text>
@ -20,7 +20,7 @@
<!-- 统计和学期选择 --> <!-- 统计和学期选择 -->
<view class="stats-bar"> <view class="stats-bar">
<view class="stats-left"> <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-text">所有教研组</text>
<text class="stats-count">{{ researchList.length }}</text> <text class="stats-count">{{ researchList.length }}</text>
</view> </view>
@ -84,7 +84,7 @@
</view> </view>
</template> </template>
<view v-else class="empty-state"> <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-text">暂无教研组数据</text>
<text class="empty-hint">当前没有符合条件的教研组</text> <text class="empty-hint">当前没有符合条件的教研组</text>
</view> </view>
@ -312,8 +312,8 @@ onMounted(() => {
} }
.banner-icon { .banner-icon {
font-size: 32px; width: 32px;
line-height: 1; height: 32px;
} }
.banner-text { .banner-text {
@ -378,7 +378,8 @@ onMounted(() => {
flex-shrink: 0; flex-shrink: 0;
.stats-icon { .stats-icon {
font-size: 20px; width: 20px;
height: 20px;
} }
.stats-text { .stats-text {
@ -612,7 +613,8 @@ onMounted(() => {
padding: 80px 20px; padding: 80px 20px;
.empty-icon { .empty-icon {
font-size: 64px; width: 64px;
height: 64px;
margin-bottom: 16px; margin-bottom: 16px;
opacity: 0.6; opacity: 0.6;
} }

View File

@ -215,8 +215,8 @@ const checkItems = ref<any[]>([]);
// //
import { type ImageItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload' 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 imageList = ref<ImageItem[]>([]);
const videoList = ref<VideoItem[]>([]); const videoList = ref<VideoItem[]>([]);

View File

@ -340,8 +340,8 @@ const checkItems = ref<any[]>([]);
// //
import { type ImageItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload' 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 imageList = ref<ImageItem[]>([]);
const videoList = ref<VideoItem[]>([]); const videoList = ref<VideoItem[]>([]);

View File

@ -283,7 +283,7 @@ const goBack = () => {
}; };
</script> </script>
<style scoped> <style lang="scss" scoped>
.add-member-page { .add-member-page {
min-height: 100vh; min-height: 100vh;
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%); background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);

View File

@ -696,7 +696,7 @@ const deleteMember = (member: MemberItem) => {
}; };
</script> </script>
<style scoped> <style lang="scss" scoped>
.detail-page { .detail-page {
min-height: 100vh; min-height: 100vh;
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%); background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB