Quellcode durchsuchen

feat: 修改BUG

DESKTOP-USV654P\pc vor 4 Monaten
Ursprung
Commit
183d4101d0

+ 2 - 0
apps/baicai-cms/.env.production

@@ -17,3 +17,5 @@ VITE_INJECT_APP_LOADING=true
 
 # 打包后是否生成dist.zip
 VITE_ARCHIVER=true
+
+VITE_APP_TITLE=众诚信息技术

+ 1 - 0
apps/baicai-cms/src/api/site/site.ts

@@ -10,6 +10,7 @@ export namespace SiteApi {
     code: string;
     path: string;
     status: number;
+    [key: string]: any;
   }
 
   export interface SiteChannelItem {

+ 4 - 1
apps/baicai-cms/src/components/bc-list/bc-list-item/bc-list-item.vue

@@ -1,6 +1,7 @@
 <script lang="ts" setup>
 import type { ArticleApi } from '#/api';
 
+import { useNavigation } from '#/components/bc-nav/src/use-navigation';
 import { errorImage } from '#/utils';
 
 interface Props {
@@ -9,11 +10,13 @@ interface Props {
 }
 
 withDefaults(defineProps<Props>(), {});
+
+const { redirect } = useNavigation();
 </script>
 <template>
   <a
     class="shadow-border-200 overflow-hidden rounded bg-white text-slate-500 shadow-md"
-    :href="`${href}?id=${item.id}`"
+    @click="redirect(href, { id: item.id })"
     target="_blank"
   >
     <div class="p-6">

+ 7 - 1
apps/baicai-cms/src/components/bc-nav/src/use-navigation.ts

@@ -53,11 +53,17 @@ function useNavigation() {
     }
   };
 
+  const redirect = (path: string, query: any) => {
+    const href = router.resolve({ path, query }).href;
+    openWindow(href, { target: '_blank' });
+    // openRouteInNewWindow(href);
+  };
+
   const willOpenedByWindow = (path: string) => {
     return shouldOpenInNewWindow(path);
   };
 
-  return { navigation, willOpenedByWindow };
+  return { navigation, willOpenedByWindow, redirect };
 }
 
 export { useNavigation };

+ 1 - 4
apps/baicai-cms/src/layouts/page/layout.vue

