mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-09-19 01:36:38 +08:00
feat(ui): 管理后台新增权限及部分组合式函数优化
This commit is contained in:
parent
07dca3e739
commit
34e3455128
10
new-ui/projects/admin/src/components/PermissionRender.vue
Normal file
10
new-ui/projects/admin/src/components/PermissionRender.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { hasPermission } from "@/directives/permission";
|
||||||
|
defineProps<{
|
||||||
|
permission: string | string[] | true;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<slot v-if="hasPermission(permission)" />
|
||||||
|
<slot v-else name="none" />
|
||||||
|
</template>
|
@ -1,5 +1,4 @@
|
|||||||
import { ref, reactive, unref } from "vue";
|
import { ref, reactive, unref } from "vue";
|
||||||
import { Message } from "@arco-design/web-vue";
|
|
||||||
import type { BaseResponse } from "@chatgpt-plus/packages/type";
|
import type { BaseResponse } from "@chatgpt-plus/packages/type";
|
||||||
function useSubmit<T extends Record<string, any> = Record<string, any>, R = any>(defaultData?: T) {
|
function useSubmit<T extends Record<string, any> = Record<string, any>, R = any>(defaultData?: T) {
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
@ -10,14 +9,16 @@ function useSubmit<T extends Record<string, any> = Record<string, any>, R = any>
|
|||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
const hasError = await formRef.value?.validate();
|
const hasError = await formRef.value?.validate();
|
||||||
if (!hasError) {
|
if (hasError) return Promise.reject({ validateErrors: hasError });
|
||||||
const { data, message } = await api({ ...formData ?? {}, ...unref(params) });
|
|
||||||
message && Message.success(message);
|
const { data, code, message } = await api({ ...formData ?? {}, ...unref(params) });
|
||||||
return Promise.resolve({ formData, data });
|
if (code) {
|
||||||
|
return Promise.reject({ requestErrors: message })
|
||||||
}
|
}
|
||||||
return Promise.reject(false);
|
|
||||||
|
return Promise.resolve({ formData, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return Promise.reject(err);
|
return Promise.reject({ errors: err });
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
|
32
new-ui/projects/admin/src/directives/permission.ts
Normal file
32
new-ui/projects/admin/src/directives/permission.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
|
// 判断操作权限
|
||||||
|
export function hasPermission(permissionTag: string | string[] | boolean) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const { is_super_admin, permissions = [] } = authStore;
|
||||||
|
if (is_super_admin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Array.isArray(permissionTag)) {
|
||||||
|
return permissionTag.every((tag) => permissions.includes(tag));
|
||||||
|
}
|
||||||
|
if (typeof permissionTag === "string") {
|
||||||
|
return permissions.includes(permissionTag);
|
||||||
|
}
|
||||||
|
return permissionTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPermission(el, binding) {
|
||||||
|
if (!hasPermission(binding.value)) {
|
||||||
|
el.parentNode && el.parentNode.removeChild(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const permission = {
|
||||||
|
mounted(el, binding) {
|
||||||
|
checkPermission(el, binding);
|
||||||
|
},
|
||||||
|
updated(el, binding) {
|
||||||
|
checkPermission(el, binding);
|
||||||
|
},
|
||||||
|
};
|
@ -8,8 +8,9 @@ export const uploadUrl = import.meta.env.VITE_PROXY_BASE_URL + "/api/admin/uploa
|
|||||||
export const instance = createInstance(import.meta.env.VITE_PROXY_BASE_URL)
|
export const instance = createInstance(import.meta.env.VITE_PROXY_BASE_URL)
|
||||||
|
|
||||||
instance.interceptors.request.use((config) => {
|
instance.interceptors.request.use((config) => {
|
||||||
config.headers[__AUTH_KEY] = localStorage.getItem(__AUTH_KEY);
|
const TOKEN = JSON.parse(localStorage.getItem(__AUTH_KEY))?.token
|
||||||
config.headers["Authorization"] = localStorage.getItem(__AUTH_KEY);
|
config.headers[__AUTH_KEY] = TOKEN;
|
||||||
|
config.headers["Authorization"] = TOKEN;
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import http from "@/http/config";
|
import http from "@/http/config";
|
||||||
|
|
||||||
export const userLogin = (data: {
|
export const userLogin = (data) => {
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}) => {
|
|
||||||
return http({
|
return http({
|
||||||
url: "/api/admin/login",
|
url: "/api/admin/login",
|
||||||
method: "post",
|
method: "post",
|
||||||
@ -26,7 +23,7 @@ export const getSession = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const loginLog = (params?: Record<string, unknown>) => {
|
export const loginLog = (params) => {
|
||||||
return http({
|
return http({
|
||||||
url: "/api/admin/user/loginLog",
|
url: "/api/admin/user/loginLog",
|
||||||
method: "get",
|
method: "get",
|
||||||
|
@ -3,6 +3,8 @@ import { createPinia } from "pinia";
|
|||||||
import ArcoVue from "@arco-design/web-vue";
|
import ArcoVue from "@arco-design/web-vue";
|
||||||
import ArcoVueIcon from "@arco-design/web-vue/es/icon";
|
import ArcoVueIcon from "@arco-design/web-vue/es/icon";
|
||||||
import "@arco-design/web-vue/dist/arco.css";
|
import "@arco-design/web-vue/dist/arco.css";
|
||||||
|
import PermissionRender from "@/components/PermissionRender.vue";
|
||||||
|
import { permission } from "@/directives/permission";
|
||||||
|
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
@ -14,6 +16,9 @@ app.use(router);
|
|||||||
app.use(ArcoVue);
|
app.use(ArcoVue);
|
||||||
app.use(ArcoVueIcon);
|
app.use(ArcoVueIcon);
|
||||||
|
|
||||||
|
app.component("PermissionRender", PermissionRender);
|
||||||
|
app.directive("permission", permission);
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
app.config.warnHandler = (msg, vm, trace) => {
|
app.config.warnHandler = (msg, vm, trace) => {
|
||||||
if (msg.includes('Invalid prop name: "key" is a reserved property.')) {
|
if (msg.includes('Invalid prop name: "key" is a reserved property.')) {
|
||||||
|
@ -1,14 +1,27 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import CustomLayout from '@/components/CustomLayout.vue'
|
import CustomLayout from '@/components/CustomLayout.vue'
|
||||||
|
import { hasPermission } from "@/directives/permission";
|
||||||
import menu from './menu'
|
import menu from './menu'
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
interface RouteMeta {
|
||||||
|
title?: string
|
||||||
|
permission?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const whiteListRoutes = [
|
const whiteListRoutes = [
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
name: "Login",
|
name: "Login",
|
||||||
component: () => import("@/views/LoginView.vue"),
|
component: () => import("@/views/LoginView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/403",
|
||||||
|
name: "403",
|
||||||
|
component: () => import("@/views/NoPermission.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
name: "404",
|
name: "404",
|
||||||
@ -44,9 +57,13 @@ router.beforeEach((to, _, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!authStore.token) {
|
if (!authStore.token) {
|
||||||
|
authStore.$reset();
|
||||||
next({ name: "Login" });
|
next({ name: "Login" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (to.meta.permission) {
|
||||||
|
next(!hasPermission(to.meta.permission) ? { name: "403" } : undefined);
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
IconCodepen,
|
IconCodepen,
|
||||||
IconWechatpay,
|
IconWechatpay,
|
||||||
IconRobot,
|
IconRobot,
|
||||||
|
IconSafe,
|
||||||
} from "@arco-design/web-vue/es/icon";
|
} from "@arco-design/web-vue/es/icon";
|
||||||
|
|
||||||
const menu = [
|
const menu = [
|
||||||
@ -133,6 +134,15 @@ const menu = [
|
|||||||
},
|
},
|
||||||
component: () => import("@/views/SysAdmin/SysAdminContainer.vue"),
|
component: () => import("@/views/SysAdmin/SysAdminContainer.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/sysPermission",
|
||||||
|
name: "SysPermission",
|
||||||
|
meta: {
|
||||||
|
title: "权限配置",
|
||||||
|
icon: IconSafe,
|
||||||
|
},
|
||||||
|
component: () => import("@/views/SysPermission/SysPermissionContainer.vue"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default menu;
|
export default menu;
|
||||||
|
@ -3,19 +3,29 @@ import { Message } from '@arco-design/web-vue'
|
|||||||
import { userLogin, userLogout } from '@/http/login'
|
import { userLogin, userLogout } from '@/http/login'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
|
||||||
|
const defaultState: {
|
||||||
|
token: string
|
||||||
|
is_super_admin?: boolean;
|
||||||
|
permissions?: string[]
|
||||||
|
} = {
|
||||||
|
token: null,
|
||||||
|
is_super_admin: false,
|
||||||
|
permissions: []
|
||||||
|
}
|
||||||
|
|
||||||
export const useAuthStore = defineStore({
|
export const useAuthStore = defineStore({
|
||||||
id: __AUTH_KEY,
|
id: Symbol(__AUTH_KEY).toString(),
|
||||||
state: () => ({ token: null } as { token: string | null }),
|
state: () => ({ ...defaultState }),
|
||||||
actions: {
|
actions: {
|
||||||
init() {
|
init() {
|
||||||
this.$state.token = localStorage.getItem(__AUTH_KEY);
|
this.$state = JSON.parse(localStorage.getItem(__AUTH_KEY));
|
||||||
},
|
},
|
||||||
async login(params: any) {
|
async login(params: any) {
|
||||||
try {
|
try {
|
||||||
const { data } = await userLogin(params)
|
const { data } = await userLogin(params)
|
||||||
if (data) {
|
if (data) {
|
||||||
this.$state.token = data;
|
this.$state = data;
|
||||||
localStorage.setItem(__AUTH_KEY, data)
|
localStorage.setItem(__AUTH_KEY, JSON.stringify(data))
|
||||||
Message.success('登录成功');
|
Message.success('登录成功');
|
||||||
router.replace({ name: 'home' })
|
router.replace({ name: 'home' })
|
||||||
return Promise.resolve(data)
|
return Promise.resolve(data)
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { ref, computed } from 'vue'
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
export const useCounterStore = defineStore('counter', () => {
|
|
||||||
const count = ref(0)
|
|
||||||
const doubleCount = computed(() => count.value * 2)
|
|
||||||
function increment() {
|
|
||||||
count.value++
|
|
||||||
}
|
|
||||||
|
|
||||||
return { count, doubleCount, increment }
|
|
||||||
})
|
|
11
new-ui/projects/admin/src/views/NoPermission.vue
Normal file
11
new-ui/projects/admin/src/views/NoPermission.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AResult status="403" subtitle="没有权限">
|
||||||
|
<template #extra>
|
||||||
|
<MButton preset="back" type="primary" />
|
||||||
|
</template>
|
||||||
|
</AResult>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less"></style>
|
@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { Message } from "@arco-design/web-vue";
|
||||||
import SearchTable from "@/components/SearchTable/SearchTable.vue";
|
import SearchTable from "@/components/SearchTable/SearchTable.vue";
|
||||||
import type { SearchTableColumns } from "@/components/SearchTable/type";
|
import type { SearchTableColumns } from "@/components/SearchTable/type";
|
||||||
import { dateFormat } from "@chatgpt-plus/packages/utils";
|
import { dateFormat } from "@chatgpt-plus/packages/utils";
|
||||||
import { getList } from "./api";
|
import { getList, remove } from "./api";
|
||||||
|
|
||||||
const columns: SearchTableColumns[] = [
|
const columns: SearchTableColumns[] = [
|
||||||
{
|
{
|
||||||
@ -71,6 +72,12 @@ const columns: SearchTableColumns[] = [
|
|||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const handleRemove = async (id, reload) => {
|
||||||
|
await remove({ id });
|
||||||
|
Message.success("删除成功");
|
||||||
|
await reload();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<SearchTable :request="getList" :columns="columns">
|
<SearchTable :request="getList" :columns="columns">
|
||||||
@ -78,8 +85,15 @@ const columns: SearchTableColumns[] = [
|
|||||||
<a-tag v-if="!record.pay_time" color="blue">未支付</a-tag>
|
<a-tag v-if="!record.pay_time" color="blue">未支付</a-tag>
|
||||||
<span v-else>{{ dateFormat(record.pay_time) }}</span>
|
<span v-else>{{ dateFormat(record.pay_time) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #actions="{ record }">
|
<template #actions="{ record, reload }">
|
||||||
<a-link :key="record.id" status="danger">删除</a-link>
|
<a-popconfirm
|
||||||
|
content="是否删除?"
|
||||||
|
position="left"
|
||||||
|
type="warning"
|
||||||
|
:on-before-ok="() => handleRemove(record.id, reload)"
|
||||||
|
>
|
||||||
|
<a-link status="danger">删除</a-link>
|
||||||
|
</a-popconfirm>
|
||||||
</template>
|
</template>
|
||||||
</SearchTable>
|
</SearchTable>
|
||||||
</template>
|
</template>
|
||||||
|
@ -6,4 +6,12 @@ export const getList = (data?: Record<string, unknown>) => {
|
|||||||
method: "post",
|
method: "post",
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remove = (data) => {
|
||||||
|
return http({
|
||||||
|
url: "/api/admin/order/remove",
|
||||||
|
method: "post",
|
||||||
|
data
|
||||||
|
})
|
||||||
}
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { TableColumnData } from "@arco-design/web-vue";
|
||||||
|
import { Message } from "@arco-design/web-vue";
|
||||||
|
import useRequest from "@/composables/useRequest";
|
||||||
|
import usePopup from "@/composables/usePopup";
|
||||||
|
import SysPermissionForm from "./SysPermissionForm.vue";
|
||||||
|
import { getList, save, remove } from "./api";
|
||||||
|
|
||||||
|
const columns: TableColumnData[] = [
|
||||||
|
{
|
||||||
|
dataIndex: "name",
|
||||||
|
title: "权限名称",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: "slug",
|
||||||
|
title: "权限标识",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: "sort",
|
||||||
|
title: "排序",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
slotName: "actions",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [reload, data, loading] = useRequest(getList);
|
||||||
|
|
||||||
|
const openFormModal = usePopup(SysPermissionForm, {
|
||||||
|
nodeProps: ([_, record]) => ({ record, options: data.value }),
|
||||||
|
popupProps: ([reload, record], exposed) => ({
|
||||||
|
title: `${record?.id ? "编辑" : "新增"}权限`,
|
||||||
|
onBeforeOk: async (done) => {
|
||||||
|
await exposed()?.handleSubmit(save, {
|
||||||
|
id: record?.id,
|
||||||
|
});
|
||||||
|
await reload();
|
||||||
|
done(true);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRemove = async (id) => {
|
||||||
|
await remove({ id });
|
||||||
|
Message.success("删除成功");
|
||||||
|
await reload();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
reload();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div style="padding-bottom: 10px; text-align: right">
|
||||||
|
<a-button type="primary" :loading="loading" @click="openFormModal(reload, {})">
|
||||||
|
<template #icon>
|
||||||
|
<icon-plus />
|
||||||
|
</template>
|
||||||
|
新增
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-table
|
||||||
|
:key="String(loading)"
|
||||||
|
:data="data"
|
||||||
|
:loading="loading"
|
||||||
|
:columns="columns"
|
||||||
|
:pagination="false"
|
||||||
|
default-expand-all-rows
|
||||||
|
>
|
||||||
|
<template #actions="{ record, reload }">
|
||||||
|
<a-link @click="openFormModal(reload, record)">编辑</a-link>
|
||||||
|
<a-popconfirm
|
||||||
|
content="是否删除?"
|
||||||
|
position="left"
|
||||||
|
type="warning"
|
||||||
|
:on-before-ok="() => handleRemove(record.id)"
|
||||||
|
>
|
||||||
|
<a-link status="danger">删除</a-link>
|
||||||
|
</a-popconfirm>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</template>
|
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<a-form ref="formRef" :model="formData" auto-label-width>
|
||||||
|
<a-form-item
|
||||||
|
field="name"
|
||||||
|
label="权限名称"
|
||||||
|
:rules="[{ required: true, message: '请输入权限名称' }]"
|
||||||
|
>
|
||||||
|
<a-input v-model="formData.name" placeholder="请输入权限名称" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
field="pid"
|
||||||
|
label="上级权限"
|
||||||
|
:rules="[{ required: true, message: '请选择上级权限' }]"
|
||||||
|
extra="默认为顶级权限"
|
||||||
|
>
|
||||||
|
<a-tree-select
|
||||||
|
v-model="formData.pid"
|
||||||
|
:data="_options"
|
||||||
|
:field-names="{ key: 'id', title: 'name' }"
|
||||||
|
placeholder="请选择上级权限"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="slug" label="权限标识" extra="规则:controller_method (注意大小写)">
|
||||||
|
<a-input v-model="formData.slug" placeholder="请输入权限标识" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
field="sort"
|
||||||
|
label="排序"
|
||||||
|
:rules="[
|
||||||
|
{ required: true, message: '请输入排序' },
|
||||||
|
{ min: 0, message: '请输入大于0的正整数', type: 'number' },
|
||||||
|
]"
|
||||||
|
extra="数字越小越靠前"
|
||||||
|
>
|
||||||
|
<a-input-number v-model="formData.sort" placeholder="请输入排序" :precision="0" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, unref } from "vue";
|
||||||
|
import useSubmit from "@/composables/useSubmit";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
record: Object,
|
||||||
|
options: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { formRef, formData, handleSubmit } = useSubmit({
|
||||||
|
name: "",
|
||||||
|
pid: "0",
|
||||||
|
slug: "",
|
||||||
|
sort: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const _options = computed(() => {
|
||||||
|
return [{ id: "0", name: "顶部权限", children: unref(props.options ?? []) }];
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(formData, props.record);
|
||||||
|
|
||||||
|
defineExpose({ handleSubmit });
|
||||||
|
</script>
|
25
new-ui/projects/admin/src/views/SysPermission/api.ts
Normal file
25
new-ui/projects/admin/src/views/SysPermission/api.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import http from "@/http/config";
|
||||||
|
|
||||||
|
export const getList = (params) => {
|
||||||
|
return http({
|
||||||
|
url: "/api/admin/sysPermission/list",
|
||||||
|
method: "get",
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const save = (data) => {
|
||||||
|
return http({
|
||||||
|
url: "/api/admin/sysPermission/save",
|
||||||
|
method: "post",
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remove = (data) => {
|
||||||
|
return http({
|
||||||
|
url: "/api/admin/sysPermission/remove",
|
||||||
|
method: "post",
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user