first commit

This commit is contained in:
chuzhongzai 2023-12-08 13:34:39 +08:00
commit 61d2037ca0
29 changed files with 5524 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).

16
index.html Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SNS</title>
</head>
<body>
<div class="dom"></div>
<div id="app">
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

2227
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "sns",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@icon-park/vue-next": "^1.4.2",
"axios": "^1.5.0",
"echarts": "^5.4.3",
"element-plus": "^2.3.12",
"qs": "^6.11.2",
"unplugin-icons": "^0.17.1",
"vue": "^3.3.4",
"vue-router": "^4.2.5",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.4.5"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

11
src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup>
</script>
<template>
<router-view>
</router-view>
</template>
<style scoped>
</style>

60
src/Util/index.js Normal file
View File

@ -0,0 +1,60 @@
let KB = 1024
let MB = KB * 1024
let GB = MB * 1024
let Util = {
formatSize:function (limit) {
let 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"
}
let sizeStr = size + ""; //转成字符串
let index = sizeStr.indexOf("."); //获取小数点处的索引
let dou = sizeStr.substring(index + 1, 2) //获取小数点后两位的值
if (dou === "00") { //判断后两位是否为00如果是则删除00
return sizeStr.substring(0, index) + sizeStr.substring(index + 3, 2)
}
return size;
},
formatDuration: function (timestamp) {
let seconds = timestamp
let str = ''
if (seconds > 3600 * 24) {
let days = Math.floor(seconds / (3600 * 24))
seconds = seconds % 3600 * 24
str += `${days}`
}
if (seconds > 3600) {
let hours = Math.floor(seconds / 3600)
seconds = seconds % 3600
str += `${hours}`
}
if (seconds > 60) {
let minutes = Math.floor(seconds / 60)
str += `${minutes}`
}
return str
},
formatDate: function (time) {
return new Date(time).toLocaleString('zh-CN')
},
KB, MB, GB,
colors: [
{ color: '#f56c6c', percentage: 90 },
{ color: '#e6a23c', percentage: 80 },
{ color: '#6f7ad3', percentage: 60 },
{ color: '#1989fa', percentage: 40 },
{ color: '#5cb87a', percentage: 20 },
],
}
export default Util

View File

@ -0,0 +1,74 @@
<script setup>
import Util from "../../Util/index.js";
import {computed, ref} from "vue";
import axios from "axios";
import store from "../../store/index.js";
import qs from "qs";
import {ElMessage} from "element-plus";
let props = defineProps(['isAdjustingShare', 'currentFile', 'currentSiteId'])
let emits = defineEmits(['close'])
let isAdjustingShare = computed(() => {
return props.isAdjustingShare
})
let isAdmin = computed(() => {
return store.state.config.isAdmin
})
let expireHour = ref(null)
let count = ref(null)
function adjustShare() {
let shareCode = props.currentFile.shareCode
if(isAdmin.value)
shareCode = props.currentSiteId + ":" + props.currentFile.shareCode
axios.post(store.state.config.host + "share/adjust?", qs.stringify({shareCode, time:expireHour.value, count:count.value})).then((res) => {
if(res.data.result === 'success')
ElMessage.success(res.data.data)
else
ElMessage.error(res.data.data)
close()
})
}
function close(){
emits('close')
}
</script>
<template>
<el-dialog title="调整分享" v-model="isAdjustingShare" width="625px" @close="close">
<el-row>
<el-col :span="10">当前文件:{{currentFile.name}}</el-col>
<el-col :span="14">(正数为添加负数为减少都留空则调整为永久分享)</el-col>
</el-row>
<br>
<el-row>
<el-col :span="10">
过期时间:{{currentFile.expireTime === undefined?'不过期':Util.formatDate(currentFile.expireTime)}}
</el-col>
<el-input v-model="expireHour" style=" width: 300px">
<template #prepend>
分享时长
</template>
<template #append>
小时
</template>
</el-input>
</el-row>
<el-row>
<el-col :span="10">
剩余ip数:{{currentFile.availableCount === undefined?'不限制':currentFile.availableCount}}
</el-col>
<el-input v-model="count" style="width: 300px">
<template #prepend>分享ip数</template>
</el-input>
</el-row>
<template #footer>
<el-button type="primary" @click="adjustShare">确定</el-button>
<el-button @click="close">取消</el-button>
</template>
</el-dialog>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,196 @@
<script setup>
import hardDisk from "@icon-park/vue-next/lib/icons/HardDisk.js";
import {computed, ref, watch} from "vue";
import store from "../../store/index.js";
import axios from "axios";
import qs from "qs";
import {ElMessage} from "element-plus";
import {Folder} from "@element-plus/icons-vue";
let props = defineProps(['isOperatingFile', 'operation', 'selectedFiles', 'currentSideId', 'currentPath', 'crossSite'])
let emits = defineEmits(['close'])
//
let isOperatingFile = computed(() => {
return props.isOperatingFile
})
let isAdmin = computed(() => {
return store.state.config.isAdmin
})
let title = computed(() => {
switch (props.operation){
case "move":
return "移动到"
case "copy":
return "复制到"
case "extract":
return "解包到"
}
})
let cursor = ref([])
let files = ref([])
let folders = computed(() => {
let folders = []
if(cursor.value.length === 0){
onlineSites.value.forEach((site) => {
folders.push({name:site.hostname, site:true, id:site.id})
})
} else {
files.value.forEach((file) => {
//
if (file.type === "FOLDER"){
let push = true
props.selectedFiles.forEach((selectedFile) => {
if(selectedFile.type === 'FOLDER' && selectedFile.path === file.path)
push = false
})
if(push)
folders.push(file)
}
})
}
return folders;
})
let onlineSites = computed(() => {
return store.getters.getOnlineSites
})
let currentPath = computed(() => {
let path = ""
for(let i=0; i<cursor.value.length; i++){
if(i === 0){
if(isAdmin.value)
path = cursor.value[0] + ":"
}else
path += cursor.value[i] + "/"
}
return path
})
//
function handleClick(node) {
if(node.name === '...')
cursor.value.pop()
else if(node.id !== undefined)
cursor.value.push(node.id)
else
cursor.value.push(node.name)
loadFilesByCursor()
}
//
function loadFilesByCursor(){
files.value.splice(0)
//
if (cursor.value.length === 0){
onlineSites.value.forEach((site) => {
files.value.push({name:site.hostname, site:true})
})
} else {
axios.get(store.state.config.host + "file/get?" + qs.stringify({path:currentPath.value}))
.then((res) => {
let date = new Date()
if(res.data.result === 'success'){
if(cursor.value.length === 1) { //
if (isAdmin.value && props.crossSite)
files.value.push({name: '...', type: 'FOLDER', size: 0, lastModify: date.getTime()})
} else
files.value.push({name: '...', type: 'FOLDER', size: 0, lastModify: date.getTime()})
files.value.push(...res.data.data)
}else{
ElMessage.error("文件查询失败:" + res.data.data)
}
})
}
}
//
function jump(index){
cursor.value.splice(index)
loadFilesByCursor()
}
function close(){
emits("close")
}
//:
function confirm(){
let source = props.currentPath
let target = currentPath.value
let fileNames = []
props.selectedFiles.forEach((file) => {
fileNames.push(file.name)
})
//
if(props.operation === 'extract') {
target += fileNames[0].replace(".tar", "") + "/"
//targetsourceId
if(target.charAt(1) === ':')
target.substring(2)
}
//sourceId
if(!isAdmin.value)
source = source.substring(2)
axios.post(store.state.config.host + `file/${props.operation}?`, qs.stringify({source, target, fileNames}, {indices:false}))
.then((res) => {
if(res.data.result === 'success')
ElMessage.success(res.data.data)
else
ElMessage.error(res.data.data)
})
close()
}
watch(isOperatingFile, (value) => {
if(value) {
cursor.value.splice(0)
if (!isAdmin.value || !props.crossSite)
cursor.value[0] = props.currentSideId
loadFilesByCursor()
}
})
</script>
<template>
<el-dialog :title="title" v-model="isOperatingFile" top="0" @close="close" style="margin-bottom: 0">
<!-- 不能跨站或不是管理员时显示该面板-->
<el-popover v-if="!crossSite || !isAdmin">
<span v-if="isAdmin">
选择文件夹或打解包时不能跨服务器
</span>
<span v-else>
用户不能跨服务器
</span>
<template #reference>
<el-link :underline="false" disabled>全部节点</el-link>
</template>
</el-popover>
<el-link :underline="false" @click="jump(0)" :disabled="cursor.length === 0 || !crossSite" v-else>全部节点</el-link>
<div v-for="(path, index) in cursor" style="display: inline-block">
&nbsp;>
<el-link :underline="false" :disabled="index === cursor.length - 1" @click="jump(index + 1)">{{path}}</el-link>
</div>
<el-table :data="folders" max-height="72vh" @rowClick="handleClick">
<el-table-column width="50px">
<template #default="scoped">
<hardDisk theme="outline" size="24" fill="#333" v-if="scoped.row.site !== undefined"/>
<el-icon v-else>
<Folder/>
</el-icon>
</template>
</el-table-column>
<el-table-column label="目标文件夹">
<template #default="scoped">
{{scoped.row.name}}
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button type="primary" :disabled="cursor.length === 0" @click="confirm">确定</el-button>
<el-button @click="close">取消</el-button>
</template>
</el-dialog>
</template>
<style>
</style>

View File

