index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. <template>
  2. <div>
  3. <PlusPage
  4. ref="plusPageInstance"
  5. :columns="tableConfig"
  6. :request="getList"
  7. :before-search-submit="handleBeforeSearch"
  8. :is-card="true"
  9. :search="{
  10. labelWidth: 100,
  11. showNumber: 3
  12. }"
  13. :table="{
  14. actionBar: { buttons, type: 'link', width: 140 },
  15. adaptive: { offsetBottom: 50 },
  16. treeProps: { children: 'children' },
  17. rowKey: 'menuId'
  18. }"
  19. :pagination="false"
  20. >
  21. <template #table-title>
  22. <el-row class="button-row">
  23. <el-button size="default" type="success" @click="handleCreate">新增</el-button>
  24. </el-row>
  25. </template>
  26. </PlusPage>
  27. <!-- 弹窗编辑 -->
  28. <PlusDialogForm
  29. ref="dialogForm"
  30. v-model="form"
  31. v-model:visible="dialogVisible"
  32. @confirm="handleSubmit"
  33. @close="handleClose"
  34. :form="{ columns, labelPosition: 'right',labelWidth: 100, rules, rowProps: {gutter: 20}, colProps: {span: 12} }"
  35. :dialog="{ title: dialogTitle + '菜单', width: 800, confirmLoading }"
  36. />
  37. </div>
  38. </template>
  39. <script lang="ts" setup>
  40. import {computed, defineOptions, h, nextTick, onMounted, reactive, ref, toRefs} from "vue";
  41. import type {FormRules} from 'element-plus'
  42. import {ElMessage} from "element-plus";
  43. import {
  44. type FieldValues,
  45. type PlusColumn,
  46. PlusDialogForm,
  47. PlusPage,
  48. PlusPageInstance,
  49. useTable
  50. } from "plus-pro-components";
  51. import {cloneDeep} from "lodash-es";
  52. import {
  53. addMenu,
  54. deleteMenu,
  55. getSystemList,
  56. getSystemMenuDetailById,
  57. getSystemMenuList,
  58. updateMenu
  59. } from "@/api/system/menu";
  60. defineOptions({
  61. name: "PageTable"
  62. });
  63. interface TableRow {
  64. id: number;
  65. name: string;
  66. status: string;
  67. tag: string;
  68. time: Date;
  69. }
  70. onMounted(async () => {
  71. await getSystem()
  72. })
  73. // 菜单列表
  74. const menuList = ref([]);
  75. const plusPageInstance = ref<PlusPageInstance | null>(null)
  76. const getList = async (query: Record<string, any>) => {
  77. let res = await getSystemMenuList(query);
  78. let list = listToTree(res.data);
  79. menuList.value = list;
  80. return {
  81. data: list,
  82. total: list.length,
  83. }
  84. };
  85. const listToTree = (data: object[]) => {
  86. // * 先生成parent建立父子关系
  87. const obj = {};
  88. data.forEach((item: any) => {
  89. obj[item.menuId] = item;
  90. });
  91. // * obj -> {1001: {id: 1001, parentId: 0, name: 'AA'}, 1002: {...}}
  92. // console.log(obj, "obj")
  93. const parentList = [];
  94. data.forEach((item: any) => {
  95. const parent = obj[item.parentId];
  96. if (parent) {
  97. // * 当前项有父节点
  98. parent.children = parent.children || [];
  99. parent.children.push(item);
  100. } else {
  101. // * 当前项没有父节点 -> 顶层
  102. parentList.push(item);
  103. }
  104. });
  105. return parentList;
  106. }
  107. // 重新请求列表接口
  108. const refresh = () => {
  109. plusPageInstance.value?.getList()
  110. }
  111. // 搜索之前函数
  112. const handleBeforeSearch = (values: any) => {
  113. // 返回新的参数
  114. return cloneDeep(values);
  115. };
  116. const dialogTitle = computed(() => (state.isCreate ? '新增' : '编辑'))
  117. const {buttons} = useTable<TableRow[]>();
  118. const systemList = ref([]);
  119. const getSystem = async () => {
  120. // 获取系统列表
  121. let res = await getSystemList();
  122. systemList.value = res.data.map((item: any) => ({
  123. label: item.sysName,
  124. value: item.sysCode
  125. }))
  126. }
  127. // 表格数据
  128. const tableConfig: PlusColumn[] = [
  129. {
  130. label: "菜单名称",
  131. prop: "menuName",
  132. width: 180,
  133. tableColumnProps: {
  134. showOverflowTooltip: true
  135. },
  136. },
  137. {
  138. label: "图标",
  139. prop: "icon",
  140. hideInSearch: true,
  141. width: 80,
  142. // 返回一个组件
  143. render: (value) =>
  144. h('i',
  145. {
  146. class: `${value} iconfont`,
  147. style: {fontSize: '16px'}
  148. },
  149. )
  150. },
  151. {
  152. label: "排序",
  153. prop: "orderNum",
  154. width: 80,
  155. hideInSearch: true
  156. },
  157. {
  158. label: "权限标识",
  159. prop: "perms",
  160. hideInSearch: true
  161. },
  162. {
  163. label: "网关URL",
  164. prop: "url",
  165. hideInSearch: true
  166. },
  167. {
  168. label: "组件路径",
  169. prop: "component",
  170. hideInSearch: true
  171. },
  172. {
  173. label: "状态",
  174. prop: "status",
  175. valueType: "tag",
  176. hideInSearch: true,
  177. fieldProps: value => ({
  178. type: value === '0' ? 'primary' : 'danger'
  179. }),
  180. fieldSlots: {
  181. default: ({value}) => (value === '0' ? '正常' : '停用')
  182. }
  183. },
  184. {
  185. label: "系统",
  186. prop: "systemCode",
  187. valueType: "select",
  188. options: computed(() => systemList.value),
  189. hideInTable: true,
  190. fieldProps: {
  191. clearable: true,
  192. placeholder: '请选择系统',
  193. style: {width: '100%'}
  194. }
  195. },
  196. {
  197. label: "菜单状态",
  198. prop: "status",
  199. valueType: "select",
  200. options: [{label: '正常', value: '0'}, {label: '停用', value: '1'}],
  201. hideInTable: true,
  202. fieldProps: {
  203. clearable: true,
  204. placeholder: '请选择菜单状态',
  205. style: {width: '100%'}
  206. }
  207. },
  208. {
  209. label: "创建时间",
  210. prop: "createTime",
  211. valueType: "date-picker",
  212. hideInSearch: true,
  213. minWidth: 180
  214. }
  215. ];
  216. /*--------------------表单--------------------*/
  217. // 表单实例
  218. const dialogForm = ref(null);
  219. interface State {
  220. dialogVisible: boolean;
  221. detailsVisible: boolean;
  222. confirmLoading: boolean;
  223. selectedIds: number[];
  224. isCreate: boolean;
  225. form: {
  226. component: string, // 组件路径
  227. icon: string, // 菜单图标
  228. isCache: string, // 是否缓存 (0缓存 1不缓存)
  229. isFrame: string, // 是否外链 (0是 1否)
  230. menuName: string,// 菜单名称
  231. menuType: string, // 菜单类型(M目录 C菜单 F按钮)
  232. orderNum: string,// 显示排序
  233. parentId: number,// 上级菜单
  234. path: string, // 路由地址
  235. perms: string, // 权限标识
  236. query: string, // 路由参数
  237. routeName: string, // 路由名称
  238. status: string, // 菜单状态(0正常 1停用)
  239. systemCode: string, // 系统编码
  240. url: string, // 网关URL
  241. urlmatch: string, // 网关权限
  242. visible: string, // 显示状态(0显示 1隐藏)
  243. };
  244. rules: FormRules;
  245. }
  246. const state = reactive<State>({
  247. dialogVisible: false,
  248. detailsVisible: false,
  249. confirmLoading: false,
  250. selectedIds: [],
  251. isCreate: false,
  252. form: {
  253. component: null, // 组件路径
  254. icon: null, // 菜单图标
  255. isCache: '0', // 是否缓存 (0缓存 1不缓存)
  256. isFrame: '1', // 是否外链 (0是 1否)
  257. menuName: null,// 菜单名称
  258. menuType: 'M', // 菜单类型(M目录 C菜单 F按钮)
  259. orderNum: null,// 显示排序
  260. parentId: 0,// 上级菜单
  261. path: null, // 路由地址
  262. perms: null, // 权限标识
  263. query: null, // 路由参数
  264. routeName: null, // 路由名称
  265. status: '0', // 菜单状态(0正常 1停用)
  266. systemCode: null, // 系统编码
  267. url: null, // 网关URL
  268. urlmatch: null, // 网关权限
  269. visible: '0', // 显示状态(0显示 1隐藏)
  270. },
  271. rules: {
  272. parentId: [{required: true, message: '请选择上级菜单', trigger: 'blur'}],
  273. orderNum: [{required: true, message: '请输入排序', trigger: 'blur'}],
  274. menuName: [{required: true, message: '请输入菜单名称', trigger: 'blur'}],
  275. path: [{required: true, message: '请输入路由地址', trigger: 'blur'}],
  276. }
  277. })
  278. const columns: PlusColumn[] = [
  279. {
  280. label: '上级菜单',
  281. prop: 'parentId',
  282. colProps: {span: 24},
  283. valueType: 'tree-select',
  284. fieldProps: {
  285. placeholder: '请选择上级菜单',
  286. clearable: true,
  287. showSearch: true,
  288. checkStrictly: true,
  289. defaultExpandAll: false,
  290. style: {width: '100%'},
  291. data: computed(() => {
  292. return [{menuId: 0, menuName: '主类目', children: [...menuList.value]},]
  293. }),
  294. // el-tree-select 组件属性
  295. props: {
  296. label: 'menuName',
  297. value: 'menuId',
  298. children: 'children'
  299. }
  300. }
  301. },
  302. {
  303. label: "系统编码",
  304. prop: "systemCode",
  305. valueType: 'input',
  306. colProps: {span: 24},
  307. fieldProps: {
  308. placeholder: '请输入系统编码',
  309. clearable: true
  310. }
  311. },
  312. {
  313. label: "菜单类型",
  314. prop: "menuType",
  315. valueType: 'radio',
  316. colProps: {span: 24},
  317. fieldProps: {
  318. placeholder: '请选择菜单类型',
  319. clearable: true,
  320. options: [
  321. {label: '目录', value: 'M'},
  322. {label: '菜单', value: 'C'},
  323. {label: '按钮', value: 'F'}
  324. ],
  325. },
  326. },
  327. {
  328. label: "菜单图标",
  329. prop: "icon",
  330. valueType: 'input',
  331. colProps: {span: 12},
  332. fieldProps: {
  333. placeholder: '请输入菜单图标',
  334. clearable: true,
  335. },
  336. hideInForm: computed(() => form.value.menuType === 'F')
  337. },
  338. {
  339. label: "显示排序",
  340. prop: "orderNum",
  341. valueType: 'input-number',
  342. colProps: {span: 12},
  343. fieldProps: {
  344. placeholder: '请输入显示排序',
  345. style: {width: '100%'},
  346. min: 0
  347. }
  348. },
  349. {
  350. label: "菜单名称",
  351. prop: "menuName",
  352. colProps: {span: 12},
  353. fieldProps: {
  354. placeholder: '请输入菜单名称',
  355. clearable: true
  356. }
  357. },
  358. {
  359. label: "路由名称",
  360. prop: "routeName",
  361. colProps: {span: 12},
  362. fieldProps: {
  363. placeholder: '请输入路由名称',
  364. clearable: true
  365. },
  366. hideInForm: computed(() => form.value.menuType !== 'C')
  367. },
  368. {
  369. label: "是否外链",
  370. prop: "isFrame",
  371. valueType: 'radio',
  372. colProps: {span: 12},
  373. fieldProps: {
  374. options: [
  375. {label: '是', value: '0'},
  376. {label: '否', value: '1'}
  377. ]
  378. },
  379. tooltip: '选择是外链则路由地址需要以`http(s)://`开头',
  380. hideInForm: computed(() => form.value.menuType === 'F')
  381. },
  382. {
  383. label: "路由地址",
  384. prop: "path",
  385. colProps: {span: 12},
  386. fieldProps: {
  387. placeholder: '请输入路由地址',
  388. clearable: true
  389. },
  390. tooltip: "访问的路由地址,如:`role`,如外网地址需内链访问则以`http(s)://`开头",
  391. hideInForm: computed(() => form.value.menuType === 'F')
  392. },
  393. {
  394. label: "组件路径",
  395. prop: "component",
  396. colProps: {span: 12},
  397. fieldProps: {
  398. placeholder: '请输入组件路径',
  399. clearable: true
  400. },
  401. tooltip: "访问的组件路径,如:`system/role/index`,默认在`views`目录下",
  402. hideInForm: computed(() => form.value.menuType !== 'C')
  403. },
  404. {
  405. label: "权限字符",
  406. prop: "perms",
  407. colProps: {span: 12},
  408. fieldProps: {
  409. placeholder: '请输入权限标识',
  410. clearable: true
  411. },
  412. tooltip: "控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:role:list')`)",
  413. hideInForm: computed(() => form.value.menuType === 'M')
  414. },
  415. {
  416. label: "网关URL",
  417. prop: "url",
  418. colProps: {span: 12},
  419. fieldProps: {
  420. placeholder: '请输入URL',
  421. clearable: true
  422. },
  423. },
  424. {
  425. label: "网关权限",
  426. prop: "urlmatch",
  427. colProps: {span: 12},
  428. fieldProps: {
  429. placeholder: '请输入网关权限标识',
  430. clearable: true
  431. },
  432. tooltip: "控制器中定义的权限字符",
  433. },
  434. {
  435. label: "路由参数",
  436. prop: "query",
  437. colProps: {span: 12},
  438. fieldProps: {
  439. placeholder: '请输入路由参数',
  440. clearable: true
  441. },
  442. tooltip: "访问路由的默认传递参数,如:`{\"id\": 1, \"name\": \"ry\"}`",
  443. hideInForm: computed(() => form.value.menuType !== 'C')
  444. },
  445. {
  446. label: "是否缓存",
  447. prop: "isCache",
  448. valueType: 'radio',
  449. colProps: {span: 12},
  450. fieldProps: {
  451. options: [
  452. {label: '是', value: '0'},
  453. {label: '否', value: '1'}
  454. ]
  455. },
  456. tooltip: "选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致",
  457. hideInForm: computed(() => form.value.menuType !== 'C')
  458. },
  459. {
  460. label: "显示状态",
  461. prop: "visible",
  462. valueType: 'radio',
  463. colProps: {span: 12},
  464. fieldProps: {
  465. options: [
  466. {label: '显示', value: '0'},
  467. {label: '隐藏', value: '1'}
  468. ]
  469. },
  470. tooltip: "选择隐藏则路由将不会出现在侧边栏,但仍然可以访问",
  471. hideInForm: computed(() => form.value.menuType === 'F')
  472. },
  473. {
  474. label: "菜单状态",
  475. prop: "status",
  476. valueType: 'radio',
  477. colProps: {span: 12},
  478. fieldProps: {
  479. options: [
  480. {label: '正常', value: '0'},
  481. {label: '停用', value: '1'}
  482. ]
  483. },
  484. tooltip: "选择停用则路由将不会出现在侧边栏,也不能被访问"
  485. },
  486. ]
  487. // 创建
  488. const handleCreate = (): void => {
  489. form.value = {
  490. component: null, // 组件路径
  491. icon: null, // 菜单图标
  492. isCache: '0', // 是否缓存 (0缓存 1不缓存)
  493. isFrame: '1', // 是否外链 (0是 1否)
  494. menuName: null,// 菜单名称
  495. menuType: 'M', // 菜单类型(M目录 C菜单 F按钮)
  496. orderNum: null,// 显示排序
  497. parentId: 0,// 上级菜单
  498. path: null, // 路由地址
  499. perms: null, // 权限标识
  500. query: null, // 路由参数
  501. routeName: null, // 路由名称
  502. status: '0', // 菜单状态(0正常 1停用)
  503. systemCode: null, // 系统编码
  504. url: null, // 网关URL
  505. urlmatch: null, // 网关权限
  506. visible: '0', // 显示状态(0显示 1隐藏)
  507. }
  508. state.isCreate = true
  509. state.dialogVisible = true
  510. }
  511. const handleSubmit = async (values: FieldValues) => {
  512. console.log(values, 'Submit')
  513. confirmLoading.value = true
  514. if (state.isCreate) {
  515. try {
  516. let params = form.value
  517. let res = await addMenu(params)
  518. if (res.code === 200) {
  519. ElMessage.success('新增成功')
  520. confirmLoading.value = false
  521. dialogVisible.value = false
  522. refresh()
  523. } else {
  524. ElMessage.error(res.msg)
  525. }
  526. } finally {
  527. confirmLoading.value = false
  528. }
  529. } else {
  530. // 编辑
  531. try {
  532. let params = form.value
  533. let res = await updateMenu(params)
  534. if (res.code === 200) {
  535. ElMessage.success('修改成功')
  536. confirmLoading.value = false
  537. dialogVisible.value = false
  538. refresh()
  539. } else {
  540. ElMessage.error(res.msg)
  541. }
  542. } finally {
  543. confirmLoading.value = false
  544. }
  545. }
  546. }
  547. const handleClose = () => {
  548. // 重置表单
  549. nextTick(() => {
  550. if (dialogForm.value) {
  551. dialogForm.value.formInstance.resetFields()
  552. }
  553. })
  554. console.log(dialogForm.value.formInstance)
  555. }
  556. buttons.value = [
  557. {
  558. // 修改
  559. text: "修改",
  560. code: "edit",
  561. // props v0.1.16 版本新增函数类型
  562. props: {
  563. type: "primary"
  564. },
  565. onClick(params) {
  566. getSystemMenuDetailById(params.row.menuId).then(res => {
  567. form.value = res.data
  568. state.isCreate = false
  569. state.dialogVisible = true
  570. })
  571. },
  572. },
  573. {
  574. // 新增
  575. text: "新增",
  576. code: "view",
  577. props: {
  578. type: "success"
  579. },
  580. onClick(params) {
  581. console.log("新增", params)
  582. form.value = {
  583. component: null, // 组件路径
  584. icon: null, // 菜单图标
  585. isCache: '0', // 是否缓存 (0缓存 1不缓存)
  586. isFrame: '1', // 是否外链 (0是 1否)
  587. menuName: null,// 菜单名称
  588. menuType: 'M', // 菜单类型(M目录 C菜单 F按钮)
  589. orderNum: null,// 显示排序
  590. parentId: params.row.menuId,// 上级菜单
  591. path: null, // 路由地址
  592. perms: null, // 权限标识
  593. query: null, // 路由参数
  594. routeName: null, // 路由名称
  595. status: '0', // 菜单状态(0正常 1停用)
  596. systemCode: null, // 系统编码
  597. url: null, // 网关URL
  598. urlmatch: null, // 网关权限
  599. visible: '0', // 显示状态(0显示 1隐藏)
  600. }
  601. state.isCreate = true
  602. state.dialogVisible = true
  603. },
  604. },
  605. {
  606. // 删除
  607. text: "删除",
  608. code: "delete",
  609. // props v0.1.16 版本新增计算属性支持
  610. props: computed(() => ({type: "danger"})),
  611. confirm: {
  612. options: {
  613. draggable: true,
  614. message: "确定删除此数据吗?"
  615. }
  616. },
  617. onConfirm: async (params) => {
  618. try {
  619. let res = await deleteMenu(params.row.menuId)
  620. if (res.code === 200) {
  621. ElMessage.success('删除成功')
  622. refresh()
  623. } else {
  624. ElMessage.error(res.msg)
  625. }
  626. } catch (e) {
  627. ElMessage.error('删除失败')
  628. }
  629. }
  630. }
  631. ];
  632. const {form, confirmLoading, rules, dialogVisible} = toRefs(state)
  633. </script>