feat: 富文本编辑

This commit is contained in:
2026-04-16 16:20:52 +08:00
parent 295cf12899
commit 6acd0ccad0
8 changed files with 150 additions and 40 deletions

View File

@@ -110,7 +110,7 @@ class ManageGuidePages extends Page
$this->nodes = $pages->map(fn(GuidePage $p) => [
'id' => $p->id,
'title' => $p->title,
'html_url' => $p->html_url,
'uri' => $p->uri,
'is_entry' => !isset($hasIncoming[$p->id]),
'options' => $p->options ?? [],
'x' => $positions[$p->id]['x'] ?? 50,
@@ -207,7 +207,7 @@ class ManageGuidePages extends Page
$page = $this->getRecord()->pages()->findOrFail($arguments['id']);
$form->fill([
'title' => $page->title,
'html_url' => $page->html_url,
'content' => $page->content,
'options' => $page->options ?? [],
]);
})
@@ -261,11 +261,13 @@ class ManageGuidePages extends Page
->required()
->maxLength(255),
Forms\Components\TextInput::make('html_url')
->label('HTML页面URL')
Forms\Components\RichEditor::make('content')
->label('页面内容')
->required()
->url()
->maxLength(500),
->fileAttachmentsDisk('public')
->fileAttachmentsDirectory('guide-pages')
->fileAttachmentsVisibility('public')
->columnSpanFull(),
Forms\Components\TagsInput::make('options')
->label('分支选项')

View File

@@ -139,7 +139,7 @@ class TerminalApiController extends Controller
$pagesMap[$page->id] = [
'id' => $page->id,
'title' => $page->title,
'html_url' => $page->html_url,
'uri' => $page->uri,
'next' => $next,
];
}

View File

@@ -9,7 +9,7 @@ class GuidePage extends Model
protected $fillable = [
'guide_id',
'title',
'html_url',
'content',
'options',
];
@@ -25,6 +25,11 @@ class GuidePage extends Model
});
}
public function getUriAttribute(): string
{
return route('guides.pages.show', $this->id);
}
public function guide()
{
return $this->belongsTo(Guide::class);

View File

@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('guide_pages', function (Blueprint $table) {
$table->dropColumn('html_url');
$table->longText('content')->nullable()->after('title')->comment('富文本正文HTML');
});
}
public function down(): void
{
Schema::table('guide_pages', function (Blueprint $table) {
$table->dropColumn('content');
$table->string('html_url', 500)->after('title')->comment('HTML页面链接');
});
}
};

View File

