Selaa lähdekoodia

feat(dynamicManagement): 新增好友关系与评论申诉管理功能

- 新增好友关系管理模块,支持添加、编辑、删除及同意好友申请
- 新增评论申诉管理模块,包含申诉提交、历史记录及详情查看功能
- 在菜单配置中添加好友关系管理与评论申诉相关路由入口
- 实现好友关系列表页面,支持搜索、状态展示及操作按钮
- 实现评论申诉列表页面,展示统计数据及评论内容,支持申诉操作
- 实现申诉详情页面,展示处理状态、审批信息及申诉凭证等内容
congxuesong 1 viikko sitten
vanhempi
commit
e84e4d47c9

+ 48 - 0
src/api/modules/friendRelation.ts

@@ -0,0 +1,48 @@
+import http from "@/api";
+
+/**
+ * @name 好友关系管理模块
+ */
+
+// 获取好友关系列表
+export const getFriendRelationList = (params: any) => {
+  return http.post(`/api/friendRelation/list`, params);
+};
+
+// 添加好友
+export const addFriendRelation = (params: {
+  friendName: string;
+  friendPhone: string;
+  remark?: string;
+  relationType: string | number;
+  couponList?: Array<{ couponId: string | number; quantity: number }>;
+}) => {
+  return http.post(`/api/friendRelation/add`, params);
+};
+
+// 编辑好友信息
+export const updateFriendRelation = (params: {
+  id: string | number;
+  friendName: string;
+  friendPhone: string;
+  remark?: string;
+  relationType: string | number;
+  couponList?: Array<{ couponId: string | number; quantity: number }>;
+}) => {
+  return http.post(`/api/friendRelation/update`, params);
+};
+
+// 删除好友
+export const deleteFriendRelation = (params: { id: string | number }) => {
+  return http.post(`/api/friendRelation/delete`, params);
+};
+
+// 同意好友申请
+export const approveFriend = (params: { id: string | number }) => {
+  return http.post(`/api/friendRelation/approve`, params);
+};
+
+// 拒绝好友申请
+export const rejectFriend = (params: { id: string | number; reason?: string }) => {
+  return http.post(`/api/friendRelation/reject`, params);
+};

+ 35 - 0
src/api/modules/reviewAppeal.ts

@@ -0,0 +1,35 @@
+import http from "@/api";
+
+/**
+ * @name 评论申诉管理模块
+ */
+
+// 获取评论列表
+export const getReviewList = (params: { type?: string; page?: number; pageSize?: number }) => {
+  return http.post(`/api/review/list`, params);
+};
+
+// 获取申诉历史列表
+export const getAppealHistory = (params: { type?: string; page?: number; pageSize?: number }) => {
+  return http.post(`/api/review/appealHistory`, params);
+};
+
+// 获取申诉详情
+export const getAppealDetail = (params: { id: string | number }) => {
+  return http.get(`/api/review/appealDetail`, { params });
+};
+
+// 提交评论申诉
+export const submitReviewAppeal = (params: { reviewId: string | number; reason: string; images?: string[] }) => {
+  return http.post(`/api/review/submitAppeal`, params);
+};
+
+// 撤销申诉
+export const cancelAppeal = (params: { appealId: string | number }) => {
+  return http.post(`/api/review/cancelAppeal`, params);
+};
+
+// 获取评论统计数据
+export const getReviewStatistics = () => {
+  return http.get(`/api/review/statistics`);
+};

+ 59 - 1
src/assets/json/authMenuList.json

