Ver Fonte

新增协议

Huanyi há 4 dias atrás
pai
commit
5a02a93613

+ 1 - 1
src/permission.ts

@@ -11,7 +11,7 @@ import { usePermissionStore } from '@/store/modules/permission';
 import { ElMessage } from 'element-plus/es';
 
 NProgress.configure({ showSpinner: false });
-const whiteList = ['/login', '/register', '/social-callback', '/register*', '/register/*'];
+const whiteList = ['/login', '/register', '/social-callback', '/register*', '/register/*', '/fulfillPath*'];
 
 const isWhiteList = (path: string) => {
   return whiteList.some((pattern) => isPathMatch(pattern, path));

+ 5 - 0
src/router/index.ts

@@ -62,6 +62,11 @@ export const constantRoutes: RouteRecordRaw[] = [
     component: () => import('@/views/error/401.vue'),
     hidden: true
   },
+  {
+    path: '/fulfillPath',
+    component: () => import('@/views/fulfillPath/index.vue'),
+    hidden: true
+  },
   {
     path: '',
     component: Layout,

+ 383 - 0
src/views/fulfillPath/index.vue

@@ -0,0 +1,383 @@
+<template>
+    <div class="fulfill-path-page">
+        <div class="page-header">
+            <div class="header-content">
+                <div class="header-title">服务进度</div>
+            </div>
+            <div class="order-info" v-if="orderCode">
+                <span class="order-no-label">订单号</span>
+                <span class="order-no-value">{{ orderCode }}</span>
+            </div>
+        </div>
+
+        <div class="page-body" v-loading="loading">
+            <div v-if="!loading && progressSteps.length === 0" class="empty-state">
+                <div class="empty-icon">
+                    <el-icon :size="48" color="#c0c4cc">
+                        <Clock />
+                    </el-icon>
+                </div>
+                <div class="empty-title">暂无服务进度</div>
+                <div class="empty-desc">履约者接单后将在此记录服务进度</div>
+            </div>
+
+            <div class="timeline-wrapper" v-else>
+                <div class="timeline">
+                    <div v-for="(step, index) in progressSteps" :key="index" class="timeline-item"
+                        :class="{ 'is-last': index === progressSteps.length - 1 }">
+                        <div class="timeline-dot-wrapper">
+                            <div class="timeline-dot" :style="{ borderColor: step.color }">
+                                <el-icon v-if="step.done" :size="14" :color="step.color">
+                                    <CircleCheck />
+                                </el-icon>
+                                <span v-else class="dot-inner" :style="{ background: step.color }"></span>
+                            </div>
+                            <div v-if="index < progressSteps.length - 1" class="timeline-line"></div>
+                        </div>
+
+                        <div class="timeline-content">
+                            <div class="step-time">{{ step.time }}</div>
+                            <div class="step-card">
+                                <h4 class="step-title">{{ step.title }}</h4>
+                                <p class="step-desc" v-if="step.desc">{{ step.desc }}</p>
+                                <div class="step-media" v-if="step.media && step.media.length">
+                                    <div v-for="(item, i) in step.media" :key="i" class="media-item">
+                                        <el-image v-if="item.type === 'image'" :src="item.url"
+                                            :preview-src-list="step.media.filter(m => m.type === 'image').map(m => m.url)"
+                                            fit="cover" class="media-img" :preview-teleported="true" />
+                                        <div v-else-if="item.type === 'video'" class="media-video"
+                                            @click="openVideo(item.url)">
+                                            <video :src="item.url" preload="metadata" class="media-img"></video>
+                                            <div class="video-play-icon">
+                                                <el-icon :size="28" color="#fff">
+                                                    <VideoPlay />
+                                                </el-icon>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <el-dialog v-model="videoVisible" title="视频播放" :close-on-click-modal="false" append-to-body
+            @closed="videoUrl = ''">
+            <div class="video-preview-box">
+                <video v-if="videoUrl" :src="videoUrl" controls autoplay style="width: 100%; max-height: 60vh;"></video>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import { Clock, CircleCheck, VideoPlay } from '@element-plus/icons-vue';
+import { listSubOrderLog } from '@/api/order/subOrderLog/index';
+import { getSubOrderInfo } from '@/api/order/subOrder/index';
+
+const route = useRoute();
+
+const loading = ref(false);
+const orderCode = ref('');
+const videoVisible = ref(false);
+const videoUrl = ref('');
+
+interface MediaItem {
+    type: 'image' | 'video';
+    url: string;
+}
+
+interface ProgressStep {
+    title: string;
+    time: string;
+    desc: string;
+    color: string;
+    done: boolean;
+    media: MediaItem[];
+}
+
+const progressSteps = ref<ProgressStep[]>([]);
+
+const videoExts = ['.mp4', '.mov', '.avi', '.wmv', '.webm', '.ogg'];
+
+const isVideo = (url: string): boolean => {
+    if (!url) return false;
+    return videoExts.some(ext => String(url).toLowerCase().trim().endsWith(ext));
+};
+
+const openVideo = (url: string) => {
+    videoUrl.value = url;
+    videoVisible.value = true;
+};
+
+const fetchProgress = async (id: string) => {
+    loading.value = true;
+    try {
+        const [logRes, orderRes] = await Promise.all([
+            listSubOrderLog({ orderId: id }),
+            getSubOrderInfo(id)
+        ]);
+
+        const list = logRes?.data?.data || logRes?.data || [];
+        const arr = Array.isArray(list) ? list : [];
+        const fLogs = arr.filter((i: any) => Number(i?.logType) === 1);
+
+        progressSteps.value = fLogs.map((i: any) => {
+            const urls = i?.photoUrls || [];
+            const media: MediaItem[] = Array.isArray(urls)
+                ? urls.map((u: string) => ({ type: isVideo(u) ? 'video' : 'image', url: u }))
+                : [];
+            const isCompleted = i.step === 4 || i.step === 99;
+            return {
+                title: i?.title || '--',
+                time: i?.createTime || i?.time || '',
+                desc: i?.content || '',
+                color: isCompleted ? '#67c23a' : '#ff9900',
+                done: isCompleted,
+                media
+            };
+        });
+
+        if (orderRes?.data) {
+            orderCode.value = orderRes.data.code || '';
+        }
+    } catch {
+        progressSteps.value = [];
+    } finally {
+        loading.value = false;
+    }
+};
+
+watch(() => route.query.orderId, (val) => {
+    if (val) {
+        fetchProgress(String(val));
+    }
+}, { immediate: true });
+
+onMounted(() => {
+    const id = route.query.orderId;
+    if (id) {
+        fetchProgress(String(id));
+    }
+});
+</script>
+
+<style scoped>
+.fulfill-path-page {
+    min-height: 100vh;
+    background: #f5f7fa;
+    font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+}
+
+.page-header {
+    position: sticky;
+    top: 0;
+    z-index: 100;
+    background: #fff;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.header-content {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 12px 16px;
+}
+
+.header-title {
+    font-size: 17px;
+    font-weight: 600;
+    color: #303133;
+}
+
+.order-info {
+    padding: 10px 16px 14px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.order-no-label {
+    font-size: 13px;
+    color: #909399;
+}
+
+.order-no-value {
+    font-size: 15px;
+    font-weight: 500;
+    color: #303133;
+    word-break: break-all;
+}
+
+.page-body {
+    padding: 16px;
+    min-height: calc(100vh - 120px);
+}
+
+.empty-state {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+    text-align: center;
+}
+
+.empty-icon {
+    margin-bottom: 16px;
+}
+
+.empty-title {
+    font-size: 16px;
+    font-weight: 500;
+    color: #909399;
+    margin-bottom: 8px;
+}
+
+.empty-desc {
+    font-size: 14px;
+    color: #c0c4cc;
+}
+
+.timeline-wrapper {
+    padding: 4px 0;
+}
+
+.timeline {
+    padding-left: 0;
+}
+
+.timeline-item {
+    display: flex;
+    position: relative;
+}
+
+.timeline-dot-wrapper {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    width: 32px;
+    flex-shrink: 0;
+}
+
+.timeline-dot {
+    width: 24px;
+    height: 24px;
+    border-radius: 50%;
+    border: 3px solid #ff9900;
+    background: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+    z-index: 1;
+}
+
+.dot-inner {
+    width: 10px;
+    height: 10px;
+    border-radius: 50%;
+}
+
+.timeline-line {
+    width: 2px;
+    flex: 1;
+    min-height: 24px;
+    background: #e4e7ed;
+    margin: 4px 0;
+}
+
+.timeline-content {
+    flex: 1;
+    padding-left: 14px;
+    padding-bottom: 28px;
+    min-width: 0;
+}
+
+.step-time {
+    font-size: 13px;
+    color: #909399;
+    margin-bottom: 10px;
+}
+
+.step-card {
+    background: #fff;
+    border-radius: 10px;
+    padding: 16px;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
+}
+
+.step-title {
+    margin: 0 0 10px;
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+    line-height: 1.4;
+}
+
+.step-desc {
+    margin: 0 0 14px;
+    font-size: 14px;
+    color: #606266;
+    line-height: 1.7;
+    word-break: break-word;
+}
+
+.step-media {
+    display: flex;
+    gap: 8px;
+    flex-wrap: wrap;
+}
+
+.media-item {
+    flex-shrink: 0;
+}
+
+.media-img {
+    width: 100px;
+    height: 100px;
+    border-radius: 6px;
+    object-fit: cover;
+    display: block;
+    border: 1px solid #ebeef5;
+}
+
+.media-video {
+    position: relative;
+    width: 100px;
+    height: 100px;
+    border-radius: 6px;
+    overflow: hidden;
+    cursor: pointer;
+}
+
+.media-video video {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+}
+
+.video-play-icon {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: rgba(0, 0, 0, 0.3);
+}
+
+.video-preview-box {
+    display: flex;
+    justify-content: center;
+    background: #000;
+    border-radius: 4px;
+    overflow: hidden;
+}
+</style>

+ 31 - 57
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -98,11 +98,11 @@
                         </div>
                         <el-descriptions :column="2" size="small" class="pet-desc" border>
                             <el-descriptions-item label="宠物品种">{{ order.petBreed || '-'
-                                }}</el-descriptions-item>
+                            }}</el-descriptions-item>
                             <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ order.petVaccine || '-'
-                                    }}</span></el-descriptions-item>
+                            }}</span></el-descriptions-item>
                             <el-descriptions-item label="性格特点">{{ order.petPersonality || order.petCharacter || '-'
-                                }}</el-descriptions-item>
+                            }}</el-descriptions-item>
                             <el-descriptions-item label="健康状况">{{ order.petHealth || '-' }}</el-descriptions-item>
                         </el-descriptions>
                     </div>
