feat: 添加音乐键位字段,更新相关数据结构和接口
This commit is contained in:
@@ -242,6 +242,7 @@ const loadContent = async (id) => {
|
||||
|
||||
form.genre = res.genre;
|
||||
form.title = res.title;
|
||||
form.key = res.key;
|
||||
form.abstract = res.description;
|
||||
form.price = res.price;
|
||||
form.priceType = res.price > 0 ? 'paid' : 'free';
|
||||
@@ -332,6 +333,7 @@ const submit = async () => {
|
||||
title: form.title,
|
||||
description: form.abstract,
|
||||
genre: form.genre,
|
||||
key: form.key,
|
||||
status: 'published', // Direct publish for demo
|
||||
visibility: 'public',
|
||||
preview_seconds: form.enableTrial ? form.trialTime : 0,
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
</div>
|
||||
<div class="ml-auto relative">
|
||||
<i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
<input type="text" placeholder="搜索标题..." v-model="searchKeyword" @keyup.enter="handleSearch" @blur="handleSearch"
|
||||
<input type="text" placeholder="搜索标题..." v-model="searchKeyword" @keyup.enter="handleSearch"
|
||||
@blur="handleSearch"
|
||||
class="h-9 pl-9 pr-4 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none w-48 transition-all focus:w-64">
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +41,8 @@
|
||||
|
||||
<!-- Cover -->
|
||||
<div class="w-40 h-[90px] bg-slate-100 rounded-lg flex-shrink-0 overflow-hidden relative">
|
||||
<img :src="item.cover || 'https://via.placeholder.com/300x168?text=No+Cover'" class="w-full h-full object-cover">
|
||||
<img :src="item.cover || 'https://via.placeholder.com/300x168?text=No+Cover'"
|
||||
class="w-full h-full object-cover">
|
||||
<div
|
||||
class="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<router-link :to="`/creator/contents/${item.id}`"
|
||||
@@ -53,9 +55,11 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs px-1.5 py-0.5 border rounded text-slate-500" v-if="item.genre">[{{ item.genre }}]</span>
|
||||
<h3
|
||||
class="font-bold text-slate-900 text-lg truncate hover:text-primary-600 cursor-pointer transition-colors"
|
||||
<span class="text-xs px-1.5 py-0.5 border rounded text-slate-500" v-if="item.genre">{{
|
||||
item.genre }}</span>
|
||||
<span class="text-xs px-1.5 py-0.5 border rounded text-slate-500" v-if="item.key">{{ item.key
|
||||
}}</span>
|
||||
<h3 class="font-bold text-slate-900 text-lg truncate hover:text-primary-600 cursor-pointer transition-colors"
|
||||
@click="$router.push(`/creator/contents/${item.id}`)">
|
||||
{{ item.title }}</h3>
|
||||
</div>
|
||||
@@ -83,16 +87,17 @@
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-4 pt-3 border-t border-slate-50 mt-2">
|
||||
<button class="text-sm text-slate-500 hover:text-primary-600 font-medium cursor-pointer" @click="$router.push(`/creator/contents/${item.id}`)"><i
|
||||
class="pi pi-file-edit mr-1"></i> 编辑</button>
|
||||
<button class="text-sm text-slate-500 hover:text-primary-600 font-medium cursor-pointer"
|
||||
@click="$router.push(`/creator/contents/${item.id}`)"><i class="pi pi-file-edit mr-1"></i>
|
||||
编辑</button>
|
||||
<button v-if="item.status === 'published'"
|
||||
class="text-sm text-slate-500 hover:text-orange-600 font-medium cursor-pointer"><i
|
||||
class="pi pi-arrow-down mr-1"></i> 下架</button>
|
||||
<button v-if="item.status === 'unpublished'"
|
||||
class="text-sm text-slate-500 hover:text-green-600 font-medium cursor-pointer"><i
|
||||
class="pi pi-arrow-up mr-1"></i> 上架</button>
|
||||
<button class="text-sm text-slate-500 hover:text-red-600 font-medium ml-auto cursor-pointer" @click="handleDelete(item.id)"><i
|
||||
class="pi pi-trash mr-1"></i> 删除</button>
|
||||
<button class="text-sm text-slate-500 hover:text-red-600 font-medium ml-auto cursor-pointer"
|
||||
@click="handleDelete(item.id)"><i class="pi pi-trash mr-1"></i> 删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,10 +106,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, watch } from 'vue';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
import { commonApi } from '../../api/common';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { commonApi } from '../../api/common';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
|
||||
const router = useRouter();
|
||||
const contents = ref([]);
|
||||
@@ -115,49 +120,49 @@ const statusOptions = ref([]);
|
||||
const genreOptions = ref([]);
|
||||
|
||||
const fetchOptions = async () => {
|
||||
try {
|
||||
const res = await commonApi.getOptions();
|
||||
if (res) {
|
||||
statusOptions.value = res.content_status || [];
|
||||
genreOptions.value = res.content_genre || [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
try {
|
||||
const res = await commonApi.getOptions();
|
||||
if (res) {
|
||||
statusOptions.value = res.content_status || [];
|
||||
genreOptions.value = res.content_genre || [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchContents = async () => {
|
||||
try {
|
||||
const params = {};
|
||||
if (filterStatus.value !== 'all') params.status = filterStatus.value;
|
||||
if (filterGenre.value !== 'all') params.genre = filterGenre.value;
|
||||
if (searchKeyword.value) params.keyword = searchKeyword.value;
|
||||
|
||||
const res = await creatorApi.listContents(params);
|
||||
contents.value = res || [];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
try {
|
||||
const params = {};
|
||||
if (filterStatus.value !== 'all') params.status = filterStatus.value;
|
||||
if (filterGenre.value !== 'all') params.genre = filterGenre.value;
|
||||
if (searchKeyword.value) params.keyword = searchKeyword.value;
|
||||
|
||||
const res = await creatorApi.listContents(params);
|
||||
contents.value = res || [];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchOptions();
|
||||
fetchContents();
|
||||
fetchOptions();
|
||||
fetchContents();
|
||||
});
|
||||
|
||||
watch([filterStatus, filterGenre], () => {
|
||||
fetchContents();
|
||||
fetchContents();
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchContents();
|
||||
fetchContents();
|
||||
};
|
||||
|
||||
const statusStyle = (status) => {
|
||||
// Map backend status to UI style. Labels should ideally come from backend option value/label map if needed,
|
||||
// Map backend status to UI style. Labels should ideally come from backend option value/label map if needed,
|
||||
// but for style/color mapping we can keep it here or use a helper.
|
||||
// Using labels from options if available would be best for text.
|
||||
|
||||
|
||||
const option = statusOptions.value.find(o => o.key === status);
|
||||
const label = option ? option.value : status;
|
||||
|
||||
@@ -172,12 +177,12 @@ const statusStyle = (status) => {
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('确定要删除吗?')) return;
|
||||
try {
|
||||
await creatorApi.deleteContent(id);
|
||||
fetchContents();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
if (!confirm('确定要删除吗?')) return;
|
||||
try {
|
||||
await creatorApi.deleteContent(id);
|
||||
fetchContents();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user