初始化版本
This commit is contained in:
334
404.html
Normal file
334
404.html
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>404 - 页面未找到</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* 基础样式重置 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 炫酷背景 */
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 粒子效果背景 */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px);
|
||||||
|
background-size: 30px 30px;
|
||||||
|
z-index: 1;
|
||||||
|
animation: moveBackground 30s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveBackground {
|
||||||
|
0% {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 300px 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 404容器 */
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 404数字动画 */
|
||||||
|
.error-number {
|
||||||
|
font-size: 180px;
|
||||||
|
font-weight: 900;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
text-shadow: 0 0 50px rgba(255, 255, 255, 0.5);
|
||||||
|
animation: glitch 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translate(0);
|
||||||
|
text-shadow: 0 0 50px rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: translate(-5px, 5px);
|
||||||
|
text-shadow: 0 0 30px rgba(255, 0, 255, 0.8), 0 0 60px rgba(255, 0, 255, 0.5);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translate(-5px, -5px);
|
||||||
|
text-shadow: 0 0 30px rgba(0, 255, 255, 0.8), 0 0 60px rgba(0, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translate(5px, 5px);
|
||||||
|
text-shadow: 0 0 30px rgba(255, 255, 0, 0.8), 0 0 60px rgba(255, 255, 0, 0.5);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translate(5px, -5px);
|
||||||
|
text-shadow: 0 0 30px rgba(0, 255, 0, 0.8), 0 0 60px rgba(0, 255, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 404数字内部动画效果 */
|
||||||
|
.error-number::after {
|
||||||
|
content: '404';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: transparent;
|
||||||
|
background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.8) 50%, transparent 70%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
animation: shine 3s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题和描述 */
|
||||||
|
h2 {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: fadeInUp 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
max-width: 600px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
opacity: 0.9;
|
||||||
|
animation: fadeInUp 1s ease-out 0.3s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 返回首页按钮 */
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 30px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #667eea;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50px;
|
||||||
|
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: fadeInUp 1s ease-out 0.6s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 15px 30px rgba(102, 126, 234, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
|
||||||
|
transition: all 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果 */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 漂浮图标效果 */
|
||||||
|
.floating-icons {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
animation: float 15s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:nth-child(1) { top: 10%; left: 10%; animation-duration: 15s; }
|
||||||
|
.icon:nth-child(2) { top: 20%; right: 15%; animation-duration: 18s; animation-delay: 2s; }
|
||||||
|
.icon:nth-child(3) { bottom: 15%; left: 20%; animation-duration: 12s; animation-delay: 1s; }
|
||||||
|
.icon:nth-child(4) { bottom: 25%; right: 25%; animation-duration: 20s; animation-delay: 3s; }
|
||||||
|
.icon:nth-child(5) { top: 40%; left: 35%; animation-duration: 14s; animation-delay: 0.5s; }
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translate(0, 0) rotate(0deg);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translate(20px, -30px) rotate(15deg);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(-10px, 20px) rotate(-10deg);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translate(30px, 10px) rotate(5deg);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.error-number {
|
||||||
|
font-size: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 25px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.error-number {
|
||||||
|
font-size: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 粒子背景 -->
|
||||||
|
|
||||||
|
<!-- 漂浮图标 -->
|
||||||
|
<div class="floating-icons">
|
||||||
|
<i class="fas fa-chart-line icon"></i>
|
||||||
|
<i class="fas fa-rocket icon"></i>
|
||||||
|
<i class="fas fa-cog icon"></i>
|
||||||
|
<i class="fas fa-code icon"></i>
|
||||||
|
<i class="fas fa-terminal icon"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="error-number">404</div>
|
||||||
|
<h2>页面未找到</h2>
|
||||||
|
<p>您访问的页面可能已经移动、删除或从未存在。请返回首页继续浏览我们的基金监控系统。</p>
|
||||||
|
<a href="index.php" class="btn">
|
||||||
|
<i class="fas fa-home mr-2"></i> 返回首页
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript增强效果 -->
|
||||||
|
<script>
|
||||||
|
// 添加鼠标跟随效果
|
||||||
|
document.addEventListener('mousemove', function(e) {
|
||||||
|
const x = e.clientX / window.innerWidth;
|
||||||
|
const y = e.clientY / window.innerHeight;
|
||||||
|
|
||||||
|
// 轻微移动背景渐变
|
||||||
|
document.body.style.backgroundPosition = `${x * 50}px ${y * 50}px`;
|
||||||
|
|
||||||
|
// 轻微移动404数字
|
||||||
|
const errorNumber = document.querySelector('.error-number');
|
||||||
|
if (errorNumber) {
|
||||||
|
errorNumber.style.transform = `translate(${x * 10 - 5}px, ${y * 10 - 5}px)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 随机生成更多漂浮图标
|
||||||
|
function createMoreIcons() {
|
||||||
|
const icons = ['fa-star', 'fa-globe', 'fa-bolt', 'fa-lightbulb', 'fa-puzzle-piece'];
|
||||||
|
const container = document.querySelector('.floating-icons');
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const icon = document.createElement('i');
|
||||||
|
const randomIcon = icons[Math.floor(Math.random() * icons.length)];
|
||||||
|
icon.className = `fas ${randomIcon} icon`;
|
||||||
|
|
||||||
|
// 随机位置
|
||||||
|
icon.style.top = `${Math.random() * 100}%`;
|
||||||
|
icon.style.left = `${Math.random() * 100}%`;
|
||||||
|
|
||||||
|
// 随机动画持续时间和延迟
|
||||||
|
const duration = 10 + Math.random() * 20;
|
||||||
|
const delay = Math.random() * 5;
|
||||||
|
icon.style.animationDuration = `${duration}s`;
|
||||||
|
icon.style.animationDelay = `${delay}s`;
|
||||||
|
|
||||||
|
container.appendChild(icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载后创建更多图标
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
createMoreIcons();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
149
api.php
Normal file
149
api.php
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
// 简易文档API:list / get / save / delete
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
require_api_auth();
|
||||||
|
|
||||||
|
$baseDir = __DIR__ . DIRECTORY_SEPARATOR . 'data';
|
||||||
|
$docsDir = $baseDir . DIRECTORY_SEPARATOR . 'docs';
|
||||||
|
$uploadsDir = $baseDir . DIRECTORY_SEPARATOR . 'uploads';
|
||||||
|
$indexFile = $baseDir . DIRECTORY_SEPARATOR . 'index.json';
|
||||||
|
$sharesFile = $baseDir . DIRECTORY_SEPARATOR . 'shares.json';
|
||||||
|
|
||||||
|
function ensureDirs($dirs) {
|
||||||
|
foreach ($dirs as $d) {
|
||||||
|
if (!is_dir($d)) {
|
||||||
|
mkdir($d, 0777, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureDirs([$baseDir, $docsDir, $uploadsDir]);
|
||||||
|
|
||||||
|
function readJson($file) {
|
||||||
|
if (!file_exists($file)) return [];
|
||||||
|
$s = file_get_contents($file);
|
||||||
|
$data = json_decode($s, true);
|
||||||
|
return is_array($data) ? $data : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJson($file, $data) {
|
||||||
|
file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeId($id) {
|
||||||
|
// 只允许字母数字下划线和中划线
|
||||||
|
return preg_match('/^[a-zA-Z0-9_-]+$/', $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'list') {
|
||||||
|
$index = readJson($indexFile);
|
||||||
|
echo json_encode(['docs' => array_values($index)], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'get') {
|
||||||
|
$id = $_GET['id'] ?? '';
|
||||||
|
if (!$id || !safeId($id)) { http_response_code(400); echo json_encode(['error'=>'bad id']); exit; }
|
||||||
|
$index = readJson($indexFile);
|
||||||
|
$title = '';
|
||||||
|
foreach ($index as $it) { if ($it['id'] === $id) { $title = $it['title'] ?? ''; break; } }
|
||||||
|
$file = $docsDir . DIRECTORY_SEPARATOR . $id . '.html';
|
||||||
|
$content = file_exists($file) ? file_get_contents($file) : '';
|
||||||
|
echo json_encode(['id'=>$id,'title'=>$title,'content'=>$content], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'save') {
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $body['id'] ?? '';
|
||||||
|
$title = trim($body['title'] ?? '');
|
||||||
|
$content = $body['content'] ?? '';
|
||||||
|
|
||||||
|
$index = readJson($indexFile);
|
||||||
|
if (!$id) {
|
||||||
|
$id = 'doc_' . uniqid();
|
||||||
|
$index[] = ['id' => $id, 'title' => $title ?: '未命名'];
|
||||||
|
} else {
|
||||||
|
if (!safeId($id)) { http_response_code(400); echo json_encode(['error'=>'bad id']); exit; }
|
||||||
|
$found = false;
|
||||||
|
foreach ($index as &$it) {
|
||||||
|
if ($it['id'] === $id) { $it['title'] = $title ?: ($it['title'] ?? '未命名'); $found = true; break; }
|
||||||
|
}
|
||||||
|
if (!$found) { $index[] = ['id' => $id, 'title' => $title ?: '未命名']; }
|
||||||
|
}
|
||||||
|
writeJson($indexFile, $index);
|
||||||
|
|
||||||
|
$file = $docsDir . DIRECTORY_SEPARATOR . $id . '.html';
|
||||||
|
file_put_contents($file, $content);
|
||||||
|
echo json_encode(['ok'=>true,'id'=>$id], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete') {
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $body['id'] ?? '';
|
||||||
|
if (!$id || !safeId($id)) { http_response_code(400); echo json_encode(['error'=>'bad id']); exit; }
|
||||||
|
$index = readJson($indexFile);
|
||||||
|
$index = array_values(array_filter($index, function($it) use ($id) { return $it['id'] !== $id; }));
|
||||||
|
writeJson($indexFile, $index);
|
||||||
|
$file = $docsDir . DIRECTORY_SEPARATOR . $id . '.html';
|
||||||
|
if (file_exists($file)) unlink($file);
|
||||||
|
echo json_encode(['ok'=>true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成分享链接(支持永久、24h、一次性)
|
||||||
|
if ($action === 'share_create' || $action === 'share') {
|
||||||
|
$id = $_GET['id'] ?? '';
|
||||||
|
$mode = $_GET['mode'] ?? 'permanent'; // permanent | 24h | one_time
|
||||||
|
if (!$id || !safeId($id)) { http_response_code(400); echo json_encode(['error'=>'bad id']); exit; }
|
||||||
|
$shares = readJson($sharesFile);
|
||||||
|
if (!is_array($shares)) $shares = [];
|
||||||
|
// 生成随机令牌
|
||||||
|
try {
|
||||||
|
$token = bin2hex(random_bytes(16));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$token = uniqid('t_', true);
|
||||||
|
}
|
||||||
|
$now = time();
|
||||||
|
$expiresAt = null;
|
||||||
|
$singleUse = false;
|
||||||
|
if ($mode === '24h') { $expiresAt = $now + 24*3600; }
|
||||||
|
if ($mode === 'one_time') { $singleUse = true; }
|
||||||
|
$record = [
|
||||||
|
'id' => $id,
|
||||||
|
'token' => $token,
|
||||||
|
'created_at' => $now,
|
||||||
|
'expires_at' => $expiresAt,
|
||||||
|
'single_use' => $singleUse,
|
||||||
|
'used' => false,
|
||||||
|
'revoked' => false,
|
||||||
|
];
|
||||||
|
$shares[] = $record;
|
||||||
|
writeJson($sharesFile, $shares);
|
||||||
|
$url = '/share.php?token=' . urlencode($token);
|
||||||
|
echo json_encode(['url' => $url, 'token' => $token, 'expires_at' => $expiresAt, 'single_use' => $singleUse], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消当前文档的所有分享
|
||||||
|
if ($action === 'share_revoke_all') {
|
||||||
|
$id = $_GET['id'] ?? '';
|
||||||
|
if (!$id || !safeId($id)) { http_response_code(400); echo json_encode(['error'=>'bad id']); exit; }
|
||||||
|
$shares = readJson($sharesFile);
|
||||||
|
if (!is_array($shares)) $shares = [];
|
||||||
|
$new = [];
|
||||||
|
foreach ($shares as $rec) {
|
||||||
|
if (($rec['id'] ?? '') !== $id) { $new[] = $rec; }
|
||||||
|
}
|
||||||
|
writeJson($sharesFile, $new);
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'unknown action']);
|
||||||
201
assets/app.js
Normal file
201
assets/app.js
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
const $ = (sel) => document.querySelector(sel);
|
||||||
|
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
currentId: null,
|
||||||
|
list: [],
|
||||||
|
dirty: false,
|
||||||
|
};
|
||||||
|
let weEditor = null;
|
||||||
|
|
||||||
|
function setStatus(text) {
|
||||||
|
const el = $('#status');
|
||||||
|
el.textContent = text || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList() {
|
||||||
|
const ul = $('#doc-list');
|
||||||
|
ul.innerHTML = '';
|
||||||
|
state.list.forEach((d) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'doc-item' + (d.id === state.currentId ? ' active' : '');
|
||||||
|
li.textContent = d.title || '(未命名)';
|
||||||
|
li.onclick = () => loadDoc(d.id);
|
||||||
|
ul.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJSON(url, opts = {}) {
|
||||||
|
const res = await fetch(url, opts);
|
||||||
|
if (!res.ok) throw new Error('请求失败: ' + res.status);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshList() {
|
||||||
|
const data = await fetchJSON('api.php?action=list');
|
||||||
|
state.list = data.docs || [];
|
||||||
|
renderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDoc(id) {
|
||||||
|
const data = await fetchJSON('api.php?action=get&id=' + encodeURIComponent(id));
|
||||||
|
state.currentId = data.id;
|
||||||
|
$('#title-input').value = data.title || '';
|
||||||
|
if (weEditor) weEditor.setHtml(data.content || '');
|
||||||
|
$('#btn-save').disabled = true;
|
||||||
|
$('#btn-delete').disabled = !state.currentId;
|
||||||
|
renderList();
|
||||||
|
setStatus('已加载: ' + (data.title || data.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDoc() {
|
||||||
|
const payload = {
|
||||||
|
id: state.currentId,
|
||||||
|
title: $('#title-input').value.trim(),
|
||||||
|
content: weEditor ? weEditor.getHtml() : $('#we-editor').innerHTML,
|
||||||
|
};
|
||||||
|
const data = await fetchJSON('api.php?action=save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
state.currentId = data.id;
|
||||||
|
$('#btn-save').disabled = true;
|
||||||
|
$('#btn-delete').disabled = !state.currentId;
|
||||||
|
await refreshList();
|
||||||
|
renderList();
|
||||||
|
setStatus('已保存: ' + (payload.title || data.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDoc() {
|
||||||
|
if (!state.currentId) return;
|
||||||
|
if (!confirm('确定删除当前文档吗?')) return;
|
||||||
|
const data = await fetchJSON('api.php?action=delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: state.currentId }),
|
||||||
|
});
|
||||||
|
state.currentId = null;
|
||||||
|
$('#title-input').value = '';
|
||||||
|
$('#editor').innerHTML = '';
|
||||||
|
$('#btn-save').disabled = true;
|
||||||
|
$('#btn-delete').disabled = true;
|
||||||
|
await refreshList();
|
||||||
|
renderList();
|
||||||
|
setStatus('文档已删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
function newDoc() {
|
||||||
|
state.currentId = null;
|
||||||
|
$('#title-input').value = '';
|
||||||
|
if (weEditor) weEditor.setHtml('');
|
||||||
|
$('#btn-save').disabled = false;
|
||||||
|
$('#btn-delete').disabled = true;
|
||||||
|
renderList();
|
||||||
|
setStatus('新建文档');
|
||||||
|
}
|
||||||
|
|
||||||
|
function markDirty() {
|
||||||
|
$('#btn-save').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 由 wangEditor 处理图片上传
|
||||||
|
|
||||||
|
function setupEvents() {
|
||||||
|
$('#btn-save').onclick = saveDoc;
|
||||||
|
$('#btn-delete').onclick = deleteDoc;
|
||||||
|
$('#btn-new').onclick = newDoc;
|
||||||
|
$('#title-input').oninput = markDirty;
|
||||||
|
const shareBtn = $('#btn-share');
|
||||||
|
if (shareBtn) shareBtn.onclick = shareCurrent;
|
||||||
|
const revokeBtn = $('#btn-share-revoke');
|
||||||
|
if (revokeBtn) revokeBtn.onclick = revokeShares;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
setupEvents();
|
||||||
|
// 初始化 wangEditor
|
||||||
|
const { createEditor, createToolbar } = window.wangEditor || {};
|
||||||
|
if (createEditor && createToolbar) {
|
||||||
|
const editorConfig = { placeholder: '在此编辑内容…' };
|
||||||
|
editorConfig.onChange = () => markDirty();
|
||||||
|
// 配置图片上传到后端
|
||||||
|
editorConfig.MENU_CONF = editorConfig.MENU_CONF || {};
|
||||||
|
editorConfig.MENU_CONF['uploadImage'] = {
|
||||||
|
server: 'upload.php',
|
||||||
|
fieldName: 'image',
|
||||||
|
maxFileSize: 10 * 1024 * 1024,
|
||||||
|
timeout: 20 * 1000,
|
||||||
|
customInsert(res, insertFn) {
|
||||||
|
if (res && res.url) insertFn(res.url);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
weEditor = createEditor({ selector: '#we-editor', config: editorConfig, mode: 'default' });
|
||||||
|
createToolbar({ editor: weEditor, selector: '#we-toolbar', mode: 'default' });
|
||||||
|
}
|
||||||
|
await refreshList();
|
||||||
|
if (state.list.length) {
|
||||||
|
loadDoc(state.list[0].id).catch(console.error);
|
||||||
|
} else {
|
||||||
|
newDoc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shareCurrent() {
|
||||||
|
if (!state.currentId) {
|
||||||
|
alert('请先保存文档,再进行分享');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openShareModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
init().catch((e) => setStatus('初始化失败: ' + e.message));
|
||||||
|
|
||||||
|
function openShareModal() {
|
||||||
|
const overlay = $('#share-modal');
|
||||||
|
const linkBox = overlay.querySelector('.link-box');
|
||||||
|
const linkInput = $('#share-link');
|
||||||
|
const copyBtn = $('#btn-copy-link');
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
overlay.setAttribute('aria-hidden', 'false');
|
||||||
|
linkBox.style.display = 'none';
|
||||||
|
const close = () => { overlay.style.display = 'none'; overlay.setAttribute('aria-hidden', 'true'); };
|
||||||
|
$('#btn-close-share').onclick = close;
|
||||||
|
overlay.querySelectorAll('.options .btn').forEach((btn) => {
|
||||||
|
btn.onclick = async () => {
|
||||||
|
const mode = btn.getAttribute('data-mode');
|
||||||
|
try {
|
||||||
|
const data = await fetchJSON('api.php?action=share_create&id=' + encodeURIComponent(state.currentId) + '&mode=' + encodeURIComponent(mode));
|
||||||
|
const url = (data && data.url) ? (location.origin + data.url) : '';
|
||||||
|
if (!url) throw new Error('未获取到分享链接');
|
||||||
|
linkInput.value = url;
|
||||||
|
linkBox.style.display = 'flex';
|
||||||
|
copyBtn.onclick = async () => {
|
||||||
|
try { await navigator.clipboard.writeText(url); showToast('已复制分享链接:', url); } catch (e) { prompt('复制分享链接', url); }
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
alert('生成分享链接失败:' + e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(text, link) {
|
||||||
|
const el = $('#share-toast');
|
||||||
|
el.innerHTML = link ? (text + '<a href="' + link + '" target="_blank">打开</a>') : text;
|
||||||
|
el.style.display = 'block';
|
||||||
|
setTimeout(() => { el.style.display = 'none'; }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revokeShares() {
|
||||||
|
if (!state.currentId) { alert('当前没有文档'); return; }
|
||||||
|
if (!confirm('取消该文档的所有分享链接?此操作不可恢复')) return;
|
||||||
|
try {
|
||||||
|
await fetchJSON('api.php?action=share_revoke_all&id=' + encodeURIComponent(state.currentId));
|
||||||
|
setStatus('已取消所有分享');
|
||||||
|
showToast('已取消所有分享');
|
||||||
|
} catch (e) {
|
||||||
|
alert('取消分享失败:' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
153
assets/style.css
Normal file
153
assets/style.css
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { height: 100%; }
|
||||||
|
:root {
|
||||||
|
--bg: #0b0e12;
|
||||||
|
--panel: #0f1318;
|
||||||
|
--text: #eaeaea;
|
||||||
|
--muted: #9fb2bf;
|
||||||
|
--accent: #ff4d4f;
|
||||||
|
--accent-2: #ff6b6f;
|
||||||
|
--shadow: 0 12px 30px rgba(0,0,0,.35);
|
||||||
|
--bg-tint: #12161d;
|
||||||
|
--scroll-thumb: #2a2f3a;
|
||||||
|
--scroll-track: #141821;
|
||||||
|
--toolbar-tint: rgba(255,77,79,.08);
|
||||||
|
--input-bg: #0f1318;
|
||||||
|
--input-border: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浅色主题变量覆盖 */
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
--bg: #f7f8fa;
|
||||||
|
--panel: #ffffff;
|
||||||
|
--text: #1b2430;
|
||||||
|
--muted: #6b7785;
|
||||||
|
--accent: #ff4d4f;
|
||||||
|
--accent-2: #ff6b6f;
|
||||||
|
--shadow: 0 10px 26px rgba(22,28,38,.12);
|
||||||
|
--bg-tint: #eef2f7;
|
||||||
|
--scroll-thumb: #cfd6e2;
|
||||||
|
--scroll-track: #e9edf3;
|
||||||
|
--toolbar-tint: rgba(255,77,79,.03);
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #e3e8ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'Microsoft Yahei', sans-serif;
|
||||||
|
background: radial-gradient(1200px 600px at 70% 10%, var(--bg-tint) 0%, var(--bg) 55%);
|
||||||
|
color: var(--text);
|
||||||
|
overflow: hidden; /* 防止整页滚动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px 1fr;
|
||||||
|
height: 100vh;
|
||||||
|
backdrop-filter: saturate(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
border-right: 2px solid var(--accent);
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(180deg, rgba(255,77,79,.06), transparent 40%);
|
||||||
|
overflow: auto; /* 侧栏独立滚动 */
|
||||||
|
}
|
||||||
|
.sidebar-header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
||||||
|
.sidebar h1 { margin:0; font-size:18px; letter-spacing:.5px; }
|
||||||
|
.actions { display:flex; gap:8px; }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: #1a1f27;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
color: #ffd6d6;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .18s ease;
|
||||||
|
}
|
||||||
|
.btn:hover { transform: translateY(-1px); box-shadow: 0 6px 16px rgba(255,77,79,.25); }
|
||||||
|
.btn.primary { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
||||||
|
.btn.primary:hover { background: var(--accent-2); }
|
||||||
|
.btn.danger { background:#2a0f12; color:#ff9aa0; border-color:var(--accent); }
|
||||||
|
.btn[disabled] { opacity:.6; cursor:not-allowed; box-shadow:none; transform:none; }
|
||||||
|
|
||||||
|
/* 浅色主题按钮更柔和 */
|
||||||
|
:root[data-theme="light"] .btn { background:#f6f8fb; color:#3b4450; border-color:#e3e8ef; }
|
||||||
|
:root[data-theme="light"] .btn:hover { box-shadow: 0 6px 16px rgba(22,28,38,.12); }
|
||||||
|
:root[data-theme="light"] .btn.danger { background:#ffe8ea; color:#d33; border-color:#ffc2c5; }
|
||||||
|
|
||||||
|
.doc-list { list-style:none; padding:0; margin:12px 0 0; display:flex; flex-direction:column; gap:8px; }
|
||||||
|
.doc-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px 10px 8px 32px;
|
||||||
|
border: 1.5px solid var(--accent);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--panel);
|
||||||
|
transition: all .2s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
min-height: 40px;
|
||||||
|
display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
.doc-item::before {
|
||||||
|
content: '\1F4C2'; /* folder emoji */
|
||||||
|
position: absolute; left: 10px; top: 8px; opacity:.8;
|
||||||
|
}
|
||||||
|
.doc-item:hover { filter: brightness(1.05); transform: translateY(-1px); }
|
||||||
|
.doc-item.active { outline: 2px solid rgba(255,77,79,.25); background: var(--panel); box-shadow: 0 0 0 3px rgba(255,77,79,.06) inset; }
|
||||||
|
|
||||||
|
.editor { display:flex; flex-direction:column; height: 100vh; }
|
||||||
|
.toolbar {
|
||||||
|
position: sticky; top: 0; z-index: 5;
|
||||||
|
display:flex; gap:12px; align-items:center; padding:12px;
|
||||||
|
border-bottom:2px solid var(--accent);
|
||||||
|
background: linear-gradient(180deg, var(--toolbar-tint), transparent 90%), var(--panel);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
.title-input {
|
||||||
|
flex:1; padding:10px 12px; background: var(--input-bg); color:var(--text);
|
||||||
|
border:1px solid var(--input-border); border-radius:8px; outline:none; transition: box-shadow .15s;
|
||||||
|
}
|
||||||
|
.title-input:focus { box-shadow: 0 0 0 3px rgba(255,77,79,.3); }
|
||||||
|
|
||||||
|
.editor-area {
|
||||||
|
flex:1; padding:18px 24px; overflow:auto; background: var(--panel);
|
||||||
|
line-height: 1.7; font-size: 15px;
|
||||||
|
}
|
||||||
|
.editor-area:empty::before { content:'在此编辑内容…'; color: var(--muted); }
|
||||||
|
.editor-area img { max-width:100%; height:auto; border-radius: 8px; box-shadow: 0 4px 22px rgba(0,0,0,.35); }
|
||||||
|
.editor-area p { margin: .7em 0; }
|
||||||
|
.editor-area h1, .editor-area h2, .editor-area h3 { margin: 1em 0 .6em; color:#2b2f36; }
|
||||||
|
.editor-area a { color:#ff9aa0; }
|
||||||
|
|
||||||
|
.status { border-top:2px solid var(--accent); padding:8px 12px; font-size:12px; color:var(--muted); background: var(--panel); }
|
||||||
|
|
||||||
|
/* 精致滚动条 */
|
||||||
|
* { scrollbar-width: thin; }
|
||||||
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||||
|
::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 6px; border:2px solid var(--scroll-track); }
|
||||||
|
::-webkit-scrollbar-thumb:hover { filter: brightness(1.1); }
|
||||||
|
::-webkit-scrollbar-track { background: var(--scroll-track); }
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.app { grid-template-columns: 240px 1fr; }
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.app { grid-template-columns: 1fr; }
|
||||||
|
.sidebar { border-right: none; border-bottom: 2px solid var(--accent); }
|
||||||
|
}
|
||||||
|
/* 分享弹窗与通知条 */
|
||||||
|
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); display: none; align-items: center; justify-content: center; z-index: 999; }
|
||||||
|
.modal { background: var(--panel); color: var(--text); border: 1px solid var(--input-border); border-radius: 12px; box-shadow: var(--shadow); width: 520px; max-width: 92vw; padding: 16px; }
|
||||||
|
.modal h3 { margin: 0 0 10px; font-size: 18px; }
|
||||||
|
.modal .options { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||||
|
.modal .options .btn { padding: 8px 12px; }
|
||||||
|
.modal .link-box { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
|
||||||
|
.modal .link-box input { flex: 1; padding: 8px 10px; background: var(--input-bg); border: 1px solid var(--input-border); border-radius: 8px; color: var(--text); }
|
||||||
|
.modal .footer { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
|
||||||
|
|
||||||
|
.toast { position: fixed; left: 50%; transform: translateX(-50%); bottom: 16px; background: var(--panel); color: var(--text); border: 1px solid var(--input-border); border-radius: 10px; box-shadow: var(--shadow); padding: 10px 14px; z-index: 998; display: none; }
|
||||||
|
.toast a { color: var(--accent); text-decoration: none; }
|
||||||
30
auth.php
Normal file
30
auth.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
// 简易会话认证
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
function is_logged_in(): bool {
|
||||||
|
return !empty($_SESSION['logged_in']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function require_login(): void {
|
||||||
|
if (!is_logged_in()) {
|
||||||
|
$target = 'login.php';
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('Location: ' . $target);
|
||||||
|
} else {
|
||||||
|
echo '<script>location.replace("' . $target . '")</script>';
|
||||||
|
echo '<noscript><meta http-equiv="refresh" content="0;url=' . $target . '"></noscript>';
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function require_api_auth(): void {
|
||||||
|
if (!is_logged_in()) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
config.php
Normal file
5
config.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
// 简单密码配置:修改为你自己的密码
|
||||||
|
const AUTH_PASSWORD = 'wd123123';
|
||||||
|
// 分享链接的签名密钥(请修改为更随机的字符串)
|
||||||
|
const SHARE_SECRET = 'share_secret_change_me_2025';
|
||||||
1
data/.gitkeep
Normal file
1
data/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
7
data/.htaccess
Normal file
7
data/.htaccess
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<IfModule mod_authz_core.c>
|
||||||
|
Require all denied
|
||||||
|
</IfModule>
|
||||||
|
<IfModule !mod_authz_core.c>
|
||||||
|
Deny from all
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
1
data/docs/doc_690b108682362.html
Normal file
1
data/docs/doc_690b108682362.html
Normal file
File diff suppressed because one or more lines are too long
1
data/docs/doc_690b10977e0f8.html
Normal file
1
data/docs/doc_690b10977e0f8.html
Normal file
File diff suppressed because one or more lines are too long
1
data/docs/doc_690c049068cf7.html
Normal file
1
data/docs/doc_690c049068cf7.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<ol><li>加密货币</li><ol><li>使用CCXT</li></ol><li>服务器面板</li><ol><li>链接:http://38.12.26.18:12768/9d2b364b</li><li>用户名:433curvp</li><li>密码:ae31437e</li></ol><li>宝塔开心版安装指令:if [ -f /usr/bin/curl ];then curl -sSO https://bt11.bthappy.com/install/install_panel.sh;else wget -O install_panel.sh https://bt11.bthappy.com/install/install_panel.sh;fi;bash install_panel.sh bt11.bthappy.com</li></ol><p><br></p><p><br></p><p><br></p>
|
||||||
1
data/docs/doc_690d692ae4057.html
Normal file
1
data/docs/doc_690d692ae4057.html
Normal file
File diff suppressed because one or more lines are too long
18
data/index.json
Normal file
18
data/index.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "doc_690b108682362",
|
||||||
|
"title": "调试全密封"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "doc_690b10977e0f8",
|
||||||
|
"title": "标准品与产品重复性波动高"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "doc_690c049068cf7",
|
||||||
|
"title": "API接口"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "doc_690d692ae4057",
|
||||||
|
"title": "报告"
|
||||||
|
}
|
||||||
|
]
|
||||||
1
data/shares.json
Normal file
1
data/shares.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
39
index.html
Normal file
39
index.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>恭喜,站点创建成功!</title>
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
width: 60%;
|
||||||
|
margin: 10% auto 0;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 2% 5%;
|
||||||
|
border-radius: 10px
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
line-height: 2.3
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #20a53a
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>恭喜, 站点创建成功!</h1>
|
||||||
|
<h3>这是默认index.html,本页面由系统自动生成</h3>
|
||||||
|
<ul>
|
||||||
|
<li>本页面在FTP根目录下的index.html</li>
|
||||||
|
<li>您可以修改、删除或覆盖本页面</li>
|
||||||
|
<li>FTP相关信息,请到“面板系统后台 > FTP” 查看</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
141
index.php
Normal file
141
index.php
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?php require_once __DIR__ . '/auth.php'; require_login(); ?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>文档编辑器</title>
|
||||||
|
<link rel="stylesheet" href="assets/style.css" />
|
||||||
|
<!-- wangEditor v5 CDN -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h1>目录</h1>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="btn-new" class="btn primary">新建</button>
|
||||||
|
<button id="btn-delete" class="btn danger" disabled>删除</button>
|
||||||
|
<a class="btn" href="logout.php">退出</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul id="doc-list" class="doc-list"></ul>
|
||||||
|
</aside>
|
||||||
|
<main class="editor">
|
||||||
|
<div class="toolbar">
|
||||||
|
<input id="title-input" class="title-input" type="text" placeholder="请输入标题" />
|
||||||
|
<div id="we-toolbar" class="we-toolbar"></div>
|
||||||
|
<div class="toolbar-buttons">
|
||||||
|
<button id="btn-save" class="btn primary" disabled>保存</button>
|
||||||
|
<button id="btn-print" class="btn">打印</button>
|
||||||
|
<button id="btn-share" class="btn">分享</button>
|
||||||
|
<button id="btn-share-revoke" class="btn danger">取消分享</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="we-editor" class="editor-area"></div>
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<!-- 分享弹窗容器(通过JS填充)-->
|
||||||
|
<div id="share-modal" class="modal-overlay" aria-hidden="true">
|
||||||
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="share-title">
|
||||||
|
<h3 id="share-title">生成分享链接</h3>
|
||||||
|
<div class="options">
|
||||||
|
<button class="btn" data-mode="permanent">永久有效</button>
|
||||||
|
<button class="btn" data-mode="24h">24小时有效</button>
|
||||||
|
<button class="btn" data-mode="one_time">一次性</button>
|
||||||
|
</div>
|
||||||
|
<div class="link-box" style="display:none;">
|
||||||
|
<input type="text" id="share-link" readonly />
|
||||||
|
<button class="btn" id="btn-copy-link">复制</button>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<button class="btn" id="btn-close-share">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="share-toast" class="toast"></div>
|
||||||
|
<script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script>
|
||||||
|
<script src="assets/app.js"></script>
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* 打印当前文档内容
|
||||||
|
* 使用简单可靠的方法,在当前页面中创建打印内容并调用浏览器打印功能
|
||||||
|
*/
|
||||||
|
const printBtn = $('#btn-print');
|
||||||
|
if (printBtn) printBtn.onclick = printDoc;
|
||||||
|
function printDoc() {
|
||||||
|
try {
|
||||||
|
// 获取文档标题和内容
|
||||||
|
var title = document.getElementById('title-input').value.trim() || '未命名文档';
|
||||||
|
var content = '';
|
||||||
|
|
||||||
|
// 获取编辑器内容
|
||||||
|
if (weEditor && typeof weEditor.getHtml === 'function') {
|
||||||
|
content = weEditor.getHtml();
|
||||||
|
} else {
|
||||||
|
var editorElement = document.getElementById('we-editor');
|
||||||
|
if (editorElement) {
|
||||||
|
content = editorElement.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建打印内容容器
|
||||||
|
var printContainer = document.createElement('div');
|
||||||
|
printContainer.id = 'print-container';
|
||||||
|
printContainer.style.position = 'absolute';
|
||||||
|
printContainer.style.left = '-9999px';
|
||||||
|
printContainer.style.width = '800px'; // 设置合适的打印宽度
|
||||||
|
|
||||||
|
// 设置打印内容
|
||||||
|
printContainer.innerHTML = '<h1 style="text-align:center; font-family:Arial, sans-serif;">' +
|
||||||
|
title +
|
||||||
|
'</h1><div style="font-family:Arial, sans-serif; line-height:1.6;">' +
|
||||||
|
content +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
// 添加到页面
|
||||||
|
document.body.appendChild(printContainer);
|
||||||
|
|
||||||
|
// 创建打印样式
|
||||||
|
var printStyle = document.createElement('style');
|
||||||
|
printStyle.media = 'print';
|
||||||
|
printStyle.innerHTML =
|
||||||
|
'#print-container { position: static !important; width: 100% !important; } ' +
|
||||||
|
'body > *:not(#print-container) { display: none !important; } ' +
|
||||||
|
'body { margin: 2cm; }';
|
||||||
|
document.head.appendChild(printStyle);
|
||||||
|
|
||||||
|
// 保存原始样式引用,以便稍后移除
|
||||||
|
var originalPrintStyle = printStyle;
|
||||||
|
|
||||||
|
// 当打印对话框关闭后恢复页面
|
||||||
|
window.onafterprint = function() {
|
||||||
|
// 移除打印容器和样式
|
||||||
|
document.body.removeChild(printContainer);
|
||||||
|
document.head.removeChild(originalPrintStyle);
|
||||||
|
window.onafterprint = null; // 清除事件处理
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用浏览器打印功能
|
||||||
|
window.print();
|
||||||
|
} catch (e) {
|
||||||
|
// 打印错误时恢复页面
|
||||||
|
try {
|
||||||
|
var printContainer = document.getElementById('print-container');
|
||||||
|
if (printContainer) document.body.removeChild(printContainer);
|
||||||
|
var printStyles = document.querySelectorAll('style[media="print"]');
|
||||||
|
printStyles.forEach(function(style) { document.head.removeChild(style); });
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('清理打印元素时出错:', cleanupError);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('打印失败:', e);
|
||||||
|
alert('打印失败,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
52
login.php
Normal file
52
login.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$pwd = $_POST['password'] ?? '';
|
||||||
|
if ($pwd === AUTH_PASSWORD) {
|
||||||
|
$_SESSION['logged_in'] = true;
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$error = '密码错误';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>登录 - 文档编辑器</title>
|
||||||
|
<link rel="stylesheet" href="assets/style.css" />
|
||||||
|
<style>
|
||||||
|
.login-wrap { height: 100vh; display:flex; align-items:center; justify-content:center; background: radial-gradient(800px 400px at 65% 10%, var(--bg-tint) 0%, var(--bg) 55%); }
|
||||||
|
.login-card { width: 360px; background: var(--panel); border: 1px solid var(--input-border); border-radius: 12px; box-shadow: var(--shadow); padding: 22px; }
|
||||||
|
.login-card h2 { margin:0 0 12px; font-size:18px; }
|
||||||
|
.login-card .field { display:flex; flex-direction:column; gap:8px; margin-top:10px; }
|
||||||
|
.login-card input[type=password] { padding:10px 12px; background: var(--input-bg); color: var(--text); border:1px solid var(--input-border); border-radius:8px; }
|
||||||
|
.login-card .actions { display:flex; gap:10px; margin-top:16px; justify-content:flex-end; }
|
||||||
|
.error { color:#d33; font-size:13px; margin-top:6px; }
|
||||||
|
.tips { color:var(--muted); font-size:12px; margin-top:10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-wrap">
|
||||||
|
<form class="login-card" method="post" autocomplete="off">
|
||||||
|
<h2>请输入访问密码</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input id="password" name="password" type="password" placeholder="输入密码" />
|
||||||
|
</div>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="error"><?= htmlspecialchars($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn primary" type="submit">登录</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
13
logout.php
Normal file
13
logout.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
$_SESSION = [];
|
||||||
|
if (ini_get('session.use_cookies')) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
setcookie(session_name(), '', time() - 42000,
|
||||||
|
$params['path'], $params['domain'], $params['secure'], $params['httponly']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
session_destroy();
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
|
||||||
158
share.php
Normal file
158
share.php
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
function readJson($file) {
|
||||||
|
if (!file_exists($file)) return [];
|
||||||
|
$s = file_get_contents($file);
|
||||||
|
$data = json_decode($s, true);
|
||||||
|
return is_array($data) ? $data : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeId($id) { return preg_match('/^[a-zA-Z0-9_-]+$/', $id); }
|
||||||
|
function writeJson($file, $data) { file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); }
|
||||||
|
|
||||||
|
$token = $_GET['token'] ?? '';
|
||||||
|
if (!$token) { http_response_code(400); echo 'Bad token'; exit; }
|
||||||
|
|
||||||
|
$baseDir = __DIR__ . DIRECTORY_SEPARATOR . 'data';
|
||||||
|
$docsDir = $baseDir . DIRECTORY_SEPARATOR . 'docs';
|
||||||
|
$indexFile = $baseDir . DIRECTORY_SEPARATOR . 'index.json';
|
||||||
|
$sharesFile = $baseDir . DIRECTORY_SEPARATOR . 'shares.json';
|
||||||
|
|
||||||
|
// 校验令牌
|
||||||
|
$shares = readJson($sharesFile);
|
||||||
|
$found = null;
|
||||||
|
foreach ($shares as $rec) { if (($rec['token'] ?? '') === $token) { $found = $rec; break; } }
|
||||||
|
if (!$found) { http_response_code(403); echo 'Unauthorized'; exit; }
|
||||||
|
if (!empty($found['revoked'])) { http_response_code(403); echo 'Unauthorized'; exit; }
|
||||||
|
if (!empty($found['expires_at']) && time() > $found['expires_at']) { http_response_code(403); echo 'Link expired'; exit; }
|
||||||
|
// 一次性链接已使用则拒绝访问
|
||||||
|
if (!empty($found['single_use']) && !empty($found['used'])) { http_response_code(403); echo 'Link already used'; exit; }
|
||||||
|
$id = $found['id'] ?? '';
|
||||||
|
if (!$id || !safeId($id)) { http_response_code(400); echo 'Bad id'; exit; }
|
||||||
|
|
||||||
|
$file = $docsDir . DIRECTORY_SEPARATOR . $id . '.html';
|
||||||
|
if (!file_exists($file)) { http_response_code(404); echo 'Not found'; exit; }
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
|
||||||
|
$index = readJson($indexFile);
|
||||||
|
$title = $id;
|
||||||
|
foreach ($index as $it) { if (($it['id'] ?? '') === $id) { $title = $it['title'] ?? $id; break; } }
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title><?= htmlspecialchars($title) ?> - 分享预览</title>
|
||||||
|
<link rel="stylesheet" href="assets/style.css" />
|
||||||
|
<style>
|
||||||
|
.share-wrap { display:grid; grid-template-columns: 1fr; height:100vh; }
|
||||||
|
.share-toolbar { display:flex; align-items:center; justify-content:space-between; padding:12px; border-bottom:2px solid var(--accent); background: var(--panel); }
|
||||||
|
.share-title { font-size:18px; margin:0; }
|
||||||
|
.theme-toggle { margin-left: 8px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="share-wrap">
|
||||||
|
<div class="share-toolbar">
|
||||||
|
<h1 class="share-title"><?= htmlspecialchars($title) ?></h1>
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<button id="share-print" class="btn">打印文档</button>
|
||||||
|
<button id="share-theme" class="btn theme-toggle">切换主题</button>
|
||||||
|
<a class="btn" href="index.php">返回主页</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-area" style="padding-top:12px;">
|
||||||
|
<?= $content ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// 初始化主题
|
||||||
|
(function(){
|
||||||
|
var t = localStorage.getItem('theme');
|
||||||
|
if (t === 'dark' || t === 'light') {
|
||||||
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
document.getElementById('share-theme').addEventListener('click', function(){
|
||||||
|
var root = document.documentElement;
|
||||||
|
var cur = root.getAttribute('data-theme') || 'light';
|
||||||
|
var next = cur === 'light' ? 'dark' : 'light';
|
||||||
|
root.setAttribute('data-theme', next);
|
||||||
|
try { localStorage.setItem('theme', next); } catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 打印功能
|
||||||
|
document.getElementById('share-print').addEventListener('click', function() {
|
||||||
|
try {
|
||||||
|
// 获取文档标题和内容
|
||||||
|
var title = document.querySelector('.share-title').textContent;
|
||||||
|
var content = document.querySelector('.editor-area').innerHTML;
|
||||||
|
|
||||||
|
// 创建打印内容容器
|
||||||
|
var printContainer = document.createElement('div');
|
||||||
|
printContainer.id = 'print-container';
|
||||||
|
printContainer.style.position = 'absolute';
|
||||||
|
printContainer.style.left = '-9999px';
|
||||||
|
printContainer.style.width = '800px'; // 设置合适的打印宽度
|
||||||
|
|
||||||
|
// 设置打印内容
|
||||||
|
printContainer.innerHTML = '<h1 style="text-align:center; font-family:Arial, sans-serif;">' +
|
||||||
|
title +
|
||||||
|
'</h1><div style="font-family:Arial, sans-serif; line-height:1.6;">' +
|
||||||
|
content +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
// 添加到页面
|
||||||
|
document.body.appendChild(printContainer);
|
||||||
|
|
||||||
|
// 创建打印样式
|
||||||
|
var printStyle = document.createElement('style');
|
||||||
|
printStyle.media = 'print';
|
||||||
|
printStyle.innerHTML =
|
||||||
|
'#print-container { position: static !important; width: 100% !important; } ' +
|
||||||
|
'body > *:not(#print-container) { display: none !important; } ' +
|
||||||
|
'body { margin: 2cm; }';
|
||||||
|
document.head.appendChild(printStyle);
|
||||||
|
|
||||||
|
// 保存原始样式引用,以便稍后移除
|
||||||
|
var originalPrintStyle = printStyle;
|
||||||
|
|
||||||
|
// 当打印对话框关闭后恢复页面
|
||||||
|
window.onafterprint = function() {
|
||||||
|
// 移除打印容器和样式
|
||||||
|
document.body.removeChild(printContainer);
|
||||||
|
document.head.removeChild(originalPrintStyle);
|
||||||
|
window.onafterprint = null; // 清除事件处理
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用浏览器打印功能
|
||||||
|
window.print();
|
||||||
|
} catch (e) {
|
||||||
|
// 打印错误时恢复页面
|
||||||
|
try {
|
||||||
|
var printContainer = document.getElementById('print-container');
|
||||||
|
if (printContainer) document.body.removeChild(printContainer);
|
||||||
|
var printStyles = document.querySelectorAll('style[media="print"]');
|
||||||
|
printStyles.forEach(function(style) { document.head.removeChild(style); });
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('清理打印元素时出错:', cleanupError);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('打印失败:', e);
|
||||||
|
alert('打印失败,请重试');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
// 标记一次性链接为已使用
|
||||||
|
if (!empty($found['single_use']) && empty($found['used'])) {
|
||||||
|
foreach ($shares as &$rec) {
|
||||||
|
if (($rec['token'] ?? '') === $token) { $rec['used'] = true; break; }
|
||||||
|
}
|
||||||
|
writeJson($sharesFile, $shares);
|
||||||
|
}
|
||||||
|
?>
|
||||||
40
upload.php
Normal file
40
upload.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
// 处理图片上传,返回可访问的URL
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
require_api_auth();
|
||||||
|
|
||||||
|
$baseDir = __DIR__ . DIRECTORY_SEPARATOR . 'data';
|
||||||
|
$uploadsDir = $baseDir . DIRECTORY_SEPARATOR . 'uploads';
|
||||||
|
if (!is_dir($uploadsDir)) { mkdir($uploadsDir, 0777, true); }
|
||||||
|
|
||||||
|
if (!isset($_FILES['image'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'no file']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['image'];
|
||||||
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'upload error']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mime = finfo_file($finfo, $file['tmp_name']);
|
||||||
|
finfo_close($finfo);
|
||||||
|
if (!preg_match('/^image\//', $mime)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'not image']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||||
|
$name = 'img_' . uniqid() . ($ext ? ('.' . preg_replace('/[^a-zA-Z0-9]/', '', $ext)) : '');
|
||||||
|
$dest = $uploadsDir . DIRECTORY_SEPARATOR . $name;
|
||||||
|
move_uploaded_file($file['tmp_name'], $dest);
|
||||||
|
|
||||||
|
// 构造相对URL(基于XAMPP的htdocs路径)
|
||||||
|
$url = '/PHP/wd/data/uploads/' . $name;
|
||||||
|
echo json_encode(['url' => $url], JSON_UNESCAPED_UNICODE);
|
||||||
Reference in New Issue
Block a user