|
|
|
@ -21,7 +21,7 @@ const props = defineProps<{ |
|
|
|
|
|
|
|
const emit = defineEmits<{ |
|
|
|
'update:visible': [v: boolean] |
|
|
|
saved: [] |
|
|
|
saved: [cardData: any] |
|
|
|
}>() |
|
|
|
|
|
|
|
// ── Tag fetching ── |
|
|
|
@ -256,12 +256,13 @@ async function onSubmit() { |
|
|
|
body.articleIds = selectedArticleIds.value.length > 0 ? [...selectedArticleIds.value] : null |
|
|
|
|
|
|
|
try { |
|
|
|
let res: any |
|
|
|
if (isEdit.value && props.card) { |
|
|
|
await request(`/api/cards/${props.card.id}`, { method: 'PUT', body }) |
|
|
|
res = await request(`/api/cards/${props.card.id}`, { method: 'PUT', body }) |
|
|
|
} else { |
|
|
|
await request('/api/cards', { method: 'POST', body }) |
|
|
|
res = await request('/api/cards', { method: 'POST', body }) |
|
|
|
} |
|
|
|
emit('saved') |
|
|
|
emit('saved', res.data) |
|
|
|
emit('update:visible', false) |
|
|
|
} catch (err: any) { |
|
|
|
error.value = err?.data?.message ?? err?.message ?? '保存失败' |
|
|
|
@ -275,7 +276,7 @@ async function onSubmit() { |
|
|
|
<BoDialog |
|
|
|
:show="visible" |
|
|
|
:mask-can-close="!saving" |
|
|
|
:style="{ width: '600px' }" |
|
|
|
:style="{ width: '85vw', maxWidth: '1400px' }" |
|
|
|
@update:show="(v: boolean) => emit('update:visible', v)" |
|
|
|
> |
|
|
|
<div class="card-form"> |
|
|
|
@ -294,288 +295,285 @@ async function onSubmit() { |
|
|
|
|
|
|
|
<!-- Body --> |
|
|
|
<div class="form-body"> |
|
|
|
<!-- Error --> |
|
|
|
<!-- Error: full width --> |
|
|
|
<div v-if="error" class="form-error"> |
|
|
|
<Icon name="lucide:alert-circle" class="form-error-icon" /> |
|
|
|
<span>{{ error }}</span> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ── 卡片类型 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">卡片类型</div> |
|
|
|
<div class="type-grid"> |
|
|
|
<button |
|
|
|
v-for="ct in CARD_TYPE_OPTIONS" |
|
|
|
:key="ct.value" |
|
|
|
type="button" |
|
|
|
class="type-card" |
|
|
|
:class="{ active: formType === ct.value }" |
|
|
|
:disabled="saving" |
|
|
|
@click="formType = ct.value" |
|
|
|
> |
|
|
|
<Icon :name="ct.icon" class="type-card-icon" /> |
|
|
|
<span class="type-card-label">{{ ct.label }}</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ── 基本信息 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">基本信息</div> |
|
|
|
|
|
|
|
<!-- Title --> |
|
|
|
<div class="form-field"> |
|
|
|
<div class="field-header"> |
|
|
|
<label class="field-label"> |
|
|
|
标题 <span class="field-required">*</span> |
|
|
|
</label> |
|
|
|
<span class="field-count">{{ title.length }}/60</span> |
|
|
|
<div class="form-columns"> |
|
|
|
<!-- ═══ 左栏:基础信息 ═══ --> |
|
|
|
<div class="form-left"> |
|
|
|
<!-- ── 卡片类型 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">卡片类型</div> |
|
|
|
<div class="type-grid"> |
|
|
|
<button |
|
|
|
v-for="ct in CARD_TYPE_OPTIONS" |
|
|
|
:key="ct.value" |
|
|
|
type="button" |
|
|
|
class="type-card" |
|
|
|
:class="{ active: formType === ct.value }" |
|
|
|
:disabled="saving" |
|
|
|
@click="formType = ct.value" |
|
|
|
> |
|
|
|
<Icon :name="ct.icon" class="type-card-icon" /> |
|
|
|
<span class="type-card-label">{{ ct.label }}</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<input |
|
|
|
v-model="title" |
|
|
|
type="text" |
|
|
|
class="field-input" |
|
|
|
placeholder="输入卡片标题" |
|
|
|
:disabled="saving" |
|
|
|
maxlength="60" |
|
|
|
@keyup.enter="onSubmit" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Category --> |
|
|
|
<div class="form-field"> |
|
|
|
<label class="field-label">分类</label> |
|
|
|
<select v-model="categoryId" class="field-select" :disabled="saving"> |
|
|
|
<option |
|
|
|
v-for="fc in flatCategories" |
|
|
|
:key="fc.id" |
|
|
|
:value="fc.id || null" |
|
|
|
> |
|
|
|
{{ '\u00A0\u00A0'.repeat(fc.depth) }}{{ fc.name }} |
|
|
|
</option> |
|
|
|
</select> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ── 图片(单张) ── --> |
|
|
|
<div v-if="needsImage" class="form-group"> |
|
|
|
<div class="form-group-label">图片 <span class="field-required">*</span></div> |
|
|
|
|
|
|
|
<div class="image-upload-row"> |
|
|
|
<label class="image-dropzone"> |
|
|
|
<Icon name="lucide:image-plus" class="dropzone-icon" /> |
|
|
|
<span class="dropzone-text">点击上传</span> |
|
|
|
<input |
|
|
|
type="file" |
|
|
|
accept="image/*" |
|
|
|
:disabled="saving" |
|
|
|
hidden |
|
|
|
@change="onFileUpload" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<input |
|
|
|
v-model="imageUrl" |
|
|
|
type="url" |
|
|
|
class="field-input" |
|
|
|
placeholder="或粘贴图片 URL…" |
|
|
|
:disabled="saving" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-if="imageUrl" class="image-preview-block"> |
|
|
|
<img |
|
|
|
:src="imageUrl" |
|
|
|
alt="预览" |
|
|
|
class="image-preview-img" |
|
|
|
@error="(e) => { (e.target as HTMLImageElement).style.display = 'none' }" |
|
|
|
/> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
class="image-preview-clear" |
|
|
|
:disabled="saving" |
|
|
|
@click="imageUrl = ''" |
|
|
|
> |
|
|
|
<Icon name="lucide:x" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ── 图片列表(图集) ── --> |
|
|
|
<div v-if="needsMultiImage" class="form-group"> |
|
|
|
<div class="form-group-label">图片列表 <span class="field-required">*</span></div> |
|
|
|
|
|
|
|
<div class="multi-image-list"> |
|
|
|
<div v-for="(_, idx) in imageUrls" :key="idx" class="multi-image-row"> |
|
|
|
<span class="multi-image-index">{{ idx + 1 }}</span> |
|
|
|
<!-- ── 标题 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">标题 <span class="field-required">*</span></div> |
|
|
|
<div class="field-header"> |
|
|
|
<span class="field-count">{{ title.length }}/60</span> |
|
|
|
</div> |
|
|
|
<input |
|
|
|
v-model="imageUrls[idx]" |
|
|
|
type="url" |
|
|
|
v-model="title" |
|
|
|
type="text" |
|
|
|
class="field-input" |
|
|
|
:placeholder="`图片 ${idx + 1} 的 URL`" |
|
|
|
placeholder="输入卡片标题" |
|
|
|
:disabled="saving" |
|
|
|
maxlength="60" |
|
|
|
@keyup.enter="onSubmit" |
|
|
|
/> |
|
|
|
<button |
|
|
|
v-if="imageUrls.length > 1" |
|
|
|
type="button" |
|
|
|
class="multi-image-remove" |
|
|
|
:disabled="saving" |
|
|
|
@click="removeImageUrl(idx)" |
|
|
|
> |
|
|
|
<Icon name="lucide:trash-2" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<button type="button" class="add-row-btn" :disabled="saving" @click="addImageUrl"> |
|
|
|
<Icon name="lucide:plus" /> |
|
|
|
<span>添加一张</span> |
|
|
|
</button> |
|
|
|
|
|
|
|
<div v-if="imageUrls.some(u => u.trim())" class="image-previews-grid"> |
|
|
|
<div |
|
|
|
v-for="(url, idx) in imageUrls.filter(u => u.trim())" |
|
|
|
:key="idx" |
|
|
|
class="image-preview-thumb" |
|
|
|
> |
|
|
|
<img |
|
|
|
:src="url" |
|
|
|
:alt="`预览 ${idx + 1}`" |
|
|
|
@error="(e) => { (e.target as HTMLImageElement).style.display = 'none' }" |
|
|
|
/> |
|
|
|
<!-- ── 分类 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">分类</div> |
|
|
|
<select v-model="categoryId" class="field-select" :disabled="saving"> |
|
|
|
<option |
|
|
|
v-for="fc in flatCategories" |
|
|
|
:key="fc.id" |
|
|
|
:value="fc.id || null" |
|
|
|
> |
|
|
|
{{ '\u00A0\u00A0'.repeat(fc.depth) }}{{ fc.name }} |
|
|
|
</option> |
|
|
|
</select> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ── 描述 ── --> |
|
|
|
<div v-if="needsDescription" class="form-group"> |
|
|
|
<div class="field-header"> |
|
|
|
<label class="form-group-label" style="margin-bottom:0">描述</label> |
|
|
|
<span class="field-count">{{ description.length }}/200</span> |
|
|
|
</div> |
|
|
|
<textarea |
|
|
|
v-model="description" |
|
|
|
class="field-textarea" |
|
|
|
rows="4" |
|
|
|
placeholder="写点什么…" |
|
|
|
:disabled="saving" |
|
|
|
maxlength="200" |
|
|
|
></textarea> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ── 宽高比 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">宽高比</div> |
|
|
|
<div class="ratio-bar"> |
|
|
|
<button |
|
|
|
v-for="r in ASPECT_PRESETS" |
|
|
|
:key="r.value" |
|
|
|
type="button" |
|
|
|
class="ratio-btn" |
|
|
|
:class="{ active: aspectRatio === r.value }" |
|
|
|
:disabled="saving" |
|
|
|
@click="aspectRatio = r.value" |
|
|
|
> |
|
|
|
{{ r.label }} |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ── 标签(仅项目类型) ── --> |
|
|
|
<div v-if="needsTags" class="form-group"> |
|
|
|
<div class="form-group-label">标签</div> |
|
|
|
|
|
|
|
<!-- 已选标签 --> |
|
|
|
<div v-if="selectedTagIds.length > 0" class="chip-row"> |
|
|
|
<span |
|
|
|
v-for="tid in selectedTagIds" |
|
|
|
:key="tid" |
|
|
|
class="chip chip-active" |
|
|
|
> |
|
|
|
{{ allTags.find(t => t.id === tid)?.name ?? `#${tid}` }} |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
class="chip-remove" |
|
|
|
<!-- ── 描述 ── --> |
|
|
|
<div v-if="needsDescription" class="form-group"> |
|
|
|
<div class="field-header"> |
|
|
|
<label class="form-group-label" style="margin-bottom:0">描述</label> |
|
|
|
<span class="field-count">{{ description.length }}/200</span> |
|
|
|
</div> |
|
|
|
<textarea |
|
|
|
v-model="description" |
|
|
|
class="field-textarea" |
|
|
|
rows="5" |
|
|
|
placeholder="写点什么…" |
|
|
|
:disabled="saving" |
|
|
|
@click="toggleTag(tid)" |
|
|
|
> |
|
|
|
<Icon name="lucide:x" /> |
|
|
|
</button> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
maxlength="200" |
|
|
|
></textarea> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 可选标签池 --> |
|
|
|
<div v-if="availableTags.length > 0" class="tag-pool"> |
|
|
|
<button |
|
|
|
v-for="tag in availableTags" |
|
|
|
:key="tag.id" |
|
|
|
type="button" |
|
|
|
class="tag-chip" |
|
|
|
:disabled="saving" |
|
|
|
@click="toggleTag(tag.id)" |
|
|
|
> |
|
|
|
{{ tag.name }} |
|
|
|
</button> |
|
|
|
<!-- ── 宽高比 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">宽高比</div> |
|
|
|
<div class="ratio-bar"> |
|
|
|
<button |
|
|
|
v-for="r in ASPECT_PRESETS" |
|
|
|
:key="r.value" |
|
|
|
type="button" |
|
|
|
class="ratio-btn" |
|
|
|
:class="{ active: aspectRatio === r.value }" |
|
|
|
:disabled="saving" |
|
|
|
@click="aspectRatio = r.value" |
|
|
|
> |
|
|
|
{{ r.label }} |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<span v-if="allTags.length === 0 && tagsLoaded" class="hint-text"> |
|
|
|
暂无可用标签,请先在标签管理中创建 |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<!-- ═══ 右栏:媒体 & 关联 ═══ --> |
|
|
|
<div class="form-right"> |
|
|
|
<!-- ── 图片(单张) ── --> |
|
|
|
<div v-if="needsImage" class="form-group"> |
|
|
|
<div class="form-group-label">图片 <span class="field-required">*</span></div> |
|
|
|
|
|
|
|
<div class="image-upload-row"> |
|
|
|
<label class="image-dropzone"> |
|
|
|
<Icon name="lucide:image-plus" class="dropzone-icon" /> |
|
|
|
<span class="dropzone-text">点击上传</span> |
|
|
|
<input |
|
|
|
type="file" |
|
|
|
accept="image/*" |
|
|
|
:disabled="saving" |
|
|
|
hidden |
|
|
|
@change="onFileUpload" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<input |
|
|
|
v-model="imageUrl" |
|
|
|
type="url" |
|
|
|
class="field-input" |
|
|
|
placeholder="或粘贴图片 URL…" |
|
|
|
:disabled="saving" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-if="imageUrl" class="image-preview-block"> |
|
|
|
<img |
|
|
|
:src="imageUrl" |
|
|
|
alt="预览" |
|
|
|
class="image-preview-img" |
|
|
|
@error="(e) => { (e.target as HTMLImageElement).style.display = 'none' }" |
|
|
|
/> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
class="image-preview-clear" |
|
|
|
:disabled="saving" |
|
|
|
@click="imageUrl = ''" |
|
|
|
> |
|
|
|
<Icon name="lucide:x" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ── 关联文章 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">关联文章</div> |
|
|
|
|
|
|
|
<!-- 已选文章 --> |
|
|
|
<div v-if="selectedArticleIds.length > 0" class="chip-row"> |
|
|
|
<span |
|
|
|
v-for="aid in selectedArticleIds" |
|
|
|
:key="aid" |
|
|
|
class="chip chip-article" |
|
|
|
> |
|
|
|
{{ allArticles.find(a => a.id === aid)?.title ?? `文章 #${aid}` }} |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
class="chip-remove" |
|
|
|
:disabled="saving" |
|
|
|
@click="selectedArticleIds = selectedArticleIds.filter(id => id !== aid)" |
|
|
|
> |
|
|
|
<Icon name="lucide:x" /> |
|
|
|
<!-- ── 图片列表(图集) ── --> |
|
|
|
<div v-if="needsMultiImage" class="form-group"> |
|
|
|
<div class="form-group-label">图片列表 <span class="field-required">*</span></div> |
|
|
|
|
|
|
|
<div class="multi-image-list"> |
|
|
|
<div v-for="(_, idx) in imageUrls" :key="idx" class="multi-image-row"> |
|
|
|
<span class="multi-image-index">{{ idx + 1 }}</span> |
|
|
|
<input |
|
|
|
v-model="imageUrls[idx]" |
|
|
|
type="url" |
|
|
|
class="field-input" |
|
|
|
:placeholder="`图片 ${idx + 1} 的 URL`" |
|
|
|
:disabled="saving" |
|
|
|
/> |
|
|
|
<button |
|
|
|
v-if="imageUrls.length > 1" |
|
|
|
type="button" |
|
|
|
class="multi-image-remove" |
|
|
|
:disabled="saving" |
|
|
|
@click="removeImageUrl(idx)" |
|
|
|
> |
|
|
|
<Icon name="lucide:trash-2" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<button type="button" class="add-row-btn" :disabled="saving" @click="addImageUrl"> |
|
|
|
<Icon name="lucide:plus" /> |
|
|
|
<span>添加一张</span> |
|
|
|
</button> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 文章搜索 --> |
|
|
|
<div class="article-picker"> |
|
|
|
<div class="article-search-wrap"> |
|
|
|
<Icon name="lucide:search" class="article-search-icon" /> |
|
|
|
<input |
|
|
|
v-model="articleSearch" |
|
|
|
type="text" |
|
|
|
class="field-input" |
|
|
|
placeholder="搜索文章然后点击添加…" |
|
|
|
:disabled="saving" |
|
|
|
@focus="onArticleFocus" |
|
|
|
@blur="onArticleBlur" |
|
|
|
/> |
|
|
|
<div v-if="imageUrls.some(u => u.trim())" class="image-previews-grid"> |
|
|
|
<div |
|
|
|
v-for="(url, idx) in imageUrls.filter(u => u.trim())" |
|
|
|
:key="idx" |
|
|
|
class="image-preview-thumb" |
|
|
|
> |
|
|
|
<img |
|
|
|
:src="url" |
|
|
|
:alt="`预览 ${idx + 1}`" |
|
|
|
@error="(e) => { (e.target as HTMLImageElement).style.display = 'none' }" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div v-if="showArticlePicker && availableArticles.length > 0" class="article-dropdown"> |
|
|
|
<button |
|
|
|
v-for="a in availableArticles.slice(0, 12)" |
|
|
|
:key="a.id" |
|
|
|
type="button" |
|
|
|
class="article-option" |
|
|
|
:disabled="saving" |
|
|
|
@mousedown.prevent="addArticleId(a.id)" |
|
|
|
> |
|
|
|
{{ a.title }} |
|
|
|
</button> |
|
|
|
|
|
|
|
<!-- ── 标签(仅项目类型) ── --> |
|
|
|
<div v-if="needsTags" class="form-group"> |
|
|
|
<div class="form-group-label">标签</div> |
|
|
|
|
|
|
|
<div v-if="selectedTagIds.length > 0" class="chip-row"> |
|
|
|
<span |
|
|
|
v-for="tid in selectedTagIds" |
|
|
|
:key="tid" |
|
|
|
class="chip chip-active" |
|
|
|
> |
|
|
|
{{ allTags.find(t => t.id === tid)?.name ?? `#${tid}` }} |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
class="chip-remove" |
|
|
|
:disabled="saving" |
|
|
|
@click="toggleTag(tid)" |
|
|
|
> |
|
|
|
<Icon name="lucide:x" /> |
|
|
|
</button> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-if="availableTags.length > 0" class="tag-pool"> |
|
|
|
<button |
|
|
|
v-for="tag in availableTags" |
|
|
|
:key="tag.id" |
|
|
|
type="button" |
|
|
|
class="tag-chip" |
|
|
|
:disabled="saving" |
|
|
|
@click="toggleTag(tag.id)" |
|
|
|
> |
|
|
|
{{ tag.name }} |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
|
|
|
|
<span v-if="allTags.length === 0 && tagsLoaded" class="hint-text"> |
|
|
|
暂无可用标签,请先在标签管理中创建 |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
v-else-if="showArticlePicker && articleSearch && availableArticles.length === 0" |
|
|
|
class="article-dropdown" |
|
|
|
> |
|
|
|
<span class="article-empty">未找到匹配文章</span> |
|
|
|
|
|
|
|
<!-- ── 关联文章 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">关联文章</div> |
|
|
|
|
|
|
|
<div v-if="selectedArticleIds.length > 0" class="chip-row"> |
|
|
|
<span |
|
|
|
v-for="aid in selectedArticleIds" |
|
|
|
:key="aid" |
|
|
|
class="chip chip-article" |
|
|
|
> |
|
|
|
{{ allArticles.find(a => a.id === aid)?.title ?? `文章 #${aid}` }} |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
class="chip-remove" |
|
|
|
:disabled="saving" |
|
|
|
@click="selectedArticleIds = selectedArticleIds.filter(id => id !== aid)" |
|
|
|
> |
|
|
|
<Icon name="lucide:x" /> |
|
|
|
</button> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="article-picker"> |
|
|
|
<div class="article-search-wrap"> |
|
|
|
<Icon name="lucide:search" class="article-search-icon" /> |
|
|
|
<input |
|
|
|
v-model="articleSearch" |
|
|
|
type="text" |
|
|
|
class="field-input" |
|
|
|
placeholder="搜索文章然后点击添加…" |
|
|
|
:disabled="saving" |
|
|
|
@focus="onArticleFocus" |
|
|
|
@blur="onArticleBlur" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
<div v-if="showArticlePicker && availableArticles.length > 0" class="article-dropdown"> |
|
|
|
<button |
|
|
|
v-for="a in availableArticles.slice(0, 12)" |
|
|
|
:key="a.id" |
|
|
|
type="button" |
|
|
|
class="article-option" |
|
|
|
:disabled="saving" |
|
|
|
@mousedown.prevent="addArticleId(a.id)" |
|
|
|
> |
|
|
|
{{ a.title }} |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
v-else-if="showArticlePicker && articleSearch && availableArticles.length === 0" |
|
|
|
class="article-dropdown" |
|
|
|
> |
|
|
|
<span class="article-empty">未找到匹配文章</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -659,7 +657,25 @@ async function onSubmit() { |
|
|
|
padding: 20px 28px; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
gap: 20px; |
|
|
|
gap: 16px; |
|
|
|
max-height: calc(85vh - 130px); |
|
|
|
overflow-y: auto; |
|
|
|
} |
|
|
|
|
|
|
|
/* ── Two-column grid ────────────────────────────────────────────── */ |
|
|
|
|
|
|
|
.form-columns { |
|
|
|
display: grid; |
|
|
|
grid-template-columns: 1fr 1fr; |
|
|
|
gap: 28px; |
|
|
|
} |
|
|
|
|
|
|
|
.form-left, |
|
|
|
.form-right { |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
gap: 16px; |
|
|
|
min-width: 0; |
|
|
|
} |
|
|
|
|
|
|
|
/* ── Error ──────────────────────────────────────────────────────── */ |
|
|
|
@ -862,7 +878,7 @@ async function onSubmit() { |
|
|
|
} |
|
|
|
.image-preview-img { |
|
|
|
width: 100%; |
|
|
|
max-height: 200px; |
|
|
|
max-height: 320px; |
|
|
|
object-fit: cover; |
|
|
|
display: block; |
|
|
|
} |
|
|
|
@ -955,8 +971,8 @@ async function onSubmit() { |
|
|
|
|
|
|
|
.image-previews-grid { |
|
|
|
display: grid; |
|
|
|
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); |
|
|
|
gap: 6px; |
|
|
|
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); |
|
|
|
gap: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
.image-preview-thumb { |
|
|
|
@ -1198,4 +1214,33 @@ async function onSubmit() { |
|
|
|
@keyframes spin { |
|
|
|
to { transform: rotate(360deg); } |
|
|
|
} |
|
|
|
|
|
|
|
/* ── Responsive: single column on mobile ────────────────────────── */ |
|
|
|
|
|
|
|
@media (max-width: 767.98px) { |
|
|
|
.form-columns { |
|
|
|
grid-template-columns: 1fr; |
|
|
|
gap: 16px; |
|
|
|
} |
|
|
|
|
|
|
|
.form-body { |
|
|
|
padding: 16px; |
|
|
|
max-height: calc(85vh - 120px); |
|
|
|
} |
|
|
|
|
|
|
|
.form-header { |
|
|
|
padding: 16px 16px 0; |
|
|
|
} |
|
|
|
|
|
|
|
.form-footer { |
|
|
|
padding: 12px 16px; |
|
|
|
flex-direction: column; |
|
|
|
gap: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
.form-footer .btn { |
|
|
|
width: 100%; |
|
|
|
justify-content: center; |
|
|
|
} |
|
|
|
} |
|
|
|
</style> |
|
|
|
|