index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. <template>
  2. <div class="page-container">
  3. <el-card shadow="never">
  4. <template #header>
  5. <div class="card-header">
  6. <span class="title">宠物档案</span>
  7. <div class="header-actions">
  8. <el-input v-model="searchKey" placeholder="搜索宠物名/主人" style="width: 200px; margin-right: 10px" clearable @keyup.enter="handleSearch" @clear="handleSearch" />
  9. <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['archieves:pet:add']">新增档案</el-button>
  10. </div>
  11. </div>
  12. </template>
  13. <el-table :data="tableData" v-loading="loading" style="width: 100%">
  14. <el-table-column label="宠物信息" width="220">
  15. <template #default="scope">
  16. <div style="display: flex; align-items: center">
  17. <el-avatar :size="50" :src="scope.row.avatarUrl" style="margin-right: 10px" />
  18. <div>
  19. <div style="font-weight: bold">{{ scope.row.name }}</div>
  20. <div style="font-size: 12px; color: #999">{{ scope.row.breed }} | {{ scope.row.age }}岁</div>
  21. </div>
  22. </div>
  23. </template>
  24. </el-table-column>
  25. <el-table-column prop="gender" label="性别" width="80" align="center">
  26. <template #default="scope">
  27. <dict-tag :options="sys_pet_gender" :value="scope.row.gender" />
  28. </template>
  29. </el-table-column>
  30. <el-table-column label="所属主人" width="180">
  31. <template #default="scope">
  32. <div>{{ scope.row.ownerName }}</div>
  33. <div style="font-size: 12px; color: #666">{{ scope.row.ownerPhone }}</div>
  34. </template>
  35. </el-table-column>
  36. <el-table-column label="标签" min-width="150">
  37. <template #default="scope">
  38. <el-tag v-for="tag in scope.row.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">{{
  39. tag.name
  40. }}</el-tag>
  41. </template>
  42. </el-table-column>
  43. <el-table-column label="健康状态" width="100" align="center">
  44. <template #default="scope">
  45. <el-tag :type="scope.row.healthStatus === '健康' ? 'success' : 'warning'" effect="dark" size="small">{{ scope.row.healthStatus }}</el-tag>
  46. </template>
  47. </el-table-column>
  48. <el-table-column label="疫苗接种" width="120" align="center">
  49. <template #default="scope">
  50. {{ scope.row.vaccineStatus || '-' }}
  51. </template>
  52. </el-table-column>
  53. <el-table-column label="操作" width="200" align="center">
  54. <template #default="scope">
  55. <el-button link type="primary" @click="handleDetail(scope.row)" v-hasPermi="['archieves:pet:query']">详情</el-button>
  56. <el-button link type="primary" @click="handleEdit(scope.row)" v-hasPermi="['archieves:pet:edit']">编辑</el-button>
  57. <el-button link type="primary" @click="handleRemark(scope.row)" v-hasPermi="['archieves:pet:remark']">备注</el-button>
  58. <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['archieves:pet:remove']">删除</el-button>
  59. </template>
  60. </el-table-column>
  61. </el-table>
  62. <div class="pagination-container">
  63. <el-pagination
  64. v-model:current-page="queryParams.pageNum"
  65. v-model:page-size="queryParams.pageSize"
  66. :page-sizes="[10, 20, 50, 100]"
  67. layout="total, sizes, prev, pager, next, jumper"
  68. :total="total"
  69. @size-change="getList"
  70. @current-change="getList"
  71. />
  72. </div>
  73. </el-card>
  74. <el-dialog v-model="dialogVisible" title="宠物档案详情" width="800px">
  75. <el-tabs v-model="activeTab">
  76. <el-tab-pane label="基本信息" name="basic">
  77. <el-form :model="form" label-width="100px">
  78. <el-row>
  79. <el-col :span="24" style="display: flex; justify-content: center; margin-bottom: 20px">
  80. <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadFile">
  81. <el-avatar v-if="avatarDisplayUrl" :src="avatarDisplayUrl" :size="80" />
  82. <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
  83. </el-upload>
  84. </el-col>
  85. <el-col :span="12">
  86. <el-form-item label="宠物姓名" required><el-input v-model="form.name" /></el-form-item>
  87. </el-col>
  88. <el-col :span="12">
  89. <el-form-item label="所属主人" required>
  90. <el-select v-model="form.userId" placeholder="选择主人" style="width: 100%" filterable>
  91. <el-option v-for="user in userList" :key="user.id" :label="user.name + ' - ' + user.phone" :value="user.id" />
  92. </el-select>
  93. </el-form-item>
  94. </el-col>
  95. <el-col :span="12">
  96. <el-form-item label="性别">
  97. <el-select v-model="form.gender" placeholder="请选择">
  98. <el-option v-for="dict in sys_pet_gender" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
  99. </el-select>
  100. </el-form-item>
  101. </el-col>
  102. <el-col :span="12">
  103. <el-form-item label="品种">
  104. <el-select v-model="form.breed" placeholder="请选择品种" style="width: 100%" filterable allow-create default-first-option>
  105. <el-option v-for="dict in sys_pet_breed" :key="dict.value" :label="dict.label" :value="dict.value" />
  106. </el-select>
  107. </el-form-item>
  108. </el-col>
  109. <el-col :span="12">
  110. <el-form-item label="体型">
  111. <el-select v-model="form.size" style="width: 100%">
  112. <el-option v-for="dict in sys_pet_size" :key="dict.value" :label="dict.label" :value="dict.value" />
  113. </el-select>
  114. </el-form-item>
  115. </el-col>
  116. <el-col :span="12">
  117. <el-form-item label="体重(kg)"><el-input-number v-model="form.weight" :min="0" :precision="1" style="width: 100%" /></el-form-item>
  118. </el-col>
  119. <el-col :span="12">
  120. <el-form-item label="年龄(岁)"><el-input-number v-model="form.age" :min="0" style="width: 100%" /></el-form-item>
  121. </el-col>
  122. <el-col :span="24">
  123. <el-form-item label="性格关键词"><el-input v-model="form.personality" placeholder="如:活泼、粘人" /></el-form-item>
  124. </el-col>
  125. <el-col :span="24">
  126. <el-form-item label="萌宠性格"><el-input v-model="form.cutePersonality" type="textarea" placeholder="详细描述" /></el-form-item>
  127. </el-col>
  128. <el-col :span="24">
  129. <el-form-item label="宠物标签">
  130. <el-select v-model="form.tagIds" multiple placeholder="选择标签" style="width: 100%">
  131. <el-option v-for="tag in allPetTags" :key="tag.id" :label="tag.name" :value="tag.id">
  132. <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
  133. </el-option>
  134. </el-select>
  135. </el-form-item>
  136. </el-col>
  137. </el-row>
  138. </el-form>
  139. </el-tab-pane>
  140. <el-tab-pane label="家庭信息" name="family">
  141. <el-form :model="form" label-width="120px">
  142. <el-form-item label="新来家庭时间">
  143. <el-date-picker v-model="form.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
  144. </el-form-item>
  145. <el-form-item label="家庭房屋类型">
  146. <el-radio-group v-model="form.houseType">
  147. <el-radio v-for="dict in sys_house_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
  148. </el-radio-group>
  149. </el-form-item>
  150. <el-form-item label="入门方式">
  151. <el-radio-group v-model="form.entryMethod">
  152. <el-radio v-for="dict in sys_entry_method" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
  153. </el-radio-group>
  154. </el-form-item>
  155. <el-form-item label="密码" v-if="form.entryMethod === 'password'">
  156. <el-input v-model="form.entryPassword" placeholder="请输入门锁密码" />
  157. </el-form-item>
  158. <el-form-item label="钥匙位置" v-if="form.entryMethod === 'key'">
  159. <el-input v-model="form.keyLocation" placeholder="请输入钥匙存放位置" />
  160. </el-form-item>
  161. </el-form>
  162. </el-tab-pane>
  163. <el-tab-pane label="健康状况" name="health">
  164. <el-form :model="form" label-width="120px">
  165. <el-form-item label="健康状态">
  166. <el-radio-group v-model="form.healthStatus">
  167. <el-radio label="健康">健康</el-radio>
  168. <el-radio label="亚健康">亚健康</el-radio>
  169. <el-radio label="疾病">疾病</el-radio>
  170. </el-radio-group>
  171. </el-form-item>
  172. <el-form-item label="是否有攻击倾向">
  173. <el-switch v-model="form.aggression" active-text="是" inactive-text="否" />
  174. </el-form-item>
  175. <el-form-item label="疫苗情况">
  176. <el-radio-group v-model="form.vaccineStatus">
  177. <el-radio label="无">无</el-radio>
  178. <el-radio label="已打1次">已打1次</el-radio>
  179. <el-radio label="已打2次">已打2次</el-radio>
  180. <el-radio label="已打3次">已打3次</el-radio>
  181. </el-radio-group>
  182. </el-form-item>
  183. <el-form-item label="疫苗凭证">
  184. <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadVaccineCert">
  185. <img v-if="vaccineCertDisplayUrl" :src="vaccineCertDisplayUrl" class="avatar" style="width: 100px; height: 100px; object-fit: cover" />
  186. <el-icon v-else class="avatar-uploader-icon" style="width: 100px; height: 100px; line-height: 100px"><Plus /></el-icon>
  187. </el-upload>
  188. </el-form-item>
  189. <el-form-item label="既往病史">
  190. <el-input v-model="form.medicalHistory" type="textarea" placeholder="如有病史请记录" />
  191. </el-form-item>
  192. <el-form-item label="过敏史">
  193. <el-input v-model="form.allergies" type="textarea" placeholder="如有过敏源请记录" />
  194. </el-form-item>
  195. </el-form>
  196. </el-tab-pane>
  197. </el-tabs>
  198. <template #footer>
  199. <span class="dialog-footer">
  200. <el-button @click="dialogVisible = false">取消</el-button>
  201. <el-button type="primary" :loading="submitLoading" @click="saveData">保存</el-button>
  202. </span>
  203. </template>
  204. </el-dialog>
  205. <!-- Pet Profile Drawer -->
  206. <PetDetailDrawer
  207. v-model:visible="drawerVisible"
  208. :pet-id="currentPet.id"
  209. :service-list="serviceList"
  210. editable
  211. @remark-saved="getList"
  212. />
  213. </div>
  214. </template>
  215. <script setup>
  216. import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
  217. import { globalHeaders } from '@/utils/request';
  218. import { ElMessage, ElMessageBox } from 'element-plus';
  219. import { listPet, getPet, addPet, updatePet, delPet } from '@/api/archieves/pet'
  220. import { listAllTag } from '@/api/archieves/tag'
  221. import { listAllCustomer } from '@/api/archieves/customer'
  222. import { listAllService } from '@/api/service/list/index'
  223. import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
  224. const { proxy } = getCurrentInstance();
  225. const { sys_pet_gender, sys_pet_type, sys_pet_size, sys_pet_breed, sys_house_type, sys_entry_method } = toRefs(
  226. proxy?.useDict('sys_pet_gender', 'sys_pet_type', 'sys_pet_size', 'sys_pet_breed', 'sys_house_type', 'sys_entry_method')
  227. );
  228. const loading = ref(false);
  229. const submitLoading = ref(false);
  230. const total = ref(0);
  231. const tableData = ref([]);
  232. const queryParams = reactive({
  233. pageNum: 1,
  234. pageSize: 10,
  235. keyword: ''
  236. });
  237. const searchKey = ref('');
  238. const dialogVisible = ref(false);
  239. const drawerVisible = ref(false);
  240. const remarkDialogVisible = ref(false);
  241. const isEdit = ref(false);
  242. const activeTab = ref('basic');
  243. const detailActiveTab = ref('info');
  244. const currentPet = ref({});
  245. const allPetTags = ref([]);
  246. const userList = ref([]);
  247. const serviceList = ref([]);
  248. const getServiceList = () => {
  249. listAllService().then((res) => {
  250. serviceList.value = res.data || []
  251. })
  252. }
  253. const avatarDisplayUrl = ref('')
  254. const vaccineCertDisplayUrl = ref('')
  255. const form = reactive({
  256. id: undefined,
  257. userId: undefined,
  258. avatar: undefined,
  259. name: '',
  260. type: 0,
  261. gender: undefined,
  262. breed: '',
  263. birthday: '',
  264. age: 1,
  265. weight: 5,
  266. size: 'small',
  267. isSterilized: 0,
  268. arrivalTime: '',
  269. houseType: '',
  270. entryMethod: '',
  271. entryPassword: '',
  272. keyLocation: '',
  273. personality: '',
  274. cutePersonality: '',
  275. healthStatus: '健康',
  276. aggression: 0,
  277. vaccineStatus: '无',
  278. vaccineCert: undefined,
  279. medicalHistory: '',
  280. allergies: '',
  281. remark: '',
  282. tagIds: []
  283. });
  284. const getList = () => {
  285. loading.value = true;
  286. queryParams.keyword = searchKey.value;
  287. listPet(queryParams).then((res) => {
  288. tableData.value = res.rows;
  289. total.value = res.total;
  290. }).finally(() => {
  291. loading.value = false;
  292. });
  293. };
  294. const handleSearch = () => {
  295. queryParams.pageNum = 1;
  296. getList();
  297. };
  298. const loadTags = () => {
  299. listAllTag({ category: 'pet', status: 0 }).then((res) => {
  300. allPetTags.value = res.data || [];
  301. });
  302. };
  303. const loadUsers = () => {
  304. listAllCustomer({ status: 0 }).then((res) => {
  305. userList.value = res.data || [];
  306. });
  307. };
  308. const handleAdd = () => {
  309. isEdit.value = false;
  310. activeTab.value = 'basic';
  311. Object.assign(form, {
  312. id: undefined, userId: undefined, avatar: undefined, name: '', type: 0, gender: undefined,
  313. breed: '', birthday: '', age: 1, weight: 5, size: 'small', isSterilized: 0,
  314. arrivalTime: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
  315. personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
  316. vaccineStatus: '无', vaccineCert: undefined, medicalHistory: '', allergies: '', remark: '', tagIds: []
  317. });
  318. avatarDisplayUrl.value = '';
  319. vaccineCertDisplayUrl.value = '';
  320. dialogVisible.value = true;
  321. };
  322. const handleEdit = (row) => {
  323. isEdit.value = true;
  324. activeTab.value = 'basic';
  325. getPet(row.id).then((res) => {
  326. const data = res.data;
  327. Object.assign(form, {
  328. id: data.id, userId: data.userId, avatar: data.avatar, name: data.name, type: data.type,
  329. gender: data.gender, breed: data.breed, birthday: data.birthday, age: data.age,
  330. weight: data.weight, size: data.size, isSterilized: data.isSterilized,
  331. arrivalTime: data.arrivalTime, houseType: data.houseType, entryMethod: data.entryMethod,
  332. entryPassword: data.entryPassword, keyLocation: data.keyLocation,
  333. personality: data.personality, cutePersonality: data.cutePersonality,
  334. healthStatus: data.healthStatus, aggression: data.aggression,
  335. vaccineStatus: data.vaccineStatus, vaccineCert: data.vaccineCert,
  336. medicalHistory: data.medicalHistory, allergies: data.allergies, remark: data.remark,
  337. tagIds: data.tags ? data.tags.map(t => t.id) : []
  338. });
  339. avatarDisplayUrl.value = data.avatarUrl || '';
  340. vaccineCertDisplayUrl.value = data.vaccineCertUrl || '';
  341. dialogVisible.value = true;
  342. });
  343. };
  344. const handleDetail = (row) => {
  345. currentPet.value = row
  346. detailActiveTab.value = 'info'
  347. drawerVisible.value = true
  348. }
  349. const handleRemark = (row) => {
  350. currentPet.value = row
  351. drawerVisible.value = true
  352. // 由于备注功能已集成在详情抽屉中,直接打开抽屉即可,后期可以根据需要调整是否直接弹出备注对话框
  353. }
  354. const handleDelete = (row) => {
  355. ElMessageBox.confirm('确认删除该宠物档案吗?', '提示', { type: 'warning' }).then(() => {
  356. delPet(row.id).then(() => {
  357. ElMessage.success('删除成功');
  358. getList();
  359. });
  360. });
  361. };
  362. const baseUrl = import.meta.env.VITE_APP_BASE_API;
  363. const uploadUrl = baseUrl + '/resource/oss/upload';
  364. const handleUploadFile = async (file) => {
  365. const formData = new FormData();
  366. formData.append('file', file.raw);
  367. try {
  368. const headers = globalHeaders();
  369. const res = await fetch(uploadUrl, {
  370. method: 'POST',
  371. headers: {
  372. 'Authorization': headers.Authorization,
  373. 'clientid': headers.clientid
  374. },
  375. body: formData
  376. });
  377. const result = await res.json();
  378. if (result.code === 200) {
  379. form.avatar = result.data.ossId;
  380. avatarDisplayUrl.value = result.data.url;
  381. } else {
  382. ElMessage.error(result.msg || '头像上传失败');
  383. }
  384. } catch (e) {
  385. ElMessage.error('头像上传失败');
  386. }
  387. };
  388. const handleUploadVaccineCert = async (file) => {
  389. const formData = new FormData();
  390. formData.append('file', file.raw);
  391. try {
  392. const headers = globalHeaders();
  393. const res = await fetch(uploadUrl, {
  394. method: 'POST',
  395. headers: {
  396. 'Authorization': headers.Authorization,
  397. 'clientid': headers.clientid
  398. },
  399. body: formData
  400. });
  401. const result = await res.json();
  402. if (result.code === 200) {
  403. form.vaccineCert = result.data.ossId;
  404. vaccineCertDisplayUrl.value = result.data.url;
  405. } else {
  406. ElMessage.error(result.msg || '疫苗凭证上传失败');
  407. }
  408. } catch (e) {
  409. ElMessage.error('疫苗凭证上传失败');
  410. }
  411. };
  412. const saveData = () => {
  413. if (!form.name) return ElMessage.warning('请输入宠物姓名');
  414. if (!form.userId) return ElMessage.warning('请选择所属主人');
  415. submitLoading.value = true;
  416. const api = isEdit.value ? updatePet(form) : addPet(form);
  417. api.then(() => {
  418. ElMessage.success('保存成功');
  419. dialogVisible.value = false;
  420. getList();
  421. }).finally(() => {
  422. submitLoading.value = false;
  423. });
  424. };
  425. onMounted(() => {
  426. getList();
  427. loadTags();
  428. loadUsers();
  429. getServiceList();
  430. });
  431. </script>
  432. <style scoped>
  433. .page-container {
  434. padding: 20px;
  435. }
  436. .card-header {
  437. display: flex;
  438. justify-content: space-between;
  439. align-items: center;
  440. }
  441. .title {
  442. font-weight: bold;
  443. }
  444. .avatar-uploader-icon {
  445. font-size: 28px;
  446. color: #8c939d;
  447. width: 80px;
  448. height: 80px;
  449. text-align: center;
  450. border: 1px dashed #dcdfe6;
  451. border-radius: 50%;
  452. display: flex;
  453. justify-content: center;
  454. align-items: center;
  455. }
  456. .avatar-uploader-icon:hover {
  457. border-color: var(--el-color-primary);
  458. }
  459. .profile-header {
  460. display: flex;
  461. align-items: center;
  462. margin-bottom: 20px;
  463. padding-bottom: 20px;
  464. border-bottom: 1px solid #f0f0f0;
  465. }
  466. .profile-basic {
  467. margin-left: 20px;
  468. }
  469. .name-row {
  470. display: flex;
  471. align-items: center;
  472. }
  473. .name {
  474. font-size: 20px;
  475. font-weight: bold;
  476. color: #303133;
  477. }
  478. .section-title {
  479. font-size: 16px;
  480. font-weight: bold;
  481. margin-bottom: 15px;
  482. border-left: 4px solid #409eff;
  483. padding-left: 10px;
  484. line-height: 1.2;
  485. }
  486. .pagination-container {
  487. margin-top: 20px;
  488. display: flex;
  489. justify-content: flex-end;
  490. }
  491. </style>