| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818 |
- <template>
- <div class="review-appeal-container">
- <!-- 顶部统计数据 -->
- <div class="statistics-section">
- <div class="statistics-cards">
- <div class="stat-card">
- <div class="stat-label">评价总数</div>
- <div class="stat-value">
- {{ total }}
- </div>
- </div>
- <div class="stat-card">
- <div class="stat-label">新增差评数</div>
- <div class="stat-value">
- {{ statistics.badTextReviews }}
- </div>
- </div>
- <div class="stat-card">
- <div class="stat-label">新增好评数</div>
- <div class="stat-value">
- {{ statistics.badImageReviews }}
- </div>
- </div>
- <div class="stat-card">
- <div class="stat-label">差评中评数</div>
- <div class="stat-value">
- {{ statistics.neutralReviews }}
- </div>
- </div>
- <div class="stat-card">
- <div class="stat-label">评论回复率</div>
- <div class="stat-value highlight">
- {{ statistics.abnormalRate }}
- </div>
- </div>
- </div>
- </div>
- <!-- 评价列表区域 -->
- <div class="review-list-section">
- <div class="section-header">
- <div class="section-title">
- 评价列表 <el-button type="primary" style="float: right" @click="goToAppealHistory"> 申诉历史 </el-button>
- </div>
- <div class="time-filter">
- <span>评论时间:</span>
- <el-select v-model="timeFilter" placeholder="请选择" class="time-filter-select" @change="handleTimeFilterChange">
- <el-option label="全部" value="" />
- <el-option label="30天" value="30" />
- </el-select>
- </div>
- <el-tabs v-model="activeTab" @tab-click="handleTabClick">
- <el-tab-pane :label="`全部 (${tabCounts.all})`" name="0" />
- <el-tab-pane :label="`待回复差评 (${tabCounts.noReplyCount})`" name="pending" />
- <el-tab-pane :label="`差评 (${tabCounts.bad})`" name="3" />
- <el-tab-pane :label="`好评 (${tabCounts.good})`" name="1" />
- <el-tab-pane :label="`中评 (${tabCounts.neutral})`" name="2" />
- </el-tabs>
- </div>
- <!-- 评论卡片列表 -->
- <div v-if="reviewList.length > 0" class="review-cards">
- <div v-for="review in reviewList" :key="review.id" class="review-card">
- <div class="review-header">
- <div class="user-info">
- <el-avatar :src="review.userAvatar" :size="40">
- <el-icon><User /></el-icon>
- </el-avatar>
- <div class="user-details">
- <div class="user-name">
- {{ review.isAnonymous == 1 || !review.userName ? "匿名用户" : review.userName }}
- </div>
- <el-rate v-model="review.score" disabled />
- </div>
- </div>
- <div class="review-time">
- {{ (review.createdTime || "").replace(/-/g, "/") }}
- </div>
- </div>
- <div class="review-content">
- {{ review.commentContent }}
- </div>
- <div v-for="(itm, idx) in review.storeComment" :key="idx">
- <div class="sjhf">商家回复: {{ itm.commentContent }}</div>
- </div>
- <div v-if="review.media && review.media.length > 0" class="review-media">
- <template v-for="(m, index) in review.media" :key="index">
- <el-image
- v-if="m.type === 'image'"
- :src="m.url"
- :preview-src-list="review.images"
- :initial-index="getMediaImageIndex(review.media, Number(index))"
- fit="cover"
- class="media-item review-image"
- />
- <div v-else class="media-item media-video-wrap" @click="openVideoPreview(m.url)">
- <video
- :src="m.url"
- class="video-thumb"
- preload="metadata"
- muted
- playsinline
- @loadeddata="onVideoLoadedData($event)"
- />
- <div class="video-mask">
- <el-icon class="video-play-icon">
- <VideoPlay />
- </el-icon>
- </div>
- </div>
- </template>
- </div>
- <div class="review-footer">
- <el-button type="warning" link v-if="review.appealStatus == 0"> 审核中 </el-button>
- <el-button
- type="danger"
- link
- v-if="review.appealStatus != 0"
- :disabled="review.appealFlag == 1 && review.appealStatus != 1 ? true : false"
- @click="delReviewAppeal(review)"
- >
- 申诉删除
- </el-button>
- <el-button type="primary" link @click="openReplayDialog(review)" v-if="!hasStoreReply(review)"> 回复 </el-button>
- </div>
- </div>
- </div>
- <!-- 空状态 -->
- <el-empty v-else description="暂无评论数据" />
- <!-- 分页 -->
- <div v-if="total > 0" class="pagination-wrapper">
- <el-pagination
- v-model:current-page="pageNum"
- v-model:page-size="pageSize"
- :page-sizes="[10, 20, 30, 50]"
- :total="total"
- layout="total, sizes, prev, pager, next, jumper"
- background
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- />
- </div>
- </div>
- <!-- 申诉提交对话框 -->
- <el-dialog v-model="appealDialogVisible" title="申诉删除" width="600px" @close="closeAppealDialog">
- <el-form ref="appealFormRef" :model="appealFormData" :rules="appealFormRules" label-width="100px">
- <el-form-item label="申诉原因" prop="reason">
- <el-input
- v-model="appealFormData.reason"
- type="textarea"
- :rows="4"
- placeholder="请输入申诉原因"
- maxlength="300"
- show-word-limit
- />
- </el-form-item>
- <el-form-item label="申诉凭证" prop="images">
- <el-upload
- v-model:file-list="appealFormData.fileList"
- list-type="picture-card"
- :limit="6"
- :http-request="handleUpload"
- :on-preview="handlePreview"
- :on-remove="handleRemove"
- :on-success="handleUploadSuccess"
- accept="image/*"
- >
- <el-icon><Plus /></el-icon>
- </el-upload>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="closeAppealDialog"> 取消 </el-button>
- <el-button type="primary" @click="submitAppeal"> 确定 </el-button>
- </template>
- </el-dialog>
- <!-- 回复对话框 -->
- <el-dialog v-model="replyDialogVisible" title="回复评价" width="600px" @close="closeReplyDialog">
- <el-form ref="replyFormRef" :model="replyFormData" :rules="replyFormRules" label-width="100px">
- <el-form-item label="回复内容" prop="content">
- <el-input
- v-model="replyFormData.content"
- type="textarea"
- :rows="6"
- placeholder="请输入回复内容"
- maxlength="300"
- show-word-limit
- />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="closeReplyDialog"> 取消 </el-button>
- <el-button type="primary" @click="submitReply"> 提交回复 </el-button>
- </template>
- </el-dialog>
- <!-- 视频预览弹窗 -->
- <el-dialog
- v-model="videoPreviewVisible"
- title="视频预览"
- width="80%"
- max-width="720px"
- destroy-on-close
- align-center
- @closed="closeVideoPreview"
- >
- <video v-if="videoPreviewUrl" :src="videoPreviewUrl" controls autoplay style="width: 100%; max-height: 70vh" />
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts" name="reviewAppeal">
- import { ref, reactive, onMounted } from "vue";
- import { useRouter } from "vue-router";
- import { ElMessage } from "element-plus";
- import { User, Plus, VideoPlay } from "@element-plus/icons-vue";
- import type { FormInstance, FormRules, UploadUserFile } from "element-plus";
- import { getList, addAppealNew, saveComment2, uploadImg, getRatingCount } from "@/api/modules/newLoginApi";
- import { localGet } from "@/utils";
- import { useUserStore } from "@/stores/modules/user";
- const router = useRouter();
- const userStore = useUserStore();
- // 店铺名称
- const storeName = ref("重庆老火锅");
- // 统计数据
- const statistics = reactive({
- totalReviews: 0,
- badTextReviews: 0,
- badImageReviews: 0,
- neutralReviews: 0,
- abnormalRate: "0%"
- });
- // 标签页计数(由 getRatingCount 接口填充)
- const tabCounts = reactive({
- all: 0,
- pending: 0,
- bad: 0,
- good: 0,
- neutral: 0,
- noReplyCount: 0
- });
- // 当前激活的标签
- const activeTab = ref("0");
- // 评论时间筛选
- const timeFilter = ref("");
- // 评论列表
- const reviewList = ref<any[]>([]);
- // 分页参数
- const pageNum = ref(1);
- const pageSize = ref(10);
- const total = ref(0);
- // 申诉提交对话框
- const appealDialogVisible = ref(false);
- const appealFormRef = ref<FormInstance>();
- const currentReviewId = ref("");
- const appealFormData = reactive({
- reason: "",
- images: [] as string[],
- files: [] as File[], // 保存原始的 File 对象
- fileList: [] as UploadUserFile[]
- });
- const hasStoreReply = (review: any) => {
- return review?.storeComment && review.storeComment.length > 0;
- };
- /** 在 media 列表中,当前项在「仅图片」列表中的下标,用于 el-image initial-index */
- const getMediaImageIndex = (media: { url: string; type: string }[], currentIndex: number): number => {
- let idx = 0;
- for (let i = 0; i < currentIndex; i++) if (media[i].type === "image") idx++;
- return idx;
- };
- const videoPreviewVisible = ref(false);
- const videoPreviewUrl = ref("");
- const openVideoPreview = (url: string) => {
- videoPreviewUrl.value = url;
- videoPreviewVisible.value = true;
- };
- const closeVideoPreview = () => {
- videoPreviewUrl.value = "";
- videoPreviewVisible.value = false;
- };
- const onVideoLoadedData = (e: Event) => {
- const video = e.target as HTMLVideoElement;
- if (video) {
- video.currentTime = 0;
- video.pause();
- }
- };
- const appealFormRules = reactive<FormRules>({
- reason: [{ required: true, message: "请输入申诉原因", trigger: "blur" }],
- images: [{ required: true, message: "请上传申诉凭证", trigger: "blur" }]
- });
- // 回复对话框
- const replyDialogVisible = ref(false);
- const replyFormRef = ref<FormInstance>();
- const currentReplyReview = ref<any>(null);
- const replyFormData = reactive({
- content: ""
- });
- const replyFormRules = reactive<FormRules>({
- content: [{ required: true, message: "请输入回复内容", trigger: "blur" }]
- });
- // 标签页切换
- const handleTabClick = (tab: any) => {
- // 获取点击的标签页的 name 值
- const tabName = tab.paneName || tab.props?.name || activeTab.value;
- loadReviewList(tabName, true);
- };
- // 跳转到申诉历史
- const goToAppealHistory = () => {
- router.push("/dynamicManagement/reviewAppealHistory");
- };
- // 评论时间筛选变化
- const handleTimeFilterChange = () => {
- loadReviewList(activeTab.value, true);
- };
- // 与商家端一致:兼容 code 0 / 200,从 getRatingCount 取 totalCount、goodCount、midCount、badCount、pending、replyRate
- const loadStatistics = async () => {
- try {
- const ratingCountRes: any = await getRatingCount({
- businessId: localGet("createdId"),
- businessType: 1
- });
- const isOk = ratingCountRes?.code === 200 || ratingCountRes?.code === 0;
- const ratingCount = isOk ? (ratingCountRes.data ?? ratingCountRes) : null;
- const rc = ratingCount as Record<string, number | undefined> | null;
- tabCounts.all = rc?.totalCount ?? 0;
- tabCounts.good = rc?.goodCount ?? 0;
- tabCounts.neutral = rc?.midCount ?? 0;
- tabCounts.bad = rc?.badCount ?? 0;
- tabCounts.pending = rc?.pending ?? 0;
- tabCounts.noReplyCount = rc?.noReplyCount ?? 0;
- statistics.totalReviews = tabCounts.all;
- statistics.badTextReviews = tabCounts.bad;
- statistics.badImageReviews = tabCounts.good;
- statistics.neutralReviews = tabCounts.neutral;
- if (rc?.replyRate != null) {
- statistics.abnormalRate = Number(rc.replyRate).toFixed(2) + "%";
- } else {
- const totalCount = tabCounts.all;
- const repliedCount = rc?.repliedCount ?? Math.max(0, totalCount - tabCounts.pending);
- if (totalCount > 0) {
- statistics.abnormalRate = ((repliedCount / totalCount) * 100).toFixed(2) + "%";
- } else {
- statistics.abnormalRate = "0%";
- }
- }
- } catch (error) {
- console.error("获取统计数据失败", error);
- }
- };
- const VIDEO_EXT = [".mp4", ".mov", ".avi", ".wmv", ".flv", ".mkv", ".webm", ".m4v"];
- const isVideoUrl = (url: string) => {
- const lower = (url || "").toLowerCase();
- return VIDEO_EXT.some(ext => lower.includes(ext));
- };
- // 与商家端一致:将 commonRating/getList 返回的单条转为列表展示格式;imageUrls 中区分图片与视频,视频用首帧展示
- function transformRatingData(item: any): any {
- if (!item || typeof item !== "object" || item.id == null) return null;
- let urls: string[] = [];
- if (item.imageUrls) {
- const raw =
- typeof item.imageUrls === "string" && item.imageUrls.trim()
- ? item.imageUrls.split(",").filter((u: string) => u?.trim())
- : Array.isArray(item.imageUrls)
- ? item.imageUrls
- : [];
- urls = raw.map((u: string) => (u && u.trim()) || "").filter(Boolean);
- }
- const media = urls.map(url => ({ url, type: isVideoUrl(url) ? ("video" as const) : ("image" as const) }));
- const images = media.filter(m => m.type === "image").map(m => m.url);
- let storeComment: { commentContent: string; createdTime?: string }[] = [];
- if (item.childCommonComments) {
- const list = Array.isArray(item.childCommonComments)
- ? item.childCommonComments
- : Object.values(item.childCommonComments || {});
- storeComment = list
- .filter((c: any) => c && Number(c.commentType) === 2)
- .map((c: any) => ({
- commentContent: c.content || c.commentContent || "",
- createdTime: c.createdTime || ""
- }));
- }
- return {
- id: item.id,
- commentContent: item.content ?? item.commentContent ?? "",
- score: item.score ?? 0,
- images,
- media,
- userAvatar: item.userImage ?? item.userAvatar ?? "",
- userName: item.userName ?? (item.isAnonymous === 1 ? "匿名用户" : "用户"),
- createdTime: item.createdTime ?? "",
- isAnonymous: item.isAnonymous ?? 0,
- storeComment,
- appealStatus: item.appealStatus !== undefined && item.appealStatus !== null ? item.appealStatus : null,
- appealFlag: item.appealFlag !== undefined ? item.appealFlag : item.appealStatus != null ? 1 : 0
- };
- }
- // 加载评论列表(与商家端 getRatingList 参数一致:pageNum、pageSize、businessType、businessId、userId、searchScore、days、replyStatus)
- const loadReviewList = async (commentLevel?: string | number, resetPage = false) => {
- try {
- if (resetPage) pageNum.value = 1;
- const level = commentLevel !== undefined ? commentLevel : activeTab.value;
- const isPending = level === "pending";
- const params: Record<string, any> = {
- pageNum: pageNum.value,
- pageSize: pageSize.value,
- businessType: 1,
- businessId: localGet("createdId"),
- userId: userStore.userInfo?.userId || userStore.userInfo?.id || ""
- // replyStatus: 2
- };
- const levelNum = isPending ? 3 : Number(level);
- if (levelNum !== 0) params.searchScore = levelNum;
- if (timeFilter.value) params.days = timeFilter.value;
- if (isPending) params.replyStatus = 2;
- const res: any = await getList(params);
- const isOk = res?.code === 200 || res?.code === 0;
- const data = res?.data ?? res;
- const records = data?.records ?? (Array.isArray(data) ? data : []);
- const totalCount = data?.total ?? res?.total ?? 0;
- if (isOk && (records.length > 0 || totalCount >= 0)) {
- reviewList.value = records.map((item: any) => transformRatingData(item)).filter(Boolean);
- total.value = totalCount;
- } else {
- reviewList.value = [];
- total.value = 0;
- if (!isOk && res?.msg) ElMessage.error(res.msg);
- }
- } catch (error: any) {
- console.error("获取评论列表失败", error);
- reviewList.value = [];
- total.value = 0;
- ElMessage.error(error?.message || error?.msg || "获取评论列表失败");
- }
- };
- // 分页大小变化
- const handleSizeChange = (val: number) => {
- pageSize.value = val;
- pageNum.value = 1;
- loadReviewList();
- };
- // 页码变化
- const handleCurrentChange = (val: number) => {
- pageNum.value = val;
- loadReviewList();
- };
- // 申诉删除
- const delReviewAppeal = (review: any) => {
- currentReviewId.value = review.id;
- appealDialogVisible.value = true;
- };
- //回复评论
- // 打开回复对话框
- const openReplayDialog = (review: any) => {
- currentReplyReview.value = review;
- replyDialogVisible.value = true;
- };
- // 关闭回复对话框
- const closeReplyDialog = () => {
- replyDialogVisible.value = false;
- replyFormRef.value?.resetFields();
- Object.assign(replyFormData, {
- content: ""
- });
- currentReplyReview.value = null;
- };
- // 提交回复(与商家端 saveComment 参数一致:businessId=店铺ID、replyId=评论ID、commentContent、storeId、userId、phoneId、businessType)
- const submitReply = async () => {
- if (!replyFormRef.value || !currentReplyReview.value) return;
- await replyFormRef.value.validate(async (valid: boolean) => {
- if (!valid) return;
- try {
- const phone = userStore.userInfo?.phone || "";
- const phoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
- const storeId = userStore.userInfo?.storeId || localGet("createdId") || "";
- const params = {
- businessId: storeId,
- businessType: "1",
- userId: String(userStore.userInfo?.userId ?? userStore.userInfo?.id ?? ""),
- storeId,
- commentContent: replyFormData.content,
- phoneId,
- replyId: currentReplyReview.value.id
- };
- const res: any = await saveComment2(params);
- const isOk = res?.code === 200 || res?.code === 0;
- if (isOk) {
- ElMessage.success("回复提交成功");
- closeReplyDialog();
- loadReviewList();
- loadStatistics();
- } else {
- ElMessage.error(res?.message || res?.msg || "回复提交失败");
- }
- } catch (error: any) {
- console.error("回复提交失败:", error);
- ElMessage.error(error?.message || "回复提交失败");
- }
- });
- };
- // 关闭申诉对话框
- const closeAppealDialog = () => {
- appealDialogVisible.value = false;
- appealFormRef.value?.resetFields();
- Object.assign(appealFormData, {
- reason: "",
- images: [],
- files: [],
- fileList: []
- });
- };
- // 提交申诉(与商家端 addAppealNew 一致:FormData 含 appealReason、storeId、commentId、file_0/file_1...,响应 result: 0成功 1失败 2已存在 3敏感词 4超长)
- const submitAppeal = async () => {
- if (!appealFormRef.value) return;
- await appealFormRef.value.validate(async (valid: boolean) => {
- if (!valid) return;
- try {
- const geekerUser = localGet("geeker-user");
- const storeId = geekerUser?.userInfo?.storeId ?? localGet("createdId") ?? "";
- const formData = new FormData();
- formData.append("appealReason", appealFormData.reason);
- formData.append("storeId", String(storeId));
- formData.append("commentId", String(currentReviewId.value));
- appealFormData.files.forEach((file: File, index: number) => {
- formData.append(`file_${index}`, file);
- });
- const res: any = await addAppealNew(formData);
- const result = res?.data?.result ?? res?.result;
- if (result === 0) {
- ElMessage.success("申诉提交成功");
- const review = reviewList.value.find((r: any) => String(r.id) === String(currentReviewId.value));
- if (review) review.appealFlag = 1;
- loadStatistics();
- } else if (result === 2) {
- ElMessage.warning("申诉已存在");
- } else if (result === 3) {
- ElMessage.warning("申诉理由包含敏感词,请修改后重试");
- } else if (result === 4) {
- ElMessage.warning("申诉理由超过300字限制");
- } else {
- ElMessage.error(res?.msg || "申诉提交失败");
- }
- closeAppealDialog();
- } catch (error: any) {
- ElMessage.error(error?.message || "申诉提交失败");
- }
- });
- };
- // 图片预览
- const handlePreview = (file: UploadUserFile) => {
- console.log("preview", file);
- };
- // 移除图片
- const handleRemove = (file: UploadUserFile, fileList: UploadUserFile[]) => {
- console.log("remove", file);
- // 找到对应的索引并删除
- const index = appealFormData.fileList.findIndex(f => f.uid === file.uid);
- if (index !== -1) {
- appealFormData.images.splice(index, 1);
- appealFormData.files.splice(index, 1);
- }
- appealFormData.fileList = fileList;
- };
- // 自定义上传方法
- const handleUpload = async (options: any) => {
- const { file, onSuccess, onError } = options;
- try {
- // 保存原始的 File 对象用于提交时上传
- appealFormData.files.push(file);
- // 创建本地预览URL
- const previewUrl = URL.createObjectURL(file);
- onSuccess({ url: previewUrl });
- // 将预览URL添加到images数组
- appealFormData.images.push(previewUrl);
- console.log("图片已添加:", file.name);
- } catch (error: any) {
- onError(error);
- ElMessage.error(error?.msg || "添加图片失败");
- }
- };
- // 上传成功回调
- const handleUploadSuccess = (response: any, file: any) => {
- console.log("上传成功回调:", response, file);
- };
- // 初始化
- onMounted(() => {
- loadStatistics(); // 加载统计数据
- loadReviewList(); // 加载评论列表
- });
- </script>
- <style lang="scss" scoped>
- .review-appeal-container {
- min-height: calc(100vh - 120px);
- background: #f5f7fa;
- // 统计数据区域
- .statistics-section {
- padding: 20px;
- margin-bottom: 20px;
- background: #ffffff;
- border-radius: 8px;
- .statistics-cards {
- display: flex;
- gap: 20px;
- margin-bottom: 16px;
- .stat-card {
- flex: 1;
- padding: 16px;
- text-align: center;
- background: #f5f7fa;
- border-radius: 4px;
- .stat-label {
- margin-bottom: 8px;
- font-size: 14px;
- color: #909399;
- }
- .stat-value {
- font-size: 24px;
- font-weight: 600;
- color: #303133;
- &.highlight {
- color: #f56c6c;
- }
- }
- }
- }
- }
- // 评价列表区域
- .review-list-section {
- padding: 20px;
- background: #ffffff;
- border-radius: 8px;
- .section-header {
- margin-bottom: 20px;
- .section-title {
- margin-bottom: 16px;
- font-size: 16px;
- font-weight: 600;
- color: #303133;
- }
- .time-filter {
- margin-bottom: 10px;
- font-size: 14px;
- .time-filter-select {
- width: 120px;
- }
- }
- }
- // 评论卡片
- .review-cards {
- display: flex;
- flex-direction: column;
- gap: 16px;
- .review-card {
- padding: 16px;
- border: 1px solid #e4e7ed;
- border-radius: 8px;
- .review-header {
- display: flex;
- justify-content: space-between;
- margin-bottom: 12px;
- .user-info {
- display: flex;
- gap: 12px;
- align-items: center;
- .user-details {
- .user-name {
- margin-bottom: 4px;
- font-size: 14px;
- font-weight: 600;
- color: #303133;
- }
- }
- }
- .review-time {
- font-size: 13px;
- color: #909399;
- }
- }
- .review-content {
- margin-bottom: 12px;
- font-size: 14px;
- line-height: 1.6;
- color: #606266;
- }
- .sjhf {
- padding-bottom: 10px;
- font-size: 14px;
- color: #606266;
- }
- .review-media {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-bottom: 12px;
- .media-item {
- flex-shrink: 0;
- width: 80px;
- height: 80px;
- overflow: hidden;
- border-radius: 4px;
- }
- .review-image {
- object-fit: cover;
- }
- .media-video-wrap {
- position: relative;
- cursor: pointer;
- background: #000000;
- .video-thumb {
- display: block;
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .video-mask {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgb(0 0 0 / 35%);
- .video-play-icon {
- font-size: 28px;
- color: #ffffff;
- }
- }
- }
- }
- .review-footer {
- display: flex;
- gap: 16px;
- padding-top: 12px;
- border-top: 1px solid #e4e7ed;
- }
- }
- }
- // 分页
- .pagination-wrapper {
- display: flex;
- justify-content: flex-end;
- padding-top: 20px;
- margin-top: 20px;
- border-top: 1px solid #e4e7ed;
- }
- }
- }
- </style>
|