feat: 富文本编辑
This commit is contained in:
@@ -110,7 +110,7 @@ class ManageGuidePages extends Page
|
|||||||
$this->nodes = $pages->map(fn(GuidePage $p) => [
|
$this->nodes = $pages->map(fn(GuidePage $p) => [
|
||||||
'id' => $p->id,
|
'id' => $p->id,
|
||||||
'title' => $p->title,
|
'title' => $p->title,
|
||||||
'html_url' => $p->html_url,
|
'uri' => $p->uri,
|
||||||
'is_entry' => !isset($hasIncoming[$p->id]),
|
'is_entry' => !isset($hasIncoming[$p->id]),
|
||||||
'options' => $p->options ?? [],
|
'options' => $p->options ?? [],
|
||||||
'x' => $positions[$p->id]['x'] ?? 50,
|
'x' => $positions[$p->id]['x'] ?? 50,
|
||||||
@@ -207,7 +207,7 @@ class ManageGuidePages extends Page
|
|||||||
$page = $this->getRecord()->pages()->findOrFail($arguments['id']);
|
$page = $this->getRecord()->pages()->findOrFail($arguments['id']);
|
||||||
$form->fill([
|
$form->fill([
|
||||||
'title' => $page->title,
|
'title' => $page->title,
|
||||||
'html_url' => $page->html_url,
|
'content' => $page->content,
|
||||||
'options' => $page->options ?? [],
|
'options' => $page->options ?? [],
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
@@ -261,11 +261,13 @@ class ManageGuidePages extends Page
|
|||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
|
||||||
Forms\Components\TextInput::make('html_url')
|
Forms\Components\RichEditor::make('content')
|
||||||
->label('HTML页面URL')
|
->label('页面内容')
|
||||||
->required()
|
->required()
|
||||||
->url()
|
->fileAttachmentsDisk('public')
|
||||||
->maxLength(500),
|
->fileAttachmentsDirectory('guide-pages')
|
||||||
|
->fileAttachmentsVisibility('public')
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
Forms\Components\TagsInput::make('options')
|
Forms\Components\TagsInput::make('options')
|
||||||
->label('分支选项')
|
->label('分支选项')
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class TerminalApiController extends Controller
|
|||||||
$pagesMap[$page->id] = [
|
$pagesMap[$page->id] = [
|
||||||
'id' => $page->id,
|
'id' => $page->id,
|
||||||
'title' => $page->title,
|
'title' => $page->title,
|
||||||
'html_url' => $page->html_url,
|
'uri' => $page->uri,
|
||||||
'next' => $next,
|
'next' => $next,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class GuidePage extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'guide_id',
|
'guide_id',
|
||||||
'title',
|
'title',
|
||||||
'html_url',
|
'content',
|
||||||
'options',
|
'options',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -25,6 +25,11 @@ class GuidePage extends Model
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUriAttribute(): string
|
||||||
|
{
|
||||||
|
return route('guides.pages.show', $this->id);
|
||||||
|
}
|
||||||
|
|
||||||
public function guide()
|
public function guide()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Guide::class);
|
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
|
class GuideSeeder extends Seeder
|
||||||
{
|
{
|
||||||
private const BASE_URL = 'https://ssrf.9z.work/guides';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the database seeds.
|
* Run the database seeds.
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +44,11 @@ class GuideSeeder extends Seeder
|
|||||||
$this->command->info(' - 关联线站数量: ' . $stations->count());
|
$this->command->info(' - 关联线站数量: ' . $stations->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function placeholder(string $title): string
|
||||||
|
{
|
||||||
|
return '<p>本步骤说明待补充。管理员可在 Filament 后台使用富文本编辑器完善「' . e($title) . '」的操作指引。</p>';
|
||||||
|
}
|
||||||
|
|
||||||
private function createHowToUseBeamGuide(User $admin): Guide
|
private function createHowToUseBeamGuide(User $admin): Guide
|
||||||
{
|
{
|
||||||
$this->command->info('创建指引: 如何用光...');
|
$this->command->info('创建指引: 如何用光...');
|
||||||
@@ -60,49 +63,47 @@ class GuideSeeder extends Seeder
|
|||||||
'published_at' => now(),
|
'published_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$baseUrl = self::BASE_URL . '/how-to-use-beam';
|
|
||||||
|
|
||||||
$step1 = GuidePage::create([
|
$step1 = GuidePage::create([
|
||||||
'guide_id' => $guide->id,
|
'guide_id' => $guide->id,
|
||||||
'title' => '打开光子光闸 PS1',
|
'title' => '打开光子光闸 PS1',
|
||||||
'html_url' => "{$baseUrl}/step-1.html",
|
'content' => $this->placeholder('打开光子光闸 PS1'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$step2 = GuidePage::create([
|
$step2 = GuidePage::create([
|
||||||
'guide_id' => $guide->id,
|
'guide_id' => $guide->id,
|
||||||
'title' => '搜索光学棚屋',
|
'title' => '搜索光学棚屋',
|
||||||
'html_url' => "{$baseUrl}/step-2.html",
|
'content' => $this->placeholder('搜索光学棚屋'),
|
||||||
'options' => ['前门12', '后门'],
|
'options' => ['前门12', '后门'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$step3a = GuidePage::create([
|
$step3a = GuidePage::create([
|
||||||
'guide_id' => $guide->id,
|
'guide_id' => $guide->id,
|
||||||
'title' => '前门12路径 - 检查设备状态',
|
'title' => '前门12路径 - 检查设备状态',
|
||||||
'html_url' => "{$baseUrl}/step-3a.html",
|
'content' => $this->placeholder('前门12路径 - 检查设备状态'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$step3b = GuidePage::create([
|
$step3b = GuidePage::create([
|
||||||
'guide_id' => $guide->id,
|
'guide_id' => $guide->id,
|
||||||
'title' => '后门路径 - 安全确认',
|
'title' => '后门路径 - 安全确认',
|
||||||
'html_url' => "{$baseUrl}/step-3b.html",
|
'content' => $this->placeholder('后门路径 - 安全确认'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$step4a = GuidePage::create([
|
$step4a = GuidePage::create([
|
||||||
'guide_id' => $guide->id,
|
'guide_id' => $guide->id,
|
||||||
'title' => '前门12路径 - 打开实验站光闸',
|
'title' => '前门12路径 - 打开实验站光闸',
|
||||||
'html_url' => "{$baseUrl}/step-4a.html",
|
'content' => $this->placeholder('前门12路径 - 打开实验站光闸'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$step4b = GuidePage::create([
|
$step4b = GuidePage::create([
|
||||||
'guide_id' => $guide->id,
|
'guide_id' => $guide->id,
|
||||||
'title' => '后门路径 - 设备检查',
|
'title' => '后门路径 - 设备检查',
|
||||||
'html_url' => "{$baseUrl}/step-4b.html",
|
'content' => $this->placeholder('后门路径 - 设备检查'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$step5 = GuidePage::create([
|
$step5 = GuidePage::create([
|
||||||
'guide_id' => $guide->id,
|
'guide_id' => $guide->id,
|
||||||
'title' => '完成',
|
'title' => '完成',
|
||||||
'html_url' => "{$baseUrl}/step-5.html",
|
'content' => $this->placeholder('完成'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// step1 → step2 (sequential)
|
// step1 → step2 (sequential)
|
||||||
@@ -185,21 +186,20 @@ class GuideSeeder extends Seeder
|
|||||||
'published_at' => now(),
|
'published_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$baseUrl = self::BASE_URL . '/vacuum-valve-issue';
|
|
||||||
$steps = [
|
$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 = [];
|
$pages = [];
|
||||||
foreach ($steps as $i => $step) {
|
foreach ($steps as $title) {
|
||||||
$pages[] = GuidePage::create([
|
$pages[] = GuidePage::create([
|
||||||
'guide_id' => $guide->id,
|
'guide_id' => $guide->id,
|
||||||
'title' => $step['title'],
|
'title' => $title,
|
||||||
'html_url' => "{$baseUrl}/{$step['file']}",
|
'content' => $this->placeholder($title),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,21 +231,20 @@ class GuideSeeder extends Seeder
|
|||||||
'published_at' => now(),
|
'published_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$baseUrl = self::BASE_URL . '/water-leak-alarm';
|
|
||||||
$steps = [
|
$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 = [];
|
$pages = [];
|
||||||
foreach ($steps as $i => $step) {
|
foreach ($steps as $title) {
|
||||||
$pages[] = GuidePage::create([
|
$pages[] = GuidePage::create([
|
||||||
'guide_id' => $guide->id,
|
'guide_id' => $guide->id,
|
||||||
'title' => $step['title'],
|
'title' => $title,
|
||||||
'html_url' => "{$baseUrl}/{$step['file']}",
|
'content' => $this->placeholder($title),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,17 +88,20 @@
|
|||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.df-node-actions button {
|
.df-node-actions button,
|
||||||
|
.df-node-actions a {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
|
text-decoration: none;
|
||||||
transition: color 0.15s;
|
transition: color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.df-node-actions button:hover {
|
.df-node-actions button:hover,
|
||||||
|
.df-node-actions a:hover {
|
||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,11 +259,12 @@
|
|||||||
const html = `
|
const html = `
|
||||||
<div class="df-node-content">
|
<div class="df-node-content">
|
||||||
<div class="df-node-header">${node.title}</div>
|
<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>' : ''}
|
${node.is_entry ? '<div><span class="df-node-badge">入口</span></div>' : ''}
|
||||||
<div class="df-node-actions">
|
<div class="df-node-actions">
|
||||||
<button onclick="event.stopPropagation(); Livewire.find('${@js($this->getId())}').mountAction('editPage', { id: ${node.id} })">编辑</button>
|
<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>
|
<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>
|
||||||
</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);
|
], $httpCode);
|
||||||
})->name('health.check');
|
})->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::middleware(['auth'])->group(function () {
|
||||||
Route::get('/documents/{document}/preview', [DocumentController::class, 'preview'])
|
Route::get('/documents/{document}/preview', [DocumentController::class, 'preview'])
|
||||||
|
|||||||
Reference in New Issue
Block a user