|
@@ -11,12 +11,17 @@
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
@if($officialTitle !== '')
|
|
@if($officialTitle !== '')
|
|
|
- <div class="inline-flex max-w-full items-center gap-2 rounded-full border border-slate-200 bg-slate-50/90 px-4 py-2 text-sm text-slate-700">
|
|
|
|
|
|
|
+ <div class="inline-flex max-w-full flex-wrap items-center gap-2 rounded-full border border-slate-200 bg-slate-50/90 px-4 py-2 text-sm text-slate-700">
|
|
|
<svg class="h-4 w-4 shrink-0 text-sky-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
|
<svg class="h-4 w-4 shrink-0 text-sky-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3A1.5 1.5 0 0 0 1.5 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008H12V8.25Z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3A1.5 1.5 0 0 0 1.5 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008H12V8.25Z" />
|
|
|
</svg>
|
|
</svg>
|
|
|
<span class="text-slate-500">当前教材</span>
|
|
<span class="text-slate-500">当前教材</span>
|
|
|
<span class="truncate font-medium text-slate-900" title="{{ $officialTitle }}">{{ $officialTitle }}</span>
|
|
<span class="truncate font-medium text-slate-900" title="{{ $officialTitle }}">{{ $officialTitle }}</span>
|
|
|
|
|
+ @if($seriesName !== '')
|
|
|
|
|
+ <span class="text-slate-300">|</span>
|
|
|
|
|
+ <span class="text-slate-500">教材系列</span>
|
|
|
|
|
+ <span class="truncate font-medium text-slate-900" title="{{ $seriesName }}">{{ $seriesName }}</span>
|
|
|
|
|
+ @endif
|
|
|
</div>
|
|
</div>
|
|
|
@endif
|
|
@endif
|
|
|
|
|
|
|
@@ -24,7 +29,7 @@
|
|
|
<div class="ui-card-header !items-start !border-b-slate-100">
|
|
<div class="ui-card-header !items-start !border-b-slate-100">
|
|
|
<div>
|
|
<div>
|
|
|
<div class="ui-section-title">上传与排序</div>
|
|
<div class="ui-section-title">上传与排序</div>
|
|
|
- <p class="ui-subtitle mt-0.5 max-w-xl">支持 JPEG / PNG / WebP,单张不超过 5MB。第一张为封面主图。</p>
|
|
|
|
|
|
|
+ <p class="ui-subtitle mt-0.5 max-w-xl">支持 JPEG / PNG / WebP,单张不超过 10MB。第一张为封面主图。</p>
|
|
|
</div>
|
|
</div>
|
|
|
<span class="shrink-0 rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-medium text-slate-600">
|
|
<span class="shrink-0 rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-medium text-slate-600">
|
|
|
已选 {{ count($coverUrls) }} 张
|
|
已选 {{ count($coverUrls) }} 张
|
|
@@ -38,7 +43,13 @@
|
|
|
x-data="{
|
|
x-data="{
|
|
|
previews: [],
|
|
previews: [],
|
|
|
uploading: false,
|
|
uploading: false,
|
|
|
|
|
+ appending: false,
|
|
|
progress: 0,
|
|
progress: 0,
|
|
|
|
|
+ autoMessage: '',
|
|
|
|
|
+ autoMessageTimer: null,
|
|
|
|
|
+ shouldPromptOnEmpty: {{ count($coverUrls) === 0 ? 'true' : 'false' }},
|
|
|
|
|
+ autoPrompted: false,
|
|
|
|
|
+ autoPromptBlocked: false,
|
|
|
refreshPreviews(event) {
|
|
refreshPreviews(event) {
|
|
|
this.previews.forEach((p) => URL.revokeObjectURL(p.url))
|
|
this.previews.forEach((p) => URL.revokeObjectURL(p.url))
|
|
|
const files = Array.from(event.target.files || [])
|
|
const files = Array.from(event.target.files || [])
|
|
@@ -54,11 +65,57 @@
|
|
|
this.previews = []
|
|
this.previews = []
|
|
|
this.progress = 0
|
|
this.progress = 0
|
|
|
},
|
|
},
|
|
|
|
|
+ showAutoMessage(message) {
|
|
|
|
|
+ this.autoMessage = message
|
|
|
|
|
+
|
|
|
|
|
+ if (this.autoMessageTimer) {
|
|
|
|
|
+ clearTimeout(this.autoMessageTimer)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.autoMessageTimer = setTimeout(() => {
|
|
|
|
|
+ this.autoMessage = ''
|
|
|
|
|
+ this.autoMessageTimer = null
|
|
|
|
|
+ }, 2200)
|
|
|
|
|
+ },
|
|
|
|
|
+ promptForPhotos() {
|
|
|
|
|
+ if (!this.shouldPromptOnEmpty || this.autoPrompted || !this.$refs.photoInput) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.autoPrompted = true
|
|
|
|
|
+
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (typeof this.$refs.photoInput.showPicker === 'function') {
|
|
|
|
|
+ this.$refs.photoInput.showPicker()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.$refs.photoInput.click()
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ this.autoPromptBlocked = true
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 250)
|
|
|
|
|
+ },
|
|
|
|
|
+ triggerAppend() {
|
|
|
|
|
+ if (this.uploading || this.appending || this.previews.length === 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.appending = true
|
|
|
|
|
+
|
|
|
|
|
+ $wire.appendPhotos()
|
|
|
|
|
+ .catch(() => {})
|
|
|
|
|
+ .finally(() => {
|
|
|
|
|
+ this.appending = false
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
}"
|
|
}"
|
|
|
|
|
+ x-init="promptForPhotos()"
|
|
|
x-on:livewire-upload-start="uploading = true; progress = 0"
|
|
x-on:livewire-upload-start="uploading = true; progress = 0"
|
|
|
x-on:livewire-upload-progress="uploading = true; progress = $event.detail.progress"
|
|
x-on:livewire-upload-progress="uploading = true; progress = $event.detail.progress"
|
|
|
- x-on:livewire-upload-finish="uploading = false; progress = 100"
|
|
|
|
|
|
|
+ x-on:livewire-upload-finish="uploading = false; progress = 100; triggerAppend()"
|
|
|
x-on:livewire-upload-error="uploading = false"
|
|
x-on:livewire-upload-error="uploading = false"
|
|
|
|
|
+ x-on:covers-appended.window="clearPreviews(); if ($refs.photoInput) { $refs.photoInput.value = '' }; showAutoMessage(`已自动加入列表${$event.detail?.count ? `(${$event.detail.count} 张)` : ''}`)"
|
|
|
>
|
|
>
|
|
|
<div class="flex gap-4" style="display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;flex-wrap:wrap;">
|
|
<div class="flex gap-4" style="display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;flex-wrap:wrap;">
|
|
|
<div class="space-y-3" style="flex:1 1 420px;min-width:320px;">
|
|
<div class="space-y-3" style="flex:1 1 420px;min-width:320px;">
|
|
@@ -76,7 +133,21 @@
|
|
|
x-on:change="refreshPreviews($event)"
|
|
x-on:change="refreshPreviews($event)"
|
|
|
class="block w-full cursor-pointer text-sm text-slate-600 file:mr-4 file:cursor-pointer file:rounded-lg file:border-0 file:bg-sky-50 file:px-4 file:py-2.5 file:text-sm file:font-medium file:text-sky-800 file:ring-1 file:ring-sky-200/80 hover:file:bg-sky-100"
|
|
class="block w-full cursor-pointer text-sm text-slate-600 file:mr-4 file:cursor-pointer file:rounded-lg file:border-0 file:bg-sky-50 file:px-4 file:py-2.5 file:text-sm file:font-medium file:text-sky-800 file:ring-1 file:ring-sky-200/80 hover:file:bg-sky-100"
|
|
|
/>
|
|
/>
|
|
|
- <p class="text-xs text-slate-500" style="white-space:normal;word-break:normal;">可多次选择,每次点「加入列表」把本次选中的图追加到下方;全部满意后再点底部保存。</p>
|
|
|
|
|
|
|
+ <p class="text-xs text-slate-500" style="white-space:normal;word-break:normal;">可多次选择;选中后会自动加入下方列表,全部满意后再点底部保存。</p>
|
|
|
|
|
+ <div
|
|
|
|
|
+ x-cloak
|
|
|
|
|
+ x-show="shouldPromptOnEmpty && autoPromptBlocked"
|
|
|
|
|
+ class="flex flex-wrap items-center gap-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span>浏览器未自动弹出选择窗口,请点这里继续。</span>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ x-on:click="$refs.photoInput?.click()"
|
|
|
|
|
+ class="inline-flex items-center rounded-md bg-amber-100 px-2.5 py-1 font-medium text-amber-800 hover:bg-amber-200"
|
|
|
|
|
+ >
|
|
|
|
|
+ 选择图片
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="space-y-2" x-show="previews.length > 0" x-cloak>
|
|
<div class="space-y-2" x-show="previews.length > 0" x-cloak>
|
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center justify-between">
|
|
@@ -112,6 +183,12 @@
|
|
|
<span class="loading loading-spinner loading-sm text-sky-500"></span>
|
|
<span class="loading loading-spinner loading-sm text-sky-500"></span>
|
|
|
读取文件中…
|
|
读取文件中…
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <p
|
|
|
|
|
+ x-cloak
|
|
|
|
|
+ x-show="autoMessage"
|
|
|
|
|
+ x-text="autoMessage"
|
|
|
|
|
+ class="inline-flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm font-medium text-emerald-700"
|
|
|
|
|
+ ></p>
|
|
|
@error('photos.*')
|
|
@error('photos.*')
|
|
|
<p class="text-sm text-error">{{ $message }}</p>
|
|
<p class="text-sm text-error">{{ $message }}</p>
|
|
|
@enderror
|
|
@enderror
|
|
@@ -120,10 +197,9 @@
|
|
|
<span class="hidden text-xs font-medium text-slate-500" aria-hidden="true">操作</span>
|
|
<span class="hidden text-xs font-medium text-slate-500" aria-hidden="true">操作</span>
|
|
|
<button
|
|
<button
|
|
|
type="button"
|
|
type="button"
|
|
|
- wire:click="appendPhotos"
|
|
|
|
|
wire:loading.attr="disabled"
|
|
wire:loading.attr="disabled"
|
|
|
wire:target="appendPhotos,photos"
|
|
wire:target="appendPhotos,photos"
|
|
|
- x-on:click="setTimeout(() => { if (!uploading) { clearPreviews(); if ($refs.photoInput) { $refs.photoInput.value = '' } } }, 300)"
|
|
|
|
|
|
|
+ x-on:click="triggerAppend()"
|
|
|
class="btn btn-primary w-full border-0 bg-sky-500 text-white shadow-sm hover:bg-sky-600"
|
|
class="btn btn-primary w-full border-0 bg-sky-500 text-white shadow-sm hover:bg-sky-600"
|
|
|
style="width:100%;"
|
|
style="width:100%;"
|
|
|
>
|
|
>
|
|
@@ -131,7 +207,7 @@
|
|
|
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
|
</svg>
|
|
</svg>
|
|
|
- 加入列表
|
|
|
|
|
|
|
+ 手动加入列表
|
|
|
</span>
|
|
</span>
|
|
|
<span wire:loading wire:target="appendPhotos" class="inline-flex items-center gap-2">
|
|
<span wire:loading wire:target="appendPhotos" class="inline-flex items-center gap-2">
|
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
@@ -157,7 +233,14 @@
|
|
|
</svg>
|
|
</svg>
|
|
|
</div>
|
|
</div>
|
|
|
<p class="ui-empty-title">暂无配图</p>
|
|
<p class="ui-empty-title">暂无配图</p>
|
|
|
- <p class="ui-empty-desc max-w-sm">从上方选择图片并「加入列表」,或先在存储/API 侧同步后再刷新本页。</p>
|
|
|
|
|
|
|
+ <p class="ui-empty-desc max-w-sm">页面会优先尝试自动打开选图窗口;如果浏览器没有放行,直接点下方按钮继续。</p>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ x-on:click="$refs.photoInput?.click()"
|
|
|
|
|
+ class="btn btn-primary mt-4 border-0 bg-sky-500 px-6 text-white shadow-sm hover:bg-sky-600"
|
|
|
|
|
+ >
|
|
|
|
|
+ 选择图片
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
@else
|
|
@else
|
|
|
<ul class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
<ul class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
@@ -184,35 +267,47 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="border-t border-slate-100 pt-5">
|
|
<div class="border-t border-slate-100 pt-5">
|
|
|
- <p class="text-xs text-slate-500" style="display:block;max-width:100%;white-space:normal;word-break:normal;">保存后会写入教材的配图字段;移除仅影响本次会话直至保存。</p>
|
|
|
|
|
- <div class="mt-3 flex justify-end">
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- wire:click="saveCovers"
|
|
|
|
|
- wire:loading.attr="disabled"
|
|
|
|
|
- wire:target="saveCovers"
|
|
|
|
|
- class="btn btn-primary border-0 bg-sky-500 px-8 text-white shadow-sm hover:bg-sky-600"
|
|
|
|
|
- style="min-width:220px;"
|
|
|
|
|
- >
|
|
|
|
|
- <span wire:loading.remove wire:target="saveCovers" class="inline-flex items-center justify-center gap-2">
|
|
|
|
|
- <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
|
|
|
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
|
|
|
- </svg>
|
|
|
|
|
- 保存到服务器
|
|
|
|
|
- </span>
|
|
|
|
|
- <span wire:loading wire:target="saveCovers" class="inline-flex items-center gap-2">
|
|
|
|
|
- <span class="loading loading-spinner loading-sm"></span>
|
|
|
|
|
- 保存中…
|
|
|
|
|
- </span>
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="mt-3">
|
|
|
|
|
- <p wire:loading wire:target="saveCovers" class="text-xs text-sky-600">正在保存到服务器,请稍候…</p>
|
|
|
|
|
- @if($saveFeedback)
|
|
|
|
|
- <p class="text-xs {{ $saveFeedbackType === 'success' ? 'text-emerald-600' : ($saveFeedbackType === 'error' ? 'text-rose-600' : 'text-amber-600') }}">
|
|
|
|
|
- {{ $saveFeedback }}
|
|
|
|
|
- </p>
|
|
|
|
|
- @endif
|
|
|
|
|
|
|
+ <div class="sticky bottom-4 z-20 -mx-2 rounded-2xl px-4 py-3 shadow-lg backdrop-blur transition-all sm:mx-0 sm:px-5 {{ $hasUnsavedChanges ? 'border border-sky-300 bg-sky-50/95 shadow-sky-200/70 ring-2 ring-sky-200/80' : 'border border-slate-200/90 bg-white/95 shadow-slate-200/60' }}">
|
|
|
|
|
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
+ <div class="min-w-0">
|
|
|
|
|
+ <p class="text-sm font-semibold {{ $hasUnsavedChanges ? 'text-sky-900' : 'text-slate-900' }}">
|
|
|
|
|
+ {{ $hasUnsavedChanges ? '有未保存的配图改动' : '配图队列已就绪' }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p class="text-xs text-slate-500" style="display:block;max-width:100%;white-space:normal;word-break:normal;">保存后会写入教材的配图字段;移除仅影响本次会话直至保存。</p>
|
|
|
|
|
+ <div class="mt-2 space-y-1">
|
|
|
|
|
+ <p wire:loading wire:target="saveCovers" class="text-xs text-sky-600">正在保存到服务器,请稍候…</p>
|
|
|
|
|
+ @if($saveFeedback)
|
|
|
|
|
+ <p class="text-xs {{ $saveFeedbackType === 'success' ? 'text-emerald-600' : ($saveFeedbackType === 'error' ? 'text-rose-600' : 'text-amber-600') }}">
|
|
|
|
|
+ {{ $saveFeedback }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ @endif
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex shrink-0 items-center gap-3">
|
|
|
|
|
+ <span class="rounded-full px-3 py-1 text-xs font-medium {{ $hasUnsavedChanges ? 'bg-sky-100 text-sky-700' : 'bg-slate-100 text-slate-600' }}">
|
|
|
|
|
+ {{ $hasUnsavedChanges ? '待保存 ' : '当前 ' }}{{ count($coverUrls) }} 张
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ wire:click="saveCovers"
|
|
|
|
|
+ wire:loading.attr="disabled"
|
|
|
|
|
+ wire:target="saveCovers"
|
|
|
|
|
+ class="btn btn-primary border-0 px-8 text-white shadow-sm {{ $hasUnsavedChanges ? 'bg-sky-600 ring-2 ring-sky-300 hover:bg-sky-700' : 'bg-sky-500 hover:bg-sky-600' }}"
|
|
|
|
|
+ style="min-width:220px;"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span wire:loading.remove wire:target="saveCovers" class="inline-flex items-center justify-center gap-2">
|
|
|
|
|
+ <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ 保存到服务器
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span wire:loading wire:target="saveCovers" class="inline-flex items-center gap-2">
|
|
|
|
|
+ <span class="loading loading-spinner loading-sm"></span>
|
|
|
|
|
+ 保存中…
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|