Jelajahi Sumber

修复部分bug

Huanyi 4 minggu lalu
induk
melakukan
35c45f22ef

+ 1 - 1
.env.production

@@ -15,7 +15,7 @@ VITE_APP_MONITOR_ADMIN = '/admin/applications'
 VITE_APP_SNAILJOB_ADMIN = '/snail-job'
 
 # 生产环境
-VITE_APP_BASE_API = '/prod-api'
+VITE_APP_BASE_API = 'http://8.136.194.143/api'
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip

+ 3 - 0
src/api/archieves/customer/types.ts

@@ -22,6 +22,8 @@ export interface UsrCustomerVO {
   remark: string;
   createTime: string;
   tags: SysTagVO[];
+  tenantId: string | number;
+  tenantName: string;
 }
 
 export interface UsrCustomerForm extends BaseEntity {
@@ -41,6 +43,7 @@ export interface UsrCustomerForm extends BaseEntity {
   status?: number;
   remark?: string;
   tagIds?: (string | number)[];
+  tenantId?: string | number;
 }
 
 export interface UsrCustomerQuery extends PageQuery {

+ 2 - 1
src/api/fulfiller/pool/types.ts

@@ -157,7 +157,7 @@ export interface FlfBalanceLogVO {
   id: string | number;
   fulfillerId: string | number;
   type: string;
-  subType: string;
+  bizType: string;
   amount: number;
   balanceAfter: number;
   reason: string;
@@ -172,6 +172,7 @@ export interface FlfRewardLogVO {
   id: string | number;
   fulfillerId: string | number;
   type: string;
+  bizType: string;
   target: string;
   amount: number;
   reason: string;

+ 48 - 0
src/api/order/subOrder/index.ts

@@ -73,3 +73,51 @@ export const nursingSummarySubOrder = (data: { orderId: string | number; content
         data
     });
 };
+
+/**
+ * 查询客户涉及的子订单列表
+ * @param customerId
+ */
+export const listSubOrderOnCustomer = (customerId: string | number): AxiosPromise<SubOrderVO[]> => {
+    return request({
+        url: '/order/subOrder/listOnCustomer',
+        method: 'get',
+        params: { customerId }
+    });
+};
+
+/**
+ * 查询宠物涉及的子订单列表
+ * @param petId
+ */
+export const listSubOrderOnPet = (petId: string | number): AxiosPromise<SubOrderVO[]> => {
+    return request({
+        url: '/order/subOrder/listOnPet',
+        method: 'get',
+        params: { petId }
+    });
+};
+
+/**
+ * 查询履约者涉及的子订单列表
+ * @param fulfillerId
+ */
+export const listSubOrderOnFulfiller = (fulfillerId: string | number): AxiosPromise<SubOrderVO[]> => {
+    return request({
+        url: '/order/subOrder/listOnFulfiller',
+        method: 'get',
+        params: { fulfillerId }
+    });
+};
+
+/**
+ * 分页查询门店关联的子订单列表
+ * @param query
+ */
+export const listSubOrderOnStore = (query: any): AxiosPromise<SubOrderVO[]> => {
+    return request({
+        url: '/order/subOrder/listOnStore',
+        method: 'get',
+        params: query
+    });
+};

+ 10 - 2
src/api/order/subOrderLog/index.ts

@@ -2,10 +2,18 @@ import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
 import { SubOrderLogVO, SubOrderLogQuery } from './types';
 
-export const listSubOrderLog = (query: SubOrderLogQuery): AxiosPromise<SubOrderLogVO[]> => {
+export function listSubOrderLog(query: SubOrderLogQuery): AxiosPromise<SubOrderLogVO[]> {
     return request({
         url: '/order/subOrderLog/list',
         method: 'get',
         params: query
     });
-};
+}
+
+/**
+ * 导出订单日志
+ * @param orderId 订单ID
+ */
+export function exportSubOrderLogUrl(orderId: string | number) {
+    return `/order/subOrderLog/export/${orderId}`;
+}

+ 7 - 5
src/api/order/subOrderLog/types.ts

@@ -1,13 +1,15 @@
 export interface SubOrderLogVO {
-    id: number;
-    subOrderId: number;
-    actioner: number;
+    id: string | number;
+    subOrderId: string | number;
+    actioner: string | number;
     actionerType: number;
     logType: number;
-    actionType: number;
+    step?: number;
     title: string;
     content: string;
-    photos?: string;
+    photos?: string | null;
+    photoUrls?: string[] | null;
+    createTime: string;
 }
 
 export interface SubOrderLogQuery {

+ 301 - 0
src/components/CustomerDetailDrawer/index.vue

@@ -0,0 +1,301 @@
+<template>
+  <el-drawer v-model="drawerVisible" title="用户档案详情" size="60%" destroy-on-close>
+    <div class="profile-header">
+      <el-avatar :size="80" :src="currentUser.avatarUrl" />
+      <div class="profile-basic">
+        <div class="name-row">
+          <span class="name">{{ currentUser.name }}</span>
+          <dict-tag :options="sys_user_sex" :value="currentUser.gender" />
+          <span class="phone">{{ currentUser.phone }}</span>
+        </div>
+        <div class="tags-row" style="margin-top: 8px">
+          <el-tag v-for="tag in currentUser.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">
+            {{ tag.name }}
+          </el-tag>
+        </div>
+      </div>
+    </div>
+
+    <el-tabs v-model="detailActiveTab" class="profile-tabs">
+      <el-tab-pane label="档案信息" name="info">
+        <div class="section-title">基本信息</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="姓名">{{ currentUser.name }}</el-descriptions-item>
+          <el-descriptions-item label="电话">{{ currentUser.phone }}</el-descriptions-item>
+          <el-descriptions-item label="所属区域">{{ currentUser.areaName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="所属站点">{{ currentUser.stationName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="录入来源">{{ currentUser.source || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="录入时间">{{ currentUser.createTime || '-' }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="section-title" style="margin-top: 20px">居住信息</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="详细住址" :span="2">{{ currentUser.address }}</el-descriptions-item>
+          <el-descriptions-item label="房屋类型">
+            <dict-tag :options="sys_house_type" :value="currentUser.houseType" />
+          </el-descriptions-item>
+          <el-descriptions-item label="入门方式">
+            <dict-tag :options="sys_entry_method" :value="currentUser.entryMethod" />
+          </el-descriptions-item>
+          <el-descriptions-item label="开门详情" :span="2">
+            {{ currentUser.entryMethod === 'password' ? currentUser.entryPassword : currentUser.keyLocation }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-tab-pane>
+
+      <el-tab-pane label="宠物列表" name="pets">
+        <div v-if="editable" style="margin-bottom: 15px">
+          <el-button type="primary" size="small" icon="Plus" @click="emit('add-pet')">新增宠物</el-button>
+        </div>
+        <el-table :data="currentPets" border style="width: 100%">
+          <el-table-column label="宠物信息" width="200">
+            <template #default="scope">
+              <div style="display: flex; align-items: center">
+                <el-avatar
+                  :size="30"
+                  :src="scope.row.avatarUrl || 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'"
+                  style="margin-right: 8px"
+                />
+                {{ scope.row.name }}
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column prop="breed" label="品种" />
+          <el-table-column label="性别" width="60" align="center">
+            <template #default="scope">
+              <dict-tag :options="sys_pet_gender" :value="String(scope.row.gender)" />
+            </template>
+          </el-table-column>
+          <el-table-column prop="age" label="年龄" width="60" />
+          <el-table-column prop="status" label="健康状态">
+            <template #default="scope">
+              <el-tag :type="getStatusTagType(scope.row.status)" size="small">{{ scope.row.healthStatus }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="疫苗接种" width="120" align="center">
+            <template #default="scope">
+              {{ scope.row.vaccineStatus || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column v-if="editable" label="操作" width="200" align="center">
+            <template #default="scope">
+              <el-button link type="primary" @click="emit('pet-detail', scope.row)">详情</el-button>
+              <el-button link type="primary" @click="emit('pet-edit', scope.row)">编辑</el-button>
+              <el-button link type="primary" @click="emit('pet-remark', scope.row)">备注</el-button>
+              <el-button link type="danger" @click="emit('pet-delete', scope.row)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-tab-pane>
+
+      <el-tab-pane label="历史订单" name="orders">
+        <el-table :data="historyOrders" border style="width: 100%">
+          <el-table-column prop="code" label="订单编号" width="180" />
+          <el-table-column label="服务项目">
+            <template #default="scope">
+              {{ getServiceName(scope.row.service) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="pet" label="服务宠物" />
+          <el-table-column prop="serviceTime" label="服务时间" width="180" />
+          <el-table-column prop="status" label="状态" width="100">
+            <template #default="scope">
+              <el-tag :type="getOrderStatusType(scope.row.status)" size="small">{{ getOrderStatusName(scope.row.status) }}</el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-tab-pane>
+
+      <el-tab-pane label="档案日志" name="logs">
+        <el-timeline style="margin-top: 10px; padding-left: 5px">
+          <el-timeline-item v-for="(log, index) in changeLogs" :key="index" :timestamp="log.createTime" type="primary">
+            [{{ log.logType }}] {{ log.content }}
+            <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operatorName }}</div>
+          </el-timeline-item>
+        </el-timeline>
+      </el-tab-pane>
+    </el-tabs>
+  </el-drawer>
+</template>
+
+<script setup name="CustomerDetailDrawer">
+import { ref, computed, watch, onMounted, getCurrentInstance, toRefs } from 'vue';
+import { getCustomer } from '@/api/archieves/customer';
+import { listPetByUser } from '@/api/archieves/pet';
+import { listAllChangeLog } from '@/api/archieves/changeLog';
+import { listOnStore } from '@/api/system/areaStation';
+import { listSubOrderOnCustomer } from '@/api/order/subOrder/index';
+import { listServiceOnOrder } from '@/api/service/list/index';
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  customerId: {
+    type: [String, Number],
+    default: null
+  },
+  editable: {
+    type: Boolean,
+    default: false
+  }
+});
+
+const emit = defineEmits(['update:visible', 'add-pet', 'pet-detail', 'pet-edit', 'pet-remark', 'pet-delete']);
+
+const drawerVisible = computed({
+  get: () => props.visible,
+  set: (val) => emit('update:visible', val)
+});
+
+const { proxy } = getCurrentInstance();
+const { sys_user_sex, sys_house_type, sys_entry_method, sys_pet_gender } = toRefs(proxy?.useDict('sys_user_sex', 'sys_house_type', 'sys_entry_method', 'sys_pet_gender'));
+
+const currentUser = ref({});
+const currentPets = ref([]);
+const changeLogs = ref([]);
+const historyOrders = ref([]);
+const serviceOptions = ref([]);
+const detailActiveTab = ref('info');
+const allNodes = ref([]);
+
+const getServiceList = () => {
+  listServiceOnOrder().then((res) => {
+    serviceOptions.value = res.data || [];
+  });
+};
+
+const getServiceName = (serviceId) => {
+  const item = serviceOptions.value.find((i) => i.id === serviceId);
+  return item ? item.name : '未知服务';
+};
+
+const getOrderStatusName = (status) => {
+  const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' };
+  return map[status] || '未知';
+};
+
+const getOrderStatusType = (status) => {
+  const map = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: 'success', 5: 'danger' };
+  return map[status] || 'info';
+};
+
+const getStatusTagType = (status) => {
+  return status === '健康' ? 'success' : 'warning';
+};
+
+const loadAreaStation = async () => {
+  if (allNodes.value.length === 0) {
+    const res = await listOnStore();
+    allNodes.value = res.data || [];
+  }
+};
+
+const loadHistoryOrders = (customerId) => {
+  listSubOrderOnCustomer(customerId).then((res) => {
+    historyOrders.value = res.data || [];
+  });
+};
+
+const loadData = async (customerId) => {
+  if (!customerId) return;
+
+  currentUser.value = {};
+  currentPets.value = [];
+  changeLogs.value = [];
+  historyOrders.value = [];
+
+  await loadAreaStation();
+
+  // 并发请求数据
+  getCustomer(customerId).then((res) => {
+    const data = res.data;
+    if (data.areaId) {
+      const area = allNodes.value.find((n) => n.id === data.areaId);
+      data.areaName = area ? area.name : '-';
+    } else {
+      data.areaName = '-';
+    }
+    if (data.stationId) {
+      const station = allNodes.value.find((n) => n.id === data.stationId);
+      data.stationName = station ? station.name : '-';
+    } else {
+      data.stationName = '-';
+    }
+    currentUser.value = data;
+  });
+
+  listPetByUser(customerId).then((res) => {
+    currentPets.value = res.data || [];
+  });
+
+  listAllChangeLog(customerId, 'customer').then((res) => {
+    changeLogs.value = res.data || [];
+  });
+
+  loadHistoryOrders(customerId);
+};
+
+// 暴露刷新方法给父组件使用
+const refresh = () => {
+  if (props.customerId) {
+    loadData(props.customerId);
+  }
+};
+
+defineExpose({ refresh });
+
+watch(
+  () => props.visible,
+  (val) => {
+    if (val && props.customerId) {
+      detailActiveTab.value = 'info';
+      loadData(props.customerId);
+    }
+  }
+);
+
+onMounted(() => {
+  getServiceList();
+});
+</script>
+
+<style scoped>
+.profile-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  padding-bottom: 20px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.profile-basic {
+  margin-left: 20px;
+}
+
+.name-row {
+  display: flex;
+  align-items: center;
+}
+
+.name {
+  font-size: 20px;
+  font-weight: bold;
+  color: #303133;
+}
+
+.phone {
+  margin-left: 10px;
+  color: #666;
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 15px;
+  border-left: 4px solid #409eff;
+  padding-left: 10px;
+  line-height: 1.2;
+}
+</style>

+ 264 - 0
src/components/PetDetailDrawer/index.vue

@@ -0,0 +1,264 @@
+<template>
+  <el-drawer v-model="drawerVisible" title="宠物档案详情" size="60%" destroy-on-close>
+    <div class="profile-header">
+      <el-avatar :size="80" :src="currentPet.avatarUrl" />
+      <div class="profile-basic">
+        <div class="name-row">
+          <span class="name">{{ currentPet.name }}</span>
+          <dict-tag :options="sys_pet_gender" :value="currentPet.gender" />
+          <el-tag size="small" effect="plain" type="info" style="margin-left: 5px">{{ currentPet.age }}岁</el-tag>
+        </div>
+        <div class="tags-row" style="margin-top: 8px">
+          <el-tag v-for="tag in currentPet.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small"
+            style="margin-right: 5px">
+            {{ tag.name }}
+          </el-tag>
+        </div>
+      </div>
+      <div v-if="editable" style="margin-left: auto">
+        <el-button type="primary" size="small" plain @click="handleRemark">添加备注</el-button>
+      </div>
+    </div>
+
+    <el-tabs v-model="detailActiveTab" class="profile-tabs">
+      <el-tab-pane label="档案信息" name="info">
+        <div class="section-title">基本信息</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="品种">{{ currentPet.breed }}</el-descriptions-item>
+          <el-descriptions-item label="体型">
+            <dict-tag :options="sys_pet_size" :value="currentPet.size" />
+          </el-descriptions-item>
+          <el-descriptions-item label="体重">{{ currentPet.weight }} kg</el-descriptions-item>
+          <el-descriptions-item label="所属主人">{{ currentPet.ownerName }} ({{ currentPet.ownerPhone }})</el-descriptions-item>
+          <el-descriptions-item label="性格关键词">{{ currentPet.personality || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="萌宠性格" :span="2">{{ currentPet.cutePersonality || '-' }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="section-title" style="margin-top: 20px">家庭信息</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="到家时间">{{ currentPet.arrivalTime || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="房屋类型">
+            <dict-tag :options="sys_house_type" :value="currentPet.houseType" />
+          </el-descriptions-item>
+          <el-descriptions-item label="入门方式">
+            <dict-tag :options="sys_entry_method" :value="currentPet.entryMethod" />
+          </el-descriptions-item>
+          <el-descriptions-item label="开门详情">
+            {{ currentPet.entryMethod === 'password' ? currentPet.entryPassword : currentPet.keyLocation }}
+          </el-descriptions-item>
+        </el-descriptions>
+
+        <div class="section-title" style="margin-top: 20px">健康状况</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="健康状态">
+            <el-tag :type="currentPet.healthStatus === '健康' ? 'success' : 'warning'" size="small">{{ currentPet.healthStatus }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="攻击倾向">
+            <el-tag :type="currentPet.aggression ? 'danger' : 'success'" size="small">{{ currentPet.aggression ? '有' : '无' }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="疫苗情况" :span="2">
+            <div>{{ currentPet.vaccineStatus || '-' }}</div>
+            <div v-if="currentPet.vaccineCertUrl" style="margin-top: 10px">
+              <el-image style="width: 100px; height: 100px; border-radius: 4px" :src="currentPet.vaccineCertUrl"
+                :preview-src-list="[currentPet.vaccineCertUrl]" fit="cover" />
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="既往病史" :span="2">{{ currentPet.medicalHistory || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="过敏史" :span="2">{{ currentPet.allergies || '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </el-tab-pane>
+
+      <el-tab-pane label="历史订单" name="orders">
+        <el-table :data="historyOrders" border style="width: 100%">
+          <el-table-column prop="code" label="订单编号" width="180" />
+          <el-table-column label="服务项目">
+            <template #default="scope">
+              {{ getServiceName(scope.row.service) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="serviceTime" label="服务时间" width="180" />
+          <el-table-column prop="price" label="金额" width="100">
+            <template #default="scope">
+              {{ scope.row.price ? (scope.row.price / 100).toFixed(2) : '0.00' }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="status" label="状态" width="100">
+            <template #default="scope">
+              <el-tag :type="getOrderStatusType(scope.row.status)" size="small">
+                {{ getOrderStatusName(scope.row.status) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-tab-pane>
+
+      <el-tab-pane label="备注日志" name="logs">
+        <el-timeline style="margin-top: 10px; padding-left: 5px">
+          <el-timeline-item v-for="(log, index) in changeLogs" :key="index" :timestamp="log.createTime" type="primary">
+            [{{ log.logType }}] {{ log.content }}
+            <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operatorName }}</div>
+          </el-timeline-item>
+        </el-timeline>
+      </el-tab-pane>
+    </el-tabs>
+
+    <!-- Remark Dialog (Internal) -->
+    <el-dialog v-model="remarkDialogVisible" title="添加备注" width="400px" append-to-body>
+      <el-input v-model="remarkContent" type="textarea" :rows="3" placeholder="请输入备注内容..." />
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="remarkDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="saveRemark">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </el-drawer>
+</template>
+
+<script setup name="PetDetailDrawer">
+import { ref, computed, watch, onMounted, getCurrentInstance, toRefs } from 'vue'
+import { getPet, updatePet } from '@/api/archieves/pet'
+import { listAllChangeLog } from '@/api/archieves/changeLog'
+import { listSubOrderOnPet } from '@/api/order/subOrder/index'
+import { listServiceOnOrder } from '@/api/service/list/index'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  petId: {
+    type: [String, Number],
+    default: null
+  },
+  editable: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits(['update:visible', 'remark-saved'])
+
+const drawerVisible = computed({
+  get: () => props.visible,
+  set: (val) => emit('update:visible', val)
+})
+
+const { proxy } = getCurrentInstance()
+const { sys_pet_gender, sys_pet_size, sys_house_type, sys_entry_method } = toRefs(
+  proxy?.useDict('sys_pet_gender', 'sys_pet_size', 'sys_house_type', 'sys_entry_method')
+)
+
+const currentPet = ref({})
+const changeLogs = ref([])
+const historyOrders = ref([])
+const serviceOptions = ref([])
+const detailActiveTab = ref('info')
+const remarkDialogVisible = ref(false)
+const remarkContent = ref('')
+
+const getServiceList = () => {
+  listServiceOnOrder().then((res) => {
+    serviceOptions.value = res.data || []
+  })
+}
+
+const getServiceName = (serviceId) => {
+  const item = serviceOptions.value.find((i) => i.id === serviceId)
+  return item ? item.name : '未知服务'
+}
+
+const getOrderStatusName = (status) => {
+  const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' }
+  return map[status] || '未知'
+}
+
+const getOrderStatusType = (status) => {
+  const map = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: 'success', 5: 'danger' }
+  return map[status] || 'info'
+}
+
+const loadData = async (id) => {
+  if (!id) return
+  currentPet.value = {}
+  changeLogs.value = []
+  historyOrders.value = []
+
+  getPet(id).then((res) => {
+    currentPet.value = res.data || {}
+  })
+
+  listAllChangeLog(id, 'pet').then((res) => {
+    changeLogs.value = res.data || []
+  })
+
+  listSubOrderOnPet(id).then((res) => {
+    historyOrders.value = res.data || []
+  })
+}
+
+const handleRemark = () => {
+  remarkContent.value = ''
+  remarkDialogVisible.value = true
+}
+
+const saveRemark = () => {
+  if (!remarkContent.value) return ElMessage.warning('请输入内容')
+  const data = { id: props.petId, remark: remarkContent.value }
+  updatePet(data).then(() => {
+    ElMessage.success('备注添加成功')
+    remarkDialogVisible.value = false
+    loadData(props.petId)
+    emit('remark-saved')
+  })
+}
+
+// 暴露刷新方法
+defineExpose({ refresh: () => loadData(props.petId) })
+
+watch(() => props.visible, (val) => {
+  if (val && props.petId) {
+    detailActiveTab.value = 'info'
+    loadData(props.petId)
+  }
+})
+
+onMounted(() => {
+  getServiceList()
+})
+</script>
+
+<style scoped>
+.profile-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  padding-bottom: 20px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.profile-basic {
+  margin-left: 20px;
+}
+
+.name-row {
+  display: flex;
+  align-items: center;
+}
+
+.name {
+  font-size: 20px;
+  font-weight: bold;
+  color: #303133;
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 15px;
+  border-left: 4px solid #409eff;
+  padding-left: 10px;
+  line-height: 1.2;
+}
+</style>

+ 88 - 0
src/enums/fulfiller.json

@@ -0,0 +1,88 @@
+{
+  "PointsBizType": {
+    "admin_reward": {
+      "label": "后台奖励",
+      "tagType": "success"
+    },
+    "admin_punish": {
+      "label": "后台惩罚",
+      "tagType": "danger"
+    },
+    "admin_adjust": {
+      "label": "后台调整",
+      "tagType": "info"
+    },
+    "order_reward": {
+      "label": "订单奖励",
+      "tagType": "success"
+    },
+    "order_punish": {
+      "label": "订单惩罚",
+      "tagType": "danger"
+    },
+    "order_finish": {
+      "label": "订单完成",
+      "tagType": "info"
+    }
+  },
+  "BalanceBizType": {
+    "admin_reward": {
+      "label": "后台奖励",
+      "tagType": "success"
+    },
+    "admin_punish": {
+      "label": "后台惩罚",
+      "tagType": "danger"
+    },
+    "admin_adjust": {
+      "label": "后台调整",
+      "tagType": "info"
+    },
+    "order_reward": {
+      "label": "订单奖励",
+      "tagType": "success"
+    },
+    "order_punish": {
+      "label": "订单惩罚",
+      "tagType": "danger"
+    },
+    "order_finish": {
+      "label": "订单完成",
+      "tagType": "info"
+    },
+    "salary": {
+      "label": "工资发放",
+      "tagType": "warning"
+    },
+    "withdraw": {
+      "label": "提现",
+      "tagType": "danger"
+    }
+  },
+  "ActionType": {
+    "ADD": "add",
+    "REDUCE": "reduce"
+  },
+  "RewardBizType": {
+    "admin_reward": {
+      "label": "后台奖励",
+      "tagType": "success"
+    },
+    "admin_punish": {
+      "label": "后台惩罚",
+      "tagType": "danger"
+    },
+    "order_reward": {
+      "label": "订单奖励",
+      "tagType": "success"
+    },
+    "order_punish": {
+      "label": "订单惩罚",
+      "tagType": "danger"
+    },
+    "order_finish": {
+      "label": "订单完成",
+      "tagType": "info"
+    }
+  }
+}

+ 3 - 3
src/types/components.d.ts

@@ -11,6 +11,7 @@ declare module 'vue' {
     ApprovalButton: typeof import('./../components/Process/approvalButton.vue')['default']
     ApprovalRecord: typeof import('./../components/Process/approvalRecord.vue')['default']
     Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
+    CustomerDetailDrawer: typeof import('./../components/CustomerDetailDrawer/index.vue')['default']
     DictTag: typeof import('./../components/DictTag/index.vue')['default']
     Editor: typeof import('./../components/Editor/index.vue')['default']
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
@@ -35,7 +36,6 @@ declare module 'vue' {
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
-    ElEmpty: typeof import('element-plus/es')['ElEmpty']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
@@ -64,7 +64,7 @@ declare module 'vue' {
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
-    ElTextArea: typeof import('element-plus/es')['ElTextArea']
+    ElText: typeof import('element-plus/es')['ElText']
     ElTimeline: typeof import('element-plus/es')['ElTimeline']
     ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
     ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
@@ -86,9 +86,9 @@ declare module 'vue' {
     LangSelect: typeof import('./../components/LangSelect/index.vue')['default']
     MessageType: typeof import('./../components/Process/MessageType.vue')['default']
     PageSelect: typeof import('./../components/PageSelect/index.vue')['default']
-    PageSelector: typeof import('./../components/PageSelector/index.vue')['default']
     Pagination: typeof import('./../components/Pagination/index.vue')['default']
     ParentView: typeof import('./../components/ParentView/index.vue')['default']
+    PetDetailDrawer: typeof import('./../components/PetDetailDrawer/index.vue')['default']
     ProcessMeddle: typeof import('./../components/Process/processMeddle.vue')['default']
     RightToolbar: typeof import('./../components/RightToolbar/index.vue')['default']
     RoleSelect: typeof import('./../components/RoleSelect/index.vue')['default']

+ 24 - 125
src/views/archieves/customer/index.vue

@@ -43,7 +43,7 @@
         </el-table-column>
         <el-table-column label="录入信息" width="200">
           <template #default="scope">
-            <div><el-tag size="small" effect="plain" :type="scope.row.source && scope.row.source.includes('平台') ? '' : 'warning'">{{ scope.row.source || '-' }}</el-tag></div>
+            <div><el-tag size="small" effect="plain" :type="scope.row.tenantName ? '' : 'warning'">{{ scope.row.tenantName || '-' }}</el-tag></div>
             <div style="font-size: 12px; color: #999; margin-top: 4px;">{{ scope.row.createTime }}</div>
           </template>
         </el-table-column>
@@ -103,116 +103,17 @@
     </el-card>
 
     <!-- User Detail Drawer -->
-    <el-drawer v-model="drawerVisible" title="用户档案详情" size="60%" destroy-on-close>
-      <div class="profile-header">
-        <el-avatar :size="80" :src="currentUser.avatarUrl" />
-        <div class="profile-basic">
-          <div class="name-row">
-            <span class="name">{{ currentUser.name }}</span>
-            <dict-tag :options="sys_user_sex" :value="currentUser.gender" />
-            <span class="phone">{{ currentUser.phone }}</span>
-          </div>
-          <div class="tags-row" style="margin-top: 8px">
-            <el-tag v-for="tag in currentUser.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">
-              {{ tag.name }}
-            </el-tag>
-          </div>
-        </div>
-      </div>
-
-      <el-tabs v-model="detailActiveTab" class="profile-tabs">
-        <el-tab-pane label="档案信息" name="info">
-          <div class="section-title">基本信息</div>
-          <el-descriptions :column="2" border>
-            <el-descriptions-item label="姓名">{{ currentUser.name }}</el-descriptions-item>
-            <el-descriptions-item label="电话">{{ currentUser.phone }}</el-descriptions-item>
-            <el-descriptions-item label="所属区域">{{ currentUser.areaName || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="所属站点">{{ currentUser.stationName || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="录入来源">{{ currentUser.source || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="录入时间">{{ currentUser.createTime || '-' }}</el-descriptions-item>
-          </el-descriptions>
-
-          <div class="section-title" style="margin-top: 20px">居住信息</div>
-          <el-descriptions :column="2" border>
-            <el-descriptions-item label="详细住址" :span="2">{{ currentUser.address }}</el-descriptions-item>
-            <el-descriptions-item label="房屋类型">
-              <dict-tag :options="sys_house_type" :value="currentUser.houseType" />
-            </el-descriptions-item>
-            <el-descriptions-item label="入门方式">
-              <dict-tag :options="sys_entry_method" :value="currentUser.entryMethod" />
-            </el-descriptions-item>
-            <el-descriptions-item label="开门详情" :span="2">
-              {{ currentUser.entryMethod === 'password' ? currentUser.entryPassword : currentUser.keyLocation }}
-            </el-descriptions-item>
-          </el-descriptions>
-        </el-tab-pane>
-
-        <el-tab-pane label="宠物列表" name="pets">
-          <div style="margin-bottom: 15px;">
-            <el-button type="primary" size="small" icon="Plus" @click="openAddPet">新增宠物</el-button>
-          </div>
-          <el-table :data="currentPets" border style="width: 100%">
-            <el-table-column label="宠物信息" width="200">
-              <template #default="scope">
-                <div style="display: flex; align-items: center;">
-                  <el-avatar :size="30" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" style="margin-right: 8px;" />
-                  {{ scope.row.name }}
-                </div>
-              </template>
-            </el-table-column>
-            <el-table-column prop="breed" label="品种" />
-            <el-table-column prop="gender" label="性别" width="60" />
-            <el-table-column prop="age" label="年龄" width="60" />
-            <el-table-column prop="status" label="健康状态">
-              <template #default="scope">
-                <el-tag :type="scope.row.status === '健康' ? 'success' : 'warning'" size="small">{{ scope.row.status }}</el-tag>
-              </template>
-            </el-table-column>
-            <el-table-column label="疫苗接种" width="120" align="center">
-              <template #default="scope">
-                {{ scope.row.vaccine || '-' }}
-              </template>
-            </el-table-column>
-            <el-table-column label="操作" width="200" align="center">
-              <template #default="scope">
-                <el-button link type="primary" @click="handlePetDetail(scope.row)">详情</el-button>
-                <el-button link type="primary" @click="handlePetEdit(scope.row)">编辑</el-button>
-                <el-button link type="primary" @click="handlePetRemark(scope.row)">备注</el-button>
-                <el-button link type="danger" @click="handlePetDelete(scope.row)">删除</el-button>
-              </template>
-            </el-table-column>
-          </el-table>
-        </el-tab-pane>
-
-        <el-tab-pane label="历史订单" name="orders">
-          <el-table :data="mockOrders" border style="width: 100%">
-            <el-table-column prop="orderNo" label="订单编号" width="180" />
-            <el-table-column prop="service" label="服务项目" />
-            <el-table-column prop="pets" label="服务宠物" />
-            <el-table-column prop="time" label="服务时间" width="180" />
-            <el-table-column prop="status" label="状态" width="100">
-              <template #default="scope">
-                <el-tag type="success" size="small">完成</el-tag>
-              </template>
-            </el-table-column>
-          </el-table>
-        </el-tab-pane>
-
-        <el-tab-pane label="档案日志" name="logs">
-          <el-timeline style="margin-top: 10px; padding-left: 5px;">
-            <el-timeline-item
-              v-for="(log, index) in changeLogs"
-              :key="index"
-              :timestamp="log.createTime"
-              type="primary"
-            >
-              [{{ log.logType }}] {{ log.content }}
-              <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operatorName }}</div>
-            </el-timeline-item>
-          </el-timeline>
-        </el-tab-pane>
-      </el-tabs>
-    </el-drawer>
+    <CustomerDetailDrawer
+      ref="customerDetailRef"
+      v-model:visible="drawerVisible"
+      :customer-id="currentUser.id"
+      editable
+      @add-pet="openAddPet"
+      @pet-detail="handlePetDetail"
+      @pet-edit="handlePetEdit"
+      @pet-remark="handlePetRemark"
+      @pet-delete="handlePetDelete"
+    />
 
     <!-- Add/Edit User Dialog -->
     <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑用户' : '新增用户'" width="700px" destroy-on-close>
@@ -227,9 +128,9 @@
 
           <el-col :span="24"><div class="form-section-header">基本资料</div></el-col>
           <el-col :span="12">
-            <el-form-item label="录入来源">
-              <PageSelect v-model="form.source"
-                :options="brandList.map(item => ({ value: item.name, label: item.name }))"
+            <el-form-item label="所属品牌">
+              <PageSelect v-model="form.tenantId"
+                :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))"
                 :total="brandTotal" :pageSize="10" placeholder="请选择所属品牌"
                 @page-change="handleBrandPageChange"
                 @visible-change="handleBrandVisibleChange" />
@@ -482,6 +383,7 @@ import { listPetByUser, addPet, updatePet, delPet } from '@/api/archieves/pet'
 import { listAllChangeLog } from '@/api/archieves/changeLog'
 import { listOnStore } from '@/api/system/areaStation'
 import { listOnStore as listBrandOnStore } from '@/api/system/tenant'
+import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
 import { regionData, codeToText } from 'element-china-area-data'
 import PageSelect from '@/components/PageSelect/index.vue'
 
@@ -564,11 +466,7 @@ const brandTotal = ref(0)
 const formAreaValue = ref([])
 const regionCascaderValue = ref([])
 
-
-const mockOrders = ref([
-  { orderNo: 'DD20231001001', service: '上门喂养 (标准版)', pets: '旺财', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
-  { orderNo: 'DD20230915002', service: '深度洗护套餐', pets: '旺财, 咪咪', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
-])
+const customerDetailRef = ref(null)
 
 const form = reactive({
   id: undefined,
@@ -593,7 +491,8 @@ const form = reactive({
   memberLevel: 0,
   status: 0,
   remark: '',
-  tagIds: []
+  tagIds: [],
+  tenantId: undefined
 })
 
 const petForm = reactive({
@@ -708,7 +607,8 @@ const handleAdd = () => {
     id: undefined, name: '', phone: '', avatar: undefined, gender: undefined, birthday: '', idCard: '',
     areaId: undefined, stationId: undefined, regionCode: '', region: [], address: '',
     houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', source: '',
-    emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: []
+    emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: [],
+    tenantId: undefined
   })
   userAvatarDisplayUrl.value = ''
   formAreaValue.value = []
@@ -727,7 +627,8 @@ const handleEdit = (row) => {
       address: data.address, houseType: data.houseType, entryMethod: data.entryMethod,
       entryPassword: data.entryPassword, keyLocation: data.keyLocation, source: data.source,
       emergencyContact: data.emergencyContact, emergencyPhone: data.emergencyPhone,
-      memberLevel: data.memberLevel, status: data.status, remark: data.remark, tagIds: []
+      memberLevel: data.memberLevel, status: data.status, remark: data.remark, tagIds: [],
+      tenantId: data.tenantId
     })
     userAvatarDisplayUrl.value = data.avatarUrl || ''
     // Restore area cascader value path
@@ -775,8 +676,6 @@ const handleDetail = (row) => {
     }
     currentUser.value = data
     detailActiveTab.value = 'info'
-    loadDetailPets(row.id)
-    loadDetailLogs(row.id, 'customer')
     drawerVisible.value = true
   })
 }
@@ -1003,7 +902,7 @@ const savePet = () => {
   api.then(() => {
     ElMessage.success('宠物档案保存成功')
     petDialogVisible.value = false
-    loadDetailPets(currentUser.value.id)
+    customerDetailRef.value?.refresh()
     getList()
   }).finally(() => {
     submitLoading.value = false

+ 20 - 147
src/views/archieves/pet/index.vue

@@ -206,113 +206,13 @@
     </el-dialog>
 
     <!-- Pet Profile Drawer -->
-    <el-drawer v-model="drawerVisible" title="宠物档案详情" size="60%" destroy-on-close>
-      <div class="profile-header">
-        <el-avatar :size="80" :src="currentPet.avatarUrl" />
-        <div class="profile-basic">
-          <div class="name-row">
-            <span class="name">{{ currentPet.name }}</span>
-            <dict-tag :options="sys_pet_gender" :value="currentPet.gender" />
-            <el-tag size="small" effect="plain" type="info" style="margin-left: 5px">{{ currentPet.age }}岁</el-tag>
-          </div>
-          <div class="tags-row" style="margin-top: 8px">
-            <el-tag v-for="tag in currentPet.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">
-              {{ tag.name }}
-            </el-tag>
-          </div>
-        </div>
-        <div style="margin-left: auto">
-          <el-button type="primary" size="small" plain @click="handleRemark(currentPet)" v-hasPermi="['archieves:pet:edit']">添加备注</el-button>
-        </div>
-      </div>
-
-      <el-tabs v-model="detailActiveTab" class="profile-tabs">
-        <el-tab-pane label="档案信息" name="info">
-          <div class="section-title">基本信息</div>
-          <el-descriptions :column="2" border>
-            <el-descriptions-item label="品种">{{ currentPet.breed }}</el-descriptions-item>
-            <el-descriptions-item label="体型">
-              <dict-tag :options="sys_pet_size" :value="currentPet.size" />
-            </el-descriptions-item>
-            <el-descriptions-item label="体重">{{ currentPet.weight }} kg</el-descriptions-item>
-            <el-descriptions-item label="所属主人">{{ currentPet.ownerName }} ({{ currentPet.ownerPhone }})</el-descriptions-item>
-            <el-descriptions-item label="性格关键词">{{ currentPet.personality || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="萌宠性格" :span="2">{{ currentPet.cutePersonality || '-' }}</el-descriptions-item>
-          </el-descriptions>
-
-          <div class="section-title" style="margin-top: 20px">家庭信息</div>
-          <el-descriptions :column="2" border>
-            <el-descriptions-item label="到家时间">{{ currentPet.arrivalTime || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="房屋类型">
-              <dict-tag :options="sys_house_type" :value="currentPet.houseType" />
-            </el-descriptions-item>
-            <el-descriptions-item label="入门方式">
-              <dict-tag :options="sys_entry_method" :value="currentPet.entryMethod" />
-            </el-descriptions-item>
-            <el-descriptions-item label="开门详情">
-              {{ currentPet.entryMethod === 'password' ? currentPet.entryPassword : currentPet.keyLocation }}
-            </el-descriptions-item>
-          </el-descriptions>
-
-          <div class="section-title" style="margin-top: 20px">健康状况</div>
-          <el-descriptions :column="2" border>
-            <el-descriptions-item label="健康状态">
-              <el-tag :type="currentPet.healthStatus === '健康' ? 'success' : 'warning'" size="small">{{ currentPet.healthStatus }}</el-tag>
-            </el-descriptions-item>
-            <el-descriptions-item label="攻击倾向">
-              <el-tag :type="currentPet.aggression ? 'danger' : 'success'" size="small">{{ currentPet.aggression ? '有' : '无' }}</el-tag>
-            </el-descriptions-item>
-            <el-descriptions-item label="疫苗情况" :span="2">
-              <div>{{ currentPet.vaccineStatus || '-' }}</div>
-              <div v-if="currentPet.vaccineCertUrl" style="margin-top: 10px">
-                <el-image
-                  style="width: 100px; height: 100px; border-radius: 4px"
-                  :src="currentPet.vaccineCertUrl"
-                  :preview-src-list="[currentPet.vaccineCertUrl]"
-                  fit="cover"
-                />
-              </div>
-            </el-descriptions-item>
-            <el-descriptions-item label="既往病史" :span="2">{{ currentPet.medicalHistory || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="过敏史" :span="2">{{ currentPet.allergies || '-' }}</el-descriptions-item>
-          </el-descriptions>
-        </el-tab-pane>
-
-        <el-tab-pane label="历史订单" name="orders">
-          <el-table :data="mockOrders" border style="width: 100%">
-            <el-table-column prop="orderNo" label="订单编号" width="180" />
-            <el-table-column prop="service" label="服务项目" />
-            <el-table-column prop="time" label="服务时间" width="180" />
-            <el-table-column prop="amount" label="金额" width="100" />
-            <el-table-column prop="status" label="状态" width="100">
-              <template #default="scope">
-                <el-tag type="success" size="small">完成</el-tag>
-              </template>
-            </el-table-column>
-          </el-table>
-        </el-tab-pane>
+    <PetDetailDrawer
+      v-model:visible="drawerVisible"
+      :pet-id="currentPet.id"
+      editable
+      @remark-saved="getList"
+    />
 
-        <el-tab-pane label="备注日志" name="logs">
-          <el-timeline style="margin-top: 10px; padding-left: 5px">
-            <el-timeline-item v-for="(log, index) in changeLogs" :key="index" :timestamp="log.createTime" type="primary">
-              [{{ log.logType }}] {{ log.content }}
-              <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operatorName }}</div>
-            </el-timeline-item>
-          </el-timeline>
-        </el-tab-pane>
-      </el-tabs>
-    </el-drawer>
-
-    <!-- Remark Dialog -->
-    <el-dialog v-model="remarkDialogVisible" title="添加备注" width="400px">
-      <el-input v-model="remarkForm.content" type="textarea" :rows="3" placeholder="请输入备注内容..." />
-      <template #footer>
-        <span class="dialog-footer">
-          <el-button @click="remarkDialogVisible = false">取消</el-button>
-          <el-button type="primary" @click="saveRemark">保存</el-button>
-        </span>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
@@ -320,10 +220,10 @@
 import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
 import { globalHeaders } from '@/utils/request';
 import { ElMessage, ElMessageBox } from 'element-plus';
-import { listPet, getPet, addPet, updatePet, delPet } from '@/api/archieves/pet';
-import { listAllTag } from '@/api/archieves/tag';
-import { listAllCustomer } from '@/api/archieves/customer';
-import { listAllChangeLog } from '@/api/archieves/changeLog';
+import { listPet, getPet, addPet, updatePet, delPet } from '@/api/archieves/pet'
+import { listAllTag } from '@/api/archieves/tag'
+import { listAllCustomer } from '@/api/archieves/customer'
+import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
 
 const { proxy } = getCurrentInstance();
 const { sys_pet_gender, sys_pet_type, sys_pet_size, sys_pet_breed, sys_house_type, sys_entry_method } = toRefs(
@@ -353,16 +253,9 @@ const currentPet = ref({});
 
 const allPetTags = ref([]);
 const userList = ref([]);
-const changeLogs = ref([]);
 
-const mockOrders = ref([
-  { orderNo: 'DD20231001001', service: '上门喂养 (标准版)', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
-  { orderNo: 'DD20230915002', service: '深度洗护套餐', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
-]);
-
-const remarkForm = reactive({ content: '' });
-const avatarDisplayUrl = ref('');
-const vaccineCertDisplayUrl = ref('');
+const avatarDisplayUrl = ref('')
+const vaccineCertDisplayUrl = ref('')
 
 
 const form = reactive({
@@ -462,37 +355,17 @@ const handleEdit = (row) => {
 };
 
 const handleDetail = (row) => {
-  getPet(row.id).then((res) => {
-    currentPet.value = res.data;
-    detailActiveTab.value = 'info';
-    listAllChangeLog(row.id, 'pet').then((logRes) => {
-      changeLogs.value = logRes.data || [];
-    });
-    drawerVisible.value = true;
-  });
-};
+  currentPet.value = row
+  detailActiveTab.value = 'info'
+  drawerVisible.value = true
+}
 
 const handleRemark = (row) => {
-  currentPet.value = row;
-  remarkForm.content = '';
-  remarkDialogVisible.value = true;
-};
+  currentPet.value = row
+  drawerVisible.value = true
+  // 由于备注功能已集成在详情抽屉中,直接打开抽屉即可,后期可以根据需要调整是否直接弹出备注对话框
+}
 
-const saveRemark = () => {
-  if (!remarkForm.content) return ElMessage.warning('请输入内容');
-  const data = { id: currentPet.value.id, remark: remarkForm.content };
-  updatePet(data).then(() => {
-    ElMessage.success('备注添加成功');
-    remarkDialogVisible.value = false;
-    getList();
-    // 刷新 drawer 中的变更日志
-    if (drawerVisible.value) {
-      listAllChangeLog(currentPet.value.id, 'pet').then((logRes) => {
-        changeLogs.value = logRes.data || [];
-      });
-    }
-  });
-};
 
 const handleDelete = (row) => {
   ElMessageBox.confirm('确认删除该宠物档案吗?', '提示', { type: 'warning' }).then(() => {

+ 1 - 1
src/views/archieves/tag/index.vue

@@ -10,7 +10,7 @@
       <el-tabs v-model="activeTab" class="demo-tabs" @tab-change="handleTabChange">
         <el-tab-pane label="用户标签" name="customer">
           <div class="operation-bar">
-            <el-button type="primary" icon="Plus" @click="handleAdd('user')" v-hasPermi="['archieves:tag:add']">新增标签</el-button>
+            <el-button type="primary" icon="Plus" @click="handleAdd('customer')" v-hasPermi="['archieves:tag:add']">新增标签</el-button>
           </div>
           <el-table :data="tagList" v-loading="loading" style="width: 100%; margin-top: 20px" :header-cell-style="{ background: '#f5f7fa' }">
             <el-table-column label="标签名称" width="200">

+ 125 - 80
src/views/fulfiller/pool/index.vue

@@ -282,22 +282,26 @@
 
           <el-tab-pane label="服务订单" name="orders">
             <div class="tab-content-wrapper">
-              <el-table :data="[]" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
-                <el-table-column prop="orderNo" label="订单号" width="160" show-overflow-tooltip />
-                <el-table-column prop="serviceName" label="服务项目" show-overflow-tooltip />
-                <el-table-column prop="serviceFee" label="收入" width="100">
+              <el-table v-loading="logLoading" :data="serviceOrderData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
+                <el-table-column prop="code" label="订单号" width="160" show-overflow-tooltip />
+                <el-table-column label="服务项目" show-overflow-tooltip>
                   <template #default="{ row }">
-                    <span style="color: #67c23a; font-weight: bold; font-size: 15px;">+{{ row.serviceFee }}</span>
+                    {{ getServiceName(row.service) }}
                   </template>
                 </el-table-column>
-                <el-table-column prop="time" label="时间" width="160" show-overflow-tooltip />
+                <el-table-column prop="price" label="收入" width="100">
+                  <template #default="{ row }">
+                    <span style="color: #67c23a; font-weight: bold; font-size: 15px;">+{{ (row.price / 100).toFixed(2) }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column prop="serviceTime" label="时间" width="160" show-overflow-tooltip />
                 <el-table-column prop="status" label="状态" width="90">
                   <template #default="{ row }">
-                    <el-tag v-if="row.status==='completed'" type="success" size="small">完成</el-tag>
-                    <el-tag v-else type="info" size="small">取消</el-tag>
+                    <el-tag :type="getSubOrderStatusType(row.status)" size="small">
+                      {{ getSubOrderStatusName(row.status) }}
+                    </el-tag>
                   </template>
                 </el-table-column>
-
               </el-table>
             </div>
           </el-tab-pane>
@@ -308,13 +312,13 @@
                 <el-table-column prop="createTime" label="变动时间" width="180" />
                 <el-table-column prop="bizType" label="业务类型" width="120">
                   <template #default="{ row }">
-                    <el-tag :type="getBizTypeTag(row.bizType)" size="small" effect="plain">{{ getBizTypeName(row.bizType) }}</el-tag>
+                    <el-tag :type="getPointsBizTypeTag(row.bizType)" size="small" effect="plain">{{ getPointsBizTypeName(row.bizType) }}</el-tag>
                   </template>
                 </el-table-column>
                 <el-table-column prop="amount" label="变动数值" width="120">
                   <template #default="{ row }">
-                                    <span :style="{ color: row.amount > 0 ? '#67c23a' : '#f56c6c', fontWeight: 'bold', fontSize: '15px' }">
-                                        {{ row.amount > 0 ? '+' : '' }}{{ row.amount }}
+                                    <span :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold', fontSize: '15px' }">
+                                        {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ row.amount }}
                                     </span>
                   </template>
                 </el-table-column>
@@ -330,13 +334,13 @@
                 <el-table-column prop="createTime" label="变动时间" width="180" show-overflow-tooltip />
                 <el-table-column prop="subType" label="资金类型" width="120">
                   <template #default="{ row }">
-                    <el-tag :type="getBizTypeTag(row.subType)" size="small" effect="plain">{{ getSubTypeName(row.subType) }}</el-tag>
+                    <el-tag :type="getBalanceBizTypeTag(row.subType)" size="small" effect="plain">{{ getBalanceBizTypeName(row.bizType) }}</el-tag>
                   </template>
                 </el-table-column>
                 <el-table-column prop="amount" label="变动金额" width="120">
                   <template #default="{ row }">
-                                    <span :style="{ color: row.amount > 0 ? '#67c23a' : '#f56c6c', fontWeight: 'bold', fontSize: '15px' }">
-                                        {{ row.amount > 0 ? '+' : '' }}{{ (row.amount / 100).toFixed(2) }}
+                                    <span :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold', fontSize: '15px' }">
+                                        {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ (row.amount / 100).toFixed(2) }}
                                     </span>
                   </template>
                 </el-table-column>
@@ -355,11 +359,18 @@
             <div class="tab-content-wrapper">
               <el-table v-loading="logLoading" :data="rewardLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
                 <el-table-column prop="createTime" label="操作时间" width="180" />
-                <el-table-column prop="type" label="奖惩类型" width="100">
+                <el-table-column prop="bizType" label="奖惩类型" width="100">
                   <template #default="{ row }">
-                    <el-tag :type="row.type==='reward' ? 'success' : 'danger'" size="small">{{ row.type === 'reward' ? '奖励' : '惩罚' }}</el-tag>
+                    <el-tag :type="fulfillerEnums.RewardBizType[row.bizType]?.tagType || 'info'" size="small" effect="plain">
+                      {{ getRewardBizTypeName(row.bizType) }}
+                    </el-tag>
                   </template>
                 </el-table-column>
+                <!-- <el-table-column prop="type" label="操作" width="80">
+                  <template #default="{ row }">
+                    <el-tag :type="['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? 'success' : 'danger'" size="small">{{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '增加' : '减少' }}</el-tag>
+                  </template>
+                </el-table-column> -->
                 <el-table-column prop="target" label="关联项目" width="100">
                   <template #default="{ row }">
                     <el-tag type="info" size="small" effect="plain">{{ row.target === 'points' ? '积分' : '余额' }}</el-tag>
@@ -367,8 +378,8 @@
                 </el-table-column>
                 <el-table-column prop="amount" label="涉及数值" width="120">
                   <template #default="{ row }">
-                                     <span :style="{ color: row.type==='reward' ? '#67c23a' : '#f56c6c', fontWeight: 'bold' }">
-                                         {{ row.type === 'reward' ? '+' : '-' }}{{ row.target === 'balance' ? (row.amount / 100).toFixed(2) : row.amount }} {{ row.target === 'points' ? '分' : '元' }}
+                                     <span :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold' }">
+                                         {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ row.target === 'balance' ? (row.amount / 100).toFixed(2) : row.amount }} {{ row.target === 'points' ? '分' : '元' }}
                                      </span>
                   </template>
                 </el-table-column>
@@ -457,6 +468,11 @@
             <el-checkbox v-for="t in allTags" :key="t.id" :label="t.id" :value="t.id">{{ t.name }}</el-checkbox>
           </el-checkbox-group>
         </el-form-item>
+        <el-form-item label="服务类型">
+          <el-select v-model="editDialog.serviceTypesArray" multiple placeholder="请选择服务类型" style="width: 100%">
+            <el-option v-for="item in serviceOptions" :key="item.id" :label="item.name" :value="String(item.id)" />
+          </el-select>
+        </el-form-item>
       </el-form>
       <template #footer>
             <span class="dialog-footer">
@@ -474,8 +490,8 @@
       <el-form :model="rewardDialog.form" label-width="80px" style="margin-top: 20px;">
         <el-form-item label="操作类型">
           <el-radio-group v-model="rewardDialog.form.type">
-            <el-radio label="reward">奖励 (增加)</el-radio>
-            <el-radio label="punish">惩罚 (扣除)</el-radio>
+            <el-radio :label="fulfillerEnums.ActionType.ADD">增加</el-radio>
+            <el-radio :label="fulfillerEnums.ActionType.REDUCE">减少</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="调整项目">
@@ -525,6 +541,11 @@
             <el-option v-for="station in createDialog.stationOptions" :key="station.id" :label="station.name" :value="station.id" />
           </el-select>
         </el-form-item>
+        <el-form-item label="服务类型">
+          <el-select v-model="createDialog.serviceTypesArray" multiple placeholder="请选择服务类型" style="width: 100%">
+            <el-option v-for="item in serviceOptions" :key="item.id" :label="item.name" :value="String(item.id)" />
+          </el-select>
+        </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="createDialog.visible = false">取消</el-button>
@@ -540,8 +561,8 @@
         </el-form-item>
         <el-form-item label="调整方式">
           <el-radio-group v-model="pointsDialog.form.type">
-            <el-radio label="add">增加</el-radio>
-            <el-radio label="reduce">扣除</el-radio>
+            <el-radio :label="fulfillerEnums.ActionType.ADD">增加</el-radio>
+            <el-radio :label="fulfillerEnums.ActionType.REDUCE">扣除</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="调整数值" required>
@@ -564,21 +585,21 @@
           <span style="color: #f56c6c; font-weight: bold">¥{{ (balanceDialog.currentRow?.balance / 100).toFixed(2) }}</span>
         </el-form-item>
         <el-form-item label="扣减类型">
-          <el-radio-group v-model="balanceDialog.form.type" @change="balanceDialog.form.subType = balanceDialog.form.type === 'add' ? 'reward' : 'punish'">
-            <el-radio label="add">增加</el-radio>
-            <el-radio label="reduce">减少</el-radio>
+          <el-radio-group v-model="balanceDialog.form.type" @change="balanceDialog.form.subType = balanceDialog.form.type === fulfillerEnums.ActionType.ADD ? 'admin_reward' : 'admin_punish'">
+            <el-radio :label="fulfillerEnums.ActionType.ADD">增加</el-radio>
+            <el-radio :label="fulfillerEnums.ActionType.REDUCE">减少</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="调整类型">
           <el-radio-group v-model="balanceDialog.form.subType">
-            <template v-if="balanceDialog.form.type === 'add'">
-              <el-radio label="reward">奖励</el-radio>
-              <el-radio label="other">其他</el-radio>
+            <template v-if="balanceDialog.form.type === fulfillerEnums.ActionType.ADD">
+              <el-radio label="admin_reward">奖励</el-radio>
+              <el-radio label="admin_ajust">其他 (后台调整)</el-radio>
             </template>
             <template v-else>
-              <el-radio label="punish">惩罚</el-radio>
+              <el-radio label="admin_punish">惩罚</el-radio>
               <el-radio label="salary">工资发放</el-radio>
-              <el-radio label="other">其他</el-radio>
+              <el-radio label="admin_ajust">其他 (后台调整)</el-radio>
             </template>
           </el-radio-group>
         </el-form-item>
@@ -605,6 +626,8 @@ import {
   changeStatus, resetPwd, reward, adjustPoints, adjustBalance,
   listPointsLog, listBalanceLog, listRewardLog
 } from '@/api/fulfiller/pool'
+import { listSubOrderOnFulfiller } from '@/api/order/subOrder/index'
+import { listOnStore as listServiceOnStore } from '@/api/service/list/index'
 import type {
   FlfFulfillerVO, FlfFulfillerForm, FlfFulfillerQuery,
   FlfRewardForm, FlfAdjustPointsForm, FlfAdjustBalanceForm,
@@ -614,6 +637,7 @@ import { listAllTag } from '@/api/fulfiller/tag'
 import type { FlfTagVO } from '@/api/fulfiller/tag/types'
 import { listOnStore } from '@/api/system/areaStation'
 import type { SysAreaStationOnStoreVo } from '@/api/system/areaStation/types'
+import fulfillerEnums from '@/enums/fulfiller.json'
 
 const loading = ref(false)
 const searchKey = ref('')
@@ -646,6 +670,8 @@ const qualImageUrlList = computed(() => {
 const pointsLogData = ref<FlfPointsLogVO[]>([])
 const balanceLogData = ref<FlfBalanceLogVO[]>([])
 const rewardLogData = ref<FlfRewardLogVO[]>([])
+const serviceOrderData = ref<any[]>([])
+const serviceOptions = ref<any[]>([])
 const logLoading = ref(false)
 
 /** 查询列表 */
@@ -722,58 +748,66 @@ const handleSizeChange = (val: number) => { queryParams.pageSize = val; getList(
 const handleCurrentChange = (val: number) => { queryParams.pageNum = val; getList() }
 
 /** 积分业务类型标签颜色 */
-const getBizTypeTag = (type: string) => {
-  const map: Record<string, string> = {
-    order: '', reward: 'success', admin_reward: 'success',
-    punish: 'danger', admin_punish: 'danger',
-    adjust: 'info', admin_adjust: 'info'
-  }
-  return map[type] || 'info'
+const getPointsBizTypeTag = (type: string) => {
+  return fulfillerEnums.PointsBizType[type]?.tagType || 'info'
 }
 
 /** 积分业务类型中文名称 */
-const getBizTypeName = (type: string) => {
-  const map: Record<string, string> = {
-    order: '订单',
-    reward: '奖励',
-    punish: '惩罚',
-    adjust: '手动调整',
-    admin_adjust: '后台调整',
-    admin_reward: '后台奖励',
-    admin_punish: '后台惩罚'
-  }
-  return map[type] || type
+const getPointsBizTypeName = (type: string) => {
+  return fulfillerEnums.PointsBizType[type]?.label || type
+}
+
+/** 余额资金类型颜色 */
+const getBalanceBizTypeTag = (type: string) => {
+  return fulfillerEnums.BalanceBizType[type]?.tagType || 'info'
 }
 
 /** 余额资金类型中文名称 */
-const getSubTypeName = (type: string) => {
-  const map: Record<string, string> = {
-    reward: '奖励',
-    punish: '惩罚',
-    salary: '工资发放',
-    withdraw: '提现',
-    settle: '结算',
-    other: '其他',
-    admin_adjust: '后台调整',
-    admin_reward: '后台奖励',
-    admin_punish: '后台惩罚',
-    order: '订单完成'
-  }
-  return map[type] || type
+const getBalanceBizTypeName = (type: string) => {
+  return fulfillerEnums.BalanceBizType[type]?.label || type
+}
+
+/** 奖惩业务类型名称 */
+const getRewardBizTypeName = (type: string) => {
+  return fulfillerEnums.RewardBizType[type]?.label || type
+}
+
+/** 加载服务项目列表用于名称映射 */
+const loadServiceOptions = async () => {
+    try {
+        const res = await listServiceOnStore()
+        serviceOptions.value = res.data || []
+    } catch { /* ignore */ }
+}
+
+const getServiceName = (serviceId: number | string) => {
+    const item = serviceOptions.value.find(i => i.id === serviceId)
+    return item ? item.name : '未知服务'
+}
+
+const getSubOrderStatusName = (status: number) => {
+    const map: Record<number, string> = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' }
+    return map[status] || '未知'
+}
+
+const getSubOrderStatusType = (status: number) => {
+    const map: Record<number, string> = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: 'success', 5: 'danger' }
+    return map[status] || 'info'
 }
 
 const rewardDialog = reactive({
   visible: false,
   userName: '',
   fulfillerId: 0 as number | string,
-  form: { type: 'reward', target: 'points', amount: 10, reason: '' }
+  form: { type: fulfillerEnums.ActionType.ADD, target: 'points', amount: 10, reason: '' }
 })
 
 const editDialog = reactive({
   visible: false,
   form: {} as FlfFulfillerForm,
   cascaderValue: [] as any[],
-  stationOptions: [] as SysAreaStationOnStoreVo[]
+  stationOptions: [] as SysAreaStationOnStoreVo[],
+  serviceTypesArray: [] as string[]
 })
 
 const createDialog = reactive({
@@ -782,19 +816,20 @@ const createDialog = reactive({
     name: '', phone: '', password: '', cityCode: '', cityName: '', stationId: undefined as any, gender: '0', workType: 'full_time'
   },
   cascaderValue: [] as any[],
-  stationOptions: [] as SysAreaStationOnStoreVo[]
+  stationOptions: [] as SysAreaStationOnStoreVo[],
+  serviceTypesArray: [] as string[]
 })
 
 const pointsDialog = reactive({
   visible: false,
   currentRow: null as FlfFulfillerVO | null,
-  form: { type: 'add', amount: 0, reason: '' }
+  form: { type: fulfillerEnums.ActionType.ADD, amount: 0, reason: '' }
 })
 
 const balanceDialog = reactive({
   visible: false,
   currentRow: null as FlfFulfillerVO | null,
-  form: { type: 'add', subType: 'reward', amount: 0, reason: '' }
+  form: { type: fulfillerEnums.ActionType.ADD, subType: 'admin_reward', amount: 0, reason: '' }
 })
 
 const getStatusText = (status: string) => {
@@ -824,14 +859,16 @@ const handleTabClick = (tab: any) => {
 const loadLogs = async (fulfillerId: string | number) => {
   logLoading.value = true
   try {
-    const [pRes, bRes, rRes] = await Promise.all([
+    const [pRes, bRes, rRes, sRes] = await Promise.all([
       listPointsLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
       listBalanceLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
-      listRewardLog(fulfillerId, { pageNum: 1, pageSize: 20 })
+      listRewardLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
+      listSubOrderOnFulfiller(fulfillerId)
     ])
     pointsLogData.value = pRes.rows || []
     balanceLogData.value = bRes.rows || []
     rewardLogData.value = rRes.rows || []
+    serviceOrderData.value = sRes.data || []
   } catch { /* ignore */ } finally {
     logLoading.value = false
   }
@@ -863,8 +900,10 @@ const handleEdit = (row: FlfFulfillerVO) => {
     status: row.status,
     authId: row.authId,
     authQual: row.authQual,
-    tagIds: row.tags ? row.tags.map(t => t.id) : []
+    tagIds: row.tags ? row.tags.map(t => t.id) : [],
+    serviceTypes: row.serviceTypes
   }
+  editDialog.serviceTypesArray = row.serviceTypes ? row.serviceTypes.split(',') : []
   // 根据cityCode构建级联选择器的值
   editDialog.cascaderValue = []
   editDialog.stationOptions = []
@@ -889,6 +928,7 @@ const handleCreate = () => {
   createDialog.form = { name: '', phone: '', password: '', cityCode: '', cityName: '', stationId: undefined, gender: '0', workType: 'full_time' }
   createDialog.cascaderValue = []
   createDialog.stationOptions = []
+  createDialog.serviceTypesArray = []
   createDialog.visible = true
 }
 
@@ -898,7 +938,8 @@ const submitCreate = async () => {
     return
   }
   try {
-    await addFulfiller(createDialog.form as FlfFulfillerForm)
+    const submitForm = { ...createDialog.form, serviceTypes: createDialog.serviceTypesArray.join(',') };
+    await addFulfiller(submitForm as FlfFulfillerForm)
     createDialog.visible = false
     ElMessage.success('创建成功')
     getList()
@@ -907,7 +948,8 @@ const submitCreate = async () => {
 
 const saveEdit = async () => {
   try {
-    await updateFulfiller(editDialog.form)
+    const submitForm = { ...editDialog.form, serviceTypes: editDialog.serviceTypesArray.join(',') };
+    await updateFulfiller(submitForm)
     ElMessage.success('更新成功')
     editDialog.visible = false
     getList()
@@ -917,7 +959,7 @@ const saveEdit = async () => {
 const handleReward = (row: FlfFulfillerVO) => {
   rewardDialog.userName = row.name
   rewardDialog.fulfillerId = row.id
-  rewardDialog.form = { type: 'reward', target: 'points', amount: 10, reason: '' }
+  rewardDialog.form = { type: fulfillerEnums.ActionType.ADD, target: 'points', amount: 10, reason: '' }
   rewardDialog.visible = true
 }
 
@@ -932,8 +974,9 @@ const submitReward = async () => {
       type: rewardDialog.form.type,
       target: rewardDialog.form.target,
       amount: amount,
-      reason: rewardDialog.form.reason
-    })
+      reason: rewardDialog.form.reason,
+      bizType: rewardDialog.form.type === fulfillerEnums.ActionType.ADD ? 'admin_reward' : 'admin_punish'
+    } as any)
     ElMessage.success('操作成功')
     rewardDialog.visible = false
     getList()
@@ -943,11 +986,11 @@ const submitReward = async () => {
 const handleCommand = async (cmd: string, row: FlfFulfillerVO) => {
   if (cmd === 'adjustPoints') {
     pointsDialog.currentRow = row
-    pointsDialog.form = { type: 'add', amount: 0, reason: '' }
+    pointsDialog.form = { type: fulfillerEnums.ActionType.ADD, amount: 0, reason: '' }
     pointsDialog.visible = true
   } else if (cmd === 'adjustBalance') {
     balanceDialog.currentRow = row
-    balanceDialog.form = { type: 'add', subType: 'reward', amount: 0, reason: '' }
+    balanceDialog.form = { type: fulfillerEnums.ActionType.ADD, subType: 'admin_reward', amount: 0, reason: '' }
     balanceDialog.visible = true
   } else if (cmd === 'disable') {
     await ElMessageBox.confirm(`确定禁用履约者【${row.name}】吗?禁用后将无法接单。`, '提示', { type: 'warning' })
@@ -979,8 +1022,9 @@ const submitPointsAdjust = async () => {
       fulfillerId: pointsDialog.currentRow.id,
       type: pointsDialog.form.type,
       amount: pointsDialog.form.amount,
-      reason: pointsDialog.form.reason
-    })
+      reason: pointsDialog.form.reason,
+      bizType: pointsDialog.form.type === fulfillerEnums.ActionType.ADD ? 'admin_reward' : 'admin_punish'
+    } as any)
     ElMessage.success('积分调整成功')
     pointsDialog.visible = false
     getList()
@@ -1056,6 +1100,7 @@ onMounted(() => {
   getList()
   loadAllTags()
   loadAreaStations()
+  loadServiceOptions()
 })
 </script>
 

+ 70 - 40
src/views/index.vue

@@ -7,14 +7,32 @@
             <path
               d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
               stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
-            <path d="M12 8V12L15 15" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+            <path d="M9 12L11 14L15 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"
               stroke-linejoin="round" />
           </svg>
         </div>
-        <h2 class="coming-soon-title">待开发</h2>
-        <p class="coming-soon-subtitle">尽情期待</p>
-        <div class="loading-spinner">
-          <div class="spinner"></div>
+        <h2 class="coming-soon-title">部分功能待开发中</h2>
+        <p class="coming-soon-subtitle">请直接进行入驻、新增和下单等流程</p>
+        
+        <div class="process-guide">
+          <div class="guide-item">
+            <div class="guide-info">
+              <span class="dot"></span>
+              <span>履约入驻流程</span>
+            </div>
+          </div>
+          <div class="guide-item">
+            <div class="guide-info">
+              <span class="dot"></span>
+              <span>数据新增与修改</span>
+            </div>
+          </div>
+          <div class="guide-item">
+            <div class="guide-info">
+              <span class="dot"></span>
+              <span>核心下单业务流</span>
+            </div>
+          </div>
         </div>
       </div>
     </div>
@@ -22,9 +40,7 @@
 </template>
 
 <script setup name="Index" lang="ts">
-const goTarget = (url: string) => {
-  window.open(url, '__blank');
-};
+// 首页逻辑组件
 </script>
 
 <style lang="scss" scoped>
@@ -34,7 +50,7 @@ const goTarget = (url: string) => {
   align-items: center;
   justify-content: center;
   background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
-  font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
   overflow-x: hidden;
 }
 
@@ -46,8 +62,8 @@ const goTarget = (url: string) => {
 
 .coming-soon-content {
   background: rgba(255, 255, 255, 0.95);
-  border-radius: 16px;
-  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
+  border-radius: 20px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.08);
   backdrop-filter: blur(10px);
   -webkit-backdrop-filter: blur(10px);
   border: 1px solid rgba(255, 255, 255, 0.3);
@@ -57,67 +73,80 @@ const goTarget = (url: string) => {
 
   &:hover {
     transform: translateY(-5px);
-    box-shadow: 0 15px 50px rgba(0, 0, 0, 0.15);
+    box-shadow: 0 25px 70px rgba(0, 0, 0, 0.12);
   }
 }
 
 .icon-wrapper {
   margin-bottom: 30px;
-  animation: pulse 2s infinite;
+  animation: float 3s ease-in-out infinite;
 
   .icon {
     width: 80px;
     height: 80px;
-    color: #4a6fa5;
+    color: #409eff;
   }
 }
 
 .coming-soon-title {
-  font-size: 36px;
+  font-size: 32px;
   font-weight: 600;
-  color: #2c3e50;
+  color: #303133;
   margin: 0 0 15px 0;
-  letter-spacing: -0.5px;
+  letter-spacing: 1px;
 }
 
 .coming-soon-subtitle {
   font-size: 18px;
-  color: #7f8c8d;
+  color: #606266;
   margin: 0 0 40px 0;
   font-weight: 400;
 }
 
-.loading-spinner {
+.process-guide {
   display: flex;
-  justify-content: center;
-  align-items: center;
-  margin-top: 30px;
+  flex-direction: column;
+  gap: 15px;
+  max-width: 320px;
+  margin: 0 auto;
+  text-align: left;
 }
 
-.spinner {
-  width: 40px;
-  height: 40px;
-  border: 4px solid rgba(74, 111, 165, 0.2);
-  border-left-color: #4a6fa5;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-}
+.guide-item {
+  padding: 12px 20px;
+  background: #f8faff;
+  border-radius: 10px;
+  border: 1px border-color(#e4e7ed);
+  transition: all 0.2s ease;
 
-@keyframes spin {
-  to {
-    transform: rotate(360deg);
+  &:hover {
+    background: #ecf5ff;
+    transform: scale(1.02);
   }
-}
 
-@keyframes pulse {
+  .guide-info {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    color: #409eff;
+    font-size: 16px;
+    font-weight: 500;
+  }
 
-  0%,
-  100% {
-    transform: scale(1);
+  .dot {
+    width: 8px;
+    height: 8px;
+    background-color: #409eff;
+    border-radius: 50%;
   }
+}
 
+@keyframes float {
+  0%, 100% {
+    transform: translateY(0);
+  }
   50% {
-    transform: scale(1.05);
+    transform: translateY(-10px);
   }
 }
 
@@ -136,7 +165,7 @@ const goTarget = (url: string) => {
   }
 
   .coming-soon-title {
-    font-size: 28px;
+    font-size: 24px;
   }
 
   .coming-soon-subtitle {
@@ -144,3 +173,4 @@ const goTarget = (url: string) => {
   }
 }
 </style>
+

+ 1 - 0
src/views/login.vue

@@ -190,6 +190,7 @@ const getLoginData = () => {
   const password = localStorage.getItem('password');
   const rememberMe = localStorage.getItem('rememberMe');
   loginForm.value = {
+    userSource: 0,
     platformId: 0,
     tenantId: tenantId === null ? String(loginForm.value.tenantId) : tenantId,
     username: username === null ? String(loginForm.value.username) : username,

+ 53 - 15
src/views/order/dispatch/components/CustomerDetailDrawer.vue

@@ -70,14 +70,18 @@
       </el-tab-pane>
 
       <el-tab-pane label="历史订单" name="orders">
-        <el-table :data="mockOrders" border style="width: 100%">
-          <el-table-column prop="orderNo" label="订单编号" width="180" />
-          <el-table-column prop="service" label="服务项目" />
-          <el-table-column prop="pets" label="服务宠物" />
-          <el-table-column prop="time" label="服务时间" width="180" />
+        <el-table :data="historyOrders" border style="width: 100%">
+          <el-table-column prop="code" label="订单编号" width="180" />
+          <el-table-column label="服务项目">
+            <template #default="scope">
+              {{ getServiceName(scope.row.service) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="petName" label="服务宠物" />
+          <el-table-column prop="serviceTime" label="服务时间" width="180" />
           <el-table-column prop="status" label="状态" width="100">
             <template #default="scope">
-              <el-tag type="success" size="small">完成</el-tag>
+              <el-tag :type="getStatusType(scope.row.status)" size="small">{{ getStatusName(scope.row.status) }}</el-tag>
             </template>
           </el-table-column>
         </el-table>
@@ -101,11 +105,13 @@
 </template>
 
 <script setup>
-import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue'
+import { ref, computed, watch, onMounted, getCurrentInstance, toRefs } from 'vue'
 import { getCustomer } from '@/api/archieves/customer'
 import { listPetByUser } from '@/api/archieves/pet'
 import { listAllChangeLog } from '@/api/archieves/changeLog'
 import { listOnStore } from '@/api/system/areaStation'
+import { listSubOrderOnCustomer } from '@/api/order/subOrder/index'
+import { listServiceOnOrder } from '@/api/service/list/index'
 
 const props = defineProps({
   visible: {
@@ -136,25 +142,51 @@ const changeLogs = ref([])
 const detailActiveTab = ref('info')
 const allNodes = ref([])
 
-const mockOrders = ref([
-  { orderNo: 'DD20231001001', service: '上门喂养 (标准版)', pets: '旺财', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
-  { orderNo: 'DD20230915002', service: '深度洗护套餐', pets: '旺财, 咪咪', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
-])
+const historyOrders = ref([])
+const serviceOptions = ref([])
+
+const getServiceList = () => {
+  listServiceOnOrder().then((res) => {
+    serviceOptions.value = res.data || []
+  })
+}
+
+const getServiceName = (serviceId) => {
+  const item = serviceOptions.value.find((i) => i.id === serviceId)
+  return item ? item.name : '未知服务'
+}
+
+const getStatusName = (status) => {
+  const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' }
+  return map[status] || '未知'
+}
+
+const getStatusType = (status) => {
+  const map = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: 'success', 5: 'danger' }
+  return map[status] || 'info'
+}
 
 const loadAreaStation = async () => {
-  if(allNodes.value.length === 0) {
+  if (allNodes.value.length === 0) {
     const res = await listOnStore()
     allNodes.value = res.data || []
   }
 }
 
+const loadHistoryOrders = (customerId) => {
+  listSubOrderOnCustomer(customerId).then((res) => {
+    historyOrders.value = res.data || []
+  })
+}
+
 watch(() => props.visible, async (val) => {
   if (val && props.customerId) {
     detailActiveTab.value = 'info'
     currentUser.value = {}
     currentPets.value = []
     changeLogs.value = []
-    
+    historyOrders.value = []
+
     await loadAreaStation()
     getCustomer(props.customerId).then((res) => {
       const data = res.data
@@ -172,16 +204,22 @@ watch(() => props.visible, async (val) => {
       }
       currentUser.value = data
     })
-    
+
     listPetByUser(props.customerId).then((res) => {
       currentPets.value = res.data || []
     })
-    
+
     listAllChangeLog(props.customerId, 'customer').then((res) => {
       changeLogs.value = res.data || []
     })
+
+    loadHistoryOrders(props.customerId)
   }
 })
+
+onMounted(() => {
+  getServiceList()
+})
 </script>
 
 <style scoped>

+ 2 - 2
src/views/order/dispatch/components/DispatchDialog.vue

@@ -160,8 +160,8 @@ import { pageFulfillerOnOrder } from '@/api/fulfiller/pool'
 import { listAllTag } from '@/api/fulfiller/tag'
 import { getSubOrderInfo } from '@/api/order/subOrder/index'
 import { listServiceOnOrder } from '@/api/service/list/index'
-import CustomerDetailDrawer from './CustomerDetailDrawer.vue'
-import PetDetailDrawer from './PetDetailDrawer.vue'
+import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
+import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
 
 const props = defineProps({
     visible: Boolean,

+ 55 - 59
src/views/order/dispatch/index.vue

@@ -19,9 +19,9 @@
     </div>
 
     <div class="main-content">
-      <!-- Left: Real Baidu Map Area -->
+      <!-- Left: Real Gaode Map Area -->
       <div class="map-wrapper">
-        <div id="baidu-map" class="map-view"></div>
+        <div id="amap-container" class="map-view"></div>
 
         <!-- Bottom Left: Map Controls & Stats -->
         <div class="map-controls-panel">
@@ -120,12 +120,10 @@ const getOrdersList = () => {
       const isTransport = item.mode === 1 || item.mode === '1';
       const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
 
-      // 模拟经纬度,如果没有真实的话。这里优先保留原有mock结构的坐标展示逻辑。
-      // 注意:真实场景下后端应返回 lng/lat
+      // 模拟经纬度
       return {
         ...item,
         typeCode,
-        // 这里只是为了演示,如果没有坐标,地图会报错。通常真实数据会有。
         lng: item.lng || (116.4 + Math.random() * 0.1),
         lat: item.lat || (39.9 + Math.random() * 0.05)
       };
@@ -147,16 +145,12 @@ const getRidersList = () => {
   }).then(res => {
     ridersList.value = (res.data || []).map(r => ({
       ...r,
-      // 转换状态:resting:休息, busy:接单中, disabled:禁用
-      // UI 映射:online/busy -> 接单中, offline -> 休息中, disabled -> 禁用
       uiStatus: r.status === 'busy' ? 'busy' : r.status === 'resting' ? 'offline' : 'disabled',
       maskPhone: r.phone ? r.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
-      // categories 处理:serviceTypes 是 ids, 需要匹配名称
       categories: (r.serviceTypes || '').split(',').map(id => {
         const s = serviceOnOrderList.value.find(item => String(item.id) === String(id));
         return s ? s.name.substring(0, 2) : '服务';
       }),
-      // 真实的下一单时间
       nextOrderTime: r.nextOrderTime || '14:30',
       pendingCount: Math.floor(Math.random() * 3),
       todoCount: Math.floor(Math.random() * 5),
@@ -167,8 +161,6 @@ const getRidersList = () => {
   })
 }
 
-
-
 const getMerchantList = () => {
   if (!filters.station) {
     merchantList.value = [];
@@ -284,32 +276,36 @@ const handleViewRiderOrders = (rider) => {
   riderOrdersVisible.value = true;
 };
 
-// Map Logic
+// --- Map Logic ---
 let map = null;
-const ak = 'E4805d16520de693a3fe707cdc962045'; // Public Key
+const amapKey = 'a30e76f457c14b6570925522be37565d';
+const securityJsCode = '531ae14ec1dff87e552e1ea51e848582';
+
+const loadAMapScript = () => {
+  // 设置安全密钥
+  window._AMapSecurityConfig = {
+    securityJsCode: securityJsCode,
+  };
 
-const loadBMapScript = () => {
   return new Promise((resolve, reject) => {
-    if (window.BMap) {
-      resolve(window.BMap);
+    if (window.AMap) {
+      resolve(window.AMap);
       return;
     }
-    window.initBMapCallback = () => resolve(window.BMap);
     const script = document.createElement('script');
-    script.src = `https://api.map.baidu.com/api?v=3.0&ak=${ak}&callback=initBMapCallback`;
+    script.src = `https://webapi.amap.com/maps?v=2.0&key=${amapKey}`;
+    script.onload = () => resolve(window.AMap);
     script.onerror = reject;
     document.head.appendChild(script);
   });
 };
 
 const initMap = () => {
-  if (!window.BMap) return;
-  map = new BMap.Map('baidu-map');
-  const point = new BMap.Point(116.4551, 39.9255); // Chaoyang center
-  map.centerAndZoom(point, 14);
-  map.enableScrollWheelZoom(true);
-  map.setMapStyleV2({
-    styleId: '3d71dc5a4ce6228d3e9680188e982438'
+  if (!window.AMap) return;
+  map = new AMap.Map('amap-container', {
+    zoom: 14,
+    center: [116.4551, 39.9255], // Chaoyang center
+    mapStyle: 'amap://styles/normal' // 可以更换其他样式
   });
 
   refreshMarkers();
@@ -317,22 +313,18 @@ const initMap = () => {
 
 const refreshMarkers = () => {
   if (!map) return;
-  map.clearOverlays();
+  map.clearMap(); // 清除所有标记
 
   const filter = activeMapFilter.value;
 
-  // 1. Merchants
+  // 1. Merchants (商家)
   if (filter === 'all' || filter === 'merchants') {
     merchantList.value.forEach((m) => {
-      const pt = new BMap.Point(m.lng, m.lat);
       const iconImg = m.icon || 'https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png';
 
-      const html = `
-            <div style="position:absolute; transform:translate(-50%, -100%); width: 220px; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.2));">
+      const content = `
+            <div style="position:relative; width: 220px; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.2));">
                 <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;">
@@ -345,29 +337,28 @@ const refreshMarkers = () => {
                     </div>
                 </div>
                 <!-- Triangle -->
-                <div style="width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 10px solid #fff; margin: 0 auto;"></div>
+                <div style="width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 10px solid #fff; margin: 0 auto; margin-top: -1px;"></div>
             </div>
             `;
 
-      const label = new BMap.Label(html, { position: pt, offset: new BMap.Size(0, 0) });
-      label.setStyle({ border: 'none', background: 'transparent' });
-      map.addOverlay(label);
+      const marker = new AMap.Marker({
+        position: [m.lng, m.lat],
+        content: content,
+        offset: new AMap.Pixel(-110, -85)
+      });
+      map.add(marker);
     });
   }
 
-  // 2. Fulfiller
+  // 2. Fulfiller (履约者)
   if (filter === 'all' || filter === 'fulfillers') {
     ridersList.value.forEach((r) => {
-      const pt = new BMap.Point(r.lng, r.lat);
       const borderColor = r.uiStatus === 'busy' ? '#67C23A' : r.uiStatus === 'offline' ? '#909399' : '#F56C6C';
       const avatar = r.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png';
 
-      const html = `
-            <div style="position:absolute; transform:translate(-50%, -100%); width: 200px; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.2));">
+      const content = `
+            <div style="position:relative; width: 200px; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.2));">
                 <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;">
@@ -380,28 +371,34 @@ const refreshMarkers = () => {
                     </div>
                 </div>
                  <!-- Triangle -->
-                <div style="width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 8px solid #fff; margin: 0 auto;"></div>
+                <div style="width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 8px solid #fff; margin: 0 auto; margin-top: -1px;"></div>
             </div>
             `;
 
-      const label = new BMap.Label(html, { position: pt, offset: new BMap.Size(0, 0) });
-      label.setStyle({ border: 'none', background: 'transparent' });
-      map.addOverlay(label);
+      const marker = new AMap.Marker({
+        position: [r.lng, r.lat],
+        content: content,
+        offset: new AMap.Pixel(-100, -75)
+      });
+      map.add(marker);
     });
   }
 
-  // 3. Orders (Blue/Purple)
+  // 3. Orders (订单)
   if (filter === 'all' || filter === 'orders') {
     filteredOrders.value.forEach((o) => {
-      const pt = new BMap.Point(o.lng, o.lat);
-      const marker = new BMap.Marker(pt);
-
+      // 默认点标记 + 文本标注
       const labelContent = `<div style="border:1px solid #409EFF;background:#fff;color:#409EFF;padding:2px 6px;border-radius:4px;font-size:12px;">${getServiceName(o.service)}</div>`;
-      const label = new BMap.Label(labelContent, { position: pt, offset: new BMap.Size(-20, -35) });
-      label.setStyle({ border: 'none', background: 'transparent' });
 
-      map.addOverlay(marker);
-      map.addOverlay(label);
+      const marker = new AMap.Marker({
+        position: [o.lng, o.lat],
+        label: {
+          content: labelContent,
+          direction: 'top',
+          offset: new AMap.Pixel(0, -5)
+        }
+      });
+      map.add(marker);
     });
   }
 };
@@ -412,8 +409,7 @@ const setMapFilter = (val) => {
 
 const focusMapPoint = (lng, lat) => {
   if (!map) return;
-  const pt = new BMap.Point(lng, lat);
-  map.panTo(pt);
+  map.setCenter([lng, lat]);
   map.setZoom(16);
 };
 
@@ -428,7 +424,7 @@ watch([currentOrderTab, currentRiderTab], () => {
 onMounted(async () => {
   getServiceList()
   await getAreaStationList()
-  loadBMapScript()
+  loadAMapScript()
     .then(() => {
       initMap();
     })

+ 53 - 15
src/views/order/orderList/components/CustomerDetailDrawer.vue

@@ -70,14 +70,18 @@
       </el-tab-pane>
 
       <el-tab-pane label="历史订单" name="orders">
-        <el-table :data="mockOrders" border style="width: 100%">
-          <el-table-column prop="orderNo" label="订单编号" width="180" />
-          <el-table-column prop="service" label="服务项目" />
-          <el-table-column prop="pets" label="服务宠物" />
-          <el-table-column prop="time" label="服务时间" width="180" />
+        <el-table :data="historyOrders" border style="width: 100%">
+          <el-table-column prop="code" label="订单编号" width="180" />
+          <el-table-column label="服务项目">
+            <template #default="scope">
+              {{ getServiceName(scope.row.service) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="petName" label="服务宠物" />
+          <el-table-column prop="serviceTime" label="服务时间" width="180" />
           <el-table-column prop="status" label="状态" width="100">
             <template #default="scope">
-              <el-tag type="success" size="small">完成</el-tag>
+              <el-tag :type="getStatusType(scope.row.status)" size="small">{{ getStatusName(scope.row.status) }}</el-tag>
             </template>
           </el-table-column>
         </el-table>
@@ -101,11 +105,13 @@
 </template>
 
 <script setup>
-import { ref, getCurrentInstance, toRefs } from 'vue'
+import { ref, computed, watch, onMounted, getCurrentInstance, toRefs } from 'vue'
 import { getCustomer } from '@/api/archieves/customer'
 import { listPetByUser } from '@/api/archieves/pet'
 import { listAllChangeLog } from '@/api/archieves/changeLog'
 import { listOnStore } from '@/api/system/areaStation'
+import { listSubOrderOnCustomer } from '@/api/order/subOrder/index'
+import { listServiceOnOrder } from '@/api/service/list/index'
 
 const props = defineProps({
   visible: {
@@ -136,25 +142,51 @@ const changeLogs = ref([])
 const detailActiveTab = ref('info')
 const allNodes = ref([])
 
-const mockOrders = ref([
-  { orderNo: 'DD20231001001', service: '上门喂养 (标准版)', pets: '旺财', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
-  { orderNo: 'DD20230915002', service: '深度洗护套餐', pets: '旺财, 咪咪', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
-])
+const historyOrders = ref([])
+const serviceOptions = ref([])
+
+const getServiceList = () => {
+  listServiceOnOrder().then((res) => {
+    serviceOptions.value = res.data || []
+  })
+}
+
+const getServiceName = (serviceId) => {
+  const item = serviceOptions.value.find((i) => i.id === serviceId)
+  return item ? item.name : '未知服务'
+}
+
+const getStatusName = (status) => {
+  const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' }
+  return map[status] || '未知'
+}
+
+const getStatusType = (status) => {
+  const map = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: 'success', 5: 'danger' }
+  return map[status] || 'info'
+}
 
 const loadAreaStation = async () => {
-  if(allNodes.value.length === 0) {
+  if (allNodes.value.length === 0) {
     const res = await listOnStore()
     allNodes.value = res.data || []
   }
 }
 
+const loadHistoryOrders = (customerId) => {
+  listSubOrderOnCustomer(customerId).then((res) => {
+    historyOrders.value = res.data || []
+  })
+}
+
 watch(() => props.visible, async (val) => {
   if (val && props.customerId) {
     detailActiveTab.value = 'info'
     currentUser.value = {}
     currentPets.value = []
     changeLogs.value = []
-    
+    historyOrders.value = []
+
     await loadAreaStation()
     getCustomer(props.customerId).then((res) => {
       const data = res.data
@@ -172,16 +204,22 @@ watch(() => props.visible, async (val) => {
       }
       currentUser.value = data
     })
-    
+
     listPetByUser(props.customerId).then((res) => {
       currentPets.value = res.data || []
     })
-    
+
     listAllChangeLog(props.customerId, 'customer').then((res) => {
       changeLogs.value = res.data || []
     })
+
+    loadHistoryOrders(props.customerId)
   }
 })
+
+onMounted(() => {
+  getServiceList()
+})
 </script>
 
 <style scoped>

+ 2 - 2
src/views/order/orderList/components/DispatchDialog.vue

@@ -160,8 +160,8 @@ import { pageFulfillerOnOrder } from '@/api/fulfiller/pool'
 import { listAllTag } from '@/api/fulfiller/tag'
 import { getSubOrderInfo } from '@/api/order/subOrder/index'
 import { listServiceOnOrder } from '@/api/service/list/index'
-import CustomerDetailDrawer from './CustomerDetailDrawer.vue'
-import PetDetailDrawer from './PetDetailDrawer.vue'
+import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
+import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
 
 const props = defineProps({
     visible: Boolean,

+ 17 - 24
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -293,11 +293,13 @@
 </template>
 
 <script setup>
-import { ref, reactive, computed, watch } from 'vue'
+import { ref, reactive, computed, watch, getCurrentInstance } from 'vue'
 import { ElMessage } from 'element-plus'
 import { getPet } from '@/api/archieves/pet'
 import { getCustomer } from '@/api/archieves/customer'
-import { listSubOrderLog } from '@/api/order/subOrderLog'
+import { listSubOrderLog, exportSubOrderLogUrl } from '@/api/order/subOrderLog/index'
+
+const { proxy } = getCurrentInstance()
 
 // 视频判定辅助函数
 const isVideo = (url) => {
@@ -516,11 +518,13 @@ const serviceProgressSteps = computed(() => {
     const list = fulfillerLogs.value || []
     return list.map((i) => {
         // 使用 photoUrls 展示,而非 photos
-        const media = (i?.photoUrls || []).map(url => {
+        const rawUrls = i?.photoUrls || [];
+        const urlList = Array.isArray(rawUrls) ? rawUrls : (typeof rawUrls === 'string' ? rawUrls.split(',').filter(Boolean) : []);
+        const media = urlList.map(url => {
             const type = isVideo(url) ? 'video' : 'image';
             return { type, url }
         });
-        
+
         return {
             title: i?.title || '--',
             time: i?.createTime || i?.time || '',
@@ -533,27 +537,16 @@ const serviceProgressSteps = computed(() => {
 })
 
 const handleExportLogs = () => {
-    const logs = orderLogs.value || []
-    if (logs.length === 0) {
-        ElMessage.warning('暂无日志可导出')
-        return
+    const id = props.order?.id;
+    if (!id) {
+        ElMessage.warning('订单信息不完整,无法导出');
+        return;
     }
-    let csvContent = "时间,类型,标题,内容\n"
-    logs.forEach(log => {
-        const time = ''
-        const type = log.logType ?? ''
-        const title = (log.title || '').replace(/"/g, '""')
-        const content = (log.content || '').replace(/"/g, '""')
-        csvContent += `${time},${type},"${title}","${content}"\n`
-    })
-    const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' })
-    const url = URL.createObjectURL(blob)
-    const link = document.createElement("a")
-    link.href = url
-    link.download = `OrderLogs_${props.order.orderNo}.csv`
-    link.click()
-    URL.revokeObjectURL(url)
-    ElMessage.success('导出成功')
+    proxy?.download(
+        exportSubOrderLogUrl(id),
+        {},
+        `OrderLogs_${props.order.orderNo}_${new Date().getTime()}.xlsx`
+    );
 }
 </script>
 

+ 5 - 4
src/views/order/orderList/components/RewardDialog.vue

@@ -22,8 +22,8 @@
       <el-form :model="rewardForm" label-width="80px">
         <el-form-item label="操作类型">
           <el-radio-group v-model="rewardForm.type">
-            <el-radio label="reward">奖励 (增加)</el-radio>
-            <el-radio label="punish">惩罚 (扣除)</el-radio>
+            <el-radio :label="fulfillerEnums.ActionType.ADD">增加</el-radio>
+            <el-radio :label="fulfillerEnums.ActionType.REDUCE">减少</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="调整项目">
@@ -50,6 +50,7 @@
 <script setup>
 import { reactive, computed, watch } from 'vue';
 import { ElMessage } from 'element-plus';
+import fulfillerEnums from '@/enums/fulfiller.json';
 
 const props = defineProps({
   visible: Boolean,
@@ -63,7 +64,7 @@ const dialogVisible = computed({
 });
 
 const rewardForm = reactive({
-  type: 'reward',
+  type: fulfillerEnums.ActionType.ADD,
   item: 'points',
   value: 10,
   reason: ''
@@ -73,7 +74,7 @@ watch(
   () => props.visible,
   (val) => {
     if (val) {
-      rewardForm.type = 'reward';
+      rewardForm.type = fulfillerEnums.ActionType.ADD;
       rewardForm.item = 'points';
       rewardForm.value = 10;
       rewardForm.reason = '';

+ 13 - 6
src/views/order/orderList/index.vue

@@ -10,7 +10,7 @@
               <el-radio-button v-for="item in serviceOptions" :key="item.id" :label="item.id">{{ item.name
               }}</el-radio-button>
             </el-radio-group>
-            <el-input v-model="filters.content" placeholder="订单号/商户/宠主/手机号" class="search-input" prefix-icon="Search"
+            <el-input v-model="filters.content" placeholder="订单号/品牌/宠主/手机号" class="search-input" prefix-icon="Search"
               clearable @clear="handleSearch" @keyup.enter="handleSearch" />
             <el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
           </div>
@@ -167,9 +167,11 @@
   </div>
 </template>
 
-<script setup>
+<script setup lang="ts">
 import { ref, reactive, onMounted, nextTick } from 'vue';
+import { useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
+import fulfillerEnums from '@/enums/fulfiller.json';
 import OrderDetailDrawer from './components/OrderDetailDrawer.vue';
 import DispatchDialog from './components/DispatchDialog.vue';
 import CareSummaryDrawer from './components/CareSummaryDrawer.vue';
@@ -641,14 +643,19 @@ const handleRewardSubmit = async (form) => {
     return;
   }
   try {
+    // 针对余额(balance)目标,将元转为分
+    const submitAmount = form.item === 'balance' ? Math.round(form.value * 100) : form.value;
+    
     await reward({
       fulfillerId: currentOperateRow.value.fulfiller,
       type: form.type,
       target: form.item,
-      amount: form.value,
-      reason: form.reason
-    });
-    ElMessage.success(`操作成功:${form.type === 'reward' ? '奖励' : '惩罚'}已执行`);
+      amount: submitAmount,
+      reason: form.reason,
+      bizType: form.type === fulfillerEnums.ActionType.ADD ? 'order_reward' : 'order_punish',
+      orderId: currentOperateRow.value.id
+    } as any);
+    ElMessage.success(`操作成功:${form.type === fulfillerEnums.ActionType.ADD ? '增加' : '减少'}已执行`);
     handleSearch();
   } catch {
     // Error handled by interceptor

+ 70 - 15
src/views/system/store/index.vue

@@ -162,7 +162,7 @@
         <el-form-item label="营业时间" prop="startBusinessTime">
           <el-row :gutter="10">
             <el-col :span="10">
-              <el-time-picker clearable v-model="form.startBusinessTime" value-format="HH:mm" placeholder="开始时间"
+              <el-time-picker clearable v-model="form.startBusinessTime" format="HH:mm" value-format="HH:mm" placeholder="开始时间"
                 style="width: 100%">
               </el-time-picker>
             </el-col>
@@ -170,7 +170,7 @@
             </el-col>
             <el-col :span="10">
-              <el-time-picker clearable v-model="form.endBusinessTime" value-format="HH:mm" placeholder="结束时间"
+              <el-time-picker clearable v-model="form.endBusinessTime" format="HH:mm" value-format="HH:mm" placeholder="结束时间"
                 style="width: 100%">
               </el-time-picker>
             </el-col>
@@ -256,18 +256,28 @@
           </el-descriptions>
         </el-tab-pane>
         <el-tab-pane label="服务订单记录" name="orders">
-          <el-table :data="orderList" border style="width: 100%">
-            <el-table-column label="订单号" prop="orderNo" min-width="150" />
-            <el-table-column label="服务项目" prop="service" min-width="120" />
+          <el-table :data="orderList" border style="width: 100%" v-loading="orderLoading">
+            <el-table-column label="订单号" prop="code" min-width="150" />
+            <el-table-column label="服务项目" min-width="120">
+              <template #default="scope">
+                {{ getServiceName(scope.row.service) }}
+              </template>
+            </el-table-column>
             <el-table-column label="客户" prop="customer" min-width="100" />
-            <el-table-column label="金额" prop="amount" min-width="100" />
-            <el-table-column label="下单时间" prop="time" min-width="160" />
+            <el-table-column label="金额" min-width="100">
+              <template #default="scope">
+                <span>¥{{ (scope.row.price / 100).toFixed(2) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="下单时间" prop="createTime" min-width="160" />
             <el-table-column label="状态" align="center" width="100">
               <template #default="scope">
-                <el-tag :type="scope.row.statusType" effect="plain" size="small">{{ scope.row.status }}</el-tag>
+                <el-tag :type="getOrderStatusType(scope.row.status)" effect="plain" size="small">{{ getOrderStatusName(scope.row.status) }}</el-tag>
               </template>
             </el-table-column>
           </el-table>
+          <pagination v-show="orderTotal > 0" :total="orderTotal" v-model:page="orderQueryParams.pageNum"
+            v-model:limit="orderQueryParams.pageSize" @pagination="getOrderList" />
         </el-tab-pane>
       </el-tabs>
       <template #footer>
@@ -302,6 +312,7 @@
 
 <script setup name="Store" lang="ts">
 import { listStore, getStore, delStore, addStore, updateStore, listStoreStatus, renewStore, banStore, enableStore } from '@/api/system/store';
+import { listSubOrderOnStore } from '@/api/order/subOrder/index';
 import { StoreVO, StoreForm, StoreQuery, StoreStatusVO, SysStorePageBo } from '@/api/system/store/types';
 import { listOnStore } from '@/api/system/tenant';
 import { listOnStore as listTenantCategoriesOnStore } from '@/api/system/tenantCategories';
@@ -383,11 +394,36 @@ const detailDialog = reactive({
 });
 const activeTab = ref('basic');
 const detailData = ref<any>({});
-const orderList = ref([
-  { orderNo: 'ORD202402040001', service: '洗澡美容', customer: '张三', amount: '¥128', time: '2024-02-04 10:00', status: '已完成', statusType: 'success' },
-  { orderNo: 'ORD202402040002', service: '寄养服务', customer: '李四', amount: '¥500', time: '2024-02-03 14:30', status: '已完成', statusType: 'success' },
-  { orderNo: 'ORD202402030005', service: '疫苗注射', customer: '王五', amount: '¥80', time: '2024-02-01 09:00', status: '已取消', statusType: 'info' }
-]);
+const orderList = ref<any[]>([]);
+const orderTotal = ref(0);
+const orderLoading = ref(false);
+const orderQueryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  storeId: undefined as any
+});
+
+const getOrderStatusName = (status: number) => {
+  const map: any = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' };
+  return map[status] || '未知';
+};
+
+const getOrderStatusType = (status: number) => {
+  const map: any = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: 'success', 5: 'danger' };
+  return map[status] || 'info';
+};
+
+/** 查询订单记录 */
+const getOrderList = async () => {
+  orderLoading.value = true;
+  try {
+    const res: any = await listSubOrderOnStore(orderQueryParams);
+    orderList.value = res.rows;
+    orderTotal.value = res.total;
+  } finally {
+    orderLoading.value = false;
+  }
+};
 
 /** 详情按钮操作 */
 const handleDetail = async (row: StoreVO) => {
@@ -395,6 +431,9 @@ const handleDetail = async (row: StoreVO) => {
   // 合并列表里的关联数据,以便能够展示名称等额外字段
   detailData.value = { ...row, ...res.data };
   activeTab.value = 'basic';
+  orderQueryParams.storeId = row.id;
+  orderQueryParams.pageNum = 1;
+  getOrderList();
   detailDialog.visible = true;
 };
 
@@ -920,11 +959,27 @@ const getStatusList = async () => {
 const formatTime = (time: string | number): string => {
   if (!time) return '';
 
+  // 如果已经是 HH:mm 格式,直接返回
+  if (typeof time === 'string' && /^\d{2}:\d{2}$/.test(time)) {
+    return time;
+  }
+
+  // 如果是 HH:mm:ss 格式,截取前5位
+  if (typeof time === 'string' && /^\d{2}:\d{2}:\d{2}$/.test(time)) {
+    return time.substring(0, 5);
+  }
+
   // 处理时间戳或日期字符串
-  const date = new Date(time);
+  let date: Date;
+  if (typeof time === 'string' && !time.includes('-') && !time.includes('T')) {
+    // 尝试补全日期以使 Date 能够解析某些纯时间字符串,或者如果上面正则没匹配到则直接返回
+    return time;
+  } else {
+    date = new Date(time);
+  }
 
   // 检查是否是有效日期
-  if (isNaN(date.getTime())) return '';
+  if (isNaN(date.getTime())) return String(time);
 
   // 格式化为 HH:mm
   const hours = date.getHours().toString().padStart(2, '0');

+ 1 - 0
vite.config.ts

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