@ -0,0 +1,81 @@
<script setup>
import {ref, computed} from 'vue'
import axios from "axios";
import store from "../../store/index.js";
import qs from "qs";
import {ElMessage} from "element-plus";
import Util from "../../Util/index.js";
let props = defineProps(['isImportFile', 'path'])
let emits = defineEmits(['close'])
let isImportFile = computed(() => {
return props.isImportFile
})
let shareCode = ref('')
let shareFile = ref(null)
function queryShareFile(){
if(shareCode.value.trim() === '')
ElMessage.error("请输入分享码")
else if(!shareCode.value.includes(":") || !(shareCode.value.split(":").length === 2))
ElMessage.error("分享码格式错误")
else
axios.get(store.state.config.host + "file/queryShareFile?" + qs.stringify({shareCode:shareCode.value}))
.then((res) => {
if(res.data.result === 'success'){
shareFile.value = res.data.data
ElMessage.success("查询成功")
} else {
ElMessage.error(res.data.data)
}
})
}
function submit(){
axios.post(store.state.config.host + "file/import?" + qs.stringify({shareCode:shareCode.value, path:props.path.substring(2)}))
.then((res) => {
if(res.data.result === 'success'){
ElMessage.success(res.data.data)
close()
} else
ElMessage.error(res.data.data)
})
}
function close(){
emits('close')
shareCode.value = ""
shareFile.value = null
}
</script>
<template>
<el-dialog v-model="isImportFile" @close="close" title="导入文件">
<el-row>
<el-col :span="10">
<el-input v-model="shareCode">
<template #prepend>
分享码
</template>
</el-input>
</el-col>
<el-col :span="10">
<el-button @click="queryShareFile">查询文件</el-button>
</el-col>
</el-row>
<div v-if="shareFile !== null">
<el-row >
<el-col>
文件名称:{{shareFile.name}}<br>
文件大小:{{Util.formatSize(shareFile.size)}}<br>
</el-col>
</el-row>
</div>
<template #footer>
<el-button @click="submit" type="primary" :disabled="shareFile === null">确认导入</el-button>
<el-button @click="close">取消</el-button>
</template>
</el-dialog>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,19 @@
<script setup>
defineProps(['str', 'length'])
</script>
<template>
<el-popover v-if="str.length > length" width="400px">
{{str}}
<template #reference>
{{str.substring(0, length) + "..."}}
</template>
</el-popover>
<span v-else>
{{str}}
</span>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,87 @@
<script setup>
import {computed, ref} from "vue";
import axios from "axios";
import store from "../../store/index.js";
import qs from "qs";
import {ElMessage} from "element-plus";
let props = defineProps(['selectedFiles', 'isSharingFile', 'currentPath'])
let emits = defineEmits(['close'])
let isSharingFile = computed(() => {
return props.isSharingFile
})
let count = ref("")
let expireHour = ref("")
let isAdmin = computed(() => {
return store.state.config.isAdmin
})
function close(){
emits('close')
}
function shareFile(){
if(count.value < 0)
ElMessage.error("分享ip数不能小于0")
else if(expireHour.value < 0)
ElMessage.error("过期时间不能小于0")
else {
let fileNames = []
props.selectedFiles.forEach((file) => {
fileNames.push(file.name)
})
let path
if(isAdmin.value)
path = props.currentPath
else
path = props.currentPath.substring(2)
axios.post(store.state.config.host + "share/?", qs.stringify({
path, time: expireHour.value, count: count.value, fileNames,
}, {indices: false})).then((res) => {
if (res.data.result === 'success')
ElMessage.success(res.data.data)
else
ElMessage.error(res.data.data)
close()
})
}
}
</script>
<template>
<el-dialog title="分享文件" @close="close" v-model="isSharingFile">
请输入分享时长或可下载ip个数(留空为该项不限制都留空则永久分享):
<el-container>
<el-aside>
已选择文件:<br>
<span v-for="file in selectedFiles">
{{file.name}}<br>
</span>
</el-aside>
<el-main>
<el-input v-model="expireHour">
<template #prepend>
分享时长
</template>
<template #append>
小时
</template>
</el-input>
<el-input v-model="count">
<template #prepend>
下载ip个数
</template>
</el-input>
</el-main>
</el-container>
<template #footer>
<el-button type="primary" @click="shareFile">确定</el-button>
<el-button @click="close">取消</el-button>
</template>
</el-dialog>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,143 @@
<script setup>
import {onMounted, ref, watch} from "vue";
import Util from "../../Util/index.js";
import minimizeStr from '../minimize-str/index.vue'
let props = defineProps(['status', 'site'])
let headerStyle = ref({})
let cpuPercentage = ref(0)
let memoryPercentage = ref(0)
let usedMemory = ref(0)
let totalMemory = ref(0)
let loadPercentage = ref(0)
let diskPercentage = ref(0)
let usedSpace = ref(0)
let totalSpace = ref(0)
let bootTime = ref()
let upTime = ref()
let systemLoad = ref([0, 0, 0])
let site = props.site
let lastOnline = Util.formatDate(site.lastOnline)
let ioRead = ref("0B/s")
let ioWrite = ref("0B/s")
let networkReceive = ref("0B/s")
let networkSend = ref("0B/s")
watch(props, () => {
let status = props.status
if(status !== null && status !== undefined) {
cpuPercentage.value = status.usedCpuPercentage
memoryPercentage.value = status.usedMemoryPercentage
usedMemory.value = Util.formatSize(status.usedMemory)
totalMemory.value = Util.formatSize(status.totalMemory)
diskPercentage.value = status.usedSpacePercentage
usedSpace.value = Util.formatSize(status.usedSpace)
totalSpace.value = Util.formatSize(status.totalSpace)
bootTime.value = new Date(status.systemBootTime * 1000).toLocaleString('zh-CN')
upTime.value = Util.formatDuration(status.systemUpTime)
systemLoad.value = status.systemLoad
ioRead.value = Util.formatSize(status.ioRead) + "/s"
ioWrite.value = Util.formatSize(status.ioWrite) + "/s"
networkReceive.value = Util.formatSize(status.networkReceive) + "/s"
networkSend.value = Util.formatSize(status.networkSend) + "/s"
if (systemLoad.value[0] !== -1)
for (let i = 0; i < 3; i++)
systemLoad.value[i] = Number(systemLoad.value[i]).toFixed(2)
}
})
onMounted(() => {
if(site.online)
headerStyle.value['--color'] = 'green'
else
headerStyle.value['--color'] = 'gray'
})
</script>
<template>
<el-card :header-color="site.online?'green':'gray'" class="home-card">
<div class="header">
<span :style="headerStyle">{{`${site.id}:${site.hostname}`}}</span>
</div>
<el-row>
<el-col :span="12" style="text-align: center">
<el-progress type="dashboard" :percentage="cpuPercentage" :color="Util.colors" style="text-align: center">
<template #default>
{{cpuPercentage}}% <br>
CPU
</template>
</el-progress>
<span style="text-align: center; display: block">{{(cpuPercentage * site.cpuCore * 0.01).toFixed(2)}} / {{site.cpuCore}}</span>
</el-col>
<el-col :span="12" style="text-align: center">
<el-progress type="dashboard" :percentage="memoryPercentage" :color="Util.colors">
<template #default>
{{memoryPercentage}}% <br>
内存
</template>
</el-progress>
<span style="text-align: center; display: block">{{usedMemory}}/{{totalMemory}}</span>
</el-col>
</el-row>
<el-row style="margin-top: 10px">
<el-col :span="12" style="text-align: center">
<el-progress type="dashboard" :percentage="loadPercentage" :color="Util.colors">
<template #default>
{{loadPercentage}}% <br>
负载
</template>
</el-progress>
<span style="text-align: center; display: block">{{systemLoad[0]}}/{{systemLoad[1]}}/{{systemLoad[2]}}</span>
</el-col>
<el-col :span="12" style="text-align: center;">
<el-progress type="dashboard" :percentage="diskPercentage" :color="Util.colors">
<template #default>
{{diskPercentage}}% <br>
硬盘
</template>
</el-progress>
<span style="text-align: center; display: block">{{usedSpace}}/{{totalSpace}}</span>
</el-col>
<el-col>
IP地址:{{site.ip}}<br>
系统:<minimize-str :str="site.system" :length="25"/>
<br>
主机名:{{site.hostname}}<br>
处理器架构:{{site.cpuArch}}<br>
处理器名称:<minimize-str :str="site.cpuName" :length="25"/><br>
处理器核心数:{{site.cpuCore}}<br>
处理器线程数:{{site.cpuThread}}<br>
<div v-if="site.online">
运行时间:{{upTime}}<br>
开机时间:{{bootTime}}<br>
磁盘读写:{{ioRead + " " + ioWrite}}<br>
网络收发:{{networkReceive + " " + networkSend}}<br>
</div>
<div v-else>
最后上线时间:{{lastOnline}}<br>
</div>
</el-col>
</el-row>
</el-card>
</template>
<style scoped>
.header > span {
position: relative;
font-size: 16px;
font-weight: 500;
margin-left: 18px;
}
.header > span::before {
position: absolute;
top: 4px;
left: -13px;
width: 4px;
height: 14px;
content: '';
background: var(--color);
border-radius: 10px;
}
</style>

View File

@ -0,0 +1,153 @@
<script setup>
import Util from "../../Util/index.js";
import {computed, ref, watch} from "vue";
import minimizeStr from '../../components/minimize-str/index.vue'
let props = defineProps(['isViewDownloadRecord', 'shareFiles', 'viewType', 'currentFile', 'currentSite'])
let emits = defineEmits(['close'])
let isViewDownloadRecord = computed(() => {
return props.isViewDownloadRecord
})
let computedDownloadRecord = computed(() => {
let downloadRecord = []
if(props.viewType === 'single')
downloadRecord.push(...props.currentFile.downloadRecords)
else
props.shareFiles.forEach((file) => {
if(file.downloadRecords !== undefined)
file.downloadRecords.forEach((record) => {
downloadRecord.push({name:file.name, path:file.filePath, username:file.username, ...record})
})
})
return downloadRecord
})
let filterResult = ref([])
let filterCondition = [
{label:'筛选时间', value:'time'},
{label:'筛选ip', value:'ip'},
{label:'筛选ua', value:'ua'},
]
let filterParam = ref('')
let filterBy = ref('ip')
watch(props.viewType, () => {
if(props.viewType === 'all') {
if (filterCondition.length === 3)
filterCondition.push({label: '筛选分享人', value: 'sharer'})
} else if(filterCondition.length === 4)
filterCondition.pop()
})
watch(filterParam, () => {
filterResult.value.splice(0)
switch (filterBy.value){
case 'time':
if(filterParam.value[0] !== undefined)
computedDownloadRecord.value.forEach((record) => {
if(filterParam.value[0].getTime() <= record.time && filterParam.value[1].getTime() >= record.time)
filterResult.value.push(record)
})
break
case 'ip':
if(filterParam.value.trim() !== ''){
computedDownloadRecord.value.forEach((record) => {
if(record.ip.includes(filterParam.value))
filterResult.value.push(record)
})
}
break
case 'ua':
if(filterParam.value.trim() !== ''){
computedDownloadRecord.value.forEach((record) => {
if(record.ua.includes(filterParam.value))
filterResult.value.push(record)
})
}
break
case 'sharer':
if(filterParam.value.trim() !== ''){
computedDownloadRecord.value.forEach((record) => {
if(record.username.includes(filterParam.value))
filterResult.value.push(record)
})
}
}
})
function handleSelectChange(){
filterParam.value = ""
}
function close(){
filterBy.value = 'ip'
filterParam.value = ""
emits('close')
}
</script>
<template>
<el-dialog title="查看下载记录" v-model="isViewDownloadRecord" :width="viewType==='single'?null:'1250px'" @close="close" top="0">
<el-row>
<el-col :span="10" v-if="viewType === 'single'">
当前文件:{{currentFile.name}}
</el-col>
<el-col :span="10" v-if="viewType === 'all'">
当前节点:{{currentSite.hostname}}
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-select placeholder="筛选" v-model="filterBy" @change="handleSelectChange">
<el-option v-for="condition in filterCondition" :label="condition.label" :value="condition.value"/>
</el-select>
<div v-show="filterBy === 'time'" style="display: inline">
<el-date-picker type="datetimerange" v-model="filterParam"/>
</div>
<el-input style="width: 300px" v-show="filterBy !== 'time'" v-model="filterParam">
</el-input>
</el-col>
</el-row>
<el-table :data="filterParam.length>1?filterResult:computedDownloadRecord" empty-text="没有记录">
<el-table-column label="文件" v-if="viewType === 'all'" width="400px">
<template #default="scoped">
<minimize-str :str="scoped.row.path" length="75"/>
</template>
</el-table-column>
<el-table-column label="分享人" v-if="viewType === 'all'" width="100px">
<template #default="scoped">
<minimize-str :str="scoped.row.username" length="10"/>
</template>
</el-table-column>
<el-table-column label="下载时间" width="200px">
<template #default="scoped">
{{Util.formatDate(scoped.row.time)}}
</template>
</el-table-column>
<el-table-column label="下载ip" width="200px">
<template #default="scoped">
{{scoped.row.ip}}
</template>
</el-table-column>
<el-table-column label="下载ua" width="300px">
<template #default="scoped">
<el-popover width="300px">
<template #reference>
{{scoped.row.ua.length>75?scoped.row.ua.substring(0, 75) + '......':scoped.row.ua}}
</template>
<template #default>
{{scoped.row.ua}}
</template>
</el-popover>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<style scoped>
</style>

