19 | {approveNodes.map((node: HtmlNodeConfig) => {
20 | return (
21 |
22 |
{
25 | props.lf.value.dnd.startDrag({
26 | type: node.type,
27 | text: node.label
28 | })
29 | }}
30 | />
31 |
{node.label}
32 |
33 | )
34 | })}
35 |
36 | )
37 | }
38 | })
39 |
--------------------------------------------------------------------------------
/src/components/LogicFlow/src/Approve/type.ts:
--------------------------------------------------------------------------------
1 | export type HtmlNodeConfig = {
2 | type: string
3 | label: string
4 | style: {
5 | width: string
6 | height: string
7 | borderRadius?: string
8 | border: string
9 | transform?: string
10 | }
11 | }
12 | export type IApproveUser = {
13 | label: string
14 | value: string
15 | }
16 | export type nodeProperty = {
17 | labelColor: string
18 | approveTypeLabel: string
19 | approveType: string
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/LogicFlow/src/Bpmn/index.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, onMounted, ref } from 'vue'
2 | import LogicFlow from '@logicflow/core'
3 | import { BpmnElement, Control, Menu, SelectionSelect } from '@logicflow/extension'
4 | import BpmnPattern from './components/Pattern'
5 | import '@logicflow/extension/lib/style/index.css'
6 | import '@logicflow/core/dist/style/index.css'
7 |
8 | const config = {
9 | stopScrollGraph: true,
10 | stopZoomGraph: true,
11 | metaKeyMultipleSelected: true,
12 | grid: {
13 | size: 10,
14 | type: 'dot'
15 | },
16 | keyboard: {
17 | enabled: true
18 | },
19 | snapline: true
20 | }
21 |
22 | export default defineComponent({
23 | name: 'Bpmn',
24 | setup() {
25 | const lf = ref(null) as unknown as LogicFlow
26 |
27 | onMounted(() => {
28 | LogicFlow.use(BpmnElement)
29 | LogicFlow.use(Control)
30 | LogicFlow.use(Menu)
31 | LogicFlow.use(SelectionSelect)
32 | const lfInstance = new LogicFlow({
33 | ...config,
34 | container: document.querySelector('#graphBpmn') as HTMLElement
35 | })
36 | lfInstance.render()
37 | lf.value = lfInstance
38 | })
39 |
40 | return () => (
41 |
42 |
43 |
{lf.value && }
44 |
45 | )
46 | }
47 | })
48 |
--------------------------------------------------------------------------------
/src/components/Menu/index.ts:
--------------------------------------------------------------------------------
1 | import Menu from './src'
2 |
3 | export { Menu }
4 |
--------------------------------------------------------------------------------
/src/components/Menu/src/components/MenuItemCont.tsx:
--------------------------------------------------------------------------------
1 | import type { CSSProperties } from 'vue'
2 | import { defineComponent, computed, unref } from 'vue'
3 | import { useI18n } from 'vue-i18n'
4 | import { menuItemContentProps } from '../props'
5 | import SvgIcon from '@/components/SvgIcon'
6 |
7 | export default defineComponent({
8 | name: 'MenuItemCont',
9 | components: { SvgIcon },
10 | props: menuItemContentProps,
11 | setup(props) {
12 | const { t } = useI18n()
13 |
14 | const getIcon = computed(() => props.item?.icon as string)
15 | const getName = computed(() => props.item?.name)
16 | const getHideName = computed(() => props.collapsed && !props.showTitle)
17 | const getContStyle = computed(
18 | (): CSSProperties => ({
19 | marginLeft: '8px',
20 | transition: 'all 0.3s ease'
21 | })
22 | )
23 |
24 | return () => (
25 |
31 | )
32 | }
33 | })
34 |
--------------------------------------------------------------------------------
/src/components/Menu/src/components/MenuItems.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { MenuItem } from 'ant-design-vue'
3 |
4 | import { menuItemProps } from '../props'
5 | import MenuItemCont from './MenuItemCont'
6 |
7 | export default defineComponent({
8 | name: 'MenuItems',
9 | props: menuItemProps,
10 | render() {
11 | const { item, collapsed, showTitle } = this
12 | return (
13 |
16 | )
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/src/components/Menu/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface MenuState {
2 | // array with the keys of currently opened sub menus
3 | openKeys: string[]
4 |
5 | // array with the keys of currently selected menu items
6 | selectedKeys: string[]
7 |
8 | // array with the keys of currently opened sub menus with collapsed status
9 | collapsedOpenKeys: string[]
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/Page/index.ts:
--------------------------------------------------------------------------------
1 | import PageWrapper from './src/PageWrapper'
2 |
3 | export { PageWrapper }
4 |
--------------------------------------------------------------------------------
/src/components/Page/src/compo.module.less:
--------------------------------------------------------------------------------
1 | .compo_page-wrapper {
2 |
3 | .page-header {
4 | width: 100%;
5 | min-height: 48px;
6 | padding: 16px 24px;
7 | margin-bottom: 12px;
8 | box-sizing: border-box;
9 |
10 | &-name {
11 | margin-bottom: 4px;
12 | font-size: 16px;
13 | font-weight: 600;
14 |
15 | svg {
16 | margin-right: 6px;
17 | }
18 | }
19 |
20 | }
21 |
22 | .page-content {
23 | min-height: 420px
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/RichText/index.ts:
--------------------------------------------------------------------------------
1 | import RichTextInput from './src/RichTextInput'
2 | import RichTextSetting from './src/RichTextSetting'
3 |
4 | export { RichTextInput, RichTextSetting }
5 |
--------------------------------------------------------------------------------
/src/components/Scrollbar/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description referenced from element-ui/scrollbar
3 | * @link see: https://github.com/ElemeFE/element/tree/dev/packages/scrollbar
4 | */
5 |
6 | import Scrollbar from './src'
7 |
8 | export default Scrollbar
9 |
--------------------------------------------------------------------------------
/src/components/Scrollbar/src/index.less:
--------------------------------------------------------------------------------
1 | .scrollbar {
2 | position: relative;
3 | height: 100%;
4 | overflow: hidden;
5 |
6 | &__wrap {
7 | height: 100%;
8 | overflow: auto;
9 |
10 | &--hidden-default {
11 | scrollbar-width: none;
12 |
13 | &::-webkit-scrollbar {
14 | display: none;
15 | width: 0;
16 | height: 0;
17 | opacity: 0%;
18 | }
19 | }
20 | }
21 |
22 | &__thumb {
23 | position: relative;
24 | display: block;
25 | width: 0;
26 | height: 0;
27 | cursor: pointer;
28 | background-color: rgb(144 147 153 / 30%);
29 | border-radius: inherit;
30 | transition: 0.3s background-color;
31 |
32 | &:hover {
33 | background-color: rgb(144 147 153 / 50%);
34 | }
35 | }
36 |
37 | &__bar {
38 | position: absolute;
39 | right: 2px;
40 | bottom: 2px;
41 | z-index: 1;
42 | border-radius: 4px;
43 | opacity: 0%;
44 | transition: opacity 80ms ease;
45 |
46 | &.is-vertical {
47 | top: 2px;
48 | width: 6px;
49 |
50 | & > div {
51 | width: 100%;
52 | }
53 | }
54 |
55 | &.is-horizontal {
56 | left: 2px;
57 | height: 6px;
58 |
59 | & > div {
60 | height: 100%;
61 | }
62 | }
63 | }
64 |
65 | &:active,
66 | &:focus,
67 | &:hover {
68 | > .scrollbar__bar {
69 | opacity: 100%;
70 | transition: opacity 340ms ease-out;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/Scrollbar/src/types.ts:
--------------------------------------------------------------------------------
1 | interface BarMapItem {
2 | offset: string
3 | scroll: string
4 | scrollSize: string
5 | size: string
6 | key: string
7 | axis: string
8 | client: string
9 | direction: string
10 | }
11 |
12 | export interface BarMap {
13 | vertical: BarMapItem
14 | horizontal: BarMapItem
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Scrollbar/src/util.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import type { BarMap } from './types'
3 |
4 | export const BAR_MAP: BarMap = {
5 | vertical: {
6 | offset: 'offsetHeight',
7 | scroll: 'scrollTop',
8 | scrollSize: 'scrollHeight',
9 | size: 'height',
10 | key: 'vertical',
11 | axis: 'Y',
12 | client: 'clientY',
13 | direction: 'top'
14 | },
15 | horizontal: {
16 | offset: 'offsetWidth',
17 | scroll: 'scrollLeft',
18 | scrollSize: 'scrollWidth',
19 | size: 'width',
20 | key: 'horizontal',
21 | axis: 'X',
22 | client: 'clientX',
23 | direction: 'left'
24 | }
25 | }
26 |
27 | // @ts-ignore
28 | export function renderThumbStyle({ move, size, bar }) {
29 | const style = {} as any
30 | const translate = `translate${bar.axis}(${move}%)`
31 |
32 | style[bar.size] = size
33 | style.transform = translate
34 | style.msTransform = translate
35 | style.webkitTransform = translate
36 |
37 | return style
38 | }
39 |
40 | function extend
(to: T, from: K): T & K {
41 | return Object.assign(to, from)
42 | }
43 |
44 | export function toObject(arr: Array): Recordable {
45 | const res = {}
46 | for (let i = 0; i < arr.length; i++) {
47 | if (arr[i]) {
48 | extend(res, arr[i])
49 | }
50 | }
51 | return res
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/SvgIcon/index.module.less:
--------------------------------------------------------------------------------
1 | .svg-icon {
2 | display: inline-block;
3 | vertical-align: -0.15em;
4 | fill: currentColor;
5 | overflow: hidden;
6 | }
--------------------------------------------------------------------------------
/src/components/SvgIcon/index.tsx:
--------------------------------------------------------------------------------
1 | import type { CSSProperties } from 'vue'
2 | import { defineComponent, computed, unref } from 'vue'
3 | import styles from './index.module.less'
4 |
5 | export default defineComponent({
6 | name: 'SvgIcon',
7 | props: {
8 | prefix: {
9 | type: String,
10 | default: 'icon'
11 | },
12 | name: {
13 | type: String,
14 | required: true
15 | },
16 | size: {
17 | type: [Number, String],
18 | default: 16
19 | }
20 | },
21 | setup(props) {
22 | const symbolId = computed(() => `#${props.prefix}-${props.name}`)
23 | const iconStyle = computed((): CSSProperties => {
24 | let size = `${props.size}`
25 | size = `${size.replace('px', '')}px`
26 | return {
27 | width: size,
28 | height: size
29 | }
30 | })
31 |
32 | return () => (
33 |
36 | )
37 | }
38 | })
39 |
--------------------------------------------------------------------------------
/src/components/Table/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baimingxuan/vue3-admin-design/d368ff2f1680414ae9b9fed93ced7cfba8b76b7f/src/components/Table/index.ts
--------------------------------------------------------------------------------
/src/components/Table/src/compoMap.ts:
--------------------------------------------------------------------------------
1 | import type { Component } from 'vue'
2 | import {
3 | AutoComplete,
4 | Checkbox,
5 | DatePicker,
6 | Input,
7 | InputNumber,
8 | Radio,
9 | Select,
10 | Switch,
11 | TimePicker
12 | } from 'ant-design-vue'
13 | import { ApiCheckboxGroup, ApiRadioGroup, ApiSelect, ApiTreeSelect } from '../../ApiCompo'
14 |
15 | export const compoMap = new Map([
16 | ['AutoComplete', AutoComplete],
17 | ['Checkbox', Checkbox],
18 | ['CheckboxGroup', Checkbox.Group],
19 | ['ApiCheckboxGroup', ApiCheckboxGroup],
20 | ['DatePicker', DatePicker],
21 | ['RangePicker', DatePicker.RangePicker],
22 | ['Input', Input],
23 | ['InputPassword', Input.Password],
24 | ['InputTextArea', Input.TextArea],
25 | ['InputNumber', InputNumber],
26 | ['RadioGroup', Radio.Group],
27 | ['ApiRadioGroup', ApiRadioGroup],
28 | ['Select', Select],
29 | ['ApiSelect', ApiSelect],
30 | ['Switch', Switch],
31 | ['TimePicker', TimePicker],
32 | ['TimeRangePicker', TimePicker.TimeRangePicker],
33 | ['ApiTreeSelect', ApiTreeSelect]
34 | ])
35 |
--------------------------------------------------------------------------------
/src/components/Table/src/components/EditableCell/index.module.less:
--------------------------------------------------------------------------------
1 | .editable-cell {
2 | position: relative;
3 |
4 | &__wrapper {
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 |
9 | > .ant-select {
10 | min-width: calc(100% - 50px);
11 | }
12 | }
13 |
14 | &__action {
15 | &-icon {
16 | &:hover {
17 | transform: scale(1.2);
18 |
19 | svg {
20 | color: @primary-color;
21 | }
22 | }
23 | }
24 | }
25 |
26 | .ellipsis-cell {
27 | .cell-content {
28 | overflow-wrap: break-word;
29 | word-break: break-word;
30 | overflow: hidden;
31 | white-space: nowrap;
32 | text-overflow: ellipsis;
33 | }
34 | }
35 |
36 | &__normal {
37 | &-icon {
38 | position: absolute;
39 | top: 4px;
40 | right: 0;
41 | display: none;
42 | width: 20px;
43 | cursor: pointer;
44 | }
45 | }
46 |
47 | &:hover {
48 | .@{prefix-cls}__normal-icon {
49 | display: inline-block;
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/Table/src/components/TableAction.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baimingxuan/vue3-admin-design/d368ff2f1680414ae9b9fed93ced7cfba8b76b7f/src/components/Table/src/components/TableAction.tsx
--------------------------------------------------------------------------------
/src/components/Table/src/constant.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baimingxuan/vue3-admin-design/d368ff2f1680414ae9b9fed93ced7cfba8b76b7f/src/components/Table/src/constant.ts
--------------------------------------------------------------------------------
/src/components/Table/src/helper.ts:
--------------------------------------------------------------------------------
1 | export function generatePlaceholder(component: any) {
2 | if (
3 | component.includes('Select') ||
4 | component.includes('Picker') ||
5 | component.includes('Cascader') ||
6 | component.includes('Checkbox') ||
7 | component.includes('Radio') ||
8 | component.includes('Switch')
9 | ) {
10 | return '请选择'
11 | } else if (component.includes('Input')) {
12 | return '请输入'
13 | } else {
14 | return ''
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Table/src/hooks/useTableContext.ts:
--------------------------------------------------------------------------------
1 | import type { Ref } from 'vue'
2 | import type { BasicTableProps, TableActionType } from '../types/table'
3 | import { provide, inject, type ComputedRef } from 'vue'
4 |
5 | const key = Symbol('basic-table')
6 |
7 | type Instance = TableActionType & {
8 | wrapRef: Ref>
9 | getBindValues: ComputedRef
10 | }
11 |
12 | type RetInstance = Omit & {
13 | getBindValues: ComputedRef
14 | }
15 |
16 | export function createTableContext(instance: Instance) {
17 | provide(key, instance)
18 | }
19 |
20 | export function useTableContext(): RetInstance {
21 | return inject(key) as RetInstance
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/Table/src/index.module.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baimingxuan/vue3-admin-design/d368ff2f1680414ae9b9fed93ced7cfba8b76b7f/src/components/Table/src/index.module.less
--------------------------------------------------------------------------------
/src/components/Table/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from 'ant-design-vue'
2 |
--------------------------------------------------------------------------------
/src/components/Table/src/props.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baimingxuan/vue3-admin-design/d368ff2f1680414ae9b9fed93ced7cfba8b76b7f/src/components/Table/src/props.ts
--------------------------------------------------------------------------------
/src/components/Table/src/types/column.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from 'ant-design-vue/lib/form/interface'
2 | import type { ComponentProps, ComponentType } from './'
3 |
4 | export interface EditColumnType {
5 | editComponent?: ComponentType
6 | editRule?: Rule[]
7 | editComponentProps?: ComponentProps
8 | editRow?: boolean
9 | editable?: boolean
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/Table/src/types/row.ts:
--------------------------------------------------------------------------------
1 | import type { Ref } from 'vue'
2 |
3 | export type EditRecordRow = Partial<
4 | {
5 | onEdit: (editable: boolean, submit?: boolean) => Promise
6 | onValid: () => Promise
7 | editable: boolean
8 | onCancel: Fn
9 | onSubmit: Fn
10 | submitCbs: Fn[]
11 | cancelCbs: Fn[]
12 | validCbs: Fn[]
13 | editValueRefs: Recordable[
14 | } & T
15 | >
16 |
--------------------------------------------------------------------------------
/src/components/Upload/index.ts:
--------------------------------------------------------------------------------
1 | import UploadImage from './src/UploadImage'
2 |
3 | export { UploadImage }
4 |
--------------------------------------------------------------------------------
/src/components/VueDrr/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baimingxuan/vue3-admin-design/d368ff2f1680414ae9b9fed93ced7cfba8b76b7f/src/components/VueDrr/index.ts
--------------------------------------------------------------------------------
/src/components/VueDrr/src/dom.ts:
--------------------------------------------------------------------------------
1 | import { isFunction } from '@/utils/is'
2 |
3 | export function matchesSelectorToParentElements(el, selector, baseNode) {
4 | let node = el
5 |
6 | const matchesSelectorFunc = [
7 | 'matches',
8 | 'webkitMatchesSelector',
9 | 'mozMatchesSelector',
10 | 'msMatchesSelector',
11 | 'oMatchesSelector'
12 | ].find(func => isFunction(node[func]))
13 |
14 | if (!isFunction(node[matchesSelectorFunc!])) return false
15 |
16 | do {
17 | if (node[matchesSelectorFunc!](selector)) return true
18 | if (node === baseNode) return false
19 | node = node.parentNode
20 | } while (node)
21 |
22 | return false
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/VueDrr/src/index.tsx:
--------------------------------------------------------------------------------
1 | import type { CSSProperties } from 'vue'
2 | import { defineComponent, reactive, unref, computed } from 'vue'
3 | import { props } from './props'
4 |
5 | export default defineComponent({
6 | name: 'VueDrr',
7 | props,
8 | setup(props) {
9 | const config = reactive({
10 | top: props.y,
11 | left: props.x,
12 | width: props.w,
13 | height: props.h,
14 | rotateAngle: props.angle,
15 | enabled: props.active,
16 | zIndex: props.z,
17 | resizing: false,
18 | dragging: false,
19 | rotating: false,
20 | handle: null,
21 | parentW: 9999,
22 | parentH: 9999,
23 | mouseX: 0,
24 | mouseY: 0,
25 | lastMouseX: 0,
26 | lastMouseY: 0,
27 | mouseOffX: 0,
28 | mouseOffY: 0,
29 | elmX: 0,
30 | elmY: 0,
31 | elmW: 0,
32 | elmH: 0
33 | })
34 |
35 | const getWrapStyle = computed(
36 | (): CSSProperties => ({
37 | top: config.top + 'px',
38 | left: config.left + 'px',
39 | width: config.width + 'px',
40 | height: config.height + 'px',
41 | transform: 'rotate(' + config.rotateAngle + 'deg)',
42 | zIndex: config.zIndex as CSSProperties['zIndex'],
43 | overflowY: props.overflowY
44 | })
45 | )
46 |
47 | return () =>
48 | }
49 | })
50 |
--------------------------------------------------------------------------------
/src/components/Widgets/index.ts:
--------------------------------------------------------------------------------
1 | import Search from './src/Search'
2 | import Refresh from './src/Refresh'
3 | import FullScreen from './src/FullScreen'
4 |
5 | export { Search, Refresh, FullScreen }
6 |
--------------------------------------------------------------------------------
/src/components/Widgets/src/FullScreen.tsx:
--------------------------------------------------------------------------------
1 | import type { PropType } from 'vue'
2 | import type { MaybeElementRef } from '@vueuse/core'
3 | import { defineComponent, unref } from 'vue'
4 | import { Tooltip } from 'ant-design-vue'
5 | import { ExpandOutlined, CompressOutlined } from '@ant-design/icons-vue'
6 | import { useFullscreen } from '@vueuse/core'
7 | import { useI18n } from 'vue-i18n'
8 |
9 | export default defineComponent({
10 | name: 'FullScreen',
11 | props: {
12 | targetElement: {
13 | type: Object as PropType,
14 | default: document.body
15 | }
16 | },
17 | setup(props) {
18 | const { t } = useI18n()
19 | const { isFullscreen, toggle } = useFullscreen(props.targetElement)
20 |
21 | return () => (
22 |
23 |
30 | {!unref(isFullscreen) ? : }
31 |
32 |
33 | )
34 | }
35 | })
36 |
--------------------------------------------------------------------------------
/src/components/Widgets/src/Refresh.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Tooltip } from 'ant-design-vue'
3 | import { SyncOutlined } from '@ant-design/icons-vue'
4 | import { useI18n } from 'vue-i18n'
5 |
6 | export default defineComponent({
7 | name: 'Refresh',
8 | emits: ['refresh'],
9 | setup(_, { emit }) {
10 | const { t } = useI18n()
11 |
12 | return () => (
13 | emit('refresh')}>
14 |
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/src/components/Widgets/src/Search.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Tooltip } from 'ant-design-vue'
3 | import { SearchOutlined } from '@ant-design/icons-vue'
4 | import { useI18n } from 'vue-i18n'
5 |
6 | export default defineComponent({
7 | name: 'Search',
8 | emits: ['toggle'],
9 | setup(_, { emit }) {
10 | const { t } = useI18n()
11 |
12 | return () => (
13 | emit('toggle')}>
14 |
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/src/design/antd/index.less:
--------------------------------------------------------------------------------
1 | @import './menu.less';
2 |
3 | .ant-drawer {
4 | &-body {
5 | padding: 0 !important;
6 | }
7 | }
8 |
9 | .ant-layout-sider{
10 |
11 | &.ant-layout-sider-dark {
12 | background: @primary-dark-bg;
13 | }
14 | }
15 |
16 | .ant-btn-link {
17 | padding: 4px 6px;
18 |
19 | &:hover,
20 | &:focus {
21 | border: solid 1px transparent !important;
22 | }
23 | }
24 |
25 | .ant-table-thead {
26 |
27 | .sub-title {
28 | font-size: 12px;
29 | color: rgba(0, 0, 0, .4);
30 | }
31 | }
32 |
33 | .ant-layout-header {
34 | padding: 0 !important;
35 | }
36 |
37 | .ant-upload {
38 | width: 100% !important;
39 | }
40 |
41 | .ant-tag {
42 |
43 | &.ant-tag-checkable-checked {
44 | border: none;
45 | color: @white !important;
46 |
47 | & .tag-dot {
48 | background: @white;
49 | }
50 |
51 | & .anticon-close {
52 | color: @white;
53 | }
54 | }
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/src/design/antd/menu.less:
--------------------------------------------------------------------------------
1 | .ant-menu {
2 | &.ant-menu-horizontal {
3 | line-height: 46px !important;
4 | border-bottom: 0 !important;
5 | }
6 |
7 | &.ant-menu-dark {
8 | background: @primary-dark-bg !important;
9 |
10 | .ant-menu-sub {
11 | background: @submenu-dark-bg !important;
12 | }
13 | }
14 |
15 | .submenu-collapsed {
16 | padding-bottom: 0 !important;
17 | }
18 |
19 | .menu-item-collapsed {
20 | height: auto !important;
21 | padding-bottom: 0 !important;
22 | padding-inline: 0 !important;
23 | }
24 |
25 | .submenu-collapsed .ant-menu-submenu-title,
26 | .menu-item-collapsed .ant-menu-title-content {
27 | display: block;
28 | height: auto !important;
29 | line-height: initial !important;
30 | padding: 12px 0 !important;
31 | margin: 0;
32 | text-align: center !important;
33 | transition: all 0.2s ease;
34 | }
35 |
36 | .submenu-collapsed,
37 | .menu-item-collapsed {
38 |
39 | .menu-item-cont {
40 | display: block;
41 |
42 | .svg-icon {
43 | vertical-align: 0;
44 | margin-bottom: 6px;
45 | transition: all 0.2s;
46 | }
47 |
48 | &__name {
49 | display: block;
50 | height: 22px;
51 | line-height: 22px;
52 | margin-left: 0 !important;
53 | transition: all 0.2s !important;
54 | }
55 | }
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/src/design/plugin.less:
--------------------------------------------------------------------------------
1 | // 视频播放器
2 | .video-js {
3 | .vjs-big-play-button {
4 | top: 45% !important;
5 | left: 45% !important;
6 | font-size: 2em !important;
7 | }
8 | }
9 |
10 | // 富文本编辑器
11 | .rich-text-wrapper {
12 | border: 1px solid #ccc;
13 |
14 | .toolbar-box {
15 | border-bottom: 1px solid #ccc;
16 | }
17 | }
18 |
19 | // 拖拽组件
20 | .vdr {
21 | .vdr-stick {
22 | border-radius: 4px;
23 | }
24 | }
25 |
26 | // 组织树
27 | .zm-tree-org {
28 | height: calc(100% - 32px) !important;
29 | padding: 14px 0 0 !important;
30 | }
31 |
32 | // nprogress
33 | #nprogress {
34 | pointer-events: none;
35 |
36 | .bar {
37 | position: fixed;
38 | z-index: 99999;
39 | top: 0;
40 | left: 0;
41 | width: 100%;
42 | height: 2px;
43 | opacity: 0.75;
44 | background-color: @primary-color;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/design/public.less:
--------------------------------------------------------------------------------
1 | span.icon-btn {
2 | display: flex;
3 | align-items: center;
4 | cursor: pointer;
5 | margin-left: 10px;
6 | }
7 |
8 | .batch-upload {
9 | height: auto!important;
10 | }
11 |
12 | .list-upload {
13 | .ant-upload {
14 | width: 100%;
15 | }
16 |
17 | .ant-btn {
18 | display: block;
19 | margin: 0 auto 8px;
20 | }
21 | }
22 |
23 | .feature-divider {
24 | height: 50%;
25 | margin: 0;
26 | border-color: rgba(0, 0, 0, .2);
27 | }
28 |
29 | .ant-layout-sider-light {
30 | border-right: solid 1px @border-color;
31 |
32 | .ant-menu-inline {
33 | border-right: none!important;
34 | }
35 | }
36 |
37 | // ScrollContainer组件样式
38 | .scroll-container {
39 | width: 100%;
40 | height: 100%;
41 |
42 | .scrollbar__wrap {
43 | margin-bottom: 18px !important;
44 | overflow-x: hidden;
45 | }
46 |
47 | .scrollbar__view {
48 | box-sizing: border-box;
49 | }
50 | }
51 |
52 | // Iframe container 样式
53 | .iframe-wrapper {
54 | background: @white;
55 | border-radius: 4px;
56 | box-sizing: border-box;
57 |
58 | .ant-spin-nested-loading {
59 |
60 | & >div>.ant-spin {
61 | max-height: 100%
62 | }
63 | }
64 | }
65 |
66 |
--------------------------------------------------------------------------------
/src/design/scroll-bar.less:
--------------------------------------------------------------------------------
1 | // Customize the scroll-bar style
2 | &::-webkit-scrollbar-track-piece {
3 | background-color: rgba(0, 0, 0, 0);
4 | border-left: 1px solid rgba(0, 0, 0, 0);
5 | }
6 |
7 | &::-webkit-scrollbar {
8 | width: 6px;
9 | height: 6px;
10 | -webkit-border-radius: 3px;
11 | -moz-border-radius: 3px;
12 | border-radius: 3px;
13 | }
14 |
15 | &::-webkit-scrollbar-thumb {
16 | background-color: rgba(0, 0, 0, 0.2);
17 | background-clip: padding-box;
18 | -webkit-border-radius: 3px;
19 | -moz-border-radius: 3px;
20 | border-radius: 3px;
21 | min-height: 28px;
22 | }
23 |
24 | &::-webkit-scrollbar-thumb:hover {
25 | background-color: rgba(0, 0, 0, 0.3);
26 | -webkit-border-radius: 3px;
27 | -moz-border-radius: 3px;
28 | border-radius: 3px;
29 | }
30 |
--------------------------------------------------------------------------------
/src/design/transition/base.less:
--------------------------------------------------------------------------------
1 | .transition-default() {
2 | &-enter-active,
3 | &-leave-active {
4 | transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1) !important;
5 | }
6 |
7 | &-move {
8 | transition: transform 0.4s;
9 | }
10 | }
11 |
12 | .expand-transition {
13 | .transition-default();
14 | }
15 |
16 | .expand-x-transition {
17 | .transition-default();
18 | }
19 |
--------------------------------------------------------------------------------
/src/design/transition/index.less:
--------------------------------------------------------------------------------
1 | @import './base.less';
2 | @import './fade.less';
3 | @import './scale.less';
4 | @import './scroll.less';
5 | @import './slide.less';
6 | @import './zoom.less';
7 |
8 | .collapse-transition {
9 | transition:
10 | 0.2s height ease-in-out,
11 | 0.2s padding-top ease-in-out,
12 | 0.2s padding-bottom ease-in-out;
13 | }
14 |
--------------------------------------------------------------------------------
/src/design/transition/scale.less:
--------------------------------------------------------------------------------
1 | .scale-transition {
2 | .transition-default();
3 |
4 | &-enter-from,
5 | &-leave,
6 | &-leave-to {
7 | transform: scale(0);
8 | opacity: 0;
9 | }
10 | }
11 |
12 | .scale-rotate-transition {
13 | .transition-default();
14 |
15 | &-enter-from,
16 | &-leave,
17 | &-leave-to {
18 | transform: scale(0) rotate(-45deg);
19 | opacity: 0;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/design/transition/scroll.less:
--------------------------------------------------------------------------------
1 | .scroll-y-transition {
2 | .transition-default();
3 |
4 | &-enter-from,
5 | &-leave-to {
6 | opacity: 0;
7 | }
8 |
9 | &-enter-from {
10 | transform: translateY(-15px);
11 | }
12 |
13 | &-leave-to {
14 | transform: translateY(15px);
15 | }
16 | }
17 |
18 | .scroll-y-reverse-transition {
19 | .transition-default();
20 |
21 | &-enter-from,
22 | &-leave-to {
23 | opacity: 0;
24 | }
25 |
26 | &-enter-from {
27 | transform: translateY(15px);
28 | }
29 |
30 | &-leave-to {
31 | transform: translateY(-15px);
32 | }
33 | }
34 |
35 | .scroll-x-transition {
36 | .transition-default();
37 |
38 | &-enter-from,
39 | &-leave-to {
40 | opacity: 0;
41 | }
42 |
43 | &-enter-from {
44 | transform: translateX(-15px);
45 | }
46 |
47 | &-leave-to {
48 | transform: translateX(15px);
49 | }
50 | }
51 |
52 | .scroll-x-reverse-transition {
53 | .transition-default();
54 |
55 | &-enter-from,
56 | &-leave-to {
57 | opacity: 0;
58 | }
59 |
60 | &-enter-from {
61 | transform: translateX(15px);
62 | }
63 |
64 | &-leave-to {
65 | transform: translateX(-15px);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/design/transition/slide.less:
--------------------------------------------------------------------------------
1 | .slide-y-transition {
2 | .transition-default();
3 |
4 | &-enter-from,
5 | &-leave-to {
6 | transform: translateY(-15px);
7 | opacity: 0;
8 | }
9 | }
10 |
11 | .slide-y-reverse-transition {
12 | .transition-default();
13 |
14 | &-enter-from,
15 | &-leave-to {
16 | transform: translateY(15px);
17 | opacity: 0;
18 | }
19 | }
20 |
21 | .slide-x-transition {
22 | .transition-default();
23 |
24 | &-enter-from,
25 | &-leave-to {
26 | transform: translateX(-15px);
27 | opacity: 0;
28 | }
29 | }
30 |
31 | .slide-x-reverse-transition {
32 | .transition-default();
33 |
34 | &-enter-from,
35 | &-leave-to {
36 | transform: translateX(15px);
37 | opacity: 0;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/design/transition/zoom.less:
--------------------------------------------------------------------------------
1 | // zoom-out
2 | .zoom-out-enter-active,
3 | .zoom-out-leave-active {
4 | transition: opacity 0.1 ease-in-out,
5 | transform 0.15s ease-out;
6 | }
7 |
8 | .zoom-out-enter-from,
9 | .zoom-out-leave-to {
10 | transform: scale(0);
11 | opacity: 0;
12 | }
13 |
14 | // zoom-fade
15 | .zoom-fade-enter-active,
16 | .zoom-fade-leave-active {
17 | transition:
18 | transform 0.2s,
19 | opacity 0.3s ease-out;
20 | }
21 |
22 | .zoom-fade-enter-from {
23 | transform: scale(0.92);
24 | opacity: 0;
25 | }
26 |
27 | .zoom-fade-leave-to {
28 | transform: scale(1.06);
29 | opacity: 0;
30 | }
31 |
--------------------------------------------------------------------------------
/src/design/variable/breakpoint.less:
--------------------------------------------------------------------------------
1 | // Small screen / tablet
2 | @screen-sm: 576px;
3 | @screen-sm-min: @screen-sm;
4 |
5 | // Medium screen / desktop
6 | @screen-md: 768px;
7 | @screen-md-min: @screen-md;
8 |
9 | // Large screen / wide desktop
10 | @screen-lg: 992px;
11 | @screen-lg-min: @screen-lg;
12 |
13 | // Extra large screen / full hd
14 | @screen-xl: 1200px;
15 | @screen-xl-min: @screen-xl;
16 |
17 | // Extra extra large screen / large desktop
18 | @screen-2xl: 1600px;
19 | @screen-2xl-min: @screen-2xl;
20 |
21 | @screen-xs-max: (@screen-sm-min - 1px);
22 | @screen-sm-max: (@screen-md-min - 1px);
23 | @screen-md-max: (@screen-lg-min - 1px);
24 | @screen-lg-max: (@screen-xl-min - 1px);
25 | @screen-xl-max: (@screen-2xl-min - 1px);
--------------------------------------------------------------------------------
/src/design/variable/color.less:
--------------------------------------------------------------------------------
1 | @white: #fff;
2 | @primary-color: #1890ff;
3 | @border-color: rgba(5, 5, 5, .06);
4 |
5 | @primary-dark-bg: #263238;
6 | @submenu-dark-bg: #202b30;
7 |
8 | @dark-mode-bg: #141414;
9 | @dark-mode-border-color: #424242;
10 | @dark-mode-border: 1px solid @dark-mode-border-color;
11 | @dark-mode-color: rgba(255, 255, 255, 0.85);
12 | @dark-mode-hover-color: rgba(255, 255, 255, 0.65);
13 |
--------------------------------------------------------------------------------
/src/design/variable/index.less:
--------------------------------------------------------------------------------
1 | @import './color.less';
2 | @import './breakpoint.less';
3 |
4 | @namespace: v-desg;
5 |
6 | @layout-hybrid-sider-fixed-z-index: 550;
7 |
8 |
--------------------------------------------------------------------------------
/src/enums/appEnum.ts:
--------------------------------------------------------------------------------
1 | export const SIDE_BAR_MIN_WIDTH = 48
2 | export const SIDE_BAR_SHOW_TITLE_MIN_WIDTH = 80
3 |
4 | // App mode enum
5 | export enum AppModeEnum {
6 | DARK = 'dark',
7 | LIGHT = 'light'
8 | }
9 |
10 | // Menu theme enum
11 | export enum ThemeEnum {
12 | DARK = 'dark',
13 | LIGHT = 'light'
14 | }
15 |
16 | // App locale enum
17 | export enum LocaleEnum {
18 | ZH_CN = 'zh_CN',
19 | Zh_TW = 'zh_TW',
20 | EN_US = 'en_US'
21 | }
22 |
23 | // Page switching animation
24 | export enum PageTransitionEnum {
25 | FADE = 'fade',
26 | FADE_SLIDE = 'fade-slide',
27 | FADE_SCALE = 'fade-scale',
28 | FADE_BOTTOM = 'fade-bottom',
29 | ZOOM_FADE = 'zoom-fade',
30 | ZOOM_OUT = 'zoom-out'
31 | }
32 |
33 | // Permission mode
34 | export enum PermissionModeEnum {
35 | // Route mapping
36 | MAPPING = 'MAPPING',
37 | // The back-end response
38 | BACKEND = 'BACKEND'
39 | }
40 |
--------------------------------------------------------------------------------
/src/enums/cacheEnum.ts:
--------------------------------------------------------------------------------
1 | export const TOKEN_KEY = 'TOKEN_KEY'
2 |
3 | export const LOCALE_KEY = 'LOCALE_KEY'
4 |
5 | export const LOCK_INFO_KEY = 'LOCK_INFO_KEY'
6 |
7 | export const USER_INFO_KEY = 'USER_INFO_KEY'
8 |
9 | export const APP_CONFIG_KEY = 'APP_CONFIG_KEY'
10 |
11 | export const APP_MODE_KEY = 'APP_MODE_KEY'
12 |
13 | export const APP_TAGS_KEY = 'APP_TAGS_KEY'
14 |
15 | export const APP_LOCAL_CACHE_KEY = 'APP_LOCAL_CACHE_KEY'
16 |
17 | export const APP_SESSION_CACHE_KEY = 'APP_SESSION_CACHE_KEY'
18 |
19 | export enum CacheTypeEnum {
20 | SESSION,
21 | LOCAL
22 | }
23 |
--------------------------------------------------------------------------------
/src/enums/exceptionEnum.ts:
--------------------------------------------------------------------------------
1 | export enum ExceptionEnum {
2 | // page not access
3 | PAGE_NOT_ACCESS = 403,
4 |
5 | // page not found
6 | PAGE_NOT_FOUND = 404,
7 |
8 | // server error
9 | SERVER_ERROR = 500
10 | }
11 |
--------------------------------------------------------------------------------
/src/enums/menuEnum.ts:
--------------------------------------------------------------------------------
1 | // Menu typings
2 | export enum MenuTypeEnum {
3 | SIDER_MENU = 'sider-menu',
4 |
5 | HEADER_MENU = 'header-menu',
6 |
7 | HYBRID_MENU = 'hybrid-menu'
8 | }
9 |
10 | // Menu modes
11 | export enum MenuModeEnum {
12 | VERTICAL = 'vertical',
13 |
14 | HORIZONTAL = 'horizontal',
15 |
16 | INLINE = 'inline'
17 | }
18 |
19 | // Menu folding button location
20 | export enum MenuFoldBtnEnum {
21 | NONE = 'none',
22 |
23 | HEADER = 'header',
24 |
25 | SIDER = 'sider'
26 | }
27 |
--------------------------------------------------------------------------------
/src/enums/roleEnum.ts:
--------------------------------------------------------------------------------
1 | export enum RoleEnum {
2 | // super admin
3 | SUPER = 'super',
4 |
5 | // tester
6 | TEST = 'test'
7 | }
8 |
--------------------------------------------------------------------------------
/src/hooks/core/useRefs.ts:
--------------------------------------------------------------------------------
1 | import type { Ref } from 'vue'
2 | import { ref, onBeforeUpdate } from 'vue'
3 |
4 | export function useRefs(): [Ref, (index: number) => (el: HTMLElement) => void] {
5 | const refs = ref([]) as Ref
6 |
7 | onBeforeUpdate(() => {
8 | refs.value = []
9 | })
10 |
11 | const setRefs = (index: number) => (el: HTMLElement) => {
12 | refs.value[index] = el
13 | }
14 |
15 | return [refs, setRefs]
16 | }
17 |
--------------------------------------------------------------------------------
/src/hooks/core/useTimeout.ts:
--------------------------------------------------------------------------------
1 | import { ref, watch } from 'vue'
2 | import { tryOnUnmounted } from '@vueuse/core'
3 | import { isFunction } from '@/utils/is'
4 |
5 | export function useTimeoutFn(handle: Fn, wait: number, native = false) {
6 | if (!isFunction(handle)) {
7 | throw new Error('handle is not Function!')
8 | }
9 |
10 | const { readyRef, stop, start } = useTimeoutRef(wait)
11 |
12 | if (native) {
13 | handle()
14 | } else {
15 | watch(
16 | readyRef,
17 | maturity => {
18 | maturity && handle()
19 | },
20 | { immediate: false }
21 | )
22 | }
23 |
24 | return { readyRef, stop, start }
25 | }
26 |
27 | export function useTimeoutRef(wait: number) {
28 | const readyRef = ref(false)
29 |
30 | let timer: TimeoutHandle
31 |
32 | function stop(): void {
33 | readyRef.value = false
34 | timer && window.clearTimeout(timer)
35 | }
36 |
37 | function start(): void {
38 | stop()
39 | timer = setTimeout(() => {
40 | readyRef.value = true
41 | }, wait)
42 | }
43 |
44 | start()
45 |
46 | tryOnUnmounted(stop)
47 |
48 | return { readyRef, stop, start }
49 | }
50 |
--------------------------------------------------------------------------------
/src/hooks/event/useWindowSizeFn.ts:
--------------------------------------------------------------------------------
1 | import type { AnyFunction } from '@/types/utils'
2 | import { tryOnMounted, tryOnUnmounted, useDebounceFn } from '@vueuse/core'
3 |
4 | interface UseWindowSizeOptions {
5 | wait?: number
6 | once?: boolean
7 | immediate?: boolean
8 | listenerOptions?: AddEventListenerOptions | boolean
9 | }
10 |
11 | function useWindowSizeFn(fn: AnyFunction, options: UseWindowSizeOptions = {}) {
12 | const { wait = 150, immediate } = options
13 | let handler = () => {
14 | fn()
15 | }
16 | const handleSize = useDebounceFn(handler, wait)
17 | handler = handleSize
18 |
19 | const start = () => {
20 | if (immediate) {
21 | handler()
22 | }
23 | window.addEventListener('resize', handler)
24 | }
25 |
26 | const stop = () => {
27 | window.removeEventListener('resize', handler)
28 | }
29 |
30 | tryOnMounted(() => {
31 | start()
32 | })
33 |
34 | tryOnUnmounted(() => {
35 | stop()
36 | })
37 | return { start, stop }
38 | }
39 |
40 | export { useWindowSizeFn, type UseWindowSizeOptions }
41 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baimingxuan/vue3-admin-design/d368ff2f1680414ae9b9fed93ced7cfba8b76b7f/src/hooks/index.ts
--------------------------------------------------------------------------------
/src/hooks/setting/useBaseSetting.ts:
--------------------------------------------------------------------------------
1 | import { computed } from 'vue'
2 | import { useAppStore } from '@/stores/modules/app'
3 | import type { AppModeEnum } from '@/enums/appEnum'
4 |
5 | export function useBaseSetting() {
6 | const appStore = useAppStore()
7 |
8 | const getAppMode = computed(() => appStore.getAppMode)
9 |
10 | const getThemeColor = computed(() => appStore.getAppConfig.themeColor)
11 |
12 | const getTagsCached = computed(() => appStore.getAppConfig.tagsCached)
13 |
14 | const getLockScreenTime = computed(() => appStore.getAppConfig.lockScreenTime)
15 |
16 | const getShowFooter = computed(() => appStore.getAppConfig.showFooter)
17 |
18 | const getColorWeak = computed(() => appStore.getAppConfig.colorWeak)
19 |
20 | const getGrayMode = computed(() => appStore.getAppConfig.grayMode)
21 |
22 | function setAppMode(mode: AppModeEnum) {
23 | appStore.setAppMode(mode)
24 | }
25 |
26 | return {
27 | setAppMode,
28 | getAppMode,
29 | getThemeColor,
30 | getTagsCached,
31 | getLockScreenTime,
32 | getShowFooter,
33 | getColorWeak,
34 | getGrayMode
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/hooks/setting/useDarkModeSetting.ts:
--------------------------------------------------------------------------------
1 | import { computed, unref } from 'vue'
2 | import { theme } from 'ant-design-vue/lib'
3 | import { ThemeEnum } from '@/enums/appEnum'
4 | import { useBaseSetting } from './useBaseSetting'
5 |
6 | export function useDarkModeSetting() {
7 | const { getAppMode } = useBaseSetting()
8 | const { defaultAlgorithm, darkAlgorithm } = theme
9 |
10 | const isDarkMode = computed(() => unref(getAppMode) === ThemeEnum.DARK)
11 |
12 | const getModeAlgorithm = computed(() => {
13 | return unref(isDarkMode) ? darkAlgorithm : defaultAlgorithm
14 | })
15 |
16 | return { getModeAlgorithm, isDarkMode, getAppMode }
17 | }
18 |
--------------------------------------------------------------------------------
/src/hooks/setting/useHeaderSetting.ts:
--------------------------------------------------------------------------------
1 | import { computed } from 'vue'
2 | import { useAppStore } from '@/stores/modules/app'
3 |
4 | export function useHeaderSetting() {
5 | const appStore = useAppStore()
6 |
7 | const getShowBreadCrumb = computed(() => appStore.getHeaderSetting.showBreadCrumb)
8 |
9 | const getShowTags = computed(() => appStore.getHeaderSetting.showTags)
10 |
11 | const getShowSearch = computed(() => appStore.getHeaderSetting.showSearch)
12 |
13 | const getShowLocale = computed(() => appStore.getHeaderSetting.showLocale)
14 |
15 | const getShowFullScreen = computed(() => appStore.getHeaderSetting.showFullScreen)
16 |
17 | const getShowDoc = computed(() => appStore.getHeaderSetting.showDoc)
18 |
19 | const getShowGithub = computed(() => appStore.getHeaderSetting.showGithub)
20 |
21 | return {
22 | getShowBreadCrumb,
23 | getShowTags,
24 | getShowSearch,
25 | getShowFullScreen,
26 | getShowLocale,
27 | getShowDoc,
28 | getShowGithub
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/hooks/setting/useLocaleSetting.ts:
--------------------------------------------------------------------------------
1 | import type { LocaleType } from '@/types'
2 | import { unref, computed } from 'vue'
3 | import { useI18n } from 'vue-i18n'
4 | import { setHtmlPageLang } from '@/locales/helper'
5 | import { genLangs } from '@/locales/langs'
6 | import { LOCALE_KEY } from '@/enums/cacheEnum'
7 | import { createLocalStorage } from '@/utils/cache'
8 | import { getBrowserLang } from '@/locales/helper'
9 |
10 | const ls = createLocalStorage()
11 |
12 | export function useLocaleSetting() {
13 | const { locale: currentLocale, setLocaleMessage } = useI18n()
14 | const getLocale = computed(() => ls.get(LOCALE_KEY) || getBrowserLang())
15 |
16 | async function changeLocale(locale: LocaleType) {
17 | if (unref(currentLocale) === locale) {
18 | return locale
19 | }
20 |
21 | const langModule = genLangs(locale)
22 | if (!langModule) return
23 |
24 | setLocaleMessage(locale, langModule.message)
25 |
26 | ls.set(LOCALE_KEY, locale)
27 | setHtmlPageLang(locale)
28 |
29 | return locale
30 | }
31 |
32 | return {
33 | getLocale,
34 | changeLocale
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/hooks/setting/useTransitionSetting.ts:
--------------------------------------------------------------------------------
1 | import { computed } from 'vue'
2 | import { useAppStoreWithOut } from '@/stores/modules/app'
3 |
4 | export function useTransitionSetting() {
5 | const appStore = useAppStoreWithOut()
6 |
7 | const getOpenNProgress = computed(() => appStore.getTransitionSetting.openNProgress)
8 |
9 | const getOpenTransition = computed(() => appStore.getTransitionSetting.openTransition)
10 |
11 | const getBasicTransition = computed(() => appStore.getTransitionSetting.basicTransition)
12 |
13 | return {
14 | getOpenNProgress,
15 | getOpenTransition,
16 | getBasicTransition
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/hooks/web/useTitle.ts:
--------------------------------------------------------------------------------
1 | import { watch, unref } from 'vue'
2 | import { useTitle as usePageTitle } from '@vueuse/core'
3 | import { useRouter } from 'vue-router'
4 | import { useI18n } from 'vue-i18n'
5 |
6 | /**
7 | * Listening to page changes and dynamically changing site titles
8 | */
9 | export function useTitle() {
10 | const { t } = useI18n()
11 | const { currentRoute } = useRouter()
12 |
13 | const pageTitle = usePageTitle()
14 | const appName = import.meta.env.VITE_APP_NAME
15 |
16 | watch(
17 | () => currentRoute.value.path,
18 | () => {
19 | const route = unref(currentRoute)
20 |
21 | if (route.name === 'RedirectTo') return
22 |
23 | const title = route.meta.title ? t(route.meta.title) : ''
24 | pageTitle.value = title ? `${title} | ${appName}` : appName
25 | },
26 | { immediate: true }
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/layout/content/index.less:
--------------------------------------------------------------------------------
1 | .layout_content {
2 | position: relative;
3 | flex: 1 1 auto;
4 | min-height: 0;
5 | margin: 12px;
6 | // background: @white;
7 |
8 | &-loading {
9 | z-index: 9000;
10 | position: absolute;
11 | top: 200px;
12 | }
13 | }
--------------------------------------------------------------------------------
/src/layout/content/index.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import Page from './components/Page'
3 | import Iframe from './components/Iframe'
4 | import './index.less'
5 |
6 | export default defineComponent({
7 | name: 'LayoutContent',
8 | setup() {
9 | return () => (
10 |
14 | )
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/src/layout/content/usePageTransition.ts:
--------------------------------------------------------------------------------
1 | import type { RouteLocation } from 'vue-router'
2 | import type { AppRoute } from '@/router/types'
3 | import { unref } from 'vue'
4 | import { useTransitionSetting } from '@/hooks/setting/useTransitionSetting'
5 |
6 | export function usePageTransition() {
7 | const { getOpenTransition, getBasicTransition } = useTransitionSetting()
8 |
9 | function getTransitionName(route: RouteLocation | AppRoute): string | undefined {
10 | if (!unref(getOpenTransition)) return undefined
11 |
12 | return (route.meta.transitionName as string) || unref(getBasicTransition)
13 | }
14 |
15 | return {
16 | getTransitionName
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/layout/feature/components/DocLink.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Tooltip } from 'ant-design-vue'
3 | import { useI18n } from 'vue-i18n'
4 | import SvgIcon from '@/components/SvgIcon'
5 |
6 | export default defineComponent({
7 | name: 'DocLink',
8 |
9 | setup() {
10 | const { t } = useI18n()
11 |
12 | return () => (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/src/layout/feature/components/FullScreen.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, unref } from 'vue'
2 | import { Tooltip } from 'ant-design-vue'
3 | import { useFullscreen } from '@vueuse/core'
4 | import { useI18n } from 'vue-i18n'
5 | import SvgIcon from '@/components/SvgIcon'
6 |
7 | export default defineComponent({
8 | name: 'FullScreen',
9 | setup() {
10 | const { t } = useI18n()
11 | const { isFullscreen, toggle } = useFullscreen()
12 |
13 | return () => (
14 |
15 |
20 |
21 | {!unref(isFullscreen) ? (
22 |
23 | ) : (
24 |
25 | )}
26 |
27 |
28 |
29 | )
30 | }
31 | })
32 |
--------------------------------------------------------------------------------
/src/layout/feature/components/GithubLink.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Tooltip } from 'ant-design-vue'
3 | import SvgIcon from '@/components/SvgIcon'
4 | import { openWindow } from '@/utils'
5 | import { GITHUB_URL } from '@/settings/websiteSetting'
6 |
7 | function openGithub() {
8 | openWindow(GITHUB_URL)
9 | }
10 |
11 | export default defineComponent({
12 | name: 'GithubLink',
13 | setup() {
14 | return () => (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 | })
25 |
--------------------------------------------------------------------------------
/src/layout/feature/components/index.ts:
--------------------------------------------------------------------------------
1 | import FullScreen from './FullScreen'
2 | import DocLink from './DocLink'
3 | import GithubLink from './GithubLink'
4 | import UserDropdown from './UserDropdown'
5 |
6 | export { FullScreen, DocLink, GithubLink, UserDropdown }
7 |
--------------------------------------------------------------------------------
/src/layout/feature/index.module.less:
--------------------------------------------------------------------------------
1 | .layout_feature {
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | min-width: 210px;
6 | height: 48px;
7 |
8 | &-main {
9 | display: flex;
10 | justify-content: right;
11 | min-width: 156px;
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/layout/footer/index.module.less:
--------------------------------------------------------------------------------
1 | .layout-footer {
2 | height: 36px;
3 | padding: 0 !important;
4 |
5 | .content {
6 | display: flex;
7 | gap: 8px;
8 | }
9 |
10 | span {
11 | color: rgba(0, 0, 0, .65);
12 | }
13 |
14 | svg {
15 | cursor: pointer;
16 |
17 | &:hover {
18 | color: rgba(0, 0, 0, .8);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/layout/footer/index.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Layout } from 'ant-design-vue'
3 | import { GithubFilled } from '@ant-design/icons-vue'
4 | import { openWindow } from '@/utils'
5 | import { GITHUB_URL } from '@/settings/websiteSetting'
6 | import style from './index.module.less'
7 |
8 | export default defineComponent({
9 | name: 'LayoutFooter',
10 | setup() {
11 | return () => (
12 |
19 | )
20 | }
21 | })
22 |
--------------------------------------------------------------------------------
/src/layout/header/HybridHeader/index.less:
--------------------------------------------------------------------------------
1 | .layout-hybrid-header {
2 | display: flex;
3 | flex-direction: column;
4 | color: #fff;
5 |
6 | &.light {
7 | background: @white;
8 |
9 | &.has-border {
10 | border-bottom: solid 1px @border-color;
11 | }
12 | }
13 |
14 | &.dark {
15 | background: @primary-dark-bg;
16 |
17 | &.has-border {
18 | border-bottom: 1px solid rgba(255, 255, 255, 0.65);
19 | }
20 | }
21 |
22 | &-wrap {
23 | display: flex;
24 | justify-content: space-between;
25 | padding-right: 12px;
26 |
27 | .logo-box {
28 | display: flex;
29 | height: 100%;
30 | }
31 |
32 | .main-box {
33 | display: flex;
34 | flex-direction: column;
35 | justify-content: space-between;
36 | flex-grow: 1;
37 | height: 100%;
38 | padding-left: 60px;
39 |
40 | &__cont {
41 | display: flex;
42 | justify-content: space-between;
43 | height: 48px;
44 | line-height: 48px;
45 |
46 | &-menu {
47 | flex: 1;
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/layout/header/components/Breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import type { RouteLocationMatched } from 'vue-router'
2 | import { useRouter } from 'vue-router'
3 | import { defineComponent, ref, unref, watchEffect } from 'vue'
4 | import { Flex, Space, Breadcrumb } from 'ant-design-vue'
5 | import { useI18n } from 'vue-i18n'
6 | import SvgIcon from '@/components/SvgIcon'
7 |
8 | export default defineComponent({
9 | name: 'LayoutBreadcrumb',
10 | setup() {
11 | const { t } = useI18n()
12 | const routeMatcheds = ref([])
13 | const { currentRoute } = useRouter()
14 |
15 | watchEffect(async () => {
16 | if (unref(currentRoute).name === 'Redirect') return
17 | routeMatcheds.value = filterMatched(currentRoute.value.matched)
18 | })
19 |
20 | function filterMatched(list: RouteLocationMatched[]) {
21 | return list.filter(item => !item.meta?.hideMenu)
22 | }
23 |
24 | function getIcon(route: RouteLocationMatched) {
25 | return route.meta?.icon || ''
26 | }
27 |
28 | return () => (
29 |
30 |
31 | {unref(routeMatcheds).map((route: RouteLocationMatched) => {
32 | return (
33 |
34 |
35 | {getIcon(route) && }
36 | {t(route.meta?.title)}
37 |
38 |
39 | )
40 | })}
41 |
42 |
43 | )
44 | }
45 | })
46 |
--------------------------------------------------------------------------------
/src/layout/header/components/FoldTrigger.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, unref } from 'vue'
2 | import { useMenuSetting } from '@/hooks/setting/useMenuSetting'
3 | import SvgIcon from '@/components/SvgIcon'
4 | import compoStyle from './compo.module.less'
5 |
6 | export default defineComponent({
7 | name: 'FoldTrigger',
8 | setup() {
9 | const { getMenuFold, toggledMenuFold } = useMenuSetting()
10 |
11 | return () => (
12 |
16 |
17 |
18 | )
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/src/layout/header/components/compo.module.less:
--------------------------------------------------------------------------------
1 | .compo_fold-trigger {
2 | display: flex;
3 | align-items: center;
4 | cursor: pointer;
5 |
6 | &.unfold {
7 |
8 | svg {
9 | transform:scaleX(-1)
10 | }
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/src/layout/header/index.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import BasicHeader from './BasicHeader'
3 | import HybridHeader from './HybridHeader'
4 |
5 | export default defineComponent({
6 | name: 'LayoutHeader',
7 | props: {
8 | isHeaderMenu: {
9 | type: Boolean,
10 | default: false
11 | }
12 | },
13 | setup(props) {
14 | return () => (props.isHeaderMenu ? : )
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/src/layout/lock/components/LockModal/index.module.less:
--------------------------------------------------------------------------------
1 | .lock-modal-content {
2 |
3 | .user-info {
4 | height: 100px;
5 | margin-top: 24px;
6 | margin-bottom: 16px;
7 | flex-direction: column;
8 | justify-content: space-between;
9 |
10 | .name {
11 | font-size: 14px;
12 | font-weight: 600;
13 | }
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/layout/lock/components/UnlockForm/index.module.less:
--------------------------------------------------------------------------------
1 | .unlock-form-wrapper {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | width: 100%;
6 | height: 100%;
7 | background: rgba(0 0 0, .5);
8 | backdrop-filter: blur(8px);
9 |
10 | .unlock-content {
11 | width: 260px;
12 | height: 240px;
13 |
14 | .user-info {
15 | height: 100px;
16 | margin-bottom: 16px;
17 | flex-direction: column;
18 | justify-content: space-between;
19 |
20 | .name {
21 | font-size: 14px;
22 | font-weight: 600;
23 | color: rgba(255, 255, 255, .85);
24 | }
25 | }
26 |
27 | :global(.ant-form-item) {
28 | margin-bottom: 20px!important;
29 | }
30 |
31 | .link-btn {
32 | height: 24px;
33 | line-height: 1;
34 | color: @white;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/layout/lock/index.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, unref, computed, Transition } from 'vue'
2 | import { useLockStore } from '@/stores/modules/lock'
3 | import LockScreen from './components/LockScreen'
4 |
5 | export default defineComponent({
6 | name: 'LockPage',
7 | setup() {
8 | const lockStore = useLockStore()
9 | const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
10 |
11 | return () => (
12 |
13 | {unref(getIsLock) && }
14 |
15 | )
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/src/layout/lock/useNowTime.ts:
--------------------------------------------------------------------------------
1 | import { reactive, toRefs } from 'vue'
2 | import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
3 | import { dateUtil } from '@/utils/dateUtil'
4 |
5 | export function useNowTime(immediate = true) {
6 | let timer: IntervalHandle
7 |
8 | const state = reactive({
9 | year: 0,
10 | month: 0,
11 | week: '',
12 | day: 0,
13 | hour: '',
14 | minute: '',
15 | second: 0
16 | })
17 |
18 | const update = () => {
19 | const now = dateUtil()
20 |
21 | const h = now.format('HH')
22 | const m = now.format('mm')
23 | const s = now.get('s')
24 |
25 | state.year = now.get('y')
26 | state.month = now.get('M') + 1
27 | state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()]
28 | state.day = now.get('date')
29 | state.hour = h
30 | state.minute = m
31 | state.second = s
32 | }
33 |
34 | function start() {
35 | update()
36 | clearInterval(timer)
37 | timer = setInterval(() => update(), 1000)
38 | }
39 |
40 | function stop() {
41 | clearInterval(timer)
42 | }
43 |
44 | tryOnMounted(() => {
45 | immediate && start()
46 | })
47 |
48 | tryOnUnmounted(() => {
49 | stop()
50 | })
51 |
52 | return {
53 | ...toRefs(state),
54 | start,
55 | stop
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/layout/setting/components/InputNumItem.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import type { PropType } from 'vue'
3 |
4 | import { InputNumber } from 'ant-design-vue'
5 | import type { HandlerEnum } from '../enum'
6 | import { baseHandler } from '../handler'
7 |
8 | export default defineComponent({
9 | name: 'InputNumItem',
10 | components: { InputNumber },
11 | props: {
12 | title: {
13 | type: String,
14 | default: ''
15 | },
16 | event: {
17 | type: Number as PropType
18 | }
19 | },
20 | setup(props, { attrs }) {
21 | function handleChange(value: number | string) {
22 | props.event && baseHandler(props.event, value)
23 | }
24 |
25 | return () => (
26 | ]
27 | {props.title}
28 |
29 |
30 | )
31 | }
32 | })
33 |
--------------------------------------------------------------------------------
/src/layout/setting/components/MenuThemeRadio/index.module.less:
--------------------------------------------------------------------------------
1 | .menu-theme-radio {
2 |
3 | :global(.ant-radio-group) {
4 | display: flex;
5 | flex-direction: column;
6 |
7 | :global(.ant-radio-button-wrapper) {
8 | padding: 0 13px;
9 | border-left-width: 1px;
10 |
11 | &:first-child {
12 | border-radius: 4px 4px 0 0;
13 | }
14 |
15 | &:last-child {
16 | border-radius: 0 0 4px 4px;
17 | }
18 |
19 | &:not(:first-child)::before {
20 | display: none;
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/layout/setting/components/MenuThemeRadio/index.tsx:
--------------------------------------------------------------------------------
1 | import type { ThemeEnum } from '@/enums/appEnum'
2 | import { defineComponent, unref } from 'vue'
3 | import { RadioGroup, RadioButton } from 'ant-design-vue'
4 | import { useI18n } from 'vue-i18n'
5 | import { useMenuSetting } from '@/hooks/setting/useMenuSetting'
6 | import { useDarkModeSetting } from '@/hooks/setting/useDarkModeSetting'
7 | import styles from './index.module.less'
8 |
9 | export default defineComponent({
10 | name: 'MenuThemeRadio',
11 | setup() {
12 | const { t } = useI18n()
13 | const { getMenuTheme, changeMenuTheme } = useMenuSetting()
14 | const { isDarkMode } = useDarkModeSetting()
15 |
16 | function handleChangeTheme(e: any) {
17 | const theme = e.target.value as ThemeEnum
18 | changeMenuTheme(theme)
19 | }
20 |
21 | return () => (
22 |
34 | )
35 | }
36 | })
37 |
--------------------------------------------------------------------------------
/src/layout/setting/components/SelectItem.tsx:
--------------------------------------------------------------------------------
1 | import type { PropType } from 'vue'
2 | import type { HandlerEnum } from '../enum'
3 | import type { SelectOptions } from '@/types'
4 | import { defineComponent, computed, unref } from 'vue'
5 | import { Select } from 'ant-design-vue'
6 | import { baseHandler } from '../handler'
7 |
8 | export default defineComponent({
9 | name: 'SelectItem',
10 | components: { Select },
11 | props: {
12 | title: {
13 | type: String,
14 | default: ''
15 | },
16 | disabled: {
17 | type: Boolean,
18 | default: false
19 | },
20 | def: {
21 | type: [String, Number] as PropType
22 | },
23 | event: {
24 | type: Number as PropType
25 | },
26 | options: {
27 | type: Array as PropType,
28 | default: () => []
29 | }
30 | },
31 | setup(props) {
32 | const getBindValue = computed(() => {
33 | return props.def ? { value: props.def, defaultValue: props.def } : {}
34 | })
35 |
36 | function handleChange(value: any) {
37 | props.event && baseHandler(props.event, value)
38 | }
39 |
40 | return () => (
41 |
42 | {props.title}
43 |
52 | )
53 | }
54 | })
55 |
--------------------------------------------------------------------------------
/src/layout/setting/components/SwitchItem.tsx:
--------------------------------------------------------------------------------
1 | import type { PropType } from 'vue'
2 | import { defineComponent } from 'vue'
3 |
4 | import { Switch } from 'ant-design-vue'
5 | import type { HandlerEnum } from '../enum'
6 | import { baseHandler } from '../handler'
7 |
8 | export default defineComponent({
9 | name: 'SwitchItem',
10 | components: { Switch },
11 | props: {
12 | title: {
13 | type: String
14 | },
15 | disabled: {
16 | type: Boolean
17 | },
18 | def: {
19 | type: Boolean
20 | },
21 | event: {
22 | type: Number as PropType
23 | }
24 | },
25 | setup(props) {
26 | function handleChange(checked: CheckedType) {
27 | props.event && baseHandler(props.event, checked)
28 | }
29 |
30 | return () => (
31 |
32 | {props.title}
33 |
34 |
35 | )
36 | }
37 | })
38 |
--------------------------------------------------------------------------------
/src/layout/setting/components/ThemeColorPicker/index.module.less:
--------------------------------------------------------------------------------
1 | .theme-color-picker {
2 | display: flex;
3 | flex-wrap: nowrap;
4 | justify-content: space-between;
5 | margin: 16px 0;
6 |
7 | &__item {
8 | width: 22px;
9 | height: 22px;
10 | border-radius: 2px;
11 | cursor: pointer;
12 |
13 | :global(svg) {
14 | display: none;
15 | }
16 |
17 | &--active {
18 | :global(svg) {
19 | display: inline-block;
20 | margin-top: 5px;
21 | margin-left: 5px;
22 | font-size: 12px;
23 | fill: #fff !important;
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/layout/setting/components/index.ts:
--------------------------------------------------------------------------------
1 | import { ImportAsyncComponent } from '@/utils/factory/ImportAsyncComponent'
2 |
3 | export const MenuThemeRadio = ImportAsyncComponent(() => import('./MenuThemeRadio'))
4 | export const MenuTypePicker = ImportAsyncComponent(() => import('./MenuTypePicker'))
5 | export const ThemeColorPicker = ImportAsyncComponent(() => import('./ThemeColorPicker'))
6 | export const SwitchItem = ImportAsyncComponent(() => import('./SwitchItem'))
7 | export const SelectItem = ImportAsyncComponent(() => import('./SelectItem'))
8 | export const InputNumItem = ImportAsyncComponent(() => import('./InputNumItem'))
9 | export const SettingFooter = ImportAsyncComponent(() => import('./SettingFooter'))
10 |
--------------------------------------------------------------------------------
/src/layout/setting/index.module.less:
--------------------------------------------------------------------------------
1 | .layout-setting-trigger {
2 | position: fixed !important;
3 | top: 320px;
4 | right: 0;
5 | z-index: 99;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | width: 40px !important;
10 | height: 40px;
11 | font-size: 16px;
12 | color: @white;
13 | border-radius: 4px 0 0 4px;
14 | cursor: pointer;
15 | pointer-events: auto;
16 |
17 | svg {
18 | color: @white;
19 | }
20 | }
21 |
22 | .layout-setting-drawer {
23 | :global(.scrollbar__wrap) {
24 | padding: 0 16px 20px !important;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/layout/sider/components/DragBar/index.module.less:
--------------------------------------------------------------------------------
1 | .drag-bar {
2 | position: absolute;
3 | top: 0;
4 | right: -2px;
5 | z-index: 200;
6 | width: 2px;
7 | height: 100%;
8 | cursor: col-resize;
9 | border-top: none;
10 | border-bottom: none;
11 |
12 | &--hide {
13 | display: none;
14 | }
15 |
16 | &:hover {
17 | background-color: #108ee9;
18 | box-shadow: 0 0 4px 0 rgb(28 36 56 / 15%);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/layout/sider/components/DragBar/index.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, computed, unref } from 'vue'
2 | import { useMenuSetting } from '@/hooks/setting/useMenuSetting'
3 | import styles from './index.module.less'
4 |
5 | export default defineComponent({
6 | name: 'DragBar',
7 |
8 | setup() {
9 | const { getMenuFold, getMenuCanDrag, getSideBarMinWidth } = useMenuSetting()
10 |
11 | const getDragBarStyle = computed(() => {
12 | if (unref(getMenuFold)) {
13 | return { left: `${unref(getSideBarMinWidth)}px` }
14 | }
15 | return {}
16 | })
17 |
18 | return () => (
19 |
23 | )
24 | }
25 | })
26 |
--------------------------------------------------------------------------------
/src/layout/sider/components/SiderTrigger/index.module.less:
--------------------------------------------------------------------------------
1 | .sider-trigger {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | height: 100%;
6 | cursor: pointer;
7 |
8 | &:deep(svg) {
9 | color: #ffffffa6;
10 | }
11 |
12 | &.collapsed {
13 | svg {
14 | transform: scaleX(-1);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/layout/sider/components/SiderTrigger/index.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, unref } from 'vue'
2 | import { useMenuSetting } from '@/hooks/setting/useMenuSetting'
3 | import SvgIcon from '@/components/SvgIcon'
4 | import styles from './index.module.less'
5 |
6 | export default defineComponent({
7 | name: 'SiderTrigger',
8 | setup() {
9 | const { getMenuFold, toggledMenuFold } = useMenuSetting()
10 |
11 | return () => (
12 |
13 |
14 |
15 | )
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/src/layout/sider/index.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, unref } from 'vue'
2 | import { useMenuSetting } from '@/hooks/setting/useMenuSetting'
3 | import BasicSider from './BasicSider'
4 | import HybridSider from './HybridSider'
5 |
6 | export default defineComponent({
7 | name: 'LayoutSider',
8 | setup() {
9 | const { getIsHybridMenu } = useMenuSetting()
10 |
11 | return () => (unref(getIsHybridMenu) ? : )
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/src/layout/tags/components/TagItem/index.module.less:
--------------------------------------------------------------------------------
1 | .tag-item {
2 | display: inline-block;
3 | vertical-align: baseline;
4 | height: 24px;
5 | margin: 0 2px;
6 | padding: 0 8px;
7 | border-radius: 3px;
8 | line-height: 24px;
9 | overflow: hidden;
10 | cursor: pointer;
11 |
12 | &.is-dark {
13 | color: rgba(255, 255, 255, .65);
14 |
15 | svg {
16 | color: rgba(255, 255, 255, .65);
17 | }
18 | }
19 |
20 | &__dot {
21 | display: inline-block;
22 | width: 8px;
23 | height: 8px;
24 | margin-right: 6px;
25 | border-radius: 50%;
26 | background: #d9d9d9;
27 | }
28 |
29 | &__name {
30 | font-size: 12px;
31 | }
32 |
33 | :global(.anticon-close) {
34 | vertical-align: baseline;
35 | svg {
36 | width: 8px;
37 | height: 8px;
38 | }
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/layout/tags/components/TagItem/index.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { propTypes } from '@/utils/propTypes'
3 | import { Tag } from 'ant-design-vue'
4 | import { useI18n } from 'vue-i18n'
5 | import styles from './index.module.less'
6 |
7 | export default defineComponent({
8 | name: 'TagItem',
9 | props: {
10 | fixed: propTypes.bool,
11 | name: propTypes.string,
12 | active: propTypes.bool,
13 | isDark: propTypes.bool
14 | },
15 | setup(props, { emit }) {
16 | const { t } = useI18n()
17 |
18 | return () => (
19 | emit('closeTag')}
27 | >
28 |
29 | {t(props.name)}
30 |
31 | )
32 | }
33 | })
34 |
--------------------------------------------------------------------------------
/src/layout/tags/index.less:
--------------------------------------------------------------------------------
1 | .layout-tags {
2 | z-index: 299;
3 | display: flex;
4 | justify-content: space-between;
5 | height: 32px;
6 | padding: 3px 12px;
7 | line-height: 32px;
8 | border-top: solid 1px @border-color;
9 | border-bottom: solid 1px @border-color;
10 | transition: all 0.3s ease;
11 | box-sizing: border-box;
12 |
13 | &.light {
14 | border-top: solid 1px @border-color;
15 | background: @white;
16 | }
17 |
18 | &.dark {
19 | border-top: 1px solid rgba(255, 255, 255, 0.65);
20 | background: @primary-dark-bg;
21 |
22 | &.single-tags {
23 | border-left: 1px solid rgba(255, 255, 255, 0.65);
24 | }
25 | }
26 |
27 | &.single-tags {
28 | border-top: none;
29 | }
30 |
31 | &__main {
32 | position: relative;
33 | width: calc(100% - 116px);
34 | height: 24px;
35 | overflow: hidden;
36 |
37 | &-cont {
38 | position: absolute;
39 | height: 100%;
40 | padding: 0 2px;
41 | overflow: visible;
42 | white-space: nowrap;
43 | transition: left 0.5s ease;
44 | }
45 | }
46 |
47 | &__btn-space {
48 | margin-left: 4px;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/layout/tags/useTags.ts:
--------------------------------------------------------------------------------
1 | import type { RouteLocationNormalized } from 'vue-router'
2 |
3 | import { toRaw } from 'vue'
4 | import { useRouter } from 'vue-router'
5 | import { useTagStore } from '@/stores/modules/tags'
6 |
7 | export function initAffixTags(): void {
8 | const tagsStore = useTagStore()
9 | const router = useRouter()
10 |
11 | // Filter all fixed routes
12 | function filterAffixTags(routes: RouteLocationNormalized[]) {
13 | const tags: RouteLocationNormalized[] = []
14 |
15 | routes &&
16 | routes.forEach(route => {
17 | if (route.meta && route.meta.affix) {
18 | tags.push(toRaw(route))
19 | }
20 | })
21 |
22 | return tags
23 | }
24 |
25 | const affixTags = filterAffixTags(router.getRoutes() as unknown as RouteLocationNormalized[])
26 |
27 | for (const tag of affixTags) {
28 | tagsStore.addVisitedTags({
29 | meta: tag.meta,
30 | name: tag.name,
31 | path: tag.path
32 | } as unknown as RouteLocationNormalized)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/layout/useLayout.ts:
--------------------------------------------------------------------------------
1 | import { computed, unref } from 'vue'
2 | import { useBaseSetting } from '@/hooks/setting/useBaseSetting'
3 | import { useHeaderSetting } from '@/hooks/setting/useHeaderSetting'
4 | import { useMenuSetting } from '@/hooks/setting/useMenuSetting'
5 |
6 | export function useLayout() {
7 | const { getShowFooter } = useBaseSetting()
8 | const { getMenuSplit } = useMenuSetting()
9 | const { getShowTags } = useHeaderSetting()
10 |
11 | const getHeaderHeight = computed(() => (unref(getShowTags) && !unref(getMenuSplit) ? '80px' : '48px'))
12 |
13 | const getFooterHeight = computed(() => (unref(getShowFooter) ? '36px' : '0px'))
14 |
15 | const getContentHeight = computed(() =>
16 | unref(getMenuSplit)
17 | ? `calc(100vh - 80px - ${unref(getFooterHeight)})`
18 | : `calc(100vh - ${unref(getHeaderHeight)} - ${unref(getFooterHeight)})`
19 | )
20 |
21 | return {
22 | getShowTags,
23 | getHeaderHeight,
24 | getContentHeight
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/locales/index.ts:
--------------------------------------------------------------------------------
1 | import type { App } from 'vue'
2 | import type { I18nOptions } from 'vue-i18n'
3 | import { LocaleEnum } from '@/enums/appEnum'
4 | import { LOCALE_KEY } from '@/enums/cacheEnum'
5 | import { localePool } from '@/settings/localeSetting'
6 | import { createI18n } from 'vue-i18n'
7 | import { setHtmlPageLang, getBrowserLang } from './helper'
8 | import { createLocalStorage } from '@/utils/cache'
9 | import { genLangs } from './langs'
10 |
11 | const ls = createLocalStorage()
12 |
13 | function createI18nOptions(): I18nOptions {
14 | const locale = ls.get(LOCALE_KEY) || getBrowserLang()
15 | const defaultLocal = genLangs(locale)
16 | const message = defaultLocal?.message ?? {}
17 |
18 | setHtmlPageLang(locale)
19 |
20 | return {
21 | legacy: false,
22 | locale,
23 | fallbackLocale: LocaleEnum.ZH_CN,
24 | messages: {
25 | [locale]: message
26 | },
27 | // If you don’t want to inherit locale from global scope, you need to set sync of i18n component option to false.
28 | sync: true,
29 | availableLocales: localePool,
30 | // Whether to cancel the warning output when localization fails, true - warning off
31 | silentTranslationWarn: true
32 | }
33 | }
34 |
35 | function createI18nInstance() {
36 | const options = createI18nOptions()
37 | return createI18n(options)
38 | }
39 |
40 | export const i18n = createI18nInstance()
41 |
42 | export function setupI18n(app: App) {
43 | app.use(i18n)
44 | }
45 |
--------------------------------------------------------------------------------
/src/locales/langs/en_US/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "widgets": {
3 | "fullScreen": "Full screen",
4 | "exitFullScreen": "Exit full screen",
5 | "refresh": "Refresh",
6 | "search": "Search"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/locales/langs/en_US/system.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": {
3 | "logoutTip": "Reminder",
4 | "logoutMsg": "Confirm to logout the system?",
5 | "logoutError": "System logout failure!"
6 | },
7 | "login": {
8 | "accountLogin": "Account Login",
9 | "accountMsg": "Please input username",
10 | "passwordMsg": "Please input password",
11 | "rememberMe": "Remember me",
12 | "forgetPassword": "Forget Password?",
13 | "loginButton": "Sign in",
14 | "loginSuccessTip": "Login successful!"
15 | },
16 | "modal": {
17 | "okText": "Ok",
18 | "closeText": "Close",
19 | "cancelText": "Cancel",
20 | "saveText": "Save",
21 | "resetText": "Reset"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/locales/langs/index.ts:
--------------------------------------------------------------------------------
1 | import type { LocaleType } from '@/types'
2 | import { genMessage } from '../helper'
3 |
4 | const modules = {
5 | en_US: import.meta.glob('./en_US/**/*.json', { eager: true }),
6 | zh_CN: import.meta.glob('./zh_CN/**/*.json', { eager: true }),
7 | zh_TW: import.meta.glob('./zh_TW/**/*.json', { eager: true })
8 | }
9 |
10 | export function genLangs(lang: LocaleType) {
11 | return {
12 | message: {
13 | ...genMessage(modules[lang] as Recordable, lang)
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/locales/langs/zh_CN/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "widgets": {
3 | "fullScreen": "全屏",
4 | "exitFullScreen": "退出全屏",
5 | "refresh": "刷新",
6 | "search": "查询"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/locales/langs/zh_CN/routes.json:
--------------------------------------------------------------------------------
1 | {
2 | "base": {
3 | "home": "首页",
4 | "notFound": "错误页面",
5 | "redirect": "Redirect",
6 | "notAuth": "403页面",
7 | "login": "登录页"
8 | },
9 | "form": {
10 | "name": "表单",
11 | "basic": "基础表单",
12 | "designer": "表单设计器"
13 | },
14 | "table": {
15 | "name": "表格",
16 | "basic": "基础表格",
17 | "editRow": "可编辑行表格"
18 | },
19 | "image": {
20 | "name": "图片处理",
21 | "cropper": "图片裁剪",
22 | "compress": "图片压缩",
23 | "composition": "图片合成"
24 | },
25 | "video": {
26 | "name": "视频处理",
27 | "player": "视频播放器",
28 | "watermark": "视频水印"
29 | },
30 | "compo": {
31 | "name": "组件",
32 | "upload": "图片上传",
33 | "drag": {
34 | "name": "拖拽",
35 | "list": "拖拽列表",
36 | "resize": "组件拖拽"
37 | },
38 | "transfer": "穿梭框",
39 | "countTo": "数字滚动"
40 | },
41 | "editor": {
42 | "name": "文本编辑器",
43 | "markdown": "Markdown编辑器",
44 | "richText": "富文本编辑器",
45 | "codeEditor": "代码编辑器"
46 | },
47 | "flow": {
48 | "name": "图形编辑器",
49 | "approve": "审批流程图",
50 | "bpmn": "BPMN流程图"
51 | },
52 | "tree": {
53 | "name": "树形结构",
54 | "org": "组织树",
55 | "antdTree": "控件树"
56 | },
57 | "excel": {
58 | "name": "Excel",
59 | "export": "导出Excel",
60 | "import": "导入Excel"
61 | },
62 | "exception": {
63 | "name": "异常页面",
64 | "403": "403页面",
65 | "404": "404页面",
66 | "500": "500页面"
67 | },
68 | "iframe": {
69 | "name": "外部页面",
70 | "vue": "Vue文档",
71 | "pinia": "Pinia文档",
72 | "antd": "Antd文档"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/locales/langs/zh_CN/system.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": {
3 | "logoutTip": "温馨提醒",
4 | "logoutMsg": "是否确认退出系统?",
5 | "logoutError": "退出系统失败!"
6 | },
7 | "login": {
8 | "accountLogin": "账 号 登 录",
9 | "accountMsg": "请输入账号",
10 | "passwordMsg": "请输入密码",
11 | "rememberMe": "记住我",
12 | "forgetPassword": "忘记密码?",
13 | "loginButton": "登 录",
14 | "loginSuccessTip": "登陆成功!"
15 | },
16 | "modal": {
17 | "okText": "确认",
18 | "closeText": "关闭",
19 | "cancelText": "取消",
20 | "saveText": "保存",
21 | "resetText": "重置"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/locales/langs/zh_TW/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "widgets": {
3 | "fullScreen": "全屏",
4 | "exitFullScreen": "退出全屏",
5 | "refresh": "刷新",
6 | "search": "查詢"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/locales/langs/zh_TW/system.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": {
3 | "logoutTip": "溫馨提醒",
4 | "logoutMsg": "是否確認退出系統?",
5 | "logoutError": "退出系統失敗!"
6 | },
7 | "login": {
8 | "accountLogin": "賬 號 登 錄",
9 | "accountMsg": "請輸入賬號",
10 | "passwordMsg": "請輸入密碼",
11 | "rememberMe": "記住我",
12 | "forgetPassword": "忘記密碼?",
13 | "loginButton": "登 錄",
14 | "loginSuccessTip": "登陸成功!"
15 | },
16 | "modal": {
17 | "okText": "確認",
18 | "closeText": "關閉",
19 | "cancelText": "取消",
20 | "saveText": "保存",
21 | "resetText": "重置"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/logics/initAppConfig.ts:
--------------------------------------------------------------------------------
1 | import type { AppConfig } from '@/types/config'
2 | import { APP_CONFIG_KEY } from '@/enums/cacheEnum'
3 | import { updateDarkTheme } from '@/logics/theme/mode'
4 | import { useAppStore } from '@/stores/modules/app'
5 | import { deepMerge } from '@/utils'
6 | import { appSetting } from '@/settings/appBaseSetting'
7 | import { Persistent } from '@/utils/cache/persistent'
8 |
9 | // Initial project configuration
10 | export function initAppConfigStore() {
11 | const appStore = useAppStore()
12 |
13 | const appMode = appStore.getAppMode
14 | updateDarkTheme(appMode)
15 |
16 | let appConfig: AppConfig = Persistent.getLocal(APP_CONFIG_KEY) as AppConfig
17 | appConfig = deepMerge(appSetting, appConfig || {})
18 |
19 | appStore.setAppConfig(appConfig)
20 | }
21 |
--------------------------------------------------------------------------------
/src/logics/mitt/routeChange.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * used to monitor routing changes to change the status of menus and tags.
3 | * there is no need to monitor the route, because the route status change is affected by the page rendering time, which will be slow.
4 | */
5 |
6 | import { mitt } from '@/utils/mitt'
7 | import type { RouteLocationNormalized } from 'vue-router'
8 | import { getRawRoute } from '@/utils'
9 |
10 | const key = Symbol()
11 |
12 | const emitter = mitt<{
13 | [key]: RouteLocationNormalized
14 | }>()
15 |
16 | let lastChangeTag: RouteLocationNormalized
17 |
18 | export function setRouteChange(lastChangeRoute: RouteLocationNormalized) {
19 | const route = getRawRoute(lastChangeRoute)
20 | emitter.emit(key, route)
21 | lastChangeTag = route
22 | }
23 |
24 | export function listenerRouteChange(callback: (route: RouteLocationNormalized) => void, immediate = true) {
25 | emitter.on(key, callback)
26 | immediate && lastChangeTag && callback(lastChangeTag)
27 | }
28 |
29 | export function removeRouteChange() {
30 | emitter.clear()
31 | }
32 |
--------------------------------------------------------------------------------
/src/logics/theme/index.ts:
--------------------------------------------------------------------------------
1 | import { computed, unref } from 'vue'
2 | import { css } from '@emotion/css'
3 | import { hasClass } from '@/utils/dom'
4 | import { useBaseSetting } from '@/hooks/setting/useBaseSetting'
5 |
6 | export function setThemColor(el: Element) {
7 | const { getThemeColor } = useBaseSetting()
8 | const themeColor = unref(getThemeColor) || '#1890ff'
9 |
10 | const htmlCls = computed(
11 | () => css`
12 | .layout-hybrid-sider {
13 | .main-menu__item.is-active {
14 | background: ${themeColor};
15 | }
16 | &.light {
17 | .main-menu__item {
18 | &.is-active {
19 | color: ${themeColor};
20 | background: #fff;
21 | }
22 | &:hover,
23 | &.is-active {
24 | &::before {
25 | background: ${themeColor};
26 | }
27 | }
28 | }
29 | .sub-menu__title {
30 | .pushpin:hover {
31 | color: ${themeColor};
32 | }
33 | }
34 | }
35 | }
36 | `
37 | )
38 |
39 | if (hasClass(el, 'dark')) {
40 | el.className = `${unref(htmlCls)} dark`
41 | } else {
42 | el.className = unref(htmlCls)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/logics/theme/mode.ts:
--------------------------------------------------------------------------------
1 | import { addClass, hasClass, removeClass } from '@/utils/dom'
2 | import { toggleClass } from './util'
3 | import { setThemColor } from './'
4 |
5 | // Change the system dark mode
6 | export async function updateDarkTheme(mode: string | null = 'light') {
7 | const htmlRoot = document.getElementById('htmlRoot')
8 | if (!htmlRoot) return
9 |
10 | setThemColor(htmlRoot)
11 | const hasDarkClass = hasClass(htmlRoot, 'dark')
12 |
13 | if (mode === 'dark') {
14 | htmlRoot.setAttribute('data-theme', 'dark')
15 |
16 | if (!hasDarkClass) {
17 | addClass(htmlRoot, 'dark')
18 | }
19 | } else {
20 | htmlRoot.setAttribute('data-theme', 'light')
21 |
22 | if (hasDarkClass) {
23 | removeClass(htmlRoot, 'dark')
24 | }
25 | }
26 | }
27 |
28 | // Change the system color weak mode
29 | export function updateColorWeak(colorWeak: boolean) {
30 | toggleClass(colorWeak, 'color-weak', document.documentElement)
31 | }
32 |
33 | // Change the system gray mode
34 | export function updateGrayMode(gray: boolean) {
35 | toggleClass(gray, 'gray-mode', document.documentElement)
36 | }
37 |
--------------------------------------------------------------------------------
/src/logics/theme/util.ts:
--------------------------------------------------------------------------------
1 | const docEle = document.documentElement
2 | export function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
3 | const targetEl = target || document.body
4 | let { className } = targetEl
5 | className = className.replace(clsName, '')
6 | targetEl.className = flag ? `${className} ${clsName} ` : className
7 | }
8 |
9 | export function setCssVar(prop: string, val: any, dom = docEle) {
10 | dom.style.setProperty(prop, val)
11 | }
12 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import '@/design/index.less'
2 | // register svg icon
3 | import 'virtual:svg-icons-register'
4 |
5 | import { createApp } from 'vue'
6 | import { initAppConfigStore } from '@/logics/initAppConfig'
7 |
8 | import App from './App'
9 |
10 | import { setupRouter, router } from './router'
11 | import { setupStore } from '@/stores'
12 | import { setupRouterGuard } from '@/router/guard'
13 | import { setupPlugins } from '@/plugins'
14 | import { setupI18n } from '@/locales'
15 |
16 | async function launchApp() {
17 | const app = createApp(App)
18 |
19 | // Configure store
20 | setupStore(app)
21 |
22 | // Initialize internal system configuration
23 | initAppConfigStore()
24 |
25 | // Configure router
26 | setupRouter(app)
27 |
28 | setupRouterGuard(router)
29 |
30 | setupI18n(app)
31 |
32 | setupPlugins(app)
33 |
34 | app.mount('#app')
35 | }
36 |
37 | launchApp()
38 |
--------------------------------------------------------------------------------
/src/plugins/index.ts:
--------------------------------------------------------------------------------
1 | import type { App } from 'vue'
2 |
3 | const pluginModules = import.meta.glob('./modules/*.ts', { eager: true }) as Object
4 |
5 | const pluginModulesList: any[] = []
6 |
7 | Object.keys(pluginModules).forEach(key => {
8 | const module = pluginModules[key].default || {}
9 | pluginModulesList.push(module)
10 | })
11 |
12 | export function setupPlugins(app: App) {
13 | pluginModulesList.forEach(plugin => {
14 | app.use(plugin)
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/src/plugins/modules/form-create-designer.ts:
--------------------------------------------------------------------------------
1 | import FcDesigner from '@form-create/designer'
2 |
3 | export default FcDesigner
4 |
--------------------------------------------------------------------------------
/src/plugins/modules/form-create-ui.ts:
--------------------------------------------------------------------------------
1 | // The form-create-designer UI component uses element-plus
2 | import formCreateUI from 'element-plus'
3 | import 'element-plus/dist/index.css'
4 |
5 | export default formCreateUI
6 |
--------------------------------------------------------------------------------
/src/plugins/modules/org-tree.ts:
--------------------------------------------------------------------------------
1 | import vue3TreeOrg from 'vue3-tree-org'
2 | import 'vue3-tree-org/lib/vue3-tree-org.css'
3 |
4 | export default vue3TreeOrg
5 |
--------------------------------------------------------------------------------
/src/router/guard/index.ts:
--------------------------------------------------------------------------------
1 | import type { Router } from 'vue-router'
2 | import { unref } from 'vue'
3 | import nProgress from 'nprogress'
4 | import { createPermissionGuard } from './permissionGuard'
5 | import { setRouteChange } from '@/logics/mitt/routeChange'
6 | import { useTransitionSetting } from '@/hooks/setting/useTransitionSetting'
7 |
8 | export function setupRouterGuard(router: Router) {
9 | createPageGuard(router)
10 | createProgressGuard(router)
11 | createPermissionGuard(router)
12 | }
13 |
14 | // Hooks for handling page state
15 | function createPageGuard(router: Router) {
16 | const loadedPageMap = new Map()
17 |
18 | router.beforeEach(async to => {
19 | // The page has already been loaded, it will be faster to open it again, you don’t need to do loading and other processing
20 | to.meta.loaded = !!loadedPageMap.get(to.path)
21 | // Notify routing changes
22 | setRouteChange(to)
23 |
24 | return true
25 | })
26 |
27 | router.afterEach(to => {
28 | loadedPageMap.set(to.path, true)
29 | })
30 | }
31 |
32 | export function createProgressGuard(router: Router) {
33 | const { getOpenNProgress } = useTransitionSetting()
34 | router.beforeEach(async to => {
35 | if (to.meta.loaded) {
36 | return true
37 | }
38 | unref(getOpenNProgress) && nProgress.start()
39 | return true
40 | })
41 |
42 | router.afterEach(async () => {
43 | unref(getOpenNProgress) && nProgress.done()
44 | return true
45 | })
46 | }
47 |
--------------------------------------------------------------------------------
/src/router/helper/routeHelper.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baimingxuan/vue3-admin-design/d368ff2f1680414ae9b9fed93ced7cfba8b76b7f/src/router/helper/routeHelper.ts
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import type { App } from 'vue'
2 | import type { RouteRecordRaw } from 'vue-router'
3 |
4 | import { createRouter, createWebHashHistory } from 'vue-router'
5 | import { basicRoutes } from './routes'
6 |
7 | // create router
8 | export const router = createRouter({
9 | history: createWebHashHistory(),
10 | routes: basicRoutes as unknown as RouteRecordRaw[],
11 | strict: true,
12 | scrollBehavior: () => ({ left: 0, top: 0 })
13 | })
14 |
15 | // reset router
16 | export function resetRouter() {
17 | router.getRoutes().forEach(route => {
18 | const { name } = route
19 | if (name) {
20 | router.hasRoute(name) && router.removeRoute(name)
21 | }
22 | })
23 | }
24 |
25 | export function setupRouter(app: App) {
26 | app.use(router)
27 | }
28 |
--------------------------------------------------------------------------------
/src/router/routes/modules/excel.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import Layout from '@/layout'
4 |
5 | // excel module page
6 | const ExcelRoute: AppRoute = {
7 | path: '/excel',
8 | name: 'Excel',
9 | component: Layout,
10 | redirect: '/excel/export-excel',
11 | meta: {
12 | title: t('routes.excel.name'),
13 | icon: 'excel',
14 | orderNo: 10
15 | },
16 | children: [
17 | {
18 | path: 'export-excel',
19 | name: 'ExportExcel',
20 | component: () => import('@/views/excel/export-excel'),
21 | meta: {
22 | title: t('routes.excel.export')
23 | }
24 | },
25 | {
26 | path: 'import-excel',
27 | name: 'ImportExcel',
28 | component: () => import('@/views/excel/import-excel'),
29 | meta: {
30 | title: t('routes.excel.import')
31 | }
32 | }
33 | ]
34 | }
35 |
36 | export default ExcelRoute
37 |
--------------------------------------------------------------------------------
/src/router/routes/modules/exception.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import { ExceptionEnum } from '@/enums/exceptionEnum'
4 | import Layout from '@/layout'
5 |
6 | // exception module page
7 | const ExceptionRoute: AppRoute = {
8 | path: '/exception',
9 | name: 'ExceptionPage',
10 | component: Layout,
11 | redirect: '/exception/page-403',
12 | meta: {
13 | title: t('routes.exception.name'),
14 | icon: 'bug',
15 | orderNo: 11
16 | },
17 | children: [
18 | {
19 | path: 'page-403',
20 | name: 'Page403',
21 | component: () => import('@/views/exception/index'),
22 | props: {
23 | status: ExceptionEnum.PAGE_NOT_ACCESS,
24 | withCard: true
25 | },
26 | meta: {
27 | title: t('routes.exception.403')
28 | }
29 | },
30 | {
31 | path: 'page-404',
32 | name: 'Page404',
33 | component: () => import('@/views/exception/index'),
34 | props: {
35 | status: ExceptionEnum.PAGE_NOT_FOUND,
36 | withCard: true
37 | },
38 | meta: {
39 | title: t('routes.exception.404')
40 | }
41 | },
42 | {
43 | path: 'page-500',
44 | name: 'Page500',
45 | component: () => import('@/views/exception/index'),
46 | props: {
47 | status: ExceptionEnum.SERVER_ERROR,
48 | withCard: true
49 | },
50 | meta: {
51 | title: t('routes.exception.500')
52 | }
53 | }
54 | ]
55 | }
56 |
57 | export default ExceptionRoute
58 |
--------------------------------------------------------------------------------
/src/router/routes/modules/flow-editor.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import Layout from '@/layout'
4 |
5 | // flow-editor module page
6 | const FlowEditorRoute: AppRoute = {
7 | path: '/flow-editor',
8 | name: 'FlowEditor',
9 | component: Layout,
10 | redirect: '/flow-editor/flow-approve',
11 | meta: {
12 | title: t('routes.flow.name'),
13 | icon: 'flow',
14 | orderNo: 8
15 | },
16 | children: [
17 | {
18 | path: 'flow-approve',
19 | name: 'FlowApprove',
20 | component: () => import('@/views/flow/flow-approve'),
21 | meta: {
22 | title: t('routes.flow.approve')
23 | }
24 | },
25 | {
26 | path: 'flow-bpmn',
27 | name: 'FlowBpmn',
28 | component: () => import('@/views/flow/flow-bpmn'),
29 | meta: {
30 | title: t('routes.flow.bpmn')
31 | }
32 | }
33 | ]
34 | }
35 |
36 | export default FlowEditorRoute
37 |
--------------------------------------------------------------------------------
/src/router/routes/modules/form.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import Layout from '@/layout'
4 |
5 | // form module page
6 | const FormRoute: AppRoute = {
7 | path: '/form',
8 | name: 'Form',
9 | component: Layout,
10 | redirect: '/form/basic-form',
11 | meta: {
12 | title: t('routes.form.name'),
13 | icon: 'form',
14 | orderNo: 2
15 | },
16 | children: [
17 | {
18 | path: 'basic-form',
19 | name: 'BasicForm',
20 | component: () => import('@/views/form/basic-form'),
21 | meta: {
22 | title: t('routes.form.basic')
23 | }
24 | },
25 | {
26 | path: 'form-designer',
27 | name: 'FormDesigner',
28 | component: () => import('@/views/form/form-designer'),
29 | meta: {
30 | title: t('routes.form.designer')
31 | }
32 | }
33 | ]
34 | }
35 |
36 | export default FormRoute
37 |
--------------------------------------------------------------------------------
/src/router/routes/modules/home.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import Layout from '@/layout'
4 |
5 | // Home route
6 | const HomeRoute: AppRoute = {
7 | path: '/home',
8 | name: 'Home',
9 | component: Layout,
10 | redirect: '/home',
11 | meta: {
12 | title: t('routes.base.home'),
13 | icon: 'home',
14 | affix: true,
15 | orderNo: 1,
16 | hideChildrenInMenu: true
17 | },
18 | children: [
19 | {
20 | path: '',
21 | name: 'HomePage',
22 | component: () => import('@/views/home'),
23 | meta: {
24 | title: t('routes.base.home'),
25 | icon: 'home',
26 | hideMenu: true
27 | }
28 | }
29 | ]
30 | }
31 |
32 | export default HomeRoute
33 |
--------------------------------------------------------------------------------
/src/router/routes/modules/iframe.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import Layout from '@/layout'
4 |
5 | const IframeBlank = { template: '' }
6 |
7 | // iframe module page
8 | const IframeRoute: AppRoute = {
9 | path: '/iframe',
10 | name: 'IframePage',
11 | component: Layout,
12 | redirect: '/iframe/vue-doc',
13 | meta: {
14 | title: t('routes.iframe.name'),
15 | icon: 'computer',
16 | orderNo: 12
17 | },
18 | children: [
19 | {
20 | path: 'vue-doc',
21 | name: 'VueDoc',
22 | component: IframeBlank,
23 | meta: {
24 | title: t('routes.iframe.vue'),
25 | iframeSrc: 'https://cn.vuejs.org/'
26 | }
27 | },
28 | {
29 | path: 'pinia-doc',
30 | name: 'PiniaDoc',
31 | component: IframeBlank,
32 | meta: {
33 | title: t('routes.iframe.pinia'),
34 | iframeSrc: 'https://pinia.vuejs.org/zh/'
35 | }
36 | },
37 | {
38 | path: 'antd-doc',
39 | name: 'AntdDoc',
40 | component: IframeBlank,
41 | meta: {
42 | title: t('routes.iframe.antd'),
43 | iframeSrc: 'https://antdv.com/docs/vue/introduce-cn'
44 | }
45 | }
46 | ]
47 | }
48 |
49 | export default IframeRoute
50 |
--------------------------------------------------------------------------------
/src/router/routes/modules/image.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import Layout from '@/layout'
4 |
5 | // image module page
6 | const ImageRoute: AppRoute = {
7 | path: '/image',
8 | name: 'Image',
9 | component: Layout,
10 | redirect: '/image/image-cropper',
11 | meta: {
12 | title: t('routes.image.name'),
13 | icon: 'image',
14 | orderNo: 4
15 | },
16 | children: [
17 | {
18 | path: 'image-cropper',
19 | name: 'ImageCropper',
20 | component: () => import('@/views/image/image-cropper'),
21 | meta: {
22 | title: t('routes.image.cropper')
23 | }
24 | },
25 | {
26 | path: 'image-compress',
27 | name: 'ImageCompress',
28 | component: () => import('@/views/image/image-compress'),
29 | meta: {
30 | title: t('routes.image.compress')
31 | }
32 | },
33 | {
34 | path: 'image-composition',
35 | name: 'ImageComposition',
36 | component: () => import('@/views/image/image-composition'),
37 | meta: {
38 | title: t('routes.image.composition')
39 | }
40 | }
41 | ]
42 | }
43 |
44 | export default ImageRoute
45 |
--------------------------------------------------------------------------------
/src/router/routes/modules/table.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import Layout from '@/layout'
4 |
5 | // table module page
6 | const TableRoute: AppRoute = {
7 | path: '/table',
8 | name: 'Table',
9 | component: Layout,
10 | redirect: '/table/table-basic',
11 | meta: {
12 | title: t('routes.table.name'),
13 | icon: 'table',
14 | orderNo: 3
15 | },
16 | children: [
17 | {
18 | path: 'table-basic',
19 | name: 'TableBasic',
20 | component: () => import('@/views/table/table-basic'),
21 | meta: {
22 | title: t('routes.table.basic')
23 | }
24 | },
25 | {
26 | path: 'table-edit-row',
27 | name: 'TableEditRow',
28 | component: () => import('@/views/table/table-edit-row'),
29 | meta: {
30 | title: t('routes.table.editRow')
31 | }
32 | }
33 | ]
34 | }
35 |
36 | export default TableRoute
37 |
--------------------------------------------------------------------------------
/src/router/routes/modules/text-editor.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import Layout from '@/layout'
4 |
5 | // text-editor module page
6 | const TextEditorRoute: AppRoute = {
7 | path: '/editor',
8 | name: 'Editor',
9 | component: Layout,
10 | redirect: '/editor/markdown',
11 | meta: {
12 | title: t('routes.editor.name'),
13 | icon: 'editor',
14 | orderNo: 7
15 | },
16 | children: [
17 | {
18 | path: 'markdown',
19 | name: 'Markdown',
20 | component: () => import('@/views/editor/markdown'),
21 | meta: {
22 | title: t('routes.editor.markdown')
23 | }
24 | },
25 | {
26 | path: 'rich-text',
27 | name: 'RichText',
28 | component: () => import('@/views/editor/rich-text'),
29 | meta: {
30 | title: t('routes.editor.richText')
31 | }
32 | },
33 | {
34 | path: 'code-editor',
35 | name: 'CodeEditor',
36 | component: () => import('@/views/editor/code-mirror'),
37 | meta: {
38 | title: t('routes.editor.codeEditor')
39 | }
40 | }
41 | ]
42 | }
43 |
44 | export default TextEditorRoute
45 |
--------------------------------------------------------------------------------
/src/router/routes/modules/tree.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import Layout from '@/layout'
4 |
5 | // tree module page
6 | const TreeRoute: AppRoute = {
7 | path: '/tree',
8 | name: 'Tree',
9 | component: Layout,
10 | redirect: '/tree/org-tree',
11 | meta: {
12 | title: t('routes.tree.name'),
13 | icon: 'tree',
14 | orderNo: 9
15 | },
16 | children: [
17 | {
18 | path: 'org-tree',
19 | name: 'OrgTree',
20 | component: () => import('@/views/tree/org-tree/index'),
21 | meta: {
22 | title: t('routes.tree.org')
23 | }
24 | },
25 | {
26 | path: 'antd-tree',
27 | name: 'AntdTree',
28 | component: () => import('@/views/tree/antd-tree/index'),
29 | meta: {
30 | title: t('routes.tree.antdTree')
31 | }
32 | }
33 | ]
34 | }
35 |
36 | export default TreeRoute
37 |
--------------------------------------------------------------------------------
/src/router/routes/modules/video.ts:
--------------------------------------------------------------------------------
1 | import type { AppRoute } from '../../types'
2 | import { t } from '@/locales/helper'
3 | import Layout from '@/layout'
4 |
5 | // video module page
6 | const VideoRoute: AppRoute = {
7 | path: '/video',
8 | name: 'Video',
9 | component: Layout,
10 | redirect: '/video/video-player',
11 | meta: {
12 | title: t('routes.video.name'),
13 | icon: 'video',
14 | orderNo: 5
15 | },
16 | children: [
17 | {
18 | path: 'video-player',
19 | name: 'VideoPlayer',
20 | component: () => import('@/views/video/video-player'),
21 | meta: {
22 | title: t('routes.video.player')
23 | }
24 | },
25 | {
26 | path: 'video-watermark',
27 | name: 'VideoWatermark',
28 | component: () => import('@/views/video/video-watermark'),
29 | meta: {
30 | title: t('routes.video.watermark')
31 | }
32 | }
33 | ]
34 | }
35 |
36 | export default VideoRoute
37 |
--------------------------------------------------------------------------------
/src/router/types.ts:
--------------------------------------------------------------------------------
1 | import type { RouteRecordRaw, RouteMeta } from 'vue-router'
2 | import type { defineComponent } from 'vue'
3 |
4 | export type Component =
5 | | ReturnType
6 | | (() => Promise)
7 | | (() => Promise)
8 |
9 | // RouteRecordRaw detail, see: https://router.vuejs.org/api/#routerecordraw
10 | export interface AppRoute extends Omit {
11 | name: string
12 | meta: RouteMeta
13 | component?: Component | string
14 | children?: AppRoute[]
15 | props?: Recordable
16 | fullPath?: string
17 | }
18 |
19 | export interface AppMenu {
20 | name: string
21 | path: string
22 | children?: AppMenu[]
23 | meta?: Partial
24 | icon?: string
25 | affix?: boolean
26 | orderNo?: number
27 | ignoreKeepAlive?: boolean
28 | hideMenu?: boolean
29 | hideChildrenInMenu?: boolean
30 | }
31 |
--------------------------------------------------------------------------------
/src/settings/designSetting.ts:
--------------------------------------------------------------------------------
1 | import { AppModeEnum } from '@/enums/appEnum'
2 |
3 | // app mode
4 | export const baseAppMode = AppModeEnum.LIGHT
5 |
6 | // app theme color
7 | export const APP_THEME_COLOR_LIST = [
8 | {
9 | name: '拂晓蓝',
10 | color: '#1765AE'
11 | },
12 | {
13 | name: '薄暮',
14 | color: '#A71A1F'
15 | },
16 | {
17 | name: '火山',
18 | color: '#AE3E17'
19 | },
20 | {
21 | name: '日暮',
22 | color: '#B8831B'
23 | },
24 | {
25 | name: '明青',
26 | color: '#269491'
27 | },
28 | {
29 | name: '极光绿',
30 | color: '#509827'
31 | },
32 | {
33 | name: '极客蓝',
34 | color: '#20389A'
35 | },
36 | {
37 | name: '酱紫',
38 | color: '#60339A'
39 | }
40 | ]
41 |
--------------------------------------------------------------------------------
/src/settings/encryptionSetting.ts:
--------------------------------------------------------------------------------
1 | import { isDevMode } from '@/utils/env'
2 |
3 | // System default cache time, in seconds
4 | export const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
5 |
6 | // aes encryption key
7 | export const cacheCipher = {
8 | key: '_11111000001111@',
9 | iv: '@11111000001111_'
10 | }
11 |
12 | // Whether the system cache is encrypted using aes
13 | export const enableStorageEncryption = !isDevMode()
14 |
--------------------------------------------------------------------------------
/src/settings/localeSetting.ts:
--------------------------------------------------------------------------------
1 | import type { LocaleType } from '@/types'
2 | import { LocaleEnum } from '@/enums/appEnum'
3 |
4 | interface DropMenu {
5 | text: string
6 | event: LocaleEnum
7 | onClick?: Fn
8 | }
9 |
10 | export const localePool: LocaleType[] = [LocaleEnum.ZH_CN, LocaleEnum.Zh_TW, LocaleEnum.EN_US]
11 |
12 | export const localeList: DropMenu[] = [
13 | {
14 | text: '简体中文',
15 | event: LocaleEnum.ZH_CN
16 | },
17 | {
18 | text: '繁體中文',
19 | event: LocaleEnum.Zh_TW
20 | },
21 | {
22 | text: 'English',
23 | event: LocaleEnum.EN_US
24 | }
25 | ]
26 |
--------------------------------------------------------------------------------
/src/stores/index.ts:
--------------------------------------------------------------------------------
1 | import type { App } from 'vue'
2 | import { createPinia } from 'pinia'
3 | import { registerPiniaPersistPlugin } from './plugin/persist'
4 |
5 | const stores = createPinia()
6 | registerPiniaPersistPlugin(stores)
7 |
8 | export function setupStore(app: App) {
9 | app.use(stores)
10 | }
11 |
12 | export { stores }
13 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export type ThemeMode = 'dark' | 'light'
2 |
3 | export type LocaleType = 'zh_CN' | 'zh_TW' | 'en_US'
4 |
5 | export type SelectOptions = {
6 | label: string
7 | value: any
8 | [key: string]: string | number | boolean
9 | }[]
10 |
11 | export interface styleState {
12 | fontFamily?: string
13 | fontSize?: string
14 | lineHeight?: string
15 | color?: string
16 | backgroundColor?: string
17 | fontWeight?: string
18 | fontStyle?: string
19 | textShadow?: string
20 | textAlign?: string
21 | }
22 |
23 | export interface LoginFormState {
24 | username: string
25 | password: string
26 | remember?: boolean
27 | }
28 |
29 | export interface UserInfo {
30 | userId: string | number
31 | username: string
32 | realName: string
33 | avatar: string
34 | desc?: string
35 | homePath?: string
36 | }
37 |
38 | export interface LockInfo {
39 | pwd?: string | undefined
40 | isLock?: boolean
41 | }
42 |
--------------------------------------------------------------------------------
/src/types/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 任意类型的异步函数
3 | */
4 | export type AnyPromiseFunction = (...arg: any[]) => PromiseLike
5 |
6 | /**
7 | * 任意类型的普通函数
8 | */
9 | export type AnyNormalFunction = (...arg: any[]) => any
10 |
11 | /**
12 | * 任意类型的函数
13 | */
14 | export type AnyFunction = AnyNormalFunction | AnyPromiseFunction
15 |
16 | /**
17 | * T | null 包装
18 | */
19 | export type Nullable = T | null
20 |
21 | /**
22 | * T | Not null 包装
23 | */
24 | export type NonNullable = T extends null | undefined ? never : T
25 |
26 | /**
27 | * 字符串类型对象
28 | */
29 | export type Recordable = Record
30 |
31 | /**
32 | * 字符串类型对象(只读)
33 | */
34 | export interface ReadonlyRecordable {
35 | readonly [key: string]: T
36 | }
37 |
38 | /**
39 | * setTimeout 返回值类型
40 | */
41 | export type TimeoutHandle = ReturnType
42 |
43 | /**
44 | * setInterval 返回值类型
45 | */
46 | export type IntervalHandle = ReturnType
47 |
--------------------------------------------------------------------------------
/src/utils/auth/index.ts:
--------------------------------------------------------------------------------
1 | import { Persistent, type BasicKeys } from '../cache/persistent'
2 | import { appSetting } from '@/settings/appBaseSetting'
3 | import { TOKEN_KEY, CacheTypeEnum } from '@/enums/cacheEnum'
4 |
5 | const { permissionCacheType } = appSetting
6 | const isLocal = permissionCacheType === CacheTypeEnum.LOCAL
7 |
8 | export function getToken() {
9 | return getAuthCache(TOKEN_KEY)
10 | }
11 |
12 | export function getAuthCache(key: BasicKeys) {
13 | const fn = isLocal ? Persistent.getLocal : Persistent.getSession
14 | return fn(key) as T
15 | }
16 |
17 | export function setAuthCache(key: BasicKeys, value) {
18 | const fn = isLocal ? Persistent.setLocal : Persistent.setSession
19 | return fn(key, value, true)
20 | }
21 |
22 | export function clearAuthCache(immediate = true) {
23 | const fn = isLocal ? Persistent.clearLocal : Persistent.clearSession
24 | return fn(immediate)
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/axios/index.ts:
--------------------------------------------------------------------------------
1 | import type { AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios'
2 | import axios from 'axios'
3 | import { message } from 'ant-design-vue'
4 |
5 | import { router } from '@/router'
6 | import { getToken, clearAuthCache } from '@/utils/auth'
7 |
8 | // Create axios instance
9 | const service = axios.create({
10 | baseURL: '/api',
11 | timeout: 10 * 1000
12 | })
13 |
14 | // Handle Error
15 | const handleError = (error: AxiosError): Promise => {
16 | if (error.response?.status === 401 || error.response?.status === 504) {
17 | clearAuthCache()
18 | router.push({ path: '/login' })
19 | }
20 | message.error(error.message || 'error')
21 | return Promise.reject(error)
22 | }
23 |
24 | // Request interceptors configuration
25 | service.interceptors.request.use((config: InternalAxiosRequestConfig) => {
26 | const token = getToken()
27 | if (token) {
28 | ;(config as Recordable).headers['Authorization'] = `${token}`
29 | }
30 | ;(config as Recordable).headers['Content-Type'] = 'application/json'
31 | return config
32 | }, handleError)
33 |
34 | // Respose interceptors configuration
35 | service.interceptors.response.use((response: AxiosResponse) => {
36 | const data = response.data
37 |
38 | if (data.code === 0) {
39 | return data.data
40 | } else {
41 | message.error(data.message)
42 |
43 | return Promise.reject('error')
44 | }
45 | }, handleError)
46 |
47 | export { service }
48 |
--------------------------------------------------------------------------------
/src/utils/cache/index.ts:
--------------------------------------------------------------------------------
1 | import { createStorage as create, type CreateStorageParams } from './storageCache'
2 | import { enableStorageEncryption, DEFAULT_CACHE_TIME } from '@/settings/encryptionSetting'
3 |
4 | type Options = Partial
5 |
6 | const createOptions = (storage: Storage, options: Options = {}): Options => {
7 | return {
8 | // No encryption in debug mode
9 | hasEncrypt: enableStorageEncryption,
10 | storage,
11 | prefixKey: 'vue-admin-design__',
12 | ...options
13 | }
14 | }
15 |
16 | const WebStorage = create(createOptions(sessionStorage))
17 |
18 | export const createStorage = (storage: Storage = sessionStorage, options: Options = {}) => {
19 | return create(createOptions(storage, options))
20 | }
21 |
22 | export const createSessionStorage = (options: Options = {}) => {
23 | return createStorage(sessionStorage, { ...options, timeout: DEFAULT_CACHE_TIME })
24 | }
25 |
26 | export const createLocalStorage = (options: Options = {}) => {
27 | return createStorage(localStorage, { ...options, timeout: DEFAULT_CACHE_TIME })
28 | }
29 |
30 | export default WebStorage
31 |
--------------------------------------------------------------------------------
/src/utils/cipher.ts:
--------------------------------------------------------------------------------
1 | import { decrypt as aesDecrypt, encrypt as aesEncrypt } from 'crypto-js/aes'
2 | import { parse } from 'crypto-js/enc-utf8'
3 | import UTF8 from 'crypto-js/enc-utf8'
4 | import pkcs7 from 'crypto-js/pad-pkcs7'
5 | import CTR from 'crypto-js/mode-ctr'
6 |
7 | export interface Encryption {
8 | encrypt(plainText: string): string
9 | decrypt(cipherText: string): string
10 | }
11 |
12 | export interface EncryptionParams {
13 | key: string
14 | iv: string
15 | }
16 |
17 | class AesEncryption implements Encryption {
18 | private readonly key
19 | private readonly iv
20 |
21 | constructor({ key, iv }: EncryptionParams) {
22 | this.key = parse(key)
23 | this.iv = parse(iv)
24 | }
25 |
26 | get getOptions() {
27 | return {
28 | mode: CTR,
29 | padding: pkcs7,
30 | iv: this.iv
31 | }
32 | }
33 |
34 | encrypt(plainText: string) {
35 | return aesEncrypt(plainText, this.key, this.getOptions).toString()
36 | }
37 |
38 | decrypt(cipherText: string) {
39 | return aesDecrypt(cipherText, this.key, this.getOptions).toString(UTF8)
40 | }
41 | }
42 |
43 | export class EncryptionFactory {
44 | public static createAesEncryption(params: EncryptionParams): Encryption {
45 | return new AesEncryption(params)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/utils/dateUtil.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Independent time operation tool to facilitate subsequent switch to dayjs
3 | */
4 | import dayjs from 'dayjs'
5 |
6 | const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
7 | const DATE_FORMAT = 'YYYY-MM-DD'
8 |
9 | export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string {
10 | return dayjs(date).format(format)
11 | }
12 |
13 | export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string {
14 | return dayjs(date).format(format)
15 | }
16 |
17 | export const dateUtil = dayjs
18 |
--------------------------------------------------------------------------------
/src/utils/echarts.ts:
--------------------------------------------------------------------------------
1 | import * as echarts from 'echarts/core'
2 | import { SVGRenderer } from 'echarts/renderers'
3 |
4 | import { BarChart, LineChart, PieChart, MapChart, PictorialBarChart, RadarChart } from 'echarts/charts'
5 |
6 | import {
7 | TitleComponent,
8 | TooltipComponent,
9 | GridComponent,
10 | PolarComponent,
11 | AriaComponent,
12 | ParallelComponent,
13 | LegendComponent,
14 | RadarComponent,
15 | ToolboxComponent,
16 | DataZoomComponent,
17 | VisualMapComponent,
18 | TimelineComponent,
19 | CalendarComponent,
20 | GraphicComponent
21 | } from 'echarts/components'
22 |
23 | echarts.use([
24 | LegendComponent,
25 | TitleComponent,
26 | TooltipComponent,
27 | GridComponent,
28 | PolarComponent,
29 | AriaComponent,
30 | ParallelComponent,
31 | BarChart,
32 | LineChart,
33 | PieChart,
34 | MapChart,
35 | RadarChart,
36 | SVGRenderer,
37 | PictorialBarChart,
38 | RadarComponent,
39 | ToolboxComponent,
40 | DataZoomComponent,
41 | VisualMapComponent,
42 | TimelineComponent,
43 | CalendarComponent,
44 | GraphicComponent
45 | ])
46 |
47 | export default echarts
48 |
--------------------------------------------------------------------------------
/src/utils/env.ts:
--------------------------------------------------------------------------------
1 | // Get environment variables
2 | export function getEnv(): string {
3 | return import.meta.env.MODE
4 | }
5 |
6 | // It is a development mode
7 | export function isDevMode(): boolean {
8 | return import.meta.env.DEV
9 | }
10 |
11 | // It is a production mode
12 | export function isProdMode(): boolean {
13 | return import.meta.env.PROD
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/factory/ImportAsyncComponent.tsx:
--------------------------------------------------------------------------------
1 | import { defineAsyncComponent } from 'vue'
2 | import { Spin } from 'ant-design-vue'
3 |
4 | interface Fn {
5 | (...arg: T[]): R
6 | }
7 |
8 | interface Options {
9 | size?: 'default' | 'small' | 'large'
10 | delay?: number
11 | timeout?: number
12 | loading?: boolean
13 | retry?: boolean
14 | }
15 |
16 | export function ImportAsyncComponent(loader: Fn, options: Options = {}) {
17 | const { size = 'small', delay = 100, timeout = 5000, loading = false, retry = true } = options
18 | return defineAsyncComponent({
19 | loader,
20 | loadingComponent: loading ? : undefined,
21 | timeout,
22 | delay,
23 | onError: !retry
24 | ? () => {}
25 | : (error, retry, fail, attempts) => {
26 | if (error.message.match(/fetch/) && attempts <= 3) {
27 | retry()
28 | } else {
29 | fail()
30 | }
31 | }
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/helper/tsxHelper.ts:
--------------------------------------------------------------------------------
1 | import type { Slots } from 'vue'
2 | import { isFunction } from '@/utils/is'
3 |
4 | /**
5 | * @description: Get slot to prevent empty error
6 | */
7 | export function getSlot(
8 | slots: Slots,
9 | slot = 'default',
10 | data?: any,
11 | opts?: {
12 | [key: string]: any
13 | disabled?: boolean
14 | }
15 | ) {
16 | if (!slots || !Reflect.has(slots, slot)) {
17 | return null
18 | }
19 | if (!isFunction(slots[slot])) {
20 | console.error(`${slot} is not a function!`)
21 | return null
22 | }
23 | const slotFn = slots[slot]
24 | if (!slotFn) return null
25 | const params = { ...data, ...opts }
26 | return slotFn(params)
27 | }
28 |
--------------------------------------------------------------------------------
/src/utils/is.ts:
--------------------------------------------------------------------------------
1 | export {
2 | isBoolean,
3 | isDate,
4 | isElement,
5 | isEmpty,
6 | isEqual,
7 | isEqualWith,
8 | isError,
9 | isFunction,
10 | isMap,
11 | isNil,
12 | isNumber,
13 | isNull,
14 | isPlainObject,
15 | isRegExp,
16 | isSet,
17 | isString,
18 | isSymbol,
19 | isWeakMap,
20 | isWeakSet
21 | } from 'lodash-es'
22 |
23 | export function is(val: unknown, type: string) {
24 | return Object.prototype.toString.call(val) === `[object ${type}]`
25 | }
26 |
27 | export function isArray(val: any): val is Array {
28 | return val && Array.isArray(val)
29 | }
30 |
31 | export function isObject(val: any): val is Record {
32 | return val !== null && is(val, 'Object')
33 | }
34 |
35 | export function isDef(val?: T): val is T {
36 | return typeof val !== 'undefined'
37 | }
38 |
39 | export function isUnDef(val?: T): val is T {
40 | return !isDef(val)
41 | }
42 |
43 | export function isUrl(path: string): boolean {
44 | const reg =
45 | /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/
46 | return reg.test(path)
47 | }
48 |
49 | export const isServer = typeof window === 'undefined'
50 |
51 | export const isClient = !isServer
52 |
53 | export const isWindow = (val: unknown): val is Window => {
54 | return val === window
55 | }
56 |
--------------------------------------------------------------------------------
/src/utils/propTypes.ts:
--------------------------------------------------------------------------------
1 | // for details, see https://dwightjack.github.io/vue-types/
2 | import { createTypes } from 'vue-types'
3 |
4 | const propTypes = createTypes({
5 | string: undefined,
6 | number: undefined,
7 | bool: undefined,
8 | integer: undefined,
9 | object: undefined,
10 | func: undefined
11 | })
12 |
13 | export { propTypes }
14 |
--------------------------------------------------------------------------------
/src/utils/resizeEvent.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description referenced from element-ui utils
3 | * @link see: https://github.com/ElemeFE/element/blob/dev/src/utils/resize-event.js
4 | */
5 |
6 | import ResizeObserver from 'resize-observer-polyfill'
7 |
8 | const isServer = typeof window === 'undefined'
9 |
10 | /* istanbul ignore next */
11 | function resizeHandler(entries: any[]) {
12 | for (const entry of entries) {
13 | const listeners = entry.target.__resizeListeners__ || []
14 | if (listeners.length) {
15 | listeners.forEach((fn: () => any) => {
16 | fn()
17 | })
18 | }
19 | }
20 | }
21 |
22 | /* istanbul ignore next */
23 | export function addResizeListener(element: any, fn: () => any) {
24 | if (isServer) return
25 | if (!element.__resizeListeners__) {
26 | element.__resizeListeners__ = []
27 | element.__ro__ = new ResizeObserver(resizeHandler)
28 | element.__ro__.observe(element)
29 | }
30 | element.__resizeListeners__.push(fn)
31 | }
32 |
33 | /* istanbul ignore next */
34 | export function removeResizeListener(element: any, fn: () => any) {
35 | if (!element || !element.__resizeListeners__) return
36 | element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1)
37 | if (!element.__resizeListeners__.length) {
38 | element.__ro__.disconnect()
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/utils/rich-text.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * div contenteditable 光标定位到最后
3 | * @param target
4 | * @returns
5 | */
6 | export function keepCursorEnd(target: HTMLElement) {
7 | // 非IE浏览器
8 | if (window.getSelection) {
9 | // 解决Firefox不获取焦点无法定位问题
10 | target.focus()
11 | // 创建range对象
12 | const range = window.getSelection()
13 | // 选择target下所有子内容
14 | range?.selectAllChildren(target)
15 | // 光标移至最后
16 | range?.collapseToEnd()
17 | } else if ((document as any).selection) {
18 | // IE浏览器
19 | // 创建range对象
20 | const range = (document as any).selection.createRange()
21 | // 定位到target
22 | range.moveToElementText(target)
23 | // 光标移至最后
24 | range.collapse(false)
25 | range.select()
26 | }
27 | }
28 |
29 | /**
30 | * 获取粘贴的纯文本
31 | * @param event
32 | * @returns {string}
33 | */
34 | export function getPasteText(event: ClipboardEvent): string {
35 | const clipboardData = event.clipboardData || (window as any).clipboardData
36 | let pasteText = ''
37 | if (clipboardData && clipboardData.getData) {
38 | pasteText = clipboardData.getData('text/plain')
39 | }
40 | return pasteText
41 | }
42 |
--------------------------------------------------------------------------------
/src/views/compo/transfer/data.ts:
--------------------------------------------------------------------------------
1 | import type { TransferProps } from 'ant-design-vue'
2 |
3 | interface MockData {
4 | key: string
5 | title: string
6 | }
7 |
8 | const mockData: MockData[] = []
9 | for (let i = 1; i < 10; i++) {
10 | mockData.push({
11 | key: i.toString(),
12 | title: `备选项 ${i}`
13 | })
14 | }
15 |
16 | const treeData: TransferProps['dataSource'] = [
17 | { key: '1', title: '备选项 1' },
18 | {
19 | key: '2',
20 | title: '备选项 2',
21 | children: [
22 | { key: '2-1', title: '备选项 2-1' },
23 | { key: '2-2', title: '备选项 2-2' },
24 | { key: '2-3', title: '备选项 2-3' }
25 | ]
26 | },
27 | { key: '3', title: '备选项 3-1' },
28 | {
29 | key: '4',
30 | title: '备选项 4',
31 | children: [
32 | { key: '4-1', title: '备选项 4-1' },
33 | {
34 | key: '4-2',
35 | title: '备选项 4-2',
36 | children: [{ key: '4-2-1', title: '备选项 4-2-1' }]
37 | },
38 | { key: '4-3', title: '备选项 4-3' }
39 | ]
40 | }
41 | ]
42 |
43 | const transferDataSource: TransferProps['dataSource'] = []
44 | function flatten(list: TransferProps['dataSource'] = []) {
45 | list.forEach(item => {
46 | transferDataSource?.push(item)
47 | flatten(item.children)
48 | })
49 | }
50 | flatten(JSON.parse(JSON.stringify(treeData)))
51 |
52 | export { mockData, treeData, transferDataSource }
53 |
--------------------------------------------------------------------------------
/src/views/editor/code-mirror/components/CodeInfo.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Form, FormItem } from 'ant-design-vue'
3 |
4 | export default defineComponent({
5 | name: 'CodeInfo',
6 | props: {
7 | config: {
8 | type: Object,
9 | default: () => {}
10 | },
11 | state: {
12 | type: Object,
13 | default: () => {}
14 | }
15 | },
16 | setup(props) {
17 | return () => (
18 |
19 |
36 |
37 | )
38 | }
39 | })
40 |
--------------------------------------------------------------------------------
/src/views/editor/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, ref, unref } from 'vue'
2 | import { Card } from 'ant-design-vue'
3 | import { PageWrapper } from '@/components/Page'
4 | import { MARKDOWN_EDITOR_PLUGIN } from '@/settings/websiteSetting'
5 | import { useBaseSetting } from '@/hooks/setting/useBaseSetting'
6 | import { MdEditor } from 'md-editor-v3'
7 | import 'md-editor-v3/lib/style.css'
8 |
9 | export default defineComponent({
10 | name: 'Markdown',
11 | setup() {
12 | const content = ref('')
13 | const { getAppMode } = useBaseSetting()
14 |
15 | return () => (
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/src/views/editor/rich-text.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, ref, shallowRef, onBeforeUnmount } from 'vue'
2 | import { Card } from 'ant-design-vue'
3 | import { PageWrapper } from '@/components/Page'
4 | import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
5 | import { WANG_EDITOR_PLUGIN } from '@/settings/websiteSetting'
6 | import '@wangeditor/editor/dist/css/style.css'
7 |
8 | export default defineComponent({
9 | name: 'RichText',
10 | setup() {
11 | const editorRef = shallowRef()
12 | const valueHtml = ref('hello
')
13 | const toolbarConfig = {}
14 | const editorConfig = { placeholder: '请输入内容...' }
15 |
16 | onBeforeUnmount(() => {
17 | const editor = editorRef.value
18 | if (editor == null) return
19 | editor.destroy()
20 | })
21 |
22 | const handleCreated = editor => {
23 | editorRef.value = editor
24 | }
25 |
26 | return () => (
27 |
28 |
29 |
30 |
31 |
38 |
39 |
40 |
41 | )
42 | }
43 | })
44 |
--------------------------------------------------------------------------------
/src/views/excel/types.ts:
--------------------------------------------------------------------------------
1 | import type { BookType } from 'xlsx'
2 |
3 | export type ImportType = 'base64' | 'binary' | 'string' | 'buffer' | 'array' | 'file'
4 |
5 | export interface DataToSheet {
6 | data: T[]
7 | header: T[]
8 | key: T[]
9 | fileName?: string
10 | autoWidth?: boolean
11 | bookType: BookType
12 | }
13 |
--------------------------------------------------------------------------------
/src/views/flow/flow-approve.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Card } from 'ant-design-vue'
3 | import { PageWrapper } from '@/components/Page'
4 | import { FLOW_EDITOR_PLUGIN } from '@/settings/websiteSetting'
5 | import { Approve } from '@/components/LogicFlow'
6 |
7 | export default defineComponent({
8 | name: 'FlowApprove',
9 | setup() {
10 | return () => (
11 |
12 |
13 |
16 |
17 |
18 | )
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/src/views/flow/flow-bpmn.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Card } from 'ant-design-vue'
3 | import { PageWrapper } from '@/components/Page'
4 | import { FLOW_EDITOR_PLUGIN } from '@/settings/websiteSetting'
5 | import { Bpmn } from '@/components/LogicFlow'
6 |
7 | export default defineComponent({
8 | name: 'FlowBpmn',
9 | setup() {
10 | return () => (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/src/views/form/form-designer.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Card } from 'ant-design-vue'
3 | import { PageWrapper } from '@/components/Page'
4 | import { FORM_CREATE_DESIGNER } from '@/settings/websiteSetting'
5 |
6 | export default defineComponent({
7 | name: 'FormDesigner',
8 | setup() {
9 | return () => (
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/src/views/home/components/ChartsCard.tsx:
--------------------------------------------------------------------------------
1 | import type { Ref, PropType } from 'vue'
2 | import type { EChartsOption } from 'echarts'
3 | import { defineComponent, ref, watch } from 'vue'
4 | import { Card } from 'ant-design-vue'
5 | import { useECharts } from '@/hooks/web/useECharts'
6 |
7 | export default defineComponent({
8 | name: 'ChartsCard',
9 | props: {
10 | loading: {
11 | type: Boolean as PropType
12 | },
13 | options: {
14 | type: Object as PropType,
15 | required: true
16 | },
17 | height: {
18 | type: Number as PropType
19 | }
20 | },
21 | setup(props) {
22 | const chartRef = ref(null)
23 | const { setOptions } = useECharts(chartRef as Ref)
24 |
25 | watch(
26 | () => props.loading,
27 | () => {
28 | if (props.loading) return
29 |
30 | setOptions(props.options)
31 | },
32 | {
33 | immediate: true
34 | }
35 | )
36 |
37 | return () => (
38 |
39 |
46 |
47 | )
48 | }
49 | })
50 |
--------------------------------------------------------------------------------
/src/views/image/image-composition/data.ts:
--------------------------------------------------------------------------------
1 | import type { ImageElementState, TextElementState, ContainerState } from './types'
2 | import { COMPOSITION_IMG_SRC, COMPOSITION_IMG_SRC2 } from '@/settings/websiteSetting'
3 |
4 | export const textElement: TextElementState = {
5 | x: 300,
6 | y: 100,
7 | z: 1,
8 | w: 180,
9 | h: 36,
10 | type: 'text',
11 | tag: 'text_1',
12 | active: false,
13 | text: '请输入文本',
14 | style: {
15 | fontFamily: '微软雅黑',
16 | fontSize: '24px',
17 | lineHeight: '24px',
18 | color: '#f70707',
19 | backgroundColor: '#05f8e8',
20 | fontWeight: '',
21 | fontStyle: '',
22 | textShadow: '',
23 | textAlign: 'left'
24 | }
25 | }
26 |
27 | export const imageElement: ImageElementState = {
28 | x: 320,
29 | y: 300,
30 | z: 2,
31 | w: 160,
32 | h: 160,
33 | type: 'image',
34 | tag: 'image_2',
35 | active: false,
36 | url: COMPOSITION_IMG_SRC2
37 | }
38 |
39 | export const containerObj: ContainerState = {
40 | width: 850,
41 | height: 530,
42 | bgImgUrl: COMPOSITION_IMG_SRC
43 | }
44 |
--------------------------------------------------------------------------------
/src/views/image/image-composition/types.ts:
--------------------------------------------------------------------------------
1 | import type { styleState } from '@/types'
2 |
3 | interface BaseElementState {
4 | x: number
5 | y: number
6 | z: number
7 | w: number
8 | h: number
9 | type: 'text' | 'image'
10 | tag: string
11 | active: boolean
12 | }
13 |
14 | export interface TextElementState extends BaseElementState {
15 | type: 'text'
16 | text: string
17 | style: styleState
18 | }
19 |
20 | export interface ImageElementState extends BaseElementState {
21 | type: 'image'
22 | url: string
23 | }
24 |
25 | export type ElementState = TextElementState | ImageElementState
26 |
27 | export interface ContainerState {
28 | width: number
29 | height: number
30 | bgImgUrl: string
31 | }
32 |
33 | export interface ImageObjState {
34 | url: string
35 | width: number
36 | height: number
37 | }
38 |
39 | export type handlerType = 'n' | 'e' | 's' | 'w' | 'nw' | 'ne' | 'se' | 'sw'
40 |
--------------------------------------------------------------------------------
/src/views/login/index.module.less:
--------------------------------------------------------------------------------
1 | .login-wrapper {
2 | position: relative;
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | width: 100vw;
7 | height: 100vh;
8 | background-image: url('@/assets/images/login-bg.png');
9 | background-size: cover;
10 |
11 | .login-setting {
12 | position: absolute;
13 | top: 20px;
14 | right: 30px;
15 | }
16 |
17 | .login-box {
18 | padding: 16px 30px 10px;
19 | background: #fff;
20 | border-radius: 4px;
21 | box-shadow: 0 15px 30px 0 rgba(0, 0, 1, .15);
22 |
23 | &-title {
24 | margin: 0 auto 35px;
25 | text-align: center;
26 | color: #707070;
27 | font-size: 18px;
28 | letter-spacing: 2px;
29 |
30 | img {
31 | width: 200px;
32 | height: 82px;
33 | margin: 12px auto 0;
34 | }
35 | }
36 |
37 | &-form {
38 | width: 320px;
39 | }
40 |
41 | .login-btn {
42 | width: 100%;
43 | }
44 |
45 | .no-margin {
46 | margin-bottom: 0;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/views/redirect.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, unref } from 'vue'
2 | import { useRouter } from 'vue-router'
3 |
4 | export default defineComponent({
5 | name: 'RedirectTo',
6 | setup() {
7 | const { currentRoute, replace } = useRouter()
8 |
9 | const { params, query } = unref(currentRoute)
10 | const { path, redirect_type = 'path' } = params
11 |
12 | Reflect.deleteProperty(params, 'path')
13 | Reflect.deleteProperty(params, 'redirect_type')
14 |
15 | const pathStr = Array.isArray(path) ? path.join('/') : path
16 |
17 | if (redirect_type === 'name') {
18 | replace({
19 | name: pathStr,
20 | params,
21 | query
22 | })
23 | } else {
24 | replace({
25 | path: pathStr.startsWith('/') ? pathStr : '/' + pathStr,
26 | query
27 | })
28 | }
29 |
30 | return () =>
31 | }
32 | })
33 |
--------------------------------------------------------------------------------
/src/views/system/account.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/views/tree/antd-tree/data.ts:
--------------------------------------------------------------------------------
1 | export const treeData = [
2 | {
3 | key: '1-0',
4 | title: '一级 1',
5 | children: [
6 | {
7 | key: '1-1',
8 | title: '二级 1-1',
9 | children: [
10 | {
11 | key: '1-1-1',
12 | title: '三级 1-1-1'
13 | },
14 | {
15 | key: '1-1-2',
16 | title: '三级 1-1-2'
17 | }
18 | ]
19 | }
20 | ]
21 | },
22 | {
23 | key: '2-0',
24 | title: '一级 2',
25 | children: [
26 | {
27 | key: '2-1',
28 | title: '二级 2-1'
29 | },
30 | {
31 | key: '2-2',
32 | title: '二级 2-2',
33 | disabled: true
34 | }
35 | ]
36 | },
37 | {
38 | key: '3-0',
39 | title: '一级 3',
40 | children: [
41 | {
42 | key: '3-1',
43 | title: '二级 3-1'
44 | },
45 | {
46 | key: '3-2',
47 | title: '二级 3-2',
48 | children: [
49 | {
50 | key: '3-2-1',
51 | title: '三级 3-2-1',
52 | disableCheckbox: true
53 | },
54 | {
55 | key: '3-2-2',
56 | title: '三级 3-2-2'
57 | },
58 | {
59 | key: '3-2-3',
60 | title: '三级 3-2-3'
61 | }
62 | ]
63 | }
64 | ]
65 | }
66 | ]
67 |
--------------------------------------------------------------------------------
/src/views/video/video-player.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Row, Col, Card } from 'ant-design-vue'
3 | import { PageWrapper } from '@/components/Page'
4 | import { VIDEO_RES_SRC, VIDEO_PLUGIN } from '@/settings/websiteSetting'
5 | import { VideoPlayer } from '@videojs-player/vue'
6 | import 'video.js/dist/video-js.css'
7 |
8 | export default defineComponent({
9 | name: 'VideoPlayer',
10 | setup() {
11 | return () => (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 | })
37 |
--------------------------------------------------------------------------------
/src/views/video/video-watermark/data.ts:
--------------------------------------------------------------------------------
1 | import type { ImageElementState, TextElementState, ContainerState } from './types'
2 | import { VIDEO_RES_SRC, VIDEO_IMG_SRC } from '@/settings/websiteSetting'
3 |
4 | export const textElement: TextElementState = {
5 | x: 300,
6 | y: 100,
7 | z: 1,
8 | w: 180,
9 | h: 36,
10 | type: 'text',
11 | tag: 'text_1',
12 | active: false,
13 | text: '请输入文本',
14 | style: {
15 | fontFamily: '微软雅黑',
16 | fontSize: '24px',
17 | lineHeight: '24px',
18 | color: '#687684',
19 | backgroundColor: '#9ac8d8',
20 | fontWeight: '',
21 | fontStyle: '',
22 | textShadow: '',
23 | textAlign: 'left'
24 | }
25 | }
26 |
27 | export const imageElement: ImageElementState = {
28 | x: 320,
29 | y: 260,
30 | z: 2,
31 | w: 160,
32 | h: 160,
33 | type: 'image',
34 | tag: 'image_2',
35 | active: false,
36 | url: VIDEO_IMG_SRC
37 | }
38 |
39 | export const containerObj: ContainerState = {
40 | width: 850,
41 | height: 480,
42 | videoUrl: VIDEO_RES_SRC
43 | }
44 |
--------------------------------------------------------------------------------
/src/views/video/video-watermark/types.ts:
--------------------------------------------------------------------------------
1 | import type { styleState } from '@/types'
2 |
3 | interface BaseElementState {
4 | x: number
5 | y: number
6 | z: number
7 | w: number
8 | h: number
9 | type: 'text' | 'image'
10 | tag: string
11 | active: boolean
12 | }
13 |
14 | export interface TextElementState extends BaseElementState {
15 | type: 'text'
16 | text: string
17 | style: styleState
18 | }
19 |
20 | export interface ImageElementState extends BaseElementState {
21 | type: 'image'
22 | url: string
23 | }
24 |
25 | export type ElementState = TextElementState | ImageElementState
26 |
27 | export interface ContainerState {
28 | width: number
29 | height: number
30 | videoUrl: string
31 | }
32 |
33 | export interface ImageObjState {
34 | url: string
35 | width: number
36 | height: number
37 | }
38 |
39 | export type handlerType = 'n' | 'e' | 's' | 'w' | 'nw' | 'ne' | 'se' | 'sw'
40 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "strict": true,
7 | "noLib": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "allowSyntheticDefaultImports": true,
10 | "strictFunctionTypes": false,
11 | "jsx": "preserve",
12 | "jsxImportSource": "vue",
13 | "baseUrl": ".",
14 | "allowJs": true,
15 | "sourceMap": true,
16 | "isolatedModules": true,
17 | "esModuleInterop": true,
18 | "resolveJsonModule": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noImplicitAny": false,
22 | "skipLibCheck": true,
23 | "removeComments": true,
24 | "useDefineForClassFields": true,
25 | "lib": ["esnext", "dom"],
26 | "paths": {
27 | "@/*": ["src/*"],
28 | "#/*": ["types/*"]
29 | },
30 | "types": [
31 | "vite/client",
32 | "vite-plugin-svg-icons/client"
33 | ]
34 | },
35 | "include": [
36 | "src/**/*.ts",
37 | "src/**/*.d.ts",
38 | "src/**/*.tsx",
39 | "src/**/*.vue",
40 | "typings/**/*.d.ts",
41 | "typings/**/*.ts",
42 | "build/**/*.ts",
43 | "build/**/*.d.ts",
44 | "mock/*.ts",
45 | "vite.config.ts"
46 | ],
47 | "exclude": [
48 | "node_modules",
49 | "dist",
50 | "**/*.js"
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/typings/global.d.ts:
--------------------------------------------------------------------------------
1 | declare type Nullable = T | null
2 | declare type Recordable = Record
3 | declare type ElRef = Nullable
4 |
5 | declare type TimeoutHandle = ReturnType
6 | declare type IntervalHandle = ReturnType
7 |
8 | declare type DeepPartial = {
9 | [P in keyof T]?: DeepPartial
10 | }
11 |
12 | declare type CheckedType = boolean | string | number
13 |
14 | declare interface ChangeEvent extends Event {
15 | target: HTMLInputElement
16 | }
17 |
18 | declare interface ViteEnv {
19 | VITE_PORT: number
20 | VITE_PUBLIC_PATH: string
21 | VITE_PROXY: [string, string][]
22 | }
23 |
--------------------------------------------------------------------------------
/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Fn {
2 | (...arg: T[]): R
3 | }
4 |
5 | declare interface PromiseFn {
6 | (...arg: T[]): Promise
7 | }
8 |
9 | declare type DeepPartial = {
10 | [P in keyof T]?: DeepPartial
11 | }
12 |
13 | declare type TargetContext = '_self' | '_blank'
14 |
15 | declare type ComponentRef = ComponentElRef | null
16 |
17 | declare type ElRef = Nullable
18 |
19 | declare type EmitType = (event: string, ...args: any[]) => void
20 |
21 | declare type LabelValueOptions = {
22 | label: string
23 | value: any
24 | [key: string]: string | number | boolean
25 | }[]
26 |
--------------------------------------------------------------------------------
/typings/module.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import type { DefineComponent } from 'vue'
3 | const Component: DefineComponent<{}, {}, any>
4 | export default Component
5 | }
6 |
7 | declare module 'vue-drag-resize/src' {
8 | import VueDragResize from 'vue-drag-resize/src'
9 | export default VueDragResize
10 | }
11 |
--------------------------------------------------------------------------------
/typings/vue-router.d.ts:
--------------------------------------------------------------------------------
1 | export {}
2 |
3 | declare module 'vue-router' {
4 | interface RouteMeta extends Record {
5 | // Route title (must set!!)
6 | title: string
7 | // Icon, which is also a menu icon
8 | icon?: string
9 | // Affix tag or not
10 | affix?: boolean
11 | // Menu sort, only for the first level
12 | orderNo?: number
13 | // Whether not to cached route
14 | ignoreKeepAlive?: boolean
15 | // Never show in menu
16 | hideMenu?: boolean
17 | // Hide submenu
18 | hideChildrenInMenu?: boolean
19 | // Whether is a iframe
20 | iframeSrc?: string
21 | // Whether is a link
22 | isLink?: boolean
23 | // current page transition
24 | transitionName?: string
25 | }
26 | }
27 |
--------------------------------------------------------------------------------