index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. <template>
  2. <div>
  3. <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
  4. <div class="form-section-title">基础信息</div>
  5. <el-row :gutter="16">
  6. <el-col :span="12">
  7. <el-form-item label="设备编码" prop="equipmentCode">
  8. <el-input v-model="form.equipmentCode" placeholder="请输入设备编码" />
  9. </el-form-item>
  10. </el-col>
  11. <el-col :span="12">
  12. <el-form-item label="设备名称" prop="equipmentName">
  13. <el-input v-model="form.equipmentName" placeholder="请输入设备名称" />
  14. </el-form-item>
  15. </el-col>
  16. <el-col :span="12">
  17. <el-form-item label="设备类型" prop="equipmentTypeId">
  18. <el-tree-select
  19. v-model="form.equipmentTypeId"
  20. :disabled="typeLocked"
  21. :data="types"
  22. node-key="id"
  23. :props="{ label: 'typeName', children: 'children', disabled: 'disabled' }"
  24. default-expand-all
  25. :render-after-expand="false"
  26. placeholder="请选择设备类型"
  27. style="width: 100%"
  28. />
  29. </el-form-item>
  30. </el-col>
  31. <el-col :span="12">
  32. <el-form-item label="所属片区" prop="district">
  33. <el-select v-model="form.district" placeholder="请选择所属片区" style="width: 100%">
  34. <el-option v-for="item in districtOptions" :key="item.value" :label="item.label" :value="item.value" />
  35. </el-select>
  36. </el-form-item>
  37. </el-col>
  38. <el-col :span="12">
  39. <el-form-item label="设备型号" prop="equipmentModel">
  40. <el-input v-model="form.equipmentModel" placeholder="请输入设备型号" />
  41. </el-form-item>
  42. </el-col>
  43. <el-col :span="12">
  44. <el-form-item label="制造商" prop="manufacturer">
  45. <el-input v-model="form.manufacturer" placeholder="请输入制造商" />
  46. </el-form-item>
  47. </el-col>
  48. <el-col :span="12">
  49. <el-form-item label="生产日期" prop="productionDate">
  50. <el-date-picker v-model="form.productionDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择生产日期" style="width: 100%" />
  51. </el-form-item>
  52. </el-col>
  53. <el-col :span="12">
  54. <el-form-item label="规格参数" prop="equipmentSpec">
  55. <el-input v-model="form.equipmentSpec" placeholder="请输入设备规格参数" />
  56. </el-form-item>
  57. </el-col>
  58. </el-row>
  59. <div class="form-section-title">资产信息</div>
  60. <el-row :gutter="16">
  61. <el-col :span="12">
  62. <el-form-item label="资产状态" prop="currentStatus">
  63. <el-select v-model="form.currentStatus" placeholder="请选择资产状态" style="width: 100%">
  64. <el-option v-for="item in equipmentStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
  65. </el-select>
  66. </el-form-item>
  67. </el-col>
  68. <el-col :span="12">
  69. <el-form-item label="资产价值(元)" prop="assetValue">
  70. <el-input-number v-model="form.assetValue" :precision="2" :step="100" style="width: 100%" />
  71. </el-form-item>
  72. </el-col>
  73. <el-col :span="12">
  74. <el-form-item label="使用年限(年)" prop="useLife">
  75. <el-input-number v-model="form.useLife" :precision="0" :step="1" style="width: 100%" />
  76. </el-form-item>
  77. </el-col>
  78. </el-row>
  79. <div class="form-section-title">部署运维</div>
  80. <el-row :gutter="16">
  81. <el-col :span="24">
  82. <el-form-item label="安装位置" prop="equipmentLocation">
  83. <el-input v-model="form.equipmentLocation" placeholder="请输入或通过地图选择安装位置" />
  84. </el-form-item>
  85. </el-col>
  86. <el-col :span="24">
  87. <el-form-item label="地图坐标">
  88. <div class="coordinate-row">
  89. <el-input-number v-model="form.longitude" :precision="6" :step="0.000001" controls-position="right" placeholder="经度" />
  90. <el-input-number v-model="form.latitude" :precision="6" :step="0.000001" controls-position="right" placeholder="纬度" />
  91. <el-button type="primary" plain @click="openMapPicker">地图选点</el-button>
  92. </div>
  93. <div class="coordinate-tip">{{ locationTip || "点击地图获取经纬度,地址将自动回填到安装位置,也可以直接手工修改。" }}</div>
  94. </el-form-item>
  95. </el-col>
  96. <el-col :span="12">
  97. <el-form-item label="安装时间" prop="installTime">
  98. <el-date-picker
  99. v-model="form.installTime"
  100. type="datetime"
  101. value-format="YYYY-MM-DD HH:mm:ss"
  102. placeholder="请选择安装时间"
  103. style="width: 100%"
  104. />
  105. </el-form-item>
  106. </el-col>
  107. <el-col :span="12">
  108. <el-form-item label="维护人员" prop="maintainer">
  109. <el-input v-model="form.maintainer" placeholder="请输入维护人员" />
  110. </el-form-item>
  111. </el-col>
  112. <el-col :span="12">
  113. <el-form-item label="联系电话" prop="maintainerPhone">
  114. <el-input v-model="form.maintainerPhone" placeholder="请输入联系电话" />
  115. </el-form-item>
  116. </el-col>
  117. <el-col :span="24">
  118. <el-form-item label="备注" prop="remark">
  119. <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
  120. </el-form-item>
  121. </el-col>
  122. <el-col :span="24">
  123. <el-form-item label="设备图片" prop="equipmentImage">
  124. <image-upload v-model="form.equipmentImage" :limit="1" />
  125. </el-form-item>
  126. </el-col>
  127. </el-row>
  128. <slot />
  129. </el-form>
  130. <!-- 地图选点弹窗 -->
  131. <el-dialog v-model="mapOpen" title="地图选点" width="760px" append-to-body>
  132. <div class="map-picker">
  133. <GisMapCanvas
  134. :center="mapCenter"
  135. :points="mapPoints"
  136. :enable-clustering="false"
  137. :show-marker-name="false"
  138. @map-click="handleMapClick"
  139. />
  140. </div>
  141. <div class="map-picker-coordinates">
  142. 当前坐标:经度 {{ form.longitude ?? "-" }},纬度 {{ form.latitude ?? "-" }}
  143. </div>
  144. <div class="map-picker-coordinates">
  145. 解析地址:{{ form.equipmentLocation || "点击地图后自动解析" }}
  146. </div>
  147. <template #footer>
  148. <el-button @click="mapOpen = false">关 闭</el-button>
  149. <el-button type="primary" @click="mapOpen = false">使用当前坐标</el-button>
  150. </template>
  151. </el-dialog>
  152. </div>
  153. </template>
  154. <script setup>
  155. import { computed, ref } from 'vue'
  156. import GisMapCanvas from '@/components/GisMapCanvas.vue'
  157. import { bd09ToWgs84 } from '@/utils/coordTransform'
  158. const props = defineProps({ modelValue: { type: Object, required: true }, types: { type: Array, default: () => [] }, typeLocked: Boolean })
  159. const emit = defineEmits(['update:modelValue'])
  160. const form = computed({ get: () => props.modelValue, set: value => emit('update:modelValue', value) })
  161. const formRef = ref(), mapOpen = ref(false), mapCenter = ref({ lng: 110.3965, lat: 28.4638 }), mapPoints = ref([]), locationTip = ref('')
  162. const districtOptions = [{ label: '城南', value: 0 }, { label: '城北', value: 1 }, { label: '太常', value: 2 }]
  163. const equipmentStatusOptions = [{ label: '在用', value: 0 }, { label: '维修', value: 1 }, { label: '报废', value: 2 }]
  164. const rules = {
  165. equipmentCode: [{ required: true, message: '设备编码不能为空', trigger: 'blur' }],
  166. equipmentName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
  167. equipmentTypeId: [{ required: true, message: '设备类型不能为空', trigger: 'change' }],
  168. district: [{ required: true, message: '所属片区不能为空', trigger: 'change' }]
  169. }
  170. // 逆地理编码:传入百度 BD-09 坐标,返回结构化地址文本,失败时返回空串
  171. function reverseGeocode(lng, lat) {
  172. return new Promise((resolve) => {
  173. const BMapGL = window.BMapGL;
  174. if (!BMapGL || typeof BMapGL.Geocoder !== "function") {
  175. resolve("");
  176. return;
  177. }
  178. try {
  179. const geocoder = new BMapGL.Geocoder();
  180. geocoder.getLocation(new BMapGL.Point(lng, lat), (result) => {
  181. resolve(extractAddress(result));
  182. });
  183. } catch (error) {
  184. resolve("");
  185. }
  186. });
  187. }
  188. function extractAddress(result) {
  189. const content = result?.content || result || {};
  190. const address = result?.address || content.address;
  191. if (address) {
  192. return String(address);
  193. }
  194. const components = result?.addressComponents || content.address_detail || {};
  195. return [components.province, components.city, components.district, components.street, components.streetNumber]
  196. .filter(Boolean)
  197. .join("");
  198. }
  199. // 地图选点后按坐标回填安装位置,解析失败时保留原值
  200. async function fillLocationByPoint(lng, lat) {
  201. locationTip.value = "正在解析地址...";
  202. const address = await reverseGeocode(lng, lat);
  203. if (address) {
  204. form.value.equipmentLocation = address;
  205. locationTip.value = "已按选点回填安装位置,可手工修改。";
  206. } else {
  207. locationTip.value = "地址解析失败,请手动填写安装位置。";
  208. }
  209. }
  210. function openMapPicker() {
  211. const longitude = Number(form.value.longitude);
  212. const latitude = Number(form.value.latitude);
  213. mapCenter.value = Number.isFinite(longitude) && Number.isFinite(latitude)
  214. ? { lng: longitude, lat: latitude }
  215. : { lng: 110.3965, lat: 28.4638 };
  216. mapPoints.value = Number.isFinite(longitude) && Number.isFinite(latitude)
  217. ? [{ lng: longitude, lat: latitude, name: form.value.equipmentName || "设备位置" }]
  218. : [];
  219. locationTip.value = "";
  220. mapOpen.value = true;
  221. }
  222. function handleMapClick(point) {
  223. if (!point) return;
  224. const [longitude, latitude] = bd09ToWgs84(point.lng, point.lat);
  225. form.value.longitude = Number(longitude.toFixed(6));
  226. form.value.latitude = Number(latitude.toFixed(6));
  227. mapPoints.value = [{ lng: form.value.longitude, lat: form.value.latitude, name: form.value.equipmentName || "设备位置" }];
  228. fillLocationByPoint(point.lng, point.lat);
  229. }
  230. defineExpose({ validate: (...args) => formRef.value.validate(...args), resetFields: () => formRef.value?.resetFields() })
  231. </script>
  232. <style scoped>
  233. .form-section-title {
  234. margin: 8px 0 12px;
  235. padding-left: 10px;
  236. border-left: 3px solid #014db2;
  237. color: #1f2937;
  238. font-size: 14px;
  239. font-weight: 600;
  240. }
  241. .coordinate-row {
  242. display: flex;
  243. align-items: center;
  244. gap: 10px;
  245. width: 100%;
  246. }
  247. .coordinate-row :deep(.el-input-number) {
  248. flex: 1;
  249. }
  250. .coordinate-tip {
  251. margin-top: 6px;
  252. color: #9ca3af;
  253. font-size: 12px;
  254. }
  255. .map-picker {
  256. height: 420px;
  257. overflow: hidden;
  258. border: 1px solid #e5e7eb;
  259. border-radius: 6px;
  260. }
  261. .map-picker-coordinates {
  262. margin-top: 10px;
  263. color: #6b7280;
  264. font-size: 13px;
  265. }
  266. </style>