zhengzhou преди 3 години
родител
ревизия
90b120c68a
променени са 10 файла, в които са добавени 957 реда и са изтрити 21 реда
  1. +1
    -1
      src/router.js
  2. +136
    -0
      src/views/main_web/recycle/components/collapse-unit.vue
  3. +46
    -0
      src/views/main_web/recycle/components/item-list.vue
  4. +122
    -0
      src/views/main_web/recycle/components/list-file-item.vue
  5. +187
    -0
      src/views/main_web/recycle/components/list-folder-item.vue
  6. +442
    -0
      src/views/main_web/recycle/index.vue
  7. +23
    -0
      src/views/main_web/recycle/service.js
  8. +0
    -0
      src/views/main_web/workspace/components/recycle-file.deprecated.vue
  9. +0
    -0
      src/views/main_web/workspace/recyclebin.deprecated.vue
  10. +0
    -20
      src/views/main_web/workspace/service.js

+ 1
- 1
src/router.js Целия файл

@@ -83,7 +83,7 @@ const router = new Router({
{
path: '/recycle',
name: 'recycle',
component: () => import('@/views/main_web/workspace/recyclebin'),
component: () => import('@/views/main_web/recycle'),
},
{
path: '/history',


+ 136
- 0
src/views/main_web/recycle/components/collapse-unit.vue Целия файл

@@ -0,0 +1,136 @@
<template>
<div class="collapse-unit" :class="{ 'collapse-unit-active': active }">
<div class="collapse-unit-title" @click.stop="active = !active">
<span>{{title}}</span>
<div class="action-button">{{active ? '收起': '展开'}}</div>
</div>
<div class="collapse-unit-body">
<div
class="collapse-unit-item"
v-for="item in list"
:key="item[itemKey]"
:class="{ 'collapse-unit-item-active': item[itemKey] === selectedKey }"
@click.stop="clickItem(item)"
>
{{item[itemLabel]}}
</div>
</div>
</div>
</template>

<script>
export default {
model: {
prop: 'selectedKey',
event: 'select',
},
props: {
itemKey: {
type: String,
default: 'key',
},
itemLabel: {
type: String,
default: 'label',
},
title: String,
defaultExpand: Boolean,
selectedKey: String,
list: Array, // [{ key, label }]
},
data() {
return {
active: this.defaultExpand || false,
}
},
methods: {
clickItem(item) {
this.$emit('select', item[this.itemKey]);
}
}
}
</script>

<style lang="scss" scoped>
.collapse-unit {
margin-bottom: 10px;
&-active {
margin-bottom: 24px;
.action-button:after {
transform: rotate(-90deg);
}
.collapse-unit-body {
height: 0;
}
.collapse-unit-title {
background-color: #fff;
}
}
&-body {
border-radius: 8px;

font-size: 14px;
color: rgba(50, 50, 60, 100);
overflow: auto;
height: calc(100% - 44px);
transition: height 0.1s linear;
}
&-title {
position: relative;
padding-left: 24px;
color: rgba(0, 0, 0, 0.56);
font-size: 14px;
height: 44px;
line-height: 44px;
cursor: pointer;
border-radius: 8px;
background-color: transparent;
transition: background-color 0.1s linear;
.action-button {
position: absolute;
top: 0;
bottom: 0;
right: 40px;
&:after {
content: '';
position: absolute;
width: 0;
height: 0;
top: 0;
bottom: 0;
right: -12 - 10px;
margin: auto 0;
border: 6px solid transparent;
border-bottom-width: 0;
border-top-color: #999B9D;
transform-origin: center;
transition: transform 0.1s linear;
}
}
}
&-item {
height: 44px;
line-height: 44px;
padding-left: 24px;
cursor: pointer;
background-color: #fff;

&:first-child {
border-radius: 8px 8px 0 0;
}
&:last-child {
border-radius: 0 0 8px 8px;
}
~ .collapse-unit-item {
border-top: 1px solid rgba(0, 0, 0, 0.2);;
}
&:hover,
&-active {
background-color: rgba(50, 50, 60, 0.1);
}
}
}
</style>

+ 46
- 0
src/views/main_web/recycle/components/item-list.vue Целия файл

@@ -0,0 +1,46 @@
<template>
<div :style="{ paddingLeft: `${indent > 1 ? 10 : 0}px` }">
<template v-for="node in list">
<list-file-item
v-if="node.nodeType === 'file'"
:key="node.id"
:file="node"
:readOnly="readOnly"
:checked="selectedKeyMap[node.id]"
@check="check"
:indent="indent"
/>
<list-folder-item
v-else
:key="node.id"
:folder="node"
:readOnly="readOnly"
@check="check"
:selectedKeyMap="selectedKeyMap"
:indent="indent"
/>
</template>
</div>
</template>
<script>
import ListFileItem from './list-file-item';
import ListFolderItem from './list-folder-item';

export default {
components: { ListFileItem, ListFolderItem },
props: {
list: Array,
readOnly: Boolean,
selectedKeyMap: Object,
indent: {
type: Number,
default: 0,
}
},
methods: {
check(node) {
this.$emit('check', node);
}
}
}
</script>

+ 122
- 0
src/views/main_web/recycle/components/list-file-item.vue Целия файл

@@ -0,0 +1,122 @@
<template>
<div class="list-file-item">
<div class="list-file-item-cell">
<el-checkbox v-show="!readOnly" :value="!!checked" @change="onCheckChange" class="list-file-item-checkbox" />
<span :title="fileName">{{fileName}}</span>
</div>
<div class="list-file-item-cell">
<span>V{{file.version}}</span>
</div>
<!-- <div class="list-file-item-cell" >
<span>V{{file.version}}</span>
</div> -->
<div class="list-file-item-cell" >
<span>{{file.modifyName}}</span>
</div>
<div class="list-file-item-cell">
<span>{{file.modifyTime | formatTime}}</span>
</div>
<div class="list-file-item-cell">
<span></span>
</div>
<!-- <div class="list-file-item-cell" :style="{ flex: '90' }"></div> -->
</div>
</template>

<script>
import dayjs from 'dayjs';

const getMap = (...args) => args.reduce((h, v) => (h[v] = true, h), {});

const imgType = getMap('jpg', 'png', 'bmp', 'gif', 'jpeg');
const docType = getMap('doc','docx','ppt','pptx','xls','xlsx', 'pdf', 'txt');
const videoType = getMap('avi', 'mov', 'wav','mp4');
const exeType = getMap('exe');
const zipType = getMap('zip');

export default {
props: {
file: Object,
readOnly: Boolean,
checked: Boolean,
},
filters: {
formatTime(v) {
return dayjs(v).format('YYYY年MM月DD日 A HH:mm');
}
},
computed: {
fileName(){
const file = this.file;
return `${file.archName}${file.extension ? `.${file.extension}`:''}`;
},
fileType() {
const ext = this.file.extension;
if(imgType[ext]) return '图片';
if(docType[ext]) return '文档';
if(videoType[ext]) return '视频';
if(exeType[ext]) return '可执行程序';
if(zipType[ext]) return '压缩文件';
return '未识别';
}
},
methods: {
onCheckChange(){
this.$emit('check', this.file);
}
}
}
</script>

<style lang="less" scoped>
.list-file-item {
display: flex;
flex-direction: row;
font-size: 12px;
padding-left: 24px;

&:hover {
// border-radius: 4px;
background-color: rgba(50, 50, 60, 0.1);
}
&-cell {
height: 32px;
line-height: 32px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
&:not(:first-child) {
&:before {
content: '';
display: inline-block;
width: 1px;
height: 1px;
margin-right: 10px;
}
}
&:nth-child(1) {
flex: 1;
}
&:nth-child(2),
&:nth-child(3) {
flex: 0 0 121px;
}
&:nth-child(4) {
flex: 0 0 181px;
}
&:nth-child(5) {
flex: 0 0 121px;
}
}
&-checkbox {
margin-right: 5px;
::v-deep .el-checkbox__input {
line-height: 1em;
}
::v-deep .el-checkbox__inner {
width: 20px;
height: 20px;
}
}
}
</style>

+ 187
- 0
src/views/main_web/recycle/components/list-folder-item.vue Целия файл

@@ -0,0 +1,187 @@
<template>
<div
class="list-folder-item"
:class="{ large: indent === 0 }"
>
<div class="list-folder-item-header">
<div
@click.stop="expanded = !expanded"
class="list-folder-item-lever"
:class="{ 'list-folder-item-expanded': expanded }"
/>
<div
class="list-folder-item-cell"
:style="{ flex: 1 }"
>
<img
class="list-folder-item-icon"
src="static/img/file.png"
/>
<span>{{folder.folderName}}</span>
<div
v-if="!readOnly"
class="list-folder-item-checkall"
>
<el-checkbox
label="全选"
:value="isChildrenAllChecked"
@change="check(folder)"
/>
</div>
</div>
<!-- <div
class="list-folder-item-cell "
:class="{ 'list-folder-item-expanded': expanded }"
:style="{ flex: '90' }"
@click.stop="expanded = !expanded"
>
<span :style="{ marginLeft: '10px' }">{{ expanded ? '折叠' : '展开' }}</span>
</div> -->
</div>
<item-list
class="list-folder-item-body"
:class="{ 'list-folder-item-body-hide': !expanded }"
:list="folder.children"
@check="check"
:readOnly="readOnly"
:selectedKeyMap="selectedKeyMap"
:indent="indent + 1"
/>
</div>
</template>

<script>
export default {
components: { ItemList: () => import("./item-list") },
props: {
folder: Object,
readOnly: Boolean,
selectedKeyMap: Object,
indent: {
type: Number,
default: 0,
},
},
data() {
return {
expanded: this.indent === 0,
};
},
computed: {
isChildrenAllChecked() {
return recursionEvery(this.folder.children, this.selectedKeyMap);
},
},
methods: {
check(node) {
this.$emit("check", node);
},
},
};

function recursionEvery(list, checkedMap) {
return list.every((node) => {
if (node.nodeType === "file") return checkedMap[node.id];
return recursionEvery(node.children, checkedMap);
});
}
</script>

<style lang="less" scoped>
.list-folder-item {
&.large {
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
> .list-folder-item-header {
> .list-folder-item-cell {
height: 44px;
line-height: 44px;
font-size: 16px;
}
.list-folder-item-icon {
display: none;
}
.list-folder-item-checkall {
margin-left: 10px;
right: 24px;
::v-deep .el-checkbox__inner {
width: 20px;
height: 20px;
}
::v-deep .el-checkbox__label {
font-size: 16px;
}
}
}
}
&-header {
position: relative;
display: flex;
flex-direction: row;
// border-bottom: 1px solid rgba(0, 0, 0, 0.2);
// &:hover {
// // border-radius: 4px;
// background-color: rgba(50, 50, 60, 0.1);
// }
}
&-body {
overflow: hidden;
&-hide {
height: 0;
}
}
&-cell {
position: relative;
height: 32px;
line-height: 32px;
font-size: 12px;
}
&-icon {
@iconWidth: 22px;
position: relative;
top: (32px - @iconWidth) / 2;
left: -1px;
width: @iconWidth;
margin-right: 5px;
}
&-checkall {
position: absolute;
right: 17px;
top: 0;
}
&-lever {
cursor: pointer;
position: relative;
flex: 0 0 24px;
// &:before {
// content: '';
// position: absolute;
// left: 0;
// top: 0;
// bottom: 0;
// margin: auto 0;
// width: 1px;
// height: 25px;
// background-color: rgba(17, 17, 17, 0.1);
// }
&:after {
content: "";
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
right: 6px;
width: 0;
height: 0;
border: 6px solid transparent;
border-top-color: #999b9d;
border-bottom-width: 0;
transition: transform 0.1s linear;
transform: rotate(-90deg);
}
}
&-expanded:after {
transform: rotate(0deg);
}
}
</style>

+ 442
- 0
src/views/main_web/recycle/index.vue Целия файл

@@ -0,0 +1,442 @@
<template>
<div>
<app-header
theme="basis"
showBackBtn
:backBtnTitle="title"
showUserCenter
isInRecycle
showRecycleBin
>
<template #center>
<div class="main-title">回收站</div>
</template>
<template #right>
<!-- <search-bar
class="proj-search-bar"
placeholder="请输入要搜索的文件"
@search="searchForFile"
:maxLength="35"
/> -->
</template>
</app-header>

<div class="recycle_content">
<div class="view-left">
<!-- <div class="view-left-title">从成果中选择交付物</div> -->
<collapse-unit
title="正在进行的项目"
:list="aliveProjectList"
v-model="selectedProjectId"
itemKey="id"
itemLabel="projName"
style="max-height: 45%"
/>
<collapse-unit
title="被删除的项目"
:list="removedProjectList"
v-model="selectedProjectId"
itemKey="id"
itemLabel="projName"
style="max-height: 45%"
/>
</div>

<div class="view-right">
<div class="view-right-title">
<span>文件</span>
<el-button
class="view-right-title-extra"
@click.stop="selectable = !selectable"
:type="selectable ? 'primary': undefined"
>{{selectable ? '取消多选' : '多选'}}</el-button>
</div>
<div class="table-like">
<div class="table-header">
<div class="table-th">文件名</div>
<div class="table-th">版本号</div>
<!-- <div class="table-th">移除人</div> -->
<div class="table-th">最后修改人</div>
<div class="table-th">移除时间</div>
<div class="table-th">
<!-- 操作 -->
</div>
</div>

<item-list
:list="rightPanelItemList"
:readOnly="!selectable"
:selectedKeyMap="selectedKeyMap"
@check="toggleCheck"
/>
</div>
<div class="view-right-bottom" v-if="selectable" >
<el-button
class="bottom-button"
type="primary"
:disabled="!canShowButton"
>放回原处</el-button>
</div>
</div>
</div>

</div>
</template>
<script>
import AppHeader from "@/components/app-header/app-header.vue";
// import SearchBar from "@/components/search-bar/search-bar";
import CollapseUnit from "./components/collapse-unit.vue";
import ItemList from "./components/item-list";
import { VirtualFolder } from "../workspace/helper";
import { firstCharToLowerCase } from "@/utils/tool";
export default {
components: { AppHeader, /* SearchBar, */ CollapseUnit, ItemList },
mounted() {
this.fetchProjectList();
},
data() {
return {
userId: sessionStorage.userId,
title: "返回" + sessionStorage.pageTitle,
// --------------------------------------
aliveProjectList: [],
removedProjectList: [],
selectedProjectId: undefined,
rightPanelItemList: [],
selectable: false,
selectedKeyMap: {}, // 选中的文件的hash
};
},
watch: {
selectedProjectId(projectId) {
this.fetchProjectRemovedFiles(projectId);
this.selectable = false;
},
selectable() {
this.selectedKeyMap = {};
}
},
computed: {
canShowButton() {
return Object.values(this.selectedKeyMap).filter(a => a).length;
}
},
methods: {
searchForFile() {
debugger;
},
async fetchProjectList() {
const res = await this.$fetchApi("project/queryAllProjectListByUserId", {
userId: this.userId,
});
const [aliveProjectList, removedProjectList] = (res.Data || []).reduce(
(arr, upperProject) => {
const project = firstCharToLowerCase(upperProject);
if (project.nodeFolder) {
project.nodeFolder = project.nodeFolder.map(firstCharToLowerCase);
}
arr[project.deleted ? 1 : 0].push(project);

return arr;
},
[[], []]
);
this.aliveProjectList = aliveProjectList;
this.removedProjectList = removedProjectList;

this.selectedProjectId = aliveProjectList.length
? aliveProjectList[0].id
: removedProjectList.length
? removedProjectList[0].id
: undefined;
},
async fetchProjectRemovedFiles(projectId) {
if (!projectId) {
return;
}
const res = await this.$fetchApi(
"file/queryFilesFromRecycleBinByProjectId",
{ projectId }
);

const fileTreeList = (res.Data || []).reduce((oList, topNode) => {
if (topNode.ProjArchives) {
oList = oList.concat(
resolveFileListToTree(topNode.ProjArchives, topNode.FolderName)
);
}
return oList;
}, []);
this.rightPanelItemList = fileTreeList;
},
toggleCheck(node) {
const tempMap = this.selectedKeyMap;
if (node.nodeType === "file") {
this.selectedKeyMap = { ...tempMap, [node.id]: !tempMap[node.id] };
} else {
// folder
const relationKeys = [];
let checkFlag = true;
recursionForEach(node.children, (fileNode) => {
relationKeys.push(fileNode.id);
checkFlag = checkFlag && tempMap[fileNode.id];
});
const stateMap = relationKeys.reduce(
(h, key) => ((h[key] = !checkFlag), h),
{}
);
this.selectedKeyMap = { ...tempMap, ...stateMap };
}
},
},
};

function generateFolder(folderMap, filePath) {
const paths = filePath.split("/");
paths.forEach((_, idx) => {
const fullRelativePath = paths.slice(0, idx + 1).join("/");
if (folderMap[fullRelativePath]) return;
const folder = new VirtualFolder(fullRelativePath);
folder.children = [];
folder.nodeType = "folder";
const parentRelativePath = folder.parentFolderPath;
folderMap[fullRelativePath] = folder;
if (folderMap[parentRelativePath]) {
folderMap[parentRelativePath].children.push(folder);
}
});
}
function recursionForEach(list, f) {
list.forEach((iNode) => {
if (iNode.nodeType === "file") {
f(iNode);
return;
}
recursionForEach(iNode.children, f);
});
}
function resolveFileListToTree(upperFileList, nodeName) {
const folderMap = {}; // [key: folderPath]: folder
const headList = [];
upperFileList.forEach((upperFile) => {
const file = firstCharToLowerCase(upperFile);
file.nodeType = "file";
file.nodeFolderName = nodeName;
// delete file.nodeName;
if (!file.nodeFolderName) {
headList.push(file);
return;
}
const filePath = `${file.nodeFolderName}${
file.relativePath ? `/${file.relativePath}` : ""
}`;
generateFolder(folderMap, filePath);
const folder = folderMap[filePath];
folder.children.push(file);
});
// const headFolder = [];

Object.values(folderMap).forEach((folder) => {
// 取顶级文件夹
if (!folder.parentFolderPath) {
headList.push(folder);
}
// 排序
folder.children = folder.children
.filter((i) => i.nodeType === "file")
.concat(folder.children.filter((i) => i.nodeType !== "file"));
});

return headList;
}
function recursionEvery(list, checkedMap) {
return list.every((node) => {
if (node.nodeType === "file") return checkedMap[node.id];
return recursionEvery(node.children, checkedMap);
});
}
</script>

<style lang="scss" scoped>
.recycle_content {
padding: 0 40px;
height: 100%;
box-sizing: border-box;
$leftViewWidth: 328px;
.view-left {
display: inline-flex;
flex-direction: column;
vertical-align: top;
width: $leftViewWidth;
height: 100%;
padding-top: 8px;
box-sizing: border-box;
}
.view-right {
display: inline-block;
vertical-align: top;
width: calc(100% - #{$leftViewWidth} - 32px);
height: 100%;
margin-left: 32px;
// padding-top: 14px;
box-sizing: border-box;
background-color: rgba(252, 252, 252, 1);
border-radius: 8px 8px 0 0;
overflow: hidden;
box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.1),
0px 1px 3px 0px rgba(0, 0, 0, 0.1);
&-title {
position: relative;
height: 56px;
line-height: 56px;
color: rgba(50, 50, 60, 100);
font-size: 20px;
padding-left: 24px;
background-color: rgba(248, 248, 248, 1);
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
&-extra {
position: absolute;
top: 12px;
right: 24px;
}
}
&-button {
margin-left: calc(50% - #{$leftViewWidth} / 2 - 120px);
box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.1),
0px 2px 5px 0px rgba(0, 0, 0, 0.16);
margin-bottom: 6px;
}

&-bottom {
height: 80px;
display: flex;
justify-content: center;
align-items: center;
.bottom-button {
width: calc(100% - 40px);
height: 40px;
}
}
}

.table-like {
height: calc(100% - 80px - 56px);
overflow: auto;
.table-header {
display: flex;
flex-direction: row;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);

.table-th {
position: relative;
box-sizing: border-box;
color: #32323c;
// flex: 124;

font-size: 12px;
height: 44px;
line-height: 44px;
&:nth-child(1) {
flex: 1;
padding-left: 24px;
}
&:nth-child(2),
&:nth-child(3) {
flex: 0 0 121px;
}
&:nth-child(4) {
flex: 0 0 181px;
}
&:nth-child(5) {
flex: 0 0 121px;
}
// &:nth-child(5) {
// flex: 90;
// }
&:not(:first-child):not(:last-child) {
&:after {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
margin: auto 0;
width: 1px;
height: 35px;
background-color: rgba(17, 17, 17, 0.1);
}
&:before {
content: "";
display: inline-block;
width: 1px;
height: 1px;
margin-right: 10px;
}
}
}
}
}

.panel {
&:not(:first-child) {
border-top: 1px solid rgba(0, 0, 0, 0.2);
}
&-wrapper {
border-top-color: transparent;
border-bottom-color: transparent;
}
::v-deep .el-collapse-item__header,
::v-deep .el-collapse-item__wrap {
background-color: transparent;
// border-bottom-color: transparent;
border-bottom: 0;
}
// ::v-deep .el-collapse-item__wrap {
// }
::v-deep .el-collapse-item__header {
padding-left: 24px;
height: 43px;
line-height: 43px;
}

.list-item {
position: relative;
padding-left: 24px;
height: 44px;
line-height: 44px;
cursor: pointer;
> span {
position: relative;
z-index: 2;
}
&:before {
content: "";
position: absolute;
top: 0px;
left: 24px;
right: 0;
height: 1px;
background-color: rgba(0, 0, 0, 0.2);
transform: scaleY(0.5);
}
&.disabled {
cursor: default;
}
&:not(.disabled):hover,
&-active {
&:after {
content: "";
position: absolute;
z-index: 1;
top: 0;
bottom: 0;
left: 10px;
right: 0px;
background-color: rgba(#cbcbce, 0.5);
border-radius: 10px 0 0 10px;
}
}
}
}
}
</style>

+ 23
- 0
src/views/main_web/recycle/service.js Целия файл

@@ -0,0 +1,23 @@
import { fetchApi, wrapErrorHint } from "@/utils/request";
import { firstCharToLowerCase } from "@/utils/tool";

/**
* 查询删除的文件
*/
export async function queryFilesFromRecycleBin() {
const res = await fetchApi('file/queryFilesFromRecycleBin');
wrapErrorHint(res);
if (res.Code !== 0) return null;
const fileList = (res.Data || []).map(file => {
const lower = firstCharToLowerCase(file);
return lower;
});
return fileList;
}
/**
* 文件放回原处
*/
export async function removeFromRecycleBin(id) {
const res = await fetchApi('file/removeFromRecycleBin', { fileId: id });
return wrapErrorHint(res);
}

src/views/main_web/workspace/components/recycle-file.vue → src/views/main_web/workspace/components/recycle-file.deprecated.vue Целия файл


src/views/main_web/workspace/recyclebin.vue → src/views/main_web/workspace/recyclebin.deprecated.vue Целия файл


+ 0
- 20
src/views/main_web/workspace/service.js Целия файл

@@ -19,26 +19,6 @@ export async function fetchWorkFlow(projectId, userId) {
}, []);
return list;
}
/**
* 查询删除的文件
*/
export async function queryFilesFromRecycleBin() {
const res = await fetchApi('file/queryFilesFromRecycleBin');
wrapErrorHint(res);
if (res.Code !== 0) return null;
const fileList = (res.Data || []).map(file => {
const lower = firstCharToLowerCase (file);
return lower;
});
return fileList;
}
/**
* 文件放回原处
*/
export async function removeFromRecycleBin(id){
const res = await fetchApi('file/removeFromRecycleBin',{fileId:id});
return wrapErrorHint(res);
}

/**
* 查询文件夹下边的文件(包含子文件夹以及协同文件)


Зареждане…
Отказ
Запис