@@ -688,12 +688,70 @@
           }
         },
         {
+          "path": "/dynamicManagement/reviewAppeal",
+          "name": "reviewAppeal",
+          "component": "/dynamicManagement/reviewAppeal",
+          "meta": {
+            "icon": "ChatDotSquare",
+            "title": "评论申诉",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/dynamicManagement/reviewAppealHistory",
+          "name": "reviewAppealHistory",
+          "component": "/dynamicManagement/reviewAppealHistory",
+          "meta": {
+            "icon": "Document",
+            "title": "申诉历史",
+            "activeMenu": "/dynamicManagement/reviewAppeal",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/dynamicManagement/reviewAppealDetail",
+          "name": "reviewAppealDetail",
+          "component": "/dynamicManagement/reviewAppealDetail",
+          "meta": {
+            "icon": "Document",
+            "title": "申诉详情",
+            "activeMenu": "/dynamicManagement/reviewAppeal",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/dynamicManagement/friendRelation",
+          "name": "friendRelation",
+          "component": "/dynamicManagement/friendRelation",
+          "meta": {
+            "icon": "User",
+            "title": "好友关系管理",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
           "path": "/dynamicManagement/friendCoupon",
           "name": "friendCoupon",
           "component": "/dynamicManagement/friendCoupon",
           "meta": {
             "icon": "Tickets",
-            "title": "好友优惠券",
+            "title": "好友赠券管理",
             "isLink": "",
             "isHide": false,
             "isFull": false,

+ 396 - 0
src/views/dynamicManagement/friendRelation.vue

@@ -0,0 +1,396 @@
+<template>
+  <div class="table-box button-table friend-relation-container">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
+      <!-- 表格 header 按钮 -->
+      <template #tableHeader="scope">
+        <div class="header-button">
+          <el-button type="primary" @click="openAddDialog"> 添加好友 </el-button>
+        </div>
+      </template>
+
+      <!-- 状态列 -->
+      <template #status="scope">
+        <el-tag :type="getStatusType(scope.row.status)">
+          {{ getStatusText(scope.row.status) }}
+        </el-tag>
+      </template>
+
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <el-button link type="primary" @click="editRow(scope.row)"> 编辑 </el-button>
+        <el-button link type="primary" @click="deleteRow(scope.row)"> 删除 </el-button>
+        <el-button v-if="scope.row.status === 0" link type="primary" @click="handleApprove(scope.row)"> 同意 </el-button>
+      </template>
+    </ProTable>
+
+    <!-- 添加/编辑好友对话框 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px" @close="closeDialog">
+      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="140px">
+        <el-form-item label="好友名称" prop="friendName">
+          <el-input v-model="formData.friendName" placeholder="请输入好友名称" clearable />
+        </el-form-item>
+
+        <el-form-item label="好友手机号" prop="friendPhone">
+          <el-input v-model="formData.friendPhone" placeholder="请输入手机号" maxlength="11" clearable />
+        </el-form-item>
+
+        <el-form-item label="好友备注" prop="remark">
+          <el-input
+            v-model="formData.remark"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入备注信息"
+            maxlength="200"
+            show-word-limit
+          />
+        </el-form-item>
+
+        <el-form-item label="关系类型" prop="relationType">
+          <el-select v-model="formData.relationType" placeholder="请选择关系类型" style="width: 100%">
+            <el-option v-for="item in relationTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="赠送优惠券" prop="couponList">
+          <div class="coupon-list">
+            <div v-for="(item, index) in formData.couponList" :key="index" class="coupon-item">
+              <el-select v-model="item.couponId" placeholder="请选择优惠券" style="width: 300px; margin-right: 10px">
+                <el-option v-for="coupon in availableCoupons" :key="coupon.id" :label="coupon.name" :value="coupon.id" />
+              </el-select>
+              <el-input-number
+                v-model="item.quantity"
+                :min="1"
+                :max="100"
+                placeholder="数量"
+                style="width: 120px; margin-right: 10px"
+              />
+              <el-button type="danger" link @click="removeCoupon(index)"> 删除 </el-button>
+            </div>
+            <el-button type="primary" link @click="addCoupon" style="margin-top: 10px">
+              <el-icon><Plus /></el-icon>
+              添加优惠券
+            </el-button>
+          </div>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="closeDialog"> 取消 </el-button>
+          <el-button type="primary" @click="handleSubmit"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="tsx" name="friendRelation">
+import { computed, onMounted, reactive, ref } from "vue";
+import type { FormInstance, FormRules } from "element-plus";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { Plus } from "@element-plus/icons-vue";
+import ProTable from "@/components/ProTable/index.vue";
+import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
+import { localGet } from "@/utils";
+// import { getFriendRelationList, addFriendRelation, updateFriendRelation, deleteFriendRelation, approveFriend } from "@/api/modules/friendRelation";
+
+// ProTable 实例
+const proTable = ref<ProTableInstance>();
+
+// 对话框相关
+const dialogVisible = ref(false);
+const dialogTitle = computed(() => (isEdit.value ? "编辑好友" : "添加好友"));
+const isEdit = ref(false);
+const currentEditId = ref("");
+
+// 表单引用
+const formRef = ref<FormInstance>();
+
+// 关系类型选项
+const relationTypeOptions = [
+  { label: "普通好友", value: 1 },
+  { label: "亲密好友", value: 2 },
+  { label: "商业伙伴", value: 3 }
+];
+
+// 可用优惠券列表(模拟数据)
+const availableCoupons = ref([
+  { id: 1, name: "满100减20代金券" },
+  { id: 2, name: "8折优惠券" },
+  { id: 3, name: "满200减50代金券" }
+]);
+
+// 表单数据
+const formData = reactive({
+  friendName: "",
+  friendPhone: "",
+  remark: "",
+  relationType: "",
+  couponList: [] as Array<{ couponId: string | number; quantity: number }>
+});
+
+// 表单验证规则
+const formRules = reactive<FormRules>({
+  friendName: [{ required: true, message: "请输入好友名称", trigger: "blur" }],
+  friendPhone: [
+    { required: true, message: "请输入手机号", trigger: "blur" },
+    {
+      pattern: /^1[3-9]\d{9}$/,
+      message: "请输入正确的手机号",
+      trigger: "blur"
+    }
+  ],
+  relationType: [{ required: true, message: "请选择关系类型", trigger: "change" }]
+});
+
+// 表格列配置
+const columns = reactive<ColumnProps<any>[]>([
+  {
+    prop: "friendName",
+    label: "好友名称",
+    search: {
+      el: "input"
+    }
+  },
+  {
+    prop: "friendPhone",
+    label: "手机号"
+  },
+  {
+    prop: "relationType",
+    label: "关系类型",
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: relationTypeOptions,
+    fieldNames: { label: "label", value: "value" },
+    render: (scope: any) => {
+      const type = relationTypeOptions.find(item => item.value === scope.row.relationType);
+      return type?.label || "--";
+    }
+  },
+  {
+    prop: "createTime",
+    label: "添加时间",
+    render: (scope: any) => {
+      return scope.row.createTime?.replace(/-/g, "/") || "--";
+    }
+  },
+  {
+    prop: "status",
+    label: "状态"
+  },
+  {
+    prop: "remark",
+    label: "备注",
+    render: (scope: any) => {
+      return scope.row.remark || "--";
+    }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 250 }
+]);
+
+// 初始化请求参数
+const initParam = reactive({
+  storeId: localGet("createdId") || ""
+});
+
+// 数据回调处理
+const dataCallback = (data: any) => {
+  return {
+    list: data?.records || [],
+    total: data?.total || 0
+  };
+};
+
+// 获取表格列表
+const getTableList = (params: any) => {
+  // TODO: 集成真实接口时,取消下面的注释
+  // return getFriendRelationList(params);
+
+  // 临时方案:返回模拟数据
+  return new Promise(resolve => {
+    setTimeout(() => {
+      const mockData = generateMockData();
+      resolve({
+        code: 200,
+        data: {
+          records: mockData,
+          total: mockData.length
+        }
+      });
+    }, 500);
+  });
+};
+
+// 生成模拟数据
+const generateMockData = () => {
+  return Array.from({ length: 5 }, (_, i) => ({
+    id: i + 1,
+    friendName: `好友${i + 1}`,
+    friendPhone: `138${String(i).padStart(8, "0")}`,
+    relationType: (i % 3) + 1,
+    createTime: "2025-03-01",
+    status: i % 3, // 0-待同意, 1-已同意, 2-已拒绝
+    remark: i % 2 === 0 ? `备注信息${i + 1}` : ""
+  }));
+};
+
+// 获取状态文本
+const getStatusText = (status: number) => {
+  const statusMap: Record<number, string> = {
+    0: "待同意",
+    1: "已同意",
+    2: "已拒绝"
+  };
+  return statusMap[status] || "--";
+};
+
+// 获取状态类型
+const getStatusType = (status: number) => {
+  const typeMap: Record<number, string> = {
+    0: "warning",
+    1: "success",
+    2: "info"
+  };
+  return typeMap[status] || "";
+};
+
+// 打开添加对话框
+const openAddDialog = () => {
+  isEdit.value = false;
+  dialogVisible.value = true;
+};
+
+// 关闭对话框
+const closeDialog = () => {
+  dialogVisible.value = false;
+  formRef.value?.resetFields();
+  Object.assign(formData, {
+    friendName: "",
+    friendPhone: "",
+    remark: "",
+    relationType: "",
+    couponList: []
+  });
+  currentEditId.value = "";
+};
+
+// 添加优惠券
+const addCoupon = () => {
+  formData.couponList.push({
+    couponId: "",
+    quantity: 1
+  });
+};
+
+// 移除优惠券
+const removeCoupon = (index: number) => {
+  formData.couponList.splice(index, 1);
+};
+
+// 编辑行数据
+const editRow = (row: any) => {
+  isEdit.value = true;
+  currentEditId.value = row.id;
+  Object.assign(formData, {
+    friendName: row.friendName,
+    friendPhone: row.friendPhone,
+    remark: row.remark,
+    relationType: row.relationType,
+    couponList: []
+  });
+  dialogVisible.value = true;
+};
+
+// 删除行数据
+const deleteRow = (row: any) => {
+  ElMessageBox.confirm("确定要删除这个好友吗?", "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning"
+  })
+    .then(async () => {
+      try {
+        // TODO: 集成真实接口时,取消下面的注释
+        // await deleteFriendRelation({ id: row.id });
+
+        ElMessage.success("删除成功");
+        proTable.value?.getTableList();
+      } catch (error) {
+        ElMessage.error("删除失败");
+      }
+    })
+    .catch(() => {
+      // 用户取消删除
+    });
+};
+
+// 同意好友申请
+const handleApprove = async (row: any) => {
+  try {
+    // TODO: 集成真实接口时,取消下面的注释
+    // await approveFriend({ id: row.id });
+
+    ElMessage.success("已同意好友申请");
+    proTable.value?.getTableList();
+  } catch (error) {
+    ElMessage.error("操作失败");
+  }
+};
+
+// 提交表单
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+
+  await formRef.value.validate(async (valid: boolean) => {
+    if (valid) {
+      try {
+        const params = {
+          ...formData,
+          id: isEdit.value ? currentEditId.value : undefined
+        };
+
+        // TODO: 集成真实接口时,取消下面的注释
+        // if (isEdit.value) {
+        //   await updateFriendRelation(params);
+        // } else {
+        //   await addFriendRelation(params);
+        // }
+
+        ElMessage.success(isEdit.value ? "编辑成功" : "添加成功");
+        closeDialog();
+        proTable.value?.getTableList();
+      } catch (error) {
+        ElMessage.error(isEdit.value ? "编辑失败" : "添加失败");
+      }
+    }
+  });
+};
+
+// 页面加载时触发查询
+onMounted(() => {
+  proTable.value?.getTableList();
+});
+</script>
+
+<style lang="scss" scoped>
+.friend-relation-container {
+  .header-button {
+    margin-bottom: 16px;
+  }
+  .coupon-list {
+    .coupon-item {
+      display: flex;
+      align-items: center;
+      margin-bottom: 10px;
+    }
+  }
+  .dialog-footer {
+    display: flex;
+    gap: 10px;
+    justify-content: flex-end;
+  }
+}
+</style>

+ 399 - 0
src/views/dynamicManagement/reviewAppeal.vue

@@ -0,0 +1,399 @@
+<template>
+  <div class="review-appeal-container">
+    <!-- 顶部统计数据 -->
+    <div class="statistics-section">
+      <div class="store-name">
+        {{ storeName }}
+      </div>
+      <div class="statistics-cards">
+        <div class="stat-card">
+          <div class="stat-label">评论数</div>
+          <div class="stat-value">
+            {{ statistics.totalReviews }}
+          </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>
+      <el-button type="primary" @click="refreshData"> 数据刷新 </el-button>
+    </div>
+
+    <!-- 评价列表区域 -->
+    <div class="review-list-section">
+      <div class="section-header">
+        <div class="section-title">评价列表</div>
+        <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+          <el-tab-pane :label="`全部 (${tabCounts.all})`" name="all" />
+          <el-tab-pane :label="`待回复差评 (${tabCounts.pending})`" name="pending" />
+          <el-tab-pane :label="`差评 (${tabCounts.bad})`" name="bad" />
+          <el-tab-pane :label="`好评 (${tabCounts.good})`" name="good" />
+          <el-tab-pane :label="`中评 (${tabCounts.neutral})`" name="neutral" />
+        </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.userName }}
+                </div>
+                <el-rate v-model="review.rating" disabled show-score />
+              </div>
+            </div>
+            <div class="review-time">
+              {{ review.createTime }}
+            </div>
+          </div>
+
+          <div class="review-content">
+            {{ review.content }}
+          </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="primary" link @click="openAppealDialog(review)"> 申诉 </el-button>
+            <el-button v-if="review.appealStatus" type="info" link @click="goToAppealHistory"> 查看申诉 </el-button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 空状态 -->
+      <el-empty v-else description="暂无评论数据" />
+    </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="500"
+            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"
+            :on-preview="handlePreview"
+            :on-remove="handleRemove"
+            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>
+  </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 { getReviewList, submitReviewAppeal } from "@/api/modules/reviewAppeal";
+
+const router = useRouter();
+
+// 店铺名称
+const storeName = ref("重庆老火锅");
+
+// 统计数据
+const statistics = reactive({
+  totalReviews: 22,
+  badTextReviews: 2,
+  badImageReviews: 2,
+  neutralReviews: 2,
+  abnormalRate: "16.67%"
+});
+
+// 标签页计数
+const tabCounts = reactive({
+  all: 22,
+  pending: 2,
+  bad: 5,
+  good: 10,
+  neutral: 7
+});
+
+// 当前激活的标签
+const activeTab = ref("all");
+
+// 评论列表
+const reviewList = ref<any[]>([]);
+
+// 申诉提交对话框
+const appealDialogVisible = ref(false);
+const appealFormRef = ref<FormInstance>();
+const currentReviewId = ref("");
+
+const appealFormData = reactive({
+  reason: "",
+  images: [],
+  fileList: [] as UploadUserFile[]
+});
+
+const appealFormRules = reactive<FormRules>({
+  reason: [{ required: true, message: "请输入申诉原因", trigger: "blur" }]
+});
+
+// 刷新数据
+const refreshData = () => {
+  loadReviewList();
+  ElMessage.success("数据刷新成功");
+};
+
+// 标签页切换
+const handleTabClick = () => {
+  loadReviewList();
+};
+
+// 跳转到申诉历史
+const goToAppealHistory = () => {
+  router.push("/dynamicManagement/reviewAppealHistory");
+};
+
+// 加载评论列表
+const loadReviewList = () => {
+  // TODO: 集成真实接口
+  // const res = await getReviewList({ type: activeTab.value });
+
+  // 模拟数据
+  reviewList.value = Array.from({ length: 5 }, (_, i) => ({
+    id: i + 1,
+    userName: "不长寿爱",
+    userAvatar: "",
+    rating: 4,
+    createTime: "2025/06/30 12:00:00",
+    content:
+      "这里是评论内容这里是评论内容这里是评论内容这里是评论内容,商家定义交易问题,非常好吃很好吃,商家定义交易问题。环境很优雅很好。正文定义交易问题,正文定义交易问题。环境很优雅很好。工作不错太不错了!",
+    images: i % 2 === 0 ? [] : ["", "", ""],
+    appealStatus: i % 3 === 0 ? 1 : 0
+  }));
+};
+
+// 打开申诉对话框
+const openAppealDialog = (review: any) => {
+  currentReviewId.value = review.id;
+  appealDialogVisible.value = true;
+};
+
+// 关闭申诉对话框
+const closeAppealDialog = () => {
+  appealDialogVisible.value = false;
+  appealFormRef.value?.resetFields();
+  Object.assign(appealFormData, {
+    reason: "",
+    images: [],
+    fileList: []
+  });
+};
+
+// 提交申诉
+const submitAppeal = async () => {
+  if (!appealFormRef.value) return;
+
+  await appealFormRef.value.validate(async (valid: boolean) => {
+    if (valid) {
+      try {
+        // TODO: 集成真实接口
+        // await submitReviewAppeal({
+        //   reviewId: currentReviewId.value,
+        //   reason: appealFormData.reason,
+        //   images: appealFormData.images
+        // });
+
+        ElMessage.success("申诉提交成功");
+        closeAppealDialog();
+        loadReviewList();
+      } catch (error) {
+        ElMessage.error("申诉提交失败");
+      }
+    }
+  });
+};
+
+// 图片预览
+const handlePreview = (file: UploadUserFile) => {
+  console.log("preview", file);
+};
+
+// 移除图片
+const handleRemove = (file: UploadUserFile) => {
+  console.log("remove", file);
+};
+
+// 初始化
+onMounted(() => {
+  loadReviewList();
+});
+</script>
+
+<style lang="scss" scoped>
+.review-appeal-container {
+  min-height: calc(100vh - 120px);
+  padding: 20px;
+  background: #f5f7fa;
+
+  // 统计数据区域
+  .statistics-section {
+    padding: 20px;
+    margin-bottom: 20px;
+    background: #ffffff;
+    border-radius: 8px;
+    .store-name {
+      margin-bottom: 16px;
+      font-size: 18px;
+      font-weight: 600;
+      color: #303133;
+    }
+    .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;
+      }
+    }
+
+    // 评论卡片
+    .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;
+        }
+        .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;
+        }
+      }
+    }
+  }
+}
+</style>

