index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. <template>
  2. <ContentWrap>
  3. <el-form :model="queryParams" ref="queryForm" :inline="true">
  4. <el-form-item label="菜单名称" prop="name">
  5. <el-input v-model="queryParams.name" placeholder="请输入菜单名称" />
  6. </el-form-item>
  7. <el-form-item label="状态" prop="status">
  8. <el-select v-model="queryParams.status" placeholder="请选择菜单状态">
  9. <el-option
  10. v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
  11. :key="dict.value"
  12. :label="dict.label"
  13. :value="dict.value"
  14. />
  15. </el-select>
  16. </el-form-item>
  17. <el-form-item>
  18. <XButton
  19. type="primary"
  20. preIcon="ep:search"
  21. :title="t('common.query')"
  22. @click="handleQuery()"
  23. />
  24. <XButton preIcon="ep:refresh-right" :title="t('common.reset')" @click="resetQuery()" />
  25. </el-form-item>
  26. </el-form>
  27. <vxe-toolbar>
  28. <template #buttons>
  29. <XButton
  30. type="primary"
  31. preIcon="ep:zoom-in"
  32. :title="t('action.add')"
  33. v-hasPermi="['system:menu:create']"
  34. @click="handleCreate()"
  35. />
  36. <XButton title="展开所有" @click="xTable?.setAllTreeExpand(true)" />
  37. <XButton title="关闭所有" @click="xTable?.clearTreeExpand()" />
  38. </template>
  39. </vxe-toolbar>
  40. <vxe-table
  41. show-overflow
  42. keep-source
  43. ref="xTable"
  44. :loading="tableLoading"
  45. :row-config="{ keyField: 'id' }"
  46. :column-config="{ resizable: true }"
  47. :tree-config="{ transform: true, rowField: 'id', parentField: 'parentId' }"
  48. :print-config="{}"
  49. :export-config="{}"
  50. :data="tableData"
  51. class="xtable"
  52. >
  53. <vxe-column title="菜单名称" field="name" width="200" tree-node>
  54. <template #default="{ row }">
  55. <Icon :icon="row.icon" />
  56. <span class="ml-3">{{ row.name }}</span>
  57. </template>
  58. </vxe-column>
  59. <vxe-column title="菜单类型" field="type">
  60. <template #default="{ row }">
  61. <DictTag :type="DICT_TYPE.SYSTEM_MENU_TYPE" :value="row.type" />
  62. </template>
  63. </vxe-column>
  64. <vxe-column title="路由地址" field="path" />
  65. <vxe-column title="组件路径" field="component" />
  66. <vxe-column title="权限标识" field="permission" />
  67. <vxe-column title="排序" field="sort" />
  68. <vxe-column title="状态" field="status">
  69. <template #default="{ row }">
  70. <DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
  71. </template>
  72. </vxe-column>
  73. <vxe-column title="创建时间" field="createTime" formatter="formatDate" />
  74. <vxe-column title="操作" width="200">
  75. <template #default="{ row }">
  76. <XTextButton
  77. preIcon="ep:edit"
  78. :title="t('action.edit')"
  79. v-hasPermi="['system:menu:update']"
  80. @click="handleUpdate(row)"
  81. />
  82. <XTextButton
  83. preIcon="ep:delete"
  84. :title="t('action.del')"
  85. v-hasPermi="['system:menu:delete']"
  86. @click="handleDelete(row)"
  87. />
  88. </template>
  89. </vxe-column>
  90. </vxe-table>
  91. </ContentWrap>
  92. <XModal v-model="dialogVisible" id="menuModel" :title="dialogTitle">
  93. <template #default>
  94. <!-- 对话框(添加 / 修改) -->
  95. <el-form
  96. :model="menuForm"
  97. :rules="rules"
  98. :inline="true"
  99. label-width="120px"
  100. label-position="right"
  101. >
  102. <el-row :gutter="24">
  103. <el-col :span="24">
  104. <el-form-item label="上级菜单">
  105. <el-tree-select
  106. node-key="id"
  107. v-model="menuForm.parentId"
  108. :props="menuProps"
  109. :data="menuOptions"
  110. check-strictly
  111. />
  112. </el-form-item>
  113. </el-col>
  114. <el-col :span="12">
  115. <el-form-item label="菜单类型" prop="type">
  116. <el-radio-group v-model="menuForm.type">
  117. <el-radio-button
  118. v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)"
  119. :key="dict.value"
  120. :label="dict.value"
  121. >
  122. {{ dict.label }}
  123. </el-radio-button>
  124. </el-radio-group>
  125. </el-form-item>
  126. </el-col>
  127. <el-col :span="12">
  128. <el-form-item label="菜单名称" prop="name">
  129. <el-input v-model="menuForm.name" placeholder="请输入菜单名称" clearable />
  130. </el-form-item>
  131. </el-col>
  132. <template v-if="menuForm.type !== 3">
  133. <el-col :span="12">
  134. <el-form-item label="菜单图标">
  135. <IconSelect v-model="menuForm.icon" clearable />
  136. </el-form-item>
  137. </el-col>
  138. <el-col :span="12">
  139. <el-form-item label="路由地址" prop="path">
  140. <template #label>
  141. <Tooltip
  142. titel="路由地址"
  143. message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
  144. />
  145. </template>
  146. <el-input v-model="menuForm.path" placeholder="请输入路由地址" clearable />
  147. </el-form-item>
  148. </el-col>
  149. </template>
  150. <template v-if="menuForm.type === 2">
  151. <el-col :span="12">
  152. <el-form-item label="路由地址" prop="component">
  153. <el-input v-model="menuForm.component" placeholder="请输入组件地址" clearable />
  154. </el-form-item>
  155. </el-col>
  156. </template>
  157. <template v-if="menuForm.type !== 1">
  158. <el-col :span="12">
  159. <el-form-item label="权限标识" prop="permission">
  160. <template #label>
  161. <Tooltip
  162. titel="权限标识"
  163. message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
  164. />
  165. </template>
  166. <el-input v-model="menuForm.permission" placeholder="请输入权限标识" clearable />
  167. </el-form-item>
  168. </el-col>
  169. </template>
  170. <el-col :span="12">
  171. <el-form-item label="显示排序" prop="sort">
  172. <el-input-number
  173. v-model="menuForm.sort"
  174. controls-position="right"
  175. :min="0"
  176. clearable
  177. />
  178. </el-form-item>
  179. </el-col>
  180. <el-col :span="12">
  181. <el-form-item label="菜单状态" prop="status">
  182. <el-radio-group v-model="menuForm.status">
  183. <el-radio-button
  184. v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
  185. :key="dict.value"
  186. :label="dict.value"
  187. >
  188. {{ dict.label }}
  189. </el-radio-button>
  190. </el-radio-group>
  191. </el-form-item>
  192. </el-col>
  193. <template v-if="menuForm.type !== 3">
  194. <el-col :span="12">
  195. <el-form-item label="显示状态" prop="status">
  196. <template #label>
  197. <Tooltip
  198. titel="显示状态"
  199. message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问"
  200. />
  201. </template>
  202. <el-radio-group v-model="menuForm.visible">
  203. <el-radio-button key="true" :label="true">显示</el-radio-button>
  204. <el-radio-button key="false" :label="false">隐藏</el-radio-button>
  205. </el-radio-group>
  206. </el-form-item>
  207. </el-col>
  208. </template>
  209. <template v-if="menuForm.type === 2">
  210. <el-col :span="12">
  211. <el-form-item label="缓存状态" prop="keepAlive">
  212. <template #label>
  213. <Tooltip
  214. titel="缓存状态"
  215. message="选择缓存时,则会被 `keep-alive` 缓存,需要匹配组件的 `name` 和路由地址保持一致"
  216. />
  217. </template>
  218. <el-radio-group v-model="menuForm.keepAlive">
  219. <el-radio-button key="true" :label="true">缓存</el-radio-button>
  220. <el-radio-button key="false" :label="false">不缓存</el-radio-button>
  221. </el-radio-group>
  222. </el-form-item>
  223. </el-col>
  224. </template>
  225. </el-row>
  226. </el-form>
  227. </template>
  228. <template #footer>
  229. <!-- 操作按钮 -->
  230. <XButton
  231. v-if="['create', 'update'].includes(actionType)"
  232. type="primary"
  233. :loading="actionLoading"
  234. @click="submitForm"
  235. :title="t('action.save')"
  236. />
  237. <XButton :loading="actionLoading" @click="dialogVisible = false" :title="t('dialog.close')" />
  238. </template>
  239. </XModal>
  240. </template>
  241. <script setup lang="ts">
  242. import * as MenuApi from '@/api/system/menu'
  243. import { MenuVO } from '@/api/system/menu/types'
  244. import { useI18n } from '@/hooks/web/useI18n'
  245. import { useMessage } from '@/hooks/web/useMessage'
  246. import { IconSelect } from '@/components/Icon'
  247. import { Tooltip } from '@/components/Tooltip'
  248. import { required } from '@/utils/formRules.js'
  249. import { onMounted, reactive, ref } from 'vue'
  250. import { VxeTableInstance } from 'vxe-table'
  251. import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
  252. import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants'
  253. import {
  254. ElRow,
  255. ElCol,
  256. ElForm,
  257. ElFormItem,
  258. ElInput,
  259. ElInputNumber,
  260. ElSelect,
  261. ElTreeSelect,
  262. ElOption,
  263. ElRadioGroup,
  264. ElRadioButton
  265. } from 'element-plus'
  266. import { handleTree } from '@/utils/tree'
  267. const { t } = useI18n() // 国际化
  268. const message = useMessage()
  269. const xTable = ref<VxeTableInstance>()
  270. const tableLoading = ref(false)
  271. const tableData = ref()
  272. const actionLoading = ref(false) // 遮罩层
  273. const actionType = ref('') // 操作按钮的类型
  274. const dialogVisible = ref(false) // 是否显示弹出层
  275. const dialogTitle = ref('edit') // 弹出层标题
  276. const statusOption = ref() // 状态选项
  277. const menuForm = ref<MenuVO>({
  278. id: 0,
  279. name: '',
  280. permission: '',
  281. type: SystemMenuTypeEnum.DIR,
  282. sort: 1,
  283. parentId: 0,
  284. path: '',
  285. icon: '',
  286. component: '',
  287. status: CommonStatusEnum.ENABLE,
  288. visible: true,
  289. keepAlive: true,
  290. createTime: ''
  291. })
  292. const menuProps = {
  293. checkStrictly: true,
  294. children: 'children',
  295. label: 'name',
  296. value: 'id'
  297. }
  298. interface Tree {
  299. id: number
  300. name: string
  301. children?: Tree[] | any[]
  302. }
  303. const menuOptions = ref<any[]>([]) // 树形结构
  304. const getTree = async () => {
  305. menuOptions.value = []
  306. const res = await MenuApi.listSimpleMenusApi()
  307. let menu: Tree = { id: 0, name: '主类目', children: [] }
  308. menu.children = handleTree(res)
  309. menuOptions.value.push(menu)
  310. }
  311. // ========== 查询 ==========
  312. const queryParams = reactive({
  313. name: null,
  314. status: null
  315. })
  316. const getList = async () => {
  317. tableLoading.value = true
  318. statusOption.value = getIntDictOptions(DICT_TYPE.COMMON_STATUS)
  319. const res = await MenuApi.getMenuListApi(queryParams)
  320. tableData.value = res
  321. tableLoading.value = false
  322. }
  323. // 设置标题
  324. const setDialogTile = (type: string) => {
  325. dialogTitle.value = t('action.' + type)
  326. actionType.value = type
  327. dialogVisible.value = true
  328. }
  329. // 新建操作
  330. const handleCreate = () => {
  331. setDialogTile('create')
  332. }
  333. // 修改操作
  334. const handleUpdate = async (row: MenuVO) => {
  335. // 设置数据
  336. const res = await MenuApi.getMenuApi(row.id)
  337. console.log(res)
  338. menuForm.value = res
  339. setDialogTile('update')
  340. }
  341. // 删除操作
  342. const handleDelete = async (row: MenuVO) => {
  343. message.confirm(t('common.delDataMessage'), t('common.confirmTitle')).then(async () => {
  344. await MenuApi.deleteMenuApi(row.id)
  345. message.success(t('common.delSuccess'))
  346. await getList()
  347. })
  348. }
  349. // 表单校验
  350. const rules = reactive({
  351. name: [required],
  352. sort: [required],
  353. path: [required],
  354. status: [required]
  355. })
  356. // 查询操作
  357. const handleQuery = async () => {
  358. await getList()
  359. }
  360. // 重置操作
  361. const resetQuery = async () => {
  362. queryParams.name = null
  363. queryParams.status = null
  364. await getList()
  365. }
  366. // 保存操作
  367. const isExternal = (path: string) => {
  368. return /^(https?:|mailto:|tel:)/.test(path)
  369. }
  370. const submitForm = async () => {
  371. actionLoading.value = true
  372. // 提交请求
  373. try {
  374. if (
  375. menuForm.value.type === SystemMenuTypeEnum.DIR ||
  376. menuForm.value.type === SystemMenuTypeEnum.MENU
  377. ) {
  378. if (!isExternal(menuForm.value.path)) {
  379. if (menuForm.value.parentId === 0 && menuForm.value.path.charAt(0) !== '/') {
  380. message.error('路径必须以 / 开头')
  381. return
  382. } else if (menuForm.value.parentId !== 0 && menuForm.value.path.charAt(0) === '/') {
  383. message.error('路径不能以 / 开头')
  384. return
  385. }
  386. }
  387. }
  388. if (actionType.value === 'create') {
  389. await MenuApi.createMenuApi(menuForm.value)
  390. message.success(t('common.createSuccess'))
  391. } else {
  392. await MenuApi.updateMenuApi(menuForm.value)
  393. message.success(t('common.updateSuccess'))
  394. }
  395. // 操作成功,重新加载列表
  396. dialogVisible.value = false
  397. await getList()
  398. } finally {
  399. actionLoading.value = false
  400. }
  401. }
  402. onMounted(async () => {
  403. await getList()
  404. getTree()
  405. })
  406. </script>