index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. <script setup lang="ts" name="Menu">
  2. import apis from "@a"
  3. import message from "@@/utils/message"
  4. const { sys_normal_disable, sys_show_hide } = useDict("sys_normal_disable", "sys_show_hide")
  5. const tableRef = ref()
  6. const modalRef = ref()
  7. const opts = reactive({
  8. columns: [
  9. { field: "menuId", name: "菜单ID", width: 100, isSort: true, visible: false },
  10. { field: "menuName", name: "菜单名称", visible: true },
  11. { field: "icon", name: "菜单图标", visible: true, width: 80 },
  12. { field: "path", name: "路由地址/按钮", visible: true, width: 120 },
  13. //{ field: "query", name: "路由参数", visible: true, width: 120 },
  14. { field: "component", name: "组件路径", visible: true },
  15. { field: "isFrame", name: "是否为外链", visible: true, width: 90 },
  16. { field: "isCache", name: "是否缓存", visible: true, width: 80 },
  17. { field: "menuType", name: "菜单类型", visible: true, width: 80 },
  18. { field: "visible", name: "菜单显示", visible: true, width: 80 },
  19. { field: "status", name: "菜单状态", visible: true, width: 80 },
  20. { field: "perms", name: "权限标识", visible: true },
  21. { field: "actions", name: `操作`, width: 150 }
  22. ] as any,
  23. queryParams: {
  24. menuName: undefined,
  25. visible: undefined,
  26. status: undefined
  27. },
  28. searchFormItems: [
  29. {
  30. field: "menuName",
  31. label: "菜单名称",
  32. class: "w-100",
  33. required: false,
  34. placeholder: "请输入菜单名称",
  35. listeners: {
  36. keyup: (e: KeyboardEvent) => {
  37. if (e.code == "Enter") {
  38. handleQuery()
  39. }
  40. }
  41. }
  42. },
  43. {
  44. field: "visible",
  45. label: "菜单显示",
  46. class: "w-100",
  47. required: false,
  48. component: "Dict",
  49. props: {
  50. placeholder: "请选择菜单显示",
  51. dictType: "sys_show_hide"
  52. }
  53. },
  54. {
  55. field: "status",
  56. label: "菜单状态",
  57. class: "w-100",
  58. required: false,
  59. component: "Dict",
  60. props: {
  61. placeholder: "请选择菜单状态",
  62. dictType: "sys_normal_disable"
  63. }
  64. }
  65. ] as any,
  66. permission: "system:menu",
  67. handleBtns: [
  68. {
  69. key: "handleUpdate",
  70. show: false
  71. },
  72. {
  73. key: "handleDelete",
  74. show: false
  75. }
  76. ],
  77. handleFuns: {
  78. handleCreate
  79. },
  80. customBtns: [],
  81. tableListFun: getTableData,
  82. getEntityFun: apis.system.menuApi.getMenu,
  83. deleteEntityFun: apis.system.menuApi.delMenu,
  84. modalTitle: "菜单",
  85. formRules: {
  86. menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
  87. orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
  88. path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
  89. },
  90. labelWidth: "80px",
  91. emptyFormData: {
  92. parentId: 0,
  93. menuId: undefined,
  94. menuName: undefined,
  95. orderNum: undefined,
  96. path: undefined,
  97. component: undefined,
  98. query: undefined,
  99. isFrame: undefined,
  100. isCache: undefined,
  101. menuType: undefined,
  102. visible: "0",
  103. status: "0",
  104. perms: undefined,
  105. icon: undefined,
  106. btnClass: undefined,
  107. btnScript: undefined,
  108. remark: undefined
  109. }
  110. })
  111. const { queryParams, emptyFormData } = toRefs(opts)
  112. const form = ref<any>(emptyFormData.value)
  113. /** 搜索按钮操作 */
  114. function handleQuery(query?: any) {
  115. query = query || tableRef.value?.getQueryParams() || queryParams.value
  116. tableRef.value?.query(query)
  117. }
  118. /** 重置按钮操作 */
  119. function resetQuery() {
  120. //
  121. }
  122. const menuOptions = ref()
  123. const defaultBtnStyle = ref()
  124. function reset() {
  125. form.value = emptyFormData.value
  126. defaultBtnStyle.value = undefined
  127. getMenuTreeSelect()
  128. }
  129. /** 新增按钮操作 */
  130. function handleCreate(row: any) {
  131. reset()
  132. if (row != null && row.menuId) {
  133. form.value.parentId = row.menuId
  134. } else {
  135. form.value.parentId = 0
  136. }
  137. modalRef.value.changePrefixTitle("添加")
  138. modalRef.value.show()
  139. }
  140. /** 修改按钮操作 */
  141. function handleUpdate(row: any) {
  142. reset()
  143. if (row.menuId) {
  144. apis.system.menuApi.getMenu(row.menuId).then((res: any) => {
  145. form.value = res.data
  146. modalRef.value.changePrefixTitle("修改")
  147. modalRef.value.show()
  148. })
  149. }
  150. }
  151. function getMenuTreeSelect() {
  152. menuOptions.value = []
  153. apis.system.menuApi.listMenu({}).then((response) => {
  154. const menu: any = { menuId: 0, menuName: "主类目", children: [] }
  155. menu.children = handleTree(response.data, "menuId")
  156. menuOptions.value.push(menu)
  157. })
  158. }
  159. /** 提交按钮 */
  160. function submitForm() {
  161. if (form.value.menuId != null) {
  162. apis.system.menuApi.updateMenu(form.value).then(() => {
  163. message.msgSuccess("修改成功")
  164. handleQuery()
  165. })
  166. } else {
  167. apis.system.menuApi.addMenu(form.value).then(() => {
  168. message.msgSuccess("新增成功")
  169. handleQuery()
  170. })
  171. }
  172. }
  173. /** 删除按钮操作 */
  174. function handleDelete(row: any) {
  175. message
  176. .confirm('是否确认删除菜单为"' + row.menuName + '"的数据项?')
  177. .then(() => {
  178. return apis.system.menuApi.delMenu(row.menuId).then(() => {
  179. handleQuery()
  180. })
  181. })
  182. .catch(() => {
  183. //
  184. })
  185. }
  186. function defaultBtnChange(v: any) {
  187. switch (v) {
  188. case "A":
  189. form.value.btnClass = "btn btn-light-primary"
  190. form.value.btnScript = "handleCreate"
  191. break
  192. case "U":
  193. form.value.btnClass = "btn btn-light-success"
  194. form.value.btnScript = "handleUpdate@1"
  195. break
  196. case "D":
  197. form.value.btnClass = "btn btn-light-danger"
  198. form.value.btnScript = "handleDelete@0"
  199. break
  200. case "I":
  201. form.value.btnClass = "btn btn-light-warning"
  202. form.value.btnScript = "handleImport"
  203. break
  204. case "E":
  205. form.value.btnClass = "btn btn-light-info"
  206. form.value.btnScript = "handleExport"
  207. break
  208. case "H":
  209. form.value.btnClass = "btn btn-light"
  210. form.value.btnScript = "hide"
  211. break
  212. }
  213. }
  214. function getTableData(q: any) {
  215. return new Promise((resolve) => {
  216. apis.system.menuApi.listMenu(q).then((res) => {
  217. res.data = handleTree(res.data, "menuId")
  218. resolve(res)
  219. })
  220. })
  221. }
  222. const showChooseIcon = ref(false)
  223. const iconSelectRef = ref()
  224. /** 展示下拉图标 */
  225. function showSelectIcon() {
  226. iconSelectRef.value?.reset()
  227. showChooseIcon.value = true
  228. }
  229. /** 选择图标 */
  230. function selected(name: string) {
  231. form.value.icon = name
  232. showChooseIcon.value = false
  233. }
  234. </script>
  235. <template>
  236. <div class="app-container">
  237. <VbTable
  238. ref="tableRef"
  239. :is-tree="true"
  240. keyField="menuId"
  241. iconField="menuName"
  242. parentField="parentId"
  243. childrenField="children"
  244. :check-multiple="false"
  245. :has-checkbox="false"
  246. :no-page="true"
  247. :expand-depth="1"
  248. :interval-left="10"
  249. :show-search-bar="false"
  250. :show-right-search-btn="false"
  251. :show-right-column-btn="false"
  252. :handle-perm="opts.permission"
  253. :handle-btns="opts.handleBtns"
  254. :handle-funs="opts.handleFuns"
  255. :search-form-items="opts.searchFormItems"
  256. :columns="opts.columns"
  257. :custom-btns="opts.customBtns"
  258. :remote-fun="apis.system.menuApi.listMenu"
  259. v-model:query-params="queryParams"
  260. :reset-search-form-fun="resetQuery"
  261. :custom-search-fun="handleQuery">
  262. <template #icon="{ row }">
  263. <i :class="`fs-4 text-primary bi bi-${row.icon}`"></i>
  264. </template>
  265. <template #path="{ row }">
  266. <template v-if="row.menuType == 'F'">
  267. <template v-if="row.btnScript">
  268. <template v-if="row.btnClass">
  269. <div
  270. class="d-flex justify-content-center align-items-center w-100"
  271. style="transform: scale(0.7)">
  272. <span :class="row.btnClass">{{ row.btnScript }}</span>
  273. </div>
  274. </template>
  275. <template v-else>{{ row.btnScript }}</template>
  276. </template>
  277. <template v-else>{{ row.btnClass }}</template>
  278. </template>
  279. <template v-else>{{ row.path }}</template>
  280. </template>
  281. <template #component="{ row }">
  282. <template v-if="row.component">
  283. <vb-tooltip :content="row.component" :auto-hide="true"></vb-tooltip>
  284. </template>
  285. <span v-else class="text-muted">-</span>
  286. </template>
  287. <template #query="{ row }">
  288. <template v-if="row.query">
  289. <vb-tooltip :content="row.query" :auto-hide="true"></vb-tooltip>
  290. </template>
  291. <span v-else class="text-muted">-</span>
  292. </template>
  293. <template #perms="{ row }">
  294. <template v-if="row.perms">
  295. <vb-tooltip :content="row.perms" :auto-hide="true"></vb-tooltip>
  296. </template>
  297. <span v-else class="text-muted">-</span>
  298. </template>
  299. <template #menuType="{ row }">
  300. <vb-tag
  301. :data="[
  302. { label: '目录', value: 'M', type: 'primary' },
  303. { label: '菜单', value: 'C', type: 'success' },
  304. { label: '按钮', value: 'F', type: 'warning' }
  305. ]"
  306. :value="row.menuType"></vb-tag>
  307. </template>
  308. <template #isCache="{ row }">
  309. <vb-tag
  310. :data="[
  311. { label: '是', value: '0', type: 'primary' },
  312. { label: '否', value: '1', type: 'danger' }
  313. ]"
  314. :value="row.isCache"></vb-tag>
  315. </template>
  316. <template #isFrame="{ row }">
  317. <vb-tag
  318. :data="[
  319. { label: '是', value: '0', type: 'primary' },
  320. { label: '否', value: '1', type: 'danger' }
  321. ]"
  322. :value="row.isFrame"></vb-tag>
  323. </template>
  324. <template #visible="{ row }">
  325. <DictTag type="sys_show_hide" :value="row.visible"></DictTag>
  326. </template>
  327. <template #status="{ row }">
  328. <DictTag type="sys_normal_disable" :value="row.status"></DictTag>
  329. </template>
  330. <template #actions="{ row }">
  331. <vb-tooltip content="新增" placement="top">
  332. <el-button
  333. link
  334. type="primary"
  335. @click="handleCreate(row)"
  336. v-hasPermission="'system:menu:add'">
  337. <template #icon>
  338. <VbIcon icon-name="plus-square" icon-type="duotone" class="fs-3"></VbIcon>
  339. </template>
  340. </el-button>
  341. </vb-tooltip>
  342. <vb-tooltip content="修改" placement="top">
  343. <el-button
  344. link
  345. type="primary"
  346. @click="handleUpdate(row)"
  347. v-hasPermission="'system:menu:edit'">
  348. <template #icon>
  349. <VbIcon icon-name="notepad-edit" icon-type="duotone" class="fs-3"></VbIcon>
  350. </template>
  351. </el-button>
  352. </vb-tooltip>
  353. <vb-tooltip content="删除" placement="top">
  354. <el-button
  355. link
  356. type="primary"
  357. @click="handleDelete(row)"
  358. v-hasPermission="'system:menu:remove'">
  359. <template #icon>
  360. <VbIcon icon-name="trash-square" icon-type="duotone" class="fs-3"></VbIcon>
  361. </template>
  362. </el-button>
  363. </vb-tooltip>
  364. </template>
  365. </VbTable>
  366. <VbModal
  367. v-model:modal="modalRef"
  368. :title="opts.modalTitle"
  369. :form-data="form"
  370. :form-rules="opts.formRules"
  371. :form-label-width="opts.labelWidth"
  372. :hide-event="() => (showChooseIcon = false)"
  373. @confirm="submitForm"
  374. append-to-body>
  375. <template #form>
  376. <el-row>
  377. <el-col :span="24">
  378. <el-form-item label="上级菜单" prop="parentId">
  379. <el-tree-select
  380. v-model="form.parentId"
  381. class="w-100"
  382. :data="menuOptions"
  383. :props="{ label: 'menuName', children: 'children' }"
  384. value-key="menuId"
  385. placeholder="选择上级菜单"
  386. check-strictly />
  387. </el-form-item>
  388. </el-col>
  389. <el-col :span="24">
  390. <el-form-item label="菜单类型" prop="menuType">
  391. <el-radio-group v-model="form.menuType">
  392. <el-radio value="M">目录</el-radio>
  393. <el-radio value="C">菜单</el-radio>
  394. <el-radio value="F">按钮</el-radio>
  395. </el-radio-group>
  396. </el-form-item>
  397. </el-col>
  398. <el-col :span="24">
  399. <el-form-item label="菜单图标" prop="icon">
  400. <el-popover
  401. placement="bottom-start"
  402. :width="600"
  403. v-model:visible="showChooseIcon"
  404. trigger="focus"
  405. @show="showSelectIcon">
  406. <template #reference>
  407. <el-input
  408. v-model="form.icon"
  409. placeholder="点击选择图标"
  410. @blur="showChooseIcon = true"
  411. @focus="showChooseIcon = true"
  412. readonly>
  413. <template #prefix>
  414. <i v-if="form.icon" :class="`text-primary bi bi-${form.icon}`"></i>
  415. <i v-else class="text-muted bi bi-search"></i>
  416. </template>
  417. </el-input>
  418. </template>
  419. <icon-select ref="iconSelectRef" @selected="selected" :active-icon="form.icon" />
  420. </el-popover>
  421. </el-form-item>
  422. </el-col>
  423. <el-col :span="12">
  424. <el-form-item label="菜单名称" prop="menuName">
  425. <el-input v-model="form.menuName" placeholder="请输入菜单名称" class="w-100" />
  426. </el-form-item>
  427. </el-col>
  428. <el-col :span="12">
  429. <el-form-item label="显示顺序" prop="orderNum">
  430. <el-input-number v-model="form.orderNum" placeholder="请输入显示顺序" class="w-100" />
  431. </el-form-item>
  432. </el-col>
  433. <el-col :span="12" v-if="form.menuType != 'M'">
  434. <el-form-item label="权限标识" prop="perms">
  435. <template #label>
  436. <span>
  437. <vb-tooltip
  438. :width="320"
  439. content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
  440. placement="top">
  441. <span class="bi bi-question-circle-fill text-muted"></span>
  442. </vb-tooltip>
  443. 权限字符
  444. </span>
  445. </template>
  446. <el-input v-model="form.perms" placeholder="请输入权限标识" class="w-100" />
  447. </el-form-item>
  448. </el-col>
  449. <el-col :span="24" v-if="form.menuType == 'F'">
  450. <el-form-item label="常用样式">
  451. <template #label>
  452. <span>
  453. <vb-tooltip
  454. :width="320"
  455. content="列举了几种常用样式脚本,方便快捷的设置按钮样式及按钮脚本"
  456. placement="top">
  457. <span class="bi bi-question-circle-fill text-muted"></span>
  458. </vb-tooltip>
  459. 常用样式
  460. </span>
  461. </template>
  462. <el-radio-group v-model="defaultBtnStyle" @change="defaultBtnChange">
  463. <el-radio value="A">
  464. <span class="btn btn-light-primary" style="transform: scale(0.7)">新增</span>
  465. </el-radio>
  466. <el-radio value="U">
  467. <span class="btn btn-light-success" style="transform: scale(0.7)">编辑</span>
  468. </el-radio>
  469. <el-radio value="D">
  470. <span class="btn btn-light-danger" style="transform: scale(0.7)">删除</span>
  471. </el-radio>
  472. <el-radio value="I">
  473. <span class="btn btn-light-warning" style="transform: scale(0.7)">导入</span>
  474. </el-radio>
  475. <el-radio value="E">
  476. <span class="btn btn-light-info" style="transform: scale(0.7)">导出</span>
  477. </el-radio>
  478. <el-radio value="H">
  479. <span class="btn btn-light" style="transform: scale(0.7)">隐藏</span>
  480. </el-radio>
  481. </el-radio-group>
  482. </el-form-item>
  483. </el-col>
  484. <el-col :span="12" v-if="form.menuType == 'F'">
  485. <el-form-item label="按钮样式" prop="btnClass">
  486. <template #label>
  487. <span>
  488. <vb-tooltip content="按钮在toolbar中的显示样式 class" placement="top">
  489. <span class="bi bi-question-circle-fill text-muted"></span>
  490. </vb-tooltip>
  491. 按钮样式
  492. </span>
  493. </template>
  494. <el-input v-model="form.btnClass" placeholder="请输入按钮样式" class="w-100" />
  495. </el-form-item>
  496. </el-col>
  497. <el-col :span="12" v-if="form.menuType == 'F'">
  498. <el-form-item label="按钮脚本" prop="btnScript">
  499. <template #label>
  500. <span>
  501. <vb-tooltip
  502. :width="320"
  503. content="点击按钮调用的方法名,前端需要向table组件内传此方法名为key的方法。[handleCreate]、[handleUpdate]、[handleDelete]、[handleExport]脚本在table组件内置了。注:设置为'hide'值会隐藏按钮。"
  504. placement="top">
  505. <span class="bi bi-question-circle-fill text-muted"></span>
  506. </vb-tooltip>
  507. 按钮脚本
  508. </span>
  509. </template>
  510. <el-input v-model="form.btnScript" placeholder="请输入按钮脚本" class="w-100" />
  511. </el-form-item>
  512. </el-col>
  513. <el-col :span="12" v-if="form.menuType != 'F'">
  514. <el-form-item label="路由地址" prop="path">
  515. <el-input v-model="form.path" placeholder="请输入路由地址" class="w-100" />
  516. </el-form-item>
  517. </el-col>
  518. <el-col :span="12" v-if="form.menuType != 'F'">
  519. <el-form-item label="是否为外链" prop="isFrame">
  520. <template #label>
  521. <span>
  522. <vb-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
  523. <span class="bi bi-question-circle-fill text-muted"></span>
  524. </vb-tooltip>
  525. 是否外链
  526. </span>
  527. </template>
  528. <el-radio-group v-model="form.isFrame">
  529. <el-radio value="0">是</el-radio>
  530. <el-radio value="1">否</el-radio>
  531. </el-radio-group>
  532. </el-form-item>
  533. </el-col>
  534. <el-col :span="12" v-if="form.menuType == 'C'">
  535. <el-form-item label="组件路径" prop="component">
  536. <template #label>
  537. <span>
  538. <vb-tooltip
  539. :width="320"
  540. content="访问的组件路径,如:`system/user/index`,默认在`views`目录下"
  541. placement="top">
  542. <span class="bi bi-question-circle-fill text-muted"></span>
  543. </vb-tooltip>
  544. 组件路径
  545. </span>
  546. </template>
  547. <el-input v-model="form.component" placeholder="请输入组件路径" class="w-100" />
  548. </el-form-item>
  549. </el-col>
  550. <el-col :span="12" v-if="form.menuType == 'C'">
  551. <el-form-item>
  552. <el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
  553. <template #label>
  554. <span>
  555. <vb-tooltip
  556. content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`'
  557. placement="top">
  558. <span class="bi bi-question-circle-fill text-muted"></span>
  559. </vb-tooltip>
  560. 路由参数
  561. </span>
  562. </template>
  563. </el-form-item>
  564. </el-col>
  565. <el-col :span="12" v-if="form.menuType == 'C'">
  566. <el-form-item>
  567. <template #label>
  568. <span>
  569. <vb-tooltip
  570. content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致"
  571. placement="top">
  572. <span class="bi bi-question-circle-fill text-muted"></span>
  573. </vb-tooltip>
  574. 是否缓存
  575. </span>
  576. </template>
  577. <el-radio-group v-model="form.isCache">
  578. <el-radio value="0">缓存</el-radio>
  579. <el-radio value="1">不缓存</el-radio>
  580. </el-radio-group>
  581. </el-form-item>
  582. </el-col>
  583. <el-col :span="12" v-if="form.menuType != 'F'">
  584. <el-form-item>
  585. <template #label>
  586. <span>
  587. <vb-tooltip
  588. content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问"
  589. placement="top">
  590. <span class="bi bi-question-circle-fill text-muted"></span>
  591. </vb-tooltip>
  592. 显示状态
  593. </span>
  594. </template>
  595. <el-radio-group v-model="form.visible">
  596. <el-radio v-for="dict in sys_show_hide" :key="dict.value" :value="dict.value">
  597. {{ dict.label }}
  598. </el-radio>
  599. </el-radio-group>
  600. </el-form-item>
  601. </el-col>
  602. <el-col :span="12" v-if="form.menuType != 'F'">
  603. <el-form-item>
  604. <template #label>
  605. <span>
  606. <vb-tooltip
  607. content="选择停用则路由将不会出现在侧边栏,也不能被访问"
  608. placement="top">
  609. <span class="bi bi-question-circle-fill text-muted"></span>
  610. </vb-tooltip>
  611. 菜单状态
  612. </span>
  613. </template>
  614. <el-radio-group v-model="form.status">
  615. <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">
  616. {{ dict.label }}
  617. </el-radio>
  618. </el-radio-group>
  619. </el-form-item>
  620. </el-col>
  621. <el-col :span="24">
  622. <el-form-item label="备注" prop="remark">
  623. <el-input
  624. v-model="form.remark"
  625. type="textarea"
  626. placeholder="请输入内容"
  627. class="w-100" />
  628. </el-form-item>
  629. </el-col>
  630. </el-row>
  631. </template>
  632. </VbModal>
  633. </div>
  634. </template>