Compare commits

...

79 Commits

Author SHA1 Message Date
Soybean
bf0e5569ac Merge branch 'v2.0' into v2.0-example 2025-07-20 00:36:40 +08:00
Soybean
1740ca2cfa Merge branch 'main' into example 2025-07-14 00:47:38 +08:00
Azir
643ccec544 refactor(types): improve namespace migration. 2025-06-28 14:48:48 +08:00
Azir
bf9baf0d14 refactor(types): move Auth and Route namespaces to separate files and clean up api.d.ts 2025-06-28 14:48:48 +08:00
Azir
bbadd41e0e typo(projects): update Vite version to 7 in i18n translate. 2025-06-28 14:48:48 +08:00
Azir
7e1e1716f9 chore(packages): update Vite version to 7 in package.json and documentation.
(cherry picked from commit ef806edd9d0c48ad8669863516d52e2eb8870d6f)
2025-06-28 14:48:48 +08:00
Soybean
a63a343e9c Merge branch 'main' into example 2025-06-24 22:37:58 +08:00
Azir
c816f4dc3b feat(projects): add video document menu. 2025-06-24 10:11:21 +08:00
Soybean
ec20829a22 Merge branch 'main' into example 2025-06-09 22:50:16 +08:00
Soybean
f5e6a205b3 fix(auth): initialize routeStore in toggle-auth. closed #754 2025-06-09 22:37:19 +08:00
Zheng-Changfu
ff83d333a5
feat(projects): add example for pro-naive-ui. 2025-05-16 13:06:59 +08:00
Soybean
9a294204a2 Merge branch 'main' into example 2025-03-19 22:42:20 +08:00
Soybean
a5c9a19838 Merge branch 'main' into example 2025-03-12 23:25:14 +08:00
Soybean
03f098bec6 chore(deps): update deps 2025-01-20 13:49:04 +08:00
Soybean
69788d0134 Merge branch 'main' into example 2025-01-20 13:45:54 +08:00
Soybean
745a9077ec Merge branch 'main' into example 2024-12-16 16:44:43 +08:00
Soybean
60c307ccda Merge branch 'main' into example 2024-11-17 22:34:18 +08:00
Soybean
823651139a chore(deps): update deps 2024-11-15 12:26:40 +08:00
Soybean
d1c6081c25 optimize(projects): optimize example code about vtable,vchart 2024-11-02 02:48:22 +08:00
Rui-Sun
b9e873e53c
feat(projects): add visactor plugin demos (#658)
* feat: add vtable plugin

* feat: add vchart hook

* feat: add dark mode support in vtable

* feat: add visactor icon

* feat: add vtable-gantt plugin

* fix: fix theme update in vtable-gantt

* feat: update visactor version
2024-11-02 01:57:43 +08:00
Soybean
aabe7fe592 optimize(projects): optimize manage page: use ref replace reactive 2024-10-28 19:29:30 +08:00
Soybean
7e436b6328 Merge branch 'main' into example 2024-10-25 01:19:37 +08:00
Soybean
e019defef3 refactor(projects): rename directory serviceAlova to service-alova 2024-10-24 23:52:45 +08:00
Soybean
a2178d3405 optimize(projects): optimize example antv 2024-10-24 23:46:11 +08:00
Ma.Jinkai
0c0d606ae5 feat(projects): add antv g6 example 2024-10-22 14:08:33 +08:00
Scott Hu
a6545265ac feat(projects): add alova examples (#647)
* feat: optimistic subpackage `@sa/alova`

* feat: add alova examples
2024-10-17 17:51:36 +08:00
Soybean
641f3160d6 Merge branch 'main' into example 2024-10-17 17:24:49 +08:00
Soybean
3fbf2e2a5f Merge branch 'main' into example 2024-10-09 23:00:35 +08:00
Soybean
405b1b41cc Merge branch 'main' into example 2024-09-21 08:42:40 +08:00
Soybean
da6cf09e76 Merge branch 'main' into example 2024-09-20 07:44:12 +08:00
Soybean
5bfc7613f7 Merge branch 'main' into example 2024-09-07 11:38:35 +08:00
Soybean
7710bcdb0a optimize(projects): optimize code 2024-08-11 23:35:45 +08:00
Ohh
b36d914e48
feat(projects): add plugin typeit (#591) 2024-08-11 23:30:12 +08:00
Soybean
bbacff0581 optimize(projects): optimize plugin gantt 2024-08-06 10:21:46 +08:00
Ohh
f86bca063a
feat(projects): add plugin gantt preview (#578) 2024-08-05 10:26:32 +08:00
Soybean
48e005bcaf Merge branch 'main' into example 2024-08-01 16:06:59 +08:00
Soybean
d825b6e260 feat(projects): add watermark override theme settings 2024-07-30 21:50:22 +08:00
Soybean
05b0147b47 Merge branch 'main' into example 2024-07-30 18:04:14 +08:00
Soybean
e7cd34ec37 chore(projects): Merge branch 'main' into example 2024-07-30 17:26:42 +08:00
Ohh
a9dce21878
feat(projects): add plugin pdf preview (#568) 2024-07-27 03:06:11 +08:00
Soybean
24320ab899 Merge branch 'main' into example 2024-07-22 19:05:11 +08:00
青菜白玉汤
2f1f2c2b2a fix(projects): fix the issue of abnormal width of the sidebar in the top menu mix and reverse mode (#562)
(cherry picked from commit c469512bd4)
2024-07-22 13:34:35 +08:00
Soybean
3a970a090d fix(components): fix component name and iconify icon name conflict 2024-07-22 01:12:00 +08:00
Soybean
4a7e9c2669 chore(projects): merge branch main into example 2024-07-22 00:57:11 +08:00
Soybean
d6f3effce0 chore(deps): update deps 2024-07-22 00:35:53 +08:00
Soybean
fa4367a7b9 optimize(projects): optimize page code: plugin_excel 2024-07-22 00:34:13 +08:00
Ohh
c425822b3a
feat(projects): add plugin excel export (#561) 2024-07-21 20:54:18 +08:00
Soybean
447bda2b48 chore(projects): merge branch main into example 2024-07-20 02:35:42 +08:00
dodu2014
c0303a8637
fix(types): Fixed the reference type error (#551) 2024-07-19 18:12:29 +08:00
Ohh
c7ab61bbd4
feat(projects): add plugin pinyin-pro (#557) 2024-07-19 16:20:36 +08:00
Soybean
ec6a497452 fix(projects): fix page manage-role default-expanded-names 2024-07-19 10:19:33 +08:00
paynezhuang
074077dfaa
feat(projects): Manage search area using collapsed components , close #537 (#549) 2024-07-15 14:46:04 +08:00
Soybean
fcf866783c chore(projects): merge branch main into example 2024-07-12 10:28:10 +08:00
Soybean
1c9e36c47b chore(projects): merge branch main into example 2024-06-21 10:15:20 +08:00
Soybean
d3210edc6a chore(projects): merge branch main into example 2024-06-15 01:13:36 +08:00
Soybean
799ebcc714 Merge branch 'main' into example 2024-06-14 00:05:12 +08:00
Soybean
4a8b9032cd chore(projects): merge branch main into example 2024-06-13 23:30:33 +08:00
Soybean
8fea803fca Merge branch 'main' into example 2024-06-12 11:10:08 +08:00
Soybean
599d0f10e8 Merge branch 'main' into example 2024-06-07 11:55:56 +08:00
Soybean
781f88d227 Merge branch 'main' into example 2024-06-06 23:33:41 +08:00
Soybean
5739346484 chore(projects): merge main to example 2024-06-06 10:36:31 +08:00
Soybean
d194f0b1b6 chore(projects): merge main to example 2024-06-04 20:24:34 +08:00
Soybean
19bea283c9 chore(projects): merge main to example 2024-06-02 01:30:02 +08:00
Soybean
53a8e5678a chore(projects): merge main to example 2024-05-24 02:28:43 +08:00
Soybean
10b231918a chore(projects): merge main to example 2024-05-20 01:24:46 +08:00
Soybean
81d51a9630 fix(projects): fix baidu map request protocol 2024-05-08 23:19:49 +08:00
Soybean
fbebef3acb fix(projects): fix page: plugin_video style 2024-05-08 23:01:38 +08:00
Soybean
12d7bf94ee optimize(projects): optimize code 2024-05-08 22:57:22 +08:00
Soybean
0478154db8 chore(projects): update pnpm-lock.yaml 2024-05-08 22:39:16 +08:00
Soybean
994ef2b9b1
merge main to example (#433)
* docs(projects): update CHANGELOG

* fix(styles): fix useTable type
2024-05-08 22:37:18 +08:00
paynezhuang
ef32eecfdb
feat(projects): add page plugin_barcode (#432) 2024-05-08 22:35:12 +08:00
Soybean
83ae0bad0f
docs(projects): update CHANGELOG (#431) 2024-05-07 22:02:11 +08:00
Soybean
9a9e2d144a chore(projects): lock deps version 2024-05-07 21:59:23 +08:00
lisong
a2110a98cf
feat(projects): add page plugin_editor、plugin_icon、plugin_map、plugin_print、plugin_swiper and plugin_video (#430) 2024-05-07 21:56:23 +08:00
Soybean
daf6600c21 feat(projects): add page plugin_charts_echarts 2024-05-07 19:51:54 +08:00
Soybean
a9b3a961af Merge branch 'main' into example 2024-05-07 19:18:27 +08:00
Soybean
3a35d62e6f Merge branch 'main' into example 2024-05-07 18:22:22 +08:00
Soybean
aaf28a42c2 optimize(projects): optimize plugin_copy 2024-05-07 18:20:23 +08:00
恕瑞玛的皇帝
019f80faef feat(projects): 添加插件示例:剪贴板 2024-05-07 17:24:16 +08:00
110 changed files with 16007 additions and 49 deletions

View File

@ -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;

View File

@ -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 })
]
}),

View File

@ -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.3",
"vue-router": "4.5.1",
"wangeditor": "4.7.15",
"xgplayer": "3.0.22",
"xlsx": "0.18.5"
},
"devDependencies": {
"@amap/amap-jsapi-types": "0.0.15",
"@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.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",

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View 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

View 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>

View 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>

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
defineOptions({ name: 'WebSiteLink' });
interface Props {
/** Web site name */
label: string;
/** Web site link */
link: string;
}
defineProps<Props>();
</script>
<template>
<p>
<span>{{ label }}</span>
<a class="text-blue-500" :href="link" target="#">
{{ link }}
</a>
</p>
</template>
<style scoped></style>

29
src/constants/business.ts Normal file
View 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
View 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';

158
src/hooks/common/vchart.ts Normal file
View 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
};
}

View File

@ -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',

View File

@ -200,7 +200,80 @@ const local: App.I18n.Schema = {
404: 'Page Not Found',
500: 'Server Error',
'iframe-page': 'Iframe',
home: 'Home'
home: 'Home',
document: 'Document',
document_project: 'Project Document',
'document_project-link': 'Project Document(External Link)',
document_video: 'Video Tutorial',
document_vue: 'Vue Document',
document_vite: 'Vite Document',
document_unocss: 'UnoCSS Document',
document_naive: 'Naive UI Document',
'document_pro-naive': 'Pro Naive UI Document',
document_antd: 'Ant Design Vue Document',
document_alova: 'Alova Document',
'user-center': 'User Center',
about: 'About',
function: 'System Function',
alova: 'Alova Example',
alova_request: 'Alova Request',
alova_scenes: 'Scenario Request',
'pro-naive': 'Pro Naive Example',
'pro-naive_form': 'Form',
'pro-naive_form_basic': 'Basic Form',
'pro-naive_form_query': 'Query Form',
'pro-naive_form_step': 'Step Form',
'pro-naive_table': 'Table',
'pro-naive_table_remote': 'Remote',
'pro-naive_table_row-edit': 'Row Edit',
function_tab: 'Tab',
'function_multi-tab': 'Multi Tab',
'function_hide-child': 'Hide Child',
'function_hide-child_one': 'Hide Child',
'function_hide-child_two': 'Two',
'function_hide-child_three': 'Three',
function_request: 'Request',
'function_toggle-auth': 'Toggle Auth',
'function_super-page': 'Super Admin Visible',
manage: 'System Manage',
manage_user: 'User Manage',
'manage_user-detail': 'User Detail',
manage_role: 'Role Manage',
manage_menu: 'Menu Manage',
'multi-menu': 'Multi Menu',
'multi-menu_first': 'Menu One',
'multi-menu_first_child': 'Menu One Child',
'multi-menu_second': 'Menu Two',
'multi-menu_second_child': 'Menu Two Child',
'multi-menu_second_child_home': 'Menu Two Child Home',
exception: 'Exception',
exception_403: '403',
exception_404: '404',
exception_500: '500',
plugin: 'Plugin',
plugin_copy: 'Copy',
plugin_charts: 'Charts',
plugin_charts_echarts: 'ECharts',
plugin_charts_antv: 'AntV',
plugin_charts_vchart: 'VChart',
plugin_editor: 'Editor',
plugin_editor_quill: 'Quill',
plugin_editor_markdown: 'Markdown',
plugin_icon: 'Icon',
plugin_map: 'Map',
plugin_print: 'Print',
plugin_swiper: 'Swiper',
plugin_video: 'Video',
plugin_barcode: 'Barcode',
plugin_pinyin: 'pinyin',
plugin_excel: 'Excel',
plugin_pdf: 'PDF preview',
plugin_gantt: 'Gantt Chart',
plugin_gantt_dhtmlx: 'dhtmlxGantt',
plugin_gantt_vtable: 'VTableGantt',
plugin_typeit: 'Typeit',
plugin_tables: 'Tables',
plugin_tables_vtable: 'VTable'
},
page: {
login: {
@ -249,6 +322,19 @@ const local: App.I18n.Schema = {
title: 'Bind WeChat'
}
},
about: {
title: 'About',
introduction: `SoybeanAdmin is an elegant and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite7, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. SoybeanAdmin provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.`,
projectInfo: {
title: 'Project Info',
version: 'Version',
latestBuildTime: 'Latest Build Time',
githubLink: 'Github Link',
previewLink: 'Preview Link'
},
prdDep: 'Production Dependency',
devDep: 'Development Dependency'
},
home: {
branchDesc:
'For the convenience of everyone in developing and updating the merge, we have streamlined the code of the main branch, only retaining the homepage menu, and the rest of the content has been moved to the example branch for maintenance. The preview address displays the content of the example branch.',
@ -277,6 +363,253 @@ const local: App.I18n.Schema = {
desc5: 'Soybean just wrote some of the workbench pages casually, and it was enough to see!'
},
creativity: 'Creativity'
},
function: {
tab: {
tabOperate: {
title: 'Tab Operation',
addTab: 'Add Tab',
addTabDesc: 'To about page',
closeTab: 'Close Tab',
closeCurrentTab: 'Close Current Tab',
closeAboutTab: 'Close "About" Tab',
addMultiTab: 'Add Multi Tab',
addMultiTabDesc1: 'To MultiTab page',
addMultiTabDesc2: 'To MultiTab page(with query params)'
},
tabTitle: {
title: 'Tab Title',
changeTitle: 'Change Title',
change: 'Change',
resetTitle: 'Reset Title',
reset: 'Reset'
}
},
multiTab: {
routeParam: 'Route Param',
backTab: 'Back function_tab'
},
toggleAuth: {
toggleAccount: 'Toggle Account',
authHook: 'Auth Hook Function `hasAuth`',
superAdminVisible: 'Super Admin Visible',
adminVisible: 'Admin Visible',
adminOrUserVisible: 'Admin and User Visible'
},
request: {
repeatedErrorOccurOnce: 'Repeated Request Error Occurs Once',
repeatedError: 'Repeated Request Error',
repeatedErrorMsg1: 'Custom Request Error 1',
repeatedErrorMsg2: 'Custom Request Error 2'
}
},
alova: {
scenes: {
captchaSend: 'Captcha Send',
autoRequest: 'Auto Request',
visibilityRequestTips: 'Automatically request when switching browser window',
pollingRequestTips: 'It will request every 3 seconds',
networkRequestTips: 'Automatically request after network reconnecting',
refreshTime: 'Refresh Time',
startRequest: 'Start Request',
stopRequest: 'Stop Request',
requestCrossComponent: 'Request Cross Component',
triggerAllRequest: 'Manually Trigger All Automated Requests'
}
},
proNaive: {
form: {
basic: {
title: 'Basic Example',
appName: 'ApplicationName',
appStatus: 'ApplicationStatus',
createTime: 'CreateTime',
responseDate: 'ResponseDate',
specificationInfo: 'SpecificationInfo',
specificate: 'Specificate',
specificationName: 'SpecificationName',
specificationValue: 'SpecificationValue',
specificationColorRed: 'Red',
specificationColorOrange: 'Orange',
addSpecificateItem: 'Add Specificate Item',
fillValue: 'FillValue',
reset: 'Reset',
submit: 'Submit',
add: 'Add',
delete: 'Delete',
color: 'Color',
normal: 'Normal',
anomaly: 'Anomaly'
},
query: {
title1: 'Query Example, which expands by default',
title2: 'Query Example, which fold by default, and two lines are retained when folding',
appName: 'ApplicationName',
appStatus: 'ApplicationStatus',
createTime: 'CreateTime',
responseDate: 'ResponseDate',
endDate: 'EndDate',
field: 'Field'
},
step: {
title: 'Step Form',
step1: {
title: 'Form 1',
field: 'Form 1 field',
nextStep: 'Next Step'
},
step2: {
title: 'Form 2',
field: 'Form 2 field',
prevStep: 'Prev Step',
submit: 'Submit'
}
}
},
table: {
remote: {
filterCondition: 'Filter Condition',
name: 'Name',
createTime: 'CreateTime',
responseTime: 'ResponseTime',
title: 'Remote Loading',
replicableText: 'Replicable Text',
tags: 'Tags',
dateFormatting: 'Date Formatting',
image: 'Image'
},
rowEdit: {
title: 'Edit Table',
reset: 'Reset',
submit: 'Submit',
edit: 'Edit',
delete: 'Delete',
save: 'Save',
task: 'Task',
score: 'Score',
time: 'Time',
name: 'Name',
action: 'Action'
}
}
},
manage: {
common: {
status: {
enable: 'Enable',
disable: 'Disable'
}
},
role: {
title: 'Role List',
roleName: 'Role Name',
roleCode: 'Role Code',
roleStatus: 'Role Status',
roleDesc: 'Role Description',
menuAuth: 'Menu Auth',
buttonAuth: 'Button Auth',
form: {
roleName: 'Please enter role name',
roleCode: 'Please enter role code',
roleStatus: 'Please select role status',
roleDesc: 'Please enter role description'
},
addRole: 'Add Role',
editRole: 'Edit Role'
},
user: {
title: 'User List',
userName: 'User Name',
userGender: 'Gender',
nickName: 'Nick Name',
userPhone: 'Phone Number',
userEmail: 'Email',
userStatus: 'User Status',
userRole: 'User Role',
form: {
userName: 'Please enter user name',
userGender: 'Please select gender',
nickName: 'Please enter nick name',
userPhone: 'Please enter phone number',
userEmail: 'Please enter email',
userStatus: 'Please select user status',
userRole: 'Please select user role'
},
addUser: 'Add User',
editUser: 'Edit User',
gender: {
male: 'Male',
female: 'Female'
}
},
menu: {
home: 'Home',
title: 'Menu List',
id: 'ID',
parentId: 'Parent ID',
menuType: 'Menu Type',
menuName: 'Menu Name',
routeName: 'Route Name',
routePath: 'Route Path',
pathParam: 'Path Param',
layout: 'Layout Component',
page: 'Page Component',
i18nKey: 'I18n Key',
icon: 'Icon',
localIcon: 'Local Icon',
iconTypeTitle: 'Icon Type',
order: 'Order',
constant: 'Constant',
keepAlive: 'Keep Alive',
href: 'Href',
hideInMenu: 'Hide In Menu',
activeMenu: 'Active Menu',
multiTab: 'Multi Tab',
fixedIndexInTab: 'Fixed Index In Tab',
query: 'Query Params',
button: 'Button',
buttonCode: 'Button Code',
buttonDesc: 'Button Desc',
menuStatus: 'Menu Status',
form: {
home: 'Please select home',
menuType: 'Please select menu type',
menuName: 'Please enter menu name',
routeName: 'Please enter route name',
routePath: 'Please enter route path',
pathParam: 'Please enter path param',
page: 'Please select page component',
layout: 'Please select layout component',
i18nKey: 'Please enter i18n key',
icon: 'Please enter iconify name',
localIcon: 'Please enter local icon name',
order: 'Please enter order',
keepAlive: 'Please select whether to cache route',
href: 'Please enter href',
hideInMenu: 'Please select whether to hide menu',
activeMenu: 'Please select route name of the highlighted menu',
multiTab: 'Please select whether to support multiple tabs',
fixedInTab: 'Please select whether to fix in the tab',
fixedIndexInTab: 'Please enter the index fixed in the tab',
queryKey: 'Please enter route parameter Key',
queryValue: 'Please enter route parameter Value',
button: 'Please select whether it is a button',
buttonCode: 'Please enter button code',
buttonDesc: 'Please enter button description',
menuStatus: 'Please select menu status'
},
addMenu: 'Add Menu',
editMenu: 'Edit Menu',
addChildMenu: 'Add Child Menu',
type: {
directory: 'Directory',
menu: 'Menu'
},
iconType: {
iconify: 'Iconify Icon',
local: 'Local Icon'
}
}
}
},
form: {

View File

@ -197,7 +197,80 @@ const local: App.I18n.Schema = {
404: '页面不存在',
500: '服务器错误',
'iframe-page': '外链页面',
home: '首页'
home: '首页',
document: '文档',
document_project: '项目文档',
'document_project-link': '项目文档(外链)',
document_video: '视频教程',
document_vue: 'Vue文档',
document_vite: 'Vite文档',
document_unocss: 'UnoCSS文档',
document_naive: 'Naive UI文档',
'document_pro-naive': 'Pro Naive UI文档',
document_antd: 'Ant Design Vue文档',
document_alova: 'Alova文档',
'user-center': '个人中心',
about: '关于',
function: '系统功能',
alova: 'alova示例',
alova_request: 'alova请求',
alova_scenes: '场景化请求',
'pro-naive': 'Pro Naive UI 示例',
'pro-naive_form': '表单',
'pro-naive_form_basic': '基础表单',
'pro-naive_form_query': '查询表单',
'pro-naive_form_step': '分步表单',
'pro-naive_table': '表格',
'pro-naive_table_remote': '远程加载',
'pro-naive_table_row-edit': '行编辑',
function_tab: '标签页',
'function_multi-tab': '多标签页',
'function_hide-child': '隐藏子菜单',
'function_hide-child_one': '隐藏子菜单',
'function_hide-child_two': '菜单二',
'function_hide-child_three': '菜单三',
function_request: '请求',
'function_toggle-auth': '切换权限',
'function_super-page': '超级管理员可见',
manage: '系统管理',
manage_user: '用户管理',
'manage_user-detail': '用户详情',
manage_role: '角色管理',
manage_menu: '菜单管理',
'multi-menu': '多级菜单',
'multi-menu_first': '菜单一',
'multi-menu_first_child': '菜单一子菜单',
'multi-menu_second': '菜单二',
'multi-menu_second_child': '菜单二子菜单',
'multi-menu_second_child_home': '菜单二子菜单首页',
exception: '异常页',
exception_403: '403',
exception_404: '404',
exception_500: '500',
plugin: '插件示例',
plugin_copy: '剪贴板',
plugin_charts: '图表',
plugin_charts_echarts: 'ECharts',
plugin_charts_antv: 'AntV',
plugin_charts_vchart: 'VChart',
plugin_editor: '编辑器',
plugin_editor_quill: '富文本编辑器',
plugin_editor_markdown: 'MD 编辑器',
plugin_icon: '图标',
plugin_map: '地图',
plugin_print: '打印',
plugin_swiper: 'Swiper',
plugin_video: '视频',
plugin_barcode: '条形码',
plugin_pinyin: '拼音',
plugin_excel: 'Excel',
plugin_pdf: 'PDF 预览',
plugin_gantt: '甘特图',
plugin_gantt_dhtmlx: 'dhtmlxGantt',
plugin_gantt_vtable: 'VTableGantt',
plugin_typeit: '打字机',
plugin_tables: '表格',
plugin_tables_vtable: 'VTable'
},
page: {
login: {
@ -246,6 +319,19 @@ const local: App.I18n.Schema = {
title: '绑定微信'
}
},
about: {
title: '关于',
introduction: `SoybeanAdmin 是一个优雅且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite7, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件代码规范严谨实现了自动化的文件路由系统。此外它还采用了基于 ApiFox 的在线Mock数据方案。SoybeanAdmin 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。`,
projectInfo: {
title: '项目信息',
version: '版本',
latestBuildTime: '最新构建时间',
githubLink: 'Github 地址',
previewLink: '预览地址'
},
prdDep: '生产依赖',
devDep: '开发依赖'
},
home: {
branchDesc:
'为了方便大家开发和更新合并我们对main分支的代码进行了精简只保留了首页菜单其余内容已移至example分支进行维护。预览地址显示的内容即为example分支的内容。',
@ -274,6 +360,253 @@ const local: App.I18n.Schema = {
desc5: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!'
},
creativity: '创意'
},
function: {
tab: {
tabOperate: {
title: '标签页操作',
addTab: '添加标签页',
addTabDesc: '跳转到关于页面',
closeTab: '关闭标签页',
closeCurrentTab: '关闭当前标签页',
closeAboutTab: '关闭"关于"标签页',
addMultiTab: '添加多标签页',
addMultiTabDesc1: '跳转到多标签页页面',
addMultiTabDesc2: '跳转到多标签页页面(带有查询参数)'
},
tabTitle: {
title: '标签页标题',
changeTitle: '修改标题',
change: '修改',
resetTitle: '重置标题',
reset: '重置'
}
},
multiTab: {
routeParam: '路由参数',
backTab: '返回 function_tab'
},
toggleAuth: {
toggleAccount: '切换账号',
authHook: '权限钩子函数 `hasAuth`',
superAdminVisible: '超级管理员可见',
adminVisible: '管理员可见',
adminOrUserVisible: '管理员和用户可见'
},
request: {
repeatedErrorOccurOnce: '重复请求错误只出现一次',
repeatedError: '重复请求错误',
repeatedErrorMsg1: '自定义请求错误 1',
repeatedErrorMsg2: '自定义请求错误 2'
}
},
alova: {
scenes: {
captchaSend: '发送验证码',
autoRequest: '自动请求',
visibilityRequestTips: '浏览器窗口切换自动请求数据',
pollingRequestTips: '每3秒自动请求一次',
networkRequestTips: '网络重连后自动请求',
refreshTime: '更新时间',
startRequest: '开始请求',
stopRequest: '停止请求',
requestCrossComponent: '跨组件触发请求',
triggerAllRequest: '手动触发所有自动请求'
}
},
proNaive: {
form: {
basic: {
title: '基础示例',
appName: '应用名称',
appStatus: '应用状态',
createTime: '创建时间',
responseDate: '响应日期',
specificationInfo: '规格信息',
specificate: '规格',
specificationName: '规格名',
specificationValue: '规格值',
specificationColorRed: '红',
specificationColorOrange: '橙',
addSpecificateItem: '添加规格项',
fillValue: '填充值',
reset: '重置',
submit: '提交',
add: '新建',
delete: '删除',
color: '颜色',
normal: '正常',
anomaly: '异常'
},
query: {
title1: '查询表单,默认展开',
title2: '查询表单默认折叠折叠时保留2行',
appName: '应用名称',
appStatus: '应用状态',
createTime: '创建时间',
responseDate: '响应日期',
endDate: '结束日期',
field: '字段'
},
step: {
title: '分步表单',
step1: {
title: '表单1',
field: '表单1字段',
nextStep: '下一步'
},
step2: {
title: '表单2',
field: '表单2字段',
prevStep: '上一步',
submit: '提交'
}
}
},
table: {
remote: {
filterCondition: '筛选条件',
name: '名称',
createTime: '创建时间',
responseTime: '响应时间',
title: '远程加载',
replicableText: '可复制文本',
tags: 'tags',
dateFormatting: '日期格式化',
image: '图片'
},
rowEdit: {
title: '编辑表格',
reset: '重置',
submit: '提交',
edit: '编辑',
delete: '删除',
save: '保存',
task: '任务',
score: '评分',
time: '时间',
name: '名称',
action: '操作'
}
}
},
manage: {
common: {
status: {
enable: '启用',
disable: '禁用'
}
},
role: {
title: '角色列表',
roleName: '角色名称',
roleCode: '角色编码',
roleStatus: '角色状态',
roleDesc: '角色描述',
menuAuth: '菜单权限',
buttonAuth: '按钮权限',
form: {
roleName: '请输入角色名称',
roleCode: '请输入角色编码',
roleStatus: '请选择角色状态',
roleDesc: '请输入角色描述'
},
addRole: '新增角色',
editRole: '编辑角色'
},
user: {
title: '用户列表',
userName: '用户名',
userGender: '性别',
nickName: '昵称',
userPhone: '手机号',
userEmail: '邮箱',
userStatus: '用户状态',
userRole: '用户角色',
form: {
userName: '请输入用户名',
userGender: '请选择性别',
nickName: '请输入昵称',
userPhone: '请输入手机号',
userEmail: '请输入邮箱',
userStatus: '请选择用户状态',
userRole: '请选择用户角色'
},
addUser: '新增用户',
editUser: '编辑用户',
gender: {
male: '男',
female: '女'
}
},
menu: {
home: '首页',
title: '菜单列表',
id: 'ID',
parentId: '父级菜单ID',
menuType: '菜单类型',
menuName: '菜单名称',
routeName: '路由名称',
routePath: '路由路径',
pathParam: '路径参数',
layout: '布局',
page: '页面组件',
i18nKey: '国际化key',
icon: '图标',
localIcon: '本地图标',
iconTypeTitle: '图标类型',
order: '排序',
constant: '常量路由',
keepAlive: '缓存路由',
href: '外链',
hideInMenu: '隐藏菜单',
activeMenu: '高亮的菜单',
multiTab: '支持多页签',
fixedIndexInTab: '固定在页签中的序号',
query: '路由参数',
button: '按钮',
buttonCode: '按钮编码',
buttonDesc: '按钮描述',
menuStatus: '菜单状态',
form: {
home: '请选择首页',
menuType: '请选择菜单类型',
menuName: '请输入菜单名称',
routeName: '请输入路由名称',
routePath: '请输入路由路径',
pathParam: '请输入路径参数',
page: '请选择页面组件',
layout: '请选择布局组件',
i18nKey: '请输入国际化key',
icon: '请输入图标',
localIcon: '请选择本地图标',
order: '请输入排序',
keepAlive: '请选择是否缓存路由',
href: '请输入外链',
hideInMenu: '请选择是否隐藏菜单',
activeMenu: '请选择高亮的菜单的路由名称',
multiTab: '请选择是否支持多标签',
fixedInTab: '请选择是否固定在页签中',
fixedIndexInTab: '请输入固定在页签中的序号',
queryKey: '请输入路由参数Key',
queryValue: '请输入路由参数Value',
button: '请选择是否按钮',
buttonCode: '请输入按钮编码',
buttonDesc: '请输入按钮描述',
menuStatus: '请选择菜单状态'
},
addMenu: '新增菜单',
editMenu: '编辑菜单',
addChildMenu: '新增子菜单',
type: {
directory: '目录',
menu: '菜单'
},
iconType: {
iconify: 'iconify图标',
local: '本地图标'
}
}
}
},
form: {

View File

@ -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();

View File

@ -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
View 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);
}

View File

@ -1,5 +1,6 @@
export * from './loading';
export * from './nprogress';
export * from './components';
export * from './iconify';
export * from './dayjs';
export * from './app';

View File

@ -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"),
};

View File

@ -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
}
}
];

View File

@ -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"
};
/**

View File

@ -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() {

View 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
});
}

View File

@ -0,0 +1,3 @@
export * from './auth';
export * from './route';
export * from './system-manage';

View 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 } });
}

View 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');
}

View 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()
}
};
}
});

View 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;
}
}
);

View 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);
}
});
}
}

View File

@ -0,0 +1,4 @@
export interface RequestInstanceState {
/** the request error message stack */
errMsgStack: string[];
}

