diff --git a/build/plugins/router.ts b/build/plugins/router.ts index 40a5ae51..65460f92 100644 --- a/build/plugins/router.ts +++ b/build/plugins/router.ts @@ -19,7 +19,8 @@ export function setupElegantRouter() { 'document_vite', 'document_unocss', 'document_naive', - 'document_antd' + 'document_antd', + 'document_alova' ] }, routePathTransformer(routeName, routePath) { diff --git a/package.json b/package.json index 5221abc2..907b26e8 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@antv/g2": "5.2.5", "@better-scroll/core": "2.5.1", "@iconify/vue": "4.1.2", + "@sa/alova": "workspace:*", "@sa/axios": "workspace:*", "@sa/color": "workspace:*", "@sa/hooks": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5d24466..d8bfa5dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@iconify/vue': specifier: 4.1.2 version: 4.1.2(vue@3.5.11(typescript@5.6.3)) + '@sa/alova': + specifier: workspace:^ + version: link:packages/alova '@sa/axios': specifier: workspace:* version: link:packages/axios diff --git a/src/assets/svg-icon/alova.svg b/src/assets/svg-icon/alova.svg new file mode 100644 index 00000000..a21d0e27 --- /dev/null +++ b/src/assets/svg-icon/alova.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts index a61a57e0..4cf367bc 100644 --- a/src/locales/langs/en-us.ts +++ b/src/locales/langs/en-us.ts @@ -163,9 +163,14 @@ const local: App.I18n.Schema = { document_unocss: 'UnoCSS Document', document_naive: 'Naive UI Document', document_antd: 'Ant Design Vue Document', + document_alova: 'Alova Document', 'user-center': 'User Center', about: 'About', function: 'System Function', + alova: 'Alova Example', + alova_request: 'Alova Request', + alova_user: 'User List', + alova_scenes: 'Scenario Request', function_tab: 'Tab', 'function_multi-tab': 'Multi Tab', 'function_hide-child': 'Hide Child', @@ -337,6 +342,20 @@ const local: App.I18n.Schema = { repeatedErrorMsg2: 'Custom Request Error 2' } }, + alova: { + scenes: { + captchaSend: 'Captcha Send', + autoRequest: 'Auto Request', + visibilityRequestTips: 'Automatically request when switching browser window', + pollingRequestTips: 'It will request every 3 seconds', + networkRequestTips: 'Automatically request after network reconnecting', + refreshTime: 'Refresh Time', + startRequest: 'Start Request', + stopRequest: 'Stop Request', + requestCrossComponent: 'Request Cross Component', + triggerAllRequest: 'Manually Trigger All Automated Requests' + } + }, manage: { common: { status: { diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts index a796f60d..5bdbda26 100644 --- a/src/locales/langs/zh-cn.ts +++ b/src/locales/langs/zh-cn.ts @@ -163,9 +163,14 @@ const local: App.I18n.Schema = { document_unocss: 'UnoCSS文档', document_naive: 'Naive UI文档', document_antd: 'Ant Design Vue文档', + document_alova: 'Alova文档', 'user-center': '个人中心', about: '关于', function: '系统功能', + alova: 'alova示例', + alova_request: 'alova请求', + alova_user: '用户列表', + alova_scenes: '场景化请求', function_tab: '标签页', 'function_multi-tab': '多标签页', 'function_hide-child': '隐藏子菜单', @@ -337,6 +342,20 @@ const local: App.I18n.Schema = { repeatedErrorMsg2: '自定义请求错误 2' } }, + alova: { + scenes: { + captchaSend: '发送验证码', + autoRequest: '自动请求', + visibilityRequestTips: '浏览器窗口切换自动请求数据', + pollingRequestTips: '每3秒自动请求一次', + networkRequestTips: '网络重连后自动请求', + refreshTime: '更新时间', + startRequest: '开始请求', + stopRequest: '停止请求', + requestCrossComponent: '跨组件触发请求', + triggerAllRequest: '手动触发所有自动请求' + } + }, manage: { common: { status: { diff --git a/src/router/elegant/imports.ts b/src/router/elegant/imports.ts index c992e847..b06a9180 100644 --- a/src/router/elegant/imports.ts +++ b/src/router/elegant/imports.ts @@ -21,6 +21,9 @@ export const views: Record Promise import("@/views/_builtin/iframe-page/[url].vue"), login: () => import("@/views/_builtin/login/index.vue"), about: () => import("@/views/about/index.vue"), + alova_request: () => import("@/views/alova/request/index.vue"), + alova_scenes: () => import("@/views/alova/scenes/index.vue"), + alova_user: () => import("@/views/alova/user/index.vue"), "function_hide-child_one": () => import("@/views/function/hide-child/one/index.vue"), "function_hide-child_three": () => import("@/views/function/hide-child/three/index.vue"), "function_hide-child_two": () => import("@/views/function/hide-child/two/index.vue"), diff --git a/src/router/elegant/routes.ts b/src/router/elegant/routes.ts index 30a21da5..ed5a3dff 100644 --- a/src/router/elegant/routes.ts +++ b/src/router/elegant/routes.ts @@ -50,6 +50,51 @@ export const generatedRoutes: GeneratedRoute[] = [ order: 10 } }, + { + name: 'alova', + path: '/alova', + component: 'layout.base', + meta: { + title: 'alova', + i18nKey: 'route.alova', + icon: 'carbon:http', + order: 7 + }, + children: [ + { + name: 'alova_request', + path: '/alova/request', + component: 'view.alova_request', + meta: { + title: 'alova_request', + i18nKey: 'route.alova_request', + order: 1 + } + }, + { + name: 'alova_scenes', + path: '/alova/scenes', + component: 'view.alova_scenes', + meta: { + title: 'alova_scenes', + i18nKey: 'route.alova_scenes', + icon: 'cbi:scene-dynamic', + order: 3 + } + }, + { + name: 'alova_user', + path: '/alova/user', + component: 'view.alova_user', + meta: { + title: 'alova_user', + i18nKey: 'route.alova_user', + icon: 'carbon:user-multiple', + order: 2 + } + } + ] + }, { name: 'function', path: '/function', diff --git a/src/router/elegant/transform.ts b/src/router/elegant/transform.ts index 05d86a8a..734197e6 100644 --- a/src/router/elegant/transform.ts +++ b/src/router/elegant/transform.ts @@ -175,10 +175,15 @@ const routeMap: RouteMap = { "document_unocss": "/document/unocss", "document_naive": "/document/naive", "document_antd": "/document/antd", + "document_alova": "/document/alova", "403": "/403", "404": "/404", "500": "/500", "about": "/about", + "alova": "/alova", + "alova_request": "/alova/request", + "alova_scenes": "/alova/scenes", + "alova_user": "/alova/user", "function": "/function", "function_hide-child": "/function/hide-child", "function_hide-child_one": "/function/hide-child/one", diff --git a/src/router/routes/index.ts b/src/router/routes/index.ts index 96322a9e..5be1e3b7 100644 --- a/src/router/routes/index.ts +++ b/src/router/routes/index.ts @@ -91,6 +91,20 @@ const customRoutes: CustomRoute[] = [ icon: 'logos:naiveui' } }, + { + name: 'document_alova', + path: '/document/alova', + component: 'view.iframe-page', + props: { + url: 'https://alova.js.org' + }, + meta: { + title: 'document_alova', + i18nKey: 'route.document_alova', + order: 7, + localIcon: 'alova' + } + }, { name: 'document_project', path: '/document/project', diff --git a/src/serviceAlova/api/auth.ts b/src/serviceAlova/api/auth.ts new file mode 100644 index 00000000..e484ad2a --- /dev/null +++ b/src/serviceAlova/api/auth.ts @@ -0,0 +1,56 @@ +import { alova } from '../request'; + +/** + * Login + * + * @param userName User name + * @param password Password + */ +export function fetchLogin(userName: string, password: string) { + return alova.Post('/auth/login', { userName, password }); +} + +/** Get user info */ +export function fetchGetUserInfo() { + return alova.Get('/auth/getUserInfo'); +} + +/** Send captcha to target phone */ +export function sendCaptcha(phone: string) { + return alova.Post('/auth/sendCaptcha', { phone }); +} + +/** Verify captcha */ +export function verifyCaptcha(phone: string, code: string) { + return alova.Post('/auth/verifyCaptcha', { phone, code }); +} + +/** + * Refresh token + * + * @param refreshToken Refresh token + */ +export function fetchRefreshToken(refreshToken: string) { + return alova.Post( + '/auth/refreshToken', + { refreshToken }, + { + meta: { + authRole: 'refreshToken' + } + } + ); +} + +/** + * return custom backend error + * + * @param code error code + * @param msg error message + */ +export function fetchCustomBackendError(code: string, msg: string) { + return alova.Get('/auth/error', { + params: { code, msg }, + shareRequest: false + }); +} diff --git a/src/serviceAlova/api/index.ts b/src/serviceAlova/api/index.ts new file mode 100644 index 00000000..c9d31d11 --- /dev/null +++ b/src/serviceAlova/api/index.ts @@ -0,0 +1,3 @@ +export * from './auth'; +export * from './route'; +export * from './system-manage'; diff --git a/src/serviceAlova/api/route.ts b/src/serviceAlova/api/route.ts new file mode 100644 index 00000000..94f1c983 --- /dev/null +++ b/src/serviceAlova/api/route.ts @@ -0,0 +1,20 @@ +import { alova } from '../request'; + +/** get constant routes */ +export function fetchGetConstantRoutes() { + return alova.Get('/route/getConstantRoutes'); +} + +/** get user routes */ +export function fetchGetUserRoutes() { + return alova.Get('/route/getUserRoutes'); +} + +/** + * whether the route is exist + * + * @param routeName route name + */ +export function fetchIsRouteExist(routeName: string) { + return alova.Get('/route/isRouteExist', { params: { routeName } }); +} diff --git a/src/serviceAlova/api/system-manage.ts b/src/serviceAlova/api/system-manage.ts new file mode 100644 index 00000000..986b5daf --- /dev/null +++ b/src/serviceAlova/api/system-manage.ts @@ -0,0 +1,59 @@ +import { alova } from '../request'; + +/** get role list */ +export function fetchGetRoleList(params?: Api.SystemManage.RoleSearchParams) { + return alova.Get('/systemManage/getRoleList', { params }); +} + +/** + * get all roles + * + * these roles are all enabled + */ +export function fetchGetAllRoles() { + return alova.Get('/systemManage/getAllRoles'); +} + +/** get user list */ +export function fetchGetUserList(params?: Api.SystemManage.UserSearchParams) { + return alova.Get('/systemManage/getUserList', { params }); +} + +export type UserModel = Pick< + Api.SystemManage.User, + 'userName' | 'userGender' | 'nickName' | 'userPhone' | 'userEmail' | 'userRoles' | 'status' +>; +/** add user */ +export function addUser(data: UserModel) { + return alova.Post('/systemManage/addUser', data); +} + +/** update user */ +export function updateUser(data: UserModel) { + return alova.Post('/systemManage/updateUser', data); +} + +/** delete user */ +export function deleteUser(id: number) { + return alova.Delete('/systemManage/deleteUser', { id }); +} + +/** batch delete user */ +export function batchDeleteUser(ids: number[]) { + return alova.Delete('/systemManage/batchDeleteUser', { ids }); +} + +/** get menu list */ +export function fetchGetMenuList() { + return alova.Get('/systemManage/getMenuList/v2'); +} + +/** get all pages */ +export function fetchGetAllPages() { + return alova.Get('/systemManage/getAllPages'); +} + +/** get menu tree */ +export function fetchGetMenuTree() { + return alova.Get('/systemManage/getMenuTree'); +} diff --git a/src/serviceAlova/mocks/feature-users-20241014.ts b/src/serviceAlova/mocks/feature-users-20241014.ts new file mode 100644 index 00000000..3746e130 --- /dev/null +++ b/src/serviceAlova/mocks/feature-users-20241014.ts @@ -0,0 +1,56 @@ +import { defineMock } from '@sa/alova/mock'; + +// you can separate the mock data into multiple files dependent on your project versions +export default defineMock({ + '[POST]/systemManage/addUser': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '[POST]/systemManage/updateUser': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '[DELETE]/systemManage/deleteUser': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '[DELETE]/systemManage/batchDeleteUser': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '[POST]/auth/sendCaptcha': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '[POST]/auth/verifyCaptcha': () => { + return { + code: '0000', + msg: 'success', + data: null + }; + }, + '/mock/getLastTime': () => { + return { + code: '0000', + msg: 'success', + data: { + time: new Date().toLocaleTimeString() + } + }; + } +}); diff --git a/src/serviceAlova/request/index.ts b/src/serviceAlova/request/index.ts new file mode 100644 index 00000000..5b9ba105 --- /dev/null +++ b/src/serviceAlova/request/index.ts @@ -0,0 +1,115 @@ +import { createAlovaRequest } from '@sa/alova'; +import { createAlovaMockAdapter } from '@sa/alova/mock'; +import adapterFetch from '@sa/alova/fetch'; +import { useAuthStore } from '@/store/modules/auth'; +import { $t } from '@/locales'; +import { getServiceBaseURL } from '@/utils/service'; +import featureUsers20241014 from '../mocks/feature-users-20241014'; +import { getAuthorization, handleRefreshToken, showErrorMsg } from './shared'; +import type { RequestInstanceState } from './type'; + +const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y'; +const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy); + +const state: RequestInstanceState = { + errMsgStack: [] +}; +const mockAdapter = createAlovaMockAdapter([featureUsers20241014], { + // using requestAdapter if not match mock request + httpAdapter: adapterFetch(), + + // response delay time + delay: 1000, + + // global mock toggle + enable: true, + matchMode: 'methodurl' +}); +export const alova = createAlovaRequest( + { + baseURL, + requestAdapter: import.meta.env.DEV ? mockAdapter : adapterFetch() + }, + { + onRequest({ config }) { + const Authorization = getAuthorization(); + config.headers.Authorization = Authorization; + config.headers.apifoxToken = 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'; + }, + tokenRefresher: { + async isExpired(response) { + const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || []; + const { code } = await response.clone().json(); + return expiredTokenCodes.includes(String(code)); + }, + async handler() { + await handleRefreshToken(); + } + }, + async isBackendSuccess(response) { + // when the backend response code is "0000"(default), it means the request is success + // to change this logic by yourself, you can modify the `VITE_SERVICE_SUCCESS_CODE` in `.env` file + const resp = response.clone(); + const data = await resp.json(); + return String(data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE; + }, + async transformBackendResponse(response) { + return (await response.clone().json()).data; + }, + async onError(error, response) { + const authStore = useAuthStore(); + + let message = error.message; + let responseCode = ''; + if (response) { + const data = await response?.clone().json(); + message = data.msg; + responseCode = String(data.code); + } + + function handleLogout() { + showErrorMsg(state, message); + authStore.resetStore(); + } + + function logoutAndCleanup() { + handleLogout(); + window.removeEventListener('beforeunload', handleLogout); + state.errMsgStack = state.errMsgStack.filter(msg => msg !== message); + } + + // when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page + const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || []; + if (logoutCodes.includes(responseCode)) { + handleLogout(); + throw error; + } + + // when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal + const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || []; + if (modalLogoutCodes.includes(responseCode) && !state.errMsgStack?.includes(message)) { + state.errMsgStack = [...(state.errMsgStack || []), message]; + + // prevent the user from refreshing the page + window.addEventListener('beforeunload', handleLogout); + + window.$dialog?.error({ + title: $t('common.error'), + content: message, + positiveText: $t('common.confirm'), + maskClosable: false, + closeOnEsc: false, + onPositiveClick() { + logoutAndCleanup(); + }, + onClose() { + logoutAndCleanup(); + } + }); + throw error; + } + showErrorMsg(state, message); + throw error; + } + } +); diff --git a/src/serviceAlova/request/shared.ts b/src/serviceAlova/request/shared.ts new file mode 100644 index 00000000..8d3cf38f --- /dev/null +++ b/src/serviceAlova/request/shared.ts @@ -0,0 +1,53 @@ +import { useAuthStore } from '@/store/modules/auth'; +import { localStg } from '@/utils/storage'; +import { fetchRefreshToken } from '../api'; +import type { RequestInstanceState } from './type'; + +export function getAuthorization() { + const token = localStg.get('token'); + const Authorization = token ? `Bearer ${token}` : null; + + return Authorization; +} + +/** refresh token */ +export async function handleRefreshToken() { + const { resetStore } = useAuthStore(); + + const rToken = localStg.get('refreshToken') || ''; + const refreshTokenMethod = fetchRefreshToken(rToken); + + // set the refreshToken role, so that the request will not be intercepted + refreshTokenMethod.meta.authRole = 'refreshToken'; + + try { + const data = await refreshTokenMethod; + localStg.set('token', data.token); + localStg.set('refreshToken', data.refreshToken); + } catch (error) { + resetStore(); + throw error; + } +} + +export function showErrorMsg(state: RequestInstanceState, message: string) { + if (!state.errMsgStack?.length) { + state.errMsgStack = []; + } + + const isExist = state.errMsgStack.includes(message); + + if (!isExist) { + state.errMsgStack.push(message); + + window.$message?.error(message, { + onLeave: () => { + state.errMsgStack = state.errMsgStack.filter(msg => msg !== message); + + setTimeout(() => { + state.errMsgStack = []; + }, 5000); + } + }); + } +} diff --git a/src/serviceAlova/request/type.ts b/src/serviceAlova/request/type.ts new file mode 100644 index 00000000..5f5ce5c8 --- /dev/null +++ b/src/serviceAlova/request/type.ts @@ -0,0 +1,4 @@ +export interface RequestInstanceState { + /** the request error message stack */ + errMsgStack: string[]; +} diff --git a/src/typings/app.d.ts b/src/typings/app.d.ts index f3a807f6..ae67f22e 100644 --- a/src/typings/app.d.ts +++ b/src/typings/app.d.ts @@ -523,6 +523,20 @@ declare namespace App { repeatedErrorMsg2: string; }; }; + alova: { + scenes: { + captchaSend: string; + autoRequest: string; + visibilityRequestTips: string; + pollingRequestTips: string; + networkRequestTips: string; + refreshTime: string; + startRequest: string; + stopRequest: string; + requestCrossComponent: string; + triggerAllRequest: string; + }; + }; manage: { common: { status: { diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 09f7cdd1..4a1ab6af 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -19,6 +19,8 @@ declare module 'vue' { IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default'] IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default'] IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default'] + IconCarbonPlay: typeof import('~icons/carbon/play')['default'] + IconCarbonStop: typeof import('~icons/carbon/stop')['default'] 'IconCharm:download': typeof import('~icons/charm/download')['default'] 'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default'] IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default'] @@ -45,6 +47,7 @@ declare module 'vue' { LangSwitch: typeof import('./../components/common/lang-switch.vue')['default'] LookForward: typeof import('./../components/custom/look-forward.vue')['default'] MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default'] + NAlert: typeof import('naive-ui')['NAlert'] NBreadcrumb: typeof import('naive-ui')['NBreadcrumb'] NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem'] NButton: typeof import('naive-ui')['NButton'] @@ -87,6 +90,7 @@ declare module 'vue' { NSelect: typeof import('naive-ui')['NSelect'] NSkeleton: typeof import('naive-ui')['NSkeleton'] NSpace: typeof import('naive-ui')['NSpace'] + NSpin: typeof import('naive-ui')['NSpin'] NStatistic: typeof import('naive-ui')['NStatistic'] NSwitch: typeof import('naive-ui')['NSwitch'] NTab: typeof import('naive-ui')['NTab'] diff --git a/src/typings/elegant-router.d.ts b/src/typings/elegant-router.d.ts index 7c905d73..3dbe3e82 100644 --- a/src/typings/elegant-router.d.ts +++ b/src/typings/elegant-router.d.ts @@ -29,10 +29,15 @@ declare module "@elegant-router/types" { "document_unocss": "/document/unocss"; "document_naive": "/document/naive"; "document_antd": "/document/antd"; + "document_alova": "/document/alova"; "403": "/403"; "404": "/404"; "500": "/500"; "about": "/about"; + "alova": "/alova"; + "alova_request": "/alova/request"; + "alova_scenes": "/alova/scenes"; + "alova_user": "/alova/user"; "function": "/function"; "function_hide-child": "/function/hide-child"; "function_hide-child_one": "/function/hide-child/one"; @@ -107,6 +112,7 @@ declare module "@elegant-router/types" { | "document_unocss" | "document_naive" | "document_antd" + | "document_alova" >; /** @@ -123,6 +129,7 @@ declare module "@elegant-router/types" { | "404" | "500" | "about" + | "alova" | "function" | "home" | "iframe-page" @@ -155,6 +162,9 @@ declare module "@elegant-router/types" { | "iframe-page" | "login" | "about" + | "alova_request" + | "alova_scenes" + | "alova_user" | "function_hide-child_one" | "function_hide-child_three" | "function_hide-child_two" @@ -205,6 +215,7 @@ declare module "@elegant-router/types" { | "document_unocss" | "document_naive" | "document_antd" + | "document_alova" >; /** diff --git a/src/views/alova/request/index.vue b/src/views/alova/request/index.vue new file mode 100644 index 00000000..e5dee1f1 --- /dev/null +++ b/src/views/alova/request/index.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/views/alova/scenes/index.vue b/src/views/alova/scenes/index.vue new file mode 100644 index 00000000..e408bf44 --- /dev/null +++ b/src/views/alova/scenes/index.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/src/views/alova/scenes/modules/browser-visibility-request.vue b/src/views/alova/scenes/modules/browser-visibility-request.vue new file mode 100644 index 00000000..0a770450 --- /dev/null +++ b/src/views/alova/scenes/modules/browser-visibility-request.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/views/alova/scenes/modules/captcha-verification.vue b/src/views/alova/scenes/modules/captcha-verification.vue new file mode 100644 index 00000000..c5f3ab6f --- /dev/null +++ b/src/views/alova/scenes/modules/captcha-verification.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/src/views/alova/scenes/modules/cross-component-request.vue b/src/views/alova/scenes/modules/cross-component-request.vue new file mode 100644 index 00000000..404c3544 --- /dev/null +++ b/src/views/alova/scenes/modules/cross-component-request.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/views/alova/scenes/modules/network-toggle-request.vue b/src/views/alova/scenes/modules/network-toggle-request.vue new file mode 100644 index 00000000..f11bd5a0 --- /dev/null +++ b/src/views/alova/scenes/modules/network-toggle-request.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/views/alova/scenes/modules/polling-request.vue b/src/views/alova/scenes/modules/polling-request.vue new file mode 100644 index 00000000..0efbf5c6 --- /dev/null +++ b/src/views/alova/scenes/modules/polling-request.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/views/alova/user/hooks/use-checked-columns.ts b/src/views/alova/user/hooks/use-checked-columns.ts new file mode 100644 index 00000000..2d4edfa4 --- /dev/null +++ b/src/views/alova/user/hooks/use-checked-columns.ts @@ -0,0 +1,78 @@ +import type { TableColumnCheck } from '@sa/hooks'; +import { computed, ref } from 'vue'; +import type { DataTableBaseColumn, DataTableColumn } from 'naive-ui'; +import { $t } from '@/locales'; +import type { AlovaGenerics, Method } from '~/packages/alova/src'; + +function isTableColumnHasKey(column: DataTableColumn): column is DataTableBaseColumn { + return Boolean((column as NaiveUI.TableColumnWithKey).key); +} + +type TableAlovaApiFn = ( + params: R +) => Method>>; + +// this hook is used to manage table columns +// if you choose alova, you can move this hook to the `src/hooks` to handle all list page in your project +export default function useCheckedColumns>['records'][number]>( + getColumns: () => DataTableColumn[] +) { + const SELECTION_KEY = '__selection__'; + + const EXPAND_KEY = '__expand__'; + + const getColumnChecks = (cols: DataTableColumn[]) => { + const checks: NaiveUI.TableColumnCheck[] = []; + cols.forEach(column => { + if (isTableColumnHasKey(column)) { + checks.push({ + key: column.key as string, + title: column.title as string, + checked: true + }); + } else if (column.type === 'selection') { + checks.push({ + key: SELECTION_KEY, + title: $t('common.check'), + checked: true + }); + } else if (column.type === 'expand') { + checks.push({ + key: EXPAND_KEY, + title: $t('common.expandColumn'), + checked: true + }); + } + }); + + return checks; + }; + + const columnChecks = ref(getColumnChecks(getColumns())); + + const columns = computed(() => { + const cols = getColumns(); + const columnMap = new Map>(); + + cols.forEach(column => { + if (isTableColumnHasKey(column)) { + columnMap.set(column.key as string, column); + } else if (column.type === 'selection') { + columnMap.set(SELECTION_KEY, column); + } else if (column.type === 'expand') { + columnMap.set(EXPAND_KEY, column); + } + }); + + const filteredColumns = columnChecks.value + .filter(item => item.checked) + .map(check => columnMap.get(check.key) as NaiveUI.TableColumn); + + return filteredColumns; + }); + + return { + columnChecks, + columns + }; +} diff --git a/src/views/alova/user/hooks/use-table-operate.ts b/src/views/alova/user/hooks/use-table-operate.ts new file mode 100644 index 00000000..a8ed5921 --- /dev/null +++ b/src/views/alova/user/hooks/use-table-operate.ts @@ -0,0 +1,83 @@ +import { useBoolean } from '@sa/hooks'; +import type { Ref } from 'vue'; +import { ref } from 'vue'; +import { jsonClone } from '@sa/utils'; +import { $t } from '@/locales'; + +type TableData = NaiveUI.TableData; +interface Operations { + delete?: (row: T) => Promise; + batchDelete?: (rows: T[]) => Promise; +} + +// this hook is used to handle the table operations +// if you choose alova, you can move this hook to the `src/hooks` to handle all list page in your project +export default function useTableOperate(data: Ref, operations: Operations) { + const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean(); + const { bool: deleting, setTrue: deletify, setFalse: antiDelete } = useBoolean(); + const { bool: batchDeleting, setTrue: batchDeletify, setFalse: antiBatchDelete } = useBoolean(); + + const operateType = ref('add'); + + const getRowByDataId = (id: T['id']) => data.value.find(item => item.id === id) || null; + + function handleAdd() { + operateType.value = 'add'; + openDrawer(); + } + + /** the editing row data */ + const editingData: Ref = ref(null); + + function handleEdit(id: T['id']) { + operateType.value = 'edit'; + editingData.value = jsonClone(getRowByDataId(id)); + + openDrawer(); + } + + /** the checked row keys of table */ + const checkedRowKeys = ref([]); + + /** handler to batch delete rows */ + async function handleBatchDelete() { + batchDeletify(); + try { + const rows = checkedRowKeys.value.map(id => getRowByDataId(id)).filter(Boolean); + await operations.batchDelete?.(rows as T[]); + window.$message?.success($t('common.deleteSuccess')); + checkedRowKeys.value = []; + } finally { + antiBatchDelete(); + } + } + + /** handler to delete row */ + async function handleDelete(id: T['id']) { + deletify(); + const row = getRowByDataId(id); + if (!row) return; + try { + await operations.delete?.(row); + window.$message?.success($t('common.deleteSuccess')); + checkedRowKeys.value = []; + } finally { + antiDelete(); + } + } + + return { + drawerVisible, + openDrawer, + closeDrawer, + operateType, + handleAdd, + editingData, + handleEdit, + checkedRowKeys, + deleting, + handleDelete, + batchDeleting, + handleBatchDelete + }; +} diff --git a/src/views/alova/user/index.vue b/src/views/alova/user/index.vue new file mode 100644 index 00000000..1d9ccbe5 --- /dev/null +++ b/src/views/alova/user/index.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/src/views/alova/user/modules/user-operate-drawer.vue b/src/views/alova/user/modules/user-operate-drawer.vue new file mode 100644 index 00000000..9c07130c --- /dev/null +++ b/src/views/alova/user/modules/user-operate-drawer.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/src/views/alova/user/modules/user-search.vue b/src/views/alova/user/modules/user-search.vue new file mode 100644 index 00000000..9b458d79 --- /dev/null +++ b/src/views/alova/user/modules/user-search.vue @@ -0,0 +1,113 @@ + + + + +