|
|
|
|
@@ -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;
|
|
|
|
|
}
|
|
|
|
|
|