chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+280
View File
@@ -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');
}
}