13
src/main.js Normal file
View File

@ -0,0 +1,13 @@
import { createApp } from 'vue'
import App from './App.vue'
import element from 'element-plus'
import 'element-plus/dist/index.css'
import {router} from "./router/index.js";
import '@icon-park/vue-next/styles/index.css';
let app = createApp(App)
app.use(router)
app.use(element)
app.mount('#app')

83
src/page/index.vue Normal file
View File

@ -0,0 +1,83 @@
<script setup>
import SideBar from "../views/sideBar.vue";
import store from '../store/index.js'
import {ref, watch} from "vue";
import {ElMessage} from "element-plus";
import Util from "../Util/index.js";
let isPair = ref(false)
let pairMessage = ref({})
let size = ref(0)
let sizeUnit = ref('GB')
watch(store.getters.getPairMessage, () =>{
pairMessage.value = store.getters.getPairMessage
isPair.value = true
})
function processPairRequest(result){
let totalSpace
if(result){
if(isNaN(size.value) || size.value.trim() === ''){
ElMessage.error("请输入正确的格式")
return
}
totalSpace = size.value * Util[sizeUnit.value]
if(totalSpace <= 0 || totalSpace > pairMessage.value.availableSpace){
ElMessage.error("容量输入错误,不能为0或超出服务器可用容量")
return
}
}
store.dispatch("processPairRequest", {result, totalSpace:totalSpace})
isPair.value = false
}
function logout(){
localStorage.removeItem("passcode")
delete store.state.config.passcode
store.state.websocket.close()
ElMessage({type: 'success', message: '退出登录成功'})
}
</script>
<template>
<el-container>
<side-bar style="width: 12vw;"/>
<el-main style="height: 95vh; width: 88vw; padding-top: 0; overflow: hidden">
<el-scrollbar max-height="95vh">
<router-view/>
</el-scrollbar>
</el-main>
</el-container>
<el-dialog v-model="isPair" title="配对请求">
ip地址:{{pairMessage.ip}}<br>
主机名:{{pairMessage.hostname}}<br>
系统版本:{{pairMessage.system}}<br>
cpu架构:{{pairMessage.cpuArch}}<br>
cpu核心数:{{pairMessage.cpuCore}}<br>
cpu线程数:{{pairMessage.cpuThread}}<br>
反代前缀:{{pairMessage.reverseProxyPrefix === null ? '无': pairMessage.reverseProxyPrefix}}<br>
域名:{{pairMessage.domain === null? '无': pairMessage.domain}}<br>
可用空间:{{Util.formatSize(pairMessage.availableSpace)}}<br>
<el-input style="width: 250px" v-model="size">
<template #prepend>
分配容量:
</template>
<template #append>
<el-select placeholder="单位" v-model="sizeUnit">
<el-option value="KB"/>
<el-option value="MB"/>
<el-option value="GB"/>
</el-select>
</template>
</el-input>
<template #footer>
<el-button @click="processPairRequest(true)" type="primary">允许</el-button>
<el-button @click="processPairRequest(false)">拒绝</el-button>
</template>
</el-dialog>
</template>
<style scoped>
</style>

99
src/page/login.vue Normal file
View File

@ -0,0 +1,99 @@
<script setup>
import {onMounted, ref} from "vue";
import axios from "axios";
import {ElMessage} from "element-plus";
import store from "../store/index.js";
import {router} from "../router/index.js";
let username = ref()
let passcode = ref()
let rememberPasscode = ref(false)
function login(){
if(passcode.value.trim() === '' || username.value.trim() === ''){
ElMessage.error('请输入用户名或密码')
return
}
axios.post(store.state.config.host + `login?username=${username.value}&passcode=${passcode.value}`).then((res) => {
if(res.data.result === 'success') {
store.state.config.username = username.value
store.state.config.passcode = passcode.value
store.state.config.isAdmin = res.data.isAdmin === 'true'
store.state.config.sessionId = res.data.sessionId
store.dispatch("loadSites")
store.dispatch("initWebsocket")
store.dispatch("loadUsers")
if(rememberPasscode.value) {
localStorage.setItem("username", username.value)
localStorage.setItem("passcode", passcode.value)
}
let lastTime = localStorage.getItem("lastTime")
ElMessage.success('登陆成功')
if(lastTime !== null && lastTime.includes("?"))
router.push(lastTime)
else
router.push('/index/status/?type=all')
} else {
ElMessage.error(res.data.data)
}
})
}
onMounted(() => {
if(localStorage.getItem("logout") !== null){
localStorage.removeItem("logout")
} else if((localStorage.getItem("passcode")) !== null){
username.value = localStorage.getItem("username")
passcode.value = localStorage.getItem("passcode")
login()
}
})
</script>
<template>
<div class="container">
登录网盘<br>
<el-form>
<el-input v-model="username">
<template #prepend>
用户名
</template>
</el-input>
<el-input v-model="passcode" type="password">
<template #prepend>
密码
</template>
</el-input>
</el-form>
<div>
<el-checkbox v-model="rememberPasscode" style="float: left">是否记住密码</el-checkbox>
<el-button @click="login(passcode)" style="text-align: right">登录</el-button>
</div>
</div>
</template>
<style>
.container {
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
padding: 20px;
width: 300px;
text-align: center;
}
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style>

70
src/router/index.js Normal file
View File

@ -0,0 +1,70 @@
import status from '../views/status/index.vue'
import fileManage from '../views/file-manage/index.vue'
import taskManage from '../views/task-manage/index.vue'
import siteManage from '../views/site-manage/index.vue'
import shareManage from '../views/share-manage/index.vue'
import userManage from '../views/user-manage/index.vue'
import { createRouter,createWebHashHistory} from "vue-router";
import login from "../page/login.vue";
import index from "../page/index.vue";
import {watch} from "vue";
import store from "../store/index.js";
const routes = [
{
path: '/login',
component: login
},
{
path: '/index',
component: index,
children: [
{
path: 'status',
component : status
},
{
path: 'fileManage',
component : fileManage
},
{
path: 'taskManage',
component : taskManage
},
{
path: "siteManage",
component: siteManage
},
{
path: 'shareManage',
component: shareManage
},
{
path: "userManage",
component: userManage
}
]
},
]
export const router = createRouter({
history: createWebHashHistory(),
routes
})
watch(() => store.state.isWsOnline, () => {
if(!store.state.isWsOnline)
router.push("/login")
})
router.beforeEach((to, from) => {
if(to.path !== '/login') {
if ('passcode' in store.state.config) {
localStorage.setItem("lastTime", to.fullPath)
return true
} else
return {path: '/login'}
}
})

270
src/store/index.js Normal file
View File

@ -0,0 +1,270 @@
import vuex from 'vuex'
import axios from "axios";
import qs from "qs";
import {ElMessage} from "element-plus";
axios.defaults.withCredentials = true
const actions = {
initWebsocket(context){
state.websocket = new WebSocket("wss://sns.lionwebsite.xyz/ws/")
state.websocket.onopen = () => {
state.websocket.send(`{\"type\": \"init\", \"username\":\"${state.config.username}\", \"passcode\": \"${state.config.passcode}\"}`)
context.commit("updateWsStatus", true)
}
state.websocket.onmessage = (event) => {
let message = JSON.parse(event.data)
if(message.type !== undefined) {
switch (message.type){
case 'status':
delete message.type
context.commit("updateStatues", message)
break
case 'pair':
context.commit("updatePairMessage", message.data)
break
case 'task':
context.commit("updateTasks", message.tasks)
break
case 'statusAlter':
if(message.status === 'online')
ElMessage.success(`服务器${message.hostname}上线`)
else
ElMessage.error(`服务器${message.hostname}下线`)
context.dispatch('loadSites')
break
}
}
}
state.websocket.onclose = (event) => {
context.commit("updateWsStatus", false)
context.commit("logout")
ElMessage.error("断开连接")
}
},
loadSites(context){
axios.get(state.config.host + "getSites").then((res) => {
if(res.data.result === 'success') {
context.commit("updateSites", res.data.data)
}
})
},
processPairRequest(context, data){
state.websocket.send(JSON.stringify({type:'pair', messageId:state.pairMessage.messageId, ...data}))
context.commit("updatePairMessage", null)
if(data.result){
setTimeout(() => {
context.dispatch("loadSites")
}, 1000)
}
},
loadFiles(context, path){
axios.get(state.config.host + "file/get?" + qs.stringify({path})).then((res) =>{
if(res.data.result === 'success'){
context.commit("updateFiles", {files:res.data.data, isRoot:(path.endsWith(":") || path.trim() === '')})
}else{
ElMessage({type:'error', message:'文件加载失败:' + res.data.data})
}
})
},
loadUsers(context){
if(context.state.config.isAdmin)
axios.get(state.config.host + "manage/user/").then((res) => {
if(res.data.result === "success")
context.commit("updateUsers", res.data.data)
else
context.commit("updateUsers", [])
})
else
axios.get(state.config.host + "getSelf").then((res) => {
if(res.data.result === "success")
context.commit("updateUsers", [res.data.data])
else
context.commit("updateUsers", [])
})
}
}
const mutations = {
updateStatues(state, statues){
for(let id in statues) {
let status = statues[id]
state.statues[status.id] = status
}
},
updateSites(state, sites){
state.sites.splice(0)
state.sites.push(...sites)
},
updatePairMessage(state, pairMessage){
if(pairMessage === null){
state.pairMessage = {}
}else{
state.pairMessage[pairMessage.messageId] = pairMessage
state.pairMessage = pairMessage
}
},
updateWsStatus(state, isOnline){
state.isWsOnline = isOnline
},
updateFiles(state, data){
state.files.splice(0)
let {files, isRoot} = data
let date = new Date()
if(files.length === 0) {
if (!isRoot)
state.files.push({name: '...', type: 'FOLDER', size: 0, lastModify: date.getTime()})
return
}
if(!isRoot)
state.files.push({name:'...', type:'FOLDER', size:0, lastModify:date.getTime()})
state.files.push(...files)
state.files.sort((pre, next) => {
if(pre.type === next.type)
return 0
else if(pre.type === "FOLDER")
return -1
else
return 1
})
},
removeTasks(state, taskIds){
let tasks = state.tasks
state.tasks.splice(0)
tasks.forEach((task) => {
let isRemove
//去除命中的id
taskIds = taskIds.filter((id) => {
if(task.taskId === id) {
isRemove = true
return false
}
return true
})
if(!isRemove)
state.tasks.push(task)
})
state.deleteTaskIds.push(...taskIds)
},
updateTasks(state, tasks){
state.tasks.splice(0)
for(let id in tasks){
let task = tasks[id]
//过滤待删除的id
if(state.deleteTaskIds.length !== 0){
let skip = false
state.deleteTaskIds.forEach((id) => {
skip = task.id === id
})
//只用一次
state.deleteTaskIds.splice(0)
if(skip)
continue;
}
switch (task.type){
case "compress":
task.type = '打包'
break
case 'extract':
task.type = '解包'
break
case 'transfer':
if(state.config.isAdmin)
task.type = '转移'
else task.type = '导入'
break
}
switch (task.status){
case "waiting":
task.status = '等待中'
break
case "proceeding":
task.status = '进行中'
break
case "success":
task.status = '已完成'
break
case "failure":
task.status = '任务失败'
break
}
task.percentage = Number(Number(task.percentage).toFixed(2))
state.tasks.push(tasks[id])
}
},
updatePasscode(state, passcode){
state.config.passcode = passcode
localStorage.setItem('passcode', passcode)
},
updateUsers(state, users){
state.users.splice(0)
state.users.push(...users)
state.users.push({username:"新建用户", id:-1})
},
logout(state){
state.sites.splice(0)
state.files.splice(0)
state.users.splice(0)
state.tasks.splice(0)
localStorage.setItem("logout", "logout")
}
}
const state = {
statues: {},
websocket: {},
sites:[],
files:[],
users:[],
config:{'host': "https://sns.lionwebsite.xyz/"},
pairMessage:{},
tasks:[],
deleteTaskIds:[], //用于清除的任务id有时候取消任务时可能任务取消了但是又发送了一份旧的任务导致这份旧的任务一直显示在前台
isWsOnline:false
}
const getters = {
getStatues(state){
return state.statues
},
getSites(state){
return state.sites
},
getOnlineSites(state){
let sites = []
state.sites.forEach((site) => {
if(site.online)
sites.push(site)
})
return sites
},
getOfflineSites(state){
let sites = []
state.sites.forEach((site) => {
if(!site.online)
sites.push(site)
})
return sites
},
getTasks(state){
return state.tasks
},
getPairMessage(state){
return state.pairMessage
},
getFiles(state){
return state.files
}
}
export default new vuex.Store({
actions,
mutations,
state,
getters
})

