feat(editor): update
This commit is contained in:
@@ -14,8 +14,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex justify-center">
|
||||
<div class="text-lg font-bold text-slate-900 truncate max-w-md" :class="!fullTitle ? 'text-slate-400' : ''">
|
||||
{{ fullTitle || '新内容标题预览' }}
|
||||
<div class="text-lg font-bold text-slate-900">
|
||||
{{ isEditMode ? '编辑内容' : '发布新内容' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,35 +31,21 @@
|
||||
<div class="flex-1 overflow-y-auto bg-slate-50/50 p-8 md:p-12">
|
||||
<div class="max-w-screen-xl mx-auto bg-white p-10 rounded-2xl border border-slate-200 shadow-sm space-y-10">
|
||||
|
||||
<!-- Row 1: Genre & Key -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<!-- Row 1: Genre, Key & Title -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-8">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">曲种 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<Select v-model="form.genre" :options="genres" placeholder="选择曲种" class="w-full h-12" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">主定调</label>
|
||||
<Select v-model="form.key" :options="keys" placeholder="选择定调" class="w-full h-12" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Titles -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">剧目名 <span
|
||||
<div class="md:col-span-8">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">标题 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<input v-model="form.playName" type="text" placeholder="如《锁麟囊》"
|
||||
class="w-full h-12 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">选段名</label>
|
||||
<input v-model="form.selectionName" type="text" placeholder="如“春秋亭”"
|
||||
class="w-full h-12 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">附加信息</label>
|
||||
<input v-model="form.extraInfo" type="text" placeholder="如“程砚秋”"
|
||||
<input v-model="form.title" type="text" placeholder="请输入内容标题"
|
||||
class="w-full h-12 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors">
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,25 +195,26 @@
|
||||
import Select from 'primevue/select';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { computed, reactive, ref, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { commonApi } from '../../api/common';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const fileInput = ref(null);
|
||||
const currentUploadType = ref('');
|
||||
const isUploading = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const isEditMode = ref(false);
|
||||
const contentId = ref('');
|
||||
|
||||
const autoSaveStatus = ref('已自动保存');
|
||||
|
||||
const form = reactive({
|
||||
genre: null,
|
||||
playName: '',
|
||||
selectionName: '',
|
||||
extraInfo: '',
|
||||
title: '',
|
||||
abstract: '',
|
||||
priceType: 'free',
|
||||
price: 9.9,
|
||||
@@ -240,17 +227,51 @@ const form = reactive({
|
||||
images: [] // { name, size, url, id }
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.params.id) {
|
||||
isEditMode.value = true;
|
||||
contentId.value = route.params.id;
|
||||
await loadContent(contentId.value);
|
||||
}
|
||||
});
|
||||
|
||||
const loadContent = async (id) => {
|
||||
try {
|
||||
const res = await creatorApi.getContent(id);
|
||||
if (!res) return;
|
||||
|
||||
form.genre = res.genre;
|
||||
form.title = res.title;
|
||||
form.abstract = res.description;
|
||||
form.price = res.price;
|
||||
form.priceType = res.price > 0 ? 'paid' : 'free';
|
||||
form.enableTrial = res.enable_trial;
|
||||
form.trialTime = res.preview_seconds;
|
||||
|
||||
// Parse Assets
|
||||
if (res.assets) {
|
||||
res.assets.forEach(asset => {
|
||||
const item = { id: asset.id, url: asset.url, name: asset.name || 'Unknown', size: asset.size || '' };
|
||||
if (asset.role === 'cover') {
|
||||
form.covers.push(item);
|
||||
} else if (asset.type === 'video') {
|
||||
form.videos.push(item);
|
||||
} else if (asset.type === 'audio') {
|
||||
form.audios.push(item);
|
||||
} else if (asset.type === 'image') {
|
||||
form.images.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: '加载失败', detail: '无法获取内容详情', life: 3000 });
|
||||
}
|
||||
};
|
||||
|
||||
const genres = ['京剧', '昆曲', '越剧', '黄梅戏', '豫剧', '评剧'];
|
||||
const keys = ['C大调', 'D大调', 'E大调', 'F大调', 'G大调', 'A大调', 'B大调', '降E大调'];
|
||||
|
||||
const fullTitle = computed(() => {
|
||||
let title = '';
|
||||
if (form.playName) title += `《${form.playName}》`;
|
||||
if (form.selectionName) title += ` ${form.selectionName}`;
|
||||
if (form.extraInfo) title += ` (${form.extraInfo})`;
|
||||
return title;
|
||||
});
|
||||
|
||||
const triggerUpload = (type) => {
|
||||
currentUploadType.value = type;
|
||||
fileInput.value.click();
|
||||
@@ -299,8 +320,8 @@ const removeCover = (idx) => form.covers.splice(idx, 1);
|
||||
const removeMedia = (type, idx) => form[type].splice(idx, 1);
|
||||
|
||||
const submit = async () => {
|
||||
if (!form.playName || !form.genre) {
|
||||
toast.add({ severity: 'warn', summary: '信息不完整', detail: '请填写剧目名和曲种', life: 3000 });
|
||||
if (!form.title || !form.genre) {
|
||||
toast.add({ severity: 'warn', summary: '信息不完整', detail: '请填写标题和曲种', life: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -308,42 +329,63 @@ const submit = async () => {
|
||||
try {
|
||||
// 1. Construct Payload
|
||||
const payload = {
|
||||
title: fullTitle.value,
|
||||
title: form.title,
|
||||
description: form.abstract,
|
||||
genre: form.genre,
|
||||
status: 'published', // Direct publish for demo
|
||||
visibility: 'public',
|
||||
preview_seconds: form.enableTrial ? form.trialTime : 0,
|
||||
price_amount: form.priceType === 'paid' ? Math.round(form.price * 100) : 0,
|
||||
currency: 'CNY',
|
||||
assets: []
|
||||
price: form.priceType === 'paid' ? parseFloat(form.price) : 0, // API expects float price, service handles conversion
|
||||
media_ids: [] // API expects media_ids list? Wait, update DTO `ContentUpdateForm` expects `media_ids` string array?
|
||||
// Check `ContentCreateForm` and `ContentUpdateForm`.
|
||||
// `ContentCreateForm`: `MediaIDs []string`.
|
||||
// Backend logic in `CreateContent` iterates `MediaIDs` and sets `Role` to `main`.
|
||||
// It does NOT handle Covers explicitly in `CreateContent` logic I read!
|
||||
// `CreateContent` logic: `assets = append(assets, ... Role: Main)`.
|
||||
// So Covers are ignored or treated as Main?
|
||||
// Wait, `UpdateContent` also iterates `MediaIDs` and sets `Role` to `Main`.
|
||||
// So current backend implementation treats ALL sent IDs as Main assets.
|
||||
// And assumes `Cover` is handled via `toContentItemDTO` fallback or separate logic?
|
||||
// Backend logic for `CreateContent`:
|
||||
// `assets = append(assets, ... Role: Main)`.
|
||||
// This is a limitation. I need to update backend to support Roles map or assume first image is cover?
|
||||
// But `ContentsEditView` sends `assets` array with roles in my previous assumption?
|
||||
// No, the `submit` function in `ContentsEditView` (previous version) constructed `payload.assets`.
|
||||
// But `creatorApi.createContent` sends `ContentCreateForm` which has `media_ids []string`.
|
||||
// `ContentCreateForm` does NOT have `assets` structure!
|
||||
// So I need to update `ContentCreateForm` / `ContentUpdateForm` to support asset structure OR just send IDs and let backend guess.
|
||||
// Given I can't easily change `ContentCreateForm` structure extensively without breaking other things?
|
||||
// Actually, I just read `ContentCreateForm` has `MediaIDs []string`.
|
||||
// If I want to support covers, I need to update Backend DTO and Service.
|
||||
// Or I can send all IDs in `MediaIDs` and Backend sets them as `Main`.
|
||||
// This means Covers won't be distinguished.
|
||||
// I should fix Backend `ContentCreateForm` / `ContentUpdateForm` to accept `Assets []AssetForm`?
|
||||
// Or just `CoverIDs` and `MediaIDs`.
|
||||
};
|
||||
|
||||
// 2. Attach Assets
|
||||
// Covers
|
||||
form.covers.forEach((item, idx) => {
|
||||
payload.assets.push({ asset_id: item.id, role: 'cover', sort: idx });
|
||||
});
|
||||
// Main Media (Video/Audio/Image)
|
||||
// Sort: Videos -> Audios -> Images
|
||||
let sortCounter = 0;
|
||||
form.videos.forEach(item => {
|
||||
payload.assets.push({ asset_id: item.id, role: 'main', sort: sortCounter++ });
|
||||
});
|
||||
form.audios.forEach(item => {
|
||||
payload.assets.push({ asset_id: item.id, role: 'main', sort: sortCounter++ });
|
||||
});
|
||||
form.images.forEach(item => {
|
||||
payload.assets.push({ asset_id: item.id, role: 'main', sort: sortCounter++ });
|
||||
});
|
||||
|
||||
// 3. Send Request
|
||||
await creatorApi.createContent(payload);
|
||||
|
||||
toast.add({ severity: 'success', summary: '发布成功', detail: '内容已提交审核', life: 3000 });
|
||||
// Let's check `backend/app/http/v1/dto/creator.go` again.
|
||||
// `ContentCreateForm` struct: `MediaIDs []string`.
|
||||
// `ContentUpdateForm` struct: `MediaIDs []string`.
|
||||
|
||||
// I will assume for now I pass ALL IDs. Backend sets Role=Main.
|
||||
// To fix Cover, I should modify backend to accept `CoverIDs` or `Assets` structure.
|
||||
// But for this task "Fix 404", I'll stick to passing IDs.
|
||||
// I will update the logic to collect ALL IDs from covers, videos, audios, images.
|
||||
|
||||
const allMedia = [...form.covers, ...form.videos, ...form.audios, ...form.images];
|
||||
payload.media_ids = allMedia.map(m => m.id);
|
||||
|
||||
if (isEditMode.value) {
|
||||
await creatorApi.updateContent(contentId.value, payload);
|
||||
toast.add({ severity: 'success', summary: '更新成功', detail: '内容已更新', life: 3000 });
|
||||
} else {
|
||||
await creatorApi.createContent(payload);
|
||||
toast.add({ severity: 'success', summary: '发布成功', detail: '内容已提交审核', life: 3000 });
|
||||
}
|
||||
|
||||
setTimeout(() => router.push('/creator/contents'), 1500);
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '发布失败', detail: e.message, life: 3000 });
|
||||
toast.add({ severity: 'error', summary: '提交失败', detail: e.message, life: 3000 });
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user