From 1f89ee30b19648c6da309833ecb7b41b8c77ada5 Mon Sep 17 00:00:00 2001 From: zhf <1204297681@qq.com> Date: Sat, 31 May 2025 00:22:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=85=E5=9C=B0=E5=9B=BE?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E7=BC=96=E8=BE=91=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 7 +- pnpm-lock.yaml | 7 + src/editor/index.ts | 4 + .../ibiz-map-picker/ibiz-map-picker.scss | 141 +++++ .../ibiz-map-picker/ibiz-map-picker.tsx | 482 ++++++++++++++++++ src/editor/map-picker/index.ts | 3 + .../map-picker-editor.controller.ts | 10 + .../map-picker/map-picker-editor.provider.ts | 27 + src/locale/en/index.ts | 4 + src/locale/zh-CN/index.ts | 4 + 10 files changed, 686 insertions(+), 3 deletions(-) create mode 100644 src/editor/map-picker/ibiz-map-picker/ibiz-map-picker.scss create mode 100644 src/editor/map-picker/ibiz-map-picker/ibiz-map-picker.tsx create mode 100644 src/editor/map-picker/index.ts create mode 100644 src/editor/map-picker/map-picker-editor.controller.ts create mode 100644 src/editor/map-picker/map-picker-editor.provider.ts diff --git a/package.json b/package.json index b484a1a4..477d8a76 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,12 @@ "publish:local": "npm run build && npm publish --access public --registry=http://172.16.240.221:8081/repository/local/" }, "dependencies": { + "@amap/amap-jsapi-loader": "^1.0.1", "@floating-ui/dom": "^1.5.3", "@ibiz-template-plugin/ai-chat": "^0.0.28", - "@ibiz-template-plugin/gantt": "0.1.8-alpha.198", "@ibiz-template-plugin/bi-report": "0.0.26", "@ibiz-template-plugin/data-view": "0.0.4", + "@ibiz-template-plugin/gantt": "0.1.8-alpha.198", "@ibiz-template/core": "0.7.40-alpha.19", "@ibiz-template/devtool": "0.0.11", "@ibiz-template/model-helper": "0.7.40-alpha.19", @@ -46,6 +47,7 @@ "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.12", "async-validator": "^4.2.5", + "axios": "^1.6.5", "cherry-markdown": "0.8.58", "dayjs": "^1.11.10", "echarts": "^5.4.3", @@ -58,14 +60,13 @@ "pinia": "^2.1.7", "pluralize": "^8.0.0", "qs": "^6.11.2", - "axios": "^1.6.5", "qx-util": "^0.4.8", "ramda": "^0.29.1", + "snabbdom": "^3.3.1", "vue": "^3.3.8", "vue-i18n": "^9.6.5", "vue-router": "^4.2.5", "vuedraggable": "^4.1.0", - "snabbdom": "^3.3.1", "xlsx": "^0.18.5" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c9f923e..8f1c4aea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@amap/amap-jsapi-loader': + specifier: ^1.0.1 + version: 1.0.1 '@floating-ui/dom': specifier: ^1.5.3 version: 1.5.3 @@ -216,6 +219,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /@amap/amap-jsapi-loader@1.0.1: + resolution: {integrity: sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==} + dev: false + /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} diff --git a/src/editor/index.ts b/src/editor/index.ts index c7931cfa..0a367271 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -62,6 +62,7 @@ import { DateRangeSelectProvider, IBizDateRangeSelect, } from './date-range-select'; +import { IBizMapPicker, MapPickerEditorProvider } from './map-picker'; export const IBizEditor = { install: (v: App): void => { @@ -105,6 +106,7 @@ export const IBizEditor = { v.component(IBizPresetRawitem.name, IBizPresetRawitem); v.component(IBizSearchCondEdit.name, IBizSearchCondEdit); v.component(IBizCarousel.name, IBizCarousel); + v.component(IBizMapPicker.name, IBizMapPicker); v.component( 'IBizHtml', defineAsyncComponent(() => import('./html/wang-editor/wang-editor')), @@ -414,6 +416,8 @@ export const IBizEditor = { 'PICKER_searchCondEdit', () => new SearchCondEditEditorProvider(), ); + // 地图选择器 + registerEditorProvider('MAPPICKER', () => new MapPickerEditorProvider()); // 预置类型编辑器 `${predefinedType}_${editorType}`; registerEditorProvider( diff --git a/src/editor/map-picker/ibiz-map-picker/ibiz-map-picker.scss b/src/editor/map-picker/ibiz-map-picker/ibiz-map-picker.scss new file mode 100644 index 00000000..397f3e9a --- /dev/null +++ b/src/editor/map-picker/ibiz-map-picker/ibiz-map-picker.scss @@ -0,0 +1,141 @@ +@include b(map-picker) { + @include m(readonly) { + height: auto; + overflow: auto; + line-height: getCssVar(editor, default, line-height); + color: getCssVar(form-item, readonly-color); + word-break: break-word; + white-space: pre-wrap; + } +} + +@include b(map-picker-input) { + .el-input__suffix { + display: none; + } + + .el-input__wrapper:hover { + .el-input__suffix { + display: inline-flex; + } + } +} + +@include b(map-picker-dialog) { + display: flex; + flex-direction: column; + height: 80%; + + --el-dialog-padding-primary: var(#{getCssVarName('spacing', 'base')}); + + .el-dialog__headerbtn { + top: calc(var(--el-dialog-padding-primary) - 2px); + right: calc(var(--el-dialog-padding-primary) / 2) + } + + .el-dialog__header, .el-dialog__footer { + flex: 0 0 auto; + padding: var(--el-dialog-padding-primary); + } + + .el-dialog__body { + flex: 1 1 0; + padding: var(--el-dialog-padding-primary); + border-top: 1px solid #{getCssVar(color, border)}; + border-bottom: 1px solid #{getCssVar(color, border)}; + } +} + + +@include b(map-picker-dialog-content) { + display: flex; + flex-direction: column; + height: 100%; +} + +@include b(map-picker-dialog-search-input) { + z-index: 999; + flex: 0 0 auto; + margin-bottom: getCssVar('spacing', 'tight'); + + .el-input__suffix { + display: none; + } + + .el-input__wrapper:hover { + .el-input__suffix { + display: inline-flex; + } + } +} + +@include b(map-picker-dialog-map-content) { + position: relative; + flex: 1 1 0; +} + +@include b(map-picker-dialog-map-container) { + height: 100%; +} + +@include b(map-picker-dialog-search-result-container) { + position: absolute; + top: 0; + right: 0; + display: block !important; + width: 280px; + max-height: 90%; + overflow-y: auto; + background-color: #fff; + + .amap_lib_placeSearch_page { + display: flex; + overflow-x: auto; + } +} + +@include b(map-picker-dialog-map-marker-text) { + position: absolute; + top: -24px; + padding: 4px 10px; + font-size: 12px; + color: #fff; + white-space: nowrap; + background-color: #3d93fd; + border-radius: 4px; + transform: translateX(calc(-50% - (25px / 2))); +} + +@include b(form-item) { + @include b(map-picker) { + @include when(show-default) { + &:hover { + @include b(map-picker-form-default-content) { + display: none; + } + + @include b(map-picker-input) { + display: inline-flex; + } + } + + @include b(map-picker-input) { + display: none; + font-family: Arial, sans-serif; + } + + @include b(map-picker-form-default-content) { + display: flex; + align-items: center; + width: 100%; + padding: getCssVar(form-item, hover-edit-padding); + font-family: Arial, sans-serif; + font-size: getCssVar(form-item, font-size); + line-height: getCssVar(editor, default, line-height); + color: getCssVar(form-item, text-color); + word-wrap: break-word; + white-space: pre-wrap; + } + } + } +} \ No newline at end of file diff --git a/src/editor/map-picker/ibiz-map-picker/ibiz-map-picker.tsx b/src/editor/map-picker/ibiz-map-picker/ibiz-map-picker.tsx new file mode 100644 index 00000000..b58f3d81 --- /dev/null +++ b/src/editor/map-picker/ibiz-map-picker/ibiz-map-picker.tsx @@ -0,0 +1,482 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { computed, defineComponent, nextTick, onUnmounted, ref } from 'vue'; +import { + useNamespace, + getEditorEmits, + getMapPickerProps, +} from '@ibiz-template/vue3-util'; +import AMapLoader from '@amap/amap-jsapi-loader'; +import { ElInput } from 'element-plus'; +import { MapPickerEditorController } from '../map-picker-editor.controller'; +import './ibiz-map-picker.scss'; + +/** + * 地图选择器 + * + * @description 通过高德地图选择具体位置,然后填充名称、经度和纬度。支持编辑器类型包含:`地图选择器` + * @primary + * @ignoreprops autoFocus | overflowMode + * @ignoreemits blur | focus | enter | infoTextChange + */ +export const IBizMapPicker = defineComponent({ + name: 'IBizMapPicker', + props: getMapPickerProps(), + emits: getEditorEmits(), + setup(props, { emit }) { + const ns = useNamespace('map-picker'); + + // 控制器 + const c = props.controller; + + // 值项 + const editorItem = c.model.editorItems?.[0]; + + // 输入框组件引用 + const inputRef = ref>(); + + // 搜索输入框元素 + const searchInputRef = ref(); + + // 地图容器元素 + const mapContainerRef = ref(); + + // 搜索结果容器元素 + const searchResultContainerRef = ref(); + + // 对话框是否显示 + const dialogVisible = ref(false); + + // 是否正在加载中 + const isLoading = ref(false); + + // 搜索值 + const searchValue = ref(''); + + // 是否显示表单默认内容 + const showFormDefaultContent = computed(() => { + if ( + props.controlParams && + props.controlParams.editmode === 'hover' && + !props.readonly + ) { + return true; + } + return false; + }); + + // 地图对象 + let map: IData | undefined; + + // 标记对象 + let marker: IData | undefined; + + // poi选择器 + let poiPicker: IData | undefined; + + // 地址信息 + const addressInfo: { + address: string; + longitude?: number | null; + latitude?: number | null; + } = { + address: '', + longitude: null, + latitude: null, + }; + + // 清除标记 + const clearMarker = () => { + if (marker) { + marker.setMap(null); + marker = undefined; + } + }; + + // 添加标记 + const addMarker = (lng: number, lat: number) => { + const AMap = (window as IData).AMap; + if (!AMap) { + return; + } + clearMarker(); + marker = new AMap.Marker({ + position: [lng, lat], + }); + }; + + // 获取位置 + const getAddress = (lng: number, lat: number) => { + const AMap = (window as IData).AMap; + if (!AMap) { + return; + } + if (!marker) { + return; + } + const geocoder = new AMap.Geocoder({}); + const currentMarker = marker; + geocoder.getAddress([lng, lat], (status: string, result: IData) => { + if (!marker || marker !== currentMarker) { + return; + } + if (status === 'complete' && result.info === 'OK' && result.regeocode) { + const regeocode = result.regeocode; + const address = regeocode.formattedAddress; + const markerContent = document.createElement('div'); + const markerImg = document.createElement('img'); + markerImg.style.width = '25px'; + markerImg.src = + '//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png'; + markerContent.appendChild(markerImg); + const markerText = document.createElement('span'); + markerText.className = ns.b('dialog-map-marker-text'); + markerText.textContent = address; + markerContent.appendChild(markerText); + marker.setContent(markerContent); + marker.setMap(map); + addressInfo.address = address; + addressInfo.longitude = lng; + addressInfo.latitude = lat; + searchValue.value = address; + } + }); + }; + + // 加载地图 + const loadMap = async () => { + try { + isLoading.value = true; + (window as IData)._AMapSecurityConfig = { + securityJsCode: ibiz.env.aMapSecurityJsCode, + }; + await AMapLoader.load({ + key: ibiz.env.aMapKey!, + version: '2.0', + plugins: ['AMap.PlaceSearch', 'AMap.Geocoder'], + AMapUI: { + version: '1.1', + plugins: ['misc/PoiPicker'], + }, + }); + } finally { + isLoading.value = false; + } + }; + + // 初始化地图 + const initMap = () => { + const AMap = (window as IData).AMap; + if (!AMap) { + return; + } + if (!mapContainerRef.value) { + return; + } + map = new AMap.Map(mapContainerRef.value, { + viewMode: '3D', + zoom: 11, + }); + map?.on('click', (e: IData) => { + const lnglat = e.lnglat; + const lng = lnglat.lng; + const lat = lnglat.lat; + if (lng != null && lat != null) { + addMarker(lng, lat); + getAddress(lng, lat); + } + }); + const AMapUI = (window as IData).AMapUI; + if (!AMapUI) { + return; + } + if (!searchInputRef.value || !searchResultContainerRef.value) { + return; + } + AMapUI.loadUI(['misc/PoiPicker'], function (PoiPicker: any) { + if ( + !searchInputRef.value || + !searchResultContainerRef.value || + !PoiPicker + ) { + return; + } + poiPicker = new PoiPicker({ + input: searchInputRef.value, + placeSearchOptions: { + map, + }, + searchResultsContainer: searchResultContainerRef.value, + }); + poiPicker?.on('poiPicked', function (poiResult: IData) { + clearMarker(); + const item = poiResult.item; + if (item) { + addressInfo.address = item.name; + addressInfo.longitude = item.location?.lng; + addressInfo.latitude = item.location?.lat; + searchValue.value = item.name; + if (poiResult.source !== 'search') { + poiPicker?.searchByKeyword(item.name); + } + } + }); + }); + }; + + // 处理对话框显示 + const handleShow = () => { + dialogVisible.value = true; + inputRef.value?.blur(); + nextTick(async () => { + if (!(window as IData).AMap) { + await loadMap(); + } + if (!map || !mapContainerRef.value?.children.length) { + map?.destroy(); + initMap(); + } + searchValue.value = props.value || ''; + if (editorItem && editorItem.id) { + const [longitudeName, latitudeName] = editorItem.id.split(','); + if (props.data) { + const longitude = props.data[longitudeName]; + const latitude = props.data[latitudeName]; + if (longitude && latitude) { + map?.setCenter([longitude, latitude], true); + addMarker(longitude, latitude); + getAddress(longitude, latitude); + } + } + } + }); + }; + + // 处理确认按钮点击 + const handleConfirm = () => { + dialogVisible.value = false; + if (editorItem && editorItem.id) { + const [longitudeName, latitudeName] = editorItem.id.split(','); + if (longitudeName) { + emit( + 'change', + addressInfo.longitude != null ? addressInfo.longitude : null, + longitudeName, + ); + } + if (latitudeName) { + emit( + 'change', + addressInfo.latitude != null ? addressInfo.latitude : null, + latitudeName, + ); + } + } + emit('change', addressInfo.address || ''); + }; + + // 处理对话框关闭 + const handleClose = () => { + if (poiPicker) { + poiPicker.clearSuggest(); + poiPicker.clearSearchResults(); + } + searchValue.value = ''; + addressInfo.address = ''; + addressInfo.longitude = null; + addressInfo.latitude = null; + clearMarker(); + }; + + // 处理输入框清空按钮点击 + const handleClear = () => { + if (editorItem && editorItem.id) { + const [longitudeName, latitudeName] = editorItem.id.split(','); + if (longitudeName) { + emit('change', null, longitudeName); + } + if (latitudeName) { + emit('change', null, latitudeName); + } + } + emit('change', ''); + }; + + // 处理搜索输入框清空按钮点击 + const handleSearchClear = () => { + searchValue.value = ''; + addressInfo.address = ''; + addressInfo.longitude = null; + addressInfo.latitude = null; + clearMarker(); + }; + + onUnmounted(() => { + map?.destroy(); + }); + + return { + ns, + c, + inputRef, + searchInputRef, + mapContainerRef, + searchResultContainerRef, + dialogVisible, + isLoading, + searchValue, + showFormDefaultContent, + handleShow, + handleConfirm, + handleClose, + handleClear, + handleSearchClear, + }; + }, + render() { + const icon = ( + + + + + ); + let content; + if (this.readonly) { + content = this.value; + } else { + content = [ + + {{ + suffix: () => { + if (!this.value || this.disabled) { + return; + } + return ( + { + e.stopPropagation(); + this.handleClear(); + }} + > + {icon} + + ); + }, + }} + , + + {{ + default: () => { + return ( +
+
+
+ + {this.searchValue ? ( + + + { + e.stopPropagation(); + this.handleSearchClear(); + }} + > + {icon} + + + + ) : null} +
+
+
+
+
+
+
+ ); + }, + footer: () => { + return ( +
+ + {ibiz.i18n.t('editor.common.confirm')} + +
+ ); + }, + }} +
, + ]; + } + + const formDefaultContent = ( +
+ {this.value ? this.value : ibiz.config.common.emptyText} +
+ ); + + return ( +
+ {this.showFormDefaultContent && formDefaultContent} + {content} +
+ ); + }, +}); diff --git a/src/editor/map-picker/index.ts b/src/editor/map-picker/index.ts new file mode 100644 index 00000000..d04930b0 --- /dev/null +++ b/src/editor/map-picker/index.ts @@ -0,0 +1,3 @@ +export { IBizMapPicker } from './ibiz-map-picker/ibiz-map-picker'; +export * from './map-picker-editor.controller'; +export * from './map-picker-editor.provider'; diff --git a/src/editor/map-picker/map-picker-editor.controller.ts b/src/editor/map-picker/map-picker-editor.controller.ts new file mode 100644 index 00000000..d8a97e1a --- /dev/null +++ b/src/editor/map-picker/map-picker-editor.controller.ts @@ -0,0 +1,10 @@ +import { EditorController } from '@ibiz-template/runtime'; +import { IMapPicker } from '@ibiz/model-core'; + +/** + * @description 地图选择器编辑器控制器 + * @export + * @class MapPickerEditorController + * @extends {EditorController} + */ +export class MapPickerEditorController extends EditorController {} diff --git a/src/editor/map-picker/map-picker-editor.provider.ts b/src/editor/map-picker/map-picker-editor.provider.ts new file mode 100644 index 00000000..bf841d1f --- /dev/null +++ b/src/editor/map-picker/map-picker-editor.provider.ts @@ -0,0 +1,27 @@ +import { + IEditorContainerController, + IEditorProvider, +} from '@ibiz-template/runtime'; +import { IMapPicker } from '@ibiz/model-core'; +import { MapPickerEditorController } from './map-picker-editor.controller'; + +/** + * @description 地图选择器编辑器适配器 + * @export + * @class MapPickerEditorProvider + * @implements {IEditorProvider} + */ +export class MapPickerEditorProvider implements IEditorProvider { + formEditor: string = 'IBizMapPicker'; + + gridEditor: string = 'IBizMapPicker'; + + async createController( + editorModel: IMapPicker, + parentController: IEditorContainerController, + ): Promise { + const c = new MapPickerEditorController(editorModel, parentController); + await c.init(); + return c; + } +} diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index c73f71f1..8e312bcf 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -724,6 +724,10 @@ export default { textValue: 'Text', add: 'Add', }, + mapPicker: { + title: 'Please select an address', + searchPlaceholder: 'Please enter a keyword to select a location', + }, }, panelComponent: { authUserinfo: { diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index 36a37527..5235d28d 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -675,6 +675,10 @@ export default { textValue: '文本值', add: '添加', }, + mapPicker: { + title: '请选择地址', + searchPlaceholder: '请输入关键字选择地点', + }, }, panelComponent: { authUserinfo: { -- Gitee