View File

@ -0,0 +1,547 @@
<script setup>
import {computed, onMounted, ref, watch} from "vue";
import store from "../../store/index.js";
import {useRouter} from "vue-router";
import {Folder, Share, UploadFilled, Files as FilesIcon} from "@element-plus/icons-vue";
import Util from "../../Util";
import {ElMessage, ElMessageBox} from "element-plus";
import axios from "axios";
import qs from "qs";
import fileOperation from '../../components/file-operation/index.vue'
import shareFile from '../../components/share-file/index.vue'
import adjustShare from '../../components/adjust-share/index.vue'
import importFile from '../../components/import-file/index.vue'
let router = useRouter()
let onlineSites = computed(() => {
return store.getters.getOnlineSites
})
let files = computed(() => {
return store.getters.getFiles
})
let users = computed(() => {
return store.state.users
})
let currentPath = computed(() => {
let currentPath = currentSiteId.value + ":"
cursor.value.forEach((path) => {
currentPath += path + "/"
})
return currentPath
})
let fileOperate = computed(() => {
if(isMoveFile.value)
return "move"
else if(isCopyFile.value)
return "copy"
else
return "extract"
})
let isMoveFile = ref(false)
let isCopyFile = ref(false)
let isExtractFile = ref(false)
let isShareFile = ref(false)
let isAdjustShare = ref(false)
let isImportFile = ref(false)
let isAdmin = computed(() => {
return store.state.config.isAdmin
})
let currentSiteId = ref(0)
let cursor = ref([])
let currentFile = ref()
let selectNone = ref(true)
let selectMultiFolder = ref(false)
let selectMultiFile = ref(false)
let selectSingleFile = ref(false)
let selectSingleFolder = ref(false)
let selectShared = ref(false)
let selectTarFile = ref(false)
let selectedFiles = []
let isUpload = ref(false)
function loadFiles(path){
store.dispatch('loadFiles', path)
}
function uploadFile(){
isUpload.value = true
}
function onUploadSuccess(res){
if(res.result === "success") {
ElMessage.success("文件上传成功")
if(!isAdmin.value)
axios.post(store.state.config.host + "verifySpace").then(() => {
store.dispatch('loadUsers')
})
} else
ElMessage.error(res.data)
}
function loadFilesByCursor(){
let path = ""
if(isAdmin.value)
path = currentSiteId.value + ":"
if(cursor.value.length !== 0)
cursor.value.forEach((str) => {
if(!path.endsWith(":"))
path += '/' + str
else
path += str
})
loadFiles(path)
}
function getSiteBySiteId(id){
let getSite
onlineSites.value.forEach((site) => {
if(site.id === id)
getSite = site
})
return getSite
}
let debounce = 0
function handleClick(file){
if(file.type === 'FOLDER'){
clearTimeout(debounce)
debounce = setTimeout(() => {
if(file.name === '...')
cursor.value.pop()
else
cursor.value.push(file.name)
loadFilesByCursor()
refreshParamsPath()
}, 300)
}
}
function refreshParamsPath(){
let query = JSON.parse(JSON.stringify(router.currentRoute.value.query))
query.path = currentPath.value
router.push({path: router.currentRoute.value.path, query})
}
function deleteFiles() {
let files = ''
selectedFiles.forEach((file) => {
files += " " + file.name
})
ElMessageBox.confirm(
'以下文件将被删除: ' + files + " 是否确认删除?",
'删除文件',
{confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning'}
).then(() => {
deleteFilesConfirm()
}).catch(() => {})
}
function deleteFilesConfirm(){
let paths = []
selectedFiles.forEach((file) => {
paths.push(file.path)
})
axios.post(store.state.config.host + "file/delete", qs.stringify({sourceId:currentSiteId.value, paths},{indices:false}))
.then((res) => {
if(res.data.result === 'success'){
ElMessage({type:'success', message:res.data.data})
loadFilesByCursor()
refresh()
}else{
ElMessage({type: 'error', message:res.data.data})
}
})
}
function compressFiles(){
let files = ''
selectedFiles.forEach((file) => {
files += " " + file.name
})
ElMessageBox.prompt(
`以下文件即将被打包:${files},请输入压缩包名称`,
'打包文件',
{
confirmButtonText: '打包',
cancelButtonText: '取消',
type: 'info',
inputValue: '.tar'
}
).then((packageName) => {
if(packageName.value.trim() === ''){
ElMessage.error("压缩包名不能为空")
return
}
let paths = []
selectedFiles.forEach((file) => {
paths.push(file.path)
})
let targetPath = ""
if(isAdmin.value)
targetPath = currentSiteId.value + ":"
cursor.value.push(packageName.value)
cursor.value.forEach((path) => {
if(targetPath.endsWith(":") || targetPath === "")
targetPath += path
else
targetPath += "/" + path
})
cursor.value.pop()
axios.post(store.state.config.host + "file/compress", qs.stringify({
paths:paths, targetPath:targetPath}, {indices: false})).then((res) => {
if(res.data.result === 'success'){
ElMessage({type: 'success', message:'打包任务提交成功'})
}else
ElMessage({type: 'error', message: '打包任务提交失败:' + res.data.data})
})
}).catch(() => {})
}
function createFolder(){
ElMessageBox.prompt("文件夹名称", '新建文件夹', {
confirmButtonText: '确认',
cancelButtonText: '取消',
inputValue:''
}).then((newFolder) => {
if(newFolder.value.trim() === ''){
ElMessage.error("新文件夹名不能为空")
return
}
let path = ""
if(isAdmin.value)
path = currentSiteId.value + ":"
cursor.value.forEach((str) => {
path += '/' + str
})
path += '/' + newFolder.value
axios.post(store.state.config.host + "file/create?" +
qs.stringify({path:path}))
.then((res) => {
if(res.data.result === 'success') {
ElMessage({type: 'success', message: '文件夹创建成功'})
loadFilesByCursor()
}else {
ElMessage({type: 'error', message: '文件夹创建失败:' + res.data.data})
}
})
})
}
function rename(){
ElMessageBox.prompt(
`即将重命名:${selectedFiles[0].name},请输入新文件名`,
'文件重命名',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
inputValue:selectedFiles[0].name,
}
).then((name) => {
if(name.value.trim() === ''){
ElMessage.error("新文件名不能为空")
return
}
let path = selectedFiles[0].path
if(isAdmin.value)
path = currentSiteId.value + ":" + path
axios.post(store.state.config.host + "file/rename", qs.stringify({
path:path, name:name.value}, {indices: false})).then((res) => {
if(res.data.result === 'success'){
ElMessage({type: 'success', message:'重命名成功'})
loadFilesByCursor()
}else
ElMessage({type: 'error', message: '重命名失败:' + res.data.data})
})
}).catch(() => {})
}
function jump(index){
cursor.value.splice(index)
loadFilesByCursor()
refreshParamsPath()
}
function switchSite(id){
currentSiteId.value = id
cursor.value.splice(0)
loadFilesByCursor()
refreshParamsPath()
}
function selectChange(files){
selectedFiles = files
selectShared.value = false
selectNone.value = true
selectSingleFile.value = false
selectSingleFolder.value = false
selectMultiFile.value = false
selectMultiFolder.value = false
selectTarFile.value = false
let selectedFile = 0
let selectedFolder = 0
files.forEach((file) => {
if(file.type === 'FOLDER')
selectedFolder++
else {
if (file.shareCode !== undefined)
selectShared.value = true
if(!selectTarFile.value && file.name.endsWith(".tar"))
selectTarFile.value = true
selectedFile++
}
})
selectNone.value = selectedFile + selectedFolder === 0;
if(selectedFile === 1)
selectSingleFile.value = true
else if(selectedFile > 1)
selectMultiFile.value = true
if(selectedFolder === 1)
selectSingleFolder.value = true
else if(selectedFolder > 1)
selectMultiFolder.value = true
}
function extractFiles(){
isExtractFile.value = true
}
function moveFile(){
isMoveFile.value = true
}
function copyFile() {
isCopyFile.value = true
}
function closeFileOperation(){
isCopyFile.value = false
isMoveFile.value = false
isExtractFile.value = false
setTimeout(() => {
loadFilesByCursor()
}, 500)
}
function closeShareFile(){
isShareFile.value = false
setTimeout(() => {
loadFilesByCursor()
}, 500)
}
function closeAdjustShare(){
isAdjustShare.value = false
setTimeout(() => {
loadFilesByCursor()
}, 500)
}
function cancelShare(shareCode){
if(isAdmin.value)
shareCode = currentSiteId.value + ":" + shareCode
axios.post(store.state.config.host + "share/cancel?", qs.stringify({shareCode}))
.then((res) => {
if(res.data.result === 'success')
ElMessage.success(res.data.data)
else
ElMessage.error(res.data.data)
setTimeout(() => {
loadFilesByCursor()
}, 500)
})
}
function adjustShareFile(file) {
isAdjustShare.value = true
currentFile.value = file
}
function downloadFile(file){
if(file.type === 'FILE')
ElMessageBox.confirm(`是否下载文件:${file.name}?`, '下载文件',
{confirmButtonText:'确认', cancelButtonText:'取消'}).then(() => {
window.open(getSiteBySiteId(currentSiteId.value).host + `file/getFile/${file.name}?` + qs.stringify({path:currentPath.value.substring(currentPath.value.indexOf(":") + 1) + file.name, sessionId: store.state.config.sessionId}))
}).catch(() => {})
}
onMounted(() => {
if(router.currentRoute.value.query.path === undefined){
loadDefaultFiles()
}else {
let path = router.currentRoute.value.query.path
if(!path.includes(":")){
ElMessage.error("路径错误")
loadDefaultFiles()
return
}
initFileCursor(path)
loadFilesByCursor()
}
})
function initFileCursor(path){
cursor.value.splice(0)
let temp = path.split(":")
currentSiteId.value = Number(temp[0])
path = temp[1]
if(path === undefined){
loadFilesByCursor()
return
}
if(path.includes("/")) {
path.split("/").forEach((t) => {
if(t.trim() !== '')
cursor.value.push(t)
})
}else if(path.trim() !== '')
cursor.value.push(path)
}
function loadDefaultFiles() {
router.push(`/index/fileManage/?path=${onlineSites.value[0].id}:`)
currentSiteId.value = onlineSites.value[0].id
loadFilesByCursor()
}
function refresh(){
if(isAdmin.value)
store.dispatch('loadSites')
else {
axios.post(store.state.config.host + "verifySpace").then(() => {
store.dispatch('loadUsers')
})
}
loadFilesByCursor()
}
//
watch(router.currentRoute, (now, old) =>{
if(now.path !== old.path && now.path === '/index/fileManage') {
if (now.query.path === undefined) {
router.replace(`/index/fileManage/?path=${onlineSites.value[0].id}:`)
initFileCursor(`${onlineSites.value[0].id}:`)
} else
initFileCursor(now.query.path)
loadFilesByCursor()
}
})
</script>
<template>
<div>
<el-menu mode="horizontal" :default-active="currentSiteId + ''">
<el-menu-item v-for="site in onlineSites" :index="site.id" @click="switchSite(site.id)">
{{site.hostname}}
</el-menu-item>
</el-menu>
<div>
<el-button-group>
<el-button type="primary" @click="uploadFile">上传文件</el-button>
<el-button type="primary" @click="isImportFile = true" v-if="!isAdmin">导入文件</el-button>
<el-button @click="createFolder">新建文件夹</el-button>
<el-button :disabled="selectNone || selectSingleFile" @click="compressFiles">打包</el-button>
<el-button :disabled="!(selectSingleFile && selectTarFile)" @click="extractFiles">解包</el-button>
<el-button :disabled="selectNone" @click="moveFile">移动到</el-button>
<el-button :disabled="selectNone" @click="copyFile">复制到</el-button>
<el-button :disabled="!(selectSingleFolder || selectSingleFile)" @click="rename">重命名</el-button>
<el-button :disabled="selectNone" @click="deleteFiles">删除</el-button>
<el-button :disabled="selectNone || selectMultiFolder || selectSingleFolder || selectShared" @click="isShareFile = true">创建分享链接</el-button>
<el-button @click="refresh">刷新</el-button>
</el-button-group>
<el-progress :percentage="((getSiteBySiteId(currentSiteId).totalSpace - getSiteBySiteId(currentSiteId).availableSpace) / getSiteBySiteId(currentSiteId).totalSpace) * 100"
style="width: 150px; display: inline-block; float: right" v-if="isAdmin && getSiteBySiteId(currentSiteId) !== undefined" :color="Util.colors"
>
<template #default>
{{Util.formatSize(getSiteBySiteId(currentSiteId).totalSpace - getSiteBySiteId(currentSiteId).availableSpace)}} / {{Util.formatSize(getSiteBySiteId(currentSiteId).totalSpace)}}
</template>
</el-progress>
<el-progress :percentage=" ((users[0].totalSpace - users[0].availableSpace) / users[0].totalSpace) * 100"
style="width: 150px; display: inline-block; float: right" v-if="!isAdmin && users[0] !== undefined"
color="Util.colors">
<template #default>
{{Util.formatSize(users[0].totalSpace - users[0].availableSpace) }} / {{ Util.formatSize(users[0].totalSpace)}}
</template>
</el-progress>
<br>
<el-link :underline="false" @click="jump(0)" :disabled="cursor.length === 0">全部文件</el-link>
<div v-for="(path, index) in cursor" style="display: inline-block">
&nbsp;>
<el-link :underline="false" :disabled="index === cursor.length - 1" @click="jump(index + 1)">{{path}}</el-link>
</div>
</div>
<div>
<el-table :data="files" size="large" @rowClick="handleClick" @selectionChange="selectChange"
max-height="76vh" @rowDblclick="downloadFile" empty-text="根目录下没有文件">
<el-table-column type="selection" width="55px"></el-table-column>
<el-table-column width="50px">
<template #default="scoped">
<el-icon v-show="scoped.row.type === 'FOLDER'">
<Folder />
</el-icon>
<el-icon v-show="scoped.row.type === 'FILE'">
<FilesIcon/>
</el-icon>
</template>
</el-table-column>
<el-table-column width="50px">
<template #default="scoped">
<el-popover v-if="scoped.row.shareCode !== undefined" width="250px">
<template #reference>
<el-icon >
<Share />
</el-icon>
</template>
<template #default>
分享码:{{currentSiteId + ":" +scoped.row.shareCode}}<br>
过期时间:{{scoped.row.expireTime === undefined?'不过期':Util.formatDate(scoped.row.expireTime)}}<br>
剩余ip数:{{scoped.row.totalCount === undefined?'不限制':scoped.row.availableCount ?? 0}}<br>
下载链接:<a :href="getSiteBySiteId(currentSiteId).host + `getFileByShareCode/${scoped.row.name}?shareCode=${scoped.row.shareCode}`">链接</a><br>
<el-button type="danger" @click="cancelShare(scoped.row.shareCode)">取消分享</el-button>
<el-button @click="adjustShareFile(scoped.row)">调整分享</el-button>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="文件名" width="500px">
<template #default="scoped">
{{scoped.row.name}}
</template>
</el-table-column>
<el-table-column label="大小" width="100px">
<template #default="scoped">
<span v-if="scoped.row.type === 'FILE'">
{{Util.formatSize(scoped.row.size)}}
</span>
</template>
</el-table-column>
<el-table-column label="修改时间">
<template #default="scoped">
{{Util.formatDate(scoped.row.lastModify)}}
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-dialog title="上传文件" v-model="isUpload" @close="loadFilesByCursor">
<el-upload drag :action="getSiteBySiteId(currentSiteId).host + `file/upload/${currentPath.substring(currentPath.indexOf(':') + 1)}?sessionId=${store.state.config.sessionId}`" multiple
:on-success="onUploadSuccess">
<el-icon class="el-icon--upload"><upload-filled/></el-icon>
<div class="el-upload__text">
拖拽文件到这里或 <em>点击上传</em> 上传至 {{getSiteBySiteId(currentSiteId).hostname}}
<span v-for="(path) in cursor" > > {{path}}</span>
</div>
<template #tip>
<div class="el-upload__tip">
上传限制大小:10Gb
</div>
</template>
</el-upload>
</el-dialog>
<file-operation :selected-files="selectedFiles" :is-operating-file="isCopyFile || isMoveFile || isExtractFile"
:cross-site="!(selectSingleFolder || selectMultiFolder || isExtractFile)" @close="closeFileOperation" :current-side-id="currentSiteId"
:operation="fileOperate" :current-path="currentPath"/>
<share-file :is-sharing-file="isShareFile" :selected-files="selectedFiles" :current-path="currentPath" @close="closeShareFile"/>
<adjust-share :current-file="currentFile" :is-adjusting-share="isAdjustShare" :current-site-id="currentSiteId" @close="closeAdjustShare"/>
<import-file :is-import-file="isImportFile" :path="currentPath" @close="() => {isImportFile = false; refresh}"/>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,163 @@
<script setup>
import {computed, onMounted, ref, watch} from "vue";
import store from "../../store/index.js";
import {useRouter} from "vue-router";
import {ElMessage} from "element-plus";
import axios from "axios";
import qs from "qs";
import Util from "../../Util/index.js";
import AdjustShare from '../../components/adjust-share/index.vue'
import ViewDownloadRecord from '../../components/view-download-record/index.vue'
import MinimizeStr from '../../components/minimize-str/index.vue'
let router = useRouter()
let onlineSites = computed(() => {
return store.getters.getOnlineSites
})
let currentSiteId = ref()
let shareFiles = ref([])
let isAdjustShare = ref(false)
let isViewDownloadRecord = ref(false)
let viewType = ref('') //all single
let currentFile = ref()
function getSiteBySiteId(id){
let getSite
onlineSites.value.forEach((site) => {
if(site.id === id)
getSite = site
})
return getSite
}
function switchSite(id){
currentSiteId.value = id
router.replace("/index/shareManage/?id=" + currentSiteId.value)
loadShareFiles()
}
function viewSingleDownloadRecord(file) {
currentFile.value = file
isViewDownloadRecord.value = true
viewType.value = 'single'
}
function viewAllDownloadRecord(){
isViewDownloadRecord.value = true
viewType.value = 'all'
}
function loadShareFiles(){
axios.post(store.state.config.host + "share/get?", qs.stringify({sourceId:currentSiteId.value})).
then((res) => {
shareFiles.value.splice(0)
if(res.data.result === 'success') {
res.data.data.forEach((shareFile) => {
shareFile.name = shareFile.filePath.substring(shareFile.filePath.lastIndexOf("/")+1)
shareFiles.value.push(shareFile)
})
}
})
}
function download(file) {
window.open(getSiteBySiteId(currentSiteId.value).host + `file/getFile/${file.name}?` + qs.stringify({path:file.filePath.substring(1)}))
}
function copyShareLink(file){
let link = getSiteBySiteId(currentSiteId.value).host + `getFileByShareCode/${file.name}?shareCode=${file.shareCode}`
navigator.clipboard.writeText(link)
ElMessage.success("复制成功")
}
function adjustShareFile(file){
if(file.filePath.includes("\\"))
file.name = file.filePath.substring(file.filePath.lastIndexOf("\\")+1)
else
file.name = file.filePath.substring(file.filePath.lastIndexOf("/")+1)
currentFile.value = file
isAdjustShare.value = true
}
function closeAdjustShare(){
isAdjustShare.value = false
loadShareFiles()
}
function cancelShare(shareCode){
axios.post(store.state.config.host + "share/cancel?", qs.stringify({shareCode:currentSiteId.value + ":" + shareCode}))
.then((res) => {
if(res.data.result === 'success')
ElMessage.success(res.data.data)
else
ElMessage.error(res.data.data)
loadShareFiles()
})
}
watch(router.currentRoute, (old, now)=>{
if(old.path !== now.path && now.path === '/index/shareManage/')
if(now.query.id === undefined) {
router.replace(`/index/shareManage/?id=${onlineSites.value[0].id}`)
currentSiteId.value = onlineSites.value[0].id
loadShareFiles()
}
})
onMounted(() => {
if(router.currentRoute.value.query.id === undefined) {
router.replace(`/index/shareManage/?id=${onlineSites.value[0].id}`)
currentSiteId.value = onlineSites.value[0].id
}else
currentSiteId.value = Number(router.currentRoute.value.query.id)
loadShareFiles()
})
</script>
<template>
<el-menu mode="horizontal" :default-active="currentSiteId">
<el-menu-item v-for="site in onlineSites" :index="site.id" @click="switchSite(site.id)" >
{{site.hostname}}
</el-menu-item>
</el-menu>
<el-button @click="viewAllDownloadRecord" v-if="store.state.config.isAdmin">查看当前节点下载记录</el-button>
<el-table :data="shareFiles" empty-text="此节点未分享文件">
<el-table-column label="路径" width="350px">
<template #default="scoped">
<minimize-str :str="scoped.row.filePath" :length="50"/>
</template>
</el-table-column>
<el-table-column label="分享人" v-if="store.state.config.isAdmin" width="100px">
<template #default="scoped">
<minimize-str :str="scoped.row.username" :length="10"/>
</template>
</el-table-column>
<el-table-column label="分享码" width="90px">
<template #default="scoped">
{{scoped.row.shareCode}}
</template>
</el-table-column>
<el-table-column label="过期时间" width="150px">
<template #default="scoped">
{{scoped.row.expireTime === undefined?"不过期":Util.formatDate(scoped.row.expireTime)}}
</template>
</el-table-column>
<el-table-column label="剩余ip数" width="100px">
<template #default="scoped">
{{scoped.row.availableCount === undefined?"不限制":scoped.row.availableCount}}
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scoped">
<el-button type="primary" @click="download(scoped.row)">下载</el-button>
<el-button type="primary" @click="adjustShareFile(scoped.row)">调整</el-button>
<el-button type="danger" @click="cancelShare(scoped.row.shareCode)">取消</el-button>
<el-button @click="copyShareLink(scoped.row)">复制链接</el-button>
<el-button v-if="scoped.row.downloadRecords !== undefined" @click="viewSingleDownloadRecord(scoped.row)">查看下载记录</el-button>
</template>
</el-table-column>
</el-table>
<adjust-share :current-site-id="currentSiteId" :is-adjusting-share="isAdjustShare"
:current-file="currentFile" @close="closeAdjustShare"/>
<view-download-record :current-file="currentFile" :is-view-download-record="isViewDownloadRecord" :share-files="shareFiles" :view-type="viewType" :current-site="getSiteBySiteId(currentSiteId)" @close="isViewDownloadRecord = false"/>
</template>
<style scoped>
</style>

