This commit is contained in:
Qiu
2026-06-13 18:35:43 +08:00
commit 868598c2fd
185 changed files with 19611 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
# ==================
# IDE & Editor
# ==================
.idea/
.vscode/
*.iws
*.iml
*.ipr
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
# ==================
# OS
# ==================
.DS_Store
Thumbs.db
# ==================
# Java (easypan-java)
# ==================
HELP.md
target/
build/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
!**/src/main/**/build/
!**/src/test/**/build/
# ==================
# Frontend (easypan-front)
# ==================
node_modules/
easypan-front/dist/
# ==================
# Logs
# ==================
*.log
+3
View File
@@ -0,0 +1,3 @@
# easypan
简单网盘项目
前后端分离
+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="/hls.min.js"></script>
<title>SmallPan</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+2384
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
{
"name": "easypan-front-web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite --mode dev",
"build": "vite build --mode build",
"preview": "vite preview"
},
"dependencies": {
"@highlightjs/vue-plugin": "^2.1.0",
"@moefe/vue-aplayer": "^2.0.0-beta.5",
"aplayer": "^1.10.1",
"axios": "^1.3.4",
"docx-preview": "^0.1.15",
"dplayer": "^1.27.1",
"element-plus": "^2.2.36",
"highlight.js": "^11.7.0",
"hls.js": "^1.1.5",
"js-md5": "^0.7.3",
"pinia": "^2.0.32",
"sass": "^1.59.2",
"sass-loader": "^13.2.0",
"spark-md5": "^3.0.2",
"vue": "^3.2.47",
"vue-clipboard3": "^2.0.0",
"vue-cookies": "^1.8.3",
"vue-pdf-embed": "^1.1.5",
"vue-router": "^4.1.6",
"vue3-pdfjs": "^0.1.6",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"vite": "^4.1.4"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
<template>
<el-config-provider :locale="locale" :message="config">
<router-view></router-view>
</el-config-provider>
</template>
<script setup>
import { reactive } from "vue";
import framework from "@/views/Framework.vue";
import zhCn from "element-plus/lib/locale/lang/zh-cn";
const locale = zhCn;
const config = reactive({
max: 1,
});
</script>
<style scoped>
</style>
+65
View File
@@ -0,0 +1,65 @@
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgb(239, 239, 239);
border-radius: 2px;
}
::-webkit-scrollbar-thumb {
background: #bfbfbf;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #bfbfbf;
}
::-webkit-scrollbar-corner {
background: #bfbfbf;
}
:root {
--link: #007fff;
--pink: #FF6699;
--text2: #61666d;
--icon: #9499a0;
}
body {
padding: 0px;
margin: 0px;
font-size: 14px;
color: #636d7e;
}
* {
box-sizing: border-box;
padding: 0;
border: 0;
outline: 0;
vertical-align: middle;
}
.a-link {
text-decoration: none;
color: var(--link);
cursor: pointer;
}
.el-form-item {
align-items: center;
}
.container-body {
margin: 0px auto;
padding-top: 5px;
}
.el-button {
.iconfont::before {
margin-right: 5px;
}
}
+133
View File
@@ -0,0 +1,133 @@
.top {
margin-top: 20px;
.top-op {
display: flex;
align-items: center;
.btn {
margin-right: 10px;
}
.search-panel {
margin-left: 10px;
width: 300px;
}
.icon-refresh {
cursor: pointer;
margin-left: 10px;
}
.not-allow {
background: #d2d2d2 !important;
cursor: not-allowed;
}
}
}
.file-list {
.file-item {
display: flex;
align-items: center;
padding: 6px 0px;
.file-name {
margin-left: 8px;
flex: 1;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
span {
cursor: pointer;
&:hover {
color: #06a7ff;
}
}
.transfer-status {
font-size: 13px;
margin-left: 10px;
color: #e6a23c;
}
.transfer-fail {
color: #f75000;
}
}
.edit-panel {
flex: 1;
width: 0;
display: flex;
align-items: center;
margin: 0px 5px;
.iconfont {
margin-left: 10px;
background: #0c95f7;
color: #fff;
padding: 3px 5px;
border-radius: 5px;
cursor: pointer;
}
.not-allow {
cursor: not-allowed;
background: #7cb1d7;
color: #ddd;
text-decoration: none;
}
}
.op {
width: 280px;
margin-left: 15px;
.iconfont {
font-size: 13px;
margin-left: 10px;
color: #06a7ff;
cursor: pointer;
}
.iconfont::before {
margin-right: 3px;
}
}
}
}
.no-data {
height: calc(100vh - 150px);
display: flex;
align-items: center;
justify-content: center;
.no-data-inner {
text-align: center;
.tips {
margin-top: 10px;
}
.op-list {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
.op-item {
cursor: pointer;
width: 100px;
height: 100px;
margin: 0px 10px;
padding: 5px 0px;
background: rgb(241, 241, 241);
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

+539
View File
@@ -0,0 +1,539 @@
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
File diff suppressed because it is too large Load Diff
+167
View File
@@ -0,0 +1,167 @@
@font-face {
font-family: "iconfont"; /* Project id 3946741 */
src: url('iconfont.woff2?t=1680443314592') format('woff2'),
url('iconfont.woff?t=1680443314592') format('woff'),
url('iconfont.ttf?t=1680443314592') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-settings:before {
content: "\e673";
}
.icon-del:before {
content: "\e636";
}
.icon-checkcode:before {
content: "\e626";
}
.icon-account:before {
content: "\e60a";
}
.icon-password:before {
content: "\e65d";
}
.icon-doc:before {
content: "\e882";
}
.icon-import:before {
content: "\e60e";
}
.icon-link:before {
content: "\e600";
}
.icon-cancel:before {
content: "\e772";
}
.icon-revert:before {
content: "\e60f";
}
.icon-close3:before {
content: "\e658";
}
.icon-search:before {
content: "\e65c";
}
.icon-transfer:before {
content: "\e6b4";
}
.icon-refresh:before {
content: "\e6a2";
}
.icon-close2:before {
content: "\e624";
}
.icon-start:before {
content: "\e625";
}
.icon-pause:before {
content: "\e629";
}
.icon-ok:before {
content: "\e613";
}
.icon-clock:before {
content: "\e7d7";
}
.icon-close:before {
content: "\e656";
}
.icon-share:before {
content: "\e65e";
}
.icon-right1:before {
content: "\e65b";
}
.icon-error:before {
content: "\e651";
}
.icon-edit:before {
content: "\e61f";
}
.icon-share1:before {
content: "\e86f";
}
.icon-move:before {
content: "\e67b";
}
.icon-folder-add:before {
content: "\e7d1";
}
.icon-right:before {
content: "\e7eb";
}
.icon-download:before {
content: "\e83a";
}
.icon-upload:before {
content: "\e83b";
}
.icon-all:before {
content: "\e6ff";
}
.icon-cloude:before {
content: "\e66d";
}
.icon-movie2:before {
content: "\e61c";
}
.icon-more:before {
content: "\e631";
}
.icon-music:before {
content: "\e6a1";
}
.icon-video:before {
content: "\e74f";
}
.icon-image:before {
content: "\e68d";
}
.icon-pan:before {
content: "\e6e2";
}
File diff suppressed because one or more lines are too long
+275
View File
@@ -0,0 +1,275 @@
{
"id": "3946741",
"name": "easypan",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "715362",
"name": "settings",
"font_class": "settings",
"unicode": "e673",
"unicode_decimal": 58995
},
{
"icon_id": "1440893",
"name": "回收站",
"font_class": "del",
"unicode": "e636",
"unicode_decimal": 58934
},
{
"icon_id": "553324",
"name": "验证 验证码",
"font_class": "checkcode",
"unicode": "e626",
"unicode_decimal": 58918
},
{
"icon_id": "2127118",
"name": "账户",
"font_class": "account",
"unicode": "e60a",
"unicode_decimal": 58890
},
{
"icon_id": "12779504",
"name": "password",
"font_class": "password",
"unicode": "e65d",
"unicode_decimal": 58973
},
{
"icon_id": "9626991",
"name": "文档",
"font_class": "doc",
"unicode": "e882",
"unicode_decimal": 59522
},
{
"icon_id": "10885271",
"name": "导入",
"font_class": "import",
"unicode": "e60e",
"unicode_decimal": 58894
},
{
"icon_id": "9223527",
"name": "link",
"font_class": "link",
"unicode": "e600",
"unicode_decimal": 58880
},
{
"icon_id": "26867547",
"name": "cancel",
"font_class": "cancel",
"unicode": "e772",
"unicode_decimal": 59250
},
{
"icon_id": "21189968",
"name": "还原",
"font_class": "revert",
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "29943",
"name": "round_close_fill",
"font_class": "close3",
"unicode": "e658",
"unicode_decimal": 58968
},
{
"icon_id": "29947",
"name": "search",
"font_class": "search",
"unicode": "e65c",
"unicode_decimal": 58972
},
{
"icon_id": "15066965",
"name": "transfer",
"font_class": "transfer",
"unicode": "e6b4",
"unicode_decimal": 59060
},
{
"icon_id": "16365914",
"name": "转码",
"font_class": "refresh",
"unicode": "e6a2",
"unicode_decimal": 59042
},
{
"icon_id": "2674473",
"name": "关闭",
"font_class": "close2",
"unicode": "e624",
"unicode_decimal": 58916
},
{
"icon_id": "2674474",
"name": "播放",
"font_class": "start",
"unicode": "e625",
"unicode_decimal": 58917
},
{
"icon_id": "2674483",
"name": "暂停",
"font_class": "pause",
"unicode": "e629",
"unicode_decimal": 58921
},
{
"icon_id": "1421450",
"name": "ok",
"font_class": "ok",
"unicode": "e613",
"unicode_decimal": 58899
},
{
"icon_id": "6151154",
"name": "clock-fill",
"font_class": "clock",
"unicode": "e7d7",
"unicode_decimal": 59351
},
{
"icon_id": "11903682",
"name": "close",
"font_class": "close",
"unicode": "e656",
"unicode_decimal": 58966
},
{
"icon_id": "300021",
"name": "分享给好友icon",
"font_class": "share",
"unicode": "e65e",
"unicode_decimal": 58974
},
{
"icon_id": "760480",
"name": "ok",
"font_class": "right1",
"unicode": "e65b",
"unicode_decimal": 58971
},
{
"icon_id": "14410256",
"name": "icon_error",
"font_class": "error",
"unicode": "e651",
"unicode_decimal": 58961
},
{
"icon_id": "1185453",
"name": "edit",
"font_class": "edit",
"unicode": "e61f",
"unicode_decimal": 58911
},
{
"icon_id": "6353306",
"name": "share",
"font_class": "share1",
"unicode": "e86f",
"unicode_decimal": 59503
},
{
"icon_id": "15838518",
"name": "move",
"font_class": "move",
"unicode": "e67b",
"unicode_decimal": 59003
},
{
"icon_id": "4766848",
"name": "folder-add",
"font_class": "folder-add",
"unicode": "e7d1",
"unicode_decimal": 59345
},
{
"icon_id": "4767011",
"name": "right",
"font_class": "right",
"unicode": "e7eb",
"unicode_decimal": 59371
},
{
"icon_id": "6151351",
"name": "download",
"font_class": "download",
"unicode": "e83a",
"unicode_decimal": 59450
},
{
"icon_id": "6151353",
"name": "upload",
"font_class": "upload",
"unicode": "e83b",
"unicode_decimal": 59451
},
{
"icon_id": "26721527",
"name": "AppstoreFilled",
"font_class": "all",
"unicode": "e6ff",
"unicode_decimal": 59135
},
{
"icon_id": "4562501",
"name": "自治云",
"font_class": "cloude",
"unicode": "e66d",
"unicode_decimal": 58989
},
{
"icon_id": "6050457",
"name": "moviesel",
"font_class": "movie2",
"unicode": "e61c",
"unicode_decimal": 58908
},
{
"icon_id": "741255",
"name": "其他",
"font_class": "more",
"unicode": "e631",
"unicode_decimal": 58929
},
{
"icon_id": "145741",
"name": "音乐_填充",
"font_class": "music",
"unicode": "e6a1",
"unicode_decimal": 59041
},
{
"icon_id": "212328",
"name": "play_fill",
"font_class": "video",
"unicode": "e74f",
"unicode_decimal": 59215
},
{
"icon_id": "14441151",
"name": "image",
"font_class": "image",
"unicode": "e68d",
"unicode_decimal": 59021
},
{
"icon_id": "34532161",
"name": "网盘-copy",
"font_class": "pan",
"unicode": "e6e2",
"unicode_decimal": 59106
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+48
View File
@@ -0,0 +1,48 @@
<template>
<span class="avatar" :style="{ width: width + 'px', height: width + 'px' }">
<img
:src="
avatar && avatar != ''
? avatar
: `${proxy.globalInfo.avatarUrl}${userId}?${timestamp}`
"
v-if="userId"
/>
</span>
</template>
<script setup>
import { getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const props = defineProps({
userId: {
type: String,
},
avatar: {
type: String,
},
timestamp: {
type: Number,
default: 0,
},
width: {
type: Number,
default: 40,
},
});
</script>
<style lang="scss" scoped>
.avatar {
display: flex;
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
object-fit: cover;
}
}
</style>
@@ -0,0 +1,91 @@
<template>
<div class="avatar-upload">
<div class="avatar-show">
<template v-if="localFile">
<img :src="localFile" />
</template>
<template v-else>
<img
:src="`${modelValue.qqAvatar}`"
v-if="modelValue && modelValue.qqAvatar"
/>
<img :src="`/api/getAvatar/${modelValue.userId}`" v-else />
</template>
</div>
<div class="select-btn">
<el-upload
name="file"
:show-file-list="false"
accept=".png,.PNG,.jpg,.JPG,.jpeg,.JPEG,.gif,.GIF,.bmp,.BMP"
:multiple="false"
:http-request="uploadImage"
>
<el-button type="primary">选择</el-button>
</el-upload>
</div>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const timestamp = ref("");
const props = defineProps({
modelValue: {
type: Object,
default: null,
},
});
const localFile = ref(null);
const emit = defineEmits();
const uploadImage = async (file) => {
file = file.file;
let img = new FileReader();
img.readAsDataURL(file);
img.onload = ({ target }) => {
localFile.value = target.result;
};
emit("update:modelValue", file);
};
</script>
<style lang="scss">
.avatar-upload {
display: flex;
justify-content: center;
align-items: end;
.avatar-show {
background: rgb(245, 245, 245);
width: 150px;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
.iconfont {
font-size: 50px;
color: #ddd;
}
img {
width: 100%;
height: 100%;
}
.op {
position: absolute;
color: #0e8aef;
top: 80px;
}
}
.select-btn {
margin-left: 10px;
vertical-align: bottom;
}
}
</style>
+95
View File
@@ -0,0 +1,95 @@
<template>
<div>
<el-dialog
:show-close="showClose"
:draggable="true"
:model-value="show"
:close-on-click-modal="false"
:title="title"
class="cust-dialog"
:top="top + 'px'"
:width="width"
@close="close"
>
<div
class="dialog-body"
:style="{ 'max-height': maxHeight + 'px', padding: padding + 'px' }"
>
<slot></slot>
</div>
<template v-if="(buttons && buttons.length > 0) || showCancel">
<div class="dialog-footer">
<el-button link @click="close" v-if="showCancel"> 取消 </el-button>
<el-button
v-for="btn in buttons"
:type="btn.type || 'primary'"
@click="btn.click"
>
{{ btn.text }}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
const props = defineProps({
title: {
type: String,
},
show: {
type: Boolean,
default: false,
},
showClose: {
type: Boolean,
default: true,
},
showCancel: {
type: Boolean,
default: true,
},
top: {
type: Number,
default: 50,
},
width: {
type: String,
default: "30%",
},
buttons: {
type: Array,
},
padding: {
type: Number,
default: 15,
},
});
const maxHeight = window.innerHeight - props.top - 100;
const emit = defineEmits();
const close = () => {
emit("close");
};
</script>
<style lang="scss">
.cust-dialog {
margin: 30px auto 10px !important;
.el-dialog__body {
padding: 0px;
}
.dialog-body {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
min-height: 80px;
overflow: auto;
}
.dialog-footer {
text-align: right;
padding: 5px 20px;
}
}
</style>
@@ -0,0 +1,148 @@
<template>
<div>
<Dialog
:show="dialogConfig.show"
:title="dialogConfig.title"
:buttons="dialogConfig.buttons"
width="600px"
:showCancel="true"
@close="close"
>
<div class="navigation-panel">
<Navigation
ref="navigationRef"
@navChange="navChange"
:watchPath="false"
></Navigation>
</div>
<div class="folder-list" v-if="folderList.length > 0">
<div
class="folder-item"
v-for="item in folderList"
@click="selectFolder(item)"
>
<icon :fileType="0"></icon>
<span class="file-name">{{ item.fileName }}</span>
</div>
</div>
<div v-else class="tips">
移动到 <span>{{ currentFolder.fileName }}</span> 文件夹
</div>
</Dialog>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, nextTick } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
loadAllFolder: "/file/loadAllFolder",
};
const dialogConfig = ref({
show: false,
title: "移动到",
buttons: [
{
type: "primary",
click: () => {
folderSelect();
},
text: "移动到此",
},
],
});
//父级ID
const filePid = ref("0");
const folderList = ref([]);
const loadAllFolder = async () => {
let result = await proxy.Request({
url: api.loadAllFolder,
params: {
filePid: filePid.value,
currentFileIds: currentFileIds.value,
},
});
if (!result) {
return;
}
folderList.value = result.data;
};
const close = () => {
dialogConfig.value.show = false;
};
//当前目录,传入后 获取目录需要排除该目录
const currentFileIds = ref({});
//展示弹出框对外的方法
const showFolderDialog = (curFileIds) => {
dialogConfig.value.show = true;
currentFileIds.value = curFileIds;
filePid.value = "0";
nextTick(() => {
navigationRef.value.init();
});
};
defineExpose({
showFolderDialog,
close,
});
//选择目录
const navigationRef = ref();
const selectFolder = (data) => {
navigationRef.value.openFolder(data);
};
//当前的目录
const currentFolder = ref({});
//导航改变回调
const navChange = (data) => {
const { curFolder } = data;
currentFolder.value = curFolder;
filePid.value = curFolder.fileId;
loadAllFolder();
};
const emit = defineEmits(["folderSelect"]);
const folderSelect = () => {
emit("folderSelect", filePid.value);
};
</script>
<style lang="scss" scoped>
.navigation-panel {
padding-left: 10px;
background: #f1f1f1;
}
.folder-list {
.folder-item {
cursor: pointer;
display: flex;
align-items: center;
padding: 10px;
.file-name {
display: inline-block;
margin-left: 10px;
}
&:hover {
background: #f8f8f8;
}
}
max-height: calc(100vh - 200px);
min-height: 200px;
}
.tips {
text-align: center;
line-height: 200px;
span {
color: #06a7ff;
}
}
</style>
+74
View File
@@ -0,0 +1,74 @@
<template>
<span :style="{ width: width + 'px', height: width + 'px' }" class="icon">
<img :src="getImage()" :style="{ 'object-fit': fit }" />
</span>
</template>
<script setup>
import { ref, reactive, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const props = defineProps({
fileType: {
type: Number,
},
iconName: {
type: String,
},
cover: {
type: String,
},
width: {
type: Number,
default: 32,
},
fit: {
type: String,
default: "cover",
},
});
const fileTypeMap = {
0: { desc: "目录", icon: "folder" },
1: { desc: "视频", icon: "video" },
2: { desc: "音频", icon: "music" },
3: { desc: "图片", icon: "image" },
4: { desc: "exe", icon: "pdf" },
5: { desc: "doc", icon: "word" },
6: { desc: "excel", icon: "excel" },
7: { desc: "纯文本", icon: "txt" },
8: { desc: "程序", icon: "code" },
9: { desc: "压缩包", icon: "zip" },
10: { desc: "其他文件", icon: "others" },
};
const getImage = () => {
if (props.cover) {
return proxy.globalInfo.imageUrl + props.cover;
}
let icon = "unknow_icon";
if (props.iconName) {
icon = props.iconName;
} else {
console.log(props.fileType);
const iconMap = fileTypeMap[props.fileType];
if (iconMap != undefined) {
icon = iconMap["icon"];
}
}
return new URL(`/src/assets/icon-image/${icon}.png`, import.meta.url).href;
};
</script>
<style lang="scss" scoped>
.icon {
text-align: center;
display: inline-block;
border-radius: 3px;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
}
</style>
+213
View File
@@ -0,0 +1,213 @@
<template>
<div class="top-navigation">
<template v-if="folderList.length > 0">
<span class="back link" @click="backParent">返回上一级</span>
<el-divider direction="vertical" />
</template>
<span v-if="folderList.length == 0" class="all-file">全部文件</span>
<span
class="link"
@click="setCurrentFolder(-1)"
v-if="folderList.length > 0"
>全部文件</span
>
<template v-for="(item, index) in folderList">
<span class="iconfont icon-right"></span>
<span
class="link"
@click="setCurrentFolder(index)"
v-if="index < folderList.length - 1"
>{{ item.fileName }}</span
>
<span v-if="index == folderList.length - 1" class="text">{{
item.fileName
}}</span>
</template>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const props = defineProps({
watchPath: {
type: Boolean, //是否监听路径变化
default: true,
},
shareId: {
type: String,
},
adminShow: {
type: Boolean,
default: false,
},
});
//初始化
const init = () => {
folderList.value = [];
currentFolder.value = { fileId: "0" };
doCallback();
};
//点击目录
const openFolder = (data) => {
const { fileId, fileName } = data;
const folder = {
fileName: fileName,
fileId: fileId,
};
folderList.value.push(folder);
currentFolder.value = folder;
setPath();
};
defineExpose({ openFolder, init });
const api = {
getFolderInfo: "/file/getFolderInfo",
getFolderInfo4Share: "/showShare/getFolderInfo",
getFolderInfo4Admin: "/admin/getFolderInfo",
};
//分类
const category = ref();
//目录
const folderList = ref([]);
//当前目录
const currentFolder = ref({ fileId: "0" });
//返回上一级
const backParent = () => {
let currentIndex = null;
for (let i = 0; i < folderList.value.length; i++) {
if (folderList.value[i].fileId == currentFolder.value.fileId) {
currentIndex = i;
break;
}
}
setCurrentFolder(currentIndex - 1);
};
//点击导航 设置当前目录
const setCurrentFolder = (index) => {
if (index == -1) {
//返回全部
currentFolder.value = { fileId: "0" };
folderList.value = [];
} else {
currentFolder.value = folderList.value[index];
folderList.value.splice(index + 1, folderList.value.length);
}
setPath();
};
//设置URL路径
const setPath = () => {
if (!props.watchPath) {
doCallback();
return;
}
let pathArray = [];
folderList.value.forEach((item) => {
pathArray.push(item.fileId);
});
router.push({
path: route.path,
query:
pathArray.length == 0
? ""
: {
path: pathArray.join("/"),
},
});
};
//获取当前路径的目录
const getNavigationFolder = async (path) => {
let url = api.getFolderInfo;
if (props.shareId) {
url = api.getFolderInfo4Share;
}
if (props.adminShow) {
url = api.getFolderInfo4Admin;
}
let result = await proxy.Request({
url: url,
showLoading: false,
params: {
path: path,
shareId: props.shareId,
},
});
if (!result) {
return;
}
folderList.value = result.data;
};
const emit = defineEmits(["navChange"]);
const doCallback = () => {
emit("navChange", {
categoryId: category.value,
curFolder: currentFolder.value,
});
};
watch(
() => route,
(newVal, oldVal) => {
if (!props.watchPath) {
return;
}
//路由切换到其他路由 首页和管理员查看文件列表页面需要监听
if (
newVal.path.indexOf("/main") === -1 &&
newVal.path.indexOf("/settings/fileList") === -1 &&
newVal.path.indexOf("/share") === -1
) {
return;
}
const path = newVal.query.path;
const categoryId = newVal.params.category;
category.value = categoryId;
if (path == undefined) {
init();
} else {
getNavigationFolder(path);
//设置当前目录
let pathArray = path.split("/");
currentFolder.value = {
fileId: pathArray[pathArray.length - 1],
};
doCallback();
}
},
{ immediate: true, deep: true }
);
</script>
<style lang="scss" scoped>
.top-navigation {
font-size: 13px;
display: flex;
align-items: center;
line-height: 40px;
.all-file {
font-weight: bold;
}
.link {
color: #06a7ff;
cursor: pointer;
}
.icon-right {
color: #06a7ff;
padding: 0px 5px;
font-size: 13px;
}
}
</style>
+30
View File
@@ -0,0 +1,30 @@
<template>
<div class="no-data">
<div class="iconfont icon-empty"></div>
<div class="msg">{{ msg }}</div>
</div>
</template>
<script setup>
const props = defineProps({
msg: {
type: String,
},
});
</script>
<style lang="scss" scoped>
.no-data {
text-align: center;
padding: 10px 0px;
.icon-empty {
font-size: 50px;
color: #bbb;
}
.msg {
margin-top: 10px;
color: #909399;
font-size: 14px;
}
}
</style>
+180
View File
@@ -0,0 +1,180 @@
<template>
<div>
<el-table
ref="dataTable"
:data="dataSource.list || []"
:height="tableHeight"
:stripe="options.stripe"
:border="options.border"
header-row-class-name="table-header-row"
highlight-current-row
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
>
<!--selection选择框-->
<el-table-column
v-if="options.selectType && options.selectType == 'checkbox'"
type="selection"
width="50"
align="center"
></el-table-column>
<!--序号-->
<el-table-column
v-if="options.showIndex"
label="序号"
type="index"
width="60"
align="center"
></el-table-column>
<!--数据列-->
<template v-for="(column, index) in columns">
<template v-if="column.scopedSlots">
<el-table-column
:key="index"
:prop="column.prop"
:label="column.label"
:align="column.align || 'left'"
:width="column.width"
>
<template #default="scope">
<slot
:name="column.scopedSlots"
:index="scope.$index"
:row="scope.row"
>
</slot>
</template>
</el-table-column>
</template>
<template v-else>
<el-table-column
:key="index"
:prop="column.prop"
:label="column.label"
:align="column.align || 'left'"
:width="column.width"
:fixed="column.fixed"
>
</el-table-column>
</template>
</template>
</el-table>
<!-- 分页 -->
<div class="pagination" v-if="showPagination">
<el-pagination
v-if="dataSource.totalCount"
background
:total="dataSource.totalCount"
:page-sizes="[15, 30, 50, 100]"
:page-size="dataSource.pageSize"
:current-page.sync="dataSource.pageNo"
:layout="layout"
@size-change="handlePageSizeChange"
@current-change="handlePageNoChange"
style="text-align: right"
></el-pagination>
</div>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
const emit = defineEmits(["rowSelected", "rowClick"]);
const props = defineProps({
dataSource: Object,
showPagination: {
type: Boolean,
default: true,
},
showPageSize: {
type: Boolean,
default: true,
},
options: {
type: Object,
default: {
extHeight: 0,
showIndex: false,
},
},
columns: Array,
fetch: Function, // 获取数据的函数
initFetch: {
type: Boolean,
default: true,
},
});
const layout = computed(() => {
return `total, ${
props.showPageSize ? "sizes" : ""
}, prev, pager, next, jumper`;
});
//顶部 60 , 内容区域距离顶部 20, 内容上下内间距 15*2 分页区域高度 46
const topHeight = 60 + 20 + 30 + 46;
const tableHeight = ref(
props.options.tableHeight
? props.options.tableHeight
: window.innerHeight - topHeight - props.options.extHeight
);
//初始化
const init = () => {
if (props.initFetch && props.fetch) {
props.fetch();
}
};
init();
const dataTable = ref();
//清除选中
const clearSelection = () => {
dataTable.value.clearSelection();
};
//设置行选中
const setCurrentRow = (rowKey, rowValue) => {
let row = props.dataSource.list.find((item) => {
return item[rowKey] === rowValue;
});
dataTable.value.setCurrentRow(row);
};
//将子组件暴露出去,否则父组件无法调用
defineExpose({ setCurrentRow, clearSelection });
//行点击
const handleRowClick = (row) => {
emit("rowClick", row);
};
//多选
const handleSelectionChange = (row) => {
emit("rowSelected", row);
};
//切换每页大小
const handlePageSizeChange = (size) => {
props.dataSource.pageSize = size;
props.dataSource.pageNo = 1;
props.fetch();
};
// 切换页码
const handlePageNoChange = (pageNo) => {
props.dataSource.pageNo = pageNo;
props.fetch();
};
</script>
<style lang="scss" scoped>
.pagination {
padding-top: 10px;
padding-right: 10px;
}
.el-pagination {
justify-content: right;
}
:deep .el-table__cell {
padding: 4px 0px;
}
</style>
+122
View File
@@ -0,0 +1,122 @@
<template>
<div class="window" v-if="show">
<div class="window-mask" v-if="show" @click="close"></div>
<div class="close" @click="close">
<span class="iconfont icon-close2"> </span>
</div>
<div
class="window-content"
:style="{
top: '0px',
left: windowContentLeft + 'px',
width: windowContentWidth + 'px',
}"
>
<div class="title">
{{ title }}
</div>
<div class="content-body" :style="{ 'align-items': align }">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref } from "vue";
const props = defineProps({
show: {
type: Boolean,
},
width: {
type: Number,
default: 1000,
},
title: {
type: String,
},
align: {
type: String,
default: "top",
},
});
const windowWidth = ref(window.innerWidth);
const windowContentWidth = computed(() => {
return props.width > windowWidth.value ? windowWidth.value : props.width;
});
const windowContentLeft = computed(() => {
let left = windowWidth.value - props.width;
return left < 0 ? 0 : left / 2;
});
const emit = defineEmits(["close"]);
const close = () => {
emit("close");
};
const resizeWindow = () => {
windowWidth.value = window.innerWidth;
};
onMounted(() => {
window.addEventListener("resize", resizeWindow);
});
onUnmounted(() => {
window.removeEventListener("resize", resizeWindow);
});
</script>
<style lang="scss" scoped>
.window {
.window-mask {
top: 0px;
left: 0px;
width: 100%;
height: calc(100vh);
z-index: 200;
opacity: 0.5;
background: #000;
position: fixed;
}
.close {
z-index: 202;
cursor: pointer;
position: absolute;
top: 40px;
right: 30px;
width: 44px;
height: 44px;
border-radius: 22px;
background: #606266;
display: flex;
justify-content: center;
align-items: center;
.iconfont {
font-size: 20px;
color: #fff;
z-index: 100000;
}
}
.window-content {
top: 0px;
z-index: 201;
position: absolute;
background: #fff;
.title {
text-align: center;
line-height: 40px;
border-bottom: 1px solid #ddd;
font-weight: bold;
}
.content-body {
height: calc(100vh - 41px);
display: flex;
overflow: auto;
}
}
}
</style>
@@ -0,0 +1,127 @@
<template>
<PreviewImage
ref="imageViewerRef"
:imageList="[imageUrl]"
v-if="fileInfo.fileCategory == 3"
></PreviewImage>
<Window
:show="windowShow"
@close="closeWindow"
:width="fileInfo.fileCategory == 1 ? 1500 : 900"
:title="fileInfo.fileName"
:align="fileInfo.fileCategory == 1 ? 'center' : 'top'"
v-else
>
<PreviewVideo :url="url" v-if="fileInfo.fileCategory == 1"></PreviewVideo>
<PreviewExcel :url="url" v-if="fileInfo.fileType == 6"></PreviewExcel>
<PreviewDoc :url="url" v-if="fileInfo.fileType == 5"></PreviewDoc>
<PreviewPdf :url="url" v-if="fileInfo.fileType == 4"></PreviewPdf>
<PreviewTxt
:url="url"
v-if="fileInfo.fileType == 7 || fileInfo.fileType == 8"
></PreviewTxt>
<!--特殊预览-->
<PreviewMusic
:url="url"
:fileName="fileInfo.fileName"
v-if="fileInfo.fileCategory == 2"
></PreviewMusic>
<PreviewDownload
:createDownloadUrl="createDownloadUrl"
:downloadUrl="downloadUrl"
:fileInfo="fileInfo"
v-if="fileInfo.fileCategory == 5 && fileInfo.fileType != 8"
></PreviewDownload>
</Window>
</template>
<script setup>
import PreviewDoc from "@/components/preview/PreviewDoc.vue";
import PreviewExcel from "@/components/preview/PreviewExcel.vue";
import PreviewImage from "@/components/preview/PreviewImage.vue";
import PreviewPdf from "@/components/preview/PreviewPdf.vue";
import PreviewVideo from "@/components/preview/PreviewVideo.vue";
import PreviewTxt from "@/components/preview/PreviewTxt.vue";
import PreviewDownload from "@/components/preview/PreviewDownload.vue";
import PreviewMusic from "@/components/preview/PreviewMusic.vue";
import { ref, reactive, getCurrentInstance, nextTick, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const imageUrl = computed(() => {
return (
proxy.globalInfo.imageUrl + fileInfo.value.fileCover.replaceAll("_.", ".")
);
});
const windowShow = ref(false);
const closeWindow = () => {
windowShow.value = false;
};
const FILE_URL_MAP = {
0: {
fileUrl: "/file/getFile",
videoUrl: "/file/ts/getVideoInfo",
createDownloadUrl: "/file/createDownloadUrl",
downloadUrl: "/api/file/download",
},
1: {
fileUrl: "/admin/getFile",
videoUrl: "/admin/ts/getVideoInfo",
createDownloadUrl: "/admin/createDownloadUrl",
downloadUrl: "/api/admin/download",
},
2: {
fileUrl: "/showShare/getFile",
videoUrl: "/showShare/ts/getVideoInfo",
createDownloadUrl: "/showShare/createDownloadUrl",
downloadUrl: "/api/showShare/download",
},
};
const url = ref(null);
const createDownloadUrl = ref(null);
const downloadUrl = ref(null);
const fileInfo = ref({});
const imageViewerRef = ref();
const showPreview = (data, showPart) => {
fileInfo.value = data;
if (data.fileCategory == 3) {
nextTick(() => {
imageViewerRef.value.show(0);
});
} else {
windowShow.value = true;
let _url = FILE_URL_MAP[showPart].fileUrl;
//视频地址单独处理
if (data.fileCategory == 1) {
_url = FILE_URL_MAP[showPart].videoUrl;
}
let _createDownloadUrl = FILE_URL_MAP[showPart].createDownloadUrl;
let _downloadUrl = FILE_URL_MAP[showPart].downloadUrl;
if (showPart == 0) {
_url = _url + "/" + data.fileId;
_createDownloadUrl = _createDownloadUrl + "/" + data.fileId;
} else if (showPart == 1) {
_url = _url + "/" + data.userId + "/" + data.fileId;
_createDownloadUrl =
_createDownloadUrl + "/" + data.userId + "/" + data.fileId;
} else if (showPart == 2) {
_url = _url + "/" + data.shareId + "/" + data.fileId;
_createDownloadUrl =
_createDownloadUrl + "/" + data.shareId + "/" + data.fileId;
}
url.value = _url;
createDownloadUrl.value = _createDownloadUrl;
downloadUrl.value = _downloadUrl;
}
};
defineExpose({ showPreview });
</script>
<style lang="scss">
</style>
@@ -0,0 +1,44 @@
<template>
<div ref="docRef" class="doc-content"></div>
</template>
<script setup>
import * as docx from "docx-preview";
import { ref, reactive, getCurrentInstance, onMounted } from "vue";
const { proxy } = getCurrentInstance();
const props = defineProps({
url: {
type: String,
},
});
const docRef = ref();
const initDoc = async () => {
let result = await proxy.Request({
url: props.url,
responseType: "blob",
});
if (!result) {
return;
}
docx.renderAsync(result, docRef.value);
};
onMounted(() => {
initDoc();
});
</script>
<style lang="scss" scoped>
.doc-content {
margin: 0px auto;
:deep .docx-wrapper {
background: #fff;
padding: 10px 0px;
}
:deep .docx-wrapper > section.docx {
margin-bottom: 0px;
}
}
</style>
@@ -0,0 +1,73 @@
<template>
<div class="others">
<div class="body-content">
<div>
<icon
:iconName="fileInfo.fileType == 9 ? 'zip' : 'others'"
:width="80"
></icon>
</div>
<div class="file-name">{{ fileInfo.fileName }}</div>
<div class="tips">该类型的文件暂不支持预览请下载后查看</div>
<div class="download-btn">
<el-button type="primary" @click="download"
>点击下载 {{ proxy.Utils.sizeToStr(fileInfo.fileSize) }}</el-button
>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const props = defineProps({
createDownloadUrl: {
type: String,
},
downloadUrl: {
type: String,
},
fileInfo: {
type: Object,
},
});
//下载文件
const download = async () => {
let result = await proxy.Request({
url: props.createDownloadUrl,
});
if (!result) {
return;
}
window.location.href = props.downloadUrl + "/" + result.data;
};
</script>
<style lang="scss" scoped>
.others {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
.body-content {
text-align: center;
.file-name {
font-weight: bold;
}
.tips {
color: #999898;
margin-top: 5px;
font-size: 13px;
}
.download-btn {
margin-top: 20px;
}
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<div v-html="excelContent" class="talbe-info"></div>
</template>
<script setup>
import * as XLSX from "xlsx";
import { ref, reactive, getCurrentInstance, onMounted } from "vue";
const { proxy } = getCurrentInstance();
const props = defineProps({
url: {
type: String,
},
});
const excelContent = ref();
const initExcel = async () => {
let result = await proxy.Request({
url: props.url,
responseType: "arraybuffer",
});
if (!result) {
return;
}
let workbook = XLSX.read(new Uint8Array(result), { type: "array" }); // 解析数据
var worksheet = workbook.Sheets[workbook.SheetNames[0]]; // workbook.SheetNames 下存的是该文件每个工作表名字,这里取出第一个工作表
excelContent.value = XLSX.utils.sheet_to_html(worksheet);
};
onMounted(() => {
initExcel();
});
</script>
<style lang="scss" scoped>
.talbe-info {
width: 100%;
padding: 10px;
:deep table {
width: 100%;
border-collapse: collapse;
td {
border: 1px solid #ddd;
border-collapse: collapse;
padding: 5px;
height: 30px;
min-width: 50px;
}
}
}
</style>
@@ -0,0 +1,53 @@
<template>
<div class="image-viewer">
<el-image-viewer
:initial-index="previewImgIndex"
hide-on-click-modal
:url-list="imageList"
@close="closeImgViewer"
v-if="previewImgIndex != null"
>
</el-image-viewer>
</div>
</template>
<script setup>
import { ref } from "vue";
const prosp = defineProps({
imageList: {
type: Array,
},
});
const previewImgIndex = ref(null);
const show = (index) => {
stopScroll();
previewImgIndex.value = index;
};
defineExpose({ show });
const closeImgViewer = () => {
startScroll();
previewImgIndex.value = null;
};
//禁止滚动
const stopScroll = () => {
document.body.style.overflow = "hidden";
};
// 开始滚动
const startScroll = () => {
document.body.style.overflow = "auto";
};
</script>
<style lang="scss" scoped>
.image-viewer {
.el-image-viewer__mask {
opacity: 0.7;
}
}
</style>
@@ -0,0 +1,79 @@
<template>
<div class="music">
<div class="body-content">
<div class="cover">
<img src="@/assets/music_cover.png" />
</div>
<div ref="playerRef" class="music-player"></div>
</div>
</div>
</template>
<script setup>
import APlayer from "APlayer";
import "APlayer/dist/APlayer.min.css";
import {
ref,
reactive,
getCurrentInstance,
computed,
onMounted,
onUnmounted,
} from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const props = defineProps({
url: {
type: String,
},
fileName: {
type: String,
},
});
const playerRef = ref();
const player = ref();
onMounted(() => {
player.value = new APlayer({
container: playerRef.value,
audio: {
url: `/api${props.url}`,
name: `${props.fileName}`,
cover: new URL(`@/assets/music_icon.png`, import.meta.url).href,
artist: "",
},
});
});
onUnmounted(() => {
player.value.destroy();
});
</script>
<style lang="scss" scoped>
.music {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
.body-content {
text-align: center;
width: 80%;
.cover {
margin: 0px auto;
width: 200px;
text-align: center;
img {
width: 100%;
}
}
.music-player {
margin-top: 20px;
}
}
}
</style>
@@ -0,0 +1,62 @@
<template>
<div class="pdf">
<vue-pdf-embed
ref="pdfRef"
:source="state.url"
class="vue-pdf-embed"
width="850"
:page="state.pageNum"
:style="scaleFun"
/>
</div>
</template>
<script setup>
import VuePdfEmbed from "vue-pdf-embed";
import { createLoadingTask } from "vue3-pdfjs";
import { ElLoading } from "element-plus";
import { ref, reactive, getCurrentInstance, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const props = defineProps({
url: {
type: String,
},
});
const scaleFun = computed(() => {
return "transform:scale(${state.scale})";
});
const state = reactive({
url: "", // 预览pdf文件地址
pageNum: 0, // 当前页面
numPages: 0, // 总页数
});
const init = () => {
const url = "/api" + props.url;
state.url = url;
const loading = ElLoading.service({
lock: true,
text: "加载中......",
background: "rgba(0, 0, 0, 0.7)",
});
const loadingTask = createLoadingTask(state.url);
loadingTask.promise.then((pdf) => {
loading.close();
state.numPages = pdf.numPages;
});
};
init();
</script>
<style lang="scss" scoped>
.pdf {
width: 100%;
}
</style>
@@ -0,0 +1,101 @@
<template>
<div class="code">
<div class="top-op">
<div class="encode-select">
<el-select
placeholder="选择编码"
v-model="encode"
@change="changeEncode"
>
<el-option value="utf8" label="utf8编码"></el-option>
<el-option value="gbk" label="gbk编码"></el-option>
</el-select>
<div class="tips">乱码了切换编码试试</div>
</div>
<div class="copy-btn">
<el-button type="primary" @click="copy">复制</el-button>
</div>
</div>
<highlightjs autodetect :code="txtContent" />
</div>
</template>
<script setup>
import useClipboard from "vue-clipboard3";
const { toClipboard } = useClipboard();
import { ref, reactive, getCurrentInstance, onMounted, nextTick } from "vue";
const { proxy } = getCurrentInstance();
const props = defineProps({
url: {
type: String,
},
});
const codeRef = ref();
const txtContent = ref("");
const blobResult = ref();
const encode = ref("utf8");
const readTxt = async () => {
let result = await proxy.Request({
url: props.url,
responseType: "blob",
});
if (!result) {
return;
}
blobResult.value = result;
showTxt();
};
const changeEncode = (e) => {
encode.value = e;
showTxt();
};
const showTxt = () => {
const reader = new FileReader();
reader.onload = () => {
let txt = reader.result;
txtContent.value = txt; //获取的数据data
};
reader.readAsText(blobResult.value, encode.value);
};
onMounted(() => {
readTxt();
});
const copy = async () => {
await toClipboard(txtContent.value);
proxy.Message.success("复制成功");
};
</script>
<style lang="scss" scoped>
.code {
width: 100%;
.top-op {
display: flex;
align-items: center;
justify-content: space-around;
}
.encode-select {
flex: 1;
display: flex;
align-items: center;
margin: 5px 10px;
.tips {
margin-left: 10px;
color: #828282;
}
}
.copy-btn {
margin-right: 10px;
}
pre {
margin: 0px;
}
}
</style>
@@ -0,0 +1,57 @@
<template>
<div ref="player" id="player"></div>
</template>
<script setup>
import DPlayer from "dplayer";
import { nextTick, onMounted, ref, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const props = defineProps({
url: {
type: String,
},
});
const videoInfo = ref({
video: null,
});
const player = ref();
const initPlayer = () => {
const dp = new DPlayer({
element: player.value,
theme: "#b7daff",
screenshot: true,
video: {
// pic: videoInfo.img, // 封面
url: `/api${props.url}`,
type: "customHls",
customType: {
customHls: function (video, player) {
const hls = new Hls();
hls.loadSource(video.src);
hls.attachMedia(video);
},
},
},
});
};
onMounted(() => {
initPlayer();
});
</script>
<style lang="scss" scoped>
#player {
width: 100%;
:deep .dplayer-video-wrap {
text-align: center;
.dplayer-video {
margin: 0px auto;
max-height: calc(100vh - 41px);
}
}
}
</style>
+20
View File
@@ -0,0 +1,20 @@
export default {
"all": {
accept: "*"
},
"video": {
accept: ".mp4,.avi,.rmvb,.mkv,.mov"
},
"music": {
accept: ".mp3,.wav,.wma,.mp2,.flac,.midi,.ra,.ape,.aac,.cda"
},
"image": {
accept: ".jpeg,.jpg,.png,.gif,.bmp,.dds,.psd,.pdt,.webp,.xmp,.svg,.tiff"
},
"doc": {
accept: ".pdf,.doc,.docx,.xls,.xlsx,.txt"
},
"others": {
accept: "*"
},
}
+67
View File
@@ -0,0 +1,67 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from '@/App.vue'
import router from '@/router'
//引入cookies
import VueCookies from 'vue-cookies'
//引入element plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//图标 图标在附件中
import '@/assets/icon/iconfont.css'
import '@/assets/base.scss'
//引入代码高亮
import HljsVuePlugin from '@highlightjs/vue-plugin'
import "highlight.js/styles/atom-one-light.css";
import 'highlight.js/lib/common'
import Request from '@/utils/Request';
import Message from '@/utils/Message'
import Confirm from '@/utils/Confirm'
import Verify from '@/utils/Verify'
import Utils from '@/utils/Utils'
//自定义组件
import Icon from "@/components/Icon.vue"
import Table from '@/components/Table.vue'
import Dialog from '@/components/Dialog.vue'
import NoData from '@/components/NoData.vue'
import Window from '@/components/Window.vue'
import Preview from '@/components/preview/Preview.vue'
import Navigation from '@/components/Navigation.vue'
import FolderSelect from '@/components/FolderSelect.vue'
import Avatar from '@/components/Avatar.vue'
const app = createApp(App)
app.use(ElementPlus);
app.use(createPinia())
app.use(HljsVuePlugin);
app.use(router)
app.component("Icon", Icon);
app.component("Table", Table);
app.component("Dialog", Dialog);
app.component("NoData", NoData);
app.component("Window", Window);
app.component("Preview", Preview);
app.component("Navigation", Navigation);
app.component("FolderSelect", FolderSelect);
app.component("Avatar", Avatar);
//配置全局变量
app.config.globalProperties.Request = Request;
app.config.globalProperties.Message = Message;
app.config.globalProperties.Confirm = Confirm;
app.config.globalProperties.Verify = Verify;
app.config.globalProperties.Utils = Utils;
app.config.globalProperties.VueCookies = VueCookies;
app.config.globalProperties.globalInfo = {
avatarUrl: "/api/getAvatar/",
imageUrl: "/api/file/getImage/"
}
app.mount('#app')
+101
View File
@@ -0,0 +1,101 @@
import { createRouter, createWebHistory } from 'vue-router'
import VueCookies from 'vue-cookies'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: '登录',
component: () => import("@/views/Login.vue")
},
{
path: "/",
component: () => import("@/views/Framework.vue"),
children: [
{
path: '/',
redirect: "/main/all"
},
{
path: '/main/:category',
name: '首页',
meta: {
needLogin: true,
menuCode: "main"
},
component: () => import("@/views/main/Main.vue")
},
{
path: '/myshare',
name: '我的分享',
meta: {
needLogin: true,
menuCode: "share"
},
component: () => import("@/views/share/Share.vue")
},
{
path: '/recycle',
name: '回收站',
meta: {
needLogin: true,
menuCode: "recycle"
},
component: () => import("@/views/recycle/Recycle.vue")
},
{
path: '/settings/sysSetting',
name: '系统设置',
meta: {
needLogin: true,
menuCode: "settings"
},
component: () => import("@/views/admin/SysSettings.vue")
},
{
path: '/settings/userList',
name: '用户管理',
meta: {
needLogin: true,
menuCode: "settings"
},
component: () => import("@/views/admin/UserList.vue")
},
{
path: '/settings/fileList',
name: '用户文件',
meta: {
needLogin: true,
menuCode: "settings"
},
component: () => import("@/views/admin/FileList.vue")
},
]
},
{
path: '/shareCheck/:shareId',
name: '分享校验',
component: () => import("@/views/webshare/ShareCheck.vue")
},
{
path: '/share/:shareId',
name: '分享',
component: () => import("@/views/webshare/Share.vue")
}, {
path: '/qqlogincalback',
name: "qq登录回调",
component: () => import('@/views/QqLoginCallback.vue'),
}
]
})
router.beforeEach((to, from, next) => {
const userInfo = VueCookies.get("userInfo");
if (to.meta.needLogin != null && to.meta.needLogin && userInfo == null) {
router.push("/login");
}
next();
})
export default router
+19
View File
@@ -0,0 +1,19 @@
import { defineStore } from 'pinia'
export const userStore = defineStore('userInfo', {
state: () => {
return {
userInfo: {},
}
},
getters: {
getUserInfo() {
return this.userInfo;
}
},
actions: {
saveUserInfo(userInfo) {
this.userInfo = userInfo
}
}
})
+16
View File
@@ -0,0 +1,16 @@
import { ElMessageBox } from 'element-plus'
const confirm = (message, okfun) => {
ElMessageBox.confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}).then(() => {
okfun();
}).catch(() => { })
};
export default confirm;
+28
View File
@@ -0,0 +1,28 @@
import { ElMessage } from 'element-plus'
const showMessage = (msg, callback, type) => {
ElMessage({
type: type,
message: msg,
duration: 2000,
onClose: () => {
if (callback) {
callback();
}
}
})
}
const message = {
error: (msg, callback) => {
showMessage(msg, callback, "error");
},
success: (msg, callback) => {
showMessage(msg, callback, "success");
},
warning: (msg, callback) => {
showMessage(msg, callback, "warning");
},
}
export default message;
+111
View File
@@ -0,0 +1,111 @@
import axios from 'axios'
import { ElLoading } from 'element-plus'
import router from '@/router'
import Message from '../utils/Message'
const contentTypeForm = 'application/x-www-form-urlencoded;charset=UTF-8'
const contentTypeJson = 'application/json'
//arraybuffer ArrayBuffer对象
//blob Blob对象
//document Documnet对象
//json JavaScript object, parsed from a JSON string returned by the server
//text DOMString
const responseTypeJson = "json"
let loading = null;
const instance = axios.create({
baseURL: '/api',
timeout: 10 * 1000,
});
//请求前拦截器
instance.interceptors.request.use(
(config) => {
if (config.showLoading) {
loading = ElLoading.service({
lock: true,
text: '加载中......',
background: 'rgba(0, 0, 0, 0.7)',
});
}
return config;
},
(error) => {
if (config.showLoading && loading) {
loading.close();
}
Message.error("请求发送失败");
return Promise.reject("请求发送失败");
}
);
//请求后拦截器
instance.interceptors.response.use(
(response) => {
const { showLoading, errorCallback, showError = true, responseType } = response.config;
if (showLoading && loading) {
loading.close()
}
const responseData = response.data;
if (responseType == "arraybuffer" || responseType == "blob") {
return responseData;
}
//正常请求
if (responseData.code == 200) {
return responseData;
} else if (responseData.code == 901) {
//登录超时
router.push("/login?redirectUrl=" + encodeURI(router.currentRoute.value.path));
return Promise.reject({ showError: false, msg: "登录超时" });
} else {
//其他错误
if (errorCallback) {
errorCallback(responseData.info);
}
return Promise.reject({ showError: showError, msg: responseData.info });
}
},
(error) => {
if (error.config.showLoading && loading) {
loading.close();
}
return Promise.reject({ showError: true, msg: "网络异常" })
}
);
const request = (config) => {
const { url, params, dataType, showLoading = true, responseType = responseTypeJson } = config;
let contentType = contentTypeForm;
let formData = new FormData();// 创建form对象
for (let key in params) {
formData.append(key, params[key] == undefined ? "" : params[key]);
}
if (dataType != null && dataType == 'json') {
contentType = contentTypeJson;
}
let headers = {
'Content-Type': contentType,
'X-Requested-With': 'XMLHttpRequest',
}
return instance.post(url, formData, {
onUploadProgress: (event) => {
if (config.uploadProgressCallback) {
config.uploadProgressCallback(event);
}
},
responseType: responseType,
headers: headers,
showLoading: showLoading,
errorCallback: config.errorCallback,
showError: config.showError
}).catch(error => {
console.log(error);
if (error.showError) {
Message.error(error.msg);
}
return null;
});
};
export default request;
+21
View File
@@ -0,0 +1,21 @@
export default {
sizeToStr: (limit) => {
var size = "";
if (limit < 0.1 * 1024) { //小于0.1KB,则转化成B
size = limit.toFixed(2) + "B"
} else if (limit < 0.1 * 1024 * 1024) { //小于0.1MB,则转化成KB
size = (limit / 1024).toFixed(2) + "KB"
} else if (limit < 0.1 * 1024 * 1024 * 1024) { //小于0.1GB,则转化成MB
size = (limit / (1024 * 1024)).toFixed(2) + "MB"
} else { //其他转化成GB
size = (limit / (1024 * 1024 * 1024)).toFixed(2) + "GB"
}
var sizeStr = size + ""; //转成字符串
var index = sizeStr.indexOf("."); //获取小数点处的索引
var dou = sizeStr.substr(index + 1, 2) //获取小数点后两位的值
if (dou == "00") { //判断后两位是否为00,如果是则删除00
return sizeStr.substring(0, index) + sizeStr.substr(index + 3, 2)
}
return size;
},
}
+32
View File
@@ -0,0 +1,32 @@
const regs = {
email: /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/,
number: /^([0]|[1-9][0-9]*)$/,
password: /^(?=.*\d)(?=.*[a-zA-Z])[\da-zA-Z~!@#$%^&*_]{8,}$/,
shareCode: /^[A-Za-z0-9]+$/
}
const verify = (rule, value, reg, callback) => {
if (value) {
if (reg.test(value)) {
callback()
} else {
callback(new Error(rule.message))
}
} else {
callback()
}
}
export default {
email: (rule, value, callback) => {
return verify(rule, value, regs.email, callback)
},
number: (rule, value, callback) => {
return verify(rule, value, regs.number, callback)
},
password: (rule, value, callback) => {
return verify(rule, value, regs.password, callback)
},
shareCode: (rule, value, callback) => {
return verify(rule, value, regs.shareCode, callback)
},
}
+495
View File
@@ -0,0 +1,495 @@
<template>
<div class="framework">
<div class="header">
<div class="logo">
<span class="iconfont icon-pan"></span>
<span class="name">Small云盘</span>
</div>
<div class="right-panel">
<el-popover
:width="800"
trigger="click"
v-model:visible="showUploader"
:offset="20"
transition="none"
:hide-after="0"
:popper-style="{ padding: '0px' }"
>
<template #reference>
<span class="iconfont icon-transfer"></span>
</template>
<template #default>
<Uploader
ref="uploaderRef"
@uploadCallback="uploadCallbackHandler"
></Uploader>
</template>
</el-popover>
<el-dropdown>
<div class="user-info">
<div class="avatar">
<Avatar
:userId="userInfo.userId"
:avatar="userInfo.avatar"
:timestamp="timestamp"
:width="46"
></Avatar>
</div>
<span class="nick-name">{{ userInfo.nickName }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="updateAvatar" class="message-item">
修改头像
</el-dropdown-item>
<el-dropdown-item @click="updatePassword" class="message-item">
修改密码
</el-dropdown-item>
<el-dropdown-item @click="logout" class="message-item">
退出
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div class="body">
<div class="left-sider">
<div class="menu-list">
<div
@click="jump(item)"
:class="[
'menu-item',
item.menuCode == currentMenu.menuCode ? 'active' : '',
]"
v-for="item in menus"
>
<template v-if="item.allShow || (!item.allShow && userInfo.isAdmin)">
<div :class="['iconfont', 'icon-' + item.icon]"></div>
<div class="text">
{{ item.name }}
</div>
</template>
</div>
</div>
<div class="menu-sub-list">
<div
@click="jump(sub)"
:class="['menu-item-sub', currentPath == sub.path ? 'active' : '']"
v-for="sub in currentMenu.children"
>
<span
:class="['iconfont', 'icon-' + sub.icon]"
v-if="sub.icon"
></span>
<span class="text">{{ sub.name }}</span>
</div>
<div class="tips" v-if="currentMenu && currentMenu.tips">
{{ currentMenu.tips }}
</div>
<div class="space-info">
<div>空间使用</div>
<div class="percent">
<el-progress
:percentage="
Math.floor(
(useSpaceInfo.useSpace / useSpaceInfo.totalSpace) * 10000
) / 100
"
color="#409eff"
/>
</div>
<div class="space-use">
<div class="use">
{{ proxy.Utils.sizeToStr(useSpaceInfo.useSpace) }}/
{{ proxy.Utils.sizeToStr(useSpaceInfo.totalSpace) }}
</div>
<div class="iconfont icon-refresh" @click="getUseSpace"></div>
</div>
</div>
</div>
</div>
<div class="body-content">
<router-view v-slot="{ Component }">
<component
@addFile="addFile"
ref="routerViewRef"
:is="Component"
@reload="getUseSpace"
/>
</router-view>
</div>
</div>
<!--修改头像-->
<UpdateAvatar
ref="updateAvatarRef"
@updateAvatar="reloadAvatar"
></UpdateAvatar>
<!--修改密码-->
<UpdatePassword ref="updatePasswordRef"></UpdatePassword>
</div>
</template>
<script setup>
import UpdateAvatar from "./UpdateAvatar.vue";
import UpdatePassword from "./UpdatePassword.vue";
import Uploader from "@/views/main/Uploader.vue";
import {
ref,
reactive,
getCurrentInstance,
watch,
nextTick,
computed,
} from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
getUseSpace: "/getUseSpace",
logout: "/logout",
};
const timestamp = ref(0);
//获取用户信息
const userInfo = ref(proxy.VueCookies.get("userInfo"));
//显示上传窗口
const showUploader = ref(false);
//添加文件
const uploaderRef = ref();
const addFile = (data) => {
const { file, filePid } = data;
showUploader.value = true;
uploaderRef.value.addFile(file, filePid);
};
//上传文件回调
const routerViewRef = ref();
const uploadCallbackHandler = () => {
nextTick(() => {
routerViewRef.value.reload();
getUseSpace();
});
};
const menus = [
{
icon: "cloude",
name: "首页",
menuCode: "main",
path: "/main/all",
allShow: true,
children: [
{
icon: "all",
name: "全部",
category: "all",
path: "/main/all",
},
{
icon: "video",
name: "视频",
category: "video",
path: "/main/video",
},
{
icon: "music",
name: "音频",
category: "music",
path: "/main/music",
},
{
icon: "image",
name: "图片",
category: "image",
path: "/main/image",
},
{
icon: "doc",
name: "文档",
category: "doc",
path: "/main/doc",
},
{
icon: "more",
name: "其他",
category: "others",
path: "/main/others",
},
],
},
{
path: "/myshare",
icon: "share",
name: "分享",
menuCode: "share",
allShow: true,
children: [
{
name: "分享记录",
path: "/myshare",
},
],
},
{
path: "/recycle",
icon: "del",
name: "回收站",
menuCode: "recycle",
tips: "回收站为你保存10天内删除的文件",
allShow: true,
children: [
{
name: "删除的文件",
path: "/recycle",
},
],
},
{
path: "/settings/fileList",
icon: "settings",
name: "设置",
menuCode: "settings",
allShow: false,
children: [
{
name: "用户文件",
path: "/settings/fileList",
},
{
name: "用户管理",
path: "/settings/userList",
},
{
path: "/settings/sysSetting",
name: "系统设置",
},
],
},
];
const jump = (data) => {
if (!data.path || data.menuCode == currentMenu.value.menuCode) {
return;
}
router.push(data.path);
};
const currentMenu = ref({});
const currentPath = ref();
const setMenu = (menuCode, path) => {
const menu = menus.find((item) => {
return item.menuCode === menuCode;
});
currentMenu.value = menu;
currentPath.value = path;
};
watch(
() => route,
(newVal, oldVal) => {
if (newVal.meta.menuCode) {
setMenu(newVal.meta.menuCode, newVal.path);
}
},
{ immediate: true, deep: true }
);
//使用空间
const useSpaceInfo = ref({ useSpace: 0, totalSpace: 1 });
const getUseSpace = async () => {
let result = await proxy.Request({
url: api.getUseSpace,
showLoading: false,
});
if (!result) {
return;
}
useSpaceInfo.value = result.data;
};
getUseSpace();
//修改头像
const updateAvatarRef = ref();
const updateAvatar = () => {
updateAvatarRef.value.show(userInfo.value);
};
const reloadAvatar = () => {
userInfo.value = proxy.VueCookies.get("userInfo");
timestamp.value = new Date().getTime();
};
//修改密码
const updatePasswordRef = ref();
const updatePassword = () => {
updatePasswordRef.value.show();
};
//退出登录
const logout = () => {
proxy.Confirm(`你确定要删除退出吗`, async () => {
let result = await proxy.Request({
url: api.logout,
});
if (!result) {
return;
}
proxy.VueCookies.remove;
router.push("/login");
});
};
</script>
<style lang="scss" scoped>
.header {
box-shadow: 0 3px 10px 0 rgb(0 0 0 / 6%);
height: 56px;
padding-left: 24px;
padding-right: 24px;
position: relative;
z-index: 200;
display: flex;
align-items: center;
justify-content: space-between;
.logo {
display: flex;
align-items: center;
.icon-pan {
font-size: 40px;
color: #1296db;
}
.name {
font-weight: bold;
margin-left: 5px;
font-size: 25px;
color: #05a1f5;
}
}
.right-panel {
display: flex;
align-items: center;
.icon-transfer {
cursor: pointer;
}
.user-info {
margin-right: 10px;
display: flex;
align-items: center;
cursor: pointer;
.avatar {
margin: 0px 5px 0px 15px;
}
.nick-name {
color: #05a1f5;
}
}
}
}
.body {
display: flex;
.left-sider {
border-right: 1px solid #f1f2f4;
display: flex;
.menu-list {
height: calc(100vh - 56px);
width: 80px;
box-shadow: 0 3px 10px 0 rgb(0 0 0 / 6%);
border-right: 1px solid #f1f2f4;
.menu-item {
text-align: center;
font-size: 14px;
font-weight: bold;
padding: 20px 0px;
cursor: pointer;
&:hover {
background: #f3f3f3;
}
.iconfont {
font-weight: normal;
font-size: 28px;
}
}
.active {
.iconfont {
color: #06a7ff;
}
.text {
color: #06a7ff;
}
}
}
.menu-sub-list {
width: 200px;
padding: 20px 10px 0px;
position: relative;
.menu-item-sub {
text-align: center;
line-height: 40px;
border-radius: 5px;
cursor: pointer;
&:hover {
background: #f3f3f3;
}
.iconfont {
font-size: 14px;
margin-right: 20px;
}
.text {
font-size: 13px;
}
}
.active {
background: #eef9fe;
.iconfont {
color: #05a1f5;
}
.text {
color: #05a1f5;
}
}
.tips {
margin-top: 10px;
color: #888888;
font-size: 13px;
}
.space-info {
position: absolute;
bottom: 10px;
width: 100%;
padding: 0px 5px;
.percent {
padding-right: 10px;
}
.space-use {
margin-top: 5px;
color: #7e7e7e;
display: flex;
justify-content: space-around;
.use {
flex: 1;
}
.iconfont {
cursor: pointer;
margin-right: 20px;
color: #05a1f5;
}
}
}
}
}
.body-content {
flex: 1;
width: 0;
padding-left: 20px;
}
}
</style>
+541
View File
@@ -0,0 +1,541 @@
<template>
<div class="login-body">
<div class="bg"></div>
<div class="login-panel">
<el-form
class="login-register"
:model="formData"
:rules="rules"
ref="formDataRef"
>
<div class="login-title">Small云盘</div>
<!--input输入-->
<el-form-item prop="email">
<el-input
size="large"
clearable
placeholder="请输入邮箱"
v-model="formData.email"
maxLength="150"
>
<template #prefix>
<span class="iconfont icon-account"></span>
</template>
</el-input>
</el-form-item>
<!--登录密码-->
<el-form-item prop="password" v-if="opType == 1">
<el-input
type="password"
size="large"
placeholder="请输入密码"
v-model="formData.password"
show-password
>
<template #prefix>
<span class="iconfont icon-password"></span>
</template>
</el-input>
</el-form-item>
<!--注册-->
<div v-if="opType == 0 || opType == 2">
<el-form-item prop="emailCode">
<div class="send-emali-panel">
<el-input
size="large"
placeholder="请输入邮箱验证码"
v-model="formData.emailCode"
>
<template #prefix>
<span class="iconfont icon-checkcode"></span>
</template>
</el-input>
<el-button
class="send-mail-btn"
type="primary"
size="large"
@click="getEmailCode"
>获取验证码</el-button
>
</div>
<el-popover placement="left" :width="500" trigger="click">
<div>
<p>1在垃圾箱中查找邮箱验证码</p>
<p>2在邮箱中头像->设置->反垃圾->白名单->设置邮件地址白名单</p>
<p>
3将邮箱laoluo@wuhancoder.com添加到白名单不知道怎么设置
</p>
</div>
<template #reference>
<span class="a-link" :style="{ 'font-size': '14px' }"
>未收到邮箱验证码</span
>
</template>
</el-popover>
</el-form-item>
<el-form-item prop="nickName" v-if="opType == 0">
<el-input
size="large"
clearable
placeholder="请输入昵称"
v-model="formData.nickName"
maxLength="20"
>
<template #prefix>
<span class="iconfont icon-account"></span>
</template>
</el-input>
</el-form-item>
<el-form-item prop="registerPassword">
<el-input
type="password"
size="large"
placeholder="请输入密码"
v-model="formData.registerPassword"
show-password
>
<template #prefix>
<span class="iconfont icon-password"></span>
</template>
</el-input>
</el-form-item>
<el-form-item prop="reRegisterPassword">
<el-input
type="password"
size="large"
placeholder="请再次输入密码"
v-model="formData.reRegisterPassword"
show-password
>
<template #prefix>
<span class="iconfont icon-password"></span>
</template>
</el-input>
</el-form-item>
</div>
<el-form-item prop="checkCode">
<div class="check-code-panel">
<el-input
size="large"
placeholder="请输入验证码"
v-model="formData.checkCode"
@keyup.enter="doSubmit"
>
<template #prefix>
<span class="iconfont icon-checkcode"></span>
</template>
</el-input>
<img
:src="checkCodeUrl"
class="check-code"
@click="changeCheckCode(0)"
/>
</div>
</el-form-item>
<el-form-item v-if="opType == 1">
<div class="rememberme-panel">
<el-checkbox v-model="formData.rememberMe">记住我</el-checkbox>
</div>
<div class="no-account">
<a href="javascript:void(0)" class="a-link" @click="showPanel(2)"
>忘记密码</a
>
<a href="javascript:void(0)" class="a-link" @click="showPanel(0)"
>没有账号</a
>
</div>
</el-form-item>
<el-form-item v-if="opType == 0">
<a href="javascript:void(0)" class="a-link" @click="showPanel(1)"
>已有账号?</a
>
</el-form-item>
<el-form-item v-if="opType == 2">
<a href="javascript:void(0)" class="a-link" @click="showPanel(1)"
>去登录?</a
>
</el-form-item>
<el-form-item>
<el-button
type="primary"
class="op-btn"
@click="doSubmit"
size="large"
>
<span v-if="opType == 0">注册</span>
<span v-if="opType == 1">登录</span>
<span v-if="opType == 2">重置密码</span>
</el-button>
</el-form-item>
<!-- <div class="login-btn-qq" v-if="opType == 1">
快捷登录 <img src="@/assets/qq.png" @click="qqLogin" />
</div> -->
</el-form>
</div>
<!--发送邮箱验证码-->
<Dialog
:show="dialogConfig4SendMailCode.show"
:title="dialogConfig4SendMailCode.title"
:buttons="dialogConfig4SendMailCode.buttons"
width="500px"
:showCancel="false"
@close="dialogConfig4SendMailCode.show = false"
>
<el-form
:model="formData4SendMailCode"
:rules="rules"
ref="formData4SendMailCodeRef"
label-width="80px"
>
<el-form-item label="邮箱">
{{ formData.email }}
</el-form-item>
<el-form-item label="验证码" prop="checkCode">
<div class="check-code-panel">
<el-input
size="large"
placeholder="请输入验证码"
v-model="formData4SendMailCode.checkCode"
>
<template #prefix>
<span class="iconfont icon-checkcode"></span>
</template>
</el-input>
<img
:src="checkCodeUrl4SendMailCode"
class="check-code"
@click="changeCheckCode(1)"
/>
</div>
</el-form-item>
</el-form>
</Dialog>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, nextTick, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import md5 from "js-md5";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
checkCode: "/api/checkCode",
sendMailCode: "/sendEmailCode",
register: "/register",
login: "/login",
resetPwd: "/resetPwd",
qqlogin: "/qqlogin",
};
// 0:注册 1:登录 2:重置密码
const opType = ref();
const showPanel = (type) => {
opType.value = type;
resetForm();
};
onMounted(() => {
showPanel(1);
});
//验证码
const checkCodeUrl = ref(api.checkCode);
const checkCodeUrl4SendMailCode = ref(api.checkCode);
const changeCheckCode = (type) => {
if (type == 0) {
checkCodeUrl.value =
api.checkCode + "?type=" + type + "&time=" + new Date().getTime();
} else {
checkCodeUrl4SendMailCode.value =
api.checkCode + "?type=" + type + "&time=" + new Date().getTime();
}
};
//发送邮箱验证码弹窗
const formData4SendMailCode = ref({});
const formData4SendMailCodeRef = ref();
const dialogConfig4SendMailCode = reactive({
show: false,
title: "发送邮箱验证码",
buttons: [
{
type: "primary",
text: "发送验证码",
click: () => {
sendEmailCode();
},
},
],
});
//获取邮箱验证码
const getEmailCode = () => {
formDataRef.value.validateField("email", (valid) => {
if (!valid) {
return;
}
dialogConfig4SendMailCode.show = true;
nextTick(() => {
changeCheckCode(1);
formData4SendMailCodeRef.value.resetFields();
formData4SendMailCode.value = {
email: formData.value.email,
};
});
});
};
//发送邮件
const sendEmailCode = () => {
formData4SendMailCodeRef.value.validate(async (valid) => {
if (!valid) {
return;
}
const params = Object.assign({}, formData4SendMailCode.value);
params.type = opType.value == 0 ? 0 : 1;
let result = await proxy.Request({
url: api.sendMailCode,
params: params,
errorCallback: () => {
changeCheckCode(1);
},
});
if (!result) {
return;
}
proxy.Message.success("验证码发送成功,请登录邮箱查看");
dialogConfig4SendMailCode.show = false;
});
};
//登录,注册 弹出配置
const dialogConfig = reactive({
show: false,
title: "标题",
});
const checkRePassword = (rule, value, callback) => {
if (value !== formData.value.registerPassword) {
callback(new Error(rule.message));
} else {
callback();
}
};
const formData = ref({});
const formDataRef = ref();
const rules = {
email: [
{ required: true, message: "请输入邮箱" },
{ validator: proxy.Verify.email, message: "请输入正确的邮箱" },
],
password: [{ required: true, message: "请输入密码" }],
emailCode: [{ required: true, message: "请输入邮箱验证码" }],
nickName: [{ required: true, message: "请输入昵称" }],
registerPassword: [
{ required: true, message: "请输入密码" },
{
validator: proxy.Verify.password,
message: "密码只能是数字,字母,特殊字符 8-18位",
},
],
reRegisterPassword: [
{ required: true, message: "请再次输入密码" },
{
validator: checkRePassword,
message: "两次输入的密码不一致",
},
],
checkCode: [{ required: true, message: "请输入图片验证码" }],
};
//重置表单
const resetForm = () => {
dialogConfig.show = true;
if (opType.value == 0) {
dialogConfig.title = "注册";
} else if (opType.value == 1) {
dialogConfig.title = "登录";
} else if (opType.value == 2) {
dialogConfig.title = "重置密码";
}
nextTick(() => {
changeCheckCode(0);
formDataRef.value.resetFields();
formData.value = {};
//登录
if (opType.value == 1) {
const cookieLoginInfo = proxy.VueCookies.get("loginInfo");
if (cookieLoginInfo) {
formData.value = cookieLoginInfo;
}
}
});
};
// 登录、注册、重置密码 提交表单
const doSubmit = () => {
formDataRef.value.validate(async (valid) => {
if (!valid) {
return;
}
let params = {};
Object.assign(params, formData.value);
//注册
if (opType.value == 0 || opType.value == 2) {
params.password = params.registerPassword;
delete params.registerPassword;
delete params.reRegisterPassword;
}
//登录
if (opType.value == 1) {
let cookieLoginInfo = proxy.VueCookies.get("loginInfo");
let cookiePassword =
cookieLoginInfo == null ? null : cookieLoginInfo.password;
if (params.password !== cookiePassword) {
params.password = md5(params.password);
}
}
let url = null;
if (opType.value == 0) {
url = api.register;
} else if (opType.value == 1) {
url = api.login;
} else if (opType.value == 2) {
url = api.resetPwd;
}
let result = await proxy.Request({
url: url,
params: params,
errorCallback: () => {
changeCheckCode(0);
},
});
if (!result) {
return;
}
//注册返回
if (opType.value == 0) {
proxy.Message.success("注册成功,请登录");
showPanel(1);
} else if (opType.value == 1) {
//登录
if (params.rememberMe) {
const loginInfo = {
email: params.email,
password: params.password,
rememberMe: params.rememberMe,
};
proxy.VueCookies.set("loginInfo", loginInfo, "7d");
} else {
proxy.VueCookies.remove("loginInfo");
}
dialogConfig.show = false;
proxy.Message.success("登录成功");
//存储cookie
proxy.VueCookies.set("userInfo", result.data, 0);
//重定向到原始页面
const redirectUrl = route.query.redirectUrl || "/";
router.push(redirectUrl);
} else if (opType.value == 2) {
//重置密码
proxy.Message.success("重置密码成功,请登录");
showPanel(1);
}
});
};
const closeDialog = () => {
dialogConfig.show = false;
};
// //QQ登录
// const qqLogin = async () => {
// let result = await proxy.Request({
// url: api.qqlogin,
// params: {
// callbackUrl: route.query.redirectUrl || "",
// },
// });
// if (!result) {
// return;
// }
// proxy.VueCookies.remove("userInfo");
// document.location.href = result.data;
// };
</script>
<style lang="scss" scoped>
.login-body {
height: calc(100vh);
background-size: cover;
background: url("../assets/login_bg.jpg");
display: flex;
.bg {
flex: 1;
background-size: cover;
background-position: center;
background-size: 800px;
background-repeat: no-repeat;
background-image: url("../assets/login_img.png");
}
.login-panel {
width: 430px;
margin-right: 15%;
margin-top: calc((100vh - 500px) / 2);
.login-register {
padding: 25px;
background: #fff;
border-radius: 5px;
.login-title {
text-align: center;
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
}
.send-emali-panel {
display: flex;
width: 100%;
justify-content: space-between;
.send-mail-btn {
margin-left: 5px;
}
}
.rememberme-panel {
width: 100%;
}
.no-account {
width: 100%;
display: flex;
justify-content: space-between;
}
.op-btn {
width: 100%;
}
}
}
.check-code-panel {
width: 100%;
display: flex;
.check-code {
margin-left: 5px;
cursor: pointer;
}
}
.login-btn-qq {
margin-top: 20px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
img {
cursor: pointer;
margin-left: 10px;
width: 20px;
}
}
}
</style>
@@ -0,0 +1,38 @@
<template>
<div>登录中请勿刷新页面</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, nextTick } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
logincallback: "/qqlogin/callback",
};
const login = async () => {
let result = await proxy.Request({
url: api.logincallback,
params: router.currentRoute.value.query,
errorCallback: () => {
router.push("/");
},
});
if (!result) {
return;
}
let redirectUrl = result.data.callbackUrl || "/";
if (redirectUrl == "/login") {
redirectUrl = "/";
}
proxy.VueCookies.set("userInfo", result.data.userInfo, 0);
console.log("路径",redirectUrl);
router.push(redirectUrl);
};
login();
</script>
<style lang="scss" scoped>
</style>
+93
View File
@@ -0,0 +1,93 @@
<template>
<div>
<Dialog
:show="dialogConfig.show"
:title="dialogConfig.title"
:buttons="dialogConfig.buttons"
width="500px"
:showCancel="true"
@close="dialogConfig.show = false"
>
<el-form
:model="formData"
ref="formDataRef"
label-width="80px"
@submit.prevent
>
<!--input输入-->
<el-form-item label="昵称" prop="">
{{ formData.nickName }}
</el-form-item>
<!--textarea输入-->
<el-form-item label="头像" prop="">
<AvatarUpload v-model="formData.avatar"></AvatarUpload>
</el-form-item>
</el-form>
</Dialog>
</div>
</template>
<script setup>
import AvatarUpload from "@/components/AvatarUpload.vue";
import { ref, reactive, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
updateUserAvatar: "updateUserAvatar",
};
const formData = ref({});
const formDataRef = ref();
const show = (data) => {
formData.value = Object.assign({}, data);
formData.value.avatar = { userId: data.userId, qqAvatar: data.avatar };
dialogConfig.value.show = true;
};
defineExpose({ show });
const dialogConfig = ref({
show: false,
title: "修改头像",
buttons: [
{
type: "primary",
text: "确定",
click: (e) => {
submitForm();
},
},
],
});
const emit = defineEmits(["updateAvatar"]);
const submitForm = async () => {
if (!(formData.value.avatar instanceof File)) {
dialogConfig.value.show = false;
return;
}
let result = await proxy.Request({
url: api.updateUserAvatar,
params: {
avatar: formData.value.avatar,
},
});
if (!result) {
return;
}
dialogConfig.value.show = false;
const cookeUserInfo = proxy.VueCookies.get("userInfo");
delete cookeUserInfo.avatar;
proxy.VueCookies.set("userInfo", cookeUserInfo, 0);
//更新cookie信息
emit("updateAvatar");
};
</script>
<style lang="scss">
</style>
+136
View File
@@ -0,0 +1,136 @@
<template>
<div>
<Dialog
:show="dialogConfig.show"
:title="dialogConfig.title"
:buttons="dialogConfig.buttons"
width="500px"
:showCancel="true"
@close="dialogConfig.show = false"
>
<el-form
:model="formData"
:rules="rules"
ref="formDataRef"
label-width="80px"
@submit.prevent
>
<!--input输入-->
<el-form-item label="新密码" prop="password">
<el-input
type="password"
size="large"
placeholder="请输入密码"
v-model="formData.password"
show-password
>
<template #prefix>
<span class="iconfont icon-password"></span>
</template>
</el-input>
</el-form-item>
<!--input输入-->
<el-form-item label="确认密码" prop="rePassword">
<el-input
type="password"
size="large"
placeholder="请再次输入密码"
v-model="formData.rePassword"
show-password
>
<template #prefix>
<span class="iconfont icon-password"></span>
</template>
</el-input>
</el-form-item>
</el-form>
</Dialog>
</div>
</template>
<script setup>
import AvatarUpload from "@/components/AvatarUpload.vue";
import { ref, reactive, getCurrentInstance, nextTick } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
updatePassword: "updatePassword",
};
const formData = ref({});
const formDataRef = ref();
const checkRePassword = (rule, value, callback) => {
if (value !== formData.value.rePassword) {
callback(new Error(rule.message));
} else {
callback();
}
};
const rules = {
password: [
{ required: true, message: "请输入密码" },
{
validator: proxy.Verify.password,
message: "密码只能是数字,字母,特殊字符 8-18位",
},
],
rePassword: [
{ required: true, message: "请再次输入密码" },
{
validator: checkRePassword,
message: "两次输入的密码不一致",
},
],
};
const show = () => {
dialogConfig.value.show = true;
nextTick(() => {
formDataRef.value.resetFields();
formData.value = {};
});
};
defineExpose({ show });
const dialogConfig = ref({
show: false,
title: "修改密码",
buttons: [
{
type: "primary",
text: "确定",
click: (e) => {
submitForm();
},
},
],
});
const submitForm = async () => {
formDataRef.value.validate(async (valid) => {
if (!valid) {
return;
}
let result = await proxy.Request({
url: api.updatePassword,
params: {
password: formData.value.password,
},
});
if (!result) {
return;
}
dialogConfig.value.show = false;
proxy.message.success("密码修改成功");
});
};
</script>
<style lang="scss">
</style>
+313
View File
@@ -0,0 +1,313 @@
<template>
<div>
<div class="top">
<div class="top-op">
<div class="search-panel">
<el-input
clearable
placeholder="输入文件名搜索"
v-model="fileNameFuzzy"
@keyup.enter="search"
>
<template #suffix>
<i class="iconfont icon-search" @click="search"></i>
</template>
</el-input>
</div>
<div class="iconfont icon-refresh" @click="loadDataList"></div>
<el-button
:style="{ 'margin-left': '10px' }"
type="danger"
:disabled="selectFileIdList.length == 0"
@click="delFileBatch"
>
<span class="iconfont icon-del"></span>
批量删除
</el-button>
</div>
<!--导航-->
<Navigation
ref="navigationRef"
@navChange="navChange"
:adminShow="true"
></Navigation>
</div>
<div class="file-list">
<Table
:columns="columns"
:showPagination="true"
:dataSource="tableData"
:fetch="loadDataList"
:initFetch="false"
:options="tableOptions"
@rowSelected="rowSelected"
>
<template #fileName="{ index, row }">
<div
class="file-item"
@mouseenter="showOp(row)"
@mouseleave="cancelShowOp(row)"
>
<template
v-if="(row.fileType == 3 || row.fileType == 1) && row.status == 2"
>
<icon :cover="row.fileCover" :width="32"></icon>
</template>
<template v-else>
<icon v-if="row.folderType == 0" :fileType="row.fileType"></icon>
<icon v-if="row.folderType == 1" :fileType="0"></icon>
</template>
<span class="file-name" v-if="!row.showEdit" :title="row.fileName">
<span @click="preview(row)">{{ row.fileName }}</span>
<span v-if="row.status == 0" class="transfer-status">转码中</span>
<span v-if="row.status == 1" class="transfer-status transfer-fail"
>转码失败</span
>
</span>
<div class="edit-panel" v-if="row.showEdit">
<el-input
v-model.trim="row.fileNameReal"
:maxLength="190"
@keyup.enter="saveNameEdit(index)"
>
<template #suffix>{{ row.fileSuffix }}</template>
</el-input>
<span
:class="[
'iconfont icon-right1',
row.fileNameReal ? '' : 'not-allow',
]"
@click="saveNameEdit(index)"
></span>
<span
class="iconfont icon-error"
@click="cancelNameEdit(index)"
></span>
</div>
<span class="op">
<template v-if="row.showOp && row.fileId">
<span
class="iconfont icon-download"
@click="download(row)"
v-if="row.folderType == 0"
>下载</span
>
<span class="iconfont icon-del" @click="delFile(row)"
>删除</span
>
</template>
</span>
</div>
</template>
<template #fileSize="{ index, row }">
<span v-if="row.fileSize">
{{ proxy.Utils.sizeToStr(row.fileSize) }}</span
>
</template>
</Table>
</div>
<!--预览-->
<Preview ref="previewRef"> </Preview>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, nextTick, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const emit = defineEmits(["addFile"]);
//添加文件
const addFile = async (fileData) => {
emit("addFile", { file: fileData.file, filePid: currentFolder.value.fileId });
};
//添加文件回调
const reload = () => {
showLoading.value = false;
loadDataList();
};
defineExpose({
reload,
});
const api = {
loadDataList: "/admin/loadFileList",
delFile: "/admin/delFile",
createDownloadUrl: "/admin/createDownloadUrl",
download: "/api/admin/download",
};
//列表
const columns = [
{
label: "文件名",
prop: "fileName",
scopedSlots: "fileName",
},
{
label: "发布人",
prop: "nickName",
width: 250,
},
{
label: "修改时间",
prop: "lastUpdateTime",
width: 200,
},
{
label: "大小",
prop: "fileSize",
scopedSlots: "fileSize",
width: 200,
},
];
//搜索
const search = () => {
showLoading.value = true;
loadDataList();
};
//列表
const tableData = ref({});
const tableOptions = {
extHeight: 50,
selectType: "checkbox",
};
//多选 批量选择
const selectFileIdList = ref([]);
const rowSelected = (rows) => {
selectFileIdList.value = [];
rows.forEach((item) => {
selectFileIdList.value.push(item.userId + "_" + item.fileId);
});
};
const fileNameFuzzy = ref();
const showLoading = ref(true);
const loadDataList = async () => {
let params = {
pageNo: tableData.value.pageNo,
pageSize: tableData.value.pageSize,
fileNameFuzzy: fileNameFuzzy.value,
filePid: currentFolder.value.fileId,
};
let result = await proxy.Request({
url: api.loadDataList,
showLoading: showLoading,
params,
});
if (!result) {
return;
}
tableData.value = result.data;
};
//展示操作按钮
const showOp = (row) => {
tableData.value.list.forEach((element) => {
element.showOp = false;
});
row.showOp = true;
};
const cancelShowOp = (row) => {
row.showOp = false;
};
const previewRef = ref();
const navigationRef = ref();
const preview = (data) => {
if (data.folderType == 1) {
navigationRef.value.openFolder(data);
return;
}
if (data.status != 2) {
proxy.Message.warning("文件正在转码中,无法预览");
return;
}
previewRef.value.showPreview(data, 1);
};
//目录
const currentFolder = ref({ fileId: 0 });
const navChange = (data) => {
const { curFolder } = data;
currentFolder.value = curFolder;
showLoading.value = true;
loadDataList();
};
//删除文件
const delFile = (row) => {
proxy.Confirm(
`你确定要删除【${row.fileName}】吗?删除的文件可在10天内通过回收站还原`,
async () => {
let result = await proxy.Request({
url: api.delFile,
params: {
fileIdAndUserIds: row.userId + "_" + row.fileId,
},
});
if (!result) {
return;
}
loadDataList();
}
);
};
//批量删除
const delFileBatch = () => {
if (selectFileIdList.value.length == 0) {
return;
}
proxy.Confirm(
`你确定要删除这些文件吗?删除的文件可在10天内通过回收站还原`,
async () => {
let result = await proxy.Request({
url: api.delFile,
params: {
fileIdAndUserIds: selectFileIdList.value.join(","),
},
});
if (!result) {
return;
}
loadDataList();
}
);
};
//下载文件
const download = async (row) => {
let result = await proxy.Request({
url: api.createDownloadUrl + "/" + row.userId + "/" + row.fileId,
});
if (!result) {
return;
}
window.location.href = api.download + "/" + result.data;
};
//分享
const shareRef = ref();
const share = (row) => {
shareRef.value.show(row);
};
</script>
<style lang="scss" scoped>
@import "@/assets/file.list.scss";
.search-panel {
margin-left: 0px !important;
}
.file-list {
margin-top: 10px;
.file-item {
.op {
width: 120px;
}
}
}
</style>
@@ -0,0 +1,107 @@
<template>
<div class="sys-setting-panel">
<el-form
:model="formData"
:rules="rules"
ref="formDataRef"
label-width="150px"
@submit.prevent
>
<!--input输入-->
<el-form-item label="注册邮件标题" prop="registerEmailTitle">
<el-input
clearable
placeholder="请输入注册邮件验证码邮件标题"
v-model="formData.registerEmailTitle"
></el-input>
</el-form-item>
<!--textarea输入-->
<el-form-item label="注册邮件标题" prop="registerEmailContent">
<el-input
clearable
placeholder="请输入注册邮件验证码邮件内容%s占位符为验证码内容"
v-model="formData.registerEmailContent"
></el-input>
</el-form-item>
<el-form-item label="初始空间大小" prop="userInitUseSpace">
<el-input
clearable
placeholder="初始空间大小"
v-model="formData.userInitUseSpace"
>
<template #suffix>MB</template>
</el-input>
</el-form-item>
<!-- 单选 -->
<el-form-item label="" prop="">
<el-button type="primary" @click="saveSettings">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
getSysSettings: "/admin/getSysSettings",
saveSettings: "/admin/saveSysSettings",
};
const formData = ref({});
const formDataRef = ref();
const rules = {
registerEmailTitle: [
{ required: true, message: "请输入注册邮件验证码邮件标题" },
],
registerEmailContent: [
{ required: true, message: "请输入注册邮件验证码邮件内容" },
],
userInitUseSpace: [
{ required: true, message: "请输入初始化空间大小" },
{
validator: proxy.Verify.number,
message: "空间大小只能是数字",
},
],
};
const getSysSettings = async () => {
let result = await proxy.Request({
url: api.getSysSettings,
});
if (!result) {
return;
}
formData.value = result.data;
};
getSysSettings();
const saveSettings = async () => {
formDataRef.value.validate(async (valid) => {
if (!valid) {
return;
}
let params = Object.assign({}, formData.value);
let result = await proxy.Request({
url: api.saveSettings,
params: params,
});
if (!result) {
return;
}
proxy.Message.success("保存成功");
});
};
</script>
<style lang="scss" scoped>
.sys-setting-panel {
margin-top: 20px;
width: 600px;
}
</style>
+263
View File
@@ -0,0 +1,263 @@
<template>
<div>
<div class="top-panel">
<el-form :model="searchFormData" label-width="80px" @submit.prevent>
<el-row>
<el-col :span="4">
<!--input输入-->
<el-form-item label="用户昵称">
<el-input
clearable
placeholder="支持模糊搜索"
v-model.trim="searchFormData.nickNameFuzzy"
@keyup.native="loadDataList"
></el-input>
</el-form-item>
</el-col>
<el-col :span="4">
<!-- 下拉框 -->
<el-form-item label="状态">
<el-select
clearable
placeholder="请选择状态"
v-model="searchFormData.status"
>
<el-option :value="1" label="启用"></el-option>
<el-option :value="0" label="禁用"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4" :style="{ 'padding-left': '10px' }">
<el-button type="primary" @click="loadDataList"> 查询 </el-button>
</el-col>
</el-row>
</el-form>
</div>
<div class="file-list">
<Table
:columns="columns"
:showPagination="true"
:dataSource="tableData"
:fetch="loadDataList"
:options="tableOptions"
>
<template #avatar="{ index, row }">
<div class="avatar">
<Avatar :userId="row.userId" :avatar="row.qqAvatar"></Avatar>
</div>
</template>
<template #space="{ index, row }">
{{ proxy.Utils.sizeToStr(row.useSpace) }}/{{
proxy.Utils.sizeToStr(row.totalSpace)
}}
</template>
<template #status="{ index, row }">
<span v-if="row.status == 1" style="color: #529b2e">启用</span>
<span v-if="row.status == 0" style="color: #f56c6c">禁用</span>
</template>
<template #op="{ index, row }">
<span class="a-link" @click="updateSpace(row)">分配空间</span>
<el-divider direction="vertical" />
<span class="a-link" @click="updateUserStatus(row)">{{
row.status == 0 ? "启用" : "禁用"
}}</span>
</template>
</Table>
</div>
<Dialog
:show="dialogConfig.show"
:title="dialogConfig.title"
:buttons="dialogConfig.buttons"
width="400px"
:showCancel="false"
@close="dialogConfig.show = false"
>
<el-form
:model="formData"
:rules="rules"
ref="formDataRef"
label-width="80px"
@submit.prevent
>
<!--input输入-->
<el-form-item label="昵称">
{{ formData.nickName }}
</el-form-item>
<el-form-item label="空间大小" prop="changeSpace">
<el-input
clearable
placeholder="请输入空间大小"
v-model="formData.changeSpace"
>
<template #suffix>MB</template>
</el-input>
</el-form-item>
</el-form>
</Dialog>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, nextTick } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
loadDataList: "/admin/loadUserList",
updateUserStatus: "/admin/updateUserStatus",
updateUserSpace: "/admin/updateUserSpace",
};
//列表
const columns = [
{
label: "头像",
prop: "avatar",
width: 80,
scopedSlots: "avatar",
},
{
label: "昵称",
prop: "nickName",
},
{
label: "邮箱",
prop: "email",
},
{
label: "空间使用",
prop: "space",
scopedSlots: "space",
},
{
label: "加入时间",
prop: "joinTime",
},
{
label: "最后登录时间",
prop: "lastLoginTime",
},
{
label: "状态",
prop: "status",
scopedSlots: "status",
width: 80,
},
{
label: "操作",
prop: "op",
width: 150,
scopedSlots: "op",
},
];
const searchFormData = ref({});
//列表
const tableData = ref({});
const tableOptions = {
extHeight: 20,
};
const loadDataList = async () => {
let params = {
pageNo: tableData.value.pageNo,
pageSize: tableData.value.pageSize,
};
Object.assign(params, searchFormData.value);
let result = await proxy.Request({
url: api.loadDataList,
params,
});
if (!result) {
return;
}
tableData.value = result.data;
};
//修改状态
const updateUserStatus = (row) => {
proxy.Confirm(
`你确定要【${row.status == 0 ? "启动" : "禁用"}】吗?`,
async () => {
let result = await proxy.Request({
url: api.updateUserStatus,
params: {
userId: row.userId,
status: row.status == 0 ? 1 : 0,
},
});
if (!result) {
return;
}
loadDataList();
}
);
};
//分配空间大小
const dialogConfig = ref({
show: false,
title: "修改空间大小",
buttons: [
{
type: "primary",
text: "确定",
click: (e) => {
submitForm();
},
},
],
});
const formData = ref({});
const formDataRef = ref();
const rules = {
changeSpace: [{ required: true, message: "请输入空间大小" }],
};
const updateSpace = (data) => {
dialogConfig.value.show = true;
nextTick(() => {
formDataRef.value.resetFields();
formData.value = Object.assign({}, data);
});
};
const submitForm = () => {
formDataRef.value.validate(async (valid) => {
if (!valid) {
return;
}
let params = {};
Object.assign(params, formData.value);
let result = await proxy.Request({
url: api.updateUserSpace,
params: params,
});
if (!result) {
return;
}
dialogConfig.value.show = false;
proxy.Message.success("操作成功");
loadDataList();
});
};
</script>
<style lang="scss" scoped>
.top-panel {
margin-top: 10px;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 25px;
overflow: hidden;
img {
width: 100%;
height: 100;
}
}
</style>
+511
View File
@@ -0,0 +1,511 @@
<template>
<div>
<div class="top">
<div class="top-op">
<div class="btn">
<el-upload
:show-file-list="false"
:with-credentials="true"
:multiple="true"
:http-request="addFile"
:accept="fileAccept"
>
<el-button type="primary">
<span class="iconfont icon-upload"></span>
上传
</el-button>
</el-upload>
</div>
<el-button type="success" @click="newFolder" v-if="category == 'all'">
<span class="iconfont icon-folder-add"></span>
新建文件夹
</el-button>
<el-button
@click="delFileBatch"
type="danger"
:disabled="selectFileIdList.length == 0"
>
<span class="iconfont icon-del"></span>
批量删除
</el-button>
<el-button
@click="moveFolderBatch"
type="warning"
:disabled="selectFileIdList.length == 0"
>
<span class="iconfont icon-move"></span>
批量移动
</el-button>
<div class="search-panel">
<el-input
clearable
placeholder="输入文件名搜索"
v-model="fileNameFuzzy"
@keyup.enter="search"
>
<template #suffix>
<i class="iconfont icon-search" @click="search"></i>
</template>
</el-input>
</div>
<div class="iconfont icon-refresh" @click="loadDataList"></div>
</div>
<!--导航-->
<Navigation ref="navigationRef" @navChange="navChange"></Navigation>
</div>
<div class="file-list" v-if="tableData.list && tableData.list.length > 0">
<Table
ref="dataTableRef"
:columns="columns"
:showPagination="true"
:dataSource="tableData"
:fetch="loadDataList"
:initFetch="false"
:options="tableOptions"
@rowSelected="rowSelected"
>
<template #fileName="{ index, row }">
<div
class="file-item"
@mouseenter="showOp(row)"
@mouseleave="cancelShowOp(row)"
>
<template
v-if="(row.fileType == 3 || row.fileType == 1) && row.status == 2"
>
<icon :cover="row.fileCover" :width="32"></icon>
</template>
<template v-else>
<icon v-if="row.folderType == 0" :fileType="row.fileType"></icon>
<icon v-if="row.folderType == 1" :fileType="0"></icon>
</template>
<span class="file-name" v-if="!row.showEdit" :title="row.fileName">
<span @click="preview(row)">{{ row.fileName }}</span>
<span v-if="row.status == 0" class="transfer-status">转码中</span>
<span v-if="row.status == 1" class="transfer-status transfer-fail"
>转码失败</span
>
</span>
<div class="edit-panel" v-show="row.showEdit">
<el-input
v-model.trim="row.fileNameReal"
ref="editNameRef"
:maxLength="190"
@keyup.enter="saveNameEdit(index)"
>
<template #suffix>{{ row.fileSuffix }}</template>
</el-input>
<span
:class="[
'iconfont icon-right1',
row.fileNameReal ? '' : 'not-allow',
]"
@click="saveNameEdit(index)"
></span>
<span
class="iconfont icon-error"
@click="cancelNameEdit(index)"
></span>
</div>
<span class="op">
<template v-if="row.showOp && row.fileId && row.status == 2">
<span class="iconfont icon-share1" @click="share(row)"
>分享</span
>
<span
class="iconfont icon-download"
@click="download(row)"
v-if="row.folderType == 0"
>下载</span
>
<span class="iconfont icon-del" @click="delFile(row)"
>删除</span
>
<span
class="iconfont icon-edit"
@click.stop="editFileName(index)"
>重命名</span
>
<span class="iconfont icon-move" @click="moveFolder(row)"
>移动</span
>
</template>
</span>
</div>
</template>
<template #fileSize="{ index, row }">
<span v-if="row.fileSize">
{{ proxy.Utils.sizeToStr(row.fileSize) }}</span
>
</template>
</Table>
</div>
<div class="no-data" v-else>
<div class="no-data-inner">
<Icon iconName="no_data" :width="120" fit="fill"></Icon>
<div class="tips">当前目录为空上传你的第一个文件吧</div>
<div class="op-list">
<el-upload
:show-file-list="false"
:with-credentials="true"
:multiple="true"
:http-request="addFile"
:accept="fileAccept"
>
<div class="op-item">
<Icon iconName="file" :width="60"></Icon>
<div>上传文件</div>
</div>
</el-upload>
<div class="op-item" v-if="category == 'all'" @click="newFolder">
<Icon iconName="folder" :width="60"></Icon>
<div>新建目录</div>
</div>
</div>
</div>
</div>
<!--预览-->
<Preview ref="previewRef"> </Preview>
<!--移动-->
<FolderSelect
ref="folderSelectRef"
@folderSelect="moveFolderDone"
></FolderSelect>
<!--分享-->
<FileShare ref="shareRef"></FileShare>
</div>
</template>
<script setup>
import CategoryInfo from "@/js/CategoryInfo.js";
import FileShare from "./ShareFile.vue";
import { ref, reactive, getCurrentInstance, nextTick, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const emit = defineEmits(["addFile"]);
//添加文件
const addFile = async (fileData) => {
emit("addFile", { file: fileData.file, filePid: currentFolder.value.fileId });
};
//添加文件回调
const reload = () => {
showLoading.value = false;
loadDataList();
};
defineExpose({
reload,
});
const api = {
loadDataList: "/file/loadDataList",
rename: "/file/rename",
newFoloder: "/file/newFoloder",
getFolderInfo: "/file/getFolderInfo",
delFile: "/file/delFile",
changeFileFolder: "/file/changeFileFolder",
createDownloadUrl: "/file/createDownloadUrl",
download: "/api/file/download",
};
const fileAccept = computed(() => {
const categoryItem = CategoryInfo[category.value];
return categoryItem ? categoryItem.accept : "*";
});
//列表
const columns = [
{
label: "文件名",
prop: "fileName",
scopedSlots: "fileName",
},
{
label: "修改时间",
prop: "lastUpdateTime",
width: 200,
},
{
label: "大小",
prop: "fileSize",
scopedSlots: "fileSize",
width: 200,
},
];
//搜索
const search = () => {
showLoading.value = true;
loadDataList();
};
//列表
const tableData = ref({});
const tableOptions = {
extHeight: 50,
selectType: "checkbox",
};
//多选 批量选择
const selectFileIdList = ref([]);
const rowSelected = (rows) => {
selectFileIdList.value = [];
rows.forEach((item) => {
selectFileIdList.value.push(item.fileId);
});
};
const fileNameFuzzy = ref();
const showLoading = ref(true);
const category = ref();
const loadDataList = async () => {
let params = {
pageNo: tableData.value.pageNo,
pageSize: tableData.value.pageSize,
fileNameFuzzy: fileNameFuzzy.value,
category: category.value,
filePid: currentFolder.value.fileId,
};
if (params.category !== "all") {
delete params.filePid;
}
let result = await proxy.Request({
url: api.loadDataList,
showLoading: showLoading,
params,
});
if (!result) {
return;
}
tableData.value = result.data;
editing.value = false;
};
//展示操作按钮
const showOp = (row) => {
tableData.value.list.forEach((element) => {
element.showOp = false;
});
row.showOp = true;
};
const cancelShowOp = (row) => {
row.showOp = false;
};
//编辑行
const editing = ref(false);
const editNameRef = ref();
//新建文件夹
const newFolder = () => {
if (editing.value) {
return;
}
tableData.value.list.forEach((element) => {
element.showEdit = false;
});
editing.value = true;
tableData.value.list.unshift({
showEdit: true,
fileType: 0,
fileId: "",
filePid: currentFolder.value.fileId,
});
nextTick(() => {
editNameRef.value.focus();
});
};
//编辑文件名
const editFileName = (index) => {
if (tableData.value.list[0].fileId == "") {
tableData.value.list.splice(0, 1);
}
tableData.value.list.forEach((element) => {
element.showEdit = false;
});
let cureentData = tableData.value.list[index];
cureentData.showEdit = true;
//编辑文件
if (cureentData.folderType == 0) {
cureentData.fileNameReal = cureentData.fileName.substring(
0,
cureentData.fileName.indexOf(".")
);
cureentData.fileSuffix = cureentData.fileName.substring(
cureentData.fileName.indexOf(".")
);
} else {
cureentData.fileNameReal = cureentData.fileName;
cureentData.fileSuffix = "";
}
editing.value = true;
nextTick(() => {
editNameRef.value.focus();
});
};
const cancelNameEdit = (index) => {
const fileData = tableData.value.list[index];
if (fileData.fileId) {
fileData.showEdit = false;
} else {
tableData.value.list.splice(index, 1);
editing.value = false;
}
};
const saveNameEdit = async (index) => {
const { fileId, filePid, fileNameReal } = tableData.value.list[index];
if (fileNameReal == "" || fileNameReal.indexOf("/") != -1) {
proxy.Message.warning("文件名不能为空且不能含有斜杠");
return;
}
let url = api.rename;
if (fileId == "") {
url = api.newFoloder;
}
let result = await proxy.Request({
url: url,
params: {
fileId,
filePid: filePid,
fileName: fileNameReal,
},
});
if (!result) {
return;
}
tableData.value.list[index] = result.data;
editing.value = false;
};
const previewRef = ref();
const navigationRef = ref();
const preview = (data) => {
if (data.folderType == 1) {
//openFolder(data);
navigationRef.value.openFolder(data);
return;
}
if (data.status != 2) {
proxy.Message.warning("文件正在转码中,无法预览");
return;
}
previewRef.value.showPreview(data, 0);
};
//目录
const currentFolder = ref({ fileId: 0 });
const navChange = (data) => {
const { curFolder, categoryId } = data;
currentFolder.value = curFolder;
showLoading.value = true;
category.value = categoryId;
loadDataList();
};
//移动目录
const folderSelectRef = ref();
const currentMoveFile = ref({});
const moveFolder = (data) => {
currentMoveFile.value = data;
folderSelectRef.value.showFolderDialog(data.fileId);
};
//批量删除
const moveFolderBatch = () => {
currentMoveFile.value = {};
folderSelectRef.value.showFolderDialog(selectFileIdList.value.join(","));
};
const moveFolderDone = async (folderId) => {
if (
currentMoveFile.value.filePid === folderId ||
currentFolder.value.fileId == folderId
) {
proxy.Message.warning("文件正在当前目录,无需移动");
return;
}
let filedIdsArray = [];
if (currentMoveFile.value.fileId) {
filedIdsArray.push(currentMoveFile.value.fileId);
} else {
filedIdsArray = filedIdsArray.concat(selectFileIdList.value);
}
let result = await proxy.Request({
url: api.changeFileFolder,
params: {
fileIds: filedIdsArray.join(","),
filePid: folderId,
},
});
if (!result) {
return;
}
folderSelectRef.value.close();
loadDataList();
};
//删除文件
const delFile = (row) => {
proxy.Confirm(
`你确定要删除【${row.fileName}】吗?删除的文件可在10天内通过回收站还原`,
async () => {
let result = await proxy.Request({
url: api.delFile,
params: {
fileIds: row.fileId,
},
});
if (!result) {
return;
}
loadDataList();
dataTableRef.value.clearSelection();
}
);
};
//批量删除
const delFileBatch = () => {
if (selectFileIdList.value.length == 0) {
return;
}
proxy.Confirm(
`你确定要删除这些文件吗?删除的文件可在10天内通过回收站还原`,
async () => {
let result = await proxy.Request({
url: api.delFile,
params: {
fileIds: selectFileIdList.value.join(","),
},
});
if (!result) {
return;
}
loadDataList();
}
);
};
//下载文件
const download = async (row) => {
let result = await proxy.Request({
url: api.createDownloadUrl + "/" + row.fileId,
});
if (!result) {
return;
}
window.location.href = api.download + "/" + result.data;
};
//分享
const shareRef = ref();
const share = (row) => {
shareRef.value.show(row);
};
</script>
<style lang="scss" scoped>
@import "@/assets/file.list.scss";
</style>
+149
View File
@@ -0,0 +1,149 @@
<template>
<div>
<Dialog
:show="dialogConfig.show"
:title="dialogConfig.title"
:buttons="dialogConfig.buttons"
width="600px"
:showCancel="showCancel"
@close="dialogConfig.show = false"
>
<el-form
:model="formData"
:rules="rules"
ref="formDataRef"
label-width="100px"
@submit.prevent
>
<el-form-item label="文件"> {{ formData.fileName }} </el-form-item>
<template v-if="showType == 0">
<el-form-item label="有效期" prop="validType">
<el-radio-group v-model="formData.validType">
<el-radio :label="0">1</el-radio>
<el-radio :label="1">7</el-radio>
<el-radio :label="2">30</el-radio>
<el-radio :label="3">永久有效</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="提取码" prop="codeType">
<el-radio-group v-model="formData.codeType">
<el-radio :label="0">自定义</el-radio>
<el-radio :label="1">系统生成</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="code" v-if="formData.codeType == 0">
<el-input
clearable
placeholder="请输入5位提取码"
v-model.trim="formData.code"
maxLength="5"
:style="{ width: '130px' }"
></el-input>
</el-form-item>
</template>
<template v-else>
<el-form-item label="分享连接" prop="validType">
{{ shareUrl }}{{ resultInfo.shareId }}
</el-form-item>
<el-form-item label="提取码" prop="validType">
{{ resultInfo.code }}
</el-form-item>
<el-form-item prop="validType">
<el-button type="primary" @click="copy">复制链接极提取码</el-button>
</el-form-item>
</template>
</el-form>
</Dialog>
</div>
</template>
<script setup>
import useClipboard from "vue-clipboard3";
const { toClipboard } = useClipboard();
import { ref, getCurrentInstance, nextTick } from "vue";
import { useRouter } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const shareUrl = ref(document.location.origin + "/share/");
const api = {
shareFile: "/share/shareFile",
};
const showType = ref(0);
const formData = ref({});
const formDataRef = ref();
const rules = {
validType: [{ required: true, message: "请选择有效期" }],
codeType: [{ required: true, message: "请选择提取码类型" }],
code: [
{ required: true, message: "请输入提取码" },
{ validator: proxy.Verify.shareCode, message: "提取码只能是数字字母" },
{ min: 5, message: "提取码最少5位" },
],
};
const showCancel = ref(true);
const dialogConfig = ref({
show: false,
title: "分享",
buttons: [
{
type: "primary",
text: "确定",
click: (e) => {
share();
},
},
],
});
const resultInfo = ref({});
const share = async () => {
if (Object.keys(resultInfo.value).length > 0) {
dialogConfig.value.show = false;
return;
}
formDataRef.value.validate(async (valid) => {
if (!valid) {
return;
}
let params = {};
Object.assign(params, formData.value);
let result = await proxy.Request({
url: api.shareFile,
params: params,
});
if (!result) {
return;
}
showType.value = 1;
resultInfo.value = result.data;
dialogConfig.value.buttons[0].text = "关闭";
showCancel.value = false;
});
};
const show = (data) => {
showCancel.value = true;
dialogConfig.value.show = true;
showType.value = 0;
resultInfo.value = {};
nextTick(() => {
formDataRef.value.resetFields();
formData.value = Object.assign({}, data);
});
};
defineExpose({ show });
const copy = async () => {
await toClipboard(
`链接:${shareUrl.value}${resultInfo.value.shareId} 提取码: ${resultInfo.value.code}`
);
proxy.Message.success("复制成功");
};
</script>
<style lang="scss" scoped>
</style>
+455
View File
@@ -0,0 +1,455 @@
<template>
<div class="uploader-panel">
<div class="uploader-title">
<span>上传任务</span>
<span class="tips">仅展示本次上传任务</span>
</div>
<div class="file-list">
<div v-for="(item, index) in fileList" class="file-item">
<div class="upload-panel">
<div class="file-name">
{{ item.fileName }}
</div>
<div class="progress">
<!--上传-->
<el-progress
:percentage="item.uploadProgress"
v-if="
item.status == STATUS.uploading.value ||
item.status == STATUS.upload_seconds.value ||
item.status == STATUS.upload_finish.value
"
/>
</div>
<div class="upload-status">
<!--图标-->
<span
:class="['iconfont', 'icon-' + STATUS[item.status].icon]"
:style="{ color: STATUS[item.status].color }"
></span>
<!--状态描述-->
<span
class="status"
:style="{ color: STATUS[item.status].color }"
>{{
item.status == "fail" ? item.errorMsg : STATUS[item.status].desc
}}</span
>
<!--上传中-->
<span
class="upload-info"
v-if="item.status == STATUS.uploading.value"
>
{{ sizeTostr(item.uploadSize) }}/{{ sizeTostr(item.totalSize) }}
</span>
</div>
</div>
<div class="op">
<!--MD5-->
<el-progress
type="circle"
:width="50"
:percentage="item.md5Progress"
v-if="item.status == STATUS.init.value"
/>
<div class="op-btn">
<span v-if="item.status === STATUS.uploading.value">
<icon
:width="28"
class="btn-item"
iconName="upload"
v-if="item.pause"
title="上传"
@click="startUpload(item.uid)"
></icon>
<icon
:width="28"
class="btn-item"
iconName="pause"
title="暂停"
@click="pauseUpload(item.uid)"
v-else
></icon>
</span>
<icon
:width="28"
class="del btn-item"
iconName="del"
title="删除"
v-if="
item.status != STATUS.init.value &&
item.status != STATUS.upload_finish.value &&
item.status != STATUS.upload_seconds.value
"
@click="delUpload(item.uid, index)"
></icon>
<icon
:width="28"
class="clean btn-item"
iconName="clean"
title="清除"
v-if="
item.status == STATUS.upload_finish.value ||
item.status == STATUS.upload_seconds.value
"
@click="delUpload(item.uid, index)"
></icon>
</div>
</div>
</div>
<div v-if="fileList.length == 0">
<NoData msg="暂无上传任务"></NoData>
</div>
</div>
</div>
</template>
<script setup>
import {
getCurrentInstance,
onMounted,
reactive,
ref,
watch,
nextTick,
} from "vue";
import SparkMD5 from "spark-md5";
const { proxy } = getCurrentInstance();
const STATUS = {
emptyfile: {
value: "emptyfile",
desc: "文件为空",
color: "#F75000",
icon: "close",
},
fail: {
value: "fail",
desc: "上传失败",
color: "#F75000",
icon: "close",
},
init: {
value: "init",
desc: "解析中",
color: "#e6a23c",
icon: "clock",
},
uploading: {
value: "uploading",
desc: "上传中",
color: "#409eff",
icon: "upload",
},
upload_finish: {
value: "upload_finish",
desc: "上传完成",
color: "#67c23a",
icon: "ok",
},
upload_seconds: {
value: "upload_seconds",
desc: "秒传",
color: "#67c23a",
icon: "ok",
},
};
const chunkSize = 1024 * 1024 * 5;
const fileList = ref([]);
const delList = ref([]);
const addFile = async (file, filePid) => {
const fileItem = {
file: file,
//文件UID
uid: file.uid,
//md5进度
md5Progress: 0,
//md5值
md5: null,
//文件名
fileName: file.name,
//上传状态
status: STATUS.init.value,
//已上传大小
uploadSize: 0,
//文件总大小
totalSize: file.size,
//进度
uploadProgress: 0,
//暂停
pause: false,
//当前分片
chunkIndex: 0,
//父级ID
filePid: filePid,
//错误信息
errorMsg: null,
};
//加入文件
fileList.value.unshift(fileItem);
if (fileItem.totalSize == 0) {
fileItem.status = STATUS.emptyfile.value;
return;
}
//文件MD5
let md5FileUid = await computeMD5(fileItem);
if (md5FileUid == null) {
return;
}
uploadFile(md5FileUid);
};
defineExpose({ addFile });
//开始上传
const startUpload = (uid) => {
let currentFile = getFileByUid(uid);
currentFile.pause = false;
uploadFile(uid, currentFile.chunkIndex);
};
//暂停上传
const pauseUpload = (uid) => {
let currentFile = getFileByUid(uid);
currentFile.pause = true;
};
//删除文件
const delUpload = (uid, index) => {
delList.value.push(uid);
fileList.value.splice(index, 1);
};
const emit = defineEmits(["uploadCallback"]);
const uploadFile = async (uid, chunkIndex) => {
chunkIndex = chunkIndex ? chunkIndex : 0;
//分片上传
let currentFile = getFileByUid(uid);
const file = currentFile.file;
const fileSize = currentFile.totalSize;
const chunks = Math.ceil(fileSize / chunkSize);
for (let i = chunkIndex; i < chunks; i++) {
let delIndex = delList.value.indexOf(uid);
if (delIndex != -1) {
delList.value.splice(delIndex, 1);
// console.log(delList.value);
break;
}
currentFile = getFileByUid(uid);
if (currentFile.pause) {
break;
}
let start = i * chunkSize;
let end = start + chunkSize >= fileSize ? fileSize : start + chunkSize;
let chunkFile = file.slice(start, end);
let uploadResult = await proxy.Request({
url: "/file/uploadFile",
showLoading: false,
dataType: "file",
params: {
file: chunkFile,
fileName: file.name,
fileMd5: currentFile.md5,
chunkIndex: i,
chunks: chunks,
fileId: currentFile.fileId,
filePid: currentFile.filePid,
},
showError: false,
errorCallback: (errorMsg) => {
currentFile.status = STATUS.fail.value;
currentFile.errorMsg = errorMsg;
},
uploadProgressCallback: (event) => {
let loaded = event.loaded;
if (loaded > fileSize) {
loaded = fileSize;
}
currentFile.uploadSize = i * chunkSize + loaded;
currentFile.uploadProgress = Math.floor(
(currentFile.uploadSize / fileSize) * 100
);
},
});
if (uploadResult == null) {
break;
}
currentFile.fileId = uploadResult.data.fileId;
currentFile.status = STATUS[uploadResult.data.status].value;
currentFile.chunkIndex = i;
if (
uploadResult.data.status == STATUS.upload_seconds.value ||
uploadResult.data.status == STATUS.upload_finish.value
) {
currentFile.uploadProgress = 100;
emit("uploadCallback");
break;
}
}
};
const computeMD5 = (fileItem) => {
let file = fileItem.file;
let blobSlice =
File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice;
let chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
let spark = new SparkMD5.ArrayBuffer();
let fileReader = new FileReader();
let time = new Date().getTime();
//file.cmd5 = true;
let loadNext = () => {
let start = currentChunk * chunkSize;
let end = start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
};
loadNext();
return new Promise((resolve, reject) => {
let resultFile = getFileByUid(file.uid);
fileReader.onload = (e) => {
spark.append(e.target.result); // Append array buffer
currentChunk++;
if (currentChunk < chunks) {
/* console.log(
`第${file.name},${currentChunk}分片解析完成, 开始第${
currentChunk + 1
} / ${chunks}分片解析`
); */
let percent = Math.floor((currentChunk / chunks) * 100);
resultFile.md5Progress = percent;
loadNext();
} else {
let md5 = spark.end();
/* console.log(
`MD5计算完成:${file.name} \nMD5${md5} \n分片:${chunks} 大小:${
file.size
} 用时:${new Date().getTime() - time} ms`
); */
spark.destroy(); //释放缓存
resultFile.md5Progress = 100;
resultFile.status = STATUS.uploading.value;
resultFile.md5 = md5;
resolve(fileItem.uid);
}
};
fileReader.onerror = () => {
resultFile.md5Progress = -1;
resultFile.status = STATUS.fail.value;
resolve(fileItem.uid);
};
}).catch((error) => {
return null;
});
};
//获取文件
const getFileByUid = (uid) => {
let file = fileList.value.find((item) => {
return item.file.uid === uid;
});
return file;
};
//字节转为M
const sizeTostr = (size) => {
var data = "";
if (size < 0.1 * 1024) {
//如果小于0.1KB转化成B
data = size.toFixed(2) + "B";
} else if (size < 0.1 * 1024 * 1024) {
//如果小于0.1MB转化成KB
data = (size / 1024).toFixed(2) + "KB";
} else if (size < 1024 * 1024 * 1024) {
//如果小于1GB转化成MB
data = (size / (1024 * 1024)).toFixed(2) + "MB";
} else {
//其他转化成GB
data = (size / (1024 * 1024 * 1024)).toFixed(2) + "GB";
}
var sizestr = data + "";
var len = sizestr.indexOf(".");
var dec = sizestr.substr(len + 1, 2);
if (dec == "00") {
//当小数点后为00时 去掉小数部分
return sizestr.substring(0, len) + sizestr.substr(len + 3, 2);
}
return sizestr;
};
</script>
<style lang="scss" scoped>
.uploader-panel {
.uploader-title {
border-bottom: 1px solid #ddd;
line-height: 40px;
padding: 0px 10px;
font-size: 15px;
.tips {
font-size: 13px;
color: rgb(169, 169, 169);
}
}
.file-list {
overflow: auto;
padding: 10px 0px;
min-height: calc(100vh / 2);
max-height: calc(100vh - 120px);
.file-item {
position: relative;
display: flex;
justify-content: center;
align-items: center;
padding: 3px 10px;
background-color: #fff;
border-bottom: 1px solid #ddd;
}
.file-item:nth-child(even) {
background-color: #fcf8f4;
}
.upload-panel {
flex: 1;
.file-name {
color: rgb(64, 62, 62);
}
.upload-status {
display: flex;
align-items: center;
margin-top: 5px;
.iconfont {
margin-right: 3px;
}
.status {
color: red;
font-size: 13px;
}
.upload-info {
margin-left: 5px;
font-size: 12px;
color: rgb(112, 111, 111);
}
}
.progress {
height: 10px;
}
}
.op {
width: 100px;
display: flex;
align-items: center;
justify-content: flex-end;
.op-btn {
.btn-item {
cursor: pointer;
}
.del,
.clean {
margin-left: 5px;
}
}
}
}
}
</style>
+228
View File
@@ -0,0 +1,228 @@
<template>
<div>
<div class="top">
<el-button
type="success"
:disabled="selectFileIdList.length == 0"
@click="revertBatch"
>
<span class="iconfont icon-revert"></span>还原
</el-button>
<el-button
type="danger"
:disabled="selectFileIdList.length == 0"
@click="delBatch"
>
<span class="iconfont icon-del"></span>批量删除
</el-button>
</div>
<div class="file-list">
<Table
:columns="columns"
:showPagination="true"
:dataSource="tableData"
:fetch="loadDataList"
:options="tableOptions"
@rowSelected="rowSelected"
>
<template #fileName="{ index, row }">
<div
class="file-item"
@mouseenter="showOp(row)"
@mouseleave="cancelShowOp(row)"
>
<template
v-if="
(row.fileType == 3 || row.fileType == 1) && row.status !== 0
"
>
<icon :cover="row.fileCover"></icon>
</template>
<template v-else>
<icon v-if="row.folderType == 0" :fileType="row.fileType"></icon>
<icon v-if="row.folderType == 1" :fileType="0"></icon>
</template>
<span class="file-name" :title="row.fileName">
<span>{{ row.fileName }}</span>
</span>
<span class="op">
<template v-if="row.showOp && row.fileId">
<span class="iconfont icon-revert" @click="revert(row)"
>还原</span
>
<span class="iconfont icon-del" @click="delFile(row)"
>删除</span
>
</template>
</span>
</div>
</template>
<template #fileSize="{ index, row }">
<span v-if="row.fileSize">
{{ proxy.Utils.sizeToStr(row.fileSize) }}</span
>
</template>
</Table>
</div>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, nextTick } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
loadDataList: "/recycle/loadRecycleList",
delFile: "/recycle/delFile",
recoverFile: "/recycle/recoverFile",
};
//列表
const columns = [
{
label: "文件名",
prop: "fileName",
scopedSlots: "fileName",
},
{
label: "删除时间",
prop: "recoveryTime",
width: 200,
},
{
label: "大小",
prop: "fileSize",
scopedSlots: "fileSize",
width: 200,
},
];
//列表
const tableData = ref({});
const tableOptions = {
extHeight: 20,
selectType: "checkbox",
};
const loadDataList = async () => {
let params = {
pageNo: tableData.value.pageNo,
pageSize: tableData.value.pageSize,
};
if (params.category !== "all") {
delete params.filePid;
}
let result = await proxy.Request({
url: api.loadDataList,
params,
});
if (!result) {
return;
}
tableData.value = result.data;
};
//展示操作按钮
const showOp = (row) => {
tableData.value.list.forEach((element) => {
element.showOp = false;
});
row.showOp = true;
};
const cancelShowOp = (row) => {
row.showOp = false;
};
const selectFileIdList = ref([]);
const rowSelected = (rows) => {
selectFileIdList.value = [];
rows.forEach((item) => {
selectFileIdList.value.push(item.fileId);
});
};
//恢复
const revert = (row) => {
proxy.Confirm(`你确定要还原【${row.fileName}】吗?`, async () => {
let result = await proxy.Request({
url: api.recoverFile,
params: {
fileIds: row.fileId,
},
});
if (!result) {
return;
}
loadDataList();
});
};
const revertBatch = () => {
if (selectFileIdList.value.length == 0) {
return;
}
proxy.Confirm(`你确定要还原这些文件吗?`, async () => {
let result = await proxy.Request({
url: api.recoverFile,
params: {
fileIds: selectFileIdList.value.join(","),
},
});
if (!result) {
return;
}
loadDataList();
});
};
//删除文件
const emit = defineEmits(["reload"]);
const delFile = (row) => {
proxy.Confirm(`你确定要删除【${row.fileName}】?`, async () => {
let result = await proxy.Request({
url: api.delFile,
params: {
fileIds: row.fileId,
},
});
if (!result) {
return;
}
loadDataList();
emit("reload");
});
};
const delBatch = (row) => {
if (selectFileIdList.value.length == 0) {
return;
}
proxy.Confirm(`你确定要删除选中的文件?删除将无法恢复`, async () => {
let result = await proxy.Request({
url: api.delFile,
params: {
fileIds: selectFileIdList.value.join(","),
},
});
if (!result) {
return;
}
loadDataList();
emit("reload");
});
};
</script>
<style lang="scss" scoped>
@import "@/assets/file.list.scss";
.file-list {
margin-top: 10px;
.file-item {
.op {
width: 120px;
}
}
}
</style>
+213
View File
@@ -0,0 +1,213 @@
<template>
<div>
<div class="top">
<el-button
type="primary"
:disabled="selectIdList.length == 0"
@click="cancelShareBatch"
>
<span class="iconfont icon-cancel"></span>取消分享
</el-button>
</div>
<div class="file-list">
<Table
:columns="columns"
:showPagination="true"
:dataSource="tableData"
:fetch="loadDataList"
:options="tableOptions"
@rowSelected="rowSelected"
>
<template #fileName="{ index, row }">
<div
class="file-item"
@mouseenter="showOp(row)"
@mouseleave="cancelShowOp(row)"
>
<template
v-if="
(row.fileType == 3 || row.fileType == 1) && row.status !== 0
"
>
<icon :cover="row.fileCover"></icon>
</template>
<template v-else>
<icon v-if="row.folderType == 0" :fileType="row.fileType"></icon>
<icon v-if="row.folderType == 1" :fileType="0"></icon>
</template>
<span
class="file-name"
v-if="!row.showRename"
:title="row.fileName"
>
<span>{{ row.fileName }}</span>
</span>
<span class="op">
<template v-if="row.showOp && row.fileId">
<span class="iconfont icon-link" @click="copy(row)"
>复制链接</span
>
<span class="iconfont icon-cancel" @click="cancelShare(row)"
>取消分享</span
>
</template>
</span>
</div>
</template>
<template #expireTime="{ index, row }">
{{ row.validType == 3 ? "永久" : row.expireTime }}
</template>
</Table>
</div>
</div>
</template>
<script setup>
import useClipboard from "vue-clipboard3";
const { toClipboard } = useClipboard();
import { ref, reactive, getCurrentInstance, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
loadDataList: "/share/loadShareList",
cancelShare: "/share/cancelShare",
};
const shareUrl = ref(document.location.origin + "/share/");
//列表
const columns = [
{
label: "文件名",
prop: "fileName",
scopedSlots: "fileName",
},
{
label: "分享时间",
prop: "shareTime",
width: 200,
},
{
label: "失效时间",
prop: "expireTime",
scopedSlots: "expireTime",
width: 200,
},
{
label: "浏览次数",
prop: "showCount",
width: 200,
},
];
//搜索
const search = () => {
showLoading.value = true;
loadDataList();
};
//列表
const tableData = ref({});
const tableOptions = {
extHeight: 20,
selectType: "checkbox",
};
const loadDataList = async () => {
let params = {
pageNo: tableData.value.pageNo,
pageSize: tableData.value.pageSize,
};
if (params.category !== "all") {
delete params.filePid;
}
let result = await proxy.Request({
url: api.loadDataList,
params,
});
if (!result) {
return;
}
tableData.value = result.data;
};
//展示操作按钮
const showOp = (row) => {
tableData.value.list.forEach((element) => {
element.showOp = false;
});
row.showOp = true;
};
const cancelShowOp = (row) => {
row.showOp = false;
};
//复制链接
const copy = async (data) => {
await toClipboard(
`链接:${shareUrl.value}${data.shareId} 提取码: ${data.code}`
);
proxy.Message.success("复制成功");
};
//多选 批量选择
const selectIdList = ref([]);
const rowSelected = (rows) => {
selectIdList.value = [];
rows.forEach((item) => {
selectIdList.value.push(item.shareId);
});
};
//取消分享
const cancelShareIdList = ref([]);
const cancelShareBatch = () => {
if (selectIdList.value.length == 0) {
return;
}
cancelShareIdList.value = selectIdList.value;
cancelShareDone();
};
const cancelShare = (row) => {
cancelShareIdList.value = [row.shareId];
cancelShareDone();
};
const cancelShareDone = async () => {
proxy.Confirm(`你确定要取消分享吗?`, async () => {
let result = await proxy.Request({
url: api.cancelShare,
params: {
shareIds: cancelShareIdList.value.join(","),
},
});
if (!result) {
return;
}
proxy.Message.success("取消分享成功");
loadDataList();
});
};
</script>
<style lang="scss" scoped>
@import "@/assets/file.list.scss";
.file-list {
margin-top: 10px;
.file-item {
.file-name {
span {
&:hover {
color: #494944;
}
}
}
.op {
width: 170px;
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More