feat: 完成前端业务闭环与后端联调

This commit is contained in:
2026-08-24 23:37:56 +08:00
parent c59e36f933
commit 9ad572907b
175 changed files with 10711 additions and 1740 deletions
+532 -18
View File
@@ -6,7 +6,7 @@
title="家族视频"
:action="pageState === 'list' ? '发布' : ''"
custom-back
@back="returnToFamily"
@back="requestBack"
@action="openPublishForm"
/>
</view>
@@ -64,6 +64,7 @@
<text v-if="coverReceipt" class="upload-receipt"
>已上传{{ coverReceipt.fileName || "视频封面" }}</text
>
<button v-if="coverReceipt" class="remove-cover-button" :disabled="coverUploading || submitting" @click="clearCover">移除封面</button>
</view>
<text v-if="coverUploadError" class="field-error">{{
coverUploadError
@@ -106,6 +107,7 @@
</view>
<view v-else-if="pageState === 'list'" class="video-list-panel">
<AppButton block type="secondary" label="观看平台视频" @click="openPlatformVideos" />
<view v-if="videoListState === 'loading'" class="video-state-card">
<AppLoading text="正在读取家族视频" />
</view>
@@ -127,12 +129,25 @@
<AppButton block label="发布视频" @click="openPublishForm" />
</view>
<view v-else class="video-card-list">
<AppButton
block
type="secondary"
label="上下滑动观看"
@click="openVerticalViewer(videos[0])"
/>
<view v-for="video in videos" :key="video.id" class="video-card">
<video
class="video-card__player"
:src="video.videoFile.accessUrl"
controls
/>
<button
class="video-card__cover-action"
:aria-label="`播放${video.title}`"
@click="openVerticalViewer(video)"
>
<image
class="video-card__cover"
:src="video.coverFile?.accessUrl || '/static/assets/modules/genealogy/transparent/empty-panel-frame.png'"
mode="aspectFill"
/>
<text class="video-card__play-copy">点击播放</text>
</button>
<text class="video-card__title">{{ video.title }}</text>
<text v-if="video.description" class="video-card__copy">{{
video.description
@@ -152,6 +167,7 @@
@click="openEditVideo(video)"
/>
<AppButton
v-if="video.canDelete"
compact
type="secondary"
:disabled="deletingVideoId === video.id"
@@ -159,6 +175,11 @@
@click="requestDeleteVideo(video)"
/>
</view>
<view class="video-card__actions">
<AppButton compact type="secondary" label="沉浸观看" @click="openVerticalViewer(video)" />
<AppButton compact type="secondary" :disabled="videoActionKey === `like-${video.id}`" :label="video.likedByCurrentUser ? `已赞 ${video.likeCount || 0}` : `点赞 ${video.likeCount || 0}`" @click="toggleVideoLike(video)" />
<AppButton compact type="secondary" :disabled="videoActionKey === `comments-${video.id}`" label="查看评论" @click="openVideoComments(video)" />
</view>
</view>
</view>
<text v-if="videoActionError" class="field-error">{{
@@ -174,31 +195,125 @@
<AppDialog
:visible="deleteConfirmVisible"
eyebrow="删除确认"
title="删除这段家族视频?"
:message="deleteTarget ? `《${deleteTarget.title}》删除后不可恢复。` : ''"
:confirm-text="deletingVideoId ? '正在' : '确认删除'"
title="这段家族视频移至回收站"
:message="deleteTarget ? `《${deleteTarget.title}》移入回收站后不再展示,管理员可在保留期内恢复。` : ''"
:confirm-text="deletingVideoId ? '正在' : '移至回收站'"
cancel-text="保留视频"
show-cancel
:close-on-mask="false"
@confirm="confirmDeleteVideo"
@cancel="deleteConfirmVisible = false"
/>
<AppDialog
:visible="discardVisible"
eyebrow="未保存修改"
title="放弃视频修改?"
message="当前修改还没有保存。"
confirm-text="确认放弃"
cancel-text="继续编辑"
show-cancel
:close-on-mask="false"
@confirm="confirmDiscard"
@cancel="cancelDiscard"
/>
<AppDialog
:visible="Boolean(commentTarget)"
eyebrow="视频评论"
:title="commentTarget?.title || '家族视频'"
:confirm-text="videoActionKey === 'send-comment' ? '正在发送' : replyTarget ? '发送回复' : '发表评论'"
cancel-text="关闭"
show-cancel
:close-on-mask="false"
@confirm="sendVideoComment"
@cancel="closeVideoComments"
>
<view v-if="replyTarget" class="video-reply-target">
<text>正在回复 {{ replyTarget.author }}</text>
<button class="video-comment-action" @click="cancelVideoReply">取消回复</button>
</view>
<view v-if="videoComments.length" class="video-comments">
<view
v-for="comment in videoComments"
:key="comment.id"
class="video-comment"
:class="{ 'video-comment--reply': comment.level === 'reply' }"
>
<view class="video-comment__heading">
<text>{{ comment.author }}</text>
<text>{{ comment.time }}</text>
</view>
<text v-if="comment.parentAuthor" class="video-comment__context"
>回复 {{ comment.parentAuthor }}</text
>
<text class="video-comment__content">{{ comment.content }}</text>
<view v-if="!comment.userDeleted || comment.canDelete" class="video-comment__actions">
<button
v-if="!comment.userDeleted && comment.level === 'root'"
class="video-comment-action"
@click="startVideoReply(comment)"
>
回复
</button>
<button
v-if="comment.canDelete"
class="video-comment-action video-comment-action--danger"
@click="requestDeleteVideoComment(comment)"
>
删除
</button>
</view>
</view>
</view>
<text v-else class="video-comments__empty">还没有评论可以先说说你的看法</text>
<text v-if="videoCommentError" class="field-error">{{ videoCommentError }}</text>
<textarea
v-model="videoCommentText"
maxlength="1000"
:placeholder="videoCommentPlaceholder"
class="video-comment-input"
@input="videoCommentError = ''"
/>
</AppDialog>
<VerticalVideoViewer
:visible="verticalViewerVisible"
:videos="videos"
:initial-video-id="verticalViewerInitialId"
title="家族视频"
:action-busy="Boolean(videoActionKey)"
@close="closeVerticalViewer"
@like="toggleVideoLike"
@comments="openVerticalViewerComments"
/>
<AppDialog
:visible="Boolean(commentDeleteTarget)"
:close-on-mask="false"
eyebrow="评论管理"
title="删除这条评论?"
message="删除后将按服务端规则保留占位或移除内容。"
:confirm-text="videoActionKey === 'delete-comment' ? '正在删除' : '确认删除'"
cancel-text="保留评论"
show-cancel
@confirm="deleteVideoComment"
@cancel="closeVideoCommentDelete"
/>
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import VerticalVideoViewer from "@/components/family/VerticalVideoViewer.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { familyMediaApi } from "@/services/api/family-media-service.js";
import { genealogyCapabilityApi } from "@/services/api/genealogy-capability-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import {
isImagePickCancelled,
@@ -207,12 +322,15 @@ import {
pickAndUploadVideo,
} from "@/utils/media-upload.js";
import { createNonIdempotentWriteGuard } from "@/utils/non-idempotent-write-guard.js";
import { goBack, returnTo } from "@/utils/navigation/gateway.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { goBack, handleBackPress, openPage, returnTo } from "@/utils/navigation/gateway.js";
const genealogyId = ref("");
const pageState = ref("list");
const videoListState = ref("loading");
const videos = ref([]);
const verticalViewerVisible = ref(false);
const verticalViewerInitialId = ref("");
const receipt = ref(null);
const coverReceipt = ref(null);
const uploading = ref(false);
@@ -223,20 +341,222 @@ const coverUploadError = ref("");
const submitError = ref("");
const form = reactive({ videoTitle: "", videoDesc: "" });
const editingVideo = ref(null);
const formBaseline = ref("");
const discardVisible = ref(false);
const videoListRequestController = createRequestController();
const videoDetailRequestController = createRequestController();
const videoUploadRequestController = createRequestController();
const coverUploadRequestController = createRequestController();
const videoSaveRequestController = createRequestController();
const videoDeletionRequestController = createRequestController();
const videoCommentListController = createRequestController();
const videoCommentWriteController = createRequestController();
const videoCommentDeleteController = createRequestController();
const videoCreateGuard = createNonIdempotentWriteGuard();
const videoCommentCreateGuard = createNonIdempotentWriteGuard();
const deleteTarget = ref(null);
const deleteConfirmVisible = ref(false);
const deletingVideoId = ref("");
const videoActionError = ref("");
const videoActionKey = ref("");
const commentTarget = ref(null);
const videoComments = ref([]);
const videoCommentText = ref("");
const videoCommentError = ref("");
const replyTarget = ref(null);
const commentDeleteTarget = ref(null);
let pageActive = true;
const discardConfirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
const videoCommentPlaceholder = computed(() =>
replyTarget.value ? `回复 ${replyTarget.value.author}` : "说说你的看法",
);
const openPlatformVideos = () => openPage("F11", { placement: "video_center" }, "F10");
const openVerticalViewer = (video) => {
if (!video || videoActionKey.value) return;
verticalViewerInitialId.value = String(video.id);
verticalViewerVisible.value = true;
};
const closeVerticalViewer = () => {
verticalViewerVisible.value = false;
};
const openVerticalViewerComments = (video) => {
closeVerticalViewer();
return openVideoComments(video);
};
const toggleVideoLike = async (video) => {
if (videoActionKey.value) return;
videoActionKey.value = `like-${video.id}`;
videoActionError.value = "";
const liked = !video.likedByCurrentUser;
try { await genealogyCapabilityApi.setVideoLike(genealogyId.value, video.id, liked); video.likedByCurrentUser = liked; video.likeCount = Math.max(0, Number(video.likeCount || 0) + (liked ? 1 : -1)); }
catch (error) { videoActionError.value = getRequestErrorMessage(error, "点赞失败,请稍后重试。"); }
finally { videoActionKey.value = ""; }
};
const loadVideoCommentThread = async (video) => {
const rootComments = await genealogyCapabilityApi.getVideoComments(
genealogyId.value,
video.id,
{ requestController: videoCommentListController },
);
const thread = [];
for (const rootComment of rootComments) {
thread.push(rootComment);
if (rootComment.replyCount <= 0) continue;
const replies = await genealogyCapabilityApi.getVideoCommentReplies(
genealogyId.value,
video.id,
rootComment.id,
{ requestController: videoCommentListController },
);
thread.push(...replies.map((reply) => ({
...reply,
parentAuthor: rootComment.author,
})));
}
return thread;
};
const openVideoComments = async (video) => {
if (videoActionKey.value) return;
videoActionKey.value = `comments-${video.id}`;
videoActionError.value = "";
videoCommentListController.abort();
try {
videoComments.value = await loadVideoCommentThread(video);
commentTarget.value = video;
videoCommentText.value = "";
videoCommentError.value = "";
replyTarget.value = null;
}
catch (error) { videoActionError.value = getRequestErrorMessage(error, "评论暂时无法读取。"); }
finally { videoActionKey.value = ""; }
};
const closeVideoComments = () => {
if (videoActionKey.value === "send-comment" || videoActionKey.value === "delete-comment") return;
commentTarget.value = null;
videoComments.value = [];
videoCommentText.value = "";
videoCommentError.value = "";
replyTarget.value = null;
commentDeleteTarget.value = null;
};
const startVideoReply = (comment) => {
if (!comment || comment.level !== "root" || comment.userDeleted || videoActionKey.value) return;
replyTarget.value = comment;
videoCommentText.value = "";
videoCommentError.value = "";
};
const cancelVideoReply = () => {
if (videoActionKey.value) return;
replyTarget.value = null;
videoCommentText.value = "";
videoCommentError.value = "";
};
const sendVideoComment = async () => {
if (!commentTarget.value || !videoCommentText.value.trim() || videoActionKey.value) return;
const commentPayload = {
genealogyId: genealogyId.value,
videoId: commentTarget.value.id,
commentContent: videoCommentText.value.trim(),
parentCommentId: replyTarget.value?.id || null,
};
const commentAttempt = videoCommentCreateGuard.begin(commentPayload);
if (commentAttempt === null) {
videoCommentError.value = "上次评论发送结果待确认,请先重新打开评论列表,避免重复发布。";
return;
}
videoActionKey.value = "send-comment";
videoCommentError.value = "";
try {
const comment = await genealogyCapabilityApi.createVideoComment(
commentPayload.genealogyId,
commentPayload.videoId,
commentPayload.commentContent,
commentPayload.parentCommentId,
{ requestController: videoCommentWriteController },
);
if (!pageActive) return;
if (commentPayload.parentCommentId) {
const rootComment = videoComments.value.find(
(currentComment) => currentComment.id === commentPayload.parentCommentId,
);
const insertedReply = {
...comment,
parentAuthor: rootComment?.author || replyTarget.value?.author || "",
};
let insertionIndex = videoComments.value.findIndex(
(currentComment) => currentComment.id === commentPayload.parentCommentId,
);
for (let index = insertionIndex + 1; index < videoComments.value.length; index += 1) {
if (videoComments.value[index].parentCommentId !== commentPayload.parentCommentId) break;
insertionIndex = index;
}
videoComments.value.splice(insertionIndex + 1, 0, insertedReply);
if (rootComment) rootComment.replyCount += 1;
} else {
videoComments.value.push(comment);
}
videoCommentText.value = "";
replyTarget.value = null;
}
catch (error) {
const isOutcomeUnknown = videoCommentCreateGuard.recordFailure(commentAttempt, error);
if (!pageActive) return;
videoCommentError.value = isOutcomeUnknown
? "评论发送结果待确认,请重新打开评论列表检查,避免重复发布。"
: getRequestErrorMessage(error, "评论发送失败,请稍后重试。");
}
finally { if (pageActive) videoActionKey.value = ""; }
};
const requestDeleteVideoComment = (comment) => {
if (!comment?.canDelete || videoActionKey.value) return;
videoCommentError.value = "";
commentDeleteTarget.value = comment;
};
const closeVideoCommentDelete = () => {
if (videoActionKey.value !== "delete-comment") commentDeleteTarget.value = null;
};
const deleteVideoComment = async () => {
if (!commentTarget.value || !commentDeleteTarget.value?.canDelete || videoActionKey.value) return;
const target = commentDeleteTarget.value;
videoActionKey.value = "delete-comment";
videoCommentError.value = "";
try {
await genealogyCapabilityApi.deleteVideoComment(
genealogyId.value,
commentTarget.value.id,
target.id,
{ requestController: videoCommentDeleteController },
);
if (!pageActive) return;
videoComments.value = await loadVideoCommentThread(commentTarget.value);
if (replyTarget.value?.id === target.id) replyTarget.value = null;
commentDeleteTarget.value = null;
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
videoCommentError.value = getRequestErrorMessage(error, "评论删除失败,请稍后重试。");
commentDeleteTarget.value = null;
} finally {
if (pageActive) videoActionKey.value = "";
}
};
const isEdit = computed(() => Boolean(editingVideo.value));
const formSnapshot = computed(() => JSON.stringify({
videoTitle: form.videoTitle,
videoDesc: form.videoDesc,
videoOssId: receipt.value?.ossId || null,
coverOssId: coverReceipt.value?.ossId || null,
editingVideoId: editingVideo.value?.id || null,
}));
const isDirty = computed(() =>
pageState.value === "form" &&
Boolean(formBaseline.value) &&
formSnapshot.value !== formBaseline.value,
);
const stateCopy = computed(() =>
pageState.value === "success"
? {
@@ -264,6 +584,10 @@ onUnload(() => {
coverUploadRequestController.abort();
videoSaveRequestController.abort();
videoDeletionRequestController.abort();
videoCommentListController.abort();
videoCommentWriteController.abort();
videoCommentDeleteController.abort();
discardConfirmation.dispose();
});
const loadVideos = async () => {
@@ -292,6 +616,7 @@ const openPublishForm = () => {
coverUploadError.value = "";
submitError.value = "";
pageState.value = "form";
formBaseline.value = formSnapshot.value;
};
const openEditVideo = async (video) => {
if (
@@ -335,6 +660,7 @@ const openEditVideo = async (video) => {
coverUploadError.value = "";
submitError.value = "";
pageState.value = "form";
formBaseline.value = formSnapshot.value;
} catch (error) {
if (!isRequestCancelled(error)) {
videoActionError.value = "视频信息不完整,暂未保存修改,以免覆盖原内容。";
@@ -418,6 +744,11 @@ const selectCover = async () => {
if (pageActive) coverUploading.value = false;
}
};
const clearCover = () => {
if (coverUploading.value || submitting.value) return;
coverReceipt.value = null;
coverUploadError.value = "";
};
const submitVideo = async () => {
if (
uploading.value ||
@@ -439,7 +770,7 @@ const submitVideo = async () => {
videoTitle,
videoDesc: form.videoDesc.trim(),
videoOssId: receipt.value.ossId,
...(coverReceipt.value ? { coverOssId: coverReceipt.value.ossId } : {}),
coverOssId: coverReceipt.value?.ossId ?? null,
...(editingVideo.value
? {
durationSeconds: editingVideo.value.durationSeconds,
@@ -493,7 +824,50 @@ const returnToFamily = () =>
hasValidContext.value
? returnTo("F01", { genealogyId: genealogyId.value })
: goBack();
const returnToVideoList = () => {
pageState.value = "list";
formBaseline.value = "";
receipt.value = null;
coverReceipt.value = null;
editingVideo.value = null;
return true;
};
const requestBack = async () => {
if (verticalViewerVisible.value) {
closeVerticalViewer();
return true;
}
if (commentDeleteTarget.value) {
closeVideoCommentDelete();
return true;
}
if (commentTarget.value) {
closeVideoComments();
return true;
}
if (deleteConfirmVisible.value) {
deleteConfirmVisible.value = false;
return true;
}
if (discardVisible.value) {
cancelDiscard();
return true;
}
if (uploading.value || coverUploading.value || submitting.value || deletingVideoId.value) {
return true;
}
if (pageState.value === "form") {
if (isDirty.value && !(await discardConfirmation.request())) return false;
return returnToVideoList();
}
if (pageState.value === "form-loading") {
videoDetailRequestController.abort();
return returnToVideoList();
}
return returnToFamily();
};
const handleStateAction = () => returnToFamily();
onBackPress((event) => handleBackPress(event, requestBack));
</script>
<style scoped lang="scss">
@@ -510,15 +884,19 @@ const handleStateAction = () => returnToFamily();
}
.page-content {
flex: 1;
padding: 18rpx 24rpx 72rpx;
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
}
.video-panel,
.video-state-card {
box-sizing: border-box;
@include adaptive-family-content;
background-color: rgba($paper, 0.82);
}
.video-list-panel {
@include adaptive-family-content;
box-sizing: border-box;
padding: 28rpx 24rpx;
background-color: rgba($paper, 0.82);
}
.video-panel {
padding: 30rpx;
@@ -563,7 +941,7 @@ const handleStateAction = () => returnToFamily();
font-size: clamp(15px, 24rpx, 18px);
}
.video-field input {
min-height: 76rpx;
min-height: 80rpx;
padding: 0 18rpx;
}
.video-field textarea {
@@ -580,6 +958,7 @@ const handleStateAction = () => returnToFamily();
width: 100%;
}
.upload-button {
min-height: 80rpx;
margin: 0;
padding: 0 26rpx;
border: 1rpx solid #b78a42;
@@ -587,12 +966,24 @@ const handleStateAction = () => returnToFamily();
background: #fffaf0;
color: #805723;
font-size: clamp(14px, 23rpx, 17px);
line-height: 64rpx;
line-height: 78rpx;
}
.upload-receipt {
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
}
.remove-cover-button {
justify-self: start;
min-height: 72rpx;
margin: 0;
padding: 0 18rpx;
border: 1rpx solid rgba($brand-red, 0.38);
border-radius: 8rpx;
background: transparent;
color: $brand-red;
font-size: clamp(13px, 21rpx, 16px);
}
.remove-cover-button::after { border: 0; }
.required-mark,
.field-error {
color: $brand-red;
@@ -621,14 +1012,38 @@ const handleStateAction = () => returnToFamily();
padding: 22rpx;
border: 1rpx solid rgba(128, 89, 49, 0.28);
border-radius: 12rpx;
background: rgba(255, 252, 245, 0.78);
background: rgba($paper, 0.9);
}
.video-card__player {
.video-card__cover-action {
position: relative;
display: block;
width: 100%;
min-height: 340rpx;
height: 340rpx;
overflow: hidden;
margin: 0;
padding: 0;
border: 0;
border-radius: 8rpx;
background: #161616;
line-height: 1;
}
.video-card__cover-action::after {
border: 0;
}
.video-card__cover {
width: 100%;
height: 100%;
}
.video-card__play-copy {
position: absolute;
right: 20rpx;
bottom: 18rpx;
padding: 10rpx 16rpx;
border-radius: 8rpx;
background: rgba(22, 22, 22, 0.78);
color: #fff9ed;
font-size: clamp(13px, 21rpx, 16px);
font-weight: 700;
}
.video-card__title,
.video-card__copy,
@@ -640,12 +1055,14 @@ const handleStateAction = () => returnToFamily();
color: $ink;
font-size: clamp(17px, 28rpx, 21px);
font-weight: 700;
overflow-wrap: anywhere;
}
.video-card__copy {
margin-top: 8rpx;
color: $ink-muted;
font-size: clamp(14px, 23rpx, 17px);
line-height: 1.55;
overflow-wrap: anywhere;
}
.video-card__meta {
margin-top: 10rpx;
@@ -657,4 +1074,101 @@ const handleStateAction = () => returnToFamily();
justify-content: flex-end;
margin-top: 14rpx;
}
.video-reply-target,
.video-comment__heading,
.video-comment__actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14rpx;
}
.video-reply-target {
width: 100%;
margin-top: 18rpx;
padding: 14rpx 16rpx;
box-sizing: border-box;
border-radius: 8rpx;
background: rgba($gold, 0.1);
color: $ink;
font-size: clamp(13px, 21rpx, 16px);
}
.video-comments {
width: 100%;
max-height: 440rpx;
margin-top: 16rpx;
overflow-y: auto;
text-align: left;
}
.video-comment {
padding: 18rpx 4rpx;
border-bottom: 1rpx solid rgba($gold, 0.2);
}
.video-comment--reply {
margin-left: 32rpx;
padding-left: 18rpx;
border-left: 3rpx solid rgba($gold, 0.34);
}
.video-comment__heading text:first-child {
color: $ink;
font-size: clamp(14px, 23rpx, 17px);
font-weight: 700;
}
.video-comment__heading text:last-child,
.video-comment__context {
color: $ink-muted;
font-size: clamp(12px, 20rpx, 15px);
}
.video-comment__context,
.video-comment__content,
.video-comments__empty {
display: block;
}
.video-comment__context {
margin-top: 6rpx;
}
.video-comment__content {
margin-top: 8rpx;
color: $ink;
font-size: clamp(14px, 22rpx, 17px);
line-height: 1.55;
overflow-wrap: anywhere;
}
.video-comment__actions {
justify-content: flex-end;
margin-top: 8rpx;
}
.video-comment-action {
min-width: 88rpx;
min-height: 58rpx;
margin: 0;
padding: 0 14rpx;
border: 0;
background: transparent;
color: #805723;
font-size: clamp(13px, 21rpx, 16px);
line-height: 58rpx;
}
.video-comment-action::after {
border: 0;
}
.video-comment-action--danger {
color: $brand-red;
}
.video-comments__empty {
width: 100%;
margin-top: 20rpx;
color: $ink-muted;
font-size: clamp(14px, 22rpx, 17px);
}
.video-comment-input {
width: 100%;
min-height: 120rpx;
margin-top: 18rpx;
padding: 16rpx;
box-sizing: border-box;
border: 1rpx solid rgba($gold, 0.38);
border-radius: 10rpx;
color: $ink;
text-align: left;
}
</style>