@@ -11,8 +11,6 @@ use Illuminate\Database\Seeder;
class GuideSeeder extends Seeder
{
private const BASE_URL = 'https://ssrf.9z.work/guides';
/**
* Run the database seeds.
*/
@@ -46,6 +44,11 @@ class GuideSeeder extends Seeder
$this->command->info(' - 关联线站数量: ' . $stations->count());
}
private function placeholder(string $title): string
{
return '<p>本步骤说明待补充。管理员可在 Filament 后台使用富文本编辑器完善「' . e($title) . '」的操作指引。</p>';
}
private function createHowToUseBeamGuide(User $admin): Guide
{
$this->command->info('创建指引: 如何用光...');
@@ -60,49 +63,47 @@ class GuideSeeder extends Seeder
'published_at' => now(),
]);
$baseUrl = self::BASE_URL . '/how-to-use-beam';
$step1 = GuidePage::create([
'guide_id' => $guide->id,
'title' => '打开光子光闸 PS1',
'html_url' => "{$baseUrl}/step-1.html",
'content' => $this->placeholder('打开光子光闸 PS1'),
]);
$step2 = GuidePage::create([
'guide_id' => $guide->id,
'title' => '搜索光学棚屋',
'html_url' => "{$baseUrl}/step-2.html",
'content' => $this->placeholder('搜索光学棚屋'),
'options' => ['前门12', '后门'],
]);
$step3a = GuidePage::create([
'guide_id' => $guide->id,
'title' => '前门12路径 - 检查设备状态',
'html_url' => "{$baseUrl}/step-3a.html",
'content' => $this->placeholder('前门12路径 - 检查设备状态'),
]);
$step3b = GuidePage::create([
'guide_id' => $guide->id,
'title' => '后门路径 - 安全确认',
'html_url' => "{$baseUrl}/step-3b.html",
'content' => $this->placeholder('后门路径 - 安全确认'),
]);
$step4a = GuidePage::create([
'guide_id' => $guide->id,
'title' => '前门12路径 - 打开实验站光闸',
'html_url' => "{$baseUrl}/step-4a.html",
'content' => $this->placeholder('前门12路径 - 打开实验站光闸'),
]);
$step4b = GuidePage::create([
'guide_id' => $guide->id,
'title' => '后门路径 - 设备检查',
'html_url' => "{$baseUrl}/step-4b.html",
'content' => $this->placeholder('后门路径 - 设备检查'),
]);
$step5 = GuidePage::create([
'guide_id' => $guide->id,
'title' => '完成',
'html_url' => "{$baseUrl}/step-5.html",
'content' => $this->placeholder('完成'),
]);
// step1 → step2 (sequential)
@@ -185,21 +186,20 @@ class GuideSeeder extends Seeder
'published_at' => now(),
]);
$baseUrl = self::BASE_URL . '/vacuum-valve-issue';
$steps = [
['title' => '检查真空度', 'file' => 'step-1.html'],
['title' => '检查联锁状态', 'file' => 'step-2.html'],
['title' => '尝试手动复位', 'file' => 'step-3.html'],
['title' => '检查气动系统', 'file' => 'step-4.html'],
['title' => '联系维护人员', 'file' => 'step-5.html'],
'检查真空度',
'检查联锁状态',
'尝试手动复位',
'检查气动系统',
'联系维护人员',
];
$pages = [];
foreach ($steps as $i => $step) {
foreach ($steps as $title) {
$pages[] = GuidePage::create([
'guide_id' => $guide->id,
'title' => $step['title'],
'html_url' => "{$baseUrl}/{$step['file']}",
'title' => $title,
'content' => $this->placeholder($title),
]);
}
@@ -231,21 +231,20 @@ class GuideSeeder extends Seeder
'published_at' => now(),
]);
$baseUrl = self::BASE_URL . '/water-leak-alarm';
$steps = [
['title' => '确认报警位置', 'file' => 'step-1.html'],
['title' => '搜索光学棚屋', 'file' => 'step-2.html'],
['title' => '定位并处理漏水点', 'file' => 'step-3.html'],
['title' => '复位报警', 'file' => 'step-4.html'],
['title' => '完成', 'file' => 'step-5.html'],
'确认报警位置',
'搜索光学棚屋',
'定位并处理漏水点',
'复位报警',
'完成',
];
$pages = [];
foreach ($steps as $i => $step) {
foreach ($steps as $title) {
$pages[] = GuidePage::create([
'guide_id' => $guide->id,
'title' => $step['title'],
'html_url' => "{$baseUrl}/{$step['file']}",
'title' => $title,
'content' => $this->placeholder($title),
]);
}

View File

@@ -88,17 +88,20 @@
padding-top: 6px;
}
.df-node-actions button {
.df-node-actions button,
.df-node-actions a {
font-size: 11px;
color: #6b7280;
cursor: pointer;
background: none;
border: none;
padding: 2px 0;
text-decoration: none;
transition: color 0.15s;
}
.df-node-actions button:hover {
.df-node-actions button:hover,
.df-node-actions a:hover {
color: #3b82f6;
}
@@ -256,11 +259,12 @@
const html = `
<div class="df-node-content">
<div class="df-node-header">${node.title}</div>
<div class="df-node-url">${node.html_url}</div>
<div class="df-node-url">${node.uri}</div>
${node.is_entry ? '<div><span class="df-node-badge">入口</span></div>' : ''}
<div class="df-node-actions">
<button onclick="event.stopPropagation(); Livewire.find('${@js($this->getId())}').mountAction('editPage', { id: ${node.id} })">编辑</button>
<button class="btn-danger" onclick="event.stopPropagation(); Livewire.find('${@js($this->getId())}').mountAction('deletePage', { id: ${node.id} })">删除</button>
<a href="${node.uri}" target="_blank" rel="noopener" onclick="event.stopPropagation()">预览</a>
</div>
</div>`;

View File

@@ -0,0 +1,71 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $page->title }}</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", Roboto, sans-serif;
color: #1f2937;
background: #f8fafc;
line-height: 1.7;
font-size: 16px;
}
article {
max-width: 820px;
margin: 0 auto;
padding: 40px 28px 80px;
}
article h1 { font-size: 28px; margin: 28px 0 16px; }
article h2 { font-size: 22px; margin: 24px 0 12px; }
article h3 { font-size: 18px; margin: 20px 0 10px; }
article p { margin: 12px 0; }
article ul, article ol { padding-left: 28px; margin: 12px 0; }
article li { margin: 4px 0; }
article img { max-width: 100%; height: auto; border-radius: 8px; margin: 12px 0; }
article a { color: #2563eb; text-decoration: underline; }
article blockquote {
border-left: 4px solid #cbd5e1;
background: #f1f5f9;
padding: 8px 16px;
margin: 12px 0;
color: #475569;
}
article code {
background: #e2e8f0;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.92em;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
article pre {
background: #0f172a;
color: #e2e8f0;
padding: 12px 16px;
border-radius: 8px;
overflow-x: auto;
}
article pre code { background: transparent; padding: 0; color: inherit; }
article table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
}
article th, article td {
border: 1px solid #e2e8f0;
padding: 8px 12px;
text-align: left;
}
article th { background: #f1f5f9; }
</style>
</head>
<body>
<article>
<h1>{{ $page->title }}</h1>
{!! $page->content !!}
</article>
</body>
</html>

View File

@@ -84,6 +84,11 @@ Route::get('/health', function () {
], $httpCode);
})->name('health.check');
// 指引步骤渲染(供终端 webview 打开,公开访问)
Route::get('/guides/pages/{page}', function (\App\Models\GuidePage $page) {
return view('guides.page', ['page' => $page]);
})->name('guides.pages.show');
// 文档预览和下载路由(需要认证)
Route::middleware(['auth'])->group(function () {
Route::get('/documents/{document}/preview', [DocumentController::class, 'preview'])