151 lines
7.2 KiB
Vue
151 lines
7.2 KiB
Vue
<template>
|
||
<view class="video-page" :class="`video-state--${pageState}`">
|
||
<ModulePageBackground module="family" />
|
||
<view class="page-header">
|
||
<PageHeader title="家族视频" custom-back @back="returnToFamily" />
|
||
</view>
|
||
<view class="page-content">
|
||
<view v-if="pageState === 'form'" class="video-panel">
|
||
<text class="video-panel__title">发布家族视频</text>
|
||
<text class="video-panel__note">视频文件由上传回执自动关联,发布时不需要填写文件 ID。</text>
|
||
|
||
<view class="video-field video-field--upload">
|
||
<text class="video-field__label"><text class="required-mark">*</text>视频文件</text>
|
||
<button class="upload-button" :disabled="uploading || submitting" @click="selectVideo">
|
||
{{ uploading ? "上传中…" : receipt ? "重新选择视频" : "选择视频" }}
|
||
</button>
|
||
<text v-if="receipt" class="upload-receipt">已上传:{{ receipt.fileName || "视频" }}</text>
|
||
</view>
|
||
<text v-if="uploadError" class="field-error">{{ uploadError }}</text>
|
||
|
||
<view class="video-field">
|
||
<text class="video-field__label"><text class="required-mark">*</text>视频标题</text>
|
||
<input v-model="form.videoTitle" maxlength="100" placeholder="例如:2026 年清明祭祖活动" placeholder-class="placeholder" @input="submitError = ''" />
|
||
</view>
|
||
<view class="video-field video-field--textarea">
|
||
<text class="video-field__label">视频说明</text>
|
||
<textarea v-model="form.videoDesc" maxlength="500" auto-height placeholder="补充视频中的人物、场景或故事" placeholder-class="placeholder" @input="submitError = ''" />
|
||
</view>
|
||
<text v-if="submitError" class="field-error">{{ submitError }}</text>
|
||
<AppButton block :disabled="uploading || submitting" :label="submitting ? '正在发布…' : '发布视频'" @click="submitVideo" />
|
||
</view>
|
||
|
||
<view v-else class="video-state-card">
|
||
<text class="video-state-card__title">{{ stateCopy.title }}</text>
|
||
<text class="video-state-card__copy">{{ stateCopy.copy }}</text>
|
||
<AppButton block :label="stateCopy.action" @click="handleStateAction" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||
import { onLoad } from "@dcloudio/uni-app";
|
||
import AppButton from "@/components/AppButton.vue";
|
||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||
import PageHeader from "@/components/PageHeader.vue";
|
||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||
import { isVideoPickCancelled, pickAndUploadVideo } from "@/utils/resumable-image-upload.js";
|
||
import { goBack, returnTo } from "@/utils/navigation.js";
|
||
|
||
const genealogyId = ref("");
|
||
const pageState = ref("form");
|
||
const receipt = ref(null);
|
||
const uploading = ref(false);
|
||
const submitting = ref(false);
|
||
const uploadError = ref("");
|
||
const submitError = ref("");
|
||
const form = reactive({ videoTitle: "", videoDesc: "" });
|
||
const controller = createRequestController();
|
||
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
|
||
const stateCopy = computed(() => pageState.value === "success"
|
||
? {
|
||
title: "视频已提交服务端",
|
||
copy: "视频已按上传回执关联到当前家谱。视频列表接口尚未提供可消费的返回字段,因此此处不猜测播放地址或卡片内容。",
|
||
action: "返回家族动态",
|
||
}
|
||
: {
|
||
title: "视频入口无效",
|
||
copy: "没有取得有效的家谱标识。",
|
||
action: "返回上一页",
|
||
});
|
||
|
||
onLoad((query) => {
|
||
genealogyId.value = String(query?.genealogyId || "");
|
||
if (!hasValidContext.value) pageState.value = "invalid";
|
||
});
|
||
onUnmounted(() => controller.abort());
|
||
|
||
const selectVideo = async () => {
|
||
if (uploading.value || submitting.value) return;
|
||
uploading.value = true;
|
||
uploadError.value = "";
|
||
try {
|
||
receipt.value = await pickAndUploadVideo({ requestController: controller });
|
||
} catch (error) {
|
||
if (!isVideoPickCancelled(error) && !isRequestCancelled(error)) {
|
||
uploadError.value = error?.message || "视频上传失败,请稍后重试";
|
||
}
|
||
} finally {
|
||
uploading.value = false;
|
||
}
|
||
};
|
||
const submitVideo = async () => {
|
||
if (uploading.value || submitting.value || !hasValidContext.value) return;
|
||
const videoTitle = form.videoTitle.trim();
|
||
if (!receipt.value) {
|
||
submitError.value = "请先选择并上传视频";
|
||
return;
|
||
}
|
||
if (!videoTitle) {
|
||
submitError.value = "请填写视频标题";
|
||
return;
|
||
}
|
||
submitting.value = true;
|
||
submitError.value = "";
|
||
try {
|
||
await appApi.createVideo(genealogyId.value, {
|
||
videoTitle,
|
||
videoDesc: form.videoDesc.trim(),
|
||
videoOssId: receipt.value.ossId,
|
||
}, { requestController: controller });
|
||
pageState.value = "success";
|
||
} catch (error) {
|
||
if (!isRequestCancelled(error)) submitError.value = error?.message || "视频发布失败,请稍后重试";
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
};
|
||
const returnToFamily = () => hasValidContext.value
|
||
? returnTo("F01", { genealogyId: genealogyId.value })
|
||
: goBack();
|
||
const handleStateAction = () => returnToFamily();
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@import "../../styles/adaptive-frame-profiles.scss";
|
||
.video-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||
.page-header, .page-content { z-index: 1; }
|
||
.page-content { flex: 1; padding: 18rpx 24rpx 72rpx; }
|
||
.video-panel, .video-state-card { box-sizing: border-box; @include adaptive-family-content; }
|
||
.video-panel { padding: 30rpx; }
|
||
.video-panel__title, .video-panel__note, .video-field__label, .video-state-card text { display: block; }
|
||
.video-panel__title, .video-state-card__title { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: clamp(19px, 34rpx, 24px); font-weight: 700; }
|
||
.video-panel__note, .video-state-card__copy { margin-top: 12rpx; color: $ink-muted; font-size: clamp(14px, 23rpx, 17px); line-height: 1.6; }
|
||
.video-field { margin-top: 24rpx; }
|
||
.video-field__label { margin-bottom: 10rpx; color: $ink; font-size: clamp(15px, 24rpx, 18px); font-weight: 700; }
|
||
.video-field input, .video-field textarea { width: 100%; box-sizing: border-box; border: 1rpx solid rgba(143, 108, 63, .34); border-radius: 8rpx; background: rgba(255, 253, 247, .8); color: $ink; font-size: clamp(15px, 24rpx, 18px); }
|
||
.video-field input { height: 76rpx; padding: 0 18rpx; }
|
||
.video-field textarea { min-height: 140rpx; padding: 16rpx 18rpx; }
|
||
.video-field--upload { display: flex; flex-wrap: wrap; align-items: center; gap: 12rpx; }
|
||
.video-field--upload .video-field__label { width: 100%; }
|
||
.upload-button { margin: 0; padding: 0 26rpx; border: 1rpx solid #b78a42; border-radius: 8rpx; background: #fffaf0; color: #805723; font-size: clamp(14px, 23rpx, 17px); line-height: 64rpx; }
|
||
.upload-receipt { color: $ink-muted; font-size: clamp(13px, 21rpx, 16px); }
|
||
.required-mark, .field-error { color: $brand-red; }
|
||
.field-error { display: block; margin-top: 12rpx; font-size: clamp(14px, 22rpx, 17px); }
|
||
.video-panel .app-button { margin-top: 28rpx; }
|
||
.video-state-card { min-height: 340rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||
.video-state-card .app-button { margin-top: 28rpx; }
|
||
</style>
|