View File

@ -1,2 +1,3 @@
export * from './auth';
export * from './route';
export * from './system-manage';

View 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'
});
}

139
src/typings/api/system-manage.d.ts vendored Normal file
View File

@ -0,0 +1,139 @@
declare namespace Api {
/**
* namespace SystemManage
*
* backend api module: "systemManage"
*/
namespace SystemManage {
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
/** role */
type Role = Common.CommonRecord<{
/** role name */
roleName: string;
/** role code */
roleCode: string;
/** role description */
roleDesc: string;
}>;
/** role search params */
type RoleSearchParams = CommonType.RecordNullable<
Pick<Api.SystemManage.Role, 'roleName' | 'roleCode' | 'status'> & CommonSearchParams
>;
/** role list */
type RoleList = Common.PaginatingQueryRecord<Role>;
/** all role */
type AllRole = Pick<Role, 'id' | 'roleName' | 'roleCode'>;
/**
* user gender
*
* - "1": "male"
* - "2": "female"
*/
type UserGender = '1' | '2';
/** user */
type User = Common.CommonRecord<{
/** user name */
userName: string;
/** user gender */
userGender: UserGender | null;
/** user nick name */
nickName: string;
/** user phone */
userPhone: string;
/** user email */
userEmail: string;
/** user role code collection */
userRoles: string[];
}>;
/** user search params */
type UserSearchParams = CommonType.RecordNullable<
Pick<Api.SystemManage.User, 'userName' | 'userGender' | 'nickName' | 'userPhone' | 'userEmail' | 'status'> &
CommonSearchParams
>;
/** user list */
type UserList = Common.PaginatingQueryRecord<User>;
/**
* menu type
*
* - "1": directory
* - "2": menu
*/
type MenuType = '1' | '2';
type MenuButton = {
/**
* button code
*
* it can be used to control the button permission
*/
code: string;
/** button description */
desc: string;
};
/**
* icon type
*
* - "1": iconify icon
* - "2": local icon
*/
type IconType = '1' | '2';
type MenuPropsOfRoute = Pick<
import('vue-router').RouteMeta,
| 'i18nKey'
| 'keepAlive'
| 'constant'
| 'order'
| 'href'
| 'hideInMenu'
| 'activeMenu'
| 'multiTab'
| 'fixedIndexInTab'
| 'query'
>;
type Menu = Common.CommonRecord<{
/** parent menu id */
parentId: number;
/** menu type */
menuType: MenuType;
/** menu name */
menuName: string;
/** route name */
routeName: string;
/** route path */
routePath: string;
/** component */
component?: string;
/** iconify icon name or local icon name */
icon: string;
/** icon type */
iconType: IconType;
/** buttons */
buttons?: MenuButton[] | null;
/** children menu */
children?: Menu[] | null;
}> &
MenuPropsOfRoute;
/** menu list */
type MenuList = Common.PaginatingQueryRecord<Menu>;
type MenuTree = {
id: number;
label: string;
pId: number;
children?: MenuTree[];
};
}
}