@@ -115,7 +115,7 @@
                         <div class="user-content">
                             <div class="u-row">
                                 <el-avatar :size="40" :src="order.userAvatar">{{ (order.userName || '').charAt(0)
-                                    }}</el-avatar>
+                                }}</el-avatar>
                                 <div class="u-info">
                                     <div class="nm">{{ order.userName }}</div>
                                     <div class="ph">{{ order.contactPhone }}</div>
@@ -148,19 +148,19 @@
                                     <el-descriptions-item label="归属门店">{{ order.merchantName }}
                                         ({{ Number(order.platformId) === 1 ? '门店下单' : '平台代下单' }})</el-descriptions-item>
                                     <el-descriptions-item label="宠主信息">{{ order.userName }} / {{ order.contactPhone
-                                        }}</el-descriptions-item>
+                                    }}</el-descriptions-item>
                                     <el-descriptions-item label="履约佣金" label-class-name="money-label">
                                         <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.fulfillerFee }}</span>
                                     </el-descriptions-item>
 
                                     <el-descriptions-item label="预约时间">{{ getServiceTimeRange(order.serviceTime)
-                                        }}</el-descriptions-item>
+                                    }}</el-descriptions-item>
                                     <el-descriptions-item label="团购套餐">{{ order.groupBuyPackage || '-'
-                                        }}</el-descriptions-item>
+                                    }}</el-descriptions-item>
                                     <el-descriptions-item label="创建时间">{{ order.createTime }}</el-descriptions-item>
                                     <el-descriptions-item label="订单佣金" label-class-name="money-label">
                                         <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.orderCommission || 0
