优化构建流v0.1.9b6b

This commit is contained in:
q
2025-07-10 19:17:22 +08:00
parent df2bfb6ac7
commit 0579588814
9 changed files with 2229 additions and 1 deletions

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>