260
src/typings/app.d.ts vendored
View File

@ -499,6 +499,19 @@ declare namespace App {
title: string;
};
};
about: {
title: string;
introduction: string;
projectInfo: {
title: string;
version: string;
latestBuildTime: string;
githubLink: string;
previewLink: string;
};
prdDep: string;
devDep: string;
};
home: {
branchDesc: string;
greeting: string;
@ -527,6 +540,253 @@ declare namespace App {
};
creativity: string;
};
function: {
tab: {
tabOperate: {
title: string;
addTab: string;
addTabDesc: string;
closeTab: string;
closeCurrentTab: string;
closeAboutTab: string;
addMultiTab: string;
addMultiTabDesc1: string;
addMultiTabDesc2: string;
};
tabTitle: {
title: string;
changeTitle: string;
change: string;
resetTitle: string;
reset: string;
};
};
multiTab: {
routeParam: string;
backTab: string;
};
toggleAuth: {
toggleAccount: string;
authHook: string;
superAdminVisible: string;
adminVisible: string;
adminOrUserVisible: string;
};
request: {
repeatedErrorOccurOnce: string;
repeatedError: string;
repeatedErrorMsg1: string;
repeatedErrorMsg2: string;
};
};
alova: {
scenes: {
captchaSend: string;
autoRequest: string;
visibilityRequestTips: string;
pollingRequestTips: string;
networkRequestTips: string;
refreshTime: string;
startRequest: string;
stopRequest: string;
requestCrossComponent: string;
triggerAllRequest: string;
};
};
proNaive: {
form: {
basic: {
title: string;
appName: string;
appStatus: string;
createTime: string;
responseDate: string;
specificationInfo: string;
specificate: string;
specificationName: string;
specificationValue: string;
specificationColorRed: string;
specificationColorOrange: string;
addSpecificateItem: string;
fillValue: string;
reset: string;
submit: string;
add: string;
delete: string;
color: string;
normal: string;
anomaly: string;
};
query: {
title1: string;
title2: string;
appName: string;
appStatus: string;
createTime: string;
responseDate: string;
endDate: string;
field: string;
};
step: {
title: string;
step1: {
title: string;
field: string;
nextStep: string;
};
step2: {
title: string;
field: string;
prevStep: string;
submit: string;
};
};
};
table: {
remote: {
filterCondition: string;
name: string;
createTime: string;
responseTime: string;
title: string;
replicableText: string;
tags: string;
dateFormatting: string;
image: string;
};
rowEdit: {
title: string;
reset: string;
submit: string;
edit: string;
delete: string;
save: string;
task: string;
score: string;
time: string;
name: string;
action: string;
};
};
};
manage: {
common: {
status: {
enable: string;
disable: string;
};
};
role: {
title: string;
roleName: string;
roleCode: string;
roleStatus: string;
roleDesc: string;
form: {
roleName: string;
roleCode: string;
roleStatus: string;
roleDesc: string;
};
addRole: string;
editRole: string;
menuAuth: string;
buttonAuth: string;
};
user: {
title: string;
userName: string;
userGender: string;
nickName: string;
userPhone: string;
userEmail: string;
userStatus: string;
userRole: string;
form: {
userName: string;
userGender: string;
nickName: string;
userPhone: string;
userEmail: string;
userStatus: string;
userRole: string;
};
addUser: string;
editUser: string;
gender: {
male: string;
female: string;
};
};
menu: {
home: string;
title: string;
id: string;
parentId: string;
menuType: string;
menuName: string;
routeName: string;
routePath: string;
pathParam: string;
layout: string;
page: string;
i18nKey: string;
icon: string;
localIcon: string;
iconTypeTitle: string;
order: string;
constant: string;
keepAlive: string;
href: string;
hideInMenu: string;
activeMenu: string;
multiTab: string;
fixedIndexInTab: string;
query: string;
button: string;
buttonCode: string;
buttonDesc: string;
menuStatus: string;
form: {
home: string;
menuType: string;
menuName: string;
routeName: string;
routePath: string;
pathParam: string;
layout: string;
page: string;
i18nKey: string;
icon: string;
localIcon: string;
order: string;
keepAlive: string;
href: string;
hideInMenu: string;
activeMenu: string;
multiTab: string;
fixedInTab: string;
fixedIndexInTab: string;
queryKey: string;
queryValue: string;
button: string;
buttonCode: string;
buttonDesc: string;
menuStatus: string;
};
addMenu: string;
editMenu: string;
addChildMenu: string;
type: {
directory: string;
menu: string;
};
iconType: {
iconify: string;
local: string;
};
};
};
};
form: {
required: string;

View File

@ -12,19 +12,45 @@ declare module 'vue' {
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
CountTo: typeof import('./../components/custom/count-to.vue')['default']
CustomIconSelect: typeof import('./../components/custom/custom-icon-select.vue')['default']
DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
GithubLink: typeof import('./../components/custom/github-link.vue')['default']
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
IconCarbonPlay: typeof import('~icons/carbon/play')['default']
IconCarbonStop: typeof import('~icons/carbon/stop')['default']
'IconCharm:download': typeof import('~icons/charm/download')['default']
'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default']
'IconF7:flagCircleFill': typeof import('~icons/f7/flag-circle-fill')['default']
'IconFe:question': typeof import('~icons/fe/question')['default']
'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default']
'IconGg:ratio': typeof import('~icons/gg/ratio')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
'IconIc:roundPlus': typeof import('~icons/ic/round-plus')['default']
'IconIconParkOutline:equalRatio': typeof import('~icons/icon-park-outline/equal-ratio')['default']
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalActivity: typeof import('~icons/local/activity')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalCast: typeof import('~icons/local/cast')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default']
'IconMaterialSymbolsLight:rotate90DegreesCcwOutlineRounded': typeof import('~icons/material-symbols-light/rotate90-degrees-ccw-outline-rounded')['default']
'IconMdi:printer': typeof import('~icons/mdi/printer')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
'IconMingcute:zoomInLine': typeof import('~icons/mingcute/zoom-in-line')['default']
'IconMingcute:zoomOutLine': typeof import('~icons/mingcute/zoom-out-line')['default']
IconUilSearch: typeof import('~icons/uil/search')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
@ -33,17 +59,26 @@ declare module 'vue' {
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import('naive-ui')['NDescriptions']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown']
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NInput: typeof import('naive-ui')['NInput']
@ -56,17 +91,39 @@ declare module 'vue' {
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPagination: typeof import('naive-ui')['NPagination']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover']
NRadio: typeof import('naive-ui')['NRadio']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NStatistic: typeof import('naive-ui')['NStatistic']
NStep: typeof import('naive-ui')['NStep']
NSteps: typeof import('naive-ui')['NSteps']
NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip']
NTree: typeof import('naive-ui')['NTree']
NWatermark: typeof import('naive-ui')['NWatermark']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ProCard: typeof import('pro-naive-ui')['ProCard']
ProConfigProvider: typeof import('pro-naive-ui')['ProConfigProvider']
ProDataTable: typeof import('pro-naive-ui')['ProDataTable']
ProDate: typeof import('pro-naive-ui')['ProDate']
ProEditDataTable: typeof import('pro-naive-ui')['ProEditDataTable']
ProForm: typeof import('pro-naive-ui')['ProForm']
ProFormList: typeof import('pro-naive-ui')['ProFormList']
ProInput: typeof import('pro-naive-ui')['ProInput']
ProSearchForm: typeof import('pro-naive-ui')['ProSearchForm']
ProSelect: typeof import('pro-naive-ui')['ProSelect']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
@ -77,5 +134,6 @@ declare module 'vue' {
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
WebSiteLink: typeof import('./../components/custom/web-site-link.vue')['default']
}
}

View File

@ -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
View 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
View 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>

View 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>

View 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>

View 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: 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View 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>

View 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>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View 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>

View 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>

View File

@ -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>

View 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>

View 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>

View 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;
}

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
interface Props {
id: string;
}
defineProps<Props>();
</script>
<template>
<LookForward />
</template>
<style scoped></style>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View 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>

