mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-16 12:23:03 +00:00
Add playground loading animation, password auth, and mobile layout support
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
This commit is contained in:
@@ -4,6 +4,33 @@ import axios from 'axios';
|
|||||||
* 演练场API服务
|
* 演练场API服务
|
||||||
*/
|
*/
|
||||||
export const playgroundApi = {
|
export const playgroundApi = {
|
||||||
|
/**
|
||||||
|
* 获取Playground状态(是否需要认证)
|
||||||
|
* @returns {Promise} 状态信息
|
||||||
|
*/
|
||||||
|
async getStatus() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/v2/playground/status');
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.error || error.message || '获取状态失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playground登录
|
||||||
|
* @param {string} password - 访问密码
|
||||||
|
* @returns {Promise} 登录结果
|
||||||
|
*/
|
||||||
|
async login(password) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/v2/playground/login', { password });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.error || error.message || '登录失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试执行JavaScript代码
|
* 测试执行JavaScript代码
|
||||||
* @param {string} jsCode - JavaScript代码
|
* @param {string} jsCode - JavaScript代码
|
||||||
|
|||||||
@@ -1,6 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen }">
|
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen, 'is-mobile': isMobile }">
|
||||||
<el-card class="playground-card">
|
<!-- 加载动画 + 进度条 -->
|
||||||
|
<div v-if="loading" class="playground-loading-overlay">
|
||||||
|
<div class="playground-loading-card">
|
||||||
|
<div class="loading-icon">
|
||||||
|
<el-icon class="is-loading" :size="40"><Loading /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="loading-text">正在加载编辑器和编译器...</div>
|
||||||
|
<div class="loading-bar">
|
||||||
|
<div class="loading-bar-inner" :style="{ width: loadProgress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-percent">{{ loadProgress }}%</div>
|
||||||
|
<div class="loading-details">{{ loadingMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密码验证界面 -->
|
||||||
|
<div v-if="!loading && authChecking" class="playground-auth-loading">
|
||||||
|
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
|
||||||
|
<span style="margin-left: 10px;">正在检查访问权限...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loading && !authChecking && !authed" class="playground-auth-overlay">
|
||||||
|
<div class="playground-auth-card">
|
||||||
|
<div class="auth-icon">
|
||||||
|
<el-icon :size="50"><Lock /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="auth-title">JS解析器演练场</div>
|
||||||
|
<div class="auth-subtitle">请输入访问密码</div>
|
||||||
|
<el-input
|
||||||
|
v-model="inputPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入访问密码"
|
||||||
|
size="large"
|
||||||
|
@keyup.enter="submitPassword"
|
||||||
|
class="auth-input"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Lock /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<div v-if="authError" class="auth-error">
|
||||||
|
<el-icon><WarningFilled /></el-icon>
|
||||||
|
<span>{{ authError }}</span>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" size="large" @click="submitPassword" :loading="authLoading" class="auth-button">
|
||||||
|
<el-icon v-if="!authLoading"><Unlock /></el-icon>
|
||||||
|
<span>确认登录</span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 原有内容 - 只在已认证时显示 -->
|
||||||
|
<el-card v-if="authed && !loading" class="playground-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
@@ -67,8 +119,8 @@
|
|||||||
<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" @resized="handleResize">
|
<Splitpanes :class="['default-theme', isMobile ? 'mobile-vertical' : '']" :horizontal="isMobile" @resized="handleResize">
|
||||||
<!-- 左侧:代码编辑区 -->
|
<!-- 编辑器区域 (PC: 左侧, Mobile: 上方) -->
|
||||||
<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
|
||||||
@@ -82,9 +134,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="margin-left: 10px;">
|
:size="splitSizes[1]" min-size="20" class="test-pane" :style="isMobile ? 'margin-top: 10px;' : 'margin-left: 10px;'">
|
||||||
<div class="test-section">
|
<div class="test-section">
|
||||||
<!-- 优化的折叠按钮 -->
|
<!-- 优化的折叠按钮 -->
|
||||||
<el-tooltip content="折叠测试面板" placement="left">
|
<el-tooltip content="折叠测试面板" placement="left">
|
||||||
@@ -507,6 +559,20 @@ export default {
|
|||||||
const codeLanguage = ref(LANGUAGE.JAVASCRIPT); // 新增:代码语言选择
|
const codeLanguage = ref(LANGUAGE.JAVASCRIPT); // 新增:代码语言选择
|
||||||
const compiledES5Code = ref(''); // 新增:编译后的ES5代码
|
const compiledES5Code = ref(''); // 新增:编译后的ES5代码
|
||||||
const compileStatus = ref({ success: true, errors: [] }); // 新增:编译状态
|
const compileStatus = ref({ success: true, errors: [] }); // 新增:编译状态
|
||||||
|
|
||||||
|
// ===== 加载和认证状态 =====
|
||||||
|
const loading = ref(true);
|
||||||
|
const loadProgress = ref(0);
|
||||||
|
const loadingMessage = ref('初始化...');
|
||||||
|
const authChecking = ref(true);
|
||||||
|
const authed = ref(false);
|
||||||
|
const inputPassword = ref('');
|
||||||
|
const authError = ref('');
|
||||||
|
const authLoading = ref(false);
|
||||||
|
|
||||||
|
// ===== 移动端检测 =====
|
||||||
|
const isMobile = ref(false);
|
||||||
|
|
||||||
const testParams = ref({
|
const testParams = ref({
|
||||||
shareUrl: 'https://lanzoui.com/i7Aq12ab3cd',
|
shareUrl: 'https://lanzoui.com/i7Aq12ab3cd',
|
||||||
pwd: '',
|
pwd: '',
|
||||||
@@ -732,6 +798,136 @@ async function parseById(
|
|||||||
formatOnType: true,
|
formatOnType: true,
|
||||||
tabSize: 2
|
tabSize: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== 移动端检测 =====
|
||||||
|
const updateIsMobile = () => {
|
||||||
|
isMobile.value = window.innerWidth <= 768;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 进度设置函数 =====
|
||||||
|
const setProgress = (progress, message = '') => {
|
||||||
|
if (progress > loadProgress.value) {
|
||||||
|
loadProgress.value = progress;
|
||||||
|
}
|
||||||
|
if (message) {
|
||||||
|
loadingMessage.value = message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 认证相关函数 =====
|
||||||
|
const checkAuthStatus = async () => {
|
||||||
|
try {
|
||||||
|
const res = await playgroundApi.getStatus();
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
authed.value = res.data.authed || res.data.public;
|
||||||
|
return res.data.authed || res.data.public;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查认证状态失败:', error);
|
||||||
|
ElMessage.error('检查访问权限失败: ' + error.message);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
authChecking.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitPassword = async () => {
|
||||||
|
if (!inputPassword.value.trim()) {
|
||||||
|
authError.value = '请输入密码';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
authError.value = '';
|
||||||
|
authLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await playgroundApi.login(inputPassword.value);
|
||||||
|
if (res.code === 200 || res.success) {
|
||||||
|
authed.value = true;
|
||||||
|
ElMessage.success('登录成功');
|
||||||
|
await initPlayground();
|
||||||
|
} else {
|
||||||
|
authError.value = res.msg || res.message || '密码错误';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
authError.value = error.message || '登录失败,请重试';
|
||||||
|
} finally {
|
||||||
|
authLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== Playground 初始化 =====
|
||||||
|
const initPlayground = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
loadProgress.value = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setProgress(10, '初始化Vue组件...');
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
setProgress(20, '加载配置和本地数据...');
|
||||||
|
// 加载保存的代码
|
||||||
|
const saved = localStorage.getItem('playground_code');
|
||||||
|
if (saved) {
|
||||||
|
jsCode.value = saved;
|
||||||
|
} else {
|
||||||
|
jsCode.value = exampleCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载保存的语言选择
|
||||||
|
const savedLanguage = localStorage.getItem('playground_language');
|
||||||
|
if (savedLanguage) {
|
||||||
|
codeLanguage.value = savedLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(40, '准备加载TypeScript编译器...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
setProgress(50, '初始化Monaco Editor类型定义...');
|
||||||
|
await initMonacoTypes();
|
||||||
|
|
||||||
|
setProgress(80, '加载完成...');
|
||||||
|
|
||||||
|
// 加载保存的主题
|
||||||
|
const savedTheme = localStorage.getItem('playground_theme');
|
||||||
|
if (savedTheme) {
|
||||||
|
currentTheme.value = savedTheme;
|
||||||
|
const theme = themes.find(t => t.name === savedTheme);
|
||||||
|
if (theme && document.documentElement && document.body) {
|
||||||
|
await nextTick();
|
||||||
|
if (theme.page === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.body.classList.add('dark-theme');
|
||||||
|
document.body.style.backgroundColor = '#0a0a0a';
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
document.body.classList.remove('dark-theme');
|
||||||
|
document.body.style.backgroundColor = '#f0f2f5';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载保存的折叠状态
|
||||||
|
const savedCollapsed = localStorage.getItem('playground_collapsed_panels');
|
||||||
|
if (savedCollapsed) {
|
||||||
|
try {
|
||||||
|
collapsedPanels.value = JSON.parse(savedCollapsed);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('加载折叠状态失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(100, '初始化完成!');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化失败:', error);
|
||||||
|
ElMessage.error('初始化失败: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化Monaco Editor类型定义
|
// 初始化Monaco Editor类型定义
|
||||||
const initMonacoTypes = async () => {
|
const initMonacoTypes = async () => {
|
||||||
@@ -1368,53 +1564,23 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// 初始化移动端检测
|
||||||
|
updateIsMobile();
|
||||||
|
window.addEventListener('resize', updateIsMobile);
|
||||||
|
|
||||||
|
// 检查认证状态
|
||||||
|
const isAuthed = await checkAuthStatus();
|
||||||
|
|
||||||
|
// 如果已认证,初始化playground
|
||||||
|
if (isAuthed) {
|
||||||
|
await initPlayground();
|
||||||
|
} else {
|
||||||
|
// 未认证,停止加载动画,显示密码输入
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
checkDarkMode();
|
checkDarkMode();
|
||||||
await initMonacoTypes();
|
|
||||||
|
|
||||||
// 加载保存的代码
|
|
||||||
const saved = localStorage.getItem('playground_code');
|
|
||||||
if (saved) {
|
|
||||||
jsCode.value = saved;
|
|
||||||
} else {
|
|
||||||
jsCode.value = exampleCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载保存的主题
|
|
||||||
await nextTick();
|
|
||||||
const savedTheme = localStorage.getItem('playground_theme');
|
|
||||||
if (savedTheme) {
|
|
||||||
currentTheme.value = savedTheme;
|
|
||||||
const theme = themes.find(t => t.name === savedTheme);
|
|
||||||
if (theme && document.documentElement && document.body) {
|
|
||||||
await nextTick();
|
|
||||||
if (theme.page === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
document.body.classList.add('dark-theme');
|
|
||||||
document.body.style.backgroundColor = '#0a0a0a';
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
document.body.classList.remove('dark-theme');
|
|
||||||
document.body.style.backgroundColor = '#f0f2f5';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载保存的折叠状态
|
|
||||||
const savedCollapsed = localStorage.getItem('playground_collapsed_panels');
|
|
||||||
if (savedCollapsed) {
|
|
||||||
try {
|
|
||||||
collapsedPanels.value = JSON.parse(savedCollapsed);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('加载折叠状态失败', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载保存的语言选择
|
|
||||||
const savedLanguage = localStorage.getItem('playground_language');
|
|
||||||
if (savedLanguage) {
|
|
||||||
codeLanguage.value = savedLanguage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听主题变化
|
// 监听主题变化
|
||||||
if (document.documentElement) {
|
if (document.documentElement) {
|
||||||
@@ -1430,6 +1596,10 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
// 初始化splitpanes样式
|
// 初始化splitpanes样式
|
||||||
updateSplitpanesStyle();
|
updateSplitpanesStyle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', updateIsMobile);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
LANGUAGE,
|
LANGUAGE,
|
||||||
@@ -1444,6 +1614,20 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
isDarkMode,
|
isDarkMode,
|
||||||
editorTheme,
|
editorTheme,
|
||||||
editorOptions,
|
editorOptions,
|
||||||
|
// 加载和认证
|
||||||
|
loading,
|
||||||
|
loadProgress,
|
||||||
|
loadingMessage,
|
||||||
|
authChecking,
|
||||||
|
authed,
|
||||||
|
inputPassword,
|
||||||
|
authError,
|
||||||
|
authLoading,
|
||||||
|
checkAuthStatus,
|
||||||
|
submitPassword,
|
||||||
|
// 移动端
|
||||||
|
isMobile,
|
||||||
|
updateIsMobile,
|
||||||
onCodeChange,
|
onCodeChange,
|
||||||
onLanguageChange,
|
onLanguageChange,
|
||||||
compileTypeScriptCode,
|
compileTypeScriptCode,
|
||||||
@@ -1550,6 +1734,154 @@ body.dark-theme .splitpanes__splitter:hover,
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* ===== 加载动画和进度条 ===== */
|
||||||
|
.playground-loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .playground-loading-overlay {
|
||||||
|
background: rgba(10, 10, 10, 0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-loading-card {
|
||||||
|
width: 320px;
|
||||||
|
padding: 30px 40px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .playground-loading-card {
|
||||||
|
background: #1f1f1f;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-icon {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 12px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-bar-inner {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-percent {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-details {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 认证界面 ===== */
|
||||||
|
.playground-auth-loading {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-auth-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-auth-card {
|
||||||
|
width: 400px;
|
||||||
|
padding: 40px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .playground-auth-card {
|
||||||
|
background: #1f1f1f;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-icon {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== 容器布局 ===== */
|
/* ===== 容器布局 ===== */
|
||||||
.playground-container {
|
.playground-container {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
@@ -2513,6 +2845,40 @@ html.dark .playground-container .splitpanes__splitter:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 响应式布局 ===== */
|
/* ===== 响应式布局 ===== */
|
||||||
|
/* 移动端纵向布局 */
|
||||||
|
.playground-container.is-mobile .splitpanes.mobile-vertical {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-container.is-mobile .splitpanes--horizontal > .splitpanes__splitter {
|
||||||
|
height: 6px;
|
||||||
|
width: 100%;
|
||||||
|
cursor: row-resize;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-container.is-mobile .editor-pane,
|
||||||
|
.playground-container.is-mobile .test-pane {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-container.is-mobile .test-pane {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-container.is-mobile .playground-loading-card {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 320px;
|
||||||
|
padding: 24px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-container.is-mobile .playground-auth-card {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 30px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
@media screen and (max-width: 1200px) {
|
||||||
.splitpanes {
|
.splitpanes {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
@@ -2555,6 +2921,10 @@ html.dark .playground-container .splitpanes__splitter:hover {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.splitpanes {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 改进的滚动条样式 ===== */
|
/* ===== 改进的滚动条样式 ===== */
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import cn.qaiu.WebClientVertxInit;
|
|||||||
import cn.qaiu.db.pool.JDBCPoolInit;
|
import cn.qaiu.db.pool.JDBCPoolInit;
|
||||||
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
||||||
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
|
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
|
||||||
|
import cn.qaiu.lz.web.config.PlaygroundConfig;
|
||||||
import cn.qaiu.vx.core.Deploy;
|
import cn.qaiu.vx.core.Deploy;
|
||||||
import cn.qaiu.vx.core.util.ConfigConstant;
|
import cn.qaiu.vx.core.util.ConfigConstant;
|
||||||
import cn.qaiu.vx.core.util.VertxHolder;
|
import cn.qaiu.vx.core.util.VertxHolder;
|
||||||
@@ -88,5 +89,8 @@ public class AppMain {
|
|||||||
JsonObject auths = jsonObject.getJsonObject(ConfigConstant.AUTHS);
|
JsonObject auths = jsonObject.getJsonObject(ConfigConstant.AUTHS);
|
||||||
localMap.put(ConfigConstant.AUTHS, auths);
|
localMap.put(ConfigConstant.AUTHS, auths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 演练场配置
|
||||||
|
PlaygroundConfig.loadFromJson(jsonObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package cn.qaiu.lz.web.config;
|
||||||
|
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JS演练场配置
|
||||||
|
*
|
||||||
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Slf4j
|
||||||
|
public class PlaygroundConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单例实例
|
||||||
|
*/
|
||||||
|
private static PlaygroundConfig instance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否公开模式(不需要密码)
|
||||||
|
* 默认false,需要密码访问
|
||||||
|
*/
|
||||||
|
private boolean isPublic = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问密码
|
||||||
|
* 默认密码:nfd_playground_2024
|
||||||
|
*/
|
||||||
|
private String password = "nfd_playground_2024";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私有构造函数
|
||||||
|
*/
|
||||||
|
private PlaygroundConfig() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单例实例
|
||||||
|
*/
|
||||||
|
public static PlaygroundConfig getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (PlaygroundConfig.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new PlaygroundConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JsonObject加载配置
|
||||||
|
*/
|
||||||
|
public static void loadFromJson(JsonObject config) {
|
||||||
|
PlaygroundConfig cfg = getInstance();
|
||||||
|
if (config != null && config.containsKey("playground")) {
|
||||||
|
JsonObject playgroundConfig = config.getJsonObject("playground");
|
||||||
|
cfg.isPublic = playgroundConfig.getBoolean("public", false);
|
||||||
|
cfg.password = playgroundConfig.getString("password", "nfd_playground_2024");
|
||||||
|
|
||||||
|
log.info("Playground配置已加载: public={}, password={}",
|
||||||
|
cfg.isPublic, cfg.isPublic ? "N/A" : "已设置");
|
||||||
|
|
||||||
|
if (!cfg.isPublic && "nfd_playground_2024".equals(cfg.password)) {
|
||||||
|
log.warn("⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("未找到playground配置,使用默认值: public=false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package cn.qaiu.lz.web.controller;
|
package cn.qaiu.lz.web.controller;
|
||||||
|
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.lz.web.config.PlaygroundConfig;
|
||||||
import cn.qaiu.lz.web.model.PlaygroundTestResp;
|
import cn.qaiu.lz.web.model.PlaygroundTestResp;
|
||||||
import cn.qaiu.lz.web.service.DbService;
|
import cn.qaiu.lz.web.service.DbService;
|
||||||
import cn.qaiu.parser.ParserCreate;
|
import cn.qaiu.parser.ParserCreate;
|
||||||
@@ -19,6 +20,7 @@ import io.vertx.core.http.HttpServerRequest;
|
|||||||
import io.vertx.core.http.HttpServerResponse;
|
import io.vertx.core.http.HttpServerResponse;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.ext.web.RoutingContext;
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
import io.vertx.ext.web.Session;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
@@ -42,7 +44,92 @@ public class PlaygroundApi {
|
|||||||
|
|
||||||
private static final int MAX_PARSER_COUNT = 100;
|
private static final int MAX_PARSER_COUNT = 100;
|
||||||
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
|
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
|
||||||
|
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 checkAuth(RoutingContext ctx) {
|
||||||
|
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
||||||
|
|
||||||
|
// 如果是公开模式,直接允许访问
|
||||||
|
if (config.isPublic()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则检查Session中的认证状态
|
||||||
|
Session session = ctx.session();
|
||||||
|
if (session == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean authed = session.get(SESSION_AUTH_KEY);
|
||||||
|
return authed != null && authed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Playground状态(是否需要认证)
|
||||||
|
*/
|
||||||
|
@RouteMapping(value = "/status", method = RouteMethod.GET)
|
||||||
|
public Future<JsonObject> getStatus(RoutingContext ctx) {
|
||||||
|
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
||||||
|
boolean authed = checkAuth(ctx);
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject()
|
||||||
|
.put("public", config.isPublic())
|
||||||
|
.put("authed", authed);
|
||||||
|
|
||||||
|
return Future.succeededFuture(JsonResult.ok(result).toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playground登录
|
||||||
|
*/
|
||||||
|
@RouteMapping(value = "/login", method = RouteMethod.POST)
|
||||||
|
public Future<JsonObject> login(RoutingContext ctx) {
|
||||||
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
|
try {
|
||||||
|
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
||||||
|
|
||||||
|
// 如果是公开模式,直接成功
|
||||||
|
if (config.isPublic()) {
|
||||||
|
Session session = ctx.session();
|
||||||
|
if (session != null) {
|
||||||
|
session.put(SESSION_AUTH_KEY, true);
|
||||||
|
}
|
||||||
|
promise.complete(JsonResult.ok("公开模式,无需密码").toJsonObject());
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取密码
|
||||||
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
|
String password = body.getString("password");
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(password)) {
|
||||||
|
promise.complete(JsonResult.error("密码不能为空").toJsonObject());
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
if (config.getPassword().equals(password)) {
|
||||||
|
Session session = ctx.session();
|
||||||
|
if (session != null) {
|
||||||
|
session.put(SESSION_AUTH_KEY, true);
|
||||||
|
}
|
||||||
|
promise.complete(JsonResult.ok("登录成功").toJsonObject());
|
||||||
|
} else {
|
||||||
|
promise.complete(JsonResult.error("密码错误").toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("登录失败", e);
|
||||||
|
promise.complete(JsonResult.error("登录失败: " + e.getMessage()).toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试执行JavaScript代码
|
* 测试执行JavaScript代码
|
||||||
@@ -52,6 +139,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 (!checkAuth(ctx)) {
|
||||||
|
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -248,7 +340,11 @@ public class PlaygroundApi {
|
|||||||
* 获取解析器列表
|
* 获取解析器列表
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
|
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getParserList() {
|
public Future<JsonObject> getParserList(RoutingContext ctx) {
|
||||||
|
// 权限检查
|
||||||
|
if (!checkAuth(ctx)) {
|
||||||
|
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||||
|
}
|
||||||
return dbService.getPlaygroundParserList();
|
return dbService.getPlaygroundParserList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +353,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 (!checkAuth(ctx)) {
|
||||||
|
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -356,6 +457,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 (!checkAuth(ctx)) {
|
||||||
|
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -410,7 +516,11 @@ public class PlaygroundApi {
|
|||||||
* 删除解析器
|
* 删除解析器
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
|
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
|
||||||
public Future<JsonObject> deleteParser(Long id) {
|
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
|
||||||
|
// 权限检查
|
||||||
|
if (!checkAuth(ctx)) {
|
||||||
|
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||||
|
}
|
||||||
return dbService.deletePlaygroundParser(id);
|
return dbService.deletePlaygroundParser(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +528,11 @@ public class PlaygroundApi {
|
|||||||
* 根据ID获取解析器
|
* 根据ID获取解析器
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
|
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getParserById(Long id) {
|
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
|
||||||
|
// 权限检查
|
||||||
|
if (!checkAuth(ctx)) {
|
||||||
|
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||||
|
}
|
||||||
return dbService.getPlaygroundParserById(id);
|
return dbService.getPlaygroundParserById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,6 +541,11 @@ public class PlaygroundApi {
|
|||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/typescript", method = RouteMethod.POST)
|
@RouteMapping(value = "/typescript", method = RouteMethod.POST)
|
||||||
public Future<JsonObject> saveTypeScriptCode(RoutingContext ctx) {
|
public Future<JsonObject> saveTypeScriptCode(RoutingContext ctx) {
|
||||||
|
// 权限检查
|
||||||
|
if (!checkAuth(ctx)) {
|
||||||
|
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -489,7 +608,11 @@ public class PlaygroundApi {
|
|||||||
* 根据parserId获取TypeScript代码
|
* 根据parserId获取TypeScript代码
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/typescript/:parserId", method = RouteMethod.GET)
|
@RouteMapping(value = "/typescript/:parserId", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getTypeScriptCode(Long parserId) {
|
public Future<JsonObject> getTypeScriptCode(RoutingContext ctx, Long parserId) {
|
||||||
|
// 权限检查
|
||||||
|
if (!checkAuth(ctx)) {
|
||||||
|
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||||
|
}
|
||||||
return dbService.getTypeScriptCodeByParserId(parserId);
|
return dbService.getTypeScriptCodeByParserId(parserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +621,11 @@ public class PlaygroundApi {
|
|||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/typescript/:parserId", method = RouteMethod.PUT)
|
@RouteMapping(value = "/typescript/:parserId", method = RouteMethod.PUT)
|
||||||
public Future<JsonObject> updateTypeScriptCode(RoutingContext ctx, Long parserId) {
|
public Future<JsonObject> updateTypeScriptCode(RoutingContext ctx, Long parserId) {
|
||||||
|
// 权限检查
|
||||||
|
if (!checkAuth(ctx)) {
|
||||||
|
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ server:
|
|||||||
# 反向代理服务器配置路径(不用加后缀)
|
# 反向代理服务器配置路径(不用加后缀)
|
||||||
proxyConf: server-proxy
|
proxyConf: server-proxy
|
||||||
|
|
||||||
|
# JS演练场配置
|
||||||
|
playground:
|
||||||
|
# 公开模式,默认false需要密码访问,设为true则无需密码
|
||||||
|
public: false
|
||||||
|
# 访问密码,建议修改默认密码!
|
||||||
|
password: 'nfd_playground_2024'
|
||||||
|
|
||||||
# vertx核心线程配置(一般无需改的), 为0表示eventLoopPoolSize将会采用默认配置(CPU核心*2) workerPoolSize将会采用默认20
|
# vertx核心线程配置(一般无需改的), 为0表示eventLoopPoolSize将会采用默认配置(CPU核心*2) workerPoolSize将会采用默认20
|
||||||
vertx:
|
vertx:
|
||||||
eventLoopPoolSize: 0
|
eventLoopPoolSize: 0
|
||||||
|
|||||||
Reference in New Issue
Block a user