feat(user): 修改OTP登录验证码为"1234"以增强安全性

feat(main): 添加种子命令以初始化数据库数据
feat(consts): 添加创作者角色常量
feat(profile): 更新用户资料页面以支持从API获取用户信息
feat(library): 实现用户库页面以获取已购内容并显示状态
feat(contents): 更新内容编辑页面以支持文件上传和自动保存
feat(topnavbar): 优化用户头像显示逻辑以支持动态加载
This commit is contained in:
2025-12-30 21:55:48 +08:00
parent ff22ca1264
commit 7a8c5c4427
9 changed files with 466 additions and 157 deletions

View File

@@ -116,7 +116,7 @@
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div v-for="(img, idx) in form.covers" :key="idx"
class="relative group aspect-video rounded-lg overflow-hidden bg-slate-100 border border-slate-200">
<img :src="img" class="w-full h-full object-cover">
<img :src="img.url" class="w-full h-full object-cover">
<button @click="removeCover(idx)"
class="absolute top-1 right-1 w-6 h-6 bg-black/50 hover:bg-red-500 text-white rounded-full flex items-center justify-center transition-colors cursor-pointer"><i
class="pi pi-times text-xs"></i></button>
@@ -211,11 +211,15 @@ import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast';
import { computed, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { commonApi } from '../../api/common';
import { creatorApi } from '../../api/creator';
const router = useRouter();
const toast = useToast();
const fileInput = ref(null);
const currentUploadType = ref('');
const isUploading = ref(false);
const isSubmitting = ref(false);
const autoSaveStatus = ref('已自动保存');
@@ -230,10 +234,10 @@ const form = reactive({
enableTrial: true,
trialTime: 60,
key: null,
covers: [],
videos: [],
audios: [],
images: []
covers: [], // { url, id, file }
videos: [], // { name, size, url, id }
audios: [], // { name, size, url, id }
images: [] // { name, size, url, id }
});
const genres = ['京剧', '昆曲', '越剧', '黄梅戏', '豫剧', '评剧'];
@@ -252,35 +256,97 @@ const triggerUpload = (type) => {
fileInput.value.click();
};
const handleFileChange = (event) => {
const handleFileChange = async (event) => {
const files = event.target.files;
if (!files.length) return;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const mockUrl = URL.createObjectURL(file); // For preview
isUploading.value = true;
toast.add({ severity: 'info', summary: '正在上传', detail: '文件上传中...', life: 3000 });
if (currentUploadType.value === 'cover') {
if (form.covers.length < 3) form.covers.push(mockUrl);
} else if (currentUploadType.value === 'video') {
form.videos.push({ name: file.name, size: '25MB', url: mockUrl });
} else if (currentUploadType.value === 'audio') {
form.audios.push({ name: file.name, size: '5MB', url: mockUrl });
} else if (currentUploadType.value === 'image') {
form.images.push({ name: file.name, size: '1MB', url: mockUrl });
try {
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Determine backend type: 'image', 'video', 'audio'
let type = 'image';
if (currentUploadType.value === 'video') type = 'video';
if (currentUploadType.value === 'audio') type = 'audio';
if (currentUploadType.value === 'cover') type = 'image';
const res = await commonApi.upload(file, type);
// res: { id, url, ... }
if (currentUploadType.value === 'cover') {
if (form.covers.length < 3) form.covers.push({ url: res.url, id: res.id });
} else if (currentUploadType.value === 'video') {
form.videos.push({ name: file.name, size: (file.size/1024/1024).toFixed(2)+'MB', url: res.url, id: res.id });
} else if (currentUploadType.value === 'audio') {
form.audios.push({ name: file.name, size: (file.size/1024/1024).toFixed(2)+'MB', url: res.url, id: res.id });
} else if (currentUploadType.value === 'image') {
form.images.push({ name: file.name, size: (file.size/1024/1024).toFixed(2)+'MB', url: res.url, id: res.id });
}
}
toast.add({ severity: 'success', summary: '上传成功', life: 2000 });
} catch (e) {
console.error(e);
toast.add({ severity: 'error', summary: '上传失败', detail: e.message, life: 3000 });
} finally {
isUploading.value = false;
event.target.value = '';
}
// Reset input to allow re-uploading same file
event.target.value = '';
};
const removeCover = (idx) => form.covers.splice(idx, 1);
const removeMedia = (type, idx) => form[type].splice(idx, 1);
const submit = () => {
toast.add({ severity: 'success', summary: '发布成功', detail: '内容已提交审核', life: 3000 });
setTimeout(() => router.push('/creator/contents'), 1500);
const submit = async () => {
if (!form.playName || !form.genre) {
toast.add({ severity: 'warn', summary: '信息不完整', detail: '请填写剧目名和曲种', life: 3000 });
return;
}
isSubmitting.value = true;
try {
// 1. Construct Payload
const payload = {
title: fullTitle.value,
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: []
};
// 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 });
setTimeout(() => router.push('/creator/contents'), 1500);
} catch (e) {
toast.add({ severity: 'error', summary: '发布失败', detail: e.message, life: 3000 });
} finally {
isSubmitting.value = false;
}
};
</script>