View 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 }
]
};
}

View 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;
// nodeNXedge
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>

View 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>

View 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
};
}

View 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
}
);
}

View 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[];
}

View 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
}
]
}
]
};

View 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>

View 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'
}
]
}
]
};

View 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>

View 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>

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue';
import Vditor from 'vditor';
import 'vditor/dist/index.css';
import { useThemeStore } from '@/store/modules/theme';
const theme = useThemeStore();
const vditor = ref<Vditor>();
const domRef = ref<HTMLElement>();
function renderVditor() {
if (!domRef.value) return;
vditor.value = new Vditor(domRef.value, {
minHeight: 400,
theme: theme.darkMode ? 'dark' : 'classic',
icon: 'material',
cache: { enable: false }
});
}
const stopHandle = watch(
() => theme.darkMode,
newValue => {
const themeMode = newValue ? 'dark' : 'classic';
vditor.value?.setTheme(themeMode);
}
);
onMounted(() => {
renderVditor();
});
onUnmounted(() => {
stopHandle();
});
</script>
<template>
<div class="h-full">
<NCard title="markdown插件" :bordered="false" class="card-wrapper">
<div ref="domRef"></div>
<template #footer>
<GithubLink link="https://github.com/Vanessa219/vditor" />
</template>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import WangEditor from 'wangeditor';
const editor = ref<WangEditor>();
const domRef = ref<HTMLElement>();
function renderWangEditor() {
editor.value = new WangEditor(domRef.value);
setEditorConfig();
editor.value.create();
}
function setEditorConfig() {
if (editor.value?.config?.zIndex) {
editor.value.config.zIndex = 10;
}
}
onMounted(() => {
renderWangEditor();
});
</script>
<template>
<div class="h-full">
<NCard title="富文本插件" :bordered="false" class="card-wrapper">
<div ref="domRef" class="bg-white dark:bg-dark"></div>
<template #footer>
<GithubLink link="https://github.com/wangeditor-team/wangEditor" />
</template>
</NCard>
</div>
</template>
<style scoped>
:deep(.w-e-toolbar) {
background: inherit !important;
border-color: #999 !important;
}
:deep(.w-e-text-container) {
background: inherit;
border-color: #999 !important;
}
</style>

View File

