mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-25 12:56:36 +08:00
Compare commits
90 Commits
bf0e5569ac
...
65c318bcc5
Author | SHA1 | Date | |
---|---|---|---|
|
65c318bcc5 | ||
|
8e6a6d6866 | ||
|
3c0a52825d | ||
|
100e0ea55d | ||
|
257f1183fc | ||
|
d73111116a | ||
|
29a2a5c66a | ||
|
4005763c00 | ||
|
a55b4dc073 | ||
|
be8f915a0c | ||
|
358e129765 | ||
|
9ea56c9b82 | ||
|
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 |
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
@ -4,7 +4,18 @@
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"eslint.validate": ["html", "css", "scss", "json", "jsonc"],
|
||||
"eslint.validate": [
|
||||
"html",
|
||||
"css",
|
||||
"scss",
|
||||
"json",
|
||||
"jsonc",
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue"
|
||||
],
|
||||
"i18n-ally.displayLanguage": "zh-cn",
|
||||
"i18n-ally.enabledParsers": ["ts"],
|
||||
"i18n-ally.enabledFrameworks": ["vue"],
|
||||
|
@ -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 })
|
||||
]
|
||||
}),
|
||||
|
29
package.json
29
package.json
@ -47,34 +47,59 @@
|
||||
"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.10",
|
||||
"vue-router": "4.5.1"
|
||||
"vue-pdf-embed": "2.1.2",
|
||||
"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.359",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.7.1",
|
||||
"@types/bmapgl": "0.0.7",
|
||||
"@types/node": "24.0.15",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "66.3.3",
|
||||
@ -89,6 +114,8 @@
|
||||
"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",
|
||||
|
@ -32,7 +32,8 @@ export function createStorage<T extends object>(type: StorageType, storagePrefix
|
||||
storageData = JSON.parse(json);
|
||||
} catch {}
|
||||
|
||||
if (storageData) {
|
||||
// storageData may be `false` if it is boolean type
|
||||
if (storageData !== null) {
|
||||
return storageData as T[K];
|
||||
}
|
||||
}
|
||||
|
3658
pnpm-lock.yaml
3658
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 |
42
src/components/common/icon-tooltip.vue
Normal file
42
src/components/common/icon-tooltip.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, useSlots } from 'vue';
|
||||
import type { PopoverPlacement } from 'naive-ui';
|
||||
|
||||
defineOptions({ name: 'IconTooltip' });
|
||||
|
||||
interface Props {
|
||||
icon?: string;
|
||||
localIcon?: string;
|
||||
desc?: string;
|
||||
placement?: PopoverPlacement;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
icon: 'mdi-help-circle',
|
||||
localIcon: '',
|
||||
desc: '',
|
||||
placement: 'top'
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
const hasCustomTrigger = computed(() => Boolean(slots.trigger));
|
||||
|
||||
if (!hasCustomTrigger.value && !props.icon && !props.localIcon) {
|
||||
throw new Error('icon or localIcon is required when no custom trigger slot is provided');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NTooltip :placement="placement">
|
||||
<template #trigger>
|
||||
<slot name="trigger">
|
||||
<div class="cursor-pointer">
|
||||
<SvgIcon :icon="icon" :local-icon="localIcon" />
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
<slot>
|
||||
<span>{{ desc }}</span>
|
||||
</slot>
|
||||
</NTooltip>
|
||||
</template>
|
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>
|
@ -58,8 +58,8 @@ export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode,
|
||||
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
|
||||
|
||||
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
|
||||
close: 'theme.layout.resetCacheStrategy.close',
|
||||
refresh: 'theme.layout.resetCacheStrategy.refresh'
|
||||
refresh: 'theme.layout.resetCacheStrategy.refresh',
|
||||
close: 'theme.layout.resetCacheStrategy.close'
|
||||
};
|
||||
|
||||
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
|
||||
|
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';
|
@ -38,6 +38,13 @@ export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptio
|
||||
getColumns
|
||||
});
|
||||
|
||||
// calculate the total width of the table this is used for horizontal scrolling
|
||||
const scrollX = computed(() => {
|
||||
return result.columns.value.reduce((acc, column) => {
|
||||
return acc + Number(column.width ?? column.minWidth ?? 120);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
@ -51,7 +58,10 @@ export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptio
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
return result;
|
||||
return {
|
||||
...result,
|
||||
scrollX
|
||||
};
|
||||
}
|
||||
|
||||
type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;
|
||||
|
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
|
||||
};
|
||||
}
|
@ -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',
|
||||
|
@ -30,6 +30,7 @@ const { selectedKey } = useMenu();
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||
<div class="h-full pt-2">
|
||||
<FirstLevelMenu
|
||||
:menus="firstLevelMenus"
|
||||
:active-menu-key="activeFirstLevelMenuKey"
|
||||
@ -39,6 +40,7 @@ const { selectedKey } = useMenu();
|
||||
@select="handleSelectFirstLevelMenu"
|
||||
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
|
@ -80,7 +80,7 @@ function handleChangeMode(mode: UnionKey.ThemeLayoutMode) {
|
||||
class="flex-col-center cursor-pointer"
|
||||
@click="handleChangeMode(key)"
|
||||
>
|
||||
<NTooltip :placement="item.placement">
|
||||
<IconTooltip :placement="item.placement">
|
||||
<template #trigger>
|
||||
<div
|
||||
class="h-64px w-96px gap-6px rd-4px p-6px shadow ring-2 ring-transparent transition-all hover:ring-primary"
|
||||
@ -92,7 +92,7 @@ function handleChangeMode(mode: UnionKey.ThemeLayoutMode) {
|
||||
</div>
|
||||
</template>
|
||||
{{ $t(`theme.layout.layoutMode.${key}_detail`) }}
|
||||
</NTooltip>
|
||||
</IconTooltip>
|
||||
<p class="mt-8px text-12px">{{ $t(themeLayoutModeRecord[key]) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,7 +13,7 @@ defineProps<Props>();
|
||||
|
||||
<template>
|
||||
<div class="w-full flex-y-center justify-between">
|
||||
<div>
|
||||
<div class="flex-y-center">
|
||||
<span class="pr-8px text-base-text">{{ label }}</span>
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ import AppearanceSettings from './modules/appearance/index.vue';
|
||||
import LayoutSettings from './modules/layout/index.vue';
|
||||
import GeneralSettings from './modules/general/index.vue';
|
||||
import ConfigOperation from './modules/config-operation.vue';
|
||||
import PresetSettings from './modules/preset/index.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ThemeDrawer'
|
||||
@ -33,6 +34,7 @@ const drawerWidth = computed(() => {
|
||||
<NTab name="appearance" :tab="$t('theme.tabs.appearance')"></NTab>
|
||||
<NTab name="layout" :tab="$t('theme.tabs.layout')"></NTab>
|
||||
<NTab name="general" :tab="$t('theme.tabs.general')"></NTab>
|
||||
<NTab name="preset" :tab="$t('theme.tabs.preset')"></NTab>
|
||||
</NTabs>
|
||||
|
||||
<div class="min-h-400px">
|
||||
@ -40,6 +42,7 @@ const drawerWidth = computed(() => {
|
||||
<AppearanceSettings v-if="activeTab === 'appearance'" />
|
||||
<LayoutSettings v-else-if="activeTab === 'layout'" />
|
||||
<GeneralSettings v-else-if="activeTab === 'general'" />
|
||||
<PresetSettings v-else-if="activeTab === 'preset'" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
|
||||
|
@ -36,12 +36,9 @@ const swatches: string[] = [
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.appearance.themeColor.title') }}</NDivider>
|
||||
<div class="flex-col-stretch gap-12px">
|
||||
<NTooltip placement="top-start">
|
||||
<template #trigger>
|
||||
<SettingItem key="recommend-color" :label="$t('theme.appearance.recommendColor')">
|
||||
<NSwitch v-model:value="themeStore.recommendColor" />
|
||||
</SettingItem>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<IconTooltip>
|
||||
<p>
|
||||
<span class="pr-12px">{{ $t('theme.appearance.recommendColorDesc') }}</span>
|
||||
<br />
|
||||
@ -56,7 +53,11 @@ const swatches: string[] = [
|
||||
https://uicolors.app/create
|
||||
</NButton>
|
||||
</p>
|
||||
</NTooltip>
|
||||
</IconTooltip>
|
||||
</template>
|
||||
<NSwitch v-model:value="themeStore.recommendColor" />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
v-for="(_, key) in themeStore.themeColors"
|
||||
:key="key"
|
||||
|
@ -19,6 +19,9 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
|
||||
<NDivider>{{ $t('theme.layout.content.title') }}</NDivider>
|
||||
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
|
||||
<SettingItem key="1" :label="$t('theme.layout.content.scrollMode.title')">
|
||||
<template #suffix>
|
||||
<IconTooltip :desc="$t('theme.layout.content.scrollMode.tip')" />
|
||||
</template>
|
||||
<NSelect
|
||||
v-model:value="themeStore.layout.scrollMode"
|
||||
:options="translateOptions(themeScrollModeOptions)"
|
||||
|
@ -27,6 +27,9 @@ const themeStore = useThemeStore();
|
||||
<NSwitch v-model:value="themeStore.tab.visible" />
|
||||
</SettingItem>
|
||||
<SettingItem v-if="themeStore.tab.visible" key="2" :label="$t('theme.layout.tab.cache')">
|
||||
<template #suffix>
|
||||
<IconTooltip :desc="$t('theme.layout.tab.cacheTip')" />
|
||||
</template>
|
||||
<NSwitch v-model:value="themeStore.tab.cache" />
|
||||
</SettingItem>
|
||||
<SettingItem v-if="themeStore.tab.visible" key="3" :label="$t('theme.layout.tab.height')">
|
||||
|
15
src/layouts/modules/theme-drawer/modules/preset/index.vue
Normal file
15
src/layouts/modules/theme-drawer/modules/preset/index.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import ThemePreset from './modules/theme-preset.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PresetSettings'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px">
|
||||
<ThemePreset />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'ThemePreset'
|
||||
});
|
||||
|
||||
type ThemePreset = Pick<
|
||||
App.Theme.ThemeSetting,
|
||||
| 'themeScheme'
|
||||
| 'grayscale'
|
||||
| 'colourWeakness'
|
||||
| 'recommendColor'
|
||||
| 'themeColor'
|
||||
| 'otherColor'
|
||||
| 'isInfoFollowPrimary'
|
||||
| 'resetCacheStrategy'
|
||||
| 'layout'
|
||||
| 'page'
|
||||
| 'header'
|
||||
| 'tab'
|
||||
| 'fixedHeaderAndTab'
|
||||
| 'sider'
|
||||
| 'footer'
|
||||
| 'watermark'
|
||||
| 'tokens'
|
||||
> & {
|
||||
name: string;
|
||||
desc: string;
|
||||
i18nkey?: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
const presetModules = import.meta.glob('@/theme/preset/*.json', { eager: true, import: 'default' });
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
// Extract preset data
|
||||
const presets = computed(() =>
|
||||
Object.entries(presetModules)
|
||||
.map(([path, presetData]) => {
|
||||
const fileName = path.split('/').pop()?.replace('.json', '') || '';
|
||||
return {
|
||||
id: fileName,
|
||||
...(presetData as ThemePreset)
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.name === 'default') return -1;
|
||||
if (b.name === 'default') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
);
|
||||
|
||||
const getPresetName = (preset: ThemePreset): string => {
|
||||
if (!preset.i18nkey) return preset.name;
|
||||
try {
|
||||
const key = `${preset.i18nkey}.name` as App.I18n.I18nKey;
|
||||
const translated = $t(key);
|
||||
return translated !== key ? translated : preset.name;
|
||||
} catch {
|
||||
return preset.name;
|
||||
}
|
||||
};
|
||||
|
||||
const getPresetDesc = (preset: ThemePreset): string => {
|
||||
if (!preset.i18nkey) return preset.desc;
|
||||
try {
|
||||
const key = `${preset.i18nkey}.desc` as App.I18n.I18nKey;
|
||||
const translated = $t(key);
|
||||
return translated !== key ? translated : preset.desc;
|
||||
} catch {
|
||||
return preset.desc;
|
||||
}
|
||||
};
|
||||
|
||||
const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark, ...rest }: ThemePreset): void => {
|
||||
themeStore.setThemeScheme(themeScheme);
|
||||
themeStore.setGrayscale(grayscale);
|
||||
themeStore.setColourWeakness(colourWeakness);
|
||||
themeStore.setThemeLayout(layout.mode);
|
||||
themeStore.setWatermarkEnableUserName(watermark.enableUserName);
|
||||
themeStore.setWatermarkEnableTime(watermark.enableTime);
|
||||
|
||||
Object.assign(themeStore, {
|
||||
...rest,
|
||||
layout: { ...themeStore.layout, scrollMode: layout.scrollMode },
|
||||
page: { ...rest.page },
|
||||
header: { ...rest.header },
|
||||
tab: { ...rest.tab },
|
||||
sider: { ...rest.sider },
|
||||
footer: { ...rest.footer },
|
||||
watermark: { ...watermark },
|
||||
tokens: { ...rest.tokens }
|
||||
});
|
||||
|
||||
window.$message?.success($t('theme.appearance.preset.applySuccess'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.appearance.preset.title') }}</NDivider>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="preset in presets"
|
||||
:key="preset.id"
|
||||
class="border border-primary/10 rounded-lg border-solid bg-white/5 p-3 backdrop-blur-10 transition-all duration-300 hover:(shadow-md -translate-y-0.5)"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="min-w-0 w-full flex flex-1 items-center justify-between gap-2">
|
||||
<h5 class="m-0 truncate text-sm text-primary font-600">
|
||||
{{ getPresetName(preset) }}
|
||||
</h5>
|
||||
<NBadge :value="`v${preset.version}`" type="info" size="small" class="flex-shrink-0 opacity-80" />
|
||||
</div>
|
||||
<NButton type="primary" size="tiny" ghost round class="ml-2 flex-shrink-0" @click="applyPreset(preset)">
|
||||
{{ $t('theme.appearance.preset.apply') }}
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<p class="line-clamp-2 mb-3 text-xs text-gray-500 leading-4">{{ getPresetDesc(preset) }}</p>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex gap-1">
|
||||
<div
|
||||
v-for="(color, key) in { primary: preset.themeColor, ...preset.otherColor }"
|
||||
:key="key"
|
||||
class="h-3 w-3 cursor-pointer border border-white/30 rounded-full transition-transform hover:scale-110"
|
||||
:style="{ backgroundColor: color }"
|
||||
:class="{ 'ring-1 ring-primary/50': key === 'primary' }"
|
||||
:title="key"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="text-lg">
|
||||
{{ preset.themeScheme === 'dark' ? '🌙' : '☀️' }}
|
||||
</div>
|
||||
<div class="text-lg">
|
||||
{{ preset.grayscale ? '🎨' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -62,7 +62,8 @@ const local: App.I18n.Schema = {
|
||||
tabs: {
|
||||
appearance: 'Appearance',
|
||||
layout: 'Layout',
|
||||
general: 'General'
|
||||
general: 'General',
|
||||
preset: 'Preset'
|
||||
},
|
||||
appearance: {
|
||||
themeSchema: {
|
||||
@ -83,7 +84,28 @@ const local: App.I18n.Schema = {
|
||||
followPrimary: 'Follow Primary'
|
||||
},
|
||||
recommendColor: 'Apply Recommended Color Algorithm',
|
||||
recommendColorDesc: 'The recommended color algorithm refers to'
|
||||
recommendColorDesc: 'The recommended color algorithm refers to',
|
||||
preset: {
|
||||
title: 'Theme Presets',
|
||||
apply: 'Apply',
|
||||
applySuccess: 'Preset applied successfully',
|
||||
default: {
|
||||
name: 'Default Preset',
|
||||
desc: 'Default theme preset with balanced settings'
|
||||
},
|
||||
dark: {
|
||||
name: 'Dark Preset',
|
||||
desc: 'Dark theme preset for night time usage'
|
||||
},
|
||||
compact: {
|
||||
name: 'Compact Preset',
|
||||
desc: 'Compact layout preset for small screens'
|
||||
},
|
||||
azir: {
|
||||
name: "Azir's Preset",
|
||||
desc: 'It is a cold and elegant preset that Azir likes'
|
||||
}
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
layoutMode: {
|
||||
@ -109,6 +131,7 @@ const local: App.I18n.Schema = {
|
||||
title: 'Tab Settings',
|
||||
visible: 'Tab Visible',
|
||||
cache: 'Tag Bar Info Cache',
|
||||
cacheTip: 'One-click to open/close global keepalive',
|
||||
height: 'Tab Height',
|
||||
mode: {
|
||||
title: 'Tab Mode',
|
||||
@ -144,6 +167,7 @@ const local: App.I18n.Schema = {
|
||||
title: 'Content Area Settings',
|
||||
scrollMode: {
|
||||
title: 'Scroll Mode',
|
||||
tip: 'The theme scroll only scrolls the main part, the outer scroll can carry the header and footer together',
|
||||
wrapper: 'Wrapper',
|
||||
content: 'Content'
|
||||
},
|
||||
@ -200,7 +224,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 +346,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 +387,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: {
|
||||
|
@ -62,7 +62,8 @@ const local: App.I18n.Schema = {
|
||||
tabs: {
|
||||
appearance: '外观',
|
||||
layout: '布局',
|
||||
general: '通用'
|
||||
general: '通用',
|
||||
preset: '预设'
|
||||
},
|
||||
appearance: {
|
||||
themeSchema: {
|
||||
@ -83,7 +84,28 @@ const local: App.I18n.Schema = {
|
||||
followPrimary: '跟随主色'
|
||||
},
|
||||
recommendColor: '应用推荐算法的颜色',
|
||||
recommendColorDesc: '推荐颜色的算法参照'
|
||||
recommendColorDesc: '推荐颜色的算法参照',
|
||||
preset: {
|
||||
title: '主题预设',
|
||||
apply: '应用',
|
||||
applySuccess: '预设应用成功',
|
||||
default: {
|
||||
name: '默认预设',
|
||||
desc: 'Soybean 默认主题预设'
|
||||
},
|
||||
dark: {
|
||||
name: '暗色预设',
|
||||
desc: '适用于夜间使用的暗色主题预设'
|
||||
},
|
||||
compact: {
|
||||
name: '紧凑型',
|
||||
desc: '适用于小屏幕的紧凑布局预设'
|
||||
},
|
||||
azir: {
|
||||
name: 'Azir的预设',
|
||||
desc: '是 Azir 比较喜欢的莫兰迪色系冷淡风'
|
||||
}
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
layoutMode: {
|
||||
@ -106,6 +128,7 @@ const local: App.I18n.Schema = {
|
||||
title: '标签栏设置',
|
||||
visible: '显示标签栏',
|
||||
cache: '标签栏信息缓存',
|
||||
cacheTip: '一键开启/关闭全局 keepalive',
|
||||
height: '标签栏高度',
|
||||
mode: {
|
||||
title: '标签栏风格',
|
||||
@ -141,6 +164,7 @@ const local: App.I18n.Schema = {
|
||||
title: '内容区域设置',
|
||||
scrollMode: {
|
||||
title: '滚动模式',
|
||||
tip: '主题滚动仅 main 部分滚动,外层滚动可携带头部底部一起滚动',
|
||||
wrapper: '外层滚动',
|
||||
content: '主体滚动'
|
||||
},
|
||||
@ -197,7 +221,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 +343,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 +384,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'
|
||||
});
|
||||
}
|
90
src/theme/preset/azir.json
Normal file
90
src/theme/preset/azir.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "Azir's Preset",
|
||||
"desc": "It is a cold and elegant preset that Azir likes",
|
||||
"i18nkey": "theme.appearance.preset.azir",
|
||||
"version": "1.0.0",
|
||||
"themeScheme": "light",
|
||||
"grayscale": false,
|
||||
"colourWeakness": false,
|
||||
"recommendColor": true,
|
||||
"themeColor": "#78a878",
|
||||
"otherColor": {
|
||||
"info": "#89b989",
|
||||
"success": "#99c299",
|
||||
"warning": "#d4bb9d",
|
||||
"error": "#c49a9a"
|
||||
},
|
||||
"isInfoFollowPrimary": true,
|
||||
"resetCacheStrategy": "refresh",
|
||||
"layout": {
|
||||
"mode": "vertical-mix",
|
||||
"scrollMode": "wrapper"
|
||||
},
|
||||
"page": {
|
||||
"animate": true,
|
||||
"animateMode": "zoom-fade"
|
||||
},
|
||||
"header": {
|
||||
"height": 64,
|
||||
"breadcrumb": {
|
||||
"visible": true,
|
||||
"showIcon": true
|
||||
},
|
||||
"multilingual": {
|
||||
"visible": true
|
||||
},
|
||||
"globalSearch": {
|
||||
"visible": true
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"visible": true,
|
||||
"cache": true,
|
||||
"height": 48,
|
||||
"mode": "chrome"
|
||||
},
|
||||
"fixedHeaderAndTab": true,
|
||||
"sider": {
|
||||
"inverted": false,
|
||||
"width": 220,
|
||||
"collapsedWidth": 64,
|
||||
"mixWidth": 90,
|
||||
"mixCollapsedWidth": 64,
|
||||
"mixChildMenuWidth": 200
|
||||
},
|
||||
"footer": {
|
||||
"visible": true,
|
||||
"fixed": true,
|
||||
"height": 56,
|
||||
"right": true
|
||||
},
|
||||
"watermark": {
|
||||
"visible": false,
|
||||
"text": "SoybeanAdmin",
|
||||
"enableUserName": false,
|
||||
"enableTime": true,
|
||||
"timeFormat": "YYYY-MM-DD HH:mm:ss"
|
||||
},
|
||||
"tokens": {
|
||||
"light": {
|
||||
"colors": {
|
||||
"container": "rgb(255, 255, 255)",
|
||||
"layout": "rgb(247, 250, 252)",
|
||||
"inverted": "rgb(0, 20, 40)",
|
||||
"base-text": "rgb(31, 31, 31)"
|
||||
},
|
||||
"boxShadow": {
|
||||
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
|
||||
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
|
||||
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"colors": {
|
||||
"container": "rgb(28, 28, 28)",
|
||||
"layout": "rgb(18, 18, 18)",
|
||||
"base-text": "rgb(224, 224, 224)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
src/theme/preset/compact.json
Normal file
90
src/theme/preset/compact.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "Compact Preset",
|
||||
"desc": "Compact layout preset for small screens",
|
||||
"i18nkey": "theme.appearance.preset.compact",
|
||||
"version": "1.0.0",
|
||||
"themeScheme": "light",
|
||||
"grayscale": false,
|
||||
"colourWeakness": false,
|
||||
"recommendColor": false,
|
||||
"themeColor": "#646cff",
|
||||
"otherColor": {
|
||||
"info": "#2080f0",
|
||||
"success": "#52c41a",
|
||||
"warning": "#faad14",
|
||||
"error": "#f5222d"
|
||||
},
|
||||
"isInfoFollowPrimary": true,
|
||||
"resetCacheStrategy": "close",
|
||||
"layout": {
|
||||
"mode": "vertical",
|
||||
"scrollMode": "content"
|
||||
},
|
||||
"page": {
|
||||
"animate": true,
|
||||
"animateMode": "fade-slide"
|
||||
},
|
||||
"header": {
|
||||
"height": 48,
|
||||
"breadcrumb": {
|
||||
"visible": true,
|
||||
"showIcon": true
|
||||
},
|
||||
"multilingual": {
|
||||
"visible": false
|
||||
},
|
||||
"globalSearch": {
|
||||
"visible": false
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"visible": true,
|
||||
"cache": true,
|
||||
"height": 36,
|
||||
"mode": "button"
|
||||
},
|
||||
"fixedHeaderAndTab": true,
|
||||
"sider": {
|
||||
"inverted": false,
|
||||
"width": 180,
|
||||
"collapsedWidth": 48,
|
||||
"mixWidth": 80,
|
||||
"mixCollapsedWidth": 48,
|
||||
"mixChildMenuWidth": 180
|
||||
},
|
||||
"footer": {
|
||||
"visible": false,
|
||||
"fixed": false,
|
||||
"height": 40,
|
||||
"right": true
|
||||
},
|
||||
"watermark": {
|
||||
"visible": false,
|
||||
"text": "SoybeanAdmin",
|
||||
"enableUserName": false,
|
||||
"enableTime": false,
|
||||
"timeFormat": "YYYY-MM-DD HH:mm"
|
||||
},
|
||||
"tokens": {
|
||||
"light": {
|
||||
"colors": {
|
||||
"container": "rgb(255, 255, 255)",
|
||||
"layout": "rgb(247, 250, 252)",
|
||||
"inverted": "rgb(0, 20, 40)",
|
||||
"base-text": "rgb(31, 31, 31)"
|
||||
},
|
||||
"boxShadow": {
|
||||
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
|
||||
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
|
||||
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"colors": {
|
||||
"container": "rgb(28, 28, 28)",
|
||||
"layout": "rgb(18, 18, 18)",
|
||||
"base-text": "rgb(224, 224, 224)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
src/theme/preset/dark.json
Normal file
90
src/theme/preset/dark.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "Dark Preset",
|
||||
"desc": "Dark theme preset for night time usage",
|
||||
"i18nkey": "theme.appearance.preset.dark",
|
||||
"version": "1.0.0",
|
||||
"themeScheme": "dark",
|
||||
"grayscale": false,
|
||||
"colourWeakness": false,
|
||||
"recommendColor": false,
|
||||
"themeColor": "#409eff",
|
||||
"otherColor": {
|
||||
"info": "#2080f0",
|
||||
"success": "#52c41a",
|
||||
"warning": "#faad14",
|
||||
"error": "#f5222d"
|
||||
},
|
||||
"isInfoFollowPrimary": true,
|
||||
"resetCacheStrategy": "close",
|
||||
"layout": {
|
||||
"mode": "vertical",
|
||||
"scrollMode": "content"
|
||||
},
|
||||
"page": {
|
||||
"animate": true,
|
||||
"animateMode": "fade-slide"
|
||||
},
|
||||
"header": {
|
||||
"height": 56,
|
||||
"breadcrumb": {
|
||||
"visible": true,
|
||||
"showIcon": true
|
||||
},
|
||||
"multilingual": {
|
||||
"visible": true
|
||||
},
|
||||
"globalSearch": {
|
||||
"visible": true
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"visible": true,
|
||||
"cache": true,
|
||||
"height": 44,
|
||||
"mode": "chrome"
|
||||
},
|
||||
"fixedHeaderAndTab": true,
|
||||
"sider": {
|
||||
"inverted": true,
|
||||
"width": 220,
|
||||
"collapsedWidth": 64,
|
||||
"mixWidth": 90,
|
||||
"mixCollapsedWidth": 64,
|
||||
"mixChildMenuWidth": 200
|
||||
},
|
||||
"footer": {
|
||||
"visible": true,
|
||||
"fixed": false,
|
||||
"height": 48,
|
||||
"right": true
|
||||
},
|
||||
"watermark": {
|
||||
"visible": false,
|
||||
"text": "SoybeanAdmin",
|
||||
"enableUserName": false,
|
||||
"enableTime": false,
|
||||
"timeFormat": "YYYY-MM-DD HH:mm"
|
||||
},
|
||||
"tokens": {
|
||||
"light": {
|
||||
"colors": {
|
||||
"container": "rgb(255, 255, 255)",
|
||||
"layout": "rgb(247, 250, 252)",
|
||||
"inverted": "rgb(0, 20, 40)",
|
||||
"base-text": "rgb(31, 31, 31)"
|
||||
},
|
||||
"boxShadow": {
|
||||
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
|
||||
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
|
||||
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"colors": {
|
||||
"container": "rgb(28, 28, 28)",
|
||||
"layout": "rgb(18, 18, 18)",
|
||||
"base-text": "rgb(224, 224, 224)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
src/theme/preset/default.json
Normal file
90
src/theme/preset/default.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "default",
|
||||
"desc": "Default theme preset with balanced settings",
|
||||
"i18nkey": "theme.appearance.preset.default",
|
||||
"version": "1.0.0",
|
||||
"themeScheme": "light",
|
||||
"grayscale": false,
|
||||
"colourWeakness": false,
|
||||
"recommendColor": false,
|
||||
"themeColor": "#646cff",
|
||||
"otherColor": {
|
||||
"info": "#2080f0",
|
||||
"success": "#52c41a",
|
||||
"warning": "#faad14",
|
||||
"error": "#f5222d"
|
||||
},
|
||||
"isInfoFollowPrimary": true,
|
||||
"resetCacheStrategy": "close",
|
||||
"layout": {
|
||||
"mode": "vertical",
|
||||
"scrollMode": "content"
|
||||
},
|
||||
"page": {
|
||||
"animate": true,
|
||||
"animateMode": "fade-slide"
|
||||
},
|
||||
"header": {
|
||||
"height": 56,
|
||||
"breadcrumb": {
|
||||
"visible": true,
|
||||
"showIcon": true
|
||||
},
|
||||
"multilingual": {
|
||||
"visible": true
|
||||
},
|
||||
"globalSearch": {
|
||||
"visible": true
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"visible": true,
|
||||
"cache": true,
|
||||
"height": 44,
|
||||
"mode": "chrome"
|
||||
},
|
||||
"fixedHeaderAndTab": true,
|
||||
"sider": {
|
||||
"inverted": false,
|
||||
"width": 220,
|
||||
"collapsedWidth": 64,
|
||||
"mixWidth": 90,
|
||||
"mixCollapsedWidth": 64,
|
||||
"mixChildMenuWidth": 200
|
||||
},
|
||||
"footer": {
|
||||
"visible": true,
|
||||
"fixed": false,
|
||||
"height": 48,
|
||||
"right": true
|
||||
},
|
||||
"watermark": {
|
||||
"visible": false,
|
||||
"text": "SoybeanAdmin",
|
||||
"enableUserName": false,
|
||||
"enableTime": false,
|
||||
"timeFormat": "YYYY-MM-DD HH:mm"
|
||||
},
|
||||
"tokens": {
|
||||
"light": {
|
||||
"colors": {
|
||||
"container": "rgb(255, 255, 255)",
|
||||
"layout": "rgb(247, 250, 252)",
|
||||
"inverted": "rgb(0, 20, 40)",
|
||||
"base-text": "rgb(31, 31, 31)"
|
||||
},
|
||||
"boxShadow": {
|
||||
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
|
||||
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
|
||||
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"colors": {
|
||||
"container": "rgb(28, 28, 28)",
|
||||
"layout": "rgb(18, 18, 18)",
|
||||
"base-text": "rgb(224, 224, 224)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
error: '#f5222d'
|
||||
},
|
||||
isInfoFollowPrimary: true,
|
||||
resetCacheStrategy: 'close',
|
||||
resetCacheStrategy: 'refresh',
|
||||
layout: {
|
||||
mode: 'vertical',
|
||||
scrollMode: 'content'
|
||||
|
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[];
|
||||
};
|
||||
}
|
||||
}
|
275
src/typings/app.d.ts
vendored
275
src/typings/app.d.ts
vendored
@ -367,6 +367,7 @@ declare namespace App {
|
||||
appearance: string;
|
||||
layout: string;
|
||||
general: string;
|
||||
preset: string;
|
||||
};
|
||||
appearance: {
|
||||
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
|
||||
@ -378,6 +379,17 @@ declare namespace App {
|
||||
} & Theme.ThemeColor;
|
||||
recommendColor: string;
|
||||
recommendColorDesc: string;
|
||||
preset: {
|
||||
title: string;
|
||||
apply: string;
|
||||
applySuccess: string;
|
||||
[key: string]:
|
||||
| {
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
| string;
|
||||
};
|
||||
};
|
||||
layout: {
|
||||
layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string> & {
|
||||
@ -387,6 +399,7 @@ declare namespace App {
|
||||
title: string;
|
||||
visible: string;
|
||||
cache: string;
|
||||
cacheTip: string;
|
||||
height: string;
|
||||
mode: { title: string } & Record<UnionKey.ThemeTabMode, string>;
|
||||
};
|
||||
@ -416,7 +429,7 @@ declare namespace App {
|
||||
};
|
||||
content: {
|
||||
title: string;
|
||||
scrollMode: { title: string } & Record<UnionKey.ThemeScrollMode, string>;
|
||||
scrollMode: { title: string; tip: string } & Record<UnionKey.ThemeScrollMode, string>;
|
||||
page: {
|
||||
animate: string;
|
||||
mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
|
||||
@ -499,6 +512,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 +553,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;
|
||||
|
53
src/typings/components.d.ts
vendored
53
src/typings/components.d.ts
vendored
@ -12,29 +12,61 @@ 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']
|
||||
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
|
||||
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['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']
|
||||
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']
|
||||
IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['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']
|
||||
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
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']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
@ -42,6 +74,7 @@ declare module 'vue' {
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
@ -56,17 +89,36 @@ 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']
|
||||
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']
|
||||
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 +129,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"
|
||||
>;
|
||||
|
||||
/**
|
||||
|
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>
|
155
src/views/plugin/charts/antv/modules/antv-g6-flow.ts
Normal file
155
src/views/plugin/charts/antv/modules/antv-g6-flow.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { Graph } from '@antv/g6';
|
||||
import type { CustomBehaviorOption, IPointerEvent } from '@antv/g6';
|
||||
import type { Canvas } from '@antv/g6/lib/runtime/canvas';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { getNodeIcon, nodeStatus } from './status';
|
||||
import type { CustomEdgeData, CustomGraphData, CustomNodeData } from './types';
|
||||
|
||||
interface AntFlowConfig {
|
||||
container: string | HTMLElement | Canvas;
|
||||
data: CustomGraphData;
|
||||
behaviors?: CustomBehaviorOption[];
|
||||
autoFit?: 'view' | 'center';
|
||||
}
|
||||
|
||||
export function useAntFlow(config: AntFlowConfig) {
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const baseColor = 'rgb(158 163 171)';
|
||||
|
||||
const { container, autoFit = 'center', data, behaviors = [] } = config;
|
||||
|
||||
const graph = new Graph({
|
||||
container,
|
||||
animation: false,
|
||||
padding: 16,
|
||||
theme: 'light',
|
||||
autoFit,
|
||||
data,
|
||||
node: {
|
||||
type: 'rect',
|
||||
|
||||
style: (node: CustomNodeData) => {
|
||||
const iconS = getNodeIcon(node);
|
||||
let labelFill = '#000000';
|
||||
if (node.taskState === 'NOT_STARTED') {
|
||||
labelFill = '#787878';
|
||||
}
|
||||
|
||||
return {
|
||||
labelText: node.name as string,
|
||||
size: [120, 26],
|
||||
radius: 99,
|
||||
fill: '#FFFFFF',
|
||||
stroke: node.isDeleted ? themeStore.otherColor.error : baseColor,
|
||||
lineDash: node.isDeleted ? 4 : 0,
|
||||
lineWidth: 1,
|
||||
labelFill,
|
||||
labelX: 2,
|
||||
labelY: 2,
|
||||
labelTextBaseline: 'middle',
|
||||
labelTextAlign: 'center',
|
||||
labelLineHeight: 13,
|
||||
labelWordWrap: true,
|
||||
labelMaxWidth: 72,
|
||||
iconSrc: iconS,
|
||||
iconWidth: 16,
|
||||
iconHeight: 16,
|
||||
iconX: -45,
|
||||
labelFontSize: 12,
|
||||
labelPlacement: 'center',
|
||||
badgeLineWidth: 6,
|
||||
badgeFontSize: 8,
|
||||
badges: [
|
||||
{ text: '延期', placement: 'top', offsetY: -11, visibility: node.isDelayed ? 'visible' : 'hidden' },
|
||||
{ text: '已删除', placement: 'bottom', offsetY: 11, visibility: node.isDeleted ? 'visible' : 'hidden' }
|
||||
],
|
||||
badgePalette: [themeStore.otherColor.error, themeStore.otherColor.error],
|
||||
ports: [{ placement: 'left' }, { placement: 'right' }]
|
||||
};
|
||||
},
|
||||
state: {
|
||||
selected: {
|
||||
lineWidth: 2,
|
||||
stroke: themeStore.themeColor,
|
||||
labelFill: themeStore.themeColor,
|
||||
halo: true,
|
||||
haloStroke: themeStore.themeColor,
|
||||
haloLineWidth: 6
|
||||
},
|
||||
active: (node: CustomNodeData) => ({
|
||||
halo: true,
|
||||
haloStroke: node.isDeleted ? themeStore.otherColor.error : themeStore.themeColor,
|
||||
haloLineWidth: 6,
|
||||
zIndex: 2
|
||||
})
|
||||
}
|
||||
},
|
||||
edge: {
|
||||
type: 'cubic-horizontal',
|
||||
style: (node: CustomEdgeData) => ({
|
||||
curveOffset: 10,
|
||||
curvePosition: 0.5,
|
||||
stroke: node.isDeleted ? themeStore.otherColor.error : baseColor,
|
||||
lineDash: node.isDeleted ? 4 : 0
|
||||
}),
|
||||
state: {
|
||||
active: (node: CustomEdgeData) => ({
|
||||
lineWidth: 2,
|
||||
stroke: node.isDeleted ? themeStore.otherColor.error : themeStore.themeColor,
|
||||
halo: true,
|
||||
haloStroke: node.isDeleted ? themeStore.otherColor.error : themeStore.themeColor,
|
||||
haloLineWidth: 6,
|
||||
zIndex: 2
|
||||
})
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
type: 'antv-dagre',
|
||||
rankdir: 'LR',
|
||||
ranksep: 20,
|
||||
nodesep: -20,
|
||||
controlPoints: true
|
||||
},
|
||||
behaviors: [
|
||||
{
|
||||
key: 'hover-activate',
|
||||
type: 'hover-activate',
|
||||
degree: 1,
|
||||
direction: 'both'
|
||||
},
|
||||
'drag-canvas',
|
||||
...behaviors
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
type: 'tooltip',
|
||||
enable: (event: IPointerEvent) => event.targetType === 'node',
|
||||
getContent: (_event: IPointerEvent, items?: CustomNodeData[]) => {
|
||||
let result = '<div style="display: flex; flex-direction: column; gap: 8px;">';
|
||||
|
||||
// 弹出提示可以自定义各种内容,但是这里很奇怪,有的class不跟随unocss的样式
|
||||
items?.forEach(item => {
|
||||
result += `<h3 style="display: flex; align-items: center; gap: 8px;">${item.name}</h3>`;
|
||||
result += `<div style="display: flex;"><b>状态:</b><div style="display: flex; gap: 4px;"><img src="${getNodeIcon(item)}" /><span style="font-weight: 400 !important;">${nodeStatus[item.status as keyof typeof nodeStatus].type}</span></div></div>`;
|
||||
|
||||
result += `<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 32px; row-gap: 4px;">`;
|
||||
result += `<div style="display: flex; flex-direction: column;"><div style="color: rgb(156 163 175);">预计开始</div><div style="font-weight: 700;">${item.startDate || '-'}</div></div>`;
|
||||
result += `<div style="display: flex; flex-direction: column;"><div style="color: rgb(156 163 175);">预计结束</div><div style="font-weight: 700;">${item.endDate || '-'}</div></div>`;
|
||||
result += `<div style="display: flex; flex-direction: column;"><div style="color: rgb(156 163 175);">实际开始</div><div style="font-weight: 700;">${item.actualStartDate || '-'}</div></div>`;
|
||||
result += `<div style="display: flex; flex-direction: column;"><div style="color: rgb(156 163 175);">实际结束</div><div style="font-weight: 700;">${item.actualEndDate || '-'}</div></div>`;
|
||||
result += `</div>`;
|
||||
});
|
||||
|
||||
result += '</div>';
|
||||
return result;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
graph.render();
|
||||
|
||||
return {
|
||||
graph
|
||||
};
|
||||
}
|
96
src/views/plugin/charts/antv/modules/status.ts
Normal file
96
src/views/plugin/charts/antv/modules/status.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { h } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import type { TagProps } from 'naive-ui';
|
||||
import type { CustomNodeData, NodeStatus } from './types';
|
||||
|
||||
interface NodeStatusConfig {
|
||||
type: string;
|
||||
color: string;
|
||||
textColor: string;
|
||||
base64: string;
|
||||
flag64: string;
|
||||
}
|
||||
|
||||
export const nodeStatus: Record<NodeStatus, NodeStatusConfig> = {
|
||||
MILESTONE: {
|
||||
type: '里程碑',
|
||||
color: '#5b5b5b',
|
||||
textColor: '',
|
||||
base64: '',
|
||||
flag64: ''
|
||||
},
|
||||
NOT_STARTED: {
|
||||
type: '未开始',
|
||||
color: '#CCCDD0',
|
||||
textColor: '#5b5b5b',
|
||||
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0NERDAiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
|
||||
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0NERDAiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
|
||||
},
|
||||
DELAYED: {
|
||||
type: '已延期',
|
||||
color: '#B81111',
|
||||
textColor: '#dccbcb',
|
||||
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNCODExMTEiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
|
||||
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNCODExMTEiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
|
||||
},
|
||||
PAUSED: {
|
||||
type: '已暂停',
|
||||
color: '#0E42D2',
|
||||
textColor: '#dae0f0',
|
||||
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMwRTQyRDIiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
|
||||
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMwRTQyRDIiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
|
||||
},
|
||||
IN_PROGRESS: {
|
||||
type: '进行中',
|
||||
color: '#E1BE0D',
|
||||
textColor: '#4f4304',
|
||||
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNFMUJFMEQiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
|
||||
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNFMUJFMEQiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
|
||||
},
|
||||
COMPLETED: {
|
||||
type: '已完成',
|
||||
color: '#33C73D',
|
||||
textColor: '#084e0c',
|
||||
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMzM0M3M0QiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
|
||||
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMzM0M3M0QiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
|
||||
},
|
||||
COMPLETED_EARLY: {
|
||||
type: '提前完成',
|
||||
color: '#CCFF99',
|
||||
textColor: '#42681d',
|
||||
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0ZGOTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
|
||||
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0ZGOTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
|
||||
},
|
||||
COMPLETED_LATE: {
|
||||
type: '延期完成',
|
||||
color: '#CC6699',
|
||||
textColor: '#4b092a',
|
||||
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQzY2OTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
|
||||
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQzY2OTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
|
||||
}
|
||||
};
|
||||
|
||||
export function getNodeIcon(node: CustomNodeData) {
|
||||
if (!node.status) return '';
|
||||
|
||||
const type = node.milestone ? 'flag64' : 'base64';
|
||||
|
||||
return nodeStatus[node.status][type];
|
||||
}
|
||||
|
||||
export function getNodeStatusTag(state: NodeStatus, tagProperty?: TagProps) {
|
||||
const { textColor, color, type } = nodeStatus[state] || {};
|
||||
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
color: { textColor, color },
|
||||
bordered: false,
|
||||
size: 'small',
|
||||
...tagProperty
|
||||
},
|
||||
{
|
||||
default: () => type
|
||||
}
|
||||
);
|
||||
}
|
28
src/views/plugin/charts/antv/modules/types.ts
Normal file
28
src/views/plugin/charts/antv/modules/types.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { EdgeData, GraphData, NodeData } from '@antv/g6';
|
||||
|
||||
export type NodeStatus =
|
||||
| 'MILESTONE'
|
||||
| 'NOT_STARTED'
|
||||
| 'DELAYED'
|
||||
| 'PAUSED'
|
||||
| 'IN_PROGRESS'
|
||||
| 'COMPLETED'
|
||||
| 'COMPLETED_EARLY'
|
||||
| 'COMPLETED_LATE';
|
||||
|
||||
export interface CustomNodeData extends NodeData {
|
||||
isDelayed?: boolean;
|
||||
isDeleted?: boolean;
|
||||
milestone?: boolean;
|
||||
status?: NodeStatus;
|
||||
}
|
||||
|
||||
export interface CustomEdgeData extends EdgeData {
|
||||
isDelayed?: boolean;
|
||||
isDeleted?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomGraphData extends GraphData {
|
||||
nodes: CustomNodeData[];
|
||||
edges: CustomEdgeData[];
|
||||
}
|
706
src/views/plugin/charts/echarts/data.ts
Normal file
706
src/views/plugin/charts/echarts/data.ts
Normal file
@ -0,0 +1,706 @@
|
||||
import { graphic } from 'echarts';
|
||||
import type { ScatterSeriesOption } from 'echarts/charts';
|
||||
import type { SingleAxisComponentOption, TitleComponentOption } from 'echarts/components';
|
||||
import type { ECOption } from '@/hooks/common/echarts';
|
||||
|
||||
export const pieOptions: ECOption = {
|
||||
legend: {},
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
mark: { show: true },
|
||||
dataView: { show: true, readOnly: false },
|
||||
restore: { show: true },
|
||||
saveAsImage: { show: true }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Nightingale Chart',
|
||||
type: 'pie',
|
||||
radius: [50, 150],
|
||||
center: ['50%', '50%'],
|
||||
roseType: 'area',
|
||||
itemStyle: {
|
||||
borderRadius: 8
|
||||
},
|
||||
data: [
|
||||
{ value: 40, name: 'rose 1' },
|
||||
{ value: 38, name: 'rose 2' },
|
||||
{ value: 32, name: 'rose 3' },
|
||||
{ value: 30, name: 'rose 4' },
|
||||
{ value: 28, name: 'rose 5' },
|
||||
{ value: 26, name: 'rose 6' },
|
||||
{ value: 22, name: 'rose 7' },
|
||||
{ value: 18, name: 'rose 8' }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const lineOptions: ECOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: 'Stacked Line'
|
||||
},
|
||||
legend: {
|
||||
data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {}
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
color: '#37a2da',
|
||||
name: 'Email',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#37a2da'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [120, 132, 101, 134, 90, 230, 210]
|
||||
},
|
||||
{
|
||||
color: '#9fe6b8',
|
||||
name: 'Union Ads',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#9fe6b8'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [220, 182, 191, 234, 290, 330, 310]
|
||||
},
|
||||
{
|
||||
color: '#fedb5c',
|
||||
name: 'Video Ads',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#fedb5c'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [150, 232, 201, 154, 190, 330, 410]
|
||||
},
|
||||
{
|
||||
color: '#fb7293',
|
||||
name: 'Direct',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#fb7293'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [320, 332, 301, 334, 390, 330, 320]
|
||||
},
|
||||
{
|
||||
color: '#e7bcf3',
|
||||
name: 'Search Engine',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#e7bcf3'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [820, 932, 901, 934, 1290, 1330, 1320]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const barOptions: ECOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [120, 200, 150, 80, 70, 110, 130],
|
||||
type: 'bar',
|
||||
color: '#8378ea',
|
||||
showBackground: true,
|
||||
barGap: 100,
|
||||
itemStyle: {
|
||||
borderRadius: [40, 40, 0, 0]
|
||||
},
|
||||
backgroundStyle: {
|
||||
color: 'rgba(180, 180, 180, 0.2)'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export function getPictorialBarOption(): ECOption {
|
||||
const category: string[] = [];
|
||||
let dottedBase = Number(new Date());
|
||||
const lineData: number[] = [];
|
||||
const barData: number[] = [];
|
||||
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
const date = new Date((dottedBase += 3600 * 24 * 1000));
|
||||
category.push([date.getFullYear(), date.getMonth() + 1, date.getDate()].join('-'));
|
||||
const b = Math.random() * 200;
|
||||
const d = Math.random() * 200;
|
||||
barData.push(b);
|
||||
lineData.push(d + b);
|
||||
}
|
||||
|
||||
const options: ECOption = {
|
||||
backgroundColor: '#0f375f',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['line', 'bar'],
|
||||
textStyle: {
|
||||
color: '#ccc'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
data: category,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#ccc'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
splitLine: { show: false },
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#ccc'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'line',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showAllSymbol: true,
|
||||
symbol: 'emptyCircle',
|
||||
symbolSize: 15,
|
||||
data: lineData
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
type: 'bar',
|
||||
barWidth: 10,
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#14c8d4' },
|
||||
{ offset: 1, color: '#43eec6' }
|
||||
])
|
||||
},
|
||||
data: barData
|
||||
},
|
||||
{
|
||||
name: 'line',
|
||||
type: 'bar',
|
||||
barGap: '-100%',
|
||||
barWidth: 10,
|
||||
itemStyle: {
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(20,200,212,0.5)' },
|
||||
{ offset: 0.2, color: 'rgba(20,200,212,0.2)' },
|
||||
{ offset: 1, color: 'rgba(20,200,212,0)' }
|
||||
])
|
||||
},
|
||||
z: -12,
|
||||
data: lineData
|
||||
},
|
||||
{
|
||||
name: 'dotted',
|
||||
type: 'pictorialBar',
|
||||
symbol: 'rect',
|
||||
itemStyle: {
|
||||
color: '#0f375f'
|
||||
},
|
||||
symbolRepeat: true,
|
||||
symbolSize: [12, 4],
|
||||
symbolMargin: 1,
|
||||
z: -10,
|
||||
data: lineData
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function getScatterOption() {
|
||||
// prettier-ignore
|
||||
const hours = ['12a', '1a', '2a', '3a', '4a', '5a', '6a', '7a', '8a', '9a','10a','11a', '12p', '1p', '2p', '3p', '4p', '5p', '6p', '7p', '8p', '9p', '10p', '11p'];
|
||||
|
||||
// prettier-ignore
|
||||
const days = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday'];
|
||||
|
||||
// prettier-ignore
|
||||
const data: [number, number, number][] = [[0,0,5],[0,1,1],[0,2,0],[0,3,0],[0,4,0],[0,5,0],[0,6,0],[0,7,0],[0,8,0],[0,9,0],[0,10,0],[0,11,2],[0,12,4],[0,13,1],[0,14,1],[0,15,3],[0,16,4],[0,17,6],[0,18,4],[0,19,4],[0,20,3],[0,21,3],[0,22,2],[0,23,5],[1,0,7],[1,1,0],[1,2,0],[1,3,0],[1,4,0],[1,5,0],[1,6,0],[1,7,0],[1,8,0],[1,9,0],[1,10,5],[1,11,2],[1,12,2],[1,13,6],[1,14,9],[1,15,11],[1,16,6],[1,17,7],[1,18,8],[1,19,12],[1,20,5],[1,21,5],[1,22,7],[1,23,2],[2,0,1],[2,1,1],[2,2,0],[2,3,0],[2,4,0],[2,5,0],[2,6,0],[2,7,0],[2,8,0],[2,9,0],[2,10,3],[2,11,2],[2,12,1],[2,13,9],[2,14,8],[2,15,10],[2,16,6],[2,17,5],[2,18,5],[2,19,5],[2,20,7],[2,21,4],[2,22,2],[2,23,4],[3,0,7],[3,1,3],[3,2,0],[3,3,0],[3,4,0],[3,5,0],[3,6,0],[3,7,0],[3,8,1],[3,9,0],[3,10,5],[3,11,4],[3,12,7],[3,13,14],[3,14,13],[3,15,12],[3,16,9],[3,17,5],[3,18,5],[3,19,10],[3,20,6],[3,21,4],[3,22,4],[3,23,1],[4,0,1],[4,1,3],[4,2,0],[4,3,0],[4,4,0],[4,5,1],[4,6,0],[4,7,0],[4,8,0],[4,9,2],[4,10,4],[4,11,4],[4,12,2],[4,13,4],[4,14,4],[4,15,14],[4,16,12],[4,17,1],[4,18,8],[4,19,5],[4,20,3],[4,21,7],[4,22,3],[4,23,0],[5,0,2],[5,1,1],[5,2,0],[5,3,3],[5,4,0],[5,5,0],[5,6,0],[5,7,0],[5,8,2],[5,9,0],[5,10,4],[5,11,1],[5,12,5],[5,13,10],[5,14,5],[5,15,7],[5,16,11],[5,17,6],[5,18,0],[5,19,5],[5,20,3],[5,21,4],[5,22,2],[5,23,0],[6,0,1],[6,1,0],[6,2,0],[6,3,0],[6,4,0],[6,5,0],[6,6,0],[6,7,0],[6,8,0],[6,9,0],[6,10,1],[6,11,0],[6,12,2],[6,13,1],[6,14,3],[6,15,4],[6,16,0],[6,17,0],[6,18,0],[6,19,0],[6,20,1],[6,21,2],[6,22,2],[6,23,6]];
|
||||
|
||||
const title: TitleComponentOption[] = [];
|
||||
const singleAxis: SingleAxisComponentOption[] = [];
|
||||
const series: ScatterSeriesOption[] = [];
|
||||
|
||||
days.forEach((day, idx) => {
|
||||
title.push({
|
||||
textBaseline: 'middle',
|
||||
top: `${((idx + 0.5) * 100) / 7}%`,
|
||||
text: day
|
||||
});
|
||||
singleAxis.push({
|
||||
left: 150,
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: hours,
|
||||
top: `${(idx * 100) / 7 + 5}%`,
|
||||
height: `${100 / 7 - 10}%`,
|
||||
axisLabel: {
|
||||
interval: 2
|
||||
}
|
||||
});
|
||||
series.push({
|
||||
singleAxisIndex: idx,
|
||||
coordinateSystem: 'singleAxis',
|
||||
type: 'scatter',
|
||||
data: [],
|
||||
symbolSize(dataItem) {
|
||||
return dataItem[1] * 4;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
data.forEach(dataItem => {
|
||||
(series as any)[dataItem[0]].data.push([dataItem[1], dataItem[2]]);
|
||||
});
|
||||
|
||||
const option: ECOption = {
|
||||
tooltip: {
|
||||
position: 'top'
|
||||
},
|
||||
title,
|
||||
singleAxis,
|
||||
series: series as any
|
||||
};
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
export const radarOptions: ECOption = {
|
||||
title: {
|
||||
text: 'Multiple Radar'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
left: 'center',
|
||||
data: ['A Software', 'A Phone', 'Another Phone', 'Precipitation', 'Evaporation']
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
indicator: [
|
||||
{ name: 'Brand', max: 100 },
|
||||
{ name: 'Content', max: 100 },
|
||||
{ name: 'Usability', max: 100 },
|
||||
{ name: 'Function', max: 100 }
|
||||
],
|
||||
center: ['25%', '40%'],
|
||||
radius: 80
|
||||
},
|
||||
{
|
||||
indicator: [
|
||||
{ name: 'Look', max: 100 },
|
||||
{ name: 'Photo', max: 100 },
|
||||
{ name: 'System', max: 100 },
|
||||
{ name: 'Performance', max: 100 },
|
||||
{ name: 'Screen', max: 100 }
|
||||
],
|
||||
radius: 80,
|
||||
center: ['50%', '60%']
|
||||
},
|
||||
{
|
||||
indicator: (() => {
|
||||
const res = [];
|
||||
for (let i = 1; i <= 12; i += 1) {
|
||||
res.push({ name: `${i}月`, max: 100 });
|
||||
}
|
||||
return res;
|
||||
})(),
|
||||
center: ['75%', '40%'],
|
||||
radius: 80
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
areaStyle: {},
|
||||
data: [
|
||||
{
|
||||
value: [60, 73, 85, 40],
|
||||
name: 'A Software'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'radar',
|
||||
radarIndex: 1,
|
||||
areaStyle: {},
|
||||
data: [
|
||||
{
|
||||
value: [85, 90, 90, 95, 95],
|
||||
name: 'A Phone'
|
||||
},
|
||||
{
|
||||
value: [95, 80, 95, 90, 93],
|
||||
name: 'Another Phone'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'radar',
|
||||
radarIndex: 2,
|
||||
areaStyle: {},
|
||||
data: [
|
||||
{
|
||||
name: 'Precipitation',
|
||||
value: [2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 75.6, 82.2, 48.7, 18.8, 6.0, 2.3]
|
||||
},
|
||||
{
|
||||
name: 'Evaporation',
|
||||
value: [2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 35.6, 62.2, 32.6, 20.0, 6.4, 3.3]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const gaugeOptions: ECOption = {
|
||||
series: [
|
||||
{
|
||||
name: 'hour',
|
||||
type: 'gauge',
|
||||
startAngle: 90,
|
||||
endAngle: -270,
|
||||
min: 0,
|
||||
max: 12,
|
||||
splitNumber: 12,
|
||||
clockwise: true,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 15,
|
||||
color: [[1, 'rgba(0,0,0,0.7)']],
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
shadowBlur: 15
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 3,
|
||||
shadowOffsetX: 1,
|
||||
shadowOffsetY: 2
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
fontSize: 50,
|
||||
distance: 25,
|
||||
formatter(value) {
|
||||
if (value === 0) {
|
||||
return '';
|
||||
}
|
||||
return `${value}`;
|
||||
}
|
||||
},
|
||||
anchor: {
|
||||
show: true,
|
||||
icon: 'path://M532.8,70.8C532.8,70.8,532.8,70.8,532.8,70.8L532.8,70.8C532.7,70.8,532.8,70.8,532.8,70.8z M456.1,49.6c-2.2-6.2-8.1-10.6-15-10.6h-37.5v10.6h37.5l0,0c2.9,0,5.3,2.4,5.3,5.3c0,2.9-2.4,5.3-5.3,5.3v0h-22.5c-1.5,0.1-3,0.4-4.3,0.9c-4.5,1.6-8.1,5.2-9.7,9.8c-0.6,1.7-0.9,3.4-0.9,5.3v16h10.6v-16l0,0l0,0c0-2.7,2.1-5,4.7-5.3h10.3l10.4,21.2h11.8l-10.4-21.2h0c6.9,0,12.8-4.4,15-10.6c0.6-1.7,0.9-3.5,0.9-5.3C457,53,456.7,51.2,456.1,49.6z M388.9,92.1h11.3L381,39h-3.6h-11.3L346.8,92v0h11.3l3.9-10.7h7.3h7.7l3.9-10.6h-7.7h-7.3l7.7-21.2v0L388.9,92.1z M301,38.9h-10.6v53.1H301V70.8h28.4l3.7-10.6H301V38.9zM333.2,38.9v10.6v10.7v31.9h10.6V38.9H333.2z M249.5,81.4L249.5,81.4L249.5,81.4c-2.9,0-5.3-2.4-5.3-5.3h0V54.9h0l0,0c0-2.9,2.4-5.3,5.3-5.3l0,0l0,0h33.6l3.9-10.6h-37.5c-1.9,0-3.6,0.3-5.3,0.9c-4.5,1.6-8.1,5.2-9.7,9.7c-0.6,1.7-0.9,3.5-0.9,5.3l0,0v21.3c0,1.9,0.3,3.6,0.9,5.3c1.6,4.5,5.2,8.1,9.7,9.7c1.7,0.6,3.5,0.9,5.3,0.9h33.6l3.9-10.6H249.5z M176.8,38.9v10.6h49.6l3.9-10.6H176.8z M192.7,81.4L192.7,81.4L192.7,81.4c-2.9,0-5.3-2.4-5.3-5.3l0,0v-5.3h38.9l3.9-10.6h-53.4v10.6v5.3l0,0c0,1.9,0.3,3.6,0.9,5.3c1.6,4.5,5.2,8.1,9.7,9.7c1.7,0.6,3.4,0.9,5.3,0.9h23.4h10.2l3.9-10.6l0,0H192.7z M460.1,38.9v10.6h21.4v42.5h10.6V49.6h17.5l3.8-10.6H460.1z M541.6,68.2c-0.2,0.1-0.4,0.3-0.7,0.4C541.1,68.4,541.4,68.3,541.6,68.2L541.6,68.2z M554.3,60.2h-21.6v0l0,0c-2.9,0-5.3-2.4-5.3-5.3c0-2.9,2.4-5.3,5.3-5.3l0,0l0,0h33.6l3.8-10.6h-37.5l0,0c-6.9,0-12.8,4.4-15,10.6c-0.6,1.7-0.9,3.5-0.9,5.3c0,1.9,0.3,3.7,0.9,5.3c2.2,6.2,8.1,10.6,15,10.6h21.6l0,0c2.9,0,5.3,2.4,5.3,5.3c0,2.9-2.4,5.3-5.3,5.3l0,0h-37.5v10.6h37.5c6.9,0,12.8-4.4,15-10.6c0.6-1.7,0.9-3.5,0.9-5.3c0-1.9-0.3-3.7-0.9-5.3C567.2,64.6,561.3,60.2,554.3,60.2z',
|
||||
showAbove: false,
|
||||
offsetCenter: [0, '-35%'],
|
||||
size: 120,
|
||||
keepAspect: true,
|
||||
itemStyle: {
|
||||
color: '#707177'
|
||||
}
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
|
||||
width: 12,
|
||||
length: '55%',
|
||||
offsetCenter: [0, '8%'],
|
||||
itemStyle: {
|
||||
color: '#C0911F',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 8,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 4
|
||||
}
|
||||
},
|
||||
detail: {
|
||||
show: false
|
||||
},
|
||||
title: {
|
||||
offsetCenter: [0, '30%']
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'minute',
|
||||
type: 'gauge',
|
||||
startAngle: 90,
|
||||
endAngle: -270,
|
||||
min: 0,
|
||||
max: 60,
|
||||
clockwise: true,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
show: false
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
|
||||
width: 8,
|
||||
length: '70%',
|
||||
offsetCenter: [0, '8%'],
|
||||
itemStyle: {
|
||||
color: '#C0911F',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 8,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 4
|
||||
}
|
||||
},
|
||||
anchor: {
|
||||
show: true,
|
||||
size: 20,
|
||||
showAbove: false,
|
||||
itemStyle: {
|
||||
borderWidth: 15,
|
||||
borderColor: '#C0911F',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 8,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 4
|
||||
}
|
||||
},
|
||||
detail: {
|
||||
show: false
|
||||
},
|
||||
title: {
|
||||
offsetCenter: ['0%', '-40%']
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'second',
|
||||
type: 'gauge',
|
||||
startAngle: 90,
|
||||
endAngle: -270,
|
||||
min: 0,
|
||||
max: 60,
|
||||
animationEasingUpdate: 'bounceOut',
|
||||
clockwise: true,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
show: false
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
|
||||
width: 4,
|
||||
length: '85%',
|
||||
offsetCenter: [0, '8%'],
|
||||
itemStyle: {
|
||||
color: '#C0911F',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 8,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 4
|
||||
}
|
||||
},
|
||||
anchor: {
|
||||
show: true,
|
||||
size: 15,
|
||||
showAbove: true,
|
||||
itemStyle: {
|
||||
color: '#C0911F',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 8,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 4
|
||||
}
|
||||
},
|
||||
detail: {
|
||||
show: false
|
||||
},
|
||||
title: {
|
||||
offsetCenter: ['0%', '-40%']
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
91
src/views/plugin/charts/echarts/index.vue
Normal file
91
src/views/plugin/charts/echarts/index.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted } from 'vue';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
import {
|
||||
barOptions,
|
||||
gaugeOptions,
|
||||
getPictorialBarOption,
|
||||
getScatterOption,
|
||||
lineOptions,
|
||||
pieOptions,
|
||||
radarOptions
|
||||
} from './data';
|
||||
|
||||
const { domRef: pieRef } = useEcharts(() => pieOptions, { onRender() {} });
|
||||
const { domRef: lineRef } = useEcharts(() => lineOptions, { onRender() {} });
|
||||
const { domRef: barRef } = useEcharts(() => barOptions, { onRender() {} });
|
||||
const { domRef: pictorialBarRef } = useEcharts(() => getPictorialBarOption(), { onRender() {} });
|
||||
const { domRef: radarRef } = useEcharts(() => radarOptions, { onRender() {} });
|
||||
const { domRef: scatterRef } = useEcharts(() => getScatterOption(), { onRender() {} });
|
||||
const { domRef: gaugeRef, setOptions: setGaugeOptions } = useEcharts(() => gaugeOptions, { onRender() {} });
|
||||
|
||||
let intervalId: NodeJS.Timeout;
|
||||
|
||||
function initGaugeChart() {
|
||||
intervalId = setInterval(() => {
|
||||
const date = new Date();
|
||||
const second = date.getSeconds();
|
||||
const minute = date.getMinutes() + second / 60;
|
||||
const hour = (date.getHours() % 12) + minute / 60;
|
||||
|
||||
setGaugeOptions({
|
||||
animationDurationUpdate: 300,
|
||||
series: [
|
||||
{
|
||||
name: 'hour',
|
||||
animation: hour !== 0,
|
||||
data: [{ value: hour }]
|
||||
},
|
||||
{
|
||||
name: 'minute',
|
||||
animation: minute !== 0,
|
||||
data: [{ value: minute }]
|
||||
},
|
||||
{
|
||||
animation: second !== 0,
|
||||
name: 'second',
|
||||
data: [{ value: second }]
|
||||
}
|
||||
]
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function clearGaugeChart() {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
|
||||
initGaugeChart();
|
||||
|
||||
onUnmounted(() => {
|
||||
clearGaugeChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<div ref="pieRef" class="h-400px" />
|
||||
</NCard>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<div ref="lineRef" class="h-400px" />
|
||||
</NCard>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<div ref="barRef" class="h-400px" />
|
||||
</NCard>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<div ref="radarRef" class="h-400px"></div>
|
||||
</NCard>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<div ref="scatterRef" class="h-600px"></div>
|
||||
</NCard>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<div ref="pictorialBarRef" class="h-600px" />
|
||||
</NCard>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<div ref="gaugeRef" class="h-640px" />
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
872
src/views/plugin/charts/vchart/data.ts
Normal file
872
src/views/plugin/charts/vchart/data.ts
Normal file
@ -0,0 +1,872 @@
|
||||
import type {
|
||||
IAnimationConfig,
|
||||
IAreaChartSpec,
|
||||
IBarChartSpec,
|
||||
ICircularProgressChartSpec,
|
||||
IHistogramChartSpec,
|
||||
IIndicatorSpec,
|
||||
ILiquidChartSpec,
|
||||
IWordCloudChartSpec
|
||||
} from '@visactor/vchart';
|
||||
|
||||
export const shapeWordCloudSpec: IWordCloudChartSpec = {
|
||||
type: 'wordCloud',
|
||||
maskShape: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/log.jpeg',
|
||||
nameField: 'challenge_name',
|
||||
valueField: 'sum_count',
|
||||
seriesField: 'challenge_name',
|
||||
data: [
|
||||
{
|
||||
name: 'data',
|
||||
values: [
|
||||
{
|
||||
challenge_name: '刘浩存',
|
||||
sum_count: 957
|
||||
},
|
||||
{
|
||||
challenge_name: '刘昊然',
|
||||
sum_count: 942
|
||||
},
|
||||
{
|
||||
challenge_name: '喜欢',
|
||||
sum_count: 842
|
||||
},
|
||||
{
|
||||
challenge_name: '真的',
|
||||
sum_count: 828
|
||||
},
|
||||
{
|
||||
challenge_name: '四海',
|
||||
sum_count: 665
|
||||
},
|
||||
{
|
||||
challenge_name: '好看',
|
||||
sum_count: 627
|
||||
},
|
||||
{
|
||||
challenge_name: '评论',
|
||||
sum_count: 574
|
||||
},
|
||||
{
|
||||
challenge_name: '好像',
|
||||
sum_count: 564
|
||||
},
|
||||
{
|
||||
challenge_name: '沈腾',
|
||||
sum_count: 554
|
||||
},
|
||||
{
|
||||
challenge_name: '不像',
|
||||
sum_count: 540
|
||||
},
|
||||
{
|
||||
challenge_name: '多少钱',
|
||||
sum_count: 513
|
||||
},
|
||||
{
|
||||
challenge_name: '韩寒',
|
||||
sum_count: 513
|
||||
},
|
||||
{
|
||||
challenge_name: '不知道',
|
||||
sum_count: 499
|
||||
},
|
||||
{
|
||||
challenge_name: '感觉',
|
||||
sum_count: 499
|
||||
},
|
||||
{
|
||||
challenge_name: '尹正',
|
||||
sum_count: 495
|
||||
},
|
||||
{
|
||||
challenge_name: '不看',
|
||||
sum_count: 487
|
||||
},
|
||||
{
|
||||
challenge_name: '奥特之父',
|
||||
sum_count: 484
|
||||
},
|
||||
{
|
||||
challenge_name: '阿姨',
|
||||
sum_count: 482
|
||||
},
|
||||
{
|
||||
challenge_name: '支持',
|
||||
sum_count: 482
|
||||
},
|
||||
{
|
||||
challenge_name: '父母',
|
||||
sum_count: 479
|
||||
},
|
||||
{
|
||||
challenge_name: '一条',
|
||||
sum_count: 462
|
||||
},
|
||||
{
|
||||
challenge_name: '女主',
|
||||
sum_count: 456
|
||||
},
|
||||
{
|
||||
challenge_name: '确实',
|
||||
sum_count: 456
|
||||
},
|
||||
{
|
||||
challenge_name: '票房',
|
||||
sum_count: 456
|
||||
},
|
||||
{
|
||||
challenge_name: '无语',
|
||||
sum_count: 443
|
||||
},
|
||||
{
|
||||
challenge_name: '干干净净',
|
||||
sum_count: 443
|
||||
},
|
||||
{
|
||||
challenge_name: '为啥',
|
||||
sum_count: 426
|
||||
},
|
||||
{
|
||||
challenge_name: '爱情',
|
||||
sum_count: 425
|
||||
},
|
||||
{
|
||||
challenge_name: '喜剧',
|
||||
sum_count: 422
|
||||
},
|
||||
{
|
||||
challenge_name: '春节',
|
||||
sum_count: 414
|
||||
},
|
||||
{
|
||||
challenge_name: '剧情',
|
||||
sum_count: 414
|
||||
},
|
||||
{
|
||||
challenge_name: '人生',
|
||||
sum_count: 409
|
||||
},
|
||||
{
|
||||
challenge_name: '风格',
|
||||
sum_count: 408
|
||||
},
|
||||
{
|
||||
challenge_name: '演员',
|
||||
sum_count: 403
|
||||
},
|
||||
{
|
||||
challenge_name: '成长',
|
||||
sum_count: 403
|
||||
},
|
||||
{
|
||||
challenge_name: '玩意',
|
||||
sum_count: 402
|
||||
},
|
||||
{
|
||||
challenge_name: '文学',
|
||||
sum_count: 397
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const circularProgressTickSpec: ICircularProgressChartSpec & { indicator: IIndicatorSpec } = {
|
||||
type: 'circularProgress',
|
||||
data: [
|
||||
{
|
||||
id: 'id0',
|
||||
values: [
|
||||
{
|
||||
type: 'Tradition Industries',
|
||||
value: 0.795,
|
||||
text: '79.5%'
|
||||
},
|
||||
{
|
||||
type: 'Business Companies',
|
||||
value: 0.5,
|
||||
text: '50%'
|
||||
},
|
||||
{
|
||||
type: 'Customer-facing Companies',
|
||||
value: 0.25,
|
||||
text: '25%'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
color: ['rgb(255, 222, 0)', 'rgb(171, 205, 5)', 'rgb(0, 154, 68)'],
|
||||
valueField: 'value',
|
||||
categoryField: 'type',
|
||||
seriesField: 'type',
|
||||
radius: 0.8,
|
||||
innerRadius: 0.4,
|
||||
tickMask: {
|
||||
visible: true,
|
||||
angle: 10,
|
||||
offsetAngle: 0,
|
||||
forceAlign: true,
|
||||
style: {
|
||||
cornerRadius: 15
|
||||
}
|
||||
},
|
||||
axes: [
|
||||
{
|
||||
visible: false,
|
||||
type: 'linear',
|
||||
orient: 'angle'
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
type: 'band',
|
||||
orient: 'radius'
|
||||
}
|
||||
],
|
||||
indicator: {
|
||||
visible: true,
|
||||
trigger: 'hover',
|
||||
title: {
|
||||
visible: true,
|
||||
field: 'type',
|
||||
autoLimit: true,
|
||||
style: {
|
||||
fontSize: 20,
|
||||
fill: 'black'
|
||||
}
|
||||
},
|
||||
content: [
|
||||
{
|
||||
visible: true,
|
||||
field: 'text',
|
||||
style: {
|
||||
fontSize: 16,
|
||||
fill: 'gray'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
legends: {
|
||||
visible: true,
|
||||
orient: 'bottom',
|
||||
title: {
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const liquidChartSmartInvertSpec: ILiquidChartSpec & { indicator: IIndicatorSpec } = {
|
||||
type: 'liquid',
|
||||
valueField: 'value',
|
||||
data: {
|
||||
id: 'data',
|
||||
values: [
|
||||
{
|
||||
value: 0.8
|
||||
}
|
||||
]
|
||||
},
|
||||
maskShape: 'drop', // 水滴
|
||||
// maskShape: 'circle',
|
||||
// maskShape: 'star',
|
||||
indicatorSmartInvert: true,
|
||||
indicator: {
|
||||
visible: true,
|
||||
title: {
|
||||
visible: true,
|
||||
style: {
|
||||
text: '进度'
|
||||
}
|
||||
},
|
||||
content: [
|
||||
{
|
||||
visible: true,
|
||||
style: {
|
||||
fill: 'black',
|
||||
text: '80%'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
liquidBackground: {
|
||||
style: {
|
||||
fill: 'blue'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const goldenMedals: Record<number, any[]> = {
|
||||
2000: [
|
||||
{ country: 'USA', value: 37 },
|
||||
{ country: 'Russia', value: 32 },
|
||||
{ country: 'China', value: 28 },
|
||||
{ country: 'Australia', value: 16 },
|
||||
{ country: 'Germany', value: 13 },
|
||||
{ country: 'France', value: 13 },
|
||||
{ country: 'Italy', value: 13 },
|
||||
{ country: 'Netherlands', value: 12 },
|
||||
{ country: 'Cuba', value: 11 },
|
||||
{ country: 'U.K.', value: 11 }
|
||||
],
|
||||
2004: [
|
||||
{ country: 'USA', value: 36 },
|
||||
{ country: 'China', value: 32 },
|
||||
{ country: 'Russia', value: 28 },
|
||||
{ country: 'Australia', value: 17 },
|
||||
{ country: 'Japan', value: 16 },
|
||||
{ country: 'Germany', value: 13 },
|
||||
{ country: 'France', value: 11 },
|
||||
{ country: 'Italy', value: 10 },
|
||||
{ country: 'South Korea', value: 9 },
|
||||
{ country: 'U.K.', value: 9 }
|
||||
],
|
||||
2008: [
|
||||
{ country: 'China', value: 48 },
|
||||
{ country: 'USA', value: 36 },
|
||||
{ country: 'Russia', value: 24 },
|
||||
{ country: 'U.K.', value: 19 },
|
||||
{ country: 'Germany', value: 16 },
|
||||
{ country: 'Australia', value: 14 },
|
||||
{ country: 'South Korea', value: 13 },
|
||||
{ country: 'Japan', value: 9 },
|
||||
{ country: 'Italy', value: 8 },
|
||||
{ country: 'France', value: 7 }
|
||||
],
|
||||
2012: [
|
||||
{ country: 'USA', value: 46 },
|
||||
{ country: 'China', value: 39 },
|
||||
{ country: 'U.K.', value: 29 },
|
||||
{ country: 'Russia', value: 19 },
|
||||
{ country: 'South Korea', value: 13 },
|
||||
{ country: 'Germany', value: 11 },
|
||||
{ country: 'France', value: 11 },
|
||||
{ country: 'Australia', value: 8 },
|
||||
{ country: 'Italy', value: 8 },
|
||||
{ country: 'Hungary', value: 8 }
|
||||
],
|
||||
2016: [
|
||||
{ country: 'USA', value: 46 },
|
||||
{ country: 'U.K.', value: 27 },
|
||||
{ country: 'China', value: 26 },
|
||||
{ country: 'Russia', value: 19 },
|
||||
{ country: 'Germany', value: 17 },
|
||||
{ country: 'Japan', value: 12 },
|
||||
{ country: 'France', value: 10 },
|
||||
{ country: 'South Korea', value: 9 },
|
||||
{ country: 'Italy', value: 8 },
|
||||
{ country: 'Australia', value: 8 }
|
||||
],
|
||||
2020: [
|
||||
{ country: 'USA', value: 39 },
|
||||
{ country: 'China', value: 38 },
|
||||
{ country: 'Japan', value: 27 },
|
||||
{ country: 'U.K.', value: 22 },
|
||||
{ country: 'Russian Olympic Committee', value: 20 },
|
||||
{ country: 'Australia', value: 17 },
|
||||
{ country: 'Netherlands', value: 10 },
|
||||
{ country: 'France', value: 10 },
|
||||
{ country: 'Germany', value: 10 },
|
||||
{ country: 'Italy', value: 10 }
|
||||
]
|
||||
};
|
||||
|
||||
const colors = {
|
||||
China: '#d62728',
|
||||
USA: '#1664FF',
|
||||
Russia: '#B2CFFF',
|
||||
'U.K.': '#1AC6FF',
|
||||
Australia: '#94EFFF',
|
||||
Japan: '#FF8A00',
|
||||
Cuba: '#FFCE7A',
|
||||
Germany: '#3CC780',
|
||||
France: '#B9EDCD',
|
||||
Italy: '#7442D4',
|
||||
'South Korea': '#DDC5FA',
|
||||
'Russian Olympic Committee': '#B2CFFF',
|
||||
Netherlands: '#FFC400',
|
||||
Hungary: '#FAE878'
|
||||
};
|
||||
|
||||
const dataSpecs = Object.keys(goldenMedals).map(year => {
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
id: 'id',
|
||||
values: (goldenMedals[year as unknown as number] as any)
|
||||
.sort((a: any, b: any) => b.value - a.value)
|
||||
.map((v: any) => {
|
||||
return { ...v, fill: (colors as any)[v.country] };
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 'year',
|
||||
values: [{ year }]
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
const duration = 1000;
|
||||
const exchangeDuration = 600;
|
||||
|
||||
export const rankingBarSpec: IBarChartSpec = {
|
||||
type: 'bar',
|
||||
padding: {
|
||||
top: 12,
|
||||
right: 100,
|
||||
bottom: 12
|
||||
},
|
||||
data: dataSpecs[0].data,
|
||||
direction: 'horizontal',
|
||||
yField: 'country',
|
||||
xField: 'value',
|
||||
seriesField: 'country',
|
||||
bar: {
|
||||
style: {
|
||||
fill: (datum: any) => datum.fill
|
||||
}
|
||||
},
|
||||
axes: [
|
||||
{
|
||||
animation: true,
|
||||
orient: 'bottom',
|
||||
type: 'linear',
|
||||
visible: true,
|
||||
max: 50,
|
||||
grid: {
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
{
|
||||
animation: true,
|
||||
id: 'axis-left',
|
||||
orient: 'left',
|
||||
width: 130,
|
||||
tick: { visible: false },
|
||||
label: { visible: true },
|
||||
type: 'band'
|
||||
}
|
||||
],
|
||||
title: {
|
||||
visible: true,
|
||||
text: 'Top 10 Olympic Gold Medals by Country Since 2000'
|
||||
},
|
||||
animationUpdate: {
|
||||
bar: [
|
||||
{
|
||||
type: 'update',
|
||||
options: { excludeChannels: ['y'] },
|
||||
easing: 'linear',
|
||||
duration
|
||||
},
|
||||
{
|
||||
channel: ['y'],
|
||||
easing: 'circInOut',
|
||||
duration: exchangeDuration
|
||||
}
|
||||
],
|
||||
axis: {
|
||||
duration: exchangeDuration,
|
||||
easing: 'circInOut'
|
||||
}
|
||||
} as Record<string, IAnimationConfig>,
|
||||
animationEnter: {
|
||||
bar: [
|
||||
{
|
||||
type: 'moveIn',
|
||||
duration: exchangeDuration,
|
||||
easing: 'circInOut',
|
||||
options: {
|
||||
direction: 'y',
|
||||
orient: 'negative'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
animationExit: {
|
||||
bar: [
|
||||
{
|
||||
type: 'fadeOut',
|
||||
duration: exchangeDuration
|
||||
}
|
||||
]
|
||||
},
|
||||
customMark: [
|
||||
{
|
||||
type: 'text',
|
||||
dataId: 'year',
|
||||
style: {
|
||||
textBaseline: 'bottom',
|
||||
fontSize: 200,
|
||||
textAlign: 'right',
|
||||
fontFamily: 'PingFang SC',
|
||||
fontWeight: 600,
|
||||
text: (datum: any) => datum.year,
|
||||
x: (_datum: any, ctx: any) => {
|
||||
return ctx.vchart.getChart().getCanvasRect()?.width - 50;
|
||||
},
|
||||
y: (_datum: any, ctx: any) => {
|
||||
return ctx.vchart.getChart().getCanvasRect()?.height - 50;
|
||||
},
|
||||
fill: 'grey',
|
||||
fillOpacity: 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
player: {
|
||||
type: 'continuous',
|
||||
orient: 'bottom',
|
||||
auto: true,
|
||||
loop: true,
|
||||
dx: 80,
|
||||
position: 'middle',
|
||||
interval: duration,
|
||||
specs: dataSpecs,
|
||||
slider: {
|
||||
railStyle: {
|
||||
height: 6
|
||||
}
|
||||
},
|
||||
controller: {
|
||||
backward: {
|
||||
style: {
|
||||
size: 12
|
||||
}
|
||||
},
|
||||
forward: {
|
||||
style: {
|
||||
size: 12
|
||||
}
|
||||
},
|
||||
start: {
|
||||
order: 1,
|
||||
position: 'end'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const stackedDashAreaSpec: IAreaChartSpec = {
|
||||
type: 'area',
|
||||
data: {
|
||||
values: [
|
||||
{ month: 'Jan', country: 'Africa', value: 4229 },
|
||||
{ month: 'Jan', country: 'EU', value: 4376 },
|
||||
{ month: 'Jan', country: 'China', value: 3054 },
|
||||
{ month: 'Jan', country: 'USA', value: 12814 },
|
||||
{ month: 'Feb', country: 'Africa', value: 3932 },
|
||||
{ month: 'Feb', country: 'EU', value: 3987 },
|
||||
{ month: 'Feb', country: 'China', value: 5067 },
|
||||
{ month: 'Feb', country: 'USA', value: 13012 },
|
||||
{ month: 'Mar', country: 'Africa', value: 5221 },
|
||||
{ month: 'Mar', country: 'EU', value: 3574 },
|
||||
{ month: 'Mar', country: 'China', value: 7004 },
|
||||
{ month: 'Mar', country: 'USA', value: 11624 },
|
||||
{ month: 'Apr', country: 'Africa', value: 9256 },
|
||||
{ month: 'Apr', country: 'EU', value: 4376 },
|
||||
{ month: 'Apr', country: 'China', value: 9054 },
|
||||
{ month: 'Apr', country: 'USA', value: 8814 },
|
||||
{ month: 'May', country: 'Africa', value: 3308 },
|
||||
{ month: 'May', country: 'EU', value: 4572 },
|
||||
{ month: 'May', country: 'China', value: 12043 },
|
||||
{ month: 'May', country: 'USA', value: 12998 },
|
||||
{ month: 'Jun', country: 'Africa', value: 5432 },
|
||||
{ month: 'Jun', country: 'EU', value: 3417 },
|
||||
{ month: 'Jun', country: 'China', value: 15067 },
|
||||
{ month: 'Jun', country: 'USA', value: 12321 },
|
||||
{ month: 'Jul', country: 'Africa', value: 13701 },
|
||||
{ month: 'Jul', country: 'EU', value: 5231 },
|
||||
{ month: 'Jul', country: 'China', value: 10119 },
|
||||
{ month: 'Jul', country: 'USA', value: 10342 },
|
||||
{ month: 'Aug', country: 'Africa', value: 4008, forecast: true },
|
||||
{ month: 'Aug', country: 'EU', value: 4572, forecast: true },
|
||||
{ month: 'Aug', country: 'China', value: 12043, forecast: true },
|
||||
{ month: 'Aug', country: 'USA', value: 22998, forecast: true },
|
||||
{ month: 'Sept', country: 'Africa', value: 18712, forecast: true },
|
||||
{ month: 'Sept', country: 'EU', value: 6134, forecast: true },
|
||||
{ month: 'Sept', country: 'China', value: 10419, forecast: true },
|
||||
{ month: 'Sept', country: 'USA', value: 11261, forecast: true }
|
||||
]
|
||||
},
|
||||
stack: true,
|
||||
xField: 'month',
|
||||
yField: 'value',
|
||||
seriesField: 'country',
|
||||
point: {
|
||||
style: {
|
||||
size: 0
|
||||
},
|
||||
state: {
|
||||
dimension_hover: {
|
||||
size: 10,
|
||||
outerBorder: {
|
||||
distance: 0,
|
||||
lineWidth: 6,
|
||||
strokeOpacity: 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
line: {
|
||||
style: {
|
||||
// Configure the lineDash attribute based on the forecast field value of the data
|
||||
lineDash: (data: any) => {
|
||||
if (data.forecast) {
|
||||
return [5, 5];
|
||||
}
|
||||
return [0];
|
||||
}
|
||||
}
|
||||
},
|
||||
area: {
|
||||
style: {
|
||||
fillOpacity: 0.5,
|
||||
textureColor: '#fff',
|
||||
textureSize: 14,
|
||||
// Configure the texture attribute based on the forecast field value of the data
|
||||
texture: (data: any) => {
|
||||
if (data.forecast) {
|
||||
return 'bias-rl';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
},
|
||||
legends: [{ visible: true, position: 'middle', orient: 'bottom' }],
|
||||
crosshair: {
|
||||
xField: {
|
||||
visible: true,
|
||||
line: {
|
||||
type: 'line'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const barMarkPointSpec: IBarChartSpec = {
|
||||
type: 'bar',
|
||||
height: 300,
|
||||
data: [
|
||||
{
|
||||
id: 'barData',
|
||||
values: [
|
||||
{ time: '10:20', cost: 2 },
|
||||
{ time: '10:30', cost: 1 },
|
||||
{ time: '10:40', cost: 1 },
|
||||
{ time: '10:50', cost: 2 },
|
||||
{ time: '11:00', cost: 2 },
|
||||
{ time: '11:10', cost: 2 },
|
||||
{ time: '11:20', cost: 1 },
|
||||
{ time: '11:30', cost: 1 },
|
||||
{ time: '11:40', cost: 2 },
|
||||
{ time: '11:50', cost: 1 }
|
||||
]
|
||||
}
|
||||
],
|
||||
xField: 'time',
|
||||
yField: 'cost',
|
||||
crosshair: {
|
||||
xField: {
|
||||
visible: true,
|
||||
line: {
|
||||
type: 'rect',
|
||||
style: {
|
||||
fill: 'rgb(85,208,93)',
|
||||
fillOpacity: 0.1
|
||||
}
|
||||
},
|
||||
bindingAxesIndex: [1],
|
||||
defaultSelect: {
|
||||
axisIndex: 1,
|
||||
datum: '10:20'
|
||||
}
|
||||
}
|
||||
},
|
||||
label: {
|
||||
visible: true,
|
||||
animation: false,
|
||||
formatMethod: (datum: any) => `${datum}分钟`,
|
||||
style: {
|
||||
fill: 'rgb(155,155,155)'
|
||||
}
|
||||
},
|
||||
bar: {
|
||||
style: {
|
||||
fill: 'rgb(85,208,93)',
|
||||
cornerRadius: [4, 4, 0, 0],
|
||||
width: 30
|
||||
}
|
||||
},
|
||||
markPoint: [
|
||||
{
|
||||
coordinate: {
|
||||
time: '10:20',
|
||||
cost: 2
|
||||
},
|
||||
itemContent: {
|
||||
type: 'text',
|
||||
// autoRotate: false,
|
||||
offsetY: -10,
|
||||
text: {
|
||||
dy: 14,
|
||||
text: '2分钟',
|
||||
style: {
|
||||
fill: 'white',
|
||||
fontSize: 14
|
||||
},
|
||||
labelBackground: {
|
||||
padding: [5, 10, 5, 10],
|
||||
style: {
|
||||
fill: '#000',
|
||||
cornerRadius: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
itemLine: {
|
||||
endSymbol: {
|
||||
visible: true,
|
||||
style: {
|
||||
angle: Math.PI,
|
||||
scaleY: 0.4,
|
||||
fill: '#000',
|
||||
dy: 4,
|
||||
stroke: '#000'
|
||||
}
|
||||
},
|
||||
startSymbol: { visible: false },
|
||||
line: {
|
||||
style: {
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
animationUpdate: false,
|
||||
axes: [
|
||||
{
|
||||
orient: 'left',
|
||||
max: 10,
|
||||
label: { visible: false },
|
||||
grid: {
|
||||
style: { lineDash: [4, 4] }
|
||||
}
|
||||
},
|
||||
{
|
||||
orient: 'bottom',
|
||||
label: {
|
||||
formatMethod: (datum: any) => {
|
||||
return datum === '10:20' ? '当前' : datum;
|
||||
},
|
||||
style: (datum: any) => {
|
||||
return {
|
||||
fontSize: datum === '10:20' ? 14 : 12,
|
||||
fill: datum === '10:20' ? 'black' : 'grey'
|
||||
};
|
||||
}
|
||||
},
|
||||
paddingOuter: 0.5,
|
||||
paddingInner: 0,
|
||||
grid: {
|
||||
visible: true,
|
||||
alignWithLabel: false,
|
||||
style: { lineDash: [4, 4] }
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const histogramDifferentBinSpec: IHistogramChartSpec = {
|
||||
type: 'histogram',
|
||||
xField: 'from',
|
||||
x2Field: 'to',
|
||||
yField: 'profit',
|
||||
seriesField: 'type',
|
||||
bar: {
|
||||
style: {
|
||||
stroke: 'white',
|
||||
lineWidth: 1
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: 'Profit',
|
||||
textStyle: {
|
||||
align: 'center',
|
||||
height: 50,
|
||||
lineWidth: 3,
|
||||
fill: '#333',
|
||||
fontSize: 25,
|
||||
fontFamily: 'Times New Roman'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
visible: true,
|
||||
mark: {
|
||||
title: {
|
||||
key: 'title',
|
||||
value: 'profit'
|
||||
},
|
||||
content: [
|
||||
{
|
||||
key: (datum?: Record<string, any>) => `${datum?.from}~${datum?.to}`,
|
||||
value: (datum?: Record<string, any>) => datum?.profit
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
axes: [
|
||||
{
|
||||
orient: 'bottom',
|
||||
nice: false
|
||||
}
|
||||
],
|
||||
data: [
|
||||
{
|
||||
name: 'data1',
|
||||
values: [
|
||||
{
|
||||
from: 0,
|
||||
to: 10,
|
||||
profit: 2,
|
||||
type: 'A'
|
||||
},
|
||||
{
|
||||
from: 10,
|
||||
to: 16,
|
||||
profit: 3,
|
||||
type: 'B'
|
||||
},
|
||||
{
|
||||
from: 16,
|
||||
to: 18,
|
||||
profit: 15,
|
||||
type: 'C'
|
||||
},
|
||||
{
|
||||
from: 18,
|
||||
to: 26,
|
||||
profit: 12,
|
||||
type: 'D'
|
||||
},
|
||||
{
|
||||
from: 26,
|
||||
to: 32,
|
||||
profit: 22,
|
||||
type: 'E'
|
||||
},
|
||||
{
|
||||
from: 32,
|
||||
to: 56,
|
||||
profit: 7,
|
||||
type: 'F'
|
||||
},
|
||||
{
|
||||
from: 56,
|
||||
to: 62,
|
||||
profit: 17,
|
||||
type: 'G'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
51
src/views/plugin/charts/vchart/index.vue
Normal file
51
src/views/plugin/charts/vchart/index.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { useVChart } from '@/hooks/common/vchart';
|
||||
import {
|
||||
barMarkPointSpec,
|
||||
circularProgressTickSpec,
|
||||
histogramDifferentBinSpec,
|
||||
liquidChartSmartInvertSpec,
|
||||
rankingBarSpec,
|
||||
shapeWordCloudSpec,
|
||||
stackedDashAreaSpec
|
||||
} from './data';
|
||||
|
||||
const { domRef: stackedDashAreaRef } = useVChart(() => stackedDashAreaSpec);
|
||||
const { domRef: barMarkPointRef } = useVChart(() => barMarkPointSpec);
|
||||
const { domRef: histogramDifferentBinRef } = useVChart(() => histogramDifferentBinSpec);
|
||||
const { domRef: rankingBarRef } = useVChart(() => rankingBarSpec);
|
||||
const { domRef: shapeWordCloudRef } = useVChart(() => shapeWordCloudSpec);
|
||||
const { domRef: circularProgressTickRef } = useVChart(() => circularProgressTickSpec);
|
||||
const { domRef: liquidChartSmartInvertRef } = useVChart(() => liquidChartSmartInvertSpec);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NCard :bordered="false" title="VChart" class="h-full card-wrapper">
|
||||
<WebSiteLink label="More Demos: " link="https://www.visactor.com/vchart/example" />
|
||||
</NCard>
|
||||
<NCard title="Stacked Dash Area Chart" :bordered="false" class="h-full card-wrapper">
|
||||
<div ref="stackedDashAreaRef" class="h-400px" />
|
||||
</NCard>
|
||||
<NCard title="Bar Mark Point Chart" :bordered="false" class="h-full card-wrapper">
|
||||
<div ref="barMarkPointRef" class="h-400px" />
|
||||
</NCard>
|
||||
<NCard title="Histogram Different Bin Chart" :bordered="false" class="h-full card-wrapper">
|
||||
<div ref="histogramDifferentBinRef" class="h-400px" />
|
||||
</NCard>
|
||||
<NCard title="Ranking Bar Chart" :bordered="false" class="h-full card-wrapper">
|
||||
<div ref="rankingBarRef" class="h-400px" />
|
||||
</NCard>
|
||||
<NCard title="Circular Progress Tick Chart" :bordered="false" class="h-full card-wrapper">
|
||||
<div ref="circularProgressTickRef" class="h-400px" />
|
||||
</NCard>
|
||||
<NCard title="Liquid Chart Smart Invert Chart" :bordered="false" class="h-full card-wrapper">
|
||||
<div ref="liquidChartSmartInvertRef" class="h-400px" />
|
||||
</NCard>
|
||||
<NCard title="Shape Word Cloud Chart" :bordered="false" class="h-full card-wrapper">
|
||||
<div ref="shapeWordCloudRef" class="h-400px" />
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
34
src/views/plugin/copy/index.vue
Normal file
34
src/views/plugin/copy/index.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
|
||||
const { copy, isSupported } = useClipboard();
|
||||
|
||||
const source = ref('');
|
||||
|
||||
async function handleCopy() {
|
||||
if (!isSupported) {
|
||||
window.$message?.error('您的浏览器不支持Clipboard API');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!source.value) {
|
||||
window.$message?.error('请输入要复制的内容');
|
||||
return;
|
||||
}
|
||||
|
||||
await copy(source.value);
|
||||
window.$message?.success(`复制成功:${source.value}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<NCard title="文本复制" :bordered="false" class="h-full card-wrapper">
|
||||
<NInputGroup>
|
||||
<NInput v-model:value="source" placeholder="请输入要复制的内容吧" />
|
||||
<NButton type="primary" @click="handleCopy">复制</NButton>
|
||||
</NInputGroup>
|
||||
</NCard>
|
||||
</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