97
src/views/sideBar.vue Normal file
View File

@ -0,0 +1,97 @@
<script setup>
import {Close, Files, House, PieChart, Share, Tools, User} from "@element-plus/icons-vue";
import store from "../store/index.js";
import {useRouter} from 'vue-router'
import everyUser from "@icon-park/vue-next/lib/icons/EveryUser.js";
import {ElMessage, ElMessageBox} from "element-plus";
import axios from "axios";
import qs from "qs";
let router = useRouter()
function logout(){
localStorage.clear()
delete store.state.config.passcode
store.state.websocket.close()
store.commit("logout")
router.push("login")
}
function alterPasscode(){
ElMessageBox.prompt('请输入新密码:', '修改密码', {
confirmButtonText : '确认',
cancelButtonText : '取消',
inputValue : ''
}).then((passcode) => {
if(passcode.value.trim() === ""){
ElMessage.error("密码不能为空")
return
}
axios.post(store.state.config.host + "alterPasscode?" + qs.stringify(
{passcode:passcode.value})).then((res) => {
ElMessage.success(res.data.data)
store.commit("updatePasscode", passcode.value)
router.push("/login")
})
}).catch(() => {})
}
</script>
<template>
<el-menu :default-active="$router.currentRoute.value.path" router>
<el-menu-item index="/index/status/">
<template #title>
<el-icon>
<House/>
</el-icon>
</template>
</el-menu-item>
<el-menu-item index="/index/fileManage/">
<template #title>
<el-icon>
<files/>
</el-icon>
文件管理
</template>
</el-menu-item>
<el-menu-item index="/index/taskManage/">
<template #title>
<el-icon>
<PieChart />
</el-icon>
任务管理
</template>
</el-menu-item>
<el-menu-item index="/index/siteManage/" v-if="store.state.config.isAdmin">
<el-icon>
<Tools />
</el-icon>
服务器管理
</el-menu-item>
<el-menu-item index="/index/userManage/" v-if="store.state.config.isAdmin">
<el-icon>
<every-user theme="outline"/>
</el-icon>
用户管理
</el-menu-item>
<el-menu-item index="/index/shareManage/">
<el-icon>
<Share />
</el-icon>
分享码管理
</el-menu-item>
<el-menu-item @click="alterPasscode">
<el-icon>
<User />
</el-icon>
修改密码
</el-menu-item>
<el-menu-item @click="logout">
<el-icon>
<Close />
</el-icon>
退出登录
</el-menu-item>
</el-menu>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,233 @@
<script setup>
import {computed, onMounted, ref, watch} from "vue";
import store from "../../store/index.js";
import {useRouter} from "vue-router";
import axios from "axios";
import {ElMessage, ElMessageBox} from "element-plus";
import qs from "qs";
import Util from "../../Util/index.js";
let router = useRouter()
let currentSiteId = ref()
let hostname = ref('')
let domain = ref('')
let reverseProxyPrefix = ref('')
let size = ref(0)
let sizeUnit = ref("GB")
let storagePath = ref("")
let sites = computed(() => {
return store.getters.getSites
})
let currentSite = computed(() => {
let s
sites.value.forEach((site) => {
if(site.id === currentSiteId.value)
s = site
})
return s
})
function switchSite(id){
currentSiteId.value = id
router.replace("/index/siteManage/?id=" + currentSiteId.value)
}
function submit(){
let site = currentSite.value
let alterSite = {id:site.id}
let adjustSize
if(!isNaN(parseInt(size.value)) && size.value !== 0) {
adjustSize = Util[sizeUnit.value] * size.value
if (adjustSize < 0) //
if (currentSite.availableSpace + adjustSize < 0) {
ElMessage.error("减少空间不能小于可分配空间,可以考虑回收用户空间")
return
}
alterSite.availableSpace = adjustSize
}
if(hostname.value.trim() !== '')
alterSite.hostname = hostname.value
if(domain.value.trim() !== '')
alterSite.domain = domain.value
if(reverseProxyPrefix.value.trim() !== '')
alterSite.reverseProxyPrefix = reverseProxyPrefix.value
if(storagePath.value.trim() !== '') {
alterSite.storagePath = storagePath.value
ElMessageBox.confirm("修改服务器存储路径会导致服务器重启,是否继续?", '警告',
{ confirmButtonText: "继续",
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
axios.post(store.state.config.host + "manage/site/alter?" + qs.stringify({
...alterSite}))
.then((res) => {
if(res.data.result === 'success') {
ElMessage.success(res.data.data)
store.dispatch('loadSites')
hostname.value = ""
domain.value = ""
reverseProxyPrefix.value = ""
size.value = 0
}
else
ElMessage.error(res.data.data)
})
})
return
}
axios.post(store.state.config.host + "manage/site/alter?" + qs.stringify({
...alterSite}))
.then((res) => {
if(res.data.result === 'success') {
ElMessage.success(res.data.data)
store.dispatch('loadSites')
hostname.value = ""
domain.value = ""
reverseProxyPrefix.value = ""
size.value = 0
}
else
ElMessage.error(res.data.data)
})
}
function unpair(){
axios.post(store.state.config.host + "manage/unpair?" + qs.stringify({passcode:store.state.config.passcode,
id:currentSiteId.value}))
.then((res) => {
if(res.data.result === 'success') {
ElMessage.success(res.data.data)
store.dispatch('loadSites')
}
else
ElMessage.error(res.data.data)
})
}
watch(router.currentRoute , (value, oldValue)=>{
if(value.path !== oldValue.path && value.path.startsWith('/index/siteManage/')) {
if (router.currentRoute.value.query.id === undefined) {
router.replace(`/index/siteManage/?id=${sites.value[0].id}`)
currentSiteId.value = sites.value[0].id
} else
currentSiteId.value = Number(router.currentRoute.value.query.id)
}
})
onMounted(() => {
if(router.currentRoute.value.query.id === undefined) {
router.replace(`/index/siteManage/?id=${sites.value[0].id}`)
currentSiteId.value = sites.value[0].id
}else
currentSiteId.value = Number(router.currentRoute.value.query.id)
})
</script>
<template>
<el-menu mode="horizontal" :default-active="currentSiteId">
<el-menu-item v-for="site in sites" :index="site.id" @click="switchSite(site.id)" >
{{site.hostname}}
</el-menu-item>
</el-menu>
<div v-if="currentSite !== undefined" style="background-color: white; padding-bottom:410px; padding-left: 25px">
<el-row>
<el-col :span="8">
服务器id:{{currentSite.id}}
</el-col>
</el-row>
<el-row>
<el-col :span="8">
服务器ip:{{currentSite.ip}}
</el-col>
<el-col :span="8">
域名跟反代前缀若要去除直接填"空"
</el-col>
</el-row>
<el-row>
<el-col :span="8">
当前存储路径:{{currentSite.storagePath}}
</el-col>
<el-col :span="8">
<el-input v-model="storagePath">
<template #prepend>存储路径</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
当前服务器名:{{currentSite.hostname}}
</el-col>
<el-col :span="8">
<el-input v-model="hostname">
<template #prepend>
服务器名
</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
当前服务器总预分配空间:{{Util.formatSize(currentSite.totalSpace)}} <br>
当前服务器可分配空间:{{Util.formatSize(currentSite.availableSpace)}}
</el-col>
<el-col :span="8" style="line-height: 40px">
<el-input v-model="size">
<template #prepend>
容量调整
</template>
<template #append>
<el-select placeholder="单位" v-model="sizeUnit">
<el-option value="KB"/>
<el-option value="MB"/>
<el-option value="GB"/>
</el-select>
</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
当前服务器域名:{{currentSite.domain===undefined?"无":currentSite.domain}}
</el-col>
<el-col :span="8">
<el-input v-model="domain">
<template #prepend>
服务器域名
</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
当前服务器反向代理前缀:{{currentSite.reverseProxyPrefix===undefined?"无":currentSite.reverseProxyPrefix}}
</el-col>
<el-col :span="8">
<el-input v-model="reverseProxyPrefix">
<template #prepend>
反向代理前缀
</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
</el-col>
<el-col :span="5">
<el-button type="danger" v-if="currentSiteId !== 1" @click="unpair">删除服务器</el-button>
<el-button type="primary" @click="submit">提交修改</el-button>
</el-col>
</el-row>
</div>
</template>
<style scoped>
</style>