@ -0,0 +1,198 @@
<script setup lang="tsx">
import { reactive } from 'vue';
import { NButton, NTag } from 'naive-ui';
import { utils, writeFile } from 'xlsx';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import { fetchGetUserList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { isTableColumnHasKey, useNaiveTable } from '@/hooks/common/table';
import { $t } from '@/locales';
const appStore = useAppStore();
const searchParams: Api.SystemManage.UserSearchParams = reactive({
current: 1,
size: 999,
status: null,
userName: null,
userGender: null,
nickName: null,
userPhone: null,
userEmail: null
});
const { columns, data, loading } = useNaiveTable({
api: () => fetchGetUserList(searchParams),
transform: response => {
const { data: list, error } = response;
if (!error) {
return list.records;
}
return [];
},
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>;
}
}
]
});
function exportExcel() {
const exportColumns = columns.value.slice(2);
const excelList = data.value.map(item => exportColumns.map(col => getTableValue(col, item)));
const titleList = exportColumns.map(col => (isTableColumnHasTitle(col) && col.title) || null);
excelList.unshift(titleList);
const workBook = utils.book_new();
const workSheet = utils.aoa_to_sheet(excelList);
workSheet['!cols'] = exportColumns.map(item => ({
width: Math.round(Number(item.width) / 10 || 20)
}));
utils.book_append_sheet(workBook, workSheet, '用户列表');
writeFile(workBook, '用户数据.xlsx');
}
function getTableValue(col: NaiveUI.TableColumn<Api.SystemManage.User>, item: Api.SystemManage.User) {
if (!isTableColumnHasKey(col)) {
return null;
}
const { key } = col;
if (key === 'userRoles') {
return item.userRoles.map(role => role).join(',');
}
if (key === 'status') {
return (item.status && $t(enableStatusRecord[item.status])) || null;
}
if (key === 'userGender') {
return (item.userGender && $t(userGenderRecord[item.userGender])) || null;
}
// @ts-expect-error the key is not in the type of Api.SystemManage.User
return item[key] || null;
}
function isTableColumnHasTitle<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> & {
title: string;
} {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).title);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<NCard title="Excel导出" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<NSpace align="end" wrap justify="end" class="lt-sm:w-200px">
<NButton size="small" ghost type="primary" @click="exportExcel">
<template #icon>
<icon-file-icons:microsoft-excel class="text-icon" />
</template>
导出excel
</NButton>
</NSpace>
</template>
<NDataTable
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="false"
:virtual-scroll="true"
class="sm:h-full"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,173 @@
import type { Task } from 'dhtmlx-gantt';
export const ganttTasks: Task[] = [
{
id: 11,
text: 'Soybean 架构设计',
type: 'project',
progress: 0,
open: true,
start_date: new Date('2024-01-10 00:00'),
duration: 12,
parent: 0
},
{
id: 12,
text: '测试版本',
start_date: new Date('2024-03-20 00:00'),
type: 'project',
duration: 5,
render: 'split',
parent: '11',
progress: 0,
open: true
},
{
id: 99,
text: '测试版本1 发布',
start_date: new Date('2024-03-20 00:00'),
end_date: new Date('2024-03-25 00:00'),
parent: '12',
progress: 0,
open: true
},
{
id: 98,
text: '测试版本2 发布',
start_date: new Date('2024-03-26 00:00'),
duration: 4,
parent: '12',
progress: 0,
open: true
},
{
id: 97,
text: '测试版本3 发布',
start_date: new Date('2024-03-31 00:00'),
duration: 10,
parent: '12',
progress: 0,
open: true
},
{
id: 13,
text: '1.0 版本',
start_date: new Date('2024-03-31 00:00'),
type: 'project',
render: 'split',
parent: '11',
progress: 0.5,
open: false,
duration: 11
},
{
id: 17,
text: '1.0正式发布',
start_date: new Date('2024-03-31 00:00'),
end_date: new Date('2024-04-03 00:00'),
parent: '13',
progress: 0,
open: true
},
{
id: 18,
text: '1.0.1 版本',
start_date: new Date('2024-04-03 00:00'),
duration: 5,
parent: '13',
progress: 0,
open: true
},
{
id: 19,
text: '1.0.2 版本',
start_date: new Date('2024-04-08 00:00'),
duration: 6,
parent: '13',
progress: 0,
open: true
},
{
id: 20,
text: '1.0.3 版本',
start_date: new Date('2024-04-16 00:00'),
duration: 8,
parent: '13',
progress: 0,
open: true
},
{
id: 31,
text: '1.0.4 版本',
start_date: new Date('2024-04-17 00:00'),
duration: 8,
parent: '13',
progress: 0,
open: true
},
{
id: 32,
text: '1.0.5 版本',
start_date: new Date('2024-04-26 00:00'),
duration: 9,
parent: '13',
progress: 0,
open: true
},
{
id: 33,
text: '1.0.9 版本',
start_date: new Date('2024-05-05 00:00'),
duration: 2,
parent: '13',
progress: 0,
open: true
},
{
id: 14,
text: '1.1 版本',
start_date: new Date('2024-05-07 00:00'),
duration: 30,
parent: '11',
progress: 0,
open: true
},
{
id: 15,
text: '1.2 版本',
start_date: new Date('2024-06-06 00:00'),
duration: 46,
parent: '11',
progress: 0,
open: true
},
{
id: 16,
text: '1.3版本',
type: 'project',
render: 'split',
parent: '11',
progress: 0,
open: true,
start_date: new Date('2024-07-22 00:00'),
duration: 11
},
{
id: 21,
text: '1.3.1版本',
start_date: new Date('2024-07-22 00:00'),
duration: 7,
parent: '16',
progress: 0,
open: true
},
{
id: 22,
text: '1.3.2版本',
start_date: new Date('2024-07-29 00:00'),
duration: 7,
parent: '16',
progress: 0,
open: true
}
];

View File

@ -0,0 +1,208 @@
<script setup lang="tsx">
import { onMounted, shallowRef } from 'vue';
import { gantt } from 'dhtmlx-gantt';
import type { GanttConfigOptions, ZoomLevel } from 'dhtmlx-gantt';
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
import { ganttTasks } from './data';
const ganttRef = shallowRef<HTMLElement>();
type TimeType = 'day' | 'week' | 'month' | 'quarter' | 'year';
const timeType = shallowRef<TimeType>('quarter');
interface TimeData {
name: string;
code: TimeType;
}
const data: TimeData[] = [
{
name: '天',
code: 'day'
},
{
name: '周',
code: 'week'
},
{
name: '月',
code: 'month'
},
{
name: '季',
code: 'quarter'
},
{
name: '年',
code: 'year'
}
];
function initGantt() {
if (!ganttRef.value) return;
const config: Partial<GanttConfigOptions> = {
grid_width: 350,
add_column: false,
autofit: false,
row_height: 60,
bar_height: 34,
auto_types: true,
xml_date: '%Y-%m-%d',
columns: [
{
name: 'text',
label: '项目名称',
tree: true,
width: '*'
},
{
name: 'start_date',
label: '开始时间',
align: 'center',
width: 150
}
]
};
Object.assign(gantt.config, config);
gantt.i18n.setLocale('cn');
gantt.init(ganttRef.value);
gantt.parse({ data: ganttTasks });
const zoomLevels: ZoomLevel[] = [
{
name: 'day',
scale_height: 60,
scales: [{ unit: 'day', step: 1, format: '%d %M' }]
},
{
name: 'week',
scale_height: 60,
scales: [
{
unit: 'week',
step: 1,
format(date: Date) {
const dateToStr = gantt.date.date_to_str('%m-%d');
const endDate = gantt.date.add(date, -6, 'day'); //
return `${dateToStr(endDate)}${dateToStr(date)}`;
}
},
{
unit: 'day',
step: 1,
format: '%d',
css(date: Date) {
if (date.getDay() === 0 || date.getDay() === 6) {
return 'day-item weekend weekend-border-bottom';
}
return 'day-item';
}
}
]
},
{
name: 'month',
scale_height: 60,
min_column_width: 18,
scales: [
{ unit: 'month', format: '%Y-%m' },
{
unit: 'day',
step: 1,
format: '%d',
css(date: Date) {
if (date.getDay() === 0 || date.getDay() === 6) {
return 'day-item weekend weekend-border-bottom';
}
return 'day-item';
}
}
]
},
{
name: 'quarter',
height: 60,
min_column_width: 110,
scales: [
{
unit: 'quarter',
step: 1,
format(date: Date) {
const yearStr = `${new Date(date).getFullYear()}`;
const dateToStr = gantt.date.date_to_str('%M');
const endDate = gantt.date.add(gantt.date.add(date, 3, 'month'), -1, 'day');
return `${yearStr + dateToStr(date)} - ${dateToStr(endDate)}`;
}
},
{
unit: 'week',
step: 1,
format(date: Date) {
const dateToStr = gantt.date.date_to_str('%m-%d');
const endDate = gantt.date.add(date, 6, 'day');
return `${dateToStr(date)}${dateToStr(endDate)}`;
}
}
]
},
{
name: 'year',
scale_height: 50,
min_column_width: 150,
scales: [
{ unit: 'year', step: 1, format: '%Y年' },
{ unit: 'month', format: '%Y-%m' }
]
}
];
gantt.ext.zoom.init({
levels: zoomLevels
});
gantt.ext.zoom.setLevel(timeType.value);
}
function changeTime(value: TimeType) {
timeType.value = value;
gantt.ext.zoom.setLevel(value);
}
onMounted(() => {
initGantt();
});
</script>
<template>
<div class="overflow-hidden lt-sm:overflow-auto">
<NCard
title="甘特图演示"
:bordered="false"
size="small"
content-class="overflow-y-hidden overflow-x-auto"
class="h-full card-wrapper"
>
<template #header-extra>
<NTabs
:value="timeType"
type="segment"
animated
size="small"
class="relative w-320px"
@update:value="changeTime"
>
<NTab v-for="item in data" :key="item.code" :name="item.code">
{{ item.name }}
</NTab>
</NTabs>
</template>
<div ref="ganttRef" class="size-full min-w-800px"></div>
</NCard>
</div>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,721 @@
export const basicGanttRecords = [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-15',
progress: 31,
priority: 'P0',
children: [
{
id: 2,
title: 'Project Feature Review',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-07-24',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-25',
end: '2024-07-26',
progress: 100,
priority: 'P1'
},
{
id: 3,
title: 'Project Create',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-27',
end: '2024-07-26',
progress: 100,
priority: 'P1'
},
{
id: 3,
title: 'Develop feature 1',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-08-01',
end: '2024-08-15',
progress: 0,
priority: 'P1'
}
]
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-01',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 100,
priority: 'P1',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-08-01',
end: '2024-08-01',
progress: 90,
priority: 'P0'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-30',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024.07.26',
end: '2024.07.08',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '07.24.2024',
end: '08.04.2024',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-16',
end: '2024-07-18',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-08-09',
end: '2024-09-11',
progress: 100,
priority: 'P1'
}
]
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0',
children: [
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
}
]
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-06',
end: '2024-07-08',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
}
]
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-06',
end: '2024-07-08',
progress: 60,
priority: 'P0'
}
]
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
}
]
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-23',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-30',
end: '2024-08-14',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 60,
priority: 'P0'
}
]
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0',
children: [
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-08-04',
end: '2024-08-04',
progress: 90,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '07/24/2024',
end: '08/04/2024',
progress: 60,
priority: 'P0'
}
]
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-27',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '07.24.2024',
end: '08.04.2024',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-08-09',
end: '2024-09-11',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
}
]
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0',
children: [
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024.07.06',
end: '2024.07.08',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
}
]
}
];
export const linkGanttRecords = [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-15',
end: '2024-07-16',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-16',
end: '2024-07-17',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-18',
end: '2024-07-19',
progress: 90,
priority: 'P0'
},
{
id: 4,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024/07/17',
end: '2024/07/18',
progress: 100,
priority: 'P1'
},
{
id: 5,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '07/19/2024',
end: '07/20/2024',
progress: 60,
priority: 'P0'
},
{
id: 6,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 100,
priority: 'P1'
},
{
id: 7,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 8,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024.07.06',
end: '2024.07.08',
progress: 60,
priority: 'P0'
},
{
id: 9,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024/07/09',
end: '2024/07/11',
progress: 100,
priority: 'P1'
},
{
id: 10,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '07.24.2024',
end: '08.04.2024',
progress: 31,
priority: 'P0'
},
{
id: 11,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 12,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-06',
end: '2024-07-08',
progress: 60,
priority: 'P0'
},
{
id: 13,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-09',
end: '2024-07-11',
progress: 100,
priority: 'P1'
}
];
export const customGanttRecords = [
{
id: 1,
title: 'Project Task 1',
developer: 'bear.xiong',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bear.jpg',
start: '2024-07-24',
end: '2024-07-26',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Project Task 2',
developer: 'wolf.lang',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/wolf.jpg',
start: '07/25/2024',
end: '07/28/2024',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Project Task 3',
developer: 'rabbit.tu',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/rabbit.jpg',
start: '2024-07-28',
end: '2024-08-01',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Project Task 4',
developer: 'cat.mao',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/cat.jpg',
start: '2024-07-31',
end: '2024-08-03',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Project Task 5',
developer: 'bird.niao',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bird.jpeg',
start: '2024-08-02',
end: '2024-08-04',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Project Task 6',
developer: 'flower.hua',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/flower.jpg',
start: '2024-08-03',
end: '2024-08-10',
progress: 100,
priority: 'P1'
}
];

