From f9114ffadfab1c01198dd980c3afb66d0c44a5ea Mon Sep 17 00:00:00 2001
From: zzw <228544128@qq.com>
Date: 星期二, 06 十月 2026 18:22:00 +0800
Subject: [PATCH] fix: limit equipment picker to selected X number

---
 emergency-repair-web/src/views/system/MenuManage.vue |  229 ++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 209 insertions(+), 20 deletions(-)

diff --git a/emergency-repair-web/src/views/system/MenuManage.vue b/emergency-repair-web/src/views/system/MenuManage.vue
index 0849396..ec72216 100644
--- a/emergency-repair-web/src/views/system/MenuManage.vue
+++ b/emergency-repair-web/src/views/system/MenuManage.vue
@@ -1,33 +1,82 @@
 <template>
   <div class="page">
     <div class="page-title">
-      <div><h2>鑿滃崟绠$悊</h2><p>鑿滃崟涓哄姩鎬侀厤缃紝宸︿晶瀵艰埅鐩存帴璇诲彇杩欓噷鐨勬暟鎹�</p></div>
-      <el-button type="primary" icon="el-icon-plus" @click="openCreate(null)">鏂板涓�绾ц彍鍗�</el-button>
+      <div><h2>鑿滃崟绠$悊</h2><p>鏀寔鐩綍銆佽彍鍗曘�佹寜閽笁绉嶇被鍨嬶紱鎸夐挳鍙兘鎸傚湪鑿滃崟涓嬬骇锛岀敤浜庨厤缃〉闈㈡搷浣滄潈闄�</p></div>
+      <el-button type="primary" icon="el-icon-plus" v-permission="'system:menus:add'" @click="openCreate(null)">鏂板涓�绾ц彍鍗�</el-button>
     </div>
     <div class="table-panel">
       <el-table :data="tree" row-key="id" border default-expand-all :tree-props="{ children: 'children' }">
         <el-table-column prop="name" label="鑿滃崟鍚嶇О" min-width="220"></el-table-column>
-        <el-table-column prop="icon" label="鍥炬爣" width="200"><template slot-scope="{ row }"><span class="icon-cell"><i :class="row.icon"></i><span>{{ row.icon }}</span></span></template></el-table-column>
-        <el-table-column prop="url" label="璺敱鍦板潃" min-width="240"></el-table-column>
-        <el-table-column prop="permissions" label="鏉冮檺鏍囪瘑" min-width="170"></el-table-column>
-        <el-table-column prop="menuType" label="绫诲瀷" width="90"><template slot-scope="{ row }"><el-tag size="mini" :type="row.menuType === 0 ? 'info' : 'primary'">{{ row.menuType === 0 ? '鐩綍' : '鑿滃崟' }}</el-tag></template></el-table-column>
+        <el-table-column prop="icon" label="鍥炬爣" width="180">
+          <template slot-scope="{ row }">
+            <span v-if="row.menuType !== 2" class="icon-cell"><i :class="row.icon"></i><span>{{ row.icon }}</span></span>
+            <span v-else class="muted">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="url" label="璺敱鍦板潃" min-width="220">
+          <template slot-scope="{ row }">
+            <span v-if="row.menuType !== 2">{{ row.url }}</span>
+            <span v-else class="muted">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="permissions" label="鏉冮檺鏍囪瘑" min-width="200"></el-table-column>
+        <el-table-column label="绫诲瀷" width="90">
+          <template slot-scope="{ row }">
+            <el-tag size="mini" :type="typeTag(row.menuType)">{{ typeName(row.menuType) }}</el-tag>
+          </template>
+        </el-table-column>
         <el-table-column prop="sortNo" label="鎺掑簭" width="80" align="center"></el-table-column>
-        <el-table-column prop="visible" label="鏄剧ず" width="80" align="center"><template slot-scope="{ row }"><el-tag size="mini" :type="row.visible === 0 ? 'info' : 'success'">{{ row.visible === 0 ? '闅愯棌' : '鏄剧ず' }}</el-tag></template></el-table-column>
-        <el-table-column label="鎿嶄綔" width="200" fixed="right"><template slot-scope="{ row }"><el-button type="text" @click="openCreate(row)">鏂板涓嬬骇</el-button><el-button type="text" @click="openEdit(row)">缂栬緫</el-button><el-button type="text" class="danger-link" @click="remove(row)">鍒犻櫎</el-button></template></el-table-column>
+        <el-table-column label="鏄剧ず" width="80" align="center">
+          <template slot-scope="{ row }">
+            <el-tag size="mini" :type="row.visible === 0 ? 'info' : 'success'">{{ row.visible === 0 ? '闅愯棌' : '鏄剧ず' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="鎿嶄綔" width="270" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button v-if="row.menuType !== 2" type="text" v-permission="'system:menus:add'" @click="openCreate(row)">鏂板涓嬬骇</el-button>
+            <el-button v-if="row.menuType === 1" type="text" v-permission="'system:menus:buttons'" @click="openButtons(row)">椤甸潰鎸夐挳</el-button>
+            <el-button type="text" v-permission="'system:menus:edit'" @click="openEdit(row)">缂栬緫</el-button>
+            <el-button type="text" class="danger-link" v-permission="'system:menus:delete'" @click="remove(row)">鍒犻櫎</el-button>
+          </template>
+        </el-table-column>
       </el-table>
     </div>
+
     <el-dialog :title="form.id ? '缂栬緫鑿滃崟' : '鏂板鑿滃崟'" :visible.sync="dialogVisible" width="640px" append-to-body>
       <el-form ref="form" :model="form" :rules="rules" label-width="105px">
-        <el-form-item label="涓婄骇鑿滃崟"><el-input :value="parentName" readonly></el-input></el-form-item>
+        <el-form-item label="涓婄骇鑿滃崟">
+          <el-cascader v-model="form.pid" :options="parentOptions" :props="parentProps" :show-all-levels="false" filterable clearable placeholder="璇烽�夋嫨涓婄骇鑿滃崟" style="width:100%"></el-cascader>
+        </el-form-item>
+        <el-form-item label="鑿滃崟绫诲瀷">
+          <el-radio-group v-model="form.menuType">
+            <el-radio-button v-for="item in typeOptions" :key="item.value" :label="item.value">{{ item.label }}</el-radio-button>
+          </el-radio-group>
+        </el-form-item>
         <el-form-item label="鑿滃崟鍚嶇О" prop="name"><el-input v-model="form.name"></el-input></el-form-item>
-        <el-form-item label="鑿滃崟绫诲瀷"><el-radio-group v-model="form.menuType"><el-radio-button :label="0">鐩綍</el-radio-button><el-radio-button :label="1">鑿滃崟</el-radio-button></el-radio-group></el-form-item>
-        <el-form-item label="璺敱鍦板潃"><el-input v-model="form.url" placeholder="/fault/report"></el-input></el-form-item>
-        <el-form-item label="鍥炬爣"><el-input v-model="form.icon" placeholder="el-icon-tickets"></el-input></el-form-item>
-        <el-form-item label="鏉冮檺鏍囪瘑"><el-input v-model="form.permissions"></el-input></el-form-item>
+        <el-form-item v-if="form.menuType !== 2" label="璺敱鍦板潃"><el-input v-model="form.url" placeholder="/fault/report"></el-input></el-form-item>
+        <el-form-item v-if="form.menuType !== 2" label="鍥炬爣"><el-input v-model="form.icon" placeholder="el-icon-tickets"></el-input></el-form-item>
+        <el-form-item label="鏉冮檺鏍囪瘑" :prop="form.menuType === 2 ? 'permissions' : ''">
+          <el-input v-model="form.permissions" :placeholder="form.menuType === 2 ? '渚嬪 fault:report:add' : '鍙�夛紝渚嬪 fault-report'"></el-input>
+        </el-form-item>
         <el-form-item label="鎺掑簭"><el-input-number v-model="form.sortNo" :min="1" style="width:160px"></el-input-number></el-form-item>
         <el-form-item label="鏄惁鏄剧ず"><el-switch v-model="form.visible" :active-value="1" :inactive-value="0"></el-switch></el-form-item>
       </el-form>
       <template slot="footer"><el-button @click="dialogVisible = false">鍙栨秷</el-button><el-button type="primary" :loading="saving" @click="save">淇濆瓨</el-button></template>
+    </el-dialog>
+
+    <el-dialog :title="'椤甸潰鎸夐挳閰嶇疆 - ' + (buttonMenu.name || '')" :visible.sync="buttonDialogVisible" width="620px" append-to-body>
+      <p class="dialog-tip">涓嬪垪鎸夐挳鏉ヨ嚜鍚庣鎸夐挳鐩綍锛屽嬀閫夊悗鑷姩鐢熸垚鎸夐挳绫诲瀷鑿滃崟锛屽彲鍦ㄨ鑹茬鐞嗙殑鏉冮檺璁剧疆涓巿鏉冪粰瑙掕壊銆�</p>
+      <el-checkbox-group v-model="buttonSelected" class="button-list">
+        <el-checkbox v-for="item in buttonAvailable" :key="item.code" :label="item.code" class="button-item">
+          <span class="button-name">{{ item.name }}</span>
+          <span class="button-code">{{ item.code }}</span>
+        </el-checkbox>
+      </el-checkbox-group>
+      <div v-if="!buttonAvailable.length" class="empty-tip">璇ラ〉闈㈡殏鏈櫥璁板彲鐢ㄦ寜閽�</div>
+      <template slot="footer">
+        <el-button @click="buttonDialogVisible = false">鍙栨秷</el-button>
+        <el-button type="primary" :loading="buttonSaving" @click="saveButtons">淇濆瓨鎸夐挳</el-button>
+      </template>
     </el-dialog>
   </div>
 </template>
@@ -35,19 +84,159 @@
 import api from '@/api'
 export default {
   name: 'MenuManage',
-  data() { return { tree: [], flat: [], dialogVisible: false, saving: false, form: {}, rules: { name: [{ required: true, message: '璇疯緭鍏ヨ彍鍗曞悕绉�', trigger: 'blur' }] } } },
-  computed: { parentName() { const parent = this.flat.find(item => item.id === this.form.pid); return parent ? parent.name : '鏃狅紙涓�绾ц彍鍗曪級' } },
+  data() {
+    return {
+      tree: [],
+      flat: [],
+      dialogVisible: false,
+      saving: false,
+      form: {},
+      rules: { name: [{ required: true, message: '璇疯緭鍏ヨ彍鍗曞悕绉�', trigger: 'blur' }] },
+      buttonDialogVisible: false,
+      buttonSaving: false,
+      buttonMenu: {},
+      buttonAvailable: [],
+      buttonSelected: []
+    }
+  },
+  computed: {
+    parentProps() {
+      return { value: 'id', label: 'name', children: 'children', checkStrictly: true, emitPath: false, expandTrigger: 'hover' }
+    },
+    selectedParent() {
+      const pid = String(this.form.pid || '0')
+      if (pid === '0') return null
+      return this.flat.find(item => String(item.id) === pid) || null
+    },
+    parentOptions() {
+      const excluded = this.excludedMenuIds()
+      const allowedType = this.form.menuType === 2 ? 1 : 0
+      const nodes = this.buildParentOptions(this.tree, allowedType, excluded)
+      return this.form.menuType === 2 ? nodes : [{ id: '0', name: '鏃狅紙涓�绾ц彍鍗曪級', children: nodes }]
+    },
+    typeOptions() {
+      if (this.selectedParent && this.selectedParent.menuType === 1) {
+        return [{ label: '鎸夐挳', value: 2 }]
+      }
+      if (this.selectedParent && this.selectedParent.menuType === 0) {
+        return [{ label: '鐩綍', value: 0 }, { label: '鑿滃崟', value: 1 }]
+      }
+      if (this.form.id && this.form.menuType === 2) {
+        return [{ label: '鎸夐挳', value: 2 }]
+      }
+      return [{ label: '鐩綍', value: 0 }, { label: '鑿滃崟', value: 1 }]
+    }
+  },
   created() { this.load() },
   methods: {
-    load() { Promise.all([api.get('/system/menu/nav'), api.get('/system/menu/list')]).then(([tree, list]) => { this.tree = tree.data || []; this.flat = list.data || [] }) },
-    openCreate(parent) { this.form = { pid: parent ? parent.id : 0, name: '', url: '', icon: 'el-icon-tickets', menuType: parent ? 1 : 0, permissions: '', sortNo: 10, visible: 1 }; this.dialogVisible = true },
-    openEdit(row) { this.form = { ...row }; this.dialogVisible = true },
-    save() { this.$refs.form.validate(valid => { if (!valid) return; this.saving = true; const request = this.form.id ? api.put('/system/menu/' + this.form.id, this.form) : api.post('/system/menu', this.form); request.then(() => { this.$message.success('淇濆瓨鎴愬姛'); this.dialogVisible = false; this.load() }).finally(() => { this.saving = false }) }) },
-    remove(row) { this.$confirm(`纭畾鍒犻櫎鑿滃崟鈥�${row.name}鈥濆悧锛焋, '鍒犻櫎纭', { type: 'warning' }).then(() => api.delete('/system/menu/' + row.id)).then(() => { this.$message.success('鍒犻櫎鎴愬姛'); this.load() }).catch(() => {}) }
+    load() {
+      Promise.all([api.get('/system/menu/tree'), api.get('/system/menu/list')]).then(([tree, list]) => {
+        this.tree = tree.data || []
+        this.flat = list.data || []
+      })
+    },
+    typeName(type) {
+      return type === 2 ? '鎸夐挳' : type === 0 ? '鐩綍' : '鑿滃崟'
+    },
+    typeTag(type) {
+      return type === 2 ? 'warning' : type === 0 ? 'info' : 'primary'
+    },
+    openCreate(parent) {
+      const menuType = parent && parent.menuType === 1 ? 2 : parent ? 1 : 0
+      this.form = {
+        pid: parent ? String(parent.id) : '0',
+        name: '',
+        url: '',
+        icon: menuType === 2 ? '' : 'el-icon-tickets',
+        menuType,
+        permissions: '',
+        sortNo: 10,
+        visible: 1
+      }
+      this.dialogVisible = true
+    },
+    openEdit(row) {
+      this.form = { ...row, pid: row.pid == null || row.pid === 0 ? '0' : String(row.pid) }
+      this.dialogVisible = true
+    },
+    save() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        if (this.form.menuType === 2 && !this.form.permissions) {
+          this.$message.error('鎸夐挳鏉冮檺鏍囪瘑涓嶈兘涓虹┖')
+          return
+        }
+        const payload = { ...this.form }
+        if (payload.pid === null || payload.pid === undefined || payload.pid === '') payload.pid = '0'
+        if (payload.menuType === 2) {
+          payload.url = null
+          payload.icon = null
+        }
+        delete payload.children
+        this.saving = true
+        const request = payload.id ? api.put('/system/menu/' + payload.id, payload) : api.post('/system/menu', payload)
+        request.then(() => {
+          this.$message.success('淇濆瓨鎴愬姛')
+          this.dialogVisible = false
+          this.load()
+        }).finally(() => { this.saving = false })
+      })
+    },
+    excludedMenuIds() {
+      const excluded = new Set()
+      const walk = (nodes, underSelf) => {
+        ;(nodes || []).forEach(node => {
+          const next = underSelf || String(node.id) === String(this.form.id)
+          if (next) excluded.add(String(node.id))
+          walk(node.children, next)
+        })
+      }
+      walk(this.tree, false)
+      return excluded
+    },
+    buildParentOptions(nodes, allowedType, excluded) {
+      return (nodes || [])
+        .filter(node => node.menuType === allowedType && !excluded.has(String(node.id)))
+        .map(node => ({
+          id: String(node.id),
+          name: node.name,
+          children: this.buildParentOptions(node.children, allowedType, excluded)
+        }))
+    },
+    remove(row) {
+      this.$confirm(`纭畾鍒犻櫎鈥�${row.name}鈥濆悧锛焋, '鍒犻櫎纭', { type: 'warning' })
+        .then(() => api.delete('/system/menu/' + row.id))
+        .then(() => { this.$message.success('鍒犻櫎鎴愬姛'); this.load() })
+        .catch(() => {})
+    },
+    openButtons(row) {
+      this.buttonMenu = row
+      api.get('/system/menu/buttons', { params: { menuId: row.id } }).then(res => {
+        const data = res.data || {}
+        this.buttonAvailable = data.available || []
+        this.buttonSelected = data.selected || []
+        this.buttonDialogVisible = true
+      })
+    },
+    saveButtons() {
+      this.buttonSaving = true
+      api.post('/system/menu/buttons', { menuId: this.buttonMenu.id, codes: this.buttonSelected }).then(() => {
+        this.$message.success('椤甸潰鎸夐挳宸插悓姝�')
+        this.buttonDialogVisible = false
+        this.load()
+      }).finally(() => { this.buttonSaving = false })
+    }
   }
 }
 </script>
 <style scoped>
 .danger-link { color: #d9534f; }
 .icon-cell { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
+.muted { color: #a3afbc; }
+.dialog-tip { margin: 0 0 12px; color: #738296; font-size: 13px; }
+.button-list { display: flex; flex-direction: column; gap: 2px; }
+.button-item { margin-right: 0; }
+.button-name { color: #31465d; }
+.button-code { margin-left: 8px; color: #93a2b1; font-size: 12px; }
+.empty-tip { padding: 30px 0; color: #93a2b1; text-align: center; }
 </style>

--
Gitblit v1.9.1