+ 408 - 0
src/views/dynamicManagement/reviewAppealDetail.vue

@@ -0,0 +1,408 @@
+<template>
+  <div class="review-appeal-detail-container">
+    <!-- 返回按钮 -->
+    <div class="page-header">
+      <el-button type="primary" link @click="goBack">
+        <el-icon><ArrowLeft /></el-icon>
+        返回
+      </el-button>
+      <h2 class="page-title">申诉详情</h2>
+    </div>
+
+    <div v-if="appealDetail" class="appeal-detail-content">
+      <!-- 处理状态 -->
+      <div class="status-section">
+        <el-result
+          :icon="getResultIcon(appealDetail.status)"
+          :title="getResultTitle(appealDetail.status)"
+          :sub-title="getResultSubTitle(appealDetail.status)"
+        />
+      </div>
+
+      <!-- 处理原因(仅已驳回时显示) -->
+      <div v-if="appealDetail.status === 2" class="section-card reason-section">
+        <div class="section-title">处理原因</div>
+        <el-radio-group v-model="rejectReason" disabled class="reason-radio-group">
+          <el-radio :label="1"> 商家定义交易问题 </el-radio>
+          <el-radio :label="2"> 违反主流价值观,已为客等可不为疏 </el-radio>
+          <el-radio :label="3"> 恶意差评 </el-radio>
+        </el-radio-group>
+      </div>
+
+      <!-- 审批详情 -->
+      <div v-if="appealDetail.status !== 0" class="section-card approval-section">
+        <div class="section-title">审批详情</div>
+        <div class="approval-info">
+          <div class="info-row">
+            <span class="info-label">申诉人:</span>
+            <span class="info-value">{{ appealDetail.userName }}</span>
+          </div>
+          <div class="info-row">
+            <span class="info-label">申诉编号:</span>
+            <span class="info-value">{{ appealDetail.appealNo }}</span>
+          </div>
+          <div class="info-row">
+            <span class="info-label">申诉时间:</span>
+            <span class="info-value">{{ appealDetail.appealTime }}</span>
+          </div>
+          <div class="info-row">
+            <span class="info-label">处理时间:</span>
+            <span class="info-value">{{ appealDetail.processTime }}</span>
+          </div>
+        </div>
+
+        <div v-if="appealDetail.approvalComment" class="approval-comment">
+          <div class="comment-title">审批评语</div>
+          <div class="comment-text">
+            {{ appealDetail.approvalComment }}
+          </div>
+        </div>
+      </div>
+
+      <!-- 评价详情 -->
+      <div class="section-card review-detail-section">
+        <div class="section-title">评价详情</div>
+        <div class="review-detail-card">
+          <div class="user-info">
+            <el-avatar :src="appealDetail.userAvatar" :size="48">
+              <el-icon><User /></el-icon>
+            </el-avatar>
+            <div class="user-details">
+              <div class="user-name">
+                {{ appealDetail.userName }}
+              </div>
+              <div class="review-time">
+                {{ appealDetail.reviewTime }}
+              </div>
+            </div>
+          </div>
+
+          <div class="review-content">
+            {{ appealDetail.reviewContent }}
+          </div>
+
+          <div class="review-rating">
+            <div class="rating-item">
+              <span class="rating-label">口味评分:</span>
+              <el-rate v-model="appealDetail.tasteRating" disabled show-score />
+            </div>
+            <div class="rating-item">
+              <span class="rating-label">环境评分:</span>
+              <el-rate v-model="appealDetail.envRating" disabled show-score />
+            </div>
+            <div class="rating-item">
+              <span class="rating-label">服务评分:</span>
+              <el-rate v-model="appealDetail.serviceRating" disabled show-score />
+            </div>
+          </div>
+
+          <div class="appeal-reason">
+            <div class="reason-label">申诉原因:</div>
+            <div class="reason-text">
+              {{ appealDetail.appealReason }}
+            </div>
+          </div>
+
+          <div v-if="appealDetail.appealImages && appealDetail.appealImages.length > 0" class="appeal-images">
+            <div class="images-label">申诉凭证:</div>
+            <div class="images-list">
+              <el-image
+                v-for="(img, index) in appealDetail.appealImages"
+                :key="index"
+                :src="img"
+                :preview-src-list="appealDetail.appealImages"
+                fit="cover"
+                class="appeal-image"
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 加载状态 -->
+    <div v-else class="loading-section">
+      <el-skeleton :rows="10" animated />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="reviewAppealDetail">
+import { ref, onMounted } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { ArrowLeft, User } from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+// import { getAppealDetail } from "@/api/modules/reviewAppeal";
+
+const router = useRouter();
+const route = useRoute();
+
+// 申诉详情
+const appealDetail = ref<any>(null);
+const rejectReason = ref(1);
+
+// 返回
+const goBack = () => {
+  router.back();
+};
+
+// 获取结果图标
+const getResultIcon = (status: number) => {
+  const iconMap: Record<number, string> = {
+    0: "info",
+    1: "success",
+    2: "error"
+  };
+  return iconMap[status] || "info";
+};
+
+// 获取结果标题
+const getResultTitle = (status: number) => {
+  const titleMap: Record<number, string> = {
+    0: "审核中",
+    1: "审核通过",
+    2: "已驳回"
+  };
+  return titleMap[status] || "";
+};
+
+// 获取结果副标题
+const getResultSubTitle = (status: number) => {
+  const subTitleMap: Record<number, string> = {
+    0: "您的申诉正在审核中,请耐心等待。",
+    1: "恭喜您的申诉已经通过审核,评论将被删除或修改。",
+    2: "很抱歉,您的申诉被驳回,如有疑问请联系客服。"
+  };
+  return subTitleMap[status] || "";
+};
+
+// 加载申诉详情
+const loadAppealDetail = async () => {
+  const id = route.query.id;
+  if (!id) {
+    ElMessage.error("缺少申诉ID");
+    router.back();
+    return;
+  }
+
+  try {
+    // TODO: 集成真实接口
+    // const res = await getAppealDetail({ id });
+    // appealDetail.value = res.data;
+
+    // 模拟数据
+    setTimeout(() => {
+      appealDetail.value = {
+        id: id,
+        userName: "不长寿爱",
+        userAvatar: "",
+        status: Number(id) % 3, // 0-审核中, 1-已通过, 2-已驳回
+        appealNo: `APL${String(1380000000 + Number(id)).padStart(10, "0")}`,
+        appealTime: "2025/06/30 12:00:00",
+        processTime: "2025/06/30 15:00:00",
+        reviewTime: "2025/06/30 12:00:00",
+        reviewContent:
+          "这是评论内容这是评论内容这是评论内容这是评论内容,商家定义交易问题,非常好吃很好吃,商家定义交易问题。环境很优雅很好。",
+        tasteRating: 4,
+        envRating: 5,
+        serviceRating: 4,
+        appealReason: "商家定义交易问题,恶意差评。该评论内容与实际消费体验不符,存在明显的恶意诋毁行为。",
+        appealImages: ["", "", ""],
+        approvalComment:
+          Number(id) % 3 === 2
+            ? "您的申诉不符合平台规则,该评论内容真实反映了用户的消费体验。"
+            : Number(id) % 3 === 1
+              ? "经核实,该评论确实存在恶意差评行为,申诉通过。"
+              : ""
+      };
+    }, 500);
+  } catch (error) {
+    ElMessage.error("加载申诉详情失败");
+  }
+};
+
+// 初始化
+onMounted(() => {
+  loadAppealDetail();
+});
+</script>
+
+<style lang="scss" scoped>
+.review-appeal-detail-container {
+  min-height: calc(100vh - 120px);
+  padding: 20px;
+  background: #f5f7fa;
+  .page-header {
+    display: flex;
+    gap: 16px;
+    align-items: center;
+    margin-bottom: 24px;
+    .page-title {
+      margin: 0;
+      font-size: 20px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+  .appeal-detail-content {
+    max-width: 1200px;
+    margin: 0 auto;
+    .status-section {
+      margin-bottom: 24px;
+      background: #ffffff;
+      border-radius: 8px;
+    }
+    .section-card {
+      padding: 24px;
+      margin-bottom: 20px;
+      background: #ffffff;
+      border-radius: 8px;
+      .section-title {
+        margin-bottom: 20px;
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+      }
+    }
+    .reason-section {
+      .reason-radio-group {
+        display: flex;
+        flex-direction: column;
+        gap: 16px;
+        :deep(.el-radio) {
+          margin-right: 0;
+        }
+      }
+    }
+    .approval-section {
+      .approval-info {
+        padding: 20px;
+        background: #f5f7fa;
+        border-radius: 4px;
+        .info-row {
+          display: flex;
+          margin-bottom: 12px;
+          font-size: 14px;
+          &:last-child {
+            margin-bottom: 0;
+          }
+          .info-label {
+            flex-shrink: 0;
+            min-width: 100px;
+            color: #909399;
+          }
+          .info-value {
+            flex: 1;
+            color: #303133;
+          }
+        }
+      }
+      .approval-comment {
+        margin-top: 20px;
+        .comment-title {
+          margin-bottom: 12px;
+          font-size: 14px;
+          font-weight: 600;
+          color: #303133;
+        }
+        .comment-text {
+          padding: 16px;
+          font-size: 14px;
+          line-height: 1.8;
+          color: #606266;
+          background: #f5f7fa;
+          border-radius: 4px;
+        }
+      }
+    }
+    .review-detail-section {
+      .review-detail-card {
+        .user-info {
+          display: flex;
+          gap: 16px;
+          align-items: center;
+          margin-bottom: 20px;
+          .user-details {
+            .user-name {
+              margin-bottom: 6px;
+              font-size: 16px;
+              font-weight: 600;
+              color: #303133;
+            }
+            .review-time {
+              font-size: 14px;
+              color: #909399;
+            }
+          }
+        }
+        .review-content {
+          margin-bottom: 20px;
+          font-size: 15px;
+          line-height: 1.8;
+          color: #606266;
+        }
+        .review-rating {
+          padding: 20px;
+          margin-bottom: 20px;
+          background: #f5f7fa;
+          border-radius: 4px;
+          .rating-item {
+            display: flex;
+            gap: 12px;
+            align-items: center;
+            margin-bottom: 12px;
+            &:last-child {
+              margin-bottom: 0;
+            }
+            .rating-label {
+              flex-shrink: 0;
+              min-width: 80px;
+              font-size: 14px;
+              color: #606266;
+            }
+          }
+        }
+        .appeal-reason {
+          margin-bottom: 20px;
+          .reason-label {
+            margin-bottom: 12px;
+            font-size: 15px;
+            font-weight: 600;
+            color: #303133;
+          }
+          .reason-text {
+            font-size: 14px;
+            line-height: 1.8;
+            color: #606266;
+          }
+        }
+        .appeal-images {
+          .images-label {
+            margin-bottom: 12px;
+            font-size: 15px;
+            font-weight: 600;
+            color: #303133;
+          }
+          .images-list {
+            display: grid;
+            grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+            gap: 12px;
+            .appeal-image {
+              width: 100%;
+              height: 150px;
+              border-radius: 8px;
+            }
+          }
+        }
+      }
+    }
+  }
+  .loading-section {
+    max-width: 1200px;
+    padding: 24px;
+    margin: 0 auto;
+    background: #ffffff;
+    border-radius: 8px;
+  }
+}
+</style>

+ 274 - 0
src/views/dynamicManagement/reviewAppealHistory.vue

@@ -0,0 +1,274 @@
+<template>
+  <div class="review-appeal-history-container">
+    <!-- 返回按钮 -->
+    <div class="page-header">
+      <el-button type="primary" link @click="goBack">
+        <el-icon><ArrowLeft /></el-icon>
+        返回
+      </el-button>
+      <h2 class="page-title">申诉历史</h2>
+    </div>
+
+    <!-- 标签页 -->
+    <div class="tabs-section">
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+        <el-tab-pane label="全部" name="all" />
+        <el-tab-pane label="审核中" name="pending" />
+        <el-tab-pane label="已驳回" name="rejected" />
+        <el-tab-pane label="已通过" name="approved" />
+      </el-tabs>
+    </div>
+
+    <!-- 申诉历史列表 -->
+    <div v-if="appealHistoryList.length > 0" class="appeal-history-list">
+      <div v-for="item in appealHistoryList" :key="item.id" class="appeal-history-card">
+        <div class="appeal-header">
+          <div class="user-info">
+            <el-avatar :src="item.userAvatar" :size="40">
+              <el-icon><User /></el-icon>
+            </el-avatar>
+            <div class="user-details">
+              <div class="user-name">
+                {{ item.userName }}
+              </div>
+              <div class="appeal-time">
+                {{ item.appealTime }}
+              </div>
+            </div>
+          </div>
+          <el-tag :type="getAppealStatusType(item.status)" size="large">
+            {{ getAppealStatusText(item.status) }}
+          </el-tag>
+        </div>
+
+        <div class="appeal-content">
+          {{ item.reviewContent }}
+        </div>
+
+        <div class="appeal-footer">
+          <div class="appeal-info">
+            <div class="info-item">
+              <span class="info-label">申诉状态:</span>
+              <span>{{ item.appealReason }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">申诉编号:</span>
+              <span>{{ item.appealNo || "--" }}</span>
+            </div>
+          </div>
+          <el-button type="primary" @click="viewDetail(item)"> 查看详情 </el-button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 空状态 -->
+    <el-empty v-else description="暂无申诉记录" />
+
+    <!-- 分页 -->
+    <div v-if="appealHistoryList.length > 0" class="pagination-section">
+      <el-pagination
+        v-model:current-page="pagination.page"
+        v-model:page-size="pagination.pageSize"
+        :page-sizes="[10, 20, 30, 50]"
+        :total="pagination.total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="reviewAppealHistory">
+import { ref, reactive, onMounted } from "vue";
+import { useRouter } from "vue-router";
+import { ArrowLeft, User } from "@element-plus/icons-vue";
+// import { getAppealHistory } from "@/api/modules/reviewAppeal";
+
+const router = useRouter();
+
+// 当前激活的标签
+const activeTab = ref("all");
+
+// 申诉历史列表
+const appealHistoryList = ref<any[]>([]);
+
+// 分页
+const pagination = reactive({
+  page: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 返回
+const goBack = () => {
+  router.back();
+};
+
+// 标签页切换
+const handleTabClick = () => {
+  pagination.page = 1;
+  loadAppealHistory();
+};
+
+// 分页大小改变
+const handleSizeChange = (val: number) => {
+  pagination.pageSize = val;
+  pagination.page = 1;
+  loadAppealHistory();
+};
+
+// 当前页改变
+const handleCurrentChange = (val: number) => {
+  pagination.page = val;
+  loadAppealHistory();
+};
+
+// 加载申诉历史
+const loadAppealHistory = () => {
+  // TODO: 集成真实接口
+  // const res = await getAppealHistory({
+  //   type: activeTab.value,
+  //   page: pagination.page,
+  //   pageSize: pagination.pageSize
+  // });
+
+  // 模拟数据
+  appealHistoryList.value = Array.from({ length: 10 }, (_, i) => ({
+    id: i + 1,
+    userName: "不长寿爱",
+    userAvatar: "",
+    appealTime: "2025/06/30 12:00:00",
+    reviewContent: "这是评论内容,商家定义交易问题,非常好吃很好吃。环境很优雅很好。",
+    status: i % 3, // 0-审核中, 1-已通过, 2-已驳回
+    appealReason: i % 3 === 0 ? "等待审核" : i % 3 === 1 ? "审核通过" : "已驳回",
+    appealNo: `APL${String(1380000000 + i).padStart(10, "0")}`
+  }));
+
+  pagination.total = 30;
+};
+
+// 查看详情
+const viewDetail = (item: any) => {
+  router.push({
+    path: "/dynamicManagement/reviewAppealDetail",
+    query: { id: item.id }
+  });
+};
+
+// 获取申诉状态类型
+const getAppealStatusType = (status: number) => {
+  const typeMap: Record<number, string> = {
+    0: "warning",
+    1: "success",
+    2: "danger"
+  };
+  return typeMap[status] || "";
+};
+
+// 获取申诉状态文本
+const getAppealStatusText = (status: number) => {
+  const textMap: Record<number, string> = {
+    0: "审核中",
+    1: "已通过",
+    2: "已驳回"
+  };
+  return textMap[status] || "";
+};
+
+// 初始化
+onMounted(() => {
+  loadAppealHistory();
+});
+</script>
+
+<style lang="scss" scoped>
+.review-appeal-history-container {
+  min-height: calc(100vh - 120px);
+  padding: 20px;
+  background: #ffffff;
+  .page-header {
+    display: flex;
+    gap: 16px;
+    align-items: center;
+    margin-bottom: 24px;
+    .page-title {
+      margin: 0;
+      font-size: 20px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+  .tabs-section {
+    margin-bottom: 24px;
+  }
+  .appeal-history-list {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+    margin-bottom: 24px;
+    .appeal-history-card {
+      padding: 20px;
+      border: 1px solid #e4e7ed;
+      border-radius: 8px;
+      transition: all 0.3s;
+      &:hover {
+        box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
+      }
+      .appeal-header {
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: 16px;
+        .user-info {
+          display: flex;
+          gap: 12px;
+          align-items: center;
+          .user-details {
+            .user-name {
+              margin-bottom: 4px;
+              font-size: 15px;
+              font-weight: 600;
+              color: #303133;
+            }
+            .appeal-time {
+              font-size: 13px;
+              color: #909399;
+            }
+          }
+        }
+      }
+      .appeal-content {
+        margin-bottom: 16px;
+        font-size: 14px;
+        line-height: 1.6;
+        color: #606266;
+      }
+      .appeal-footer {
+        display: flex;
+        align-items: flex-end;
+        justify-content: space-between;
+        padding-top: 16px;
+        border-top: 1px solid #e4e7ed;
+        .appeal-info {
+          flex: 1;
+          .info-item {
+            margin-bottom: 8px;
+            font-size: 14px;
+            &:last-child {
+              margin-bottom: 0;
+            }
+            .info-label {
+              color: #909399;
+            }
+          }
+        }
+      }
+    }
+  }
+  .pagination-section {
+    display: flex;
+    justify-content: center;
+    padding: 20px 0;
+  }
+}
+</style>