Huanyi před 4 týdny
rodič
revize
05cd3db0f7

+ 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 - 1
src/api/archieves/customer/types.ts

@@ -22,6 +22,8 @@ export interface UsrCustomerVO {
   remark: string;
   createTime: string;
   tags: SysTagVO[];
+  tenantName?: string;
+  source?: 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 {
@@ -48,7 +51,6 @@ export interface UsrCustomerQuery extends PageQuery {
   areaId?: number;
   stationId?: number;
   status?: number;
-  tab?: number;
 }
 
 export interface CustomerOnOrderVO {

+ 0 - 1
src/api/archieves/pet/types.ts

@@ -68,5 +68,4 @@ export interface UsrPetForm extends BaseEntity {
 export interface UsrPetQuery extends PageQuery {
   keyword?: string;
   userId?: number;
-  tab?: number;
 }

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

@@ -96,6 +96,8 @@ export interface FlfRewardForm {
   target: string;
   amount: number;
   reason: string;
+  bizType?: string;
+  orderId?: string | number;
 }
 
 /**

+ 36 - 1
src/api/order/subOrder/index.ts

@@ -1,6 +1,6 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { SubOrderVO, SubOrderQuery, SubOrderListParams, SubOrderListResult } from './types';
+import { SubOrderVO, SubOrderQuery, SubOrderListParams, SubOrderListResult, SubOrderHistoryVO, SubOrderPetVO, SubOrderStoreVO } from './types';
 
 /**
  * 查询子订单列表
@@ -70,3 +70,38 @@ export const nursingSummarySubOrder = (data: { orderId: string | number; content
         data
     });
 };
+
+/**
+ * 根据客户ID查询子订单列表
+ * @param customerId 
+ * @returns 
+ */
+export const listSubOrderOnCustomer = (customerId: string | number): AxiosPromise<SubOrderHistoryVO[]> => {
+    return request({
+        url: '/order/subOrder/listOnCustomer',
+        method: 'get',
+        params: { customerId }
+    });
+};
+
+/**
+ * 根据宠物ID查询子订单列表
+ */
+export const listSubOrderOnPet = (petId: string | number): AxiosPromise<SubOrderPetVO[]> => {
+  return request({
+    url: '/order/subOrder/listOnPet',
+    method: 'get',
+    params: { petId }
+  });
+};
+
+/**
+ * 根据门店查询服务订单记录
+ */
+export const listSubOrderOnStore = (params: { storeId: string | number; pageNum: number; pageSize: number; }): AxiosPromise<{ total: number, rows: SubOrderStoreVO[] }> => {
+  return request({
+    url: '/order/subOrder/listOnStore',
+    method: 'get',
+    params
+  });
+};

+ 28 - 0
src/api/order/subOrder/types.ts

@@ -51,3 +51,31 @@ export interface SubOrderListResult {
     code: number;
     msg: string;
 }
+
+export interface SubOrderHistoryVO {
+    id: number;
+    code: string;
+    service: string;
+    pet: string;
+    serviceTime: string;
+    status: number;
+}
+
+export interface SubOrderPetVO {
+  id: number;
+  code: string;
+  service: number;
+  price: number;
+  serviceTime: string;
+  status: number;
+}
+
+export interface SubOrderStoreVO {
+  id: number;
+  code: string;
+  service: number;
+  customer: string;
+  price: number;
+  createTime: string;
+  status: number;
+}

+ 11 - 0
src/api/order/subOrderLog/index.ts

@@ -9,3 +9,14 @@ export const listSubOrderLog = (query: SubOrderLogQuery): AxiosPromise<SubOrderL
         params: query
     });
 };
+
+/**
+ * 导出订单日志
+ * @param orderId 订单ID
+ */
+export const exportSubOrderLog = (orderId: string | number) => {
+    return request({
+        url: '/order/subOrderLog/export/' + orderId,
+        method: 'post'
+    });
+};

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

@@ -0,0 +1,302 @@
+<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.tenantName || '-' }}</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"
+    }
+  }
+}

+ 48 - 182
src/views/archieves/customer/index.vue

@@ -1,9 +1,6 @@
 <template>
   <div class="page-container">