View File

@ -0,0 +1,796 @@
<script setup lang="tsx">
import { onMounted, onUnmounted, shallowRef, watch } from 'vue';
import * as VTableGantt from '@visactor/vtable-gantt';
import * as VTable_editors from '@visactor/vtable-editors';
import { useThemeStore } from '@/store/modules/theme';
import { basicGanttRecords, customGanttRecords, linkGanttRecords } from './data';
const theme = useThemeStore();
const input_editor = new VTable_editors.InputEditor();
const date_input_editor = new VTable_editors.DateInputEditor();
VTableGantt.VTable.register.editor('input', input_editor);
VTableGantt.VTable.register.editor('date-input', date_input_editor);
const basicGanttDomRef = shallowRef<HTMLElement>();
const linkGanttDomRef = shallowRef<HTMLElement>();
const customGanttDomRef = shallowRef<HTMLElement>();
const basicGanttInstance = shallowRef<VTableGantt.Gantt>();
const linkGanttInstance = shallowRef<VTableGantt.Gantt>();
const customGanttInstance = shallowRef<VTableGantt.Gantt>();
const basicGanttColumns = [
{
field: 'title',
title: 'title',
width: 'auto',
sort: true,
tree: true,
editor: 'input'
},
{
field: 'start',
title: 'start',
width: 'auto',
sort: true,
editor: 'date-input'
},
{
field: 'end',
title: 'end',
width: 'auto',
sort: true,
editor: 'date-input'
},
{
field: 'priority',
title: 'priority',
width: 'auto',
sort: true,
editor: 'input'
},
{
field: 'progress',
title: 'progress',
width: 'auto',
sort: true,
headerStyle: {
borderColor: '#e1e4e8'
},
style: {
borderColor: '#e1e4e8',
color: 'green'
},
editor: 'input'
}
];
const basicGanttOption: VTableGantt.GanttConstructorOptions = {
overscrollBehavior: 'none',
records: basicGanttRecords,
taskListTable: {
columns: basicGanttColumns,
tableWidth: 250,
minTableWidth: 100,
maxTableWidth: 600
// rightFrozenColCount: 1
},
frame: {
outerFrameStyle: {
borderLineWidth: 2,
borderColor: '#e1e4e8',
cornerRadius: 8
},
verticalSplitLine: {
lineColor: '#e1e4e8',
lineWidth: 3
},
horizontalSplitLine: {
lineColor: '#e1e4e8',
lineWidth: 3
},
verticalSplitLineMoveable: true,
verticalSplitLineHighlight: {
lineColor: 'green',
lineWidth: 3
}
},
grid: {
// backgroundColor: 'gray',
verticalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
horizontalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
}
},
headerRowHeight: 40,
rowHeight: 40,
taskBar: {
startDateField: 'start',
endDateField: 'end',
progressField: 'progress',
// resizable: false,
moveable: true,
hoverBarStyle: {
barOverlayColor: 'rgba(99, 144, 0, 0.4)'
},
labelText: '{title} {progress}%',
labelTextStyle: {
// padding: 2,
fontFamily: 'Arial',
fontSize: 16,
textAlign: 'left',
textOverflow: 'ellipsis'
},
barStyle: {
width: 20,
/** 任务条的颜色 */
barColor: '#ee8800',
/** 已完成部分任务条的颜色 */
completedBarColor: '#91e8e0',
/** 任务条的圆角 */
cornerRadius: 8
}
},
timelineHeader: {
colWidth: 100,
backgroundColor: '#EEF1F5',
horizontalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
verticalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
scales: [
{
unit: 'week',
step: 1,
startOfWeek: 'sunday',
format(date: any) {
return `Week ${date.dateIndex}`;
},
style: {
fontSize: 20,
fontWeight: 'bold',
color: 'white',
strokeColor: 'black',
textAlign: 'right',
textBaseline: 'bottom',
textStick: true
// padding: [0, 30, 0, 20]
}
},
{
unit: 'day',
step: 1,
format(date: any) {
return date.dateIndex.toString();
},
style: {
fontSize: 20,
fontWeight: 'bold',
color: 'white',
strokeColor: 'black',
textAlign: 'right',
textBaseline: 'bottom'
}
}
]
},
markLine: [
{
content: '',
date: '2024-07-28',
style: {
lineWidth: 1,
lineColor: 'blue',
lineDash: [8, 4]
}
},
{
content: '',
date: '2024-08-17',
style: {
lineWidth: 2,
lineColor: 'red',
lineDash: [8, 4]
}
}
],
rowSeriesNumber: {
title: '行号',
dragOrder: true
},
scrollStyle: {
scrollRailColor: 'RGBA(246,246,246,0.5)',
visible: 'scrolling',
width: 6,
scrollSliderCornerRadius: 2,
scrollSliderColor: '#5cb85c'
}
};
const linkGanttColumns = [
{
field: 'title',
title: 'title',
width: 'auto',
tree: true
},
{
field: 'start',
title: 'start',
width: 'auto',
editor: 'date-input'
},
{
field: 'end',
title: 'end',
width: 'auto',
editor: 'date-input'
},
{
field: 'priority',
title: 'priority',
width: 'auto',
editor: 'input'
},
{
field: 'progress',
title: 'progress',
width: 'auto',
headerStyle: {
borderColor: '#e1e4e8'
},
style: {
borderColor: '#e1e4e8',
color: 'green'
},
editor: 'input'
}
];
const linkGanttOption: VTableGantt.GanttConstructorOptions = {
records: linkGanttRecords,
taskListTable: {
columns: linkGanttColumns,
tableWidth: 400,
minTableWidth: 100,
maxTableWidth: 600
},
dependency: {
links: [
{
type: VTableGantt.TYPES.DependencyType.FinishToStart,
linkedFromTaskKey: 1,
linkedToTaskKey: 2
},
{
type: VTableGantt.TYPES.DependencyType.StartToFinish,
linkedFromTaskKey: 2,
linkedToTaskKey: 3
},
{
type: VTableGantt.TYPES.DependencyType.StartToStart,
linkedFromTaskKey: 3,
linkedToTaskKey: 4
},
{
type: VTableGantt.TYPES.DependencyType.FinishToFinish,
linkedFromTaskKey: 4,
linkedToTaskKey: 5
}
],
// linkSelectable: false,
linkSelectedLineStyle: {
shadowBlur: 5, //
shadowColor: 'red',
lineColor: 'red',
lineWidth: 1
}
},
frame: {
verticalSplitLineMoveable: true,
outerFrameStyle: {
borderLineWidth: 2,
// borderColor: 'red',
cornerRadius: 8
},
verticalSplitLine: {
lineWidth: 3,
lineColor: '#e1e4e8'
},
verticalSplitLineHighlight: {
lineColor: 'green',
lineWidth: 3
}
},
grid: {
// backgroundColor: 'gray',
verticalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
horizontalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
}
},
headerRowHeight: 60,
rowHeight: 40,
taskBar: {
startDateField: 'start',
endDateField: 'end',
progressField: 'progress',
labelText: '{title} {progress}%',
labelTextStyle: {
fontFamily: 'Arial',
fontSize: 16,
textAlign: 'left'
},
barStyle: {
width: 20,
/** 任务条的颜色 */
barColor: '#ee8800',
/** 已完成部分任务条的颜色 */
completedBarColor: '#91e8e0',
/** 任务条的圆角 */
cornerRadius: 10
},
selectedBarStyle: {
shadowBlur: 5, //
shadowOffsetX: 0, // x
shadowOffsetY: 0, // Y
shadowColor: 'black', //
borderColor: 'red', //
borderLineWidth: 1 //
}
},
timelineHeader: {
verticalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
horizontalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
backgroundColor: '#EEF1F5',
colWidth: 60,
scales: [
{
unit: 'week',
step: 1,
startOfWeek: 'sunday',
format(date: any) {
return `Week ${date.dateIndex}`;
},
style: {
fontSize: 20,
fontWeight: 'bold',
color: 'red'
}
},
{
unit: 'day',
step: 1,
format(date: any) {
return date.dateIndex.toString();
},
style: {
fontSize: 20,
fontWeight: 'bold',
color: 'red'
}
}
]
},
minDate: '2024-07-14',
maxDate: '2024-10-15',
rowSeriesNumber: {
title: '行号',
dragOrder: true
},
scrollStyle: {
visible: 'scrolling'
},
overscrollBehavior: 'none'
};
const barColors0 = ['#aecde6', '#c6a49a', '#ffb582', '#eec1de', '#b3d9b3', '#cccccc', '#e59a9c', '#d9d1a5', '#c9bede'];
const barColors = ['#1f77b4', '#8c564b', '#ff7f0e', '#e377c2', '#2ca02c', '#7f7f7f', '#d62728', '#bcbd22', '#9467bd'];
const customGanttColumns: VTableGantt.ColumnsDefine = [
{
field: 'title',
title: 'TASK',
width: '200',
headerStyle: {
textAlign: 'center',
fontSize: 20,
fontWeight: 'bold'
// color: 'black',
// bgColor: '#f0f0fb'
},
style: {
// bgColor: '#f0f0fb'
},
customLayout: (args: any) => {
const { table, row, col, rect } = args;
const taskRecord = table.getCellOriginRecord(col, row);
const { height, width } = rect ?? table.getCellRect(col, row);
const container = new VTableGantt.VRender.Group({
y: 10,
x: 20,
height: height - 20,
width: width - 40,
fill: '#ddd',
display: 'flex',
flexDirection: 'column',
cornerRadius: 30
});
const developer = new VTableGantt.VRender.Text({
text: taskRecord.developer,
fontSize: 16,
fontFamily: 'sans-serif',
fill: barColors[args.row],
fontWeight: 'bold',
maxLineWidth: width - 120,
boundsPadding: [10, 0, 0, 0],
alignSelf: 'center'
});
container.add(developer);
const days = new VTableGantt.VRender.Text({
text: `${VTableGantt.tools.formatDate(new Date(taskRecord.start), 'mm/dd')}-${VTableGantt.tools.formatDate(
new Date(taskRecord.end),
'mm/dd'
)}`,
fontSize: 12,
fontFamily: 'sans-serif',
fontWeight: 'bold',
fill: 'black',
boundsPadding: [10, 0, 0, 0],
alignSelf: 'center'
});
container.add(days);
return {
rootContainer: container,
expectedWidth: 160
};
}
}
];
const customGanttOption: VTableGantt.GanttConstructorOptions = {
records: customGanttRecords,
taskListTable: {
columns: customGanttColumns,
tableWidth: 'auto'
},
frame: {
outerFrameStyle: {
borderLineWidth: 2,
borderColor: '#E1E4E8',
cornerRadius: 8
}
// verticalSplitLineHighlight: {
// lineColor: 'green',
// lineWidth: 3
// }
},
grid: {
// backgroundColor: '#f0f0fb',
// vertical: {
// lineWidth: 1,
// lineColor: '#e1e4e8'
// },
horizontalLine: {
lineWidth: 2,
lineColor: '#d5d9ee'
}
},
headerRowHeight: 60,
rowHeight: 80,
taskBar: {
startDateField: 'start',
endDateField: 'end',
progressField: 'progress',
barStyle: { width: 60 },
customLayout: (args: any) => {
const colorLength = barColors.length;
const { width, height, index, taskDays, progress, taskRecord } = args;
const container = new VTableGantt.VRender.Group({
width,
height,
cornerRadius: 30,
fill: {
gradient: 'linear',
x0: 0,
y0: 0,
x1: 1,
y1: 0,
stops: [
{
offset: 0,
color: barColors0[index % colorLength]
},
{
offset: 0.5,
color: barColors[index % colorLength]
},
{
offset: 1,
color: barColors0[index % colorLength]
}
]
},
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap'
});
const containerLeft = new VTableGantt.VRender.Group({
height,
width: 60,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-around'
// fill: 'red'
});
container.add(containerLeft as any);
const avatar = new VTableGantt.VRender.Image({
width: 50,
height: 50,
image: taskRecord.avatar,
cornerRadius: 25
});
containerLeft.add(avatar);
const containerCenter = new VTableGantt.VRender.Group({
height,
width: width - 120,
display: 'flex',
flexDirection: 'column'
// alignItems: 'left'
});
container.add(containerCenter as any);
const developer = new VTableGantt.VRender.Text({
text: taskRecord.developer,
fontSize: 16,
fontFamily: 'sans-serif',
fill: 'white',
fontWeight: 'bold',
maxLineWidth: width - 120,
boundsPadding: [10, 0, 0, 0]
});
containerCenter.add(developer);
const days = new VTableGantt.VRender.Text({
text: `${taskDays}`,
fontSize: 13,
fontFamily: 'sans-serif',
fill: 'white',
boundsPadding: [10, 0, 0, 0]
});
containerCenter.add(days);
if (width >= 120) {
const containerRight = new VTableGantt.VRender.Group({
cornerRadius: 20,
fill: 'white',
height: 40,
width: 40,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center', //
boundsPadding: [10, 0, 0, 0]
});
container.add(containerRight as any);
const progressText = new VTableGantt.VRender.Text({
text: `${progress}%`,
fontSize: 12,
fontFamily: 'sans-serif',
fill: 'black',
alignSelf: 'center',
fontWeight: 'bold',
maxLineWidth: (width - 60) / 2,
boundsPadding: [0, 0, 0, 0]
});
containerRight.add(progressText);
}
return {
rootContainer: container
// renderDefaultBar: true
// renderDefaultText: true
};
},
hoverBarStyle: {
cornerRadius: 30
}
},
timelineHeader: {
backgroundColor: '#f0f0fb',
colWidth: 80,
// verticalLine: {
// lineColor: 'red',
// lineWidth: 1,
// lineDash: [4, 2]
// },
// horizontalLine: {
// lineColor: 'green',
// lineWidth: 1,
// lineDash: [4, 2]
// },
scales: [
{
unit: 'day',
step: 1,
format(date: any) {
return date.dateIndex.toString();
},
customLayout: (args: any) => {
const { width, height, startDate, dateIndex } = args;
const container = new VTableGantt.VRender.Group({
width,
height,
// fill: '#f0f0fb',
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap'
});
const containerLeft = new VTableGantt.VRender.Group({
height,
width: 30,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-around'
// fill: 'red'
});
container.add(containerLeft as any);
const avatar = new VTableGantt.VRender.Image({
width: 20,
height: 30,
image:
'<svg t="1724675965803" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4299" width="200" height="200"><path d="M53.085678 141.319468C23.790257 141.319468 0 165.035326 0 194.34775L0 918.084273C0 947.295126 23.796789 971.112572 53.085678 971.112572L970.914322 971.112572C1000.209743 971.112572 1024 947.396696 1024 918.084273L1024 194.34775C1024 165.136896 1000.203211 141.319468 970.914322 141.319468L776.827586 141.319468 812.137931 176.629813 812.137931 88.275862C812.137931 68.774506 796.328942 52.965517 776.827586 52.965517 757.32623 52.965517 741.517241 68.774506 741.517241 88.275862L741.517241 176.629813 741.517241 211.940158 776.827586 211.940158 970.914322 211.940158C961.186763 211.940158 953.37931 204.125926 953.37931 194.34775L953.37931 918.084273C953.37931 908.344373 961.25643 900.491882 970.914322 900.491882L53.085678 900.491882C62.813237 900.491882 70.62069 908.306097 70.62069 918.084273L70.62069 194.34775C70.62069 204.087649 62.74357 211.940158 53.085678 211.940158L247.172414 211.940158C266.67377 211.940158 282.482759 196.131169 282.482759 176.629813 282.482759 157.128439 266.67377 141.319468 247.172414 141.319468L53.085678 141.319468ZM211.862069 176.629813C211.862069 196.131169 227.671058 211.940158 247.172414 211.940158 266.67377 211.940158 282.482759 196.131169 282.482759 176.629813L282.482759 88.275862C282.482759 68.774506 266.67377 52.965517 247.172414 52.965517 227.671058 52.965517 211.862069 68.774506 211.862069 88.275862L211.862069 176.629813ZM1024 353.181537 1024 317.871192 988.689655 317.871192 35.310345 317.871192 0 317.871192 0 353.181537 0 441.457399C0 460.958755 15.808989 476.767744 35.310345 476.767744 54.811701 476.767744 70.62069 460.958755 70.62069 441.457399L70.62069 353.181537 35.310345 388.491882 988.689655 388.491882 953.37931 353.181537 953.37931 441.457399C953.37931 460.958755 969.188299 476.767744 988.689655 476.767744 1008.191011 476.767744 1024 460.958755 1024 441.457399L1024 353.181537ZM776.937913 582.62069C796.439287 582.62069 812.248258 566.811701 812.248258 547.310345 812.248258 527.808989 796.439287 512 776.937913 512L247.172414 512C227.671058 512 211.862069 527.808989 211.862069 547.310345 211.862069 566.811701 227.671058 582.62069 247.172414 582.62069L776.937913 582.62069ZM247.172414 688.551724C227.671058 688.551724 211.862069 704.360713 211.862069 723.862069 211.862069 743.363425 227.671058 759.172414 247.172414 759.172414L600.386189 759.172414C619.887563 759.172414 635.696534 743.363425 635.696534 723.862069 635.696534 704.360713 619.887563 688.551724 600.386189 688.551724L247.172414 688.551724ZM776.827586 211.940158 741.517241 176.629813 741.517241 247.328574C741.517241 266.829948 757.32623 282.638919 776.827586 282.638919 796.328942 282.638919 812.137931 266.829948 812.137931 247.328574L812.137931 176.629813 812.137931 141.319468 776.827586 141.319468 247.172414 141.319468C227.671058 141.319468 211.862069 157.128439 211.862069 176.629813 211.862069 196.131169 227.671058 211.940158 247.172414 211.940158L776.827586 211.940158ZM282.482759 176.629813C282.482759 157.128439 266.67377 141.319468 247.172414 141.319468 227.671058 141.319468 211.862069 157.128439 211.862069 176.629813L211.862069 247.328574C211.862069 266.829948 227.671058 282.638919 247.172414 282.638919 266.67377 282.638919 282.482759 266.829948 282.482759 247.328574L282.482759 176.629813Z" fill="#389BFF" p-id="4300"></path></svg>'
});
containerLeft.add(avatar);
const containerCenter = new VTableGantt.VRender.Group({
height,
width: width - 30,
display: 'flex',
flexDirection: 'column'
// alignItems: 'left'
});
container.add(containerCenter as any);
const dayNumber = new VTableGantt.VRender.Text({
text: String(dateIndex).padStart(2, '0'),
fontSize: 20,
fontWeight: 'bold',
fontFamily: 'sans-serif',
fill: '#777',
textAlign: 'right',
maxLineWidth: width - 30,
boundsPadding: [15, 0, 0, 0]
});
containerCenter.add(dayNumber);
const weekDay = new VTableGantt.VRender.Text({
text: VTableGantt.tools.getWeekday(startDate, 'short').toLocaleUpperCase(),
fontSize: 12,
fontFamily: 'sans-serif',
fill: '#777',
boundsPadding: [0, 0, 0, 0]
});
containerCenter.add(weekDay);
return {
rootContainer: container
// renderDefaultText: true
};
}
}
]
},
minDate: '2024-07-20',
maxDate: '2024-08-15',
markLine: [
{
content: '',
date: '2024-07-29',
style: {
lineWidth: 1,
lineColor: 'blue',
lineDash: [8, 4]
}
},
{
content: '',
date: '2024-08-17',
style: {
lineWidth: 2,
lineColor: 'red',
lineDash: [8, 4]
}
}
],
scrollStyle: {
scrollRailColor: 'RGBA(246,246,246,0.5)',
visible: 'focus',
width: 6,
scrollSliderCornerRadius: 2,
scrollSliderColor: '#5cb85c'
}
};
function initVTableGantt() {
basicGanttInstance.value = new VTableGantt.Gantt(basicGanttDomRef.value as HTMLElement, getOption(basicGanttOption));
linkGanttInstance.value = new VTableGantt.Gantt(linkGanttDomRef.value as HTMLElement, getOption(linkGanttOption));
customGanttInstance.value = new VTableGantt.Gantt(
customGanttDomRef.value as HTMLElement,
getOption(customGanttOption)
);
}
function getOption(option: VTableGantt.GanttConstructorOptions) {
const isDark = theme.darkMode;
if (isDark) {
option.taskListTable!.theme = VTableGantt.VTable.themes.DARK;
option.timelineHeader.backgroundColor = '#212121';
option.underlayBackgroundColor = '#000';
} else {
option.taskListTable!.theme = VTableGantt.VTable.themes.DEFAULT;
option.timelineHeader.backgroundColor = '#f0f0fb';
option.underlayBackgroundColor = '#fff';
}
return option;
}
const stopHandle = watch(
() => theme.darkMode,
_newValue => {
basicGanttInstance.value?.release();
linkGanttInstance.value?.release();
customGanttInstance.value?.release();
initVTableGantt();
}
);
onMounted(() => {
initVTableGantt();
});
onUnmounted(() => {
stopHandle();
});
</script>
<template>
<NSpace vertical :size="16">
<NCard :bordered="false" title="VTableGantt" class="h-full card-wrapper">
<WebSiteLink label="More Demos: " link="https://www.visactor.com/vtable/example" />
</NCard>
<NCard :bordered="false" class="h-full card-wrapper">
<div ref="basicGanttDomRef" class="relative h-400px"></div>
</NCard>
<NCard :bordered="false" class="h-full card-wrapper">
<div ref="linkGanttDomRef" class="relative h-400px"></div>
</NCard>
<NCard :bordered="false" class="h-full card-wrapper">
<div ref="customGanttDomRef" class="relative h-400px"></div>
</NCard>
</NSpace>
</template>

