Commit e3c503d5 authored by lijin's avatar lijin

修复图片上传

parent ef53e082
......@@ -107,7 +107,7 @@ export const constantRouterMap = [
{
path: "/tools/Youtube",
name: "tools.Youtube",
component: () => import("@/views/tools/YouTubeAuth"),
component: () => import("@/views/createMaterial/AdMaterialManager"),
meta: { title: "Youtube管理", icon: "chart" }
}
]
......
<template>
<div class="el-image-uploader">
图片上传组件
<el-upload
ref="upload"
action
:headers="headers"
list-type="picture-card"
:multiple="true"
:on-preview="handlePreview"
:on-remove="handleRemove"
:on-error="handleError"
:before-upload="beforeUpload"
:http-request="uploadMulFile"
:file-list="fileList"
:data="uploadData"
>
<i class="el-icon-plus"></i>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过5MB</div>
</el-upload>
<!-- 图片预览对话框 -->
<el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="Preview">
</el-dialog>
</div>
</template>
<script>
import ossClient from "@/utils/ossClient";
import md5 from "md5";
export default {
name: 'ImageUploader',
props: {
// 上传时附带的额外参数
uploadData: {
type: Object,
default: () => ({})
},
// 上传的请求头
headers: {
type: Object,
default: () => ({})
},
// 最大文件大小(MB)
maxSize: {
type: Number,
default: 5
}
},
data() {
return {
fileList: [], // 文件列表
dialogImageUrl: '', // 预览图片URL
dialogVisible: false, // 预览对话框显示状态
uploadConf: {
region: null,
accessKeyId: null,
accessKeySecret: null,
bucket: null,
// stsToken: null,
},
}
},
created() {
this.initOssInfo();
},
methods: {
// 处理图片预览
handlePreview(file) {
this.dialogImageUrl = file.url
this.dialogVisible = true
},
//初始化从后端获取到 TST账户对OSS进行操作 在created初始化
initOssInfo() {
//获取阿里云token 这里是后台返回来的数据 暂时使用死数据
const aliyun = {
// "你的Region 注意 这个只要 空间名 不要 http:// 和 .aliyunoss.com !!",
Region: "oss-cn-beijing",
AccessKeyId: "LTAI5tJzo2DiQxQqh9EipDNh",
AccessKeySecret: "JnduhWY5Tr5VfuFHgDOv9XjqYx1mDg",
Bucket: "zx-material-new",
//"你的SecurityToken"
// SecurityToken: null,
};
const {
Region,
AccessKeyId,
AccessKeySecret,
Bucket,
// SecurityToken,
} = aliyun;
//初始化连接oss参数
this.uploadConf.region = Region;
this.uploadConf.accessKeyId = AccessKeyId;
this.uploadConf.accessKeySecret = AccessKeySecret;
this.uploadConf.bucket = Bucket;
// this.uploadConf.stsToken = SecurityToken;
},
// 处理图片删除
handleRemove(file, fileList) {
this.fileList = fileList
this.$emit('change', this.getFiles())
},
// 处理上传失败
handleError(err, file) {
this.$message.error('上传失败:' + (err.message || '未知错误'))
},
// 上传前的验证
beforeUpload(file) {
// 验证文件类型
const isImage = file.type.startsWith('image/')
if (!isImage) {
this.$message.error('只能上传图片文件!')
return false
}
// 验证文件大小
const isLtMaxSize = file.size / 1024 / 1024 < this.maxSize
if (!isLtMaxSize) {
this.$message.error(`图片大小不能超过 ${this.maxSize}MB!`)
return false
}
return true
},
// 获取文件列表
getFiles() {
return this.fileList.map(file => ({
name: file.name,
url: file.url,
size: file.size,
raw: file.raw
}))
},
// 清空上传列表
clearFiles() {
this.$refs.upload.clearFiles()
this.fileList = []
this.$emit('change', [])
},
// 手动上传
submit() {
this.$refs.upload.submit()
},
//上传到阿里云OSS
uploadMulFile(uploader) {
console.log("文件信息", uploader);
//获取到文件的信息内容
let file = uploader.file;
//获取文件类型 和 上传文件的日期
let fileType = file.name.split(".").pop();
if (file.name.split(".").length === 1) {
fileType = "jpg";
}
let curTime = new Date();
//定义上传到云端的路径和文件名字
let fileName = md5(file.name + Date.now()) +
"." +
fileType;
let upFilePath =
"ad_putin_materials" +
"/" +
curTime.getFullYear() +
"/" +
(curTime.getMonth() + 1) +
"/" +
curTime.getDate() +
"/" + fileName
//分片上传配置,回显进度以及上传总进度重置,分片大小,超时设置
let optionsFile = {
progress: function (p) {
uploader.onProgress({ percent: Math.floor(p * 10000) / 100 });
},
partSize: 1000 * 1024, //设置分片大小 小于1m 1024*1024
timeout: 36000000, //设置超时时间 - 10小时
};
//实例化上传对象
let client = ossClient(this.uploadConf);
/* 分片上传 显示上传进度 */
const that = this;
//测试地址替换为 "test/" +upFilePath
client
.multipartUpload(upFilePath, file, optionsFile)
.then((res) => {
//解析url并替换前缀可访问
let strUrl = res.res.requestUrls[0].split("?")[0];
strUrl = strUrl.replace(
"http://zx-material-new.oss-cn-beijing.aliyuncs.com",
"https://cdn.zhangxingames.com"
);
that.fileList = [...that.fileList, { uid: file.uid, url: strUrl, name: fileName }];
uploader.onSuccess(); //上传成功(打钩的小图标)
client = null;
this.$emit('change', this.getFiles())
})
.catch((err) => {
that.$message.warning(`上传终止,提示:${err}`);
console.log(err);
if (client.isCancel()) {
uploader.onError();
}
});
},
}
}
</script>
<style>
.el-image-uploader {
padding: 20px;
}
.el-upload__tip {
margin-top: 10px;
color: #909399;
}
</style>
......@@ -615,21 +615,8 @@
<div class="drawer-item-title">制作创意</div>
<div class="drawer-item-con">
<el-form ref="form">
<el-form-item label="选择素材组">
<el-select
v-model="makeCreative.materialGroupId"
placeholder="选择素材组"
style="width: 200px"
>
<el-option
v-for="item in materialList"
:key="item.id"
:label="item.materialGroupName"
:value="item.id"
>
</el-option>
</el-select>
</el-form-item>
<ImageUploader @change="handleUploadChange"/>
</el-form>
</div>
</div>
......@@ -721,6 +708,7 @@ import TextTextarea from "./childComponents/TextTextarea";
import CountrySelector from "./childComponents/CountrySelector";
import LanguageSelector from "./childComponents/LanguageSelector";
import TextInputList from "./childComponents/TextInputList";
import ImageUploader from "./childComponents/ImageUploader.vue";
import { stepList } from "./childComponents/util";
......@@ -736,6 +724,7 @@ export default {
TextTextarea,
CountrySelector,
TextInputList,
ImageUploader
}, // 注册
data() {
......@@ -2019,6 +2008,10 @@ export default {
filterAccount(query, item){
query = query.toLowerCase()
return item.key.toString().toLowerCase().includes(query)||item.label.toString().toLowerCase().includes(query)
},
handleUploadChange(files){
console.log("aabbccddeeff", files)
this.putinTask.imageAssets = files
}
},
};
......
<template>
<div class="ad-material-manager">
<el-container>
<!-- 左侧目录树 -->
<el-aside width="250px" class="aside">
<div class="directory-header">
<span>素材目录</span>
<el-button
type="text"
size="small"
@click="showNewDirDialog(null)"
>
<i class="el-icon-plus"></i> 新建目录
</el-button>
</div>
<el-tree
ref="directoryTree"
:data="directories"
:props="defaultProps"
node-key="id"
:expand-on-click-node="false"
@node-click="handleNodeClick"
>
<span slot-scope="{ node, data }" class="custom-tree-node">
<span>{{ node.label }}</span>
<span class="directory-actions">
<el-button
type="text"
size="mini"
@click.stop="showNewDirDialog(data.id)"
>
<i class="el-icon-plus"></i>
</el-button>
<el-button
type="text"
size="mini"
@click.stop="handleDeleteDirectory(node, data)"
>
<i class="el-icon-delete"></i>
</el-button>
</span>
</span>
</el-tree>
</el-aside>
<!-- 右侧内容区 -->
<el-main class="main">
<div class="content-header">
<div class="current-path">
当前位置:{{ currentPath }}
</div>
<el-upload
ref="upload"
:action="uploadUrl"
:data="{ directoryId: currentDirectory }"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:before-upload="beforeUpload"
multiple
:show-file-list="false"
>
<el-button type="primary" :disabled="!currentDirectory">
<i class="el-icon-upload"></i> 上传文件
</el-button>
</el-upload>
</div>
<!-- 文件列表 -->
<el-table
v-loading="loading"
:data="materials"
style="width: 100%"
>
<el-table-column label="预览" width="120">
<template slot-scope="scope">
<div class="preview-container">
<img
v-if="scope.row.type === 'image'"
:src="scope.row.url"
class="preview-image"
>
<video
v-else-if="scope.row.type === 'video'"
:src="scope.row.url"
class="preview-video"
controls
>
</video>
</div>
</template>
</el-table-column>
<el-table-column prop="name" label="文件名"></el-table-column>
<el-table-column prop="type" label="类型" width="100">
<template slot-scope="scope">
{{ scope.row.type === 'image' ? '图片' : '视频' }}
</template>
</el-table-column>
<el-table-column prop="size" label="大小" width="100">
<template slot-scope="scope">
{{ formatFileSize(scope.row.size) }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="上传时间" width="180">
<template slot-scope="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
@click="showMoveDialog(scope.row)"
>移动</el-button>
<el-button
size="mini"
type="text"
@click="handlePreview(scope.row)"
>预览</el-button>
<el-button
size="mini"
type="text"
class="delete-btn"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
</el-container>
<!-- 新建目录弹窗 -->
<el-dialog
:title="parentId ? '新建子目录' : '新建目录'"
:visible.sync="newDirDialogVisible"
width="30%"
>
<el-form :model="newDirForm" ref="newDirForm" :rules="dirFormRules">
<el-form-item label="目录名称" prop="name">
<el-input v-model="newDirForm.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="newDirDialogVisible = false">取消</el-button>
<el-button type="primary" @click="createDirectory">确定</el-button>
</div>
</el-dialog>
<!-- 移动文件弹窗 -->
<el-dialog
title="移动到"
:visible.sync="moveDialogVisible"
width="30%"
>
<el-tree
ref="moveTree"
:data="directories"
:props="defaultProps"
node-key="id"
:default-expand-all="true"
@node-click="handleMoveNodeClick"
></el-tree>
<div slot="footer">
<el-button @click="moveDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmMove">确定</el-button>
</div>
</el-dialog>
<!-- 预览弹窗 -->
<el-dialog
:visible.sync="previewDialogVisible"
width="50%"
class="preview-dialog"
>
<div class="preview-content">
<img
v-if="previewFile && previewFile.type === 'image'"
:src="previewFile.url"
class="preview-image"
>
<video
v-else-if="previewFile && previewFile.type === 'video'"
:src="previewFile.url"
controls
class="preview-video"
></video>
</div>
</el-dialog>
</div>
</template>
<script>
import { MockService } from './mockService'
export default {
name: 'AdMaterialManager',
data() {
return {
// 目录树数据
directories: [],
mockService: null,
defaultProps: {
children: 'children',
label: 'name'
},
currentDirectory: null, // 当前选中的目录ID
currentPath: '根目录', // 当前路径
// 文件列表数据
materials: [],
loading: false,
// 新建目录相关
newDirDialogVisible: false,
newDirForm: {
name: '',
parentId: null
},
dirFormRules: {
name: [
{ required: true, message: '请输入目录名称', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
]
},
parentId: null,
// 移动文件相关
moveDialogVisible: false,
selectedMoveDirectory: null,
currentMoveFile: null,
// 预览相关
previewDialogVisible: false,
previewFile: null,
// 上传相关
uploadUrl: '/api/materials/upload'
}
},
created() {
this.mockService = new MockService()
this.fetchDirectories()
},
methods: {
// 获取目录树数据
async fetchDirectories() {
try {
const directories = await this.mockService.getDirectories()
this.directories = directories
} catch (error) {
this.$message.error('获取目录失败')
console.error('获取目录失败:', error)
}
},
// 获取当前目录下的文件
async fetchMaterials(directoryId) {
this.loading = true
try {
const materials = await this.mockService.getMaterials(directoryId)
this.materials = materials
} catch (error) {
this.$message.error('获取文件列表失败')
console.error('获取文件列表失败:', error)
} finally {
this.loading = false
}
},
// 处理目录点击
handleNodeClick(data) {
this.currentDirectory = data.id
this.currentPath = this.getNodePath(data)
this.fetchMaterials(data.id)
},
// 获取节点路径
getNodePath(node) {
const path = []
let currentNode = node
while (currentNode) {
path.unshift(currentNode.name)
currentNode = currentNode.parent
}
return path.join(' / ')
},
// 显示新建目录弹窗
showNewDirDialog(parentId) {
this.parentId = parentId
this.newDirForm.name = ''
this.newDirForm.parentId = parentId
this.newDirDialogVisible = true
},
// 创建目录
async createDirectory() {
this.$refs.newDirForm.validate(async (valid) => {
if (valid) {
try {
await this.mockService.createDirectory({
name: this.newDirForm.name,
parentId: this.newDirForm.parentId
})
this.$message.success('创建成功')
this.newDirDialogVisible = false
await this.fetchDirectories() // 重新获取目录树
} catch (error) {
this.$message.error('创建失败')
console.error('创建目录失败:', error)
}
}
})
},
// 删除目录
handleDeleteDirectory(node, data) {
this.$confirm('确认删除该目录吗?其下的所有文件将被移动到根目录', '提示', {
type: 'warning'
}).then(async () => {
try {
await this.mockService.deleteDirectory(data.id)
this.$message.success('删除成功')
await this.fetchDirectories() // 重新获取目录树
// 如果删除的是当前选中的目录,清空文件列表
if (data.id === this.currentDirectory) {
this.materials = []
this.currentDirectory = null
this.currentPath = '根目录'
}
} catch (error) {
this.$message.error('删除失败')
console.error('删除目录失败:', error)
}
}).catch(() => {})
},
// 上传前验证
beforeUpload(file) {
const isImage = file.type.startsWith('image/')
const isVideo = file.type.startsWith('video/')
const isLt100M = file.size / 1024 / 1024 < 100
if (!isImage && !isVideo) {
this.$message.error('只能上传图片或视频文件!')
return false
}
if (!isLt100M) {
this.$message.error('文件大小不能超过 100MB!')
return false
}
return true
},
// 处理上传成功
async handleUploadSuccess(response, file, fileList) {
try {
const result = await this.mockService.uploadFile(file.raw, this.currentDirectory)
if (result.code === 0) {
this.$message.success('上传成功')
await this.fetchMaterials(this.currentDirectory) // 重新获取文件列表
} else {
this.$message.error(result.message || '上传失败')
}
} catch (error) {
this.$message.error('上传失败')
console.error('上传文件失败:', error)
}
},
// 处理上传失败
handleUploadError(err) {
this.$message.error('上传失败')
console.error('上传文件失败:', err)
},
// 显示移动文件弹窗
showMoveDialog(file) {
this.currentMoveFile = file
this.selectedMoveDirectory = null
this.moveDialogVisible = true
},
// 处理移动目标目录选择
handleMoveNodeClick(data) {
this.selectedMoveDirectory = data.id
},
// 确认移动文件
async confirmMove() {
if (!this.selectedMoveDirectory) {
this.$message.warning('请选择目标目录')
return
}
try {
await this.mockService.moveFile(
this.currentMoveFile.id,
this.selectedMoveDirectory
)
this.$message.success('移动成功')
this.moveDialogVisible = false
await this.fetchMaterials(this.currentDirectory) // 重新获取当前目录的文件列表
} catch (error) {
this.$message.error('移动失败')
console.error('移动文件失败:', error)
}
},
// 删除文件
handleDelete(file) {
this.$confirm('确认删除该文件吗?', '提示', {
type: 'warning'
}).then(async () => {
try {
await this.mockService.deleteFile(file.id)
this.$message.success('删除成功')
await this.fetchMaterials(this.currentDirectory) // 重新获取文件列表
} catch (error) {
this.$message.error('删除失败')
console.error('删除文件失败:', error)
}
}).catch(() => {})
},
// 预览文件
handlePreview(file) {
this.previewFile = file
this.previewDialogVisible = true
},
// 工具方法 - 格式化文件大小
formatFileSize(size) {
if (size < 1024) {
return size + ' B'
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + ' KB'
} else {
return (size / 1024 / 1024).toFixed(2) + ' MB'
}
},
// 工具方法 - 格式化日期
formatDate(date) {
return new Date(date).toLocaleString()
}
}
}
</script>
<style scoped>
.ad-material-manager {
height: 100%;
min-height: 500px;
}
.el-container {
height: 100%;
}
.aside {
background-color: #f5f7fa;
border-right: 1px solid #e6e6e6;
}
.directory-header {
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e6e6e6;
}
.main {
padding: 20px;
}
.content-header {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.current-path {
font-size: 14px;
color: #606266;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.directory-actions {
opacity: 0;
transition: opacity 0.2s;
}
.custom-tree-node:hover .directory-actions {
opacity: 1;
}
.preview-container {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 4px;
border: 1px solid #e6e6e6;
}
.preview-image,
.preview-video {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.delete-btn {
color: #f56c6c;
}
.delete-btn:hover {
color: #f78989;
}
.preview-dialog .preview-content {
display: flex;
justify-content: center;
align-items: center;
}
.preview-dialog .preview-image {
max-width: 100%;
max-height: 70vh;
}
.preview-dialog .preview-video {
max-width: 100%;
max-height: 70vh;
}
</style>
......@@ -10,9 +10,6 @@
<el-tab-pane label="文案素材" name="copywriting">
<copywritinglibrary v-if="isCopywriting"></copywritinglibrary>
</el-tab-pane>
<el-tab-pane label="素材推荐" name="materialRecommend">
<material-recommend v-if="isRecommend" />
</el-tab-pane>
</el-tabs>
</div>
</template>
......@@ -30,7 +27,6 @@ export default {
materialGroup,
copywritinglibrary,
materiallibrary,
MaterialRecommend,
},
data() {
return {
......
// 生成唯一ID
const generateId = () => Math.random().toString(36).substr(2, 9);
// 初始化本地存储数据
const initLocalStorage = () => {
if (!localStorage.getItem('directories')) {
localStorage.setItem('directories', JSON.stringify([
{
id: 'root',
name: '根目录',
parentId: null,
children: [
{
id: 'dir1',
name: '2024投放素材',
parentId: 'root',
children: [
{
id: 'dir1-1',
name: '春节活动',
parentId: 'dir1',
children: []
}
]
},
{
id: 'dir2',
name: '产品图库',
parentId: 'root',
children: []
}
]
}
]));
}
if (!localStorage.getItem('materials')) {
localStorage.setItem('materials', JSON.stringify([
{
id: generateId(),
name: '示例图片1.jpg',
type: 'image',
size: 1024 * 1024 * 2, // 2MB
url: 'https://picsum.photos/400/300',
directoryId: 'dir1-1',
createTime: new Date().toISOString()
},
{
id: generateId(),
name: '示例视频1.mp4',
type: 'video',
size: 1024 * 1024 * 10, // 10MB
url: 'https://www.w3schools.com/html/mov_bbb.mp4',
directoryId: 'dir2',
createTime: new Date().toISOString()
}
]));
}
};
// Mock Service 类
export class MockService {
constructor() {
initLocalStorage();
}
// 获取目录树
async getDirectories() {
return new Promise((resolve) => {
setTimeout(() => {
const directories = JSON.parse(localStorage.getItem('directories'));
resolve(directories);
}, 300);
});
}
// 创建目录
async createDirectory({ name, parentId }) {
return new Promise((resolve) => {
setTimeout(() => {
const directories = JSON.parse(localStorage.getItem('directories'));
const newDir = {
id: generateId(),
name,
parentId,
children: []
};
const addToParent = (dirs) => {
for (let dir of dirs) {
if (dir.id === parentId) {
dir.children.push(newDir);
return true;
}
if (dir.children && dir.children.length) {
if (addToParent(dir.children)) return true;
}
}
return false;
};
if (parentId) {
addToParent(directories);
} else {
directories.push(newDir);
}
localStorage.setItem('directories', JSON.stringify(directories));
resolve(newDir);
}, 300);
});
}
// 删除目录
async deleteDirectory(id) {
return new Promise((resolve) => {
setTimeout(() => {
const directories = JSON.parse(localStorage.getItem('directories'));
const materials = JSON.parse(localStorage.getItem('materials'));
const removeDir = (dirs) => {
for (let i = 0; i < dirs.length; i++) {
if (dirs[i].id === id) {
dirs.splice(i, 1);
return true;
}
if (dirs[i].children && dirs[i].children.length) {
if (removeDir(dirs[i].children)) return true;
}
}
return false;
};
// 将目录下的文件移动到根目录
const updatedMaterials = materials.map(material => {
if (material.directoryId === id) {
return { ...material, directoryId: 'root' };
}
return material;
});
removeDir(directories);
localStorage.setItem('directories', JSON.stringify(directories));
localStorage.setItem('materials', JSON.stringify(updatedMaterials));
resolve({ success: true });
}, 300);
});
}
// 获取文件列表
async getMaterials(directoryId) {
return new Promise((resolve) => {
setTimeout(() => {
const materials = JSON.parse(localStorage.getItem('materials'));
const filtered = materials.filter(m => m.directoryId === directoryId);
resolve(filtered);
}, 300);
});
}
// 上传文件
async uploadFile(file, directoryId) {
return new Promise((resolve) => {
setTimeout(() => {
const reader = new FileReader();
reader.onload = (e) => {
const materials = JSON.parse(localStorage.getItem('materials'));
const newMaterial = {
id: generateId(),
name: file.name,
type: file.type.startsWith('image/') ? 'image' : 'video',
size: file.size,
url: file.type.startsWith('image/')
? 'https://picsum.photos/400/300?' + new Date().getTime() // 模拟新的图片URL
: 'https://www.w3schools.com/html/mov_bbb.mp4', // 模拟视频URL
directoryId,
createTime: new Date().toISOString()
};
materials.push(newMaterial);
localStorage.setItem('materials', JSON.stringify(materials));
resolve({ code: 0, message: 'success', url: newMaterial.url });
};
reader.readAsDataURL(file);
}, 1000); // 模拟上传耗时
});
}
// 移动文件
async moveFile(fileId, targetDirectoryId) {
return new Promise((resolve) => {
setTimeout(() => {
const materials = JSON.parse(localStorage.getItem('materials'));
const updatedMaterials = materials.map(material => {
if (material.id === fileId) {
return { ...material, directoryId: targetDirectoryId };
}
return material;
});
localStorage.setItem('materials', JSON.stringify(updatedMaterials));
resolve({ success: true });
}, 300);
});
}
// 删除文件
async deleteFile(fileId) {
return new Promise((resolve) => {
setTimeout(() => {
const materials = JSON.parse(localStorage.getItem('materials'));
const index = materials.findIndex(m => m.id === fileId);
if (index > -1) {
materials.splice(index, 1);
localStorage.setItem('materials', JSON.stringify(materials));
}
resolve({ success: true });
}, 300);
});
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment