|
@@ -1,30 +1,32 @@
|
|
|
|
|
+import {defineStore} from 'pinia'
|
|
|
|
|
|
|
|
-import { defineStore } from 'pinia'
|
|
|
|
|
-
|
|
|
|
|
-import { ref, onMounted } from 'vue'
|
|
|
|
|
|
|
+import {ref, onMounted} from 'vue'
|
|
|
import axios from 'axios'
|
|
import axios from 'axios'
|
|
|
import SparkMD5 from 'spark-md5'
|
|
import SparkMD5 from 'spark-md5'
|
|
|
import tool from '@/utils/tool'
|
|
import tool from '@/utils/tool'
|
|
|
-import { message } from 'ant-design-vue'
|
|
|
|
|
|
|
+import {message} from 'ant-design-vue'
|
|
|
import sysConfig from '@/config/index'
|
|
import sysConfig from '@/config/index'
|
|
|
-
|
|
|
|
|
|
|
+import resourceAuditApi from '@/api/resourceAudit.js'
|
|
|
|
|
+import EventBus from "@/utils/EventBus";
|
|
|
export const miniyunStore = defineStore({
|
|
export const miniyunStore = defineStore({
|
|
|
id: 'miniyun',
|
|
id: 'miniyun',
|
|
|
state: () => ({
|
|
state: () => ({
|
|
|
- pauseFlags : {}, // 控制每个文件是否暂停 { md5: true/false }
|
|
|
|
|
- uploadingTasks : {}, // 正在上传的任务 { md5: true }
|
|
|
|
|
|
|
+ pauseFlags: {}, // 控制每个文件是否暂停 { md5: true/false }
|
|
|
|
|
+ uploadingTasks: {}, // 正在上传的任务 { md5: true }
|
|
|
|
|
+ checkingFiles: new Set(), // 正在检查的文件MD5
|
|
|
|
|
+ checkedFiles: new Map(), // 已检查的文件结果缓存
|
|
|
//当前选中的文件
|
|
//当前选中的文件
|
|
|
- currentFile : null,
|
|
|
|
|
- spinning : false,
|
|
|
|
|
- chunkSize : 5 * 1024 * 1024,
|
|
|
|
|
|
|
+ currentFile: null,
|
|
|
|
|
+ spinning: false,
|
|
|
|
|
+ chunkSize: 5 * 1024 * 1024,
|
|
|
|
|
|
|
|
- uploadedSize : 0, // 已上传文件大小(字节)
|
|
|
|
|
|
|
+ uploadedSize: 0, // 已上传文件大小(字节)
|
|
|
|
|
|
|
|
- chunkCount : 0,
|
|
|
|
|
- chunksUploaded : 0,
|
|
|
|
|
- fileMd5 : '', //
|
|
|
|
|
|
|
+ chunkCount: 0,
|
|
|
|
|
+ chunksUploaded: 0,
|
|
|
|
|
+ fileMd5: '', //
|
|
|
// const emit = defineEmits(['onUpLoading', 'onSuccess'])
|
|
// const emit = defineEmits(['onUpLoading', 'onSuccess'])
|
|
|
- progress : {
|
|
|
|
|
|
|
+ progress: {
|
|
|
strokeColor: {
|
|
strokeColor: {
|
|
|
'0%': '#108ee9',
|
|
'0%': '#108ee9',
|
|
|
'100%': '#87d068'
|
|
'100%': '#87d068'
|
|
@@ -33,27 +35,340 @@ export const miniyunStore = defineStore({
|
|
|
format: (percent) => `${parseFloat(percent.toFixed(2))}%`,
|
|
format: (percent) => `${parseFloat(percent.toFixed(2))}%`,
|
|
|
class: 'test'
|
|
class: 'test'
|
|
|
},
|
|
},
|
|
|
- allChunks : 0, // 文件的md5值
|
|
|
|
|
- successfulChunkPercents : 0, // 上传成功的分片百分比
|
|
|
|
|
- fileSuffix : '', // 文件后缀
|
|
|
|
|
- chunkList : [], // 文件后缀
|
|
|
|
|
- uploadList : [], // 文件后缀
|
|
|
|
|
- fileList : [] ,// 文件后缀
|
|
|
|
|
- uploadFileList : [], // 文件后缀
|
|
|
|
|
- uploadChunks : [],// 文件后缀
|
|
|
|
|
-
|
|
|
|
|
- upLoadTag : false, // 文件的md5值
|
|
|
|
|
- startTime : 0, // 开始时间戳(毫秒
|
|
|
|
|
- totalSize :0, // 开始时间戳(毫秒
|
|
|
|
|
|
|
+ allChunks: 0, // 文件的md5值
|
|
|
|
|
+ successfulChunkPercents: 0, // 上传成功的分片百分比
|
|
|
|
|
+ fileSuffix: '', // 文件后缀
|
|
|
|
|
+ chunkList: [], // 文件后缀
|
|
|
|
|
+ uploadList: [], // 文件后缀
|
|
|
|
|
+ fileList: [],// 文件后缀
|
|
|
|
|
+ uploadFileList: [], // 文件后缀
|
|
|
|
|
+ uploadChunks: [],// 文件后缀
|
|
|
|
|
+ uploadFileListTemp: [],
|
|
|
|
|
+ upLoadTag: false, // 文件的md5值
|
|
|
|
|
+ startTime: 0, // 开始时间戳(毫秒
|
|
|
|
|
+ totalSize: 0, // 开始时间戳(毫秒
|
|
|
|
|
+ tempIndex: 0,
|
|
|
//文件数据放一起
|
|
//文件数据放一起
|
|
|
- fileForms : [],
|
|
|
|
|
|
|
+ fileForms: [],
|
|
|
}),
|
|
}),
|
|
|
getters: {
|
|
getters: {
|
|
|
getFileForms: (state) => state.fileForms,
|
|
getFileForms: (state) => state.fileForms,
|
|
|
|
|
+ getPauseFlags: (state) => state.pauseFlags,
|
|
|
},
|
|
},
|
|
|
actions: {
|
|
actions: {
|
|
|
- addFileForms(fileForm){
|
|
|
|
|
|
|
+ async addFileForms(fileForm) {
|
|
|
this.fileForms.push(fileForm)
|
|
this.fileForms.push(fileForm)
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < fileForm.uploadFileList.length; i++) {
|
|
|
|
|
+
|
|
|
|
|
+ await this.checkMd5List(fileForm.uploadFileList[i])
|
|
|
|
|
+ await this.uploadSingleFile(fileForm.uploadFileList[i])
|
|
|
|
|
+ }
|
|
|
|
|
+ //准备开启去下载
|
|
|
|
|
+ // for (let i = 0; i < fileForm.uploadFileList.length; i++) {
|
|
|
|
|
+ // this.tempIndex+=1
|
|
|
|
|
+ // fileForm.uploadFileList[i].tempIndex = this.tempIndex
|
|
|
|
|
+ // this.uploadFileListTemp.push(fileForm.uploadFileList[i])
|
|
|
|
|
+ // }
|
|
|
|
|
+ },
|
|
|
|
|
+ handlerRemoveItem(mmyIndex, mmmyIndex) {
|
|
|
|
|
+ // let myIndex = -1
|
|
|
|
|
+ // for (let i = 0; i < this.uploadFileListTemp.length; i++) {
|
|
|
|
|
+ // if(this.uploadFileListTemp[i].tempIndex == item.tempIndex){
|
|
|
|
|
+ // myIndex = i
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }
|
|
|
|
|
+ // if(myIndex != -1){
|
|
|
|
|
+ // this.uploadFileListTemp.splice(myIndex, 1)
|
|
|
|
|
+ // }
|
|
|
|
|
+ // let mmyIndex = -1
|
|
|
|
|
+ // let mmmyIndex = -1
|
|
|
|
|
+ // for (let i = 0; i < this.fileForms.length; i++) {
|
|
|
|
|
+ // for (let ii = 0; ii < this.fileForms[i].uploadFileList.length; ii++) {
|
|
|
|
|
+ // if(this.fileForms[i].uploadFileList[ii].tempIndex == item.tempIndex){
|
|
|
|
|
+ // mmyIndex = i
|
|
|
|
|
+ // mmmyIndex = ii
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }
|
|
|
|
|
+ // if(mmyIndex != -1 && mmmyIndex != -1){
|
|
|
|
|
+ // this.fileForms[mmyIndex].uploadFileList.splice(mmmyIndex, 1)
|
|
|
|
|
+ // }
|
|
|
|
|
+
|
|
|
|
|
+ this.fileForms[mmyIndex].uploadFileList.splice(mmmyIndex, 1)
|
|
|
|
|
+
|
|
|
|
|
+ },
|
|
|
|
|
+ pauseUpload(md5) {
|
|
|
|
|
+ this.pauseFlags[md5] = true
|
|
|
|
|
+ },
|
|
|
|
|
+ resumeUpload(md5) {
|
|
|
|
|
+ this.pauseFlags[md5] = false
|
|
|
|
|
+ },
|
|
|
|
|
+ async checkMd5List(uploadFile) {
|
|
|
|
|
+ // 标记文件正在检查
|
|
|
|
|
+ this.checkingFiles.add(uploadFile.md5)
|
|
|
|
|
+
|
|
|
|
|
+ const md5List = [{
|
|
|
|
|
+ md5: uploadFile.md5,
|
|
|
|
|
+ size: uploadFile.size,
|
|
|
|
|
+ chunkSize: uploadFile.chunks.length,
|
|
|
|
|
+ fileName: uploadFile.name,
|
|
|
|
|
+ fileSuffix: uploadFile.fileSuffix,
|
|
|
|
|
+ affiliationFuncType : 0
|
|
|
|
|
+ }]
|
|
|
|
|
+ await axios
|
|
|
|
|
+ .post(sysConfig.API_URL + '/api/webapp/minio/checkMd5List', md5List, {headers: {Token: tool.data.get('TOKEN')}})
|
|
|
|
|
+ .then((res) => {
|
|
|
|
|
+ console.log('文件上传返回结果:', res.data)
|
|
|
|
|
+ // return
|
|
|
|
|
+ var list = res.data
|
|
|
|
|
+ if (list.length !== 0) {
|
|
|
|
|
+ let upList = []
|
|
|
|
|
+ for (let item of list) {
|
|
|
|
|
+ console.log('item回来的', JSON.stringify(item))
|
|
|
|
|
+ if (uploadFile.md5 === item.md5) {
|
|
|
|
|
+ uploadFile.userFileId = item.userFileId
|
|
|
|
|
+ //重要的步骤
|
|
|
|
|
+ if (item.userFileId) {
|
|
|
|
|
+ uploadFile.percents = 100
|
|
|
|
|
+ this.checkedFiles.set(uploadFile.md5, {
|
|
|
|
|
+ userFileId: item.userFileId,
|
|
|
|
|
+ status: 'uploaded'
|
|
|
|
|
+ })
|
|
|
|
|
+ // emit('onSuccess', uploadFile)
|
|
|
|
|
+ }
|
|
|
|
|
+ // upList.push(item)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log('upList是:', upList)
|
|
|
|
|
+ // uploadFileList.value.push(uploadFile)
|
|
|
|
|
+ // emit('onSuccess', uploadFileList.value)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 从正在检查列表中移除
|
|
|
|
|
+ this.checkingFiles.delete(uploadFile.md5)
|
|
|
|
|
+ return uploadFile
|
|
|
|
|
+ // 文件均存在minio中了,无需上传
|
|
|
|
|
+ // if (uploadFileList.value.length === 0) {
|
|
|
|
|
+ // successfulChunkPercents.value = 100
|
|
|
|
|
+ // alert('文件上传成功')
|
|
|
|
|
+ // }
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch((error) => {
|
|
|
|
|
+ console.log('检查返回错误', error)
|
|
|
|
|
+ // 从正在检查列表中移除
|
|
|
|
|
+ this.checkingFiles.delete(uploadFile.md5)
|
|
|
|
|
+ throw error
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ async uploadFilesChunk(data, onSuccess) {
|
|
|
|
|
+ console.log('进入了uploadFileChunk方法...')
|
|
|
|
|
+ let retryTime = 5 //重试次数
|
|
|
|
|
+ const formData = new FormData()
|
|
|
|
|
+ formData.append('md5', data.md5)
|
|
|
|
|
+ // formData.append('md5', md5)
|
|
|
|
|
+ formData.append('chunkIndex', data.chunkIndex)
|
|
|
|
|
+ formData.append('chunk', data.chunk)
|
|
|
|
|
+ formData.append('chunkSize', data.chunkSize)
|
|
|
|
|
+ formData.append('fileSuffix', data.fileSuffix)
|
|
|
|
|
+ formData.append('fileName', data.fileName)
|
|
|
|
|
+ formData.append('affiliationFuncType', data.affiliationFuncType)
|
|
|
|
|
+ return axios
|
|
|
|
|
+ .post(sysConfig.API_URL + '/api/webapp/minio/upload', formData, {
|
|
|
|
|
+ headers: {'Content-Type': 'multipart/form-data', Token: tool.data.get('TOKEN')}
|
|
|
|
|
+ })
|
|
|
|
|
+ .then((res) => onSuccess())
|
|
|
|
|
+ .catch((error) => {
|
|
|
|
|
+ console.log('上传分片失败了...', error)
|
|
|
|
|
+ if (retryTime > 0) {
|
|
|
|
|
+ retryTime--
|
|
|
|
|
+ return this.uploadChunk(data, onSuccess)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 上传分片 旧
|
|
|
|
|
+ async uploadChunk(data, onSuccess) {
|
|
|
|
|
+ let retryTime = 5 //重试次数
|
|
|
|
|
+ const formData = new FormData()
|
|
|
|
|
+ // formData.append('identifier', fileMd5.value)
|
|
|
|
|
+ formData.append('md5', data.md5)
|
|
|
|
|
+ // formData.append('md5', md5)
|
|
|
|
|
+ formData.append('chunkIndex', data.chunkIndex)
|
|
|
|
|
+ formData.append('chunk', data.chunk)
|
|
|
|
|
+ formData.append('chunkSize', data.chunkSize)
|
|
|
|
|
+ formData.append('fileSuffix', data.fileSuffix)
|
|
|
|
|
+ formData.append('fileName', data.fileName)
|
|
|
|
|
+ formData.append('affiliationFuncType', data.affiliationFuncType)
|
|
|
|
|
+ return axios
|
|
|
|
|
+ .post(sysConfig.API_URL + '/api/webapp/minio/upload', formData, {
|
|
|
|
|
+ headers: {'Content-Type': 'multipart/form-data', Token: tool.data.get('TOKEN')}
|
|
|
|
|
+ })
|
|
|
|
|
+ .then((res) => onSuccess())
|
|
|
|
|
+ .catch((error) => {
|
|
|
|
|
+ if (retryTime > 0) {
|
|
|
|
|
+ retryTime--
|
|
|
|
|
+ return this.uploadChunk(data, onSuccess)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ calculateSpeed(startTime, uploadedSize) {
|
|
|
|
|
+ const currentTime = new Date().getTime()
|
|
|
|
|
+ const timeElapsed = (currentTime - startTime) / 1000 // 单位:秒
|
|
|
|
|
+ if (timeElapsed > 0) {
|
|
|
|
|
+ const speed = uploadedSize / timeElapsed // 单位:字节/秒
|
|
|
|
|
+ return speed
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0
|
|
|
|
|
+ },
|
|
|
|
|
+ estimateRemainingTime(startTime, uploadedSize, totalSize) {
|
|
|
|
|
+ console.log('疑问', ' 总的 ', totalSize, ' 变化的 ', uploadedSize)
|
|
|
|
|
+ const remainingSize = totalSize - uploadedSize // 剩余文件大小
|
|
|
|
|
+ const speed = this.calculateSpeed(startTime, uploadedSize) // 平均上传速度(字节/秒)
|
|
|
|
|
+
|
|
|
|
|
+ if (speed > 0) {
|
|
|
|
|
+ const remainingTimeSeconds = remainingSize / speed // 剩余时间(秒)
|
|
|
|
|
+ return remainingTimeSeconds
|
|
|
|
|
+ }
|
|
|
|
|
+ return Infinity // 如果上传速度为 0,则无法估算
|
|
|
|
|
+ },
|
|
|
|
|
+ formatTime(seconds) {
|
|
|
|
|
+ const minutes = Math.floor(seconds / 60)
|
|
|
|
|
+ const secs = Math.floor(seconds % 60)
|
|
|
|
|
+ if (minutes == 0 && secs == 0) {
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+ return `${minutes} 分 ${secs} 秒`
|
|
|
|
|
+ },
|
|
|
|
|
+ async uploadSingleFile(fileObj) {
|
|
|
|
|
+ const file = fileObj
|
|
|
|
|
+ const md5 = file.md5
|
|
|
|
|
+ // const index = uploadFileList.value.findIndex((item) => item.md5 === md5)
|
|
|
|
|
+ //
|
|
|
|
|
+ // if (index === -1) return
|
|
|
|
|
+
|
|
|
|
|
+ const item = fileObj
|
|
|
|
|
+
|
|
|
|
|
+ if (item && item.userFileId) {
|
|
|
|
|
+ this.getUp(fileObj)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // // 如果是暂停状态则不执行上传
|
|
|
|
|
+ // while (pauseFlags.value[md5]) {
|
|
|
|
|
+ // await new Promise((resolve) => setTimeout(resolve, 500))
|
|
|
|
|
+ // }
|
|
|
|
|
+ // 添加到正在上传任务中
|
|
|
|
|
+ // uploadingTasks.value[md5] = true
|
|
|
|
|
+ file.startTime = new Date().getTime()
|
|
|
|
|
+ file.uploadedSize = 0
|
|
|
|
|
+
|
|
|
|
|
+ const chunkPromises = []
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < item.chunks.length; i++) {
|
|
|
|
|
+ let chunk = item.chunks[i]
|
|
|
|
|
+
|
|
|
|
|
+ while (this.pauseFlags[md5]) {
|
|
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, 500))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ chunkPromises.push(
|
|
|
|
|
+ await this.uploadFilesChunk(
|
|
|
|
|
+ {
|
|
|
|
|
+ affiliationFuncType : 0,
|
|
|
|
|
+ md5,
|
|
|
|
|
+ chunk,
|
|
|
|
|
+ chunkIndex: i + 1,
|
|
|
|
|
+ fileSuffix: item.fileSuffix,
|
|
|
|
|
+ chunkSize: item.chunks.length,
|
|
|
|
|
+ fileName: item.name
|
|
|
|
|
+ },
|
|
|
|
|
+ () => {
|
|
|
|
|
+ // chunksUploaded.value++
|
|
|
|
|
+ // uploadChunks.value++
|
|
|
|
|
+
|
|
|
|
|
+ // successfulChunkPercents.value = 100 * (uploadChunks.value / allChunks.value).toFixed(2)
|
|
|
|
|
+ item.uploadedSize += chunk.size // 更新已上传大小
|
|
|
|
|
+ const remainingTime = this.estimateRemainingTime(item.startTime, item.uploadedSize, item.size)
|
|
|
|
|
+ console.log(`预计剩余时间: ${this.formatTime(remainingTime)}`)
|
|
|
|
|
+ // item.percents = (100 * (uploadChunks.value / item.chunks.length)).toFixed(2)
|
|
|
|
|
+ item.time = this.formatTime(remainingTime)
|
|
|
|
|
+
|
|
|
|
|
+ const percent = ((i + 1) / item.chunks.length) * 100
|
|
|
|
|
+ item.percents = percent.toFixed(2)
|
|
|
|
|
+ console.log(`我得名字: `, item.name, ' i ', i, ' item.chunks.length ', item.chunks.length)
|
|
|
|
|
+ // item.time = formatTime(estimateRemainingTime())
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await Promise.all(chunkPromises)
|
|
|
|
|
+ item.affiliationFuncType = 0
|
|
|
|
|
+ // 合并分片
|
|
|
|
|
+ const mergeResult = await axios.post(
|
|
|
|
|
+ sysConfig.API_URL + `/api/webapp/minio/merge?md5=${md5}&fileSuffix=${item.fileSuffix}&chunkTotal=${item.chunks.length}&fileName=${item.name}&fileSize=${item.size}&affiliationFuncType=${item.affiliationFuncType}`,
|
|
|
|
|
+ null,
|
|
|
|
|
+ {headers: {Token: tool.data.get('TOKEN')}}
|
|
|
|
|
+ )
|
|
|
|
|
+ console.log('怎么说呢', ' 啊网络请求 ', mergeResult)
|
|
|
|
|
+ fileObj.userFileId = mergeResult.data.userFileId
|
|
|
|
|
+ fileObj.percents = 100
|
|
|
|
|
+
|
|
|
|
|
+ // uploadingTasks.value[item.md5] = false
|
|
|
|
|
+ // item.time = '上传完成'
|
|
|
|
|
+ // 尝试恢复一个被暂停的任务
|
|
|
|
|
+ // autoResumePausedUpload()
|
|
|
|
|
+ // upLoadTag.value = false
|
|
|
|
|
+ // emit('onSuccess', uploadFileList.value)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ this.getUp(fileObj)
|
|
|
},
|
|
},
|
|
|
- }
|
|
|
|
|
|
|
+ async getUp(fileObj) {
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < this.fileForms.length; i++) {
|
|
|
|
|
+ console.log('怎么说呢', this.fileForms[i].upTag != undefined,' 里面的 ', this.fileForms[i], ' 外面的1 ', fileObj)
|
|
|
|
|
+
|
|
|
|
|
+ if (this.fileForms[i].upTag == undefined) {
|
|
|
|
|
+ let count = 0
|
|
|
|
|
+ let list = []
|
|
|
|
|
+ for (let ii = 0; ii < this.fileForms[i].uploadFileList.length; ii++) {
|
|
|
|
|
+ console.log('怎么说呢', ' 里面的 ', this.fileForms[i], ' 外面的2 ', fileObj)
|
|
|
|
|
+ let item = this.fileForms[i].uploadFileList[ii]
|
|
|
|
|
+ if (item.userFileId != undefined && item.percents == 100){
|
|
|
|
|
+ count ++
|
|
|
|
|
+ list.push(item.userFileId)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if(this.fileForms[i].uploadFileList.length == count){
|
|
|
|
|
+ let formData = this.fileForms[i]
|
|
|
|
|
+ formData.userfileIds = list.join(',')
|
|
|
|
|
+
|
|
|
|
|
+ //去上传
|
|
|
|
|
+ let res = await resourceAuditApi.add(formData)
|
|
|
|
|
+ // .then((res) => {
|
|
|
|
|
+ // Modal.success({ content: '资源上传成功' })
|
|
|
|
|
+ // })
|
|
|
|
|
+ // .catch((err) => {
|
|
|
|
|
+ // Modal.success({ content: '资源上传失败' })
|
|
|
|
|
+ // console.log(err)
|
|
|
|
|
+ // })
|
|
|
|
|
+ console.log('上传至hi偶',res)
|
|
|
|
|
+ this.fileForms[i].upTag = true
|
|
|
|
|
+ EventBus.emit('onUpTag')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
})
|
|
})
|