2025-10-29 10:30:04 +08:00

707 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="add-resource-page">
<!-- 表单内容 -->
<scroll-view scroll-y class="form-scroll-view">
<view class="form-container">
<!-- 资源目录 -->
<view class="info-card">
<view class="form-item">
<text class="form-label">资源目录 <text class="required">*</text></text>
<view class="picker-row" @click="showResourceTypeTree">
<text :class="{ placeholder: !formData.resourType }">
{{ getResourceTypeText() || '请选择资源目录' }}
</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- 课题名称 -->
<view class="info-card">
<view class="form-item">
<text class="form-label">课题名称 <text class="required">*</text></text>
<uni-easyinput
v-model="formData.resourName"
placeholder="请输入课题名称"
:inputBorder="false"
class="content-input"
></uni-easyinput>
</view>
</view>
<!-- 课时 -->
<view class="info-card">
<view class="form-item">
<text class="form-label">课时 <text class="required">*</text></text>
<uni-easyinput
v-model="formData.hour"
placeholder="请输入课时"
type="number"
:inputBorder="false"
class="content-input"
></uni-easyinput>
</view>
</view>
<!-- 资源类别 -->
<view class="info-card">
<view class="form-item">
<text class="form-label">资源类别 <text class="required">*</text></text>
<picker
mode="selector"
:range="categoryOptions"
range-key="label"
@change="handleCategoryChange"
>
<view class="picker-row">
<text :class="{ placeholder: !formData.category }">
{{ getCategoryText() || '请选择资源类别' }}
</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</picker>
</view>
</view>
<!-- 资源描述 -->
<view class="info-card">
<view class="form-item">
<text class="form-label">资源描述</text>
<uni-easyinput
type="textarea"
autoHeight
v-model="formData.content"
placeholder="请输入资源描述"
:inputBorder="false"
class="content-input"
></uni-easyinput>
</view>
</view>
<!-- 上传资源 -->
<view class="info-card">
<view class="form-item">
<text class="form-label">上传资源 <text class="required">*</text></text>
<view class="attachment-list">
<view
v-for="(att, index) in formData.attachments"
:key="index"
class="attachment-item"
>
<uni-icons
:type="getAttachmentIcon(att.type)"
size="20"
color="#666"
class="attachment-icon"
></uni-icons>
<text class="attachment-name" @click="previewAttachment(att)">{{
att.name
}}</text>
<uni-icons
type="closeempty"
size="18"
color="#999"
class="remove-icon"
@click="removeAttachment(index)"
></uni-icons>
</view>
</view>
<view class="add-attachment-placeholder" @click="addAttachment">
<view class="add-icon"
><uni-icons type="plusempty" size="20" color="#ccc"></uni-icons
></view>
<text class="placeholder-text">添加图文/视频/文件</text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部提交按钮 -->
<view class="bottom-actions">
<button class="action-btn confirm-btn" @click="handleSubmitForm" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
</view>
<!-- 资源目录选择树 -->
<BasicTree
ref="treeRef"
:range="treeData"
idKey="key"
rangeKey="title"
title="选择资源目录"
:multiple="false"
:selectParent="false"
@confirm="onTreeConfirm"
@cancel="onTreeCancel"
/>
</view>
</template>
<script lang="ts" setup>
import { zymxSaveApi } from "@/api/base/server";
import { zymlFindTreeApi } from "@/api/base/server";
import { attachmentUpload } from "@/api/system/upload";
import { imagUrl } from "@/utils";
import { useDicStore } from "@/store/modules/dic";
import BasicTree from "@/components/BasicTree/Tree.vue";
const { findByPid } = useDicStore();
interface Attachment {
name: string;
type: string;
url: string;
size?: number;
path?: string;
id?: number;
}
// 表单数据
const formData = reactive({
resourName: '',
resourType: '',
category: '',
content: '',
fileId: '',
filePath: '',
resSuf: '',
hour: '',
id: '',
fileName: '',
attachments: [] as Attachment[] // 新增附件列表
});
// 树形数据
const treeData = ref([]);
// 树组件引用
const treeRef = ref();
// 提交状态
const isSubmitting = ref(false);
// 资源类别选项 - 从字典表获取
const categoryOptions = ref([]);
// 加载资源类别数据
const loadCategoryOptions = async () => {
try {
// 使用正确的字典ID
const result = await findByPid({ pid: 1391443399 });
// 检查返回的数据结构
if (result && result.resultCode === 1 && result.result && Array.isArray(result.result) && result.result.length > 0) {
categoryOptions.value = result.result.map(item => ({
value: item.dictionaryCode,
label: item.dictionaryValue
}));
} else {
// 如果没有数据,使用默认选项
categoryOptions.value = [
{ value: '1', label: '课件' },
{ value: '2', label: '教案' },
{ value: '3', label: '学案' },
{ value: '4', label: '作业' },
{ value: '5', label: '试卷' },
{ value: '6', label: '教材' },
{ value: '7', label: '示范课' },
{ value: '8', label: '音视频合集' },
];
}
} catch (error) {
// 如果加载失败,使用默认选项
categoryOptions.value = [
{ value: '1', label: '课件' },
{ value: '2', label: '教案' },
{ value: '3', label: '学案' },
{ value: '4', label: '作业' },
{ value: '5', label: '试卷' },
{ value: '6', label: '教材' },
{ value: '7', label: '示范课' },
{ value: '8', label: '音视频合集' },
];
}
};
// 显示资源目录选择树
const showResourceTypeTree = () => {
if (treeRef.value) {
treeRef.value._show();
}
};
// 树形选择确认
const onTreeConfirm = (selectedItems: any[]) => {
if (selectedItems.length > 0) {
const selectedItem = selectedItems[0]; // 单选模式
formData.resourType = selectedItem.key;
}
};
// 树形选择取消
const onTreeCancel = () => {
// 取消选择资源目录
};
// 资源类别选择
const handleCategoryChange = (e: any) => {
const index = e.detail.value;
const selectedItem = categoryOptions.value[index];
formData.category = selectedItem.value;
};
// 添加附件
const addAttachment = () => {
uni.chooseFile({
count: 5,
type: 'all',
success: async (res) => {
const tempFiles = res.tempFiles;
if (Array.isArray(tempFiles) && tempFiles.length > 0) {
uni.showLoading({ title: '上传中...' });
try {
for (const file of tempFiles) {
const fileInfo = file as any;
let fileType = 'file';
const fileName = fileInfo.name || '';
const fileExtension = fileName.split('.').pop()?.toLowerCase();
// 根据文件扩展名判断类型
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(fileExtension || '')) {
fileType = 'image';
} else if (['mp4', 'mov', 'avi', 'wmv', 'flv'].includes(fileExtension || '')) {
fileType = 'video';
} else if (['mp3', 'wav', 'aac', 'ogg'].includes(fileExtension || '')) {
fileType = 'audio';
}
// 根据MIME类型判断
if (fileInfo.type && typeof fileInfo.type === 'string' &&
(fileInfo.type.startsWith('image/') || fileInfo.type.startsWith('video/') || fileInfo.type.startsWith('audio/'))) {
fileType = fileInfo.type.split('/')[0];
}
// 先添加到附件列表
formData.attachments.push({
name: fileName,
type: fileType,
url: '',
size: fileInfo.size,
path: fileInfo.path,
id: Date.now()
});
// 上传文件
await uploadFile(fileInfo);
}
uni.showToast({ title: '附件上传完成', icon: 'success' });
} catch (error) {
console.error('附件上传失败:', error);
uni.showToast({ title: '附件上传失败', icon: 'error' });
} finally {
uni.hideLoading();
}
}
},
fail: (err) => {
console.error('选择附件失败:', err);
if (err.errMsg && !err.errMsg.includes('cancel')) {
uni.showToast({ title: '选择附件失败', icon: 'none' });
}
}
});
};
// 移除附件
const removeAttachment = (index: number) => {
formData.attachments.splice(index, 1);
};
// 预览附件
const previewAttachment = (attachment: Attachment) => {
// 如果是图片类型,可以预览
if (attachment.type === 'image') {
const fullUrl = imagUrl(attachment.url);
uni.previewImage({
urls: [fullUrl],
current: fullUrl,
});
} else {
uni.showToast({
title: `预览 ${attachment.name} 功能待实现`,
icon: 'none',
});
}
};
// 获取附件图标
const getAttachmentIcon = (type: string): string => {
if (type === 'image') return 'image';
if (type === 'video') return 'videocam';
if (type === 'audio') return 'mic';
return 'paperclip';
};
// 上传文件
const uploadFile = async (file: any) => {
uni.showLoading({ title: '上传中...' });
try {
// 使用 attachmentUpload 接口
const uploadResult: any = await attachmentUpload(file.path as any);
if (uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
// 保存原始的 filePath用于提交到服务器
const originalPath = uploadResult.result[0].filePath;
const fileId = uploadResult.result[0].id;
const fileType = uploadResult.result[0].fileType;
// 更新最后一个附件的信息
const lastAttachment = formData.attachments[formData.attachments.length - 1];
if (lastAttachment) {
lastAttachment.url = originalPath;
lastAttachment.id = fileId;
}
// 同时更新原有的文件信息(保持兼容性)
formData.fileId = fileId;
formData.filePath = originalPath;
formData.resSuf = fileType;
formData.fileName = file.name;
uni.showToast({ title: '上传成功', icon: 'success' });
} else {
throw new Error('上传失败');
}
} catch (error) {
console.error('上传失败:', error);
uni.showToast({ title: '上传失败', icon: 'none' });
// 移除上传失败的附件
formData.attachments.pop();
} finally {
uni.hideLoading();
}
};
// 提交表单
const handleSubmitForm = async () => {
// 表单验证
if (!formData.resourName.trim()) {
uni.showToast({ title: '请输入课题名称', icon: 'none' });
return;
}
if (!formData.resourType) {
uni.showToast({ title: '请选择资源目录', icon: 'none' });
return;
}
if (!formData.hour.trim()) {
uni.showToast({ title: '请输入课时', icon: 'none' });
return;
}
if (!formData.category) {
uni.showToast({ title: '请选择资源类别', icon: 'none' });
return;
}
if (formData.attachments.length === 0) {
uni.showToast({ title: '请上传资源文件', icon: 'none' });
return;
}
if (isSubmitting.value) {
return;
}
isSubmitting.value = true;
try {
const params = {
resourName: formData.resourName,
resourType: formData.resourType,
category: formData.category,
remark: formData.content,
resourId: formData.fileId, // 保留原有的文件ID
resourUrl: formData.filePath, // 保留原有的文件路径
resSuf: formData.resSuf, // 保留原有的文件后缀
hour: formData.hour,
id: formData.id
};
const result = await zymxSaveApi(params);
if (result.resultCode === 1) {
uni.showToast({ title: '操作成功', icon: 'success' });
// 返回上一页并刷新列表
setTimeout(() => {
uni.navigateBack();
// 通过事件总线通知列表页面刷新
uni.$emit('refreshResourceList');
}, 1500);
} else {
uni.showToast({ title: '操作失败', icon: 'none' });
}
} catch (error) {
console.error('提交失败:', error);
uni.showToast({ title: '操作失败,请重试', icon: 'none' });
} finally {
isSubmitting.value = false;
}
};
// 重置表单数据
const resetFormData = () => {
formData.resourName = '';
formData.resourType = '';
formData.category = '';
formData.content = '';
formData.fileId = '';
formData.filePath = '';
formData.resSuf = '';
formData.hour = '';
formData.id = '';
formData.fileName = '';
formData.attachments = []; // 重置附件列表
};
// 加载树形数据
const loadTreeData = async () => {
try {
const res = await zymlFindTreeApi();
// 处理返回的数据结构确保与BasicTree组件兼容
if (res && Array.isArray(res)) {
treeData.value = res.map(item => ({
key: item.key || item.id,
title: item.title || item.name,
children: item.children ? item.children.map(child => ({
key: child.key || child.id,
title: child.title || child.name,
children: child.children || []
})) : []
}));
} else {
treeData.value = res.result || [];
}
} catch (error) {
// 如果加载失败,使用空数组
treeData.value = [];
}
};
// 获取资源目录文本
const getResourceTypeText = () => {
// 递归查找选中的项目
const findSelectedItem = (items: any[], targetKey: string): any => {
for (const item of items) {
if (item.key === targetKey) {
return item;
}
if (item.children && item.children.length > 0) {
const found = findSelectedItem(item.children, targetKey);
if (found) return found;
}
}
return null;
};
const selectedItem = findSelectedItem(treeData.value, formData.resourType);
return selectedItem ? selectedItem.title : '';
};
// 获取资源类别文本
const getCategoryText = () => {
const selectedItem = categoryOptions.value.find(item => item.value === formData.category);
return selectedItem ? selectedItem.label : '';
};
onMounted(async () => {
loadTreeData();
loadCategoryOptions(); // 加载资源类别选项
});
</script>
<style scoped lang="scss">
.add-resource-page {
min-height: 100vh;
background-color: #f4f5f7;
display: flex;
flex-direction: column;
}
.form-scroll-view {
flex: 1;
}
.form-container {
padding: 30rpx;
}
.info-card {
background: white;
border-radius: 12rpx;
margin-bottom: 20rpx;
padding: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.form-item {
margin-bottom: 15rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 10rpx;
display: block;
}
.required {
color: #ff3b30;
}
.content-input {
font-size: 28rpx;
color: #333;
:deep(.uni-easyinput__content) {
background: transparent;
}
:deep(.uni-easyinput__content-input) {
color: #333;
}
:deep(.uni-easyinput__placeholder-class) {
color: #999;
}
}
.picker-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
color: #333;
font-size: 28rpx;
}
.placeholder {
color: #999;
}
.attachment-list {
margin-bottom: 12px;
}
.attachment-item {
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 6px;
padding: 8px 12px;
margin-bottom: 8px;
border: 1px solid #e9ecef;
.attachment-icon {
margin-right: 8px;
flex-shrink: 0;
}
.attachment-name {
flex-grow: 1;
font-size: 14px;
color: #495057;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 10px;
cursor: pointer;
}
.remove-icon {
flex-shrink: 0;
cursor: pointer;
opacity: 0.7;
&:hover {
opacity: 1;
color: #dc3545 !important;
}
}
}
.add-attachment-placeholder {
display: flex;
align-items: center;
border: 1px dashed #d5d8de;
border-radius: 6px;
padding: 15px;
background-color: #f8f8f8;
cursor: pointer;
transition: background-color 0.2s;
.add-icon {
width: 30px;
height: 30px;
border: 1px solid #d5d8de;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 12px;
background-color: #fff;
.uni-icons {
color: #999 !important;
}
}
.placeholder-text {
font-size: 14px;
color: #909399;
}
&:active {
background-color: #eee;
}
}
.bottom-actions {
display: flex;
gap: 20rpx;
padding: 30rpx;
background: white;
border-top: 1rpx solid #f0f0f0;
}
.confirm-btn {
width: 100%;
padding: 20rpx;
border-radius: 8rpx;
font-size: 28rpx;
font-weight: 500;
border: none;
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
color: white;
box-shadow: 0 2rpx 8rpx rgba(0, 122, 255, 0.3);
}
.confirm-btn:disabled {
opacity: 0.6;
}
</style>