diff --git a/api/.vscode/launch.json b/api/.vscode/launch.json index 48e8f7a..0c227c0 100644 --- a/api/.vscode/launch.json +++ b/api/.vscode/launch.json @@ -16,9 +16,9 @@ "-p 4600", "-f http://localhost:4600", // 研发 - // "-g postgres://postgres:123@10.8.30.32:5432/orational_service", + "-g postgres://postgres:123@10.8.30.32:5432/orational_service", // 测试 - "-g postgres://FashionAdmin:123456@10.8.30.156:5432/POMS", + // "-g postgres://FashionAdmin:123456@10.8.30.156:5432/POMS", "-k 10.8.30.72:29092,10.8.30.73:29092,10.8.30.74:29092", "--iotaProxy http://10.8.30.157:17007", "--redisHost localhost", @@ -57,8 +57,8 @@ // "--clickHouseIot iot", // 测试 "--clickHouseAnxincloud anxinyun1", - "--clickHousePepEmis pepca8", - "--clickHouseProjectManage peppm8", + "--clickHousePepEmis pepca9", + "--clickHouseProjectManage peppm", "--clickHouseVcmp video_access_dev", "--clickHouseDataAlarm default", "--clickHouseIot iota", diff --git a/api/app/lib/controllers/means/index.js b/api/app/lib/controllers/means/index.js new file mode 100644 index 0000000..878de2c --- /dev/null +++ b/api/app/lib/controllers/means/index.js @@ -0,0 +1,155 @@ +'use strict'; + +async function addEditFile (ctx, next) { + try { + const models = ctx.fs.dc.models; + const data = ctx.request.body; + const { higherFileId, fileName } = data + + + let onefind = await models.ProjectFolder.findOne({ + where: { + higherFileId: higherFileId || null, + fileName + } + }) + if (onefind) { + throw '文件夹名称重复' + } + + + if (data && data.id) { + await models.ProjectFolder.update(data, { + where: { + id: data.id + } + }) + } else { + await models.ProjectFolder.create(data) + } + + ctx.status = 204; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`) + ctx.status = 400; + ctx.body = { + "message": '添加文件夹失败' + } + } +} + +async function fileList (ctx, next) { + try { + const models = ctx.fs.dc.models; + const { projectId, limit, page } = ctx.query; + + let options = { where: {}, } + if (projectId) { + options.where.projectId = projectId + } + + let res = await models.ProjectFolder.findAll(options) + + ctx.status = 200; + ctx.body = res || [] + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`) + ctx.status = 400; + ctx.body = { + "message": '添加文件夹失败' + } + } +} + +async function delFile (ctx, next) { + try { + const models = ctx.fs.dc.models; + const { id } = ctx.params; + + + await models.ProjectFolder.destroy({ where: { id } }) + + ctx.status = 204; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`) + ctx.status = 400; + ctx.body = { + "message": '删除文件夹失败' + } + } +} + +async function addFile (ctx, next) { + try { + const models = ctx.fs.dc.models; + const data = ctx.request.body; + + await models.ProjectFolderFile.create(data) + + ctx.status = 204; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`) + ctx.status = 400; + ctx.body = { + "message": '添加文件夹失败' + } + } +} + +async function folderFileList (ctx, next) { + try { + const models = ctx.fs.dc.models; + const { fileId, limit, page } = ctx.query; + + let options = { where: {}, } + if (JSON.parse(fileId).length) { + options.where.fileId = { $in: JSON.parse(fileId) } + } + + if (limit) { + options.limit = Number(limit) + } + if (page && limit) { + options.offset = Number(page) * Number(limit) + } + + let res = await models.ProjectFolderFile.findAndCountAll(options) + + ctx.status = 200 + ctx.body = res || {} + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`) + ctx.status = 400; + ctx.body = { + "message": '添加文件列表失败' + } + } +} + + +async function delfolderFile (ctx, next) { + try { + const models = ctx.fs.dc.models; + const { id } = ctx.params; + + await models.ProjectFolderFile.destroy({ where: { id } }) + + ctx.status = 204; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`) + ctx.status = 400; + ctx.body = { + "message": '删除文件失败' + } + } +} + + +module.exports = { + addEditFile, + fileList, + delFile, + addFile, + folderFileList, + delfolderFile +} \ No newline at end of file diff --git a/api/app/lib/models/project_folder.js b/api/app/lib/models/project_folder.js new file mode 100644 index 0000000..000cf4a --- /dev/null +++ b/api/app/lib/models/project_folder.js @@ -0,0 +1,61 @@ +/* eslint-disable*/ +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const ProjectFolder = sequelize.define("projectFolder", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: true, + unique: "project_folder_id_uindex" + }, + projectId: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: false, + field: "project_id", + autoIncrement: false, + }, + higherFileId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "higher_file_id", + autoIncrement: false, + }, + type: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: false, + field: "type", + autoIncrement: false, + }, + fileName: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: false, + field: "file_name", + autoIncrement: false + } + }, { + tableName: "project_folder", + comment: "", + indexes: [] + }); + dc.models.ProjectFolder = ProjectFolder; + return ProjectFolder; +}; \ No newline at end of file diff --git a/api/app/lib/models/project_folder_file.js b/api/app/lib/models/project_folder_file.js new file mode 100644 index 0000000..bd18c04 --- /dev/null +++ b/api/app/lib/models/project_folder_file.js @@ -0,0 +1,70 @@ +/* eslint-disable*/ +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const ProjectFolderFile = sequelize.define("projectFolderFile", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: true, + unique: "project_folder_file_id_uindex" + }, + fileId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "file_id", + autoIncrement: false, + }, + size: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "size", + autoIncrement: false, + }, + uploadTime: { + type:DataTypes.DATE, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "upload_time", + autoIncrement: false, + }, + name: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "name", + autoIncrement: false + }, + url: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "url", + autoIncrement: false + } + }, { + tableName: "project_folder_file", + comment: "", + indexes: [] + }); + dc.models.ProjectFolderFile = ProjectFolderFile; + return ProjectFolderFile; +}; \ No newline at end of file diff --git a/api/app/lib/routes/means/index.js b/api/app/lib/routes/means/index.js new file mode 100644 index 0000000..f64de9f --- /dev/null +++ b/api/app/lib/routes/means/index.js @@ -0,0 +1,24 @@ +'use strict'; + +const means = require('../../controllers/means/index'); + +module.exports = function (app, router, opts) { + + app.fs.api.logAttr['GET/file/list'] = { content: '获取文件夹列表', visible: true }; + router.get('/file/list', means.fileList); + + app.fs.api.logAttr['POST/file/addEdit'] = { content: '添加/编辑文件夹', visible: true }; + router.post('/file/addEdit', means.addEditFile); + + app.fs.api.logAttr['DEL/file/del/:id'] = { content: '删除文件夹', visible: true }; + router.del('/file/del/:id', means.delFile); + + app.fs.api.logAttr['POST/file'] = { content: '添加文件', visible: true }; + router.post('/file', means.addFile); + + app.fs.api.logAttr['GET/file'] = { content: '获取文件列表', visible: true }; + router.get('/file', means.folderFileList); + + app.fs.api.logAttr['DEL/file/:id'] = { content: '删除文件夹', visible: true }; + router.del('/file/:id', means.delfolderFile); +}; \ No newline at end of file diff --git a/script/0.16/schema/1.alter_project_file.sql b/script/0.16/schema/1.alter_project_file.sql new file mode 100644 index 0000000..7b77aeb --- /dev/null +++ b/script/0.16/schema/1.alter_project_file.sql @@ -0,0 +1,39 @@ +create table project_folder +( + id serial not null, + file_name varchar not null, + project_id integer not null, + higher_file_id integer, + type int not null +); + +comment on column project_folder.project_id is '自定义项目或者项企项目'; + +comment on column project_folder.higher_file_id is '上级文件id'; + +comment on column project_folder.type is '1.项目资料 +2.维修FQA +3.故障资料 +4.运维规范'; + +create unique index project_folder_id_uindex + on project_folder (id); + +alter table project_folder + add constraint project_folder_pk + primary key (id); + + + +create table project_folder_file +( + id serial not null, + name varchar, + size integer, + upload_time timestamp, + file_id integer, + url varchar +); + +create unique index project_folder_file_id_uindex + on project_folder_file (id); diff --git a/web/client/assets/images/icon/icon_cb_数据@2x.png b/web/client/assets/images/icon/icon_cb_数据@2x.png new file mode 100644 index 0000000..902862c Binary files /dev/null and b/web/client/assets/images/icon/icon_cb_数据@2x.png differ diff --git a/web/client/assets/images/icon/project-icon.png b/web/client/assets/images/icon/project-icon.png new file mode 100644 index 0000000..238af93 Binary files /dev/null and b/web/client/assets/images/icon/project-icon.png differ diff --git a/web/client/src/components/Uploads/index.js b/web/client/src/components/Uploads/index.js new file mode 100644 index 0000000..ef4535c --- /dev/null +++ b/web/client/src/components/Uploads/index.js @@ -0,0 +1,517 @@ +'use strict'; + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Spin, Upload, message, Modal, Card, Button } from '@douyinfe/semi-ui'; +import moment from 'moment'; +import { IconPlus, IconCloudUploadStroked, IconCrossStroked } from '@douyinfe/semi-icons'; +import OSS from 'ali-oss'; +import { RouteRequest } from '@peace/utils'; +import { RouteTable } from '$utils' +import { v4 as uuidv4 } from 'uuid'; +import request from 'superagent'; + +class Uploads extends Component { + constructor(props) { + super(props); + this.ApiRoot = localStorage.getItem('tyApiRoot') + this.qnDomain = localStorage.getItem('qnDomain'); + this.aliAdmin = localStorage.getItem('aliAdmin'); + this.aliBucket = localStorage.getItem('aliBucket'); + this.aliRegion = localStorage.getItem('aliRegion'); + this.state = { + fileUploading: false, + fileList: [], + curPreviewPic: '', + curPreviewVideo: '', + delPicIng: false, + removeFilesList: [], + stsRes: {} + }; + } + + dealName = (uploaded) => { + let realName = uploaded && uploaded.split('/')[2] + // let x1 = realName.split('.') + // let postfix = x1.pop() + // let allName = x1.join('.') + // let x2 = allName.split('_') + // let showName = `${x2[0]}.${postfix}` + return realName + } + + // setFileList = (value) => { + // let defaultFileList = []; + // defaultFileList = value.map((u, index) => { + // let fileUrl = `${this.ApiRoot}/${u.url}`; + // return { + // uid: -index - 1, + // name: this.dealName(u.url), + // status: 'done', + // storageUrl: u.url, + // url: fileUrl + // }; + // }); + // onChange(defaultFileList) + // this.setState({ + // fileList: defaultFileList + // }); + // }; + + setFileList = (nextEditData, isQiniu, isAli) => { + let defaultFileList = []; + if (nextEditData.length) { + defaultFileList = nextEditData.map((u, index) => { + let fileUrl = + isQiniu ? `/_file-server/${u.storageUrl}` + : isAli ? `/_file-ali-server/${u.storageUrl}` + : `${this.ApiRoot}/${u.storageUrl}`; + + return { + uid: -index - 1, + name: this.dealName(u.storageUrl), + status: 'done', + storageUrl: u.storageUrl, + url: fileUrl, + size: u.size || -1 + }; + }); + } + this.setState({ + fileList: defaultFileList + }); + }; + + componentWillMount () { + this.setState({ + delPicIng: true + }) + RouteRequest.get(RouteTable.getAliSts).then(async (result) => { + this.setState({ + delPicIng: false, + stsRes: result + }) + }, (err) => { + this.setState({ + delPicIng: false + }) + }) + } + + componentDidMount () { + const { value, defaultValue, isQiniu, isAli } = this.props; + if (defaultValue) { + this.setFileList(defaultValue, isQiniu, isAli) + } + } + + UNSAFE_componentWillReceiveProps (np) { + const { dispatch, value: thisEditData, onChange } = this.props; + const { value: nextEditData, isQiniu, isAli } = np; + // this.setFileList(nextEditData, isQiniu) + // const setFileList = () => { + // let defaultFileList = []; + // defaultFileList = nextEditData.map((u, index) => { + // let fileUrl = isQiniu ? `/_file-server/${u.storageUrl}` : `${this.ApiRoot}/${u.storageUrl}`; + // return { + // uid: -index - 1, + // name: this.dealName(u.storageUrl), + // status: 'done', + // storageUrl: u.storageUrl, + // url: fileUrl, + // size: u.size || -1 + // }; + // }); + // this.setState({ + // fileList: defaultFileList + // }); + // }; + if (nextEditData && nextEditData.length) { + if (!thisEditData || !this.state.fileList.length) { + this.setFileList(nextEditData, isQiniu, isAli); + } else if (nextEditData.length != thisEditData.length) { + this.setFileList(nextEditData, isQiniu, isAli); + } else { + let repeat = true; + for (let i = 0; i < thisEditData.length; i++) { + if (thisEditData[i] != nextEditData[i]) { + repeat = false; + break; + } + } + if (!repeat) { + this.setFileList(nextEditData, isQiniu, isAli); + } + } + } + // else{ + // this.setState({ + // fileList:[], + // }) + // } + } + + render () { + const UploadPath = { + project: ['txt', 'dwg', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'pdf', 'pptx', 'png', 'jpg', 'svg', 'jpeg', 'rar', 'zip', 'jpeg', 'mp4'], + report: ['doc', 'docx', 'xls', 'xlsx', 'csv', 'pdf'], + data: ['txt', 'xls', 'xlsx', 'csv'], + image: ['png', 'jpg', 'svg', 'jpeg'], + three: ['js'], + video: ['mp4'] + }; + /** + * uploadType 【string】 主要区别文件上传路径 以及类型 以 web/routes/attachment/index.js 中 UploadPath 的 key 值为准;默认 project; + * disabled 【boolean】 上传是否可用 + * maxFilesNum 【number】 最大上传数量 + * fileTypes 【array[string]】 可允许上传的文件类型; + * maxFileSize 【number】 单个文件最大大小 M + * listType 【antd】 upload 组件的属性 + * onChange 【function】 文件数量变化时候回调 返回文件 + * value 【array[obj]】 编辑数据 [{url:'xxx', [size:999]}] + * onStateChange 【function】 文件状态改变回调函数 上传中 return { uploading:true/false } + */ + const { + uploadType, + disabled, + maxFilesNum, + fileTypes, + maxFileSize, + listType, + onChange = () => { }, + value, + showUploadList, + onStateChange, + isQiniu, + isAli, + } = this.props; + const { fileList, curPreviewPic, curPreviewVideo, delPicIng, removeFilesList, stsRes } = this.state; + const that = this; + let uploadType_ = uploadType || 'project'; + let maxFilesNum_ = maxFilesNum || 1; + let defaultFileTypes = fileTypes || UploadPath[uploadType_]; + // debugger + const uploadProps = { + name: 'checkFile_', + multiple: false, + showUploadList: showUploadList || true, + action: + isQiniu ? `/_upload/attachments/${uploadType_}` + : isAli ? `/_upload/attachments/ali/${uploadType_}` + : `${this.ApiRoot}/attachments/${uploadType_}`, + listType: listType || 'text', + disabled: disabled, + beforeUpload: (file) => { + if (fileList.length >= maxFilesNum_) { + message.warning(`最多选择${maxFilesNum_}个文件上传`); + return false; + } + if (file.name.length > 60) { + message.warning(`文件名过长(大于60字符),请修改后上传`); + return false; + } + const extNames = file.name.split('.'); + // var reg = /^[\.\s\u4e00-\u9fa5a-zA-Z0-9_-]{0,}$/; + // if (!reg.exec(file.name)) { + // message.warning(`文件名包含除字母、汉字、数字、中划线、下划线之外的字符,请修改后上传`); + // return false; + // } + let isDAE = false; + if (extNames.length > 0) { + let fileType = extNames[extNames.length - 1].toLowerCase(); + isDAE = defaultFileTypes.some((f) => f == fileType); + } + if (!isDAE) { + message.error(`只能上传 ${defaultFileTypes.join()} 格式的文件!`); + return false; + } + const isLt = file.size / 1024 / 1024 < (maxFileSize || 3); + if (!isLt) { + message.error(`文件必须小于${maxFileSize || 3}MB!`); + return false; + } + this.setState({ + fileUploading: true + }); + if (onStateChange) { + onStateChange({ uploading: true }); + } + }, + + customRequest: isQiniu ? undefined : async (params) => { + try { + const client = new OSS({ + // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。 + region: that.aliRegion, + // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。 + accessKeyId: stsRes.AccessKeyId, + accessKeySecret: stsRes.AccessKeySecret, + // 从STS服务获取的安全令牌(SecurityToken)。 + stsToken: stsRes.SecurityToken, + // 填写Bucket名称,例如examplebucket。 + bucket: that.aliBucket, + }); + + let uploadRes = null + let uploadPath = `/${uploadType_}/${uuidv4()}/` + params.file.name + if ( + // false && + params.file.size < 1024 * 1024 * 1 + ) { + params.onProgress({ percent: 40 }) + uploadRes = await client.put( + uploadPath, + params.file + ); + } else { + uploadRes = await client.multipartUpload(uploadPath, params.file, { + progress: (p, _checkpoint) => { + // Object的上传进度。 + // console.log(p); + // 分片上传的断点信息。 + // console.log(_checkpoint); + params.onProgress({ percent: p * 100 }) + }, + // 设置并发上传的分片数量。 + parallel: 4, + // 设置分片大小。默认值为1 MB,最小值为100 KB。 + partSize: 1024 * 1024 * 3, // 3m + }); + } + + // console.log(uploadRes); + let { name: url, res } = uploadRes; + let size = params.file.size; + let nextFileList = fileList; + let url_ = url.startsWith('/') ? url.substring(1) : url + nextFileList[nextFileList.length - 1] = { + uid: -moment().unix(), + name: params.file.name, + status: 'done', + storageUrl: url_, + url: `/_file-ali-server/${url_}`, + size: size + }; + onChange(nextFileList); + that.setState({ + fileUploading: false, + fileList: nextFileList + }); + if (onStateChange) { + onStateChange({ uploading: false }); + } + params.onSuccess({ + result: { + uploaded: url, + url: res.requestUrls[0] + }, + }) + } catch (error) { + console.error(error); + params.onError({}) + } + + }, + + onChange (info) { + console.log(111,info); + const status = info.file.status; + if (status === 'uploading') { + that.setState({ + fileList: info.fileList + }); + } + if (status === 'done') { + let { uploaded, url } = info.file.response; + let size = info.file.size; + let nextFileList = fileList; + nextFileList[nextFileList.length - 1] = { + uid: -moment().unix(), + name: that.dealName(uploaded), + status: 'done', + storageUrl: uploaded, + url: + isQiniu ? '/_file-server/' + uploaded : + isAli ? `/_file-ali-server/${uploaded}` : + url, + size: size + }; + onChange(nextFileList); + that.setState({ + fileUploading: false, + fileList: nextFileList + }); + if (onStateChange) { + onStateChange({ uploading: false }); + } + } else if (status === 'error') { + that.setState({ + fileUploading: false + }); + message.error(`${info.file.name} 上传失败,请重试`); + if (onStateChange) { + onStateChange({ uploading: false }); + } + } + }, + onRemove (file) { + let nextFileList = []; + fileList.map((f, i) => { + if (f.uid != file.uid) { + nextFileList.push(f); + } + }); + let nextRemoveFiles = removeFilesList.concat([file.storageUrl]); + if (curPreviewPic == file.url) { + that.setState({ + curPreviewPic: '' + }); + } + if (curPreviewVideo == file.url) { + that.setState({ + curPreviewVideo: '' + }); + } + onChange(nextFileList); + that.setState({ + fileList: nextFileList, + removeFilesList: nextRemoveFiles + }); + }, + onPreview (file) { + let filePostfix = file.url.split('.').pop(); + filePostfix = filePostfix.toLowerCase(); + if (UploadPath.image.some((img) => img == filePostfix)) { + that.setState({ + curPreviewPic: file.url + }); + } else if ( + UploadPath.video.some((img) => img == filePostfix) + && isAli + ) { + that.setState({ + curPreviewVideo: that.aliAdmin + '/' + file.storageUrl + }); + } else { + //message.warn('仅支持图片预览'); + preview(file.storageUrl) + } + } + }; + + const preview = (url) => { + let link = isQiniu ? encodeURI(`${this.qnDomain}/${url}`) : + isAli ? encodeURI(`${this.aliAdmin}/${url}`) : '' + if (link) + if (url.indexOf("pdf") !== -1 || url.indexOf("csv") !== -1) { + window.open(link) + } else { + window.open(`https://view.officeapps.live.com/op/view.aspx?src=${link}`) + } + } + + let fileList_ = fileList + // .map(f => { + // if (f.storageUrl) { + // let realName = f.storageUrl.split('/').pop() + // if (f.name != realName) { + // f.name = realName + // } + // } + // return f + // }) + //下载文件 + const handleDownload = (file) => { + saveAs(file) + }; + const saveAs = (file) => { + let url = null + if (file.storageUrl.endsWith('mp4')) { + const client = new OSS({ + // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。 + region: that.aliRegion, + // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。 + accessKeyId: stsRes.AccessKeyId, + accessKeySecret: stsRes.AccessKeySecret, + // 从STS服务获取的安全令牌(SecurityToken)。 + stsToken: stsRes.SecurityToken, + // 填写Bucket名称,例如examplebucket。 + bucket: that.aliBucket, + }); + // 配置响应头实现通过URL访问时自动下载文件,并设置下载后的文件名。 + const response = { + 'content-disposition': `attachment; filename=${encodeURIComponent(file.name)}` + } + // 填写Object完整路径。Object完整路径中不能包含Bucket名称。 + url = client.signatureUrl(file.storageUrl, { response }); + } + + const link = document.createElement('a'); + link.href = url || file.url; + link.download = file.name; + link.style.display = 'none'; + link.click(); + } + //自定义下载 + return ( +
+ + + { + disabled ? ( + '' + ) : + listType == 'picture-card' ? + ( + fileList.length >= maxFilesNum_ ? null : ( +
+ +
添加附件
+
+ ) + ) : ( + + ) + } +
+ { + curPreviewPic ? ( + +
+ 图片预览 + { this.setState({ curPreviewPic: '' }) }}> + + +
+ +
+ ) : '' + } + { + curPreviewVideo ? ( +
+ 视频预览 + { this.setState({ curPreviewVideo: '' }) }}> + + +
+ +
) : '' + } +
+
+ ); + } +} + +function mapStateToProps (state) { + const { auth } = state + return { + user: auth.user + }; +} + +export default connect(mapStateToProps)(Uploads); \ No newline at end of file diff --git a/web/client/src/components/index.js b/web/client/src/components/index.js index e560b3f..effcb7c 100644 --- a/web/client/src/components/index.js +++ b/web/client/src/components/index.js @@ -4,6 +4,7 @@ import ReminderBox from './reminderBox' import Setup from './setup' import { SkeletonScreen } from './skeletonScreen' import OutHidden from './outHidden' +import Uploads from './Uploads/index' export { SimpleFileDownButton, @@ -11,4 +12,5 @@ export { Setup, SkeletonScreen, OutHidden, + Uploads }; diff --git a/web/client/src/index.less b/web/client/src/index.less index ea81c19..b12da1d 100644 --- a/web/client/src/index.less +++ b/web/client/src/index.less @@ -2,6 +2,7 @@ // @import '~@douyinfe/semi-ui/dist/css/semi.min.css'; @import '~perfect-scrollbar/css/perfect-scrollbar.css'; @import '~nprogress/nprogress.css'; +@import '~simplebar-react/dist/simplebar.min.css'; *, diff --git a/web/client/src/sections/means/actions/index.js b/web/client/src/sections/means/actions/index.js index eb109ab..17b2cc6 100644 --- a/web/client/src/sections/means/actions/index.js +++ b/web/client/src/sections/means/actions/index.js @@ -1,2 +1,8 @@ 'use strict'; +import * as means from './means' + +export default { + ...means, + +} \ No newline at end of file diff --git a/web/client/src/sections/means/actions/means.js b/web/client/src/sections/means/actions/means.js new file mode 100644 index 0000000..1d004fc --- /dev/null +++ b/web/client/src/sections/means/actions/means.js @@ -0,0 +1,75 @@ +'use strict'; + +import { ApiTable, basicAction } from '$utils' + +//添加、编辑文件夹 +export function addEditFile (data = {}) { + return (dispatch) => basicAction({ + type: "post", + dispatch: dispatch, + actionType: "POST_ADDEDITFILE", + data, + url: `${ApiTable.addEditFile}`, + msg: { option: data.id ? '文件夹编辑' : '添加文件夹' }, + }); +} + +//获取文件夹列表 +export function fileList (query = {}) { + return (dispatch) => basicAction({ + type: "get", + dispatch: dispatch, + actionType: "GET_FILELIST", + query, + url: `${ApiTable.fileList}`, + msg: { option: '获取文件夹列表' }, + }); +} + + +//删除文件夹 +export function delFile (id) { + return (dispatch) => basicAction({ + type: "del", + dispatch: dispatch, + actionType: "DEL_DELFILE", + url: `${ApiTable.delFile.replace('{id}', id)}`, + msg: { option: '删除文件夹' }, + }); +} + +//添加文件 +export function addFile (data = {}) { + return (dispatch) => basicAction({ + type: "post", + dispatch: dispatch, + actionType: "POST_FILE", + data, + url: `${ApiTable.file}`, + msg: { option: '添加文件' }, + }); +} + + +//获取文件列表 +export function folderFileList (query = {}) { + return (dispatch) => basicAction({ + type: "get", + dispatch: dispatch, + actionType: "GET_FOLDERFILELIST", + query, + url: `${ApiTable.file}`, + msg: { option: '获取文件列表' }, + }); +} + +//删除文件 +export function delfolderFile (id) { + return (dispatch) => basicAction({ + type: "del", + dispatch: dispatch, + actionType: "DEL_DELFILE", + url: `${ApiTable.delfolderFile.replace('{id}', id)}`, + msg: { option: '删除文件' }, + }); +} diff --git a/web/client/src/sections/means/components/fileModal.jsx b/web/client/src/sections/means/components/fileModal.jsx new file mode 100644 index 0000000..7f72f78 --- /dev/null +++ b/web/client/src/sections/means/components/fileModal.jsx @@ -0,0 +1,86 @@ +import React, { useState, useRef, useEffect } from "react"; +import { connect } from "react-redux"; +import { Modal, Form } from "@douyinfe/semi-ui"; +import { IconAlertCircle } from '@douyinfe/semi-icons'; + + +function FileModal (props) { + const { close, success, dispatch, actions, editData, pepProjectId, higherFile } = props; + const { means } = actions; + const form = useRef();//表单 + + + return ( + <> + { + form.current.validate().then((v) => { + dispatch(means.addEditFile({ + id: editData?.id, + projectId: pepProjectId, + fileName: v.fileName, + higherFileId: editData?.higherFileId || v.higherFileId || null, + type: 1 + })).then(v => { + if (v.success) { + close() + success() + } + }) + }) + }} + width={607} + onCancel={() => { + close() + }} + > +
+
(form.current = formApi)} + > + + + + {editData?.id ? "" : + {higherFile?.map((item, index) => ( + + {item.name} + + ))} + } + + + +
+
+ + ); +} +function mapStateToProps (state) { + const { auth, global, members } = state; + return { + // loading: members.isRequesting, + user: auth.user, + actions: global.actions, + // members: members.data, + }; +} + +export default connect(mapStateToProps)(FileModal); diff --git a/web/client/src/sections/means/containers/projectMeans.jsx b/web/client/src/sections/means/containers/projectMeans.jsx index 27f758a..66da006 100644 --- a/web/client/src/sections/means/containers/projectMeans.jsx +++ b/web/client/src/sections/means/containers/projectMeans.jsx @@ -1,33 +1,485 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { connect } from 'react-redux'; +import { Input, Button, Tree, Modal, Table, Upload, Pagination, Popconfirm } from '@douyinfe/semi-ui'; +import { IconDeleteStroked, IconEditStroked, IconUpload } from '@douyinfe/semi-icons'; +import SimpleBar from 'simplebar-react'; +import FileModal from '../components/fileModal'; +import moment from 'moment'; +import './style.less' const Rest = (props) => { - const { dispatch, actions, user, loading, socket } = props + const { dispatch, actions, user, loading, clientHeight, overallProjectId } = props + const { install, means } = actions + const [pomsList, setPomsList] = useState([]); //项目 + const [showPomsList, setShowPomsList] = useState([]); //项目 + const [pepProjectId, setPepProjectId] = useState() //项目id + const [projectSearch, setProjectSearch] = useState() //项目搜索 + const [isFileModal, setIsFileModal] = useState(false) //添加文件弹窗 + const [editData, setEditData] = useState({}) //编辑参数 + const [treeData, settreeData] = useState([]); //文件夹列表 + const [higherFile, setHigherFile] = useState([]); //上级文件夹 + const [fileId, setFileId] = useState(); //文件夹id + const [uploadModal, setUploadModal] = useState(false) //上传弹窗 + const [uploadData, setUploadData] = useState({}) + const [dataSource, setDataSource] = useState([]) //表格数据 + const [query, setQuery] = useState({ limit: 10, page: 0 }) + const [count, setCount] = useState(0) + + useEffect(() => { + dispatch(install.getProjectPoms({ global: 1 })).then((res => { + if (res.success) { + let data = res.payload.data?.rows?.filter(v => v.pepProjectIsDelete !== 1)?.map(v => ({ pepProjectId: v.id, pepProjectName: v.pepProjectName || v.name })) + setPomsList(data) + setShowPomsList(data) + setPepProjectId(data[0]?.pepProjectId) + fileList(data[0]?.pepProjectId) + } + })) }, []) + useEffect(() => { + let data + if (overallProjectId) { + data = pomsList?.filter(v => (v.pepProjectName?.indexOf(projectSearch) != -1 && v.pepProjectId == overallProjectId)) + } else { + data = pomsList?.filter(v => v.pepProjectName?.indexOf(projectSearch) != -1) + } + setShowPomsList(data) + setPepProjectId(data[0]?.pepProjectId) + fileList(data[0]?.pepProjectId) + }, [projectSearch]) + + useEffect(() => { + setProjectSearch('') + let data + if (overallProjectId) { + data = pomsList?.filter(v => v.pepProjectId == overallProjectId) + } else { + data = pomsList + } + setShowPomsList(data) + setPepProjectId(data[0]?.pepProjectId) + fileList(data[0]?.pepProjectId) + }, [overallProjectId]) + + useEffect(() => { + if (fileId) { + + filfolderFileListe() + } + }, [fileId]) + + const fileList = (id) => { + dispatch(means.fileList({ projectId: id })).then((res => { + if (res.success) { + let data = res.payload.data + let oneLevel = res.payload.data?.filter(f => !f.higherFileId) || [] + settreeData(listErgodic(oneLevel, data)) + setHigherFile(data?.map(d => ({ name: d.fileName, value: d.id }))) + } + })) + } + + + + const filfolderFileListe = (params) => { + let fileIds = [] + const treeDataList = (data, value) => { + data?.map(c => { + if (c.key == fileId || value) { + fileIds.push(c.key) + if (c.children?.length) { + treeDataList(c.children, true) + } + } else if (c.children?.length) { + treeDataList(c.children) + } + }) + } + console.log(treeData); + treeDataList(treeData) + let datas = params || query + console.log(fileIds); + dispatch(means.folderFileList({ fileId: JSON.stringify(fileIds), ...datas })).then((res => { + if (res.success) { + setDataSource(res.payload.data?.rows || []) + setCount(res.payload.data?.count) + } + })) + } + + const listErgodic = (level, datas) => { + let data = [] + level.map(v => { + let list = { + value: v.id, + key: v.id, + } + let childrenList = datas?.filter(c => c.higherFileId == list.value) + if (childrenList?.length) { + list.children = listErgodic(childrenList, datas) + } + let fileData + dispatch(means.folderFileList({ fileId: JSON.stringify([v.id]) })).then((res => { + if (res.success) { + fileData = res.payload.data?.rows?.length + } + })) + list.label =
+
{v.fileName}
+ { + setIsFileModal(true) + setEditData({ id: v.id, fileName: v.fileName, higherFileId: v.higherFileId }) + }} /> + { + if (!fileData || !list?.children?.length) { + dispatch(means.delFile(v.id)).then((res => { + if (res.success) { + fileList(pepProjectId) + } + })) + } + }} + // onCancel={onCancel} + > + + + +
+ + data.push(list) + }) + return data + } + + + const column = [ + { + title: '文件名', + dataIndex: 'name', + key: 'name', + render: (text) => {text}, + }, + { + title: '文件大小', + dataIndex: 'size', + key: 'size', + render: (text) => { + let show = '--' + if (text < 1048576) { + show = (text / 1024).toFixed(1) + ' KB' + } else { + show = (text / 1048576).toFixed(1) + ' MB' + } + return show + } + }, + { + title: '上传时间', + dataIndex: 'uploadTime', + key: 'uploadTime', + render: (text) => text ? moment(text).format("YYYY-MM-DD HH:mm:ss") : '--' + }, + { + title: '操作', + key: 'operation', + dataIndex: 'operation', + render: (_, r) => { + return
+ + { + dispatch(means.delfolderFile(r.id)).then((res => { + if (res.success) { + filfolderFileListe({ limit: 10, page: 0 }) + } + })) + }} + // onCancel={onCancel} + > + + + +
+ } + }, + ] + const data = [ + { + key: '1', + name: 'John Brown', + age: 32, + address: 'New York No. 1 Lake Park', + tags: ['nice', 'developer'], + }, + { + key: '2', + name: 'Jim Green', + age: 42, + address: 'London No. 1 Lake Park', + tags: ['loser'], + }, + { + key: '3', + name: 'Joe Black', + age: 32, + address: 'Sydney No. 1 Lake Park', + tags: ['cool', 'teacher'], + }, + ]; return ( - <> -
- + //
+
+ +
+ setProjectSearch(v)} /> + + {showPomsList?.map(v => { + return
{ + setPepProjectId(v.pepProjectId) + fileList(v.pepProjectId) + }}> + +
{v.pepProjectName}
+ +
+ })} +
- + +
+ {/* 文件夹———树 */} +
+ + {/* 添加文件弹窗 */} + {isFileModal ? + { + setIsFileModal(false) + setEditData({}) + }} + success={() => { + fileList(pepProjectId) + }} + /> : "" + } + + { + setFileId(selectedKey) + }} + /> + + +
+ {/* 表格 */} +
+
+ +
当前文件夹:{higherFile?.filter(c => c.value == fileId)[0]?.name ||
请选择文件夹
}
+ {uploadModal ? + { + dispatch(means.addFile({ ...uploadData, fileId: fileId })).then(v => { + if (v.success) { + setUploadModal(false) + filfolderFileListe({ limit: 10, page: 0 }) + } + }) + }} + width={607} + onCancel={() => { + setUploadModal(false) + }} + > +
+
文件:
+ { + console.log(11111, responseBody, file); + setUploadData({ + name: file.name, + size: file.size, + url: responseBody?.uploaded, + uploadTime: moment().format("YYYY-MM-DD HH:mm:ss") + }) + }} + > + + +
+
: "" + + } +
+ + + 暂无告警数据 + + } + onRow={(record, index) => { + if (index % 1 === 0) { + return { style: { background: '#FAFCFF' } } + } + }} + // rowSelection={{ + // // selectedRowKeys: selected || [], + // getCheckboxProps: record => ({ + // // disabled: record.confirmTime ? true : false, + // // name: record.name, + // }), + // onSelect: (record, selected) => { + // // console.log(`select row: ${selected}`, record); + // }, + // // onSelectAll: (selected, selectedRows) => { + // // console.log(`select all rows: ${selected}`, selectedRows); + // // }, + // onChange: (selectedRowKeys, selectedRows) => { + // // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); + // // setSelected(selectedRows?.map(v => v.id)) + // }, + // }} + /> + + + {count > 0 ?
+ + 共{count}个文件 + + { + setQuery({ limit: pageSize, page: currentPage - 1 }); + filfolderFileListe({ limit: pageSize, page: currentPage - 1 }) + }} + /> +
: ""} + {/*
+
+
勾选{selected.length}条问题
+ + +
+ {count > 0 ?
+ + 共{count}个问题 + + { + setQuery({ limit: pageSize, page: currentPage - 1 }); + }} + /> +
: ""} + +
*/} + + + + + // ) } function mapStateToProps (state) { const { auth, global, members, webSocket } = state; + return { // loading: members.isRequesting, - // user: auth.user, - // actions: global.actions, - // members: members.data, + user: auth.user, + actions: global.actions, + overallProjectId: global.pepProjectId, // socket: webSocket.socket + clientHeight: global.clientHeight }; } diff --git a/web/client/src/sections/means/containers/style.less b/web/client/src/sections/means/containers/style.less new file mode 100644 index 0000000..9b6c5f5 --- /dev/null +++ b/web/client/src/sections/means/containers/style.less @@ -0,0 +1,9 @@ + +.tip{ + display: none; +} +.dd:hover{ + .tip{ + display: inline-block; + } +} \ No newline at end of file diff --git a/web/client/src/sections/means/index.js b/web/client/src/sections/means/index.js index ddcb65f..f06d7e3 100644 --- a/web/client/src/sections/means/index.js +++ b/web/client/src/sections/means/index.js @@ -6,7 +6,7 @@ import actions from './actions'; import { getNavItem } from './nav-item'; export default { - key: 'facility', + key: 'means', name: '设备', reducers: reducers, routes: routes, diff --git a/web/client/src/utils/webapi.js b/web/client/src/utils/webapi.js index 86b60e5..529e6b2 100644 --- a/web/client/src/utils/webapi.js +++ b/web/client/src/utils/webapi.js @@ -69,7 +69,12 @@ export const ApiTable = { getAlarmsHandleStatistics: '/alarms/handle/statistics', //查询BI分析数据-问题处理效率分析 getLatestDynamic: 'latest/dynamic', // 查询最新动态 - + //资料库 + addEditFile: 'file/addEdit', //添加、编辑文件夹 + fileList: 'file/list', //获取问文件夹列表 + delFile: 'file/del/{id}', //删除文件夹 + file: 'file', + delfolderFile: 'file/{id}', //删除文件 }; export const RouteTable = { apiRoot: "/api/root", diff --git a/web/package.json b/web/package.json index fdff1bd..396a774 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,7 @@ "test": "mocha", "start-vite": "cross-env NODE_ENV=developmentVite npm run start-params", "start": "cross-env NODE_ENV=development npm run start-params", - "start-params": "node server -p 5600 -u http://localhost:4600 --apiPomsUrl http://localhost:4600 --apiAnxinyunUrl http://10.8.30.112:4100 --apiEmisUrl http://10.8.30.112:14000 --qnak XuDgkao6cL0HidoMAPnA5OB10Mc_Ew08mpIfRJK5 --qnsk yewcieZLzKZuDfig0wLZ9if9jKp2P_1jd3CMJPSa --qnbkt dev-highways4good --qndmn http://rhvqdivo5.hn-bkt.clouddn.com --iotVcmpWeb https://mediaconsole.ngaiot.com --pomsMonitor https://monitor.anxinyun.cn --dcWeb https://fsiot-oamss.anxinyun.cn", + "start-params": "node server -p 5600 -u http://localhost:4600 --apiPomsUrl http://localhost:4600 --apiAnxinyunUrl http://10.8.30.112:4100 --apiEmisUrl http://10.8.30.112:14000 --qnak 5XrM4wEB9YU6RQwT64sPzzE6cYFKZgssdP5Kj3uu --qnsk w6j2ixR_i-aelc6I7S3HotKIX-ukMzcKmDfH6-M5 --qnbkt anxinyun-test --qndmn http://test.resources.anxinyun.cn --iotVcmpWeb https://mediaconsole.ngaiot.com --pomsMonitor https://monitor.anxinyun.cn --dcWeb https://fsiot-oamss.anxinyun.cn", "deploy": "export NODE_ENV=production&& npm run build && node server", "build-dev": "cross-env NODE_ENV=development&&webpack --config webpack.config.js", "build": "cross-env NODE_ENV=production&&webpack --config webpack.config.prod.js" @@ -54,7 +54,9 @@ "@semi-bot/semi-theme-fscamera": "^1.0.0", "@vitejs/plugin-react": "^1.3.1", "@vitejs/plugin-react-refresh": "^1.3.6", + "ali-oss": "^6.17.1", "args": "^5.0.1", + "async-busboy": "^1.1.0", "babel-polyfill": "^6.26.0", "copy-to-clipboard": "^3.3.1", "cross-env": "^7.0.3", @@ -70,6 +72,7 @@ "npm": "^7.20.6", "perfect-scrollbar": "^1.5.5", "screenfull": "5.2.0", + "simplebar-react": "^3.2.4", "socket.io-client": "^1.7.4", "socket.io-parser": "^3.4.1", "superagent": "^6.1.0", diff --git a/web/routes/attachment/index.js b/web/routes/attachment/index.js index cff0286..e45964b 100644 --- a/web/routes/attachment/index.js +++ b/web/routes/attachment/index.js @@ -1,6 +1,6 @@ 'use strict'; const request = require('superagent'); -// const parse = require('async-busboy'); +const parse = require('async-busboy'); // const path = require('path') // const fs = require('fs'); @@ -49,8 +49,39 @@ module.exports = { await next(); } } + let upload_ = async function (ctx, next) { + let fkey = null; + try { + // const { p } = ctx.params; + console.log(111,ctx); + const { files } = await parse(ctx.req); + // console.log(files); + const file = files[0]; + // const extname = path.extname(file.filename).toLowerCase(); + // if (!UploadPath[p]) { + // ctx.status = 400; + // ctx.body = JSON.stringify({ error: '附件存放的文件夹名称无效' }); + // return; + // } else if (UploadPath[p].indexOf(extname) < 0) { + // ctx.status = 400; + // ctx.body = JSON.stringify({ error: '文件格式无效' }); + // return; + // } else { + const fileInfo = await ctx.app.fs.attachment.upload(file); + fkey = fileInfo.key; + console.log(11,fileInfo); + ctx.body = { uploaded: fkey }; + // } + } catch (err) { + ctx.status = 500; + ctx.fs.logger.error(err); + ctx.body = { err: 'upload error.' }; + } + } router.use(download); router.get('/api/root', getApiRoot); + router.post('/_upload/attachments', upload_); + } };