View File

@ -0,0 +1,32 @@
export const icons = [
'mdi:emoticon',
'mdi:ab-testing',
'ph:alarm',
'ph:android-logo',
'ph:align-bottom',
'ph:archive-box-light',
'uil:basketball',
'uil:brightness-plus',
'uil:capture',
'mdi:apps-box',
'mdi:alert',
'mdi:airballoon',
'mdi:airplane-edit',
'mdi:alpha-f-box-outline',
'mdi:arm-flex-outline',
'ic:baseline-10mp',
'ic:baseline-access-time',
'ic:baseline-brightness-4',
'ic:baseline-brightness-5',
'ic:baseline-credit-card',
'ic:baseline-filter-1',
'ic:baseline-filter-2',
'ic:baseline-filter-3',
'ic:baseline-filter-4',
'ic:baseline-filter-5',
'ic:baseline-filter-6',
'ic:baseline-filter-7',
'ic:baseline-filter-8',
'ic:baseline-filter-9',
'ic:baseline-filter-9-plus'
];

View File

@ -0,0 +1,51 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { icons } from './icons';
const selectValue = ref('');
const localIcons = ['custom-icon', 'activity', 'at-sign', 'cast', 'chrome', 'copy', 'wind'];
</script>
<template>
<div class="h-full">
<NCard title="Icon组件示例" :bordered="false" class="card-wrapper">
<div class="grid grid-cols-10">
<template v-for="item in icons" :key="item">
<div class="mt-5px flex-x-center">
<SvgIcon :icon="item" class="text-30px" />
</div>
</template>
</div>
<div class="mt-50px">
<h1 class="mb-20px text-18px font-500">Icon图标选择器</h1>
<CustomIconSelect v-model:value="selectValue" :icons="icons" />
</div>
<template #footer>
<WebSiteLink label="iconify地址" link="https://icones.js.org/" class="mt-10px" />
</template>
</NCard>
<NCard title="自定义图标示例" :bordered="false" class="mt-10px card-wrapper">
<div class="pb-12px text-16px">
在src/assets/svg-icon文件夹下的svg文件通过在template里面以 icon - local - {文件名} 直接渲染,
其中icon-local为.env文件里的 VITE_ICON_LOCAL_PREFIX
</div>
<div class="grid grid-cols-10">
<div class="mt-5px flex-x-center">
<icon-local-activity class="text-40px text-success" />
</div>
<div class="mt-5px flex-x-center">
<icon-local-cast class="text-20px text-error" />
</div>
</div>
<div class="py-12px text-16px">通过SvgIcon组件动态渲染, 菜单通过meta的localIcon属性渲染自定义图标</div>
<div class="grid grid-cols-10">
<div v-for="(fileName, index) in localIcons" :key="index" class="mt-5px flex-x-center">
<SvgIcon :local-icon="fileName" class="text-30px text-primary" />
</div>
</div>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { BAIDU_MAP_SDK_URL } from '@/constants/map-sdk';
defineOptions({ name: 'BaiduMap' });
window.HOST_TYPE = '2';
const { load } = useScriptTag(BAIDU_MAP_SDK_URL);
const domRef = ref<HTMLDivElement>();
async function renderMap() {
await load(true);
if (!domRef.value) return;
const map = new BMap.Map(domRef.value);
const point = new BMap.Point(114.05834626586915, 22.546789983033168);
map.centerAndZoom(point, 15);
map.enableScrollWheelZoom();
}
onMounted(() => {
renderMap();
});
</script>
<template>
<div ref="domRef" class="h-full w-full"></div>
</template>
<style scoped></style>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { AMAP_SDK_URL } from '@/constants/map-sdk';
defineOptions({ name: 'GaodeMap' });
const { load } = useScriptTag(AMAP_SDK_URL);
const domRef = ref<HTMLDivElement>();
async function renderMap() {
await load(true);
if (!domRef.value) return;
const map = new AMap.Map(domRef.value, {
zoom: 11,
center: [114.05834626586915, 22.546789983033168],
viewMode: '3D'
});
map.getCenter();
}
onMounted(() => {
renderMap();
});
</script>
<template>
<div ref="domRef" class="h-full w-full"></div>
</template>
<style scoped></style>

