Compare commits

...

3 Commits

Author SHA1 Message Date
q
0579588814 优化构建流v0.1.9b6b 2025-07-10 19:17:22 +08:00
q
df2bfb6ac7 优化构建流 2025-07-10 19:12:15 +08:00
q
517b6f8910 del 2025-07-10 19:07:02 +08:00
10 changed files with 2229 additions and 35 deletions

View File

@@ -1,34 +0,0 @@
name: Update Release Badge
on:
push:
tags:
- 'v*' # 可按需调整
jobs:
update-badge:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Get latest tag
id: get_tag
run: echo "tag_name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Update README badge
run: |
TAG=${{ steps.get_tag.outputs.tag_name }}
BADGE="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=$TAG"
echo "Using badge: $BADGE"
# 替换 README 中 badge 行(标记行需特殊注释)
sed -i -E "s#(!\[release-badge\]\(.*\))#![release-badge]($BADGE)#" README.md
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "🔄 update release badge for ${{ steps.get_tag.outputs.tag_name }}" || echo "No changes"
git push

View 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>

View File

@@ -9,7 +9,7 @@ import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import "vue3-json-viewer/dist/index.css";
import './styles/dark/css-vars.css'
import router from './router'
import router from './router/index.js'
const app = Vue.createApp(App)
app.use(router)

View 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

View 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

View 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>

View 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>

View 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>

View File

@@ -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

View File

@@ -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://