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();
}
/**