-    <el-tabs v-model="queryParams.tab" class="customer-tabs" @tab-change="handleTabChange">
-      <el-tab-pane label="本品牌所属用户" :name="0" />
-      <el-tab-pane label="订单关联用户" :name="1" />
-    </el-tabs>
+
     <el-card shadow="never">
       <template #header>
         <div class="card-header">
@@ -45,12 +42,15 @@
             <el-tag v-for="tag in scope.row.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px;">{{ tag.name }}</el-tag>
           </template>
         </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 style="font-size: 12px; color: #999; margin-top: 4px;">{{ scope.row.createTime }}</div>
-          </template>
-        </el-table-column>
+<!--        <el-table-column label="录入信息" width="200">-->
+<!--          <template #default="scope">-->
+<!--            <div v-if="scope.row.tenantName" style="margin-bottom: 4px;">-->
+<!--              <el-tag size="small" effect="light">{{ scope.row.tenantName }}</el-tag>-->
+<!--            </div>-->
+<!--&lt;!&ndash;            <div><el-tag size="small" effect="plain" :type="scope.row.source && scope.row.source.includes('平台') ? '' : 'warning'">{{ scope.row.source || '-' }}</el-tag></div>&ndash;&gt;-->
+<!--            <div style="font-size: 12px; color: #999; margin-top: 4px;">{{ scope.row.createTime }}</div>-->
+<!--          </template>-->
+<!--        </el-table-column>-->
         <el-table-column label="订单数量" width="120" align="center" sortable prop="orderCount">
           <template #default="scope">
             <div>{{ scope.row.orderCount }}单</div>
@@ -107,116 +107,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.changeType }}] {{ 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="currentCustomerId"
+      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>
@@ -230,15 +131,15 @@
           </el-col>
 
           <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 }))"
-                          :total="brandTotal" :pageSize="10" placeholder="请选择所属品牌"
-                          @page-change="handleBrandPageChange"
-                          @visible-change="handleBrandVisibleChange" />
-            </el-form-item>
-          </el-col>
+<!--          <el-col :span="12">-->
+<!--            <el-form-item label="录入来源">-->
+<!--              <PageSelect v-model="form.tenantId"-->
+<!--                          :options="brandList.map(item => ({ value: item.id, label: item.name }))"-->
+<!--                          :total="brandTotal" :pageSize="10" placeholder="请选择所属品牌"-->
+<!--                          @page-change="handleBrandPageChange"-->
+<!--                          @visible-change="handleBrandVisibleChange" />-->
+<!--            </el-form-item>-->
+<!--          </el-col>-->
           <el-col :span="12">
             <el-form-item label="所属区域">
               <el-cascader v-model="formAreaValue" :options="areaTreeOptions" placeholder="请选择区域"
@@ -488,7 +389,10 @@ import { listOnStore } from '@/api/system/areaStation'
 import { listOnStore as listBrandOnStore } from '@/api/system/tenant'
 import { regionData, codeToText } from 'element-china-area-data'
 import PageSelect from '@/components/PageSelect/index.vue'
+import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
+import { useUserStore } from '@/store/modules/user'
 
