index.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. <template>
  2. <div class="page-container">
  3. <el-tabs v-model="queryParams.tab" class="pet-tabs" @tab-change="handleTabChange">
  4. <el-tab-pane label="本品牌所属宠物" :name="0" />
  5. <el-tab-pane label="订单关联宠物" :name="1" />
  6. </el-tabs>
  7. <el-card shadow="never">
  8. <template #header>
  9. <div class="card-header">
  10. <span class="title">宠物档案</span>
  11. <div class="header-actions">
  12. <el-input v-model="searchKey" placeholder="搜索宠物名/主人" style="width: 200px; margin-right: 10px" clearable @keyup.enter="handleSearch" @clear="handleSearch" />
  13. <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['archieves:pet:add']">新增档案</el-button>
  14. </div>
  15. </div>
  16. </template>
  17. <el-table :data="tableData" v-loading="loading" style="width: 100%">
  18. <el-table-column label="宠物信息" width="220">
  19. <template #default="scope">
  20. <div style="display: flex; align-items: center">
  21. <el-avatar :size="50" :src="scope.row.avatarUrl" style="margin-right: 10px" />
  22. <div>
  23. <div style="font-weight: bold">{{ scope.row.name }}</div>
  24. <div style="font-size: 12px; color: #999">{{ scope.row.breed }} | {{ scope.row.age }}岁</div>
  25. </div>
  26. </div>
  27. </template>
  28. </el-table-column>
  29. <el-table-column prop="gender" label="性别" width="80" align="center">
  30. <template #default="scope">
  31. <dict-tag :options="sys_pet_gender" :value="scope.row.gender" />
  32. </template>
  33. </el-table-column>
  34. <el-table-column label="所属主人" width="180">
  35. <template #default="scope">
  36. <div>{{ scope.row.ownerName }}</div>
  37. <div style="font-size: 12px; color: #666">{{ scope.row.ownerPhone }}</div>
  38. </template>
  39. </el-table-column>
  40. <el-table-column label="标签" min-width="150">
  41. <template #default="scope">
  42. <el-tag v-for="tag in scope.row.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">{{
  43. tag.name
  44. }}</el-tag>
  45. </template>
  46. </el-table-column>
  47. <el-table-column label="健康状态" width="100" align="center">
  48. <template #default="scope">
  49. <el-tag :type="scope.row.healthStatus === '健康' ? 'success' : 'warning'" effect="dark" size="small">{{ scope.row.healthStatus }}</el-tag>
  50. </template>
  51. </el-table-column>
  52. <el-table-column label="疫苗接种" width="120" align="center">
  53. <template #default="scope">
  54. {{ scope.row.vaccineStatus || '-' }}
  55. </template>
  56. </el-table-column>
  57. <el-table-column label="操作" width="200" align="center">
  58. <template #default="scope">
  59. <el-button link type="primary" @click="handleDetail(scope.row)" v-hasPermi="['archieves:pet:query']">详情</el-button>
  60. <el-button link type="primary" @click="handleEdit(scope.row)" v-hasPermi="['archieves:pet:edit']">编辑</el-button>
  61. <el-button link type="primary" @click="handleRemark(scope.row)" v-hasPermi="['archieves:pet:edit']">备注</el-button>
  62. <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['archieves:pet:remove']">删除</el-button>
  63. </template>
  64. </el-table-column>
  65. </el-table>
  66. <div class="pagination-container">
  67. <el-pagination
  68. v-model:current-page="queryParams.pageNum"
  69. v-model:page-size="queryParams.pageSize"
  70. :page-sizes="[10, 20, 50, 100]"
  71. layout="total, sizes, prev, pager, next, jumper"
  72. :total="total"
  73. @size-change="getList"
  74. @current-change="getList"
  75. />
  76. </div>
  77. </el-card>
  78. <el-dialog v-model="dialogVisible" title="宠物档案详情" width="800px">
  79. <el-tabs v-model="activeTab">
  80. <el-tab-pane label="基本信息" name="basic">
  81. <el-form :model="form" label-width="100px">
  82. <el-row>
  83. <el-col :span="24" style="display: flex; justify-content: center; margin-bottom: 20px">
  84. <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadFile">
  85. <el-avatar v-if="avatarDisplayUrl" :src="avatarDisplayUrl" :size="80" />
  86. <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
  87. </el-upload>
  88. </el-col>
  89. <el-col :span="12">
  90. <el-form-item label="宠物姓名" required><el-input v-model="form.name" /></el-form-item>
  91. </el-col>
  92. <el-col :span="12">
  93. <el-form-item label="所属主人" required>
  94. <el-select v-model="form.userId" placeholder="选择主人" style="width: 100%" filterable>
  95. <el-option v-for="user in userList" :key="user.id" :label="user.name + ' - ' + user.phone" :value="user.id" />
  96. </el-select>
  97. </el-form-item>
  98. </el-col>
  99. <el-col :span="12">
  100. <el-form-item label="性别">
  101. <el-select v-model="form.gender" placeholder="请选择">
  102. <el-option v-for="dict in sys_pet_gender" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
  103. </el-select>
  104. </el-form-item>
  105. </el-col>
  106. <el-col :span="12">
  107. <el-form-item label="品种">
  108. <el-select v-model="form.breed" placeholder="请选择品种" style="width: 100%" filterable allow-create default-first-option>
  109. <el-option v-for="dict in sys_pet_breed" :key="dict.value" :label="dict.label" :value="dict.value" />
  110. </el-select>
  111. </el-form-item>
  112. </el-col>
  113. <el-col :span="12">
  114. <el-form-item label="体型">
  115. <el-select v-model="form.size" style="width: 100%">
  116. <el-option v-for="dict in sys_pet_size" :key="dict.value" :label="dict.label" :value="dict.value" />
  117. </el-select>
  118. </el-form-item>
  119. </el-col>
  120. <el-col :span="12">
  121. <el-form-item label="体重(kg)"><el-input-number v-model="form.weight" :min="0" :precision="1" style="width: 100%" /></el-form-item>
  122. </el-col>
  123. <el-col :span="12">
  124. <el-form-item label="年龄(岁)"><el-input-number v-model="form.age" :min="0" style="width: 100%" /></el-form-item>
  125. </el-col>
  126. <el-col :span="24">
  127. <el-form-item label="性格关键词"><el-input v-model="form.personality" placeholder="如:活泼、粘人" /></el-form-item>
  128. </el-col>
  129. <el-col :span="24">
  130. <el-form-item label="萌宠性格"><el-input v-model="form.cutePersonality" type="textarea" placeholder="详细描述" /></el-form-item>
  131. </el-col>
  132. <el-col :span="24">
  133. <el-form-item label="宠物标签">
  134. <el-select v-model="form.tagIds" multiple placeholder="选择标签" style="width: 100%">
  135. <el-option v-for="tag in allPetTags" :key="tag.id" :label="tag.name" :value="tag.id">
  136. <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
  137. </el-option>
  138. </el-select>
  139. </el-form-item>
  140. </el-col>
  141. </el-row>
  142. </el-form>
  143. </el-tab-pane>
  144. <el-tab-pane label="家庭信息" name="family">
  145. <el-form :model="form" label-width="120px">
  146. <el-form-item label="新来家庭时间">
  147. <el-date-picker v-model="form.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
  148. </el-form-item>
  149. <el-form-item label="家庭房屋类型">
  150. <el-radio-group v-model="form.houseType">
  151. <el-radio v-for="dict in sys_house_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
  152. </el-radio-group>
  153. </el-form-item>
  154. <el-form-item label="入门方式">
  155. <el-radio-group v-model="form.entryMethod">
  156. <el-radio v-for="dict in sys_entry_method" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
  157. </el-radio-group>
  158. </el-form-item>
  159. <el-form-item label="密码" v-if="form.entryMethod === 'password'">
  160. <el-input v-model="form.entryPassword" placeholder="请输入门锁密码" />
  161. </el-form-item>
  162. <el-form-item label="钥匙位置" v-if="form.entryMethod === 'key'">
  163. <el-input v-model="form.keyLocation" placeholder="请输入钥匙存放位置" />
  164. </el-form-item>
  165. </el-form>
  166. </el-tab-pane>
  167. <el-tab-pane label="健康状况" name="health">
  168. <el-form :model="form" label-width="120px">
  169. <el-form-item label="健康状态">
  170. <el-radio-group v-model="form.healthStatus">
  171. <el-radio label="健康">健康</el-radio>
  172. <el-radio label="亚健康">亚健康</el-radio>
  173. <el-radio label="疾病">疾病</el-radio>
  174. </el-radio-group>
  175. </el-form-item>
  176. <el-form-item label="是否有攻击倾向">
  177. <el-switch v-model="form.aggression" active-text="是" inactive-text="否" />
  178. </el-form-item>
  179. <el-form-item label="疫苗情况">
  180. <el-radio-group v-model="form.vaccineStatus">
  181. <el-radio label="无">无</el-radio>
  182. <el-radio label="已打1次">已打1次</el-radio>
  183. <el-radio label="已打2次">已打2次</el-radio>
  184. <el-radio label="已打3次">已打3次</el-radio>
  185. </el-radio-group>
  186. </el-form-item>
  187. <el-form-item label="疫苗凭证">
  188. <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadVaccineCert">
  189. <img v-if="vaccineCertDisplayUrl" :src="vaccineCertDisplayUrl" class="avatar" style="width: 100px; height: 100px; object-fit: cover" />
  190. <el-icon v-else class="avatar-uploader-icon" style="width: 100px; height: 100px; line-height: 100px"><Plus /></el-icon>
  191. </el-upload>
  192. </el-form-item>
  193. <el-form-item label="既往病史">
  194. <el-input v-model="form.medicalHistory" type="textarea" placeholder="如有病史请记录" />
  195. </el-form-item>
  196. <el-form-item label="过敏史">
  197. <el-input v-model="form.allergies" type="textarea" placeholder="如有过敏源请记录" />
  198. </el-form-item>
  199. </el-form>
  200. </el-tab-pane>
  201. </el-tabs>
  202. <template #footer>
  203. <span class="dialog-footer">
  204. <el-button @click="dialogVisible = false">取消</el-button>
  205. <el-button type="primary" :loading="submitLoading" @click="saveData">保存</el-button>
  206. </span>
  207. </template>
  208. </el-dialog>
  209. <!-- Pet Profile Drawer -->
  210. <el-drawer v-model="drawerVisible" title="宠物档案详情" size="60%" destroy-on-close>
  211. <div class="profile-header">
  212. <el-avatar :size="80" :src="currentPet.avatarUrl" />
  213. <div class="profile-basic">
  214. <div class="name-row">
  215. <span class="name">{{ currentPet.name }}</span>
  216. <dict-tag :options="sys_pet_gender" :value="currentPet.gender" />
  217. <el-tag size="small" effect="plain" type="info" style="margin-left: 5px">{{ currentPet.age }}岁</el-tag>
  218. </div>
  219. <div class="tags-row" style="margin-top: 8px">
  220. <el-tag v-for="tag in currentPet.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">
  221. {{ tag.name }}
  222. </el-tag>
  223. </div>
  224. </div>
  225. <div style="margin-left: auto">
  226. <el-button type="primary" size="small" plain @click="handleRemark(currentPet)" v-hasPermi="['archieves:pet:edit']">添加备注</el-button>
  227. </div>
  228. </div>
  229. <el-tabs v-model="detailActiveTab" class="profile-tabs">
  230. <el-tab-pane label="档案信息" name="info">
  231. <div class="section-title">基本信息</div>
  232. <el-descriptions :column="2" border>
  233. <el-descriptions-item label="品种">{{ currentPet.breed }}</el-descriptions-item>
  234. <el-descriptions-item label="体型">
  235. <dict-tag :options="sys_pet_size" :value="currentPet.size" />
  236. </el-descriptions-item>
  237. <el-descriptions-item label="体重">{{ currentPet.weight }} kg</el-descriptions-item>
  238. <el-descriptions-item label="所属主人">{{ currentPet.ownerName }} ({{ currentPet.ownerPhone }})</el-descriptions-item>
  239. <el-descriptions-item label="性格关键词">{{ currentPet.personality || '-' }}</el-descriptions-item>
  240. <el-descriptions-item label="萌宠性格" :span="2">{{ currentPet.cutePersonality || '-' }}</el-descriptions-item>
  241. </el-descriptions>
  242. <div class="section-title" style="margin-top: 20px">家庭信息</div>
  243. <el-descriptions :column="2" border>
  244. <el-descriptions-item label="到家时间">{{ currentPet.arrivalTime || '-' }}</el-descriptions-item>
  245. <el-descriptions-item label="房屋类型">
  246. <dict-tag :options="sys_house_type" :value="currentPet.houseType" />
  247. </el-descriptions-item>
  248. <el-descriptions-item label="入门方式">
  249. <dict-tag :options="sys_entry_method" :value="currentPet.entryMethod" />
  250. </el-descriptions-item>
  251. <el-descriptions-item label="开门详情">
  252. {{ currentPet.entryMethod === 'password' ? currentPet.entryPassword : currentPet.keyLocation }}
  253. </el-descriptions-item>
  254. </el-descriptions>
  255. <div class="section-title" style="margin-top: 20px">健康状况</div>
  256. <el-descriptions :column="2" border>
  257. <el-descriptions-item label="健康状态">
  258. <el-tag :type="currentPet.healthStatus === '健康' ? 'success' : 'warning'" size="small">{{ currentPet.healthStatus }}</el-tag>
  259. </el-descriptions-item>
  260. <el-descriptions-item label="攻击倾向">
  261. <el-tag :type="currentPet.aggression ? 'danger' : 'success'" size="small">{{ currentPet.aggression ? '有' : '无' }}</el-tag>
  262. </el-descriptions-item>
  263. <el-descriptions-item label="疫苗情况" :span="2">
  264. <div>{{ currentPet.vaccineStatus || '-' }}</div>
  265. <div v-if="currentPet.vaccineCertUrl" style="margin-top: 10px">
  266. <el-image
  267. style="width: 100px; height: 100px; border-radius: 4px"
  268. :src="currentPet.vaccineCertUrl"
  269. :preview-src-list="[currentPet.vaccineCertUrl]"
  270. fit="cover"
  271. />
  272. </div>
  273. </el-descriptions-item>
  274. <el-descriptions-item label="既往病史" :span="2">{{ currentPet.medicalHistory || '-' }}</el-descriptions-item>
  275. <el-descriptions-item label="过敏史" :span="2">{{ currentPet.allergies || '-' }}</el-descriptions-item>
  276. </el-descriptions>
  277. </el-tab-pane>
  278. <el-tab-pane label="历史订单" name="orders">
  279. <el-table :data="mockOrders" border style="width: 100%">
  280. <el-table-column prop="orderNo" label="订单编号" width="180" />
  281. <el-table-column prop="service" label="服务项目" />
  282. <el-table-column prop="time" label="服务时间" width="180" />
  283. <el-table-column prop="amount" label="金额" width="100" />
  284. <el-table-column prop="status" label="状态" width="100">
  285. <template #default="scope">
  286. <el-tag type="success" size="small">完成</el-tag>
  287. </template>
  288. </el-table-column>
  289. </el-table>
  290. </el-tab-pane>
  291. <el-tab-pane label="备注日志" name="logs">
  292. <el-timeline style="margin-top: 10px; padding-left: 5px">
  293. <el-timeline-item v-for="(log, index) in changeLogs" :key="index" :timestamp="log.createTime" type="primary">
  294. [{{ log.logType }}] {{ log.content }}
  295. <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operatorName }}</div>
  296. </el-timeline-item>
  297. </el-timeline>
  298. </el-tab-pane>
  299. </el-tabs>
  300. </el-drawer>
  301. <!-- Remark Dialog -->
  302. <el-dialog v-model="remarkDialogVisible" title="添加备注" width="400px">
  303. <el-input v-model="remarkForm.content" type="textarea" :rows="3" placeholder="请输入备注内容..." />
  304. <template #footer>
  305. <span class="dialog-footer">
  306. <el-button @click="remarkDialogVisible = false">取消</el-button>
  307. <el-button type="primary" @click="saveRemark">保存</el-button>
  308. </span>
  309. </template>
  310. </el-dialog>
  311. </div>
  312. </template>
  313. <script setup>
  314. import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
  315. import { globalHeaders } from '@/utils/request';
  316. import { ElMessage, ElMessageBox } from 'element-plus';
  317. import { listPet, getPet, addPet, updatePet, delPet } from '@/api/archieves/pet';
  318. import { listAllTag } from '@/api/archieves/tag';
  319. import { listAllCustomer } from '@/api/archieves/customer';
  320. import { listAllChangeLog } from '@/api/archieves/changeLog';
  321. const { proxy } = getCurrentInstance();
  322. const { sys_pet_gender, sys_pet_type, sys_pet_size, sys_pet_breed, sys_house_type, sys_entry_method } = toRefs(
  323. proxy?.useDict('sys_pet_gender', 'sys_pet_type', 'sys_pet_size', 'sys_pet_breed', 'sys_house_type', 'sys_entry_method')
  324. );
  325. const loading = ref(false);
  326. const submitLoading = ref(false);
  327. const total = ref(0);
  328. const tableData = ref([]);
  329. const queryParams = reactive({
  330. pageNum: 1,
  331. pageSize: 10,
  332. keyword: '',
  333. tab: 0
  334. });
  335. /** 处理标签页切换 */
  336. const handleTabChange = () => {
  337. queryParams.pageNum = 1;
  338. getList();
  339. };
  340. const searchKey = ref('');
  341. const dialogVisible = ref(false);
  342. const drawerVisible = ref(false);
  343. const remarkDialogVisible = ref(false);
  344. const isEdit = ref(false);
  345. const activeTab = ref('basic');
  346. const detailActiveTab = ref('info');
  347. const currentPet = ref({});
  348. const allPetTags = ref([]);
  349. const userList = ref([]);
  350. const changeLogs = ref([]);
  351. const mockOrders = ref([
  352. { orderNo: 'DD20231001001', service: '上门喂养 (标准版)', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
  353. { orderNo: 'DD20230915002', service: '深度洗护套餐', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
  354. ]);
  355. const remarkForm = reactive({ content: '' });
  356. const avatarDisplayUrl = ref('');
  357. const vaccineCertDisplayUrl = ref('');
  358. const form = reactive({
  359. id: undefined,
  360. userId: undefined,
  361. avatar: undefined,
  362. name: '',
  363. type: 0,
  364. gender: undefined,
  365. breed: '',
  366. birthday: '',
  367. age: 1,
  368. weight: 5,
  369. size: 'small',
  370. isSterilized: 0,
  371. arrivalTime: '',
  372. houseType: '',
  373. entryMethod: '',
  374. entryPassword: '',
  375. keyLocation: '',
  376. personality: '',
  377. cutePersonality: '',
  378. healthStatus: '健康',
  379. aggression: 0,
  380. vaccineStatus: '无',
  381. vaccineCert: undefined,
  382. medicalHistory: '',
  383. allergies: '',
  384. remark: '',
  385. tagIds: []
  386. });
  387. const getList = () => {
  388. loading.value = true;
  389. queryParams.keyword = searchKey.value;
  390. listPet(queryParams).then((res) => {
  391. tableData.value = res.rows;
  392. total.value = res.total;
  393. }).finally(() => {
  394. loading.value = false;
  395. });
  396. };
  397. const handleSearch = () => {
  398. queryParams.pageNum = 1;
  399. getList();
  400. };
  401. const loadTags = () => {
  402. listAllTag({ category: 'pet', status: 0 }).then((res) => {
  403. allPetTags.value = res.data || [];
  404. });
  405. };
  406. const loadUsers = () => {
  407. listAllCustomer({ status: 0 }).then((res) => {
  408. userList.value = res.data || [];
  409. });
  410. };
  411. const handleAdd = () => {
  412. isEdit.value = false;
  413. activeTab.value = 'basic';
  414. Object.assign(form, {
  415. id: undefined, userId: undefined, avatar: undefined, name: '', type: 0, gender: undefined,
  416. breed: '', birthday: '', age: 1, weight: 5, size: 'small', isSterilized: 0,
  417. arrivalTime: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
  418. personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
  419. vaccineStatus: '无', vaccineCert: undefined, medicalHistory: '', allergies: '', remark: '', tagIds: []
  420. });
  421. avatarDisplayUrl.value = '';
  422. vaccineCertDisplayUrl.value = '';
  423. dialogVisible.value = true;
  424. };
  425. const handleEdit = (row) => {
  426. isEdit.value = true;
  427. activeTab.value = 'basic';
  428. getPet(row.id).then((res) => {
  429. const data = res.data;
  430. Object.assign(form, {
  431. id: data.id, userId: data.userId, avatar: data.avatar, name: data.name, type: data.type,
  432. gender: data.gender, breed: data.breed, birthday: data.birthday, age: data.age,
  433. weight: data.weight, size: data.size, isSterilized: data.isSterilized,
  434. arrivalTime: data.arrivalTime, houseType: data.houseType, entryMethod: data.entryMethod,
  435. entryPassword: data.entryPassword, keyLocation: data.keyLocation,
  436. personality: data.personality, cutePersonality: data.cutePersonality,
  437. healthStatus: data.healthStatus, aggression: data.aggression,
  438. vaccineStatus: data.vaccineStatus, vaccineCert: data.vaccineCert,
  439. medicalHistory: data.medicalHistory, allergies: data.allergies, remark: data.remark,
  440. tagIds: data.tags ? data.tags.map(t => t.id) : []
  441. });
  442. avatarDisplayUrl.value = data.avatarUrl || '';
  443. vaccineCertDisplayUrl.value = data.vaccineCertUrl || '';
  444. dialogVisible.value = true;
  445. });
  446. };
  447. const handleDetail = (row) => {
  448. getPet(row.id).then((res) => {
  449. currentPet.value = res.data;
  450. detailActiveTab.value = 'info';
  451. listAllChangeLog(row.id, 'pet').then((logRes) => {
  452. changeLogs.value = logRes.data || [];
  453. });
  454. drawerVisible.value = true;
  455. });
  456. };
  457. const handleRemark = (row) => {
  458. currentPet.value = row;
  459. remarkForm.content = '';
  460. remarkDialogVisible.value = true;
  461. };
  462. const saveRemark = () => {
  463. if (!remarkForm.content) return ElMessage.warning('请输入内容');
  464. const data = { id: currentPet.value.id, remark: remarkForm.content };
  465. updatePet(data).then(() => {
  466. ElMessage.success('备注添加成功');
  467. remarkDialogVisible.value = false;
  468. getList();
  469. // 刷新 drawer 中的变更日志
  470. if (drawerVisible.value) {
  471. listAllChangeLog(currentPet.value.id, 'pet').then((logRes) => {
  472. changeLogs.value = logRes.data || [];
  473. });
  474. }
  475. });
  476. };
  477. const handleDelete = (row) => {
  478. ElMessageBox.confirm('确认删除该宠物档案吗?', '提示', { type: 'warning' }).then(() => {
  479. delPet(row.id).then(() => {
  480. ElMessage.success('删除成功');
  481. getList();
  482. });
  483. });
  484. };
  485. const baseUrl = import.meta.env.VITE_APP_BASE_API;
  486. const uploadUrl = baseUrl + '/resource/oss/upload';
  487. const handleUploadFile = async (file) => {
  488. const formData = new FormData();
  489. formData.append('file', file.raw);
  490. try {
  491. const headers = globalHeaders();
  492. const res = await fetch(uploadUrl, {
  493. method: 'POST',
  494. headers: {
  495. 'Authorization': headers.Authorization,
  496. 'clientid': headers.clientid
  497. },
  498. body: formData
  499. });
  500. const result = await res.json();
  501. if (result.code === 200) {
  502. form.avatar = result.data.ossId;
  503. avatarDisplayUrl.value = result.data.url;
  504. } else {
  505. ElMessage.error(result.msg || '头像上传失败');
  506. }
  507. } catch (e) {
  508. ElMessage.error('头像上传失败');
  509. }
  510. };
  511. const handleUploadVaccineCert = async (file) => {
  512. const formData = new FormData();
  513. formData.append('file', file.raw);
  514. try {
  515. const headers = globalHeaders();
  516. const res = await fetch(uploadUrl, {
  517. method: 'POST',
  518. headers: {
  519. 'Authorization': headers.Authorization,
  520. 'clientid': headers.clientid
  521. },
  522. body: formData
  523. });
  524. const result = await res.json();
  525. if (result.code === 200) {
  526. form.vaccineCert = result.data.ossId;
  527. vaccineCertDisplayUrl.value = result.data.url;
  528. } else {
  529. ElMessage.error(result.msg || '疫苗凭证上传失败');
  530. }
  531. } catch (e) {
  532. ElMessage.error('疫苗凭证上传失败');
  533. }
  534. };
  535. const saveData = () => {
  536. if (!form.name) return ElMessage.warning('请输入宠物姓名');
  537. if (!form.userId) return ElMessage.warning('请选择所属主人');
  538. submitLoading.value = true;
  539. const api = isEdit.value ? updatePet(form) : addPet(form);
  540. api.then(() => {
  541. ElMessage.success('保存成功');
  542. dialogVisible.value = false;
  543. getList();
  544. }).finally(() => {
  545. submitLoading.value = false;
  546. });
  547. };
  548. onMounted(() => {
  549. getList();
  550. loadTags();
  551. loadUsers();
  552. });
  553. </script>
  554. <style scoped>
  555. .pet-tabs {
  556. margin-bottom: 20px;
  557. background-color: #fff;
  558. padding: 10px 20px 0;
  559. border-radius: 4px;
  560. }
  561. :deep(.el-tabs__header) {
  562. margin-bottom: 0;
  563. }
  564. :deep(.el-tabs__nav-wrap::after) {
  565. height: 0;
  566. }
  567. .page-container {
  568. padding: 20px;
  569. }
  570. .card-header {
  571. display: flex;
  572. justify-content: space-between;
  573. align-items: center;
  574. }
  575. .title {
  576. font-weight: bold;
  577. }
  578. .avatar-uploader-icon {
  579. font-size: 28px;
  580. color: #8c939d;
  581. width: 80px;
  582. height: 80px;
  583. text-align: center;
  584. border: 1px dashed #dcdfe6;
  585. border-radius: 50%;
  586. display: flex;
  587. justify-content: center;
  588. align-items: center;
  589. }
  590. .avatar-uploader-icon:hover {
  591. border-color: var(--el-color-primary);
  592. }
  593. .profile-header {
  594. display: flex;
  595. align-items: center;
  596. margin-bottom: 20px;
  597. padding-bottom: 20px;
  598. border-bottom: 1px solid #f0f0f0;
  599. }
  600. .profile-basic {
  601. margin-left: 20px;
  602. }
  603. .name-row {
  604. display: flex;
  605. align-items: center;
  606. }
  607. .name {
  608. font-size: 20px;
  609. font-weight: bold;
  610. color: #303133;
  611. }
  612. .section-title {
  613. font-size: 16px;
  614. font-weight: bold;
  615. margin-bottom: 15px;
  616. border-left: 4px solid #409eff;
  617. padding-left: 10px;
  618. line-height: 1.2;
  619. }
  620. .pagination-container {
  621. margin-top: 20px;
  622. display: flex;
  623. justify-content: flex-end;
  624. }
  625. </style>