Fix rich editor image preview URLs

This commit is contained in:
2026-04-20 13:41:27 +08:00
parent 6acd0ccad0
commit 0b35e54fe1
4 changed files with 85 additions and 15 deletions

View File

@@ -30,7 +30,7 @@ class ManageGuidePages extends Page
public function getTitle(): string public function getTitle(): string
{ {
return $this->getRecord()->name . ' - 页面流程'; return $this->getRecord()->name.' - 页面流程';
} }
public function loadGraph(): void public function loadGraph(): void
@@ -56,17 +56,17 @@ class ManageGuidePages extends Page
$queue = []; $queue = [];
foreach ($pages as $p) { foreach ($pages as $p) {
if (!isset($hasIncoming[$p->id])) { if (! isset($hasIncoming[$p->id])) {
$queue[] = $p->id; $queue[] = $p->id;
$levels[$p->id] = 0; $levels[$p->id] = 0;
$visited[$p->id] = true; $visited[$p->id] = true;
} }
} }
while (!empty($queue)) { while (! empty($queue)) {
$cur = array_shift($queue); $cur = array_shift($queue);
foreach ($children[$cur] ?? [] as $child) { foreach ($children[$cur] ?? [] as $child) {
if (!isset($visited[$child])) { if (! isset($visited[$child])) {
$visited[$child] = true; $visited[$child] = true;
$levels[$child] = $levels[$cur] + 1; $levels[$child] = $levels[$cur] + 1;
$queue[] = $child; $queue[] = $child;
@@ -77,7 +77,7 @@ class ManageGuidePages extends Page
// Orphans at bottom // Orphans at bottom
$maxLevel = empty($levels) ? 0 : max($levels); $maxLevel = empty($levels) ? 0 : max($levels);
foreach ($pages as $p) { foreach ($pages as $p) {
if (!isset($levels[$p->id])) { if (! isset($levels[$p->id])) {
$levels[$p->id] = $maxLevel + 1; $levels[$p->id] = $maxLevel + 1;
} }
} }
@@ -107,17 +107,17 @@ 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,
'uri' => $p->uri, '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,
'y' => $positions[$p->id]['y'] ?? 50, 'y' => $positions[$p->id]['y'] ?? 50,
])->values()->toArray(); ])->values()->toArray();
$this->edges = $edgeModels->map(fn(GuidePageEdge $e) => [ $this->edges = $edgeModels->map(fn (GuidePageEdge $e) => [
'id' => $e->id, 'id' => $e->id,
'from' => $e->from_page_id, 'from' => $e->from_page_id,
'to' => $e->to_page_id, 'to' => $e->to_page_id,
@@ -132,8 +132,8 @@ class ManageGuidePages extends Page
$guide = $this->getRecord(); $guide = $this->getRecord();
if ( if (
!$guide->pages()->where('id', $fromPageId)->exists() || ! $guide->pages()->where('id', $fromPageId)->exists() ||
!$guide->pages()->where('id', $toPageId)->exists() ! $guide->pages()->where('id', $toPageId)->exists()
) { ) {
return; return;
} }
@@ -156,7 +156,7 @@ class ManageGuidePages extends Page
$options = $page->options ?? []; $options = $page->options ?? [];
$label = null; $label = null;
if (!empty($options)) { if (! empty($options)) {
$outputIndex = (int) str_replace('output_', '', $outputClass) - 1; $outputIndex = (int) str_replace('output_', '', $outputClass) - 1;
$label = $options[$outputIndex] ?? null; $label = $options[$outputIndex] ?? null;
} }
@@ -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,
'content' => $page->content, 'content' => $page->normalized_content,
'options' => $page->options ?? [], 'options' => $page->options ?? [],
]); ]);
}) })
@@ -267,6 +267,8 @@ class ManageGuidePages extends Page
->fileAttachmentsDisk('public') ->fileAttachmentsDisk('public')
->fileAttachmentsDirectory('guide-pages') ->fileAttachmentsDirectory('guide-pages')
->fileAttachmentsVisibility('public') ->fileAttachmentsVisibility('public')
->getUploadedAttachmentUrlUsing(fn (string $file): string => GuidePage::uploadedAttachmentUrl($file))
->dehydrateStateUsing(fn (?string $state): string => GuidePage::normalizeRichTextContent($state))
->columnSpanFull(), ->columnSpanFull(),
Forms\Components\TagsInput::make('options') Forms\Components\TagsInput::make('options')

View File

@@ -30,6 +30,35 @@ class GuidePage extends Model
return route('guides.pages.show', $this->id); return route('guides.pages.show', $this->id);
} }
public function getNormalizedContentAttribute(): string
{
return static::normalizeRichTextContent($this->content);
}
public static function normalizeRichTextContent(?string $content): string
{
if (blank($content)) {
return '';
}
$content = preg_replace_callback(
'~(?:https?:)?//[^"\'\s<>()]+(?<path>/storage/guide-pages/[^"\'\s<>()]*)~i',
static fn (array $matches): string => $matches['path'],
$content,
) ?? $content;
return preg_replace(
'~(?<=["\'])storage/guide-pages/~i',
'/storage/guide-pages/',
$content,
) ?? $content;
}
public static function uploadedAttachmentUrl(string $path): string
{
return '/storage/'.ltrim($path, '/');
}
public function guide() public function guide()
{ {
return $this->belongsTo(Guide::class); return $this->belongsTo(Guide::class);
@@ -60,7 +89,6 @@ class GuidePage extends Model
public function isEntry(): bool public function isEntry(): bool
{ {
return !$this->incomingEdges()->exists(); return ! $this->incomingEdges()->exists();
} }
} }

View File

@@ -65,7 +65,7 @@
<body> <body>
<article> <article>
<h1>{{ $page->title }}</h1> <h1>{{ $page->title }}</h1>
{!! $page->content !!} {!! $page->normalized_content !!}
</article> </article>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,40 @@
<?php
use App\Models\GuidePage;
it('normalizes uploaded guide image urls for rendering', function () {
$page = new GuidePage([
'content' => <<<'HTML'
<figure data-trix-attachment='{"url":"http://localhost:8000/storage/guide-pages/example.png?signature=abc"}'>
<img src="http://localhost:8000/storage/guide-pages/example.png?signature=abc">
</figure>
HTML,
]);
expect($page->normalized_content)
->toContain('/storage/guide-pages/example.png?signature=abc')
->not->toContain('http://localhost:8000/storage/guide-pages/example.png?signature=abc');
});
it('adds a leading slash to relative guide image urls', function () {
$page = new GuidePage([
'content' => '<img src="storage/guide-pages/example.png">',
]);
expect($page->normalized_content)
->toBe('<img src="/storage/guide-pages/example.png">');
});
it('keeps external image urls unchanged', function () {
$page = new GuidePage([
'content' => '<img src="https://example.com/images/example.png">',
]);
expect($page->normalized_content)
->toBe('<img src="https://example.com/images/example.png">');
});
it('builds root relative upload urls for new attachments', function () {
expect(GuidePage::uploadedAttachmentUrl('guide-pages/example.png'))
->toBe('/storage/guide-pages/example.png');
});