Kaynağa Gözat

完成用户管理接口对接联调

steelwei 1 ay önce
ebeveyn
işleme
11901687d7

+ 25 - 0
src/api/archieves/changeLog/index.ts

@@ -0,0 +1,25 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ArcChangeLogVO } from '@/api/archieves/changeLog/types';
+
+/**
+ * 分页查询变更日志
+ */
+export const listChangeLog = (targetId: string | number, targetType: string, query?: PageQuery): AxiosPromise<ArcChangeLogVO[]> => {
+  return request({
+    url: '/archieves/changeLog/list',
+    method: 'get',
+    params: { targetId, targetType, ...query }
+  });
+};
+
+/**
+ * 查询全部变更日志(不分页)
+ */
+export const listAllChangeLog = (targetId: string | number, targetType: string): AxiosPromise<ArcChangeLogVO[]> => {
+  return request({
+    url: '/archieves/changeLog/listAll',
+    method: 'get',
+    params: { targetId, targetType }
+  });
+};

+ 10 - 0
src/api/archieves/changeLog/types.ts

@@ -0,0 +1,10 @@
+export interface ArcChangeLogVO {
+  id: string | number;
+  targetId: number;
+  targetType: string;
+  changeType: string;
+  content: string;
+  operatorId: number;
+  operatorName: string;
+  createTime: string;
+}

+ 78 - 0
src/api/archieves/customer/index.ts

