mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-16 04:13:03 +00:00
优化构建流v0.1.9b6b
This commit is contained in:
2
.github/workflows/maven.yml
vendored
2
.github/workflows/maven.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
cache: maven
|
||||
|
||||
- name: Build Frontend
|
||||
run: cd web-front && npm install && npm run build
|
||||
run: cd web-front && yarn install && yarn run build
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvn -B package --file pom.xml
|
||||
|
||||
993
web-front/src/components/DirectoryTree.vue
Normal file
993
web-front/src/components/DirectoryTree.vue
Normal file
@@ -0,0 +1,993 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<div class="directory-tree" :class="{ 'dark-theme': isDarkTheme }">
|
||||
<template v-if="viewMode === 'pane'">
|
||||
<!-- 窗格模式(原有) -->
|
||||
<div class="breadcrumb">
|
||||
<div
|
||||
v-for="(item, index) in pathStack"
|
||||
:key="index"
|
||||
class="breadcrumb-item"
|
||||
:class="{ 'active': index === pathStack.length - 1 }"
|
||||
@click="goToDirectory(index)"
|
||||
>
|
||||
<i class="fas fa-folder" v-if="index === 0"></i>
|
||||
<i class="fas fa-chevron-right" v-else-if="index > 0"></i>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-grid" v-loading="loading">
|
||||
<div
|
||||
v-for="file in currentFileList"
|
||||
:key="file.fileName"
|
||||
class="file-item"
|
||||
:class="getFileTypeClass(file)"
|
||||
@click="handleFileClick(file)"
|
||||
>
|
||||
<div class="file-icon">
|
||||
<i :class="getFileIcon(file)"></i>
|
||||
</div>
|
||||
<div class="file-name">{{ file.fileName }}</div>
|
||||
<div class="file-meta">
|
||||
{{ file.sizeStr || '0B' }} · {{ formatDate(file.createTime) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!loading && (!currentFileList || currentFileList.length === 0)" class="empty-state">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<h3>此文件夹为空</h3>
|
||||
<p>暂无文件或文件夹</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-bar">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="goBack"
|
||||
:disabled="pathStack.length <= 1"
|
||||
icon="el-icon-arrow-left"
|
||||
>
|
||||
返回上一级
|
||||
</el-button>
|
||||
<div class="stats">
|
||||
<span class="stat-item">
|
||||
<i class="fas fa-folder"></i> {{ folderCount }} 个文件夹
|
||||
</span>
|
||||
<span class="stat-item">
|
||||
<i class="fas fa-file"></i> {{ fileCount }} 个文件
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="viewMode === 'tree'">
|
||||
<div class="content-card">
|
||||
<splitpanes class="split-theme custom-splitpanes" style="height:100%;">
|
||||
<pane>
|
||||
<div class="tree-sidebar">
|
||||
<el-tree
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
lazy
|
||||
:load="loadNode"
|
||||
highlight-current
|
||||
@node-click="onNodeClick"
|
||||
:default-expand-all="false"
|
||||
:default-expanded-keys="['root']"
|
||||
:render-content="renderContent"
|
||||
style="background:transparent;"
|
||||
/>
|
||||
</div>
|
||||
</pane>
|
||||
<pane>
|
||||
<div class="tree-content">
|
||||
<div v-if="selectedNode">
|
||||
<div class="file-detail-icon-wrap">
|
||||
<i :class="getFileIcon(selectedNode)" class="file-detail-icon"></i>
|
||||
</div>
|
||||
<h4>{{ selectedNode.fileName }}</h4>
|
||||
<div v-if="selectedNode.fileType === 'folder'">
|
||||
<ul>
|
||||
<li v-for="file in selectedNode.children || []" :key="file.id">
|
||||
<i :class="getFileIcon(file)"></i> {{ file.fileName }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>类型: {{ getFileTypeClass(selectedNode) }}</p>
|
||||
<p>大小: {{ selectedNode.sizeStr || '0B' }}</p>
|
||||
<p>创建时间: {{ formatDate(selectedNode.createTime) }}</p>
|
||||
<!-- 文件详情区下载按钮 -->
|
||||
<el-button v-if="selectedNode && selectedNode.parserUrl" @click="previewFile(selectedNode)">打开</el-button>
|
||||
<a
|
||||
v-if="selectedNode && selectedNode.parserUrl"
|
||||
:href="selectedNode.parserUrl"
|
||||
download
|
||||
target="_blank"
|
||||
class="el-button el-button--success"
|
||||
style="margin-left: 8px;"
|
||||
>
|
||||
下载
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="color: #888;">请选择左侧文件或文件夹</div>
|
||||
</div>
|
||||
</pane>
|
||||
</splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 文件操作对话框(窗格模式下) -->
|
||||
<el-dialog
|
||||
v-if="viewMode === 'pane'"
|
||||
title="文件操作"
|
||||
v-model="fileDialogVisible"
|
||||
width="400px"
|
||||
:before-close="closeFileDialog"
|
||||
>
|
||||
<div class="file-dialog-content">
|
||||
<p><strong>{{ selectedFile?.fileName || '未命名文件' }}</strong></p>
|
||||
<p class="file-info">
|
||||
大小: {{ selectedFile?.sizeStr || '0B' }}<br>
|
||||
创建时间: {{ formatDate(selectedFile?.createTime) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="previewFile(selectedFile)">打开</el-button>
|
||||
<!-- 弹窗下载按钮 -->
|
||||
<a
|
||||
v-if="selectedFile && selectedFile.parserUrl"
|
||||
:href="selectedFile.parserUrl"
|
||||
download
|
||||
target="_blank"
|
||||
class="el-button el-button--success"
|
||||
style="margin-left: 8px;"
|
||||
>
|
||||
下载
|
||||
</a>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<div v-if="isPreviewing" class="preview-mask">
|
||||
<div class="preview-toolbar">
|
||||
<el-button size="small" @click="closePreview">关闭预览</el-button>
|
||||
<el-button size="small" type="primary" @click="openPreviewInNewTab">新窗口打开</el-button>
|
||||
</div>
|
||||
<iframe :src="previewUrl" frameborder="0" class="preview-iframe"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import { ElTree } from 'element-plus'
|
||||
import { Splitpanes, Pane } from 'splitpanes'
|
||||
import 'splitpanes/dist/splitpanes.css'
|
||||
import fileTypeUtils from '@/utils/fileTypeUtils'
|
||||
|
||||
export default {
|
||||
name: 'DirectoryTree',
|
||||
components: { ElTree, Splitpanes, Pane },
|
||||
props: {
|
||||
fileList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
shareUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
viewMode: {
|
||||
type: String,
|
||||
default: 'pane' // 'pane' or 'tree'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
pathStack: [{ name: '全部文件', url: '' }],
|
||||
currentFileList: [],
|
||||
fileDialogVisible: false,
|
||||
selectedFile: null,
|
||||
isDarkTheme: false,
|
||||
initialized: false,
|
||||
// 文件树模式相关
|
||||
treeData: [],
|
||||
selectedNode: null,
|
||||
isPreviewing: false,
|
||||
previewUrl: '',
|
||||
treeProps: {
|
||||
label: 'fileName',
|
||||
children: 'children',
|
||||
isLeaf: 'isLeaf'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
folderCount() {
|
||||
return this.currentFileList.filter(file => file.fileType === 'folder').length
|
||||
},
|
||||
fileCount() {
|
||||
return this.currentFileList.filter(file => file.fileType !== 'folder').length
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
fileList: {
|
||||
immediate: true,
|
||||
handler(newList) {
|
||||
// 根节点children为当前目录下所有文件/文件夹
|
||||
this.treeData = [
|
||||
{
|
||||
id: 'root',
|
||||
fileName: '全部文件',
|
||||
fileType: 'folder',
|
||||
children: (newList || []).map(item => ({
|
||||
...item,
|
||||
isLeaf: item.fileType !== 'folder'
|
||||
})),
|
||||
isLeaf: false
|
||||
}
|
||||
]
|
||||
this.currentFileList = newList
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...fileTypeUtils,
|
||||
// 构建API URL
|
||||
buildApiUrl() {
|
||||
const baseUrl = `${window.location.origin}/v2/getFileList`
|
||||
const params = new URLSearchParams({
|
||||
url: this.shareUrl
|
||||
})
|
||||
if (this.password) {
|
||||
params.append('pwd', this.password)
|
||||
}
|
||||
return `${baseUrl}?${params.toString()}`
|
||||
},
|
||||
// 文件树与窗格同源:直接返回当前目录数据
|
||||
buildTree(list) {
|
||||
return list || []
|
||||
},
|
||||
// 懒加载子节点
|
||||
loadNode(node, resolve) {
|
||||
if (node.level === 0) {
|
||||
// 根节点
|
||||
resolve(this.treeData[0].children)
|
||||
} else if (node.data.fileType === 'folder' && node.data.parserUrl) {
|
||||
axios.get(node.data.parserUrl).then(res => {
|
||||
if (res.data.code === 200) {
|
||||
const children = (res.data.data || []).map(item => ({
|
||||
...item,
|
||||
isLeaf: item.fileType !== 'folder'
|
||||
}))
|
||||
resolve(children)
|
||||
} else {
|
||||
resolve([])
|
||||
}
|
||||
}).catch(() => resolve([]))
|
||||
} else {
|
||||
resolve([])
|
||||
}
|
||||
},
|
||||
onNodeClick(data) {
|
||||
this.selectedNode = data
|
||||
},
|
||||
// 处理文件点击
|
||||
handleFileClick(file) {
|
||||
console.log('点击文件', file, this.viewMode)
|
||||
if (file.fileType === 'folder') {
|
||||
this.enterFolder(file)
|
||||
} else if (this.viewMode === 'pane') {
|
||||
this.selectedFile = file
|
||||
this.fileDialogVisible = true
|
||||
}
|
||||
},
|
||||
// 进入文件夹
|
||||
async enterFolder(folder) {
|
||||
if (!folder.parserUrl) {
|
||||
this.$message.error('无法进入该文件夹,缺少访问链接')
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await axios.get(folder.parserUrl)
|
||||
if (response.data.code === 200) {
|
||||
const newDir = {
|
||||
url: folder.parserUrl,
|
||||
name: folder.fileName || '未命名文件夹'
|
||||
}
|
||||
this.pathStack.push(newDir)
|
||||
this.currentFileList = response.data.data || []
|
||||
} else {
|
||||
this.$message.error(response.data.msg || '获取文件夹内容失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('进入文件夹失败:', error)
|
||||
this.$message.error('进入文件夹失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
goBack() {
|
||||
if (this.pathStack.length > 1) {
|
||||
this.pathStack.pop()
|
||||
this.loadCurrentDirectory()
|
||||
}
|
||||
},
|
||||
goToDirectory(index) {
|
||||
this.pathStack.splice(index + 1)
|
||||
this.loadCurrentDirectory()
|
||||
},
|
||||
async loadCurrentDirectory() {
|
||||
const currentDir = this.pathStack[this.pathStack.length - 1]
|
||||
if (!currentDir.url) {
|
||||
this.currentFileList = this.fileList
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await axios.get(currentDir.url)
|
||||
if (response.data.code === 200) {
|
||||
this.currentFileList = response.data.data || []
|
||||
} else {
|
||||
this.$message.error(response.data.msg || '加载目录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载目录失败:', error)
|
||||
this.$message.error('加载目录失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
// 预览文件
|
||||
previewFile(file) {
|
||||
if (file?.previewUrl || file?.parserUrl) {
|
||||
this.previewUrl = file.previewUrl || file.parserUrl
|
||||
this.isPreviewing = true
|
||||
} else {
|
||||
this.$message.warning('该文件暂无预览链接')
|
||||
}
|
||||
this.closeFileDialog()
|
||||
},
|
||||
// 下载文件
|
||||
downloadFile(file) {
|
||||
if (file?.parserUrl) {
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.style.display = 'none'
|
||||
iframe.src = file.parserUrl
|
||||
document.body.appendChild(iframe)
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(iframe)
|
||||
}, 1000)
|
||||
this.$message.success('开始下载文件')
|
||||
} else {
|
||||
this.$message.warning('该文件暂无下载链接')
|
||||
}
|
||||
this.closeFileDialog()
|
||||
},
|
||||
closeFileDialog() {
|
||||
this.fileDialogVisible = false
|
||||
this.selectedFile = null
|
||||
},
|
||||
closePreview() {
|
||||
this.isPreviewing = false
|
||||
this.previewUrl = ''
|
||||
},
|
||||
openPreviewInNewTab() {
|
||||
if (this.previewUrl) {
|
||||
window.open(this.previewUrl, '_blank')
|
||||
}
|
||||
},
|
||||
formatDate(timestamp) {
|
||||
if (!timestamp) return '未知时间'
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString('zh-CN')
|
||||
},
|
||||
checkTheme() {
|
||||
this.isDarkTheme = document.body.classList.contains('dark-theme') ||
|
||||
document.documentElement.classList.contains('dark-theme')
|
||||
},
|
||||
renderContent(h, { node, data, store }) {
|
||||
const isFolder = data.fileType === 'folder'
|
||||
return h('div', {
|
||||
class: 'custom-tree-node'
|
||||
}, [
|
||||
h('i', {
|
||||
class: [this.getFileIcon(data), { 'folder-icon': isFolder, 'file-icon': !isFolder }]
|
||||
}),
|
||||
h('span', {
|
||||
class: ['node-label', { 'folder-text': isFolder, 'file-text': !isFolder }]
|
||||
}, node.label)
|
||||
])
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.checkTheme()
|
||||
this.initialized = true
|
||||
|
||||
// 监听主题变化
|
||||
this._observer = new MutationObserver(() => {
|
||||
this.checkTheme()
|
||||
})
|
||||
this._observer.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
})
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this._observer) {
|
||||
this._observer.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body, #app, .main-container, .directory-tree, .content-card {
|
||||
/* overflow: hidden; */
|
||||
/* overflow: auto; */
|
||||
/* position: relative; */
|
||||
}
|
||||
.main-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.directory-tree {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.directory-tree.dark-theme {
|
||||
background: #2d2d2d;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
background: #f8f9fa;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dark-theme .breadcrumb {
|
||||
background: #404040;
|
||||
border-bottom-color: #555555;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.95rem;
|
||||
color: #7f8c8d;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark-theme .breadcrumb-item.active {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.breadcrumb-item i {
|
||||
margin: 0 8px;
|
||||
font-size: 0.8rem;
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
padding: 10px 4px;
|
||||
min-height: 80px;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.dark-theme .file-item {
|
||||
background: #404040;
|
||||
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.dark-theme .file-item:hover {
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.file-item:hover .file-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.folder .file-icon {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.image .file-icon {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.document .file-icon {
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.archive .file-icon {
|
||||
color: #9b59b6;
|
||||
}
|
||||
|
||||
.audio .file-icon {
|
||||
color: #1abc9c;
|
||||
}
|
||||
|
||||
.video .file-icon {
|
||||
color: #d35400;
|
||||
}
|
||||
|
||||
.code .file-icon {
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.85rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #95a5a6;
|
||||
}
|
||||
|
||||
.dark-theme .file-meta {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 30px 10px;
|
||||
color: #7f8c8d;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dark-theme .empty-state {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 10px;
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.dark-theme .empty-state i {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 6px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.dark-theme .empty-state h3 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.dark-theme .action-bar {
|
||||
background: #404040;
|
||||
border-top-color: #555555;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #7f8c8d;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.dark-theme .stats {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.file-dialog-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dark-theme .file-info {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.tree-layout {
|
||||
display: flex;
|
||||
height: 500px;
|
||||
}
|
||||
.tree-sidebar {
|
||||
width: 220px;
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #eaeaea;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.directory-tree.dark-theme .tree-sidebar {
|
||||
background: #232323;
|
||||
border-right: 1px solid #404040;
|
||||
}
|
||||
.file-tree-root, .tree-node ul {
|
||||
list-style: none;
|
||||
padding-left: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
.tree-node {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.tree-node.selected > .tree-node-label {
|
||||
background: #e6f7ff;
|
||||
color: #409eff;
|
||||
}
|
||||
.directory-tree.dark-theme .tree-node.selected > .tree-node-label {
|
||||
background: #333c4d;
|
||||
color: #4a9eff;
|
||||
}
|
||||
.tree-node-label {
|
||||
cursor: pointer;
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: background 0.2s;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.tree-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 自定义树节点样式 */
|
||||
.custom-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-tree-node i {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.dark-theme .custom-tree-node i {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.custom-tree-node .node-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.dark-theme .custom-tree-node .node-label {
|
||||
color: #e1e1e1;
|
||||
}
|
||||
|
||||
/* 文件夹样式 */
|
||||
.custom-tree-node .folder-icon {
|
||||
color: #409eff !important;
|
||||
}
|
||||
|
||||
.dark-theme .custom-tree-node .folder-icon {
|
||||
color: #4a9eff !important;
|
||||
}
|
||||
|
||||
.custom-tree-node .folder-text {
|
||||
color: #409eff !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dark-theme .custom-tree-node .folder-text {
|
||||
color: #4a9eff !important;
|
||||
}
|
||||
|
||||
/* 文件样式 */
|
||||
.custom-tree-node .file-icon {
|
||||
color: #95a5a6 !important;
|
||||
}
|
||||
|
||||
.dark-theme .custom-tree-node .file-icon {
|
||||
color: #bdc3c7 !important;
|
||||
}
|
||||
|
||||
.custom-tree-node .file-text {
|
||||
color: #606266 !important;
|
||||
}
|
||||
|
||||
.dark-theme .custom-tree-node .file-text {
|
||||
color: #e1e1e1 !important;
|
||||
}
|
||||
|
||||
/* 特殊文件类型图标颜色 */
|
||||
.custom-tree-node i.fa-file-image {
|
||||
color: #e74c3c !important;
|
||||
}
|
||||
|
||||
.custom-tree-node i.fa-file-pdf {
|
||||
color: #e74c3c !important;
|
||||
}
|
||||
|
||||
.custom-tree-node i.fa-file-word {
|
||||
color: #3498db !important;
|
||||
}
|
||||
|
||||
.custom-tree-node i.fa-file-excel {
|
||||
color: #27ae60 !important;
|
||||
}
|
||||
|
||||
.custom-tree-node i.fa-file-powerpoint {
|
||||
color: #f39c12 !important;
|
||||
}
|
||||
|
||||
.custom-tree-node i.fa-file-archive {
|
||||
color: #9b59b6 !important;
|
||||
}
|
||||
|
||||
.custom-tree-node i.fa-file-audio {
|
||||
color: #1abc9c !important;
|
||||
}
|
||||
|
||||
.custom-tree-node i.fa-file-video {
|
||||
color: #d35400 !important;
|
||||
}
|
||||
|
||||
.custom-tree-node i.fa-file-code {
|
||||
color: #27ae60 !important;
|
||||
}
|
||||
|
||||
/* 树节点悬停效果 */
|
||||
.el-tree-node__content:hover .custom-tree-node {
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark-theme .el-tree-node__content:hover .custom-tree-node {
|
||||
background-color: #2c2c2c;
|
||||
}
|
||||
|
||||
/* 选中节点样式 */
|
||||
.el-tree-node.is-current > .el-tree-node__content .custom-tree-node {
|
||||
background-color: #e6f7ff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark-theme .el-tree-node.is-current > .el-tree-node__content .custom-tree-node {
|
||||
background-color: #333c4d;
|
||||
}
|
||||
|
||||
.preview-mask { position: fixed; z-index: 9999; left: 0; top: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.85); display: flex; flex-direction: column; }
|
||||
.preview-toolbar { padding: 12px; background: #232323; text-align: right; }
|
||||
.preview-iframe { flex: 1; width: 100vw; border: none; background: #222; }
|
||||
|
||||
.content-card {
|
||||
min-height: 500px;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
margin: 0 0 12px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.dark-theme .content-card {
|
||||
background: #232323;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||
}
|
||||
|
||||
.split-theme {
|
||||
flex: 1 1 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tree-sidebar, .tree-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tree-content {
|
||||
padding: 40px 16px 16px 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.file-detail-icon-wrap {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.file-detail-icon {
|
||||
font-size: 48px;
|
||||
color: #409eff;
|
||||
display: block;
|
||||
}
|
||||
.dark-theme .file-detail-icon {
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
/* splitpanes 拖拽条自定义按钮 */
|
||||
.custom-splitpanes .splitpanes__splitter {
|
||||
position: relative;
|
||||
background: #e0e0e0;
|
||||
transition: background 0.2s;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.custom-splitpanes .splitpanes__splitter:hover {
|
||||
background: #b3b3b3;
|
||||
}
|
||||
.custom-splitpanes .splitpanes__splitter:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
border: 1.5px solid #d0d0d0;
|
||||
z-index: 2;
|
||||
display: block;
|
||||
}
|
||||
.dark-theme .custom-splitpanes .splitpanes__splitter:after {
|
||||
background: #232323;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.feedback-bar {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
padding: 12px 18px 0 0;
|
||||
}
|
||||
.feedback-link {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
font-size: 1.08rem;
|
||||
text-decoration: none;
|
||||
border: 1px solid #e74c3c;
|
||||
border-radius: 6px;
|
||||
padding: 4px 14px;
|
||||
background: #fff5f5;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.feedback-link:first-child { margin-left: 0; }
|
||||
.feedback-link:hover {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
.dark-theme .feedback-link {
|
||||
background: #2d2d2d;
|
||||
color: #ff7675;
|
||||
border-color: #ff7675;
|
||||
}
|
||||
.dark-theme .feedback-link:hover {
|
||||
background: #ff7675;
|
||||
color: #232323;
|
||||
}
|
||||
.feedback-icon {
|
||||
font-size: 1.15em;
|
||||
color: #e74c3c;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.feedback-link:hover .feedback-icon {
|
||||
color: #fff;
|
||||
}
|
||||
.feedback-link:nth-child(2) .feedback-icon { color: #333; }
|
||||
.feedback-link:nth-child(3) .feedback-icon { color: #f39c12; }
|
||||
.dark-theme .feedback-icon {
|
||||
color: #ff7675;
|
||||
}
|
||||
.dark-theme .feedback-link:nth-child(2) .feedback-icon { color: #fff; }
|
||||
.dark-theme .feedback-link:nth-child(3) .feedback-icon { color: #f7ca77; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.file-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
||||
gap: 8px;
|
||||
padding: 6px;
|
||||
}
|
||||
.file-item {
|
||||
padding: 6px 2px;
|
||||
min-height: 60px;
|
||||
}
|
||||
.file-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.file-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
|
||||
gap: 4px;
|
||||
}
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
17
web-front/src/router/index.js
Normal file
17
web-front/src/router/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Home from '@/views/Home.vue'
|
||||
import ShowFile from '@/views/ShowFile.vue'
|
||||
import ShowList from '@/views/ShowList.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: Home },
|
||||
{ path: '/showFile', component: ShowFile },
|
||||
{ path: '/showList', component: ShowList }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/'),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
85
web-front/src/utils/fileTypeUtils.js
Normal file
85
web-front/src/utils/fileTypeUtils.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const fileTypeUtils = {
|
||||
getFileExtension(filename) {
|
||||
if (!filename) return ''
|
||||
return filename.split('.').pop()
|
||||
},
|
||||
getFileTypeClass(file) {
|
||||
if (file.fileType === 'folder') return 'folder'
|
||||
const ext = this.getFileExtension(file.fileName)
|
||||
const fileTypes = {
|
||||
'image': ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'],
|
||||
'document': ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf'],
|
||||
'archive': ['zip', 'rar', '7z', 'tar', 'gz'],
|
||||
'audio': ['mp3', 'wav', 'ogg', 'flac'],
|
||||
'video': ['mp4', 'avi', 'mov', 'wmv', 'mkv', 'flv'],
|
||||
'code': ['html', 'htm', 'css', 'js', 'json', 'php', 'py', 'java', 'c', 'cpp', 'h', 'sh', 'bat', 'md']
|
||||
}
|
||||
for (const [type, extensions] of Object.entries(fileTypes)) {
|
||||
if (extensions.includes(ext.toLowerCase())) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
return 'document'
|
||||
},
|
||||
getFileIcon(file) {
|
||||
if (file.fileType === 'folder') return 'fas fa-folder'
|
||||
const ext = this.getFileExtension(file.fileName)
|
||||
const iconMap = {
|
||||
'jpg': 'fas fa-file-image', 'jpeg': 'fas fa-file-image', 'png': 'fas fa-file-image',
|
||||
'gif': 'fas fa-file-image', 'bmp': 'fas fa-file-image', 'svg': 'fas fa-file-image', 'webp': 'fas fa-file-image',
|
||||
'pdf': 'fas fa-file-pdf', 'doc': 'fas fa-file-word', 'docx': 'fas fa-file-word',
|
||||
'xls': 'fas fa-file-excel', 'xlsx': 'fas fa-file-excel', 'ppt': 'fas fa-file-powerpoint', 'pptx': 'fas fa-file-powerpoint',
|
||||
'txt': 'fas fa-file-alt', 'rtf': 'fas fa-file-alt',
|
||||
'zip': 'fas fa-file-archive', 'rar': 'fas fa-file-archive', '7z': 'fas fa-file-archive',
|
||||
'tar': 'fas fa-file-archive', 'gz': 'fas fa-file-archive',
|
||||
'mp3': 'fas fa-file-audio', 'wav': 'fas fa-file-audio', 'ogg': 'fas fa-file-audio', 'flac': 'fas fa-file-audio',
|
||||
'mp4': 'fas fa-file-video', 'avi': 'fas fa-file-video', 'mov': 'fas fa-file-video',
|
||||
'wmv': 'fas fa-file-video', 'mkv': 'fas fa-file-video', 'flv': 'fas fa-file-video',
|
||||
'html': 'fas fa-file-code', 'htm': 'fas fa-file-code', 'css': 'fas fa-file-code',
|
||||
'js': 'fas fa-file-code', 'json': 'fas fa-file-code', 'php': 'fas fa-file-code',
|
||||
'py': 'fas fa-file-code', 'java': 'fas fa-file-code', 'c': 'fas fa-file-code',
|
||||
'cpp': 'fas fa-file-code', 'h': 'fas fa-file-code', 'sh': 'fas fa-file-code',
|
||||
'bat': 'fas fa-file-code', 'md': 'fas fa-file-code'
|
||||
}
|
||||
return iconMap[ext.toLowerCase()] || 'fas fa-file'
|
||||
},
|
||||
extractFileNameAndExt(url) {
|
||||
if (!url) return { name: '', ext: '' }
|
||||
const filenameParams = [
|
||||
'response-content-disposition', 'filename', 'filename*', 'fn', 'fname', 'download_name'
|
||||
];
|
||||
let name = null;
|
||||
try {
|
||||
const u = new URL(url, window.location.origin);
|
||||
for (const param of filenameParams) {
|
||||
const value = u.searchParams.get(param);
|
||||
if (value) {
|
||||
if (param === 'response-content-disposition') {
|
||||
const match = value.match(/filename\*?=(.*'')?(?<FN>.*)/i);
|
||||
name = match && match.groups && match.groups['FN'] ? match.groups['FN'] : value;
|
||||
} else {
|
||||
name = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (name) {
|
||||
name = decodeURIComponent(name).replace(/['"]/g, '');
|
||||
} else {
|
||||
const decodedUrl = decodeURIComponent(url);
|
||||
const paths = decodedUrl.split('/');
|
||||
name = paths[paths.length - 1].split('?')[0];
|
||||
}
|
||||
let ext = '';
|
||||
if (name) {
|
||||
const spl = name.split('.');
|
||||
ext = spl.length > 1 ? spl[spl.length - 1].toLowerCase() : '';
|
||||
}
|
||||
return { name, ext };
|
||||
} catch {
|
||||
return { name: '', ext: '' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default fileTypeUtils
|
||||
793
web-front/src/views/Home.vue
Normal file
793
web-front/src/views/Home.vue
Normal file
@@ -0,0 +1,793 @@
|
||||
<template>
|
||||
<div id="app" :class="{ 'dark-theme': isDarkMode }">
|
||||
<!-- <el-dialog
|
||||
v-model="showRiskDialog"
|
||||
title="使用本网站您应改同意"
|
||||
width="300px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
center
|
||||
>
|
||||
<div style="font-size:1.08em;line-height:1.8;">
|
||||
请勿在本平台分享、传播任何违法内容,包括但不限于:<br>
|
||||
违规视频、游戏外挂、侵权资源、涉政涉黄等。<br>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="ackRisk">我知道了</el-button>
|
||||
</template>
|
||||
</el-dialog> -->
|
||||
<!-- 顶部反馈栏(小号、灰色、无红边框) -->
|
||||
<div class="feedback-bar">
|
||||
<a href="https://github.com/qaiu/lz.qaiu.top/issues" target="_blank" rel="noopener" class="feedback-link mini">
|
||||
<i class="fas fa-bug feedback-icon"></i>
|
||||
反馈
|
||||
</a>
|
||||
<a href="https://github.com/qaiu/lz.qaiu.top" target="_blank" rel="noopener" class="feedback-link mini">
|
||||
<i class="fab fa-github feedback-icon"></i>
|
||||
GitHub
|
||||
</a>
|
||||
<a href="https://blog.qaiu.top" target="_blank" rel="noopener" class="feedback-link mini">
|
||||
<i class="fas fa-blog feedback-icon"></i>
|
||||
博客
|
||||
</a>
|
||||
<a href="https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng" target="_blank" rel="noopener" class="feedback-link mini">
|
||||
<i class="fas fa-server feedback-icon"></i>
|
||||
部署
|
||||
</a>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-card class="box-card">
|
||||
<div style="text-align: right">
|
||||
<DarkMode @theme-change="handleThemeChange" />
|
||||
</div>
|
||||
<div class="demo-basic--circle">
|
||||
<div class="block" style="text-align: center;">
|
||||
<img :height="150" src="../../public/images/lanzou111.png" alt="lz"></img>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 项目简介移到卡片内 -->
|
||||
<div class="project-intro">
|
||||
<div class="intro-title">NFD网盘直链解析0.1.9_bate6</div>
|
||||
<div class="intro-desc">
|
||||
<div>支持网盘:蓝奏云、蓝奏云优享、小飞机盘、123云盘、奶牛快传、移动云空间、亿方云、文叔叔、QQ邮箱文件中转站等</div>
|
||||
<div>文件夹解析支持:蓝奏云、蓝奏云优享、小飞机盘、123云盘</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="typo">
|
||||
<p>节点1: 回源请求数:{{ node1Info.parserTotal }}, 缓存请求数:{{ node1Info.cacheTotal }}, 总数:{{ node1Info.total }}</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="main" v-loading="isLoading">
|
||||
<div class="grid-content">
|
||||
<!-- 开关按钮,控制是否自动读取剪切板 -->
|
||||
<el-switch v-model="autoReadClipboard" active-text="自动识别剪切板"></el-switch>
|
||||
|
||||
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url">
|
||||
<template #prepend>分享链接</template>
|
||||
<template #append v-if="!autoReadClipboard">
|
||||
<el-button @click="getPaste(true)">读取剪切板</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-input placeholder="请输入密码" v-model="password" id="url">
|
||||
<template #prepend>分享密码</template>
|
||||
</el-input>
|
||||
|
||||
<el-input v-show="directLink" :value="directLink" id="url">
|
||||
<template #prepend>智能直链</template>
|
||||
<template #append>
|
||||
<el-button v-clipboard:copy="directLink" v-clipboard:success="onCopy" v-clipboard:error="onError">
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<p style="text-align: center">
|
||||
<el-button style="margin-left: 40px" @click="parseFile">解析文件</el-button>
|
||||
<el-button style="margin-left: 20px" @click="parseDirectory">解析目录</el-button>
|
||||
<el-button style="margin-left: 20px" @click="generateMarkdown">生成Markdown</el-button>
|
||||
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</el-button>
|
||||
<el-button style="margin-left: 20px" @click="getStatistics">分享统计</el-button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 解析结果 -->
|
||||
<div v-if="parseResult.code" style="margin-top: 10px">
|
||||
<strong>解析结果: </strong>
|
||||
<json-viewer :value="parseResult" :expand-depth="5" copyable boxed sort />
|
||||
<!-- 文件信息美化展示区 -->
|
||||
<div v-if="downloadUrl" class="file-meta-info-card">
|
||||
<div class="file-meta-row">
|
||||
<span class="file-meta-label">下载链接:</span>
|
||||
<a :href="downloadUrl" target="_blank" class="file-meta-link">{{ downloadUrl }}</a>
|
||||
</div>
|
||||
<div class="file-meta-row" v-if="parseResult.data?.downloadShortUrl">
|
||||
<span class="file-meta-label">下载短链:</span>
|
||||
<a :href="parseResult.data.downloadShortUrl" target="_blank" class="file-meta-link">{{ parseResult.data.downloadShortUrl }}</a>
|
||||
</div>
|
||||
<div class="file-meta-row">
|
||||
<span class="file-meta-label">文件预览:</span>
|
||||
<a :href="previewBaseUrl + encodeURIComponent(downloadUrl)" target="_blank" class="file-meta-link">点击预览</a>
|
||||
</div>
|
||||
<div class="file-meta-row">
|
||||
<span class="file-meta-label">文件名:</span>{{ extractFileNameAndExt(downloadUrl).name }}
|
||||
</div>
|
||||
<div class="file-meta-row">
|
||||
<span class="file-meta-label">文件类型:</span>{{ getFileTypeClass({ fileName: extractFileNameAndExt(downloadUrl).name }) }}
|
||||
</div>
|
||||
<div class="file-meta-row" v-if="parseResult.data?.sizeStr">
|
||||
<span class="file-meta-label">文件大小:</span>{{ parseResult.data.sizeStr }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Markdown链接 -->
|
||||
<div v-if="markdownText" style="text-align: center">
|
||||
<el-input :value="markdownText" readonly>
|
||||
<template #append>
|
||||
<el-button v-clipboard:copy="markdownText" v-clipboard:success="onCopy" v-clipboard:error="onError">
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 二维码 -->
|
||||
<div style="text-align: center" v-show="showQRCode">
|
||||
<canvas ref="qrcodeCanvas"></canvas>
|
||||
<div style="text-align: center">
|
||||
<el-link target="_blank" :href="qrCodeUrl">{{ qrCodeUrl }}</el-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div v-if="statisticsData.shareLinkInfo">
|
||||
<el-descriptions class="margin-top" title="分享详情" :column="1" border>
|
||||
<template slot="extra">
|
||||
<el-button type="primary" size="small">操作</el-button>
|
||||
</template>
|
||||
<el-descriptions-item label="网盘名称">{{ statisticsData.shareLinkInfo.panName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="网盘标识">{{ statisticsData.shareLinkInfo.type }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分享Key">{{ statisticsData.shareLinkInfo.shareKey }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分享链接">
|
||||
<el-link target="_blank" :href="statisticsData.shareLinkInfo.shareUrl">{{ statisticsData.shareLinkInfo.shareUrl }}</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="jsonApi链接">
|
||||
<el-link target="_blank" :href="statisticsData.apiLink">{{ statisticsData.apiLink }}</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="302下载链接">
|
||||
<el-link target="_blank" :href="statisticsData.downLink">{{ statisticsData.downLink }}</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="解析次数">{{ statisticsData.parserTotal }}</el-descriptions-item>
|
||||
<el-descriptions-item label="缓存命中次数">{{ statisticsData.cacheHitTotal }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总请求次数">{{ statisticsData.sumTotal }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 目录树组件 -->
|
||||
<div v-if="showDirectoryTree" class="directory-tree-container">
|
||||
<div style="margin-bottom: 10px; text-align: right;">
|
||||
<el-radio-group v-model="directoryViewMode" size="small">
|
||||
<el-radio-button label="pane">窗格</el-radio-button>
|
||||
<el-radio-button label="tree">文件树</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<DirectoryTree
|
||||
:file-list="directoryData"
|
||||
:share-url="link"
|
||||
:password="password"
|
||||
:view-mode="directoryViewMode"
|
||||
@file-click="handleFileClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-row>
|
||||
<!-- 文件解析结果区下方加分享按钮 -->
|
||||
<div v-if="parseResult.code && downloadUrl" style="margin-top: 10px; text-align: right;">
|
||||
<el-button type="primary" @click="copyShowFileLink">分享文件直链</el-button>
|
||||
</div>
|
||||
<!-- 目录解析结果区下方加分享按钮 -->
|
||||
<div v-if="showDirectoryTree && directoryData.length" style="margin-top: 10px; text-align: right;">
|
||||
<el-button type="primary" @click="copyShowListLink">分享目录直链</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import QRCode from 'qrcode'
|
||||
import DarkMode from '@/components/DarkMode'
|
||||
import DirectoryTree from '@/components/DirectoryTree'
|
||||
import parserUrl from '../parserUrl1'
|
||||
import fileTypeUtils from '@/utils/fileTypeUtils'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: { DarkMode, DirectoryTree },
|
||||
mixins: [fileTypeUtils],
|
||||
data() {
|
||||
return {
|
||||
baseAPI: `${location.protocol}//${location.host}`,
|
||||
autoReadClipboard: true,
|
||||
isDarkMode: false,
|
||||
isLoading: false,
|
||||
|
||||
// 输入数据
|
||||
link: "",
|
||||
password: "",
|
||||
|
||||
// 解析结果
|
||||
parseResult: {},
|
||||
downloadUrl: null,
|
||||
directLink: '',
|
||||
previewBaseUrl,
|
||||
|
||||
// 功能结果
|
||||
markdownText: '',
|
||||
showQRCode: false,
|
||||
qrCodeUrl: '',
|
||||
statisticsData: {},
|
||||
|
||||
// 目录树
|
||||
showDirectoryTree: false,
|
||||
directoryData: [],
|
||||
|
||||
// 统计信息
|
||||
node1Info: {},
|
||||
node2Info: {},
|
||||
hasWarnedNoLink: false,
|
||||
directoryViewMode: 'pane', // 新增,目录树展示模式
|
||||
hasClipboardSuccessTip: false, // 新增,聚焦期间只提示一次
|
||||
showRiskDialog: false,
|
||||
baseUrl: location.origin
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 主题切换
|
||||
handleThemeChange(isDark) {
|
||||
this.isDarkMode = isDark
|
||||
},
|
||||
|
||||
// 验证输入
|
||||
validateInput() {
|
||||
this.clearResults()
|
||||
|
||||
if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) {
|
||||
this.$message.error("请输入有效链接!")
|
||||
throw new Error('请输入有效链接')
|
||||
}
|
||||
},
|
||||
|
||||
// 清除结果
|
||||
clearResults() {
|
||||
this.parseResult = {}
|
||||
this.downloadUrl = null
|
||||
this.markdownText = ''
|
||||
this.showQRCode = false
|
||||
this.statisticsData = {}
|
||||
this.showDirectoryTree = false
|
||||
this.directoryData = []
|
||||
},
|
||||
|
||||
// 统一API调用
|
||||
async callAPI(endpoint, params = {}) {
|
||||
try {
|
||||
this.isLoading = true
|
||||
const response = await axios.get(`${this.baseAPI}${endpoint}`, { params })
|
||||
|
||||
if (response.data.code === 200) {
|
||||
// this.$message.success(response.data.msg || '操作成功')
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.data.msg || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '网络错误')
|
||||
throw error
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 文件解析
|
||||
async parseFile() {
|
||||
try {
|
||||
this.validateInput()
|
||||
const params = { url: this.link }
|
||||
if (this.password) params.pwd = this.password
|
||||
|
||||
const result = await this.callAPI('/json/parser', params)
|
||||
this.parseResult = result
|
||||
this.downloadUrl = result.data?.directLink
|
||||
this.directLink = `${this.baseAPI}/parser?url=${this.link}${this.password ? `&pwd=${this.password}` : ''}`
|
||||
this.$message.success('文件解析成功!')
|
||||
} catch (error) {
|
||||
console.error('文件解析失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 目录解析
|
||||
async parseDirectory() {
|
||||
try {
|
||||
this.validateInput()
|
||||
const params = { url: this.link }
|
||||
if (this.password) params.pwd = this.password
|
||||
|
||||
const result = await this.callAPI('/v2/linkInfo', params)
|
||||
const data = result.data
|
||||
|
||||
// 检查是否支持目录解析
|
||||
const supportedPans = ["iz", "lz", "fj", "ye"]
|
||||
if (!supportedPans.includes(data.shareLinkInfo.type)) {
|
||||
this.$message.error("当前网盘不支持目录解析")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取目录数据
|
||||
const directoryResult = await this.callAPI('/v2/getFileList', params)
|
||||
this.directoryData = directoryResult.data || []
|
||||
this.showDirectoryTree = true
|
||||
|
||||
this.$message.success(`目录解析成功!共找到 ${this.directoryData.length} 个文件/文件夹`)
|
||||
} catch (error) {
|
||||
console.error('目录解析失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 生成Markdown
|
||||
async generateMarkdown() {
|
||||
try {
|
||||
this.validateInput()
|
||||
const params = { url: this.link }
|
||||
if (this.password) params.pwd = this.password
|
||||
|
||||
const result = await this.callAPI('/v2/linkInfo', params)
|
||||
this.markdownText = this.buildMarkdown('快速下载地址', result.data.downLink)
|
||||
this.$message.success('Markdown生成成功!')
|
||||
} catch (error) {
|
||||
console.error('生成Markdown失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 生成二维码
|
||||
async generateQRCode() {
|
||||
try {
|
||||
this.validateInput()
|
||||
const params = { url: this.link }
|
||||
if (this.password) params.pwd = this.password
|
||||
|
||||
const result = await this.callAPI('/v2/linkInfo', params)
|
||||
this.qrCodeUrl = result.data.downLink
|
||||
|
||||
const options = {
|
||||
width: 150,
|
||||
height: 150,
|
||||
margin: 2
|
||||
}
|
||||
|
||||
QRCode.toCanvas(this.$refs.qrcodeCanvas, this.qrCodeUrl, options, error => {
|
||||
if (error) console.error(error)
|
||||
})
|
||||
|
||||
this.showQRCode = true
|
||||
this.$message.success('二维码生成成功!')
|
||||
} catch (error) {
|
||||
console.error('生成二维码失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 获取统计信息
|
||||
async getStatistics() {
|
||||
try {
|
||||
this.validateInput()
|
||||
const params = { url: this.link }
|
||||
if (this.password) params.pwd = this.password
|
||||
|
||||
const result = await this.callAPI('/v2/linkInfo', params)
|
||||
this.statisticsData = result.data
|
||||
this.$message.success('统计信息获取成功!')
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 构建Markdown链接
|
||||
buildMarkdown(title, url) {
|
||||
return `[${title}](${url})`
|
||||
},
|
||||
|
||||
// 复制成功
|
||||
onCopy() {
|
||||
this.$message.success('复制成功')
|
||||
},
|
||||
|
||||
// 复制失败
|
||||
onError() {
|
||||
this.$message.error('复制失败')
|
||||
},
|
||||
|
||||
// 文件点击处理
|
||||
handleFileClick(file) {
|
||||
if (file.parserUrl) {
|
||||
window.open(file.parserUrl, '_blank')
|
||||
} else {
|
||||
this.$message.warning('该文件暂无下载链接')
|
||||
}
|
||||
},
|
||||
|
||||
// 获取剪切板内容
|
||||
async getPaste(isManual = false) {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
console.log('获取到的文本内容是:', text)
|
||||
|
||||
const linkInfo = parserUrl.parseLink(text)
|
||||
const pwd = parserUrl.parsePwd(text) || ''
|
||||
|
||||
if (linkInfo.link) {
|
||||
if (linkInfo.link !== this.link || pwd !== this.password) {
|
||||
this.password = pwd
|
||||
this.link = linkInfo.link
|
||||
this.directLink = `${this.baseAPI}/parser?url=${this.link}${this.password ? `&pwd=${this.password}` : ''}`
|
||||
// 聚焦期间只提示一次
|
||||
if (!this.hasClipboardSuccessTip) {
|
||||
this.$message.success(`自动识别分享成功, 网盘类型: ${linkInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`)
|
||||
this.hasClipboardSuccessTip = true
|
||||
}
|
||||
} else {
|
||||
this.$message.warning(`[${linkInfo.name}]分享信息无变化`)
|
||||
}
|
||||
this.hasWarnedNoLink = false // 有效链接后重置
|
||||
} else {
|
||||
if (isManual || !this.hasWarnedNoLink) {
|
||||
this.$message.warning("未能提取到分享链接, 该分享可能尚未支持, 你可以复制任意网盘/音乐App的分享到该页面, 系统智能识别")
|
||||
this.hasWarnedNoLink = true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('读取剪切板失败:', error)
|
||||
this.$message.error('读取剪切板失败,请检查浏览器权限')
|
||||
}
|
||||
},
|
||||
|
||||
// 获取统计信息
|
||||
async getInfo() {
|
||||
try {
|
||||
const response = await axios.get('/v2/statisticsInfo')
|
||||
if (response.data.success) {
|
||||
this.node1Info = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 新增切换目录树展示模式方法
|
||||
setDirectoryViewMode(mode) {
|
||||
this.directoryViewMode = mode
|
||||
},
|
||||
|
||||
// 文件名和类型提取方法(复用 DirectoryTree 的静态方法)
|
||||
extractFileNameAndExt(url) {
|
||||
return fileTypeUtils.extractFileNameAndExt(url)
|
||||
},
|
||||
getFileTypeClass(file) {
|
||||
return fileTypeUtils.getFileTypeClass(file)
|
||||
},
|
||||
ackRisk() {
|
||||
window.localStorage.setItem('nfd_risk_ack', '1')
|
||||
this.showRiskDialog = false
|
||||
},
|
||||
copyShowFileLink() {
|
||||
const url = `${this.baseUrl}/showFile?url=${encodeURIComponent(this.downloadUrl)}`
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
ElMessage.success('文件分享链接已复制!')
|
||||
})
|
||||
},
|
||||
copyShowListLink() {
|
||||
const url = `${this.baseUrl}/showList?url=${encodeURIComponent(this.link)}`
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
ElMessage.success('目录分享链接已复制!')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 从localStorage读取设置
|
||||
const savedAutoRead = window.localStorage.getItem("autoReadClipboard")
|
||||
if (savedAutoRead !== null) {
|
||||
this.autoReadClipboard = savedAutoRead === 'true'
|
||||
}
|
||||
|
||||
// 获取初始统计信息
|
||||
this.getInfo()
|
||||
|
||||
// 自动读取剪切板
|
||||
if (this.autoReadClipboard) {
|
||||
this.getPaste()
|
||||
}
|
||||
|
||||
// 监听窗口焦点事件
|
||||
window.addEventListener('focus', () => {
|
||||
if (this.autoReadClipboard) {
|
||||
this.hasClipboardSuccessTip = false // 聚焦时重置,只提示一次
|
||||
this.getPaste()
|
||||
}
|
||||
})
|
||||
|
||||
// 首次打开页面弹出风险提示
|
||||
if (!window.localStorage.getItem('nfd_risk_ack')) {
|
||||
this.showRiskDialog = true
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
downloadUrl(val) {
|
||||
if (!val) {
|
||||
this.$router.push('/')
|
||||
}
|
||||
},
|
||||
autoReadClipboard(val) {
|
||||
window.localStorage.setItem("autoReadClipboard", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #f5f7fa;
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body.dark-theme {
|
||||
background-color: #181818;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#app {
|
||||
/* 不设置 background-color */
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
#app.dark-theme {
|
||||
/* 不设置 background-color */
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.box-card {
|
||||
margin-top: 4em !important;
|
||||
margin-bottom: 4em !important;
|
||||
opacity: 1 !important; /* 只要不透明 */
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
|
||||
border: none;
|
||||
}
|
||||
|
||||
#app.dark-theme .box-card {
|
||||
background: #232323 !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
border: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.box-card {
|
||||
margin-top: 1em !important;
|
||||
margin-bottom: 1em !important;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
margin-top: 1em;
|
||||
border-radius: 4px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.el-select .el-input {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.directory-tree-container {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
#app.dark-theme .directory-tree-container {
|
||||
background-color: #2d2d2d;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.download h3 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.download button {
|
||||
margin-right: 0.5em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.typo {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.typo a {
|
||||
color: #0077ff;
|
||||
}
|
||||
|
||||
#app.dark-theme .typo a {
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 10px;
|
||||
margin-bottom: .8em;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
#app.dark-theme hr {
|
||||
border-bottom-color: rgba(255, 255, 255, .12);
|
||||
}
|
||||
|
||||
.feedback-bar {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
padding: 10px 10px 0 0;
|
||||
}
|
||||
.feedback-link {
|
||||
color: #888;
|
||||
font-weight: 500;
|
||||
font-size: 0.98rem;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 2px 10px;
|
||||
background: transparent;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.feedback-link:first-child { margin-left: 0; }
|
||||
.feedback-link:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
.dark-theme .feedback-link {
|
||||
background: transparent;
|
||||
color: #bbb;
|
||||
border: none;
|
||||
}
|
||||
.dark-theme .feedback-link:hover {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
.feedback-link.mini {
|
||||
font-size: 0.92rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.feedback-icon {
|
||||
font-size: 1em;
|
||||
color: #888;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.feedback-link:hover .feedback-icon {
|
||||
color: #333;
|
||||
}
|
||||
.feedback-link:nth-child(2) .feedback-icon { color: #333; }
|
||||
.feedback-link:nth-child(3) .feedback-icon { color: #f39c12; }
|
||||
.dark-theme .feedback-icon {
|
||||
color: #bbb;
|
||||
}
|
||||
.dark-theme .feedback-link:nth-child(2) .feedback-icon { color: #fff; }
|
||||
.dark-theme .feedback-link:nth-child(3) .feedback-icon { color: #f7ca77; }
|
||||
.feedback-link:nth-child(4) .feedback-icon { color: #409eff; }
|
||||
.dark-theme .feedback-link:nth-child(4) .feedback-icon { color: #4a9eff; }
|
||||
|
||||
.project-intro {
|
||||
margin: 0 auto 18px auto;
|
||||
max-width: 700px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-size: 1.02rem;
|
||||
}
|
||||
.intro-title {
|
||||
font-size: 1.18rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
color: #666;
|
||||
}
|
||||
.intro-desc {
|
||||
color: #888;
|
||||
font-size: 0.98rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.dark-theme .project-intro, .dark-theme .intro-title, .dark-theme .intro-desc {
|
||||
color: #bbb;
|
||||
}
|
||||
.dark-theme .intro-title {
|
||||
color: #eee;
|
||||
}
|
||||
.file-meta-info-card {
|
||||
margin: 18px auto 0 auto;
|
||||
max-width: 600px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
padding: 18px 24px 12px 24px;
|
||||
font-size: 1.02rem;
|
||||
color: #333;
|
||||
}
|
||||
#app.dark-theme .file-meta-info-card {
|
||||
background: #232323;
|
||||
color: #eee;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||
}
|
||||
.file-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.01em;
|
||||
}
|
||||
.file-meta-label {
|
||||
min-width: 90px;
|
||||
color: #888;
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
}
|
||||
#app.dark-theme .file-meta-label {
|
||||
color: #bbb;
|
||||
}
|
||||
.file-meta-link {
|
||||
color: #409eff;
|
||||
word-break: break-all;
|
||||
text-decoration: underline;
|
||||
}
|
||||
#app.dark-theme .file-meta-link {
|
||||
color: #4a9eff;
|
||||
}
|
||||
#app.dark-theme .jv-container {
|
||||
background: #232323 !important;
|
||||
color: #eee !important;
|
||||
border-color: #444 !important;
|
||||
}
|
||||
#app.dark-theme .jv-key {
|
||||
color: #4a9eff !important;
|
||||
}
|
||||
#app.dark-theme .jv-number {
|
||||
color: #f39c12 !important;
|
||||
}
|
||||
#app.dark-theme .jv-string {
|
||||
color: #27ae60 !important;
|
||||
}
|
||||
#app.dark-theme .jv-boolean {
|
||||
color: #e67e22 !important;
|
||||
}
|
||||
#app.dark-theme .jv-null {
|
||||
color: #e74c3c !important;
|
||||
}
|
||||
#app.jv-container .jv-item.jv-object {
|
||||
color: #32ba6d;
|
||||
}
|
||||
</style>
|
||||
118
web-front/src/views/ShowFile.vue
Normal file
118
web-front/src/views/ShowFile.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="show-file-page">
|
||||
<div v-if="loading" style="text-align:center;margin-top:40px;">加载中...</div>
|
||||
<div v-else-if="error" style="color:red;text-align:center;margin-top:40px;">{{ error }}</div>
|
||||
<div v-else>
|
||||
<div v-if="parseResult.code">
|
||||
<div class="file-meta-info-card">
|
||||
<div class="file-meta-row">
|
||||
<span class="file-meta-label">下载链接:</span>
|
||||
<a :href="downloadUrl" target="_blank" class="file-meta-link">{{ downloadUrl }}</a>
|
||||
</div>
|
||||
<div class="file-meta-row">
|
||||
<span class="file-meta-label">文件名:</span>{{ fileTypeUtils.extractFileNameAndExt(downloadUrl).name }}
|
||||
</div>
|
||||
<div class="file-meta-row">
|
||||
<span class="file-meta-label">文件类型:</span>{{ fileTypeUtils.getFileTypeClass({ fileName: fileTypeUtils.extractFileNameAndExt(downloadUrl).name }) }}
|
||||
</div>
|
||||
<div class="file-meta-row" v-if="parseResult.data?.sizeStr">
|
||||
<span class="file-meta-label">文件大小:</span>{{ parseResult.data.sizeStr }}
|
||||
</div>
|
||||
<div class="file-meta-row">
|
||||
<span class="file-meta-label">在线预览:</span>
|
||||
<a :href="previewBaseUrl + encodeURIComponent(downloadUrl)" target="_blank" class="preview-btn">点击在线预览</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="text-align:center;margin-top:40px;">未获取到有效解析结果</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import fileTypeUtils from '@/utils/fileTypeUtils'
|
||||
import { previewBaseUrl } from '@/views/Home.vue'
|
||||
|
||||
export default {
|
||||
name: 'ShowFile',
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
error: '',
|
||||
parseResult: {},
|
||||
downloadUrl: '',
|
||||
fileTypeUtils,
|
||||
previewBaseUrl
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchFile() {
|
||||
const url = this.$route.query.url
|
||||
if (!url) {
|
||||
this.error = '缺少 url 参数'
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await axios.get('/json/parser', { params: { url } })
|
||||
this.parseResult = res.data
|
||||
this.downloadUrl = res.data.data?.directLink
|
||||
} catch (e) {
|
||||
this.error = '解析失败'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchFile()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.show-file-page {
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
}
|
||||
.file-meta-info-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
padding: 18px 24px 12px 24px;
|
||||
font-size: 1.02rem;
|
||||
color: #333;
|
||||
}
|
||||
.file-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.01em;
|
||||
}
|
||||
.file-meta-label {
|
||||
min-width: 90px;
|
||||
color: #888;
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.file-meta-link {
|
||||
color: #409eff;
|
||||
word-break: break-all;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.preview-btn {
|
||||
display: inline-block;
|
||||
padding: 4px 16px;
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.preview-btn:hover {
|
||||
background: #1867c0;
|
||||
}
|
||||
</style>
|
||||
107
web-front/src/views/ShowList.vue
Normal file
107
web-front/src/views/ShowList.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="show-list-page">
|
||||
<div class="list-title-wrap">
|
||||
<h2 class="list-title">{{ url }} 目录</h2>
|
||||
<div class="list-subtitle">
|
||||
<a :href="url" target="_blank">原始分享链接</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;margin-bottom:12px;">
|
||||
<DarkMode @theme-change="toggleTheme" style="float: left;"/>
|
||||
<el-radio-group v-model="viewMode" size="small" style="margin-left:20px;">
|
||||
<el-radio-button label="pane">窗格</el-radio-button>
|
||||
<el-radio-button label="tree">目录树</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div v-if="loading" style="text-align:center;margin-top:40px;">加载中...</div>
|
||||
<div v-else-if="error" style="color:red;text-align:center;margin-top:40px;">{{ error }}</div>
|
||||
<div v-else>
|
||||
<DirectoryTree
|
||||
:file-list="directoryData"
|
||||
:share-url="url"
|
||||
:password="''"
|
||||
:view-mode="viewMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import DirectoryTree from '@/components/DirectoryTree'
|
||||
import DarkMode from '@/components/DarkMode'
|
||||
|
||||
export default {
|
||||
name: 'ShowList',
|
||||
components: { DirectoryTree, DarkMode },
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
error: '',
|
||||
directoryData: [],
|
||||
url: '',
|
||||
viewMode: 'pane'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchList() {
|
||||
this.url = this.$route.query.url
|
||||
if (!this.url) {
|
||||
this.error = '缺少 url 参数'
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await axios.get('/v2/getFileList', { params: { url: this.url } })
|
||||
this.directoryData = res.data.data || []
|
||||
} catch (e) {
|
||||
this.error = '目录解析失败'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
toggleTheme(isDark) {
|
||||
if (isDark) {
|
||||
document.body.classList.add('dark-theme')
|
||||
document.documentElement.classList.add('dark-theme')
|
||||
} else {
|
||||
document.body.classList.remove('dark-theme')
|
||||
document.documentElement.classList.remove('dark-theme')
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchList()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.show-list-page {
|
||||
max-width: 900px;
|
||||
margin: 40px auto;
|
||||
}
|
||||
.list-title-wrap {
|
||||
text-align: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.list-title {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
margin-bottom: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.list-subtitle {
|
||||
font-size: 1.05rem;
|
||||
color: #888;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.list-subtitle a {
|
||||
color: #409eff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.list-subtitle a:hover {
|
||||
color: #1867c0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
# 网盘分享链接云解析服务 API 测试
|
||||
# 本文件包含了系统所有API接口的测试请求
|
||||
# 使用方法:
|
||||
# 1. 先运行登录接口获取token
|
||||
# 2. 将返回的token替换所有请求中的YOUR_TOKEN_HERE
|
||||
# 3. 对于需要ID的请求,将实际ID替换TOKEN_ID
|
||||
|
||||
### 用户接口 ###
|
||||
|
||||
### 登录接口
|
||||
POST http://localhost:6400/api/user/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
|
||||
### 用户注册
|
||||
POST http://localhost:6400/api/user/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"email": "testuser@example.com",
|
||||
"phone": "13800138000"
|
||||
}
|
||||
|
||||
### 获取用户信息
|
||||
# 使用登录接口返回的token替换下面的YOUR_TOKEN_HERE
|
||||
GET http://localhost:6400/api/user/info
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
|
||||
|
||||
### 验证Token
|
||||
POST http://localhost:6400/api/user/validate-token
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc"
|
||||
}
|
||||
|
||||
### 更新用户信息
|
||||
PUT http://localhost:6400/api/user/update
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
|
||||
|
||||
{
|
||||
"email": "new-email@example.com",
|
||||
"phone": "13900139000",
|
||||
"avatar": "https://example.com/avatar.jpg"
|
||||
}
|
||||
|
||||
### 管理员接口 ###
|
||||
|
||||
### 获取所有网盘Token
|
||||
GET http://localhost:6400/api/admin/tokens
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
|
||||
|
||||
### 添加网盘Token
|
||||
POST http://localhost:6400/api/admin/token
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer YOUR_TOKEN_HERE
|
||||
|
||||
{
|
||||
"type": "yidong",
|
||||
"description": "移动云盘token",
|
||||
"token": "abc123xyz456"
|
||||
}
|
||||
|
||||
### 获取单个网盘Token
|
||||
# 替换下面的TOKEN_ID为实际的token ID
|
||||
GET http://localhost:6400/api/admin/token/TOKEN_ID
|
||||
Authorization: Bearer YOUR_TOKEN_HERE
|
||||
|
||||
### 更新网盘Token
|
||||
# 替换下面的TOKEN_ID为实际的token ID
|
||||
PUT http://localhost:6400/api/admin/token/TOKEN_ID
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer YOUR_TOKEN_HERE
|
||||
|
||||
{
|
||||
"description": "更新后的描述",
|
||||
"token": "new-token-value"
|
||||
}
|
||||
|
||||
### 删除网盘Token
|
||||
# 替换下面的TOKEN_ID为实际的token ID
|
||||
DELETE http://localhost:6400/api/admin/token/TOKEN_ID
|
||||
Authorization: Bearer YOUR_TOKEN_HERE
|
||||
@@ -0,0 +1,25 @@
|
||||
POST https://login.123pan.com/api/user/sign_in
|
||||
Accept: application/json, text/plain, */*
|
||||
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
|
||||
App-Version: 3
|
||||
Connection: keep-alive
|
||||
Content-Type: application/json
|
||||
LoginUuid: 694eff443c1896851f0fa32abbb8c6ec69a422aa21721f4556d1e9f07a568bee
|
||||
Referer: https://login.123pan.com/
|
||||
Sec-Fetch-Dest: empty
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Site: same-origin
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0
|
||||
platform: web
|
||||
sec-ch-ua: "Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"
|
||||
sec-ch-ua-mobile: ?0
|
||||
sec-ch-ua-platform: "macOS"
|
||||
|
||||
{
|
||||
"passport": "",
|
||||
"password": "",
|
||||
"remember": true
|
||||
}
|
||||
|
||||
###
|
||||
POST http://
|
||||
Reference in New Issue
Block a user