mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-10-08 02:46:39 +08:00
Compare commits
91 Commits
0fa49f927b
...
e94a42be6a
Author | SHA1 | Date | |
---|---|---|---|
|
e94a42be6a | ||
|
8a7cd5934b | ||
|
c962f7b2c5 | ||
|
8cc5177cda | ||
|
3a343eea33 | ||
|
f83eefbc3e | ||
|
936b834e62 | ||
|
c965140b87 | ||
|
32b8f99071 | ||
|
abaaa4a068 | ||
|
b4e125300e | ||
|
50a5cba088 | ||
|
d6c8142bb4 | ||
|
1740ca2cfa | ||
|
643ccec544 | ||
|
bf9baf0d14 | ||
|
bbadd41e0e | ||
|
7e1e1716f9 | ||
|
a63a343e9c | ||
|
c816f4dc3b | ||
|
ec20829a22 | ||
|
f5e6a205b3 | ||
|
ff83d333a5 | ||
|
9a294204a2 | ||
|
a5c9a19838 | ||
|
03f098bec6 | ||
|
69788d0134 | ||
|
745a9077ec | ||
|
60c307ccda | ||
|
823651139a | ||
|
d1c6081c25 | ||
|
b9e873e53c | ||
|
aabe7fe592 | ||
|
7e436b6328 | ||
|
e019defef3 | ||
|
a2178d3405 | ||
|
0c0d606ae5 | ||
|
a6545265ac | ||
|
641f3160d6 | ||
|
3fbf2e2a5f | ||
|
405b1b41cc | ||
|
da6cf09e76 | ||
|
5bfc7613f7 | ||
|
7710bcdb0a | ||
|
b36d914e48 | ||
|
bbacff0581 | ||
|
f86bca063a | ||
|
48e005bcaf | ||
|
d825b6e260 | ||
|
05b0147b47 | ||
|
e7cd34ec37 | ||
|
a9dce21878 | ||
|
24320ab899 | ||
|
2f1f2c2b2a | ||
|
3a970a090d | ||
|
4a7e9c2669 | ||
|
d6f3effce0 | ||
|
fa4367a7b9 | ||
|
c425822b3a | ||
|
447bda2b48 | ||
|
c0303a8637 | ||
|
c7ab61bbd4 | ||
|
ec6a497452 | ||
|
074077dfaa | ||
|
fcf866783c | ||
|
1c9e36c47b | ||
|
d3210edc6a | ||
|
799ebcc714 | ||
|
4a8b9032cd | ||
|
8fea803fca | ||
|
599d0f10e8 | ||
|
781f88d227 | ||
|
5739346484 | ||
|
d194f0b1b6 | ||
|
19bea283c9 | ||
|
53a8e5678a | ||
|
10b231918a | ||
|
81d51a9630 | ||
|
fbebef3acb | ||
|
12d7bf94ee | ||
|
0478154db8 | ||
|
994ef2b9b1 | ||
|
ef32eecfdb | ||
|
83ae0bad0f | ||
|
9a9e2d144a | ||
|
a2110a98cf | ||
|
daf6600c21 | ||
|
a9b3a961af | ||
|
3a35d62e6f | ||
|
aaf28a42c2 | ||
|
019f80faef |
@ -8,6 +8,23 @@ export function setupElegantRouter() {
|
||||
base: 'src/layouts/base-layout/index.vue',
|
||||
blank: 'src/layouts/blank-layout/index.vue'
|
||||
},
|
||||
customRoutes: {
|
||||
names: [
|
||||
'exception_403',
|
||||
'exception_404',
|
||||
'exception_500',
|
||||
'document_project',
|
||||
'document_project-link',
|
||||
'document_video',
|
||||
'document_vue',
|
||||
'document_vite',
|
||||
'document_unocss',
|
||||
'document_naive',
|
||||
'document_pro-naive',
|
||||
'document_antd',
|
||||
'document_alova'
|
||||
]
|
||||
},
|
||||
routePathTransformer(routeName, routePath) {
|
||||
const key = routeName as RouteKey;
|
||||
|
||||
|
@ -6,6 +6,7 @@ import Icons from 'unplugin-icons/vite';
|
||||
import IconsResolver from 'unplugin-icons/resolver';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||
import { ProNaiveUIResolver } from 'pro-naive-ui-resolver';
|
||||
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
||||
|
||||
export function setupUnplugin(viteEnv: Env.ImportMeta) {
|
||||
@ -32,6 +33,7 @@ export function setupUnplugin(viteEnv: Env.ImportMeta) {
|
||||
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
||||
resolvers: [
|
||||
NaiveUiResolver(),
|
||||
ProNaiveUIResolver(),
|
||||
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFIX })
|
||||
]
|
||||
}),
|
||||
|
39
package.json
39
package.json
@ -47,35 +47,60 @@
|
||||
"update-pkg": "sa update-pkg"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/data-set": "0.11.8",
|
||||
"@antv/g2": "5.3.4",
|
||||
"@antv/g6": "5.0.49",
|
||||
"@better-scroll/core": "2.5.1",
|
||||
"@iconify/vue": "5.0.0",
|
||||
"@sa/alova": "workspace:*",
|
||||
"@sa/axios": "workspace:*",
|
||||
"@sa/color": "workspace:*",
|
||||
"@sa/hooks": "workspace:*",
|
||||
"@sa/materials": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@visactor/vchart": "2.0.0",
|
||||
"@visactor/vchart-theme": "1.12.2",
|
||||
"@visactor/vtable-editors": "1.19.3",
|
||||
"@visactor/vtable-gantt": "1.19.3",
|
||||
"@visactor/vue-vtable": "1.19.3",
|
||||
"@vueuse/components": "13.5.0",
|
||||
"@vueuse/core": "13.5.0",
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "1.11.13",
|
||||
"defu": "6.1.4",
|
||||
"dhtmlx-gantt": "9.0.13",
|
||||
"dompurify": "3.2.6",
|
||||
"echarts": "5.6.0",
|
||||
"jsbarcode": "3.12.1",
|
||||
"json5": "2.2.3",
|
||||
"naive-ui": "2.42.0",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "3.0.3",
|
||||
"pinyin-pro": "3.26.0",
|
||||
"print-js": "1.6.0",
|
||||
"pro-naive-ui": "2.3.2",
|
||||
"swiper": "11.2.10",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"typeit": "8.8.7",
|
||||
"vditor": "3.11.1",
|
||||
"vue": "3.5.17",
|
||||
"vue-draggable-plus": "0.6.0",
|
||||
"vue-i18n": "11.1.9",
|
||||
"vue-router": "4.5.1"
|
||||
"vue-i18n": "11.1.10",
|
||||
"vue-pdf-embed": "2.1.3",
|
||||
"vue-router": "4.5.1",
|
||||
"wangeditor": "4.7.15",
|
||||
"xgplayer": "3.0.22",
|
||||
"xlsx": "0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@amap/amap-jsapi-types": "0.0.15",
|
||||
"@elegant-router/vue": "0.3.8",
|
||||
"@iconify/json": "2.2.357",
|
||||
"@iconify/json": "2.2.359",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.7.1",
|
||||
"@types/node": "24.0.13",
|
||||
"@types/bmapgl": "0.0.7",
|
||||
"@types/node": "24.0.15",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "66.3.3",
|
||||
"@unocss/preset-icons": "66.3.3",
|
||||
@ -89,18 +114,20 @@
|
||||
"eslint": "9.31.0",
|
||||
"eslint-plugin-vue": "10.3.0",
|
||||
"kolorist": "1.8.0",
|
||||
"lint-staged": "16.1.2",
|
||||
"pro-naive-ui-resolver": "1.0.2",
|
||||
"sass": "1.89.2",
|
||||
"simple-git-hooks": "2.13.0",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "5.8.3",
|
||||
"unplugin-icons": "22.1.0",
|
||||
"unplugin-vue-components": "28.8.0",
|
||||
"vite": "7.0.4",
|
||||
"vite": "7.0.5",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "7.7.7",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.0.1"
|
||||
"vue-tsc": "3.0.3"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"commit-msg": "pnpm sa git-commit-verify",
|
||||
|
@ -13,11 +13,12 @@ import type {
|
||||
ResponseType
|
||||
} from './type';
|
||||
|
||||
function createCommonRequest<ResponseData = any>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
) {
|
||||
const opts = createDefaultOptions<ResponseData>(options);
|
||||
function createCommonRequest<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>
|
||||
>(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||
const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
|
||||
|
||||
const axiosConf = createAxiosConfig(axiosConfig);
|
||||
const instance = axios.create(axiosConf);
|
||||
@ -80,14 +81,6 @@ function createCommonRequest<ResponseData = any>(
|
||||
}
|
||||
);
|
||||
|
||||
function cancelRequest(requestId: string) {
|
||||
const abortController = abortControllerMap.get(requestId);
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortControllerMap.delete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAllRequest() {
|
||||
abortControllerMap.forEach(abortController => {
|
||||
abortController.abort();
|
||||
@ -98,7 +91,6 @@ function createCommonRequest<ResponseData = any>(
|
||||
return {
|
||||
instance,
|
||||
opts,
|
||||
cancelRequest,
|
||||
cancelAllRequest
|
||||
};
|
||||
}
|
||||
@ -109,27 +101,27 @@ function createCommonRequest<ResponseData = any>(
|
||||
* @param axiosConfig axios config
|
||||
* @param options request options
|
||||
*/
|
||||
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
|
||||
export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||
) {
|
||||
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
||||
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||
|
||||
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
) {
|
||||
const request: RequestInstance<ApiData, State> = async function request<
|
||||
T extends ApiData = ApiData,
|
||||
R extends ResponseType = 'json'
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
const response: AxiosResponse<ResponseData> = await instance(config);
|
||||
|
||||
const responseType = response.config?.responseType || 'json';
|
||||
|
||||
if (responseType === 'json') {
|
||||
return opts.transformBackendResponse(response);
|
||||
return opts.transform(response);
|
||||
}
|
||||
|
||||
return response.data as MappedType<R, T>;
|
||||
} as RequestInstance<State>;
|
||||
} as RequestInstance<ApiData, State>;
|
||||
|
||||
request.cancelRequest = cancelRequest;
|
||||
request.cancelAllRequest = cancelAllRequest;
|
||||
request.state = {} as State;
|
||||
|
||||
@ -144,14 +136,14 @@ export function createRequest<ResponseData = any, State = Record<string, unknown
|
||||
* @param axiosConfig axios config
|
||||
* @param options request options
|
||||
*/
|
||||
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
|
||||
export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||
) {
|
||||
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
||||
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||
|
||||
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
|
||||
T = any,
|
||||
const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
|
||||
T extends ApiData = ApiData,
|
||||
R extends ResponseType = 'json'
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
try {
|
||||
@ -160,20 +152,21 @@ export function createFlatRequest<ResponseData = any, State = Record<string, unk
|
||||
const responseType = response.config?.responseType || 'json';
|
||||
|
||||
if (responseType === 'json') {
|
||||
const data = opts.transformBackendResponse(response);
|
||||
const data = await opts.transform(response);
|
||||
|
||||
return { data, error: null, response };
|
||||
}
|
||||
|
||||
return { data: response.data as MappedType<R, T>, error: null };
|
||||
return { data: response.data as MappedType<R, T>, error: null, response };
|
||||
} catch (error) {
|
||||
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
|
||||
}
|
||||
} as FlatRequestInstance<State, ResponseData>;
|
||||
} as FlatRequestInstance<ResponseData, ApiData, State>;
|
||||
|
||||
flatRequest.cancelRequest = cancelRequest;
|
||||
flatRequest.cancelAllRequest = cancelAllRequest;
|
||||
flatRequest.state = {} as State;
|
||||
flatRequest.state = {
|
||||
...opts.defaultState
|
||||
} as State;
|
||||
|
||||
return flatRequest;
|
||||
}
|
||||
|
@ -4,15 +4,27 @@ import { stringify } from 'qs';
|
||||
import { isHttpSuccess } from './shared';
|
||||
import type { RequestOption } from './type';
|
||||
|
||||
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
|
||||
const opts: RequestOption<ResponseData> = {
|
||||
export function createDefaultOptions<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>
|
||||
>(options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||
const opts: RequestOption<ResponseData, ApiData, State> = {
|
||||
defaultState: {} as State,
|
||||
transform: async response => response.data as unknown as ApiData,
|
||||
transformBackendResponse: async response => response.data as unknown as ApiData,
|
||||
onRequest: async config => config,
|
||||
isBackendSuccess: _response => true,
|
||||
onBackendFail: async () => {},
|
||||
transformBackendResponse: async response => response.data,
|
||||
onError: async () => {}
|
||||
};
|
||||
|
||||
if (options?.transform) {
|
||||
opts.transform = options.transform;
|
||||
} else {
|
||||
opts.transform = options?.transformBackendResponse || opts.transform;
|
||||
}
|
||||
|
||||
Object.assign(opts, options);
|
||||
|
||||
return opts;
|
||||
|
@ -8,7 +8,30 @@ export type ContentType =
|
||||
| 'application/x-www-form-urlencoded'
|
||||
| 'application/octet-stream';
|
||||
|
||||
export interface RequestOption<ResponseData = any> {
|
||||
export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>;
|
||||
|
||||
export interface RequestOption<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>
|
||||
> {
|
||||
/**
|
||||
* The default state
|
||||
*/
|
||||
defaultState?: State;
|
||||
/**
|
||||
* transform the response data to the api data
|
||||
*
|
||||
* @param response Axios response
|
||||
*/
|
||||
transform: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
|
||||
/**
|
||||
* transform the response data to the api data
|
||||
*
|
||||
* @deprecated use `transform` instead, will be removed in the next major version v3
|
||||
* @param response Axios response
|
||||
*/
|
||||
transformBackendResponse: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
|
||||
/**
|
||||
* The hook before request
|
||||
*
|
||||
@ -35,12 +58,6 @@ export interface RequestOption<ResponseData = any> {
|
||||
response: AxiosResponse<ResponseData>,
|
||||
instance: AxiosInstance
|
||||
) => Promise<AxiosResponse | null> | Promise<void>;
|
||||
/**
|
||||
* transform backend response when the responseType is json
|
||||
*
|
||||
* @param response Axios response
|
||||
*/
|
||||
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
|
||||
/**
|
||||
* The hook to handle error
|
||||
*
|
||||
@ -68,15 +85,7 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
|
||||
responseType?: R;
|
||||
};
|
||||
|
||||
export interface RequestInstanceCommon<T> {
|
||||
/**
|
||||
* cancel the request by request id
|
||||
*
|
||||
* if the request provide abort controller sign from config, it will not collect in the abort controller map
|
||||
*
|
||||
* @param requestId
|
||||
*/
|
||||
cancelRequest: (requestId: string) => void;
|
||||
export interface RequestInstanceCommon<State extends Record<string, unknown>> {
|
||||
/**
|
||||
* cancel all request
|
||||
*
|
||||
@ -84,32 +93,35 @@ export interface RequestInstanceCommon<T> {
|
||||
*/
|
||||
cancelAllRequest: () => void;
|
||||
/** you can set custom state in the request instance */
|
||||
state: T;
|
||||
state: State;
|
||||
}
|
||||
|
||||
/** The request instance */
|
||||
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
|
||||
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
|
||||
export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig<R>
|
||||
): Promise<MappedType<R, T>>;
|
||||
}
|
||||
|
||||
export type FlatResponseSuccessData<T = any, ResponseData = any> = {
|
||||
data: T;
|
||||
export type FlatResponseSuccessData<ResponseData, ApiData> = {
|
||||
data: ApiData;
|
||||
error: null;
|
||||
response: AxiosResponse<ResponseData>;
|
||||
};
|
||||
|
||||
export type FlatResponseFailData<ResponseData = any> = {
|
||||
export type FlatResponseFailData<ResponseData> = {
|
||||
data: null;
|
||||
error: AxiosError<ResponseData>;
|
||||
response: AxiosResponse<ResponseData>;
|
||||
};
|
||||
|
||||
export type FlatResponseData<T = any, ResponseData = any> =
|
||||
| FlatResponseSuccessData<T, ResponseData>
|
||||
export type FlatResponseData<ResponseData, ApiData> =
|
||||
| FlatResponseSuccessData<ResponseData, ApiData>
|
||||
| FlatResponseFailData<ResponseData>;
|
||||
|
||||
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
|
||||
<T = any, R extends ResponseType = 'json'>(
|
||||
export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
|
||||
extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig<R>
|
||||
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
|
||||
): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>;
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import useLoading from './use-loading';
|
||||
import useCountDown from './use-count-down';
|
||||
import useContext from './use-context';
|
||||
import useSvgIconRender from './use-svg-icon-render';
|
||||
import useHookTable from './use-table';
|
||||
import useTable from './use-table';
|
||||
|
||||
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
|
||||
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useTable };
|
||||
|
||||
export * from './use-signal';
|
||||
export * from './use-table';
|
||||
export type * from './use-table';
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { inject, provide } from 'vue';
|
||||
import type { InjectionKey } from 'vue';
|
||||
|
||||
/**
|
||||
* Use context
|
||||
@ -12,7 +11,7 @@ import type { InjectionKey } from 'vue';
|
||||
* import { ref } from 'vue';
|
||||
* import { useContext } from '@sa/hooks';
|
||||
*
|
||||
* export const { setupStore, useStore } = useContext('demo', () => {
|
||||
* export const [provideDemoContext, useDemoContext] = useContext('demo', () => {
|
||||
* const count = ref(0);
|
||||
*
|
||||
* function increment() {
|
||||
@ -35,10 +34,10 @@ import type { InjectionKey } from 'vue';
|
||||
* <div>A</div>
|
||||
* </template>
|
||||
* <script setup lang="ts">
|
||||
* import { setupStore } from './context';
|
||||
* import { provideDemoContext } from './context';
|
||||
*
|
||||
* setupStore();
|
||||
* // const { increment } = setupStore(); // also can control the store in the parent component
|
||||
* provideDemoContext();
|
||||
* // const { increment } = provideDemoContext(); // also can control the store in the parent component
|
||||
* </script>
|
||||
* ``` // B.vue
|
||||
* ```vue
|
||||
@ -46,9 +45,9 @@ import type { InjectionKey } from 'vue';
|
||||
* <div>B</div>
|
||||
* </template>
|
||||
* <script setup lang="ts">
|
||||
* import { useStore } from './context';
|
||||
* import { useDemoContext } from './context';
|
||||
*
|
||||
* const { count, increment } = useStore();
|
||||
* const { count, increment } = useDemoContext();
|
||||
* </script>
|
||||
* ```;
|
||||
*
|
||||
@ -57,40 +56,41 @@ import type { InjectionKey } from 'vue';
|
||||
* @param contextName Context name
|
||||
* @param fn Context function
|
||||
*/
|
||||
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
|
||||
type Context = ReturnType<T>;
|
||||
export default function useContext<Arguments extends Array<any>, T>(
|
||||
contextName: string,
|
||||
composable: (...args: Arguments) => T
|
||||
) {
|
||||
const key = Symbol(contextName);
|
||||
|
||||
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
|
||||
/**
|
||||
* Injects the context value.
|
||||
*
|
||||
* @param consumerName - The name of the component that is consuming the context. If provided, the component must be
|
||||
* used within the context provider.
|
||||
* @param defaultValue - The default value to return if the context is not provided.
|
||||
* @returns The context value.
|
||||
*/
|
||||
const useInject = <N extends string | null | undefined = undefined>(
|
||||
consumerName?: N,
|
||||
defaultValue?: T
|
||||
): N extends null | undefined ? T | null : T => {
|
||||
const value = inject(key, defaultValue);
|
||||
|
||||
function setupStore(...args: Parameters<T>) {
|
||||
const context: Context = fn(...args);
|
||||
return useProvide(context);
|
||||
}
|
||||
if (consumerName && !value) {
|
||||
throw new Error(`\`${consumerName}\` must be used within \`${contextName}\``);
|
||||
}
|
||||
|
||||
return {
|
||||
/** Setup store in the parent component */
|
||||
setupStore,
|
||||
/** Use store in the child component */
|
||||
useStore
|
||||
// @ts-expect-error - we want to return null if the value is undefined or null
|
||||
return value || null;
|
||||
};
|
||||
}
|
||||
|
||||
/** Create context */
|
||||
function createContext<T>(contextName: string) {
|
||||
const injectKey: InjectionKey<T> = Symbol(contextName);
|
||||
const useProvide = (...args: Arguments) => {
|
||||
const value = composable(...args);
|
||||
|
||||
function useProvide(context: T) {
|
||||
provide(injectKey, context);
|
||||
provide(key, value);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function useInject() {
|
||||
return inject(injectKey) as T;
|
||||
}
|
||||
|
||||
return {
|
||||
useProvide,
|
||||
useInject
|
||||
return value;
|
||||
};
|
||||
|
||||
return [useProvide, useInject] as const;
|
||||
}
|
||||
|
@ -6,31 +6,31 @@ import type {
|
||||
CreateAxiosDefaults,
|
||||
CustomAxiosRequestConfig,
|
||||
MappedType,
|
||||
RequestInstanceCommon,
|
||||
RequestOption,
|
||||
ResponseType
|
||||
} from '@sa/axios';
|
||||
import useLoading from './use-loading';
|
||||
|
||||
export type HookRequestInstanceResponseSuccessData<T = any> = {
|
||||
data: Ref<T>;
|
||||
export type HookRequestInstanceResponseSuccessData<ApiData> = {
|
||||
data: Ref<ApiData>;
|
||||
error: Ref<null>;
|
||||
};
|
||||
|
||||
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
|
||||
export type HookRequestInstanceResponseFailData<ResponseData> = {
|
||||
data: Ref<null>;
|
||||
error: Ref<AxiosError<ResponseData>>;
|
||||
};
|
||||
|
||||
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
|
||||
export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
|
||||
loading: Ref<boolean>;
|
||||
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||
} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||
|
||||
export interface HookRequestInstance<ResponseData = any> {
|
||||
<T = any, R extends ResponseType = 'json'>(
|
||||
export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
|
||||
extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
|
||||
cancelRequest: (requestId: string) => void;
|
||||
cancelAllRequest: () => void;
|
||||
): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,25 +39,26 @@ export interface HookRequestInstance<ResponseData = any> {
|
||||
* @param axiosConfig
|
||||
* @param options
|
||||
*/
|
||||
export default function createHookRequest<ResponseData = any>(
|
||||
export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||
) {
|
||||
const request = createFlatRequest<ResponseData>(axiosConfig, options);
|
||||
const request = createFlatRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||
|
||||
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
) {
|
||||
const hookRequest: HookRequestInstance<ResponseData, ApiData, State> = function hookRequest<
|
||||
T extends ApiData = ApiData,
|
||||
R extends ResponseType = 'json'
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
|
||||
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
|
||||
const data = ref(null) as Ref<MappedType<R, T>>;
|
||||
const error = ref(null) as Ref<AxiosError<ResponseData> | null>;
|
||||
|
||||
startLoading();
|
||||
|
||||
request(config).then(res => {
|
||||
if (res.data) {
|
||||
data.value = res.data;
|
||||
data.value = res.data as MappedType<R, T>;
|
||||
} else {
|
||||
error.value = res.error;
|
||||
}
|
||||
@ -70,9 +71,8 @@ export default function createHookRequest<ResponseData = any>(
|
||||
data,
|
||||
error
|
||||
};
|
||||
} as HookRequestInstance<ResponseData>;
|
||||
} as HookRequestInstance<ResponseData, ApiData, State>;
|
||||
|
||||
hookRequest.cancelRequest = request.cancelRequest;
|
||||
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
||||
|
||||
return hookRequest;
|
||||
|
@ -1,12 +1,20 @@
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { Ref, VNodeChild } from 'vue';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import useBoolean from './use-boolean';
|
||||
import useLoading from './use-loading';
|
||||
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
export interface PaginationData<T> {
|
||||
data: T[];
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export type ApiFn = (args: any) => Promise<unknown>;
|
||||
type GetApiData<ApiData, Pagination extends boolean> = Pagination extends true ? PaginationData<ApiData> : ApiData[];
|
||||
|
||||
type Transform<ResponseData, ApiData, Pagination extends boolean> = (
|
||||
response: ResponseData
|
||||
) => GetApiData<ApiData, Pagination>;
|
||||
|
||||
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
|
||||
|
||||
@ -14,72 +22,64 @@ export type TableColumnCheck = {
|
||||
key: string;
|
||||
title: TableColumnCheckTitle;
|
||||
checked: boolean;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
export type TableDataWithIndex<T> = T & { index: number };
|
||||
|
||||
export type TransformedData<T> = {
|
||||
data: TableDataWithIndex<T>[];
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
|
||||
|
||||
export type TableConfig<A extends ApiFn, T, C> = {
|
||||
/** api function to get table data */
|
||||
apiFn: A;
|
||||
/** api params */
|
||||
apiParams?: Parameters<A>[0];
|
||||
/** transform api response to table data */
|
||||
transformer: Transformer<T, Awaited<ReturnType<A>>>;
|
||||
/** columns factory */
|
||||
columns: () => C[];
|
||||
export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
|
||||
/**
|
||||
* api function to get table data
|
||||
*/
|
||||
api: () => Promise<ResponseData>;
|
||||
/**
|
||||
* whether to enable pagination
|
||||
*/
|
||||
pagination?: Pagination;
|
||||
/**
|
||||
* transform api response to table data
|
||||
*/
|
||||
transform: Transform<ResponseData, ApiData, Pagination>;
|
||||
/**
|
||||
* columns factory
|
||||
*/
|
||||
columns: () => Column[];
|
||||
/**
|
||||
* get column checks
|
||||
*
|
||||
* @param columns
|
||||
*/
|
||||
getColumnChecks: (columns: C[]) => TableColumnCheck[];
|
||||
getColumnChecks: (columns: Column[]) => TableColumnCheck[];
|
||||
/**
|
||||
* get columns
|
||||
*
|
||||
* @param columns
|
||||
*/
|
||||
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
|
||||
getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[];
|
||||
/**
|
||||
* callback when response fetched
|
||||
*
|
||||
* @param transformed transformed data
|
||||
*/
|
||||
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
|
||||
onFetched?: (data: GetApiData<ApiData, Pagination>) => void | Promise<void>;
|
||||
/**
|
||||
* whether to get data immediately
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
immediate?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
|
||||
export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>(
|
||||
options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
|
||||
) {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { bool: empty, setBool: setEmpty } = useBoolean();
|
||||
|
||||
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
|
||||
const { api, pagination, transform, columns, getColumnChecks, getColumns, onFetched, immediate = true } = options;
|
||||
|
||||
const searchParams: NonNullable<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams }));
|
||||
const data = ref([]) as Ref<ApiData[]>;
|
||||
|
||||
const allColumns = ref(config.columns()) as Ref<C[]>;
|
||||
const allColumns = ref(columns()) as Ref<Column[]>;
|
||||
|
||||
const data: Ref<TableDataWithIndex<T>[]> = ref([]);
|
||||
const columnChecks = ref(getColumnChecks(columns())) as Ref<TableColumnCheck[]>;
|
||||
|
||||
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
|
||||
|
||||
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
|
||||
const $columns = computed(() => getColumns(columns(), columnChecks.value));
|
||||
|
||||
function reloadColumns() {
|
||||
allColumns.value = config.columns();
|
||||
allColumns.value = columns();
|
||||
|
||||
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
|
||||
|
||||
@ -92,47 +92,21 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
startLoading();
|
||||
try {
|
||||
startLoading();
|
||||
|
||||
const formattedParams = formatSearchParams(searchParams);
|
||||
const response = await api();
|
||||
|
||||
const response = await apiFn(formattedParams);
|
||||
const transformed = transform(response);
|
||||
|
||||
const transformed = transformer(response as Awaited<ReturnType<A>>);
|
||||
data.value = getTableData(transformed, pagination);
|
||||
|
||||
data.value = transformed.data;
|
||||
setEmpty(data.value.length === 0);
|
||||
|
||||
setEmpty(transformed.data.length === 0);
|
||||
|
||||
await config.onFetched?.(transformed);
|
||||
|
||||
endLoading();
|
||||
}
|
||||
|
||||
function formatSearchParams(params: Record<string, unknown>) {
|
||||
const formattedParams: Record<string, unknown> = {};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
formattedParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return formattedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* update search params
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
|
||||
Object.assign(searchParams, params);
|
||||
}
|
||||
|
||||
/** reset search params */
|
||||
function resetSearchParams() {
|
||||
Object.assign(searchParams, jsonClone(apiParams));
|
||||
await onFetched?.(transformed);
|
||||
} finally {
|
||||
endLoading();
|
||||
}
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
@ -143,12 +117,20 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
|
||||
loading,
|
||||
empty,
|
||||
data,
|
||||
columns,
|
||||
columns: $columns,
|
||||
columnChecks,
|
||||
reloadColumns,
|
||||
getData,
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
getData
|
||||
};
|
||||
}
|
||||
|
||||
function getTableData<ApiData, Pagination extends boolean>(
|
||||
data: GetApiData<ApiData, Pagination>,
|
||||
pagination?: Pagination
|
||||
) {
|
||||
if (pagination) {
|
||||
return (data as PaginationData<ApiData>).data;
|
||||
}
|
||||
|
||||
return data as ApiData[];
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "@sa/fetch",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ofetch": "1.4.1"
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { ofetch } from 'ofetch';
|
||||
import type { FetchOptions } from 'ofetch';
|
||||
|
||||
export function createRequest(options: FetchOptions) {
|
||||
const request = ofetch.create(options);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
export default createRequest;
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
"devDependencies": {
|
||||
"@soybeanjs/changelog": "0.3.24",
|
||||
"bumpp": "10.2.0",
|
||||
"c12": "3.0.4",
|
||||
"c12": "3.1.0",
|
||||
"cac": "6.7.14",
|
||||
"consola": "3.4.2",
|
||||
"enquirer": "2.4.1",
|
||||
|
3798
pnpm-lock.yaml
3798
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
1
src/assets/svg-icon/alova.svg
Normal file
1
src/assets/svg-icon/alova.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.7 KiB |
3
src/assets/svg-icon/visactor.svg
Normal file
3
src/assets/svg-icon/visactor.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M100 10C149.706 10 190 50.2944 190 100C190 108.029 188.949 115.812 186.976 123.219L136.254 39.4666H136.195C131.638 32.531 123.789 27.9518 114.87 27.9518C105.952 27.9518 98.1024 32.531 93.5456 39.4666H93.4853L92.8532 40.5821C92.731 40.7907 92.6117 41.0012 92.4953 41.2136L36.9057 139.312C34.7053 143.082 33.4445 147.467 33.4445 152.147C33.4445 156.134 34.3599 159.908 35.9919 163.269C19.9218 147.012 10 124.665 10 100C10 50.2944 50.2944 10 100 10ZM53.4722 177.056C55.2341 177.441 57.0641 177.644 58.9415 177.644C73.0231 177.644 84.4384 166.228 84.4384 152.147C84.4384 147.63 83.2642 143.388 81.2043 139.709L81.2172 139.704L68.7932 118.202C62.0989 106.608 66.0716 91.7816 77.6663 85.0874C89.261 78.3932 104.087 82.3658 110.781 93.9605L154.607 168.236L154.625 168.229C155.082 168.971 155.575 169.689 156.103 170.378C140.717 182.659 121.216 190 100 190C82.9725 190 67.0494 185.271 53.4722 177.056Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
78
src/components/custom/custom-icon-select.vue
Normal file
78
src/components/custom/custom-icon-select.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
defineOptions({ name: 'CustomIconSelect' });
|
||||
|
||||
interface Props {
|
||||
/** Selected icon */
|
||||
value: string;
|
||||
/** List of icons */
|
||||
icons: string[];
|
||||
/** Icon for when nothing is selected */
|
||||
emptyIcon?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
emptyIcon: 'mdi:apps'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', val: string): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const modelValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val: string) {
|
||||
emit('update:value', val);
|
||||
}
|
||||
});
|
||||
|
||||
const selectedIcon = computed(() => modelValue.value || props.emptyIcon);
|
||||
|
||||
const searchValue = ref('');
|
||||
|
||||
const iconsList = computed(() => props.icons.filter(v => v.includes(searchValue.value)));
|
||||
|
||||
function handleChange(iconItem: string) {
|
||||
modelValue.value = iconItem;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NPopover placement="bottom-end" trigger="click">
|
||||
<template #trigger>
|
||||
<NInput v-model:value="modelValue" readonly placeholder="点击选择图标">
|
||||
<template #suffix>
|
||||
<SvgIcon :icon="selectedIcon" class="p-5px text-30px" />
|
||||
</template>
|
||||
</NInput>
|
||||
</template>
|
||||
<template #header>
|
||||
<NInput v-model:value="searchValue" placeholder="搜索图标"></NInput>
|
||||
</template>
|
||||
<div v-if="iconsList.length > 0" class="grid grid-cols-9 h-auto overflow-auto">
|
||||
<span v-for="iconItem in iconsList" :key="iconItem" @click="handleChange(iconItem)">
|
||||
<SvgIcon
|
||||
:icon="iconItem"
|
||||
class="m-2px cursor-pointer border-1px border-#d9d9d9 p-5px text-30px"
|
||||
:class="{ 'border-primary': modelValue === iconItem }"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<NEmpty v-else class="w-306px" description="你什么也找不到" />
|
||||
</NPopover>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.n-input-wrapper) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
:deep(.n-input__suffix) {
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
</style>
|
18
src/components/custom/github-link.vue
Normal file
18
src/components/custom/github-link.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import WebSiteLink from './web-site-link.vue';
|
||||
|
||||
defineOptions({ name: 'GithubLink' });
|
||||
|
||||
interface Props {
|
||||
/** github link */
|
||||
link: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WebSiteLink label="github地址:" :link="link" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
23
src/components/custom/web-site-link.vue
Normal file
23
src/components/custom/web-site-link.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'WebSiteLink' });
|
||||
|
||||
interface Props {
|
||||
/** Web site name */
|
||||
label: string;
|
||||
/** Web site link */
|
||||
link: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>
|
||||
<span>{{ label }}</span>
|
||||
<a class="text-blue-500" :href="link" target="#">
|
||||
{{ link }}
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
29
src/constants/business.ts
Normal file
29
src/constants/business.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { transformRecordToOption } from '@/utils/common';
|
||||
|
||||
export const enableStatusRecord: Record<Api.Common.EnableStatus, App.I18n.I18nKey> = {
|
||||
'1': 'page.manage.common.status.enable',
|
||||
'2': 'page.manage.common.status.disable'
|
||||
};
|
||||
|
||||
export const enableStatusOptions = transformRecordToOption(enableStatusRecord);
|
||||
|
||||
export const userGenderRecord: Record<Api.SystemManage.UserGender, App.I18n.I18nKey> = {
|
||||
'1': 'page.manage.user.gender.male',
|
||||
'2': 'page.manage.user.gender.female'
|
||||
};
|
||||
|
||||
export const userGenderOptions = transformRecordToOption(userGenderRecord);
|
||||
|
||||
export const menuTypeRecord: Record<Api.SystemManage.MenuType, App.I18n.I18nKey> = {
|
||||
'1': 'page.manage.menu.type.directory',
|
||||
'2': 'page.manage.menu.type.menu'
|
||||
};
|
||||
|
||||
export const menuTypeOptions = transformRecordToOption(menuTypeRecord);
|
||||
|
||||
export const menuIconTypeRecord: Record<Api.SystemManage.IconType, App.I18n.I18nKey> = {
|
||||
'1': 'page.manage.menu.iconType.iconify',
|
||||
'2': 'page.manage.menu.iconType.local'
|
||||
};
|
||||
|
||||
export const menuIconTypeOptions = transformRecordToOption(menuIconTypeRecord);
|
8
src/constants/map-sdk.ts
Normal file
8
src/constants/map-sdk.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/** baidu map sdk url */
|
||||
export const BAIDU_MAP_SDK_URL = `https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1`;
|
||||
|
||||
/** Amap sdk url */
|
||||
export const AMAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d';
|
||||
|
||||
/** tencent sdk url */
|
||||
export const TENCENT_MAP_SDK_URL = 'https://map.qq.com/api/gljs?v=1.exp&key=A6DBZ-KXPLW-JKSRY-ONZF4-CPHY3-K6BL7';
|
@ -1,4 +1,4 @@
|
||||
import { computed, effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
|
||||
import { computed, effectScope, nextTick, onScopeDispose, shallowRef, watch } from 'vue';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
|
||||
@ -86,11 +86,11 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
const themeStore = useThemeStore();
|
||||
const darkMode = computed(() => themeStore.darkMode);
|
||||
|
||||
const domRef = ref<HTMLElement | null>(null);
|
||||
const domRef = shallowRef<HTMLElement | null>(null);
|
||||
const initialSize = { width: 0, height: 0 };
|
||||
const { width, height } = useElementSize(domRef, initialSize);
|
||||
|
||||
let chart: echarts.ECharts | null = null;
|
||||
const chart = shallowRef<echarts.ECharts | null>(null);
|
||||
const chartOptions: T = optionsFactory();
|
||||
|
||||
const {
|
||||
@ -111,18 +111,9 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
onDestroy
|
||||
} = hooks;
|
||||
|
||||
/**
|
||||
* whether can render chart
|
||||
*
|
||||
* when domRef is ready and initialSize is valid
|
||||
*/
|
||||
function canRender() {
|
||||
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
|
||||
}
|
||||
|
||||
/** is chart rendered */
|
||||
function isRendered() {
|
||||
return Boolean(domRef.value && chart);
|
||||
return Boolean(domRef.value && chart.value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -131,59 +122,59 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
* @param callback callback function
|
||||
*/
|
||||
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
||||
if (!isRendered()) return;
|
||||
|
||||
const updatedOpts = callback(chartOptions, optionsFactory);
|
||||
|
||||
Object.assign(chartOptions, updatedOpts);
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (!isRendered()) return;
|
||||
|
||||
if (isRendered()) {
|
||||
chart?.clear();
|
||||
chart.value?.clear();
|
||||
}
|
||||
|
||||
chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
|
||||
chart.value?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
|
||||
|
||||
await onUpdated?.(chart!);
|
||||
await onUpdated?.(chart.value!);
|
||||
}
|
||||
|
||||
function setOptions(options: T) {
|
||||
chart?.setOption(options);
|
||||
chart.value?.setOption(options);
|
||||
}
|
||||
|
||||
/** render chart */
|
||||
async function render() {
|
||||
if (!isRendered()) {
|
||||
const chartTheme = darkMode.value ? 'dark' : 'light';
|
||||
if (isRendered()) return;
|
||||
|
||||
await nextTick();
|
||||
const chartTheme = darkMode.value ? 'dark' : 'light';
|
||||
|
||||
chart = echarts.init(domRef.value, chartTheme);
|
||||
chart.value = echarts.init(domRef.value, chartTheme);
|
||||
|
||||
chart.setOption({ ...chartOptions, backgroundColor: 'transparent' });
|
||||
chart.value?.setOption({ ...chartOptions, backgroundColor: 'transparent' });
|
||||
|
||||
await onRender?.(chart);
|
||||
}
|
||||
await onRender?.(chart.value!);
|
||||
}
|
||||
|
||||
/** resize chart */
|
||||
function resize() {
|
||||
chart?.resize();
|
||||
chart.value?.resize();
|
||||
}
|
||||
|
||||
/** destroy chart */
|
||||
async function destroy() {
|
||||
if (!chart) return;
|
||||
if (!chart.value) return;
|
||||
|
||||
await onDestroy?.(chart);
|
||||
chart?.dispose();
|
||||
chart = null;
|
||||
await onDestroy?.(chart.value);
|
||||
chart.value?.dispose();
|
||||
chart.value = null;
|
||||
}
|
||||
|
||||
/** change chart theme */
|
||||
async function changeTheme() {
|
||||
await destroy();
|
||||
await render();
|
||||
await onUpdated?.(chart!);
|
||||
await onUpdated?.(chart.value!);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -196,30 +187,29 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
initialSize.width = w;
|
||||
initialSize.height = h;
|
||||
|
||||
// size is abnormal, destroy chart
|
||||
if (!canRender()) {
|
||||
await destroy();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// resize chart
|
||||
if (isRendered()) {
|
||||
resize();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// render chart
|
||||
await render();
|
||||
|
||||
if (chart) {
|
||||
await onUpdated?.(chart);
|
||||
if (chart.value) {
|
||||
await onUpdated?.(chart.value);
|
||||
}
|
||||
}
|
||||
|
||||
scope.run(() => {
|
||||
watch([width, height], ([newWidth, newHeight]) => {
|
||||
renderChartBySize(newWidth, newHeight);
|
||||
});
|
||||
watch(
|
||||
[width, height],
|
||||
([newWidth, newHeight]) => {
|
||||
renderChartBySize(newWidth, newHeight);
|
||||
},
|
||||
{ flush: 'post' }
|
||||
);
|
||||
|
||||
watch(darkMode, () => {
|
||||
changeTheme();
|
||||
@ -233,6 +223,7 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
|
||||
return {
|
||||
domRef,
|
||||
chart,
|
||||
updateOptions,
|
||||
setOptions
|
||||
};
|
||||
|
@ -1,192 +1,48 @@
|
||||
import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue';
|
||||
import { computed, effectScope, onScopeDispose, reactive, shallowRef, watch } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type { PaginationProps } from 'naive-ui';
|
||||
import { useBoolean, useTable } from '@sa/hooks';
|
||||
import type { PaginationData, TableColumnCheck, UseTableOptions } from '@sa/hooks';
|
||||
import type { FlatResponseData } from '@sa/axios';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import { useBoolean, useHookTable } from '@sa/hooks';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
type TableData = NaiveUI.TableData;
|
||||
type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>;
|
||||
type TableColumn<T> = NaiveUI.TableColumn<T>;
|
||||
export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
|
||||
UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>,
|
||||
'pagination' | 'getColumnChecks' | 'getColumns'
|
||||
> & {
|
||||
/**
|
||||
* get column visible
|
||||
*
|
||||
* @param column
|
||||
*
|
||||
* @default true
|
||||
*
|
||||
* @returns true if the column is visible, false otherwise
|
||||
*/
|
||||
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
|
||||
};
|
||||
|
||||
export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
|
||||
const SELECTION_KEY = '__selection__';
|
||||
|
||||
const EXPAND_KEY = '__expand__';
|
||||
|
||||
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>) {
|
||||
const scope = effectScope();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const isMobile = computed(() => appStore.isMobile);
|
||||
|
||||
const { apiFn, apiParams, immediate, showTotal } = config;
|
||||
|
||||
const SELECTION_KEY = '__selection__';
|
||||
|
||||
const EXPAND_KEY = '__expand__';
|
||||
|
||||
const {
|
||||
loading,
|
||||
empty,
|
||||
data,
|
||||
columns,
|
||||
columnChecks,
|
||||
reloadColumns,
|
||||
getData,
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
|
||||
apiFn,
|
||||
apiParams,
|
||||
columns: config.columns,
|
||||
transformer: res => {
|
||||
const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
|
||||
|
||||
// Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
|
||||
const pageSize = size <= 0 ? 10 : size;
|
||||
|
||||
const recordsWithIndex = records.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
index: (current - 1) * pageSize + index + 1
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: recordsWithIndex,
|
||||
pageNum: current,
|
||||
pageSize,
|
||||
total
|
||||
};
|
||||
},
|
||||
getColumnChecks: cols => {
|
||||
const checks: NaiveUI.TableColumnCheck[] = [];
|
||||
|
||||
cols.forEach(column => {
|
||||
if (isTableColumnHasKey(column)) {
|
||||
checks.push({
|
||||
key: column.key as string,
|
||||
title: column.title!,
|
||||
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;
|
||||
},
|
||||
getColumns: (cols, checks) => {
|
||||
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
|
||||
|
||||
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 = checks
|
||||
.filter(item => item.checked)
|
||||
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
|
||||
|
||||
return filteredColumns;
|
||||
},
|
||||
onFetched: async transformed => {
|
||||
const { pageNum, pageSize, total } = transformed;
|
||||
|
||||
updatePagination({
|
||||
page: pageNum,
|
||||
pageSize,
|
||||
itemCount: total
|
||||
});
|
||||
},
|
||||
immediate
|
||||
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
|
||||
...options,
|
||||
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
||||
getColumns
|
||||
});
|
||||
|
||||
const pagination: PaginationProps = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
itemCount: 0,
|
||||
pageSizes: [10, 15, 20, 25, 30],
|
||||
onUpdatePage: async (page: number) => {
|
||||
pagination.page = page;
|
||||
|
||||
updateSearchParams({
|
||||
current: page,
|
||||
size: pagination.pageSize!
|
||||
});
|
||||
|
||||
getData();
|
||||
},
|
||||
onUpdatePageSize: async (pageSize: number) => {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
|
||||
updateSearchParams({
|
||||
current: pagination.page,
|
||||
size: pageSize
|
||||
});
|
||||
|
||||
getData();
|
||||
},
|
||||
...(showTotal
|
||||
? {
|
||||
prefix: page => $t('datatable.itemCount', { total: page.itemCount })
|
||||
}
|
||||
: {})
|
||||
});
|
||||
|
||||
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
|
||||
const mobilePagination = computed(() => {
|
||||
const p: PaginationProps = {
|
||||
...pagination,
|
||||
pageSlot: isMobile.value ? 3 : 9,
|
||||
prefix: !isMobile.value && showTotal ? pagination.prefix : undefined
|
||||
};
|
||||
|
||||
return p;
|
||||
});
|
||||
|
||||
function updatePagination(update: Partial<PaginationProps>) {
|
||||
Object.assign(pagination, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* get data by page number
|
||||
*
|
||||
* @param pageNum the page number. default is 1
|
||||
*/
|
||||
async function getDataByPage(pageNum: number = 1) {
|
||||
updatePagination({
|
||||
page: pageNum
|
||||
});
|
||||
|
||||
updateSearchParams({
|
||||
current: pageNum,
|
||||
size: pagination.pageSize!
|
||||
});
|
||||
|
||||
await getData();
|
||||
}
|
||||
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
() => {
|
||||
reloadColumns();
|
||||
result.reloadColumns();
|
||||
}
|
||||
);
|
||||
});
|
||||
@ -195,28 +51,124 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;
|
||||
|
||||
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
|
||||
paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>;
|
||||
/**
|
||||
* whether to show the total count of the table
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
showTotal?: boolean;
|
||||
onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export function useNaivePaginatedTable<ResponseData, ApiData>(
|
||||
options: UseNaivePaginatedTableOptions<ResponseData, ApiData>
|
||||
) {
|
||||
const scope = effectScope();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const isMobile = computed(() => appStore.isMobile);
|
||||
|
||||
const showTotal = computed(() => options.showTotal ?? true);
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 15, 20, 25, 30],
|
||||
prefix: showTotal.value ? page => $t('datatable.itemCount', { total: page.itemCount }) : undefined,
|
||||
onUpdatePage(page) {
|
||||
pagination.page = page;
|
||||
},
|
||||
onUpdatePageSize(pageSize) {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
},
|
||||
...options.paginationProps
|
||||
}) as PaginationProps;
|
||||
|
||||
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
|
||||
const mobilePagination = computed(() => {
|
||||
const p: PaginationProps = {
|
||||
...pagination,
|
||||
pageSlot: isMobile.value ? 3 : 9,
|
||||
prefix: !isMobile.value && showTotal.value ? pagination.prefix : undefined
|
||||
};
|
||||
|
||||
return p;
|
||||
});
|
||||
|
||||
const paginationParams = computed(() => {
|
||||
const { page, pageSize } = pagination;
|
||||
|
||||
return {
|
||||
page,
|
||||
pageSize
|
||||
};
|
||||
});
|
||||
|
||||
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
|
||||
...options,
|
||||
pagination: true,
|
||||
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
||||
getColumns,
|
||||
onFetched: data => {
|
||||
pagination.itemCount = data.total;
|
||||
}
|
||||
});
|
||||
|
||||
async function getDataByPage(page: number = 1) {
|
||||
if (page !== pagination.page) {
|
||||
pagination.page = page;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await result.getData();
|
||||
}
|
||||
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
() => {
|
||||
result.reloadColumns();
|
||||
}
|
||||
);
|
||||
|
||||
watch(paginationParams, async newVal => {
|
||||
await options.onPaginationParamsChange?.(newVal);
|
||||
|
||||
await result.getData();
|
||||
});
|
||||
});
|
||||
|
||||
onScopeDispose(() => {
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
return {
|
||||
loading,
|
||||
empty,
|
||||
data,
|
||||
columns,
|
||||
columnChecks,
|
||||
reloadColumns,
|
||||
pagination,
|
||||
mobilePagination,
|
||||
updatePagination,
|
||||
getData,
|
||||
...result,
|
||||
getDataByPage,
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
pagination,
|
||||
mobilePagination
|
||||
};
|
||||
}
|
||||
|
||||
export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
|
||||
export function useTableOperate<TableData>(
|
||||
data: Ref<TableData[]>,
|
||||
idKey: keyof TableData,
|
||||
getData: () => Promise<void>
|
||||
) {
|
||||
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
|
||||
|
||||
const operateType = ref<NaiveUI.TableOperateType>('add');
|
||||
const operateType = shallowRef<NaiveUI.TableOperateType>('add');
|
||||
|
||||
function handleAdd() {
|
||||
operateType.value = 'add';
|
||||
@ -224,18 +176,18 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
|
||||
}
|
||||
|
||||
/** the editing row data */
|
||||
const editingData: Ref<T | null> = ref(null);
|
||||
const editingData = shallowRef<TableData | null>(null);
|
||||
|
||||
function handleEdit(id: T['id']) {
|
||||
function handleEdit(id: TableData[keyof TableData]) {
|
||||
operateType.value = 'edit';
|
||||
const findItem = data.value.find(item => item.id === id) || null;
|
||||
const findItem = data.value.find(item => item[idKey] === id) || null;
|
||||
editingData.value = jsonClone(findItem);
|
||||
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
/** the checked row keys of table */
|
||||
const checkedRowKeys = ref<string[]>([]);
|
||||
const checkedRowKeys = shallowRef<string[]>([]);
|
||||
|
||||
/** the hook after the batch delete operation is completed */
|
||||
async function onBatchDeleted() {
|
||||
@ -267,6 +219,84 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
|
||||
};
|
||||
}
|
||||
|
||||
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
|
||||
export function defaultTransform<ApiData>(
|
||||
response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
|
||||
): PaginationData<ApiData> {
|
||||
const { data, error } = response;
|
||||
|
||||
if (!error) {
|
||||
const { records, current, size, total } = data;
|
||||
|
||||
return {
|
||||
data: records,
|
||||
pageNum: current,
|
||||
pageSize: size,
|
||||
total
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: [],
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
|
||||
function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
|
||||
cols: Column[],
|
||||
getColumnVisible?: (column: Column) => boolean
|
||||
) {
|
||||
const checks: TableColumnCheck[] = [];
|
||||
|
||||
cols.forEach(column => {
|
||||
const visible = getColumnVisible?.(column) ?? true;
|
||||
|
||||
if (isTableColumnHasKey(column)) {
|
||||
checks.push({
|
||||
key: column.key as string,
|
||||
title: column.title!,
|
||||
checked: true,
|
||||
visible
|
||||
});
|
||||
} else if (column.type === 'selection') {
|
||||
checks.push({
|
||||
key: SELECTION_KEY,
|
||||
title: $t('common.check'),
|
||||
checked: true,
|
||||
visible
|
||||
});
|
||||
} else if (column.type === 'expand') {
|
||||
checks.push({
|
||||
key: EXPAND_KEY,
|
||||
title: $t('common.expandColumn'),
|
||||
checked: true,
|
||||
visible
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
function getColumns<Column extends NaiveUI.TableColumn<any>>(cols: Column[], checks: TableColumnCheck[]) {
|
||||
const columnMap = new Map<string, Column>();
|
||||
|
||||
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 = checks.filter(item => item.checked).map(check => columnMap.get(check.key) as Column);
|
||||
|
||||
return filteredColumns;
|
||||
}
|
||||
|
||||
export function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
|
||||
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
|
||||
}
|
||||
|
158
src/hooks/common/vchart.ts
Normal file
158
src/hooks/common/vchart.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { computed, effectScope, onScopeDispose, ref, watch } from 'vue';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import VChart, { registerLiquidChart } from '@visactor/vchart';
|
||||
import type { ISpec, ITheme } from '@visactor/vchart';
|
||||
import light from '@visactor/vchart-theme/public/light.json';
|
||||
import dark from '@visactor/vchart-theme/public/dark.json';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
registerLiquidChart();
|
||||
|
||||
// register the theme
|
||||
VChart.ThemeManager.registerTheme('light', light as ITheme);
|
||||
VChart.ThemeManager.registerTheme('dark', dark as ITheme);
|
||||
|
||||
interface ChartHooks {
|
||||
onRender?: (chart: VChart) => void | Promise<void>;
|
||||
onUpdated?: (chart: VChart) => void | Promise<void>;
|
||||
onDestroy?: (chart: VChart) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function useVChart<T extends ISpec>(specFactory: () => T, hooks: ChartHooks = {}) {
|
||||
const scope = effectScope();
|
||||
const themeStore = useThemeStore();
|
||||
const darkMode = computed(() => themeStore.darkMode);
|
||||
|
||||
const domRef = ref<HTMLElement | null>(null);
|
||||
const initialSize = { width: 0, height: 0 };
|
||||
const { width, height } = useElementSize(domRef, initialSize);
|
||||
|
||||
let chart: VChart | null = null;
|
||||
const spec: T = specFactory();
|
||||
|
||||
const { onRender, onUpdated, onDestroy } = hooks;
|
||||
|
||||
/**
|
||||
* whether can render chart
|
||||
*
|
||||
* when domRef is ready and initialSize is valid
|
||||
*/
|
||||
function canRender() {
|
||||
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
|
||||
}
|
||||
|
||||
/** is chart rendered */
|
||||
function isRendered() {
|
||||
return Boolean(domRef.value && chart);
|
||||
}
|
||||
|
||||
/**
|
||||
* update chart spec
|
||||
*
|
||||
* @param callback callback function
|
||||
*/
|
||||
async function updateSpec(callback: (opts: T, optsFactory: () => T) => ISpec = () => spec) {
|
||||
if (!isRendered()) return;
|
||||
|
||||
const updatedOpts = callback(spec, specFactory);
|
||||
|
||||
Object.assign(spec, updatedOpts);
|
||||
|
||||
if (isRendered()) {
|
||||
chart?.release();
|
||||
}
|
||||
|
||||
chart?.updateSpec({ ...updatedOpts }, true);
|
||||
|
||||
await onUpdated?.(chart!);
|
||||
}
|
||||
|
||||
function setSpec(newSpec: T) {
|
||||
chart?.updateSpec(newSpec);
|
||||
}
|
||||
|
||||
/** render chart */
|
||||
async function render() {
|
||||
if (!isRendered()) {
|
||||
// apply the theme
|
||||
if (darkMode.value) {
|
||||
VChart.ThemeManager.setCurrentTheme('dark');
|
||||
} else {
|
||||
VChart.ThemeManager.setCurrentTheme('light');
|
||||
}
|
||||
|
||||
chart = new VChart(spec, { dom: domRef.value as HTMLElement });
|
||||
chart.renderSync();
|
||||
|
||||
await onRender?.(chart);
|
||||
}
|
||||
}
|
||||
|
||||
/** resize chart */
|
||||
function resize() {
|
||||
// chart?.resize();
|
||||
}
|
||||
|
||||
/** destroy chart */
|
||||
async function destroy() {
|
||||
if (!chart) return;
|
||||
|
||||
await onDestroy?.(chart);
|
||||
chart?.release();
|
||||
chart = null;
|
||||
}
|
||||
|
||||
/** change chart theme */
|
||||
async function changeTheme() {
|
||||
await destroy();
|
||||
await render();
|
||||
await onUpdated?.(chart!);
|
||||
}
|
||||
|
||||
/**
|
||||
* render chart by size
|
||||
*
|
||||
* @param w width
|
||||
* @param h height
|
||||
*/
|
||||
async function renderChartBySize(w: number, h: number) {
|
||||
initialSize.width = w;
|
||||
initialSize.height = h;
|
||||
|
||||
// size is abnormal, destroy chart
|
||||
if (!canRender()) {
|
||||
await destroy();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// resize chart
|
||||
if (isRendered()) {
|
||||
resize();
|
||||
}
|
||||
|
||||
// render chart
|
||||
await render();
|
||||
}
|
||||
|
||||
scope.run(() => {
|
||||
watch([width, height], ([newWidth, newHeight]) => {
|
||||
renderChartBySize(newWidth, newHeight);
|
||||
});
|
||||
|
||||
watch(darkMode, () => {
|
||||
changeTheme();
|
||||
});
|
||||
});
|
||||
|
||||
onScopeDispose(() => {
|
||||
destroy();
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
return {
|
||||
domRef,
|
||||
updateSpec,
|
||||
setSpec
|
||||
};
|
||||
}
|
@ -10,7 +10,7 @@ import GlobalTab from '../modules/global-tab/index.vue';
|
||||
import GlobalContent from '../modules/global-content/index.vue';
|
||||
import GlobalFooter from '../modules/global-footer/index.vue';
|
||||
import ThemeDrawer from '../modules/theme-drawer/index.vue';
|
||||
import { setupMixMenuContext } from '../context';
|
||||
import { provideMixMenuContext } from '../modules/global-menu/context';
|
||||
|
||||
defineOptions({
|
||||
name: 'BaseLayout'
|
||||
@ -18,7 +18,7 @@ defineOptions({
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
|
||||
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = provideMixMenuContext();
|
||||
|
||||
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
|
||||
|
||||
|
@ -18,7 +18,7 @@ function loginOrRegister() {
|
||||
toLogin();
|
||||
}
|
||||
|
||||
type DropdownKey = 'logout';
|
||||
type DropdownKey = 'user-center' | 'logout';
|
||||
|
||||
type DropdownOption =
|
||||
| {
|
||||
@ -33,6 +33,15 @@ type DropdownOption =
|
||||
|
||||
const options = computed(() => {
|
||||
const opts: DropdownOption[] = [
|
||||
{
|
||||
label: $t('common.userCenter'),
|
||||
key: 'user-center',
|
||||
icon: SvgIconVNode({ icon: 'ph:user-circle', fontSize: 18 })
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
key: 'divider'
|
||||
},
|
||||
{
|
||||
label: $t('common.logout'),
|
||||
key: 'logout',
|
||||
|
@ -5,7 +5,7 @@ import type { RouteKey } from '@elegant-router/types';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
|
||||
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
|
||||
export const [provideMixMenuContext, useMixMenuContext] = useContext('MixMenu', useMixMenu);
|
||||
|
||||
function useMixMenu() {
|
||||
const route = useRoute();
|
@ -2,7 +2,7 @@
|
||||
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useMenu } from '../../../context';
|
||||
import { useMenu } from '../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'HorizontalMenu'
|
||||
|
@ -7,7 +7,7 @@ import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
import { useMenu, useMixMenuContext } from '../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'TopHybridHeaderFirst'
|
||||
@ -18,7 +18,8 @@ const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
|
||||
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
|
||||
useMixMenuContext('TopHybridHeaderFirst');
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
@ -4,7 +4,7 @@ import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
import { useMenu, useMixMenuContext } from '../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'TopHybridSidebarFirst'
|
||||
@ -13,7 +13,8 @@ defineOptions({
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
|
||||
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
|
||||
useMixMenuContext('TopHybridSidebarFirst');
|
||||
const { selectedKey } = useMenu();
|
||||
</script>
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
import { useMenu, useMixMenuContext } from '../context';
|
||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||
import GlobalLogo from '../../global-logo/index.vue';
|
||||
|
||||
@ -34,7 +34,7 @@ const {
|
||||
handleSelectSecondLevelMenu,
|
||||
getActiveSecondLevelMenuKey,
|
||||
childLevelMenus
|
||||
} = useMixMenuContext();
|
||||
} = useMixMenuContext('VerticalHybridHeaderFirst');
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
||||
|
@ -7,7 +7,7 @@ import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useMenu } from '../../../context';
|
||||
import { useMenu } from '../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'VerticalMenu'
|
||||
|
@ -10,7 +10,7 @@ import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { $t } from '@/locales';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
import { useMenu, useMixMenuContext } from '../context';
|
||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||
import GlobalLogo from '../../global-logo/index.vue';
|
||||
|
||||
@ -31,7 +31,7 @@ const {
|
||||
isActiveFirstLevelMenuHasChildren,
|
||||
getActiveFirstLevelMenuKey,
|
||||
handleSelectFirstLevelMenu
|
||||
} = useMixMenuContext();
|
||||
} = useMixMenuContext('VerticalMixMenu');
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
||||
|
@ -200,7 +200,80 @@ const local: App.I18n.Schema = {
|
||||
404: 'Page Not Found',
|
||||
500: 'Server Error',
|
||||
'iframe-page': 'Iframe',
|
||||
home: 'Home'
|
||||
home: 'Home',
|
||||
document: 'Document',
|
||||
document_project: 'Project Document',
|
||||
'document_project-link': 'Project Document(External Link)',
|
||||
document_video: 'Video Tutorial',
|
||||
document_vue: 'Vue Document',
|
||||
document_vite: 'Vite Document',
|
||||
document_unocss: 'UnoCSS Document',
|
||||
document_naive: 'Naive UI Document',
|
||||
'document_pro-naive': 'Pro 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_scenes: 'Scenario Request',
|
||||
'pro-naive': 'Pro Naive Example',
|
||||
'pro-naive_form': 'Form',
|
||||
'pro-naive_form_basic': 'Basic Form',
|
||||
'pro-naive_form_query': 'Query Form',
|
||||
'pro-naive_form_step': 'Step Form',
|
||||
'pro-naive_table': 'Table',
|
||||
'pro-naive_table_remote': 'Remote',
|
||||
'pro-naive_table_row-edit': 'Row Edit',
|
||||
function_tab: 'Tab',
|
||||
'function_multi-tab': 'Multi Tab',
|
||||
'function_hide-child': 'Hide Child',
|
||||
'function_hide-child_one': 'Hide Child',
|
||||
'function_hide-child_two': 'Two',
|
||||
'function_hide-child_three': 'Three',
|
||||
function_request: 'Request',
|
||||
'function_toggle-auth': 'Toggle Auth',
|
||||
'function_super-page': 'Super Admin Visible',
|
||||
manage: 'System Manage',
|
||||
manage_user: 'User Manage',
|
||||
'manage_user-detail': 'User Detail',
|
||||
manage_role: 'Role Manage',
|
||||
manage_menu: 'Menu Manage',
|
||||
'multi-menu': 'Multi Menu',
|
||||
'multi-menu_first': 'Menu One',
|
||||
'multi-menu_first_child': 'Menu One Child',
|
||||
'multi-menu_second': 'Menu Two',
|
||||
'multi-menu_second_child': 'Menu Two Child',
|
||||
'multi-menu_second_child_home': 'Menu Two Child Home',
|
||||
exception: 'Exception',
|
||||
exception_403: '403',
|
||||
exception_404: '404',
|
||||
exception_500: '500',
|
||||
plugin: 'Plugin',
|
||||
plugin_copy: 'Copy',
|
||||
plugin_charts: 'Charts',
|
||||
plugin_charts_echarts: 'ECharts',
|
||||
plugin_charts_antv: 'AntV',
|
||||
plugin_charts_vchart: 'VChart',
|
||||
plugin_editor: 'Editor',
|
||||
plugin_editor_quill: 'Quill',
|
||||
plugin_editor_markdown: 'Markdown',
|
||||
plugin_icon: 'Icon',
|
||||
plugin_map: 'Map',
|
||||
plugin_print: 'Print',
|
||||
plugin_swiper: 'Swiper',
|
||||
plugin_video: 'Video',
|
||||
plugin_barcode: 'Barcode',
|
||||
plugin_pinyin: 'pinyin',
|
||||
plugin_excel: 'Excel',
|
||||
plugin_pdf: 'PDF preview',
|
||||
plugin_gantt: 'Gantt Chart',
|
||||
plugin_gantt_dhtmlx: 'dhtmlxGantt',
|
||||
plugin_gantt_vtable: 'VTableGantt',
|
||||
plugin_typeit: 'Typeit',
|
||||
plugin_tables: 'Tables',
|
||||
plugin_tables_vtable: 'VTable'
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
@ -249,6 +322,19 @@ const local: App.I18n.Schema = {
|
||||
title: 'Bind WeChat'
|
||||
}
|
||||
},
|
||||
about: {
|
||||
title: 'About',
|
||||
introduction: `SoybeanAdmin is an elegant and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite7, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. SoybeanAdmin provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.`,
|
||||
projectInfo: {
|
||||
title: 'Project Info',
|
||||
version: 'Version',
|
||||
latestBuildTime: 'Latest Build Time',
|
||||
githubLink: 'Github Link',
|
||||
previewLink: 'Preview Link'
|
||||
},
|
||||
prdDep: 'Production Dependency',
|
||||
devDep: 'Development Dependency'
|
||||
},
|
||||
home: {
|
||||
branchDesc:
|
||||
'For the convenience of everyone in developing and updating the merge, we have streamlined the code of the main branch, only retaining the homepage menu, and the rest of the content has been moved to the example branch for maintenance. The preview address displays the content of the example branch.',
|
||||
@ -277,6 +363,253 @@ const local: App.I18n.Schema = {
|
||||
desc5: 'Soybean just wrote some of the workbench pages casually, and it was enough to see!'
|
||||
},
|
||||
creativity: 'Creativity'
|
||||
},
|
||||
function: {
|
||||
tab: {
|
||||
tabOperate: {
|
||||
title: 'Tab Operation',
|
||||
addTab: 'Add Tab',
|
||||
addTabDesc: 'To about page',
|
||||
closeTab: 'Close Tab',
|
||||
closeCurrentTab: 'Close Current Tab',
|
||||
closeAboutTab: 'Close "About" Tab',
|
||||
addMultiTab: 'Add Multi Tab',
|
||||
addMultiTabDesc1: 'To MultiTab page',
|
||||
addMultiTabDesc2: 'To MultiTab page(with query params)'
|
||||
},
|
||||
tabTitle: {
|
||||
title: 'Tab Title',
|
||||
changeTitle: 'Change Title',
|
||||
change: 'Change',
|
||||
resetTitle: 'Reset Title',
|
||||
reset: 'Reset'
|
||||
}
|
||||
},
|
||||
multiTab: {
|
||||
routeParam: 'Route Param',
|
||||
backTab: 'Back function_tab'
|
||||
},
|
||||
toggleAuth: {
|
||||
toggleAccount: 'Toggle Account',
|
||||
authHook: 'Auth Hook Function `hasAuth`',
|
||||
superAdminVisible: 'Super Admin Visible',
|
||||
adminVisible: 'Admin Visible',
|
||||
adminOrUserVisible: 'Admin and User Visible'
|
||||
},
|
||||
request: {
|
||||
repeatedErrorOccurOnce: 'Repeated Request Error Occurs Once',
|
||||
repeatedError: 'Repeated Request Error',
|
||||
repeatedErrorMsg1: 'Custom Request Error 1',
|
||||
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'
|
||||
}
|
||||
},
|
||||
proNaive: {
|
||||
form: {
|
||||
basic: {
|
||||
title: 'Basic Example',
|
||||
appName: 'ApplicationName',
|
||||
appStatus: 'ApplicationStatus',
|
||||
createTime: 'CreateTime',
|
||||
responseDate: 'ResponseDate',
|
||||
specificationInfo: 'SpecificationInfo',
|
||||
specificate: 'Specificate',
|
||||
specificationName: 'SpecificationName',
|
||||
specificationValue: 'SpecificationValue',
|
||||
specificationColorRed: 'Red',
|
||||
specificationColorOrange: 'Orange',
|
||||
addSpecificateItem: 'Add Specificate Item',
|
||||
fillValue: 'FillValue',
|
||||
reset: 'Reset',
|
||||
submit: 'Submit',
|
||||
add: 'Add',
|
||||
delete: 'Delete',
|
||||
color: 'Color',
|
||||
normal: 'Normal',
|
||||
anomaly: 'Anomaly'
|
||||
},
|
||||
query: {
|
||||
title1: 'Query Example, which expands by default',
|
||||
title2: 'Query Example, which fold by default, and two lines are retained when folding',
|
||||
appName: 'ApplicationName',
|
||||
appStatus: 'ApplicationStatus',
|
||||
createTime: 'CreateTime',
|
||||
responseDate: 'ResponseDate',
|
||||
endDate: 'EndDate',
|
||||
field: 'Field'
|
||||
},
|
||||
step: {
|
||||
title: 'Step Form',
|
||||
step1: {
|
||||
title: 'Form 1',
|
||||
field: 'Form 1 field',
|
||||
nextStep: 'Next Step'
|
||||
},
|
||||
step2: {
|
||||
title: 'Form 2',
|
||||
field: 'Form 2 field',
|
||||
prevStep: 'Prev Step',
|
||||
submit: 'Submit'
|
||||
}
|
||||
}
|
||||
},
|
||||
table: {
|
||||
remote: {
|
||||
filterCondition: 'Filter Condition',
|
||||
name: 'Name',
|
||||
createTime: 'CreateTime',
|
||||
responseTime: 'ResponseTime',
|
||||
title: 'Remote Loading',
|
||||
replicableText: 'Replicable Text',
|
||||
tags: 'Tags',
|
||||
dateFormatting: 'Date Formatting',
|
||||
image: 'Image'
|
||||
},
|
||||
rowEdit: {
|
||||
title: 'Edit Table',
|
||||
reset: 'Reset',
|
||||
submit: 'Submit',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
save: 'Save',
|
||||
task: 'Task',
|
||||
score: 'Score',
|
||||
time: 'Time',
|
||||
name: 'Name',
|
||||
action: 'Action'
|
||||
}
|
||||
}
|
||||
},
|
||||
manage: {
|
||||
common: {
|
||||
status: {
|
||||
enable: 'Enable',
|
||||
disable: 'Disable'
|
||||
}
|
||||
},
|
||||
role: {
|
||||
title: 'Role List',
|
||||
roleName: 'Role Name',
|
||||
roleCode: 'Role Code',
|
||||
roleStatus: 'Role Status',
|
||||
roleDesc: 'Role Description',
|
||||
menuAuth: 'Menu Auth',
|
||||
buttonAuth: 'Button Auth',
|
||||
form: {
|
||||
roleName: 'Please enter role name',
|
||||
roleCode: 'Please enter role code',
|
||||
roleStatus: 'Please select role status',
|
||||
roleDesc: 'Please enter role description'
|
||||
},
|
||||
addRole: 'Add Role',
|
||||
editRole: 'Edit Role'
|
||||
},
|
||||
user: {
|
||||
title: 'User List',
|
||||
userName: 'User Name',
|
||||
userGender: 'Gender',
|
||||
nickName: 'Nick Name',
|
||||
userPhone: 'Phone Number',
|
||||
userEmail: 'Email',
|
||||
userStatus: 'User Status',
|
||||
userRole: 'User Role',
|
||||
form: {
|
||||
userName: 'Please enter user name',
|
||||
userGender: 'Please select gender',
|
||||
nickName: 'Please enter nick name',
|
||||
userPhone: 'Please enter phone number',
|
||||
userEmail: 'Please enter email',
|
||||
userStatus: 'Please select user status',
|
||||
userRole: 'Please select user role'
|
||||
},
|
||||
addUser: 'Add User',
|
||||
editUser: 'Edit User',
|
||||
gender: {
|
||||
male: 'Male',
|
||||
female: 'Female'
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
home: 'Home',
|
||||
title: 'Menu List',
|
||||
id: 'ID',
|
||||
parentId: 'Parent ID',
|
||||
menuType: 'Menu Type',
|
||||
menuName: 'Menu Name',
|
||||
routeName: 'Route Name',
|
||||
routePath: 'Route Path',
|
||||
pathParam: 'Path Param',
|
||||
layout: 'Layout Component',
|
||||
page: 'Page Component',
|
||||
i18nKey: 'I18n Key',
|
||||
icon: 'Icon',
|
||||
localIcon: 'Local Icon',
|
||||
iconTypeTitle: 'Icon Type',
|
||||
order: 'Order',
|
||||
constant: 'Constant',
|
||||
keepAlive: 'Keep Alive',
|
||||
href: 'Href',
|
||||
hideInMenu: 'Hide In Menu',
|
||||
activeMenu: 'Active Menu',
|
||||
multiTab: 'Multi Tab',
|
||||
fixedIndexInTab: 'Fixed Index In Tab',
|
||||
query: 'Query Params',
|
||||
button: 'Button',
|
||||
buttonCode: 'Button Code',
|
||||
buttonDesc: 'Button Desc',
|
||||
menuStatus: 'Menu Status',
|
||||
form: {
|
||||
home: 'Please select home',
|
||||
menuType: 'Please select menu type',
|
||||
menuName: 'Please enter menu name',
|
||||
routeName: 'Please enter route name',
|
||||
routePath: 'Please enter route path',
|
||||
pathParam: 'Please enter path param',
|
||||
page: 'Please select page component',
|
||||
layout: 'Please select layout component',
|
||||
i18nKey: 'Please enter i18n key',
|
||||
icon: 'Please enter iconify name',
|
||||
localIcon: 'Please enter local icon name',
|
||||
order: 'Please enter order',
|
||||
keepAlive: 'Please select whether to cache route',
|
||||
href: 'Please enter href',
|
||||
hideInMenu: 'Please select whether to hide menu',
|
||||
activeMenu: 'Please select route name of the highlighted menu',
|
||||
multiTab: 'Please select whether to support multiple tabs',
|
||||
fixedInTab: 'Please select whether to fix in the tab',
|
||||
fixedIndexInTab: 'Please enter the index fixed in the tab',
|
||||
queryKey: 'Please enter route parameter Key',
|
||||
queryValue: 'Please enter route parameter Value',
|
||||
button: 'Please select whether it is a button',
|
||||
buttonCode: 'Please enter button code',
|
||||
buttonDesc: 'Please enter button description',
|
||||
menuStatus: 'Please select menu status'
|
||||
},
|
||||
addMenu: 'Add Menu',
|
||||
editMenu: 'Edit Menu',
|
||||
addChildMenu: 'Add Child Menu',
|
||||
type: {
|
||||
directory: 'Directory',
|
||||
menu: 'Menu'
|
||||
},
|
||||
iconType: {
|
||||
iconify: 'Iconify Icon',
|
||||
local: 'Local Icon'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
form: {
|
||||
|
@ -197,7 +197,80 @@ const local: App.I18n.Schema = {
|
||||
404: '页面不存在',
|
||||
500: '服务器错误',
|
||||
'iframe-page': '外链页面',
|
||||
home: '首页'
|
||||
home: '首页',
|
||||
document: '文档',
|
||||
document_project: '项目文档',
|
||||
'document_project-link': '项目文档(外链)',
|
||||
document_video: '视频教程',
|
||||
document_vue: 'Vue文档',
|
||||
document_vite: 'Vite文档',
|
||||
document_unocss: 'UnoCSS文档',
|
||||
document_naive: 'Naive UI文档',
|
||||
'document_pro-naive': 'Pro Naive UI文档',
|
||||
document_antd: 'Ant Design Vue文档',
|
||||
document_alova: 'Alova文档',
|
||||
'user-center': '个人中心',
|
||||
about: '关于',
|
||||
function: '系统功能',
|
||||
alova: 'alova示例',
|
||||
alova_request: 'alova请求',
|
||||
alova_scenes: '场景化请求',
|
||||
'pro-naive': 'Pro Naive UI 示例',
|
||||
'pro-naive_form': '表单',
|
||||
'pro-naive_form_basic': '基础表单',
|
||||
'pro-naive_form_query': '查询表单',
|
||||
'pro-naive_form_step': '分步表单',
|
||||
'pro-naive_table': '表格',
|
||||
'pro-naive_table_remote': '远程加载',
|
||||
'pro-naive_table_row-edit': '行编辑',
|
||||
function_tab: '标签页',
|
||||
'function_multi-tab': '多标签页',
|
||||
'function_hide-child': '隐藏子菜单',
|
||||
'function_hide-child_one': '隐藏子菜单',
|
||||
'function_hide-child_two': '菜单二',
|
||||
'function_hide-child_three': '菜单三',
|
||||
function_request: '请求',
|
||||
'function_toggle-auth': '切换权限',
|
||||
'function_super-page': '超级管理员可见',
|
||||
manage: '系统管理',
|
||||
manage_user: '用户管理',
|
||||
'manage_user-detail': '用户详情',
|
||||
manage_role: '角色管理',
|
||||
manage_menu: '菜单管理',
|
||||
'multi-menu': '多级菜单',
|
||||
'multi-menu_first': '菜单一',
|
||||
'multi-menu_first_child': '菜单一子菜单',
|
||||
'multi-menu_second': '菜单二',
|
||||
'multi-menu_second_child': '菜单二子菜单',
|
||||
'multi-menu_second_child_home': '菜单二子菜单首页',
|
||||
exception: '异常页',
|
||||
exception_403: '403',
|
||||
exception_404: '404',
|
||||
exception_500: '500',
|
||||
plugin: '插件示例',
|
||||
plugin_copy: '剪贴板',
|
||||
plugin_charts: '图表',
|
||||
plugin_charts_echarts: 'ECharts',
|
||||
plugin_charts_antv: 'AntV',
|
||||
plugin_charts_vchart: 'VChart',
|
||||
plugin_editor: '编辑器',
|
||||
plugin_editor_quill: '富文本编辑器',
|
||||
plugin_editor_markdown: 'MD 编辑器',
|
||||
plugin_icon: '图标',
|
||||
plugin_map: '地图',
|
||||
plugin_print: '打印',
|
||||
plugin_swiper: 'Swiper',
|
||||
plugin_video: '视频',
|
||||
plugin_barcode: '条形码',
|
||||
plugin_pinyin: '拼音',
|
||||
plugin_excel: 'Excel',
|
||||
plugin_pdf: 'PDF 预览',
|
||||
plugin_gantt: '甘特图',
|
||||
plugin_gantt_dhtmlx: 'dhtmlxGantt',
|
||||
plugin_gantt_vtable: 'VTableGantt',
|
||||
plugin_typeit: '打字机',
|
||||
plugin_tables: '表格',
|
||||
plugin_tables_vtable: 'VTable'
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
@ -246,6 +319,19 @@ const local: App.I18n.Schema = {
|
||||
title: '绑定微信'
|
||||
}
|
||||
},
|
||||
about: {
|
||||
title: '关于',
|
||||
introduction: `SoybeanAdmin 是一个优雅且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite7, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。SoybeanAdmin 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。`,
|
||||
projectInfo: {
|
||||
title: '项目信息',
|
||||
version: '版本',
|
||||
latestBuildTime: '最新构建时间',
|
||||
githubLink: 'Github 地址',
|
||||
previewLink: '预览地址'
|
||||
},
|
||||
prdDep: '生产依赖',
|
||||
devDep: '开发依赖'
|
||||
},
|
||||
home: {
|
||||
branchDesc:
|
||||
'为了方便大家开发和更新合并,我们对main分支的代码进行了精简,只保留了首页菜单,其余内容已移至example分支进行维护。预览地址显示的内容即为example分支的内容。',
|
||||
@ -274,6 +360,253 @@ const local: App.I18n.Schema = {
|
||||
desc5: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!'
|
||||
},
|
||||
creativity: '创意'
|
||||
},
|
||||
function: {
|
||||
tab: {
|
||||
tabOperate: {
|
||||
title: '标签页操作',
|
||||
addTab: '添加标签页',
|
||||
addTabDesc: '跳转到关于页面',
|
||||
closeTab: '关闭标签页',
|
||||
closeCurrentTab: '关闭当前标签页',
|
||||
closeAboutTab: '关闭"关于"标签页',
|
||||
addMultiTab: '添加多标签页',
|
||||
addMultiTabDesc1: '跳转到多标签页页面',
|
||||
addMultiTabDesc2: '跳转到多标签页页面(带有查询参数)'
|
||||
},
|
||||
tabTitle: {
|
||||
title: '标签页标题',
|
||||
changeTitle: '修改标题',
|
||||
change: '修改',
|
||||
resetTitle: '重置标题',
|
||||
reset: '重置'
|
||||
}
|
||||
},
|
||||
multiTab: {
|
||||
routeParam: '路由参数',
|
||||
backTab: '返回 function_tab'
|
||||
},
|
||||
toggleAuth: {
|
||||
toggleAccount: '切换账号',
|
||||
authHook: '权限钩子函数 `hasAuth`',
|
||||
superAdminVisible: '超级管理员可见',
|
||||
adminVisible: '管理员可见',
|
||||
adminOrUserVisible: '管理员和用户可见'
|
||||
},
|
||||
request: {
|
||||
repeatedErrorOccurOnce: '重复请求错误只出现一次',
|
||||
repeatedError: '重复请求错误',
|
||||
repeatedErrorMsg1: '自定义请求错误 1',
|
||||
repeatedErrorMsg2: '自定义请求错误 2'
|
||||
}
|
||||
},
|
||||
alova: {
|
||||
scenes: {
|
||||
captchaSend: '发送验证码',
|
||||
autoRequest: '自动请求',
|
||||
visibilityRequestTips: '浏览器窗口切换自动请求数据',
|
||||
pollingRequestTips: '每3秒自动请求一次',
|
||||
networkRequestTips: '网络重连后自动请求',
|
||||
refreshTime: '更新时间',
|
||||
startRequest: '开始请求',
|
||||
stopRequest: '停止请求',
|
||||
requestCrossComponent: '跨组件触发请求',
|
||||
triggerAllRequest: '手动触发所有自动请求'
|
||||
}
|
||||
},
|
||||
proNaive: {
|
||||
form: {
|
||||
basic: {
|
||||
title: '基础示例',
|
||||
appName: '应用名称',
|
||||
appStatus: '应用状态',
|
||||
createTime: '创建时间',
|
||||
responseDate: '响应日期',
|
||||
specificationInfo: '规格信息',
|
||||
specificate: '规格',
|
||||
specificationName: '规格名',
|
||||
specificationValue: '规格值',
|
||||
specificationColorRed: '红',
|
||||
specificationColorOrange: '橙',
|
||||
addSpecificateItem: '添加规格项',
|
||||
fillValue: '填充值',
|
||||
reset: '重置',
|
||||
submit: '提交',
|
||||
add: '新建',
|
||||
delete: '删除',
|
||||
color: '颜色',
|
||||
normal: '正常',
|
||||
anomaly: '异常'
|
||||
},
|
||||
query: {
|
||||
title1: '查询表单,默认展开',
|
||||
title2: '查询表单,默认折叠,折叠时保留2行',
|
||||
appName: '应用名称',
|
||||
appStatus: '应用状态',
|
||||
createTime: '创建时间',
|
||||
responseDate: '响应日期',
|
||||
endDate: '结束日期',
|
||||
field: '字段'
|
||||
},
|
||||
step: {
|
||||
title: '分步表单',
|
||||
step1: {
|
||||
title: '表单1',
|
||||
field: '表单1字段',
|
||||
nextStep: '下一步'
|
||||
},
|
||||
step2: {
|
||||
title: '表单2',
|
||||
field: '表单2字段',
|
||||
prevStep: '上一步',
|
||||
submit: '提交'
|
||||
}
|
||||
}
|
||||
},
|
||||
table: {
|
||||
remote: {
|
||||
filterCondition: '筛选条件',
|
||||
name: '名称',
|
||||
createTime: '创建时间',
|
||||
responseTime: '响应时间',
|
||||
title: '远程加载',
|
||||
replicableText: '可复制文本',
|
||||
tags: 'tags',
|
||||
dateFormatting: '日期格式化',
|
||||
image: '图片'
|
||||
},
|
||||
rowEdit: {
|
||||
title: '编辑表格',
|
||||
reset: '重置',
|
||||
submit: '提交',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
save: '保存',
|
||||
task: '任务',
|
||||
score: '评分',
|
||||
time: '时间',
|
||||
name: '名称',
|
||||
action: '操作'
|
||||
}
|
||||
}
|
||||
},
|
||||
manage: {
|
||||
common: {
|
||||
status: {
|
||||
enable: '启用',
|
||||
disable: '禁用'
|
||||
}
|
||||
},
|
||||
role: {
|
||||
title: '角色列表',
|
||||
roleName: '角色名称',
|
||||
roleCode: '角色编码',
|
||||
roleStatus: '角色状态',
|
||||
roleDesc: '角色描述',
|
||||
menuAuth: '菜单权限',
|
||||
buttonAuth: '按钮权限',
|
||||
form: {
|
||||
roleName: '请输入角色名称',
|
||||
roleCode: '请输入角色编码',
|
||||
roleStatus: '请选择角色状态',
|
||||
roleDesc: '请输入角色描述'
|
||||
},
|
||||
addRole: '新增角色',
|
||||
editRole: '编辑角色'
|
||||
},
|
||||
user: {
|
||||
title: '用户列表',
|
||||
userName: '用户名',
|
||||
userGender: '性别',
|
||||
nickName: '昵称',
|
||||
userPhone: '手机号',
|
||||
userEmail: '邮箱',
|
||||
userStatus: '用户状态',
|
||||
userRole: '用户角色',
|
||||
form: {
|
||||
userName: '请输入用户名',
|
||||
userGender: '请选择性别',
|
||||
nickName: '请输入昵称',
|
||||
userPhone: '请输入手机号',
|
||||
userEmail: '请输入邮箱',
|
||||
userStatus: '请选择用户状态',
|
||||
userRole: '请选择用户角色'
|
||||
},
|
||||
addUser: '新增用户',
|
||||
editUser: '编辑用户',
|
||||
gender: {
|
||||
male: '男',
|
||||
female: '女'
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
home: '首页',
|
||||
title: '菜单列表',
|
||||
id: 'ID',
|
||||
parentId: '父级菜单ID',
|
||||
menuType: '菜单类型',
|
||||
menuName: '菜单名称',
|
||||
routeName: '路由名称',
|
||||
routePath: '路由路径',
|
||||
pathParam: '路径参数',
|
||||
layout: '布局',
|
||||
page: '页面组件',
|
||||
i18nKey: '国际化key',
|
||||
icon: '图标',
|
||||
localIcon: '本地图标',
|
||||
iconTypeTitle: '图标类型',
|
||||
order: '排序',
|
||||
constant: '常量路由',
|
||||
keepAlive: '缓存路由',
|
||||
href: '外链',
|
||||
hideInMenu: '隐藏菜单',
|
||||
activeMenu: '高亮的菜单',
|
||||
multiTab: '支持多页签',
|
||||
fixedIndexInTab: '固定在页签中的序号',
|
||||
query: '路由参数',
|
||||
button: '按钮',
|
||||
buttonCode: '按钮编码',
|
||||
buttonDesc: '按钮描述',
|
||||
menuStatus: '菜单状态',
|
||||
form: {
|
||||
home: '请选择首页',
|
||||
menuType: '请选择菜单类型',
|
||||
menuName: '请输入菜单名称',
|
||||
routeName: '请输入路由名称',
|
||||
routePath: '请输入路由路径',
|
||||
pathParam: '请输入路径参数',
|
||||
page: '请选择页面组件',
|
||||
layout: '请选择布局组件',
|
||||
i18nKey: '请输入国际化key',
|
||||
icon: '请输入图标',
|
||||
localIcon: '请选择本地图标',
|
||||
order: '请输入排序',
|
||||
keepAlive: '请选择是否缓存路由',
|
||||
href: '请输入外链',
|
||||
hideInMenu: '请选择是否隐藏菜单',
|
||||
activeMenu: '请选择高亮的菜单的路由名称',
|
||||
multiTab: '请选择是否支持多标签',
|
||||
fixedInTab: '请选择是否固定在页签中',
|
||||
fixedIndexInTab: '请输入固定在页签中的序号',
|
||||
queryKey: '请输入路由参数Key',
|
||||
queryValue: '请输入路由参数Value',
|
||||
button: '请选择是否按钮',
|
||||
buttonCode: '请输入按钮编码',
|
||||
buttonDesc: '请输入按钮描述',
|
||||
menuStatus: '请选择菜单状态'
|
||||
},
|
||||
addMenu: '新增菜单',
|
||||
editMenu: '编辑菜单',
|
||||
addChildMenu: '新增子菜单',
|
||||
type: {
|
||||
directory: '目录',
|
||||
menu: '菜单'
|
||||
},
|
||||
iconType: {
|
||||
iconify: 'iconify图标',
|
||||
local: '本地图标'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
form: {
|
||||
|
11
src/main.ts
11
src/main.ts
@ -1,6 +1,13 @@
|
||||
import { createApp } from 'vue';
|
||||
import './plugins/assets';
|
||||
import { setupAppVersionNotification, setupDayjs, setupIconifyOffline, setupLoading, setupNProgress } from './plugins';
|
||||
import {
|
||||
setupAppVersionNotification,
|
||||
setupDayjs,
|
||||
setupIconifyOffline,
|
||||
setupLoading,
|
||||
setupNProgress,
|
||||
setupProNaiveComponents
|
||||
} from './plugins';
|
||||
import { setupStore } from './store';
|
||||
import { setupRouter } from './router';
|
||||
import { setupI18n } from './locales';
|
||||
@ -21,6 +28,8 @@ async function setupApp() {
|
||||
|
||||
await setupRouter(app);
|
||||
|
||||
setupProNaiveComponents(app);
|
||||
|
||||
setupI18n(app);
|
||||
|
||||
setupAppVersionNotification();
|
||||
|
@ -1,3 +1,6 @@
|
||||
import 'virtual:svg-icons-register';
|
||||
import 'uno.css';
|
||||
import '../styles/css/global.css';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
import 'swiper/css/pagination';
|
||||
|
10
src/plugins/components.ts
Normal file
10
src/plugins/components.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { App } from 'vue';
|
||||
import { ProDate, ProDateTime, ProInput, ProRate, create } from 'pro-naive-ui';
|
||||
|
||||
/** pro-naive-ui 支持配置表单的按需加载,所以需要注册 */
|
||||
export function setupProNaiveComponents(app: App) {
|
||||
const proNaive = create({
|
||||
components: [ProInput, ProDate, ProDateTime, ProRate]
|
||||
});
|
||||
app.use(proNaive);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
export * from './loading';
|
||||
export * from './nprogress';
|
||||
export * from './components';
|
||||
export * from './iconify';
|
||||
export * from './dayjs';
|
||||
export * from './app';
|
||||
|
@ -20,5 +20,47 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
500: () => import("@/views/_builtin/500/index.vue"),
|
||||
"iframe-page": () => 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"),
|
||||
"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"),
|
||||
"function_multi-tab": () => import("@/views/function/multi-tab/index.vue"),
|
||||
function_request: () => import("@/views/function/request/index.vue"),
|
||||
"function_super-page": () => import("@/views/function/super-page/index.vue"),
|
||||
function_tab: () => import("@/views/function/tab/index.vue"),
|
||||
"function_toggle-auth": () => import("@/views/function/toggle-auth/index.vue"),
|
||||
home: () => import("@/views/home/index.vue"),
|
||||
manage_menu: () => import("@/views/manage/menu/index.vue"),
|
||||
manage_role: () => import("@/views/manage/role/index.vue"),
|
||||
"manage_user-detail": () => import("@/views/manage/user-detail/[id].vue"),
|
||||
manage_user: () => import("@/views/manage/user/index.vue"),
|
||||
"multi-menu_first_child": () => import("@/views/multi-menu/first_child/index.vue"),
|
||||
"multi-menu_second_child_home": () => import("@/views/multi-menu/second_child_home/index.vue"),
|
||||
plugin_barcode: () => import("@/views/plugin/barcode/index.vue"),
|
||||
plugin_charts_antv: () => import("@/views/plugin/charts/antv/index.vue"),
|
||||
plugin_charts_echarts: () => import("@/views/plugin/charts/echarts/index.vue"),
|
||||
plugin_charts_vchart: () => import("@/views/plugin/charts/vchart/index.vue"),
|
||||
plugin_copy: () => import("@/views/plugin/copy/index.vue"),
|
||||
plugin_editor_markdown: () => import("@/views/plugin/editor/markdown/index.vue"),
|
||||
plugin_editor_quill: () => import("@/views/plugin/editor/quill/index.vue"),
|
||||
plugin_excel: () => import("@/views/plugin/excel/index.vue"),
|
||||
plugin_gantt_dhtmlx: () => import("@/views/plugin/gantt/dhtmlx/index.vue"),
|
||||
plugin_gantt_vtable: () => import("@/views/plugin/gantt/vtable/index.vue"),
|
||||
plugin_icon: () => import("@/views/plugin/icon/index.vue"),
|
||||
plugin_map: () => import("@/views/plugin/map/index.vue"),
|
||||
plugin_pdf: () => import("@/views/plugin/pdf/index.vue"),
|
||||
plugin_pinyin: () => import("@/views/plugin/pinyin/index.vue"),
|
||||
plugin_print: () => import("@/views/plugin/print/index.vue"),
|
||||
plugin_swiper: () => import("@/views/plugin/swiper/index.vue"),
|
||||
plugin_tables_vtable: () => import("@/views/plugin/tables/vtable/index.vue"),
|
||||
plugin_typeit: () => import("@/views/plugin/typeit/index.vue"),
|
||||
plugin_video: () => import("@/views/plugin/video/index.vue"),
|
||||
"pro-naive_form_basic": () => import("@/views/pro-naive/form/basic/index.vue"),
|
||||
"pro-naive_form_query": () => import("@/views/pro-naive/form/query/index.vue"),
|
||||
"pro-naive_form_step": () => import("@/views/pro-naive/form/step/index.vue"),
|
||||
"pro-naive_table_remote": () => import("@/views/pro-naive/table/remote/index.vue"),
|
||||
"pro-naive_table_row-edit": () => import("@/views/pro-naive/table/row-edit/index.vue"),
|
||||
"user-center": () => import("@/views/user-center/index.vue"),
|
||||
};
|
||||
|
@ -39,6 +39,169 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'about',
|
||||
path: '/about',
|
||||
component: 'layout.base$view.about',
|
||||
meta: {
|
||||
title: 'about',
|
||||
i18nKey: 'route.about',
|
||||
icon: 'fluent:book-information-24-regular',
|
||||
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: 'function',
|
||||
path: '/function',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'function',
|
||||
i18nKey: 'route.function',
|
||||
icon: 'icon-park-outline:all-application',
|
||||
order: 6
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'function_hide-child',
|
||||
path: '/function/hide-child',
|
||||
meta: {
|
||||
title: 'function_hide-child',
|
||||
i18nKey: 'route.function_hide-child',
|
||||
icon: 'material-symbols:filter-list-off',
|
||||
order: 2
|
||||
},
|
||||
redirect: '/function/hide-child/one',
|
||||
children: [
|
||||
{
|
||||
name: 'function_hide-child_one',
|
||||
path: '/function/hide-child/one',
|
||||
component: 'view.function_hide-child_one',
|
||||
meta: {
|
||||
title: 'function_hide-child_one',
|
||||
i18nKey: 'route.function_hide-child_one',
|
||||
icon: 'material-symbols:filter-list-off',
|
||||
hideInMenu: true,
|
||||
activeMenu: 'function_hide-child'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'function_hide-child_three',
|
||||
path: '/function/hide-child/three',
|
||||
component: 'view.function_hide-child_three',
|
||||
meta: {
|
||||
title: 'function_hide-child_three',
|
||||
i18nKey: 'route.function_hide-child_three',
|
||||
hideInMenu: true,
|
||||
activeMenu: 'function_hide-child'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'function_hide-child_two',
|
||||
path: '/function/hide-child/two',
|
||||
component: 'view.function_hide-child_two',
|
||||
meta: {
|
||||
title: 'function_hide-child_two',
|
||||
i18nKey: 'route.function_hide-child_two',
|
||||
hideInMenu: true,
|
||||
activeMenu: 'function_hide-child'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'function_multi-tab',
|
||||
path: '/function/multi-tab',
|
||||
component: 'view.function_multi-tab',
|
||||
meta: {
|
||||
title: 'function_multi-tab',
|
||||
i18nKey: 'route.function_multi-tab',
|
||||
icon: 'ic:round-tab',
|
||||
multiTab: true,
|
||||
hideInMenu: true,
|
||||
activeMenu: 'function_tab'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'function_request',
|
||||
path: '/function/request',
|
||||
component: 'view.function_request',
|
||||
meta: {
|
||||
title: 'function_request',
|
||||
i18nKey: 'route.function_request',
|
||||
icon: 'carbon:network-overlay',
|
||||
order: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'function_super-page',
|
||||
path: '/function/super-page',
|
||||
component: 'view.function_super-page',
|
||||
meta: {
|
||||
title: 'function_super-page',
|
||||
i18nKey: 'route.function_super-page',
|
||||
icon: 'ic:round-supervisor-account',
|
||||
order: 5,
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'function_tab',
|
||||
path: '/function/tab',
|
||||
component: 'view.function_tab',
|
||||
meta: {
|
||||
title: 'function_tab',
|
||||
i18nKey: 'route.function_tab',
|
||||
icon: 'ic:round-tab',
|
||||
order: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'function_toggle-auth',
|
||||
path: '/function/toggle-auth',
|
||||
component: 'view.function_toggle-auth',
|
||||
meta: {
|
||||
title: 'function_toggle-auth',
|
||||
i18nKey: 'route.function_toggle-auth',
|
||||
icon: 'ic:round-construction',
|
||||
order: 4
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'home',
|
||||
path: '/home',
|
||||
@ -74,5 +237,468 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
constant: true,
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'manage',
|
||||
path: '/manage',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'manage',
|
||||
i18nKey: 'route.manage',
|
||||
icon: 'carbon:cloud-service-management',
|
||||
order: 9,
|
||||
roles: ['R_ADMIN']
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'manage_menu',
|
||||
path: '/manage/menu',
|
||||
component: 'view.manage_menu',
|
||||
meta: {
|
||||
title: 'manage_menu',
|
||||
i18nKey: 'route.manage_menu',
|
||||
icon: 'material-symbols:route',
|
||||
order: 3,
|
||||
roles: ['R_ADMIN'],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'manage_role',
|
||||
path: '/manage/role',
|
||||
component: 'view.manage_role',
|
||||
meta: {
|
||||
title: 'manage_role',
|
||||
i18nKey: 'route.manage_role',
|
||||
icon: 'carbon:user-role',
|
||||
order: 2,
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'manage_user',
|
||||
path: '/manage/user',
|
||||
component: 'view.manage_user',
|
||||
meta: {
|
||||
title: 'manage_user',
|
||||
i18nKey: 'route.manage_user',
|
||||
icon: 'ic:round-manage-accounts',
|
||||
order: 1,
|
||||
roles: ['R_ADMIN']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'manage_user-detail',
|
||||
path: '/manage/user-detail/:id',
|
||||
component: 'view.manage_user-detail',
|
||||
props: true,
|
||||
meta: {
|
||||
title: 'manage_user-detail',
|
||||
i18nKey: 'route.manage_user-detail',
|
||||
hideInMenu: true,
|
||||
roles: ['R_ADMIN'],
|
||||
activeMenu: 'manage_user'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'multi-menu',
|
||||
path: '/multi-menu',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'multi-menu',
|
||||
i18nKey: 'route.multi-menu',
|
||||
order: 8
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'multi-menu_first',
|
||||
path: '/multi-menu/first',
|
||||
meta: {
|
||||
title: 'multi-menu_first',
|
||||
i18nKey: 'route.multi-menu_first',
|
||||
order: 1
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'multi-menu_first_child',
|
||||
path: '/multi-menu/first/child',
|
||||
component: 'view.multi-menu_first_child',
|
||||
meta: {
|
||||
title: 'multi-menu_first_child',
|
||||
i18nKey: 'route.multi-menu_first_child'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'multi-menu_second',
|
||||
path: '/multi-menu/second',
|
||||
meta: {
|
||||
title: 'multi-menu_second',
|
||||
i18nKey: 'route.multi-menu_second',
|
||||
order: 2
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'multi-menu_second_child',
|
||||
path: '/multi-menu/second/child',
|
||||
meta: {
|
||||
title: 'multi-menu_second_child',
|
||||
i18nKey: 'route.multi-menu_second_child'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'multi-menu_second_child_home',
|
||||
path: '/multi-menu/second/child/home',
|
||||
component: 'view.multi-menu_second_child_home',
|
||||
meta: {
|
||||
title: 'multi-menu_second_child_home',
|
||||
i18nKey: 'route.multi-menu_second_child_home'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'plugin',
|
||||
path: '/plugin',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: '插件示例',
|
||||
i18nKey: 'route.plugin',
|
||||
order: 7,
|
||||
icon: 'clarity:plugin-line'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_barcode',
|
||||
path: '/plugin/barcode',
|
||||
component: 'view.plugin_barcode',
|
||||
meta: {
|
||||
title: 'plugin_barcode',
|
||||
i18nKey: 'route.plugin_barcode',
|
||||
icon: 'ic:round-barcode'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_charts',
|
||||
path: '/plugin/charts',
|
||||
meta: {
|
||||
title: 'plugin_charts',
|
||||
i18nKey: 'route.plugin_charts',
|
||||
icon: 'mdi:chart-areaspline'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_charts_antv',
|
||||
path: '/plugin/charts/antv',
|
||||
component: 'view.plugin_charts_antv',
|
||||
meta: {
|
||||
title: 'plugin_charts_antv',
|
||||
i18nKey: 'route.plugin_charts_antv',
|
||||
icon: 'hugeicons:flow-square'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_charts_echarts',
|
||||
path: '/plugin/charts/echarts',
|
||||
component: 'view.plugin_charts_echarts',
|
||||
meta: {
|
||||
title: 'plugin_charts_echarts',
|
||||
i18nKey: 'route.plugin_charts_echarts',
|
||||
icon: 'simple-icons:apacheecharts'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_charts_vchart',
|
||||
path: '/plugin/charts/vchart',
|
||||
component: 'view.plugin_charts_vchart',
|
||||
meta: {
|
||||
title: 'plugin_charts_vchart',
|
||||
i18nKey: 'route.plugin_charts_vchart',
|
||||
localIcon: 'visactor'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'plugin_copy',
|
||||
path: '/plugin/copy',
|
||||
component: 'view.plugin_copy',
|
||||
meta: {
|
||||
title: 'plugin_copy',
|
||||
i18nKey: 'route.plugin_copy',
|
||||
icon: 'mdi:clipboard-outline'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_editor',
|
||||
path: '/plugin/editor',
|
||||
meta: {
|
||||
title: 'plugin_editor',
|
||||
i18nKey: 'route.plugin_editor',
|
||||
icon: 'icon-park-outline:editor'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_editor_markdown',
|
||||
path: '/plugin/editor/markdown',
|
||||
component: 'view.plugin_editor_markdown',
|
||||
meta: {
|
||||
title: 'plugin_editor_markdown',
|
||||
i18nKey: 'route.plugin_editor_markdown',
|
||||
icon: 'ri:markdown-line'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_editor_quill',
|
||||
path: '/plugin/editor/quill',
|
||||
component: 'view.plugin_editor_quill',
|
||||
meta: {
|
||||
title: 'plugin_editor_quill',
|
||||
i18nKey: 'route.plugin_editor_quill',
|
||||
icon: 'mdi:file-document-edit-outline'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'plugin_excel',
|
||||
path: '/plugin/excel',
|
||||
component: 'view.plugin_excel',
|
||||
meta: {
|
||||
title: 'plugin_excel',
|
||||
i18nKey: 'route.plugin_excel',
|
||||
icon: 'ri:file-excel-2-line',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_gantt',
|
||||
path: '/plugin/gantt',
|
||||
meta: {
|
||||
title: 'plugin_gantt',
|
||||
i18nKey: 'route.plugin_gantt',
|
||||
icon: 'ant-design:bar-chart-outlined'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_gantt_dhtmlx',
|
||||
path: '/plugin/gantt/dhtmlx',
|
||||
component: 'view.plugin_gantt_dhtmlx',
|
||||
meta: {
|
||||
title: 'plugin_gantt_dhtmlx',
|
||||
i18nKey: 'route.plugin_gantt_dhtmlx'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_gantt_vtable',
|
||||
path: '/plugin/gantt/vtable',
|
||||
component: 'view.plugin_gantt_vtable',
|
||||
meta: {
|
||||
title: 'plugin_gantt_vtable',
|
||||
i18nKey: 'route.plugin_gantt_vtable',
|
||||
localIcon: 'visactor'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'plugin_icon',
|
||||
path: '/plugin/icon',
|
||||
component: 'view.plugin_icon',
|
||||
meta: {
|
||||
title: 'plugin_icon',
|
||||
i18nKey: 'route.plugin_icon',
|
||||
localIcon: 'custom-icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_map',
|
||||
path: '/plugin/map',
|
||||
component: 'view.plugin_map',
|
||||
meta: {
|
||||
title: 'plugin_map',
|
||||
i18nKey: 'route.plugin_map',
|
||||
icon: 'mdi:map'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_pdf',
|
||||
path: '/plugin/pdf',
|
||||
component: 'view.plugin_pdf',
|
||||
meta: {
|
||||
title: 'plugin_pdf',
|
||||
i18nKey: 'route.plugin_pdf',
|
||||
icon: 'uiw:file-pdf'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_pinyin',
|
||||
path: '/plugin/pinyin',
|
||||
component: 'view.plugin_pinyin',
|
||||
meta: {
|
||||
title: 'plugin_pinyin',
|
||||
i18nKey: 'route.plugin_pinyin',
|
||||
icon: 'entypo-social:google-hangouts'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_print',
|
||||
path: '/plugin/print',
|
||||
component: 'view.plugin_print',
|
||||
meta: {
|
||||
title: 'plugin_print',
|
||||
i18nKey: 'route.plugin_print',
|
||||
icon: 'mdi:printer'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_swiper',
|
||||
path: '/plugin/swiper',
|
||||
component: 'view.plugin_swiper',
|
||||
meta: {
|
||||
title: 'plugin_swiper',
|
||||
i18nKey: 'route.plugin_swiper',
|
||||
icon: 'simple-icons:swiper'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_tables',
|
||||
path: '/plugin/tables',
|
||||
meta: {
|
||||
title: 'plugin_tables',
|
||||
i18nKey: 'route.plugin_tables',
|
||||
icon: 'icon-park-outline:table'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'plugin_tables_vtable',
|
||||
path: '/plugin/tables/vtable',
|
||||
component: 'view.plugin_tables_vtable',
|
||||
meta: {
|
||||
title: 'plugin_tables_vtable',
|
||||
i18nKey: 'route.plugin_tables_vtable',
|
||||
localIcon: 'visactor'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'plugin_typeit',
|
||||
path: '/plugin/typeit',
|
||||
component: 'view.plugin_typeit',
|
||||
meta: {
|
||||
title: 'plugin_typeit',
|
||||
i18nKey: 'route.plugin_typeit',
|
||||
icon: 'mdi:typewriter'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plugin_video',
|
||||
path: '/plugin/video',
|
||||
component: 'view.plugin_video',
|
||||
meta: {
|
||||
title: 'plugin_video',
|
||||
i18nKey: 'route.plugin_video',
|
||||
icon: 'mdi:video'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'pro-naive',
|
||||
path: '/pro-naive',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'pro-naive',
|
||||
i18nKey: 'route.pro-naive',
|
||||
order: 7,
|
||||
icon: 'material-symbols-light:demography-outline-rounded'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'pro-naive_form',
|
||||
path: '/pro-naive/form',
|
||||
meta: {
|
||||
title: 'pro-naive_form',
|
||||
i18nKey: 'route.pro-naive_form',
|
||||
icon: 'fluent:form-28-regular'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'pro-naive_form_basic',
|
||||
path: '/pro-naive/form/basic',
|
||||
component: 'view.pro-naive_form_basic',
|
||||
meta: {
|
||||
title: 'pro-naive_form_basic',
|
||||
i18nKey: 'route.pro-naive_form_basic'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'pro-naive_form_query',
|
||||
path: '/pro-naive/form/query',
|
||||
component: 'view.pro-naive_form_query',
|
||||
meta: {
|
||||
title: 'pro-naive_form_query',
|
||||
i18nKey: 'route.pro-naive_form_query'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'pro-naive_form_step',
|
||||
path: '/pro-naive/form/step',
|
||||
component: 'view.pro-naive_form_step',
|
||||
meta: {
|
||||
title: 'pro-naive_form_step',
|
||||
i18nKey: 'route.pro-naive_form_step'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'pro-naive_table',
|
||||
path: '/pro-naive/table',
|
||||
meta: {
|
||||
title: 'pro-naive_table',
|
||||
i18nKey: 'route.pro-naive_table',
|
||||
icon: 'mynaui:table'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'pro-naive_table_remote',
|
||||
path: '/pro-naive/table/remote',
|
||||
component: 'view.pro-naive_table_remote',
|
||||
meta: {
|
||||
title: 'pro-naive_table_remote',
|
||||
i18nKey: 'route.pro-naive_table_remote'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'pro-naive_table_row-edit',
|
||||
path: '/pro-naive/table/row-edit',
|
||||
component: 'view.pro-naive_table_row-edit',
|
||||
meta: {
|
||||
title: 'pro-naive_table_row-edit',
|
||||
i18nKey: 'route.pro-naive_table_row-edit'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'user-center',
|
||||
path: '/user-center',
|
||||
component: 'layout.base$view.user-center',
|
||||
meta: {
|
||||
title: 'user-center',
|
||||
i18nKey: 'route.user-center',
|
||||
hideInMenu: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
@ -163,12 +163,85 @@ function transformElegantRouteToVueRoute(
|
||||
const routeMap: RouteMap = {
|
||||
"root": "/",
|
||||
"not-found": "/:pathMatch(.*)*",
|
||||
"exception": "/exception",
|
||||
"exception_403": "/exception/403",
|
||||
"exception_404": "/exception/404",
|
||||
"exception_500": "/exception/500",
|
||||
"document": "/document",
|
||||
"document_project": "/document/project",
|
||||
"document_project-link": "/document/project-link",
|
||||
"document_video": "/document/video",
|
||||
"document_vue": "/document/vue",
|
||||
"document_vite": "/document/vite",
|
||||
"document_unocss": "/document/unocss",
|
||||
"document_naive": "/document/naive",
|
||||
"document_pro-naive": "/document/pro-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",
|
||||
"function": "/function",
|
||||
"function_hide-child": "/function/hide-child",
|
||||
"function_hide-child_one": "/function/hide-child/one",
|
||||
"function_hide-child_three": "/function/hide-child/three",
|
||||
"function_hide-child_two": "/function/hide-child/two",
|
||||
"function_multi-tab": "/function/multi-tab",
|
||||
"function_request": "/function/request",
|
||||
"function_super-page": "/function/super-page",
|
||||
"function_tab": "/function/tab",
|
||||
"function_toggle-auth": "/function/toggle-auth",
|
||||
"home": "/home",
|
||||
"iframe-page": "/iframe-page/:url",
|
||||
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"
|
||||
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
|
||||
"manage": "/manage",
|
||||
"manage_menu": "/manage/menu",
|
||||
"manage_role": "/manage/role",
|
||||
"manage_user": "/manage/user",
|
||||
"manage_user-detail": "/manage/user-detail/:id",
|
||||
"multi-menu": "/multi-menu",
|
||||
"multi-menu_first": "/multi-menu/first",
|
||||
"multi-menu_first_child": "/multi-menu/first/child",
|
||||
"multi-menu_second": "/multi-menu/second",
|
||||
"multi-menu_second_child": "/multi-menu/second/child",
|
||||
"multi-menu_second_child_home": "/multi-menu/second/child/home",
|
||||
"plugin": "/plugin",
|
||||
"plugin_barcode": "/plugin/barcode",
|
||||
"plugin_charts": "/plugin/charts",
|
||||
"plugin_charts_antv": "/plugin/charts/antv",
|
||||
"plugin_charts_echarts": "/plugin/charts/echarts",
|
||||
"plugin_charts_vchart": "/plugin/charts/vchart",
|
||||
"plugin_copy": "/plugin/copy",
|
||||
"plugin_editor": "/plugin/editor",
|
||||
"plugin_editor_markdown": "/plugin/editor/markdown",
|
||||
"plugin_editor_quill": "/plugin/editor/quill",
|
||||
"plugin_excel": "/plugin/excel",
|
||||
"plugin_gantt": "/plugin/gantt",
|
||||
"plugin_gantt_dhtmlx": "/plugin/gantt/dhtmlx",
|
||||
"plugin_gantt_vtable": "/plugin/gantt/vtable",
|
||||
"plugin_icon": "/plugin/icon",
|
||||
"plugin_map": "/plugin/map",
|
||||
"plugin_pdf": "/plugin/pdf",
|
||||
"plugin_pinyin": "/plugin/pinyin",
|
||||
"plugin_print": "/plugin/print",
|
||||
"plugin_swiper": "/plugin/swiper",
|
||||
"plugin_tables": "/plugin/tables",
|
||||
"plugin_tables_vtable": "/plugin/tables/vtable",
|
||||
"plugin_typeit": "/plugin/typeit",
|
||||
"plugin_video": "/plugin/video",
|
||||
"pro-naive": "/pro-naive",
|
||||
"pro-naive_form": "/pro-naive/form",
|
||||
"pro-naive_form_basic": "/pro-naive/form/basic",
|
||||
"pro-naive_form_query": "/pro-naive/form/query",
|
||||
"pro-naive_form_step": "/pro-naive/form/step",
|
||||
"pro-naive_table": "/pro-naive/table",
|
||||
"pro-naive_table_remote": "/pro-naive/table/remote",
|
||||
"pro-naive_table_row-edit": "/pro-naive/table/row-edit",
|
||||
"user-center": "/user-center"
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -8,7 +8,200 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
|
||||
*
|
||||
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
|
||||
*/
|
||||
const customRoutes: CustomRoute[] = [];
|
||||
const customRoutes: CustomRoute[] = [
|
||||
{
|
||||
name: 'exception',
|
||||
path: '/exception',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'exception',
|
||||
i18nKey: 'route.exception',
|
||||
icon: 'ant-design:exception-outlined',
|
||||
order: 7
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'exception_403',
|
||||
path: '/exception/403',
|
||||
component: 'view.403',
|
||||
meta: {
|
||||
title: 'exception_403',
|
||||
i18nKey: 'route.exception_403',
|
||||
icon: 'ic:baseline-block'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'exception_404',
|
||||
path: '/exception/404',
|
||||
component: 'view.404',
|
||||
meta: {
|
||||
title: 'exception_404',
|
||||
i18nKey: 'route.exception_404',
|
||||
icon: 'ic:baseline-web-asset-off'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'exception_500',
|
||||
path: '/exception/500',
|
||||
component: 'view.500',
|
||||
meta: {
|
||||
title: 'exception_500',
|
||||
i18nKey: 'route.exception_500',
|
||||
icon: 'ic:baseline-wifi-off'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'document',
|
||||
path: '/document',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'document',
|
||||
i18nKey: 'route.document',
|
||||
order: 2,
|
||||
icon: 'mdi:file-document-multiple-outline'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'document_antd',
|
||||
path: '/document/antd',
|
||||
component: 'view.iframe-page',
|
||||
props: {
|
||||
url: 'https://antdv.com/components/overview-cn'
|
||||
},
|
||||
meta: {
|
||||
title: 'document_antd',
|
||||
i18nKey: 'route.document_antd',
|
||||
order: 7,
|
||||
icon: 'logos:ant-design'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'document_naive',
|
||||
path: '/document/naive',
|
||||
component: 'view.iframe-page',
|
||||
props: {
|
||||
url: 'https://www.naiveui.com/zh-CN/os-theme/docs/introduction'
|
||||
},
|
||||
meta: {
|
||||
title: 'document_naive',
|
||||
i18nKey: 'route.document_naive',
|
||||
order: 6,
|
||||
icon: 'logos:naiveui'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'document_pro-naive',
|
||||
path: '/document/pro-naive',
|
||||
component: 'view.iframe-page',
|
||||
props: {
|
||||
url: 'https://naive-ui.pro-components.cn/'
|
||||
},
|
||||
meta: {
|
||||
title: 'document_pro-naive',
|
||||
i18nKey: 'route.document_pro-naive',
|
||||
order: 6,
|
||||
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',
|
||||
component: 'view.iframe-page',
|
||||
props: {
|
||||
url: 'https://docs.soybeanjs.cn/zh'
|
||||
},
|
||||
meta: {
|
||||
title: 'document_project',
|
||||
i18nKey: 'route.document_project',
|
||||
order: 1,
|
||||
localIcon: 'logo'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'document_project-link',
|
||||
path: '/document/project-link',
|
||||
component: 'view.iframe-page',
|
||||
meta: {
|
||||
title: 'document_project-link',
|
||||
i18nKey: 'route.document_project-link',
|
||||
order: 2,
|
||||
localIcon: 'logo',
|
||||
href: 'https://docs.soybeanjs.cn/zh'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'document_video',
|
||||
path: '/document/video',
|
||||
component: 'view.iframe-page',
|
||||
meta: {
|
||||
title: 'document_video',
|
||||
i18nKey: 'route.document_video',
|
||||
order: 2,
|
||||
localIcon: 'logo',
|
||||
href: 'https://www.bilibili.com/video/BV1YKdRYXELC'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'document_unocss',
|
||||
path: '/document/unocss',
|
||||
component: 'view.iframe-page',
|
||||
props: {
|
||||
url: 'https://unocss.dev/'
|
||||
},
|
||||
meta: {
|
||||
title: 'document_unocss',
|
||||
i18nKey: 'route.document_unocss',
|
||||
order: 5,
|
||||
icon: 'logos:unocss'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'document_vite',
|
||||
path: '/document/vite',
|
||||
component: 'view.iframe-page',
|
||||
props: {
|
||||
url: 'https://cn.vitejs.dev/'
|
||||
},
|
||||
meta: {
|
||||
title: 'document_vite',
|
||||
i18nKey: 'route.document_vite',
|
||||
order: 4,
|
||||
icon: 'logos:vitejs'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'document_vue',
|
||||
path: '/document/vue',
|
||||
component: 'view.iframe-page',
|
||||
props: {
|
||||
url: 'https://cn.vuejs.org/'
|
||||
},
|
||||
meta: {
|
||||
title: 'document_vue',
|
||||
i18nKey: 'route.document_vue',
|
||||
order: 3,
|
||||
icon: 'logos:vue'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/** create routes when the auth route mode is static */
|
||||
export function createStaticRoutes() {
|
||||
|
56
src/service-alova/api/auth.ts
Normal file
56
src/service-alova/api/auth.ts
Normal file
@ -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<Api.Auth.LoginToken>('/auth/login', { userName, password });
|
||||
}
|
||||
|
||||
/** Get user info */
|
||||
export function fetchGetUserInfo() {
|
||||
return alova.Get<Api.Auth.UserInfo>('/auth/getUserInfo');
|
||||
}
|
||||
|
||||
/** Send captcha to target phone */
|
||||
export function sendCaptcha(phone: string) {
|
||||
return alova.Post<null>('/auth/sendCaptcha', { phone });
|
||||
}
|
||||
|
||||
/** Verify captcha */
|
||||
export function verifyCaptcha(phone: string, code: string) {
|
||||
return alova.Post<null>('/auth/verifyCaptcha', { phone, code });
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh token
|
||||
*
|
||||
* @param refreshToken Refresh token
|
||||
*/
|
||||
export function fetchRefreshToken(refreshToken: string) {
|
||||
return alova.Post<Api.Auth.LoginToken>(
|
||||
'/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
|
||||
});
|
||||
}
|
3
src/service-alova/api/index.ts
Normal file
3
src/service-alova/api/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './route';
|
||||
export * from './system-manage';
|
20
src/service-alova/api/route.ts
Normal file
20
src/service-alova/api/route.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { alova } from '../request';
|
||||
|
||||
/** get constant routes */
|
||||
export function fetchGetConstantRoutes() {
|
||||
return alova.Get<Api.Route.MenuRoute[]>('/route/getConstantRoutes');
|
||||
}
|
||||
|
||||
/** get user routes */
|
||||
export function fetchGetUserRoutes() {
|
||||
return alova.Get<Api.Route.UserRoute>('/route/getUserRoutes');
|
||||
}
|
||||
|
||||
/**
|
||||
* whether the route is exist
|
||||
*
|
||||
* @param routeName route name
|
||||
*/
|
||||
export function fetchIsRouteExist(routeName: string) {
|
||||
return alova.Get<boolean>('/route/isRouteExist', { params: { routeName } });
|
||||
}
|
59
src/service-alova/api/system-manage.ts
Normal file
59
src/service-alova/api/system-manage.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { alova } from '../request';
|
||||
|
||||
/** get role list */
|
||||
export function fetchGetRoleList(params?: Api.SystemManage.RoleSearchParams) {
|
||||
return alova.Get<Api.SystemManage.RoleList>('/systemManage/getRoleList', { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* get all roles
|
||||
*
|
||||
* these roles are all enabled
|
||||
*/
|
||||
export function fetchGetAllRoles() {
|
||||
return alova.Get<Api.SystemManage.AllRole[]>('/systemManage/getAllRoles');
|
||||
}
|
||||
|
||||
/** get user list */
|
||||
export function fetchGetUserList(params?: Api.SystemManage.UserSearchParams) {
|
||||
return alova.Get<Api.SystemManage.UserList>('/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<null>('/systemManage/addUser', data);
|
||||
}
|
||||
|
||||
/** update user */
|
||||
export function updateUser(data: UserModel) {
|
||||
return alova.Post<null>('/systemManage/updateUser', data);
|
||||
}
|
||||
|
||||
/** delete user */
|
||||
export function deleteUser(id: number) {
|
||||
return alova.Delete<null>('/systemManage/deleteUser', { id });
|
||||
}
|
||||
|
||||
/** batch delete user */
|
||||
export function batchDeleteUser(ids: number[]) {
|
||||
return alova.Delete<null>('/systemManage/batchDeleteUser', { ids });
|
||||
}
|
||||
|
||||
/** get menu list */
|
||||
export function fetchGetMenuList() {
|
||||
return alova.Get<Api.SystemManage.MenuList>('/systemManage/getMenuList/v2');
|
||||
}
|
||||
|
||||
/** get all pages */
|
||||
export function fetchGetAllPages() {
|
||||
return alova.Get<string[]>('/systemManage/getAllPages');
|
||||
}
|
||||
|
||||
/** get menu tree */
|
||||
export function fetchGetMenuTree() {
|
||||
return alova.Get<Api.SystemManage.MenuTree[]>('/systemManage/getMenuTree');
|
||||
}
|
56
src/service-alova/mocks/feature-users-20241014.ts
Normal file
56
src/service-alova/mocks/feature-users-20241014.ts
Normal file
@ -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()
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
115
src/service-alova/request/index.ts
Normal file
115
src/service-alova/request/index.ts
Normal file
@ -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 { getServiceBaseURL } from '@/utils/service';
|
||||
import { $t } from '@/locales';
|
||||
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;
|
||||
}
|
||||
}
|
||||
);
|
53
src/service-alova/request/shared.ts
Normal file
53
src/service-alova/request/shared.ts
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
4
src/service-alova/request/type.ts
Normal file
4
src/service-alova/request/type.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface RequestInstanceState {
|
||||
/** the request error message stack */
|
||||
errMsgStack: string[];
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './route';
|
||||
export * from './system-manage';
|
||||
|
55
src/service/api/system-manage.ts
Normal file
55
src/service/api/system-manage.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { request } from '../request';
|
||||
|
||||
/** get role list */
|
||||
export function fetchGetRoleList(params?: Api.SystemManage.RoleSearchParams) {
|
||||
return request<Api.SystemManage.RoleList>({
|
||||
url: '/systemManage/getRoleList',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* get all roles
|
||||
*
|
||||
* these roles are all enabled
|
||||
*/
|
||||
export function fetchGetAllRoles() {
|
||||
return request<Api.SystemManage.AllRole[]>({
|
||||
url: '/systemManage/getAllRoles',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/** get user list */
|
||||
export function fetchGetUserList(params?: Api.SystemManage.UserSearchParams) {
|
||||
return request<Api.SystemManage.UserList>({
|
||||
url: '/systemManage/getUserList',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/** get menu list */
|
||||
export function fetchGetMenuList() {
|
||||
return request<Api.SystemManage.MenuList>({
|
||||
url: '/systemManage/getMenuList/v2',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/** get all pages */
|
||||
export function fetchGetAllPages() {
|
||||
return request<string[]>({
|
||||
url: '/systemManage/getAllPages',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/** get menu tree */
|
||||
export function fetchGetMenuTree() {
|
||||
return request<Api.SystemManage.MenuTree[]>({
|
||||
url: '/systemManage/getMenuTree',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
@ -10,7 +10,7 @@ import type { RequestInstanceState } from './type';
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||
|
||||
export const request = createFlatRequest<App.Service.Response, RequestInstanceState>(
|
||||
export const request = createFlatRequest(
|
||||
{
|
||||
baseURL,
|
||||
headers: {
|
||||
@ -18,6 +18,13 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
}
|
||||
},
|
||||
{
|
||||
defaultState: {
|
||||
errMsgStack: [],
|
||||
refreshTokenPromise: null
|
||||
} as RequestInstanceState,
|
||||
transform(response: AxiosResponse<App.Service.Response<any>>) {
|
||||
return response.data.data;
|
||||
},
|
||||
async onRequest(config) {
|
||||
const Authorization = getAuthorization();
|
||||
Object.assign(config.headers, { Authorization });
|
||||
@ -91,9 +98,6 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
|
||||
return null;
|
||||
},
|
||||
transformBackendResponse(response) {
|
||||
return response.data.data;
|
||||
},
|
||||
onError(error) {
|
||||
// when the request is fail, you can show error message
|
||||
|
||||
@ -123,11 +127,14 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
}
|
||||
);
|
||||
|
||||
export const demoRequest = createRequest<App.Service.DemoResponse>(
|
||||
export const demoRequest = createRequest(
|
||||
{
|
||||
baseURL: otherBaseURL.demo
|
||||
},
|
||||
{
|
||||
transform(response: AxiosResponse<App.Service.DemoResponse>) {
|
||||
return response.data.result;
|
||||
},
|
||||
async onRequest(config) {
|
||||
const { headers } = config;
|
||||
|
||||
@ -147,9 +154,6 @@ export const demoRequest = createRequest<App.Service.DemoResponse>(
|
||||
// when the backend response code is not "200", it means the request is fail
|
||||
// for example: the token is expired, refresh token and retry request
|
||||
},
|
||||
transformBackendResponse(response) {
|
||||
return response.data.result;
|
||||
},
|
||||
onError(error) {
|
||||
// when the request is fail, you can show error message
|
||||
|
||||
|
@ -28,14 +28,14 @@ async function handleRefreshToken() {
|
||||
}
|
||||
|
||||
export async function handleExpiredRequest(state: RequestInstanceState) {
|
||||
if (!state.refreshTokenFn) {
|
||||
state.refreshTokenFn = handleRefreshToken();
|
||||
if (!state.refreshTokenPromise) {
|
||||
state.refreshTokenPromise = handleRefreshToken();
|
||||
}
|
||||
|
||||
const success = await state.refreshTokenFn;
|
||||
const success = await state.refreshTokenPromise;
|
||||
|
||||
setTimeout(() => {
|
||||
state.refreshTokenFn = null;
|
||||
state.refreshTokenPromise = null;
|
||||
}, 1000);
|
||||
|
||||
return success;
|
||||
|
@ -1,6 +1,7 @@
|
||||
export interface RequestInstanceState {
|
||||
/** whether the request is refreshing token */
|
||||
refreshTokenFn: Promise<boolean> | null;
|
||||
/** the promise of refreshing token */
|
||||
refreshTokenPromise: Promise<boolean> | null;
|
||||
/** the request error message stack */
|
||||
errMsgStack: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
@ -318,7 +318,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
}
|
||||
|
||||
async function onRouteSwitchWhenLoggedIn() {
|
||||
await authStore.initUserInfo();
|
||||
// some global init logic when logged in and switch route
|
||||
}
|
||||
|
||||
async function onRouteSwitchWhenNotLoggedIn() {
|
||||
|
139
src/typings/api/system-manage.d.ts
vendored
Normal file
139
src/typings/api/system-manage.d.ts
vendored
Normal file
@ -0,0 +1,139 @@
|
||||
declare namespace Api {
|
||||
/**
|
||||
* namespace SystemManage
|
||||
*
|
||||
* backend api module: "systemManage"
|
||||
*/
|
||||
namespace SystemManage {
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
|
||||
|
||||
/** role */
|
||||
type Role = Common.CommonRecord<{
|
||||
/** role name */
|
||||
roleName: string;
|
||||
/** role code */
|
||||
roleCode: string;
|
||||
/** role description */
|
||||
roleDesc: string;
|
||||
}>;
|
||||
|
||||
/** role search params */
|
||||
type RoleSearchParams = CommonType.RecordNullable<
|
||||
Pick<Api.SystemManage.Role, 'roleName' | 'roleCode' | 'status'> & CommonSearchParams
|
||||
>;
|
||||
|
||||
/** role list */
|
||||
type RoleList = Common.PaginatingQueryRecord<Role>;
|
||||
|
||||
/** all role */
|
||||
type AllRole = Pick<Role, 'id' | 'roleName' | 'roleCode'>;
|
||||
|
||||
/**
|
||||
* user gender
|
||||
*
|
||||
* - "1": "male"
|
||||
* - "2": "female"
|
||||
*/
|
||||
type UserGender = '1' | '2';
|
||||
|
||||
/** user */
|
||||
type User = Common.CommonRecord<{
|
||||
/** user name */
|
||||
userName: string;
|
||||
/** user gender */
|
||||
userGender: UserGender | null;
|
||||
/** user nick name */
|
||||
nickName: string;
|
||||
/** user phone */
|
||||
userPhone: string;
|
||||
/** user email */
|
||||
userEmail: string;
|
||||
/** user role code collection */
|
||||
userRoles: string[];
|
||||
}>;
|
||||
|
||||
/** user search params */
|
||||
type UserSearchParams = CommonType.RecordNullable<
|
||||
Pick<Api.SystemManage.User, 'userName' | 'userGender' | 'nickName' | 'userPhone' | 'userEmail' | 'status'> &
|
||||
CommonSearchParams
|
||||
>;
|
||||
|
||||
/** user list */
|
||||
type UserList = Common.PaginatingQueryRecord<User>;
|
||||
|
||||
/**
|
||||
* menu type
|
||||
*
|
||||
* - "1": directory
|
||||
* - "2": menu
|
||||
*/
|
||||
type MenuType = '1' | '2';
|
||||
|
||||
type MenuButton = {
|
||||
/**
|
||||
* button code
|
||||
*
|
||||
* it can be used to control the button permission
|
||||
*/
|
||||
code: string;
|
||||
/** button description */
|
||||
desc: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* icon type
|
||||
*
|
||||
* - "1": iconify icon
|
||||
* - "2": local icon
|
||||
*/
|
||||
type IconType = '1' | '2';
|
||||
|
||||
type MenuPropsOfRoute = Pick<
|
||||
import('vue-router').RouteMeta,
|
||||
| 'i18nKey'
|
||||
| 'keepAlive'
|
||||
| 'constant'
|
||||
| 'order'
|
||||
| 'href'
|
||||
| 'hideInMenu'
|
||||
| 'activeMenu'
|
||||
| 'multiTab'
|
||||
| 'fixedIndexInTab'
|
||||
| 'query'
|
||||
>;
|
||||
|
||||
type Menu = Common.CommonRecord<{
|
||||
/** parent menu id */
|
||||
parentId: number;
|
||||
/** menu type */
|
||||
menuType: MenuType;
|
||||
/** menu name */
|
||||
menuName: string;
|
||||
/** route name */
|
||||
routeName: string;
|
||||
/** route path */
|
||||
routePath: string;
|
||||
/** component */
|
||||
component?: string;
|
||||
/** iconify icon name or local icon name */
|
||||
icon: string;
|
||||
/** icon type */
|
||||
iconType: IconType;
|
||||
/** buttons */
|
||||
buttons?: MenuButton[] | null;
|
||||
/** children menu */
|
||||
children?: Menu[] | null;
|
||||
}> &
|
||||
MenuPropsOfRoute;
|
||||
|
||||
/** menu list */
|
||||
type MenuList = Common.PaginatingQueryRecord<Menu>;
|
||||
|
||||
type MenuTree = {
|
||||
id: number;
|
||||
label: string;
|
||||
pId: number;
|
||||
children?: MenuTree[];
|
||||
};
|
||||
}
|
||||
}
|
260
src/typings/app.d.ts
vendored
260
src/typings/app.d.ts
vendored
@ -499,6 +499,19 @@ declare namespace App {
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
about: {
|
||||
title: string;
|
||||
introduction: string;
|
||||
projectInfo: {
|
||||
title: string;
|
||||
version: string;
|
||||
latestBuildTime: string;
|
||||
githubLink: string;
|
||||
previewLink: string;
|
||||
};
|
||||
prdDep: string;
|
||||
devDep: string;
|
||||
};
|
||||
home: {
|
||||
branchDesc: string;
|
||||
greeting: string;
|
||||
@ -527,6 +540,253 @@ declare namespace App {
|
||||
};
|
||||
creativity: string;
|
||||
};
|
||||
function: {
|
||||
tab: {
|
||||
tabOperate: {
|
||||
title: string;
|
||||
addTab: string;
|
||||
addTabDesc: string;
|
||||
closeTab: string;
|
||||
closeCurrentTab: string;
|
||||
closeAboutTab: string;
|
||||
addMultiTab: string;
|
||||
addMultiTabDesc1: string;
|
||||
addMultiTabDesc2: string;
|
||||
};
|
||||
tabTitle: {
|
||||
title: string;
|
||||
changeTitle: string;
|
||||
change: string;
|
||||
resetTitle: string;
|
||||
reset: string;
|
||||
};
|
||||
};
|
||||
multiTab: {
|
||||
routeParam: string;
|
||||
backTab: string;
|
||||
};
|
||||
toggleAuth: {
|
||||
toggleAccount: string;
|
||||
authHook: string;
|
||||
superAdminVisible: string;
|
||||
adminVisible: string;
|
||||
adminOrUserVisible: string;
|
||||
};
|
||||
request: {
|
||||
repeatedErrorOccurOnce: string;
|
||||
repeatedError: string;
|
||||
repeatedErrorMsg1: string;
|
||||
repeatedErrorMsg2: string;
|
||||
};
|
||||
};
|
||||
alova: {
|
||||
scenes: {
|
||||
captchaSend: string;
|
||||
autoRequest: string;
|
||||
visibilityRequestTips: string;
|
||||
pollingRequestTips: string;
|
||||
networkRequestTips: string;
|
||||
refreshTime: string;
|
||||
startRequest: string;
|
||||
stopRequest: string;
|
||||
requestCrossComponent: string;
|
||||
triggerAllRequest: string;
|
||||
};
|
||||
};
|
||||
proNaive: {
|
||||
form: {
|
||||
basic: {
|
||||
title: string;
|
||||
appName: string;
|
||||
appStatus: string;
|
||||
createTime: string;
|
||||
responseDate: string;
|
||||
specificationInfo: string;
|
||||
specificate: string;
|
||||
specificationName: string;
|
||||
specificationValue: string;
|
||||
specificationColorRed: string;
|
||||
specificationColorOrange: string;
|
||||
addSpecificateItem: string;
|
||||
fillValue: string;
|
||||
reset: string;
|
||||
submit: string;
|
||||
add: string;
|
||||
delete: string;
|
||||
color: string;
|
||||
normal: string;
|
||||
anomaly: string;
|
||||
};
|
||||
query: {
|
||||
title1: string;
|
||||
title2: string;
|
||||
appName: string;
|
||||
appStatus: string;
|
||||
createTime: string;
|
||||
responseDate: string;
|
||||
endDate: string;
|
||||
field: string;
|
||||
};
|
||||
step: {
|
||||
title: string;
|
||||
step1: {
|
||||
title: string;
|
||||
field: string;
|
||||
nextStep: string;
|
||||
};
|
||||
step2: {
|
||||
title: string;
|
||||
field: string;
|
||||
prevStep: string;
|
||||
submit: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
table: {
|
||||
remote: {
|
||||
filterCondition: string;
|
||||
name: string;
|
||||
createTime: string;
|
||||
responseTime: string;
|
||||
title: string;
|
||||
replicableText: string;
|
||||
tags: string;
|
||||
dateFormatting: string;
|
||||
image: string;
|
||||
};
|
||||
rowEdit: {
|
||||
title: string;
|
||||
reset: string;
|
||||
submit: string;
|
||||
edit: string;
|
||||
delete: string;
|
||||
save: string;
|
||||
task: string;
|
||||
score: string;
|
||||
time: string;
|
||||
name: string;
|
||||
action: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
manage: {
|
||||
common: {
|
||||
status: {
|
||||
enable: string;
|
||||
disable: string;
|
||||
};
|
||||
};
|
||||
role: {
|
||||
title: string;
|
||||
roleName: string;
|
||||
roleCode: string;
|
||||
roleStatus: string;
|
||||
roleDesc: string;
|
||||
form: {
|
||||
roleName: string;
|
||||
roleCode: string;
|
||||
roleStatus: string;
|
||||
roleDesc: string;
|
||||
};
|
||||
addRole: string;
|
||||
editRole: string;
|
||||
menuAuth: string;
|
||||
buttonAuth: string;
|
||||
};
|
||||
user: {
|
||||
title: string;
|
||||
userName: string;
|
||||
userGender: string;
|
||||
nickName: string;
|
||||
userPhone: string;
|
||||
userEmail: string;
|
||||
userStatus: string;
|
||||
userRole: string;
|
||||
form: {
|
||||
userName: string;
|
||||
userGender: string;
|
||||
nickName: string;
|
||||
userPhone: string;
|
||||
userEmail: string;
|
||||
userStatus: string;
|
||||
userRole: string;
|
||||
};
|
||||
addUser: string;
|
||||
editUser: string;
|
||||
gender: {
|
||||
male: string;
|
||||
female: string;
|
||||
};
|
||||
};
|
||||
menu: {
|
||||
home: string;
|
||||
title: string;
|
||||
id: string;
|
||||
parentId: string;
|
||||
menuType: string;
|
||||
menuName: string;
|
||||
routeName: string;
|
||||
routePath: string;
|
||||
pathParam: string;
|
||||
layout: string;
|
||||
page: string;
|
||||
i18nKey: string;
|
||||
icon: string;
|
||||
localIcon: string;
|
||||
iconTypeTitle: string;
|
||||
order: string;
|
||||
constant: string;
|
||||
keepAlive: string;
|
||||
href: string;
|
||||
hideInMenu: string;
|
||||
activeMenu: string;
|
||||
multiTab: string;
|
||||
fixedIndexInTab: string;
|
||||
query: string;
|
||||
button: string;
|
||||
buttonCode: string;
|
||||
buttonDesc: string;
|
||||
menuStatus: string;
|
||||
form: {
|
||||
home: string;
|
||||
menuType: string;
|
||||
menuName: string;
|
||||
routeName: string;
|
||||
routePath: string;
|
||||
pathParam: string;
|
||||
layout: string;
|
||||
page: string;
|
||||
i18nKey: string;
|
||||
icon: string;
|
||||
localIcon: string;
|
||||
order: string;
|
||||
keepAlive: string;
|
||||
href: string;
|
||||
hideInMenu: string;
|
||||
activeMenu: string;
|
||||
multiTab: string;
|
||||
fixedInTab: string;
|
||||
fixedIndexInTab: string;
|
||||
queryKey: string;
|
||||
queryValue: string;
|
||||
button: string;
|
||||
buttonCode: string;
|
||||
buttonDesc: string;
|
||||
menuStatus: string;
|
||||
};
|
||||
addMenu: string;
|
||||
editMenu: string;
|
||||
addChildMenu: string;
|
||||
type: {
|
||||
directory: string;
|
||||
menu: string;
|
||||
};
|
||||
iconType: {
|
||||
iconify: string;
|
||||
local: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
form: {
|
||||
required: string;
|
||||
|
58
src/typings/components.d.ts
vendored
58
src/typings/components.d.ts
vendored
@ -12,19 +12,45 @@ declare module 'vue' {
|
||||
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
|
||||
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
||||
CountTo: typeof import('./../components/custom/count-to.vue')['default']
|
||||
CustomIconSelect: typeof import('./../components/custom/custom-icon-select.vue')['default']
|
||||
DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
|
||||
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
|
||||
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
|
||||
GithubLink: typeof import('./../components/custom/github-link.vue')['default']
|
||||
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']
|
||||
'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default']
|
||||
'IconF7:flagCircleFill': typeof import('~icons/f7/flag-circle-fill')['default']
|
||||
'IconFe:question': typeof import('~icons/fe/question')['default']
|
||||
'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default']
|
||||
'IconGg:ratio': typeof import('~icons/gg/ratio')['default']
|
||||
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
|
||||
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
|
||||
'IconIc:roundPlus': typeof import('~icons/ic/round-plus')['default']
|
||||
'IconIconParkOutline:equalRatio': typeof import('~icons/icon-park-outline/equal-ratio')['default']
|
||||
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
|
||||
IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
|
||||
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
|
||||
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
|
||||
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
|
||||
IconLocalActivity: typeof import('~icons/local/activity')['default']
|
||||
IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||
IconLocalCast: typeof import('~icons/local/cast')['default']
|
||||
IconLocalLogo: typeof import('~icons/local/logo')['default']
|
||||
'IconMaterialSymbolsLight:rotate90DegreesCcwOutlineRounded': typeof import('~icons/material-symbols-light/rotate90-degrees-ccw-outline-rounded')['default']
|
||||
'IconMdi:printer': typeof import('~icons/mdi/printer')['default']
|
||||
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
||||
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
'IconMingcute:zoomInLine': typeof import('~icons/mingcute/zoom-in-line')['default']
|
||||
'IconMingcute:zoomOutLine': typeof import('~icons/mingcute/zoom-out-line')['default']
|
||||
IconUilSearch: typeof import('~icons/uil/search')['default']
|
||||
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
|
||||
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
|
||||
@ -33,17 +59,26 @@ declare module 'vue' {
|
||||
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
|
||||
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDescriptions: typeof import('naive-ui')['NDescriptions']
|
||||
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
@ -56,17 +91,39 @@ declare module 'vue' {
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
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']
|
||||
NStep: typeof import('naive-ui')['NStep']
|
||||
NSteps: typeof import('naive-ui')['NSteps']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NTree: typeof import('naive-ui')['NTree']
|
||||
NWatermark: typeof import('naive-ui')['NWatermark']
|
||||
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
|
||||
ProCard: typeof import('pro-naive-ui')['ProCard']
|
||||
ProConfigProvider: typeof import('pro-naive-ui')['ProConfigProvider']
|
||||
ProDataTable: typeof import('pro-naive-ui')['ProDataTable']
|
||||
ProDate: typeof import('pro-naive-ui')['ProDate']
|
||||
ProEditDataTable: typeof import('pro-naive-ui')['ProEditDataTable']
|
||||
ProForm: typeof import('pro-naive-ui')['ProForm']
|
||||
ProFormList: typeof import('pro-naive-ui')['ProFormList']
|
||||
ProInput: typeof import('pro-naive-ui')['ProInput']
|
||||
ProSearchForm: typeof import('pro-naive-ui')['ProSearchForm']
|
||||
ProSelect: typeof import('pro-naive-ui')['ProSelect']
|
||||
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
@ -77,5 +134,6 @@ declare module 'vue' {
|
||||
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
|
||||
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
|
||||
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
|
||||
WebSiteLink: typeof import('./../components/custom/web-site-link.vue')['default']
|
||||
}
|
||||
}
|
||||
|
153
src/typings/elegant-router.d.ts
vendored
153
src/typings/elegant-router.d.ts
vendored
@ -17,12 +17,85 @@ declare module "@elegant-router/types" {
|
||||
export type RouteMap = {
|
||||
"root": "/";
|
||||
"not-found": "/:pathMatch(.*)*";
|
||||
"exception": "/exception";
|
||||
"exception_403": "/exception/403";
|
||||
"exception_404": "/exception/404";
|
||||
"exception_500": "/exception/500";
|
||||
"document": "/document";
|
||||
"document_project": "/document/project";
|
||||
"document_project-link": "/document/project-link";
|
||||
"document_video": "/document/video";
|
||||
"document_vue": "/document/vue";
|
||||
"document_vite": "/document/vite";
|
||||
"document_unocss": "/document/unocss";
|
||||
"document_naive": "/document/naive";
|
||||
"document_pro-naive": "/document/pro-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";
|
||||
"function": "/function";
|
||||
"function_hide-child": "/function/hide-child";
|
||||
"function_hide-child_one": "/function/hide-child/one";
|
||||
"function_hide-child_three": "/function/hide-child/three";
|
||||
"function_hide-child_two": "/function/hide-child/two";
|
||||
"function_multi-tab": "/function/multi-tab";
|
||||
"function_request": "/function/request";
|
||||
"function_super-page": "/function/super-page";
|
||||
"function_tab": "/function/tab";
|
||||
"function_toggle-auth": "/function/toggle-auth";
|
||||
"home": "/home";
|
||||
"iframe-page": "/iframe-page/:url";
|
||||
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
|
||||
"manage": "/manage";
|
||||
"manage_menu": "/manage/menu";
|
||||
"manage_role": "/manage/role";
|
||||
"manage_user": "/manage/user";
|
||||
"manage_user-detail": "/manage/user-detail/:id";
|
||||
"multi-menu": "/multi-menu";
|
||||
"multi-menu_first": "/multi-menu/first";
|
||||
"multi-menu_first_child": "/multi-menu/first/child";
|
||||
"multi-menu_second": "/multi-menu/second";
|
||||
"multi-menu_second_child": "/multi-menu/second/child";
|
||||
"multi-menu_second_child_home": "/multi-menu/second/child/home";
|
||||
"plugin": "/plugin";
|
||||
"plugin_barcode": "/plugin/barcode";
|
||||
"plugin_charts": "/plugin/charts";
|
||||
"plugin_charts_antv": "/plugin/charts/antv";
|
||||
"plugin_charts_echarts": "/plugin/charts/echarts";
|
||||
"plugin_charts_vchart": "/plugin/charts/vchart";
|
||||
"plugin_copy": "/plugin/copy";
|
||||
"plugin_editor": "/plugin/editor";
|
||||
"plugin_editor_markdown": "/plugin/editor/markdown";
|
||||
"plugin_editor_quill": "/plugin/editor/quill";
|
||||
"plugin_excel": "/plugin/excel";
|
||||
"plugin_gantt": "/plugin/gantt";
|
||||
"plugin_gantt_dhtmlx": "/plugin/gantt/dhtmlx";
|
||||
"plugin_gantt_vtable": "/plugin/gantt/vtable";
|
||||
"plugin_icon": "/plugin/icon";
|
||||
"plugin_map": "/plugin/map";
|
||||
"plugin_pdf": "/plugin/pdf";
|
||||
"plugin_pinyin": "/plugin/pinyin";
|
||||
"plugin_print": "/plugin/print";
|
||||
"plugin_swiper": "/plugin/swiper";
|
||||
"plugin_tables": "/plugin/tables";
|
||||
"plugin_tables_vtable": "/plugin/tables/vtable";
|
||||
"plugin_typeit": "/plugin/typeit";
|
||||
"plugin_video": "/plugin/video";
|
||||
"pro-naive": "/pro-naive";
|
||||
"pro-naive_form": "/pro-naive/form";
|
||||
"pro-naive_form_basic": "/pro-naive/form/basic";
|
||||
"pro-naive_form_query": "/pro-naive/form/query";
|
||||
"pro-naive_form_step": "/pro-naive/form/step";
|
||||
"pro-naive_table": "/pro-naive/table";
|
||||
"pro-naive_table_remote": "/pro-naive/table/remote";
|
||||
"pro-naive_table_row-edit": "/pro-naive/table/row-edit";
|
||||
"user-center": "/user-center";
|
||||
};
|
||||
|
||||
/**
|
||||
@ -42,6 +115,21 @@ declare module "@elegant-router/types" {
|
||||
RouteKey,
|
||||
| "root"
|
||||
| "not-found"
|
||||
| "exception"
|
||||
| "exception_403"
|
||||
| "exception_404"
|
||||
| "exception_500"
|
||||
| "document"
|
||||
| "document_project"
|
||||
| "document_project-link"
|
||||
| "document_video"
|
||||
| "document_vue"
|
||||
| "document_vite"
|
||||
| "document_unocss"
|
||||
| "document_naive"
|
||||
| "document_pro-naive"
|
||||
| "document_antd"
|
||||
| "document_alova"
|
||||
>;
|
||||
|
||||
/**
|
||||
@ -57,9 +145,17 @@ declare module "@elegant-router/types" {
|
||||
| "403"
|
||||
| "404"
|
||||
| "500"
|
||||
| "about"
|
||||
| "alova"
|
||||
| "function"
|
||||
| "home"
|
||||
| "iframe-page"
|
||||
| "login"
|
||||
| "manage"
|
||||
| "multi-menu"
|
||||
| "plugin"
|
||||
| "pro-naive"
|
||||
| "user-center"
|
||||
>;
|
||||
|
||||
/**
|
||||
@ -69,6 +165,8 @@ declare module "@elegant-router/types" {
|
||||
CustomRouteKey,
|
||||
| "root"
|
||||
| "not-found"
|
||||
| "exception"
|
||||
| "document"
|
||||
>;
|
||||
|
||||
/**
|
||||
@ -81,7 +179,49 @@ declare module "@elegant-router/types" {
|
||||
| "500"
|
||||
| "iframe-page"
|
||||
| "login"
|
||||
| "about"
|
||||
| "alova_request"
|
||||
| "alova_scenes"
|
||||
| "function_hide-child_one"
|
||||
| "function_hide-child_three"
|
||||
| "function_hide-child_two"
|
||||
| "function_multi-tab"
|
||||
| "function_request"
|
||||
| "function_super-page"
|
||||
| "function_tab"
|
||||
| "function_toggle-auth"
|
||||
| "home"
|
||||
| "manage_menu"
|
||||
| "manage_role"
|
||||
| "manage_user-detail"
|
||||
| "manage_user"
|
||||
| "multi-menu_first_child"
|
||||
| "multi-menu_second_child_home"
|
||||
| "plugin_barcode"
|
||||
| "plugin_charts_antv"
|
||||
| "plugin_charts_echarts"
|
||||
| "plugin_charts_vchart"
|
||||
| "plugin_copy"
|
||||
| "plugin_editor_markdown"
|
||||
| "plugin_editor_quill"
|
||||
| "plugin_excel"
|
||||
| "plugin_gantt_dhtmlx"
|
||||
| "plugin_gantt_vtable"
|
||||
| "plugin_icon"
|
||||
| "plugin_map"
|
||||
| "plugin_pdf"
|
||||
| "plugin_pinyin"
|
||||
| "plugin_print"
|
||||
| "plugin_swiper"
|
||||
| "plugin_tables_vtable"
|
||||
| "plugin_typeit"
|
||||
| "plugin_video"
|
||||
| "pro-naive_form_basic"
|
||||
| "pro-naive_form_query"
|
||||
| "pro-naive_form_step"
|
||||
| "pro-naive_table_remote"
|
||||
| "pro-naive_table_row-edit"
|
||||
| "user-center"
|
||||
>;
|
||||
|
||||
/**
|
||||
@ -91,6 +231,19 @@ declare module "@elegant-router/types" {
|
||||
CustomRouteKey,
|
||||
| "root"
|
||||
| "not-found"
|
||||
| "exception_403"
|
||||
| "exception_404"
|
||||
| "exception_500"
|
||||
| "document_project"
|
||||
| "document_project-link"
|
||||
| "document_video"
|
||||
| "document_vue"
|
||||
| "document_vite"
|
||||
| "document_unocss"
|
||||
| "document_naive"
|
||||
| "document_pro-naive"
|
||||
| "document_antd"
|
||||
| "document_alova"
|
||||
>;
|
||||
|
||||
/**
|
||||
|
32
src/typings/naive-ui.d.ts
vendored
32
src/typings/naive-ui.d.ts
vendored
@ -6,30 +6,14 @@ declare namespace NaiveUI {
|
||||
type DataTableExpandColumn<T> = import('naive-ui').DataTableExpandColumn<T>;
|
||||
type DataTableSelectionColumn<T> = import('naive-ui').DataTableSelectionColumn<T>;
|
||||
type TableColumnGroup<T> = import('naive-ui/es/data-table/src/interface').TableColumnGroup<T>;
|
||||
type PaginationProps = import('naive-ui').PaginationProps;
|
||||
type TableColumnCheck = import('@sa/hooks').TableColumnCheck;
|
||||
type TableDataWithIndex<T> = import('@sa/hooks').TableDataWithIndex<T>;
|
||||
type FlatResponseData<T> = import('@sa/axios').FlatResponseData<T>;
|
||||
|
||||
/**
|
||||
* the custom column key
|
||||
*
|
||||
* if you want to add a custom column, you should add a key to this type
|
||||
*/
|
||||
type CustomColumnKey = 'operate';
|
||||
|
||||
type SetTableColumnKey<C, T> = Omit<C, 'key'> & { key: keyof T | CustomColumnKey };
|
||||
|
||||
type TableData = Api.Common.CommonRecord<object>;
|
||||
type SetTableColumnKey<C, T> = Omit<C, 'key'> & { key: keyof T | (string & {}) };
|
||||
|
||||
type TableColumnWithKey<T> = SetTableColumnKey<DataTableBaseColumn<T>, T> | SetTableColumnKey<TableColumnGroup<T>, T>;
|
||||
|
||||
type TableColumn<T> = TableColumnWithKey<T> | DataTableSelectionColumn<T> | DataTableExpandColumn<T>;
|
||||
|
||||
type TableApiFn<T = any, R = Api.Common.CommonSearchParams> = (
|
||||
params: R
|
||||
) => Promise<FlatResponseData<Api.Common.PaginatingQueryRecord<T>>>;
|
||||
|
||||
/**
|
||||
* the type of table operation
|
||||
*
|
||||
@ -37,18 +21,4 @@ declare namespace NaiveUI {
|
||||
* - edit: edit table item
|
||||
*/
|
||||
type TableOperateType = 'add' | 'edit';
|
||||
|
||||
type GetTableData<A extends TableApiFn> = A extends TableApiFn<infer T> ? T : never;
|
||||
|
||||
type NaiveTableConfig<A extends TableApiFn> = Pick<
|
||||
import('@sa/hooks').TableConfig<A, GetTableData<A>, TableColumn<TableDataWithIndex<GetTableData<A>>>>,
|
||||
'apiFn' | 'apiParams' | 'columns' | 'immediate'
|
||||
> & {
|
||||
/**
|
||||
* whether to display the total items count
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
showTotal?: boolean;
|
||||
};
|
||||
}
|
||||
|
20
src/typings/package.d.ts
vendored
Normal file
20
src/typings/package.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
/// <reference types="@amap/amap-jsapi-types" />
|
||||
/// <reference types="bmapgl" />
|
||||
|
||||
declare namespace BMap {
|
||||
class Map extends BMapGL.Map {}
|
||||
class Point extends BMapGL.Point {}
|
||||
}
|
||||
|
||||
declare const TMap: any;
|
||||
|
||||
interface Window {
|
||||
/**
|
||||
* make baidu map request under https protocol
|
||||
*
|
||||
* - 0: http
|
||||
* - 1: https
|
||||
* - 2: https
|
||||
*/
|
||||
HOST_TYPE: '0' | '1' | '2';
|
||||
}
|
85
src/views/about/index.vue
Normal file
85
src/views/about/index.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { $t } from '@/locales';
|
||||
import pkg from '~/package.json';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const column = computed(() => (appStore.isMobile ? 1 : 2));
|
||||
|
||||
interface PkgJson {
|
||||
name: string;
|
||||
version: string;
|
||||
dependencies: PkgVersionInfo[];
|
||||
devDependencies: PkgVersionInfo[];
|
||||
}
|
||||
|
||||
interface PkgVersionInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
const { name, version, dependencies, devDependencies } = pkg;
|
||||
|
||||
function transformVersionData(tuple: [string, string]): PkgVersionInfo {
|
||||
const [$name, $version] = tuple;
|
||||
return {
|
||||
name: $name,
|
||||
version: $version
|
||||
};
|
||||
}
|
||||
|
||||
const pkgJson: PkgJson = {
|
||||
name,
|
||||
version,
|
||||
dependencies: Object.entries(dependencies).map(item => transformVersionData(item)),
|
||||
devDependencies: Object.entries(devDependencies).map(item => transformVersionData(item))
|
||||
};
|
||||
|
||||
const latestBuildTime = BUILD_TIME;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NCard :title="$t('page.about.title')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<p>{{ $t('page.about.introduction') }}</p>
|
||||
</NCard>
|
||||
<NCard :title="$t('page.about.projectInfo.title')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NDescriptions label-placement="left" bordered size="small" :column="column">
|
||||
<NDescriptionsItem :label="$t('page.about.projectInfo.version')">
|
||||
<NTag type="primary">{{ pkgJson.version }}</NTag>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem :label="$t('page.about.projectInfo.latestBuildTime')">
|
||||
<NTag type="primary">{{ latestBuildTime }}</NTag>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem :label="$t('page.about.projectInfo.githubLink')">
|
||||
<a class="text-primary" :href="pkg.homepage" target="_blank" rel="noopener noreferrer">
|
||||
{{ $t('page.about.projectInfo.githubLink') }}
|
||||
</a>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem :label="$t('page.about.projectInfo.previewLink')">
|
||||
<a class="text-primary" :href="pkg.website" target="_blank" rel="noopener noreferrer">
|
||||
{{ $t('page.about.projectInfo.previewLink') }}
|
||||
</a>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NCard>
|
||||
<NCard :title="$t('page.about.prdDep')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NDescriptions label-placement="left" bordered size="small" :column="column">
|
||||
<NDescriptionsItem v-for="item in pkgJson.dependencies" :key="item.name" :label="item.name">
|
||||
{{ item.version }}
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NCard>
|
||||
<NCard :title="$t('page.about.devDep')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NDescriptions label-placement="left" bordered size="small" :column="column">
|
||||
<NDescriptionsItem v-for="item in pkgJson.devDependencies" :key="item.name" :label="item.name">
|
||||
{{ item.version }}
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
63
src/views/alova/request/index.vue
Normal file
63
src/views/alova/request/index.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { fetchCustomBackendError } from '@/service-alova/api';
|
||||
|
||||
async function logout() {
|
||||
await fetchCustomBackendError('8888', $t('request.logoutMsg'));
|
||||
}
|
||||
|
||||
async function logoutWithModal() {
|
||||
await fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'));
|
||||
}
|
||||
|
||||
async function refreshToken() {
|
||||
await fetchCustomBackendError('9999', $t('request.tokenExpired'));
|
||||
}
|
||||
|
||||
async function handleRepeatedMessageError() {
|
||||
await Promise.all([
|
||||
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
|
||||
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
|
||||
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
|
||||
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2')),
|
||||
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2')),
|
||||
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2'))
|
||||
]);
|
||||
}
|
||||
|
||||
async function handleRepeatedModalError() {
|
||||
await Promise.all([
|
||||
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg')),
|
||||
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg')),
|
||||
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'))
|
||||
]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NCard :title="$t('request.logout')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NButton @click="logout">{{ $t('common.trigger') }}</NButton>
|
||||
</NCard>
|
||||
<NCard :title="$t('request.logoutWithModal')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NButton @click="logoutWithModal">{{ $t('common.trigger') }}</NButton>
|
||||
</NCard>
|
||||
<NCard :title="$t('request.refreshToken')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NButton @click="refreshToken">{{ $t('common.trigger') }}</NButton>
|
||||
</NCard>
|
||||
<NCard
|
||||
:title="$t('page.function.request.repeatedErrorOccurOnce')"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
segmented
|
||||
class="card-wrapper"
|
||||
>
|
||||
<NButton @click="handleRepeatedMessageError">{{ $t('page.function.request.repeatedError') }}(Message)</NButton>
|
||||
<NButton class="ml-12px" @click="handleRepeatedModalError">
|
||||
{{ $t('page.function.request.repeatedError') }}(Modal)
|
||||
</NButton>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
34
src/views/alova/scenes/index.vue
Normal file
34
src/views/alova/scenes/index.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import CaptchaVerification from './modules/captcha-verification.vue';
|
||||
import BrowserVisibilityRequest from './modules/browser-visibility-request.vue';
|
||||
import PollingRequest from './modules/polling-request.vue';
|
||||
import NetworkToggleRequest from './modules/network-toggle-request.vue';
|
||||
import CrossComponentRequest from './modules/cross-component-request.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NCard :title="$t('page.alova.scenes.captchaSend')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<CaptchaVerification class="w-1/3" />
|
||||
</NCard>
|
||||
<NCard :title="$t('page.alova.scenes.autoRequest')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NSpace :wrap="false">
|
||||
<BrowserVisibilityRequest />
|
||||
<PollingRequest />
|
||||
<NetworkToggleRequest />
|
||||
</NSpace>
|
||||
</NCard>
|
||||
<NCard
|
||||
:title="$t('page.alova.scenes.requestCrossComponent')"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
segmented
|
||||
class="card-wrapper"
|
||||
>
|
||||
<CrossComponentRequest />
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { actionDelegationMiddleware, useAutoRequest } from '@sa/alova/client';
|
||||
import { alova } from '@/service-alova/request';
|
||||
|
||||
const getLastTime = alova.Get<{ time: string }>('/mock/getLastTime', { cacheFor: null });
|
||||
const isStop = ref(false);
|
||||
const { loading, data } = useAutoRequest(getLastTime, {
|
||||
enableVisibility: true,
|
||||
enableNetwork: false,
|
||||
enableFocus: false,
|
||||
initialData: {
|
||||
time: ''
|
||||
},
|
||||
async middleware(_, next) {
|
||||
await actionDelegationMiddleware('autoRequest:1')(_, () => Promise.resolve());
|
||||
if (!isStop.value) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const toggleStop = () => {
|
||||
isStop.value = !isStop.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical>
|
||||
<NAlert type="info">
|
||||
{{ $t('page.alova.scenes.visibilityRequestTips') }}
|
||||
</NAlert>
|
||||
<NButton type="primary" @click="toggleStop">
|
||||
<icon-carbon-play v-if="isStop" class="mr-2" />
|
||||
<icon-carbon-stop v-else class="mr-2" />
|
||||
{{ isStop ? $t('page.alova.scenes.startRequest') : $t('page.alova.scenes.stopRequest') }}
|
||||
</NButton>
|
||||
<NSpace align="center">
|
||||
<span>{{ $t('page.alova.scenes.refreshTime') }}: {{ data.time || '--' }}</span>
|
||||
<NSpin v-if="loading" :size="12" />
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
71
src/views/alova/scenes/modules/captcha-verification.vue
Normal file
71
src/views/alova/scenes/modules/captcha-verification.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { actionDelegationMiddleware, useCaptcha, useForm } from '@sa/alova/client';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import { sendCaptcha, verifyCaptcha } from '@/service-alova/api';
|
||||
|
||||
defineOptions({
|
||||
name: 'CaptchaVerification'
|
||||
});
|
||||
|
||||
const { loading, send, countdown } = useCaptcha(sendCaptcha, {
|
||||
middleware: actionDelegationMiddleware('captcha:send')
|
||||
});
|
||||
const label = computed(() => {
|
||||
return countdown.value > 0
|
||||
? $t('page.login.codeLogin.reGetCode', { time: countdown.value })
|
||||
: $t('page.login.codeLogin.getCode');
|
||||
});
|
||||
const {
|
||||
form,
|
||||
loading: submiting,
|
||||
send: submit
|
||||
} = useForm(formData => verifyCaptcha(formData.phone, formData.code), {
|
||||
initialForm: {
|
||||
phone: '',
|
||||
code: ''
|
||||
}
|
||||
});
|
||||
|
||||
const { formRef, validate } = useNaiveForm();
|
||||
|
||||
const rules = computed<Record<keyof typeof form.value, App.Global.FormRule[]>>(() => {
|
||||
const { formRules } = useFormRules();
|
||||
|
||||
return {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code
|
||||
};
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
await submit();
|
||||
// request
|
||||
window.$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NForm ref="formRef" :model="form" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
|
||||
<NFormItem path="phone">
|
||||
<NInput v-model:value="form.phone" :placeholder="$t('page.login.common.phonePlaceholder')" :maxlength="11" />
|
||||
</NFormItem>
|
||||
<NFormItem path="code">
|
||||
<div class="w-full flex-y-center gap-16px">
|
||||
<NInput v-model:value="form.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
<NButton size="large" :disabled="countdown > 0" :loading="loading" @click="send(form.phone)">
|
||||
{{ label }}
|
||||
</NButton>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<NSpace vertical :size="18" class="w-full">
|
||||
<NButton type="primary" size="large" round block :loading="submiting" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
13
src/views/alova/scenes/modules/cross-component-request.vue
Normal file
13
src/views/alova/scenes/modules/cross-component-request.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { accessAction } from '@sa/alova/client';
|
||||
|
||||
const handleAutoRequestSend = async () => {
|
||||
accessAction(/^autoRequest/, async ({ send }) => {
|
||||
await send();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NButton @click="handleAutoRequestSend">{{ $t('page.alova.scenes.triggerAllRequest') }}</NButton>
|
||||
</template>
|
43
src/views/alova/scenes/modules/network-toggle-request.vue
Normal file
43
src/views/alova/scenes/modules/network-toggle-request.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { actionDelegationMiddleware, useAutoRequest } from '@sa/alova/client';
|
||||
import { alova } from '@/service-alova/request';
|
||||
|
||||
const getLastTime = alova.Get<{ time: string }>('/mock/getLastTime', { cacheFor: null });
|
||||
const isStop = ref(false);
|
||||
const { loading, data } = useAutoRequest(getLastTime, {
|
||||
enableVisibility: false,
|
||||
enableNetwork: true,
|
||||
enableFocus: false,
|
||||
initialData: {
|
||||
time: ''
|
||||
},
|
||||
async middleware(_, next) {
|
||||
await actionDelegationMiddleware('autoRequest:2')(_, () => Promise.resolve());
|
||||
if (!isStop.value) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const toggleStop = () => {
|
||||
isStop.value = !isStop.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical>
|
||||
<NAlert type="info">
|
||||
{{ $t('page.alova.scenes.networkRequestTips') }}
|
||||
</NAlert>
|
||||
<NButton type="primary" @click="toggleStop">
|
||||
<icon-carbon-play v-if="isStop" class="mr-2" />
|
||||
<icon-carbon-stop v-else class="mr-2" />
|
||||
{{ isStop ? $t('page.alova.scenes.startRequest') : $t('page.alova.scenes.stopRequest') }}
|
||||
</NButton>
|
||||
<NSpace align="center">
|
||||
<span>{{ $t('page.alova.scenes.refreshTime') }}: {{ data.time || '--' }}</span>
|
||||
<NSpin v-if="loading" :size="12" />
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
41
src/views/alova/scenes/modules/polling-request.vue
Normal file
41
src/views/alova/scenes/modules/polling-request.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { actionDelegationMiddleware, useAutoRequest } from '@sa/alova/client';
|
||||
import { alova } from '@/service-alova/request';
|
||||
|
||||
const getLastTime = alova.Get<{ time: string }>('/mock/getLastTime', { cacheFor: null });
|
||||
const isStop = ref(false);
|
||||
const { loading, data } = useAutoRequest(getLastTime, {
|
||||
pollingTime: 3000,
|
||||
initialData: {
|
||||
time: ''
|
||||
},
|
||||
async middleware(_, next) {
|
||||
await actionDelegationMiddleware('autoRequest:3')(_, () => Promise.resolve());
|
||||
if (!isStop.value) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const toggleStop = () => {
|
||||
isStop.value = !isStop.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical>
|
||||
<NAlert type="info">
|
||||
{{ $t('page.alova.scenes.pollingRequestTips') }}
|
||||
</NAlert>
|
||||
<NButton type="primary" @click="toggleStop">
|
||||
<icon-carbon-play v-if="isStop" class="mr-2" />
|
||||
<icon-carbon-stop v-else class="mr-2" />
|
||||
{{ isStop ? $t('page.alova.scenes.startRequest') : $t('page.alova.scenes.stopRequest') }}
|
||||
</NButton>
|
||||
<NSpace align="center">
|
||||
<span>{{ $t('page.alova.scenes.refreshTime') }}: {{ data.time || '--' }}</span>
|
||||
<NSpin v-if="loading" :size="12" />
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
7
src/views/function/hide-child/one/index.vue
Normal file
7
src/views/function/hide-child/one/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
7
src/views/function/hide-child/three/index.vue
Normal file
7
src/views/function/hide-child/three/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
7
src/views/function/hide-child/two/index.vue
Normal file
7
src/views/function/hide-child/two/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
24
src/views/function/multi-tab/index.vue
Normal file
24
src/views/function/multi-tab/index.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
const route = useRoute();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const routeQuery = computed(() => JSON.stringify(route.query));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<LookForward>
|
||||
<div>
|
||||
<NButton @click="routerPushByKey('function_tab')">{{ $t('page.function.multiTab.backTab') }}</NButton>
|
||||
<div class="py-24px">{{ $t('page.function.multiTab.routeParam') }}: {{ routeQuery }}</div>
|
||||
</div>
|
||||
</LookForward>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
63
src/views/function/request/index.vue
Normal file
63
src/views/function/request/index.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { fetchCustomBackendError } from '@/service/api';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
async function logout() {
|
||||
await fetchCustomBackendError('8888', $t('request.logoutMsg'));
|
||||
}
|
||||
|
||||
async function logoutWithModal() {
|
||||
await fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'));
|
||||
}
|
||||
|
||||
async function refreshToken() {
|
||||
await fetchCustomBackendError('9999', $t('request.tokenExpired'));
|
||||
}
|
||||
|
||||
async function handleRepeatedMessageError() {
|
||||
await Promise.all([
|
||||
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
|
||||
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
|
||||
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
|
||||
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2')),
|
||||
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2')),
|
||||
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2'))
|
||||
]);
|
||||
}
|
||||
|
||||
async function handleRepeatedModalError() {
|
||||
await Promise.all([
|
||||
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg')),
|
||||
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg')),
|
||||
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'))
|
||||
]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NCard :title="$t('request.logout')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NButton @click="logout">{{ $t('common.trigger') }}</NButton>
|
||||
</NCard>
|
||||
<NCard :title="$t('request.logoutWithModal')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NButton @click="logoutWithModal">{{ $t('common.trigger') }}</NButton>
|
||||
</NCard>
|
||||
<NCard :title="$t('request.refreshToken')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NButton @click="refreshToken">{{ $t('common.trigger') }}</NButton>
|
||||
</NCard>
|
||||
<NCard
|
||||
:title="$t('page.function.request.repeatedErrorOccurOnce')"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
segmented
|
||||
class="card-wrapper"
|
||||
>
|
||||
<NButton @click="handleRepeatedMessageError">{{ $t('page.function.request.repeatedError') }}(Message)</NButton>
|
||||
<NButton class="ml-12px" @click="handleRepeatedModalError">
|
||||
{{ $t('page.function.request.repeatedError') }}(Modal)
|
||||
</NButton>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
7
src/views/function/super-page/index.vue
Normal file
7
src/views/function/super-page/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
71
src/views/function/tab/index.vue
Normal file
71
src/views/function/tab/index.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useTabStore } from '@/store/modules/tab';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
const tabStore = useTabStore();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const tabLabel = ref('');
|
||||
|
||||
function changeTabLabel() {
|
||||
tabStore.setTabLabel(tabLabel.value);
|
||||
}
|
||||
|
||||
function resetTabLabel() {
|
||||
tabStore.resetTabLabel();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NCard
|
||||
:title="$t('page.function.tab.tabOperate.title')"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
segmented
|
||||
class="card-wrapper"
|
||||
>
|
||||
<NDivider title-placement="left">{{ $t('page.function.tab.tabOperate.addTab') }}</NDivider>
|
||||
<NButton @click="routerPushByKey('about')">{{ $t('page.function.tab.tabOperate.addTabDesc') }}</NButton>
|
||||
<NDivider title-placement="left">{{ $t('page.function.tab.tabOperate.closeTab') }}</NDivider>
|
||||
<NSpace>
|
||||
<NButton @click="tabStore.removeActiveTab">
|
||||
{{ $t('page.function.tab.tabOperate.closeCurrentTab') }}
|
||||
</NButton>
|
||||
<NButton @click="tabStore.removeTabByRouteName('about')">
|
||||
{{ $t('page.function.tab.tabOperate.closeAboutTab') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<NDivider title-placement="left">{{ $t('page.function.tab.tabOperate.addMultiTab') }}</NDivider>
|
||||
<NSpace>
|
||||
<NButton @click="routerPushByKey('function_multi-tab')">
|
||||
{{ $t('page.function.tab.tabOperate.addMultiTabDesc1') }}
|
||||
</NButton>
|
||||
<NButton @click="routerPushByKey('function_multi-tab', { query: { a: '1' } })">
|
||||
{{ $t('page.function.tab.tabOperate.addMultiTabDesc2') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
<NCard
|
||||
:title="$t('page.function.tab.tabTitle.title')"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
segmented
|
||||
class="card-wrapper"
|
||||
>
|
||||
<NDivider title-placement="left">{{ $t('page.function.tab.tabTitle.changeTitle') }}</NDivider>
|
||||
<NInputGroup class="max-w-240px">
|
||||
<NInput v-model:value="tabLabel" />
|
||||
<NButton type="primary" @click="changeTabLabel">{{ $t('page.function.tab.tabTitle.change') }}</NButton>
|
||||
</NInputGroup>
|
||||
<NDivider title-placement="left">{{ $t('page.function.tab.tabTitle.resetTitle') }}</NDivider>
|
||||
<NButton type="error" ghost class="w-80px" @click="resetTabLabel">
|
||||
{{ $t('page.function.tab.tabTitle.reset') }}
|
||||
</NButton>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
103
src/views/function/toggle-auth/index.vue
Normal file
103
src/views/function/toggle-auth/index.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useTabStore } from '@/store/modules/tab';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const authStore = useAuthStore();
|
||||
const tabStore = useTabStore();
|
||||
const { hasAuth } = useAuth();
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
type AccountKey = 'super' | 'admin' | 'user';
|
||||
|
||||
interface Account {
|
||||
key: AccountKey;
|
||||
label: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const accounts = computed<Account[]>(() => [
|
||||
{
|
||||
key: 'super',
|
||||
label: $t('page.login.pwdLogin.superAdmin'),
|
||||
userName: 'Super',
|
||||
password: '123456'
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
label: $t('page.login.pwdLogin.admin'),
|
||||
userName: 'Admin',
|
||||
password: '123456'
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: $t('page.login.pwdLogin.user'),
|
||||
userName: 'User',
|
||||
password: '123456'
|
||||
}
|
||||
]);
|
||||
|
||||
const loginAccount = ref<AccountKey>('super');
|
||||
|
||||
async function handleToggleAccount(account: Account) {
|
||||
loginAccount.value = account.key;
|
||||
|
||||
startLoading();
|
||||
await authStore.login(account.userName, account.password, false);
|
||||
tabStore.initTabStore(route);
|
||||
endLoading();
|
||||
appStore.reloadPage();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NCard :title="$t('route.function_toggle-auth')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<NDescriptions bordered :column="1">
|
||||
<NDescriptionsItem :label="$t('page.manage.user.userRole')">
|
||||
<NSpace>
|
||||
<NTag v-for="role in authStore.userInfo.roles" :key="role">{{ role }}</NTag>
|
||||
</NSpace>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem ions-item :label="$t('page.function.toggleAuth.toggleAccount')">
|
||||
<NSpace>
|
||||
<NButton
|
||||
v-for="account in accounts"
|
||||
:key="account.key"
|
||||
:loading="loading && loginAccount === account.key"
|
||||
:disabled="loading && loginAccount !== account.key"
|
||||
@click="handleToggleAccount(account)"
|
||||
>
|
||||
{{ account.label }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NCard>
|
||||
<NCard
|
||||
:title="$t('page.function.toggleAuth.authHook')"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
segmented
|
||||
class="card-wrapper"
|
||||
>
|
||||
<NSpace>
|
||||
<NButton v-if="hasAuth('B_CODE1')">{{ $t('page.function.toggleAuth.superAdminVisible') }}</NButton>
|
||||
<NButton v-if="hasAuth('B_CODE2')">{{ $t('page.function.toggleAuth.adminVisible') }}</NButton>
|
||||
<NButton v-if="hasAuth('B_CODE3')">
|
||||
{{ $t('page.function.toggleAuth.adminOrUserVisible') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -15,9 +15,6 @@ const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NAlert :title="$t('common.warning')" type="warning">
|
||||
{{ $t('page.home.branchDesc') }}
|
||||
</NAlert>
|
||||
<HeaderBanner />
|
||||
<CardData />
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
|
||||
|
267
src/views/manage/menu/index.vue
Normal file
267
src/views/manage/menu/index.vue
Normal file
@ -0,0 +1,267 @@
|
||||
<script setup lang="tsx">
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { NButton, NPopconfirm, NTag } from 'naive-ui';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import { yesOrNoRecord } from '@/constants/common';
|
||||
import { enableStatusRecord, menuTypeRecord } from '@/constants/business';
|
||||
import { fetchGetAllPages, fetchGetMenuList } from '@/service/api';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue';
|
||||
import MenuOperateModal, { type OperateType } from './modules/menu-operate-modal.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { bool: visible, setTrue: openModal } = useBoolean();
|
||||
|
||||
const wrapperRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const { columns, columnChecks, data, loading, pagination, getData, getDataByPage } = useNaivePaginatedTable({
|
||||
api: () => fetchGetMenuList(),
|
||||
transform: response => defaultTransform(response),
|
||||
columns: () => [
|
||||
{
|
||||
type: 'selection',
|
||||
align: 'center',
|
||||
width: 48
|
||||
},
|
||||
{
|
||||
key: 'id',
|
||||
title: $t('page.manage.menu.id'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'menuType',
|
||||
title: $t('page.manage.menu.menuType'),
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: row => {
|
||||
const tagMap: Record<Api.SystemManage.MenuType, NaiveUI.ThemeColor> = {
|
||||
1: 'default',
|
||||
2: 'primary'
|
||||
};
|
||||
|
||||
const label = $t(menuTypeRecord[row.menuType]);
|
||||
|
||||
return <NTag type={tagMap[row.menuType]}>{label}</NTag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'menuName',
|
||||
title: $t('page.manage.menu.menuName'),
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
render: row => {
|
||||
const { i18nKey, menuName } = row;
|
||||
|
||||
const label = i18nKey ? $t(i18nKey) : menuName;
|
||||
|
||||
return <span>{label}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'icon',
|
||||
title: $t('page.manage.menu.icon'),
|
||||
align: 'center',
|
||||
width: 60,
|
||||
render: row => {
|
||||
const icon = row.iconType === '1' ? row.icon : undefined;
|
||||
|
||||
const localIcon = row.iconType === '2' ? row.icon : undefined;
|
||||
|
||||
return (
|
||||
<div class="flex-center">
|
||||
<SvgIcon icon={icon} localIcon={localIcon} class="text-icon" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'routeName',
|
||||
title: $t('page.manage.menu.routeName'),
|
||||
align: 'center',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
key: 'routePath',
|
||||
title: $t('page.manage.menu.routePath'),
|
||||
align: 'center',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: $t('page.manage.menu.menuStatus'),
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: row => {
|
||||
if (row.status === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
|
||||
1: 'success',
|
||||
2: 'warning'
|
||||
};
|
||||
|
||||
const label = $t(enableStatusRecord[row.status]);
|
||||
|
||||
return <NTag type={tagMap[row.status]}>{label}</NTag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'hideInMenu',
|
||||
title: $t('page.manage.menu.hideInMenu'),
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: row => {
|
||||
const hide: CommonType.YesOrNo = row.hideInMenu ? 'Y' : 'N';
|
||||
|
||||
const tagMap: Record<CommonType.YesOrNo, NaiveUI.ThemeColor> = {
|
||||
Y: 'error',
|
||||
N: 'default'
|
||||
};
|
||||
|
||||
const label = $t(yesOrNoRecord[hide]);
|
||||
|
||||
return <NTag type={tagMap[hide]}>{label}</NTag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'parentId',
|
||||
title: $t('page.manage.menu.parentId'),
|
||||
width: 90,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'order',
|
||||
title: $t('page.manage.menu.order'),
|
||||
align: 'center',
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: $t('common.operate'),
|
||||
align: 'center',
|
||||
width: 230,
|
||||
render: row => (
|
||||
<div class="flex-center justify-end gap-8px">
|
||||
{row.menuType === '1' && (
|
||||
<NButton type="primary" ghost size="small" onClick={() => handleAddChildMenu(row)}>
|
||||
{$t('page.manage.menu.addChildMenu')}
|
||||
</NButton>
|
||||
)}
|
||||
<NButton type="primary" ghost size="small" onClick={() => handleEdit(row)}>
|
||||
{$t('common.edit')}
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
|
||||
{{
|
||||
default: () => $t('common.confirmDelete'),
|
||||
trigger: () => (
|
||||
<NButton type="error" ghost size="small">
|
||||
{$t('common.delete')}
|
||||
</NButton>
|
||||
)
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(data, 'id', getData);
|
||||
|
||||
const operateType = ref<OperateType>('add');
|
||||
|
||||
function handleAdd() {
|
||||
operateType.value = 'add';
|
||||
openModal();
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
// request
|
||||
console.log(checkedRowKeys.value);
|
||||
|
||||
onBatchDeleted();
|
||||
}
|
||||
|
||||
function handleDelete(id: number) {
|
||||
// request
|
||||
console.log(id);
|
||||
|
||||
onDeleted();
|
||||
}
|
||||
|
||||
/** the edit menu data or the parent menu data when adding a child menu */
|
||||
const editingData: Ref<Api.SystemManage.Menu | null> = ref(null);
|
||||
|
||||
function handleEdit(item: Api.SystemManage.Menu) {
|
||||
operateType.value = 'edit';
|
||||
editingData.value = { ...item };
|
||||
|
||||
openModal();
|
||||
}
|
||||
|
||||
function handleAddChildMenu(item: Api.SystemManage.Menu) {
|
||||
operateType.value = 'addChild';
|
||||
|
||||
editingData.value = { ...item };
|
||||
|
||||
openModal();
|
||||
}
|
||||
|
||||
const allPages = ref<string[]>([]);
|
||||
|
||||
async function getAllPages() {
|
||||
const { data: pages } = await fetchGetAllPages();
|
||||
allPages.value = pages || [];
|
||||
}
|
||||
|
||||
function init() {
|
||||
getAllPages();
|
||||
}
|
||||
|
||||
// init
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="wrapperRef" class="flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<NCard :title="$t('page.manage.menu.title')" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
|
||||
<template #header-extra>
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks"
|
||||
:disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<NDataTable
|
||||
v-model:checked-row-keys="checkedRowKeys"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
size="small"
|
||||
:flex-height="!appStore.isMobile"
|
||||
:scroll-x="1088"
|
||||
:loading="loading"
|
||||
:row-key="row => row.id"
|
||||
remote
|
||||
:pagination="pagination"
|
||||
class="sm:h-full"
|
||||
/>
|
||||
<MenuOperateModal
|
||||
v-model:visible="visible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
:all-pages="allPages"
|
||||
@submitted="getDataByPage"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
467
src/views/manage/menu/modules/menu-operate-modal.vue
Normal file
467
src/views/manage/menu/modules/menu-operate-modal.vue
Normal file
@ -0,0 +1,467 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { SelectOption } from 'naive-ui';
|
||||
import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business';
|
||||
import { fetchGetAllRoles } from '@/service/api';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { getLocalIcons } from '@/utils/icon';
|
||||
import { $t } from '@/locales';
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue';
|
||||
import {
|
||||
getLayoutAndPage,
|
||||
getPathParamFromRoutePath,
|
||||
getRoutePathByRouteName,
|
||||
getRoutePathWithParam,
|
||||
transformLayoutAndPageToComponent
|
||||
} from './shared';
|
||||
|
||||
defineOptions({
|
||||
name: 'MenuOperateModal'
|
||||
});
|
||||
|
||||
export type OperateType = NaiveUI.TableOperateType | 'addChild';
|
||||
|
||||
interface Props {
|
||||
/** the type of operation */
|
||||
operateType: OperateType;
|
||||
/** the edit menu data or the parent menu data when adding a child menu */
|
||||
rowData?: Api.SystemManage.Menu | null;
|
||||
/** all pages */
|
||||
allPages: string[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<OperateType, string> = {
|
||||
add: $t('page.manage.menu.addMenu'),
|
||||
addChild: $t('page.manage.menu.addChildMenu'),
|
||||
edit: $t('page.manage.menu.editMenu')
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
type Model = Pick<
|
||||
Api.SystemManage.Menu,
|
||||
| 'menuType'
|
||||
| 'menuName'
|
||||
| 'routeName'
|
||||
| 'routePath'
|
||||
| 'component'
|
||||
| 'order'
|
||||
| 'i18nKey'
|
||||
| 'icon'
|
||||
| 'iconType'
|
||||
| 'status'
|
||||
| 'parentId'
|
||||
| 'keepAlive'
|
||||
| 'constant'
|
||||
| 'href'
|
||||
| 'hideInMenu'
|
||||
| 'activeMenu'
|
||||
| 'multiTab'
|
||||
| 'fixedIndexInTab'
|
||||
> & {
|
||||
query: NonNullable<Api.SystemManage.Menu['query']>;
|
||||
buttons: NonNullable<Api.SystemManage.Menu['buttons']>;
|
||||
layout: string;
|
||||
page: string;
|
||||
pathParam: string;
|
||||
};
|
||||
|
||||
const model = ref(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
menuType: '1',
|
||||
menuName: '',
|
||||
routeName: '',
|
||||
routePath: '',
|
||||
pathParam: '',
|
||||
component: '',
|
||||
layout: '',
|
||||
page: '',
|
||||
i18nKey: null,
|
||||
icon: '',
|
||||
iconType: '1',
|
||||
parentId: 0,
|
||||
status: '1',
|
||||
keepAlive: false,
|
||||
constant: false,
|
||||
order: 0,
|
||||
href: null,
|
||||
hideInMenu: false,
|
||||
activeMenu: null,
|
||||
multiTab: false,
|
||||
fixedIndexInTab: null,
|
||||
query: [],
|
||||
buttons: []
|
||||
};
|
||||
}
|
||||
|
||||
type RuleKey = Extract<keyof Model, 'menuName' | 'status' | 'routeName' | 'routePath'>;
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
menuName: defaultRequiredRule,
|
||||
status: defaultRequiredRule,
|
||||
routeName: defaultRequiredRule,
|
||||
routePath: defaultRequiredRule
|
||||
};
|
||||
|
||||
const disabledMenuType = computed(() => props.operateType === 'edit');
|
||||
|
||||
const localIcons = getLocalIcons();
|
||||
const localIconOptions = localIcons.map<SelectOption>(item => ({
|
||||
label: () => (
|
||||
<div class="flex-y-center gap-16px">
|
||||
<SvgIcon localIcon={item} class="text-icon" />
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
),
|
||||
value: item
|
||||
}));
|
||||
|
||||
const showLayout = computed(() => model.value.parentId === 0);
|
||||
|
||||
const showPage = computed(() => model.value.menuType === '2');
|
||||
|
||||
const pageOptions = computed(() => {
|
||||
const allPages = [...props.allPages];
|
||||
|
||||
if (model.value.routeName && !allPages.includes(model.value.routeName)) {
|
||||
allPages.unshift(model.value.routeName);
|
||||
}
|
||||
|
||||
const opts: CommonType.Option[] = allPages.map(page => ({
|
||||
label: page,
|
||||
value: page
|
||||
}));
|
||||
|
||||
return opts;
|
||||
});
|
||||
|
||||
const layoutOptions: CommonType.Option[] = [
|
||||
{
|
||||
label: 'base',
|
||||
value: 'base'
|
||||
},
|
||||
{
|
||||
label: 'blank',
|
||||
value: 'blank'
|
||||
}
|
||||
];
|
||||
|
||||
/** the enabled role options */
|
||||
const roleOptions = ref<CommonType.Option<string>[]>([]);
|
||||
|
||||
async function getRoleOptions() {
|
||||
const { error, data } = await fetchGetAllRoles();
|
||||
|
||||
if (!error) {
|
||||
const options = data.map(item => ({
|
||||
label: item.roleName,
|
||||
value: item.roleCode
|
||||
}));
|
||||
|
||||
roleOptions.value = [...options];
|
||||
}
|
||||
}
|
||||
|
||||
function handleInitModel() {
|
||||
model.value = createDefaultModel();
|
||||
|
||||
if (!props.rowData) return;
|
||||
|
||||
if (props.operateType === 'addChild') {
|
||||
const { id } = props.rowData;
|
||||
|
||||
Object.assign(model.value, { parentId: id });
|
||||
}
|
||||
|
||||
if (props.operateType === 'edit') {
|
||||
const { component, ...rest } = props.rowData;
|
||||
|
||||
const { layout, page } = getLayoutAndPage(component);
|
||||
const { path, param } = getPathParamFromRoutePath(rest.routePath);
|
||||
|
||||
Object.assign(model.value, rest, { layout, page, routePath: path, pathParam: param });
|
||||
}
|
||||
|
||||
if (!model.value.query) {
|
||||
model.value.query = [];
|
||||
}
|
||||
if (!model.value.buttons) {
|
||||
model.value.buttons = [];
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function handleUpdateRoutePathByRouteName() {
|
||||
if (model.value.routeName) {
|
||||
model.value.routePath = getRoutePathByRouteName(model.value.routeName);
|
||||
} else {
|
||||
model.value.routePath = '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleUpdateI18nKeyByRouteName() {
|
||||
if (model.value.routeName) {
|
||||
model.value.i18nKey = `route.${model.value.routeName}` as App.I18n.I18nKey;
|
||||
} else {
|
||||
model.value.i18nKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateButton() {
|
||||
const buttonItem: Api.SystemManage.MenuButton = {
|
||||
code: '',
|
||||
desc: ''
|
||||
};
|
||||
|
||||
return buttonItem;
|
||||
}
|
||||
|
||||
function getSubmitParams() {
|
||||
const { layout, page, pathParam, ...params } = model.value;
|
||||
|
||||
const component = transformLayoutAndPageToComponent(layout, page);
|
||||
const routePath = getRoutePathWithParam(model.value.routePath, pathParam);
|
||||
|
||||
params.component = component;
|
||||
params.routePath = routePath;
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
const params = getSubmitParams();
|
||||
|
||||
console.log('params: ', params);
|
||||
|
||||
// request
|
||||
window.$message?.success($t('common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
watch(visible, () => {
|
||||
if (visible.value) {
|
||||
handleInitModel();
|
||||
restoreValidation();
|
||||
getRoleOptions();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => model.value.routeName,
|
||||
() => {
|
||||
handleUpdateRoutePathByRouteName();
|
||||
handleUpdateI18nKeyByRouteName();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="visible" :title="title" preset="card" class="w-800px">
|
||||
<NScrollbar class="h-480px pr-20px">
|
||||
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="100">
|
||||
<NGrid responsive="screen" item-responsive>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.menuType')" path="menuType">
|
||||
<NRadioGroup v-model:value="model.menuType" :disabled="disabledMenuType">
|
||||
<NRadio v-for="item in menuTypeOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.menuName')" path="menuName">
|
||||
<NInput v-model:value="model.menuName" :placeholder="$t('page.manage.menu.form.menuName')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.routeName')" path="routeName">
|
||||
<NInput v-model:value="model.routeName" :placeholder="$t('page.manage.menu.form.routeName')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.routePath')" path="routePath">
|
||||
<NInput v-model:value="model.routePath" disabled :placeholder="$t('page.manage.menu.form.routePath')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.pathParam')" path="pathParam">
|
||||
<NInput v-model:value="model.pathParam" :placeholder="$t('page.manage.menu.form.pathParam')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="showLayout" span="24 m:12" :label="$t('page.manage.menu.layout')" path="layout">
|
||||
<NSelect
|
||||
v-model:value="model.layout"
|
||||
:options="layoutOptions"
|
||||
:placeholder="$t('page.manage.menu.form.layout')"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="showPage" span="24 m:12" :label="$t('page.manage.menu.page')" path="page">
|
||||
<NSelect
|
||||
v-model:value="model.page"
|
||||
:options="pageOptions"
|
||||
:placeholder="$t('page.manage.menu.form.page')"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.i18nKey')" path="i18nKey">
|
||||
<NInput v-model:value="model.i18nKey" :placeholder="$t('page.manage.menu.form.i18nKey')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.order')" path="order">
|
||||
<NInputNumber v-model:value="model.order" class="w-full" :placeholder="$t('page.manage.menu.form.order')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.iconTypeTitle')" path="iconType">
|
||||
<NRadioGroup v-model:value="model.iconType">
|
||||
<NRadio
|
||||
v-for="item in menuIconTypeOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="$t(item.label)"
|
||||
/>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.icon')" path="icon">
|
||||
<template v-if="model.iconType === '1'">
|
||||
<NInput v-model:value="model.icon" :placeholder="$t('page.manage.menu.form.icon')" class="flex-1">
|
||||
<template #suffix>
|
||||
<SvgIcon v-if="model.icon" :icon="model.icon" class="text-icon" />
|
||||
</template>
|
||||
</NInput>
|
||||
</template>
|
||||
<template v-if="model.iconType === '2'">
|
||||
<NSelect
|
||||
v-model:value="model.icon"
|
||||
:placeholder="$t('page.manage.menu.form.localIcon')"
|
||||
:options="localIconOptions"
|
||||
/>
|
||||
</template>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.menuStatus')" path="status">
|
||||
<NRadioGroup v-model:value="model.status">
|
||||
<NRadio
|
||||
v-for="item in enableStatusOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="$t(item.label)"
|
||||
/>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.keepAlive')" path="keepAlive">
|
||||
<NRadioGroup v-model:value="model.keepAlive">
|
||||
<NRadio :value="true" :label="$t('common.yesOrNo.yes')" />
|
||||
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.constant')" path="constant">
|
||||
<NRadioGroup v-model:value="model.constant">
|
||||
<NRadio :value="true" :label="$t('common.yesOrNo.yes')" />
|
||||
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.href')" path="href">
|
||||
<NInput v-model:value="model.href" :placeholder="$t('page.manage.menu.form.href')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.hideInMenu')" path="hideInMenu">
|
||||
<NRadioGroup v-model:value="model.hideInMenu">
|
||||
<NRadio :value="true" :label="$t('common.yesOrNo.yes')" />
|
||||
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi
|
||||
v-if="model.hideInMenu"
|
||||
span="24 m:12"
|
||||
:label="$t('page.manage.menu.activeMenu')"
|
||||
path="activeMenu"
|
||||
>
|
||||
<NSelect
|
||||
v-model:value="model.activeMenu"
|
||||
:options="pageOptions"
|
||||
clearable
|
||||
:placeholder="$t('page.manage.menu.form.activeMenu')"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.multiTab')" path="multiTab">
|
||||
<NRadioGroup v-model:value="model.multiTab">
|
||||
<NRadio :value="true" :label="$t('common.yesOrNo.yes')" />
|
||||
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.fixedIndexInTab')" path="fixedIndexInTab">
|
||||
<NInputNumber
|
||||
v-model:value="model.fixedIndexInTab"
|
||||
class="w-full"
|
||||
clearable
|
||||
:placeholder="$t('page.manage.menu.form.fixedIndexInTab')"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24" :label="$t('page.manage.menu.query')">
|
||||
<NDynamicInput
|
||||
v-model:value="model.query"
|
||||
preset="pair"
|
||||
:key-placeholder="$t('page.manage.menu.form.queryKey')"
|
||||
:value-placeholder="$t('page.manage.menu.form.queryValue')"
|
||||
>
|
||||
<template #action="{ index, create, remove }">
|
||||
<NSpace class="ml-12px">
|
||||
<NButton size="medium" @click="() => create(index)">
|
||||
<icon-ic:round-plus class="text-icon" />
|
||||
</NButton>
|
||||
<NButton size="medium" @click="() => remove(index)">
|
||||
<icon-ic-round-remove class="text-icon" />
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDynamicInput>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24" :label="$t('page.manage.menu.button')">
|
||||
<NDynamicInput v-model:value="model.buttons" :on-create="handleCreateButton">
|
||||
<template #default="{ value }">
|
||||
<div class="ml-8px flex-y-center flex-1 gap-12px">
|
||||
<NInput
|
||||
v-model:value="value.code"
|
||||
:placeholder="$t('page.manage.menu.form.buttonCode')"
|
||||
class="flex-1"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="value.desc"
|
||||
:placeholder="$t('page.manage.menu.form.buttonDesc')"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ index, create, remove }">
|
||||
<NSpace class="ml-12px">
|
||||
<NButton size="medium" @click="() => create(index)">
|
||||
<icon-ic:round-plus class="text-icon" />
|
||||
</NButton>
|
||||
<NButton size="medium" @click="() => remove(index)">
|
||||
<icon-ic-round-remove class="text-icon" />
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDynamicInput>
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
</NScrollbar>
|
||||
<template #footer>
|
||||
<NSpace justify="end" :size="16">
|
||||
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
79
src/views/manage/menu/modules/shared.ts
Normal file
79
src/views/manage/menu/modules/shared.ts
Normal file
@ -0,0 +1,79 @@
|
||||
const LAYOUT_PREFIX = 'layout.';
|
||||
const VIEW_PREFIX = 'view.';
|
||||
const FIRST_LEVEL_ROUTE_COMPONENT_SPLIT = '$';
|
||||
|
||||
export function getLayoutAndPage(component?: string | null) {
|
||||
let layout = '';
|
||||
let page = '';
|
||||
|
||||
const [layoutOrPage = '', pageItem = ''] = component?.split(FIRST_LEVEL_ROUTE_COMPONENT_SPLIT) || [];
|
||||
|
||||
layout = getLayout(layoutOrPage);
|
||||
page = getPage(pageItem || layoutOrPage);
|
||||
|
||||
return { layout, page };
|
||||
}
|
||||
|
||||
function getLayout(layout: string) {
|
||||
return layout.startsWith(LAYOUT_PREFIX) ? layout.replace(LAYOUT_PREFIX, '') : '';
|
||||
}
|
||||
|
||||
function getPage(page: string) {
|
||||
return page.startsWith(VIEW_PREFIX) ? page.replace(VIEW_PREFIX, '') : '';
|
||||
}
|
||||
|
||||
export function transformLayoutAndPageToComponent(layout: string, page: string) {
|
||||
const hasLayout = Boolean(layout);
|
||||
const hasPage = Boolean(page);
|
||||
|
||||
if (hasLayout && hasPage) {
|
||||
return `${LAYOUT_PREFIX}${layout}${FIRST_LEVEL_ROUTE_COMPONENT_SPLIT}${VIEW_PREFIX}${page}`;
|
||||
}
|
||||
|
||||
if (hasLayout) {
|
||||
return `${LAYOUT_PREFIX}${layout}`;
|
||||
}
|
||||
|
||||
if (hasPage) {
|
||||
return `${VIEW_PREFIX}${page}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route name by route path
|
||||
*
|
||||
* @param routeName
|
||||
*/
|
||||
export function getRoutePathByRouteName(routeName: string) {
|
||||
return `/${routeName.replace(/_/g, '/')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path param from route path
|
||||
*
|
||||
* @param routePath route path
|
||||
*/
|
||||
export function getPathParamFromRoutePath(routePath: string) {
|
||||
const [path, param = ''] = routePath.split('/:');
|
||||
|
||||
return {
|
||||
path,
|
||||
param
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route path with param
|
||||
*
|
||||
* @param routePath route path
|
||||
* @param param path param
|
||||
*/
|
||||
export function getRoutePathWithParam(routePath: string, param: string) {
|
||||
if (param.trim()) {
|
||||
return `${routePath}/:${param}`;
|
||||
}
|
||||
|
||||
return routePath;
|
||||
}
|
173
src/views/manage/role/index.vue
Normal file
173
src/views/manage/role/index.vue
Normal file
@ -0,0 +1,173 @@
|
||||
<script setup lang="tsx">
|
||||
import { reactive } from 'vue';
|
||||
import { NButton, NPopconfirm, NTag } from 'naive-ui';
|
||||
import { enableStatusRecord } from '@/constants/business';
|
||||
import { fetchGetRoleList } from '@/service/api';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
import RoleOperateDrawer from './modules/role-operate-drawer.vue';
|
||||
import RoleSearch from './modules/role-search.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const searchParams: Api.SystemManage.RoleSearchParams = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
roleName: null,
|
||||
roleCode: null,
|
||||
status: null
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useNaivePaginatedTable({
|
||||
api: () => fetchGetRoleList(searchParams),
|
||||
transform: response => defaultTransform(response),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.current = params.page;
|
||||
searchParams.size = params.pageSize;
|
||||
},
|
||||
columns: () => [
|
||||
{
|
||||
type: 'selection',
|
||||
align: 'center',
|
||||
width: 48
|
||||
},
|
||||
{
|
||||
key: 'index',
|
||||
title: $t('common.index'),
|
||||
width: 64,
|
||||
align: 'center',
|
||||
render: (_, index) => index + 1
|
||||
},
|
||||
{
|
||||
key: 'roleName',
|
||||
title: $t('page.manage.role.roleName'),
|
||||
align: 'center',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
key: 'roleCode',
|
||||
title: $t('page.manage.role.roleCode'),
|
||||
align: 'center',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
key: 'roleDesc',
|
||||
title: $t('page.manage.role.roleDesc'),
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: $t('page.manage.role.roleStatus'),
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: row => {
|
||||
if (row.status === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
|
||||
1: 'success',
|
||||
2: 'warning'
|
||||
};
|
||||
|
||||
const label = $t(enableStatusRecord[row.status]);
|
||||
|
||||
return <NTag type={tagMap[row.status]}>{label}</NTag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: $t('common.operate'),
|
||||
align: 'center',
|
||||
width: 130,
|
||||
render: row => (
|
||||
<div class="flex-center gap-8px">
|
||||
<NButton type="primary" ghost size="small" onClick={() => edit(row.id)}>
|
||||
{$t('common.edit')}
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
|
||||
{{
|
||||
default: () => $t('common.confirmDelete'),
|
||||
trigger: () => (
|
||||
<NButton type="error" ghost size="small">
|
||||
{$t('common.delete')}
|
||||
</NButton>
|
||||
)
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const {
|
||||
drawerVisible,
|
||||
operateType,
|
||||
editingData,
|
||||
handleAdd,
|
||||
handleEdit,
|
||||
checkedRowKeys,
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, 'id', getData);
|
||||
|
||||
async function handleBatchDelete() {
|
||||
// request
|
||||
console.log(checkedRowKeys.value);
|
||||
|
||||
onBatchDeleted();
|
||||
}
|
||||
|
||||
function handleDelete(id: number) {
|
||||
// request
|
||||
console.log(id);
|
||||
|
||||
onDeleted();
|
||||
}
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<RoleSearch v-model:model="searchParams" @search="getDataByPage" />
|
||||
<NCard :title="$t('page.manage.role.title')" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
|
||||
<template #header-extra>
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks"
|
||||
:disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<NDataTable
|
||||
v-model:checked-row-keys="checkedRowKeys"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
size="small"
|
||||
:flex-height="!appStore.isMobile"
|
||||
:scroll-x="702"
|
||||
:loading="loading"
|
||||
remote
|
||||
:row-key="row => row.id"
|
||||
:pagination="mobilePagination"
|
||||
class="sm:h-full"
|
||||
/>
|
||||
<RoleOperateDrawer
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
101
src/views/manage/role/modules/button-auth-modal.vue
Normal file
101
src/views/manage/role/modules/button-auth-modal.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'ButtonAuthModal'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the roleId */
|
||||
roleId: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
function closeModal() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
const title = computed(() => $t('common.edit') + $t('page.manage.role.buttonAuth'));
|
||||
|
||||
type ButtonConfig = {
|
||||
id: number;
|
||||
label: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
const tree = shallowRef<ButtonConfig[]>([]);
|
||||
|
||||
async function getAllButtons() {
|
||||
// request
|
||||
tree.value = [
|
||||
{ id: 1, label: 'button1', code: 'code1' },
|
||||
{ id: 2, label: 'button2', code: 'code2' },
|
||||
{ id: 3, label: 'button3', code: 'code3' },
|
||||
{ id: 4, label: 'button4', code: 'code4' },
|
||||
{ id: 5, label: 'button5', code: 'code5' },
|
||||
{ id: 6, label: 'button6', code: 'code6' },
|
||||
{ id: 7, label: 'button7', code: 'code7' },
|
||||
{ id: 8, label: 'button8', code: 'code8' },
|
||||
{ id: 9, label: 'button9', code: 'code9' },
|
||||
{ id: 10, label: 'button10', code: 'code10' }
|
||||
];
|
||||
}
|
||||
|
||||
const checks = shallowRef<number[]>([]);
|
||||
|
||||
async function getChecks() {
|
||||
console.log(props.roleId);
|
||||
// request
|
||||
checks.value = [1, 2, 3, 4, 5];
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
console.log(checks.value, props.roleId);
|
||||
// request
|
||||
|
||||
window.$message?.success?.($t('common.modifySuccess'));
|
||||
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function init() {
|
||||
getAllButtons();
|
||||
getChecks();
|
||||
}
|
||||
|
||||
// init
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="visible" :title="title" preset="card" class="w-480px">
|
||||
<NTree
|
||||
v-model:checked-keys="checks"
|
||||
:data="tree"
|
||||
key-field="id"
|
||||
block-line
|
||||
checkable
|
||||
expand-on-click
|
||||
virtual-scroll
|
||||
class="h-280px"
|
||||
/>
|
||||
<template #footer>
|
||||
<NSpace justify="end">
|
||||
<NButton size="small" class="mt-16px" @click="closeModal">
|
||||
{{ $t('common.cancel') }}
|
||||
</NButton>
|
||||
<NButton type="primary" size="small" class="mt-16px" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
130
src/views/manage/role/modules/menu-auth-modal.vue
Normal file
130
src/views/manage/role/modules/menu-auth-modal.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, shallowRef, watch } from 'vue';
|
||||
import { fetchGetAllPages, fetchGetMenuTree } from '@/service/api';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'MenuAuthModal'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the roleId */
|
||||
roleId: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
function closeModal() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
const title = computed(() => $t('common.edit') + $t('page.manage.role.menuAuth'));
|
||||
|
||||
const home = shallowRef('');
|
||||
|
||||
async function getHome() {
|
||||
console.log(props.roleId);
|
||||
|
||||
home.value = 'home';
|
||||
}
|
||||
|
||||
async function updateHome(val: string) {
|
||||
// request
|
||||
|
||||
home.value = val;
|
||||
}
|
||||
|
||||
const pages = shallowRef<string[]>([]);
|
||||
|
||||
async function getPages() {
|
||||
const { error, data } = await fetchGetAllPages();
|
||||
|
||||
if (!error) {
|
||||
pages.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
const pageSelectOptions = computed(() => {
|
||||
const opts: CommonType.Option[] = pages.value.map(page => ({
|
||||
label: page,
|
||||
value: page
|
||||
}));
|
||||
|
||||
return opts;
|
||||
});
|
||||
|
||||
const tree = shallowRef<Api.SystemManage.MenuTree[]>([]);
|
||||
|
||||
async function getTree() {
|
||||
const { error, data } = await fetchGetMenuTree();
|
||||
|
||||
if (!error) {
|
||||
tree.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
const checks = shallowRef<number[]>([]);
|
||||
|
||||
async function getChecks() {
|
||||
console.log(props.roleId);
|
||||
// request
|
||||
checks.value = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
console.log(checks.value, props.roleId);
|
||||
// request
|
||||
|
||||
window.$message?.success?.($t('common.modifySuccess'));
|
||||
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function init() {
|
||||
getHome();
|
||||
getPages();
|
||||
getTree();
|
||||
getChecks();
|
||||
}
|
||||
|
||||
watch(visible, val => {
|
||||
if (val) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="visible" :title="title" preset="card" class="w-480px">
|
||||
<div class="flex-y-center gap-16px pb-12px">
|
||||
<div>{{ $t('page.manage.menu.home') }}</div>
|
||||
<NSelect :value="home" :options="pageSelectOptions" size="small" class="w-160px" @update:value="updateHome" />
|
||||
</div>
|
||||
<NTree
|
||||
v-model:checked-keys="checks"
|
||||
:data="tree"
|
||||
key-field="id"
|
||||
checkable
|
||||
expand-on-click
|
||||
virtual-scroll
|
||||
block-line
|
||||
class="h-280px"
|
||||
/>
|
||||
<template #footer>
|
||||
<NSpace justify="end">
|
||||
<NButton size="small" class="mt-16px" @click="closeModal">
|
||||
{{ $t('common.cancel') }}
|
||||
</NButton>
|
||||
<NButton type="primary" size="small" class="mt-16px" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
134
src/views/manage/role/modules/role-operate-drawer.vue
Normal file
134
src/views/manage/role/modules/role-operate-drawer.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import MenuAuthModal from './menu-auth-modal.vue';
|
||||
import ButtonAuthModal from './button-auth-modal.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'RoleOperateDrawer'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the type of operation */
|
||||
operateType: NaiveUI.TableOperateType;
|
||||
/** the edit row data */
|
||||
rowData?: Api.SystemManage.Role | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
const { bool: menuAuthVisible, setTrue: openMenuAuthModal } = useBoolean();
|
||||
const { bool: buttonAuthVisible, setTrue: openButtonAuthModal } = useBoolean();
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<NaiveUI.TableOperateType, string> = {
|
||||
add: $t('page.manage.role.addRole'),
|
||||
edit: $t('page.manage.role.editRole')
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
type Model = Pick<Api.SystemManage.Role, 'roleName' | 'roleCode' | 'roleDesc' | 'status'>;
|
||||
|
||||
const model = ref(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
roleName: '',
|
||||
roleCode: '',
|
||||
roleDesc: '',
|
||||
status: null
|
||||
};
|
||||
}
|
||||
|
||||
type RuleKey = Exclude<keyof Model, 'roleDesc'>;
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
roleName: defaultRequiredRule,
|
||||
roleCode: defaultRequiredRule,
|
||||
status: defaultRequiredRule
|
||||
};
|
||||
|
||||
const roleId = computed(() => props.rowData?.id || -1);
|
||||
|
||||
const isEdit = computed(() => props.operateType === 'edit');
|
||||
|
||||
function handleInitModel() {
|
||||
model.value = createDefaultModel();
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
Object.assign(model.value, props.rowData);
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
// request
|
||||
window.$message?.success($t('common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
watch(visible, () => {
|
||||
if (visible.value) {
|
||||
handleInitModel();
|
||||
restoreValidation();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.manage.role.roleName')" path="roleName">
|
||||
<NInput v-model:value="model.roleName" :placeholder="$t('page.manage.role.form.roleName')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.role.roleCode')" path="roleCode">
|
||||
<NInput v-model:value="model.roleCode" :placeholder="$t('page.manage.role.form.roleCode')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.role.roleStatus')" path="status">
|
||||
<NRadioGroup v-model:value="model.status">
|
||||
<NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.role.roleDesc')" path="roleDesc">
|
||||
<NInput v-model:value="model.roleDesc" :placeholder="$t('page.manage.role.form.roleDesc')" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<NSpace v-if="isEdit">
|
||||
<NButton @click="openMenuAuthModal">{{ $t('page.manage.role.menuAuth') }}</NButton>
|
||||
<MenuAuthModal v-model:visible="menuAuthVisible" :role-id="roleId" />
|
||||
<NButton @click="openButtonAuthModal">{{ $t('page.manage.role.buttonAuth') }}</NButton>
|
||||
<ButtonAuthModal v-model:visible="buttonAuthVisible" :role-id="roleId" />
|
||||
</NSpace>
|
||||
<template #footer>
|
||||
<NSpace :size="16">
|
||||
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
76
src/views/manage/role/modules/role-search.vue
Normal file
76
src/views/manage/role/modules/role-search.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
import { translateOptions } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'RoleSearch'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.SystemManage.RoleSearchParams>('model', { required: true });
|
||||
|
||||
function resetModel() {
|
||||
model.value = {
|
||||
current: 1,
|
||||
size: 10,
|
||||
roleName: null,
|
||||
roleCode: null,
|
||||
status: null
|
||||
};
|
||||
}
|
||||
|
||||
function search() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :bordered="false" size="small" class="card-wrapper">
|
||||
<NCollapse :default-expanded-names="['role-search']">
|
||||
<NCollapseItem :title="$t('common.search')" name="role-search">
|
||||
<NForm :model="model" label-placement="left" :label-width="80">
|
||||
<NGrid responsive="screen" item-responsive>
|
||||
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.role.roleName')" path="roleName" class="pr-24px">
|
||||
<NInput v-model:value="model.roleName" :placeholder="$t('page.manage.role.form.roleName')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.role.roleCode')" path="roleCode" class="pr-24px">
|
||||
<NInput v-model:value="model.roleCode" :placeholder="$t('page.manage.role.form.roleCode')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.role.roleStatus')" path="status" class="pr-24px">
|
||||
<NSelect
|
||||
v-model:value="model.status"
|
||||
:placeholder="$t('page.manage.role.form.roleStatus')"
|
||||
:options="translateOptions(enableStatusOptions)"
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="resetModel">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.reset') }}
|
||||
</NButton>
|
||||
<NButton type="primary" ghost @click="search">
|
||||
<template #icon>
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.search') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
13
src/views/manage/user-detail/[id].vue
Normal file
13
src/views/manage/user-detail/[id].vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
203
src/views/manage/user/index.vue
Normal file
203
src/views/manage/user/index.vue
Normal file
@ -0,0 +1,203 @@
|
||||
<script setup lang="tsx">
|
||||
import { reactive } from 'vue';
|
||||
import { NButton, NPopconfirm, NTag } from 'naive-ui';
|
||||
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
|
||||
import { fetchGetUserList } from '@/service/api';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
import UserOperateDrawer from './modules/user-operate-drawer.vue';
|
||||
import UserSearch from './modules/user-search.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const searchParams: Api.SystemManage.UserSearchParams = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
status: null,
|
||||
userName: null,
|
||||
userGender: null,
|
||||
nickName: null,
|
||||
userPhone: null,
|
||||
userEmail: null
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination } = useNaivePaginatedTable({
|
||||
api: () => fetchGetUserList(searchParams),
|
||||
transform: response => defaultTransform(response),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.current = params.page;
|
||||
searchParams.size = params.pageSize;
|
||||
},
|
||||
columns: () => [
|
||||
{
|
||||
type: 'selection',
|
||||
align: 'center',
|
||||
width: 48
|
||||
},
|
||||
{
|
||||
key: 'index',
|
||||
title: $t('common.index'),
|
||||
align: 'center',
|
||||
width: 64,
|
||||
render: (_, index) => index + 1
|
||||
},
|
||||
{
|
||||
key: 'userName',
|
||||
title: $t('page.manage.user.userName'),
|
||||
align: 'center',
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
key: 'userGender',
|
||||
title: $t('page.manage.user.userGender'),
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: row => {
|
||||
if (row.userGender === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagMap: Record<Api.SystemManage.UserGender, NaiveUI.ThemeColor> = {
|
||||
1: 'primary',
|
||||
2: 'error'
|
||||
};
|
||||
|
||||
const label = $t(userGenderRecord[row.userGender]);
|
||||
|
||||
return <NTag type={tagMap[row.userGender]}>{label}</NTag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'nickName',
|
||||
title: $t('page.manage.user.nickName'),
|
||||
align: 'center',
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
key: 'userPhone',
|
||||
title: $t('page.manage.user.userPhone'),
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'userEmail',
|
||||
title: $t('page.manage.user.userEmail'),
|
||||
align: 'center',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: $t('page.manage.user.userStatus'),
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: row => {
|
||||
if (row.status === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
|
||||
1: 'success',
|
||||
2: 'warning'
|
||||
};
|
||||
|
||||
const label = $t(enableStatusRecord[row.status]);
|
||||
|
||||
return <NTag type={tagMap[row.status]}>{label}</NTag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: $t('common.operate'),
|
||||
align: 'center',
|
||||
width: 130,
|
||||
render: row => (
|
||||
<div class="flex-center gap-8px">
|
||||
<NButton type="primary" ghost size="small" onClick={() => edit(row.id)}>
|
||||
{$t('common.edit')}
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
|
||||
{{
|
||||
default: () => $t('common.confirmDelete'),
|
||||
trigger: () => (
|
||||
<NButton type="error" ghost size="small">
|
||||
{$t('common.delete')}
|
||||
</NButton>
|
||||
)
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const {
|
||||
drawerVisible,
|
||||
operateType,
|
||||
editingData,
|
||||
handleAdd,
|
||||
handleEdit,
|
||||
checkedRowKeys,
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, 'id', getData);
|
||||
|
||||
async function handleBatchDelete() {
|
||||
// request
|
||||
console.log(checkedRowKeys.value);
|
||||
|
||||
onBatchDeleted();
|
||||
}
|
||||
|
||||
function handleDelete(id: number) {
|
||||
// request
|
||||
console.log(id);
|
||||
|
||||
onDeleted();
|
||||
}
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<UserSearch v-model:model="searchParams" @search="getDataByPage" />
|
||||
<NCard :title="$t('page.manage.user.title')" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
|
||||
<template #header-extra>
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks"
|
||||
:disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<NDataTable
|
||||
v-model:checked-row-keys="checkedRowKeys"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
size="small"
|
||||
:flex-height="!appStore.isMobile"
|
||||
:scroll-x="962"
|
||||
:loading="loading"
|
||||
remote
|
||||
:row-key="row => row.id"
|
||||
:pagination="mobilePagination"
|
||||
class="sm:h-full"
|
||||
/>
|
||||
<UserOperateDrawer
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
166
src/views/manage/user/modules/user-operate-drawer.vue
Normal file
166
src/views/manage/user/modules/user-operate-drawer.vue
Normal file
@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
|
||||
import { fetchGetAllRoles } from '@/service/api';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserOperateDrawer'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the type of operation */
|
||||
operateType: NaiveUI.TableOperateType;
|
||||
/** the edit row data */
|
||||
rowData?: Api.SystemManage.User | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<NaiveUI.TableOperateType, string> = {
|
||||
add: $t('page.manage.user.addUser'),
|
||||
edit: $t('page.manage.user.editUser')
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
type Model = Pick<
|
||||
Api.SystemManage.User,
|
||||
'userName' | 'userGender' | 'nickName' | 'userPhone' | 'userEmail' | 'userRoles' | 'status'
|
||||
>;
|
||||
|
||||
const model = ref(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
userName: '',
|
||||
userGender: null,
|
||||
nickName: '',
|
||||
userPhone: '',
|
||||
userEmail: '',
|
||||
userRoles: [],
|
||||
status: null
|
||||
};
|
||||
}
|
||||
|
||||
type RuleKey = Extract<keyof Model, 'userName' | 'status'>;
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
userName: defaultRequiredRule,
|
||||
status: defaultRequiredRule
|
||||
};
|
||||
|
||||
/** the enabled role options */
|
||||
const roleOptions = ref<CommonType.Option<string>[]>([]);
|
||||
|
||||
async function getRoleOptions() {
|
||||
const { error, data } = await fetchGetAllRoles();
|
||||
|
||||
if (!error) {
|
||||
const options = data.map(item => ({
|
||||
label: item.roleName,
|
||||
value: item.roleCode
|
||||
}));
|
||||
|
||||
// the mock data does not have the roleCode, so fill it
|
||||
// if the real request, remove the following code
|
||||
const userRoleOptions = model.value.userRoles.map(item => ({
|
||||
label: item,
|
||||
value: item
|
||||
}));
|
||||
// end
|
||||
|
||||
roleOptions.value = [...userRoleOptions, ...options];
|
||||
}
|
||||
}
|
||||
|
||||
function handleInitModel() {
|
||||
model.value = createDefaultModel();
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
Object.assign(model.value, props.rowData);
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
// request
|
||||
window.$message?.success($t('common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
watch(visible, () => {
|
||||
if (visible.value) {
|
||||
handleInitModel();
|
||||
restoreValidation();
|
||||
getRoleOptions();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.manage.user.userName')" path="userName">
|
||||
<NInput v-model:value="model.userName" :placeholder="$t('page.manage.user.form.userName')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.userGender')" path="userGender">
|
||||
<NRadioGroup v-model:value="model.userGender">
|
||||
<NRadio v-for="item in userGenderOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.nickName')" path="nickName">
|
||||
<NInput v-model:value="model.nickName" :placeholder="$t('page.manage.user.form.nickName')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.userPhone')" path="userPhone">
|
||||
<NInput v-model:value="model.userPhone" :placeholder="$t('page.manage.user.form.userPhone')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.userEmail')" path="email">
|
||||
<NInput v-model:value="model.userEmail" :placeholder="$t('page.manage.user.form.userEmail')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.userStatus')" path="status">
|
||||
<NRadioGroup v-model:value="model.status">
|
||||
<NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.userRole')" path="roles">
|
||||
<NSelect
|
||||
v-model:value="model.userRoles"
|
||||
multiple
|
||||
:options="roleOptions"
|
||||
:placeholder="$t('page.manage.user.form.userRole')"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<NSpace :size="16">
|
||||
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
124
src/views/manage/user/modules/user-search.vue
Normal file
124
src/views/manage/user/modules/user-search.vue
Normal file
@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { translateOptions } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserSearch'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
|
||||
const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true });
|
||||
|
||||
type RuleKey = Extract<keyof Api.SystemManage.UserSearchParams, 'userEmail' | 'userPhone'>;
|
||||
|
||||
const rules = computed<Record<RuleKey, App.Global.FormRule>>(() => {
|
||||
const { patternRules } = useFormRules(); // inside computed to make locale reactive
|
||||
|
||||
return {
|
||||
userEmail: patternRules.email,
|
||||
userPhone: patternRules.phone
|
||||
};
|
||||
});
|
||||
|
||||
function resetModel() {
|
||||
model.value = {
|
||||
current: 1,
|
||||
size: 10,
|
||||
status: null,
|
||||
userName: null,
|
||||
userGender: null,
|
||||
nickName: null,
|
||||
userPhone: null,
|
||||
userEmail: null
|
||||
};
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
await restoreValidation();
|
||||
resetModel();
|
||||
}
|
||||
|
||||
async function search() {
|
||||
await validate();
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :bordered="false" size="small" class="card-wrapper">
|
||||
<NCollapse>
|
||||
<NCollapseItem :title="$t('common.search')" name="user-search">
|
||||
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="80">
|
||||
<NGrid responsive="screen" item-responsive>
|
||||
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userName')" path="userName" class="pr-24px">
|
||||
<NInput v-model:value="model.userName" :placeholder="$t('page.manage.user.form.userName')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi
|
||||
span="24 s:12 m:6"
|
||||
:label="$t('page.manage.user.userGender')"
|
||||
path="userGender"
|
||||
class="pr-24px"
|
||||
>
|
||||
<NSelect
|
||||
v-model:value="model.userGender"
|
||||
:placeholder="$t('page.manage.user.form.userGender')"
|
||||
:options="translateOptions(userGenderOptions)"
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.nickName')" path="nickName" class="pr-24px">
|
||||
<NInput v-model:value="model.nickName" :placeholder="$t('page.manage.user.form.nickName')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userPhone')" path="userPhone" class="pr-24px">
|
||||
<NInput v-model:value="model.userPhone" :placeholder="$t('page.manage.user.form.userPhone')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userEmail')" path="userEmail" class="pr-24px">
|
||||
<NInput v-model:value="model.userEmail" :placeholder="$t('page.manage.user.form.userEmail')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi
|
||||
span="24 s:12 m:6"
|
||||
:label="$t('page.manage.user.userStatus')"
|
||||
path="userStatus"
|
||||
class="pr-24px"
|
||||
>
|
||||
<NSelect
|
||||
v-model:value="model.status"
|
||||
:placeholder="$t('page.manage.user.form.userStatus')"
|
||||
:options="translateOptions(enableStatusOptions)"
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 m:12" class="pr-24px">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.reset') }}
|
||||
</NButton>
|
||||
<NButton type="primary" ghost @click="search">
|
||||
<template #icon>
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.search') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
7
src/views/multi-menu/first_child/index.vue
Normal file
7
src/views/multi-menu/first_child/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
7
src/views/multi-menu/second_child_home/index.vue
Normal file
7
src/views/multi-menu/second_child_home/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
114
src/views/plugin/barcode/index.vue
Normal file
114
src/views/plugin/barcode/index.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import JsBarcode from 'jsbarcode';
|
||||
import type { Options } from 'jsbarcode';
|
||||
|
||||
const text = 'Soybean';
|
||||
|
||||
interface CodeConfig {
|
||||
id: string;
|
||||
title: string;
|
||||
text: string;
|
||||
options: Options;
|
||||
}
|
||||
|
||||
const codes: CodeConfig[] = [
|
||||
{
|
||||
id: 'code39',
|
||||
title: 'CODE 39 正常尺寸',
|
||||
text: 'Hello',
|
||||
options: { format: 'code39' }
|
||||
},
|
||||
{
|
||||
id: 'code128',
|
||||
title: 'CODE 128 正常尺寸',
|
||||
text,
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
id: 'ean-13',
|
||||
title: 'ENA-13 商品条形码',
|
||||
text: '1234567890128',
|
||||
options: { format: 'ean13' }
|
||||
},
|
||||
{
|
||||
id: 'upc-a',
|
||||
title: 'UPC-A 商品条形码',
|
||||
text: '123456789012',
|
||||
options: { format: 'upc' }
|
||||
},
|
||||
{
|
||||
id: 'barcode',
|
||||
title: '不一样的高度,不一样的颜色',
|
||||
text: 'Hello',
|
||||
options: {
|
||||
height: 30,
|
||||
lineColor: '#9ca3af'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'barcode1',
|
||||
title: '加个背景色',
|
||||
text,
|
||||
options: {
|
||||
background: '#9ca3af',
|
||||
lineColor: '#ffffff'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'barcode2',
|
||||
title: '字体好大',
|
||||
text,
|
||||
options: {
|
||||
fontSize: 40
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'barcode3',
|
||||
title: '粗狂的条码,文字离远点',
|
||||
text: 'Hi',
|
||||
options: {
|
||||
textMargin: 30,
|
||||
width: 4
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'barcode4',
|
||||
title: '字体跑上面来,还是粗体',
|
||||
text,
|
||||
options: {
|
||||
textPosition: 'top',
|
||||
fontOptions: 'bold'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function generateBarcode() {
|
||||
codes.forEach(code => {
|
||||
JsBarcode(`#${code.id}`, code.text, code.options);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
generateBarcode();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<NCard title="条形码" :bordered="false" class="h-full card-wrapper" content-class="overflow-hidden">
|
||||
<NScrollbar class="h-full">
|
||||
<NGrid cols="1 s:2 l:3" :x-gap="12" :y-gap="24" responsive="screen" item-responsive>
|
||||
<NGi v-for="item in codes" :key="item.id">
|
||||
<div class="flex-col-center">
|
||||
<h3>{{ item.title }}</h3>
|
||||
<svg :id="item.id" class="h-130px" />
|
||||
</div>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NScrollbar>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
60
src/views/plugin/charts/antv/data.ts
Normal file
60
src/views/plugin/charts/antv/data.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import type { CustomGraphData } from './modules/types';
|
||||
|
||||
// 日期可以自己随便设置,就是字符串展示,也可以修改为业务需要的字段
|
||||
export function getFlowData(): CustomGraphData {
|
||||
return {
|
||||
nodes: [
|
||||
{
|
||||
id: 'NS',
|
||||
name: 'Start',
|
||||
status: 'COMPLETED',
|
||||
startDate: '2024-10-01',
|
||||
endDate: '2024-10-07',
|
||||
actualStartDate: '2024-10-01',
|
||||
actualEndDate: '2024-10-07'
|
||||
},
|
||||
{
|
||||
id: 'N1',
|
||||
name: 'Node1',
|
||||
status: 'COMPLETED_EARLY',
|
||||
startDate: '2024-10-08',
|
||||
endDate: '2024-10-10',
|
||||
actualStartDate: '2024-10-08',
|
||||
actualEndDate: '2024-10-09',
|
||||
milestone: true
|
||||
},
|
||||
{
|
||||
id: 'N2',
|
||||
name: 'Node2',
|
||||
status: 'COMPLETED_EARLY',
|
||||
startDate: '2024-10-11',
|
||||
endDate: '2024-10-13',
|
||||
actualStartDate: '2024-10-11',
|
||||
actualEndDate: '2024-10-12'
|
||||
},
|
||||
{ id: 'N3', name: 'Node3', status: 'IN_PROGRESS', isDeleted: true },
|
||||
{ id: 'N4', name: 'Node4', status: 'COMPLETED_LATE' },
|
||||
{ id: 'N5', name: 'Node5', status: 'DELAYED', isDelayed: true, milestone: true },
|
||||
{ id: 'N6', name: 'Node6', status: 'PAUSED' },
|
||||
{ id: 'N7', name: 'Node7', status: 'NOT_STARTED' },
|
||||
{ id: 'N8', name: 'Node8', status: 'NOT_STARTED' },
|
||||
{ id: 'N9', name: 'End', status: 'NOT_STARTED' },
|
||||
{ id: 'NX', name: 'NodeX', status: 'NOT_STARTED', isDeleted: true }
|
||||
],
|
||||
edges: [
|
||||
{ id: 'E1', source: 'NS', target: 'N1' },
|
||||
{ id: 'E2', source: 'N1', target: 'N2' },
|
||||
{ id: 'E3', source: 'N1', target: 'N3', isDeleted: true },
|
||||
{ id: 'E4', source: 'N1', target: 'N4' },
|
||||
{ id: 'E5', source: 'N2', target: 'N5' },
|
||||
{ id: 'E6', source: 'N3', target: 'N5', isDeleted: true },
|
||||
{ id: 'E7', source: 'N4', target: 'N5' },
|
||||
{ id: 'E8', source: 'N5', target: 'N6' },
|
||||
{ id: 'E9', source: 'N6', target: 'N7' },
|
||||
{ id: 'E10', source: 'N6', target: 'N8' },
|
||||
{ id: 'E11', source: 'N7', target: 'N9' },
|
||||
{ id: 'EX', source: 'N8', target: 'N9' },
|
||||
{ id: 'EO', source: 'N5', target: 'NX', isDeleted: true }
|
||||
]
|
||||
};
|
||||
}
|
67
src/views/plugin/charts/antv/index.vue
Normal file
67
src/views/plugin/charts/antv/index.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type { CustomBehaviorOption, IPointerEvent } from '@antv/g6';
|
||||
import AntvFlow from './modules/antv-flow.vue';
|
||||
import type { CustomGraphData } from './modules/types';
|
||||
import { getFlowData } from './data';
|
||||
|
||||
const antvFlowRef = useTemplateRef('antvFlowRef');
|
||||
|
||||
const flowData = ref({
|
||||
nodes: [],
|
||||
edges: []
|
||||
}) as Ref<CustomGraphData>;
|
||||
|
||||
const selectedNode = ref<string | undefined>('N2');
|
||||
|
||||
const behaviors: CustomBehaviorOption[] = [
|
||||
{
|
||||
type: 'click-select',
|
||||
enable: (event: IPointerEvent) => event.targetType === 'node',
|
||||
onClick: (event: IPointerEvent) => {
|
||||
const node = event.target as unknown as HTMLElement;
|
||||
const nodeData = flowData.value.nodes.find(item => item.id === node.id);
|
||||
selectedNode.value = nodeData?.id;
|
||||
window.$message?.success(`选中节点:[${node.id}]${nodeData?.name}`);
|
||||
}
|
||||
}
|
||||
];
|
||||
const hasNodeN = computed(() => flowData.value.nodes.some(node => node.id === 'NN'));
|
||||
|
||||
function addNode() {
|
||||
const { nodes, edges } = flowData.value;
|
||||
|
||||
nodes.push({ id: 'NN', name: 'New node', status: 'NOT_STARTED' });
|
||||
edges.push({ id: 'EN', source: 'N5', target: 'NN' });
|
||||
flowData.value = { nodes, edges };
|
||||
}
|
||||
|
||||
function removeNode(id: string) {
|
||||
const { nodes, edges } = flowData.value;
|
||||
// 删除node的同时,也需要删除包含NX的edge
|
||||
flowData.value = {
|
||||
nodes: nodes.filter(node => node.id !== id),
|
||||
edges: edges.filter(edge => edge.source !== id && edge.target !== id)
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
flowData.value = getFlowData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<NCard title="AntV G6 Next" :bordered="false" class="h-full card-wrapper">
|
||||
<AntvFlow ref="antvFlowRef" :data="flowData" :selected="selectedNode" :behaviors="behaviors" />
|
||||
<NDivider />
|
||||
<NFlex>
|
||||
<NButton @click="selectedNode = 'N5'">选中节点N5(需要自行处理选中事件,不会触发元素点击)</NButton>
|
||||
<NButton v-if="!hasNodeN" @click="addNode">添加节点并与Node5连线</NButton>
|
||||
<NButton v-else @click="() => removeNode('NN')">删除新添加的节点</NButton>
|
||||
<NButton @click="() => removeNode('NX')">删除NodeX</NButton>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
137
src/views/plugin/charts/antv/modules/antv-flow.vue
Normal file
137
src/views/plugin/charts/antv/modules/antv-flow.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<script setup lang="tsx">
|
||||
import { shallowRef, useTemplateRef, watch } from 'vue';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { vResizeObserver } from '@vueuse/components';
|
||||
import type { CustomBehaviorOption, Graph } from '@antv/g6';
|
||||
import { useAntFlow } from './antv-g6-flow';
|
||||
import { nodeStatus } from './status';
|
||||
import type { CustomGraphData } from './types';
|
||||
|
||||
defineOptions({
|
||||
name: 'AntvFLow'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
behaviors?: CustomBehaviorOption[];
|
||||
data: CustomGraphData;
|
||||
selected?: string;
|
||||
height?: string;
|
||||
autoFit?: 'view' | 'center';
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const containerRef = useTemplateRef('containerRef');
|
||||
const graphRef = shallowRef<Graph | null>(null);
|
||||
|
||||
// 监听容器尺寸变化,调整画布大小为图容器大小
|
||||
const onContainerResize = useDebounceFn(() => {
|
||||
if (graphRef.value) {
|
||||
graphRef.value.resize();
|
||||
}
|
||||
}, 5);
|
||||
|
||||
async function draw() {
|
||||
if (graphRef.value) {
|
||||
graphRef.value.destroy();
|
||||
}
|
||||
const { graph } = useAntFlow({
|
||||
container: 'antv-flow',
|
||||
data: props.data,
|
||||
behaviors: props.behaviors,
|
||||
autoFit: props.autoFit
|
||||
});
|
||||
graphRef.value = graph;
|
||||
await selectNode();
|
||||
}
|
||||
|
||||
async function selectNode() {
|
||||
if (props.selected && graphRef.value) {
|
||||
try {
|
||||
await graphRef.value.setElementState(props.selected, 'selected');
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
graphRef.value?.zoomBy(0.9);
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
graphRef.value?.zoomBy(1.1);
|
||||
}
|
||||
|
||||
function resetZoom() {
|
||||
graphRef.value?.zoomTo(1);
|
||||
graphRef.value?.fitCenter();
|
||||
}
|
||||
|
||||
function fitZoom() {
|
||||
graphRef.value?.fitView();
|
||||
graphRef.value?.fitCenter();
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => props.data, () => props.selected],
|
||||
() => {
|
||||
draw();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
defineExpose({ selectNode, graph: graphRef });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<!-- 画布操作栏 -->
|
||||
<div class="absolute left-0 right-0 z-1 flex items-center items-stretch justify-between">
|
||||
<NButtonGroup size="small" class="bg-white!">
|
||||
<NButton @click="zoomOut">
|
||||
<icon-mingcute:zoom-out-line />
|
||||
</NButton>
|
||||
<NButton @click="zoomIn">
|
||||
<icon-mingcute:zoom-in-line />
|
||||
</NButton>
|
||||
<NButton @click="resetZoom">
|
||||
<icon-icon-park-outline:equal-ratio />
|
||||
</NButton>
|
||||
<NButton @click="fitZoom">
|
||||
<icon-gg:ratio />
|
||||
</NButton>
|
||||
</NButtonGroup>
|
||||
<div class="flex-center gap-12px">
|
||||
<NPopover placement="bottom-end" :animated="false">
|
||||
<template #trigger>
|
||||
<NButton size="small" class="bg-white!">
|
||||
<icon-fe:question />
|
||||
</NButton>
|
||||
</template>
|
||||
<div class="flex-col gap-8px">
|
||||
<div span="2" class="text-12px font-bold">节点图例</div>
|
||||
<NGrid :cols="2" :y-gap="8" class="w-180px!">
|
||||
<NGi v-for="(config, status) in nodeStatus" :key="status" class="flex-center">
|
||||
<NTag size="small" round :bordered="false">
|
||||
<template #icon>
|
||||
<icon-f7:flag-circle-fill v-if="status === 'MILESTONE'" :style="{ color: config.color }" />
|
||||
<icon-f7:circle-fill v-else :style="{ color: config.color }" />
|
||||
</template>
|
||||
{{ config.type }}
|
||||
</NTag>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</div>
|
||||
</NPopover>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 画布 -->
|
||||
<div
|
||||
id="antv-flow"
|
||||
ref="containerRef"
|
||||
v-resize-observer="onContainerResize"
|
||||
class="w-full"
|
||||
:style="{ height: props.height || '300px' }"
|
||||
@contextmenu="event => event.preventDefault()"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user