@@ -37,10 +37,7 @@ const wrapperMenus = (menus: MenuRecordRaw[], deep: boolean = true) => {
     <template #header>
       <div class="mx-2 flex py-2">
         <div class="mr-8 flex w-[46px] items-center justify-center">
-          <img
-            src="https://vben.vitejs.dev/logo.png"
-            class="h-[32px] w-[32px]"
-          />
+          <img :src="webStore.config?.logo" class="h-[32px] w-[32px]" />
         </div>
         <BcNav :menus="wrapperMenus(menus)" class="w-full" />
         <div class="flex items-center justify-center">

+ 32 - 28
apps/baicai-cms/src/router/guard.ts

@@ -6,7 +6,7 @@ import { useAccessStore, useUserStore } from '@vben/stores';
 import { startProgress, stopProgress } from '@vben/utils';
 
 import { accessRoutes, coreRouteNames } from '#/router/routes';
-import { useAuthStore } from '#/store';
+import { useAuthStore, useWebStore } from '#/store';
 
 import { generateAccess } from './access';
 
@@ -48,6 +48,7 @@ function setupAccessGuard(router: Router) {
     const accessStore = useAccessStore();
     const userStore = useUserStore();
     const authStore = useAuthStore();
+    const webStore = useWebStore();
     // 基本路由,这些路由不需要进入权限拦截
     if (coreRouteNames.includes(to.name as string)) {
       if (to.path === LOGIN_PATH && accessStore.accessToken) {
@@ -61,27 +62,27 @@ function setupAccessGuard(router: Router) {
     }
 
     // accessToken 检查
-    if (!accessStore.accessToken) {
-      // 明确声明忽略权限访问权限,则可以访问
-      if (to.meta.ignoreAccess) {
-        return true;
-      }
-
-      // 没有访问权限,跳转登录页面
-      if (to.fullPath !== LOGIN_PATH) {
-        return {
-          path: LOGIN_PATH,
-          // 如不需要,直接删除 query
-          query:
-            to.fullPath === preferences.app.defaultHomePath
-              ? {}
-              : { redirect: encodeURIComponent(to.fullPath) },
-          // 携带当前跳转的页面,登录后重新跳转该页面
-          replace: true,
-        };
-      }
-      return to;
-    }
+    // if (!accessStore.accessToken) {
+    //   // 明确声明忽略权限访问权限,则可以访问
+    //   if (to.meta.ignoreAccess) {
+    //     return true;
+    //   }
+
+    //   // 没有访问权限,跳转登录页面
+    //   if (to.fullPath !== LOGIN_PATH) {
+    //     return {
+    //       path: LOGIN_PATH,
+    //       // 如不需要,直接删除 query
+    //       query:
+    //         to.fullPath === preferences.app.defaultHomePath
+    //           ? {}
+    //           : { redirect: encodeURIComponent(to.fullPath) },
+    //       // 携带当前跳转的页面,登录后重新跳转该页面
+    //       replace: true,
+    //     };
+    //   }
+    //   return to;
+    // }
 
     // 是否已经生成过动态路由
     if (accessStore.isAccessChecked) {
@@ -90,12 +91,13 @@ function setupAccessGuard(router: Router) {
 
     // 生成路由表
     // 当前登录用户拥有的角色标识列表
-    const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
-    const userRoles = userInfo.roles ?? [];
+    webStore.config || (await authStore.loadConfig());
+    // const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
+    // const userRoles = userInfo.roles ?? [];
 
     // 生成菜单和路由
     const { accessibleMenus, accessibleRoutes } = await generateAccess({
-      roles: userRoles,
+      roles: [],
       router,
       // 则会在菜单中显示,但是访问会被重定向到403
       routes: accessRoutes,
@@ -110,9 +112,11 @@ function setupAccessGuard(router: Router) {
       redirectPath = from.query.redirect as string;
     } else if (to.path === preferences.app.defaultHomePath) {
       redirectPath = preferences.app.defaultHomePath;
-    } else if (userInfo.homePath && to.path === userInfo.homePath) {
-      redirectPath = userInfo.homePath;
-    } else {
+    }
+    // else if (userInfo.homePath && to.path === userInfo.homePath) {
+    //   redirectPath = userInfo.homePath;
+    // }
+    else {
       redirectPath = to.fullPath;
     }
     return {

+ 0 - 1
apps/baicai-cms/src/views/default/hardware/components/menu.vue

@@ -120,7 +120,6 @@ onMounted(async () => {
         >
           <a
             class="flex min-h-[2rem] w-full min-w-0 flex-col items-start justify-center gap-0"
-            href="#"
           >
             <h4 class="text-base">
               {{ item.label }}

+ 4 - 2
apps/baicai-cms/src/views/default/software/components/menu.vue

@@ -9,6 +9,7 @@ import { Loading } from '@vben/common-ui';
 import { cn } from '@vben-core/shared/utils';
 
 import { ArticleApi, SiteApi } from '#/api';
+import { useNavigation } from '#/components/bc-nav/src/use-navigation';
 import { useLazyLoad } from '#/components/useLazyLoad';
 import { $t } from '#/locales';
 import { useWebStore } from '#/store';
@@ -26,6 +27,8 @@ defineProps({
 
 const webSite = useWebStore();
 
+const { redirect } = useNavigation();
+
 const route = useRoute();
 
 const listRef = ref<HTMLDivElement | null>(null);
@@ -112,8 +115,7 @@ onMounted(async () => {
         >
           <a
             class="flex min-h-[2rem] w-full min-w-0 flex-col items-start justify-center gap-0"
-            :href="`${href}?id=${item.id}`"
-            target="_blank"
+            @click="redirect(href, { id: item.id })"
           >
             <h4 class="text-base">
               {{ item.title }}

+ 18 - 0
apps/web-baicai/src/api/core/auth.ts

@@ -6,6 +6,8 @@ export namespace AuthApi {
   export interface LoginParams {
     password: string;
     username?: string;
+    code?: string;
+    token?: string;
   }
 
   /** 登录接口返回值 */
@@ -26,6 +28,8 @@ export async function loginApi(data: AuthApi.LoginParams) {
   const postData = {
     password: encrypt(data.password),
     username: data.username,
+    code: data.code,
+    token: data.token,
   };
   return requestClient.post<AuthApi.LoginResult>('/security/login', postData, {
     withCredentials: true,
@@ -60,3 +64,17 @@ export async function logoutApi() {
 export async function getAccessCodesApi() {
   return requestClient.get<string[]>('/security/permission-list');
 }
+
+/**
+ * 图片验证码
+ */
+export async function createCaptcha(data: any) {
+  return requestClient.post('/captcha', data);
+}
+
+/**
+ * 是否开启验证码
+ */
+export async function getCaptchaFlag() {
+  return requestClient.get('/captcha/open-flag');
+}

+ 70 - 0
apps/web-baicai/src/components/image-captcha/index.vue

@@ -0,0 +1,70 @@
+<script setup lang="ts">
+import type { PropType } from 'vue';
+
+import { onMounted, ref } from 'vue';
+
+import { useVModel } from '@vueuse/core';
+
+const props = defineProps({
+  api: {
+    type: Function as PropType<(params: any) => Promise<unknown>>,
+    default: null,
+  },
+  uuidField: {
+    type: String,
+    default: 'uuid',
+  },
+  base64Field: {
+    type: String,
+    default: 'base64',
+  },
+  prefix: {
+    type: String,
+    default: 'data:image/png;base64,',
+  },
+  value: {
+    type: String,
+    default: '',
+  },
+  params: {
+    type: Object,
+    default: () => {
+      return {};
+    },
+  },
+  immediate: {
+    type: Boolean,
+    default: true,
+  },
+});
+const emit = defineEmits(['update:modelValue']);
+
+const modelValue = useVModel(props, 'value', emit, {
+  defaultValue: props.value,
+  passive: true,
+});
+
+const base64 = ref<string>('');
+const fetch = () => {
+  props.api?.(props.params).then((res: any) => {
+    modelValue.value = res[props.uuidField];
+    base64.value = `${props.prefix}${res[props.base64Field]}`;
+    emit('update:modelValue', res[props.uuidField]);
+  });
+};
+onMounted(() => {
+  if (props.immediate) {
+    fetch();
+  }
+});
+
+defineExpose({
+  resume: fetch,
+});
+</script>
+<template>
+  <div class="m-captcha">
+    <img :src="base64" @click="fetch" />
+  </div>
+</template>
+<style lang="less" scoped></style>

+ 112 - 37
apps/web-baicai/src/views/_core/authentication/login.vue

@@ -1,60 +1,135 @@
 <script lang="ts" setup>
-import type { SliderCaptcha, VbenFormSchema } from '@vben/common-ui';
+import type { VbenFormSchema } from '@vben/common-ui';
 import type { Recordable } from '@vben/types';
 
-import { computed, useTemplateRef } from 'vue';
+import { computed, markRaw, ref, useTemplateRef } from 'vue';
 
 import { AuthenticationLogin, z } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
+import { createCaptcha, getCaptchaFlag } from '#/api';
+import ImageCaptcha from '#/components/image-captcha/index.vue';
 import { useAuthStore } from '#/store';
 
 defineOptions({ name: 'Login' });
 
 const authStore = useAuthStore();
+const captchaFlag = ref(true);
+
+getCaptchaFlag().then((res: any) => {
+  captchaFlag.value = res;
+});
+
+const defaultUserName = ['development', 'mock'].includes(import.meta.env.MODE)
+  ? 'baicai'
+  : '';
+const defaultPassword = ['development', 'mock'].includes(import.meta.env.MODE)
+  ? '123456'
+  : '';
 
 const formSchema = computed((): VbenFormSchema[] => {
-  return [
-    {
-      component: 'VbenInput',
-      componentProps: {
-        placeholder: $t('authentication.usernameTip'),
-      },
-      fieldName: 'username',
-      label: $t('authentication.username'),
-      rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
-    },
-    {
-      component: 'VbenInputPassword',
-      componentProps: {
-        placeholder: $t('authentication.password'),
-      },
-      fieldName: 'password',
-      label: $t('authentication.password'),
-      rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
-    },
-    // {
-    //   component: markRaw(SliderCaptcha),
-    //   fieldName: 'captcha',
-    //   rules: z.boolean().refine((value) => value, {
-    //     message: $t('authentication.verifyRequiredTip'),
-    //   }),
-    // },
-  ];
+  return captchaFlag.value
+    ? [
+        {
+          component: 'VbenInput',
+          defaultValue: defaultUserName,
+          componentProps: {
+            placeholder: $t('authentication.usernameTip'),
+          },
+          fieldName: 'username',
+          label: $t('authentication.username'),
+          rules: z
+            .string()
+            .min(1, { message: $t('authentication.usernameTip') }),
+          formItemClass: 'col-span-12',
+        },
+        {
+          component: 'VbenInputPassword',
+          defaultValue: defaultPassword,
+          componentProps: {
+            placeholder: $t('authentication.password'),
+          },
+          fieldName: 'password',
+          label: $t('authentication.password'),
+          rules: z
+            .string()
+            .min(1, { message: $t('authentication.passwordTip') }),
+          formItemClass: 'col-span-12',
+        },
+        {
+          component: 'VbenInput',
+          // defaultValue: defaultCode,
+          componentProps: {
+            placeholder: '请输入图片验证码',
+          },
+          formItemClass: 'col-span-8',
+          fieldName: 'code',
+          label: '验证码',
+          rules: z.string().min(1, { message: '请输入图片验证码' }),
+        },
+        {
+          component: markRaw(ImageCaptcha),
+          fieldName: 'token',
+          componentProps: {
+            api: createCaptcha,
+            uuidField: 'token',
+            base64Field: 'imageBase64',
+            immediate: true,
+          },
+          formItemClass: 'col-span-4',
+        },
+        // {
+        //   component: markRaw(SliderCaptcha),
+        //   fieldName: 'captcha',
+        //   rules: z.boolean().refine((value) => value, {
+        //     message: $t('authentication.verifyRequiredTip'),
+        //   }),
+        // },
+      ]
+    : [
+        {
+          component: 'VbenInput',
+          defaultValue: defaultUserName,
+          componentProps: {
+            placeholder: $t('authentication.usernameTip'),
+          },
+          fieldName: 'username',
+          label: $t('authentication.username'),
+          rules: z
+            .string()
+            .min(1, { message: $t('authentication.usernameTip') }),
+          formItemClass: 'col-span-12',
+        },
+        {
+          component: 'VbenInputPassword',
+          defaultValue: defaultPassword,
+          componentProps: {
+            placeholder: $t('authentication.password'),
+          },
+          fieldName: 'password',
+          label: $t('authentication.password'),
+          rules: z
+            .string()
+            .min(1, { message: $t('authentication.passwordTip') }),
+          formItemClass: 'col-span-12',
+        },
+      ];
 });
 const loginRef =
   useTemplateRef<InstanceType<typeof AuthenticationLogin>>('loginRef');
 
 async function onSubmit(params: Recordable<any>) {
   authStore.authLogin(params).catch(() => {
-    // 登陆失败,刷新验证码的演示
-    const formApi = loginRef.value?.getFormApi();
-    // 重置验证码组件的值
-    formApi?.setFieldValue('captcha', false, false);
-    // 使用表单API获取验证码组件实例,并调用其resume方法来重置验证码
-    formApi
-      ?.getFieldComponentRef<InstanceType<typeof SliderCaptcha>>('captcha')
-      ?.resume();
+    if (captchaFlag.value) {
+      // 登陆失败,刷新验证码的演示
+      const formApi = loginRef.value?.getFormApi();
+      // 重置验证码组件的值
+      // formApi?.setFieldValue('captcha', false, false);
+      // 使用表单API获取验证码组件实例,并调用其resume方法来重置验证码
+      formApi
+        ?.getFieldComponentRef<InstanceType<typeof ImageCaptcha>>('token')
+        ?.resume();
+    }
   });
 }
 </script>

+ 2 - 2
apps/web-baicai/src/views/system/menu/components/grant.vue

@@ -82,7 +82,7 @@ const [Drawer, { close, setState, getData, lock, unlock }] = useVbenDrawer({
       modelRef.value = { ...data.baseData };
 
       if (state.treeData.length === 0) {
-        loadTreeData();
+        await loadTreeData();
       }
 
       await getRoleDetailData();
@@ -119,7 +119,7 @@ const loadTreeData = async () => {
   <Drawer class="w-[1000px]" title="授权菜单">
     <Form>
       <template #permissions="slotProps">
-        <Spin :spinning="state.loadingTree">
+        <Spin :spinning="state.loadingTree" wrapper-class-name="w-full">
           <VbenTree
             :tree-data="state.treeData"
             multiple

+ 7 - 2
apps/web-baicai/src/views/system/tenant/data.config.ts

@@ -64,9 +64,14 @@ export function useColumns(
               content: `确定创建/更新租户数据库?`,
             },
           },
+          {
+            code: 'grantMenu',
+            label: '授权菜单',
+            auth: ['tenant:grantMenu'],
+          },
           {
             code: 'edit',
-            auth: ['role:edit'],
+            auth: ['tenant:edit'],
           },
           {
             code: 'resetPwd',
@@ -84,7 +89,7 @@ export function useColumns(
       headerAlign: 'center',
       showOverflow: false,
       title: '操作',
-      width: 100,
+      width: 170,
     },
   ];
 }

+ 24 - 6
apps/web-baicai/src/views/system/tenant/index.vue

@@ -1,12 +1,13 @@
 <script lang="ts" setup>
 import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter';
 
-import { Page, useVbenModal } from '@vben/common-ui';
+import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
 
 import { Button, message } from 'ant-design-vue';
 
 import { useTableGridOptions, useVbenVxeGrid } from '#/adapter';
 import { TenantApi } from '#/api';
+import MenuGrant from '#/views/system/menu/components/grant.vue';
 
 import FormEdit from './components/edit.vue';
 import { useColumns, useSearchSchema } from './data.config';
@@ -15,6 +16,10 @@ const [FormEditModal, formEditApi] = useVbenModal({
   connectedComponent: FormEdit,
 });
 
+const [MenuGrantModal, menuGrantApi] = useVbenDrawer({
+  connectedComponent: MenuGrant,
+});
+
 const handleCreateDb = async (id: number) => {
   await TenantApi.createDb(id);
   message.success('创建/更新租户数据库成功');
@@ -27,12 +32,20 @@ const handleDelete = async (id: number) => {
 };
 
 const handleEdit = (record: any, isUpdate: boolean) => {
-  formEditApi.setData({
-    isUpdate,
-    baseData: { id: record.id },
-  });
+  formEditApi
+    .setData({
+      isUpdate,
+      baseData: { id: record.id },
+    })
+    .open();
+};
 
-  formEditApi.open();
+const handleGrantMenu = async (id: number) => {
+  menuGrantApi
+    .setData({
+      baseData: { id, type: 'tenant' },
+    })
+    .open();
 };
 
 const handelSuccess = () => {
@@ -56,6 +69,10 @@ const handleActionClick = async ({
       handleEdit(row, true);
       break;
     }
+    case 'grantMenu': {
+      handleGrantMenu(row.id);
+      break;
+    }
     case 'resetPwd': {
       break;
     }
@@ -88,6 +105,7 @@ const [Grid, { reload }] = useVbenVxeGrid(
 <template>
   <Page auto-content-height>
     <FormEditModal @success="handelSuccess" />
+    <MenuGrantModal />
     <Grid>
       <template #table-title>
         <span class="border-l-primary border-l-8 border-solid pl-2">