类似Xftp这种本地文件传到服务端在网页的实现
需要开发本地应用,可以获取本地的目录文件信息,通过接口通过应用去获取本地的目录文件信息,上传也是通过应用去上传
接口
getDir接口 传入path
初始化传入的是空字符串,返回
{
"fileList": [
{
"name": "C",
"type": "disk",
"path": "C:/"
},
{
"name": "D",
"type": "disk",
"path": "D:/"
}
],
"msg": "ok",
"msg-no": 0,
"system": "windows"
}
如果是硬盘,传的是D:/ ,带后缀斜杆
文件夹,传的是 C:/Users,不带后缀斜杆
传入C:/user,返回
{
"fileList": [
{
"name": "All Users",
"type": "file",
"path": "C:/Users/All Users"
},
{
"name": "Default",
"type": "dir",
"path": "C:/Users/Default"
},
{
"name": "Default User",
"type": "file",
"path": "C:/Users/Default User"
}
],
"msg": "ok",
"msg-no": 0,
"system": "windows"
}
资源管理器
用table来实现文件列表,主要是双击,单击有现成的api可以用,挺方便吧,看下面
<Table
onRow={(record, index, b) => {
return {
onDoubleClick:
渲染文件列表
componentDidMount() {
// 初始化两个表格数据
this.getFilesFromPath("");
}
getFilesFromPath = (path, type) => {
// 去掉 S:/壁纸/ 这种 后面的斜杆 C:/这种不用去
if (path.endsWith("/") && path.lastIndexOf("/") > 2) {
path = path.slice(0, -1);
}
getDir(path)
.then((res) => {
this.setState({ nowChangePath: path });
if (res.data["msg-no"] !== 0) {
message.info("没有这个路径!", 2);
this.setState({ nowChangePath: this.state.nowPath });
return;
}
let fileList = res?.data?.fileList;
let prePath;
//这里获取上一层目录路径
let singleFolder = path.indexOf("/") == path.lastIndexOf("/");
if (singleFolder) {
if (path.indexOf("/") + 1 == path.length) {
prePath = "";
} else {
prePath = path.slice(0, path.indexOf("/") + 1);
}
} else {
prePath = path.slice(0, path.lastIndexOf("/"));
}
//筛选掉一些文件,只显示需要的类型的文件,
fileList = fileList.filter((i) => {
if (i.type == "file") {
let after = i.name.split(".").pop();
//allowedFileTypes :['jpg','png']
if (!this.allowedFileTypes.includes(after.toLowerCase())) {
return false;
} else {
let color = "";
switch (after.toLowerCase()) {
case "tif":
case "tiff":
color = "#91BE89";
break;
//。。。
default:
break;
}
i.icon = <PictureOutlined style={{ ...this.iconStyle, color }} />;
return true;
}
} else if (i.type == "disk") {
i.icon = (
<Icon component={disk} name="disk" style={this.iconStyle} />
);
} else if (i.type == "dir") {
i.icon = (
<FolderFilled style={{ ...this.iconStyle, color: "#f8b700" }} />
);
}
return true;
});
//如果不是在disk,要有个双击可以返回上一层的文件夹显示
if (!fileList[0] || fileList[0].type !== "disk") {
fileList.unshift({
name: "..",
type: "dir",
path: prePath,
});
}
this.setState({ fileList, nowPath: path });
})
.catch((e) => {
console.log({ e });
});
};
<Table
sticky={true}
columns={columns}
dataSource={fileList}
pagination={false}
rowClassName="rowStyle"
rowKey="path"
ctrl,shift+单击选文件
表格rowClassName="rowStyle"
文件选中后样式改为pathSelected,获取文件直接通过dom选择器得到选中的文件
至于为什么不用变量数组存,我觉得用dom上存的挺好呀,进入上下层文件夹也不用清空数组,样式变的也挺快
用数组存的话也要搞样式,那不如只搞样式呗
逻辑最复杂的应当是shift+click多选
单击选中,其他全不选
ctrl多选,选中的改为不选,不选的改为选
shift的多选,从nowSelected选中元素到当前元素,如果nowSelected为空,从最前面到当前元素
<Table
sticky={true}
columns={columns}
dataSource={fileList}
pagination={false}
rowClassName="rowStyle"
rowKey="path"
onRow={(record, index) => {
return {
onDoubleClick: (event) => {
if (record.name == "..") {
this.setState({ nowSelected: "" });
}
// console.log({ record }, 233);
if (record.type !== "file") {
this.getFilesFromPath(record.path);
}
},
onClick: (event) => {
if (record.name == "..") return;
// 分三种情况,单点
let status = "click";
if (event.ctrlKey) {
status = "ctrlKey";
}
if (event.shiftKey) {
status = "shiftKey";
}
if (status == "click") {
this.setState({
nowSelected: event.target.parentNode,
});
document
.querySelectorAll(".pathSelected")
.forEach((i) => (i.className = ""));
event.target.className = "pathSelected";
} else if (status == "ctrlKey") {
this.setState({
nowSelected: event.target.parentNode,
});
event.target.className === "pathSelected"
? (event.target.className = "")
: (event.target.className = "pathSelected");
} else if (status == "shiftKey") {
// 所有的已选
let selected = Array.from(
document.querySelectorAll(".pathSelected")
);
// 所有的列表
let row = Array.from(
document.querySelectorAll(".rowStyle")
);
// 去掉上一级的这个文件夹
if (row[0].innerText == "..") {
row = row.slice(1);
}
console.log({ selected, row });
// 一个都么有
if (selected.length == 0) {
// console.log("??");
for (let i = 0; i < index; i++) {
row[i].childNodes[0].className = "pathSelected";
}
} else if (selected.length >= 1) {
document
.querySelectorAll(".pathSelected")
.forEach((i) => (i.className = ""));
// 只有一个
// 有多个
let firstIndex = row.indexOf(
this.state.nowSelected
);
if (firstIndex >= index) {
for (let i = index - 1; i < firstIndex + 1; i++) {
row[i].childNodes[0].className = "pathSelected";
}
} else {
for (let i = firstIndex; i < index; i++) {
row[i].childNodes[0].className = "pathSelected";
}
}
}
}
}, // 点击行
};
}}
/>
双击进入子文件夹
具体参考上面的
onDoubleClick: (event) => {
if (record.name == "..") {
this.setState({ nowSelected: "" });
}
// console.log({ record }, 233);
if (record.type !== "file") {
this.getFilesFromPath(record.path);
}
},
ctrl+a全选文件
document.onkeydown = function (e) {
var keyCode = e.keyCode || e.which || e.charCode;
var ctrlKey = e.ctrlKey;
// 拦截 ctrl+a ,把第一个表格的文件都改样式
if (ctrlKey && keyCode == 65) {
let row = Array.from(document.querySelectorAll(".table1 .rowStyle"));
if (row[0].innerText == "..") {
row = row.slice(1);
}
row.forEach((i) => (i.childNodes[0].className = "pathSelected"));
e.preventDefault();
}
};
计算父文件夹路径
path:当前路径
prePath:上层路径
//如果只有一个斜杆,说明是硬盘,上层就是空
//如果多个斜杆,截取最后一个斜杆前的字符串
let singleFolder = path.indexOf("/") == path.lastIndexOf("/");
if (singleFolder) {
if (path.indexOf("/") + 1 == path.length) {
prePath = "";
} else {
//忘记为啥这样做了
prePath = path.slice(0, path.indexOf("/") + 1);
}
} else {
prePath = path.slice(0, path.lastIndexOf("/"));
}
支持通过输入修改路径
<Input
style={{ margin: "0 10px" }}
size="small"
value={nowChangePath}
onChange={this.nowPathChange}
onPressEnter={(e) =>
this.getFilesFromPath(e.target.value)
}
/>
nowPathChange = (e, type) => {
let value = e.target.value;
let path = type ? "nowRemoteChangePath" : "nowChangePath";
this.setState({ [path]: value });
};
getFilesFromPath:参考 渲染文件列表
跳转后路径焦点聚焦到最后面
this.remoteInputRef.current.focus({
cursor: "end",
});
样式
主要是边距,字体,文件选中的样式。
# index.scss
.uploadModal {
.ant-modal-body {
padding: 24px 24px 0;
}
.ant-table-thead {
line-height: 0;
}
.rowStyle {
//rowStyle嵌pathSelected,选中的时候容易选错地方,所以要设user-select: none;
line-height: 0;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.ant-table-cell {
padding: 0 !important;
border: none;
}
}
.ant-table-tbody > tr > td {
padding: 0 !important;
border: none;
}
.pathSelected {
background-color: #cce8ff !important;
padding: 0 !important;
}
.progressText {
position: absolute;
left: 60px;
z-index: 999;
}
.ant-progress-success-bg,
.ant-progress-bg {
border-radius: 0;
}
.ant-progress-inner {
border-radius: 0;
}
}
全代码
import React, { Component } from "react";
import { Modal, Input, Button, Table, message } from "antd";
import Icon, {
UndoOutlined,
PictureOutlined,
FolderFilled,
FileFilled,
ArrowUpOutlined,
} from "@ant-design/icons";
import { ReactComponent as disk } from "../../../shared/img/files/disk.svg";
import axios from "axios";
import { getDir, uploadFile } from "./file.js";
export default class UploadModal extends Component {
state = {
fileList: [], //第一个表格的数据
remoteFileList: [],
nowPath: "", //本地当前路径
nowChangePath: "", //本地路径更改
nowRemoteChangePath: "", //远程更改路径
nowSelected: "", //当前点击选中元素
remotePath: window.localStorage.getItem("remotePath") || "/datasets", //远程当前路径
};
remoteInputRef = React.createRef();
//第一个表格文件里要筛选展示的文件类型,其他的不要
allowedFileTypes = [
"tif",
"tiff",
"tmap",
"kfb",
"mrxs",
"svs",
"sdpc",
"zyp",
"czi",
"png",
"jpg",
// 下面很少见,可以搞掉
"svslide",
"vms",
"ndpi",
"scn",
"bif",
"ini",
"dat",
];
iconStyle = { marginRight: "5px", pointerEvents: "none" };
orgForm = React.createRef();
componentDidMount() {
// 初始化两个表格数据
this.getFilesFromPath("");
this.getFilesFromRemotePath(this.state.remotePath);
document.onkeydown = function (e) {
var keyCode = e.keyCode || e.which || e.charCode;
var ctrlKey = e.ctrlKey;
// 拦截 ctrl+a ,把第一个表格的文件都改样式
if (ctrlKey && keyCode == 65) {
let row = Array.from(document.querySelectorAll(".table1 .rowStyle"));
if (row[0].innerText == "..") {
row = row.slice(1);
}
row.forEach((i) => (i.childNodes[0].className = "pathSelected"));
e.preventDefault();
}
};
// xiao
}
componentWillUnmount() {
document.onkeydown = null;
}
// 传入路径,得到对应的文件列表
getFilesFromPath = (path, type) => {
// 去掉 S:/壁纸/ 这种 后面的斜杆 C:/这种不用去
if (path.endsWith("/") && path.lastIndexOf("/") > 2) {
path = path.slice(0, -1);
console.log({ path });
}
getDir(path)
.then((res) => {
this.setState({ nowChangePath: path });
if (res.data["msg-no"] !== 0) {
message.info("没有这个路径!", 2);
this.setState({ nowChangePath: this.state.nowPath });
return;
}
let fileList = res?.data?.fileList;
let prePath;
let singleFolder = path.indexOf("/") == path.lastIndexOf("/");
if (singleFolder) {
if (path.indexOf("/") + 1 == path.length) {
prePath = "";
} else {
prePath = path.slice(0, path.indexOf("/") + 1);
}
} else {
prePath = path.slice(0, path.lastIndexOf("/"));
}
//这一条是筛选出allowedFileTypes包含类型的文件,并赋予相应的特殊图标颜色
fileList = fileList.filter((i) => {
if (i.type == "file") {
let after = i.name.split(".").pop();
if (!this.allowedFileTypes.includes(after.toLowerCase())) {
// if (!this.allowedFileTypes.includes(after)) {
return false;
} else {
let color = "";
switch (after.toLowerCase()) {
case "tif":
case "tiff":
color = "#91BE89";
break;
case "tmap":
color = "#184C0E";
break;
case "kfb":
color = "#E2FF85";
break;
case "mrxs":
color = "#92BA0F";
break;
case "svs":
color = "#8FA058";
break;
case "sdpc":
color = "#757703";
break;
case "zyp":
color = "#989A40";
break;
case "czi":
color = "#F2DD92";
break;
case "png":
color = "#0c7d9d";
break;
case "jpg":
color = "#0c7d9d";
break;
default:
break;
}
i.icon = <PictureOutlined style={{ ...this.iconStyle, color }} />;
return true;
}
} else if (i.type == "disk") {
i.icon = (
<Icon component={disk} name="disk" style={this.iconStyle} />
);
} else if (i.type == "dir") {
i.icon = (
<FolderFilled style={{ ...this.iconStyle, color: "#f8b700" }} />
);
}
return true;
});
if (!fileList[0] || fileList[0].type !== "disk") {
fileList.unshift({
name: "..",
type: "dir",
path: prePath,
});
}
// if(!this.state.ifRunSoftware){this.setState({ifRunSoftware:true})}
this.setState({ fileList, nowPath: path });
// console.log({ fileList });
})
.catch((e) => {
console.log({ e });
});
};
getFilesFromRemotePath = (remotePath) => {
if (!remotePath.startsWith("/datasets")) {
message.info("只能访问以 /datasets 开头的文件夹");
return;
}
if (remotePath.endsWith("/")) {
remotePath = remotePath.slice(0, -1);
}
axios
.get(
`${process.env.REACT_APP_BASE_APP.slice(
0,
-4
)}/client_api/content?path=${remotePath}`
)
.then((res) => {
this.setState({ nowRemoteChangePath: remotePath });
if (res.data["msg-no"] !== 0) {
message.info("没有这个路径!", 2);
this.setState({ nowRemoteChangePath: this.state.remotePath });
return;
}
window.localStorage.setItem("remotePath", remotePath);
this.remoteInputRef.current.focus({
cursor: "end",
});
let fileList = res?.data?.fileList;
let prePath = remotePath.slice(0, remotePath.lastIndexOf("/"));
fileList = fileList.filter((i) => {
if (i.type == "file") {
let after = i.name.split(".").pop();
// console.log({ after });
if (!this.allowedFileTypes.includes(after.toLowerCase())) {
i.icon = (
<FileFilled style={{ ...this.iconStyle, color: "#aaa" }} />
);
// return false;
return true;
} else {
let color = "";
switch (after.toLowerCase()) {
case "tif":
case "tiff":
color = "#91BE89";
break;
case "tmap":
color = "#184C0E";
break;
case "kfb":
color = "#E2FF85";
break;
case "mrxs":
color = "#92BA0F";
break;
case "svs":
color = "#8FA058";
break;
case "sdpc":
color = "#757703";
break;
case "zyp":
color = "#989A40";
break;
case "czi":
color = "#F2DD92";
break;
case "png":
color = "#0c7d9d";
break;
case "jpg":
color = "#0c7d9d";
break;
case "svslide":
case "dat":
case "vms":
case "ndpi":
case "scn":
case "bif":
case "ini":
color = "#aaa";
break;
default:
break;
}
i.icon = <PictureOutlined style={{ ...this.iconStyle, color }} />;
return true;
}
} else if (i.type == "disk") {
i.icon = (
<Icon component={disk} name="disk" style={this.iconStyle} />
);
} else if (i.type == "dir") {
i.icon = (
<FolderFilled style={{ ...this.iconStyle, color: "#f8b700" }} />
);
}
return true;
});
if (remotePath !== "/datasets") {
fileList.unshift({
name: "..",
type: "dir",
path: prePath,
});
}
this.setState({ remoteFileList: fileList, remotePath });
});
};
upDir = (type) => {
let { fileList, remoteFileList } = this.state;
let list = type == "remote" ? remoteFileList : fileList;
if (list[0].name == "..") {
let fun =
type == "remote" ? this.getFilesFromRemotePath : this.getFilesFromPath;
fun(list[0].path);
} else {
message.info("不能再上一层了", 2);
}
};
upload = async () => {
let selected = Array.from(document.querySelectorAll(".pathSelected"));
if (selected.length == 0) {
message.info("请选择至少一个文件", 2);
return;
}
try {
await this.orgForm.current.validateFields();
let value = this.orgForm.current.getFieldsValue();
value.slide_type = value.slide_type?.[0];
selected = selected.map((i) => i.parentNode.getAttribute("data-row-key"));
document
.querySelectorAll(".pathSelected")
.forEach((i) => (i.className = ""));
let upload_folder = this.state.remotePath;
let { user_id } = this.state;
let file_list = selected.map((i) => ({
path: i,
upload_folder,
user_id,
...value,
}));
console.log({ file_list });
uploadFile({ file_list }).then((res) => {
if (res.data.data.failList.length > 0) {
message.info(res.data.data.failList[0].errMsg, 2);
} else {
message.success("操作成功", 2);
}
});
console.log({ file_list });
setTimeout(
() => this.getFilesFromRemotePath(this.state.remotePath),
1000
);
} catch (err) {
message.error("上面的选项要填才可以上传喔", 2);
}
};
nowPathChange = (e, type) => {
let value = e.target.value;
let path = type ? "nowRemoteChangePath" : "nowChangePath";
this.setState({ [path]: value });
};
render() {
const {
fileList,
diseases,
origins,
nowChangePath,
remoteFileList,
nowRemoteChangePath,
createPathLoading,
} = this.state;
const { visible, onCancel } = this.props;
const columns = [
{
title: "名字",
dataIndex: "name",
render: (text, record) => (
<div style={{ padding: "4px", height: 20, pointerEvents: "none" }}>
{record.icon}
<span
style={{
fontSite: "12px",
fontWeight: "normal",
pointerEvents: "none",
}}
>
{text}
</span>
</div>
),
},
];
return (
<Modal
className="uploadModal"
title="上传"
visible={visible}
footer={null}
width="90%"
onCancel={onCancel}
centered={true}
destroyOnClose
>
<div style={{ display: "flex", height: 400 }}>
<div
style={{
flex: 1,
height: "100%",
flexDirection: "column",
display: "flex",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
background: "#eee",
}}
>
<div style={{ display: "flex", height: 32, flex: 1 }}>
<span
style={{ padding: 5, background: "#aaa", cursor: "pointer" }}
onClick={this.upDir.bind(this, "local")}
>
<ArrowUpOutlined />
</span>
<span
style={{
padding: 5,
width: 50,
marginLeft: 10,
// marginRight: 10,
}}
>
本地
</span>
<Input
style={{ margin: "0 10px" }}
size="small"
value={nowChangePath}
onChange={this.nowPathChange}
onPressEnter={(e) =>
this.getFilesFromPath(e.target.value, "change")
}
/>
</div>
<span
style={{ background: "#aaa", padding: 5, cursor: "pointer" }}
onClick={() => this.getFilesFromPath(this.state.nowPath)}
>
<UndoOutlined />
</span>
</div>
<div style={{ flex: 1, overflow: "auto" }} className="table1">
{fileList.length > 0 ? (
<Table
sticky={true}
columns={columns}
dataSource={fileList}
pagination={false}
rowClassName="rowStyle"
rowKey="path"
onRow={(record, index, b) => {
return {
onDoubleClick: (event) => {
if (record.name == "..") {
this.setState({ nowSelected: "" });
}
// console.log({ record }, 233);
if (record.type !== "file") {
this.getFilesFromPath(record.path);
}
},
onClick: (event) => {
// console.log(
// event.target.parentNode.getAttribute("data-row-key")
// );
if (record.name == "..") return;
// 分三种情况,单点
let status = "click";
if (event.ctrlKey) {
status = "ctrlKey";
}
if (event.shiftKey) {
status = "shiftKey";
}
if (status == "click") {
this.setState({
nowSelected: event.target.parentNode,
});
document
.querySelectorAll(".pathSelected")
.forEach((i) => (i.className = ""));
event.target.className = "pathSelected";
} else if (status == "ctrlKey") {
this.setState({
nowSelected: event.target.parentNode,
});
event.target.className === "pathSelected"
? (event.target.className = "")
: (event.target.className = "pathSelected");
} else if (status == "shiftKey") {
// 所有的已选
let selected = Array.from(
document.querySelectorAll(".pathSelected")
);
// 所有的列表
let row = Array.from(
document.querySelectorAll(".rowStyle")
);
// 去掉上一级的这个文件夹
if (row[0].innerText == "..") {
row = row.slice(1);
}
console.log({ selected, row });
// 一个都么有
if (selected.length == 0) {
// console.log("??");
for (let i = 0; i < index; i++) {
row[i].childNodes[0].className = "pathSelected";
}
} else if (selected.length >= 1) {
document
.querySelectorAll(".pathSelected")
.forEach((i) => (i.className = ""));
// 只有一个
// 有多个
let firstIndex = row.indexOf(
this.state.nowSelected
);
if (firstIndex >= index) {
for (let i = index - 1; i < firstIndex + 1; i++) {
row[i].childNodes[0].className = "pathSelected";
}
} else {
for (let i = firstIndex; i < index; i++) {
row[i].childNodes[0].className = "pathSelected";
}
}
}
}
}, // 点击行
};
}}
/>
) : (
<div>
插件不能正常运行,尝试下载最新的插件,
<a
href="/cvs_client/CVSClient_Setup_x64_1.0.0.8.exe"
target="_blank"
style={{ color: "blue" }}
>
点此下载插件
</a>
</div>
)}
</div>
</div>
<div
style={{
width: 100,
height: "100%",
background: "#eee",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
}}
>
{/* <Button type="primary" onClick={this.upload}> */}
<Button type="primary" onClick={this.upload}>
2、上传
</Button>
</div>
<div
style={{
flex: 1,
height: "100%",
flexDirection: "column",
display: "flex",
}}
>
<div
style={{
// marginLeft: 10,
background: "#eee",
display: "flex",
justifyContent: "space-between",
}}
>
<div style={{ display: "flex", height: 32, flex: 1 }}>
<span
style={{ padding: 5, background: "#aaa", cursor: "pointer" }}
onClick={this.upDir.bind(this, "remote")}
>
<ArrowUpOutlined />
</span>
<span
style={{
padding: 5,
width: 50,
marginLeft: 10,
}}
>
远程
</span>
<Input
style={{ margin: "0 10px" }}
size="small"
ref={this.remoteInputRef}
value={nowRemoteChangePath}
onChange={(e) => this.nowPathChange(e, "remote")}
onPressEnter={(e) =>
this.getFilesFromRemotePath(e.target.value)
}
/>
</div>
<span
style={{
background: "#aaa",
padding: 5,
cursor: "pointer",
}}
onClick={() =>
this.getFilesFromRemotePath(this.state.remotePath)
}
>
<UndoOutlined />
</span>
</div>
<div style={{ flex: 1, overflow: "auto" }}>
<Table
columns={columns}
dataSource={remoteFileList}
pagination={false}
sticky={true}
rowClassName="rowStyle"
rowKey="path"
onRow={(record, index, b) => {
return {
onDoubleClick: (event) => {
if (record.name == "..") {
this.setState({ nowSelected: "" });
}
console.log({ record }, 233);
if (record.type !== "file") {
this.getFilesFromRemotePath(record.path, record.name);
}
},
};
}}
/>
</div>
</div>
</div>
</Modal>
);
}
}