feat: 富文本编辑
This commit is contained in:
@@ -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('分支选项')
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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页面链接');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
|
||||
71
resources/views/guides/page.blade.php
Normal file
71
resources/views/guides/page.blade.php
Normal 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>
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user