Files
quyun-v2/frontend/portal/src/views/management/ContentPublish.vue

209 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { requestJson } from '@/service/apiClient';
import { useSession } from '@/service/session';
import { useToast } from 'primevue/usetoast';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const toast = useToast();
const router = useRouter();
const route = useRoute();
const { isLoggedIn } = useSession();
const submitting = ref(false);
const tenantCode = ref('');
const title = ref('');
const summary = ref('');
const detail = ref('');
const tags = ref([]);
const coverAssetIDsInput = ref('');
const audioAssetIDsInput = ref('');
const videoAssetIDsInput = ref('');
const imageAssetIDsInput = ref('');
const priceAmount = ref(0);
const hasText = computed(() => String(detail.value || '').trim().length > 0);
const hasAnyMedia = computed(() => {
return (
String(audioAssetIDsInput.value || '').trim() ||
String(videoAssetIDsInput.value || '').trim() ||
String(imageAssetIDsInput.value || '').trim()
);
});
function parseIDList(input) {
const raw = String(input || '')
.split(/[,\n\r\t ]+/)
.map((v) => v.trim())
.filter(Boolean);
const ids = raw.map((v) => Number.parseInt(v, 10)).filter((v) => Number.isFinite(v) && v > 0);
const uniq = Array.from(new Set(ids));
return uniq;
}
async function submit() {
if (submitting.value) return;
const tenant = String(tenantCode.value || '').trim();
if (!tenant) {
toast.add({ severity: 'warn', summary: '请填写租户 ID', detail: '例如abcdedf', life: 2500 });
return;
}
const t = String(title.value || '').trim();
if (!t) {
toast.add({ severity: 'warn', summary: '请填写标题', detail: '标题不能为空', life: 2500 });
return;
}
const coverAssetIDs = parseIDList(coverAssetIDsInput.value);
if (coverAssetIDs.length < 1 || coverAssetIDs.length > 3) {
toast.add({ severity: 'warn', summary: '展示图数量不正确', detail: '展示图需为 1-3 张(填图片资源 ID', life: 2800 });
return;
}
const audioAssetIDs = parseIDList(audioAssetIDsInput.value);
const videoAssetIDs = parseIDList(videoAssetIDsInput.value);
const imageAssetIDs = parseIDList(imageAssetIDsInput.value);
if (!hasText.value && audioAssetIDs.length === 0 && videoAssetIDs.length === 0 && imageAssetIDs.length === 0) {
toast.add({ severity: 'warn', summary: '内容为空', detail: '请至少提供一种内容类型(文字/音频/视频/多图)', life: 2800 });
return;
}
const amount = Number(priceAmount.value || 0);
if (!Number.isFinite(amount) || amount < 0) {
toast.add({ severity: 'warn', summary: '价格不正确', detail: '价格需为 0 或正整数(单位:分)', life: 2500 });
return;
}
try {
submitting.value = true;
const payload = await requestJson(`/t/${encodeURIComponent(tenant)}/v1/admin/contents/publish`, {
method: 'POST',
auth: true,
body: {
title: t,
summary: String(summary.value || '').trim(),
detail: String(detail.value || '').trim(),
tags: Array.isArray(tags.value) ? tags.value : [],
cover_asset_ids: coverAssetIDs,
audio_asset_ids: audioAssetIDs,
video_asset_ids: videoAssetIDs,
image_asset_ids: imageAssetIDs,
price_amount: amount,
currency: 'CNY'
}
});
toast.add({
severity: 'success',
summary: '提交成功',
detail: `内容已进入审核ID: ${payload?.content?.id || '-'})`,
life: 2500
});
await router.push('/management/contents');
} catch (err) {
const status = err?.status;
const msg = String(err?.payload?.message || err?.payload?.error || err?.message || '').trim();
if (status === 401) {
toast.add({ severity: 'warn', summary: '请先登录', detail: '登录后再提交发布', life: 2500 });
const redirect = typeof route.fullPath === 'string' ? route.fullPath : '/management/contents/new';
await router.push(`/auth/login?redirect=${encodeURIComponent(redirect)}`);
return;
}
toast.add({ severity: 'error', summary: '提交失败', detail: msg || '请检查资源是否已处理完成ready然后重试', life: 3500 });
} finally {
submitting.value = false;
}
}
onMounted(async () => {
if (!isLoggedIn.value) {
const redirect = typeof route.fullPath === 'string' ? route.fullPath : '/management/contents/new';
await router.push(`/auth/login?redirect=${encodeURIComponent(redirect)}`);
}
});
</script>
<template>
<div class="card max-w-3xl mx-auto">
<div class="flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-semibold">内容发布</h1>
<div class="text-muted-color mt-2">支持文字/音频/视频/多图组合展示图需 1-3 价格单位为分</div>
</div>
<Button label="提交" icon="pi pi-send" size="large" :loading="submitting" @click="submit" />
</div>
<Divider class="my-6" />
<div class="flex flex-col gap-5">
<div>
<label for="tenantCode" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-1">租户 ID</label>
<InputText id="tenantCode" v-model="tenantCode" size="large" class="w-full text-xl py-3" placeholder="例如 abcdedf" autocomplete="off" />
<small class="text-muted-color">当前 Portal 暂不支持自动选择租户这里先手动填写</small>
</div>
<div>
<label for="title" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-1">标题</label>
<InputText id="title" v-model="title" size="large" class="w-full text-xl py-3" placeholder="请输入标题" autocomplete="off" />
</div>
<div>
<label for="summary" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-1">简介</label>
<InputText id="summary" v-model="summary" size="large" class="w-full text-xl py-3" placeholder="用于列表展示(建议 ≤ 256 字符)" autocomplete="off" />
</div>
<div>
<label for="detail" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-1">详细文字内容</label>
<Textarea id="detail" v-model="detail" autoResize rows="6" class="w-full text-lg" placeholder="可选:填写后视为包含文字内容类型" />
</div>
<div>
<label class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-1">标签</label>
<Chips v-model="tags" class="w-full" placeholder="回车添加标签(最多建议 20 个)" />
</div>
<div>
<label for="coverAssets" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-1">展示图图片资源 ID</label>
<InputText
id="coverAssets"
v-model="coverAssetIDsInput"
size="large"
class="w-full text-xl py-3"
placeholder="1-3 个图片资源 ID使用逗号分隔例如12,13,14"
autocomplete="off"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="videoAssets" class="block text-surface-900 dark:text-surface-0 font-medium mb-1">视频资源 ID</label>
<InputText id="videoAssets" v-model="videoAssetIDsInput" size="large" class="w-full" placeholder="例如21,22" autocomplete="off" />
</div>
<div>
<label for="audioAssets" class="block text-surface-900 dark:text-surface-0 font-medium mb-1">音频资源 ID</label>
<InputText id="audioAssets" v-model="audioAssetIDsInput" size="large" class="w-full" placeholder="例如31,32" autocomplete="off" />
</div>
<div>
<label for="imageAssets" class="block text-surface-900 dark:text-surface-0 font-medium mb-1">多图资源 ID</label>
<InputText id="imageAssets" v-model="imageAssetIDsInput" size="large" class="w-full" placeholder="例如41,42,43" autocomplete="off" />
</div>
</div>
<div>
<label for="priceAmount" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-1">价格</label>
<InputNumber id="priceAmount" v-model="priceAmount" :min="0" size="large" class="w-full" placeholder="0 表示免费" />
</div>
<div class="text-sm text-muted-color">
提示如提交失败且提示资源未处理完成请先确保对应资源已变为 ready媒体转码/处理完成
</div>
</div>
</div>
</template>