+const userStore = useUserStore()
 const { proxy } = getCurrentInstance()
 const { sys_user_sex, sys_customer_status, sys_house_type, sys_entry_method, sys_pet_gender, sys_pet_size, sys_pet_type, sys_pet_breed } = toRefs(
   proxy?.useDict('sys_user_sex', 'sys_customer_status', 'sys_house_type', 'sys_entry_method', 'sys_pet_gender', 'sys_pet_size', 'sys_pet_type', 'sys_pet_breed')
@@ -534,15 +438,10 @@ const queryParams = reactive({
   keyword: '',
   areaId: undefined,
   stationId: undefined,
-  status: undefined,
-  tab: 0
+  status: undefined
 })
 
-/** 处理标签页切换 */
-const handleTabChange = () => {
-  queryParams.pageNum = 1
-  getList()
-}
+
 
 const searchForm = reactive({
   keyword: '',
@@ -560,12 +459,12 @@ const petDialogActiveTab = ref('basic')
 
 const selectedTagIds = ref([])
 const currentUser = ref({})
-const currentPets = ref([])
 const tableData = ref([])
 
 const allUserTags = ref([])
 const allPetTags = ref([])
-const changeLogs = ref([])
+const currentCustomerId = ref(null)
+const customerDetailRef = ref(null)
 const userAvatarDisplayUrl = ref('')
 const petAvatarDisplayUrl = ref('')
 const petVaccineCertDisplayUrl = ref('')
@@ -576,10 +475,7 @@ 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' }
-])
+// 移除 mockOrders
 
 const form = reactive({
   id: undefined,
@@ -598,7 +494,7 @@ const form = reactive({
   entryMethod: '',
   entryPassword: '',
   keyLocation: '',
-  source: '',
+  tenantId: undefined,
   emergencyContact: '',
   emergencyPhone: '',
   memberLevel: 0,
@@ -700,7 +596,7 @@ const handleSearch = () => {
 }
 
 const loadTags = () => {
-  listAllTag({ category: 'user', status: 0 }).then((res) => {
+  listAllTag({ category: 'customer', status: 0 }).then((res) => {
     allUserTags.value = res.data || []
   }).catch((err) => {
     console.error('加载用户标签失败', err)
@@ -718,7 +614,7 @@ const handleAdd = () => {
   Object.assign(form, {
     id: undefined, name: '', phone: '', avatar: undefined, gender: undefined, birthday: '', idCard: '',
     areaId: undefined, stationId: undefined, regionCode: '', region: [], address: '',
-    houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', source: '',
+    houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', tenantId: userStore.tenantId,
     emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: []
   })
   userAvatarDisplayUrl.value = ''
@@ -736,7 +632,7 @@ const handleEdit = (row) => {
       birthday: data.birthday, idCard: data.idCard, areaId: data.areaId, stationId: data.stationId,
       regionCode: data.regionCode, region: data.regionCode ? data.regionCode.split('/') : [],
       address: data.address, houseType: data.houseType, entryMethod: data.entryMethod,
-      entryPassword: data.entryPassword, keyLocation: data.keyLocation, source: data.source,
+      entryPassword: data.entryPassword, keyLocation: data.keyLocation, tenantId: data.tenantId,
       emergencyContact: data.emergencyContact, emergencyPhone: data.emergencyPhone,
       memberLevel: data.memberLevel, status: data.status, remark: data.remark, tagIds: []
     })
@@ -768,41 +664,11 @@ const handleEdit = (row) => {
 }
 
 const handleDetail = (row) => {
-  getCustomer(row.id).then((res) => {
-    const data = res.data
-    // Convert areaId to area name
-    if (data.areaId) {
-      const area = allNodes.value.find(n => n.id === data.areaId)
-      data.areaName = area ? area.name : '-'
-    } else {
-      data.areaName = '-'
-    }
-    // Convert stationId to station name
-    if (data.stationId) {
-      const station = allNodes.value.find(n => n.id === data.stationId)
-      data.stationName = station ? station.name : '-'
-    } else {
-      data.stationName = '-'
-    }
-    currentUser.value = data
-    detailActiveTab.value = 'info'
-    loadDetailPets(row.id)
-    loadDetailLogs(row.id, 'customer')
-    drawerVisible.value = true
-  })
+  currentCustomerId.value = row.id
+  drawerVisible.value = true
 }
 
-const loadDetailPets = (userId) => {
-  listPetByUser(userId).then((res) => {
-    currentPets.value = res.data || []
-  })
-}
-
-const loadDetailLogs = (targetId, targetType) => {
-  listAllChangeLog(targetId, targetType).then((res) => {
-    changeLogs.value = res.data || []
-  })
-}
+// 移除不需要的方法
 
 const handleRemark = (row) => {
   currentUser.value = row
@@ -954,7 +820,7 @@ const handlePetUploadVaccineCert = async (file) => {
 const openAddPet = () => {
   petDialogActiveTab.value = 'basic'
   Object.assign(petForm, {
-    id: undefined, userId: currentUser.value.id, avatar: undefined, name: '', type: 0, gender: undefined,
+    id: undefined, userId: currentCustomerId.value, avatar: undefined, name: '', type: 0, gender: undefined,
     breed: '', birthday: '', age: 1, weight: 5, size: 'small', isSterilized: 0,
     arrivalTime: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
     personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
@@ -997,7 +863,7 @@ const handlePetDelete = (row) => {
   ElMessageBox.confirm(`确认删除宠物 [${row.name}] 吗?`, '提示', { type: 'warning' }).then(() => {
     delPet(row.id).then(() => {
       ElMessage.success('宠物删除成功')
-      loadDetailPets(currentUser.value.id)
+      customerDetailRef.value.refresh()
       getList()
     })
   })
@@ -1011,7 +877,7 @@ const savePet = () => {
   api.then(() => {
     ElMessage.success('宠物档案保存成功')
     petDialogVisible.value = false
-    loadDetailPets(currentUser.value.id)
+    customerDetailRef.value.refresh()
     getList()
   }).finally(() => {
     submitLoading.value = false

+ 16 - 142
src/views/archieves/pet/index.vue

@@ -1,9 +1,6 @@
 <template>
   <div class="page-container">
-    <el-tabs v-model="queryParams.tab" class="pet-tabs" @tab-change="handleTabChange">
-      <el-tab-pane label="本品牌所属宠物" :name="0" />
-      <el-tab-pane label="订单关联宠物" :name="1" />
-    </el-tabs>
+
     <el-card shadow="never">
       <template #header>
         <div class="card-header">
@@ -77,7 +74,7 @@
       </div>
     </el-card>
 
-    <el-dialog v-model="dialogVisible" title="宠物档案详情" width="800px">
+    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑宠物档案' : '新增宠物档案'" width="800px">
       <el-tabs v-model="activeTab">
         <el-tab-pane label="基本信息" name="basic">
           <el-form :model="form" label-width="100px">
@@ -176,7 +173,7 @@
               </el-radio-group>
             </el-form-item>
             <el-form-item label="是否有攻击倾向">
-              <el-switch v-model="form.aggression" active-text="是" inactive-text="否" />
+              <el-switch v-model="form.aggression" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否" />
             </el-form-item>
             <el-form-item label="疫苗情况">
               <el-radio-group v-model="form.vaccineStatus">
@@ -202,110 +199,15 @@
         </el-tab-pane>
       </el-tabs>
       <template #footer>
-        <span class="dialog-footer">
-          <el-button @click="dialogVisible = false">取消</el-button>
-          <el-button type="primary" :loading="submitLoading" @click="saveData">保存</el-button>
-        </span>
+        <div class="dialog-footer">
+          <el-button @click="dialogVisible = false">取 消</el-button>
+          <el-button type="primary" :loading="submitLoading" @click="saveData">确 定</el-button>
+        </div>
       </template>
     </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>
-
-        <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>
+    <!-- Pet Profile Drawer (已抽离为公共组件) -->
+    <pet-detail-drawer v-model:visible="drawerVisible" :pet-id="currentPetId" editable @remark-saved="getList" />
 
     <!-- Remark Dialog -->
     <el-dialog v-model="remarkDialogVisible" title="添加备注" width="400px">
@@ -322,12 +224,12 @@
 
 <script setup>
 import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
+import PetDetailDrawer from '@/components/PetDetailDrawer/index.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';
 
 const { proxy } = getCurrentInstance();
 const { sys_pet_gender, sys_pet_type, sys_pet_size, sys_pet_breed, sys_house_type, sys_entry_method } = toRefs(
@@ -342,15 +244,10 @@ const tableData = ref([]);
 const queryParams = reactive({
   pageNum: 1,
   pageSize: 10,
-  keyword: '',
-  tab: 0
+  keyword: ''
 });
 
-/** 处理标签页切换 */
-const handleTabChange = () => {
-  queryParams.pageNum = 1;
-  getList();
-};
+
 
 const searchKey = ref('');
 
@@ -359,17 +256,11 @@ const drawerVisible = ref(false);
 const remarkDialogVisible = ref(false);
 const isEdit = ref(false);
 const activeTab = ref('basic');
-const detailActiveTab = ref('info');
 const currentPet = ref({});
+const currentPetId = ref(null);
 
 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('');
@@ -473,14 +364,8 @@ 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;
-  });
+  currentPetId.value = row.id;
+  drawerVisible.value = true;
 };
 
 const handleRemark = (row) => {
@@ -589,18 +474,7 @@ onMounted(() => {
 </script>
 
 <style scoped>
-.pet-tabs {
-  margin-bottom: 20px;
-  background-color: #fff;
-  padding: 10px 20px 0;
-  border-radius: 4px;
-}
-:deep(.el-tabs__header) {
-  margin-bottom: 0;
-}
-:deep(.el-tabs__nav-wrap::after) {
-  height: 0;
-}
+
 
 .page-container {
   padding: 20px;

+ 69 - 38
src/views/index.vue

@@ -11,13 +11,31 @@
               stroke-linecap="round"
               stroke-linejoin="round"
             />
-            <path d="M12 8V12L15 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="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>
@@ -25,9 +43,7 @@
 </template>
 
 <script setup name="Index" lang="ts">
-const goTarget = (url: string) => {
-  window.open(url, '__blank');
-};
+// 首页逻辑组件
 </script>
 
 <style lang="scss" scoped>
@@ -37,7 +53,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;
 }
 
@@ -49,8 +65,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);
@@ -60,66 +76,81 @@ 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;
+
+  &:hover {
+    background: #ecf5ff;
+    transform: scale(1.02);
+  }
 
-@keyframes spin {
-  to {
-    transform: rotate(360deg);
+  .guide-info {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    color: #409eff;
+    font-size: 16px;
+    font-weight: 500;
+  }
+
+  .dot {
+    width: 8px;
+    height: 8px;
+    background-color: #409eff;
+    border-radius: 50%;
   }
 }
 
-@keyframes pulse {
+@keyframes float {
   0%,
   100% {
-    transform: scale(1);
+    transform: translateY(0);
   }
-
   50% {
-    transform: scale(1.05);
+    transform: translateY(-10px);
   }
 }
 
@@ -138,7 +169,7 @@ const goTarget = (url: string) => {
   }
 
   .coming-soon-title {
-    font-size: 28px;
+    font-size: 24px;
   }
 
   .coming-soon-subtitle {

+ 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: 1,
     tenantId: tenantId === null ? String(loginForm.value.tenantId) : tenantId,
     username: username === null ? String(loginForm.value.username) : username,

+ 55 - 13
src/views/merchant/storeManagement/index.vue

@@ -157,7 +157,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>
@@ -165,7 +165,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>
@@ -251,18 +251,30 @@
           </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="服务项目" prop="service" 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="金额" prop="price" min-width="100">
+              <template #default="scope">
+                ¥ {{ (scope.row.price / 100).toFixed(2) }}
+              </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>
@@ -301,6 +313,8 @@ import { StoreVO, StoreQuery, StoreForm, StoreStatusVO, SysStorePageBo, StoreRen
 import { listOnStore } from '@/api/system/tenant';
 import { listOnStore as listTenantCategoriesOnStore } from '@/api/system/tenantCategories';
 import { listOnStore as listServiceOnStore } from '@/api/service/list';
+import { listSubOrderOnStore } from '@/api/order/subOrder';
+import { SubOrderStoreVO } from '@/api/order/subOrder/types';
 import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation';
 import { SysAreaStationOnStoreVo } from '@/api/system/areaStation/types';
 import { regionData, codeToText, textToCode } from 'element-china-area-data';
@@ -392,11 +406,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<SubOrderStoreVO[]>([]);
+const orderTotal = ref(0);
+const orderLoading = ref(false);
+const orderQueryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  storeId: undefined as any
+});
+
+const getOrderList = async () => {
+  if (!orderQueryParams.storeId) return;
+  orderLoading.value = true;
+  try {
+    const res = await listSubOrderOnStore(orderQueryParams);
+    orderList.value = res.rows;
+    orderTotal.value = res.total;
+  } finally {
+    orderLoading.value = false;
+  }
+};
+
+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 renewDialog = reactive({
   visible: false,
@@ -449,6 +488,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;
 };
 

+ 1 - 1
src/views/order/management/components/DispatchDialog.vue

@@ -160,7 +160,7 @@ 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 CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
 import PetDetailDrawer from './PetDetailDrawer.vue'
 
 const props = defineProps({

+ 41 - 39
src/views/order/management/components/OrderDetailDrawer.vue

@@ -15,11 +15,11 @@
                 <div class="right-head">
                     <!-- Action Buttons Group -->
                     <div class="detail-actions">
-                        <template v-if="[0, 1, 2].includes(order.status)">
-                            <el-button type="success" icon="Bicycle" @click="emit('dispatch', order)">
-                                {{ order.fulfiller || order.fulfillerName ? '重新派单' : '立即派单' }}
-                            </el-button>
-                        </template>
+<!--                        <template v-if="[0, 1, 2].includes(order.status)">-->
+<!--                            <el-button type="success" icon="Bicycle" @click="emit('dispatch', order)">-->
+<!--                                {{ order.fulfiller || order.fulfillerName ? '重新派单' : '立即派单' }}-->
+<!--                            </el-button>-->
+<!--                        </template>-->
 
                         <template v-if="order.status === 0">
                             <el-button type="danger" plain icon="CircleClose"
@@ -64,7 +64,7 @@
                 <!-- 3. Top Info: Pet & User -->
                 <div class="top-info-row">
                     <!-- Left: Pet Info -->
-                    <div class="info-section pet-section">
+                    <div class="info-section pet-section" style="cursor: pointer" @click="petDetailVisible = true">
                         <div class="sec-header">
                             <span class="label">宠物档案</span>
                             <el-tag size="small" effect="plain">{{ order.petBreed }}</el-tag>
@@ -185,7 +185,7 @@
                             <div v-if="['feeding', 'washing'].includes(order.type)" class="section-block">
                                 <div class="sec-title-bar">服务执行要求</div>
                                 <el-descriptions :column="2" border size="default" class="custom-desc">
-                                    <el-descriptions-item label="服务地址" :span="2">{{ order.detail.area || order.address
+                                    <el-descriptions-item label="服务地址" :span="2">{{ order.detail?.area || order.address
                                     }}</el-descriptions-item>
                                 </el-descriptions>
                             </div>
@@ -242,8 +242,10 @@
                                         <div class="p-media" v-if="step.media && step.media.length">
                                             <div v-for="(item, i) in step.media" :key="i" class="media-item">
                                                 <el-image v-if="item.type === 'image'" :src="item.url"
-                                                    :preview-src-list="step.media.map(m => m.url)" fit="cover"
-                                                    class="p-img" :preview-teleported="true" />
+                                                    :preview-src-list="step.media.filter(m => m.type === 'image').map(m => m.url)"
+                                                    fit="cover" class="p-img" :preview-teleported="true" />
+                                                <video v-else-if="item.type === 'video'" :src="item.url" controls
+                                                    class="p-video"></video>
                                             </div>
                                         </div>
                                     </div>
@@ -272,16 +274,20 @@
                     </el-tab-pane>
                 </el-tabs>
             </div>
+            <PetDetailDrawer v-model:visible="petDetailVisible" :pet-id="order?.pet || order?.petId" />
         </div>
     </el-drawer>
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue'
+import { ref, 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, exportSubOrderLog } from '@/api/order/subOrderLog'
+import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
+
+const { proxy } = getCurrentInstance()
 
 const props = defineProps({
     visible: Boolean,
@@ -380,6 +386,8 @@ watch(() => props.order, (val) => {
     loadOrderLogs(val)
 }, { immediate: true, deep: true })
 
+const petDetailVisible = ref(false)
+
 const activeDetailTab = ref('basic')
 
 const getStatusName = (status) => {
@@ -486,44 +494,35 @@ const currentOrderSteps = computed(() => {
 const serviceProgressSteps = computed(() => {
     const list = fulfillerLogs.value || []
     return list.map((i) => {
-        const photos = (i?.photos || '')
-            .split(',')
-            .map(s => s.trim())
-            .filter(Boolean)
-            .map(url => ({ type: 'image', url }))
+        const media = (i?.photoUrls || []).map(url => {
+            const isVideo = url.toLowerCase().match(/\.(mp4|mov|avi|wmv|flv|mkv)$/)
+            return {
+                type: isVideo ? 'video' : 'image',
+                url
+            }
+        })
         return {
             title: i?.title || '--',
-            time: '',
+            time: i?.createTime || '',
             icon: undefined,
             color: '#ff9900',
             desc: i?.content || '',
-            media: photos
+            media: media
         }
     })
 })
 
 const handleExportLogs = () => {
-    const logs = orderLogs.value || []
-    if (logs.length === 0) {
-        ElMessage.warning('暂无日志可导出')
+    const id = order.value?.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(
+        `order/subOrderLog/export/${id}`,
+        {},
+        `OrderLogs_${order.value.orderNo}_${new Date().getTime()}.xlsx`
+    )
 }
 </script>
 
@@ -792,8 +791,11 @@ const handleExportLogs = () => {
     flex-wrap: wrap;
 }
 
-.media-item {
-    display: inline-block;
+.p-video {
+    width: 140px;
+    height: 80px;
+    border-radius: 4px;
+    background: #000;
 }
 
 .p-img {

+ 6 - 6
src/views/order/management/components/RewardDialog.vue

@@ -18,14 +18,14 @@
             <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 value="add">增加</el-radio>
+                        <el-radio value="reduce">减少</el-radio>
                     </el-radio-group>
                 </el-form-item>
                 <el-form-item label="调整项目">
                     <el-radio-group v-model="rewardForm.item">
-                        <el-radio label="points">积分</el-radio>
-                        <el-radio label="balance">金额 (元)</el-radio>
+                        <el-radio value="points">积分</el-radio>
+                        <el-radio value="balance">金额 (元)</el-radio>
                     </el-radio-group>
                 </el-form-item>
                 <el-form-item label="数额" required>
@@ -64,7 +64,7 @@ const dialogVisible = computed({
 })
 
 const rewardForm = reactive({
-    type: 'reward',
+    type: 'add',
     item: 'points',
     value: 10,
     reason: ''
@@ -72,7 +72,7 @@ const rewardForm = reactive({
 
 watch(() => props.visible, (val) => {
     if (val) {
-        rewardForm.type = 'reward'
+        rewardForm.type = 'add'
         rewardForm.item = 'points'
         rewardForm.value = 10
         rewardForm.reason = ''

+ 25 - 8
src/views/order/management/index.vue

@@ -6,8 +6,8 @@
           <span class="title">订单列表</span>
           <div class="right-panel">
             <el-radio-group v-model="filters.service" size="default" @change="handleSearch">
-              <el-radio-button label="">全部类型</el-radio-button>
-              <el-radio-button v-for="item in serviceOptions" :key="item.id" :label="item.id">{{ item.name }}</el-radio-button>
+              <el-radio-button value="">全部类型</el-radio-button>
+              <el-radio-button v-for="item in serviceOptions" :key="item.id" :value="item.id">{{ item.name }}</el-radio-button>
             </el-radio-group>
             <el-input
               v-model="filters.content"
@@ -50,7 +50,7 @@
 
         <el-table-column label="宠物信息" min-width="150">
           <template #default="{ row }">
-            <div class="pet-info">
+            <div class="pet-info" style="cursor: pointer" @click="handlePetDetail(row)">
               <el-avatar :size="30" class="avatar-type">{{ row.petName?.charAt(0) }}</el-avatar>
               <div class="pet-detail">
                 <div class="pet-name">
@@ -119,8 +119,8 @@
           <template #default="{ row }">
             <div class="op-cell">
               <el-button link type="primary" size="small" @click="handleDetail(row)">详情</el-button>
-              <el-button v-if="row.status === 0" link type="success" size="small" @click="openDispatchDialog(row)">派单</el-button>
-              <el-button v-if="![0, 4].includes(row.status)" link type="warning" size="small" @click="openDispatchDialog(row)">重新派单</el-button>
+              <!--              <el-button v-if="row.status === 0" link type="success" size="small" @click="openDispatchDialog(row)">派单</el-button>-->
+              <!--              <el-button v-if="![0, 4].includes(row.status)" link type="warning" size="small" @click="openDispatchDialog(row)">重新派单</el-button>-->
               <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small" @click="handleCancel(row)">取消</el-button>
 
               <el-dropdown v-if="[3, 4].includes(row.status)" trigger="click" @command="(cmd) => handleCommand(cmd, row)">
@@ -173,6 +173,8 @@
     <RewardDialog v-model:visible="rewardDialogVisible" :order="currentOperateRow" @submit="handleRewardSubmit" />
 
     <RemarkDialog v-model:visible="remarkDialogVisible" :order="currentOperateRow" @submit="handleRemarkSubmit" />
+
+    <PetDetailDrawer v-model:visible="petDetailVisible" :pet-id="currentPetId" />
   </div>
 </template>
 
@@ -184,6 +186,7 @@ import DispatchDialog from './components/DispatchDialog.vue';
 import CareSummaryDrawer from './components/CareSummaryDrawer.vue';
 import RewardDialog from './components/RewardDialog.vue';
 import RemarkDialog from './components/RemarkDialog.vue';
+import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue';
 import { listServiceOnOrder } from '@/api/service/list/index';
 import { listSubOrder, dispatchSubOrder, getSubOrderInfo, cancelSubOrder, remarkSubOrder, confirmSubOrder, nursingSummarySubOrder } from '@/api/order/subOrder/index';
 import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation';
@@ -386,6 +389,9 @@ const rewardDialogVisible = ref(false);
 const remarkDialogVisible = ref(false);
 const currentOperateRow = ref(null);
 
+const petDetailVisible = ref(false);
+const currentPetId = ref(null);
+
 // 详情
 const handleDetail = async (row) => {
   const typeName = getServiceName(row?.service);
@@ -485,6 +491,12 @@ const handleDetail = async (row) => {
   detailVisible.value = true;
 };
 
+// 宠物详情
+const handlePetDetail = (row) => {
+  currentPetId.value = row.pet || row.petId;
+  petDetailVisible.value = true;
+};
+
 // 取消订单
 const handleCancel = (row) => {
   ElMessageBox.confirm('确认取消该订单吗?', '提示', { type: 'warning' }).then(() => {
@@ -656,14 +668,19 @@ const handleRewardSubmit = async (form) => {
     return
   }
   try {
+    const bizType = form.type === 'add' ? 'order_reward' : 'order_punish'
+    // 如果调整项目是金额(balance),则将元转换为分
+    const amount = 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
+      amount: amount,
+      reason: form.reason,
+      bizType: bizType,
+      orderId: currentOperateRow.value.id
     })
-    ElMessage.success(`操作成功:${form.type === 'reward' ? '奖励' : '惩罚'}已执行`)
+    ElMessage.success(`操作成功:${form.type === 'add' ? '奖励' : '惩罚'}已执行`)
     handleSearch()
   } catch {
     // Error handled by interceptor

+ 7 - 7
src/views/order/purchase/components/AddPetDialog.vue

@@ -92,9 +92,9 @@
         <el-form :model="form" label-width="120px">
           <el-form-item label="健康状态">
             <el-radio-group v-model="form.healthStatus">
-              <el-radio label="健康">健康</el-radio>
-              <el-radio label="亚健康">亚健康</el-radio>
-              <el-radio label="疾病">疾病</el-radio>
+              <el-radio value="健康">健康</el-radio>
+              <el-radio value="亚健康">亚健康</el-radio>
+              <el-radio value="疾病">疾病</el-radio>
             </el-radio-group>
           </el-form-item>
           <el-form-item label="是否有攻击倾向">
@@ -102,10 +102,10 @@
           </el-form-item>
           <el-form-item label="疫苗情况">
             <el-radio-group v-model="form.vaccineStatus">
-              <el-radio label="无">无</el-radio>
-              <el-radio label="已打1次">已打1次</el-radio>
-              <el-radio label="已打2次">已打2次</el-radio>
-              <el-radio label="已打3次">已打3次</el-radio>
+              <el-radio value="无">无</el-radio>
+              <el-radio value="已打1次">已打1次</el-radio>
+              <el-radio value="已打2次">已打2次</el-radio>
+              <el-radio value="已打3次">已打3次</el-radio>
             </el-radio-group>
           </el-form-item>
           <el-form-item label="疫苗凭证">

+ 3 - 3
src/views/order/purchase/components/TransportForm.vue

@@ -2,9 +2,9 @@
   <div class="business-form">
     <el-form-item label="接送模式">
       <el-radio-group v-model="transportData.subType" size="large" @change="$emit('change', 'transport')">
-        <el-radio-button label="round">往返接送</el-radio-button>
-        <el-radio-button label="pick">单程接 (到店)</el-radio-button>
-        <el-radio-button label="drop">单程送 (回家)</el-radio-button>
+        <el-radio-button value="round">往返接送</el-radio-button>
+        <el-radio-button value="pick">单程接 (到店)</el-radio-button>
+        <el-radio-button value="drop">单程送 (回家)</el-radio-button>
       </el-radio-group>
     </el-form-item>
 

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 391 - 311
src/views/order/purchase/index.vue


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů