chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\docs\controller;
|
||||
|
||||
use addon\docs\model\DocsDoc;
|
||||
use addon\docs\model\DocsProject;
|
||||
use addon\docs\model\DocsVersion;
|
||||
use addon\docs\service\DocTreeService;
|
||||
|
||||
/**
|
||||
* 文档阅读页(三栏布局)
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Read extends DocsFrontend
|
||||
{
|
||||
/**
|
||||
* 项目入口:跳转到该项目第一篇文档
|
||||
*
|
||||
* @param string $project 项目标识
|
||||
*/
|
||||
public function entry($project = '')
|
||||
{
|
||||
$projectModel = DocsProject::findByName((string) $project);
|
||||
if (!$projectModel) {
|
||||
return $this->notFound('文档项目不存在或已下线');
|
||||
}
|
||||
|
||||
$version = DocsVersion::resolve((int) $projectModel->id, (string) $projectModel->default_version);
|
||||
if (!$version) {
|
||||
return $this->notFound('该项目尚未创建版本');
|
||||
}
|
||||
|
||||
$first = DocTreeService::firstDoc((int) $projectModel->id, (int) $version->id);
|
||||
if (!$first) {
|
||||
return $this->notFound('该项目暂无可阅读的文档');
|
||||
}
|
||||
|
||||
return redirect('/docs/' . $projectModel->name . '/' . $first['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档正文
|
||||
*
|
||||
* @param string $project 项目标识
|
||||
* @param string $name 文档标识
|
||||
* @param string $version 版本标识,可为空
|
||||
*/
|
||||
public function index($project = '', $name = '', $version = '')
|
||||
{
|
||||
$projectModel = DocsProject::findByName((string) $project);
|
||||
if (!$projectModel) {
|
||||
return $this->notFound('文档项目不存在或已下线');
|
||||
}
|
||||
|
||||
$versionModel = DocsVersion::resolve(
|
||||
(int) $projectModel->id,
|
||||
(string) ($version !== '' ? $version : $projectModel->default_version)
|
||||
);
|
||||
if (!$versionModel) {
|
||||
return $this->notFound('该项目尚未创建版本');
|
||||
}
|
||||
|
||||
$projectId = (int) $projectModel->id;
|
||||
$versionId = (int) $versionModel->id;
|
||||
|
||||
$doc = DocsDoc::findByName($projectId, $versionId, (string) $name);
|
||||
if (!$doc) {
|
||||
return $this->notFound('文档不存在或已下线');
|
||||
}
|
||||
|
||||
// 纯目录节点没有正文,跳到它下面第一篇可读文档
|
||||
if ((int) $doc->is_dir === 1) {
|
||||
$child = DocsDoc::where('project_id', $projectId)
|
||||
->where('version_id', $versionId)
|
||||
->where('pid', $doc->id)
|
||||
->where('status', 1)
|
||||
->where('is_dir', 0)
|
||||
->order('sort', 'asc')->order('id', 'asc')
|
||||
->find();
|
||||
if ($child) {
|
||||
return redirect($this->docUrl($projectModel, $versionModel, (string) $child->name));
|
||||
}
|
||||
}
|
||||
|
||||
DocsDoc::addViews((int) $doc->id);
|
||||
|
||||
$tree = DocTreeService::buildTree($projectId, $versionId);
|
||||
$siblings = DocTreeService::siblings($projectId, $versionId, (int) $doc->id);
|
||||
$crumbs = DocTreeService::breadcrumb($projectId, $versionId, (int) $doc->id);
|
||||
|
||||
// 给正文标题补 id 锚点,供右侧目录跳转
|
||||
[$content, $toc] = $this->buildToc((string) $doc->content);
|
||||
|
||||
// 默认版本用短地址,非默认版本地址里带版本段
|
||||
$baseUrl = '/docs/' . $projectModel->name;
|
||||
if ((int) $versionModel->is_default !== 1) {
|
||||
$baseUrl .= '/' . $versionModel->name;
|
||||
}
|
||||
|
||||
$treeHtml = $this->renderTree(
|
||||
$tree,
|
||||
$baseUrl,
|
||||
(int) $doc->id,
|
||||
array_column($crumbs, 'id')
|
||||
);
|
||||
|
||||
$updateAt = $doc->update_at;
|
||||
$updateTs = !is_numeric($updateAt) ? strtotime((string) $updateAt) : (int) $updateAt;
|
||||
$updateDate = date('Y-m-d', (int) $updateTs);
|
||||
|
||||
$this->assign([
|
||||
'baseUrl' => $baseUrl,
|
||||
'project' => $projectModel,
|
||||
'version' => $versionModel,
|
||||
'versionList' => DocsVersion::listByProject($projectId),
|
||||
'doc' => $doc,
|
||||
'content' => $content,
|
||||
'toc' => $toc,
|
||||
'treeHtml' => $treeHtml,
|
||||
'updateDate' => $updateDate,
|
||||
'crumbs' => $crumbs,
|
||||
'prev' => $siblings['prev'],
|
||||
'next' => $siblings['next'],
|
||||
'activeId' => (int) $doc->id,
|
||||
'openIds' => array_column($crumbs, 'id'),
|
||||
'pageTitle' => ($doc->seo_title !== '' ? $doc->seo_title : $doc->title) . ' - ' . $projectModel->title,
|
||||
'seoKeywords' => $doc->seo_keywords,
|
||||
'seoDesc' => $doc->seo_desc !== '' ? $doc->seo_desc : mb_substr(strip_tags((string) $doc->content), 0, 150, 'UTF-8'),
|
||||
]);
|
||||
|
||||
return $this->fetch('read/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归渲染侧边文档树为 HTML(避免模板 {php} 块内定义函数导致的编译错误)
|
||||
*
|
||||
* @param array $nodes 树节点
|
||||
* @param string $baseUrl 文档基础 URL
|
||||
* @param int $activeId 当前文档 ID
|
||||
* @param array $openIds 需要展开的祖先节点 ID
|
||||
* @return string
|
||||
*/
|
||||
protected function renderTree(array $nodes, string $baseUrl, int $activeId, array $openIds): string
|
||||
{
|
||||
if (empty($nodes)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '<ul class="dc-tree">';
|
||||
foreach ($nodes as $node) {
|
||||
$id = (int) $node['id'];
|
||||
$isDir = (int) ($node['is_dir'] ?? 0) === 1;
|
||||
$hasChild = !empty($node['children']);
|
||||
$isActive = $id === $activeId;
|
||||
$isOpen = $isActive || in_array($id, $openIds, true);
|
||||
$title = htmlspecialchars((string) $node['title'], ENT_QUOTES, 'UTF-8');
|
||||
$url = $baseUrl . '/' . rawurlencode((string) $node['name']);
|
||||
|
||||
$liClass = [];
|
||||
if ($hasChild) {
|
||||
$liClass[] = 'has-child';
|
||||
}
|
||||
if ($isOpen) {
|
||||
$liClass[] = 'is-open';
|
||||
}
|
||||
|
||||
$html .= '<li class="' . implode(' ', $liClass) . '">';
|
||||
$html .= '<div class="dc-tree-item' . ($isActive ? ' is-active' : '') . '">';
|
||||
|
||||
if ($hasChild) {
|
||||
$html .= '<span class="dc-tree-toggle" aria-label="展开"></span>';
|
||||
}
|
||||
|
||||
if ($isDir && $hasChild) {
|
||||
$html .= '<span class="dc-tree-link is-dir">' . $title . '</span>';
|
||||
} else {
|
||||
$html .= '<a class="dc-tree-link" href="' . htmlspecialchars($url, ENT_QUOTES) . '">' . $title . '</a>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
if ($hasChild) {
|
||||
$html .= $this->renderTree($node['children'], $baseUrl, $activeId, $openIds);
|
||||
}
|
||||
|
||||
$html .= '</li>';
|
||||
}
|
||||
$html .= '</ul>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为正文中的 h2 / h3 标题补充锚点并抽取目录
|
||||
*
|
||||
* @param string $html 正文 HTML
|
||||
* @return array{0: string, 1: array}
|
||||
*/
|
||||
protected function buildToc(string $html): array
|
||||
{
|
||||
if ($html === '') {
|
||||
return ['', []];
|
||||
}
|
||||
|
||||
$toc = [];
|
||||
$index = 0;
|
||||
|
||||
$result = preg_replace_callback(
|
||||
'#<h([23])([^>]*)>(.*?)</h\1>#is',
|
||||
function ($m) use (&$toc, &$index) {
|
||||
$level = (int) $m[1];
|
||||
$attrs = $m[2];
|
||||
$inner = $m[3];
|
||||
$text = trim(strip_tags($inner));
|
||||
|
||||
if ($text === '') {
|
||||
return $m[0];
|
||||
}
|
||||
|
||||
$index++;
|
||||
$anchor = 'doc-h-' . $index;
|
||||
|
||||
// 已有 id 时沿用,避免破坏作者自定义锚点
|
||||
if (preg_match('/\sid\s*=\s*["\']([^"\']+)["\']/i', $attrs, $idMatch)) {
|
||||
$anchor = $idMatch[1];
|
||||
} else {
|
||||
$attrs .= ' id="' . $anchor . '"';
|
||||
}
|
||||
|
||||
$toc[] = [
|
||||
'level' => $level,
|
||||
'text' => $text,
|
||||
'anchor' => $anchor,
|
||||
];
|
||||
|
||||
return '<h' . $level . $attrs . '>' . $inner . '</h' . $level . '>';
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
return [$result ?? $html, $toc];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文档访问地址
|
||||
*
|
||||
* @param DocsProject $project 项目
|
||||
* @param DocsVersion $version 版本
|
||||
* @param string $name 文档标识
|
||||
* @return string
|
||||
*/
|
||||
protected function docUrl(DocsProject $project, DocsVersion $version, string $name): string
|
||||
{
|
||||
// 默认版本走短地址,非默认版本带上版本段
|
||||
if ((int) $version->is_default === 1) {
|
||||
return '/docs/' . $project->name . '/' . $name;
|
||||
}
|
||||
return '/docs/' . $project->name . '/' . $version->name . '/' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 404 提示页
|
||||
*
|
||||
* @param string $message 提示语
|
||||
*/
|
||||
protected function notFound(string $message)
|
||||
{
|
||||
$this->assign('message', $message);
|
||||
return $this->fetch('read/notfound');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user