|
|
|
@ -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,12 +295,15 @@ 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-columns"> |
|
|
|
<!-- ═══ 左栏:基础信息 ═══ --> |
|
|
|
<div class="form-left"> |
|
|
|
<!-- ── 卡片类型 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">卡片类型</div> |
|
|
|
@ -319,16 +323,10 @@ async function onSubmit() { |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ── 基本信息 ── --> |
|
|
|
<!-- ── 标题 ── --> |
|
|
|
<div class="form-group"> |
|
|
|
<div class="form-group-label">基本信息</div> |
|
|
|
|
|
|
|
<!-- Title --> |
|
|
|
<div class="form-field"> |
|
|
|
<div class="form-group-label">标题 <span class="field-required">*</span></div> |
|
|
|
<div class="field-header"> |
|
|
|
<label class="field-label"> |
|
|
|
标题 <span class="field-required">*</span> |
|
|
|
</label> |
|
|
|
<span class="field-count">{{ title.length }}/60</span> |
|
|
|
</div> |
|
|
|
<input |
|
|
|
@ -342,9 +340,9 @@ async function onSubmit() { |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Category --> |
|
|
|
<div class="form-field"> |
|
|
|
<label class="field-label">分类</label> |
|
|
|
<!-- ── 分类 ── --> |
|
|
|
<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" |
|
|
|
@ -355,8 +353,44 @@ async function onSubmit() { |
|
|
|
</option> |
|
|
|
</select> |
|
|
|
</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="5" |
|
|
|
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> |
|
|
|
|
|
|
|
<!-- ═══ 右栏:媒体 & 关联 ═══ --> |
|
|
|
<div class="form-right"> |
|
|
|
<!-- ── 图片(单张) ── --> |
|
|
|
<div v-if="needsImage" class="form-group"> |
|
|
|
<div class="form-group-label">图片 <span class="field-required">*</span></div> |
|
|
|
@ -446,45 +480,10 @@ async function onSubmit() { |
|
|
|
</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" |
|
|
|
@ -503,7 +502,6 @@ async function onSubmit() { |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 可选标签池 --> |
|
|
|
<div v-if="availableTags.length > 0" class="tag-pool"> |
|
|
|
<button |
|
|
|
v-for="tag in availableTags" |
|
|
|
@ -526,7 +524,6 @@ async function onSubmit() { |
|
|
|
<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" |
|
|
|
@ -545,7 +542,6 @@ async function onSubmit() { |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 文章搜索 --> |
|
|
|
<div class="article-picker"> |
|
|
|
<div class="article-search-wrap"> |
|
|
|
<Icon name="lucide:search" class="article-search-icon" /> |
|
|
|
@ -580,6 +576,8 @@ async function onSubmit() { |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Footer --> |
|
|
|
<div class="form-footer"> |
|
|
|
@ -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> |
|
|
|
|