更新代码和文档

This commit is contained in:
q
2026-01-03 21:11:04 +08:00
parent 48aa5b6148
commit d8f0dc4f8e
25 changed files with 789 additions and 161 deletions

View File

@@ -23,6 +23,8 @@ import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.*; import io.vertx.ext.web.handler.*;
import io.vertx.ext.web.handler.sockjs.SockJSHandler; import io.vertx.ext.web.handler.sockjs.SockJSHandler;
import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions; import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions;
import io.vertx.ext.web.sstore.LocalSessionStore;
import io.vertx.ext.web.sstore.SessionStore;
import javassist.CtClass; import javassist.CtClass;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@@ -98,6 +100,16 @@ public class RouterHandlerFactory implements BaseHttpApi {
// 配置文件上传路径 // 配置文件上传路径
mainRouter.route().handler(BodyHandler.create().setUploadsDirectory("uploads")); mainRouter.route().handler(BodyHandler.create().setUploadsDirectory("uploads"));
// 配置Session管理 - 用于演练场登录状态持久化
// 30天过期时间毫秒
SessionStore sessionStore = LocalSessionStore.create(VertxHolder.getVertxInstance());
SessionHandler sessionHandler = SessionHandler.create(sessionStore)
.setSessionTimeout(30L * 24 * 60 * 60 * 1000) // 30天
.setSessionCookieName("SESSIONID") // Cookie名称
.setCookieHttpOnlyFlag(true) // 防止XSS攻击
.setCookieSecureFlag(false); // 非HTTPS环境设置为false
mainRouter.route().handler(sessionHandler);
// 拦截器 // 拦截器
Set<Handler<RoutingContext>> interceptorSet = getInterceptorSet(); Set<Handler<RoutingContext>> interceptorSet = getInterceptorSet();
Route route0 = mainRouter.route("/*"); Route route0 = mainRouter.route("/*");

View File

@@ -29,7 +29,7 @@ import java.util.regex.Pattern;
*/ */
public class LzTool extends PanBase { public class LzTool extends PanBase {
public static final String SHARE_URL_PREFIX = "https://wwww.lanzoum.com"; public static final String SHARE_URL_PREFIX = "https://wwwwp.lanzoup.com";
MultiMap headers0 = HeaderUtils.parseHeaders(""" MultiMap headers0 = HeaderUtils.parseHeaders("""
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate Accept-Encoding: gzip, deflate

View File

@@ -4,7 +4,7 @@
### 1. NPM包安装 ### 1. NPM包安装
已在 `package.json` 中安装: 已在 `package.json` 中安装:
- `monaco-editor`: ^0.45.0 - Monaco Editor核心包 - `monaco-editor`: ^0.55.1 - Monaco Editor核心包
- `@monaco-editor/loader`: ^1.4.0 - Monaco Editor加载器 - `@monaco-editor/loader`: ^1.4.0 - Monaco Editor加载器
- `monaco-editor-webpack-plugin`: ^7.1.1 - Webpack打包插件devDependencies - `monaco-editor-webpack-plugin`: ^7.1.1 - Webpack打包插件devDependencies
@@ -172,3 +172,5 @@ Monaco Editor 打包后会增加构建产物大小约2-3MB但这是正

View File

@@ -16,7 +16,7 @@
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"element-plus": "2.11.3", "element-plus": "2.11.3",
"monaco-editor": "^0.45.0", "monaco-editor": "^0.55.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"splitpanes": "^4.0.4", "splitpanes": "^4.0.4",
"vue": "^3.5.12", "vue": "^3.5.12",

View File

@@ -9,8 +9,8 @@
content="Netdisk fast download,网盘直链解析工具"> content="Netdisk fast download,网盘直链解析工具">
<meta name="description" <meta name="description"
content="Netdisk fast download 网盘直链解析工具"> content="Netdisk fast download 网盘直链解析工具">
<!-- Font Awesome 图标库 --> <!-- Font Awesome 图标库 - 使用国内CDN -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style> <style>
.page-loading-wrap { .page-loading-wrap {
padding: 120px; padding: 120px;
@@ -154,11 +154,26 @@
} }
</style> </style>
<script> <script>
const saved = localStorage.getItem('isDarkMode') === 'true' // 等待DOM加载完成后再操作
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches (function() {
if (saved || (!saved && systemDark)) { function applyDarkTheme() {
document.body.classList.add('dark-theme') const body = document.body;
if (body && body.classList) {
// 只在用户明确选择暗色模式时才应用,不自动检测系统偏好
if (localStorage.getItem('isDarkMode') === 'true') {
body.classList.add('dark-theme')
} }
}
}
// 如果DOM已加载立即执行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', applyDarkTheme);
} else {
// DOM已加载立即执行
applyDarkTheme();
}
})();
</script> </script>
</head> </head>
<body> <body>

View File

@@ -43,12 +43,16 @@ watch(darkMode, (newValue) => {
emit('theme-change', newValue) emit('theme-change', newValue)
// 应用主题到body // 应用主题到body
const html = document.documentElement;
const body = document.body;
if (html && body && html.classList && body.classList) {
if (newValue) { if (newValue) {
document.body.classList.add('dark-theme') body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme') html.classList.add('dark-theme')
} else { } else {
document.body.classList.remove('dark-theme') body.classList.remove('dark-theme')
document.documentElement.classList.remove('dark-theme') html.classList.remove('dark-theme')
}
} }
}) })
@@ -57,9 +61,11 @@ onMounted(() => {
emit('theme-change', darkMode.value) emit('theme-change', darkMode.value)
// 应用初始主题 // 应用初始主题
if (darkMode.value) { const html = document.documentElement;
document.body.classList.add('dark-theme') const body = document.body;
document.documentElement.classList.add('dark-theme') if (html && body && html.classList && body.classList && darkMode.value) {
body.classList.add('dark-theme')
html.classList.add('dark-theme')
} }
}) })
</script> </script>

View File