-                                            }}</span>
+                                        }}</span>
                                     </el-descriptions-item>
 
                                     <el-descriptions-item label="订单备注" :span="3">
@@ -180,7 +180,7 @@
                                     <div class="t-row">
                                         <span class="t-k">起点</span>
                                         <span class="t-v">{{ order.detail?.fromAddress || order.detail?.pickAddr || '-'
-                                        }}</span>
+                                            }}</span>
                                     </div>
                                     <div class="t-row">
                                         <span class="t-k">终点</span>
@@ -200,7 +200,7 @@
                                 <div class="sec-title-bar">服务执行要求</div>
                                 <el-descriptions :column="2" border size="default" class="custom-desc">
                                     <el-descriptions-item label="服务地址" :span="2">{{ order.detail.area || order.address
-                                        }}</el-descriptions-item>
+                                    }}</el-descriptions-item>
                                 </el-descriptions>
                             </div>
                         </div>
@@ -212,7 +212,7 @@
                             <div v-if="order.fulfillerName" class="fulfiller-card">
                                 <div class="f-left">
                                     <el-avatar :size="60" :src="order.fulfillerAvatar">{{ order.fulfillerName.charAt(0)
-                                        }}</el-avatar>
+                                    }}</el-avatar>
                                 </div>
                                 <div class="f-right">
                                     <div class="f-row1">