View File

@ -0,0 +1,5 @@
import BaiduMap from './baidu-map.vue';
import GaodeMap from './gaode-map.vue';
import TencentMap from './tencent-map.vue';
export { BaiduMap, GaodeMap, TencentMap };

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { TENCENT_MAP_SDK_URL } from '@/constants/map-sdk';
defineOptions({ name: 'TencentMap' });
const { load } = useScriptTag(TENCENT_MAP_SDK_URL);
const domRef = ref<HTMLDivElement | null>(null);
async function renderMap() {
await load(true);
if (!domRef.value) return;
// eslint-disable-next-line no-new
new TMap.Map(domRef.value, {
center: new TMap.LatLng(39.98412, 116.307484),
zoom: 11,
viewMode: '3D'
});
}
onMounted(() => {
renderMap();
});
</script>
<template>
<div ref="domRef" class="h-full w-full"></div>
</template>
<style scoped></style>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import type { Component } from 'vue';
import { BaiduMap, GaodeMap, TencentMap } from './components';
interface Map {
id: string;
label: string;
component: Component;
}
const maps: Map[] = [
{ id: 'gaode', label: '高德地图', component: GaodeMap },
{ id: 'tencent', label: '腾讯地图', component: TencentMap },
{ id: 'baidu', label: '百度地图', component: BaiduMap }
];
</script>
<template>
<div class="h-full">
<NCard title="地图插件" :bordered="false" class="h-full card-wrapper" content-style="overflow:hidden">
<NTabs type="line" class="h-full flex-col-stretch" pane-class="flex-1-hidden">
<NTabPane v-for="item in maps" :key="item.id" :name="item.id" :tab="item.label">
<component :is="item.component" />
</NTabPane>
</NTabs>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,82 @@
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import VuePdfEmbed from 'vue-pdf-embed';
import { useLoading } from '@sa/hooks';
const { loading, endLoading } = useLoading(true);
const pdfRef = shallowRef<InstanceType<typeof VuePdfEmbed> | null>(null);
const source = `https://xiaoxian521.github.io/hyperlink/pdf/Cookie%E5%92%8CSession%E5%8C%BA%E5%88%AB%E7%94%A8%E6%B3%95.pdf`;
const showAllPages = ref(false);
const currentPage = ref<undefined | number>(1);
const pageCount = ref(1);
function onPdfRendered() {
endLoading();
if (pdfRef.value?.doc) {
pageCount.value = pdfRef.value.doc.numPages;
}
}
function showAllPagesChange() {
currentPage.value = showAllPages.value ? undefined : 1;
}
const rotations = [0, 90, 180, 270];
const currentRotation = ref(0);
function handleRotate() {
currentRotation.value = (currentRotation.value + 1) % 4;
}
async function handlePrint() {
await pdfRef.value?.print(undefined, 'test.pdf', true);
}
async function handleDownload() {
await pdfRef.value?.download('test.pdf');
}
</script>
<template>
<div class="overflow-hidden">
<NCard title="PDF 预览" :bordered="false" class="h-full card-wrapper" content-class="overflow-hidden">
<div class="h-full flex-col-stretch">
<GithubLink link="https://github.com/hrynko/vue-pdf-embed" />
<WebSiteLink label="文档地址:" link="https://www.npmjs.com/package/vue-pdf-embed" />
<div class="flex-y-center justify-end gap-12px">
<NCheckbox v-model:checked="showAllPages" @update:checked="showAllPagesChange">显示所有页面</NCheckbox>
<ButtonIcon tooltip-content="旋转90度" @click="handleRotate">
<icon-material-symbols-light:rotate-90-degrees-ccw-outline-rounded />
</ButtonIcon>
<ButtonIcon tooltip-content="打印" @click="handlePrint">
<icon-mdi:printer />
</ButtonIcon>
<ButtonIcon tooltip-content="下载" @click="handleDownload">
<icon-charm:download />
</ButtonIcon>
</div>
<NScrollbar class="flex-1-hidden">
<NSkeleton v-if="loading" size="small" class="mt-12px" text :repeat="12" />
<VuePdfEmbed
ref="pdfRef"
class="container overflow-auto"
:class="{ 'h-0': loading }"
:rotation="rotations[currentRotation]"
:page="currentPage"
:source="source"
@rendered="onPdfRendered"
/>
</NScrollbar>
<div class="flex-y-center justify-between">
<div v-if="showAllPages" class="text-18px font-medium">{{ pageCount }}</div>
<NPagination v-else v-model:page="currentPage" :page-count="pageCount" :page-size="1" />
</div>
</div>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,57 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { html } from 'pinyin-pro';
import domPurify from 'dompurify';
const domRef = ref<HTMLElement | null>(null);
const domRef2 = ref<HTMLElement | null>(null);
const domRef3 = ref<HTMLElement | null>(null);
function renderHtml() {
if (!domRef.value || !domRef2.value || !domRef3.value) return;
const text = 'SoybeanAdmin是一个清新优雅、高颜值且功能强大的后台管理模板';
const code = domPurify.sanitize(html(text));
const code2 = domPurify.sanitize(html(text, { toneType: 'none' }));
domRef.value.innerHTML = code;
domRef2.value.innerHTML = code2;
domRef3.value.innerHTML = code;
}
onMounted(() => {
renderHtml();
});
</script>
<template>
<div>
<NCard title="pinyin 插件" :bordered="false" class="h-full card-wrapper">
<NSpace :vertical="true">
<GithubLink link="https://github.com/zh-lx/pinyin-pro" />
<WebSiteLink label="文档地址:" link="https://pinyin-pro.cn/" />
</NSpace>
<NDivider title-placement="left">常规使用</NDivider>
<p ref="domRef" class="text-18px"></p>
<NDivider title-placement="left">不带音调</NDivider>
<p ref="domRef2" class="text-18px"></p>
<NDivider title-placement="left">自定义样式</NDivider>
<p ref="domRef3" class="custom-style text-18px"></p>
</NCard>
</div>
</template>
<style lang="scss" scoped>
.custom-style {
:deep(.py-result-item) {
.py-chinese-item {
--uno: text-primary;
}
.py-pinyin-item {
--uno: text-error;
}
}
}
</style>

View File

@ -0,0 +1,39 @@
<script lang="ts" setup>
import printJS from 'print-js';
function printTable() {
printJS({
printable: [
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' },
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' }
],
properties: ['name', 'wechat', 'remark'],
type: 'json'
});
}
function printImage() {
printJS({
printable: [
'https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg',
'https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg'
],
type: 'image',
header: 'Multiple Images',
imageStyle: 'width:100%;'
});
}
</script>
<template>
<div class="h-full">
<NCard title="打印" :bordered="false" class="card-wrapper">
<NButton type="primary" class="mr-10px" @click="printTable">打印表格</NButton>
<NButton type="primary" @click="printImage">打印图片</NButton>
<template #footer>
<GithubLink label="printJS" link="https://github.com/crabbly/Print.js" class="mt-10px" />
</template>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,110 @@
<script setup lang="ts">
import SwiperCore from 'swiper';
import { Navigation, Pagination } from 'swiper/modules';
import { Swiper, SwiperSlide } from 'swiper/vue';
import type { SwiperOptions } from 'swiper/types';
type SwiperExampleOptions = Pick<
SwiperOptions,
'navigation' | 'pagination' | 'scrollbar' | 'slidesPerView' | 'slidesPerGroup' | 'spaceBetween' | 'direction' | 'loop'
>;
interface SwiperExample {
id: number;
label: string;
options: Partial<SwiperExampleOptions>;
}
SwiperCore.use([Navigation, Pagination]);
const swiperExample: SwiperExample[] = [
{ id: 0, label: 'Default', options: {} },
{
id: 1,
label: 'Navigation',
options: {
navigation: true
}
},
{
id: 2,
label: 'Pagination',
options: {
pagination: true
}
},
{
id: 3,
label: 'Pagination dynamic',
options: {
pagination: { dynamicBullets: true }
}
},
{
id: 4,
label: 'Pagination progress',
options: {
navigation: true,
pagination: {
type: 'progressbar'
}
}
},
{
id: 5,
label: 'Pagination fraction',
options: {
navigation: true,
pagination: {
type: 'fraction'
}
}
},
{
id: 6,
label: 'Slides per view',
options: {
pagination: {
clickable: true
},
slidesPerView: 3,
spaceBetween: 30
}
},
{
id: 7,
label: 'Infinite loop',
options: {
navigation: true,
pagination: {
clickable: true
},
loop: true
}
}
];
</script>
<template>
<div>
<NCard title="Swiper插件" :bordered="false" class="card-wrapper">
<NSpace :vertical="true">
<GithubLink link="https://github.com/nolimits4web/swiper" />
<WebSiteLink label="vue3版文档地址" link="https://swiperjs.com/vue" />
<WebSiteLink label="插件demo地址" link="https://swiperjs.com/demos" />
</NSpace>
<NSpace :vertical="true">
<div v-for="item in swiperExample" :key="item.id">
<h3 class="py-24px text-24px font-bold">{{ item.label }}</h3>
<Swiper v-bind="item.options">
<SwiperSlide v-for="i in 5" :key="i">
<div class="h-240px w-full flex-center border-1px border-#999 text-18px font-bold">Slide{{ i }}</div>
</SwiperSlide>
</Swiper>
</div>
</NSpace>
</NCard>
</div>
</template>
<style scoped></style>

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More