index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. <template>
  2. <a-spin :spinning="spinning">
  3. <a-form
  4. ref="baseFormRef"
  5. :model="baseFormState"
  6. :rules="rules"
  7. :label-col="{ style: { width: '100px' } }"
  8. :wrapper-col="{ style: { width: '330px' } }"
  9. >
  10. <a-form-item label="设备名称" name="deviceName">
  11. <a-input
  12. v-model:value.trim="baseFormState.deviceName"
  13. placeholder="请输入设备名称"
  14. :maxlength="10"
  15. show-count
  16. :style="inputStyle"
  17. />
  18. </a-form-item>
  19. <a-form-item label="安装方式" name="installWay">
  20. <a-select
  21. v-model:value="baseFormState.installWay"
  22. placeholder="请选择安装方式"
  23. :style="inputStyle"
  24. >
  25. <a-select-option value="Wall">墙装</a-select-option>
  26. <a-select-option value="Ceiling">顶装</a-select-option>
  27. </a-select>
  28. </a-form-item>
  29. <a-form-item label="安装位置" name="installPosition">
  30. <a-select
  31. v-model:value="baseFormState.installPosition"
  32. placeholder="请选择安装位置"
  33. :style="inputStyle"
  34. :options="installPositionOptions"
  35. >
  36. </a-select>
  37. </a-form-item>
  38. <a-form-item label="X范围" class="outSideInput">
  39. <div class="rangeInput">
  40. <a-form-item name="xRangeStart">
  41. <a-input
  42. v-model:value.trim="baseFormState.xRangeStart"
  43. name="xRangeStart"
  44. :style="rangeInputStyle"
  45. placeholder="请输入"
  46. >
  47. <template #suffix>
  48. <a-tooltip>
  49. <template #title>
  50. <div>范围:-200 - 200 cm</div>
  51. </template>
  52. <info-circle-outlined style="color: rgba(0, 0, 0, 0.45)" />
  53. </a-tooltip>
  54. </template>
  55. </a-input>
  56. </a-form-item>
  57. <span class="line"> — </span>
  58. <a-form-item name="xRangeEnd">
  59. <a-input
  60. v-model:value.trim="baseFormState.xRangeEnd"
  61. name="xRangeEnd"
  62. :style="rangeInputStyle"
  63. placeholder="请输入"
  64. >
  65. <template #suffix>
  66. <a-tooltip>
  67. <template #title>
  68. <div>范围:-200 - 200 cm</div>
  69. </template>
  70. <info-circle-outlined style="color: rgba(0, 0, 0, 0.45)" />
  71. </a-tooltip>
  72. </template>
  73. </a-input>
  74. </a-form-item>
  75. </div>
  76. </a-form-item>
  77. <a-form-item label="Y范围" class="outSideInput">
  78. <div class="rangeInput">
  79. <a-form-item name="yRangeStart">
  80. <a-input
  81. v-model:value.trim="baseFormState.yRangeStart"
  82. name="yRangeStart"
  83. :style="rangeInputStyle"
  84. placeholder="请输入"
  85. >
  86. <template #suffix>
  87. <a-tooltip>
  88. <template #title>
  89. <div>范围:-250 - 250 cm</div>
  90. </template>
  91. <info-circle-outlined style="color: rgba(0, 0, 0, 0.45)" />
  92. </a-tooltip>
  93. </template>
  94. </a-input>
  95. </a-form-item>
  96. <span class="line"> — </span>
  97. <a-form-item name="yRangeEnd">
  98. <a-input
  99. v-model:value.trim="baseFormState.yRangeEnd"
  100. name="yRangeEnd"
  101. :style="rangeInputStyle"
  102. placeholder="请输入"
  103. >
  104. <template #suffix>
  105. <a-tooltip>
  106. <template #title>
  107. <div>范围:-250 - 250 cm</div>
  108. </template>
  109. <info-circle-outlined style="color: rgba(0, 0, 0, 0.45)" />
  110. </a-tooltip>
  111. </template>
  112. </a-input>
  113. </a-form-item>
  114. </div>
  115. </a-form-item>
  116. <a-form-item label="Z范围" class="outSideInput">
  117. <div class="rangeInput">
  118. <a-form-item name="zRangeStart">
  119. <a-input
  120. v-model:value.trim="baseFormState.zRangeStart"
  121. name="zRangeStart"
  122. :style="rangeInputStyle"
  123. placeholder="请输入"
  124. >
  125. <template #suffix>
  126. <a-tooltip>
  127. <template #title>
  128. <div>范围:0 - 5 cm</div>
  129. </template>
  130. <info-circle-outlined style="color: rgba(0, 0, 0, 0.45)" />
  131. </a-tooltip>
  132. </template>
  133. </a-input>
  134. </a-form-item>
  135. <span class="line"> — </span>
  136. <a-form-item name="zRangeEnd">
  137. <a-input
  138. v-model:value.trim="baseFormState.zRangeEnd"
  139. name="zRangeEnd"
  140. :style="rangeInputStyle"
  141. placeholder="请输入"
  142. >
  143. <template #suffix>
  144. <a-tooltip>
  145. <template #title>
  146. <div>范围:200 - 300 cm</div>
  147. </template>
  148. <info-circle-outlined style="color: rgba(0, 0, 0, 0.45)" />
  149. </a-tooltip>
  150. </template>
  151. </a-input>
  152. </a-form-item>
  153. </div>
  154. </a-form-item>
  155. <a-form-item label="安装高度" name="installHeight">
  156. <a-input
  157. v-model:value.trim="baseFormState.installHeight"
  158. placeholder="请输入安装高度"
  159. :style="inputStyle"
  160. >
  161. <template #suffix>
  162. <a-tooltip>
  163. <template #title>
  164. <div>范围:250 - 370 cm</div>
  165. </template>
  166. <info-circle-outlined style="color: rgba(0, 0, 0, 0.45)" />
  167. </a-tooltip>
  168. </template>
  169. </a-input>
  170. </a-form-item>
  171. <a-form-item label="电源灯朝向" name="northAngle">
  172. <a-select ref="select" v-model:value="baseFormState.northAngle" :style="inputStyle">
  173. <a-select-option :value="0">北(0度)</a-select-option>
  174. <a-select-option :value="90">东(90度)</a-select-option>
  175. <a-select-option :value="180">南(180度)</a-select-option>
  176. <a-select-option :value="270">西(270度)</a-select-option>
  177. </a-select>
  178. </a-form-item>
  179. <a-form-item label="归属租户" name="tenantId">
  180. <a-select
  181. v-model:value="baseFormState.tenantId"
  182. placeholder="请选择归属租户"
  183. :options="tenantOptions"
  184. :style="inputStyle"
  185. allow-clear
  186. >
  187. </a-select>
  188. </a-form-item>
  189. <a-form-item label="跌倒确认时间" name="fallingConfirm">
  190. <a-input
  191. v-model:value.trim="baseFormState.fallingConfirm"
  192. placeholder="请输入跌倒确认时间"
  193. :style="inputStyle"
  194. >
  195. <template #suffix>
  196. <a-tooltip>
  197. <template #title>
  198. <div>需大于0,默认: 53秒</div>
  199. </template>
  200. <info-circle-outlined style="color: rgba(0, 0, 0, 0.45)" />
  201. </a-tooltip>
  202. </template>
  203. </a-input>
  204. </a-form-item>
  205. <a-form-item label="监护对象年龄" name="guardAge">
  206. <a-input
  207. v-model:value.trim="baseFormState.guardAge"
  208. placeholder="请输入监护对象年龄"
  209. :style="inputStyle"
  210. >
  211. <template #suffix>
  212. <a-tooltip>
  213. <template #title>
  214. <div>年龄范围: 1 - 120</div>
  215. </template>
  216. <info-circle-outlined style="color: rgba(0, 0, 0, 0.45)" />
  217. </a-tooltip>
  218. </template>
  219. </a-input>
  220. </a-form-item>
  221. <a-form-item label="监护对象类型" name="guardType">
  222. <a-select
  223. v-model:value="baseFormState.guardType"
  224. placeholder="请选择监护对象类型"
  225. :options="guardTypeOptions"
  226. :style="inputStyle"
  227. allow-clear
  228. >
  229. </a-select>
  230. </a-form-item>
  231. <div class="footer" :style="{ marginLeft: '100px' }">
  232. <a-space>
  233. <a-button
  234. type="primary"
  235. :loading="saveBaseLoading"
  236. :disabled="props.online === 0"
  237. @click="saveBaseConfig"
  238. >保存配置</a-button
  239. >
  240. <span v-if="props.online === 0" style="color: red">⚠️设备离线,不允许编辑保存</span>
  241. </a-space>
  242. </div>
  243. </a-form>
  244. </a-spin>
  245. </template>
  246. <script setup lang="ts">
  247. import { ref, reactive } from 'vue'
  248. import { message, type FormInstance } from 'ant-design-vue'
  249. import { InfoCircleOutlined } from '@ant-design/icons-vue'
  250. import type { Rule } from 'ant-design-vue/es/form'
  251. import * as deviceApi from '@/api/device'
  252. import * as tenantAPI from '@/api/tenant'
  253. import type { TenantItem } from '@/api/tenant/types'
  254. import { useUserStore } from '@/stores/user'
  255. import { useDict } from '@/hooks/useDict'
  256. defineOptions({
  257. name: 'deviceBaseConfig',
  258. })
  259. type Props = {
  260. devId: string // 设备id 查询使用
  261. clientId: string // 设备id 更新使用
  262. online: SwitchType | 9 // 设备是否在线
  263. }
  264. const emit = defineEmits<{
  265. (e: 'success', value: void): void
  266. }>()
  267. const props = withDefaults(defineProps<Props>(), {
  268. devId: '',
  269. clientId: '',
  270. online: 0,
  271. })
  272. const userStore = useUserStore()
  273. // 基础配置表单
  274. interface BaseFormState {
  275. deviceName: string // 设备名称
  276. installWay: InstallWay // 安装方式
  277. installPosition: InstallPosition // 安装位置
  278. xRangeStart: ID // x范围
  279. xRangeEnd: ID // x范围
  280. yRangeStart: ID // y范围
  281. yRangeEnd: ID // y范围
  282. zRangeStart: ID // z范围
  283. zRangeEnd: ID // z范围
  284. installHeight: ID // 安装高度
  285. northAngle: NorthAngle // 正北向夹角
  286. tenantId: ID // 租户id
  287. fallingConfirm: ID // 跌倒确认
  288. guardAge?: ID // 监护对象年龄
  289. guardType?: ID // 监护对象类型
  290. }
  291. const spinning = ref(false)
  292. const baseFormState = reactive<BaseFormState>({
  293. deviceName: '',
  294. installWay: 'Wall',
  295. installPosition: 'Toilet',
  296. xRangeStart: '',
  297. xRangeEnd: '',
  298. yRangeStart: '',
  299. yRangeEnd: '',
  300. zRangeStart: '',
  301. zRangeEnd: '',
  302. installHeight: '',
  303. northAngle: 0,
  304. tenantId: null,
  305. fallingConfirm: 53,
  306. guardAge: null,
  307. guardType: null,
  308. })
  309. // 范围输入框尺寸
  310. const rangeInputStyle = {
  311. width: '150px',
  312. }
  313. // 普通输入框尺寸
  314. const inputStyle = {
  315. width: '330px',
  316. }
  317. const baseFormRef = ref<FormInstance>()
  318. const saveBaseLoading = ref(false)
  319. // 保存基础配置
  320. const saveBaseConfig = async () => {
  321. baseFormRef.value
  322. ?.validate()
  323. .then(async () => {
  324. saveBaseLoading.value = true
  325. console.log('saveBaseConfig 校验通过', baseFormState, props)
  326. try {
  327. await deviceApi.updateDevice({
  328. clientId: props.clientId,
  329. userId: userStore?.userInfo?.userId,
  330. devName: baseFormState.deviceName,
  331. mountPlain: baseFormState.installWay,
  332. installPosition: baseFormState.installPosition,
  333. xxStart: baseFormState.xRangeStart,
  334. xxEnd: baseFormState.xRangeEnd,
  335. yyStart: baseFormState.yRangeStart,
  336. yyEnd: baseFormState.yRangeEnd,
  337. zzStart: baseFormState.zRangeStart,
  338. zzEnd: baseFormState.zRangeEnd,
  339. height: baseFormState.installHeight,
  340. northAngle: baseFormState.northAngle,
  341. tenantId: baseFormState?.tenantId,
  342. fallingConfirm:
  343. Number(baseFormState?.fallingConfirm) > 0
  344. ? Number(baseFormState?.fallingConfirm)
  345. : null,
  346. age: baseFormState?.guardAge ? Number(baseFormState?.guardAge) : null,
  347. guardianshipType: baseFormState?.guardType ? String(baseFormState?.guardType) : null,
  348. })
  349. saveBaseLoading.value = false
  350. message.success('保存成功')
  351. emit('success')
  352. } catch (error) {
  353. console.error('saveBaseConfig 保存失败', error)
  354. saveBaseLoading.value = false
  355. }
  356. })
  357. .catch((err) => {
  358. console.error('saveBaseConfig 校验失败', err)
  359. saveBaseLoading.value = false
  360. })
  361. }
  362. // 校验输入的范围大小
  363. // const createRangeValidator = (min: number, max: number) => async (_rule: Rule, value: string) => {
  364. // if (!value) {
  365. // return Promise.reject(new Error('不能为空'))
  366. // }
  367. // if (!/^-?\d+(\.\d+)?$/.test(value)) {
  368. // return Promise.reject(new Error('必须为数字'))
  369. // }
  370. // const num = parseFloat(value)
  371. // if (num < min || num > max) {
  372. // return Promise.reject(new Error(`范围:${min} ~ ${max}`))
  373. // }
  374. // return Promise.resolve()
  375. // }
  376. // 校验起始值和结束值的大小关系
  377. // const validateRangeOrder = (startField: string, endField: string) => {
  378. // return async () => {
  379. // const startVal = parseFloat(baseFormState[startField as keyof BaseFormState] as string)
  380. // const endVal = parseFloat(baseFormState[endField as keyof BaseFormState] as string)
  381. // if (startVal >= endVal) {
  382. // return Promise.reject(new Error('结束值须大于起始值'))
  383. // }
  384. // return Promise.resolve()
  385. // }
  386. // }
  387. // 统一校验规则
  388. const rules: Record<string, Rule[]> = {
  389. installWay: [
  390. {
  391. required: true,
  392. message: '请选择安装方式',
  393. trigger: ['change', 'blur'],
  394. },
  395. ],
  396. guardAge: [
  397. {
  398. validator: (_rule: Rule, value: string) => {
  399. if (!value) {
  400. return Promise.resolve()
  401. }
  402. if (!/^\d+$/.test(value)) {
  403. return Promise.reject(new Error('必须为整数'))
  404. }
  405. const num = parseInt(value, 10)
  406. if (num < 1 || num > 120) {
  407. return Promise.reject(new Error('年龄范围: 1 - 120'))
  408. }
  409. return Promise.resolve()
  410. },
  411. trigger: ['change', 'blur'],
  412. },
  413. ],
  414. // xRangeStart: [
  415. // {
  416. // required: true,
  417. // validator: createRangeValidator(-200, 200),
  418. // trigger: ['change', 'blur'],
  419. // },
  420. // ],
  421. // xRangeEnd: [
  422. // {
  423. // validator: createRangeValidator(-200, 200),
  424. // trigger: ['change', 'blur'],
  425. // },
  426. // {
  427. // validator: validateRangeOrder('xRangeStart', 'xRangeEnd'),
  428. // trigger: ['change', 'blur'],
  429. // },
  430. // ],
  431. // yRangeStart: [
  432. // {
  433. // validator: createRangeValidator(-250, 250),
  434. // trigger: ['change', 'blur'],
  435. // },
  436. // ],
  437. // yRangeEnd: [
  438. // {
  439. // validator: createRangeValidator(-250, 250),
  440. // trigger: ['change', 'blur'],
  441. // },
  442. // {
  443. // validator: validateRangeOrder('yRangeStart', 'yRangeEnd'),
  444. // trigger: ['change', 'blur'],
  445. // },
  446. // ],
  447. // zRangeStart: [
  448. // {
  449. // validator: createRangeValidator(0, 5),
  450. // trigger: ['change', 'blur'],
  451. // },
  452. // ],
  453. // zRangeEnd: [
  454. // {
  455. // validator: createRangeValidator(200, 300),
  456. // trigger: ['change', 'blur'],
  457. // },
  458. // ],
  459. }
  460. const tenantOptions = ref<{ label: string; value: string }[]>([])
  461. // 获取租户下拉列表
  462. const fetchTenantList = async () => {
  463. try {
  464. const res = await tenantAPI.queryTenant({
  465. pageNo: 1,
  466. pageSize: 10000,
  467. })
  468. const { rows } = res.data
  469. tenantOptions.value = rows.map((item: TenantItem) => ({
  470. label: item.tenantName,
  471. value: item?.tenantId as string,
  472. }))
  473. } catch (err) {
  474. console.log('❌ 获取数据失败', err)
  475. }
  476. }
  477. fetchTenantList()
  478. const { dictList: guardTypeOptions, fetchDict: fetchGuardTypeOptions } =
  479. useDict('guardianship_type')
  480. fetchGuardTypeOptions()
  481. const { dictList: installPositionOptions, fetchDict: fetchDictInstallPosition } =
  482. useDict('install_position')
  483. fetchDictInstallPosition()
  484. // 获取设备回显数据
  485. const fetchDeviceBaseInfo = async () => {
  486. console.log('fetchDeviceDetail', props)
  487. if (!props.devId) {
  488. message.error('设备ID不能为空')
  489. return
  490. }
  491. try {
  492. spinning.value = true
  493. const res = await deviceApi.getDeviceDetailByDevId({
  494. devId: props.devId,
  495. })
  496. console.log('✅获取到设备详情', res)
  497. baseFormState.deviceName = res.data.devName
  498. baseFormState.installWay = res.data.mountPlain
  499. baseFormState.installPosition = res.data.installPosition
  500. baseFormState.xRangeStart = res.data.xxStart
  501. baseFormState.xRangeEnd = res.data.xxEnd
  502. baseFormState.yRangeStart = res.data.yyStart
  503. baseFormState.yRangeEnd = res.data.yyEnd
  504. baseFormState.zRangeStart = res.data.zzStart
  505. baseFormState.zRangeEnd = res.data.zzEnd
  506. baseFormState.installHeight = res.data.height
  507. baseFormState.northAngle = res.data.northAngle
  508. baseFormState.tenantId = res.data.tenantId
  509. baseFormState.fallingConfirm = res.data.fallingConfirm
  510. baseFormState.guardAge = res.data.age
  511. baseFormState.guardType = res.data.guardianshipType
  512. } catch (error) {
  513. console.error('❌获取设备详情失败', error)
  514. }
  515. spinning.value = false
  516. }
  517. fetchDeviceBaseInfo()
  518. </script>
  519. <style scoped lang="less">
  520. // 取消嵌套的a-form-item的margin-bottom
  521. :deep(.outSideInput.ant-form-item) {
  522. margin-bottom: 0;
  523. }
  524. .rangeInput {
  525. display: flex;
  526. align-items: baseline;
  527. gap: 8px;
  528. }
  529. </style>