@@ -0,0 +1,78 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { UsrCustomerVO, UsrCustomerForm, UsrCustomerQuery } from '@/api/archieves/customer/types';
+
+/**
+ * 查询用户列表
+ */
+export const listCustomer = (query?: UsrCustomerQuery): AxiosPromise<UsrCustomerVO[]> => {
+  return request({
+    url: '/archieves/customer/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询全部用户(不分页)
+ */
+export const listAllCustomer = (query?: UsrCustomerQuery): AxiosPromise<UsrCustomerVO[]> => {
+  return request({
+    url: '/archieves/customer/listAll',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询用户详细
+ */
+export const getCustomer = (id: string | number): AxiosPromise<UsrCustomerVO> => {
+  return request({
+    url: '/archieves/customer/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增用户
+ */
+export const addCustomer = (data: UsrCustomerForm) => {
+  return request({
+    url: '/archieves/customer',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改用户
+ */
+export const updateCustomer = (data: UsrCustomerForm) => {
+  return request({
+    url: '/archieves/customer',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除用户
+ */
+export const delCustomer = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/archieves/customer/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 切换用户状态
+ */
+export const changeCustomerStatus = (id: string | number, status: number) => {
+  return request({
+    url: '/archieves/customer/changeStatus',
+    method: 'put',
+    params: { id, status }
+  });
+};

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

@@ -0,0 +1,51 @@
+import { SysTagVO } from '@/api/archieves/tag/types';
+
+export interface UsrCustomerVO {
+  id: string | number;
+  name: string;
+  phone: string;
+  avatar: number;
+  avatarUrl: string;
+  gender: number;
+  birthday: string;
+  idCard: string;
+  areaId: number;
+  areaName: string;
+  stationId: number;
+  stationName: string;
+  address: string;
+  emergencyContact: string;
+  emergencyPhone: string;
+  petCount: number;
+  memberLevel: number;
+  status: number;
+  remark: string;
+  createTime: string;
+  tags: SysTagVO[];
+}
+
+export interface UsrCustomerForm extends BaseEntity {
+  id?: string | number;
+  name?: string;
+  phone?: string;
+  avatar?: number;
+  gender?: number;
+  birthday?: string;
+  idCard?: string;
+  areaId?: number;
+  stationId?: number;
+  address?: string;
+  emergencyContact?: string;
+  emergencyPhone?: string;
+  memberLevel?: number;
+  status?: number;
+  remark?: string;
+  tagIds?: (string | number)[];
+}
+
+export interface UsrCustomerQuery extends PageQuery {
+  keyword?: string;
+  areaId?: number;
+  stationId?: number;
+  status?: number;
+}

+ 66 - 0
src/api/archieves/pet/index.ts

@@ -0,0 +1,66 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { UsrPetVO, UsrPetForm, UsrPetQuery } from '@/api/archieves/pet/types';
+
+/**
+ * 查询宠物列表
+ */
+export const listPet = (query?: UsrPetQuery): AxiosPromise<UsrPetVO[]> => {
+  return request({
+    url: '/archieves/pet/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 按用户查询宠物列表
+ */
+export const listPetByUser = (userId: string | number): AxiosPromise<UsrPetVO[]> => {
+  return request({
+    url: '/archieves/pet/listByUser/' + userId,
+    method: 'get'
+  });
+};
+
+/**
+ * 查询宠物详细
+ */
+export const getPet = (id: string | number): AxiosPromise<UsrPetVO> => {
+  return request({
+    url: '/archieves/pet/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增宠物
+ */
+export const addPet = (data: UsrPetForm) => {
+  return request({
+    url: '/archieves/pet',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改宠物
+ */
+export const updatePet = (data: UsrPetForm) => {
+  return request({
+    url: '/archieves/pet',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除宠物
+ */
+export const delPet = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/archieves/pet/' + id,
+    method: 'delete'
+  });
+};

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

@@ -0,0 +1,71 @@
+import { SysTagVO } from '@/api/archieves/tag/types';
+
+export interface UsrPetVO {
+  id: string | number;
+  userId: number;
+  name: string;
+  avatar: number;
+  avatarUrl: string;
+  type: number;
+  breed: string;
+  gender: number;
+  birthday: string;
+  age: number;
+  weight: number;
+  size: string;
+  isSterilized: number;
+  arrivalTime: string;
+  houseType: string;
+  entryMethod: string;
+  entryPassword: string;
+  keyLocation: string;
+  personality: string;
+  cutePersonality: string;
+  healthStatus: string;
+  aggression: number;
+  vaccineStatus: string;
+  vaccineCert: number;
+  vaccineCertUrl: string;
+  medicalHistory: string;
+  allergies: string;
+  remark: string;
+  createTime: string;
+  ownerName: string;
+  ownerPhone: string;
+  tags: SysTagVO[];
+}
+
+export interface UsrPetForm extends BaseEntity {
+  id?: string | number;
+  userId?: number;
+  name?: string;
+  avatar?: number;
+  type?: number;
+  breed?: string;
+  gender?: number;
+  birthday?: string;
+  age?: number;
+  weight?: number;
+  size?: string;
+  isSterilized?: number;
+  arrivalTime?: string;
+  houseType?: string;
+  entryMethod?: string;
+  entryPassword?: string;
+  keyLocation?: string;
+  personality?: string;
+  cutePersonality?: string;
+  healthStatus?: string;
+  aggression?: number;
+  vaccineStatus?: string;
+  vaccineCert?: number;
+  medicalHistory?: string;
+  allergies?: string;
+  remark?: string;
+  tagIds?: (string | number)[];
+}
+
+export interface UsrPetQuery extends PageQuery {
+  keyword?: string;
+  userId?: number;
+}

+ 67 - 0
src/api/archieves/tag/index.ts

@@ -0,0 +1,67 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SysTagVO, SysTagForm, SysTagQuery } from '@/api/archieves/tag/types';
+
+/**
+ * 查询标签列表
+ */
+export const listTag = (query?: SysTagQuery): AxiosPromise<SysTagVO[]> => {
+  return request({
+    url: '/archieves/tag/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询全部标签(不分页)
+ */
+export const listAllTag = (query?: SysTagQuery): AxiosPromise<SysTagVO[]> => {
+  return request({
+    url: '/archieves/tag/listAll',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询标签详细
+ */
+export const getTag = (id: string | number): AxiosPromise<SysTagVO> => {
+  return request({
+    url: '/archieves/tag/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增标签
+ */
+export const addTag = (data: SysTagForm) => {
+  return request({
+    url: '/archieves/tag',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改标签
+ */
+export const updateTag = (data: SysTagForm) => {
+  return request({
+    url: '/archieves/tag',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除标签
+ */
+export const delTag = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/archieves/tag/' + id,
+    method: 'delete'
+  });
+};

+ 26 - 0
src/api/archieves/tag/types.ts

@@ -0,0 +1,26 @@
+export interface SysTagVO {
+  id: string | number;
+  name: string;
+  category: string;
+  colorType: string;
+  description: string;
+  type: number;
+  status: number;
+  createTime: string;
+}
+
+export interface SysTagForm extends BaseEntity {
+  id?: string | number;
+  name?: string;
+  category?: string;
+  colorType?: string;
+  description?: string;
+  type?: number;
+  status?: number;
+}
+
+export interface SysTagQuery extends PageQuery {
+  name?: string;
+  category?: string;
+  status?: number;
+}

+ 318 - 309
src/views/archieves/customer/index.vue

@@ -5,46 +5,46 @@
         <div class="card-header">
           <span class="title">用户管理</span>
           <div class="header-actions">
-            <el-select v-model="searchForm.area" placeholder="所属区域" style="width: 150px; margin-right: 10px" clearable>
-              <el-option label="朝阳区" value="朝阳区" />
-              <el-option label="海淀区" value="海淀区" />
-              <el-option label="浦东新区" value="浦东新区" />
+            <el-select v-model="searchForm.areaId" placeholder="所属区域" style="width: 150px; margin-right: 10px" clearable @change="onSearchAreaChange">
+              <el-option v-for="area in areaList" :key="area.id" :label="area.name" :value="area.id" />
             </el-select>
-            <el-select v-model="searchForm.station" placeholder="所属站点" style="width: 150px; margin-right: 10px" clearable>
-              <el-option label="三里屯服务站" value="三里屯服务站" />
-              <el-option label="中关村服务站" value="中关村服务站" />
+            <el-select v-model="searchForm.stationId" placeholder="所属站点" style="width: 150px; margin-right: 10px" clearable @change="handleSearch">
+              <el-option v-for="station in filteredStationList" :key="station.id" :label="station.name" :value="station.id" />
             </el-select>
-            <el-input v-model="searchForm.keyword" placeholder="搜索姓名/手机号" style="width: 200px; margin-right: 10px;" clearable />
+            <el-input v-model="searchForm.keyword" placeholder="搜索姓名/手机号" style="width: 200px; margin-right: 10px;" clearable @keyup.enter="handleSearch" @clear="handleSearch" />
             <el-button type="primary" icon="Plus" @click="handleAdd">新增用户</el-button>
           </div>
         </div>
       </template>
 
-      <el-table :data="filteredTableData" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }">
+      <el-table :data="tableData" v-loading="loading" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }">
         <el-table-column label="用户基本信息" width="250">
           <template #default="scope">
             <div style="display: flex; align-items: center;">
               <el-avatar :size="40" :src="scope.row.avatar" style="margin-right: 10px;" />
               <div>
                 <div style="font-weight: bold;">{{ scope.row.name }}
-                  <el-icon v-if="scope.row.gender === '女'" color="#F56C6C"><Female /></el-icon>
-                  <el-icon v-else color="#409EFF"><Male /></el-icon>
+                  <dict-tag :options="sys_user_sex" :value="scope.row.gender" />
                 </div>
                 <div style="font-size: 12px; color: #999;">{{ scope.row.phone }}</div>
               </div>
             </div>
           </template>
         </el-table-column>
-        <el-table-column prop="address" label="住址" show-overflow-tooltip min-width="150" />
+        <el-table-column label="住址" show-overflow-tooltip min-width="150">
+          <template #default="scope">
+            {{ [scope.row.regionCode ? scope.row.regionCode.replace(/\//g, ' ') : '', scope.row.address].filter(Boolean).join(' ') || '-' }}
+          </template>
+        </el-table-column>
         <el-table-column label="用户标签" width="200">
           <template #default="scope">
-            <el-tag v-for="tag in scope.row.tags" :key="tag.name" :type="tag.type" effect="light" size="small" style="margin-right: 5px;">{{ tag.name }}</el-tag>
+            <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.includes('平台') ? '' : 'warning'">{{ scope.row.source }}</el-tag></div>
-            <div style="font-size: 12px; color: #999; margin-top: 4px;">Created: {{ scope.row.entryTime }}</div>
+            <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="120" align="center" sortable prop="orderCount">
@@ -61,6 +61,8 @@
           <template #default="scope">
             <el-switch
               v-model="scope.row.status"
+              :active-value="0"
+              :inactive-value="1"
               inline-prompt
               active-text="正常"
               inactive-text="停用"
@@ -92,13 +94,13 @@
       </el-table>
       <div class="pagination-container">
         <el-pagination
-          v-model:current-page="currentPage"
-          v-model:page-size="pageSize"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
           :page-sizes="[10, 20, 50, 100]"
           layout="total, sizes, prev, pager, next, jumper"
           :total="total"
-          @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
+          @size-change="getList"
+          @current-change="getList"
         />
       </div>
     </el-card>
@@ -110,13 +112,11 @@
         <div class="profile-basic">
           <div class="name-row">
             <span class="name">{{ currentUser.name }}</span>
-            <el-tag size="small" :type="currentUser.gender === '公' ? '' : 'danger'" effect="dark" style="margin-left: 10px">
-              {{ currentUser.gender }}
-            </el-tag>
+            <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.name" :type="tag.type" effect="light" size="small" style="margin-right: 5px">
+            <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>
@@ -139,10 +139,10 @@
           <el-descriptions :column="2" border>
             <el-descriptions-item label="详细住址" :span="2">{{ currentUser.address }}</el-descriptions-item>
             <el-descriptions-item label="房屋类型">
-              {{ currentUser.houseType === 'stairs' ? '楼梯' : '电梯' }}
+              <dict-tag :options="sys_house_type" :value="currentUser.houseType" />
             </el-descriptions-item>
             <el-descriptions-item label="入门方式">
-              {{ currentUser.entryMethod === 'password' ? '密码开门' : '钥匙开门' }}
+              <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 }}
@@ -204,13 +204,13 @@
         <el-tab-pane label="档案日志" name="logs">
           <el-timeline style="margin-top: 10px; padding-left: 5px;">
             <el-timeline-item
-              v-for="(log, index) in mockLogs"
+              v-for="(log, index) in changeLogs"
               :key="index"
-              :timestamp="log.timestamp"
-              :type="log.type"
+              :timestamp="log.createTime"
+              type="primary"
             >
-              {{ log.content }}
-              <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operator }}</div>
+              [{{ 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>
@@ -239,9 +239,15 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="所属区域">
-              <el-select v-model="form.area" style="width: 100%" filterable allow-create default-first-option placeholder="请选择或输入">
-                <el-option label="朝阳区" value="朝阳区" />
-                <el-option label="海淀区" value="海淀区" />
+              <el-select v-model="form.areaId" style="width: 100%" filterable placeholder="请选择区域" clearable @change="form.stationId = undefined">
+                <el-option v-for="area in areaList" :key="area.id" :label="area.name" :value="area.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="所属站点">
+              <el-select v-model="form.stationId" style="width: 100%" filterable placeholder="请选择站点" clearable>
+                <el-option v-for="station in formStationList" :key="station.id" :label="station.name" :value="station.id" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -253,10 +259,9 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="性别">
-              <el-radio-group v-model="form.gender">
-                <el-radio label="男">男</el-radio>
-                <el-radio label="女">女</el-radio>
-              </el-radio-group>
+              <el-select v-model="form.gender" placeholder="请选择">
+                <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+              </el-select>
             </el-form-item>
           </el-col>
 
@@ -272,21 +277,19 @@
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="详细住址"><el-input v-model="form.detailAddress" placeholder="请输入街道/门牌号" /></el-form-item>
+            <el-form-item label="详细住址"><el-input v-model="form.address" placeholder="请输入街道/门牌号" /></el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="房屋类型">
               <el-radio-group v-model="form.houseType">
-                <el-radio label="stairs">楼梯</el-radio>
-                <el-radio label="elevator">电梯</el-radio>
+                <el-radio v-for="dict in sys_house_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="入门方式">
               <el-radio-group v-model="form.entryMethod">
-                <el-radio label="password">密码开门</el-radio>
-                <el-radio label="key">钥匙开门</el-radio>
+                <el-radio v-for="dict in sys_entry_method" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>
@@ -306,7 +309,7 @@
             <el-form-item label="用户标签">
               <el-select v-model="selectedTagIds" multiple placeholder="选择标签" style="width: 100%">
                 <el-option v-for="tag in allUserTags" :key="tag.id" :label="tag.name" :value="tag.id">
-                  <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
+                  <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
                 </el-option>
               </el-select>
             </el-form-item>
@@ -318,7 +321,7 @@
       </el-form>
       <div style="text-align: center; margin-top: 20px;">
         <el-button @click="dialogVisible = false" size="large" style="width: 120px;">取消</el-button>
-        <el-button type="primary" @click="saveUser" size="large" style="width: 120px;">保存</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="saveUser" size="large" style="width: 120px;">保存</el-button>
       </div>
     </el-dialog>
 
@@ -356,16 +359,15 @@
               </el-col>
               <el-col :span="12">
                 <el-form-item label="性别">
-                  <el-radio-group v-model="petForm.gender">
-                    <el-radio label="公">公</el-radio>
-                    <el-radio label="母">母</el-radio>
-                  </el-radio-group>
+                  <el-select v-model="petForm.gender" placeholder="请选择">
+                    <el-option v-for="dict in sys_pet_gender" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                  </el-select>
                 </el-form-item>
               </el-col>
               <el-col :span="12">
                 <el-form-item label="品种">
                   <el-select v-model="petForm.breed" placeholder="请选择品种" style="width: 100%" filterable allow-create default-first-option>
-                    <el-option v-for="breed in petBreeds" :key="breed" :label="breed" :value="breed" />
+                    <el-option v-for="dict in sys_pet_breed" :key="dict.value" :label="dict.label" :value="dict.value" />
                   </el-select>
                 </el-form-item>
               </el-col>
@@ -392,9 +394,9 @@
               </el-col>
               <el-col :span="24">
                 <el-form-item label="宠物标签">
-                  <el-select v-model="petForm.tags" multiple placeholder="选择标签" style="width: 100%">
-                    <el-option v-for="tag in allPetTags" :key="tag.name" :label="tag.name" :value="tag.name">
-                      <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
+                  <el-select v-model="petForm.tagIds" multiple placeholder="选择标签" style="width: 100%">
+                    <el-option v-for="tag in allPetTags" :key="tag.id" :label="tag.name" :value="tag.id">
+                      <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
                     </el-option>
                   </el-select>
                 </el-form-item>
@@ -437,7 +439,7 @@
               </el-radio-group>
             </el-form-item>
             <el-form-item label="是否有攻击倾向">
-              <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" />
+              <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" :active-value="1" :inactive-value="0" />
             </el-form-item>
             <el-form-item label="疫苗情况">
               <el-radio-group v-model="petForm.vaccine">
@@ -465,7 +467,7 @@
       <template #footer>
             <span class="dialog-footer">
                 <el-button @click="petDialogVisible = false">取消</el-button>
-                <el-button type="primary" @click="savePet">保存</el-button>
+                <el-button type="primary" :loading="submitLoading" @click="savePet">保存</el-button>
             </span>
       </template>
     </el-dialog>
@@ -473,24 +475,66 @@
 </template>
 
 <script setup>
-import { ref, reactive, computed } from 'vue'
+import { ref, reactive, computed, onMounted, getCurrentInstance, toRefs } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer, changeCustomerStatus } from '@/api/archieves/customer'
+import { listAllTag } from '@/api/archieves/tag'
+import { listPetByUser, addPet, updatePet, delPet } from '@/api/archieves/pet'
+import { listAllChangeLog } from '@/api/archieves/changeLog'
+import { listOnStore } from '@/api/system/areaStation'
+
+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')
+)
+
+const loading = ref(false)
+const submitLoading = ref(false)
+const total = ref(0)
+
+const allNodes = ref([])
+const areaList = computed(() => allNodes.value.filter(n => n.type === 1))
+const filteredStationList = computed(() => {
+  const areaId = searchForm.areaId
+  const stations = allNodes.value.filter(n => n.type === 2)
+  if (areaId) {
+    return stations.filter(s => s.parentId === areaId)
+  }
+  return stations
+})
+const formStationList = computed(() => {
+  const areaId = form.areaId
+  const stations = allNodes.value.filter(n => n.type === 2)
+  if (areaId) {
+    return stations.filter(s => s.parentId === areaId)
+  }
+  return stations
+})
 
-const currentPage = ref(1)
-const pageSize = ref(10)
-const total = ref(100)
-
-const handleSizeChange = (val) => {
-  console.log(`每页 ${val} 条`)
+const loadAreaStation = () => {
+  listOnStore().then((res) => {
+    allNodes.value = res.data || []
+  })
 }
-const handleCurrentChange = (val) => {
-  console.log(`当前页: ${val}`)
+
+const onSearchAreaChange = () => {
+  searchForm.stationId = undefined
+  handleSearch()
 }
 
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  keyword: '',
+  areaId: undefined,
+  stationId: undefined,
+  status: undefined
+})
+
 const searchForm = reactive({
   keyword: '',
-  area: '',
-  station: ''
+  areaId: undefined,
+  stationId: undefined
 })
 
 const dialogVisible = ref(false)
@@ -504,192 +548,171 @@ 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([])
 
-// Mock Data
-const allUserTags = [
-  { id: 1, name: '优质客户', type: 'success' },
-  { id: 2, name: '潜在流失', type: 'warning' },
-  { id: 3, name: '黑名单', type: 'danger' }
-]
-
-const petBreeds = [
-  '金毛', '拉布拉多', '柴犬', '柯基', '哈士奇', '阿拉斯加', '萨摩耶', '边境牧羊犬', '德国牧羊犬', '贵宾犬/泰迪', '比熊', '博美', '雪纳瑞', '法斗', '中华田园犬',
-  '英短', '美短', '布偶猫', '加菲猫', '暹罗猫', '波斯猫', '缅因猫', '中华田园猫'
-]
-
-const allPetTags = [
-  { name: '易过敏', type: 'danger' },
-  { name: '胆小', type: 'warning' },
-  { name: '攻击性', type: 'info' },
-  { name: '粘人', type: 'success' }
-]
-
-const tableData = ref([
-  {
-    id: 101,
-    avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
-    name: '张先生',
-    phone: '13800138000',
-    gender: '男',
-    address: '北京市朝阳区三里屯',
-    houseType: 'elevator',
-    entryMethod: 'password',
-    entryPassword: '456',
-    keyLocation: '',
-    remark: '经常周末来',
-    tags: [{ name: '优质客户', type: 'success' }],
-    petCount: 2,
-    entryTime: '2025-01-15 10:00:00',
-    source: '平台录入',
-    orderCount: 12,
-    totalAmount: 3580.00,
-    area: '朝阳区',
-    station: '三里屯服务站',
-    status: true
-  },
-  {
-    id: 102,
-    avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
-    name: '李小姐',
-    phone: '13900139000',
-    gender: '女',
-    address: '上海市浦东新区',
-    houseType: 'stairs',
-    entryMethod: 'key',
-    entryPassword: '',
-    keyLocation: '门口地垫下',
-    remark: '',
-    tags: [],
-    petCount: 0,
-    entryTime: '2025-02-01 14:30:00',
-    source: '萌它宠物连锁录入',
-    orderCount: 0,
-    totalAmount: 0.00,
-    area: '浦东新区',
-    station: '',
-    status: true
-  }
-])
 
 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 mockLogs = ref([
-  { content: '用户注册成功', timestamp: '2025-01-15 10:00:00', operator: '系统', type: 'success' },
-  { content: '新增宠物档案 [旺财]', timestamp: '2025-01-15 10:05:00', operator: '张先生', type: 'primary' }
-])
-
 const form = reactive({
-  id: null,
-  avatar: '',
+  id: undefined,
   name: '',
   phone: '',
-  gender: '男',
-  address: '',
-  detailAddress: '',
+  avatar: undefined,
+  gender: 0,
+  birthday: '',
+  idCard: '',
+  areaId: undefined,
+  stationId: undefined,
+  regionCode: '',
   region: [],
-  houseType: 'elevator',
-  entryMethod: 'password',
+  address: '',
+  houseType: '',
+  entryMethod: '',
   entryPassword: '',
   keyLocation: '',
+  source: '',
+  emergencyContact: '',
+  emergencyPhone: '',
+  memberLevel: 0,
+  status: 0,
   remark: '',
-  source: '平台录入',
-  entryTime: '',
-  area: '',
-  status: true
+  tagIds: []
 })
 
 const petForm = reactive({
-  id: null,
-  avatar: '',
+  id: undefined,
+  userId: undefined,
+  avatar: undefined,
   name: '',
-  gender: '公',
+  type: 0,
+  gender: 0,
   breed: '',
+  birthday: '',
   age: 1,
-  size: 'small',
   weight: 5,
-  personality: '',
-  cutePersonality: '',
-  tags: [],
-
+  size: 'small',
+  isSterilized: 0,
   arrivalTime: '',
-  houseType: 'stairs',
-  entryMethod: 'key',
+  houseType: '',
+  entryMethod: '',
   entryPassword: '',
   keyLocation: '',
-
-  healthStatus: '健康',
-  aggression: false,
-  vaccine: '无',
-  vaccineCert: '',
+  personality: '',
+  cutePersonality: '',
+  healthStatus: '',
+  aggression: 0,
+  vaccineStatus: '',
+  vaccineCert: undefined,
   medicalHistory: '',
-  allergies: ''
+  allergies: '',
+  remark: '',
+  tagIds: []
 })
 
 const remarkForm = reactive({ content: '' })
 
-const filteredTableData = computed(() => {
-  return tableData.value.filter(item => {
-    const matchKey = !searchForm.keyword || item.name.includes(searchForm.keyword) || item.phone.includes(searchForm.keyword)
-    const matchArea = !searchForm.area || item.area === searchForm.area
-    const matchStation = !searchForm.station || item.station === searchForm.station
-    return matchKey && matchArea && matchStation
-  })
+const pcaOptions = computed(() => {
+  const cities = allNodes.value.filter(n => n.type === 0)
+  return cities.map(city => ({
+    value: city.name,
+    label: city.name,
+    children: allNodes.value
+      .filter(n => n.type === 1 && n.parentId === city.id)
+      .map(district => ({
+        value: district.name,
+        label: district.name
+      }))
+  }))
 })
 
+const getList = () => {
+  loading.value = true
+  queryParams.keyword = searchForm.keyword
+  queryParams.areaId = searchForm.areaId || undefined
+  queryParams.stationId = searchForm.stationId || undefined
+  listCustomer(queryParams).then((res) => {
+    tableData.value = res.rows
+    total.value = res.total
+  }).finally(() => {
+    loading.value = false
+  })
+}
+
+const handleSearch = () => {
+  queryParams.pageNum = 1
+  getList()
+}
+
+const loadTags = () => {
+  listAllTag({ category: 'user', status: 0 }).then((res) => {
+    allUserTags.value = res.data || []
+  }).catch((err) => {
+    console.error('加载用户标签失败', err)
+  })
+  listAllTag({ category: 'pet', status: 0 }).then((res) => {
+    allPetTags.value = res.data || []
+  }).catch((err) => {
+    console.error('加载宠物标签失败', err)
+  })
+}
+
 const handleAdd = () => {
   isEdit.value = false
   selectedTagIds.value = []
   Object.assign(form, {
-    id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
-    houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
-    source: '平台录入', entryTime: new Date().toLocaleString().replace(/\//g, '-'), area: '', status: true
+    id: undefined, name: '', phone: '', avatar: undefined, gender: 0, birthday: '', idCard: '',
+    areaId: undefined, stationId: undefined, regionCode: '', region: [], address: '',
+    houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', source: '',
+    emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: []
   })
   dialogVisible.value = true
 }
 
 const handleEdit = (row) => {
   isEdit.value = true
-  Object.assign(form, row)
-  // Mock parsing address to region? For now just keep address string
-  form.detailAddress = row.address // Simplify: edit mode just show full string in detail
-  form.region = []
+  getCustomer(row.id).then((res) => {
+    const data = res.data
+    Object.assign(form, {
+      id: data.id, name: data.name, phone: data.phone, avatar: data.avatar, gender: data.gender,
+      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,
+      emergencyContact: data.emergencyContact, emergencyPhone: data.emergencyPhone,
+      memberLevel: data.memberLevel, status: data.status, remark: data.remark, tagIds: []
+    })
+    selectedTagIds.value = data.tags ? data.tags.map(t => t.id) : []
+    dialogVisible.value = true
+  })
+}
 
-  selectedTagIds.value = row.tags.map(t => allUserTags.find(at => at.name === t.name)?.id).filter(id => id)
-  dialogVisible.value = true
+const handleDetail = (row) => {
+  getCustomer(row.id).then((res) => {
+    currentUser.value = res.data
+    detailActiveTab.value = 'info'
+    loadDetailPets(row.id)
+    loadDetailLogs(row.id, 'customer')
+    drawerVisible.value = true
+  })
 }
 
-// Mock PCA Data
-const pcaOptions = [
-  {
-    value: '北京市', label: '北京市',
-    children: [
-      { value: '市辖区', label: '市辖区', children: [ { value: '朝阳区', label: '朝阳区' }, { value: '海淀区', label: '海淀区' } ] }
-    ]
-  },
-  {
-    value: '上海市', label: '上海市',
-    children: [
-      { value: '市辖区', label: '市辖区', children: [ { value: '浦东新区', label: '浦东新区' }, { value: '徐汇区', label: '徐汇区' } ] }
-    ]
-  }
-]
+const loadDetailPets = (userId) => {
+  listPetByUser(userId).then((res) => {
+    currentPets.value = res.data || []
+  })
+}
 
-const handleDetail = (row) => {
-  currentUser.value = { ...row }
-  detailActiveTab.value = 'info'
-  // Mock Load Pets
-  if(row.petCount > 0) {
-    currentPets.value = [
-      { id: 1, name: '旺财', breed: '金毛', gender: '公', age: 3, status: '健康', vaccine: '已打3次' },
-      { id: 2, name: '咪咪', breed: '加菲猫', gender: '母', age: 2, status: '健康', vaccine: '无' }
-    ].slice(0, row.petCount)
-  } else {
-    currentPets.value = []
-  }
-  drawerVisible.value = true
+const loadDetailLogs = (targetId, targetType) => {
+  listAllChangeLog(targetId, targetType).then((res) => {
+    changeLogs.value = res.data || []
+  })
 }
 
 const handleRemark = (row) => {
@@ -700,51 +723,69 @@ const handleRemark = (row) => {
 
 const saveRemark = () => {
   if (!remarkForm.content) return ElMessage.warning('请输入内容')
-  mockLogs.value.unshift({
-    content: remarkForm.content,
-    timestamp: new Date().toLocaleString(),
-    operator: '管理员',
-    type: 'warning'
+  // For now, update customer remark via API
+  const data = { id: currentUser.value.id, remark: remarkForm.content }
+  updateCustomer(data).then(() => {
+    ElMessage.success('备注添加成功')
+    remarkDialogVisible.value = false
+    getList()
   })
-  ElMessage.success('备注添加成功')
-  remarkDialogVisible.value = false
 }
 
 const saveUser = () => {
   if (!form.name) return ElMessage.warning('请输入姓名')
-
-  const newTags = selectedTagIds.value.map(id => allUserTags.find(t => t.id === id))
-
-  const fullAddress = (form.region ? form.region.join('') : '') + form.detailAddress
-  const saveForm = { ...form, address: fullAddress }
-
-  if (isEdit.value) {
-    const idx = tableData.value.findIndex(item => item.id === form.id)
-    if (idx !== -1) Object.assign(tableData.value[idx], { ...saveForm, tags: newTags })
+  if (!form.phone) return ElMessage.warning('请输入电话')
+  submitLoading.value = true
+  form.tagIds = selectedTagIds.value
+  if (form.region && form.region.length > 0) {
+    form.regionCode = form.region.join('/')
   } else {
-    tableData.value.push({
-      id: Date.now(),
-      ...saveForm,
-      // entryTime is auto set in handleAdd, or here if we want current time on save
-      entryTime: new Date().toLocaleString().replace(/\//g, '-'),
-      tags: newTags,
-      avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
-      petCount: 0,
-      orderCount: 0,
-      totalAmount: 0.00
-    })
+    form.regionCode = ''
   }
-  ElMessage.success('保存成功')
-  dialogVisible.value = false
+  const api = isEdit.value ? updateCustomer(form) : addCustomer(form)
+  api.then(() => {
+    ElMessage.success('保存成功')
+    dialogVisible.value = false
+    getList()
+  }).finally(() => {
+    submitLoading.value = false
+  })
 }
 
 const handleDelete = (row) => {
   ElMessageBox.confirm('确认删除该用户档案吗?', '提示', { type: 'warning' }).then(() => {
-    tableData.value = tableData.value.filter(item => item.id !== row.id)
-    ElMessage.success('删除成功')
+    delCustomer(row.id).then(() => {
+      ElMessage.success('删除成功')
+      getList()
+    })
   })
 }
 
+const handleStatusChange = (row) => {
+  const statusText = row.status === 0 ? '启用' : '停用'
+  ElMessageBox.confirm(`确认${statusText}用户 "${row.name}" 吗?`, '提示', { type: 'warning' }).then(() => {
+    changeCustomerStatus(row.id, row.status).then(() => {
+      ElMessage.success(`${statusText}成功`)
+    })
+  }).catch(() => {
+    row.status = row.status === 0 ? 1 : 0
+  })
+}
+
+const handleCommand = (command, row) => {
+  if (command === 'remark') {
+    handleRemark(row)
+  } else if (command === 'delete') {
+    handleDelete(row)
+  } else if (command === 'enable') {
+    row.status = 0
+    handleStatusChange(row)
+  } else if (command === 'disable') {
+    row.status = 1
+    handleStatusChange(row)
+  }
+}
+
 const handlePetUploadFile = (file) => {
   petForm.avatar = URL.createObjectURL(file.raw)
 }
@@ -755,103 +796,71 @@ const handlePetUploadVaccineCert = (file) => {
 const openAddPet = () => {
   petDialogActiveTab.value = 'basic'
   Object.assign(petForm, {
-    id: null, avatar: '', name: '', gender: '公', breed: '', age: 1, size: 'small', weight: 5, ownerId: null,
-    personality: '', cutePersonality: '', tags: [],
-    arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
-    healthStatus: '健康', aggression: false, vaccine: '无', vaccineCert: '', medicalHistory: '', allergies: ''
+    id: undefined, userId: currentUser.value.id, avatar: undefined, name: '', type: 0, gender: 0,
+    breed: '', birthday: '', age: 1, weight: 5, size: 'small', isSterilized: 0,
+    arrivalTime: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
+    personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
+    vaccineStatus: '无', vaccineCert: undefined, medicalHistory: '', allergies: '', remark: '', tagIds: []
   })
   petDialogVisible.value = true
 }
 
 const handlePetDetail = (row) => {
-  // Show simple details or full? The dialog is editable, so detail view can just be the grid or a text overview.
-  // For now, let's just make it show the edit dialog but read-only?
-  // Or just alert like before? User didn't specify, but "功能复刻" implies full detail.
-  // The "Pet Archive" has a Drawer for details. I should probably use that or just use the Edit Dialog for now as "Detailed View".
-  // But since "handlePetEdit" exists, maybe "handlePetDetail" isn't fully implemented in UserList yet.
-  // I will stick to existing alert or simple functionality unless asked.
   ElMessage.info(`查看宠物 [${row.name}] 详情`)
 }
 
 const handlePetEdit = (row) => {
   petDialogActiveTab.value = 'basic'
-  const defaults = {
-    avatar: '', name: '', gender: '公', breed: '', age: 1, size: 'small', weight: 5,
-    personality: '', cutePersonality: '', tags: [],
-    arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
-    healthStatus: '健康', aggression: false, vaccine: '无', vaccineCert: '', medicalHistory: '', allergies: ''
-  }
-  Object.assign(petForm, { ...defaults, ...row })
+  Object.assign(petForm, {
+    id: row.id, userId: row.userId, avatar: row.avatar, name: row.name, type: row.type,
+    gender: row.gender, breed: row.breed, birthday: row.birthday, age: row.age,
+    weight: row.weight, size: row.size, isSterilized: row.isSterilized,
+    arrivalTime: row.arrivalTime, houseType: row.houseType, entryMethod: row.entryMethod,
+    entryPassword: row.entryPassword, keyLocation: row.keyLocation,
+    personality: row.personality, cutePersonality: row.cutePersonality,
+    healthStatus: row.healthStatus, aggression: row.aggression,
+    vaccineStatus: row.vaccineStatus, vaccineCert: row.vaccineCert,
+    medicalHistory: row.medicalHistory, allergies: row.allergies, remark: row.remark,
+    tagIds: row.tags ? row.tags.map(t => t.id) : []
+  })
   petDialogVisible.value = true
 }
 
 const handlePetRemark = (row) => {
-  // Reuse main remark dialog but maybe prefix content?
   remarkForm.content = `[宠物:${row.name}] `
   remarkDialogVisible.value = true
 }
 
 const handlePetDelete = (row) => {
   ElMessageBox.confirm(`确认删除宠物 [${row.name}] 吗?`, '提示', { type: 'warning' }).then(() => {
-    currentPets.value = currentPets.value.filter(p => p.id !== row.id)
-    // Update counts
-    if (currentUser.value.id) {
-      const idx = tableData.value.findIndex(item => item.id === currentUser.value.id)
-      if(idx !== -1) {
-        tableData.value[idx].petCount = currentPets.value.length
-        currentUser.value.petCount = currentPets.value.length
-      }
-    }
-    ElMessage.success('宠物删除成功')
+    delPet(row.id).then(() => {
+      ElMessage.success('宠物删除成功')
+      loadDetailPets(currentUser.value.id)
+      getList()
+    })
   })
 }
 
-const handleCommand = (command, row) => {
-  if (command === 'remark') {
-    handleRemark(row)
-  } else if (command === 'delete') {
-    handleDelete(row)
-  } else if (command === 'enable' || command === 'disable') {
-    // Toggle status status relies on row.status being updated or we update it manually here if not using v-model in switch
-    // But since we use switch v-model in table, this command might be redundant if switch is there.
-    // However, user asked for 'Operations can modify status'.
-    // If we strictly follow 'Operations bar has Modify Status', then the Switch column is optional but good UX.
-    // Let's assume the switch is the primary way, butdropdown item toggles it too.
-    row.status = command === 'enable'
-    handleStatusChange(row)
-  }
-}
-
-const handleStatusChange = (row) => {
-  ElMessage.success(`${row.name} 已${row.status ? '启用' : '停用'}`)
-}
-
 const savePet = () => {
-  if(!petForm.name) return ElMessage.warning('请输入宠物昵称')
-
-  if (petForm.id) {
-    // Edit existing
-    const idx = currentPets.value.findIndex(p => p.id === petForm.id)
-    if (idx !== -1) Object.assign(currentPets.value[idx], petForm)
-  } else {
-    // Add new
-    currentPets.value.push({
-      id: Date.now(),
-      ...petForm
-    })
-  }
-
-  // Update main count
-  if (currentUser.value.id) {
-    const idx = tableData.value.findIndex(item => item.id === currentUser.value.id)
-    if(idx !== -1) {
-      tableData.value[idx].petCount = currentPets.value.length
-      currentUser.value.petCount = currentPets.value.length
-    }
-  }
-  ElMessage.success('宠物档案保存成功')
-  petDialogVisible.value = false
+  if (!petForm.name) return ElMessage.warning('请输入宠物昵称')
+  submitLoading.value = true
+  const data = { ...petForm, aggression: Number(petForm.aggression) || 0 }
+  const api = data.id ? updatePet(data) : addPet(data)
+  api.then(() => {
+    ElMessage.success('宠物档案保存成功')
+    petDialogVisible.value = false
+    loadDetailPets(currentUser.value.id)
+    getList()
+  }).finally(() => {
+    submitLoading.value = false
+  })
 }
+
+onMounted(() => {
+  getList()
+  loadTags()
+  loadAreaStation()
+})
 </script>
 
 <style scoped>

+ 160 - 212
src/views/archieves/pet/index.vue

@@ -5,13 +5,13 @@
         <div class="card-header">
           <span class="title">宠物档案</span>
           <div class="header-actions">
-            <el-input v-model="searchKey" placeholder="搜索宠物名/主人" style="width: 200px; margin-right: 10px" clearable />
+            <el-input v-model="searchKey" placeholder="搜索宠物名/主人" style="width: 200px; margin-right: 10px" clearable @keyup.enter="handleSearch" @clear="handleSearch" />
             <el-button type="primary" icon="Plus" @click="handleAdd">新增档案</el-button>
           </div>
         </div>
       </template>
 
-      <el-table :data="filteredTableData" style="width: 100%">
+      <el-table :data="tableData" v-loading="loading" style="width: 100%">
         <el-table-column label="宠物信息" width="220">
           <template #default="scope">
             <div style="display: flex; align-items: center">
@@ -25,7 +25,7 @@
         </el-table-column>
         <el-table-column prop="gender" label="性别" width="80" align="center">
           <template #default="scope">
-            <el-tag :type="scope.row.gender === '公' ? '' : 'danger'" effect="plain" size="small">{{ scope.row.gender }}</el-tag>
+            <dict-tag :options="sys_pet_gender" :value="scope.row.gender" />
           </template>
         </el-table-column>
         <el-table-column label="所属主人" width="180">
@@ -36,8 +36,8 @@
         </el-table-column>
         <el-table-column label="标签" min-width="150">
           <template #default="scope">
-            <el-tag v-for="tag in scope.row.tags" :key="tag" :type="getTagType(tag)" effect="light" size="small" style="margin-right: 5px">{{
-              tag
+            <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>
@@ -48,7 +48,7 @@
         </el-table-column>
         <el-table-column label="疫苗接种" width="120" align="center">
           <template #default="scope">
-            {{ scope.row.vaccine || '-' }}
+            {{ scope.row.vaccineStatus || '-' }}
           </template>
         </el-table-column>
         <el-table-column label="操作" width="200" align="center">
@@ -62,13 +62,13 @@
       </el-table>
       <div class="pagination-container">
         <el-pagination
-          v-model:current-page="currentPage"
-          v-model:page-size="pageSize"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
           :page-sizes="[10, 20, 50, 100]"
           layout="total, sizes, prev, pager, next, jumper"
           :total="total"
-          @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
+          @size-change="getList"
+          @current-change="getList"
         />
       </div>
     </el-card>
@@ -89,32 +89,29 @@
               </el-col>
               <el-col :span="12">
                 <el-form-item label="所属主人" required>
-                  <el-select v-model="form.ownerId" placeholder="选择主人" style="width: 100%" filterable>
+                  <el-select v-model="form.userId" placeholder="选择主人" style="width: 100%" filterable>
                     <el-option v-for="user in userList" :key="user.id" :label="user.name + ' - ' + user.phone" :value="user.id" />
                   </el-select>
                 </el-form-item>
               </el-col>
               <el-col :span="12">
                 <el-form-item label="性别">
-                  <el-radio-group v-model="form.gender">
-                    <el-radio label="公">公</el-radio>
-                    <el-radio label="母">母</el-radio>
-                  </el-radio-group>
+                  <el-select v-model="form.gender" placeholder="请选择">
+                    <el-option v-for="dict in sys_pet_gender" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                  </el-select>
                 </el-form-item>
               </el-col>
               <el-col :span="12">
                 <el-form-item label="品种">
                   <el-select v-model="form.breed" placeholder="请选择品种" style="width: 100%" filterable allow-create default-first-option>
-                    <el-option v-for="breed in petBreeds" :key="breed" :label="breed" :value="breed" />
+                    <el-option v-for="dict in sys_pet_breed" :key="dict.value" :label="dict.label" :value="dict.value" />
                   </el-select>
                 </el-form-item>
               </el-col>
               <el-col :span="12">
                 <el-form-item label="体型">
                   <el-select v-model="form.size" style="width: 100%">
-                    <el-option label="小型" value="small" />
-                    <el-option label="中型" value="medium" />
-                    <el-option label="大型" value="large" />
+                    <el-option v-for="dict in sys_pet_size" :key="dict.value" :label="dict.label" :value="dict.value" />
                   </el-select>
                 </el-form-item>
               </el-col>
@@ -132,9 +129,9 @@
               </el-col>
               <el-col :span="24">
                 <el-form-item label="宠物标签">
-                  <el-select v-model="form.tags" multiple placeholder="选择标签" style="width: 100%">
-                    <el-option v-for="tag in allPetTags" :key="tag.name" :label="tag.name" :value="tag.name">
-                      <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
+                  <el-select v-model="form.tagIds" multiple placeholder="选择标签" style="width: 100%">
+                    <el-option v-for="tag in allPetTags" :key="tag.id" :label="tag.name" :value="tag.id">
+                      <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
                     </el-option>
                   </el-select>
                 </el-form-item>
@@ -149,14 +146,12 @@
             </el-form-item>
             <el-form-item label="家庭房屋类型">
               <el-radio-group v-model="form.houseType">
-                <el-radio label="stairs">楼梯</el-radio>
-                <el-radio label="elevator">电梯</el-radio>
+                <el-radio v-for="dict in sys_house_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
               </el-radio-group>
             </el-form-item>
             <el-form-item label="入门方式">
               <el-radio-group v-model="form.entryMethod">
-                <el-radio label="password">密码开门</el-radio>
-                <el-radio label="key">钥匙开门</el-radio>
+                <el-radio v-for="dict in sys_entry_method" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
               </el-radio-group>
             </el-form-item>
             <el-form-item label="密码" v-if="form.entryMethod === 'password'">
@@ -180,7 +175,7 @@
               <el-switch v-model="form.aggression" active-text="是" inactive-text="否" />
             </el-form-item>
             <el-form-item label="疫苗情况">
-              <el-radio-group v-model="form.vaccine">
+              <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>
@@ -205,7 +200,7 @@
       <template #footer>
         <span class="dialog-footer">
           <el-button @click="dialogVisible = false">取消</el-button>
-          <el-button type="primary" @click="saveData">保存</el-button>
+          <el-button type="primary" :loading="submitLoading" @click="saveData">保存</el-button>
         </span>
       </template>
     </el-dialog>
@@ -217,14 +212,12 @@
         <div class="profile-basic">
           <div class="name-row">
             <span class="name">{{ currentPet.name }}</span>
-            <el-tag size="small" :type="currentPet.gender === '公' ? '' : 'danger'" effect="dark" style="margin-left: 10px">
-              {{ currentPet.gender }}
-            </el-tag>
+            <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" :type="getTagType(tag)" effect="light" size="small" style="margin-right: 5px">
-              {{ tag }}
+            <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>
@@ -239,7 +232,7 @@
           <el-descriptions :column="2" border>
             <el-descriptions-item label="品种">{{ currentPet.breed }}</el-descriptions-item>
             <el-descriptions-item label="体型">
-              {{ currentPet.size === 'small' ? '小型' : currentPet.size === 'medium' ? '中型' : '大型' }}
+              <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>
@@ -251,10 +244,10 @@
           <el-descriptions :column="2" border>
             <el-descriptions-item label="到家时间">{{ currentPet.arrivalTime || '-' }}</el-descriptions-item>
             <el-descriptions-item label="房屋类型">
-              {{ currentPet.houseType === 'stairs' ? '楼梯' : '电梯' }}
+              <dict-tag :options="sys_house_type" :value="currentPet.houseType" />
             </el-descriptions-item>
             <el-descriptions-item label="入门方式">
-              {{ currentPet.entryMethod === 'password' ? '密码开门' : '钥匙开门' }}
+              <dict-tag :options="sys_entry_method" :value="currentPet.entryMethod" />
             </el-descriptions-item>
             <el-descriptions-item label="开门详情">
               {{ currentPet.entryMethod === 'password' ? currentPet.entryPassword : currentPet.keyLocation }}
@@ -270,12 +263,12 @@
               <el-tag :type="currentPet.aggression ? 'danger' : 'success'" size="small">{{ currentPet.aggression ? '有' : '无' }}</el-tag>
             </el-descriptions-item>
             <el-descriptions-item label="疫苗情况" :span="2">
-              <div>{{ currentPet.vaccine || '-' }}</div>
-              <div v-if="currentPet.vaccineCert" style="margin-top: 10px">
+              <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.vaccineCert"
-                  :preview-src-list="[currentPet.vaccineCert]"
+                  :src="currentPet.vaccineCertUrl"
+                  :preview-src-list="[currentPet.vaccineCertUrl]"
                   fit="cover"
                 />
               </div>
@@ -301,9 +294,9 @@
 
         <el-tab-pane label="备注日志" name="logs">
           <el-timeline style="margin-top: 10px; padding-left: 5px">
-            <el-timeline-item v-for="(activity, index) in mockLogs" :key="index" :timestamp="activity.timestamp" :type="activity.type">
-              {{ activity.content }}
-              <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ activity.operator }}</div>
+            <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>
@@ -324,20 +317,30 @@
 </template>
 
 <script setup>
-import { ref, reactive, computed } from 'vue';
+import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
 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(
+  proxy?.useDict('sys_pet_gender', 'sys_pet_type', 'sys_pet_size', 'sys_pet_breed', 'sys_house_type', 'sys_entry_method')
+);
+
+const loading = ref(false);
+const submitLoading = ref(false);
+const total = ref(0);
+const tableData = ref([]);
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  keyword: ''
+});
 
 const searchKey = ref('');
-const currentPage = ref(1);
-const pageSize = ref(10);
-const total = ref(100);
-
-const handleSizeChange = (val) => {
-  console.log(`每页 ${val} 条`);
-};
-const handleCurrentChange = (val) => {
-  console.log(`当前页: ${val}`);
-};
 
 const dialogVisible = ref(false);
 const drawerVisible = ref(false);
@@ -347,151 +350,85 @@ const activeTab = ref('basic');
 const detailActiveTab = ref('info');
 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 mockLogs = ref([
-  { content: '因宠物胆小,建议安排资深服务师', timestamp: '2023-10-01 09:00', operator: '客服小美', type: 'warning' },
-  { content: '更新了疫苗接种记录', timestamp: '2023-09-10 11:30', operator: '管理员', type: 'primary' },
-  { content: '创建宠物档案', timestamp: '2023-01-01 10:00', operator: '系统', type: 'info' }
-]);
-
 const remarkForm = reactive({ content: '' });
 
-// Mock Users for selection
-const userList = [
-  { id: 101, name: '张先生', phone: '13800138000' },
-  { id: 102, name: '李小姐', phone: '13900139000' }
-];
-
-const petBreeds = [
-  '金毛',
-  '拉布拉多',
-  '柴犬',
-  '柯基',
-  '哈士奇',
-  '阿拉斯加',
-  '萨摩耶',
-  '边境牧羊犬',
-  '德国牧羊犬',
-  '贵宾犬/泰迪',
-  '比熊',
-  '博美',
-  '雪纳瑞',
-  '法斗',
-  '中华田园犬',
-  '英短',
-  '美短',
-  '布偶猫',
-  '加菲猫',
-  '暹罗猫',
-  '波斯猫',
-  '缅因猫',
-  '中华田园猫'
-];
-
-const allPetTags = [
-  { name: '易过敏', type: 'danger' },
-  { name: '胆小', type: 'warning' },
-  { name: '攻击性', type: 'info' },
-  { name: '粘人', type: 'success' }
-];
-
-const getTagType = (name) => {
-  const tag = allPetTags.find((t) => t.name === name);
-  return tag ? tag.type : 'info';
-};
-
-const tableData = ref([
-  {
-    id: 1,
-    avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
-    name: '旺财',
-    gender: '公',
-    breed: '金毛',
-    age: 3,
-    size: 'large',
-    weight: 30,
-    ownerId: 101,
-    ownerName: '张先生',
-    ownerPhone: '13800138000',
-    tags: ['易过敏', '胆小'],
-    healthStatus: '健康',
-    personality: '活泼',
-    cutePersonality: '超级粘人,喜欢玩球',
-    arrivalTime: '2023-01-01',
-    houseType: 'elevator',
-    entryMethod: 'password',
-    entryPassword: '456',
-    aggression: false,
-    vaccine: '已打3次',
-    vaccineCert: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
-    medicalHistory: '',
-    allergies: '海鲜'
-  }
-]);
-
-const filteredTableData = computed(() => {
-  return tableData.value.filter((item) => !searchKey.value || item.name.includes(searchKey.value) || item.ownerName.includes(searchKey.value));
-});
 
 const form = reactive({
-  id: null,
-  avatar: '',
+  id: undefined,
+  userId: undefined,
+  avatar: undefined,
   name: '',
-  gender: '公',
+  type: 0,
+  gender: 0,
   breed: '',
+  birthday: '',
   age: 1,
-  size: 'small',
   weight: 5,
-  ownerId: null,
-  personality: '',
-  cutePersonality: '',
-  tags: [],
-
+  size: 'small',
+  isSterilized: 0,
   arrivalTime: '',
-  houseType: 'stairs',
-  entryMethod: 'key',
+  houseType: '',
+  entryMethod: '',
   entryPassword: '',
   keyLocation: '',
-
+  personality: '',
+  cutePersonality: '',
   healthStatus: '健康',
-  aggression: false,
-  vaccine: '无',
-  vaccineCert: '',
+  aggression: 0,
+  vaccineStatus: '无',
+  vaccineCert: undefined,
   medicalHistory: '',
-  allergies: ''
+  allergies: '',
+  remark: '',
+  tagIds: []
 });
 
+const getList = () => {
+  loading.value = true;
+  queryParams.keyword = searchKey.value;
+  listPet(queryParams).then((res) => {
+    tableData.value = res.rows;
+    total.value = res.total;
+  }).finally(() => {
+    loading.value = false;
+  });
+};
+
+const handleSearch = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const loadTags = () => {
+  listAllTag({ category: 'pet', status: 0 }).then((res) => {
+    allPetTags.value = res.data || [];
+  });
+};
+
+const loadUsers = () => {
+  listAllCustomer({ status: 0 }).then((res) => {
+    userList.value = res.data || [];
+  });
+};
+
 const handleAdd = () => {
   isEdit.value = false;
   activeTab.value = 'basic';
   Object.assign(form, {
-    id: null,
-    avatar: '',
-    name: '',
-    gender: '公',
-    breed: '',
-    age: 1,
-    size: 'small',
-    weight: 5,
-    ownerId: null,
-    personality: '',
-    cutePersonality: '',
-    tags: [],
-    arrivalTime: '',
-    houseType: 'stairs',
-    entryMethod: 'key',
-    entryPassword: '',
-    keyLocation: '',
-    healthStatus: '健康',
-    aggression: false,
-    vaccine: '无',
-    vaccineCert: '',
-    medicalHistory: '',
-    allergies: ''
+    id: undefined, userId: undefined, avatar: undefined, name: '', type: 0, gender: 0,
+    breed: '', birthday: '', age: 1, weight: 5, size: 'small', isSterilized: 0,
+    arrivalTime: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
+    personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
+    vaccineStatus: '无', vaccineCert: undefined, medicalHistory: '', allergies: '', remark: '', tagIds: []
   });
   dialogVisible.value = true;
 };
@@ -499,13 +436,33 @@ const handleAdd = () => {
 const handleEdit = (row) => {
   isEdit.value = true;
   activeTab.value = 'basic';
-  Object.assign(form, row);
-  dialogVisible.value = true;
+  getPet(row.id).then((res) => {
+    const data = res.data;
+    Object.assign(form, {
+      id: data.id, userId: data.userId, avatar: data.avatar, name: data.name, type: data.type,
+      gender: data.gender, breed: data.breed, birthday: data.birthday, age: data.age,
+      weight: data.weight, size: data.size, isSterilized: data.isSterilized,
+      arrivalTime: data.arrivalTime, houseType: data.houseType, entryMethod: data.entryMethod,
+      entryPassword: data.entryPassword, keyLocation: data.keyLocation,
+      personality: data.personality, cutePersonality: data.cutePersonality,
+      healthStatus: data.healthStatus, aggression: data.aggression,
+      vaccineStatus: data.vaccineStatus, vaccineCert: data.vaccineCert,
+      medicalHistory: data.medicalHistory, allergies: data.allergies, remark: data.remark,
+      tagIds: data.tags ? data.tags.map(t => t.id) : []
+    });
+    dialogVisible.value = true;
+  });
 };
 
 const handleDetail = (row) => {
-  currentPet.value = { ...row };
-  drawerVisible.value = true;
+  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;
+  });
 };
 
 const handleRemark = (row) => {
@@ -516,20 +473,20 @@ const handleRemark = (row) => {
 
 const saveRemark = () => {
   if (!remarkForm.content) return ElMessage.warning('请输入内容');
-  mockLogs.value.unshift({
-    content: remarkForm.content,
-    timestamp: new Date().toLocaleString(),
-    operator: '当前用户',
-    type: 'primary'
+  const data = { id: currentPet.value.id, remark: remarkForm.content };
+  updatePet(data).then(() => {
+    ElMessage.success('备注添加成功');
+    remarkDialogVisible.value = false;
+    getList();
   });
-  ElMessage.success('备注添加成功');
-  remarkDialogVisible.value = false;
 };
 
 const handleDelete = (row) => {
   ElMessageBox.confirm('确认删除该宠物档案吗?', '提示', { type: 'warning' }).then(() => {
-    tableData.value = tableData.value.filter((item) => item.id !== row.id);
-    ElMessage.success('删除成功');
+    delPet(row.id).then(() => {
+      ElMessage.success('删除成功');
+      getList();
+    });
   });
 };
 
@@ -543,32 +500,23 @@ const handleUploadVaccineCert = (file) => {
 
 const saveData = () => {
   if (!form.name) return ElMessage.warning('请输入宠物姓名');
-  if (!form.ownerId) return ElMessage.warning('请选择所属主人');
-
-  const owner = userList.find((u) => u.id === form.ownerId);
-  const saveData = {
-    ...form,
-    ownerName: owner ? owner.name : 'Unknown',
-    ownerPhone: owner ? owner.phone : ''
-  };
-
-  if (isEdit.value) {
-    const idx = tableData.value.findIndex((item) => item.id === form.id);
-    if (idx !== -1) Object.assign(tableData.value[idx], saveData);
-  } else {
-    tableData.value.push({
-      id: Date.now(),
-      ...saveData
-    });
-  }
-  ElMessage.success('保存成功');
-  dialogVisible.value = false;
+  if (!form.userId) return ElMessage.warning('请选择所属主人');
+  submitLoading.value = true;
+  const api = isEdit.value ? updatePet(form) : addPet(form);
+  api.then(() => {
+    ElMessage.success('保存成功');
+    dialogVisible.value = false;
+    getList();
+  }).finally(() => {
+    submitLoading.value = false;
+  });
 };
-</script>
 
-<script>
-// Separate setup logic needed for function declarations not hoisted? No, script setup handles it.
-// Additional functions
+onMounted(() => {
+  getList();
+  loadTags();
+  loadUsers();
+});
 </script>
 
 <style scoped>

+ 134 - 86
src/views/archieves/tag/index.vue

@@ -7,33 +7,39 @@
         </div>
       </template>
 
-      <el-tabs v-model="activeTab" class="demo-tabs">
+      <el-tabs v-model="activeTab" class="demo-tabs" @tab-change="handleTabChange">
         <el-tab-pane label="用户标签" name="user">
           <div class="operation-bar">
             <el-button type="primary" icon="Plus" @click="handleAdd('user')">新增标签</el-button>
           </div>
-          <el-table :data="userTags" style="width: 100%; margin-top: 20px" :header-cell-style="{ background: '#f5f7fa' }">
+          <el-table :data="tagList" v-loading="loading" style="width: 100%; margin-top: 20px" :header-cell-style="{ background: '#f5f7fa' }">
             <el-table-column label="标签名称" width="200">
               <template #default="scope">
-                <el-tag :type="scope.row.type" effect="light">{{ scope.row.name }}</el-tag>
+                <el-tag :type="scope.row.colorType || 'info'" effect="light">{{ scope.row.name }}</el-tag>
               </template>
             </el-table-column>
             <el-table-column prop="description" label="说明" show-overflow-tooltip />
+                        <el-table-column label="状态" width="80" align="center">
+              <template #default="scope">
+                <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+              </template>
+            </el-table-column>
             <el-table-column label="操作" width="180">
               <template #default="scope">
-                <el-button link type="primary" @click="handleEdit(scope.row, 'user')">编辑</el-button>
-                <el-button link type="danger" @click="handleDelete(scope.row, 'user')">删除</el-button>
+                <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
+                <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
               </template>
             </el-table-column>
           </el-table>
           <div class="pagination-container">
             <el-pagination
-              v-model:current-page="userPage.current"
-              v-model:page-size="userPage.size"
-              :total="userPage.total"
+              v-model:current-page="queryParams.pageNum"
+              v-model:page-size="queryParams.pageSize"
+              :total="total"
+              :page-sizes="[10, 20, 50]"
               layout="total, sizes, prev, pager, next, jumper"
-              @size-change="handleSizeChange"
-              @current-change="handleCurrentChange"
+              @size-change="getList"
+              @current-change="getList"
             />
           </div>
         </el-tab-pane>
@@ -41,28 +47,34 @@
           <div class="operation-bar">
             <el-button type="primary" icon="Plus" @click="handleAdd('pet')">新增标签</el-button>
           </div>
-          <el-table :data="petTags" style="width: 100%; margin-top: 20px" :header-cell-style="{ background: '#f5f7fa' }">
+          <el-table :data="tagList" v-loading="loading" style="width: 100%; margin-top: 20px" :header-cell-style="{ background: '#f5f7fa' }">
             <el-table-column label="标签名称" width="200">
               <template #default="scope">
-                <el-tag :type="scope.row.type" effect="light">{{ scope.row.name }}</el-tag>
+                <el-tag :type="scope.row.colorType || 'info'" effect="light">{{ scope.row.name }}</el-tag>
               </template>
             </el-table-column>
             <el-table-column prop="description" label="说明" show-overflow-tooltip />
+            <el-table-column label="状态" width="80" align="center">
+              <template #default="scope">
+                <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+              </template>
+            </el-table-column>
             <el-table-column label="操作" width="180">
               <template #default="scope">
-                <el-button link type="primary" @click="handleEdit(scope.row, 'pet')">编辑</el-button>
-                <el-button link type="danger" @click="handleDelete(scope.row, 'pet')">删除</el-button>
+                <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
+                <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
               </template>
             </el-table-column>
           </el-table>
           <div class="pagination-container">
             <el-pagination
-              v-model:current-page="petPage.current"
-              v-model:page-size="petPage.size"
-              :total="petPage.total"
+              v-model:current-page="queryParams.pageNum"
+              v-model:page-size="queryParams.pageSize"
+              :total="total"
+              :page-sizes="[10, 20, 50]"
               layout="total, sizes, prev, pager, next, jumper"
-              @size-change="handleSizeChange"
-              @current-change="handleCurrentChange"
+              @size-change="getList"
+              @current-change="getList"
             />
           </div>
         </el-tab-pane>
@@ -74,10 +86,23 @@
         <el-form-item label="标签名称" required>
           <el-input v-model="form.name" placeholder="如:非常有耐心" />
         </el-form-item>
-        <el-form-item label="标签颜色" required>
-          <el-select v-model="form.type" placeholder="Select">
-            <el-option v-for="color in tagColors" :key="color.value" :label="color.label" :value="color.value" />
-          </el-select>
+        <el-form-item label="标签颜色">
+          <div class="color-picker-row">
+            <span
+              v-for="item in colorOptions"
+              :key="item.value"
+              class="color-dot"
+              :class="{ active: form.colorType === item.value }"
+              :style="{ backgroundColor: item.color }"
+              :title="item.label"
+              @click="form.colorType = item.value"
+            />
+          </div>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="parseInt(dict.value)">{{ dict.label }}</el-radio>
+          </el-radio-group>
         </el-form-item>
         <el-form-item label="描述说明">
           <el-input v-model="form.description" type="textarea" placeholder="请输入描述说明" />
@@ -86,7 +111,7 @@
       <template #footer>
         <span class="dialog-footer">
           <el-button @click="dialogVisible = false">取消</el-button>
-          <el-button type="primary" @click="saveData">保存</el-button>
+          <el-button type="primary" :loading="submitLoading" @click="saveData">保存</el-button>
         </span>
       </template>
     </el-dialog>
@@ -94,92 +119,98 @@
 </template>
 
 <script setup>
-import { ref, reactive } from 'vue';
+import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
+import { listTag, addTag, updateTag, delTag } from '@/api/archieves/tag';
+
+const { proxy } = getCurrentInstance();
+const { sys_normal_disable } = toRefs(proxy?.useDict('sys_normal_disable'));
+
+const colorOptions = [
+  { label: '默认', value: 'info', color: '#909399' },
+  { label: '主要', value: 'primary', color: '#409EFF' },
+  { label: '成功', value: 'success', color: '#67C23A' },
+  { label: '警告', value: 'warning', color: '#E6A23C' },
+  { label: '危险', value: 'danger', color: '#F56C6C' }
+];
 
 const activeTab = ref('user');
 const dialogVisible = ref(false);
 const isEdit = ref(false);
-const currentType = ref('user'); // 'user' or 'pet'
-
-const userPage = reactive({ current: 1, size: 10, total: 20 });
-const petPage = reactive({ current: 1, size: 10, total: 15 });
-
-const handleSizeChange = (val) => {
-  console.log(`每页 ${val} 条`);
-};
-const handleCurrentChange = (val) => {
-  console.log(`当前页: ${val}`);
-};
+const loading = ref(false);
+const submitLoading = ref(false);
+const tagList = ref([]);
+const total = ref(0);
 
-const userTags = ref([
-  { id: 1, name: '优质客户', type: 'success', description: '累计消费金额TOP10' },
-  { id: 2, name: '潜在流失', type: 'warning', description: '近半年无消费记录' },
-  { id: 3, name: '黑名单', type: 'danger', description: '有过多次投诉行为' },
-  { id: 4, name: '新手', type: 'primary', description: '注册未满1个月' }
-]);
-
-const petTags = ref([
-  { id: 1, name: '易过敏', type: 'danger', description: '对海鲜、谷物过敏' },
-  { id: 2, name: '胆小', type: 'warning', description: '害怕陌生环境和声音' },
-  { id: 3, name: '攻击性', type: 'info', description: '洗澡时需要戴嘴套' },
-  { id: 4, name: '粘人', type: 'success', description: '非常喜欢和人互动' }
-]);
-
-const tagColors = [
-  { label: '默认蓝', value: 'primary' },
-  { label: '成功绿', value: 'success' },
-  { label: '警告橙', value: 'warning' },
-  { label: '危险红', value: 'danger' },
-  { label: '信息灰', value: 'info' }
-];
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  category: 'user'
+});
 
 const form = reactive({
-  id: null,
+  id: undefined,
   name: '',
-  type: 'primary',
-  description: ''
+  category: 'user',
+  colorType: 'info',
+  description: '',
+  type: 2,
+  status: 0
 });
 
+const getList = () => {
+  loading.value = true;
+  queryParams.category = activeTab.value;
+  listTag(queryParams).then((res) => {
+    tagList.value = res.rows;
+    total.value = res.total;
+  }).finally(() => {
+    loading.value = false;
+  });
+};
+
+const handleTabChange = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
 const handleAdd = (type) => {
   isEdit.value = false;
-  currentType.value = type;
-  Object.assign(form, { id: null, name: '', type: 'primary', description: '' });
+  Object.assign(form, { id: undefined, name: '', category: type, colorType: 'info', description: '', type: 2, status: 0 });
   dialogVisible.value = true;
 };
 
-const handleEdit = (row, type) => {
+const handleEdit = (row) => {
   isEdit.value = true;
-  currentType.value = type;
-  Object.assign(form, row);
+  Object.assign(form, { id: row.id, name: row.name, category: row.category, colorType: row.colorType, description: row.description, type: row.type, status: row.status });
   dialogVisible.value = true;
 };
 
-const handleDelete = (row, type) => {
+const handleDelete = (row) => {
   ElMessageBox.confirm('确认删除该标签吗?', '提示', { type: 'warning' }).then(() => {
-    const targetList = type === 'user' ? userTags : petTags;
-    targetList.value = targetList.value.filter((item) => item.id !== row.id);
-    ElMessage.success('删除成功');
+    delTag(row.id).then(() => {
+      ElMessage.success('删除成功');
+      getList();
+    });
   });
 };
 
 const saveData = () => {
   if (!form.name) return ElMessage.warning('请输入标签名称');
-
-  const targetList = currentType.value === 'user' ? userTags : petTags;
-
-  if (isEdit.value) {
-    const idx = targetList.value.findIndex((item) => item.id === form.id);
-    if (idx !== -1) Object.assign(targetList.value[idx], form);
-  } else {
-    targetList.value.push({
-      id: Date.now(),
-      ...form
-    });
-  }
-  ElMessage.success('保存成功');
-  dialogVisible.value = false;
+  submitLoading.value = true;
+  const api = isEdit.value ? updateTag(form) : addTag(form);
+  api.then(() => {
+    ElMessage.success('保存成功');
+    dialogVisible.value = false;
+    getList();
+  }).finally(() => {
+    submitLoading.value = false;
+  });
 };
+
+onMounted(() => {
+  getList();
+});
 </script>
 
 <style scoped>
@@ -194,9 +225,6 @@ const saveData = () => {
 .title {
   font-weight: bold;
 }
-.title {
-  font-weight: bold;
-}
 .operation-bar {
   margin-bottom: 10px;
 }
@@ -205,4 +233,24 @@ const saveData = () => {
   display: flex;
   justify-content: flex-end;
 }
+.color-picker-row {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+}
+.color-dot {
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  cursor: pointer;
+  border: 2px solid transparent;
+  transition: all 0.2s;
+}
+.color-dot:hover {
+  transform: scale(1.2);
+}
+.color-dot.active {
+  border-color: #303133;
+  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
+}
 </style>