# cms-view
**Repository Path**: skyllyf/cms-view
## Basic Information
- **Project Name**: cms-view
- **Description**: 简单的图床
- **Primary Language**: PHP
- **License**: AGPL-3.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-03-24
- **Last Updated**: 2026-10-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 林枫视界 - SkyView v3
一个面向图库的 CMS 媒体管理系统(图片托管 / 图床)。
**v3 的核心变化**:应用更名为「林枫视界 - SkyView」,模块图升级为整目录版本化(部署即换 URL,旧缓存自然失效),手机端兼容性大幅增强(去 importmap、去 nonce、懒加载 ES5 兜底、后台渲染看门狗)。
**v2 的核心变化**:后端从「单文件过程式 + 全局函数」重构为分层架构(PSR-4 自动加载 / 路由 / 控制器 / 仓储 / 服务),前端从 Tailwind Play CDN + Petite-Vue + 全局函数重构为**免构建 Vue 3 ESM 模块**。整站定位为图床(**只接受图片,不含视频上传 / 播放**),因此**不依赖 `exec`、`shell_exec`、`system`、`passthru`、`popen`、`proc_open` 等任何命令执行函数**,可以放心地把它们写进 `disable_functions`。
---
## 目录
- [设计要点](#设计要点)
- [功能特性](#功能特性)
- [技术栈](#技术栈)
- [运行环境](#运行环境)
- [快速开始](#快速开始)
- [目录结构](#目录结构)
- [路由与接口](#路由与接口)
- [插件开发](#插件开发)
- [从旧版本迁移](#从旧版本迁移)
- [安全说明](#安全说明)
- [性能说明](#性能说明)
- [文档](#文档)
- [License](#license)
---
## 设计要点
### 1. 首页 PHP 预渲染(SSR)+ Vue 渐进增强
首屏 HTML 由 PHP 直接输出,**图片、标题、标签都已经在 HTML 里**。搜索引擎和无 JS 环境拿到的是完整的页面,不会看到空白骨架。
Vue 挂载在同一份 DOM 上(`v-cloak` 在挂载完成后移除),只接管后续的增量交互:
| 交给 PHP(SSR) | 交给 Vue |
| ------------------------------- | ------------------------------ |
| 首屏卡片列表、缩略图、`
` | 关键词筛选(无刷新) |
| ``、Open Graph、canonical | 无限滚动加载下一页 |
| 分页号、总数 | 详情页左右切换、键盘 / 触摸导航 |
| 导航栏、页脚、公告 | 后台表格、上传进度、表单保存 |
这样做的收益很直接:**首屏零请求瀑布**,滚动到第 2 页才发第一次 XHR。
### 2. PHP → Vue 的单向数据通道 `page_bootstrap()`
`View::render()` 会分两次 `include`:先渲染内容视图(`home.php`),再用结果去渲染布局(`base.php`)。**这两次 `include` 是两个独立的函数作用域**——内容视图里 `$bootstrap = [...]` 定义的局部变量,布局里读不到。
所以用一个静态存储做中转:
```php
// 内容视图 app/Views/home.php
page_bootstrap([
'panel' => 'waterfall',
'initial' => $items, // 首屏数据,Vue 直接用,不再请求
'total' => $total,
]);
// 布局 app/Views/layouts/base.php
```
入口 JS(`assets/js/app.js`)再按 `__BOOTSTRAP__.panel` 分派到对应的 Vue 组件。首屏数据因此**不会**被重复请求一次。
### 3. 严格 CSP + 每请求 nonce
响应头里的 `script-src` 不含 `'unsafe-inline'`,所有内联脚本都带 `nonce`:
```
Content-Security-Policy:
default-src 'self';
img-src 'self' data: blob: https:;
media-src 'self' blob: https:;
style-src 'self' 'unsafe-inline';
script-src 'self' 'nonce-<每请求随机>' 'unsafe-eval';
connect-src 'self'; font-src 'self' data:;
object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
```
`'unsafe-eval'` 是 Vue 运行时模板编译器所必需的。另有两点必须注意(**这是 v2 开发中踩到的真实坑,见 `docs/BUGS-FIXED.md` #31 / #33**):
- **`new Function()` 需要 `'unsafe-eval'`**。只写 `'unsafe-inline'` 时,浏览器会抛 `EvalError`,**整个 Vue 交互层静默失效**——页面看着正常(PHP 首屏完整、`v-cloak` 被兜底移除),实际筛选、滚动、翻页、后台全部不响应。
- **内联事件属性(`onerror="..."` / `onclick="..."`)在 nonce 模式下会被拦截**。项目内已全部改为 `data-*` 属性 + `theme.js` 里的 `bindImageFallbacks()` 统一处理(并且处理了「脚本执行时图片已经加载失败、`error` 事件不会再触发」的边界)。
### 4. 双数据库驱动:MySQL 主 + SQLite 兜底
`App\Database\Connection` 接口 + `Grammar` 方言适配器。差异点被收敛到四个方法里:
| 差异点 | MySQL | SQLite |
| ------------------ | ---------------------------------------- | ------------------------- |
| 自增主键 | `BIGINT UNSIGNED AUTO_INCREMENT` | `INTEGER PRIMARY KEY AUTOINCREMENT` |
| 表后缀 | `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4` | 无 |
| 分页 | `LIMIT n OFFSET m` | 同上 |
| Upsert | `ON DUPLICATE KEY UPDATE` | `ON CONFLICT(...) DO UPDATE SET` |
生产用 MySQL 8,本地开发/临时部署可以零配置落到 SQLite(`storage/database.sqlite`),安装向导里选一下即可。**注意**:`settings` 表的 `key` / `value` 是 MySQL 8 保留字,SQL 里已全部加反引号。
### 5. 免构建 Vue 3
没有 webpack / vite / npm 依赖,没有 `node_modules`,直接 `