103
src/views/status/index.vue Normal file
View File

@ -0,0 +1,103 @@
<script setup>
import SiteStatusCard from '../../components/site-statue-card/index.vue'
import {computed, onMounted, ref, watch} from "vue";
import store from "../../store/index.js";
import {useRouter} from "vue-router";
let route = useRouter()
let currentType = ref("all")
let statues = ref({})
let allSites = computed(() => {
return store.state.sites
})
let onlineSites = computed(() => {
return store.getters.getOnlineSites
})
let offlineSites = computed(() => {
return store.getters.getOfflineSites
})
let currentSites = ref([])
let menu = ref()
function switchType(type){
currentType.value = type
}
function refreshType(){
let query = JSON.parse(JSON.stringify(route.currentRoute.value.query))
query.type = currentType.value
route.push({path: route.currentRoute.value.path, query})
switch (currentType.value) {
case "all":
currentSites.value = allSites.value
break
case "online":
currentSites.value = onlineSites.value
break
case "offline":
currentSites.value = offlineSites.value
break
default:
break
}
}
watch(store.getters.getStatues, () => {
statues.value = store.getters.getStatues
})
watch(currentType, () => {
refreshType()
})
watch(route.currentRoute, () => {
if(route.currentRoute.value.path.startsWith("/index/status/")){
if(route.currentRoute.value.query.type === undefined) {
currentType.value = 'all'
} else if(route.currentRoute.value.query.type !== 'all' &&
route.currentRoute.value.query.type !== 'online' &&
route.currentRoute.value.query.type !== 'offline') {
currentType.value = 'all'
} else
currentType.value = route.currentRoute.value.query.type
refreshType()
}
})
onMounted(() => {
if(route.currentRoute.value.path.startsWith("/index/status/")){
if(route.currentRoute.value.query.type === undefined) {
currentType.value = 'all'
} else if(route.currentRoute.value.query.type !== 'all' &&
route.currentRoute.value.query.type !== 'online' &&
route.currentRoute.value.query.type !== 'offline') {
currentType.value = 'all'
} else
currentType.value = route.currentRoute.value.query.type
refreshType()
}
})
</script>
<template>
<div>
<el-menu mode="horizontal" :default-active="currentType" ref="menu" v-if="store.state.config.isAdmin">
<el-menu-item index="all" @click="switchType('all')">全部服务器</el-menu-item>
<el-menu-item index="online" @click="switchType('online')">在线服务器</el-menu-item>
<el-menu-item index="offline" @click="switchType('offline')">离线服务器</el-menu-item>
</el-menu>
<el-menu mode="horizontal" default-active="all" router ref="menu" v-else>
<el-menu-item index="all">当前服务器</el-menu-item>
</el-menu>
<el-row>
<el-col :span="7" v-for="site in currentSites" :key="site.id">
<site-status-card :status="statues[site.id]" :site="site"/>
</el-col>
</el-row>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,235 @@
<script setup>
import {computed, onMounted, ref, watch} from "vue";
import store from "../../store/index.js";
import Util from "../../Util/index.js";
import {useRouter} from "vue-router";
import axios from "axios";
import qs from "qs";
import {ElMessage} from "element-plus";
import {InfoFilled, WarningFilled} from "@element-plus/icons-vue";
let router = useRouter()
let currentTasks = ref()
let selectedTasks = []
let isSelected = ref(false)
let currentType = ref('all')
let isAdmin = computed(() => {
return store.state.config.isAdmin
})
let allTask = computed(() => {
return store.state.tasks
})
let waitingTask = computed(() => {
let tasks = []
allTask.value.forEach((task) => {
if(task.status === '等待中')
tasks.push(task)
})
return tasks
})
let proceedingTask = computed(() => {
let tasks = []
allTask.value.forEach((task) => {
if(task.status === '进行中')
tasks.push(task)
})
return tasks
})
let completeTask = computed(() => {
let tasks = []
allTask.value.forEach((task) => {
if(task.complete)
tasks.push(task)
})
return tasks
})
let users = computed(() => {
return store.state.users
})
function getUsernameById(userid){
let username
users.value.forEach((user) => {
if(user.id === userid)
username = user.username
})
return username
}
function getSiteBySiteId(id){
id = Number(id)
let getSite
store.getters.getOnlineSites.forEach((site) => {
if(site.id === id)
getSite = site
})
return getSite
}
function handleSelection(tasks){
selectedTasks = tasks
isSelected.value = tasks.length !== 0;
}
function cancelTasks() {
let taskIds = []
selectedTasks.forEach((task) => {
taskIds.push(task.taskId)
})
axios.post(store.state.config.host + "file/cancel", qs.stringify({passcode: store.state.config.passcode, taskIds}, {indices: false}))
.then(() => {
ElMessage.success("任务移除成功")
})
store.commit("removeTasks", taskIds)
}
function refreshType(){
let query = JSON.parse(JSON.stringify(router.currentRoute.value.query))
query.type = currentType.value
router.push({path: router.currentRoute.value.path, query})
switch (currentType.value) {
case "all":
currentTasks.value = allTask.value
break
case "complete":
currentTasks.value = completeTask.value
break
case "proceeding":
currentTasks.value = proceedingTask.value
break
case "waiting":
currentTasks.value = waitingTask.value
break
default:
break
}
}
function switchType(type){
currentType.value = type
}
watch(currentType, () => {
refreshType()
})
onMounted(() => {
if(router.currentRoute.value.path.startsWith("/index/taskManage/")){
if(router.currentRoute.value.query.type === undefined) {
currentType.value = 'all'
} else if(router.currentRoute.value.query.type !== 'all' &&
router.currentRoute.value.query.type !== 'online' &&
router.currentRoute.value.query.type !== 'offline') {
currentType.value = 'all'
} else
currentType.value = router.currentRoute.value.query.type
refreshType()
}
})
watch(router.currentRoute, () => {
if(router.currentRoute.value.path.startsWith('/index/taskManage/')){
if(router.currentRoute.value.query.type === undefined) {
router.replace('/index/taskManage/?type=all')
currentType.value = 'all'
} else if(router.currentRoute.value.query.type !== 'all' &&
router.currentRoute.value.query.type !== 'complete' &&
router.currentRoute.value.query.type !== 'proceeding' &&
router.currentRoute.value.query.type !== 'waiting') {
router.replace("/index/taskManage/?type=all")
currentType.value = 'all'
} else
currentType.value = router.currentRoute.value.query.type
}
})
</script>
<template>
<div>
<el-menu mode="horizontal" :default-active="currentType">
<el-menu-item index="all" @click="switchType('all')">全部</el-menu-item>
<el-menu-item index="complete" @click="switchType('complete')">已完成/已停止</el-menu-item>
<el-menu-item index="proceeding" @click="switchType('proceeding')">进行中</el-menu-item>
<el-menu-item index="waiting" @click="switchType('waiting')">等待中</el-menu-item>
</el-menu>
<el-button-group>
<el-button @click="cancelTasks" :disabled="!isSelected">删除/取消</el-button>
</el-button-group>
<el-table :data="currentTasks" empty-text="当前分类没有任务" :onSelectionChange="handleSelection" row-key="taskId">
<el-table-column type="selection" reserve-selection />
<el-table-column label="任务id" width="75px">
<template #default="scope">
{{scope.row.taskId}}
</template>
</el-table-column>
<el-table-column label="文件名">
<template #default="scope">
{{scope.row.filename}}
</template>
</el-table-column>
<el-table-column label="用户" v-if="isAdmin">
<template #default="scope">
{{getUsernameById(scope.row.userid)}}
</template>
</el-table-column>
<el-table-column label="任务类型" width="100px">
<template #default="scope">
{{scope.row.type}}
<el-popover width="450px" v-if="(isAdmin || scope.row.type !== '导入')">
<span v-if="scope.row.type === '打包'">
服务器:{{getSiteBySiteId(scope.row.siteId).hostname}}<br>
源文件:{{scope.row.paths}}<br>
目标文件:{{scope.row.targetPath}}
</span>
<span v-if="scope.row.type === '转移'">
源服务器:{{getSiteBySiteId(scope.row.sender).hostname}}<br>
源路径:{{scope.row.sourcePath}}<br>
目标服务器:{{getSiteBySiteId(scope.row.receiver).hostname}}<br>
目标路径:{{scope.row.targetPath}}
</span>
<span v-if="scope.row.type === '解包'">
服务器:{{getSiteBySiteId(scope.row.siteId).hostname}}<br>
源文件:{{scope.row.sourcePath + scope.row.filename}}<br>
目标路径:{{scope.row.targetPath}}
</span>
<template #reference>
<el-icon>
<InfoFilled />
</el-icon>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="任务进度">
<template #default="scope">
<el-progress :percentage="scope.row.percentage"></el-progress>
</template>
</el-table-column>
<el-table-column label="任务状态">
<template #default="scope">
{{scope.row.status}}
<el-popover trigger="hover" v-if="scope.row.status === '任务失败'">
{{scope.row.cause}}
<template #reference>
<el-icon>
<WarningFilled />
</el-icon>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="任务速度">
<template #default="scope">
{{Util.formatSize(scope.row.speed)+"/S"}}
</template>
</el-table-column>
<el-table-column label="任务大小">
<template #default="scope">
{{Util.formatSize(scope.row.total)}}
</template>
</el-table-column>
</el-table>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,445 @@
<script setup>
import {computed, ref, watch, onMounted} from "vue"
import store from "../../store/index.js";
import Util from "../../Util/index.js";
import {router} from "../../router/index.js";
import {ElMessage, ElMessageBox} from "element-plus";
import axios from "axios";
import qs from "qs";
let currentUserId = ref()
let currentUser = computed(() => {
let u
users.value.forEach((user) => {
if(user.id === currentUserId.value)
u = user
})
return u
})
let sites = computed(() => {
let sites = []
if(!isNewUser.value)
sites.push({id: 0, hostname: '不修改服务器', availableSpace: 0})
else
siteId.value = 1
sites.push(...store.state.sites)
return sites
})
let users = computed(() => {
let us = []
store.state.users.forEach((user) => {
if(user.id !== 1)
us.push(user)
})
return us
})
let isNewUser = ref(false)
let username = ref('')
let passcode = ref('')
let totalSpace = ref(0)
let spaceUnit = ref("GB")
let storagePath = ref('')
let siteId = ref(0)
function setToDefault(){
username.value = ""
passcode.value = ""
totalSpace.value = 0
storagePath.value = ""
siteId.value = 0
}
function getSite(siteId){
let site
sites.value.forEach((s) => {
if(s.id === siteId)
site = s
})
return site
}
function switchUser(userid){
isNewUser.value = userid === -1;
currentUserId.value = userid
setToDefault()
}
function getSiteName(siteId){
let name
sites.value.forEach((site) => {
if(site.id === siteId)
name = site.hostname
})
return name
}
function submit(){
if(isNewUser.value)
newUser()
else
alterUser()
}
function newUser(){
let newUser = {}
let size
if(username.value.trim() !== ''){
let error = ''
users.value.forEach((u) => {
if(u.username === username.value)
error = '不允许重复用户名'
})
if(error !== '') {
ElMessage.error(error)
return
}
newUser.username = username.value
} else {
ElMessage.error("请输入用户名")
return
}
if(passcode.value.trim() !== '')
newUser.passcode = passcode.value
else {
ElMessage.error("请输入密码")
return
}
if(isNaN(parseInt(totalSpace.value))){ // 1. 2.
ElMessage.error("容量大小输入错误,请输入数字")
return
} else {
size = totalSpace.value * Util[spaceUnit.value]
if(size <= 0){
ElMessage.error('请输入正确容量')
return
} else { //
let site = getSite(siteId.value)
if(size > site.availableSpace){
ElMessage.error('容量调整错误,不能超过服务器可分配空间,请先释放其他用户空间或增大服务器可分配看见后再尝试')
return
}
}
newUser.availableSpace = size
newUser.totalSpace = size
}
if(storagePath.value.trim() !== '') {
if(!(storagePath.value.endsWith("/") || storagePath.value.endsWith("\\")))
storagePath.value += '/'
if(storagePath.value.startsWith("/") || storagePath.value.startsWith("\\"))
storagePath.value = storagePath.value.substring(0)
newUser.storagePath = storagePath.value
} else {
ElMessage.error("请输入存储路径")
return
}
if(siteId.value !== 0){
let site = getSite(siteId.value)
if(site.availableSpace < newUser.totalSpace){
ElMessage.error("分配服务器可分配空间不足,请重新输入容量")
return
}
newUser.siteId = siteId.value
} else {
ElMessage.error("请绑定服务器")
return
}
axios.post(store.state.config.host + "manage/user/create?" + qs.stringify({...newUser}))
.then((res) => {
if(res.data.result === 'success'){
ElMessage.success(res.data.data)
setToDefault()
store.dispatch("loadUsers")
store.dispatch("loadSites")
} else {
ElMessage.error(res.data.data)
}
})
}
function alterUser(){
let alterUser = {id:currentUserId.value}
let adjustSize
if(username.value.trim() !== ''){
let error = ''
users.value.forEach((u) => {
if(u.id !== currentUserId.value && u.username === username.value)
error = '重命名失败,不允许重复用户名'
})
if(error !== '') {
ElMessage.error(error)
return
}
alterUser.username = username.value
}
if(passcode.value.trim() !== '')
alterUser.passcode = passcode.value
if(isNaN(parseInt(totalSpace.value))){ // 1. 2.
if(totalSpace.value.trim() !== ''){
ElMessage.error("容量大小输入错误,请输入数字")
return
}
} else if(totalSpace.value !== 0) {
adjustSize = totalSpace.value * Util[spaceUnit.value]
if(adjustSize < 0){ //
if(adjustSize + currentUser.value.availableSpace < 0) {
ElMessage.error('容量调整错误,释放空间不能小于用户可用空间,请先删除用户空间并同步后再尝试')
return
}
} else { //
let site = getSite(currentUser.value.siteId)
if(adjustSize > site.availableSpace){
ElMessage.error('容量调整错误,增加空间不能超过服务器可分配空间,请先释放其他用户空间或增大服务器可分配看见后再尝试')
return
}
}
alterUser.totalSpace = adjustSize
}
if(storagePath.value.trim() !== '') {
if ('totalSpace' in alterUser) {
ElMessage.error("不允许同时调整容量以及存储路径,请重新输入")
return
}
if(!(storagePath.value.endsWith("/") || storagePath.value.endsWith("\\")))
storagePath.value += '/'
alterUser.storagePath = storagePath.value
}
if(siteId.value !== 0){
if('totalSpace' in alterUser){
ElMessage.error("不允许同时调整容量以及绑定服务器,请重新输入")
return
}
if('storagePath' in alterUser){
ElMessage.error("不允许同时调整存储路径以及绑定服务器,请重新输入")
return
}
ElMessageBox.confirm('请注意,用户换绑服务器会导致原文件直接删除,请确保用户的文件已备份或转移到目标服务器了!',
'警告',
{
confirmButtonText: "确认",
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
alterUser.siteId = siteId.value
axios.post(store.state.config.host + "manage/user/alter?" + qs.stringify(alterUser))
.then((res) => {
if(res.data.result === 'success') {
ElMessage.success(res.data.data)
setToDefault()
store.dispatch('loadUsers')
store.dispatch('loadSites')
} else
ElMessage.error(res.data.data)
})
})
return
}
if(Object.keys(alterUser).length === 1)
return
axios.post(store.state.config.host + "manage/user/alter?" + qs.stringify(alterUser))
.then((res) => {
if(res.data.result === 'success') {
ElMessage.success(res.data.data)
setToDefault()
store.dispatch('loadUsers')
store.dispatch('loadSites')
} else
ElMessage.error(res.data.data)
})
}
function removeUser(){
ElMessageBox.confirm("删除用户会直接删除用户的存储文件夹,请确保用户数据已备份,是否继续?", '警告',
{
confirmButtonText: "删除",
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.post(store.state.config.host + "manage/user/remove?" + qs.stringify({userid:currentUserId.value}))
.then((res) => {
if(res.data.result === 'success'){
ElMessage.success("用户删除成功")
store.dispatch("loadSites")
store.dispatch("loadUsers")
switchUser(users.value[0].id)
} else {
ElMessage.error(res.data.data)
}
})
})
}
function verifySpace(){
axios.post(store.state.config.host + "manage/user/verifySpace?userid=" + currentUserId.value)
.then((res) => {
if(res.data.result === 'success') {
ElMessage.success(res.data.data)
store.dispatch('loadUsers')
}
else
ElMessage.error(res.data.data)
})
}
function siteLabel(site){
if(site.id === 0)
return '不修改服务器'
else if(site.id === currentUser.value.siteId)
return '当前绑定服务器:' + site.id + ' ' + site.hostname + ' 剩余空间:' + Util.formatSize(site.availableSpace)
else
return site.id + ' ' + site.hostname + ' 剩余空间:' + Util.formatSize(site.availableSpace)
}
watch(router.currentRoute, (now, old) => {
if(now.path !== old.path && now.path.startsWith("/index/userManage/")){
if(router.currentRoute.value.query.id === undefined){
router.replace(`/index/userManage/?id=${users.value[0].id}`)
currentUserId.value = Number(users.value[0].id)
} else
currentUserId.value = Number(users.value[0].id)
if(users.value[0].id === -1)
isNewUser.value = true
}
})
onMounted(() => {
if(router.currentRoute.value.query.id === undefined){
router.replace(`/index/userManage/?id=${users.value[0].id}`)
currentUserId.value = Number(users.value[0].id)
} else
currentUserId.value = Number(router.currentRoute.value.query.id)
if(currentUserId.value === -1)
isNewUser.value = true
})
</script>
<template>
<el-menu mode="horizontal" :default-active="currentUserId">
<el-menu-item v-for="user in users" :index="user.id" @click="switchUser(user.id)">
{{user.username}}
</el-menu-item>
</el-menu>
<div v-if="currentUser !== undefined" style="background-color: white; padding-bottom:400px; padding-left: 25px;">
<el-row>
<el-col :span="8">
<span v-if="!isNewUser">
用户名:{{currentUser.username}}
</span>
</el-col>
<el-col :span="8">
<el-input v-model="username">
<template #prepend>
{{isNewUser?'':'新'}}用户名
</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<span v-if="!isNewUser">
密码:{{currentUser.passcode}}
</span>
</el-col>
<el-col :span="8">
<el-input v-model="passcode">
<template #prepend>
{{isNewUser?'':'新'}}密码
</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<span v-if="!isNewUser">
当前可用空间:{{Util.formatSize(currentUser.availableSpace)}}<br>
总分配空间:{{Util.formatSize(currentUser.totalSpace)}}
</span>
</el-col>
<el-col :span="8" style="line-height: 37px">
<el-input v-model="totalSpace">
<template #prepend>
{{(isNewUser?'分配':'调整') + '空间'}}
</template>
<template #append>
<el-select v-model="spaceUnit" style="width: 100px">
<el-option value="KB"/>
<el-option value="MB"/>
<el-option value="GB"/>
</el-select>
</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<span v-if="!isNewUser">
存储路径:{{currentUser.storagePath}}
</span>
</el-col>
<el-col :span="8">
<el-input v-model="storagePath">
<template #prepend>
{{isNewUser?'':'新'}}存储路径
</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<span v-if="!isNewUser">
绑定服务器:{{getSiteName(currentUser.siteId)}}
</span>
</el-col>
<el-col :span="4">
<el-select v-model="siteId" style="width: 425px">
<template #prefix>
{{isNewUser?'绑定服务器':''}}
</template>
<el-option v-for="site in sites" :label="siteLabel(site)" :value="site.id" :disabled="site.id === currentUser.siteId || site.availableSpace === 0"/>
</el-select>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
</el-col>
<el-col :span="8">
<el-button @click="submit" type="primary">提交{{isNewUser?'新建':'修改'}}</el-button>
<el-button @click="removeUser" v-show="!isNewUser" type="danger">删除用户</el-button>
<el-button @click="verifySpace" v-show="!isNewUser">校准用户可用空间</el-button>
</el-col>
</el-row>
</div>
</template>
<style scoped>
</style>

35
vite.config.js Normal file
View File

@ -0,0 +1,35 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import {ElementPlusResolver} from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver(),IconsResolver({
prefix: 'i'
})],
}),Icons({
compiler:"vue3",
autoInstall: true
})],
server:{
proxy: {
"/api":{
target: "https://sns.lionwebsite.xyz/",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, "")
}
}
},
})