@@ -0,0 +1,91 @@
|
||||
# PDF 预览系统 (PDF Preview Site)
|
||||
|
||||
这是一个安全、现代化的 PDF 预览系统,具有管理员后台和公共预览功能。
|
||||
|
||||
## 🖥 本地开发
|
||||
|
||||
前后端分开运行:
|
||||
|
||||
```bash
|
||||
# 终端 1:启动后端(API 服务)
|
||||
cd backend && npm install && npm start
|
||||
# → http://localhost:3000
|
||||
|
||||
# 终端 2:启动前端(开发服务器,支持热更新)
|
||||
cd frontend && npm install && npm run dev
|
||||
# → http://localhost:5173
|
||||
```
|
||||
|
||||
## 📦 打包 & 部署到服务器
|
||||
|
||||
只需要上传 `backend` 文件夹即可,步骤如下:
|
||||
|
||||
### 第一步:在本地打包前端
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
执行后,前端产物会自动输出到 `backend/public/` 目录中。
|
||||
|
||||
### 第二步:上传 backend 文件夹到服务器
|
||||
|
||||
将整个 `backend/` 文件夹上传到服务器(通过 FTP/SCP/Git 均可)。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 上传时注意包含以下内容:
|
||||
> - `backend/public/`(前端打包产物,刚才 build 生成的)
|
||||
> - `backend/uploads/`(已上传的 PDF 文件)
|
||||
> - `backend/database.sqlite`(数据库文件,包含管理员账号和文档记录)
|
||||
>
|
||||
> 可以把 `backend/node_modules/` 排除,在服务器上重新 `npm install` 即可。
|
||||
|
||||
### 第三步:在服务器上启动
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm install # 仅首次或依赖变更时需要
|
||||
npm start # 启动服务
|
||||
```
|
||||
|
||||
访问 `http://服务器IP:3000` 即可看到网站,**前后端都由这一个进程提供服务**。
|
||||
|
||||
### 第四步(推荐):用 PM2 长期运行
|
||||
|
||||
```bash
|
||||
npm install pm2 -g # 全局安装 PM2(仅首次)
|
||||
pm2 start server.js --name pdf-site # 后台启动
|
||||
pm2 save && pm2 startup # 设置开机自启
|
||||
```
|
||||
|
||||
常用管理命令:
|
||||
```bash
|
||||
pm2 status # 查看运行状态
|
||||
pm2 logs pdf-site # 查看日志
|
||||
pm2 restart pdf-site # 重启服务
|
||||
```
|
||||
|
||||
## 🔐 管理后台
|
||||
|
||||
- **后台地址**: `http://服务器IP:3000/admin`
|
||||
- **默认用户名**: `admin`
|
||||
- **默认密码**: `123456`
|
||||
|
||||
> [!WARNING]
|
||||
> 生产环境请务必修改默认密码!详见修改方法:在 `backend` 目录运行以下命令(替换新用户名/密码):
|
||||
> ```bash
|
||||
> node -e "const db=require('./database');const bcrypt=require('bcryptjs');const h=bcrypt.hashSync('新密码',10);db.prepare('UPDATE users SET username=?,password=? WHERE id=1').run('新用户名',h);console.log('已更新');"
|
||||
> ```
|
||||
|
||||
## 🛠 技术栈
|
||||
- **前端**: Vue 3 + Vite + Glassmorphism UI
|
||||
- **后端**: Node.js + Express(同时托管前端静态文件)
|
||||
- **数据库**: SQLite (better-sqlite3)
|
||||
- **PDF 处理**: 提取前两页进行预览
|
||||
|
||||
## 📁 目录结构
|
||||
- `/frontend`: Vue 3 源码(本地开发用,部署后不需要上传)
|
||||
- `/backend`: Node.js 服务 + 前端打包产物(部署只需上传这个)
|
||||
- `/backend/public/`: 前端 build 产物(由 `npm run build` 自动生成)
|
||||
- `/backend/database.sqlite`: 存储文档信息和管理员账号
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,6 +4,7 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
<script type="module" crossorigin src="/assets/index-CBnsKpxD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DG_v3cGl.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -15,6 +15,8 @@ const JWT_SECRET = 'super-secret-key-pdf-preview';
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use('/uploads/processed', express.static(path.join(__dirname, 'uploads/processed')));
|
||||
// Serve the built frontend from the public folder
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Ensure upload directories exist
|
||||
const uploadDirOrig = path.join(__dirname, 'uploads/original');
|
||||
@@ -24,7 +26,8 @@ if (!fs.existsSync(uploadDirProc)) fs.mkdirSync(uploadDirProc, { recursive: true
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, 'uploads/original/');
|
||||
// 使用绝对路径,避免服务器 CWD 不在项目目录时写入失败
|
||||
cb(null, path.join(__dirname, 'uploads/original/'));
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
@@ -79,7 +82,13 @@ app.post('/api/admin/upload', authenticateJWT, upload.single('document'), async
|
||||
|
||||
res.json({ message: 'Upload successful', documentId: info.lastInsertRowid });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: 'Error processing PDF', error: error.toString() });
|
||||
// 输出完整堆栈到服务器日志,便于排查
|
||||
console.error('Upload error details:', error.stack || error);
|
||||
res.status(500).json({
|
||||
message: 'Error processing PDF',
|
||||
error: error.message || error.toString(),
|
||||
stack: process.env.NODE_ENV !== 'production' ? error.stack : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -105,7 +114,7 @@ app.get('/api/documents', (req, res) => {
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
created_at: doc.created_at,
|
||||
url: `http://localhost:3000/${doc.processed_path}`
|
||||
url: `/${doc.processed_path}`
|
||||
}));
|
||||
res.json(responseData);
|
||||
});
|
||||
@@ -119,10 +128,22 @@ app.get('/api/documents/:id', (req, res) => {
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
created_at: doc.created_at,
|
||||
url: `http://localhost:3000/${doc.processed_path}`
|
||||
url: `/${doc.processed_path}`
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Backend server running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
// SPA fallback: must be after all API routes so Vue Router can handle client-side routing
|
||||
// Note: app.use() (not app.get('*')) is required for Express 5 compatibility
|
||||
app.use((req, res) => {
|
||||
const indexPath = path.join(__dirname, 'public', 'index.html');
|
||||
if (fs.existsSync(indexPath)) {
|
||||
res.sendFile(indexPath);
|
||||
} else {
|
||||
res.status(404).send('Frontend not built yet. Run: cd frontend && npm run build');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
# 开发环境:前后端分离,后端运行在 3000 端口
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
@@ -0,0 +1,2 @@
|
||||
# 生产环境:前端由后端托管,API 使用相对路径(空字符串即可)
|
||||
VITE_API_BASE_URL=
|
||||
@@ -1,5 +1,25 @@
|
||||
# Vue 3 + Vite
|
||||
# PDF 预览系统 - 前端 (Frontend)
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
这是基于 Vue 3 和 Vite 构建的现代化前端界面。
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
## 🔐 管理员入口
|
||||
|
||||
如果你想进入管理后台:
|
||||
- **地址**: 访问网站首页并在 URL 后添加 `/admin` (例如: `http://localhost:5173/admin`)
|
||||
- **账号**: `admin`
|
||||
- **密码**: `123456`
|
||||
|
||||
## 🛠 常用命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
```
|
||||
|
||||
有关完整项目说明,请参阅根目录下的 [README.md](../README.md)。
|
||||
|
||||
Binary file not shown.
@@ -61,6 +61,8 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const API = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const isLoggedIn = ref(false);
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
@@ -82,7 +84,7 @@ onMounted(() => {
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const res = await axios.post('http://localhost:3000/api/admin/login', {
|
||||
const res = await axios.post(`${API}/api/admin/login`, {
|
||||
username: username.value,
|
||||
password: password.value
|
||||
});
|
||||
@@ -102,7 +104,7 @@ const logout = () => {
|
||||
|
||||
const fetchDocs = async () => {
|
||||
try {
|
||||
const res = await axios.get('http://localhost:3000/api/documents');
|
||||
const res = await axios.get(`${API}/api/documents`);
|
||||
documents.value = res.data;
|
||||
} catch (e) {
|
||||
console.error("Failed to load documents");
|
||||
@@ -122,7 +124,7 @@ const handleUpload = async () => {
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
await axios.post('http://localhost:3000/api/admin/upload', formData, {
|
||||
await axios.post(`${API}/api/admin/upload`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': `Bearer ${token}`
|
||||
@@ -145,7 +147,7 @@ const deleteDoc = async (id) => {
|
||||
if(!confirm("确定要执行此操作吗?")) return;
|
||||
try {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
await axios.delete(`http://localhost:3000/api/admin/documents/${id}`, {
|
||||
await axios.delete(`${API}/api/admin/documents/${id}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
fetchDocs();
|
||||
|
||||
@@ -31,10 +31,11 @@ import axios from 'axios';
|
||||
const documents = ref([]);
|
||||
const loading = ref(true);
|
||||
const router = useRouter();
|
||||
const API = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axios.get('http://localhost:3000/api/documents');
|
||||
const res = await axios.get(`${API}/api/documents`);
|
||||
documents.value = res.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching docs", error);
|
||||
|
||||
@@ -27,12 +27,18 @@ const router = useRouter();
|
||||
const document = ref(null);
|
||||
const loading = ref(true);
|
||||
const error = ref('');
|
||||
const API = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const id = route.params.id;
|
||||
const res = await axios.get(`http://localhost:3000/api/documents/${id}`);
|
||||
document.value = res.data;
|
||||
const res = await axios.get(`${API}/api/documents/${id}`);
|
||||
// 将相对路径拼接为完整 URL,解决开发模式下跨端口访问问题
|
||||
const data = res.data;
|
||||
if (data.url && data.url.startsWith('/')) {
|
||||
data.url = `${API}${data.url}`;
|
||||
}
|
||||
document.value = data;
|
||||
} catch (err) {
|
||||
error.value = "文档未找到或无法访问。";
|
||||
} finally {
|
||||
|
||||
@@ -4,4 +4,9 @@ import vue from '@vitejs/plugin-vue'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
// Build output goes directly into the backend's public folder
|
||||
outDir: '../backend/public',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user