| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692 |
- <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.pending})`" 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.images && review.images.length > 0" class="review-images">
- <el-image
- v-for="(img, index) in review.images"
- :key="index"
- :src="img"
- :preview-src-list="review.images"
- fit="cover"
- class="review-image"
- />
- </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="500"
- 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>
- </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 } from "@element-plus/icons-vue";
- import type { FormInstance, FormRules, UploadUserFile } from "element-plus";
- import { getList, addAppealNew, saveComment, uploadImg } 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%"
- });
- // 标签页计数
- const tabCounts = reactive({
- all: 22,
- pending: 2,
- bad: 5,
- good: 10,
- neutral: 7
- });
- // 当前激活的标签
- 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) => {
- console.log(review);
- return review.storeComment && review.storeComment.length > 0;
- };
- 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);
- };
- // 加载统计数据(只在初始化时调用一次)
- const loadStatistics = async () => {
- try {
- const baseParams = {
- pageNum: 1,
- pageSize: 1,
- phoneId: `store_${localGet("geeker-user").userInfo.phone}`,
- businessType: 5,
- days: timeFilter.value,
- replyStatus: 0,
- storeId: localGet("createdId"),
- userType: 0
- };
- // 并行请求各个评论等级的数量
- const [allRes, goodRes, neutralRes, badRes, pendingRes]: any[] = await Promise.all([
- getList({ ...baseParams, commentLevel: 0 }), // 全部
- getList({ ...baseParams, commentLevel: 1 }), // 好评
- getList({ ...baseParams, commentLevel: 2 }), // 中评
- getList({ ...baseParams, commentLevel: 3 }), // 差评
- getList({ ...baseParams, commentLevel: 3, replyStatus: 2 }) // 待回复差评
- ]);
- // 更新标签页计数
- tabCounts.all = allRes?.code === 200 ? allRes.data?.total || 0 : 0;
- tabCounts.good = goodRes?.code === 200 ? goodRes.data?.total || 0 : 0;
- tabCounts.neutral = neutralRes?.code === 200 ? neutralRes.data?.total || 0 : 0;
- tabCounts.bad = badRes?.code === 200 ? badRes.data?.total || 0 : 0;
- tabCounts.pending = pendingRes?.code === 200 ? pendingRes.data?.total || 0 : 0;
- // 更新统计数据
- statistics.totalReviews = tabCounts.all;
- statistics.badTextReviews = tabCounts.bad; // 新增差评数
- statistics.badImageReviews = tabCounts.good; // 新增好评数
- statistics.neutralReviews = tabCounts.neutral; // 中评数
- // 计算评论回复率 = (已回复评论数 ÷ 总评论数) × 100%
- const totalCount = tabCounts.all;
- const repliedCount = totalCount - tabCounts.pending; // 已回复 = 总数 - 未回复
- if (totalCount > 0) {
- const rate = (repliedCount / totalCount) * 100;
- statistics.abnormalRate = rate.toFixed(2) + "%";
- } else {
- statistics.abnormalRate = "0%";
- }
- } catch (error) {
- console.error("获取统计数据失败", error);
- }
- };
- // 加载评论列表
- 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: any = {
- pageNum: pageNum.value,
- pageSize: pageSize.value,
- phoneId: `store_${localGet("geeker-user").userInfo.phone}`,
- businessType: 5, //业务类型(1:订单评论, 2:动态社区评论, 3:活动评论, 4:店铺打卡评论, 5:订单评价, 6:订单评论的评论)
- commentLevel: isPending ? 3 : level, // 待回复差评传3,其他按原值
- days: timeFilter.value, // 评论时间筛选
- replyStatus: isPending ? 2 : 0, // 待回复差评传2,其他传0
- storeId: localGet("createdId"),
- userType: 0
- };
- const res: any = await getList(params);
- if (res.code === 200) {
- reviewList.value = res.data.records || [];
- total.value = res.data.total || 0;
- } else {
- reviewList.value = [];
- total.value = 0;
- ElMessage.error(res.msg || "获取评论列表失败");
- }
- } catch (error: any) {
- console.error("获取评论列表失败", error);
- reviewList.value = [];
- total.value = 0;
- ElMessage.error(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) => {
- console.log("打开回复对话框:", review);
- currentReplyReview.value = review;
- replyDialogVisible.value = true;
- };
- // 关闭回复对话框
- const closeReplyDialog = () => {
- replyDialogVisible.value = false;
- replyFormRef.value?.resetFields();
- Object.assign(replyFormData, {
- content: ""
- });
- currentReplyReview.value = null;
- };
- // 提交回复
- const submitReply = async () => {
- if (!replyFormRef.value) return;
- await replyFormRef.value.validate(async (valid: boolean) => {
- if (valid) {
- try {
- // 获取当前用户的手机号,并在前面拼接 "store_"
- const phone = userStore.userInfo?.phone || "";
- const phoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
- // 调用 saveComment 接口
- const params = {
- businessId: currentReplyReview.value.id, // 评论ID
- businessType: "1", // 业务类型:1表示订单评价的回复
- userId: userStore.userInfo?.userId || userStore.userInfo?.id || "",
- storeId: userStore.userInfo?.storeId || localGet("createdId") || "",
- commentContent: replyFormData.content, // 回复内容
- phoneId: phoneId, // 店铺phoneId
- replyId: currentReplyReview.value.id // 被回复的评论ID
- };
- console.log("提交回复参数:", params);
- const res: any = await saveComment(params);
- if (res.code === 200) {
- ElMessage.success("回复提交成功");
- closeReplyDialog();
- loadReviewList();
- } else {
- ElMessage.error(res.message || "回复提交失败");
- }
- } catch (error) {
- console.error("回复提交失败:", error);
- ElMessage.error("回复提交失败");
- }
- }
- });
- };
- // 关闭申诉对话框
- const closeAppealDialog = () => {
- appealDialogVisible.value = false;
- appealFormRef.value?.resetFields();
- Object.assign(appealFormData, {
- reason: "",
- images: [],
- files: [],
- fileList: []
- });
- };
- // 提交申诉
- const submitAppeal = async () => {
- if (!appealFormRef.value) return;
- await appealFormRef.value.validate(async (valid: boolean) => {
- if (valid) {
- try {
- // 使用 FormData 发送图片
- const formData = new FormData();
- formData.append("appealReason", appealFormData.reason);
- formData.append("storeId", localGet("geeker-user").userInfo.storeId);
- formData.append("commentId", currentReviewId.value);
- // 添加图片文件,使用 file_0, file_1, file_2 等格式
- appealFormData.files.forEach((file, index) => {
- formData.append(`file_${index}`, file);
- });
- const res: any = await addAppealNew(formData);
- if (res.code === 200) {
- ElMessage.success("申诉提交成功");
- // 更新评价的申诉状态,使按钮置灰
- const review = reviewList.value.find((r: any) => r.id === currentReviewId.value);
- if (review) {
- review.isAppealed = true;
- }
- }
- closeAppealDialog();
- } catch (error) {
- ElMessage.error("申诉提交失败");
- }
- }
- });
- };
- // 图片预览
- 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-images {
- display: flex;
- gap: 8px;
- margin-bottom: 12px;
- .review-image {
- width: 80px;
- height: 80px;
- border-radius: 4px;
- }
- }
- .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>
|