@@ -89,6 +89,15 @@ export default {
type: String,
default: 'airspace' // 'airspace' or 'ranging'
},
+ scaleConfig: {
+ type: Object,
+ default: () => ({
+ scaleNumerator: 1,
+ scaleDenominator: 1000,
+ unit: 'km',
+ position: 'bottom-right'
+ })
+ }
},
watch: {
drawDomClick: {
@@ -99,6 +108,14 @@ export default {
// this.initMap()
}
}
+ },
+ scaleConfig: {
+ deep: true,
+ handler(newVal) {
+ if (newVal) {
+ this.updateScaleFromConfig(newVal)
+ }
+ }
}
},
data() {
@@ -149,6 +166,10 @@ export default {
// 比例尺(高德风格)
scaleBarText: '--',
scaleBarWidthPx: 80,
+ useCustomScale: false,
+ customScaleText: '',
+ currentScaleUnit: 'm',
+ isApplyingScale: false,
// 定位相关
locateDialogVisible: false
}
@@ -174,6 +195,102 @@ export default {
this.destroyViewer()
},
methods: {
+ updateScaleFromConfig(config) {
+ if (!config || !this.viewer) return
+
+ const { scaleNumerator, scaleDenominator, unit } = config
+ if (!scaleDenominator || scaleDenominator <= 0) return
+
+ let displayValue = ''
+ let metersPerPixel = 0
+
+ const standardDPI = 96
+ const pixelsPerCm = standardDPI * 0.393701
+
+ const scaleFactor = scaleDenominator / scaleNumerator
+
+ switch (unit) {
+ case 'm':
+ displayValue = `1:${scaleDenominator}`
+ metersPerPixel = scaleFactor / pixelsPerCm
+ break
+ case 'km':
+ displayValue = `1:${scaleDenominator}`
+ metersPerPixel = (scaleFactor * 1000) / pixelsPerCm
+ break
+ default:
+ displayValue = `1:${scaleDenominator}`
+ metersPerPixel = scaleFactor / pixelsPerCm
+ }
+
+ console.log('比例尺设置:', displayValue, '每像素米数:', metersPerPixel.toFixed(4))
+
+ this.isApplyingScale = true
+ this.useCustomScale = true
+ this.customScaleText = displayValue
+ this.currentScaleUnit = unit
+
+ this.applyScaleToCamera(metersPerPixel)
+ this.updateScaleBar()
+ this.$forceUpdate()
+
+ setTimeout(() => {
+ this.isApplyingScale = false
+ }, 1000)
+ },
+
+ applyScaleToCamera(metersPerPixel) {
+ if (!this.viewer || !this.viewer.camera) return
+
+ const canvas = this.viewer.scene.canvas
+ const width = canvas.clientWidth
+ const height = canvas.clientHeight
+
+ if (width <= 0 || height <= 0) return
+
+ const camera = this.viewer.camera
+ const scene = this.viewer.scene
+
+ if (scene.mode === Cesium.SceneMode.SCENE2D) {
+ const frustumWidth = width * metersPerPixel
+ camera.frustum.right = frustumWidth / 2
+ camera.frustum.left = -frustumWidth / 2
+ const frustumHeight = height * metersPerPixel
+ camera.frustum.top = frustumHeight / 2
+ camera.frustum.bottom = -frustumHeight / 2
+ } else {
+ const cameraPosition = camera.positionCartographic
+ const groundWidth = width * metersPerPixel
+
+ const fov = camera.frustum.fov || Cesium.Math.PI_OVER_THREE
+ const aspectRatio = width / height
+ const verticalFov = fov / aspectRatio
+
+ const targetHeight = (groundWidth / 2) / Math.tan(fov / 2)
+
+ const destination = Cesium.Cartesian3.fromRadians(
+ cameraPosition.longitude,
+ cameraPosition.latitude,
+ targetHeight
+ )
+
+ camera.flyTo({
+ destination: destination,
+ duration: 0.5,
+ orientation: {
+ heading: camera.heading,
+ pitch: camera.pitch,
+ roll: camera.roll
+ }
+ })
+ }
+ },
+ handleScaleClick() {
+ this.$emit('scale-click', {
+ useCustomScale: this.useCustomScale,
+ customScaleText: this.customScaleText
+ })
+ },
preventContextMenu(e) {
e.preventDefault();
},
@@ -2766,6 +2883,10 @@ export default {
initScaleBar() {
const that = this
const update = () => {
+ if (!that.isApplyingScale) {
+ that.useCustomScale = false
+ that.customScaleText = ''
+ }
that.updateScaleBar()
}
update()
@@ -2806,10 +2927,12 @@ export default {
if (leftCartesian && rightCartesian) {
const rawMeters = Cesium.Cartesian3.distance(leftCartesian, rightCartesian)
if (rawMeters > 0) {
- const niceMeters = this.niceScaleValue(rawMeters)
- const widthPx = Math.round((niceMeters / rawMeters) * barPx)
- const text = niceMeters >= 1000 ? `${(niceMeters / 1000).toFixed(0)} 公里` : `${Math.round(niceMeters)} 米`
- return { text, widthPx, niceMeters }
+ const metersPerPx = rawMeters / barPx
+ const scaleRatio = this.calculateScaleRatio(metersPerPx)
+ const pixelsPerCm = this.getPixelsPerCm()
+ const widthPx = Math.min(120, Math.max(40, Math.round(pixelsPerCm)))
+ const text = `1:${scaleRatio}${this.currentScaleUnit}`
+ return { text, widthPx, niceMeters: rawMeters }
}
}
// 2D/WebMercator 备用:用整屏宽度对应的地理范围计算(四角 pick 得到视口矩形)
@@ -2819,11 +2942,11 @@ export default {
const widthMeters = Cesium.Cartesian3.distance(leftBottom, rightBottom)
if (widthMeters > 0) {
const metersPerPx = widthMeters / w
- const rawMeters = metersPerPx * barPx
- const niceMeters = this.niceScaleValue(rawMeters)
- const widthPx = Math.round(niceMeters / metersPerPx)
- const text = niceMeters >= 1000 ? `${(niceMeters / 1000).toFixed(0)} 公里` : `${Math.round(niceMeters)} 米`
- return { text, widthPx: Math.min(120, Math.max(40, widthPx)), niceMeters }
+ const scaleRatio = this.calculateScaleRatio(metersPerPx)
+ const pixelsPerCm = this.getPixelsPerCm()
+ const widthPx = Math.min(120, Math.max(40, Math.round(pixelsPerCm)))
+ const text = `1:${scaleRatio}${this.currentScaleUnit}`
+ return { text, widthPx, niceMeters: widthMeters }
}
}
// 最后备用:从 2D 相机视锥估算(正交宽度 -> 米)
@@ -2831,15 +2954,34 @@ export default {
const frustumWidth = camera.frustum.right - camera.frustum.left
if (frustumWidth > 0) {
const metersPerPx = frustumWidth / w
- const rawMeters = metersPerPx * barPx
- const niceMeters = this.niceScaleValue(rawMeters)
- const widthPx = Math.round(niceMeters / metersPerPx)
- const text = niceMeters >= 1000 ? `${(niceMeters / 1000).toFixed(0)} 公里` : `${Math.round(niceMeters)} 米`
- return { text, widthPx: Math.min(120, Math.max(40, widthPx)), niceMeters }
+ const scaleRatio = this.calculateScaleRatio(metersPerPx)
+ const pixelsPerCm = this.getPixelsPerCm()
+ const widthPx = Math.min(120, Math.max(40, Math.round(pixelsPerCm)))
+ const text = `1:${scaleRatio}${this.currentScaleUnit}`
+ return { text, widthPx, niceMeters: frustumWidth }
}
}
return null
},
+ /** 计算比例尺比例 */
+ calculateScaleRatio(metersPerPx) {
+ const standardDPI = 96
+ const pixelsPerCm = standardDPI * 0.393701
+ const metersPerCm = metersPerPx * pixelsPerCm
+ let scaleRatio = 0
+ if (this.currentScaleUnit === 'km') {
+ scaleRatio = Math.round(metersPerCm / 1000)
+ } else {
+ scaleRatio = Math.round(metersPerCm)
+ }
+ return scaleRatio
+ },
+ /** 获取1厘米对应的像素数 */
+ getPixelsPerCm() {
+ const standardDPI = 96
+ const pixelsPerCm = standardDPI * 0.393701
+ return pixelsPerCm
+ },
/** 将实际距离圆整为易读的刻度值(米) */
niceScaleValue(meters) {
const candidates = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000]
@@ -2850,13 +2992,19 @@ export default {
return best
},
updateScaleBar() {
- const info = this.getScaleBarInfo()
- if (info) {
- this.scaleBarText = info.text
- this.scaleBarWidthPx = Math.min(120, Math.max(40, info.widthPx))
+ if (this.useCustomScale && this.customScaleText) {
+ this.scaleBarText = `${this.customScaleText}${this.currentScaleUnit}`
+ const pixelsPerCm = this.getPixelsPerCm()
+ this.scaleBarWidthPx = Math.min(120, Math.max(40, Math.round(pixelsPerCm)))
} else {
- this.scaleBarText = '--'
- this.scaleBarWidthPx = 80
+ const info = this.getScaleBarInfo()
+ if (info) {
+ this.scaleBarText = info.text
+ this.scaleBarWidthPx = Math.min(120, Math.max(40, info.widthPx))
+ } else {
+ this.scaleBarText = '--'
+ this.scaleBarWidthPx = 80
+ }
}
this.$nextTick()
},
diff --git a/ruoyi-ui/src/views/childRoom/TopHeader.vue b/ruoyi-ui/src/views/childRoom/TopHeader.vue
index b3e3b34..2d21f3e 100644
--- a/ruoyi-ui/src/views/childRoom/TopHeader.vue
+++ b/ruoyi-ui/src/views/childRoom/TopHeader.vue
@@ -319,6 +319,14 @@ export default {
isIconEditMode: {
type: Boolean,
default: false
+ },
+ currentScaleConfig: {
+ type: Object,
+ default: () => ({
+ scaleNumerator: 1,
+ scaleDenominator: 1000,
+ unit: 'm'
+ })
}
},
data() {
@@ -351,6 +359,16 @@ export default {
]
}
},
+ watch: {
+ currentScaleConfig: {
+ deep: true,
+ handler(newVal) {
+ if (newVal) {
+ this.currentScale = { ...newVal }
+ }
+ }
+ }
+ },
methods: {
selectTopNav(item) {
this.$emit('select-nav', item)
diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue
index b3956c1..331e55b 100644
--- a/ruoyi-ui/src/views/childRoom/index.vue
+++ b/ruoyi-ui/src/views/childRoom/index.vue
@@ -6,8 +6,10 @@
+ @open-waypoint-dialog="handleOpenWaypointEdit"
+ @scale-click="handleScaleClick" />
二维GIS地图区域
@@ -44,6 +46,7 @@
:astro-time="astroTime"
:user-avatar="userAvatar"
:is-icon-edit-mode="isIconEditMode"
+ :current-scale-config="scaleConfig"
@select-nav="selectTopNav"
@save-plan="savePlan"
@import-plan-file="importPlanFile"
@@ -238,6 +241,7 @@
v-model="showScaleDialog"
:scale="currentScale"
@save="saveScale"
+ @unit-change="handleScaleUnitChange"
/>
@@ -335,7 +339,16 @@ export default {
showPowerZoneDialog: false,
currentPowerZone: {},
showScaleDialog: false,
- currentScale: {},
+ currentScale: {
+ scaleNumerator: 1,
+ scaleDenominator: 1000,
+ unit: 'm'
+ },
+ scaleConfig: {
+ scaleNumerator: 1,
+ scaleDenominator: 1000,
+ unit: 'm'
+ },
showExternalParamsDialog: false,
currentExternalParams: {},
showPageLayoutDialog: false,
@@ -1111,10 +1124,36 @@ export default {
saveScale(scale) {
console.log('保存比例尺:', scale)
+ this.scaleConfig = {
+ scaleNumerator: scale.scaleNumerator,
+ scaleDenominator: scale.scaleDenominator,
+ unit: scale.unit
+ }
+
+ if (this.$refs.cesiumMap) {
+ this.$refs.cesiumMap.updateScaleFromConfig(this.scaleConfig)
+ }
+
const scaleText = `${scale.scaleNumerator}:${scale.scaleDenominator}`
this.$message.success(`比例尺 "${scaleText}" 保存成功`);
},
+ handleScaleClick(scaleInfo) {
+ this.currentScale = {
+ scaleNumerator: this.scaleConfig.scaleNumerator,
+ scaleDenominator: this.scaleConfig.scaleDenominator,
+ unit: this.scaleConfig.unit
+ }
+ this.showScaleDialog = true
+ },
+
+ handleScaleUnitChange(unit) {
+ this.scaleConfig.unit = unit
+ if (this.$refs.cesiumMap) {
+ this.$refs.cesiumMap.currentScaleUnit = unit
+ }
+ },
+
// 地图下拉菜单方法
loadTerrain() {
this.$message.success('加载/切换地形');
diff --git a/ruoyi-ui/src/views/dialogs/ScaleDialog.vue b/ruoyi-ui/src/views/dialogs/ScaleDialog.vue
index 4015c8e..c4d64fb 100644
--- a/ruoyi-ui/src/views/dialogs/ScaleDialog.vue
+++ b/ruoyi-ui/src/views/dialogs/ScaleDialog.vue
@@ -30,17 +30,6 @@
-
-
-
-
-
-
-
-
-
-
-
@@ -72,8 +61,7 @@ export default {
formData: {
scaleNumerator: 1,
scaleDenominator: 1000,
- unit: 'km',
- position: 'bottom-right'
+ unit: 'm'
},
rules: {
scaleNumerator: [
@@ -84,9 +72,6 @@ export default {
],
unit: [
{ required: true, message: '请选择单位', trigger: 'change' }
- ],
- position: [
- { required: true, message: '请选择显示位置', trigger: 'change' }
]
}
};
@@ -101,6 +86,11 @@ export default {
if (this.value && newVal) {
this.initFormData();
}
+ },
+ 'formData.unit'(newVal) {
+ if (this.value) {
+ this.$emit('unit-change', newVal);
+ }
}
},
methods: {
@@ -108,8 +98,7 @@ export default {
this.formData = {
scaleNumerator: this.scale.scaleNumerator || 1,
scaleDenominator: this.scale.scaleDenominator || 1000,
- unit: this.scale.unit || 'km',
- position: this.scale.position || 'bottom-right'
+ unit: this.scale.unit || 'm'
};
},
closeDialog() {
@@ -151,7 +140,7 @@ export default {
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
width: 90%;
- max-width: 500px;
+ max-width: 400px;
max-height: 90vh;
overflow-y: auto;
animation: dialog-fade-in 0.3s ease;
@@ -173,7 +162,7 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
- padding: 16px 20px;
+ padding: 12px 16px;
border-bottom: 1px solid #e8e8e8;
}
@@ -196,7 +185,17 @@ export default {
}
.dialog-body {
- padding: 20px;
+ padding: 16px;
+}
+
+.quick-scale-buttons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.quick-scale-buttons .el-button {
+ margin: 0;
}
.scale-inputs {
From 4e14d47edc387c506bf0a8cc9fcb9d7e262c5c3c Mon Sep 17 00:00:00 2001
From: sd <1504629600@qq.com>
Date: Thu, 5 Feb 2026 15:29:30 +0800
Subject: [PATCH 2/3] =?UTF-8?q?=E6=AF=94=E4=BE=8B=E5=B0=BA=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ruoyi-ui/src/views/cesiumMap/index.vue | 38 ++++++++++++++++++------------
ruoyi-ui/src/views/dialogs/ScaleDialog.vue | 8 +++----
2 files changed, 27 insertions(+), 19 deletions(-)
diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue
index c5f5a32..fd6a625 100644
--- a/ruoyi-ui/src/views/cesiumMap/index.vue
+++ b/ruoyi-ui/src/views/cesiumMap/index.vue
@@ -2928,11 +2928,10 @@ export default {
const rawMeters = Cesium.Cartesian3.distance(leftCartesian, rightCartesian)
if (rawMeters > 0) {
const metersPerPx = rawMeters / barPx
- const scaleRatio = this.calculateScaleRatio(metersPerPx)
- const pixelsPerCm = this.getPixelsPerCm()
- const widthPx = Math.min(120, Math.max(40, Math.round(pixelsPerCm)))
- const text = `1:${scaleRatio}${this.currentScaleUnit}`
- return { text, widthPx, niceMeters: rawMeters }
+ const niceMeters = this.niceScaleValue(rawMeters)
+ const widthPx = Math.round((niceMeters / rawMeters) * barPx)
+ const text = this.formatScaleText(niceMeters)
+ return { text, widthPx, niceMeters }
}
}
// 2D/WebMercator 备用:用整屏宽度对应的地理范围计算(四角 pick 得到视口矩形)
@@ -2942,11 +2941,11 @@ export default {
const widthMeters = Cesium.Cartesian3.distance(leftBottom, rightBottom)
if (widthMeters > 0) {
const metersPerPx = widthMeters / w
- const scaleRatio = this.calculateScaleRatio(metersPerPx)
- const pixelsPerCm = this.getPixelsPerCm()
- const widthPx = Math.min(120, Math.max(40, Math.round(pixelsPerCm)))
- const text = `1:${scaleRatio}${this.currentScaleUnit}`
- return { text, widthPx, niceMeters: widthMeters }
+ const rawMeters = metersPerPx * barPx
+ const niceMeters = this.niceScaleValue(rawMeters)
+ const widthPx = Math.round(niceMeters / metersPerPx)
+ const text = this.formatScaleText(niceMeters)
+ return { text, widthPx, niceMeters }
}
}
// 最后备用:从 2D 相机视锥估算(正交宽度 -> 米)
@@ -2954,11 +2953,11 @@ export default {
const frustumWidth = camera.frustum.right - camera.frustum.left
if (frustumWidth > 0) {
const metersPerPx = frustumWidth / w
- const scaleRatio = this.calculateScaleRatio(metersPerPx)
- const pixelsPerCm = this.getPixelsPerCm()
- const widthPx = Math.min(120, Math.max(40, Math.round(pixelsPerCm)))
- const text = `1:${scaleRatio}${this.currentScaleUnit}`
- return { text, widthPx, niceMeters: frustumWidth }
+ const rawMeters = metersPerPx * barPx
+ const niceMeters = this.niceScaleValue(rawMeters)
+ const widthPx = Math.round(niceMeters / metersPerPx)
+ const text = this.formatScaleText(niceMeters)
+ return { text, widthPx, niceMeters }
}
}
return null
@@ -2991,6 +2990,15 @@ export default {
}
return best
},
+ /** 格式化比例尺文本 */
+ formatScaleText(meters) {
+ if (meters >= 1000) {
+ const kmValue = (meters / 1000).toFixed(0)
+ return `${kmValue}km`
+ } else {
+ return `${meters}m`
+ }
+ },
updateScaleBar() {
if (this.useCustomScale && this.customScaleText) {
this.scaleBarText = `${this.customScaleText}${this.currentScaleUnit}`
diff --git a/ruoyi-ui/src/views/dialogs/ScaleDialog.vue b/ruoyi-ui/src/views/dialogs/ScaleDialog.vue
index c4d64fb..b6650ba 100644
--- a/ruoyi-ui/src/views/dialogs/ScaleDialog.vue
+++ b/ruoyi-ui/src/views/dialogs/ScaleDialog.vue
@@ -60,8 +60,8 @@ export default {
return {
formData: {
scaleNumerator: 1,
- scaleDenominator: 1000,
- unit: 'm'
+ scaleDenominator: 20,
+ unit: 'km'
},
rules: {
scaleNumerator: [
@@ -97,8 +97,8 @@ export default {
initFormData() {
this.formData = {
scaleNumerator: this.scale.scaleNumerator || 1,
- scaleDenominator: this.scale.scaleDenominator || 1000,
- unit: this.scale.unit || 'm'
+ scaleDenominator: this.scale.scaleDenominator || 20,
+ unit: this.scale.unit || 'km'
};
},
closeDialog() {
From b3c4d7884a55d2135754976bc9cbbe532e721739 Mon Sep 17 00:00:00 2001
From: ctw <1051735452@qq.com>
Date: Thu, 5 Feb 2026 15:48:14 +0800
Subject: [PATCH 3/3] =?UTF-8?q?=E5=B7=A6=E4=BE=A7=E8=8F=9C=E5=8D=95?=
=?UTF-8?q?=E6=A0=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../system/SysUserMenuConfigController.java | 52 ++++++++++++++++++
.../com/ruoyi/system/domain/SysUserMenuConfig.java | 63 ++++++++++++++++++++++
.../system/mapper/SysUserMenuConfigMapper.java | 35 ++++++++++++
.../system/service/ISysUserMenuConfigService.java | 30 +++++++++++
.../service/impl/SysUserMenuConfigServiceImpl.java | 55 +++++++++++++++++++
.../mapper/system/SysUserMenuConfigMapper.xml | 58 ++++++++++++++++++++
ruoyi-ui/src/api/system/userMenuConfig.js | 23 ++++++++
7 files changed, 316 insertions(+)
create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserMenuConfigController.java
create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserMenuConfig.java
create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMenuConfigMapper.java
create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserMenuConfigService.java
create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserMenuConfigServiceImpl.java
create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysUserMenuConfigMapper.xml
create mode 100644 ruoyi-ui/src/api/system/userMenuConfig.js
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserMenuConfigController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserMenuConfigController.java
new file mode 100644
index 0000000..eec291e
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserMenuConfigController.java
@@ -0,0 +1,52 @@
+package com.ruoyi.web.controller.system;
+
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.system.domain.SysUserMenuConfig;
+import com.ruoyi.system.service.ISysUserMenuConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 用户左侧菜单配置(当前登录用户自己的配置)
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/user/menuConfig")
+public class SysUserMenuConfigController extends BaseController {
+
+ @Autowired
+ private ISysUserMenuConfigService menuConfigService;
+
+ /**
+ * 获取当前用户的左侧菜单配置
+ */
+ @GetMapping
+ public AjaxResult getMyConfig() {
+ Long userId = getUserId();
+ SysUserMenuConfig config = menuConfigService.selectByUserId(userId);
+ if (config == null) {
+ return success(null);
+ }
+ return success(config);
+ }
+
+ /**
+ * 保存当前用户的左侧菜单配置
+ * 请求体: { "menuItems": "[{...}]", "position": "left" }
+ */
+ @PutMapping
+ public AjaxResult saveMyConfig(@RequestBody Map
body) {
+ Long userId = getUserId();
+ String menuItems = body != null && body.get("menuItems") != null ? body.get("menuItems").toString() : null;
+ String position = body != null && body.get("position") != null ? body.get("position").toString() : "left";
+ if (menuItems == null) {
+ return error("菜单项不能为空");
+ }
+ int rows = menuConfigService.saveConfig(userId, menuItems, position, getUsername());
+ return toAjax(rows);
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserMenuConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserMenuConfig.java
new file mode 100644
index 0000000..6b08c56
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserMenuConfig.java
@@ -0,0 +1,63 @@
+package com.ruoyi.system.domain;
+
+import javax.validation.constraints.Size;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 用户左侧菜单配置对象 sys_user_menu_config
+ *
+ * @author ruoyi
+ */
+public class SysUserMenuConfig extends BaseEntity {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 配置主键 */
+ private Long configId;
+
+ /** 用户ID */
+ @Excel(name = "用户ID")
+ private Long userId;
+
+ /** 菜单项JSON数组 */
+ @Excel(name = "菜单项")
+ private String menuItems;
+
+ /** 菜单位置 left/top/bottom */
+ @Excel(name = "菜单位置")
+ @Size(max = 20)
+ private String position;
+
+ public Long getConfigId() {
+ return configId;
+ }
+
+ public void setConfigId(Long configId) {
+ this.configId = configId;
+ }
+
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ public String getMenuItems() {
+ return menuItems;
+ }
+
+ public void setMenuItems(String menuItems) {
+ this.menuItems = menuItems;
+ }
+
+ public String getPosition() {
+ return position;
+ }
+
+ public void setPosition(String position) {
+ this.position = position;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMenuConfigMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMenuConfigMapper.java
new file mode 100644
index 0000000..1b5c771
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMenuConfigMapper.java
@@ -0,0 +1,35 @@
+package com.ruoyi.system.mapper;
+
+import com.ruoyi.system.domain.SysUserMenuConfig;
+
+/**
+ * 用户左侧菜单配置 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysUserMenuConfigMapper {
+
+ /**
+ * 根据用户ID查询配置
+ *
+ * @param userId 用户ID
+ * @return 配置信息
+ */
+ SysUserMenuConfig selectByUserId(Long userId);
+
+ /**
+ * 新增配置
+ *
+ * @param config 配置信息
+ * @return 结果
+ */
+ int insertConfig(SysUserMenuConfig config);
+
+ /**
+ * 更新配置
+ *
+ * @param config 配置信息
+ * @return 结果
+ */
+ int updateConfig(SysUserMenuConfig config);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserMenuConfigService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserMenuConfigService.java
new file mode 100644
index 0000000..d3ccd59
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserMenuConfigService.java
@@ -0,0 +1,30 @@
+package com.ruoyi.system.service;
+
+import com.ruoyi.system.domain.SysUserMenuConfig;
+
+/**
+ * 用户左侧菜单配置 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysUserMenuConfigService {
+
+ /**
+ * 根据当前登录用户ID查询配置
+ *
+ * @param userId 用户ID
+ * @return 配置信息,无则返回 null
+ */
+ SysUserMenuConfig selectByUserId(Long userId);
+
+ /**
+ * 保存当前用户的菜单配置(有则更新,无则新增)
+ *
+ * @param userId 用户ID
+ * @param menuItems 菜单项JSON字符串
+ * @param position 菜单位置
+ * @param operator 操作人(createBy/updateBy)
+ * @return 结果
+ */
+ int saveConfig(Long userId, String menuItems, String position, String operator);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserMenuConfigServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserMenuConfigServiceImpl.java
new file mode 100644
index 0000000..367a20c
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserMenuConfigServiceImpl.java
@@ -0,0 +1,55 @@
+package com.ruoyi.system.service.impl;
+
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.SysUserMenuConfig;
+import com.ruoyi.system.mapper.SysUserMenuConfigMapper;
+import com.ruoyi.system.service.ISysUserMenuConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 用户左侧菜单配置 服务层实现
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysUserMenuConfigServiceImpl implements ISysUserMenuConfigService {
+
+ @Autowired
+ private SysUserMenuConfigMapper menuConfigMapper;
+
+ @Override
+ public SysUserMenuConfig selectByUserId(Long userId) {
+ if (userId == null) {
+ return null;
+ }
+ return menuConfigMapper.selectByUserId(userId);
+ }
+
+ @Override
+ public int saveConfig(Long userId, String menuItems, String position, String operator) {
+ if (userId == null) {
+ return 0;
+ }
+ SysUserMenuConfig existing = menuConfigMapper.selectByUserId(userId);
+ if (existing != null) {
+ existing.setMenuItems(menuItems);
+ if (StringUtils.isNotEmpty(position)) {
+ existing.setPosition(position);
+ }
+ if (StringUtils.isNotEmpty(operator)) {
+ existing.setUpdateBy(operator);
+ }
+ return menuConfigMapper.updateConfig(existing);
+ } else {
+ SysUserMenuConfig config = new SysUserMenuConfig();
+ config.setUserId(userId);
+ config.setMenuItems(menuItems);
+ config.setPosition(StringUtils.isEmpty(position) ? "left" : position);
+ if (StringUtils.isNotEmpty(operator)) {
+ config.setCreateBy(operator);
+ }
+ return menuConfigMapper.insertConfig(config);
+ }
+ }
+}
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMenuConfigMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMenuConfigMapper.xml
new file mode 100644
index 0000000..b03f615
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMenuConfigMapper.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+ select config_id, user_id, menu_items, position, create_by, create_time, update_by, update_time, remark
+ from sys_user_menu_config
+
+
+
+
+
+ insert into sys_user_menu_config (
+ user_id,
+ menu_items,
+ position,
+ create_by,
+ remark,
+ create_time
+ ) values (
+ #{userId},
+ #{menuItems},
+ #{position},
+ #{createBy},
+ #{remark},
+ sysdate()
+ )
+
+
+
+ update sys_user_menu_config
+
+ menu_items = #{menuItems},
+ position = #{position},
+ update_by = #{updateBy},
+ remark = #{remark},
+ update_time = sysdate()
+
+ where user_id = #{userId}
+
+
diff --git a/ruoyi-ui/src/api/system/userMenuConfig.js b/ruoyi-ui/src/api/system/userMenuConfig.js
new file mode 100644
index 0000000..a87faba
--- /dev/null
+++ b/ruoyi-ui/src/api/system/userMenuConfig.js
@@ -0,0 +1,23 @@
+import request from '@/utils/request'
+
+/**
+ * 获取当前用户的左侧菜单配置
+ */
+export function getMenuConfig() {
+ return request({
+ url: '/system/user/menuConfig',
+ method: 'get'
+ })
+}
+
+/**
+ * 保存当前用户的左侧菜单配置
+ * @param {Object} data - { menuItems: string (JSON), position: string }
+ */
+export function saveMenuConfig(data) {
+ return request({
+ url: '/system/user/menuConfig',
+ method: 'put',
+ data: data
+ })
+}