707 lines
18 KiB
Vue
Raw Normal View History

2025-08-02 11:15:22 +08:00
<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>
2025-08-09 11:36:56 +08:00
<view class="picker-row" @click="showResourceTypeTree">
<text :class="{ placeholder: !formData.resourType }">
{{ getResourceTypeText() || '请选择资源目录' }}
</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
2025-08-02 11:15:22 +08:00
</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>
2025-08-09 11:36:56 +08:00
<!-- 资源目录选择树 -->
<BasicTree
ref="treeRef"
:range="treeData"
idKey="key"
rangeKey="title"
title="选择资源目录"
:multiple="false"
:selectParent="false"
@confirm="onTreeConfirm"
@cancel="onTreeCancel"
/>
2025-08-02 11:15:22 +08:00
</view>
</template>
<script lang="ts" setup>
import { resourcesSaveApi } from "@/api/base/server";
import { typesFindTreeApi } from "@/api/base/server";
import { attachmentUpload } from "@/api/system/upload";
import { imagUrl } from "@/utils";
import { useDicStore } from "@/store/modules/dic";
2025-08-09 11:36:56 +08:00
import BasicTree from "@/components/BasicTree/Tree.vue";
2025-08-02 11:15:22 +08:00
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([]);
2025-08-09 11:36:56 +08:00
// 树组件引用
const treeRef = ref();
2025-08-02 11:15:22 +08:00
// 提交状态
const isSubmitting = ref(false);
// 资源类别选项 - 从字典表获取
const categoryOptions = ref([]);
// 加载资源类别数据
const loadCategoryOptions = async () => {
try {
2025-08-09 11:36:56 +08:00
// 使用正确的字典ID
2025-08-02 11:15:22 +08:00
const result = await findByPid({ pid: 1391443399 });
2025-08-09 11:36:56 +08:00
// 检查返回的数据结构
if (result && result.resultCode === 1 && result.result && Array.isArray(result.result) && result.result.length > 0) {
categoryOptions.value = result.result.map(item => ({
2025-08-02 11:15:22 +08:00
value: item.dictionaryCode,
label: item.dictionaryValue
}));
2025-08-09 11:36:56 +08:00
} 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: '音视频合集' },
];
2025-08-02 11:15:22 +08:00
}
} 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: '音视频合集' },
];
}
};
2025-08-09 11:36:56 +08:00
// 显示资源目录选择树
const showResourceTypeTree = () => {
if (treeRef.value) {
treeRef.value._show();
}
};
// 树形选择确认
const onTreeConfirm = (selectedItems: any[]) => {
if (selectedItems.length > 0) {
const selectedItem = selectedItems[0]; // 单选模式
formData.resourType = selectedItem.key;
}
2025-08-02 11:15:22 +08:00
};
2025-08-09 11:36:56 +08:00
// 树形选择取消
const onTreeCancel = () => {
// 取消选择资源目录
2025-08-02 11:15:22 +08:00
};
// 资源类别选择
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
};
2025-08-09 11:36:56 +08:00
2025-08-02 11:15:22 +08:00
const result = await resourcesSaveApi(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 typesFindTreeApi();
2025-08-09 11:36:56 +08:00
// 处理返回的数据结构确保与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 || [];
}
2025-08-02 11:15:22 +08:00
} catch (error) {
2025-08-09 11:36:56 +08:00
// 如果加载失败,使用空数组
treeData.value = [];
2025-08-02 11:15:22 +08:00
}
};
// 获取资源目录文本
const getResourceTypeText = () => {
2025-08-09 11:36:56 +08:00
// 递归查找选中的项目
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);
2025-08-02 11:15:22 +08:00
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 {
2025-08-09 11:36:56 +08:00
width: 100%;
2025-08-02 11:15:22 +08:00
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>