@@ -437,13 +437,13 @@
                         <!-- 时间 -->
                         <div style="font-size: 14px; color: #909399; margin-bottom: 10px; font-weight: 500;">{{
                             step.time
-                            }}</div>
+                        }}</div>
                         <!-- 进度卡片 -->
                         <div
                             style="background: #f8fcfb; border-radius: 8px; padding: 20px; border: 1px solid #ebeef5; width: calc(100% - 10px); box-sizing: border-box;">
                             <h4 style="margin: 0 0 12px; font-size: 17px; font-weight: bold; color: #303133;">{{
                                 step.title
-                                }}</h4>
+                            }}</h4>
                             <p
                                 style="margin: 0 0 18px; color: #606266; font-size: 14px; line-height: 1.7; text-align: justify;">
                                 {{ step.desc }}</p>
@@ -492,7 +492,6 @@ import { getFulfiller } from '@/api/fulfiller/fulfiller/index'
 import { listSubOrderLog, exportSubOrderLogUrl } from '@/api/order/subOrderLog/index'
 import { listComplaintByOrder } from '@/api/fulfiller/complaint'
 import { listSubOrderAppealByOrderId } from '@/api/order/subOrderAppeal'
-import { listByIds, downloadOssBlob } from '@/api/system/oss'
 import ImagePreview from '@/components/ImagePreview/index.vue'
 
 const { proxy } = getCurrentInstance()
