You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

311 line
11 KiB

  1. {% extends "../layout.html" %}
  2. {% block title %}角色权限{% endblock %}
  3. {% block content %}
  4. <div id="roleApp" v-cloak>
  5. <!-- 搜索栏 -->
  6. <el-card shadow="never" class="mb-4">
  7. <el-form :inline="true" @submit.prevent="loadList()" class="flex items-center flex-wrap gap-2">
  8. <el-form-item label="关键字" class="!mb-0">
  9. <el-input v-model="keyword" placeholder="角色名称" clearable style="width:180px;" />
  10. </el-form-item>
  11. <el-form-item class="!mb-0">
  12. <el-button type="primary" @click="loadList()">搜索</el-button>
  13. <el-button @click="resetFilter">重置</el-button>
  14. </el-form-item>
  15. </el-form>
  16. </el-card>
  17. <!-- 角色列表 -->
  18. <el-card shadow="never">
  19. <template #header>
  20. <el-button type="primary" @click="showAddModal">+ 新增</el-button>
  21. </template>
  22. <el-table :data="tableData" v-loading="loading" stripe border>
  23. <el-table-column prop="name" label="角色名称">
  24. <template #default="{ row }">
  25. <span class="text-orange-500 font-medium">${ row.name }</span>
  26. </template>
  27. </el-table-column>
  28. <el-table-column label="默认" width="80" align="center">
  29. <template #default="{ row }">
  30. <el-tag v-if="row.is_default" type="success" size="small">是</el-tag>
  31. <span v-else>-</span>
  32. </template>
  33. </el-table-column>
  34. <el-table-column prop="code" label="角色编码">
  35. <template #default="{ row }">${ row.code || '-' }</template>
  36. </el-table-column>
  37. <el-table-column prop="sort" label="排序" width="80" align="center"></el-table-column>
  38. <el-table-column prop="description" label="描述">
  39. <template #default="{ row }">${ row.description || '-' }</template>
  40. </el-table-column>
  41. <el-table-column label="操作" width="200" align="center">
  42. <template #default="{ row }">
  43. <el-button type="primary" link @click="openPermDrawer(row)" :disabled="row.is_default === 1">分配权限</el-button>
  44. <el-button v-if="!row.is_default" type="primary" link @click="editRole(row)">编辑</el-button>
  45. <el-button v-if="!row.is_default" type="danger" link @click="deleteRole(row)">删除</el-button>
  46. </template>
  47. </el-table-column>
  48. </el-table>
  49. <div class="flex justify-end mt-4">
  50. <el-pagination
  51. v-model:current-page="pagination.page"
  52. :page-size="pagination.pageSize"
  53. :total="pagination.total"
  54. layout="total, prev, pager, next"
  55. @current-change="loadList"
  56. />
  57. </div>
  58. </el-card>
  59. <!-- 新增/编辑弹窗 -->
  60. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close draggable :close-on-click-modal="false">
  61. <el-form :model="form" label-width="80px">
  62. <el-form-item label="角色名称" required>
  63. <el-input v-model="form.name" placeholder="请输入角色名称" />
  64. </el-form-item>
  65. <el-form-item label="角色编码">
  66. <el-input v-model="form.code" placeholder="请输入角色编码(如 ADMIN)" />
  67. </el-form-item>
  68. <el-form-item label="描述">
  69. <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入角色描述" />
  70. </el-form-item>
  71. <el-form-item label="排序">
  72. <el-input-number v-model="form.sort" :min="0" :precision="0" :step="1" />
  73. </el-form-item>
  74. <el-form-item label="默认角色">
  75. <el-switch v-model="form.is_default" :active-value="1" :inactive-value="0" />
  76. </el-form-item>
  77. </el-form>
  78. <template #footer>
  79. <el-button @click="dialogVisible = false">取消</el-button>
  80. <el-button type="primary" @click="saveRole" :loading="saving">确定</el-button>
  81. </template>
  82. </el-dialog>
  83. <!-- 权限分配抽屉 -->
  84. <el-drawer v-model="permDrawerVisible" :title="permTitle" size="420px" destroy-on-close>
  85. <template #header>
  86. <span class="font-semibold">${ permTitle }</span>
  87. </template>
  88. <div class="mb-3">
  89. <el-checkbox v-model="expandAll" @change="handleExpandAll">全部展开</el-checkbox>
  90. </div>
  91. <el-tree
  92. ref="permTreeRef"
  93. :data="permissionTree"
  94. show-checkbox
  95. node-key="key"
  96. :default-expand-all="expandAll"
  97. :props="{ label: 'name', children: 'children' }"
  98. ></el-tree>
  99. <template #footer>
  100. <el-button @click="permDrawerVisible = false">取消</el-button>
  101. <el-button type="primary" @click="savePermissions" :loading="permSaving">确定</el-button>
  102. </template>
  103. </el-drawer>
  104. </div>
  105. {% endblock %}
  106. {% block js %}
  107. <script>
  108. const permissionTreeData = {{ permissionTree | dump | safe }};
  109. const { createApp, ref, reactive, onMounted, nextTick } = Vue;
  110. const app = createApp({
  111. delimiters: ['${', '}'],
  112. setup() {
  113. const keyword = ref('');
  114. const loading = ref(false);
  115. const tableData = ref([]);
  116. const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
  117. // 弹窗
  118. const dialogVisible = ref(false);
  119. const dialogTitle = ref('新增角色');
  120. const saving = ref(false);
  121. const form = reactive({ id: null, name: '', code: '', description: '', sort: 0, is_default: 0 });
  122. // 权限抽屉
  123. const permDrawerVisible = ref(false);
  124. const permTitle = ref('权限分配');
  125. const permTreeRef = ref(null);
  126. const expandAll = ref(false);
  127. const permSaving = ref(false);
  128. const currentPermRoleId = ref(null);
  129. const permissionTree = ref(permissionTreeData || []);
  130. // 加载列表
  131. async function loadList(page) {
  132. if (typeof page === 'number') pagination.page = page;
  133. loading.value = true;
  134. try {
  135. const params = new URLSearchParams({ page: pagination.page, pageSize: pagination.pageSize, keyword: keyword.value });
  136. const res = await fetch('/admin/system/role/list?' + params).then(r => r.json());
  137. if (res.code === 0) {
  138. tableData.value = res.data.data || [];
  139. pagination.total = res.data.count || 0;
  140. } else {
  141. ElementPlus.ElMessage.error(res.msg || '加载失败');
  142. }
  143. } finally {
  144. loading.value = false;
  145. }
  146. }
  147. // 重置筛选
  148. function resetFilter() {
  149. keyword.value = '';
  150. pagination.page = 1;
  151. loadList();
  152. }
  153. // 显示新增弹窗
  154. function showAddModal() {
  155. dialogTitle.value = '新增角色';
  156. Object.assign(form, { id: null, name: '', code: '', description: '', sort: 0, is_default: 0 });
  157. dialogVisible.value = true;
  158. }
  159. // 编辑角色
  160. function editRole(row) {
  161. dialogTitle.value = '编辑角色';
  162. Object.assign(form, { id: row.id, name: row.name, code: row.code || '', description: row.description || '', sort: row.sort || 0, is_default: row.is_default || 0 });
  163. dialogVisible.value = true;
  164. }
  165. // 保存角色
  166. async function saveRole() {
  167. if (!form.name.trim()) {
  168. ElementPlus.ElMessage.warning('角色名称不能为空');
  169. return;
  170. }
  171. saving.value = true;
  172. try {
  173. const url = form.id ? '/admin/system/role/edit' : '/admin/system/role/add';
  174. const body = { name: form.name, code: form.code, description: form.description, sort: form.sort, is_default: form.is_default };
  175. if (form.id) body.id = form.id;
  176. const res = await fetch(url, {
  177. method: 'POST',
  178. headers: { 'Content-Type': 'application/json' },
  179. body: JSON.stringify(body)
  180. }).then(r => r.json());
  181. if (res.code === 0) {
  182. ElementPlus.ElMessage.success('保存成功');
  183. dialogVisible.value = false;
  184. loadList();
  185. } else {
  186. ElementPlus.ElMessage.error(res.msg || '保存失败');
  187. }
  188. } finally {
  189. saving.value = false;
  190. }
  191. }
  192. // 删除角色
  193. async function deleteRole(row) {
  194. try {
  195. await ElementPlus.ElMessageBox.confirm('确定要删除该角色吗?', '提示', { type: 'warning' });
  196. const res = await fetch('/admin/system/role/delete', {
  197. method: 'POST',
  198. headers: { 'Content-Type': 'application/json' },
  199. body: JSON.stringify({ id: row.id })
  200. }).then(r => r.json());
  201. if (res.code === 0) {
  202. ElementPlus.ElMessage.success('删除成功');
  203. loadList();
  204. } else {
  205. ElementPlus.ElMessage.error(res.msg || '删除失败');
  206. }
  207. } catch {}
  208. }
  209. // 打开权限抽屉
  210. async function openPermDrawer(row) {
  211. currentPermRoleId.value = row.id;
  212. permTitle.value = `【${row.name}】权限分配`;
  213. permDrawerVisible.value = true;
  214. const res = await fetch('/admin/system/role/detail?id=' + row.id).then(r => r.json());
  215. const currentPerms = res.code === 0 ? (res.data.permissions || []) : [];
  216. // 收集所有叶子节点 key
  217. const leafKeys = [];
  218. function collectLeaves(nodes) {
  219. nodes.forEach(function(n) {
  220. if (n.children && n.children.length) {
  221. collectLeaves(n.children);
  222. } else {
  223. leafKeys.push(n.key);
  224. }
  225. });
  226. }
  227. collectLeaves(permissionTree.value);
  228. // 只设置叶子节点的选中状态,父节点会自动半选/全选
  229. const leafPerms = currentPerms.filter(function(k) { return leafKeys.indexOf(k) !== -1; });
  230. // 等待 drawer 和 tree 完全渲染
  231. await nextTick();
  232. await nextTick();
  233. if (permTreeRef.value) {
  234. permTreeRef.value.setCheckedKeys(leafPerms);
  235. }
  236. }
  237. // 全部展开/折叠
  238. function handleExpandAll(val) {
  239. if (permTreeRef.value) {
  240. const nodes = permTreeRef.value.store.nodesMap;
  241. for (const key in nodes) {
  242. nodes[key].expanded = val;
  243. }
  244. }
  245. }
  246. // 保存权限
  247. async function savePermissions() {
  248. permSaving.value = true;
  249. try {
  250. const checked = permTreeRef.value ? permTreeRef.value.getCheckedKeys() : [];
  251. const halfChecked = permTreeRef.value ? permTreeRef.value.getHalfCheckedKeys() : [];
  252. const permissions = [...new Set([...checked, ...halfChecked])];
  253. const res = await fetch('/admin/system/role/assignPermissions', {
  254. method: 'POST',
  255. headers: { 'Content-Type': 'application/json' },
  256. body: JSON.stringify({ id: currentPermRoleId.value, permissions })
  257. }).then(r => r.json());
  258. if (res.code === 0) {
  259. ElementPlus.ElMessage.success('权限保存成功');
  260. permDrawerVisible.value = false;
  261. } else {
  262. ElementPlus.ElMessage.error(res.msg || '保存失败');
  263. }
  264. } finally {
  265. permSaving.value = false;
  266. }
  267. }
  268. onMounted(() => loadList());
  269. return {
  270. keyword, loading, tableData, pagination,
  271. dialogVisible, dialogTitle, saving, form,
  272. permDrawerVisible, permTitle, permTreeRef, expandAll, permSaving, permissionTree,
  273. loadList, resetFilter, showAddModal, editRole, saveRole, deleteRole,
  274. openPermDrawer, handleExpandAll, savePermissions
  275. };
  276. }
  277. });
  278. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  279. app.mount('#roleApp');
  280. </script>
  281. {% endblock %}