From 4c5699cb12817b1d72880dfae82b629790701a8d Mon Sep 17 00:00:00 2001 From: zeyan <258785420@qq.com> Date: Wed, 6 Aug 2025 18:15:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/components/TencentMapPicker.vue | 499 +++++++++++------- niucloud/app/model/sys/SysMenu.php | 4 +- .../app/service/admin/sys/MenuService.php | 40 +- .../app/service/admin/sys/SysMenuService.php | 46 +- 4 files changed, 386 insertions(+), 203 deletions(-) diff --git a/admin/src/components/TencentMapPicker.vue b/admin/src/components/TencentMapPicker.vue index 8272f0ad..e6d2a120 100644 --- a/admin/src/components/TencentMapPicker.vue +++ b/admin/src/components/TencentMapPicker.vue @@ -53,11 +53,16 @@ :value="item.id" /> - + {{ t('search') }} @@ -127,12 +132,140 @@ const provinceList = ref([]) const cityList = ref([]) const districtList = ref([]) +// ====================== 地图相关方法 ====================== +const cleanupMap = () => { + if (mapScript) { + try { + document.body.removeChild(mapScript) + } catch (e) { + console.warn('移除地图脚本失败:', e) + } + mapScript = null + } + + if (map) { + try { + map.destroy() + } catch (e) { + console.warn('销毁地图实例失败:', e) + } + map = null + } + + marker = null + + if (resizeTimer) { + clearTimeout(resizeTimer) + resizeTimer = null + } + + window.removeEventListener('resize', handleResize) +} + +const loadMapSDK = (): Promise => { + return new Promise((resolve, reject) => { + // 检查是否已经加载 + if ((window as any).TMap) { + resolve() + return + } + + mapKey.value = import.meta.env.VITE_MAP_KEY + mapScript = document.createElement('script') + mapScript.type = 'text/javascript' + mapScript.src = `https://map.qq.com/api/gljs?libraries=tools,service&v=1.exp&key=${mapKey.value}` + + mapScript.onload = () => { + if ((window as any).TMap) { + resolve() + } else { + reject(new Error('TMap未定义')) + } + } + + mapScript.onerror = (error) => { + reject(error) + } + + document.body.appendChild(mapScript) + }) +} + +const initMap = () => { + const container = document.getElementById('container') + if (!(window as any).TMap || !container) { + throw new Error('地图SDK未加载或容器不存在') + } + + const TMap = (window as any).TMap + let center + + // 优先使用传入的坐标 + if (props.modelValue.lat && props.modelValue.lng) { + center = new TMap.LatLng(props.modelValue.lat, props.modelValue.lng) + } else { + // 默认使用北京坐标 + center = new TMap.LatLng(39.90403, 116.407526) + } + + // 创建地图实例 + map = new TMap.Map('container', { + center, + zoom: 12, + }) + + // 创建标记 + marker = createMarker(map) + + // 初始化时设置标记位置 + if (props.modelValue.lat && props.modelValue.lng) { + marker.updateGeometries({ + id: 'center', + position: new TMap.LatLng(props.modelValue.lat, props.modelValue.lng), + }) + } + + // 地图点击事件 + map.on('click', (evt: any) => { + map.setCenter(evt.latLng) + marker.updateGeometries({ + id: 'center', + position: evt.latLng, + }) + emit('update:modelValue', { + lat: evt.latLng.lat, + lng: evt.latLng.lng, + address: detailAddress.value, + }) + }) +} + +const bindResizeListener = () => { + window.addEventListener('resize', handleResize) +} + +const handleResize = () => { + if (resizeTimer) clearTimeout(resizeTimer) + resizeTimer = setTimeout(() => { + if (map && typeof map.setSize === 'function') { + map.setSize() + } else if (map && typeof map.getSize === 'function') { + // 对于一些版本,可能需要使用不同的方法 + map.invalidateSize && map.invalidateSize() + } + }, 300) +} + +// ====================== 错误处理 ====================== +const handleError = (error: any) => { + console.error('地图组件错误:', error) + ElMessage.error('地图加载失败,请重试') +} + // ====================== 生命周期 ====================== onMounted(() => { // 预加载地图SDK(可选优化) - const preloadScript = document.createElement('script') - preloadScript.src = `https://map.qq.com/api/gljs?key= ${mapKey.value}` - document.head.appendChild(preloadScript) + mapKey.value = import.meta.env.VITE_MAP_KEY || 'YOUR_API_KEY' }) // ====================== 监听器 ====================== @@ -211,190 +344,137 @@ const handleConfirm = () => { dialogVisible.value = false } -// 地址解析逻辑 +// 地址解析逻辑(增强版) const parseAndSetAddress = async (address: string) => { - // 解析地址 - const { provinceName, cityName, districtName, detail } = parseAddress(address) - detailAddress.value = detail - - // 1. 处理省份选择 - if (provinceName) { - const provinceItem = provinceList.value.find((p) => - p.name.includes(provinceName) - ) - if (provinceItem) { - province.value = provinceItem.id - - // 2. 处理城市选择 - const cityRes = await getAreaListByPid(province.value) - cityList.value = cityRes.data - - if (cityName) { - const cityItem = cityList.value.find((c) => c.name.includes(cityName)) - if (cityItem) { - city.value = cityItem.id - - // 3. 处理区县选择 - const districtRes = await getAreaListByPid(city.value) - districtList.value = districtRes.data - - if (districtName) { - const districtItem = districtList.value.find((d) => - d.name.includes(districtName) - ) - if (districtItem) { - district.value = districtItem.id + if (!address) return + + try { + // 解析地址 + const { provinceName, cityName, districtName, detail } = parseAddress(address) + detailAddress.value = detail + + // 1. 处理省级选择 + if (provinceName) { + const provinceItem = provinceList.value.find((p) => { + return p.name.includes(provinceName.replace(/[省市区]/g, '')) || + p.name === provinceName + }) + + if (provinceItem) { + province.value = provinceItem.id + + // 2. 处理地级市选择 + const cityRes = await getAreaListByPid(province.value) + cityList.value = cityRes.data + + if (cityName) { + const cityItem = cityList.value.find((c) => { + return c.name.includes(cityName.replace(/[市盟州]/g, '')) || + c.name === cityName + }) + + if (cityItem) { + city.value = cityItem.id + + // 3. 处理区县级选择 + const districtRes = await getAreaListByPid(city.value) + districtList.value = districtRes.data + + if (districtName) { + const districtItem = districtList.value.find((d) => { + return d.name.includes(districtName.replace(/[区县旗市]/g, '')) || + d.name === districtName + }) + + if (districtItem) { + district.value = districtItem.id + } + } + } + } else if (provinceName.includes('市')) { + // 如果省名包含"市",可能是直辖市,直接在市级查找 + const directCity = cityList.value.find((c) => c.name === provinceName) + if (directCity) { + city.value = directCity.id + + const districtRes = await getAreaListByPid(city.value) + districtList.value = districtRes.data + + if (districtName) { + const districtItem = districtList.value.find((d) => { + return d.name.includes(districtName.replace(/[区县旗市]/g, '')) || + d.name === districtName + }) + + if (districtItem) { + district.value = districtItem.id + } } } } } } + + // 等待下一个事件循环,然后触发地址搜索 + await nextTick() + if (province.value && city.value) { + handleAddressSearch() + } + } catch (error) { + console.warn('地址解析失败:', error) } - - // 触发地址搜索 - handleAddressSearch() } -// 地址拆分函数 +// 地址拆分函数(增强版) const parseAddress = (address: string) => { + if (!address) return { provinceName: '', cityName: '', districtName: '', detail: '' } + let provinceName = '' let cityName = '' let districtName = '' - let detail = address - - // 提取省/直辖市/自治区 - const provinceMatch = address.match(/^([^省市自治区]+[省市区]|[^市]+市)/) - if (provinceMatch) { - provinceName = provinceMatch[0] - detail = address.slice(provinceName.length) + let detail = address.trim() + + // 提取省级行政区(省/直辖市/自治区/特别行政区) + const provincePatterns = [ + /^([^省市自治区特别行政人民共和]+[省自治区])/, // XX省/XX自治区 + /^(北京市|上海市|天津市|重庆市)/, // 直辖市 + /^([^市]+特别行政区)/, // 特别行政区 + ] + + for (const pattern of provincePatterns) { + const match = detail.match(pattern) + if (match) { + provinceName = match[1] + detail = detail.slice(provinceName.length) + break + } } - // 提取市 - const cityMatch = detail.match(/^([^市区]+市)/) - if (cityMatch) { - cityName = cityMatch[0] - detail = detail.slice(cityName.length) + // 提取地级市(除非是直辖市) + if (!provinceName.includes('市')) { + const cityMatch = detail.match(/^([^市区县盟州]+[市盟州])/) + if (cityMatch) { + cityName = cityMatch[1] + detail = detail.slice(cityName.length) + } } - // 提取区/县 - const districtMatch = detail.match(/^([^区县]+[区县])/) + // 提取区县级 + const districtMatch = detail.match(/^([^区县旗市]+[区县旗市])/) if (districtMatch) { - districtName = districtMatch[0] + districtName = districtMatch[1] detail = detail.slice(districtName.length) } return { - provinceName, - cityName, - districtName, - detail: detail.trim() || '', + provinceName: provinceName.trim(), + cityName: cityName.trim(), + districtName: districtName.trim(), + detail: detail.trim(), } } -// ====================== 地图相关方法 ====================== -const cleanupMap = () => { - if (mapScript) { - document.body.removeChild(mapScript) - mapScript = null - } - - if (map) { - map.destroy() - map = null - } - - marker = null - - if (resizeTimer) { - clearTimeout(resizeTimer) - resizeTimer = null - } - - window.removeEventListener('resize', handleResize) -} - -const loadMapSDK = (): Promise => { - return new Promise((resolve, reject) => { - mapKey.value = import.meta.env.VITE_MAP_KEY - mapScript = document.createElement('script') - mapScript.type = 'text/javascript' - mapScript.src = `https://map.qq.com/api/gljs?libraries=tools ,service&v=1.exp&key=${mapKey.value}` - - mapScript.onload = () => { - if ((window as any).TMap) { - resolve() - } else { - reject(new Error('TMap未定义')) - } - } - - mapScript.onerror = (error) => { - reject(error) - } - - document.body.appendChild(mapScript) - }) -} - -const initMap = () => { - const container = document.getElementById('container') - if (!(window as any).TMap || !container) { - throw new Error('地图SDK未加载或容器不存在') - } - - const TMap = (window as any).TMap - let center - - // 优先使用传入的坐标 - if (props.modelValue.lat && props.modelValue.lng) { - center = new TMap.LatLng(props.modelValue.lat, props.modelValue.lng) - } else { - // 默认使用北京坐标 - center = new TMap.LatLng(39.90403, 116.407526) - } - - // 创建地图实例 - map = new TMap.Map('container', { - center, - zoom: 12, - }) - - // 创建标记 - marker = createMarker(map) - - // 初始化时设置标记位置 - if (props.modelValue.lat && props.modelValue.lng) { - marker.updateGeometries({ - id: 'center', - position: new TMap.LatLng(props.modelValue.lat, props.modelValue.lng), - }) - } - // 地图点击事件 - map.on('click', (evt: any) => { - map.setCenter(evt.latLng) - marker.updateGeometries({ - id: 'center', - position: evt.latLng, - }) - emit('update:modelValue', { - lat: evt.latLng.lat, - lng: evt.latLng.lng, - address: detailAddress.value, - }) - }) -} - -const bindResizeListener = () => { - window.addEventListener('resize', handleResize) -} - -const handleResize = () => { - if (resizeTimer) clearTimeout(resizeTimer) - resizeTimer = setTimeout(() => { - map?.setSize() - }, 300) -} // ====================== 区域选择处理 ====================== const handleProvinceChange = async (val: string) => { @@ -419,32 +499,54 @@ const handleCityChange = async (val: string) => { } const handleDistrictChange = (val: string) => { - // TODO: 处理区县选择 + // 区县选择完成,可以自动触发搜索 + if (val && detailAddress.value.trim()) { + handleAddressSearch() + } +} + +// 地址输入框失焦处理 +const handleAddressBlur = () => { + // 如果已选择省市且输入了详细地址,自动搜索 + if (province.value && city.value && detailAddress.value.trim()) { + handleAddressSearch() + } } // 地址搜索 const handleAddressSearch = async () => { try { + // 检查必要参数 + if (!province.value || !city.value) { + ElMessage.warning('请选择省市') + return + } + const address = [ - province.value - ? provinceList.value.find((p) => p.id === province.value)?.name - : '', - city.value ? cityList.value.find((c) => c.id === city.value)?.name : '', + provinceList.value.find((p) => p.id === province.value)?.name || '', + cityList.value.find((c) => c.id === city.value)?.name || '', district.value - ? districtList.value.find((d) => d.id === district.value)?.name + ? districtList.value.find((d) => d.id === district.value)?.name || '' : '', - detailAddress.value, - ].join('') + detailAddress.value.trim(), + ].filter(Boolean).join('') + + if (!address.trim()) { + ElMessage.warning('请输入地址') + return + } - const { message, result } = await addressToLatLng({ + const result = await addressToLatLng({ mapKey: mapKey.value, address, }) - if (message == 'Success' || message == 'query ok') { + // 处理不同的响应状态 + if (result.status === 0 && result.result) { + // 成功获取到坐标 const latLng = new (window as any).TMap.LatLng( - result.location.lat, - result.location.lng + result.result.location.lat, + result.result.location.lng ) map.setCenter(latLng) marker.updateGeometries({ @@ -452,22 +554,47 @@ const handleAddressSearch = async () => { position: latLng, }) emit('update:modelValue', { - lat: result.location.lat, - lng: result.location.lng, - address: detailAddress.value, + lat: result.result.location.lat, + lng: result.result.location.lng, + address: address, // 使用完整地址 }) + ElMessage.success('地址搜索成功') + } else if (result.status === 112) { + // IP授权问题的特殊处理 + ElMessage({ + type: 'warning', + message: '地址搜索暂不可用(API授权限制),请直接在地图上点击选择位置', + duration: 5000 + }) + console.warn('腾讯地图API IP授权限制:', result.message) + + // 仍然更新地址信息,但不移动地图位置 + emit('update:modelValue', { + lat: props.modelValue.lat || null, + lng: props.modelValue.lng || null, + address: address, // 保存用户输入的地址 + }) + } else { + // 其他错误 + ElMessage.warning(`搜索失败:${result.message || '未找到对应地址,请检查输入'}`) + } + } catch (error: any) { + console.error('地址搜索异常:', error) + + // 检查是否是IP授权问题 + if (error.status === 112 || (error.message && error.message.includes('IP未被授权'))) { + ElMessage({ + type: 'info', + message: '地址搜索暂不可用,请直接在地图上点击选择位置', + duration: 5000 + }) + } else { + ElMessage.error('搜索失败,请重试') } - } catch (error) { - handleError(error) } } -// ====================== 错误处理 ====================== -const handleError = (error: any) => { - console.error('地图组件错误:', error) - ElMessage.error(t('mapLoadFailed')) - dialogVisible.value = false -} + // 回显处理 watch( diff --git a/niucloud/app/model/sys/SysMenu.php b/niucloud/app/model/sys/SysMenu.php index 147b9177..88e41d89 100644 --- a/niucloud/app/model/sys/SysMenu.php +++ b/niucloud/app/model/sys/SysMenu.php @@ -35,13 +35,13 @@ class SysMenu extends BaseModel * 模型名称 * @var string */ - protected $name = 'sys_menus'; + protected $name = 'sys_menu'; /** * 表名(不使用前缀) * @var string */ - protected $table = 'sys_menus'; + protected $table = 'school_sys_menu'; /** * 追加字段 * @var array diff --git a/niucloud/app/service/admin/sys/MenuService.php b/niucloud/app/service/admin/sys/MenuService.php index 68e1f082..2b701ebb 100644 --- a/niucloud/app/service/admin/sys/MenuService.php +++ b/niucloud/app/service/admin/sys/MenuService.php @@ -98,21 +98,47 @@ class MenuService extends BaseAdminService } /** - * 菜单删除 + * 菜单删除(递归删除子菜单) * @param string $menu_key * @return bool * @throws DbException */ public function del(string $menu_key) { - //查询是否有下级菜单或按钮 - $menu = $this->find($menu_key); - if (( new SysMenu() )->where([ [ 'parent_key', '=', $menu_key ] ])->count() > 0) - throw new AdminException('MENU_NOT_ALLOW_DELETE'); + // 直接查询菜单是否存在,不依赖缓存 + $menu = (new SysMenu())->where('menu_key', $menu_key)->find(); + if (!$menu) { + throw new AdminException('菜单不存在'); + } - $res = $menu->delete(); + // 递归删除菜单及其子菜单 + $this->deleteMenuRecursive($menu_key); + + // 清除缓存 Cache::tag(self::$cache_tag_name)->clear(); - return $res; + return true; + } + + /** + * 递归删除菜单及其子菜单 + * @param string $menu_key + * @return void + */ + private function deleteMenuRecursive(string $menu_key) + { + // 查找所有子菜单 + $childMenus = (new SysMenu())->where('parent_key', $menu_key)->select(); + + // 递归删除所有子菜单 + foreach ($childMenus as $childMenu) { + $this->deleteMenuRecursive($childMenu->menu_key); + } + + // 删除当前菜单 + $currentMenu = (new SysMenu())->where('menu_key', $menu_key)->find(); + if ($currentMenu) { + $currentMenu->delete(); + } } /** diff --git a/niucloud/app/service/admin/sys/SysMenuService.php b/niucloud/app/service/admin/sys/SysMenuService.php index f103af56..876f7bb5 100644 --- a/niucloud/app/service/admin/sys/SysMenuService.php +++ b/niucloud/app/service/admin/sys/SysMenuService.php @@ -110,7 +110,7 @@ class SysMenuService extends BaseAdminService } /** - * 删除菜单 + * 删除菜单(递归删除子菜单) * @param int $id * @return bool */ @@ -121,15 +121,45 @@ class SysMenuService extends BaseAdminService throw new AdminException('菜单不存在'); } - // 检查是否有角色在使用此菜单 - $result = Db::query("SELECT COUNT(*) as count FROM role_menu_permissions WHERE menu_id = ?", [$id]); - $roleMenuCount = $result[0]['count'] ?? 0; - if ($roleMenuCount > 0) { - throw new AdminException('该菜单已被角色使用,无法删除'); + Db::startTrans(); + try { + // 递归删除所有子菜单 + $this->deleteMenuRecursive($model->menu_key); + + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + throw new AdminException('删除菜单失败:' . $e->getMessage()); + } + } + + /** + * 递归删除菜单及其子菜单 + * @param string $menuKey + * @return void + */ + private function deleteMenuRecursive(string $menuKey) + { + // 获取当前菜单信息 + $currentMenu = $this->model->where('menu_key', $menuKey)->find(); + if (!$currentMenu) { + return; + } + + // 查找所有子菜单 + $childMenus = $this->model->where('parent_key', $menuKey)->select(); + + // 递归删除所有子菜单 + foreach ($childMenus as $childMenu) { + $this->deleteMenuRecursive($childMenu->menu_key); } - $res = $model->delete(); - return $res !== false; + // 删除角色菜单权限关联 + Db::execute("DELETE FROM role_menu_permissions WHERE menu_id = ?", [$currentMenu->id]); + + // 删除当前菜单 + $currentMenu->delete(); } /**