@@ -551,24 +550,7 @@ const loadOrderLogs = async (order) => {
         const list = res?.data?.data || res?.data || []
         const arr = Array.isArray(list) ? list : []
         orderLogs.value = arr.filter(i => Number(i?.logType) === 0)
-
-        // 对履约者日志中含有 photos(ossId 串)的条目,调用 listByIds 解析出可访问的 OSS URL
-        const fLogs = arr.filter(i => Number(i?.logType) === 1)
-        await Promise.all(fLogs.map(async (item) => {
-            const photoIds = item?.photos
-            if (photoIds) {
-                try {
-                    const ossRes = await listByIds(photoIds)
-                    const ossList = ossRes?.data || []
-                    item._resolvedUrls = ossList.map(o => ({ type: isVideo(o.url) ? 'video' : 'image', url: o.url, ossId: o.ossId }))
-                } catch {
-                    item._resolvedUrls = []
-                }
-            } else {
-                item._resolvedUrls = []
-            }
-        }))
-        fulfillerLogs.value = fLogs
+        fulfillerLogs.value = arr.filter(i => Number(i?.logType) === 1)
     } catch {
         orderLogs.value = []
         fulfillerLogs.value = []
@@ -827,8 +809,10 @@ const acceptTime = computed(() => orderLogs.value.find(l => l.title === '接单
 const serviceProgressSteps = computed(() => {
     const list = fulfillerLogs.value || []
     return list.map((i) => {
-        // 使用 _resolvedUrls(通过 listByIds 解析后的 OSS 可访问 URL),而非直接使用 photoUrls
-        const media = Array.isArray(i?._resolvedUrls) ? i._resolvedUrls : []
+        const urls = i?.photoUrls || []
+        const media = Array.isArray(urls)
+            ? urls.map(url => ({ type: isVideo(url) ? 'video' : 'image', url }))
+            : []
 
         return {
             title: i?.title || '--',
@@ -872,28 +856,20 @@ const captureTime = ref('')
 const captureBase64Cache = ref({})
 
 /**
- * 将图片/OSS对象转换为 base64 DataURL @Author: Antigravity
- * 优先采用后端代理下载以解决跨域问题,如果无 ossId 则尝试直接 fetch
+ * 将图片URL转换为 base64 DataURL @Author: Antigravity
  */
-const loadImageAsBase64 = async (item) => {
-    const { ossId, url } = item;
+const loadImageAsBase64 = async (url) => {
     try {
-        let blob;
-        if (ossId) {
-            blob = await downloadOssBlob(ossId);
-        } else if (url) {
-            const response = await fetch(url);
-            blob = await response.blob();
-        }
+        const response = await fetch(url);
+        const blob = await response.blob();
 
         if (!blob || blob.size === 0) {
-            console.warn('[FlowChart] 下载到的 Blob 为空:', url || ossId);
+            console.warn('[FlowChart] 下载到的 Blob 为空:', url);
             return '';
         }
 
-        console.log(`[FlowChart] 成功获取 Blob: 尺寸=${blob.size}, 类型=${blob.type}, ID=${ossId || 'N/A'}`);
+        console.log(`[FlowChart] 成功获取 Blob: 尺寸=${blob.size}, 类型=${blob.type}`);
 
-        // 如果是报错返回的 JSON
         if (blob.type === 'application/json') {
             const text = await blob.text();
             console.error('[FlowChart] 图片下载返回了错误 JSON:', text);
@@ -904,11 +880,10 @@ const loadImageAsBase64 = async (item) => {
             const reader = new FileReader();
             reader.onloadend = () => {
                 const result = reader.result;
-                // 只要是有效的 DataURL 且长度合理就通过,不再严格限制 image/ 前缀
                 if (typeof result === 'string' && result.length > 100) {
                     resolve(result);
                 } else {
-                    console.warn('[FlowChart] 生成的 Base64 长度不足或无效:', url || ossId);
+                    console.warn('[FlowChart] 生成的 Base64 长度不足或无效:', url);
                     resolve('');
                 }
             };
@@ -919,7 +894,7 @@ const loadImageAsBase64 = async (item) => {
             reader.readAsDataURL(blob);
         });
     } catch (err) {
-        console.error('[FlowChart] 图片加载异常:', url || ossId, err);
+        console.error('[FlowChart] 图片加载异常:', url, err);
         return '';
     }
 }
@@ -942,15 +917,14 @@ const handleExportProgressImage = async () => {
     captureTime.value = new Date().toLocaleString()
     captureBase64Cache.value = {}
 
-    // 收集所有需要预加载的图片 @Author: Antigravity
+    // 收集所有需要预加载的图片URL @Author: Antigravity
     const itemsToLoad = []
     for (const step of serviceProgressSteps.value) {
         if (!step.media || !step.media.length) continue
         for (const mediaItem of step.media) {
             if (mediaItem.type === 'image' && mediaItem.url) {
-                // 仅收集还未加载过的
                 if (!captureBase64Cache.value[mediaItem.url]) {
-                    itemsToLoad.push(mediaItem)
+                    itemsToLoad.push(mediaItem.url)
                 }
             }
         }
@@ -960,9 +934,9 @@ const handleExportProgressImage = async () => {
     if (itemsToLoad.length > 0) {
         console.log(`[FlowChart] 开始预加载 ${itemsToLoad.length} 张图片...`);
         const results = await Promise.all(
-            itemsToLoad.map(async (item) => {
-                const b64 = await loadImageAsBase64(item);
-                return { url: item.url, b64 };
+            itemsToLoad.map(async (url) => {
+                const b64 = await loadImageAsBase64(url);
+                return { url, b64 };
             })
         );
 

+ 73 - 80
src/views/systemConfig/protocol/index.vue

@@ -1,47 +1,30 @@
 <template>
   <div class="protocol-config-setting">
-    <!-- 顶部提示信息 -->
-    <div class="setting-hint">
-      <el-alert :closable="false" type="info" class="custom-alert">
-        <template #title>
-          <div class="alert-content">
-            <el-icon class="info-icon">
-              <InfoFilled />
-            </el-icon>
-            <span>协议配置:目前支持用户协议、隐私政策、履约者说明、托运协议、宠物洗护服务规范;请确保内容准确合规。</span>
-          </div>
-        </template>
-      </el-alert>
-    </div>
-
-    <!-- 配置表单区 -->
     <div class="setting-body">
+      <!-- 一级 Tab:平台 -->
+      <el-tabs v-model="activePlatform" class="platform-tabs" @tab-change="handlePlatformChange">
+        <el-tab-pane label="好萌友" name="haomengyou" />
+        <el-tab-pane label="履约守护" name="fulfiller" />
+      </el-tabs>
+
+      <!-- 二级 Tab:协议类型 -->
+      <el-tabs v-model="activeProtocol" class="protocol-tabs" @tab-change="handleProtocolChange">
+        <el-tab-pane v-for="item in currentProtocols" :key="item.id" :label="item.label" :name="String(item.id)" />
+      </el-tabs>
+
+      <!-- 配置表单区 -->
       <el-form ref="protocolFormRef" :model="form" :rules="rules" label-width="140px" label-position="right"
-        class="premium-setting-form">
-
-        <!-- 协议选择 -->
-        <el-form-item label="协议选择:">
-          <el-radio-group v-model="currentId" @change="handleTypeChange" class="custom-radio-group">
-            <el-radio :label="1" border>用户协议</el-radio>
-            <el-radio :label="2" border>隐私政策</el-radio>
-            <el-radio :label="3" border>履约者说明</el-radio>
-            <el-radio :label="4" border>托运协议</el-radio>
-            <el-radio :label="5" border>宠物洗护服务规范</el-radio>
-          </el-radio-group>
-        </el-form-item>
+        class="premium-setting-form" :key="currentId">
 
-        <!-- 协议标题 -->
         <el-form-item label="协议标题:" prop="title">
           <el-input v-model="form.title" placeholder="请输入协议标题" class="config-input" />
         </el-form-item>
 
-        <!-- 协议内容 -->
         <el-form-item label="协议内容:" prop="content">
           <Editor v-model="form.content" :min-height="400" class="config-editor" />
           <div class="form-tip">协议内容将以 HTML 格式保存,支持图片和视频插入。</div>
         </el-form-item>
 
-        <!-- 保存按钮 -->
         <el-form-item class="action-item">
           <el-button type="primary" class="save-btn" :loading="buttonLoading" @click="submitForm">保存设置</el-button>
         </el-form-item>
@@ -51,9 +34,8 @@
 </template>
 
 <script setup name="ProtocolConfig" lang="ts">
-import { ref, reactive, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
+import { ref, reactive, computed, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
 import { getAgreement, editAgreement } from '@/api/system/agreement';
-import { InfoFilled } from '@element-plus/icons-vue';
 import { ElForm } from 'element-plus';
 import Editor from '@/components/Editor/index.vue';
 
@@ -61,7 +43,32 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const buttonLoading = ref(false);
 const protocolFormRef = ref<InstanceType<typeof ElForm>>();
-const currentId = ref(1); // 默认选择用户协议 ID:1
+const activePlatform = ref('haomengyou');
+const currentId = ref(1);
+
+interface ProtocolItem {
+  id: number;
+  label: string;
+}
+
+const haomengyouProtocols: ProtocolItem[] = [
+  { id: 1, label: '用户协议' },
+  { id: 2, label: '隐私政策' },
+  // { id: 4, label: '托运协议' },
+  // { id: 5, label: '宠物洗护服务规范' }
+];
+
+const fulfillerProtocols: ProtocolItem[] = [
+  { id: 6, label: '用户协议' },
+  { id: 7, label: '隐私政策' },
+  { id: 3, label: '履约者说明' }
+];
+
+const currentProtocols = computed<ProtocolItem[]>(() => {
+  return activePlatform.value === 'haomengyou' ? haomengyouProtocols : fulfillerProtocols;
+});
+
+const activeProtocol = ref('1');
 
 const form = reactive({
   id: undefined,
@@ -74,7 +81,6 @@ const rules = {
   content: [{ required: true, message: "协议内容不能为空", trigger: "blur" }],
 };
 
-/** 加载协议配置 */
 const loadProtocolConfig = async (id: number) => {
   try {
     const res = await getAgreement(id);
@@ -86,18 +92,26 @@ const loadProtocolConfig = async (id: number) => {
   }
 };
 
-/** 切换协议 */
-const handleTypeChange = (val: any) => {
-  loadProtocolConfig(val as number);
+const handlePlatformChange = () => {
+  const first = currentProtocols.value[0];
+  if (first) {
+    activeProtocol.value = String(first.id);
+    currentId.value = first.id;
+    loadProtocolConfig(first.id);
+  }
+};
+
+const handleProtocolChange = (val: string | number) => {
+  const id = Number(val);
+  currentId.value = id;
+  loadProtocolConfig(id);
 };
 
-/** 提交按钮 */
 const submitForm = () => {
   protocolFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
       buttonLoading.value = true;
       try {
-        // 将 content 按照 Base64 方式编码,同时处理非西欧语言字符集
         const contentBase64 = btoa(unescape(encodeURIComponent(form.content)));
 
         const submitData = {
@@ -128,32 +142,29 @@ onMounted(() => {
   padding: 8px 0;
 }
 
-.setting-hint {
-  margin-bottom: 32px;
-
-  .custom-alert {
-    background-color: #e8f4ff;
-    border: none;
-    border-radius: 8px;
-    padding: 12px 16px;
-
-    .alert-content {
-      display: flex;
-      align-items: center;
-      gap: 10px;
-      color: #1890ff;
-      font-size: 14px;
-      line-height: 1.5;
-
-      .info-icon {
-        font-size: 18px;
-      }
-    }
+.setting-body {
+  padding-left: 20px;
+}
+
+.platform-tabs {
+  margin-bottom: 4px;
+
+  :deep(.el-tabs__header) {
+    margin-bottom: 8px;
+  }
+
+  :deep(.el-tabs__item) {
+    font-size: 15px;
+    font-weight: 500;
   }
 }
 
-.setting-body {
-  padding-left: 20px;
+.protocol-tabs {
+  margin-bottom: 24px;
+
+  :deep(.el-tabs__header) {
+    margin-bottom: 0;
+  }
 }
 
 .premium-setting-form {
@@ -189,24 +200,6 @@ onMounted(() => {
     line-height: 1.4;
   }
 
-  .custom-radio-group {
-    :deep(.el-radio) {
-      margin-right: 16px;
-      border-radius: 6px;
-      padding: 0 20px;
-      height: 36px;
-
-      &.is-bordered.is-checked {
-        border-color: #409eff;
-        background-color: rgba(64, 158, 255, 0.04);
-      }
-
-      .el-radio__label {
-        font-size: 14px;
-      }
-    }
-  }
-
   .action-item {
     margin-top: 40px;
   }

+ 2 - 2
vite.config.ts

@@ -25,8 +25,8 @@ export default defineConfig(({ mode, command }) => {
       // open: true,
       proxy: {
         [env.VITE_APP_BASE_API]: {
-          target: 'http://127.0.0.1:8080',
-          // target: 'http://www.hoomeng.pet/api',
+          // target: 'http://127.0.0.1:8080',
+          target: 'http://www.hoomeng.pet/api',
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')