@@ -388,8 +388,14 @@ export default {
return date.toLocaleString('zh-CN') return date.toLocaleString('zh-CN')
}, },
checkTheme() { checkTheme() {
this.isDarkTheme = document.body.classList.contains('dark-theme') || const html = document.documentElement;
document.documentElement.classList.contains('dark-theme') const body = document.body;
if (html && body && html.classList && body.classList) {
this.isDarkTheme = body.classList.contains('dark-theme') ||
html.classList.contains('dark-theme')
} else {
this.isDarkTheme = false;
}
}, },
renderContent(h, { node, data, store }) { renderContent(h, { node, data, store }) {
const isFolder = data.fileType === 'folder' const isFolder = data.fileType === 'folder'

View File

@@ -94,6 +94,14 @@ export default {
return; return;
} }
// 配置Monaco Editor使用国内CDN (npmmirror)
// npmmirror的路径格式: https://registry.npmmirror.com/包名/版本号/files/文件路径
loader.config({
paths: {
vs: 'https://registry.npmmirror.com/monaco-editor/0.55.1/files/min/vs'
}
});
// 初始化Monaco Editor // 初始化Monaco Editor
monaco = await loader.init(); monaco = await loader.init();

View File

@@ -10,7 +10,13 @@ const routes = [
{ path: '/showFile', component: ShowFile }, { path: '/showFile', component: ShowFile },
{ path: '/showList', component: ShowList }, { path: '/showList', component: ShowList },
{ path: '/clientLinks', component: ClientLinks }, { path: '/clientLinks', component: ClientLinks },
{ path: '/playground', component: Playground } { path: '/playground', component: Playground },
// 404页面 - 必须放在最后
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
] ]
const router = createRouter({ const router = createRouter({

View File

@@ -1,5 +1,10 @@
import axios from 'axios'; import axios from 'axios';
// 创建axios实例配置携带cookie
const axiosInstance = axios.create({
withCredentials: true // 重要允许跨域请求携带cookie
});
/** /**
* 演练场API服务 * 演练场API服务
*/ */
@@ -10,7 +15,7 @@ export const playgroundApi = {
*/ */
async getStatus() { async getStatus() {
try { try {
const response = await axios.get('/v2/playground/status'); const response = await axiosInstance.get('/v2/playground/status');
return response.data; return response.data;
} catch (error) { } catch (error) {
throw new Error(error.response?.data?.error || error.message || '获取状态失败'); throw new Error(error.response?.data?.error || error.message || '获取状态失败');
@@ -24,7 +29,7 @@ export const playgroundApi = {
*/ */
async login(password) { async login(password) {
try { try {
const response = await axios.post('/v2/playground/login', { password }); const response = await axiosInstance.post('/v2/playground/login', { password });
return response.data; return response.data;
} catch (error) { } catch (error) {
throw new Error(error.response?.data?.error || error.message || '登录失败'); throw new Error(error.response?.data?.error || error.message || '登录失败');
@@ -41,7 +46,7 @@ export const playgroundApi = {
*/ */
async testScript(jsCode, shareUrl, pwd = '', method = 'parse') { async testScript(jsCode, shareUrl, pwd = '', method = 'parse') {
try { try {
const response = await axios.post('/v2/playground/test', { const response = await axiosInstance.post('/v2/playground/test', {
jsCode, jsCode,
shareUrl, shareUrl,
pwd, pwd,
@@ -69,7 +74,7 @@ export const playgroundApi = {
*/ */
async getTypesJs() { async getTypesJs() {
try { try {
const response = await axios.get('/v2/playground/types.js', { const response = await axiosInstance.get('/v2/playground/types.js', {
responseType: 'text' responseType: 'text'
}); });
return response.data; return response.data;
@@ -83,7 +88,7 @@ export const playgroundApi = {
*/ */
async getParserList() { async getParserList() {
try { try {
const response = await axios.get('/v2/playground/parsers'); const response = await axiosInstance.get('/v2/playground/parsers');
// 框架会自动包装成JsonResult需要从data字段获取 // 框架会自动包装成JsonResult需要从data字段获取
if (response.data && response.data.data) { if (response.data && response.data.data) {
return { return {
@@ -104,7 +109,7 @@ export const playgroundApi = {
*/ */
async saveParser(jsCode) { async saveParser(jsCode) {
try { try {
const response = await axios.post('/v2/playground/parsers', { jsCode }); const response = await axiosInstance.post('/v2/playground/parsers', { jsCode });
// 框架会自动包装成JsonResult // 框架会自动包装成JsonResult
if (response.data && response.data.data) { if (response.data && response.data.data) {
return { return {
@@ -130,7 +135,7 @@ export const playgroundApi = {
*/ */
async updateParser(id, jsCode, enabled = true) { async updateParser(id, jsCode, enabled = true) {
try { try {
const response = await axios.put(`/v2/playground/parsers/${id}`, { jsCode, enabled }); const response = await axiosInstance.put(`/v2/playground/parsers/${id}`, { jsCode, enabled });
return response.data; return response.data;
} catch (error) { } catch (error) {
throw new Error(error.response?.data?.error || error.message || '更新解析器失败'); throw new Error(error.response?.data?.error || error.message || '更新解析器失败');
@@ -142,7 +147,7 @@ export const playgroundApi = {
*/ */
async deleteParser(id) { async deleteParser(id) {
try { try {
const response = await axios.delete(`/v2/playground/parsers/${id}`); const response = await axiosInstance.delete(`/v2/playground/parsers/${id}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw new Error(error.response?.data?.error || error.message || '删除解析器失败'); throw new Error(error.response?.data?.error || error.message || '删除解析器失败');
@@ -154,7 +159,7 @@ export const playgroundApi = {
*/ */
async getParserById(id) { async getParserById(id) {
try { try {
const response = await axios.get(`/v2/playground/parsers/${id}`); const response = await axiosInstance.get(`/v2/playground/parsers/${id}`);
// 框架会自动包装成JsonResult // 框架会自动包装成JsonResult
if (response.data && response.data.data) { if (response.data && response.data.data) {
return { return {

View File

@@ -218,7 +218,7 @@
<!-- 版本号显示 --> <!-- 版本号显示 -->
<div class="version-info"> <div class="version-info">
<span class="version-text">内部版本: {{ buildVersion }}</span> <span class="version-text">内部版本: {{ buildVersion }}</span>
<!-- <el-link :href="'/playground'" class="playground-link">JS演练场</el-link>--> <el-link v-if="playgroundEnabled" :href="'/playground'" class="playground-link">脚本演练场</el-link>
</div> </div>
<!-- 文件解析结果区下方加分享按钮 --> <!-- 文件解析结果区下方加分享按钮 -->
@@ -248,6 +248,7 @@ import DirectoryTree from '@/components/DirectoryTree'
import parserUrl from '../parserUrl1' import parserUrl from '../parserUrl1'
import fileTypeUtils from '@/utils/fileTypeUtils' import fileTypeUtils from '@/utils/fileTypeUtils'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { playgroundApi } from '@/utils/playgroundApi'
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src='; export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
@@ -297,7 +298,10 @@ export default {
errorButtonVisible: false, errorButtonVisible: false,
// 版本信息 // 版本信息
buildVersion: '' buildVersion: '',
// 演练场启用状态
playgroundEnabled: false
} }
}, },
methods: { methods: {
@@ -316,7 +320,9 @@ export default {
// 主题切换 // 主题切换
handleThemeChange(isDark) { handleThemeChange(isDark) {
this.isDarkMode = isDark this.isDarkMode = isDark
if (document.body && document.body.classList) {
document.body.classList.toggle('dark-theme', isDark) document.body.classList.toggle('dark-theme', isDark)
}
window.localStorage.setItem('isDarkMode', isDark) window.localStorage.setItem('isDarkMode', isDark)
}, },
@@ -552,6 +558,19 @@ export default {
} }
}, },
// 检查演练场是否启用
async checkPlaygroundEnabled() {
try {
const result = await playgroundApi.getStatus()
if (result && result.data) {
this.playgroundEnabled = result.data.enabled === true
}
} catch (error) {
console.error('检查演练场状态失败:', error)
this.playgroundEnabled = false
}
},
// 新增切换目录树展示模式方法 // 新增切换目录树展示模式方法
setDirectoryViewMode(mode) { setDirectoryViewMode(mode) {
this.directoryViewMode = mode this.directoryViewMode = mode
@@ -656,6 +675,9 @@ export default {
// 获取版本号 // 获取版本号
this.getBuildVersion() this.getBuildVersion()
// 检查演练场是否启用
this.checkPlaygroundEnabled()
// 自动读取剪切板 // 自动读取剪切板
if (this.autoReadClipboard) { if (this.autoReadClipboard) {
this.getPaste() this.getPaste()

View File

@@ -0,0 +1,135 @@
<template>
<div class="not-found-container">
<div class="not-found-content">
<div class="not-found-icon">
<el-icon :size="120"><DocumentDelete /></el-icon>
</div>
<h1 class="not-found-title">404</h1>
<p class="not-found-message">抱歉您访问的页面不存在</p>
<div class="not-found-actions">
<el-button type="primary" @click="goHome">返回首页</el-button>
<el-button @click="goBack">返回上一页</el-button>
</div>
</div>
</div>
</template>
<script>
import { useRouter } from 'vue-router'
import { DocumentDelete } from '@element-plus/icons-vue'
export default {
name: 'NotFound',
components: {
DocumentDelete
},
setup() {
const router = useRouter()
const goHome = () => {
router.push('/')
}
const goBack = () => {
if (window.history.length > 1) {
router.go(-1)
} else {
router.push('/')
}
}
return {
goHome,
goBack
}
}
}
</script>
<style scoped>
.not-found-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
/* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); */
padding: 20px;
}
.not-found-content {
text-align: center;
background: white;
padding: 60px 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 100%;
}
.not-found-icon {
color: #909399;
margin-bottom: 20px;
}
.not-found-title {
font-size: 72px;
font-weight: bold;
color: #303133;
margin: 0 0 20px 0;
line-height: 1;
}
.not-found-message {
font-size: 18px;
color: #606266;
margin: 0 0 40px 0;
}
.not-found-actions {
display: flex;
gap: 15px;
justify-content: center;
}
/* 暗色主题支持 */
.dark-theme .not-found-content {
background: #1d1e1f;
}
.dark-theme .not-found-title {
color: #e5eaf3;
}
.dark-theme .not-found-message {
color: #a3a6ad;
}
.dark-theme .not-found-icon {
color: #6c6e72;
}
/* 移动端适配 */
@media (max-width: 768px) {
.not-found-content {
padding: 40px 20px;
}
.not-found-title {
font-size: 48px;
}
.not-found-message {
font-size: 16px;
}
.not-found-actions {
flex-direction: column;
}
.not-found-actions .el-button {
width: 100%;
}
}
</style>

View File

@@ -21,12 +21,26 @@
<span style="margin-left: 10px;">正在检查访问权限...</span> <span style="margin-left: 10px;">正在检查访问权限...</span>
</div> </div>
<!-- 演练场禁用提示 -->
<div v-if="!loading && !authChecking && !playgroundEnabled" class="playground-auth-overlay">
<div class="playground-auth-card">
<div class="auth-icon" style="color: #f56c6c;">
<el-icon :size="50"><WarningFilled /></el-icon>
</div>
<div class="auth-title">演练场功能已禁用</div>
<div class="auth-subtitle">请联系管理员启用演练场功能</div>
<el-button type="primary" size="large" @click="goHome" class="auth-button">
<span>返回首页</span>
</el-button>
</div>
</div>
<div v-if="shouldShowAuthUI" class="playground-auth-overlay"> <div v-if="shouldShowAuthUI" class="playground-auth-overlay">
<div class="playground-auth-card"> <div class="playground-auth-card">
<div class="auth-icon"> <div class="auth-icon">
<el-icon :size="50"><Lock /></el-icon> <el-icon :size="50"><Lock /></el-icon>
</div> </div>
<div class="auth-title">JS解析器演练场</div> <div class="auth-title">脚本解析器演练场</div>
<div class="auth-subtitle">请输入访问密码</div> <div class="auth-subtitle">请输入访问密码</div>
<el-input <el-input
v-model="inputPassword" v-model="inputPassword"
@@ -56,7 +70,7 @@
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<div class="header-left"> <div class="header-left">
<span class="title">JS解析器演练场</span> <span class="title">脚本解析器演练场</span>
<!-- 语言显示仅支持JavaScript --> <!-- 语言显示仅支持JavaScript -->
<span style="margin-left: 15px; color: var(--el-text-color-secondary); font-size: 12px;"> <span style="margin-left: 15px; color: var(--el-text-color-secondary); font-size: 12px;">
JavaScript (ES5) JavaScript (ES5)
@@ -118,8 +132,133 @@
<el-tabs v-model="activeTab" @tab-change="handleTabChange"> <el-tabs v-model="activeTab" @tab-change="handleTabChange">
<!-- 代码编辑标签页 --> <!-- 代码编辑标签页 -->
<el-tab-pane label="代码编辑" name="editor"> <el-tab-pane label="代码编辑" name="editor">
<Splitpanes :class="['default-theme', isMobile ? 'mobile-vertical' : '']" :horizontal="isMobile" @resized="handleResize"> <!-- 移动端不使用 splitpanes内容自然向下流动 -->
<!-- 编辑器区域 (PC: 左侧, Mobile: 上方) --> <div v-if="isMobile" class="mobile-layout">
<!-- 编辑器区域 -->
<div class="editor-section">
<MonacoEditor
ref="editorRef"
v-model="jsCode"
:theme="editorTheme"
:height="'400px'"
:options="editorOptions"
@change="onCodeChange"
/>
</div>
<!-- 测试参数和结果区域 -->
<div v-if="!collapsedPanels.rightPanel" class="test-section mobile-test-section">
<!-- 测试参数 -->
<el-card class="test-params-card collapsible-card" shadow="never" style="margin-top: 12px">
<template #header>
<div class="card-header-with-collapse">
<span>测试参数</span>
<el-button
text
size="small"
:icon="collapsedPanels.testParams ? 'ArrowDown' : 'ArrowUp'"
@click="togglePanel('testParams')"
/>
</div>
</template>
<transition name="collapse">
<div v-show="!collapsedPanels.testParams">
<el-form :model="testParams" label-width="0px" size="small" class="test-params-form">
<el-form-item label="" class="share-url-item">
<el-input
v-model="testParams.shareUrl"
placeholder="请输入分享链接"
clearable
/>
</el-form-item>
<el-form-item label="" class="password-item">
<el-input
v-model="testParams.pwd"
placeholder="密码(可选)"
clearable
/>
</el-form-item>
<el-form-item label="" class="method-item-horizontal">
<el-radio-group v-model="testParams.method" size="small">
<el-radio label="parse">parse</el-radio>
<el-radio label="parseFileList">parseFileList</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item class="button-item">
<el-button
type="primary"
:loading="testing"
@click="executeTest"
style="width: 100%"
>
执行测试
</el-button>
</el-form-item>
</el-form>
</div>
</transition>
</el-card>
<!-- 执行结果 -->
<el-card class="result-card collapsible-card" shadow="never" style="margin-top: 10px">
<template #header>
<div class="card-header-with-collapse">
<span>执行结果</span>
<el-button
text
size="small"
:icon="collapsedPanels.testResult ? 'ArrowDown' : 'ArrowUp'"
@click="togglePanel('testResult')"
/>
</div>
</template>
<transition name="collapse">
<div v-show="!collapsedPanels.testResult">
<div v-if="testResult" class="result-content">
<el-alert
:type="testResult.success ? 'success' : 'error'"
:title="testResult.success ? '执行成功' : '执行失败'"
:closable="false"
style="margin-bottom: 10px"
/>
<div v-if="testResult.success" class="result-section">
<div class="section-title">结果数据</div>
<div v-if="testResult.result" class="result-debug-box">
<strong>结果内容</strong>{{ testResult.result }}
</div>
<JsonViewer :value="testResult.result" :expand-depth="3" />
</div>
<div v-if="testResult.error" class="result-section">
<div class="section-title">错误信息</div>
<el-alert type="error" :title="testResult.error" :closable="false" />
<div v-if="testResult.stackTrace" class="stack-trace">
<el-collapse>
<el-collapse-item title="查看堆栈信息" name="stack">
<pre>{{ testResult.stackTrace }}</pre>
</el-collapse-item>
</el-collapse>
</div>
</div>
<div v-if="testResult.executionTime" class="result-section">
<div class="section-title">执行时间</div>
<div>{{ testResult.executionTime }}ms</div>
</div>
</div>
<div v-else class="empty-result">
<el-empty description="暂无执行结果" :image-size="80" />
</div>
</div>
</transition>
</el-card>
</div>
</div>
<!-- 桌面端使用 splitpanes -->
<Splitpanes v-else class="default-theme" @resized="handleResize">
<!-- 编辑器区域 (左侧) -->
<Pane :size="collapsedPanels.rightPanel ? 100 : splitSizes[0]" min-size="30" class="editor-pane"> <Pane :size="collapsedPanels.rightPanel ? 100 : splitSizes[0]" min-size="30" class="editor-pane">
<div class="editor-section"> <div class="editor-section">
<MonacoEditor <MonacoEditor
@@ -133,9 +272,9 @@
</div> </div>
</Pane> </Pane>
<!-- 测试参数和结果区域 (PC: 右侧, Mobile: 下方) --> <!-- 测试参数和结果区域 (右侧) -->
<Pane v-if="!collapsedPanels.rightPanel" <Pane v-if="!collapsedPanels.rightPanel"
:size="splitSizes[1]" min-size="20" class="test-pane" :style="isMobile ? 'margin-top: 10px;' : 'margin-left: 10px;'"> :size="splitSizes[1]" min-size="20" class="test-pane" style="margin-left: 10px;">
<div class="test-section"> <div class="test-section">
<!-- 优化的折叠按钮 --> <!-- 优化的折叠按钮 -->
<el-tooltip content="折叠测试面板" placement="left"> <el-tooltip content="折叠测试面板" placement="left">
@@ -331,7 +470,7 @@
<transition name="collapse"> <transition name="collapse">
<div v-show="!collapsedPanels.help"> <div v-show="!collapsedPanels.help">
<div class="help-content"> <div class="help-content">
<h3>什么是JS解析器演练场</h3> <h3>什么是脚本解析器演练场</h3>
<p>演练场允许您快速编写测试和发布JavaScript解析脚本无需重启服务器即可调试和验证解析逻辑</p> <p>演练场允许您快速编写测试和发布JavaScript解析脚本无需重启服务器即可调试和验证解析逻辑</p>
<h3>快速开始</h3> <h3>快速开始</h3>
@@ -459,7 +598,7 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="120" fixed="right">
<template #default="scope"> <template #default="scope">
<el-button size="small" @click="loadParserToEditor(scope.row)">编辑</el-button> <el-button size="small" @click="loadParserToEditor(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteParser(scope.row.id)">删除</el-button> <el-button size="small" type="danger" @click="deleteParser(scope.row.id)">删除</el-button>
@@ -472,26 +611,33 @@
</el-card> </el-card>
<!-- 发布对话框 --> <!-- 发布对话框 -->
<!-- 发布对话框 --> <el-dialog
<el-dialog v-model="publishDialogVisible" title="发布解析器" width="600px"> v-model="publishDialogVisible"
<el-form :model="publishForm" label-width="100px"> title="发布解析器"
:width="isMobile ? '90%' : '600px'"
:close-on-click-modal="false"
class="publish-dialog"
>
<el-form :model="publishForm" :label-width="isMobile ? '80px' : '100px'">
<el-form-item label="脚本代码"> <el-form-item label="脚本代码">
<el-input <el-input
v-model="publishForm.jsCode" v-model="publishForm.jsCode"
type="textarea" type="textarea"
:rows="10" :rows="isMobile ? 8 : 10"
readonly readonly
class="publish-code-textarea"
/> />
</el-form-item> </el-form-item>
<el-alert <el-alert
type="warning" type="warning"
:closable="false" :closable="false"
style="margin-bottom: 20px" style="margin-bottom: 20px"
class="publish-alert"
> >
<template #title> <template #title>
<div> <div class="publish-checklist">
<p>发布前请确保</p> <p style="margin-bottom: 8px; font-weight: 500;">发布前请确保</p>
<ul> <ul style="margin: 0; padding-left: 20px;">
<li>脚本已通过测试</li> <li>脚本已通过测试</li>
<li>元数据信息完整@name, @type, @displayName, @match</li> <li>元数据信息完整@name, @type, @displayName, @match</li>
<li>类型标识@type唯一不与现有解析器冲突</li> <li>类型标识@type唯一不与现有解析器冲突</li>
@@ -502,25 +648,32 @@
</el-alert> </el-alert>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="publishDialogVisible = false">取消</el-button> <div class="dialog-footer-mobile">
<el-button type="primary" :loading="publishing" @click="confirmPublish">确认发布</el-button> <el-button @click="publishDialogVisible = false" :size="isMobile ? 'default' : 'default'">取消</el-button>
<el-button type="primary" :loading="publishing" @click="confirmPublish" :size="isMobile ? 'default' : 'default'">确认发布</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
<!-- 快捷键帮助对话框 --> <!-- 快捷键帮助对话框 -->
<el-dialog v-model="shortcutsDialogVisible" title="⌨️ 快捷键" width="500px"> <el-dialog
<el-table :data="shortcutsData" style="width: 100%" :show-header="false"> v-model="shortcutsDialogVisible"
<el-table-column prop="name" label="功能" width="200" /> title="⌨️ 快捷键"
:width="isMobile ? '90%' : '500px'"
class="shortcuts-dialog"
>
<el-table :data="shortcutsData" style="width: 100%" :show-header="false" class="shortcuts-table">
<el-table-column prop="name" label="功能" :width="isMobile ? 120 : 200" />
<el-table-column prop="keys" label="快捷键"> <el-table-column prop="keys" label="快捷键">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-for="key in row.keys" :key="key" size="small" style="margin-right: 5px;"> <el-tag v-for="key in row.keys" :key="key" size="small" style="margin-right: 5px; margin-bottom: 4px;">
{{ key }} {{ key }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<template #footer> <template #footer>
<el-button type="primary" @click="shortcutsDialogVisible = false">知道了</el-button> <el-button type="primary" @click="shortcutsDialogVisible = false" :size="isMobile ? 'default' : 'default'">知道了</el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
@@ -530,6 +683,7 @@
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'; import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { useMagicKeys, useFullscreen, useEventListener } from '@vueuse/core'; import { useMagicKeys, useFullscreen, useEventListener } from '@vueuse/core';
import { useRouter } from 'vue-router';
import { Splitpanes, Pane } from 'splitpanes'; import { Splitpanes, Pane } from 'splitpanes';
import 'splitpanes/dist/splitpanes.css'; import 'splitpanes/dist/splitpanes.css';
import MonacoEditor from '@/components/MonacoEditor.vue'; import MonacoEditor from '@/components/MonacoEditor.vue';
@@ -546,6 +700,8 @@ export default {
Pane Pane
}, },
setup() { setup() {
const router = useRouter();
// 语言常量 // 语言常量
const LANGUAGE = { const LANGUAGE = {
JAVASCRIPT: 'JavaScript' JAVASCRIPT: 'JavaScript'
@@ -563,12 +719,13 @@ export default {
const inputPassword = ref(''); const inputPassword = ref('');
const authError = ref(''); const authError = ref('');
const authLoading = ref(false); const authLoading = ref(false);
const playgroundEnabled = ref(true); // 演练场是否启用
// ===== 移动端检测 ===== // ===== 移动端检测 =====
const isMobile = ref(false); const isMobile = ref(false);
const testParams = ref({ const testParams = ref({
shareUrl: 'https://lanzoui.com/i7Aq12ab3cd', shareUrl: 'https://example.com/s/abc',
pwd: '', pwd: '',
method: 'parse' method: 'parse'
}); });
@@ -631,7 +788,7 @@ export default {
// @type example_parser // @type example_parser
// @displayName 示例网盘 // @displayName 示例网盘
// @description 使用JavaScript实现的示例解析器 // @description 使用JavaScript实现的示例解析器
// @match https?://example\.com/s/(?<KEY>\w+) // @match https?://example\.com/s/(?<KEY>\\w+)
// @author yourname // @author yourname
// @version 1.0.0 // @version 1.0.0
// ==/UserScript== // ==/UserScript==
@@ -647,7 +804,7 @@ function parse(shareLinkInfo, http, logger) {
var url = shareLinkInfo.getShareUrl(); var url = shareLinkInfo.getShareUrl();
logger.info("开始解析: " + url); logger.info("开始解析: " + url);
var response = http.get(url); var response = http.get('https://example.com');
if (!response.isSuccess()) { if (!response.isSuccess()) {
throw new Error("请求失败: " + response.statusCode()); throw new Error("请求失败: " + response.statusCode());
} }
@@ -700,7 +857,7 @@ function parseById(shareLinkInfo, http, logger) {
// 计算属性:是否需要显示密码输入界面 // 计算属性:是否需要显示密码输入界面
const shouldShowAuthUI = computed(() => { const shouldShowAuthUI = computed(() => {
return !loading.value && !authChecking.value && !authed.value; return !loading.value && !authChecking.value && !authed.value && playgroundEnabled.value;
}); });
// 编辑器配置 // 编辑器配置
@@ -716,7 +873,14 @@ function parseById(shareLinkInfo, http, logger) {
// ===== 移动端检测 ===== // ===== 移动端检测 =====
const updateIsMobile = () => { const updateIsMobile = () => {
const wasMobile = isMobile.value;
isMobile.value = window.innerWidth <= 768; isMobile.value = window.innerWidth <= 768;
// 如果是移动端,调整分栏大小,让测试面板有更多空间
if (isMobile.value && !wasMobile) {
splitSizes.value = [50, 50]; // 移动端编辑器50%测试面板50%
} else if (!isMobile.value && wasMobile) {
splitSizes.value = [70, 30]; // 桌面端编辑器70%测试面板30%
}
}; };
// ===== 进度设置函数 ===== // ===== 进度设置函数 =====
@@ -734,19 +898,59 @@ function parseById(shareLinkInfo, http, logger) {
try { try {
const res = await playgroundApi.getStatus(); const res = await playgroundApi.getStatus();
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
authed.value = res.data.authed || res.data.public; // 检查是否启用
return res.data.authed || res.data.public; playgroundEnabled.value = res.data.enabled === true;
if (!playgroundEnabled.value) {
authChecking.value = false;
return false;
} }
// 先检查localStorage中是否有保存的登录信息
const savedAuth = localStorage.getItem('playground_authed');
const authTime = localStorage.getItem('playground_auth_time');
// 如果30天内登录过直接认为已认证实际认证状态由后端session决定
if (savedAuth === 'true' && authTime) {
const daysSinceAuth = (Date.now() - parseInt(authTime)) / (1000 * 60 * 60 * 24);
if (daysSinceAuth < 30) {
// 先设置为已认证然后验证后端session
authed.value = true;
}
}
const isAuthed = res.data.authed || res.data.public;
authed.value = isAuthed;
// 如果后端session已失效清除localStorage
if (!isAuthed && savedAuth === 'true') {
localStorage.removeItem('playground_authed');
localStorage.removeItem('playground_auth_time');
}
return isAuthed;
}
playgroundEnabled.value = false;
return false; return false;
} catch (error) { } catch (error) {
console.error('检查认证状态失败:', error); console.error('检查认证状态失败:', error);
// 如果错误信息包含"已禁用"则设置启用状态为false
if (error.message && error.message.includes('已禁用')) {
playgroundEnabled.value = false;
} else {
ElMessage.error('检查访问权限失败: ' + error.message); ElMessage.error('检查访问权限失败: ' + error.message);
}
return false; return false;
} finally { } finally {
authChecking.value = false; authChecking.value = false;
} }
}; };
// 返回首页
const goHome = () => {
router.push('/');
};
const submitPassword = async () => { const submitPassword = async () => {
if (!inputPassword.value.trim()) { if (!inputPassword.value.trim()) {
authError.value = '请输入密码'; authError.value = '请输入密码';
@@ -760,6 +964,9 @@ function parseById(shareLinkInfo, http, logger) {
const res = await playgroundApi.login(inputPassword.value); const res = await playgroundApi.login(inputPassword.value);
if (res.code === 200 || res.success) { if (res.code === 200 || res.success) {
authed.value = true; authed.value = true;
// 保存登录信息到localStorage避免每次都需要登录
localStorage.setItem('playground_authed', 'true');
localStorage.setItem('playground_auth_time', Date.now().toString());
ElMessage.success('登录成功'); ElMessage.success('登录成功');
await initPlayground(); await initPlayground();
} else { } else {
@@ -787,7 +994,11 @@ function parseById(shareLinkInfo, http, logger) {
if (saved) { if (saved) {
jsCode.value = saved; jsCode.value = saved;
} else { } else {
// 默认加载示例代码和示例参数
jsCode.value = exampleCode; jsCode.value = exampleCode;
testParams.value.shareUrl = 'https://example.com/s/abc';
testParams.value.pwd = '';
testParams.value.method = 'parse';
} }
setProgress(50, '初始化Monaco Editor类型定义...'); setProgress(50, '初始化Monaco Editor类型定义...');
@@ -800,16 +1011,20 @@ function parseById(shareLinkInfo, http, logger) {
if (savedTheme) { if (savedTheme) {
currentTheme.value = savedTheme; currentTheme.value = savedTheme;
const theme = themes.find(t => t.name === savedTheme); const theme = themes.find(t => t.name === savedTheme);
if (theme && document.documentElement && document.body) { if (theme) {
await nextTick(); await nextTick();
const html = document.documentElement;
const body = document.body;
if (html && body && html.classList && body.classList) {
if (theme.page === 'dark') { if (theme.page === 'dark') {
document.documentElement.classList.add('dark'); html.classList.add('dark');
document.body.classList.add('dark-theme'); body.classList.add('dark-theme');
document.body.style.backgroundColor = '#0a0a0a'; body.style.backgroundColor = '#0a0a0a';
} else { } else {
document.documentElement.classList.remove('dark'); html.classList.remove('dark');
document.body.classList.remove('dark-theme'); body.classList.remove('dark-theme');
document.body.style.backgroundColor = '#f0f2f5'; body.style.backgroundColor = '#f0f2f5';
}
} }
} }
} }
@@ -847,6 +1062,13 @@ function parseById(shareLinkInfo, http, logger) {
return; return;
} }
// 配置Monaco Editor使用国内CDN (npmmirror)
loader.config({
paths: {
vs: 'https://registry.npmmirror.com/monaco-editor/0.55.1/files/min/vs'
}
});
const monaco = await loader.init(); const monaco = await loader.init();
if (monaco) { if (monaco) {
await configureMonacoTypes(monaco); await configureMonacoTypes(monaco);
@@ -867,6 +1089,13 @@ function parseById(shareLinkInfo, http, logger) {
// 加载示例代码 // 加载示例代码
const loadTemplate = () => { const loadTemplate = () => {
jsCode.value = exampleCode; jsCode.value = exampleCode;
// 重置测试参数为示例链接
testParams.value.shareUrl = 'https://example.com/s/abc';
testParams.value.pwd = '';
testParams.value.method = 'parse';
// 清空测试结果
testResult.value = null;
consoleLogs.value = [];
ElMessage.success('已加载JavaScript示例代码'); ElMessage.success('已加载JavaScript示例代码');
}; };
@@ -1183,7 +1412,7 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
const html = document.documentElement; const html = document.documentElement;
const body = document.body; const body = document.body;
if (html && body) { if (html && body && html.classList && body.classList) {
if (theme.page === 'dark') { if (theme.page === 'dark') {
html.classList.add('dark'); html.classList.add('dark');
body.classList.add('dark-theme'); body.classList.add('dark-theme');
@@ -1328,8 +1557,13 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
const checkDarkMode = () => { const checkDarkMode = () => {
try { try {
const html = document.documentElement; const html = document.documentElement;
if (html) { const body = document.body;
isDarkMode.value = html.classList?.contains('dark') || if (!html || !body) {
return; // DOM未准备好直接返回
}
if (html.classList) {
isDarkMode.value = html.classList.contains('dark') ||
html.getAttribute('data-theme') === 'dark'; html.getAttribute('data-theme') === 'dark';
// 强制更新splitpanes分隔线样式 // 强制更新splitpanes分隔线样式
updateSplitpanesStyle(); updateSplitpanesStyle();
@@ -1342,11 +1576,19 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
// 强制更新splitpanes分隔线样式 // 强制更新splitpanes分隔线样式
const updateSplitpanesStyle = () => { const updateSplitpanesStyle = () => {
setTimeout(() => { setTimeout(() => {
try {
const html = document.documentElement;
const body = document.body;
if (!html || !body) {
return; // DOM未准备好直接返回
}
const splitters = document.querySelectorAll('.splitpanes__splitter'); const splitters = document.querySelectorAll('.splitpanes__splitter');
const isDark = document.documentElement?.classList?.contains('dark') || const isDark = html.classList?.contains('dark') ||
document.body?.classList?.contains('dark-theme'); body.classList?.contains('dark-theme');
splitters.forEach(splitter => { splitters.forEach(splitter => {
if (splitter) {
if (isDark) { if (isDark) {
splitter.style.setProperty('background-color', 'rgba(255, 255, 255, 0.08)', 'important'); splitter.style.setProperty('background-color', 'rgba(255, 255, 255, 0.08)', 'important');
splitter.style.setProperty('background', 'rgba(255, 255, 255, 0.08)', 'important'); splitter.style.setProperty('background', 'rgba(255, 255, 255, 0.08)', 'important');
@@ -1354,7 +1596,11 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
splitter.style.removeProperty('background-color'); splitter.style.removeProperty('background-color');
splitter.style.removeProperty('background'); splitter.style.removeProperty('background');
} }
}
}); });
} catch (error) {
console.warn('更新splitpanes样式失败:', error);
}
}, 100); }, 100);
}; };
@@ -1378,14 +1624,19 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
checkDarkMode(); checkDarkMode();
// 监听主题变化 // 监听主题变化
if (document.documentElement) { const html = document.documentElement;
if (html && html.classList) {
try {
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
checkDarkMode(); checkDarkMode();
}); });
observer.observe(document.documentElement, { observer.observe(html, {
attributes: true, attributes: true,
attributeFilter: ['class', 'data-theme'] attributeFilter: ['class', 'data-theme']
}); });
} catch (error) {
console.warn('创建主题监听器失败:', error);
}
} }
// 初始化splitpanes样式 // 初始化splitpanes样式
@@ -1416,8 +1667,10 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
inputPassword, inputPassword,
authError, authError,
authLoading, authLoading,
playgroundEnabled,
checkAuthStatus, checkAuthStatus,
submitPassword, submitPassword,
goHome,
// 移动端 // 移动端
isMobile, isMobile,
updateIsMobile, updateIsMobile,
@@ -2441,7 +2694,6 @@ html.dark .playground-container .splitpanes__splitter:hover {
background-color: #fafafa; background-color: #fafafa;
font-family: 'Monaco', 'Menlo', 'Courier New', monospace; font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
font-size: 13px; font-size: 13px;
min-height: 250px;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
@@ -2636,26 +2888,28 @@ html.dark .playground-container .splitpanes__splitter:hover {
} }
/* ===== 响应式布局 ===== */ /* ===== 响应式布局 ===== */
/* 移动端纵向布局 */ /* 移动端布局:内容自然向下流动 */
.playground-container.is-mobile .splitpanes.mobile-vertical { .mobile-layout {
flex-direction: column !important; display: flex;
} flex-direction: column;
.playground-container.is-mobile .splitpanes--horizontal > .splitpanes__splitter {
height: 6px;
width: 100%; width: 100%;
cursor: row-resize;
margin: 5px 0;
} }
.playground-container.is-mobile .editor-pane, .mobile-layout .editor-section {
.playground-container.is-mobile .test-pane { width: 100%;
width: 100% !important; margin-bottom: 12px;
} }
.playground-container.is-mobile .test-pane { .mobile-test-section {
margin-left: 0 !important; width: 100%;
margin-top: 10px; height: auto !important;
overflow-y: visible !important;
padding: 0;
}
.mobile-test-section .test-params-card,
.mobile-test-section .result-card {
margin-bottom: 12px;
} }
.playground-container.is-mobile .playground-loading-card { .playground-container.is-mobile .playground-loading-card {
@@ -2680,7 +2934,11 @@ html.dark .playground-container .splitpanes__splitter:hover {
} }
.console-container { .console-container {
max-height: 200px; max-height: 250px;
}
.empty-console {
padding: 20px 0;
} }
} }
@@ -2695,15 +2953,26 @@ html.dark .playground-container .splitpanes__splitter:hover {
gap: 10px; gap: 10px;
} }
.console-container {
max-height: 300px;
padding: 8px;
}
.empty-console {
padding: 12px 0;
font-size: 13px;
}
.console-entry {
font-size: 12px;
padding: 6px 0;
}
.header-actions { .header-actions {
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;
} }
.test-section {
margin-top: 20px;
}
.panel-expand-btn { .panel-expand-btn {
right: 10px; right: 10px;
} }
@@ -2713,9 +2982,70 @@ html.dark .playground-container .splitpanes__splitter:hover {
gap: 8px; gap: 8px;
} }
.splitpanes { /* 移动端结果区域自适应高度 */
.result-content {
max-height: none !important;
overflow-y: visible !important;
}
/* 移动端测试区域不使用固定高度 */
.test-section {
height: auto !important;
overflow-y: visible !important;
}
/* 移动端对话框样式 */
.publish-dialog :deep(.el-dialog) {
margin: 5vh auto !important;
max-height: 90vh;
display: flex;
flex-direction: column; flex-direction: column;
} }
.publish-dialog :deep(.el-dialog__body) {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.publish-code-textarea :deep(.el-textarea__inner) {
font-size: 12px;
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
}
.publish-checklist {
font-size: 13px;
}
.publish-checklist ul {
line-height: 1.8;
}
.publish-checklist li {
margin-bottom: 4px;
}
.dialog-footer-mobile {
display: flex;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
.shortcuts-dialog :deep(.el-dialog) {
margin: 5vh auto !important;
max-height: 90vh;
}
.shortcuts-dialog :deep(.el-dialog__body) {
padding: 15px;
max-height: calc(90vh - 120px);
overflow-y: auto;
}
.shortcuts-table :deep(.el-table__body) {
font-size: 13px;
}
} }
/* ===== 改进的滚动条样式 ===== */ /* ===== 改进的滚动条样式 ===== */

View File

@@ -61,12 +61,16 @@ export default {
} }
}, },
toggleTheme(isDark) { toggleTheme(isDark) {
const html = document.documentElement;
const body = document.body;
if (html && body && html.classList && body.classList) {
if (isDark) { if (isDark) {
document.body.classList.add('dark-theme') body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme') html.classList.add('dark-theme')
} else { } else {
document.body.classList.remove('dark-theme') body.classList.remove('dark-theme')
document.documentElement.classList.remove('dark-theme') html.classList.remove('dark-theme')
}
} }
} }
}, },

View File

@@ -43,7 +43,7 @@ module.exports = {
'@': resolve('src') '@': resolve('src')
} }
}, },
// Monaco Editor配置 // Monaco Editor配置 - 使用国内CDN
module: { module: {
rules: [ rules: [
{ {

View File

View File

@@ -122,13 +122,23 @@ public class AppMain {
if (parser.getBoolean("enabled", false)) { if (parser.getBoolean("enabled", false)) {
try { try {
String jsCode = parser.getString("jsCode"); String jsCode = parser.getString("jsCode");
if (jsCode == null || jsCode.trim().isEmpty()) {
log.error("加载演练场解析器失败: {} - JavaScript代码为空", parser.getString("name"));
continue;
}
CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode); CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
CustomParserRegistry.register(config); CustomParserRegistry.register(config);
loadedCount++; loadedCount++;
log.info("已加载演练场解析器: {} ({})", log.info("已加载演练场解析器: {} ({})",
config.getDisplayName(), config.getType()); config.getDisplayName(), config.getType());
} catch (Exception e) { } catch (Exception e) {
log.error("加载演练场解析器失败: {}", parser.getString("name"), e); String parserName = parser.getString("name");
String errorMsg = e.getMessage();
log.error("加载演练场解析器失败: {} - {}", parserName, errorMsg, e);
// 如果是require相关错误提供更详细的提示
if (errorMsg != null && errorMsg.contains("require")) {
log.error("提示演练场解析器不支持CommonJS模块系统require请确保代码使用ES5.1语法");
}
} }
} }
} }

View File

@@ -18,6 +18,12 @@ public class PlaygroundConfig {
*/ */
private static PlaygroundConfig instance; private static PlaygroundConfig instance;
/**
* 是否启用演练场
* 默认false不启用
*/
private boolean enabled = false;
/** /**
* 是否公开模式(不需要密码) * 是否公开模式(不需要密码)
* 默认false需要密码访问 * 默认false需要密码访问
@@ -57,17 +63,20 @@ public class PlaygroundConfig {
PlaygroundConfig cfg = getInstance(); PlaygroundConfig cfg = getInstance();
if (config != null && config.containsKey("playground")) { if (config != null && config.containsKey("playground")) {
JsonObject playgroundConfig = config.getJsonObject("playground"); JsonObject playgroundConfig = config.getJsonObject("playground");
cfg.enabled = playgroundConfig.getBoolean("enabled", false);
cfg.isPublic = playgroundConfig.getBoolean("public", false); cfg.isPublic = playgroundConfig.getBoolean("public", false);
cfg.password = playgroundConfig.getString("password", "nfd_playground_2024"); cfg.password = playgroundConfig.getString("password", "nfd_playground_2024");
log.info("Playground配置已加载: public={}, password={}", log.info("Playground配置已加载: enabled={}, public={}, password={}",
cfg.isPublic, cfg.isPublic ? "N/A" : "已设置"); cfg.enabled, cfg.isPublic, cfg.isPublic ? "N/A" : "已设置");
if (!cfg.isPublic && "nfd_playground_2024".equals(cfg.password)) { if (!cfg.enabled) {
log.info("演练场功能已禁用");
} else if (!cfg.isPublic && "nfd_playground_2024".equals(cfg.password)) {
log.warn("⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!"); log.warn("⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!");
} }
} else { } else {
log.info("未找到playground配置使用默认值: public=false"); log.info("未找到playground配置使用默认值: enabled=false, public=false");
} }
} }
} }

View File

@@ -50,10 +50,23 @@ public class PlaygroundApi {
private static final String SESSION_AUTH_KEY = "playgroundAuthed"; private static final String SESSION_AUTH_KEY = "playgroundAuthed";
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
/**
* 检查Playground是否启用
*/
private boolean checkEnabled() {
PlaygroundConfig config = PlaygroundConfig.getInstance();
return config.isEnabled();
}
/** /**
* 检查Playground访问权限 * 检查Playground访问权限
*/ */
private boolean checkAuth(RoutingContext ctx) { private boolean checkAuth(RoutingContext ctx) {
// 首先检查是否启用
if (!checkEnabled()) {
return false;
}
PlaygroundConfig config = PlaygroundConfig.getInstance(); PlaygroundConfig config = PlaygroundConfig.getInstance();
// 如果是公开模式,直接允许访问 // 如果是公开模式,直接允许访问
@@ -77,9 +90,11 @@ public class PlaygroundApi {
@RouteMapping(value = "/status", method = RouteMethod.GET) @RouteMapping(value = "/status", method = RouteMethod.GET)
public Future<JsonObject> getStatus(RoutingContext ctx) { public Future<JsonObject> getStatus(RoutingContext ctx) {
PlaygroundConfig config = PlaygroundConfig.getInstance(); PlaygroundConfig config = PlaygroundConfig.getInstance();
boolean authed = checkAuth(ctx); boolean enabled = config.isEnabled();
boolean authed = enabled && checkAuth(ctx);
JsonObject result = new JsonObject() JsonObject result = new JsonObject()
.put("enabled", enabled)
.put("public", config.isPublic()) .put("public", config.isPublic())
.put("authed", authed); .put("authed", authed);
@@ -91,6 +106,11 @@ public class PlaygroundApi {
*/ */
@RouteMapping(value = "/login", method = RouteMethod.POST) @RouteMapping(value = "/login", method = RouteMethod.POST)
public Future<JsonObject> login(RoutingContext ctx) { public Future<JsonObject> login(RoutingContext ctx) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise(); Promise<JsonObject> promise = Promise.promise();
try { try {
@@ -142,6 +162,11 @@ public class PlaygroundApi {
*/ */
@RouteMapping(value = "/test", method = RouteMethod.POST) @RouteMapping(value = "/test", method = RouteMethod.POST)
public Future<JsonObject> test(RoutingContext ctx) { public Future<JsonObject> test(RoutingContext ctx) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查 // 权限检查
if (!checkAuth(ctx)) { if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject()); return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
@@ -345,6 +370,12 @@ public class PlaygroundApi {
*/ */
@RouteMapping(value = "/types.js", method = RouteMethod.GET) @RouteMapping(value = "/types.js", method = RouteMethod.GET)
public void getTypesJs(RoutingContext ctx, HttpServerResponse response) { public void getTypesJs(RoutingContext ctx, HttpServerResponse response) {
// 检查是否启用
if (!checkEnabled()) {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("演练场功能已禁用"));
return;
}
// 权限检查 // 权限检查
if (!checkAuth(ctx)) { if (!checkAuth(ctx)) {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("未授权访问")); ResponseUtil.fireJsonResultResponse(response, JsonResult.error("未授权访问"));
@@ -377,6 +408,11 @@ public class PlaygroundApi {
*/ */
@RouteMapping(value = "/parsers", method = RouteMethod.GET) @RouteMapping(value = "/parsers", method = RouteMethod.GET)
public Future<JsonObject> getParserList(RoutingContext ctx) { public Future<JsonObject> getParserList(RoutingContext ctx) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查 // 权限检查
if (!checkAuth(ctx)) { if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject()); return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
@@ -389,6 +425,11 @@ public class PlaygroundApi {
*/ */
@RouteMapping(value = "/parsers", method = RouteMethod.POST) @RouteMapping(value = "/parsers", method = RouteMethod.POST)
public Future<JsonObject> saveParser(RoutingContext ctx) { public Future<JsonObject> saveParser(RoutingContext ctx) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查 // 权限检查
if (!checkAuth(ctx)) { if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject()); return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
@@ -504,6 +545,11 @@ public class PlaygroundApi {
*/ */
@RouteMapping(value = "/parsers/:id", method = RouteMethod.PUT) @RouteMapping(value = "/parsers/:id", method = RouteMethod.PUT)
public Future<JsonObject> updateParser(RoutingContext ctx, Long id) { public Future<JsonObject> updateParser(RoutingContext ctx, Long id) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查 // 权限检查
if (!checkAuth(ctx)) { if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject()); return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
@@ -585,6 +631,11 @@ public class PlaygroundApi {
*/ */
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE) @RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) { public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查 // 权限检查
if (!checkAuth(ctx)) { if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject()); return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
@@ -628,6 +679,11 @@ public class PlaygroundApi {
*/ */
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET) @RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) { public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查 // 权限检查
if (!checkAuth(ctx)) { if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject()); return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());

View File

@@ -15,6 +15,8 @@ proxyConf: server-proxy
# JS演练场配置 # JS演练场配置
playground: playground:
# 是否启用演练场默认false不启用
enabled: false
# 公开模式默认false需要密码访问设为true则无需密码 # 公开模式默认false需要密码访问设为true则无需密码
public: false public: false
# 访问密码,建议修改默认密码! # 访问密码,建议修改默认密码!

View File

@@ -4,7 +4,7 @@ server-name: Vert.x-proxy-server(v4.1.2)
proxy: proxy:
- listen: 6401 - listen: 6401
# 404的路径 # 404的路径
page404: webroot/err/404.html page404: webroot/nfd-front/index.html
static: static:
path: / path: /
add-headers: add-headers: