Răsfoiți Sursa

异常上报初步完成;服务列表基本完成

Huanyi 1 lună în urmă
părinte
comite
b0461da184

+ 26 - 0
src/api/fulfiller/anamaly/index.ts

@@ -0,0 +1,26 @@
+import request from '@/utils/request';
+import type { AnamalyQuery, AnamalyForm } from './types';
+
+/**
+ * 获取异常上报列表
+ * @param query 查询参数
+ */
+export function getAnamalyList(query: AnamalyQuery) {
+    return request({
+        url: '/fulfiller/anamaly/list',
+        method: 'get',
+        params: query
+    });
+}
+
+/**
+ * 新增异常上报
+ * @param data 表单数据
+ */
+export function addAnamaly(data: AnamalyForm) {
+    return request({
+        url: '/fulfiller/anamaly/add',
+        method: 'post',
+        data: data
+    });
+}

+ 41 - 0
src/api/fulfiller/anamaly/types.ts

@@ -0,0 +1,41 @@
+export interface AnamalyQuery {
+    content?: string;
+    type?: string;
+    status?: number | string;
+    pageNum: number;
+    pageSize: number;
+}
+
+export interface AnamalyVO {
+    id: number;
+    fulfiller: number;
+    orderId: number;
+    type: string;
+    content: string;
+    photos: string;
+    photosUrls: string[];
+    status: number;
+    createTime: string;
+    // 以下为前端展示可能需要的扩充字段,因为在 Swagger 示例中可能没有体现
+    orderNo?: string;
+    fulfillerName?: string;
+    fulfillerPhone?: string;
+    auditRemark?: string;
+    auditTime?: string;
+}
+
+export interface AnamalyForm {
+    id?: number;
+    fulfiller?: number;
+    orderCode?: string;
+    type?: string;
+    content?: string;
+    photos?: string;
+    status?: number;
+    createDept?: number;
+    createBy?: number;
+    createTime?: string;
+    updateBy?: number;
+    updateTime?: string;
+    params?: any;
+}

+ 14 - 0
src/api/fulfiller/fulfiller/index.ts

@@ -0,0 +1,14 @@
+import request from '@/utils/request';
+import type { FulfillerSearchQuery } from './types';
+
+/**
+ * 模糊检索履约者(通过姓名和手机号)
+ * @param query 查询参数
+ */
+export function listByNameAndPhoneNumber(query: FulfillerSearchQuery) {
+    return request({
+        url: '/fulfiller/fulfiller/listByNameAndPhoneNumber',
+        method: 'get',
+        params: query
+    });
+}

+ 11 - 0
src/api/fulfiller/fulfiller/types.ts

@@ -0,0 +1,11 @@
+export interface FulfillerSearchQuery {
+    content?: string;
+    pageNum: number;
+    pageSize: number;
+}
+
+export interface FulfillerSearchResultVO {
+    id: number;
+    name: string;
+    phoneNumber: string;
+}

+ 170 - 198
src/views/fulfiller/anamaly/index.vue

@@ -7,48 +7,46 @@
             <span class="title">异常上报管理</span>
           </div>
           <div class="right-panel" style="display: flex; align-items: center; gap: 10px">
-            <el-input v-model="searchKey" placeholder="订单号/履约者姓名" prefix-icon="Search" clearable style="width: 220px" />
-            <el-select v-model="searchType" placeholder="异常类型" clearable style="width: 140px">
-              <el-option label="无法联系用户" value="contact_fail" />
-              <el-option label="地址错误" value="addr_error" />
-              <el-option label="宠物攻击性强" value="pet_aggressive" />
-              <el-option label="天气原因" value="weather_delay" />
-              <el-option label="突发意外" value="accident" />
+            <el-input v-model="queryParams.content" placeholder="订单号/履约者姓名" prefix-icon="Search" clearable @keyup.enter="handleQuery" style="width: 220px" />
+            <el-select v-model="queryParams.type" placeholder="异常类型" clearable @change="handleQuery" style="width: 140px">
+              <el-option v-for="dict in flf_anamaly_type" :key="dict.value" :label="dict.label" :value="dict.value" />
             </el-select>
-            <el-select v-model="searchStatus" placeholder="审核状态" clearable style="width: 120px">
+            <el-select v-model="queryParams.status" placeholder="审核状态" clearable @change="handleQuery" style="width: 120px">
               <el-option label="待审核" :value="0" />
               <el-option label="已通过" :value="1" />
               <el-option label="已驳回" :value="2" />
             </el-select>
+            <el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
             <el-button type="primary" icon="Plus" @click="handleAdd">新增上报</el-button>
           </div>
         </div>
       </template>
 
-      <el-table :data="filteredTableData" style="width: 100%" v-loading="loading">
-        <el-table-column prop="orderNo" label="关联订单号" width="180">
+      <el-table :data="tableData" style="width: 100%" v-loading="loading">
+        <el-table-column prop="orderId" label="关联订单号" width="180">
           <template #default="scope">
-            <el-link type="primary" :underline="false">{{ scope.row.orderNo }}</el-link>
+            <el-link type="primary" :underline="false">{{ scope.row.orderNo || scope.row.orderId }}</el-link>
           </template>
         </el-table-column>
         <el-table-column prop="type" label="异常类型" width="120">
           <template #default="scope">
-            <el-tag :type="getExceptionTag(scope.row.type)">{{ getExceptionLabel(scope.row.type) }}</el-tag>
+            <dict-tag :options="flf_anamaly_type" :value="scope.row.type" />
           </template>
         </el-table-column>
-        <el-table-column prop="fulfillerName" label="履约者" width="150">
+        <el-table-column prop="fulfiller" label="履约者" width="150">
           <template #default="scope">
-            <div>{{ scope.row.fulfillerName }}</div>
-            <div style="font-size: 12px; color: #999">{{ scope.row.fulfillerPhone }}</div>
+            <div>{{ scope.row.fulfillerName || scope.row.fulfiller }}</div>
+            <div style="font-size: 12px; color: #999" v-if="scope.row.fulfillerPhone">{{ scope.row.fulfillerPhone }}</div>
           </template>
         </el-table-column>
         <el-table-column prop="content" label="上报内容" show-overflow-tooltip />
-        <el-table-column prop="images" label="上报图片" width="120">
+        <el-table-column prop="photosUrls" label="上报图片" width="120">
           <template #default="scope">
-            <div v-if="scope.row.images && scope.row.images.length">
+            <div v-if="scope.row.photosUrls && scope.row.photosUrls.length">
               <el-image
-                :src="scope.row.images[0]"
-                :preview-src-list="scope.row.images"
+                :src="scope.row.photosUrls[0]"
+                :preview-src-list="scope.row.photosUrls"
                 fit="cover"
                 style="width: 40px; height: 40px; border-radius: 4px"
                 preview-teleported
@@ -59,7 +57,7 @@
                   </div>
                 </template>
               </el-image>
-              <span v-if="scope.row.images.length > 1" style="font-size: 12px; color: #999; margin-left: 5px">+{{ scope.row.images.length }}</span>
+              <span v-if="scope.row.photosUrls.length > 1" style="font-size: 12px; color: #999; margin-left: 5px">+{{ scope.row.photosUrls.length }}</span>
             </div>
             <span v-else style="color: #ccc">无图</span>
           </template>
@@ -89,13 +87,13 @@
 
       <div class="pagination-container">
         <el-pagination
-          v-model:current-page="currentPage"
-          v-model:page-size="pageSize"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
           :page-sizes="[10, 20, 50, 100]"
           layout="total, sizes, prev, pager, next, jumper"
           :total="total"
-          @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
+          @size-change="getList"
+          @current-change="getList"
         />
       </div>
     </el-card>
@@ -105,12 +103,12 @@
       <div class="drawer-content">
         <!-- 1. Basic Info -->
         <el-descriptions title="基础信息" :column="2" border>
-          <el-descriptions-item label="订单号">{{ currentItem.orderNo }}</el-descriptions-item>
+          <el-descriptions-item label="订单号">{{ currentItem.orderNo || currentItem.orderId }}</el-descriptions-item>
           <el-descriptions-item label="提交时间">{{ currentItem.createTime }}</el-descriptions-item>
-          <el-descriptions-item label="履约者">{{ currentItem.fulfillerName }}</el-descriptions-item>
-          <el-descriptions-item label="联系电话">{{ currentItem.fulfillerPhone }}</el-descriptions-item>
+          <el-descriptions-item label="履约者">{{ currentItem.fulfillerName || currentItem.fulfiller }}</el-descriptions-item>
+          <el-descriptions-item label="联系电话">{{ currentItem.fulfillerPhone || '-' }}</el-descriptions-item>
           <el-descriptions-item label="异常类型">
-            <el-tag :type="getExceptionTag(currentItem.type)">{{ getExceptionLabel(currentItem.type) }}</el-tag>
+            <dict-tag :options="flf_anamaly_type" :value="currentItem.type" />
           </el-descriptions-item>
           <el-descriptions-item label="当前状态">
             <el-tag v-if="currentItem.status === 0" type="warning">待审核</el-tag>
@@ -123,12 +121,12 @@
         <div class="section-block">
           <div class="section-title">上报内容</div>
           <div class="text-content">{{ currentItem.content }}</div>
-          <div class="image-list" v-if="currentItem.images && currentItem.images.length">
+          <div class="image-list" v-if="currentItem.photosUrls && currentItem.photosUrls.length">
             <el-image
-              v-for="(img, idx) in currentItem.images"
+              v-for="(img, idx) in currentItem.photosUrls"
               :key="idx"
               :src="img"
-              :preview-src-list="currentItem.images"
+              :preview-src-list="currentItem.photosUrls"
               :initial-index="idx"
               fit="cover"
               class="detail-img"
@@ -163,12 +161,12 @@
             <el-timeline-item :timestamp="currentItem.createTime" placement="top" type="primary">
               <el-card shadow="never" class="log-card">
                 <h4>提交上报</h4>
-                <p>履约者 {{ currentItem.fulfillerName }} 提交了异常上报</p>
+                <p>履约者 {{ currentItem.fulfillerName || currentItem.fulfiller }} 提交了异常上报</p>
               </el-card>
             </el-timeline-item>
             <el-timeline-item
               v-if="currentItem.status !== 0"
-              :timestamp="currentItem.auditTime || '2026-02-05 18:00:00'"
+              :timestamp="currentItem.auditTime || '未知时间'"
               placement="top"
               :type="currentItem.status === 1 ? 'success' : 'danger'"
             >
@@ -187,36 +185,31 @@
     <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑异常' : '新增异常'" width="600px">
       <el-form :model="form" label-width="100px">
         <el-form-item label="关联订单" required>
-          <el-input v-model="form.orderNo" placeholder="请输入订单号" />
+          <el-input v-model="form.orderCode" placeholder="请输入订单号" />
         </el-form-item>
         <el-form-item label="履约者" required>
-          <el-select v-model="form.fulfillerId" filterable placeholder="请输入姓名/电话检索" style="width: 100%" @change="handleFulfillerSelect">
-            <el-option v-for="item in fulfillerOptions" :key="item.id" :label="item.name + ' - ' + item.phone" :value="item.id" />
-          </el-select>
+          <PageSelect
+            v-model="form.fulfiller"
+            :options="fulfillerOptions"
+            :total="fulfillerTotal"
+            :pageSize="fulfillerQueryParams.pageSize"
+            placeholder="请输入姓名/电话检索"
+            :filter-method="handleFulfillerSearch"
+            @page-change="handleFulfillerPageChange"
+            @change="handleFulfillerSelect"
+            style="width: 100%"
+          />
         </el-form-item>
         <el-form-item label="异常类型" required>
           <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
-            <el-option label="无法联系用户" value="contact_fail" />
-            <el-option label="地址错误/不存在" value="addr_error" />
-            <el-option label="宠物攻击性强" value="pet_aggressive" />
-            <el-option label="恶劣天气延迟" value="weather_delay" />
-            <el-option label="突发意外" value="accident" />
+            <el-option v-for="dict in flf_anamaly_type" :key="dict.value" :label="dict.label" :value="dict.value" />
           </el-select>
         </el-form-item>
         <el-form-item label="上报内容" required>
           <el-input v-model="form.content" type="textarea" :rows="3" placeholder="请详细描述异常情况..." />
         </el-form-item>
         <el-form-item label="现场图片">
-          <el-upload
-            action="#"
-            list-type="picture-card"
-            :auto-upload="false"
-            v-model:file-list="fileList"
-            :on-change="handleFileChange"
-            :on-remove="handleFileRemove"
-          >
-            <el-icon><Plus /></el-icon>
-          </el-upload>
+          <ImageUpload v-model="form.photos" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -229,158 +222,149 @@
   </div>
 </template>
 
-<script setup>
-import { ref, reactive, computed } from 'vue';
+<script setup lang="ts">
+import { ref, reactive, onMounted, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
+import { getAnamalyList, addAnamaly } from '@/api/fulfiller/anamaly';
+import type { AnamalyQuery, AnamalyVO, AnamalyForm } from '@/api/fulfiller/anamaly/types';
+import { listByNameAndPhoneNumber } from '@/api/fulfiller/fulfiller';
+import type { FulfillerSearchQuery } from '@/api/fulfiller/fulfiller/types';
+import PageSelect from '@/components/PageSelect/index.vue';
+import ImageUpload from '@/components/ImageUpload/index.vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { flf_anamaly_type } = toRefs<any>(proxy?.useDict('flf_anamaly_type'));
 
 const loading = ref(false);
-const searchKey = ref('');
-const searchType = ref('');
-const searchStatus = ref('');
-const currentPage = ref(1);
-const pageSize = ref(10);
-const total = ref(42);
+
+const queryParams = reactive<AnamalyQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  content: '',
+  type: '',
+  status: undefined
+});
+
+const total = ref(0);
+const tableData = ref<AnamalyVO[]>([]);
+
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await getAnamalyList(queryParams);
+    tableData.value = res.rows;
+    total.value = res.total;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryParams.content = '';
+  queryParams.type = '';
+  queryParams.status = undefined;
+  handleQuery();
+};
 
 // Dialog State (For Add/Edit)
 const dialogVisible = ref(false);
 const isEdit = ref(false);
-const fileList = ref([]);
-
-// Mock Fulfiller Search Options
-const fulfillerOptions = ref([
-  { id: 101, name: '李建国', phone: '13812341234' },
-  { id: 102, name: '王大力', phone: '13987654321' },
-  { id: 103, name: '张小妹', phone: '13766668888' },
-  { id: 104, name: '刘跑跑', phone: '13611112222' }
-]);
-
-const handleFulfillerSelect = (id) => {
-  const target = fulfillerOptions.value.find((item) => item.id === id);
-  if (target) {
-    form.fulfillerName = target.name;
-    form.fulfillerPhone = target.phone;
-  }
+const fulfillerOptions = ref<any[]>([]);
+const fulfillerTotal = ref(0);
+const fulfillerQueryParams = reactive<FulfillerSearchQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  content: ''
+});
+
+const getFulfillerList = async () => {
+  const res = await listByNameAndPhoneNumber(fulfillerQueryParams);
+  fulfillerTotal.value = res.total;
+  fulfillerOptions.value = res.rows.map((item: any) => ({
+    label: `${item.name} - ${item.phoneNumber}`,
+    value: item.id,
+    name: item.name,
+    phone: item.phoneNumber
+  }));
+};
+
+const handleFulfillerSearch = (query: string) => {
+  fulfillerQueryParams.content = query;
+  fulfillerQueryParams.pageNum = 1;
+  getFulfillerList();
+};
+
+const handleFulfillerPageChange = (page: number) => {
+  fulfillerQueryParams.pageNum = page;
+  getFulfillerList();
+};
+
+const handleFulfillerSelect = (id: any) => {
+  // 备用选择回调
 };
 
 // Drawer State (For Audit/View)
 const drawerVisible = ref(false);
 const drawerMode = ref('view'); // 'audit' or 'view'
-const currentItem = ref({});
+const currentItem = ref<Partial<AnamalyVO>>({});
 const auditForm = reactive({ result: 1, remark: '' });
 
-const form = reactive({
-  id: '',
-  orderNo: '',
-  fulfillerId: '',
-  fulfillerName: '',
-  fulfillerPhone: '',
+const form = reactive<AnamalyForm>({
+  id: undefined,
+  orderCode: undefined,
+  fulfiller: undefined,
   type: '',
   content: '',
-  images: [],
-  status: 0,
-  auditRemark: ''
-});
-
-// Mock Data
-const tableData = ref([
-  {
-    id: 1,
-    orderNo: 'ORD202402048805',
-    fulfillerName: '李建国',
-    fulfillerPhone: '13812341234',
-    type: 'contact_fail',
-    content: '到达指定地点后,多次拨打宠主电话无人接听,敲门无响应超过15分钟。',
-    images: [
-      'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
-      'https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png'
-    ],
-    status: 0,
-    createTime: '2026-02-04 14:30:00',
-    auditRemark: '',
-    auditTime: ''
-  },
-  {
-    id: 2,
-    orderNo: 'ORD202402048802',
-    fulfillerName: '王大力',
-    fulfillerPhone: '13987654321',
-    type: 'pet_aggressive',
-    content: '进门时金毛表现出强烈护食行为,试图攻击,无法进行喂食操作。',
-    images: [],
-    status: 1,
-    auditRemark: '已确认,取消本次订单,按此单结算跑腿费。',
-    createTime: '2026-02-04 10:15:00',
-    auditTime: '2026-02-04 11:00:00'
-  }
-]);
-
-const filteredTableData = computed(() => {
-  return tableData.value.filter((item) => {
-    const matchKey = !searchKey.value || item.orderNo.includes(searchKey.value) || item.fulfillerName.includes(searchKey.value);
-    const matchType = !searchType.value || item.type === searchType.value;
-    const matchStatus = searchStatus.value === '' || item.status === searchStatus.value;
-    return matchKey && matchType && matchStatus;
-  });
+  photos: '',
+  status: 0
 });
 
-// Helpers
-const getExceptionLabel = (type) => {
-  const map = {
-    'contact_fail': '无法联系用户',
-    'addr_error': '地址错误',
-    'pet_aggressive': '宠物攻击性强',
-    'weather_delay': '恶劣天气延迟',
-    'accident': '突发意外'
-  };
-  return map[type] || '其他';
-};
-
-const getExceptionTag = (type) => {
-  const map = {
-    'contact_fail': 'warning',
-    'addr_error': 'info',
-    'pet_aggressive': 'danger',
-    'weather_delay': 'primary',
-    'accident': 'danger'
-  };
-  return map[type] || 'info';
-};
-
 // Actions
 const handleAdd = () => {
   isEdit.value = false;
-  fileList.value = [];
   Object.assign(form, {
-    id: '',
-    orderNo: '',
-    fulfillerId: '',
-    fulfillerName: '',
-    fulfillerPhone: '',
+    id: undefined,
+    orderCode: undefined,
+    fulfiller: undefined,
     type: '',
     content: '',
-    images: [],
-    status: 0,
-    auditRemark: ''
+    photos: '',
+    status: 0
   });
+  getFulfillerList();
   dialogVisible.value = true;
 };
 
-const handleEdit = (row) => {
+const handleEdit = (row: AnamalyVO) => {
   isEdit.value = true;
-  Object.assign(form, JSON.parse(JSON.stringify(row)));
-  fileList.value = (row.images || []).map((url, i) => ({ name: `img${i}`, url: url }));
+  Object.assign(form, {
+    id: row.id,
+    orderCode: row.orderNo || String(row.orderId),
+    fulfiller: row.fulfiller,
+    type: row.type,
+    content: row.content,
+    photos: row.photos,
+    status: row.status
+  });
+  getFulfillerList();
   dialogVisible.value = true;
 };
 
-const handleDelete = (row) => {
+const handleDelete = (row: AnamalyVO) => {
   ElMessageBox.confirm('确定删除该条记录吗?', '警告', { type: 'warning' }).then(() => {
-    tableData.value = tableData.value.filter((item) => item.id !== row.id);
-    ElMessage.success('删除成功');
+    // 假设未来需要对接删除接口: await deleteAnamaly(row.id);
+    // tableData.value = tableData.value.filter((item) => item.id !== row.id);
+    ElMessage.success('请完善删除接口');
   });
 };
 
 // Open Drawer for Audit or View
-const handleOpenDrawer = (row, mode) => {
+const handleOpenDrawer = (row: AnamalyVO, mode: string) => {
   drawerMode.value = mode;
   currentItem.value = JSON.parse(JSON.stringify(row));
   // Reset audit form
@@ -392,43 +376,31 @@ const handleOpenDrawer = (row, mode) => {
 };
 
 const submitAudit = () => {
-  const target = tableData.value.find((i) => i.id === currentItem.value.id);
-  if (target) {
-    target.status = auditForm.result;
-    target.auditRemark = auditForm.remark;
-    target.auditTime = new Date().toLocaleString();
-    ElMessage.success('审核已提交');
-    drawerVisible.value = false;
-  }
+  // 假设未来对接审核接口: await auditAnamaly(currentItem.value.id, auditForm.result, auditForm.remark);
+  ElMessage.success('请完善审核接口');
+  drawerVisible.value = false;
 };
 
-const saveData = () => {
-  if (!form.orderNo || !form.type) return ElMessage.warning('请填写必填项');
+const saveData = async () => {
+  if (!form.orderCode || !form.type) return ElMessage.warning('请填写必填项');
 
-  form.images = fileList.value.map((f) => f.url || 'https://placeholder.com/img');
-
-  if (isEdit.value) {
-    const idx = tableData.value.findIndex((i) => i.id === form.id);
-    if (idx !== -1) Object.assign(tableData.value[idx], form);
-  } else {
-    tableData.value.unshift({
-      id: Date.now(),
-      ...form,
-      createTime: new Date().toLocaleString()
-    });
+  try {
+    if (!isEdit.value) {
+      await addAnamaly(form);
+      ElMessage.success('新增上报成功');
+    } else {
+      ElMessage.success('请完善编辑接口');
+    }
+    dialogVisible.value = false;
+    getList();
+  } catch (error) {
+    console.error(error);
   }
-  ElMessage.success('保存成功');
-  dialogVisible.value = false;
 };
 
-const handleFileChange = (uploadFile, uploadFiles) => {
-  fileList.value = uploadFiles;
-};
-const handleFileRemove = (uploadFile, uploadFiles) => {
-  fileList.value = uploadFiles;
-};
-const handleSizeChange = (val) => {};
-const handleCurrentChange = (val) => {};
+onMounted(() => {
+  getList();
+});
 </script>
 
 <style scoped>

+ 4 - 4
src/views/order/dispatch/index.vue

@@ -322,12 +322,12 @@ const refreshMarkers = () => {
                 <div style="background:#fff; border-radius:8px; padding: 12px; display: flex; align-items: center; gap: 10px; position: relative;">
                      <!-- Close Icon -->
                     <div style="position: absolute; top: 4px; right: 8px; color: #999; font-size: 16px; cursor: pointer;">×</div>
-                    
+
                     <!-- Icon Ring -->
                     <div style="width: 48px; height: 48px; border-radius: 50%; border: 4px solid #F56C6C; padding: 2px; flex-shrink: 0; box-sizing: border-box; display: flex; align-items: center; justify-content: center;">
                          <img src="${iconImg}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">
                     </div>
-                    
+
                     <!-- Info -->
                     <div style="flex: 1; overflow: hidden;">
                         <div style="font-weight: bold; font-size: 14px; color: #333; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${m.name}</div>
@@ -357,12 +357,12 @@ const refreshMarkers = () => {
                 <div style="background:#fff; border-radius:8px; padding: 10px; display: flex; align-items: center; gap: 10px; position: relative;">
                     <!-- Close Icon -->
                     <div style="position: absolute; top: 2px; right: 6px; color: #999; font-size: 14px; cursor: pointer;">×</div>
-                    
+
                     <!-- Avatar Ring -->
                     <div style="width: 44px; height: 44px; border-radius: 50%; border: 3px solid ${borderColor}; padding: 1px; flex-shrink: 0; box-sizing: border-box;">
                         <img src="${avatar}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">
                     </div>
-                    
+
                     <!-- Info -->
                     <div style="flex: 1; overflow: hidden;">
                         <div style="font-weight: bold; font-size: 13px; color: #333; margin-bottom: 2px;">[履约者]${r.name}</div>

+ 12 - 21
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -26,12 +26,12 @@
                                 @click="emit('cancel', order)">取消订单</el-button>
                         </template>
 
-                        <template v-if="order.status === 4">
+                        <template v-if="order.status === 3">
                             <el-button type="primary" icon="CircleCheck"
                                 @click="emit('command', 'complete', order)">确认完成</el-button>
                         </template>
 
-                        <template v-if="[4, 5].includes(order.status)">
+                        <template v-if="[3, 4].includes(order.status)">
                             <el-button icon="Notebook" @click="emit('care-summary', order)">护理小结</el-button>
                         </template>
 
@@ -42,7 +42,7 @@
                                 <el-dropdown-menu>
                                     <el-dropdown-item command="reward" icon="Trophy">奖惩操作</el-dropdown-item>
                                     <el-dropdown-item command="remark" icon="EditPen">订单备注</el-dropdown-item>
-                                    <el-dropdown-item command="delete" v-if="[5, 6].includes(order.status)" divided
+                                    <el-dropdown-item command="delete" v-if="[4, 5].includes(order.status)" divided
                                         icon="Delete" style="color: #f56c6c;">删除订单</el-dropdown-item>
                                 </el-dropdown-menu>
                             </template>
@@ -260,7 +260,7 @@
                                     @click="handleExportLogs">导出日志Excel</el-button>
                             </div>
                             <el-timeline>
-                                <el-timeline-item v-for="(log, index) in (orderLogs || [])" :key="index" :timestamp="''"
+                                <el-timeline-item v-for="(log, index) in (orderLogs || [])" :key="index" :timestamp="log.createTime || log.time || ''"
                                     :type="'primary'" :icon="undefined" placement="top">
                                     <div class="log-card">
                                         <div class="l-tit">{{ log.title }}</div>
@@ -383,11 +383,11 @@ watch(() => props.order, (val) => {
 const activeDetailTab = ref('basic')
 
 const getStatusName = (status) => {
-    const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '待商家确认', 5: '已完成', 6: '已取消' }
+    const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' }
     return map[status] || '未知'
 }
 const getStatusTag = (status) => {
-    const map = { 0: 'danger', 1: 'warning', 2: 'primary', 3: 'primary', 4: 'warning', 5: 'success', 6: 'info' }
+    const map = { 0: 'danger', 1: 'warning', 2: 'primary', 3: 'primary', 4: 'success', 5: 'info' }
     return map[status] || 'info'
 }
 const getTypeName = (type) => {
@@ -428,7 +428,6 @@ const currentOrderSteps = computed(() => {
         { title: '运营派单', status: 'dispatched', time: '' },
         { title: '履约接单', status: 'accepted', time: '' },
         { title: '服务中', status: 'serving', time: '' },
-        { title: '待商家确认', status: 'confirming', time: '' },
         { title: '已完成', status: 'completed', time: '' }
     ]
     const logs = orderLogs.value || []
@@ -445,11 +444,11 @@ const currentOrderSteps = computed(() => {
     } else {
         steps[1].time = findTime('派单') || ''
     }
-    if ([1, 2, 3, 4, 5].includes(status)) active = 2
+    if ([1, 2, 3, 4].includes(status)) active = 2
     steps[2].time = findTime('接单')
     if ([1].includes(status)) {
         steps[2].title = '待履约者接单'
-    } else if ([2, 3, 4, 5].includes(status)) {
+    } else if ([2, 3, 4].includes(status)) {
         steps[2].title = '履约者已接单'
         active = 3
     }
@@ -459,23 +458,15 @@ const currentOrderSteps = computed(() => {
     } else if ([3].includes(status)) {
         steps[3].title = '服务进行中'
         active = 4
-    } else if ([4, 5].includes(status)) {
+    } else if ([4].includes(status)) {
         steps[3].title = '服务已完成'
         active = 4
     }
-    steps[4].time = findTime('等待商家确认') || findTime('待验收')
-    if ([4].includes(status)) {
-        steps[4].title = '待商家确认'
-        active = 5
-    } else if ([5].includes(status)) {
-        steps[4].title = '商家已确认'
+    if (status === 4) {
+        steps[4].time = findTime('完成')
         active = 5
     }
     if (status === 5) {
-        steps[5].time = findTime('完成')
-        active = 6
-    }
-    if (status === 6) {
         return {
             active: 1,
             steps: [
@@ -497,7 +488,7 @@ const serviceProgressSteps = computed(() => {
             .map(url => ({ type: 'image', url }))
         return {
             title: i?.title || '--',
-            time: '',
+            time: i?.createTime || i?.time || '',
             icon: undefined,
             color: '#ff9900',
             desc: i?.content || '',

+ 7 - 8
src/views/order/orderList/index.vue

@@ -22,9 +22,8 @@
           <el-tab-pane label="待接单" name="1" />
           <el-tab-pane label="待服务" name="2" />
           <el-tab-pane label="服务中" name="3" />
-          <el-tab-pane label="待商家确认" name="4" />
-          <el-tab-pane label="已完成" name="5" />
-          <el-tab-pane label="已取消" name="6" />
+          <el-tab-pane label="已完成" name="4" />
+          <el-tab-pane label="已取消" name="5" />
         </el-tabs>
       </template>
 
@@ -125,7 +124,7 @@
               <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small"
                 @click="handleCancel(row)">取消</el-button>
 
-              <el-dropdown v-if="[3, 4, 5].includes(row.status)" trigger="click"
+              <el-dropdown v-if="[3, 4].includes(row.status)" trigger="click"
                 @command="(cmd) => handleCommand(cmd, row)">
                 <span class="el-dropdown-link">
                   更多<el-icon class="el-icon--right">
@@ -134,8 +133,8 @@
                 </span>
                 <template #dropdown>
                   <el-dropdown-menu>
-                    <el-dropdown-item v-if="row.status === 4" command="complete">确认完成</el-dropdown-item>
-                    <el-dropdown-item v-if="[4, 5].includes(row.status)" command="care_summary">护理小结</el-dropdown-item>
+                    <el-dropdown-item v-if="row.status === 3" command="complete">确认完成</el-dropdown-item>
+                    <el-dropdown-item v-if="row.status === 4" command="care_summary">护理小结</el-dropdown-item>
                     <el-dropdown-item command="reward">奖惩</el-dropdown-item>
                     <el-dropdown-item command="remark">备注</el-dropdown-item>
                   </el-dropdown-menu>
@@ -333,12 +332,12 @@ const getServiceOrderTypeTag = (row) => {
 };
 
 const getStatusName = (status) => {
-  const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '待商家确认', 5: '已完成', 6: '已取消' };
+  const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' };
   return map[status] || '未知';
 };
 
 const getStatusClass = (status) => {
-  const map = { 0: 'pending_dispatch', 1: 'pending_accept', 2: 'pending_service', 3: 'in_service', 4: 'pending_confirm', 5: 'completed', 6: 'cancelled' };
+  const map = { 0: 'pending_dispatch', 1: 'pending_accept', 2: 'pending_service', 3: 'in_service', 4: 'completed', 5: 'cancelled' };
   return map[status] || 'pending_dispatch';
 };
 

+ 47 - 2
src/views/service/list/index.vue

@@ -74,7 +74,7 @@
         v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
     <!-- 添加或修改服务项目对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="650px" append-to-body>
       <el-form ref="serviceFormRef" :model="form" :rules="rules" label-width="80px">
         <el-form-item label="服务名称" prop="name">
           <el-input v-model="form.name" placeholder="请输入服务名称" />
@@ -101,7 +101,21 @@
           <el-input v-model="form.remark" placeholder="请输入备注说明" />
         </el-form-item>
         <el-form-item label="打卡备注" prop="clockInRemark">
-          <el-input v-model="form.clockInRemark" placeholder="请输入打卡备注信息(将对小程序端展示)" />
+          <div class="w-full">
+            <el-card v-for="(item, index) in clockInRemarkList" :key="index" class="mb-2" shadow="never">
+              <template #header>
+                <div style="display: flex; align-items: center; justify-content: space-between;">
+                  <span>步骤 {{ index + 1 }}</span>
+                </div>
+              </template>
+              <el-form-item label="步骤标题" label-width="80px" style="margin-bottom: 12px;">
+                <el-input v-model="item.title" placeholder="请输入步骤标题" />
+              </el-form-item>
+              <el-form-item label="打卡备注" label-width="80px" style="margin-bottom: 0;">
+                <el-input v-model="item.remark" type="textarea" placeholder="请输入当前步骤打卡备注" />
+              </el-form-item>
+            </el-card>
+          </div>
         </el-form-item>
       </el-form>
       <template #footer>
@@ -121,6 +135,14 @@ import { list as listMode } from '@/api/service/mode';
 import { SysServiceModeVo } from '@/api/service/mode/types';
 import PageSelect from '@/components/PageSelect/index.vue';
 
+interface ClockInRemarkItem {
+  step: number;
+  title: string;
+  remark: string;
+}
+
+const clockInRemarkList = ref<ClockInRemarkItem[]>([]);
+
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const serviceList = ref<ServiceVO[]>([]);
@@ -215,9 +237,19 @@ const cancel = () => {
   dialog.visible = false;
 };
 
+/** 初始打卡备注三步骤 */
+const getInitialClockInRemarks = (): ClockInRemarkItem[] => {
+  return Array.from({ length: 3 }, (_, i) => ({
+    step: i + 1,
+    title: '',
+    remark: ''
+  }));
+};
+
 /** 表单重置 */
 const reset = () => {
   form.value = { ...initFormData };
+  clockInRemarkList.value = getInitialClockInRemarks();
   serviceFormRef.value?.resetFields();
 };
 
@@ -253,6 +285,18 @@ const handleUpdate = async (row?: ServiceVO) => {
   const _id = row?.id || ids.value[0];
   const res = await getService(_id);
   Object.assign(form.value, res.data);
+  if (form.value.clockInRemark) {
+    try {
+      const parsed = JSON.parse(form.value.clockInRemark);
+      if (Array.isArray(parsed)) {
+        clockInRemarkList.value = getInitialClockInRemarks().map((item, index) => {
+          return parsed[index] ? { ...item, ...parsed[index] } : item;
+        });
+      }
+    } catch (e) {
+      clockInRemarkList.value = getInitialClockInRemarks();
+    }
+  }
   dialog.visible = true;
   dialog.title = '修改服务项目';
 };
@@ -261,6 +305,7 @@ const handleUpdate = async (row?: ServiceVO) => {
 const submitForm = () => {
   serviceFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
+      form.value.clockInRemark = clockInRemarkList.value.length > 0 ? JSON.stringify(clockInRemarkList.value) : undefined;
       buttonLoading.value = true;
       if (form.value.id) {
         await updateService(form.value).finally(() => (buttonLoading.value = false));

+ 1 - 1
vite.config.ts

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