209 lines
9.1 KiB
Vue
209 lines
9.1 KiB
Vue
<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>
|