feat: 搭建后台管理框架
This commit is contained in:
1 parent
6d28bab72b
commit
40546bfd01
238 files changed
+18160
-3837
No files matched your search
+47
-3
@@ -1,5 +1,49 @@
|
||||
<template><RouterView /></template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { ConfigGlobal } from '@/components/ConfigGlobal'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('app')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const currentSize = computed(() => appStore.getCurrentSize)
|
||||
|
||||
const greyMode = computed(() => appStore.getGreyMode)
|
||||
|
||||
appStore.initTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigGlobal :size="currentSize">
|
||||
<RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
|
||||
</ConfigGlobal>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-app';
|
||||
|
||||
.size {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0 !important;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
.size;
|
||||
|
||||
#app {
|
||||
.size;
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls}-grey-mode {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
import request from '@/axios'
|
||||
import type { UserType } from './types'
|
||||
|
||||
interface RoleParams {
|
||||
roleName: string
|
||||
}
|
||||
|
||||
export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
|
||||
return request.post({ url: '/mock/user/login', data })
|
||||
}
|
||||
|
||||
export const loginOutApi = (): Promise<IResponse> => {
|
||||
return request.get({ url: '/mock/user/loginOut' })
|
||||
}
|
||||
|
||||
export const getAdminRoleApi = (
|
||||
params: RoleParams
|
||||
): Promise<IResponse<AppCustomRouteRecordRaw[]>> => {
|
||||
return request.get({ url: '/mock/role/list', params })
|
||||
}
|
||||
|
||||
export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => {
|
||||
return request.get({ url: '/mock/role/list2', params })
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export type UserLoginType = {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export type UserType = {
|
||||
username: string
|
||||
password: string
|
||||
role: string
|
||||
roleId: string
|
||||
permissions: string | string[]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" enable-background="new 0 0 800 800"><style>.st26{fill:#fff}</style><g id="图层_11"><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="401.773" y1="162.104" x2="401.773" y2="717.596"><stop offset="0" stop-color="#F4F2FB"/><stop offset="1" stop-color="#E1EEF5"/></linearGradient><path d="M485.03 203.46c-38.37 30.29-120.74 33.81-181.17-2.22s-172-31.38-202.22 34.87 37.19 131.33 12.78 178.98S8.66 530.13 64.45 611.49s126.6 60.62 169.22 52.45c84.17-16.13 189.79 115.67 308.62 16.13 68.47-57.35 170.44 42.09 210.17-81.36 32.78-101.86-85.67-139.5-49.97-208.03 37.96-72.88 30.67-159.24-10.46-201.06-38.31-38.96-140.75-38.46-207 13.84z" style="fill:url(#SVGID_1_)"/><linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="494.782" y1="599.604" x2="494.782" y2="428.659"><stop offset=".34" stop-color="#B0B9E1"/><stop offset=".866" stop-color="#EAF0F8"/></linearGradient><path d="M406.65 428.66h216.44l-22.53 49.03s59.19 57.87-14.13 121.91c-134.28-44.17-221.74-37.1-219.98-38.87 1.77-1.76 40.2-132.07 40.2-132.07z" style="fill:url(#SVGID_2_)"/><linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="116.855" y1="542.49" x2="116.855" y2="405.316"><stop offset=".227" stop-color="#B7ACE0"/><stop offset=".789" stop-color="#E8E7FA"/></linearGradient><path d="M117.64 405.56s-.22-.57-.52.04c-2.7 5.49-27.15 64.96-29.09 110.86 0 0-4.08 26.37 30.11 26.02 28.54-.29 27.78-24.6 27.68-32.79-.39-33.22-28.18-104.13-28.18-104.13z" style="fill:url(#SVGID_3_)"/><linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="116.857" y1="420.547" x2="116.857" y2="571.681"><stop offset="0" stop-color="#ECF1FB"/><stop offset=".818" stop-color="#AFB0E7"/></linearGradient><path d="M116.86 571.68c-.55 0-1-.45-1-1V421.55c0-.55.45-1 1-1s1 .45 1 1v149.13c0 .55-.45 1-1 1z" style="fill:url(#SVGID_4_)"/><linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="617.984" y1="450.968" x2="617.984" y2="362.644"><stop offset=".227" stop-color="#CCD4F4"/><stop offset=".789" stop-color="#ECF1FB"/></linearGradient><path d="M618.49 362.8s-.14-.37-.33.03c-1.74 3.53-17.48 41.83-18.73 71.38 0 0-2.63 16.98 19.39 16.76 18.38-.18 17.89-15.84 17.82-21.11-.25-21.4-18.15-67.06-18.15-67.06z" style="fill:url(#SVGID_5_)"/><linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="617.985" y1="372.451" x2="617.985" y2="469.764"><stop offset="0" stop-color="#ECF1FB"/><stop offset="1" stop-color="#A6A8E2"/></linearGradient><path d="M617.99 469.76c-.36 0-.64-.29-.64-.64V373.1c0-.36.29-.64.64-.64s.64.29.64.64v96.02c0 .36-.29.64-.64.64z" style="fill:url(#SVGID_6_)"/><linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="463.902" y1="88.362" x2="429.148" y2="148.558"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><circle cx="446.52" cy="118.46" r="34.75" style="fill:url(#SVGID_7_)"/><linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="421.565" y1="118.828" x2="421.565" y2="176.282"><stop offset="0" stop-color="#F9FAFE"/><stop offset="1" stop-color="#E5EDF7"/></linearGradient><path d="M466.3 137.41h-34.57c-2.23-10.61-11.65-18.58-22.93-18.58s-20.69 7.97-22.93 18.58h-9.05c-10.73 0-19.44 8.7-19.44 19.44 0 10.73 8.7 19.44 19.44 19.44h89.47c10.73 0 19.44-8.7 19.44-19.44.01-10.74-8.69-19.44-19.43-19.44z" style="fill:url(#SVGID_8_)"/><g><linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="688.586" y1="540.208" x2="688.586" y2="512.38"><stop offset=".227" stop-color="#AFB0E7"/><stop offset="1" stop-color="#ECF1FB"/></linearGradient><circle cx="688.59" cy="526.29" r="13.91" style="fill:url(#SVGID_9_)"/><linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="688.635" y1="515.894" x2="688.635" y2="560.69"><stop offset="0" stop-color="#DDE1F6"/><stop offset=".818" stop-color="#A6A8E2"/></linearGradient><path d="M688.64 560.69c-.24 0-.43-.19-.43-.43v-43.94c0-.24.19-.43.43-.43s.43.19.43.43v43.94a.44.44 0 01-.43.43z" style="fill:url(#SVGID_10_)"/></g><g><linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="2622.045" y1="266.481" x2="2451.058" y2="562.64" gradientTransform="matrix(-1 0 0 1 2941.346 0)"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M248.82 393.99c0-24.52-.03-49.03.01-73.54.02-14.37 4.24-18.36 17.97-20.53 41.87-6.61 82.03-18.72 117.91-42.29 10.38-6.82 18.3-7.59 29.06-.47 34.85 23.06 73.26 37.11 114.55 42.8 13.12 1.81 16.84 5.88 16.85 19.25.04 45.72-.4 91.44.18 137.15.34 26.77-8.17 49.99-24.02 70.73-31.46 41.17-74.88 63.76-122.21 80.03-2.5.86-5.83.67-8.36-.23-38.47-13.74-74.58-31.84-104.15-61.09-22.97-22.73-37.84-49.56-37.79-83.22.03-22.87.01-45.73 0-68.59z" style="fill:url(#SVGID_11_)"/><linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="2625.25" y1="279.944" x2="2462.749" y2="561.403" gradientTransform="matrix(-1 0 0 1 2941.346 0)"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBLine truncated
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" enable-background="new 0 0 800 800"><style>.st49{fill:#d4e4fe}</style><g id="图层_5"><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="401.193" y1="159.763" x2="401.193" y2="715.254"><stop offset="0" stop-color="#F4F2FB"/><stop offset="1" stop-color="#E1EEF5"/></linearGradient><path d="M484.45 201.12c-38.37 30.29-120.74 33.81-181.17-2.22s-172-31.38-202.22 34.87 37.19 131.33 12.78 178.98S8.08 527.79 63.87 609.15s126.6 60.62 169.22 52.45c84.17-16.13 189.79 115.67 308.62 16.13 68.47-57.35 170.44 42.09 210.17-81.36 32.78-101.86-85.67-139.5-49.97-208.03 37.96-72.88 30.67-159.24-10.46-201.06-38.31-38.96-140.75-38.46-207 13.84z" style="fill:url(#SVGID_1_)"/><linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="484.537" y1="604.68" x2="484.537" y2="493.367"><stop offset=".34" stop-color="#B0B9E1"/><stop offset=".866" stop-color="#EAF0F8"/></linearGradient><path d="M285.1 583.44c1.77-1.63 77.74-90.07 77.74-90.07h321.13l-99.5 111.31-299.37-21.24z" style="fill:url(#SVGID_2_)"/><linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="616.023" y1="627.266" x2="657.332" y2="555.716"><stop offset="0" stop-color="#B0B9E1"/><stop offset=".866" stop-color="#EAF0F8"/></linearGradient><path d="M604.49 620.61L659.43 556.93 633.22 624.12z" style="fill:url(#SVGID_3_)"/><linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="116.275" y1="540.149" x2="116.275" y2="402.974"><stop offset=".003" stop-color="#9A9ADB"/><stop offset=".789" stop-color="#CECDF1"/></linearGradient><path d="M117.06 403.22s-.22-.57-.52.04c-2.7 5.49-27.15 64.96-29.09 110.86 0 0-4.08 26.37 30.11 26.02 28.54-.29 27.78-24.6 27.68-32.79-.39-33.22-28.18-104.13-28.18-104.13z" style="fill:url(#SVGID_4_)"/><linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="116.277" y1="418.206" x2="116.277" y2="569.34"><stop offset="0" stop-color="#ECF1FB"/><stop offset=".818" stop-color="#AFB0E7"/></linearGradient><path d="M116.28 569.34c-.55 0-1-.45-1-1V419.21c0-.55.45-1 1-1s1 .45 1 1v149.13c0 .55-.45 1-1 1z" style="fill:url(#SVGID_5_)"/><linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="617.404" y1="448.627" x2="617.404" y2="360.303"><stop offset=".227" stop-color="#CCD4F4"/><stop offset=".789" stop-color="#ECF1FB"/></linearGradient><path d="M617.91 360.46s-.14-.37-.33.03c-1.74 3.53-17.48 41.83-18.73 71.38 0 0-2.63 16.98 19.39 16.76 18.38-.18 17.89-15.84 17.82-21.11-.25-21.4-18.15-67.06-18.15-67.06z" style="fill:url(#SVGID_6_)"/><linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="617.405" y1="370.11" x2="617.405" y2="467.422"><stop offset="0" stop-color="#ECF1FB"/><stop offset="1" stop-color="#A6A8E2"/></linearGradient><path d="M617.41 467.42c-.36 0-.64-.29-.64-.64v-96.02c0-.36.29-.64.64-.64.36 0 .64.29.64.64v96.02c0 .35-.29.64-.64.64z" style="fill:url(#SVGID_7_)"/><linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="463.322" y1="86.02" x2="428.568" y2="146.217"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><circle cx="445.95" cy="116.12" r="34.75" style="fill:url(#SVGID_8_)"/><linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="420.985" y1="116.487" x2="420.985" y2="173.941"><stop offset="0" stop-color="#F9FAFE"/><stop offset="1" stop-color="#E5EDF7"/></linearGradient><path d="M465.72 135.07h-34.57c-2.23-10.61-11.65-18.58-22.93-18.58s-20.69 7.97-22.93 18.58h-9.05c-10.73 0-19.44 8.7-19.44 19.44 0 10.73 8.7 19.44 19.44 19.44h89.47c10.73 0 19.44-8.7 19.44-19.44.01-10.74-8.69-19.44-19.43-19.44z" style="fill:url(#SVGID_9_)"/><g><linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="688.006" y1="537.867" x2="688.006" y2="510.039"><stop offset=".227" stop-color="#AFB0E7"/><stop offset="1" stop-color="#ECF1FB"/></linearGradient><circle cx="688.01" cy="523.95" r="13.91" style="fill:url(#SVGID_10_)"/><linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="688.056" y1="513.553" x2="688.056" y2="558.349"><stop offset="0" stop-color="#DDE1F6"/><stop offset=".818" stop-color="#A6A8E2"/></linearGradient><path d="M688.06 558.35c-.24 0-.43-.19-.43-.43v-43.94c0-.24.19-.43.43-.43s.43.19.43.43v43.94a.44.44 0 01-.43.43z" style="fill:url(#SVGID_11_)"/></g><g><linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="2879.853" y1="308.382" x2="2737.462" y2="450.774" gradientTransform="matrix(-1 0 0 1 3207.18 0)"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M270.73 392.79l91.4-73.3c7.43 11.92 20.65 19.87 35.7 19.87 16.43 0 30.69-9.48 37.6-23.26l92.11 76.85 10.83-12.98-98.5-82.19c0-.16.01-.31.01-.47 0-23.18-18.86-42.04-42.05-42.04-23.18 0-42.04 18.86-42.04 42.04 0 1.8.13 3.58.35 5.32l-95.98 76.97 10.57 13.19zm101.96-95.48c0-13.86 11.28-25.14 25.14-25.14s25.14 11.28 25.14 25.14-11.28 25.14-25.14 25.14-25.14-11.27-25.14-25.14z" style="fill:url(#SVGID_12_)"/><linearGradient id="SVGID_13_" gradientUnits="uLine truncated
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" enable-background="new 0 0 800 800"><style>.st26{fill:#fff}</style><g id="图层_16"><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="402.832" y1="159.843" x2="402.832" y2="715.335"><stop offset="0" stop-color="#F4F2FB"/><stop offset="1" stop-color="#E1EEF5"/></linearGradient><path d="M486.09 201.2c-38.37 30.29-120.74 33.81-181.17-2.22s-172-31.38-202.22 34.87 37.19 131.33 12.78 178.98S9.72 527.87 65.5 609.23s126.6 60.62 169.22 52.45c84.17-16.13 189.79 115.67 308.62 16.13 68.47-57.35 170.44 42.09 210.17-81.36 32.78-101.86-85.67-139.5-49.97-208.03 37.96-72.88 30.67-159.24-10.46-201.06-38.3-38.96-140.75-38.46-206.99 13.84z" style="fill:url(#SVGID_1_)"/><linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="117.913" y1="540.229" x2="117.913" y2="403.055"><stop offset=".227" stop-color="#B7ACE0"/><stop offset=".789" stop-color="#E8E7FA"/></linearGradient><path d="M118.7 403.3s-.22-.57-.52.04c-2.7 5.49-27.15 64.96-29.09 110.86 0 0-4.08 26.37 30.11 26.02 28.54-.29 27.78-24.6 27.68-32.79-.39-33.22-28.18-104.13-28.18-104.13z" style="fill:url(#SVGID_2_)"/><linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="117.915" y1="418.287" x2="117.915" y2="569.42"><stop offset="0" stop-color="#ECF1FB"/><stop offset=".818" stop-color="#AFB0E7"/></linearGradient><path d="M117.92 569.42c-.55 0-1-.45-1-1V419.29c0-.55.45-1 1-1s1 .45 1 1v149.13c0 .55-.45 1-1 1z" style="fill:url(#SVGID_3_)"/><linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="619.042" y1="448.707" x2="619.042" y2="360.383"><stop offset=".227" stop-color="#CCD4F4"/><stop offset=".789" stop-color="#ECF1FB"/></linearGradient><path d="M619.55 360.54s-.14-.37-.33.03c-1.74 3.53-17.48 41.83-18.73 71.38 0 0-2.63 16.98 19.39 16.76 18.38-.18 17.89-15.84 17.82-21.11-.26-21.4-18.15-67.06-18.15-67.06z" style="fill:url(#SVGID_4_)"/><linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="619.043" y1="370.19" x2="619.043" y2="467.503"><stop offset="0" stop-color="#ECF1FB"/><stop offset="1" stop-color="#A6A8E2"/></linearGradient><path d="M619.04 467.5c-.36 0-.64-.29-.64-.64v-96.02c0-.36.29-.64.64-.64s.64.29.64.64v96.02c.01.35-.28.64-.64.64z" style="fill:url(#SVGID_5_)"/><linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="464.96" y1="86.101" x2="430.206" y2="146.297"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><circle cx="447.58" cy="116.2" r="34.75" style="fill:url(#SVGID_6_)"/><linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="422.623" y1="116.567" x2="422.623" y2="174.021"><stop offset="0" stop-color="#F9FAFE"/><stop offset="1" stop-color="#E5EDF7"/></linearGradient><path d="M467.36 135.15h-34.57c-2.23-10.61-11.65-18.58-22.93-18.58s-20.69 7.97-22.93 18.58h-9.05c-10.73 0-19.44 8.7-19.44 19.44 0 10.73 8.7 19.44 19.44 19.44h89.47c10.73 0 19.44-8.7 19.44-19.44.01-10.74-8.7-19.44-19.43-19.44z" style="fill:url(#SVGID_7_)"/><linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="689.644" y1="537.948" x2="689.644" y2="510.119"><stop offset=".227" stop-color="#AFB0E7"/><stop offset="1" stop-color="#ECF1FB"/></linearGradient><circle cx="689.64" cy="524.03" r="13.91" style="fill:url(#SVGID_8_)"/><linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="689.694" y1="513.633" x2="689.694" y2="558.429"><stop offset="0" stop-color="#DDE1F6"/><stop offset=".818" stop-color="#A6A8E2"/></linearGradient><path d="M689.69 558.43c-.24 0-.43-.19-.43-.43v-43.94c0-.24.19-.43.43-.43s.43.19.43.43V558c0 .24-.19.43-.43.43z" style="fill:url(#SVGID_9_)"/><linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="289.384" y1="477.19" x2="289.384" y2="411.226"><stop offset="0" stop-color="#B0B9E1"/><stop offset="1" stop-color="#E7EFF7"/></linearGradient><path d="M202.07 451.28L270.1 411.23 376.7 411.23 315.15 477.19 237.41 476.01z" style="fill:url(#SVGID_10_)"/><linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="454.145" y1="502.809" x2="454.145" y2="420.65"><stop offset="0" stop-color="#B0B9E1"/><stop offset="1" stop-color="#E7EFF7"/></linearGradient><path d="M386.71 479.55L431.76 420.65 521.58 420.65 423.81 502.81 394.37 495.15z" style="fill:url(#SVGID_11_)"/><linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="589.016" y1="472.132" x2="589.016" y2="397.68"><stop offset="0" stop-color="#B0B9E1"/><stop offset="1" stop-color="#E7EFF7"/></linearGradient><path d="M501.26 458.64l64.79-60.96h110.72l-48.99 66.61a19.243 19.243 0 01-17.85 7.7l-108.67-13.35z" style="fill:url(#SVGID_12_)"/><linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="314.267" y1="607.349" x2="314.267" y2="497.361"><stop offset="0" stop-color="#B0B9E1"/><stop offset="1" stop-color="#E7EFF7"/></linearGradient><path d="M212.23 592.77L303.67 497.36 416.3 497.36 297.04 607.35 247.57 604.7z" style="fill:url(#SVGID_13_)"/><g><linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="515.604Line truncated
|
||||
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,52 @@
|
||||
import { AxiosResponse, InternalAxiosRequestConfig } from './types'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import qs from 'qs'
|
||||
import { SUCCESS_CODE, TRANSFORM_REQUEST_DATA } from '@/constants'
|
||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||
import { objToFormData } from '@/utils'
|
||||
|
||||
const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => {
|
||||
if (
|
||||
config.method === 'post' &&
|
||||
config.headers['Content-Type'] === 'application/x-www-form-urlencoded'
|
||||
) {
|
||||
config.data = qs.stringify(config.data)
|
||||
} else if (
|
||||
TRANSFORM_REQUEST_DATA &&
|
||||
config.method === 'post' &&
|
||||
config.headers['Content-Type'] === 'multipart/form-data'
|
||||
) {
|
||||
config.data = objToFormData(config.data)
|
||||
}
|
||||
if (config.method === 'get' && config.params) {
|
||||
let url = config.url as string
|
||||
url += '?'
|
||||
const keys = Object.keys(config.params)
|
||||
for (const key of keys) {
|
||||
if (config.params[key] !== void 0 && config.params[key] !== null) {
|
||||
url += `${key}=${encodeURIComponent(config.params[key])}&`
|
||||
}
|
||||
}
|
||||
url = url.substring(0, url.length - 1)
|
||||
config.params = {}
|
||||
config.url = url
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
const defaultResponseInterceptors = (response: AxiosResponse) => {
|
||||
if (response?.config?.responseType === 'blob') {
|
||||
// 如果是文件流,直接过
|
||||
return response
|
||||
} else if (response.data.code === SUCCESS_CODE) {
|
||||
return response.data
|
||||
} else {
|
||||
ElMessage.error(response?.data?.message)
|
||||
if (response?.data?.code === 401) {
|
||||
const userStore = useUserStoreWithOut()
|
||||
userStore.logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { defaultResponseInterceptors, defaultRequestInterceptors }
|
||||
@@ -0,0 +1,41 @@
|
||||
import service from './service'
|
||||
import { CONTENT_TYPE } from '@/constants'
|
||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||
|
||||
const request = (option: AxiosConfig) => {
|
||||
const { url, method, params, data, headers, responseType } = option
|
||||
const userStore = useUserStoreWithOut()
|
||||
return service.request({
|
||||
url: url,
|
||||
method,
|
||||
params,
|
||||
data,
|
||||
responseType: responseType,
|
||||
headers: {
|
||||
'Content-Type': CONTENT_TYPE,
|
||||
[userStore.getTokenKey ?? 'Authorization']: userStore.getToken ?? '',
|
||||
...headers
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
get: <T = any>(option: AxiosConfig) => {
|
||||
return request({ method: 'get', ...option }) as Promise<IResponse<T>>
|
||||
},
|
||||
post: <T = any>(option: AxiosConfig) => {
|
||||
return request({ method: 'post', ...option }) as Promise<IResponse<T>>
|
||||
},
|
||||
delete: <T = any>(option: AxiosConfig) => {
|
||||
return request({ method: 'delete', ...option }) as Promise<IResponse<T>>
|
||||
},
|
||||
put: <T = any>(option: AxiosConfig) => {
|
||||
return request({ method: 'put', ...option }) as Promise<IResponse<T>>
|
||||
},
|
||||
cancelRequest: (url: string | string[]) => {
|
||||
return service.cancelRequest(url)
|
||||
},
|
||||
cancelAllRequest: () => {
|
||||
return service.cancelAllRequest()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
|
||||
|
||||
import { AxiosInstance, InternalAxiosRequestConfig, RequestConfig, AxiosResponse } from './types'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { REQUEST_TIMEOUT } from '@/constants'
|
||||
|
||||
export const PATH_URL = import.meta.env.VITE_API_BASE_PATH
|
||||
|
||||
const abortControllerMap: Map<string, AbortController> = new Map()
|
||||
|
||||
const axiosInstance: AxiosInstance = axios.create({
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
baseURL: PATH_URL
|
||||
})
|
||||
|
||||
axiosInstance.interceptors.request.use((res: InternalAxiosRequestConfig) => {
|
||||
const controller = new AbortController()
|
||||
const url = res.url || ''
|
||||
res.signal = controller.signal
|
||||
abortControllerMap.set(url, controller)
|
||||
return res
|
||||
})
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(res: AxiosResponse) => {
|
||||
const url = res.config.url || ''
|
||||
abortControllerMap.delete(url)
|
||||
// 这里不能做任何处理,否则后面的 interceptors 拿不到完整的上下文了
|
||||
return res
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
console.log('err: ' + error) // for debug
|
||||
ElMessage.error(error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
axiosInstance.interceptors.request.use(defaultRequestInterceptors)
|
||||
axiosInstance.interceptors.response.use(defaultResponseInterceptors)
|
||||
|
||||
const service = {
|
||||
request: (config: RequestConfig) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (config.interceptors?.requestInterceptors) {
|
||||
config = config.interceptors.requestInterceptors(config as any)
|
||||
}
|
||||
|
||||
axiosInstance
|
||||
.request(config)
|
||||
.then((res) => {
|
||||
resolve(res)
|
||||
})
|
||||
.catch((err: any) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
cancelRequest: (url: string | string[]) => {
|
||||
const urlList = Array.isArray(url) ? url : [url]
|
||||
for (const _url of urlList) {
|
||||
abortControllerMap.get(_url)?.abort()
|
||||
abortControllerMap.delete(_url)
|
||||
}
|
||||
},
|
||||
cancelAllRequest() {
|
||||
for (const [_, controller] of abortControllerMap) {
|
||||
controller.abort()
|
||||
}
|
||||
abortControllerMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export default service
|
||||
@@ -0,0 +1,31 @@
|
||||
import type {
|
||||
InternalAxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosRequestConfig,
|
||||
AxiosInstance,
|
||||
AxiosRequestHeaders,
|
||||
AxiosError
|
||||
} from 'axios'
|
||||
|
||||
interface RequestInterceptors<T> {
|
||||
// 请求拦截
|
||||
requestInterceptors?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
|
||||
requestInterceptorsCatch?: (err: any) => any
|
||||
// 响应拦截
|
||||
responseInterceptors?: (config: T) => T
|
||||
responseInterceptorsCatch?: (err: any) => any
|
||||
}
|
||||
|
||||
interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
|
||||
interceptors?: RequestInterceptors<T>
|
||||
}
|
||||
|
||||
export {
|
||||
AxiosResponse,
|
||||
RequestInterceptors,
|
||||
RequestConfig,
|
||||
AxiosInstance,
|
||||
InternalAxiosRequestConfig,
|
||||
AxiosRequestHeaders,
|
||||
AxiosError
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Backtop from './src/Backtop.vue'
|
||||
|
||||
export { Backtop }
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ElBacktop } from 'element-plus'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls, variables } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('backtop')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElBacktop
|
||||
:class="`${prefixCls}-backtop`"
|
||||
:target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
import Breadcrumb from './src/Breadcrumb.vue'
|
||||
|
||||
export { Breadcrumb }
|
||||
@@ -0,0 +1,126 @@
|
||||
<script lang="tsx">
|
||||
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
|
||||
import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { filterBreadcrumb } from './helper'
|
||||
import { filter, treeToList } from '@/utils/tree'
|
||||
import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('breadcrumb')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 面包屑图标
|
||||
const breadcrumbIcon = computed(() => appStore.getBreadcrumbIcon)
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Breadcrumb',
|
||||
setup() {
|
||||
const { currentRoute } = useRouter()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const levelList = ref<AppRouteRecordRaw[]>([])
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const menuRouters = computed(() => {
|
||||
const routers = permissionStore.getRouters
|
||||
return filterBreadcrumb(routers)
|
||||
})
|
||||
|
||||
const getBreadcrumb = () => {
|
||||
const currentPath = currentRoute.value.matched.slice(-1)[0].path
|
||||
levelList.value = filter<AppRouteRecordRaw>(unref(menuRouters), (node: AppRouteRecordRaw) => {
|
||||
return node.path === currentPath
|
||||
})
|
||||
}
|
||||
|
||||
const renderBreadcrumb = () => {
|
||||
const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList))
|
||||
return breadcrumbList.map((v) => {
|
||||
const disabled = !v.redirect || v.redirect === 'noredirect'
|
||||
const meta = v.meta as RouteMeta
|
||||
return (
|
||||
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
|
||||
{meta?.icon && breadcrumbIcon.value ? (
|
||||
<>
|
||||
<Icon icon={meta.icon} class="mr-[5px]"></Icon> {t(v?.meta?.title)}
|
||||
</>
|
||||
) : (
|
||||
t(v?.meta?.title)
|
||||
)}
|
||||
</ElBreadcrumbItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
(route: RouteLocationNormalizedLoaded) => {
|
||||
if (route.path.startsWith('/redirect/')) {
|
||||
return
|
||||
}
|
||||
getBreadcrumb()
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
return () => (
|
||||
<ElBreadcrumb separator="/" class={`${prefixCls} flex items-center h-full ml-[10px]`}>
|
||||
<TransitionGroup appear enter-active-class="animate__animated animate__fadeInRight">
|
||||
{renderBreadcrumb()}
|
||||
</TransitionGroup>
|
||||
</ElBreadcrumb>
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{elNamespace}-breadcrumb';
|
||||
|
||||
.@{prefix-cls} {
|
||||
:deep(&__item) {
|
||||
display: flex;
|
||||
.@{prefix-cls}__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--top-header-text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(&__item):not(:last-child) {
|
||||
.@{prefix-cls}__inner {
|
||||
color: var(--top-header-text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(&__item):last-child {
|
||||
.@{prefix-cls}__inner {
|
||||
color: var(--el-text-color-placeholder);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { pathResolve } from '@/utils/routerHelper'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
|
||||
export const filterBreadcrumb = (
|
||||
routes: AppRouteRecordRaw[],
|
||||
parentPath = ''
|
||||
): AppRouteRecordRaw[] => {
|
||||
const res: AppRouteRecordRaw[] = []
|
||||
|
||||
for (const route of routes) {
|
||||
const meta = route?.meta as RouteMeta
|
||||
if (meta.hidden && !meta.canTo) {
|
||||
continue
|
||||
}
|
||||
|
||||
const data: AppRouteRecordRaw =
|
||||
!meta.alwaysShow && route.children?.length === 1
|
||||
? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) }
|
||||
: { ...route }
|
||||
|
||||
data.path = pathResolve(parentPath, data.path)
|
||||
|
||||
if (data.children) {
|
||||
data.children = filterBreadcrumb(data.children, data.path)
|
||||
}
|
||||
if (data) {
|
||||
res.push(data)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import BaseButton from './src/Button.vue'
|
||||
|
||||
export { BaseButton }
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { ElButton, ComponentSize, ButtonType } from 'element-plus'
|
||||
import { PropType, Component, computed, unref } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const getTheme = computed(() => appStore.getTheme)
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('button')
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String as PropType<ComponentSize>,
|
||||
default: undefined
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<ButtonType>,
|
||||
default: 'default'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
plain: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
text: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
bg: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
link: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
round: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loadingIcon: {
|
||||
type: [String, Object] as PropType<String | Component>,
|
||||
default: undefined
|
||||
},
|
||||
icon: {
|
||||
type: [String, Object] as PropType<String | Component>,
|
||||
default: undefined
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nativeType: {
|
||||
type: String as PropType<'button' | 'submit' | 'reset'>,
|
||||
default: 'button'
|
||||
},
|
||||
autoInsertSpace: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
darker: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tag: {
|
||||
type: [String, Object] as PropType<String | Component>,
|
||||
default: 'button'
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['click'])
|
||||
|
||||
const color = computed(() => {
|
||||
const { type } = props
|
||||
if (type === 'primary') {
|
||||
return unref(getTheme).elColorPrimary
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const style = computed(() => {
|
||||
const { type } = props
|
||||
if (type === 'primary') {
|
||||
return '--el-button-text-color: #fff; --el-button-hover-text-color: #fff'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElButton
|
||||
:class="`${prefixCls} color-#fff`"
|
||||
v-bind="{ ...props }"
|
||||
:color="color"
|
||||
:style="style"
|
||||
@click="() => emits('click')"
|
||||
>
|
||||
<slot></slot>
|
||||
<slot name="icon"></slot>
|
||||
<slot name="loading"></slot>
|
||||
</ElButton>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
import Collapse from './src/Collapse.vue'
|
||||
|
||||
export { Collapse }
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, unref } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('collapse')
|
||||
|
||||
defineProps({
|
||||
color: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const collapse = computed(() => appStore.getCollapse)
|
||||
|
||||
const toggleCollapse = () => {
|
||||
const collapsed = unref(collapse)
|
||||
appStore.setCollapse(!collapsed)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls" @click="toggleCollapse">
|
||||
<Icon
|
||||
:size="18"
|
||||
:icon="collapse ? 'ant-design:menu-unfold-outlined' : 'ant-design:menu-fold-outlined'"
|
||||
:color="color"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import ConfigGlobal from './src/ConfigGlobal.vue'
|
||||
|
||||
export type { ConfigGlobalTypes } from './src/types'
|
||||
|
||||
export { ConfigGlobal }
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { provide, computed, watch, onMounted } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { ComponentSize, ElConfigProvider } from 'element-plus'
|
||||
import { useLocaleStore } from '@/store/modules/locale'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { setCssVar } from '@/utils'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { variables } = useDesign()
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const props = defineProps({
|
||||
size: propTypes.oneOf<ComponentSize>(['default', 'small', 'large']).def('default')
|
||||
})
|
||||
|
||||
provide('configGlobal', props)
|
||||
|
||||
// 初始化所有主题色
|
||||
onMounted(() => {
|
||||
appStore.setCssVarTheme()
|
||||
})
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
// 监听窗口变化
|
||||
watch(
|
||||
() => width.value,
|
||||
(width: number) => {
|
||||
if (width < 768) {
|
||||
!appStore.getMobile ? appStore.setMobile(true) : undefined
|
||||
setCssVar('--left-menu-min-width', '0')
|
||||
appStore.setCollapse(true)
|
||||
appStore.getLayout !== 'classic' ? appStore.setLayout('classic') : undefined
|
||||
} else {
|
||||
appStore.getMobile ? appStore.setMobile(false) : undefined
|
||||
setCssVar('--left-menu-min-width', '64px')
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 多语言相关
|
||||
const localeStore = useLocaleStore()
|
||||
|
||||
const currentLocale = computed(() => localeStore.currentLocale)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElConfigProvider
|
||||
:namespace="variables.elNamespace"
|
||||
:locale="currentLocale.elLocale"
|
||||
:message="{ max: 1 }"
|
||||
:size="size"
|
||||
>
|
||||
<slot></slot>
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ComponentSize } from 'element-plus'
|
||||
|
||||
export interface ConfigGlobalTypes {
|
||||
size?: ComponentSize
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import ContentDetailWrap from './src/ContentDetailWrap.vue'
|
||||
|
||||
export { ContentDetailWrap }
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ElCard } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('content-detail-wrap')
|
||||
|
||||
defineProps({
|
||||
title: propTypes.string.def(''),
|
||||
message: propTypes.string.def('')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[`${prefixCls}-container`, 'relative']">
|
||||
<ElCard :class="[`${prefixCls}-body`, 'mb-20px']" shadow="never">
|
||||
<div class="mb-20px pb-20px" style="border-bottom: 1px solid var(--el-border-color)">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
import ContentWrap from './src/ContentWrap.vue'
|
||||
|
||||
export { ContentWrap }
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { ElCard, ElTooltip } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('content-wrap')
|
||||
|
||||
defineProps({
|
||||
title: propTypes.string.def(''),
|
||||
message: propTypes.string.def('')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard :class="[prefixCls]" shadow="never">
|
||||
<template v-if="title" #header>
|
||||
<div class="flex items-center">
|
||||
<span class="text-16px font-700">{{ title }}</span>
|
||||
<ElTooltip v-if="message" effect="dark" placement="right">
|
||||
<template #content>
|
||||
<div class="max-w-200px">{{ message }}</div>
|
||||
</template>
|
||||
<Icon class="ml-5px" icon="bi:question-circle-fill" :size="14" />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
import ContextMenu from './src/ContextMenu.vue'
|
||||
import { ElDropdown } from 'element-plus'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
export type { ContextMenuSchema } from './src/types'
|
||||
|
||||
export interface ContextMenuExpose {
|
||||
elDropdownMenuRef: ComponentRef<typeof ElDropdown>
|
||||
tagItem: RouteLocationNormalizedLoaded
|
||||
}
|
||||
|
||||
export { ContextMenu }
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||
import { PropType, ref } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { ContextMenuSchema } from './types'
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('context-menu')
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits(['visibleChange'])
|
||||
|
||||
const props = defineProps({
|
||||
schema: {
|
||||
type: Array as PropType<ContextMenuSchema[]>,
|
||||
default: () => []
|
||||
},
|
||||
trigger: {
|
||||
type: String as PropType<'click' | 'hover' | 'focus' | 'contextmenu'>,
|
||||
default: 'contextmenu'
|
||||
},
|
||||
tagItem: {
|
||||
type: Object as PropType<RouteLocationNormalizedLoaded>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const command = (item: ContextMenuSchema) => {
|
||||
item.command && item.command(item)
|
||||
}
|
||||
|
||||
const visibleChange = (visible: boolean) => {
|
||||
emit('visibleChange', visible, props.tagItem)
|
||||
}
|
||||
|
||||
const elDropdownMenuRef = ref<ComponentRef<typeof ElDropdown>>()
|
||||
|
||||
defineExpose({
|
||||
elDropdownMenuRef,
|
||||
tagItem: props.tagItem
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDropdown
|
||||
ref="elDropdownMenuRef"
|
||||
:class="prefixCls"
|
||||
:trigger="trigger"
|
||||
placement="bottom-start"
|
||||
@command="command"
|
||||
@visible-change="visibleChange"
|
||||
popper-class="v-context-menu-popper"
|
||||
>
|
||||
<slot></slot>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="(item, index) in schema"
|
||||
:key="`dropdown${index}`"
|
||||
:divided="item.divided"
|
||||
:disabled="item.disabled"
|
||||
:command="item"
|
||||
>
|
||||
<Icon :icon="item.icon" /> {{ t(item.label) }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface ContextMenuSchema {
|
||||
disabled?: boolean
|
||||
divided?: boolean
|
||||
icon?: string
|
||||
label: string
|
||||
command?: (item: ContextMenuSchema) => void
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Descriptions from './src/Descriptions.vue'
|
||||
|
||||
export type { DescriptionsSchema } from './src/types'
|
||||
|
||||
export { Descriptions }
|
||||
@@ -0,0 +1,161 @@
|
||||
<script lang="tsx">
|
||||
import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { ref, unref, PropType, computed, defineComponent } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { DescriptionsSchema } from './types'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { get } from 'lodash-es'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const mobile = computed(() => appStore.getMobile)
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('descriptions')
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Descriptions',
|
||||
props: {
|
||||
title: propTypes.string.def(''),
|
||||
message: propTypes.string.def(''),
|
||||
collapse: propTypes.bool.def(true),
|
||||
border: propTypes.bool.def(true),
|
||||
column: propTypes.number.def(2),
|
||||
size: propTypes.oneOf(['large', 'default', 'small']).def('default'),
|
||||
direction: propTypes.oneOf(['horizontal', 'vertical']).def('horizontal'),
|
||||
extra: propTypes.string.def(''),
|
||||
schema: {
|
||||
type: Array as PropType<DescriptionsSchema[]>,
|
||||
default: () => []
|
||||
},
|
||||
data: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup(props, { slots, attrs }) {
|
||||
const getBindValue = computed((): any => {
|
||||
const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
|
||||
const obj = { ...attrs, ...props }
|
||||
for (const key in obj) {
|
||||
if (delArr.indexOf(key) !== -1) {
|
||||
delete obj[key]
|
||||
}
|
||||
}
|
||||
if (unref(mobile)) {
|
||||
obj.direction = 'vertical'
|
||||
}
|
||||
return obj
|
||||
})
|
||||
|
||||
const getBindItemValue = (item: DescriptionsSchema) => {
|
||||
const delArr: string[] = ['field']
|
||||
const obj = { ...item }
|
||||
for (const key in obj) {
|
||||
if (delArr.indexOf(key) !== -1) {
|
||||
delete obj[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// 折叠
|
||||
const show = ref(true)
|
||||
|
||||
const toggleClick = () => {
|
||||
if (props.collapse) {
|
||||
show.value = !unref(show)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
prefixCls,
|
||||
'bg-[var(--el-color-white)] dark:bg-[var(--el-bg-color)] dark:border-[var(--el-border-color)] dark:border-1px'
|
||||
]}
|
||||
>
|
||||
{props.title ? (
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}-header`,
|
||||
'relative h-50px flex justify-between items-center layout-border__bottom px-10px cursor-pointer'
|
||||
]}
|
||||
onClick={toggleClick}
|
||||
>
|
||||
<div class={[`${prefixCls}-header__title`, 'relative font-18px font-bold ml-10px']}>
|
||||
<div class="flex items-center">
|
||||
{props.title}
|
||||
{props.message ? (
|
||||
<ElTooltip content={props.message} placement="right">
|
||||
<Icon icon="bi:question-circle-fill" class="ml-5px" size={14} />
|
||||
</ElTooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{props.collapse ? <Icon icon={show.value ? 'ep:arrow-down' : 'ep:arrow-up'} /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ElCollapseTransition>
|
||||
<div v-show={unref(show)} class={[`${prefixCls}-content`]}>
|
||||
<ElDescriptions {...unref(getBindValue)}>
|
||||
{{
|
||||
extra: () => (slots['extra'] ? slots['extra']() : props.extra),
|
||||
default: () => {
|
||||
return props.schema.map((item) => {
|
||||
return (
|
||||
<ElDescriptionsItem key={item.field} {...getBindItemValue(item)}>
|
||||
{{
|
||||
label: () => (item.slots?.label ? item.slots?.label(item) : item.label),
|
||||
default: () =>
|
||||
item.slots?.default
|
||||
? item.slots?.default(props.data)
|
||||
: get(props.data, item.field)
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
}}
|
||||
</ElDescriptions>
|
||||
</div>
|
||||
</ElCollapseTransition>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-descriptions';
|
||||
|
||||
:deep(.@{elNamespace}-descriptions__header) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.@{prefix-cls}-header {
|
||||
&__title {
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: -10px;
|
||||
width: 4px;
|
||||
height: 70%;
|
||||
background: var(--el-color-primary);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls}-content {
|
||||
:deep(.@{elNamespace}-descriptions__cell) {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface DescriptionsSchema {
|
||||
span?: number // 占多少分
|
||||
field: string // 字段名
|
||||
label?: string // label名
|
||||
width?: string | number
|
||||
minWidth?: string | number
|
||||
align?: 'left' | 'center' | 'right'
|
||||
labelAlign?: 'left' | 'center' | 'right'
|
||||
className?: string
|
||||
labelClassName?: string
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element | null
|
||||
label?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Dialog from './src/Dialog.vue'
|
||||
|
||||
export { Dialog }
|
||||
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { ElDialog, ElScrollbar } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { computed, useAttrs, ref, unref, useSlots, watch, nextTick } from 'vue'
|
||||
import { isNumber } from '@/utils/is'
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: propTypes.bool.def(false),
|
||||
title: propTypes.string.def('Dialog'),
|
||||
fullscreen: propTypes.bool.def(true),
|
||||
maxHeight: propTypes.oneOfType([String, Number]).def('400px')
|
||||
})
|
||||
|
||||
const getBindValue = computed(() => {
|
||||
const delArr: string[] = ['fullscreen', 'title', 'maxHeight']
|
||||
const attrs = useAttrs()
|
||||
const obj = { ...attrs, ...props }
|
||||
for (const key in obj) {
|
||||
if (delArr.indexOf(key) !== -1) {
|
||||
delete obj[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
})
|
||||
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const toggleFull = () => {
|
||||
isFullscreen.value = !unref(isFullscreen)
|
||||
}
|
||||
|
||||
const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight)
|
||||
|
||||
watch(
|
||||
() => isFullscreen.value,
|
||||
async (val: boolean) => {
|
||||
await nextTick()
|
||||
if (val) {
|
||||
const windowHeight = document.documentElement.offsetHeight
|
||||
dialogHeight.value = `${windowHeight - 55 - 60 - (slots.footer ? 63 : 0)}px`
|
||||
} else {
|
||||
dialogHeight.value = isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
const dialogStyle = computed(() => {
|
||||
return {
|
||||
height: unref(dialogHeight)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-bind="getBindValue"
|
||||
:fullscreen="isFullscreen"
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
draggable
|
||||
top="0"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header="{ close }">
|
||||
<div class="flex justify-between items-center h-54px pl-15px pr-15px relative">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
<div
|
||||
class="h-54px flex justify-between items-center absolute top-[50%] right-15px translate-y-[-50%]"
|
||||
>
|
||||
<Icon
|
||||
v-if="fullscreen"
|
||||
class="cursor-pointer is-hover !h-54px mr-10px"
|
||||
:icon="isFullscreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
|
||||
color="var(--el-color-info)"
|
||||
hover-color="var(--el-color-primary)"
|
||||
@click="toggleFull"
|
||||
/>
|
||||
<Icon
|
||||
class="cursor-pointer is-hover !h-54px"
|
||||
icon="ep:close"
|
||||
hover-color="var(--el-color-primary)"
|
||||
color="var(--el-color-info)"
|
||||
@click="close"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElScrollbar :style="dialogStyle">
|
||||
<slot></slot>
|
||||
</ElScrollbar>
|
||||
|
||||
<template v-if="slots.footer" #footer>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.@{elNamespace}-overlay-dialog {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.@{elNamespace}-dialog {
|
||||
margin: 0 !important;
|
||||
|
||||
&__header {
|
||||
height: 54px;
|
||||
padding: 0;
|
||||
margin-right: 0 !important;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 15px !important;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
&__headerbtn {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import Editor from './src/Editor.vue'
|
||||
import { IDomEditor } from '@wangeditor/editor'
|
||||
|
||||
export interface EditorExpose {
|
||||
getEditorRef: () => Promise<IDomEditor>
|
||||
}
|
||||
|
||||
export { Editor }
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, computed, PropType, unref, nextTick, ref, watch, shallowRef } from 'vue'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { IDomEditor, IEditorConfig, i18nChangeLanguage } from '@wangeditor/editor'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { isNumber } from '@/utils/is'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useLocaleStore } from '@/store/modules/locale'
|
||||
|
||||
const localeStore = useLocaleStore()
|
||||
|
||||
const currentLocale = computed(() => localeStore.getCurrentLocale)
|
||||
|
||||
i18nChangeLanguage(unref(currentLocale).lang)
|
||||
|
||||
const props = defineProps({
|
||||
editorId: propTypes.string.def('wangeEditor-1'),
|
||||
height: propTypes.oneOfType([Number, String]).def('500px'),
|
||||
editorConfig: {
|
||||
type: Object as PropType<IEditorConfig>,
|
||||
default: () => undefined
|
||||
},
|
||||
modelValue: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change', 'update:modelValue'])
|
||||
|
||||
// 编辑器实例,必须用 shallowRef
|
||||
const editorRef = shallowRef<IDomEditor>()
|
||||
|
||||
const valueHtml = ref('')
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: string) => {
|
||||
if (val === unref(valueHtml)) return
|
||||
valueHtml.value = val
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 监听
|
||||
watch(
|
||||
() => valueHtml.value,
|
||||
(val: string) => {
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
)
|
||||
|
||||
const handleCreated = (editor: IDomEditor) => {
|
||||
editorRef.value = editor
|
||||
}
|
||||
|
||||
// 编辑器配置
|
||||
const editorConfig = computed((): IEditorConfig => {
|
||||
return Object.assign(
|
||||
{
|
||||
readOnly: false,
|
||||
customAlert: (s: string, t: string) => {
|
||||
switch (t) {
|
||||
case 'success':
|
||||
ElMessage.success(s)
|
||||
break
|
||||
case 'info':
|
||||
ElMessage.info(s)
|
||||
break
|
||||
case 'warning':
|
||||
ElMessage.warning(s)
|
||||
break
|
||||
case 'error':
|
||||
ElMessage.error(s)
|
||||
break
|
||||
default:
|
||||
ElMessage.info(s)
|
||||
break
|
||||
}
|
||||
},
|
||||
autoFocus: false,
|
||||
scroll: true,
|
||||
uploadImgShowBase64: true
|
||||
},
|
||||
props.editorConfig || {}
|
||||
)
|
||||
})
|
||||
|
||||
const editorStyle = computed(() => {
|
||||
return {
|
||||
height: isNumber(props.height) ? `${props.height}px` : props.height
|
||||
}
|
||||
})
|
||||
|
||||
// 回调函数
|
||||
const handleChange = (editor: IDomEditor) => {
|
||||
emit('change', editor)
|
||||
}
|
||||
|
||||
// 组件销毁时,及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = unref(editorRef.value)
|
||||
|
||||
// 销毁,并移除 editor
|
||||
editor?.destroy()
|
||||
})
|
||||
|
||||
const getEditorRef = async (): Promise<IDomEditor> => {
|
||||
await nextTick()
|
||||
return unref(editorRef.value) as IDomEditor
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getEditorRef
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-1 border-solid border-[var(--el-border-color)] z-10">
|
||||
<!-- 工具栏 -->
|
||||
<Toolbar
|
||||
:editor="editorRef"
|
||||
:editorId="editorId"
|
||||
class="border-0 b-b-1 border-solid border-[var(--el-border-color)]"
|
||||
/>
|
||||
<!-- 编辑器 -->
|
||||
<Editor
|
||||
v-model="valueHtml"
|
||||
:editorId="editorId"
|
||||
:defaultConfig="editorConfig"
|
||||
:style="editorStyle"
|
||||
@on-change="handleChange"
|
||||
@on-created="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style src="@wangeditor/editor/dist/css/style.css"></style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import Error from './src/Error.vue'
|
||||
|
||||
export { Error }
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import pageError from '@/assets/svgs/404.svg'
|
||||
import networkError from '@/assets/svgs/500.svg'
|
||||
import noPermission from '@/assets/svgs/403.svg'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { ElButton } from 'element-plus'
|
||||
|
||||
interface ErrorMap {
|
||||
url: string
|
||||
message: string
|
||||
buttonText: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const errorMap: {
|
||||
[key: string]: ErrorMap
|
||||
} = {
|
||||
'404': {
|
||||
url: pageError,
|
||||
message: t('error.pageError'),
|
||||
buttonText: t('error.returnToHome')
|
||||
},
|
||||
'500': {
|
||||
url: networkError,
|
||||
message: t('error.networkError'),
|
||||
buttonText: t('error.returnToHome')
|
||||
},
|
||||
'403': {
|
||||
url: noPermission,
|
||||
message: t('error.noPermission'),
|
||||
buttonText: t('error.returnToHome')
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
type: propTypes.string.validate((v: string) => ['404', '500', '403'].includes(v)).def('404')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['errorClick'])
|
||||
|
||||
const btnClick = () => {
|
||||
emit('errorClick', props.type)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center">
|
||||
<div class="text-center">
|
||||
<img width="350" :src="errorMap[type].url" alt="" />
|
||||
<div class="text-14px text-[var(--el-color-info)]">{{ errorMap[type].message }}</div>
|
||||
<div class="mt-20px">
|
||||
<ElButton type="primary" @click="btnClick">{{ errorMap[type].buttonText }}</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,48 @@
|
||||
import Form from './src/Form.vue'
|
||||
import type { FormSchema, FormSetProps } from './src/types'
|
||||
export type {
|
||||
ComponentNameEnum,
|
||||
ComponentName,
|
||||
InputComponentProps,
|
||||
AutocompleteComponentProps,
|
||||
InputNumberComponentProps,
|
||||
SelectOption,
|
||||
SelectComponentProps,
|
||||
SelectV2ComponentProps,
|
||||
CascaderComponentProps,
|
||||
SwitchComponentProps,
|
||||
RateComponentProps,
|
||||
ColorPickerComponentProps,
|
||||
TransferComponentProps,
|
||||
RadioOption,
|
||||
RadioGroupComponentProps,
|
||||
RadioButtonComponentProps,
|
||||
CheckboxOption,
|
||||
CheckboxGroupComponentProps,
|
||||
DividerComponentProps,
|
||||
DatePickerComponentProps,
|
||||
DateTimePickerComponentProps,
|
||||
TimePickerComponentProps,
|
||||
TimeSelectComponentProps,
|
||||
ColProps,
|
||||
FormSetProps,
|
||||
FormItemProps,
|
||||
FormSchema,
|
||||
FormProps,
|
||||
PlaceholderModel,
|
||||
InputPasswordComponentProps,
|
||||
TreeSelectComponentProps
|
||||
} from './src/types'
|
||||
|
||||
export interface FormExpose {
|
||||
setValues: (data: Recordable) => void
|
||||
setProps: (props: Recordable) => void
|
||||
delSchema: (field: string) => void
|
||||
addSchema: (formSchema: FormSchema, index?: number) => void
|
||||
setSchema: (schemaProps: FormSetProps[]) => void
|
||||
formModel: Recordable
|
||||
getComponentExpose: (field: string) => any
|
||||
getFormItemExpose: (field: string) => any
|
||||
}
|
||||
|
||||
export { Form }
|
||||
@@ -0,0 +1,420 @@
|
||||
<script lang="tsx">
|
||||
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRow,
|
||||
ElCol,
|
||||
FormRules,
|
||||
ComponentSize
|
||||
// FormItemProp
|
||||
} from 'element-plus'
|
||||
import { componentMap } from './helper/componentMap'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { getSlot } from '@/utils/tsxHelper'
|
||||
import {
|
||||
setTextPlaceholder,
|
||||
setGridProp,
|
||||
setComponentProps,
|
||||
setItemComponentSlots,
|
||||
initModel
|
||||
} from './helper'
|
||||
import { useRenderSelect } from './components/useRenderSelect'
|
||||
import { useRenderRadio } from './components/useRenderRadio'
|
||||
import { useRenderCheckbox } from './components/useRenderCheckbox'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { findIndex } from '@/utils'
|
||||
import { get, set } from 'lodash-es'
|
||||
import { FormProps } from './types'
|
||||
import {
|
||||
FormSchema,
|
||||
FormSetProps,
|
||||
ComponentNameEnum,
|
||||
SelectComponentProps,
|
||||
RadioGroupComponentProps,
|
||||
CheckboxGroupComponentProps
|
||||
} from './types'
|
||||
|
||||
const { renderSelectOptions } = useRenderSelect()
|
||||
const { renderRadioOptions } = useRenderRadio()
|
||||
const { renderCheckboxOptions } = useRenderCheckbox()
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('form')
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Form',
|
||||
props: {
|
||||
// 生成Form的布局结构数组
|
||||
schema: {
|
||||
type: Array as PropType<FormSchema[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 是否需要栅格布局
|
||||
isCol: propTypes.bool.def(true),
|
||||
// 表单数据对象
|
||||
model: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({})
|
||||
},
|
||||
// 是否自动设置placeholder
|
||||
autoSetPlaceholder: propTypes.bool.def(true),
|
||||
// 是否自定义内容
|
||||
isCustom: propTypes.bool.def(false),
|
||||
// 表单label宽度
|
||||
labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
|
||||
rules: {
|
||||
type: Object as PropType<FormRules>,
|
||||
default: () => ({})
|
||||
},
|
||||
labelPosition: propTypes.oneOf(['left', 'right', 'top']).def('right'),
|
||||
labelSuffix: propTypes.string.def(''),
|
||||
hideRequiredAsterisk: propTypes.bool.def(false),
|
||||
requireAsteriskPosition: propTypes.oneOf(['left', 'right']).def('left'),
|
||||
showMessage: propTypes.bool.def(true),
|
||||
inlineMessage: propTypes.bool.def(false),
|
||||
statusIcon: propTypes.bool.def(false),
|
||||
validateOnRuleChange: propTypes.bool.def(true),
|
||||
size: {
|
||||
type: String as PropType<ComponentSize>,
|
||||
default: undefined
|
||||
},
|
||||
disabled: propTypes.bool.def(false),
|
||||
scrollToError: propTypes.bool.def(false),
|
||||
scrollToErrorOffset: propTypes.oneOfType([Boolean, Object]).def(undefined)
|
||||
// onValidate: {
|
||||
// type: Function as PropType<(prop: FormItemProp, isValid: boolean, message: string) => void>,
|
||||
// default: () => {}
|
||||
// }
|
||||
},
|
||||
emits: ['register'],
|
||||
setup(props, { slots, expose, emit }) {
|
||||
// element form 实例
|
||||
const elFormRef = ref<ComponentRef<typeof ElForm>>()
|
||||
|
||||
const mergeProps = ref<FormProps>({})
|
||||
|
||||
const getProps = computed(() => {
|
||||
const propsObj = { ...props }
|
||||
Object.assign(propsObj, unref(mergeProps))
|
||||
return propsObj
|
||||
})
|
||||
|
||||
// 存储表单实例
|
||||
const formComponents = ref({})
|
||||
|
||||
// 存储form-item实例
|
||||
const formItemComponents = ref({})
|
||||
|
||||
// 表单数据
|
||||
const formModel = ref<Recordable>(props.model)
|
||||
|
||||
onMounted(() => {
|
||||
emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
|
||||
})
|
||||
|
||||
// 对表单赋值
|
||||
const setValues = (data: Recordable = {}) => {
|
||||
formModel.value = Object.assign(unref(formModel), data)
|
||||
}
|
||||
|
||||
const setProps = (props: FormProps = {}) => {
|
||||
mergeProps.value = Object.assign(unref(mergeProps), props)
|
||||
}
|
||||
|
||||
const delSchema = (field: string) => {
|
||||
const { schema } = unref(getProps)
|
||||
|
||||
const index = findIndex(schema, (v: FormSchema) => v.field === field)
|
||||
if (index > -1) {
|
||||
schema.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const addSchema = (formSchema: FormSchema, index?: number) => {
|
||||
const { schema } = unref(getProps)
|
||||
if (index !== void 0) {
|
||||
schema.splice(index, 0, formSchema)
|
||||
return
|
||||
}
|
||||
schema.push(formSchema)
|
||||
}
|
||||
|
||||
const setSchema = (schemaProps: FormSetProps[]) => {
|
||||
const { schema } = unref(getProps)
|
||||
for (const v of schema) {
|
||||
for (const item of schemaProps) {
|
||||
if (v.field === item.field) {
|
||||
set(v, item.path, item.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getOptions = async (fn: Function, item: FormSchema) => {
|
||||
const options = await fn()
|
||||
setSchema([
|
||||
{
|
||||
field: item.field,
|
||||
path:
|
||||
item.component === ComponentNameEnum.TREE_SELECT
|
||||
? 'componentProps.data'
|
||||
: 'componentProps.options',
|
||||
value: options
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取表单组件实例
|
||||
* @param filed 表单字段
|
||||
*/
|
||||
const getComponentExpose = (filed: string) => {
|
||||
return unref(formComponents)[filed]
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取formItem实例
|
||||
* @param filed 表单字段
|
||||
*/
|
||||
const getFormItemExpose = (filed: string) => {
|
||||
return unref(formItemComponents)[filed]
|
||||
}
|
||||
|
||||
const setComponentRefMap = (ref: any, filed: string) => {
|
||||
formComponents.value[filed] = ref
|
||||
}
|
||||
|
||||
const setFormItemRefMap = (ref: any, filed: string) => {
|
||||
formItemComponents.value[filed] = ref
|
||||
}
|
||||
|
||||
expose({
|
||||
setValues,
|
||||
formModel,
|
||||
setProps,
|
||||
delSchema,
|
||||
addSchema,
|
||||
setSchema,
|
||||
getComponentExpose,
|
||||
getFormItemExpose
|
||||
})
|
||||
|
||||
// 监听表单结构化数组,重新生成formModel
|
||||
watch(
|
||||
() => unref(getProps).schema,
|
||||
(schema = []) => {
|
||||
formModel.value = initModel(schema, unref(formModel))
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
// 渲染包裹标签,是否使用栅格布局
|
||||
const renderWrap = () => {
|
||||
const { isCol } = unref(getProps)
|
||||
const content = isCol ? (
|
||||
<ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
|
||||
) : (
|
||||
renderFormItemWrap()
|
||||
)
|
||||
return content
|
||||
}
|
||||
|
||||
// 是否要渲染el-col
|
||||
const renderFormItemWrap = () => {
|
||||
// hidden属性表示隐藏,不做渲染
|
||||
const { schema = [], isCol } = unref(getProps)
|
||||
|
||||
return schema
|
||||
.filter((v) => !v.remove && !v.hidden)
|
||||
.map((item) => {
|
||||
// 如果是 Divider 组件,需要自己占用一行
|
||||
const isDivider = item.component === 'Divider'
|
||||
const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
|
||||
return isDivider ? (
|
||||
<Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
|
||||
) : isCol ? (
|
||||
// 如果需要栅格,需要包裹 ElCol
|
||||
<ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
|
||||
) : (
|
||||
renderFormItem(item)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染formItem
|
||||
const renderFormItem = (item: FormSchema) => {
|
||||
// 如果有optionApi,优先使用optionApi
|
||||
if (item.optionApi) {
|
||||
// 内部自动调用接口,不影响其它渲染
|
||||
getOptions(item.optionApi, item)
|
||||
}
|
||||
const formItemSlots: Recordable = {
|
||||
default: () => {
|
||||
if (item?.formItemProps?.slots?.default) {
|
||||
return item?.formItemProps?.slots?.default(formModel.value)
|
||||
} else {
|
||||
const Com = componentMap[item.component as string] as ReturnType<typeof defineComponent>
|
||||
|
||||
const { autoSetPlaceholder } = unref(getProps)
|
||||
|
||||
const componentSlots = (item?.componentProps as any)?.slots || {}
|
||||
const slotsMap: Recordable = {
|
||||
...setItemComponentSlots(componentSlots)
|
||||
}
|
||||
// // 如果是select组件,并且没有自定义模板,自动渲染options
|
||||
if (item.component === ComponentNameEnum.SELECT) {
|
||||
slotsMap.default = !componentSlots.default
|
||||
? () => renderSelectOptions(item)
|
||||
: () => {
|
||||
return componentSlots.default(
|
||||
unref((item?.componentProps as SelectComponentProps)?.options)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 虚拟列表
|
||||
if (item.component === ComponentNameEnum.SELECT_V2 && componentSlots.default) {
|
||||
slotsMap.default = ({ item }) => {
|
||||
return componentSlots.default(item)
|
||||
}
|
||||
}
|
||||
|
||||
// 单选框组和按钮样式
|
||||
if (
|
||||
item.component === ComponentNameEnum.RADIO_GROUP ||
|
||||
item.component === ComponentNameEnum.RADIO_BUTTON
|
||||
) {
|
||||
slotsMap.default = !componentSlots.default
|
||||
? () => renderRadioOptions(item)
|
||||
: () => {
|
||||
return componentSlots.default(
|
||||
unref((item?.componentProps as CheckboxGroupComponentProps)?.options)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 多选框组和按钮样式
|
||||
if (
|
||||
item.component === ComponentNameEnum.CHECKBOX_GROUP ||
|
||||
item.component === ComponentNameEnum.CHECKBOX_BUTTON
|
||||
) {
|
||||
slotsMap.default = !componentSlots.default
|
||||
? () => renderCheckboxOptions(item)
|
||||
: () => {
|
||||
return componentSlots.default(
|
||||
unref((item?.componentProps as RadioGroupComponentProps)?.options)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Comp = () => {
|
||||
// 如果field是多层路径,需要转换成对象
|
||||
const itemVal = computed({
|
||||
get: () => {
|
||||
return get(formModel.value, item.field)
|
||||
},
|
||||
set: (val) => {
|
||||
set(formModel.value, item.field, val)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Com
|
||||
vModel={itemVal.value}
|
||||
ref={(el: any) => setComponentRefMap(el, item.field)}
|
||||
{...(autoSetPlaceholder && setTextPlaceholder(item))}
|
||||
{...setComponentProps(item)}
|
||||
style={
|
||||
item.componentProps?.style || {
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
>
|
||||
{{ ...slotsMap }}
|
||||
</Com>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{Comp()}</>
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item?.formItemProps?.slots?.label) {
|
||||
formItemSlots.label = (...args: any[]) => {
|
||||
return (item?.formItemProps?.slots as any)?.label(...args)
|
||||
}
|
||||
}
|
||||
if (item?.formItemProps?.slots?.error) {
|
||||
formItemSlots.error = (...args: any[]) => {
|
||||
return (item?.formItemProps?.slots as any)?.error(...args)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ElFormItem
|
||||
v-show={!item.hidden}
|
||||
ref={(el: any) => setFormItemRefMap(el, item.field)}
|
||||
{...(item.formItemProps || {})}
|
||||
prop={item.field}
|
||||
label={item.label || ''}
|
||||
>
|
||||
{formItemSlots}
|
||||
</ElFormItem>
|
||||
)
|
||||
}
|
||||
|
||||
// 过滤传入Form组件的属性
|
||||
const getFormBindValue = () => {
|
||||
// 避免在标签上出现多余的属性
|
||||
const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
|
||||
const props = { ...unref(getProps) }
|
||||
for (const key in props) {
|
||||
if (delKeys.indexOf(key) !== -1) {
|
||||
delete props[key]
|
||||
}
|
||||
}
|
||||
return props as FormProps
|
||||
}
|
||||
|
||||
return () => (
|
||||
<ElForm
|
||||
ref={elFormRef}
|
||||
{...getFormBindValue()}
|
||||
model={unref(getProps).isCustom ? unref(getProps).model : formModel}
|
||||
class={prefixCls}
|
||||
>
|
||||
{{
|
||||
// 如果需要自定义,就什么都不渲染,而是提供默认插槽
|
||||
default: () => {
|
||||
const { isCustom } = unref(getProps)
|
||||
return isCustom ? getSlot(slots, 'default') : renderWrap()
|
||||
}
|
||||
}}
|
||||
</ElForm>
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.@{elNamespace}-form.@{namespace}-form .@{elNamespace}-row {
|
||||
margin-right: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.@{elNamespace}-form--inline {
|
||||
:deep(.el-form-item__content) {
|
||||
& > :first-child {
|
||||
min-width: 229.5px;
|
||||
}
|
||||
}
|
||||
.@{elNamespace}-input-number {
|
||||
// 229.5px是兼容el-input-number的最小宽度,
|
||||
min-width: 229.5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FormSchema, ComponentNameEnum, CheckboxGroupComponentProps } from '../types'
|
||||
import { ElCheckbox, ElCheckboxButton } from 'element-plus'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export const useRenderCheckbox = () => {
|
||||
const renderCheckboxOptions = (item: FormSchema) => {
|
||||
// 如果有别名,就取别名
|
||||
const componentProps = item?.componentProps as CheckboxGroupComponentProps
|
||||
const valueAlias = componentProps?.props?.value || 'value'
|
||||
const labelAlias = componentProps?.props?.label || 'label'
|
||||
const disabledAlias = componentProps?.props?.disabled || 'disabled'
|
||||
const Com = (
|
||||
item.component === ComponentNameEnum.CHECKBOX_GROUP ? ElCheckbox : ElCheckboxButton
|
||||
) as ReturnType<typeof defineComponent>
|
||||
return componentProps?.options?.map((option) => {
|
||||
const { value, ...other } = option
|
||||
return (
|
||||
<Com
|
||||
{...other}
|
||||
disabled={option[disabledAlias || 'disabled']}
|
||||
label={option[valueAlias || 'value']}
|
||||
>
|
||||
{option[labelAlias || 'label']}
|
||||
</Com>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
renderCheckboxOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FormSchema, ComponentNameEnum, RadioGroupComponentProps } from '../types'
|
||||
import { ElRadio, ElRadioButton } from 'element-plus'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export const useRenderRadio = () => {
|
||||
const renderRadioOptions = (item: FormSchema) => {
|
||||
// 如果有别名,就取别名
|
||||
const componentProps = item?.componentProps as RadioGroupComponentProps
|
||||
const valueAlias = componentProps?.props?.value || 'value'
|
||||
const labelAlias = componentProps?.props?.label || 'label'
|
||||
const disabledAlias = componentProps?.props?.disabled || 'disabled'
|
||||
const Com = (
|
||||
item.component === ComponentNameEnum.RADIO_GROUP ? ElRadio : ElRadioButton
|
||||
) as ReturnType<typeof defineComponent>
|
||||
return componentProps?.options?.map((option) => {
|
||||
const { value, ...other } = option
|
||||
return (
|
||||
<Com
|
||||
{...other}
|
||||
disabled={option[disabledAlias || 'disabled']}
|
||||
label={option[valueAlias || 'value']}
|
||||
>
|
||||
{option[labelAlias || 'label']}
|
||||
</Com>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
renderRadioOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ElOption, ElOptionGroup } from 'element-plus'
|
||||
import { FormSchema, SelectComponentProps, SelectOption } from '../types'
|
||||
|
||||
export const useRenderSelect = () => {
|
||||
// 渲染 select options
|
||||
const renderSelectOptions = (item: FormSchema) => {
|
||||
const componentsProps = item?.componentProps as SelectComponentProps
|
||||
const optionGroupDefaultSlot = componentsProps?.slots?.optionGroupDefault
|
||||
// 如果有别名,就取别名
|
||||
const labelAlias = componentsProps?.props?.label
|
||||
const keyAlias = componentsProps?.props?.key
|
||||
return componentsProps?.options?.map((option) => {
|
||||
if (option?.options?.length) {
|
||||
return optionGroupDefaultSlot ? (
|
||||
optionGroupDefaultSlot(option)
|
||||
) : (
|
||||
<ElOptionGroup label={option[labelAlias || 'label']} key={option[keyAlias || 'key']}>
|
||||
{{
|
||||
default: () =>
|
||||
option?.options?.map((v) => {
|
||||
return renderSelectOptionItem(item, v)
|
||||
})
|
||||
}}
|
||||
</ElOptionGroup>
|
||||
)
|
||||
} else {
|
||||
return renderSelectOptionItem(item, option)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染 select option item
|
||||
const renderSelectOptionItem = (item: FormSchema, option: SelectOption) => {
|
||||
// 如果有别名,就取别名
|
||||
const componentsProps = item.componentProps as SelectComponentProps
|
||||
const labelAlias = componentsProps?.props?.label
|
||||
const valueAlias = componentsProps?.props?.value
|
||||
const keyAlias = componentsProps?.props?.key
|
||||
const optionDefaultSlot = componentsProps.slots?.optionDefault
|
||||
|
||||
return (
|
||||
<ElOption
|
||||
{...option}
|
||||
key={option[keyAlias || 'key']}
|
||||
label={option[labelAlias || 'label']}
|
||||
value={option[valueAlias || 'value']}
|
||||
>
|
||||
{{
|
||||
default: () => (optionDefaultSlot ? optionDefaultSlot(option) : undefined)
|
||||
}}
|
||||
</ElOption>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
renderSelectOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Component } from 'vue'
|
||||
import {
|
||||
ElCascader,
|
||||
ElCheckboxGroup,
|
||||
ElColorPicker,
|
||||
ElDatePicker,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElRadioGroup,
|
||||
ElRate,
|
||||
ElSelect,
|
||||
ElSelectV2,
|
||||
ElSlider,
|
||||
ElSwitch,
|
||||
ElTimePicker,
|
||||
ElTimeSelect,
|
||||
ElTransfer,
|
||||
ElAutocomplete,
|
||||
ElDivider,
|
||||
ElTreeSelect,
|
||||
ElUpload
|
||||
} from 'element-plus'
|
||||
import { InputPassword } from '@/components/InputPassword'
|
||||
import { Editor } from '@/components/Editor'
|
||||
import { ComponentName } from '../types'
|
||||
|
||||
const componentMap: Recordable<Component, ComponentName> = {
|
||||
RadioGroup: ElRadioGroup,
|
||||
RadioButton: ElRadioGroup,
|
||||
CheckboxGroup: ElCheckboxGroup,
|
||||
CheckboxButton: ElCheckboxGroup,
|
||||
Input: ElInput,
|
||||
Autocomplete: ElAutocomplete,
|
||||
InputNumber: ElInputNumber,
|
||||
Select: ElSelect,
|
||||
Cascader: ElCascader,
|
||||
Switch: ElSwitch,
|
||||
Slider: ElSlider,
|
||||
TimePicker: ElTimePicker,
|
||||
DatePicker: ElDatePicker,
|
||||
Rate: ElRate,
|
||||
ColorPicker: ElColorPicker,
|
||||
Transfer: ElTransfer,
|
||||
Divider: ElDivider,
|
||||
TimeSelect: ElTimeSelect,
|
||||
SelectV2: ElSelectV2,
|
||||
InputPassword: InputPassword,
|
||||
Editor: Editor,
|
||||
TreeSelect: ElTreeSelect,
|
||||
Upload: ElUpload
|
||||
}
|
||||
|
||||
export { componentMap }
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { PlaceholderModel, FormSchema, ComponentNameEnum, ColProps } from '../types'
|
||||
import { isFunction } from '@/utils/is'
|
||||
import { firstUpperCase, humpToDash } from '@/utils'
|
||||
import { set, get } from 'lodash-es'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
/**
|
||||
*
|
||||
* @param schema 对应组件数据
|
||||
* @returns 返回提示信息对象
|
||||
* @description 用于自动设置placeholder
|
||||
*/
|
||||
export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => {
|
||||
const textMap = [
|
||||
ComponentNameEnum.INPUT,
|
||||
ComponentNameEnum.AUTOCOMPLETE,
|
||||
ComponentNameEnum.INPUT_NUMBER,
|
||||
ComponentNameEnum.INPUT_PASSWORD
|
||||
]
|
||||
const selectMap = [
|
||||
ComponentNameEnum.SELECT,
|
||||
ComponentNameEnum.TIME_PICKER,
|
||||
ComponentNameEnum.DATE_PICKER,
|
||||
ComponentNameEnum.TIME_SELECT,
|
||||
ComponentNameEnum.SELECT_V2
|
||||
]
|
||||
if (textMap.includes(schema?.component as ComponentNameEnum)) {
|
||||
return {
|
||||
placeholder: t('common.inputText')
|
||||
}
|
||||
}
|
||||
if (selectMap.includes(schema?.component as ComponentNameEnum)) {
|
||||
// 一些范围选择器
|
||||
const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange']
|
||||
if (
|
||||
twoTextMap.includes(
|
||||
((schema?.componentProps as any)?.type ||
|
||||
(schema?.componentProps as any)?.isRange) as string
|
||||
)
|
||||
) {
|
||||
return {
|
||||
startPlaceholder: t('common.startTimeText'),
|
||||
endPlaceholder: t('common.endTimeText'),
|
||||
rangeSeparator: '-'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
placeholder: t('common.selectText')
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param col 内置栅格
|
||||
* @returns 返回栅格属性
|
||||
* @description 合并传入进来的栅格属性
|
||||
*/
|
||||
export const setGridProp = (col: ColProps = {}): ColProps => {
|
||||
const colProps: ColProps = {
|
||||
// 如果有span,代表用户优先级更高,所以不需要默认栅格
|
||||
...(col.span
|
||||
? {}
|
||||
: {
|
||||
xs: 24,
|
||||
sm: 12,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}),
|
||||
...col
|
||||
}
|
||||
return colProps
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param item 传入的组件属性
|
||||
* @returns 默认添加 clearable 属性
|
||||
*/
|
||||
export const setComponentProps = (item: FormSchema): Recordable => {
|
||||
// const notNeedClearable = ['ColorPicker']
|
||||
// 拆分事件并组合
|
||||
const onEvents = (item?.componentProps as any)?.on || {}
|
||||
const newOnEvents: Recordable = {}
|
||||
|
||||
for (const key in onEvents) {
|
||||
if (onEvents[key]) {
|
||||
newOnEvents[`on${firstUpperCase(key)}`] = (...args: any[]) => {
|
||||
onEvents[key](...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const componentProps: Recordable = {
|
||||
clearable: true,
|
||||
...item.componentProps,
|
||||
...newOnEvents
|
||||
}
|
||||
// 需要删除额外的属性
|
||||
if (componentProps.slots) {
|
||||
delete componentProps.slots
|
||||
}
|
||||
if (componentProps.on) {
|
||||
delete componentProps.on
|
||||
}
|
||||
return componentProps
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param formModel 表单数据
|
||||
* @param slotsProps 插槽属性
|
||||
*/
|
||||
export const setItemComponentSlots = (slotsProps: Recordable = {}): Recordable => {
|
||||
const slotObj: Recordable = {}
|
||||
for (const key in slotsProps) {
|
||||
if (slotsProps[key]) {
|
||||
if (isFunction(slotsProps[key])) {
|
||||
slotObj[humpToDash(key)] = (...args: any[]) => {
|
||||
return slotsProps[key]?.(...args)
|
||||
}
|
||||
} else {
|
||||
slotObj[humpToDash(key)] = () => {
|
||||
return slotsProps[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return slotObj
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param schema Form表单结构化数组
|
||||
* @param formModel FormMoel
|
||||
* @returns FormMoel
|
||||
* @description 生成对应的formModel
|
||||
*/
|
||||
export const initModel = (schema: FormSchema[], formModel: Recordable) => {
|
||||
const model: Recordable = { ...formModel }
|
||||
schema.map((v) => {
|
||||
if (v.remove) {
|
||||
delete model[v.field]
|
||||
} else if (v.component !== 'Divider') {
|
||||
// const hasField = Reflect.has(model, v.field)
|
||||
const hasField = get(model, v.field)
|
||||
// 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
|
||||
set(
|
||||
model,
|
||||
v.field,
|
||||
hasField !== void 0 ? get(model, v.field) : v.value !== void 0 ? v.value : undefined
|
||||
)
|
||||
// model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : undefined
|
||||
}
|
||||
})
|
||||
return model
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
import {
|
||||
AutocompleteProps,
|
||||
InputNumberProps,
|
||||
CascaderProps,
|
||||
CascaderNode,
|
||||
CascaderValue,
|
||||
SwitchProps,
|
||||
ComponentSize,
|
||||
InputProps,
|
||||
RateProps,
|
||||
ColorPickerProps,
|
||||
TransferProps,
|
||||
RadioGroupProps,
|
||||
RadioButtonProps,
|
||||
CheckboxGroupProps,
|
||||
DividerProps,
|
||||
DatePickerProps,
|
||||
FormItemProps as ElFormItemProps,
|
||||
FormProps as ElFormProps,
|
||||
ISelectProps,
|
||||
UploadProps
|
||||
} from 'element-plus'
|
||||
import { IEditorConfig } from '@wangeditor/editor'
|
||||
import { CSSProperties } from 'vue'
|
||||
|
||||
export interface PlaceholderModel {
|
||||
placeholder?: string
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
rangeSeparator?: string
|
||||
}
|
||||
|
||||
export enum ComponentNameEnum {
|
||||
RADIO_GROUP = 'RadioGroup',
|
||||
RADIO_BUTTON = 'RadioButton',
|
||||
CHECKBOX_GROUP = 'CheckboxGroup',
|
||||
CHECKBOX_BUTTON = 'CheckboxButton',
|
||||
INPUT = 'Input',
|
||||
AUTOCOMPLETE = 'Autocomplete',
|
||||
INPUT_NUMBER = 'InputNumber',
|
||||
SELECT = 'Select',
|
||||
CASCADER = 'Cascader',
|
||||
SWITCH = 'Switch',
|
||||
SLIDER = 'Slider',
|
||||
TIME_PICKER = 'TimePicker',
|
||||
DATE_PICKER = 'DatePicker',
|
||||
RATE = 'Rate',
|
||||
COLOR_PICKER = 'ColorPicker',
|
||||
TRANSFER = 'Transfer',
|
||||
DIVIDER = 'Divider',
|
||||
TIME_SELECT = 'TimeSelect',
|
||||
SELECT_V2 = 'SelectV2',
|
||||
INPUT_PASSWORD = 'InputPassword',
|
||||
EDITOR = 'Editor',
|
||||
TREE_SELECT = 'TreeSelect',
|
||||
UPLOAD = 'Upload'
|
||||
}
|
||||
|
||||
type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K
|
||||
? K extends string
|
||||
? K extends `${infer A}_${infer B}`
|
||||
? `${Capitalize<Lowercase<A>>}${Capitalize<Lowercase<B>>}`
|
||||
: Capitalize<Lowercase<K>>
|
||||
: never
|
||||
: never
|
||||
|
||||
export type ComponentName = CamelCaseComponentName
|
||||
|
||||
export interface InputPasswordComponentProps {
|
||||
strength?: boolean
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface InputComponentProps extends Partial<InputProps> {
|
||||
rows?: number
|
||||
on?: {
|
||||
blur?: (event: FocusEvent) => void
|
||||
focus?: (event: FocusEvent) => void
|
||||
change?: (value: string | number) => void
|
||||
clear?: () => void
|
||||
input?: (value: string | number) => void
|
||||
}
|
||||
slots?: {
|
||||
prefix?: (...args: any[]) => JSX.Element | null
|
||||
suffix?: (...args: any[]) => JSX.Element | null
|
||||
prepend?: (...args: any[]) => JSX.Element | null
|
||||
append?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface AutocompleteComponentProps extends Partial<AutocompleteProps> {
|
||||
on?: {
|
||||
select?: (item: any) => void
|
||||
change?: (value: string | number) => void
|
||||
}
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element | null
|
||||
prefix?: (...args: any[]) => JSX.Element | null
|
||||
suffix?: (...args: any[]) => JSX.Element | null
|
||||
prepend?: (...args: any[]) => JSX.Element | null
|
||||
append?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface InputNumberComponentProps extends Partial<InputNumberProps> {
|
||||
on?: {
|
||||
change?: (currentValue: number | undefined, oldValue: number | undefined) => void
|
||||
blur?: (event: FocusEvent) => void
|
||||
focus?: (event: FocusEvent) => void
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
value?: any
|
||||
key?: string | number
|
||||
options?: SelectOption[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface SelectComponentProps extends Omit<Partial<ISelectProps>, 'options'> {
|
||||
/**
|
||||
* 数据源的字段别名
|
||||
*/
|
||||
props?: {
|
||||
key?: string
|
||||
value?: string
|
||||
label?: string
|
||||
children?: string
|
||||
}
|
||||
on?: {
|
||||
change?: (value: string | number | boolean | Object) => void
|
||||
visibleChange?: (visible: boolean) => void
|
||||
removeTag?: (tag: any) => void
|
||||
clear?: () => void
|
||||
blur?: (event: FocusEvent) => void
|
||||
focus?: (event: FocusEvent) => void
|
||||
}
|
||||
slots?: {
|
||||
default?: (options: SelectOption[]) => JSX.Element[] | null
|
||||
optionGroupDefault?: (item: SelectOption) => JSX.Element
|
||||
optionDefault?: (option: SelectOption) => JSX.Element | null
|
||||
prefix?: (...args: any[]) => JSX.Element | null
|
||||
empty?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
options?: SelectOption[]
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface SelectV2ComponentProps {
|
||||
multiple?: boolean
|
||||
disabled?: boolean
|
||||
valueKey?: string
|
||||
size?: ComponentSize
|
||||
clearable?: boolean
|
||||
clearIcon?: string | JSX.Element | null
|
||||
collapseTags?: boolean
|
||||
multipleLimit?: number
|
||||
name?: string
|
||||
effect?: string
|
||||
autocomplete?: string
|
||||
placeholder?: string
|
||||
filterable?: boolean
|
||||
allowCreate?: boolean
|
||||
reserveKeyword?: boolean
|
||||
noDataText?: string
|
||||
popperClass?: string
|
||||
teleported?: boolean
|
||||
persistent?: boolean
|
||||
popperOptions?: any
|
||||
automaticDropdown?: boolean
|
||||
height?: number
|
||||
scrollbarAlwaysOn?: boolean
|
||||
remote?: boolean
|
||||
remoteMethod?: (query: string) => void
|
||||
validateEvent?: boolean
|
||||
placement?: AutocompleteProps['placement']
|
||||
collapseTagsTooltip?: boolean
|
||||
on?: {
|
||||
change?: (value: string | number | boolean | Object) => void
|
||||
visibleChange?: (visible: boolean) => void
|
||||
removeTag?: (tag: any) => void
|
||||
clear?: () => void
|
||||
blur?: (event: FocusEvent) => void
|
||||
focus?: (event: FocusEvent) => void
|
||||
}
|
||||
options?: SelectOption[]
|
||||
slots?: {
|
||||
default?: (option: SelectOption) => JSX.Element | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface CascaderComponentProps {
|
||||
options?: Record<string, unknown>[]
|
||||
props?: CascaderProps
|
||||
size?: ComponentSize
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
clearable?: boolean
|
||||
showAllLevels?: boolean
|
||||
collapseTags?: boolean
|
||||
collapseTagsTooltip?: boolean
|
||||
separator?: string
|
||||
filterable?: boolean
|
||||
filterMethod?: (node: CascaderNode, keyword: string) => boolean
|
||||
debounce?: number
|
||||
beforeFilter?: (value: string) => boolean
|
||||
popperClass?: string
|
||||
teleported?: boolean
|
||||
tagType?: ElementPlusInfoType
|
||||
validateEvent?: boolean
|
||||
on?: {
|
||||
change?: (value: CascaderValue) => void
|
||||
expandChange?: (value: CascaderValue) => void
|
||||
blur?: (event: FocusEvent) => void
|
||||
focus?: (event: FocusEvent) => void
|
||||
visibleChange?: (value: boolean) => void
|
||||
removeTag?: (value: CascaderNode['valueByOption']) => void
|
||||
}
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element | null
|
||||
empty?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface SwitchComponentProps extends Partial<SwitchProps> {
|
||||
on?: {
|
||||
change?: (value: boolean | string | number) => void
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface RateComponentProps extends Partial<RateProps> {
|
||||
on?: {
|
||||
change?: (value: number) => void
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface ColorPickerComponentProps extends Partial<ColorPickerProps> {
|
||||
on?: {
|
||||
change?: (value: string) => void
|
||||
activeChange?: (value: string) => void
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface TransferComponentProps extends Partial<TransferProps> {
|
||||
on?: {
|
||||
change?: (
|
||||
value: number | string,
|
||||
direction: 'left' | 'right',
|
||||
movedKeys: string[] | number[]
|
||||
) => void
|
||||
leftCheckChange?: (value: any[]) => void
|
||||
rightCheckChange?: (value: any[]) => void
|
||||
}
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element | null
|
||||
leftFooter?: (...args: any[]) => JSX.Element | null
|
||||
rightFooter?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface RadioOption {
|
||||
label?: string
|
||||
value?: string | number | boolean
|
||||
disabled?: boolean
|
||||
border?: boolean
|
||||
size?: ComponentSize
|
||||
name?: string
|
||||
[key: string]: any
|
||||
}
|
||||
export interface RadioGroupComponentProps extends Partial<RadioGroupProps> {
|
||||
options?: RadioOption[]
|
||||
/**
|
||||
* 数据源的字段别名
|
||||
*/
|
||||
props?: {
|
||||
label?: string
|
||||
value?: string
|
||||
disabled?: string
|
||||
}
|
||||
on?: {
|
||||
change?: (value: string | number | boolean) => void
|
||||
}
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element[] | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface RadioButtonComponentProps extends Partial<RadioButtonProps> {
|
||||
options?: RadioOption[]
|
||||
/**
|
||||
* 数据源的字段别名
|
||||
*/
|
||||
props?: {
|
||||
label?: string
|
||||
value?: string
|
||||
disabled?: string
|
||||
}
|
||||
on?: {
|
||||
change?: (value: string | number | boolean) => void
|
||||
}
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element[] | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface CheckboxOption {
|
||||
label?: string
|
||||
value?: string | number | boolean
|
||||
disabled?: boolean
|
||||
trueLabel?: string | number
|
||||
falseLabel?: string | number
|
||||
border?: boolean
|
||||
size?: ComponentSize
|
||||
name?: string
|
||||
checked?: boolean
|
||||
indeterminate?: boolean
|
||||
validateEvent?: boolean
|
||||
tabindex?: number | string
|
||||
id?: string
|
||||
controls?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface CheckboxGroupComponentProps extends Partial<CheckboxGroupProps> {
|
||||
options?: CheckboxOption[]
|
||||
/**
|
||||
* 数据源的字段别名
|
||||
*/
|
||||
props?: {
|
||||
label?: string
|
||||
value?: string
|
||||
disabled?: string
|
||||
}
|
||||
on?: {
|
||||
change?: (value: string | number | boolean) => void
|
||||
}
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element[] | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface DividerComponentProps extends Partial<DividerProps> {
|
||||
on?: {
|
||||
change?: (value: number) => void
|
||||
input?: (value: number) => void
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface DatePickerComponentProps extends Partial<DatePickerProps> {
|
||||
on?: {
|
||||
change?: (value: string | Date | number | string[]) => void
|
||||
blur?: (event: FocusEvent) => void
|
||||
focus?: (event: FocusEvent) => void
|
||||
calendarChange?: (val: [Date, Date]) => void
|
||||
panelChange?: (date, mode, view) => void
|
||||
visibleChange?: (visibility: boolean) => void
|
||||
}
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element | null
|
||||
rangeSeparator?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface DateTimePickerComponentProps {
|
||||
readonly?: boolean
|
||||
disabled?: boolean
|
||||
editable?: boolean
|
||||
clearable?: boolean
|
||||
size?: ComponentSize
|
||||
placeholder?: string
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
timeArrowControl?: boolean
|
||||
type?: 'year' | 'month' | 'date' | 'datetime' | 'datetimerange' | 'daterange' | 'week'
|
||||
format?: string
|
||||
popperClass?: string
|
||||
rangeSeparator?: string
|
||||
defaultValue?: Date | [Date, Date]
|
||||
defaultTime?: Date | [Date, Date]
|
||||
valueFormat?: string
|
||||
id?: string
|
||||
name?: string
|
||||
unlinkPanels?: boolean
|
||||
prefixIcon?: string | JSX.Element
|
||||
clearIcon?: string | JSX.Element
|
||||
shortcuts?: Array<{ text: string; value: Date | Function }>
|
||||
disabledDate?: (date: Date) => boolean
|
||||
cellClassName?: string | ((date: Date) => string | undefined)
|
||||
teleported?: boolean
|
||||
on?: {
|
||||
change?: (value: string | Date | number | string[]) => void
|
||||
blur?: (event: FocusEvent) => void
|
||||
focus?: (event: FocusEvent) => void
|
||||
calendarChange?: (val: [Date, Date]) => void
|
||||
visibleChange?: (visibility: boolean) => void
|
||||
}
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element | null
|
||||
rangeSeparator?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface TimePickerComponentProps {
|
||||
readonly?: boolean
|
||||
disabled?: boolean
|
||||
editable?: boolean
|
||||
clearable?: boolean
|
||||
size?: ComponentSize
|
||||
placeholder?: string
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
isRange?: boolean
|
||||
arrowControl?: boolean
|
||||
popperClass?: string
|
||||
rangeSeparator?: string
|
||||
format?: string
|
||||
defaultValue?: Date | [Date, Date]
|
||||
id?: string
|
||||
name?: string
|
||||
label?: string
|
||||
prefixIcon?: string | JSX.Element
|
||||
clearIcon?: string | JSX.Element
|
||||
disabledHours?: (role: string, comparingDate?: any) => number[]
|
||||
disabledMinutes?: (hour: number, role: string, comparingDate?: any) => number[]
|
||||
disabledSeconds?: (hour: number, minute: number, role: string, comparingDate?: any) => number[]
|
||||
teleported?: boolean
|
||||
tabindex?: number | string
|
||||
on?: {
|
||||
change: (
|
||||
val: number | string | Date | [number, number] | [string, string] | [Date, Date]
|
||||
) => void
|
||||
blur?: (event: FocusEvent) => void
|
||||
focus?: (event: FocusEvent) => void
|
||||
visibleChange?: (visibility: boolean) => void
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface TimeSelectComponentProps {
|
||||
disabled?: boolean
|
||||
editable?: boolean
|
||||
clearable?: boolean
|
||||
size?: ComponentSize
|
||||
placeholder?: string
|
||||
name?: string
|
||||
effect?: string
|
||||
prefixIcon?: string | JSX.Element
|
||||
clearIcon?: string | JSX.Element
|
||||
start?: string
|
||||
end?: string
|
||||
step?: string
|
||||
minTime?: string
|
||||
maxTime?: string
|
||||
format?: string
|
||||
on?: {
|
||||
change?: (val: string) => void
|
||||
blur?: (event: FocusEvent) => void
|
||||
focus?: (event: FocusEvent) => void
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface EditorComponentProps {
|
||||
editorConfig?: IEditorConfig
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface ColProps {
|
||||
span?: number
|
||||
xs?: number
|
||||
sm?: number
|
||||
md?: number
|
||||
lg?: number
|
||||
xl?: number
|
||||
tag?: string
|
||||
}
|
||||
|
||||
export interface FormSetProps {
|
||||
field: string
|
||||
path: string
|
||||
value: any
|
||||
}
|
||||
|
||||
export interface FormItemProps extends Partial<ElFormItemProps> {
|
||||
style?: CSSProperties
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element | null
|
||||
label?: (...args: any[]) => JSX.Element | null
|
||||
error?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface UploadComponentProps extends Partial<UploadProps> {
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element | null
|
||||
trigger?: (...args: any[]) => JSX.Element | null
|
||||
tip?: (...args: any[]) => JSX.Element | null
|
||||
file?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface TreeSelectComponentProps extends Omit<
|
||||
Partial<SelectComponentProps>,
|
||||
'props' | 'on' | 'slots'
|
||||
> {
|
||||
data?: any[]
|
||||
emptyText?: string
|
||||
nodeKey?: string
|
||||
props?: {
|
||||
children?: string
|
||||
label?: string | ((...args: any[]) => string)
|
||||
disabled?: string | ((...args: any[]) => string)
|
||||
isLeaf?: string | ((...args: any[]) => string)
|
||||
class?: string | ((...args: any[]) => string)
|
||||
}
|
||||
renderAfterExpand?: boolean
|
||||
load?: (...args: any[]) => Promise<any>
|
||||
renderContent?: (...args: any[]) => JSX.Element | null
|
||||
highlightCurrent?: boolean
|
||||
defaultExpandAll?: boolean
|
||||
expandOnClickNode?: boolean
|
||||
checkOnClickNode?: boolean
|
||||
autoExpandParent?: boolean
|
||||
defaultExpandedKeys?: any[]
|
||||
showCheckbox?: boolean
|
||||
checkStrictly?: boolean
|
||||
defaultCheckedKeys?: any[]
|
||||
currentNodeKey?: string | number
|
||||
filterNodeMethod?: (...args: any[]) => boolean
|
||||
accordion?: boolean
|
||||
indent?: number
|
||||
icon?: string | ((...args: any[]) => JSX.Element | null)
|
||||
lazy?: boolean
|
||||
draggable?: boolean
|
||||
allowDrag?: (...args: any[]) => boolean
|
||||
allowDrop?: (...args: any[]) => boolean
|
||||
on?: {
|
||||
change?: (value: string | number | boolean | Object) => void
|
||||
visibleChange?: (visible: boolean) => void
|
||||
removeTag?: (tag: any) => void
|
||||
clear?: () => void
|
||||
blur?: (event: FocusEvent) => void
|
||||
focus?: (event: FocusEvent) => void
|
||||
nodeClick?: (...args: any[]) => void
|
||||
nodeContextMenu?: (...args: any[]) => void
|
||||
checkChange?: (...args: any[]) => void
|
||||
check?: (...args: any[]) => void
|
||||
currentChange?: (...args: any[]) => void
|
||||
nodeExpand?: (...args: any[]) => void
|
||||
nodeCollapse?: (...args: any[]) => void
|
||||
nodeDragStart?: (...args: any[]) => void
|
||||
nodeDragEnter?: (...args: any[]) => void
|
||||
nodeDragLeave?: (...args: any[]) => void
|
||||
nodeDragOver?: (...args: any[]) => void
|
||||
nodeDragEnd?: (...args: any[]) => void
|
||||
nodeDrop?: (...args: any[]) => void
|
||||
}
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element | null
|
||||
optionGroupDefault?: (item: SelectOption) => JSX.Element
|
||||
optionDefault?: (option: SelectOption) => JSX.Element | null
|
||||
prefix?: (...args: any[]) => JSX.Element | null
|
||||
empty?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export interface FormSchema {
|
||||
/**
|
||||
* 唯一标识
|
||||
*/
|
||||
field: string
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
label?: string
|
||||
|
||||
/**
|
||||
* col组件属性
|
||||
*/
|
||||
colProps?: ColProps
|
||||
|
||||
/**
|
||||
* 表单组件属性,具体可以查看element-plus文档
|
||||
*/
|
||||
componentProps?:
|
||||
| InputComponentProps
|
||||
| AutocompleteComponentProps
|
||||
| InputNumberComponentProps
|
||||
| SelectComponentProps
|
||||
| SelectV2ComponentProps
|
||||
| CascaderComponentProps
|
||||
| SwitchComponentProps
|
||||
| RateComponentProps
|
||||
| ColorPickerComponentProps
|
||||
| TransferComponentProps
|
||||
| RadioGroupComponentProps
|
||||
| RadioButtonComponentProps
|
||||
| DividerComponentProps
|
||||
| DatePickerComponentProps
|
||||
| DateTimePickerComponentProps
|
||||
| TimePickerComponentProps
|
||||
| InputPasswordComponentProps
|
||||
| TreeSelectComponentProps
|
||||
| UploadComponentProps
|
||||
| any
|
||||
|
||||
/**
|
||||
* formItem组件属性,具体可以查看element-plus文档
|
||||
*/
|
||||
formItemProps?: FormItemProps
|
||||
|
||||
/**
|
||||
* 渲染的组件名称
|
||||
*/
|
||||
component?: ComponentName
|
||||
|
||||
/**
|
||||
* 初始值
|
||||
*/
|
||||
value?: any
|
||||
|
||||
/**
|
||||
* 是否隐藏,如果为true,会连同值一同删除,类似v-if
|
||||
*/
|
||||
remove?: boolean
|
||||
|
||||
/**
|
||||
* 样式隐藏,不会把值一同删掉,类似v-show
|
||||
*/
|
||||
hidden?: boolean
|
||||
|
||||
/**
|
||||
* @returns 远程加载下拉项
|
||||
*/
|
||||
optionApi?: any
|
||||
}
|
||||
|
||||
export interface FormProps extends Partial<ElFormProps> {
|
||||
schema?: FormSchema[]
|
||||
isCol?: boolean
|
||||
model?: Recordable
|
||||
autoSetPlaceholder?: boolean
|
||||
isCustom?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Icon from './src/Icon.vue'
|
||||
|
||||
export type { IconTypes } from './src/types'
|
||||
|
||||
export { Icon }
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, unref } from 'vue'
|
||||
import { ElIcon } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('icon')
|
||||
|
||||
const props = defineProps({
|
||||
// icon name
|
||||
icon: propTypes.string,
|
||||
// icon color
|
||||
color: propTypes.string,
|
||||
// icon size
|
||||
size: propTypes.number.def(16),
|
||||
hoverColor: propTypes.string
|
||||
})
|
||||
|
||||
const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
|
||||
|
||||
const symbolId = computed(() => {
|
||||
return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon
|
||||
})
|
||||
|
||||
// 是否使用在线图标
|
||||
const isUseOnline = computed(() => {
|
||||
return import.meta.env.VITE_USE_ONLINE_ICON === 'true'
|
||||
})
|
||||
|
||||
const getIconifyStyle = computed(() => {
|
||||
const { color, size } = props
|
||||
return {
|
||||
fontSize: `${size}px`,
|
||||
color
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElIcon :class="prefixCls" :size="size" :color="color">
|
||||
<svg v-if="isLocal" aria-hidden="true">
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
|
||||
<template v-else>
|
||||
<Icon v-if="isUseOnline" :icon="icon" :style="getIconifyStyle" />
|
||||
<div v-else :class="`${icon} iconify`" :style="getIconifyStyle"></div>
|
||||
</template>
|
||||
</ElIcon>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-icon';
|
||||
|
||||
.@{prefix-cls},
|
||||
.iconify {
|
||||
:deep(svg) {
|
||||
&:hover {
|
||||
// stylelint-disable-next-line
|
||||
color: v-bind(hoverColor) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
&:hover {
|
||||
// stylelint-disable-next-line
|
||||
color: v-bind(hoverColor) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface IconTypes {
|
||||
size?: number
|
||||
color?: string
|
||||
icon: string
|
||||
hoverColor?: string
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import InputPassword from './src/InputPassword.vue'
|
||||
|
||||
export { InputPassword }
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, unref, computed, watch } from 'vue'
|
||||
import { ElInput } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useConfigGlobal } from '@/hooks/web/useConfigGlobal'
|
||||
import { zxcvbn } from '@zxcvbn-ts/core'
|
||||
import type { ZxcvbnResult } from '@zxcvbn-ts/core'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('input-password')
|
||||
|
||||
const props = defineProps({
|
||||
// 是否显示密码强度
|
||||
strength: propTypes.bool.def(false),
|
||||
modelValue: propTypes.string.def('')
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: string) => {
|
||||
if (val === unref(valueRef)) return
|
||||
valueRef.value = val
|
||||
}
|
||||
)
|
||||
|
||||
const { configGlobal } = useConfigGlobal()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 设置input的type属性
|
||||
const textType = ref<'password' | 'text'>('password')
|
||||
|
||||
// 输入框的值
|
||||
const valueRef = ref(props.modelValue)
|
||||
|
||||
// 监听
|
||||
watch(
|
||||
() => valueRef.value,
|
||||
(val: string) => {
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
)
|
||||
|
||||
// 获取密码强度
|
||||
const getPasswordStrength = computed(() => {
|
||||
const value = unref(valueRef)
|
||||
const zxcvbnRef = zxcvbn(unref(valueRef)) as ZxcvbnResult
|
||||
return value ? zxcvbnRef.score : -1
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[prefixCls, `${prefixCls}--${configGlobal?.size}`]">
|
||||
<ElInput v-bind="$attrs" v-model="valueRef" showPassword :type="textType" />
|
||||
<div
|
||||
v-if="strength"
|
||||
:class="`${prefixCls}__bar`"
|
||||
class="relative h-6px mt-10px mb-6px mr-auto ml-auto"
|
||||
>
|
||||
<div :class="`${prefixCls}__bar--fill`" :data-score="getPasswordStrength"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-input-password';
|
||||
|
||||
.@{prefix-cls} {
|
||||
:deep(.@{elNamespace}-input__clear) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
background-color: var(--el-text-color-disabled);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: block;
|
||||
width: 20%;
|
||||
height: inherit;
|
||||
background-color: transparent;
|
||||
border-color: var(--el-color-white);
|
||||
border-style: solid;
|
||||
border-width: 0 5px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 20%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: 20%;
|
||||
}
|
||||
|
||||
&--fill {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: inherit;
|
||||
background-color: transparent;
|
||||
border-radius: inherit;
|
||||
transition:
|
||||
width 0.5s ease-in-out,
|
||||
background 0.25s;
|
||||
|
||||
&[data-score='0'] {
|
||||
width: 20%;
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&[data-score='1'] {
|
||||
width: 40%;
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&[data-score='2'] {
|
||||
width: 60%;
|
||||
background-color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
&[data-score='3'] {
|
||||
width: 80%;
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&[data-score='4'] {
|
||||
width: 100%;
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--mini > &__bar {
|
||||
border-radius: var(--el-border-radius-small);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import LocaleDropdown from './src/LocaleDropdown.vue'
|
||||
|
||||
export type { Language, LocaleDropdownType } from './src/types'
|
||||
|
||||
export { LocaleDropdown }
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, unref } from 'vue'
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||
import { useLocaleStore } from '@/store/modules/locale'
|
||||
import { useLocale } from '@/hooks/web/useLocale'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('locale-dropdown')
|
||||
|
||||
defineProps({
|
||||
color: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const localeStore = useLocaleStore()
|
||||
|
||||
const langMap = computed(() => localeStore.getLocaleMap)
|
||||
|
||||
const currentLang = computed(() => localeStore.getCurrentLocale)
|
||||
|
||||
const setLang = (lang: LocaleType) => {
|
||||
if (lang === unref(currentLang).lang) return
|
||||
// 需要重新加载页面让整个语言多初始化
|
||||
window.location.reload()
|
||||
localeStore.setCurrentLocale({
|
||||
lang
|
||||
})
|
||||
const { changeLocale } = useLocale()
|
||||
changeLocale(lang)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDropdown :class="prefixCls" trigger="click" @command="setLang">
|
||||
<Icon
|
||||
:size="18"
|
||||
icon="ion:language-sharp"
|
||||
class="cursor-pointer !p-0"
|
||||
:class="$attrs.class"
|
||||
:color="color"
|
||||
/>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem v-for="item in langMap" :key="item.lang" :command="item.lang">
|
||||
{{ item.name }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface Language {
|
||||
el: Recordable
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface LocaleDropdownType {
|
||||
lang: LocaleType
|
||||
name?: string
|
||||
elLocale?: Language
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Logo from './src/Logo.vue'
|
||||
|
||||
export { Logo }
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, onMounted, unref } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('logo')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const show = ref(true)
|
||||
|
||||
const title = computed(() => appStore.getTitle)
|
||||
|
||||
const layout = computed(() => appStore.getLayout)
|
||||
|
||||
const collapse = computed(() => appStore.getCollapse)
|
||||
|
||||
onMounted(() => {
|
||||
if (unref(collapse)) show.value = false
|
||||
})
|
||||
|
||||
watch(
|
||||
() => collapse.value,
|
||||
(collapse: boolean) => {
|
||||
if (unref(layout) === 'topLeft' || unref(layout) === 'cutMenu') {
|
||||
show.value = true
|
||||
return
|
||||
}
|
||||
if (!collapse) {
|
||||
setTimeout(() => {
|
||||
show.value = !collapse
|
||||
}, 400)
|
||||
} else {
|
||||
show.value = !collapse
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => layout.value,
|
||||
(layout) => {
|
||||
if (layout === 'top' || layout === 'cutMenu') {
|
||||
show.value = true
|
||||
} else {
|
||||
if (unref(collapse)) {
|
||||
show.value = false
|
||||
} else {
|
||||
show.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<router-link
|
||||
:class="[
|
||||
prefixCls,
|
||||
layout !== 'classic' ? `${prefixCls}__Top` : '',
|
||||
'flex !h-[var(--logo-height)] items-center cursor-pointer pl-8px relative decoration-none overflow-hidden'
|
||||
]"
|
||||
to="/"
|
||||
>
|
||||
<img
|
||||
src="@/assets/imgs/logo.png"
|
||||
class="w-[calc(var(--logo-height)-10px)] h-[calc(var(--logo-height)-10px)]"
|
||||
/>
|
||||
<div
|
||||
v-if="show"
|
||||
:class="[
|
||||
'ml-10px text-16px font-700',
|
||||
{
|
||||
'text-[var(--logo-title-text-color)]': layout === 'classic',
|
||||
'text-[var(--top-header-text-color)]':
|
||||
layout === 'topLeft' || layout === 'top' || layout === 'cutMenu'
|
||||
}
|
||||
]"
|
||||
>
|
||||
{{ title }}
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
import Menu from './src/Menu.vue'
|
||||
|
||||
export { Menu }
|
||||
@@ -0,0 +1,260 @@
|
||||
<script lang="tsx">
|
||||
import { computed, defineComponent, unref, PropType } from 'vue'
|
||||
import { ElMenu, ElScrollbar } from 'element-plus'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { useRenderMenuItem } from './components/useRenderMenuItem'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { isUrl } from '@/utils/is'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('menu')
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Menu',
|
||||
props: {
|
||||
menuSelect: {
|
||||
type: Function as PropType<(index: string) => void>,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const appStore = useAppStore()
|
||||
|
||||
const layout = computed(() => appStore.getLayout)
|
||||
|
||||
const { push, currentRoute } = useRouter()
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const menuMode = computed((): 'vertical' | 'horizontal' => {
|
||||
// 竖
|
||||
const vertical: LayoutType[] = ['classic', 'topLeft', 'cutMenu']
|
||||
|
||||
if (vertical.includes(unref(layout))) {
|
||||
return 'vertical'
|
||||
} else {
|
||||
return 'horizontal'
|
||||
}
|
||||
})
|
||||
|
||||
const routers = computed(() =>
|
||||
unref(layout) === 'cutMenu' ? permissionStore.getMenuTabRouters : permissionStore.getRouters
|
||||
)
|
||||
|
||||
const collapse = computed(() => appStore.getCollapse)
|
||||
|
||||
const uniqueOpened = computed(() => appStore.getUniqueOpened)
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = unref(currentRoute)
|
||||
// if set path, the sidebar will highlight the path you set
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu as string
|
||||
}
|
||||
return path
|
||||
})
|
||||
|
||||
const menuSelect = (index: string) => {
|
||||
if (props.menuSelect) {
|
||||
props.menuSelect(index)
|
||||
}
|
||||
// 自定义事件
|
||||
if (isUrl(index)) {
|
||||
window.open(index)
|
||||
} else {
|
||||
push(index)
|
||||
}
|
||||
}
|
||||
|
||||
const renderMenuWrap = () => {
|
||||
if (unref(layout) === 'top') {
|
||||
return renderMenu()
|
||||
} else {
|
||||
return <ElScrollbar>{renderMenu()}</ElScrollbar>
|
||||
}
|
||||
}
|
||||
|
||||
const renderMenu = () => {
|
||||
return (
|
||||
<ElMenu
|
||||
defaultActive={unref(activeMenu)}
|
||||
mode={unref(menuMode)}
|
||||
collapse={
|
||||
unref(layout) === 'top' || unref(layout) === 'cutMenu' ? false : unref(collapse)
|
||||
}
|
||||
uniqueOpened={unref(layout) === 'top' ? false : unref(uniqueOpened)}
|
||||
backgroundColor="var(--left-menu-bg-color)"
|
||||
textColor="var(--left-menu-text-color)"
|
||||
activeTextColor="var(--left-menu-text-active-color)"
|
||||
popperClass={
|
||||
unref(menuMode) === 'vertical'
|
||||
? `${prefixCls}-popper--vertical`
|
||||
: `${prefixCls}-popper--horizontal`
|
||||
}
|
||||
onSelect={menuSelect}
|
||||
>
|
||||
{{
|
||||
default: () => {
|
||||
const { renderMenuItem } = useRenderMenuItem()
|
||||
return renderMenuItem(unref(routers))
|
||||
}
|
||||
}}
|
||||
</ElMenu>
|
||||
)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div
|
||||
id={prefixCls}
|
||||
class={[
|
||||
`${prefixCls} ${prefixCls}__${unref(menuMode)}`,
|
||||
'h-[100%] overflow-hidden flex-col bg-[var(--left-menu-bg-color)]',
|
||||
{
|
||||
'w-[var(--left-menu-min-width)]': unref(collapse) && unref(layout) !== 'cutMenu',
|
||||
'w-[var(--left-menu-max-width)]': !unref(collapse) && unref(layout) !== 'cutMenu'
|
||||
}
|
||||
]}
|
||||
>
|
||||
{renderMenuWrap()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-menu';
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: relative;
|
||||
transition: width var(--transition-time-02);
|
||||
|
||||
:deep(.@{elNamespace}-menu) {
|
||||
width: 100% !important;
|
||||
border-right: none;
|
||||
|
||||
// 设置选中时子标题的颜色
|
||||
.is-active {
|
||||
& > .@{elNamespace}-sub-menu__title {
|
||||
color: var(--left-menu-text-active-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置子菜单悬停的高亮和背景色
|
||||
.@{elNamespace}-sub-menu__title,
|
||||
.@{elNamespace}-menu-item {
|
||||
&:hover {
|
||||
color: var(--left-menu-text-active-color) !important;
|
||||
background-color: var(--left-menu-bg-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置选中时的高亮背景和高亮颜色
|
||||
.@{elNamespace}-menu-item.is-active {
|
||||
color: var(--left-menu-text-active-color) !important;
|
||||
background-color: var(--left-menu-bg-active-color) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--left-menu-bg-active-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.@{elNamespace}-menu-item.is-active {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 设置子菜单的背景颜色
|
||||
.@{elNamespace}-menu {
|
||||
.@{elNamespace}-sub-menu__title,
|
||||
.@{elNamespace}-menu-item:not(.is-active) {
|
||||
background-color: var(--left-menu-bg-light-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠时的最小宽度
|
||||
:deep(.@{elNamespace}-menu--collapse) {
|
||||
width: var(--left-menu-min-width);
|
||||
|
||||
& > .is-active,
|
||||
& > .is-active > .@{elNamespace}-sub-menu__title {
|
||||
position: relative;
|
||||
background-color: var(--left-menu-collapse-bg-active-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠动画的时候,就需要把文字给隐藏掉
|
||||
:deep(.horizontal-collapse-transition) {
|
||||
.@{prefix-cls}__title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 水平菜单
|
||||
&__horizontal {
|
||||
height: calc(~'var(--top-tool-height)') !important;
|
||||
|
||||
:deep(.@{elNamespace}-menu--horizontal) {
|
||||
height: calc(~'var(--top-tool-height)');
|
||||
border-bottom: none;
|
||||
// 重新设置底部高亮颜色
|
||||
& > .@{elNamespace}-sub-menu.is-active {
|
||||
.@{elNamespace}-sub-menu__title {
|
||||
border-bottom-color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.@{elNamespace}-menu-item.is-active {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls}__title {
|
||||
/* stylelint-disable-next-line */
|
||||
max-height: calc(~'var(--top-tool-height) - 2px') !important;
|
||||
/* stylelint-disable-next-line */
|
||||
line-height: calc(~'var(--top-tool-height) - 2px');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-menu-popper';
|
||||
|
||||
.@{prefix-cls}--vertical,
|
||||
.@{prefix-cls}--horizontal {
|
||||
// 设置选中时子标题的颜色
|
||||
.is-active {
|
||||
& > .el-sub-menu__title {
|
||||
color: var(--left-menu-text-active-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置子菜单悬停的高亮和背景色
|
||||
.el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
color: var(--left-menu-text-active-color) !important;
|
||||
background-color: var(--left-menu-bg-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置选中时的高亮背景
|
||||
.el-menu-item.is-active {
|
||||
position: relative;
|
||||
background-color: var(--left-menu-bg-active-color) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--left-menu-bg-active-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ElSubMenu, ElMenuItem } from 'element-plus'
|
||||
import { hasOneShowingChild } from '../helper'
|
||||
import { isUrl } from '@/utils/is'
|
||||
import { useRenderMenuTitle } from './useRenderMenuTitle'
|
||||
import { pathResolve } from '@/utils/routerHelper'
|
||||
|
||||
const { renderMenuTitle } = useRenderMenuTitle()
|
||||
|
||||
export const useRenderMenuItem = () =>
|
||||
// allRouters: AppRouteRecordRaw[] = [],
|
||||
{
|
||||
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
|
||||
return routers
|
||||
.filter((v) => !v.meta?.hidden)
|
||||
.map((v) => {
|
||||
const meta = v.meta ?? {}
|
||||
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
|
||||
const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
|
||||
|
||||
if (
|
||||
oneShowingChild &&
|
||||
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
|
||||
!meta?.alwaysShow
|
||||
) {
|
||||
return (
|
||||
<ElMenuItem
|
||||
index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
|
||||
>
|
||||
{{
|
||||
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
|
||||
}}
|
||||
</ElMenuItem>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<ElSubMenu index={fullPath}>
|
||||
{{
|
||||
title: () => renderMenuTitle(meta),
|
||||
default: () => renderMenuItem(v.children!, fullPath)
|
||||
}}
|
||||
</ElSubMenu>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
renderMenuItem
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
|
||||
export const useRenderMenuTitle = () => {
|
||||
const renderMenuTitle = (meta: RouteMeta) => {
|
||||
const { t } = useI18n()
|
||||
const { title = 'Please set title', icon } = meta
|
||||
|
||||
return icon ? (
|
||||
<>
|
||||
<Icon icon={meta.icon}></Icon>
|
||||
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{t(title as string)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{t(title as string)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
renderMenuTitle
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ref, unref } from 'vue'
|
||||
import { findPath } from '@/utils/tree'
|
||||
|
||||
type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean }
|
||||
|
||||
interface HasOneShowingChild {
|
||||
oneShowingChild?: boolean
|
||||
onlyOneChild?: OnlyOneChildType
|
||||
}
|
||||
|
||||
export const getAllParentPath = <T = Recordable>(treeData: T[], path: string) => {
|
||||
const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[]
|
||||
return (menuList || []).map((item) => item.path)
|
||||
}
|
||||
|
||||
export const hasOneShowingChild = (
|
||||
children: AppRouteRecordRaw[] = [],
|
||||
parent: AppRouteRecordRaw
|
||||
): HasOneShowingChild => {
|
||||
const onlyOneChild = ref<OnlyOneChildType>()
|
||||
|
||||
const showingChildren = children.filter((v) => {
|
||||
const meta = v.meta ?? {}
|
||||
if (meta.hidden) {
|
||||
return false
|
||||
} else {
|
||||
// Temp set(will be used if only has one showing child)
|
||||
onlyOneChild.value = v
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// When there is only one child router, the child router is displayed by default
|
||||
if (showingChildren.length === 1) {
|
||||
return {
|
||||
oneShowingChild: true,
|
||||
onlyOneChild: unref(onlyOneChild)
|
||||
}
|
||||
}
|
||||
|
||||
// Show parent if there are no child router to display
|
||||
if (!showingChildren.length) {
|
||||
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
|
||||
return {
|
||||
oneShowingChild: true,
|
||||
onlyOneChild: unref(onlyOneChild)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oneShowingChild: false,
|
||||
onlyOneChild: unref(onlyOneChild)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Screenfull from './src/Screenfull.vue'
|
||||
|
||||
export { Screenfull }
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('screenfull')
|
||||
|
||||
defineProps({
|
||||
color: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const { toggle, isFullscreen } = useFullscreen()
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
toggle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls" @click="toggleFullscreen">
|
||||
<Icon
|
||||
:size="18"
|
||||
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
|
||||
:color="color"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FormSchema, FormSetProps } from '../Form'
|
||||
import Search from './src/Search.vue'
|
||||
|
||||
export type { SearchProps } from './src/types'
|
||||
|
||||
export interface SearchExpose {
|
||||
setValues: (data: Recordable) => void
|
||||
setProps: (props: Recordable) => void
|
||||
delSchema: (field: string) => void
|
||||
addSchema: (formSchema: FormSchema, index?: number) => void
|
||||
setSchema: (schemaProps: FormSetProps[]) => void
|
||||
formModel: Recordable
|
||||
}
|
||||
|
||||
export { Search }
|
||||
@@ -0,0 +1,269 @@
|
||||
<script setup lang="tsx">
|
||||
import { Form, FormSchema, FormSetProps } from '@/components/Form'
|
||||
import { PropType, computed, unref, ref, watch, onMounted } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { findIndex } from '@/utils'
|
||||
import { cloneDeep, set } from 'lodash-es'
|
||||
import { initModel } from '@/components/Form/src/helper'
|
||||
import ActionButton from './components/ActionButton.vue'
|
||||
import { SearchProps } from './types'
|
||||
import { FormItemProp } from 'element-plus'
|
||||
import { isObject, isEmptyVal } from '@/utils/is'
|
||||
|
||||
const props = defineProps({
|
||||
// 生成Form的布局结构数组
|
||||
schema: {
|
||||
type: Array as PropType<FormSchema[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 是否需要栅格布局
|
||||
isCol: propTypes.bool.def(false),
|
||||
// 表单label宽度
|
||||
labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
|
||||
// 操作按钮风格位置
|
||||
layout: propTypes.string.validate((v: string) => ['inline', 'bottom'].includes(v)).def('inline'),
|
||||
// 底部按钮的对齐方式
|
||||
buttonPosition: propTypes.string
|
||||
.validate((v: string) => ['left', 'center', 'right'].includes(v))
|
||||
.def('center'),
|
||||
showSearch: propTypes.bool.def(true),
|
||||
showReset: propTypes.bool.def(true),
|
||||
// 是否显示伸缩
|
||||
showExpand: propTypes.bool.def(false),
|
||||
// 伸缩的界限字段
|
||||
expandField: propTypes.string.def(''),
|
||||
inline: propTypes.bool.def(true),
|
||||
// 是否去除空值项
|
||||
removeNoValueItem: propTypes.bool.def(true),
|
||||
model: {
|
||||
type: Object as PropType<Recordable>,
|
||||
default: () => ({})
|
||||
},
|
||||
searchLoading: propTypes.bool.def(false),
|
||||
resetLoading: propTypes.bool.def(false)
|
||||
})
|
||||
|
||||
const emit = defineEmits(['search', 'reset', 'register', 'validate'])
|
||||
|
||||
const visible = ref(true)
|
||||
|
||||
// 表单数据
|
||||
const formModel = ref<Recordable>(props.model)
|
||||
|
||||
const newSchema = computed(() => {
|
||||
const propsComputed = unref(getProps)
|
||||
let schema: FormSchema[] = cloneDeep(propsComputed.schema)
|
||||
if (propsComputed.showExpand && propsComputed.expandField && !unref(visible)) {
|
||||
const index = findIndex(schema, (v: FormSchema) => v.field === propsComputed.expandField)
|
||||
schema.map((v, i) => {
|
||||
if (i >= index) {
|
||||
v.hidden = true
|
||||
} else {
|
||||
v.hidden = false
|
||||
}
|
||||
return v
|
||||
})
|
||||
}
|
||||
if (propsComputed.layout === 'inline') {
|
||||
schema = schema.concat([
|
||||
{
|
||||
field: 'action',
|
||||
formItemProps: {
|
||||
labelWidth: '0px',
|
||||
slots: {
|
||||
default: () => {
|
||||
return (
|
||||
<div>
|
||||
<ActionButton
|
||||
showSearch={propsComputed.showSearch}
|
||||
showReset={propsComputed.showReset}
|
||||
showExpand={propsComputed.showExpand}
|
||||
searchLoading={propsComputed.searchLoading}
|
||||
resetLoading={propsComputed.resetLoading}
|
||||
visible={visible.value}
|
||||
onExpand={setVisible}
|
||||
onReset={reset}
|
||||
onSearch={search}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
label: () => {
|
||||
return <span> </span>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
return schema
|
||||
})
|
||||
|
||||
const { formRegister, formMethods } = useForm()
|
||||
const { getElFormExpose, getFormData, getFormExpose } = formMethods
|
||||
|
||||
// useSearch传入的props
|
||||
const outsideProps = ref<SearchProps>({})
|
||||
|
||||
const mergeProps = ref<SearchProps>({})
|
||||
|
||||
const getProps = computed(() => {
|
||||
const propsObj = { ...props }
|
||||
Object.assign(propsObj, unref(mergeProps))
|
||||
return propsObj
|
||||
})
|
||||
|
||||
const setProps = (props: SearchProps = {}) => {
|
||||
mergeProps.value = Object.assign(unref(mergeProps), props)
|
||||
// @ts-ignore
|
||||
outsideProps.value = props
|
||||
}
|
||||
|
||||
const schemaRef = ref<FormSchema[]>([])
|
||||
|
||||
// 监听表单结构化数组,重新生成formModel
|
||||
watch(
|
||||
() => unref(newSchema),
|
||||
async (schema = []) => {
|
||||
formModel.value = initModel(schema, unref(formModel))
|
||||
schemaRef.value = schema
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
const filterModel = async () => {
|
||||
const model = await getFormData()
|
||||
if (unref(getProps).removeNoValueItem) {
|
||||
// 使用reduce过滤空值,并返回一个新对象
|
||||
return Object.keys(model).reduce((prev, next) => {
|
||||
const value = model[next]
|
||||
if (!isEmptyVal(value)) {
|
||||
if (isObject(value)) {
|
||||
if (Object.keys(value).length > 0) {
|
||||
prev[next] = value
|
||||
}
|
||||
} else {
|
||||
prev[next] = value
|
||||
}
|
||||
}
|
||||
return prev
|
||||
}, {})
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
const search = async () => {
|
||||
const elFormExpose = await getElFormExpose()
|
||||
await elFormExpose?.validate(async (isValid) => {
|
||||
if (isValid) {
|
||||
const model = await filterModel()
|
||||
emit('search', model)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const reset = async () => {
|
||||
const elFormExpose = await getElFormExpose()
|
||||
elFormExpose?.resetFields()
|
||||
const model = await filterModel()
|
||||
emit('reset', model)
|
||||
}
|
||||
|
||||
const bottomButtonStyle = computed(() => {
|
||||
return {
|
||||
textAlign: unref(getProps).buttonPosition as unknown as 'left' | 'center' | 'right'
|
||||
}
|
||||
})
|
||||
|
||||
const setVisible = async () => {
|
||||
visible.value = !unref(visible)
|
||||
}
|
||||
|
||||
const setSchema = (schemaProps: FormSetProps[]) => {
|
||||
const { schema } = unref(getProps)
|
||||
for (const v of schema) {
|
||||
for (const item of schemaProps) {
|
||||
if (v.field === item.field) {
|
||||
set(v, item.path, item.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 对表单赋值
|
||||
const setValues = async (data: Recordable = {}) => {
|
||||
formModel.value = Object.assign(props.model, unref(formModel), data)
|
||||
const formExpose = await getFormExpose()
|
||||
formExpose?.setValues(data)
|
||||
}
|
||||
|
||||
const delSchema = (field: string) => {
|
||||
const { schema } = unref(getProps)
|
||||
|
||||
const index = findIndex(schema, (v: FormSchema) => v.field === field)
|
||||
if (index > -1) {
|
||||
schema.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const addSchema = (formSchema: FormSchema, index?: number) => {
|
||||
const { schema } = unref(getProps)
|
||||
if (index !== void 0) {
|
||||
schema.splice(index, 0, formSchema)
|
||||
return
|
||||
}
|
||||
schema.push(formSchema)
|
||||
}
|
||||
|
||||
const defaultExpose = {
|
||||
getElFormExpose,
|
||||
setProps,
|
||||
setSchema,
|
||||
setValues,
|
||||
delSchema,
|
||||
addSchema
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emit('register', defaultExpose)
|
||||
})
|
||||
|
||||
defineExpose(defaultExpose)
|
||||
|
||||
const onFormValidate = (prop: FormItemProp, isValid: boolean, message: string) => {
|
||||
emit('validate', prop, isValid, message)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
:model="formModel"
|
||||
:is-custom="false"
|
||||
:label-width="getProps.labelWidth"
|
||||
hide-required-asterisk
|
||||
:inline="getProps.inline"
|
||||
:is-col="getProps.isCol"
|
||||
:schema="schemaRef"
|
||||
@register="formRegister"
|
||||
@validate="onFormValidate"
|
||||
/>
|
||||
|
||||
<template v-if="layout === 'bottom'">
|
||||
<div :style="bottomButtonStyle">
|
||||
<ActionButton
|
||||
:show-reset="getProps.showReset"
|
||||
:show-search="getProps.showSearch"
|
||||
:show-expand="getProps.showExpand"
|
||||
:search-loading="getProps.searchLoading"
|
||||
:reset-loading="getProps.resetLoading"
|
||||
@expand="setVisible"
|
||||
@reset="reset"
|
||||
@search="search"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { ElButton } from 'element-plus'
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
|
||||
const emit = defineEmits(['search', 'reset', 'expand'])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps({
|
||||
showSearch: propTypes.bool.def(true),
|
||||
showReset: propTypes.bool.def(true),
|
||||
showExpand: propTypes.bool.def(false),
|
||||
visible: propTypes.bool.def(true),
|
||||
searchLoading: propTypes.bool.def(false),
|
||||
resetLoading: propTypes.bool.def(false)
|
||||
})
|
||||
|
||||
const onSearch = () => {
|
||||
emit('search')
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
const onExpand = () => {
|
||||
emit('expand')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElButton
|
||||
v-if="showSearch"
|
||||
type="primary"
|
||||
:loading="searchLoading"
|
||||
:icon="useIcon({ icon: 'ep:search' })"
|
||||
@click="onSearch"
|
||||
>
|
||||
{{ t('common.query') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="showReset"
|
||||
:loading="resetLoading"
|
||||
:icon="useIcon({ icon: 'ep:refresh-right' })"
|
||||
@click="onReset"
|
||||
>
|
||||
{{ t('common.reset') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="showExpand"
|
||||
:icon="useIcon({ icon: visible ? 'ep:arrow-down' : 'ep:arrow-up' })"
|
||||
text
|
||||
@click="onExpand"
|
||||
>
|
||||
{{ t(visible ? 'common.shrink' : 'common.expand') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
import { FormSchema } from '@/components/Form'
|
||||
|
||||
export interface SearchProps {
|
||||
schema?: FormSchema[]
|
||||
isCol?: boolean
|
||||
labelWidth?: string | number
|
||||
layout?: 'inline' | 'bottom'
|
||||
buttonPosition?: 'left' | 'right' | 'center'
|
||||
showSearch?: boolean
|
||||
showReset?: boolean
|
||||
showExpand?: boolean
|
||||
expandField?: string
|
||||
inline?: boolean
|
||||
removeNoValueItem?: boolean
|
||||
model?: Recordable
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Setting from './src/Setting.vue'
|
||||
|
||||
export { Setting }
|
||||
@@ -0,0 +1,257 @@
|
||||
<script setup lang="ts">
|
||||
import { ElDrawer, ElDivider, ElMessage } from 'element-plus'
|
||||
import { ref, unref } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { ThemeSwitch } from '@/components/ThemeSwitch'
|
||||
import { useCssVar } from '@vueuse/core'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { trim, setCssVar, getCssVar } from '@/utils'
|
||||
import ColorRadioPicker from './components/ColorRadioPicker.vue'
|
||||
import InterfaceDisplay from './components/InterfaceDisplay.vue'
|
||||
import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
|
||||
import { useStorage } from '@/hooks/web/useStorage'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { clear: storageClear } = useStorage('localStorage')
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('setting')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const drawer = ref(false)
|
||||
|
||||
// 主题色相关
|
||||
const systemTheme = ref(appStore.getTheme.elColorPrimary)
|
||||
|
||||
const setSystemTheme = (color: string) => {
|
||||
setCssVar('--el-color-primary', color)
|
||||
appStore.setTheme({ elColorPrimary: color })
|
||||
const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement)
|
||||
setMenuTheme(trim(unref(leftMenuBgColor)))
|
||||
}
|
||||
|
||||
// 头部主题相关
|
||||
const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '')
|
||||
|
||||
const setHeaderTheme = (color: string) => {
|
||||
appStore.setHeaderTheme(color)
|
||||
}
|
||||
|
||||
// 菜单主题相关
|
||||
const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
|
||||
|
||||
const setMenuTheme = (color: string) => {
|
||||
appStore.setMenuTheme(color)
|
||||
}
|
||||
|
||||
// 监听layout变化,重置一些主题色
|
||||
// watch(
|
||||
// () => layout.value,
|
||||
// (n) => {
|
||||
// if (n === 'top' && !appStore.getIsDark) {
|
||||
// headerTheme.value = '#fff'
|
||||
// setHeaderTheme('#fff')
|
||||
// } else {
|
||||
// setMenuTheme(unref(menuTheme))
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
|
||||
// 拷贝
|
||||
const copyConfig = async () => {
|
||||
const { copy, copied, isSupported } = useClipboard({
|
||||
source: `
|
||||
// 面包屑
|
||||
breadcrumb: ${appStore.getBreadcrumb},
|
||||
// 面包屑图标
|
||||
breadcrumbIcon: ${appStore.getBreadcrumbIcon},
|
||||
// 折叠图标
|
||||
hamburger: ${appStore.getHamburger},
|
||||
// 全屏图标
|
||||
screenfull: ${appStore.getScreenfull},
|
||||
// 尺寸图标
|
||||
size: ${appStore.getSize},
|
||||
// 多语言图标
|
||||
locale: ${appStore.getLocale},
|
||||
// 标签页
|
||||
tagsView: ${appStore.getTagsView},
|
||||
// 标签页图标
|
||||
getTagsViewIcon: ${appStore.getTagsViewIcon},
|
||||
// logo
|
||||
logo: ${appStore.getLogo},
|
||||
// 菜单手风琴
|
||||
uniqueOpened: ${appStore.getUniqueOpened},
|
||||
// 固定header
|
||||
fixedHeader: ${appStore.getFixedHeader},
|
||||
// 页脚
|
||||
footer: ${appStore.getFooter},
|
||||
// 灰色模式
|
||||
greyMode: ${appStore.getGreyMode},
|
||||
// layout布局
|
||||
layout: '${appStore.getLayout}',
|
||||
// 暗黑模式
|
||||
isDark: ${appStore.getIsDark},
|
||||
// 组件尺寸
|
||||
currentSize: '${appStore.getCurrentSize}',
|
||||
// 主题相关
|
||||
theme: {
|
||||
// 主题色
|
||||
elColorPrimary: '${appStore.getTheme.elColorPrimary}',
|
||||
// 左侧菜单边框颜色
|
||||
leftMenuBorderColor: '${appStore.getTheme.leftMenuBorderColor}',
|
||||
// 左侧菜单背景颜色
|
||||
leftMenuBgColor: '${appStore.getTheme.leftMenuBgColor}',
|
||||
// 左侧菜单浅色背景颜色
|
||||
leftMenuBgLightColor: '${appStore.getTheme.leftMenuBgLightColor}',
|
||||
// 左侧菜单选中背景颜色
|
||||
leftMenuBgActiveColor: '${appStore.getTheme.leftMenuBgActiveColor}',
|
||||
// 左侧菜单收起选中背景颜色
|
||||
leftMenuCollapseBgActiveColor: '${appStore.getTheme.leftMenuCollapseBgActiveColor}',
|
||||
// 左侧菜单字体颜色
|
||||
leftMenuTextColor: '${appStore.getTheme.leftMenuTextColor}',
|
||||
// 左侧菜单选中字体颜色
|
||||
leftMenuTextActiveColor: '${appStore.getTheme.leftMenuTextActiveColor}',
|
||||
// logo字体颜色
|
||||
logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}',
|
||||
// logo边框颜色
|
||||
logoBorderColor: '${appStore.getTheme.logoBorderColor}',
|
||||
// 头部背景颜色
|
||||
topHeaderBgColor: '${appStore.getTheme.topHeaderBgColor}',
|
||||
// 头部字体颜色
|
||||
topHeaderTextColor: '${appStore.getTheme.topHeaderTextColor}',
|
||||
// 头部悬停颜色
|
||||
topHeaderHoverColor: '${appStore.getTheme.topHeaderHoverColor}',
|
||||
// 头部边框颜色
|
||||
topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
|
||||
}
|
||||
`,
|
||||
legacy: true
|
||||
})
|
||||
if (!isSupported) {
|
||||
ElMessage.error(t('setting.copyFailed'))
|
||||
} else {
|
||||
await copy()
|
||||
if (unref(copied)) {
|
||||
ElMessage.success(t('setting.copySuccess'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清空缓存
|
||||
const clear = () => {
|
||||
storageClear()
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const themeChange = () => {
|
||||
const color = getCssVar('--el-bg-color')
|
||||
setMenuTheme(color)
|
||||
setHeaderTheme(color)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="prefixCls"
|
||||
class="fixed top-[45%] right-0 w-40px h-40px flex items-center justify-center bg-[var(--el-color-primary)] cursor-pointer z-10"
|
||||
@click="drawer = true"
|
||||
>
|
||||
<Icon icon="ant-design:setting-outlined" color="#fff" />
|
||||
</div>
|
||||
|
||||
<ElDrawer v-model="drawer" direction="rtl" size="350px" :z-index="4000">
|
||||
<template #header>
|
||||
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
|
||||
</template>
|
||||
|
||||
<div class="text-center">
|
||||
<!-- 主题 -->
|
||||
<ElDivider>{{ t('setting.theme') }}</ElDivider>
|
||||
<ThemeSwitch @change="themeChange" />
|
||||
|
||||
<!-- 布局 -->
|
||||
<ElDivider>{{ t('setting.layout') }}</ElDivider>
|
||||
<LayoutRadioPicker />
|
||||
|
||||
<!-- 系统主题 -->
|
||||
<ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
|
||||
<ColorRadioPicker
|
||||
v-model="systemTheme"
|
||||
:schema="[
|
||||
'#409eff',
|
||||
'#009688',
|
||||
'#536dfe',
|
||||
'#ff5c93',
|
||||
'#ee4f12',
|
||||
'#0096c7',
|
||||
'#9c27b0',
|
||||
'#ff9800'
|
||||
]"
|
||||
@change="setSystemTheme"
|
||||
/>
|
||||
|
||||
<!-- 头部主题 -->
|
||||
<ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
|
||||
<ColorRadioPicker
|
||||
v-model="headerTheme"
|
||||
:schema="[
|
||||
'#fff',
|
||||
'#151515',
|
||||
'#5172dc',
|
||||
'#e74c3c',
|
||||
'#24292e',
|
||||
'#394664',
|
||||
'#009688',
|
||||
'#383f45'
|
||||
]"
|
||||
@change="setHeaderTheme"
|
||||
/>
|
||||
|
||||
<!-- 菜单主题 -->
|
||||
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
|
||||
<ColorRadioPicker
|
||||
v-model="menuTheme"
|
||||
:schema="[
|
||||
'#fff',
|
||||
'#001529',
|
||||
'#212121',
|
||||
'#273352',
|
||||
'#191b24',
|
||||
'#383f45',
|
||||
'#001628',
|
||||
'#344058'
|
||||
]"
|
||||
@change="setMenuTheme"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 界面显示 -->
|
||||
<ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
|
||||
<InterfaceDisplay />
|
||||
|
||||
<ElDivider />
|
||||
<div>
|
||||
<BaseButton type="primary" class="w-full" @click="copyConfig">{{
|
||||
t('setting.copy')
|
||||
}}</BaseButton>
|
||||
</div>
|
||||
<div class="mt-5px">
|
||||
<BaseButton type="danger" class="w-full" @click="clear">
|
||||
{{ t('setting.clearAndReset') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</ElDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-setting';
|
||||
|
||||
.@{prefix-cls} {
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { PropType, watch, unref, ref } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('color-radio-picker')
|
||||
|
||||
const props = defineProps({
|
||||
schema: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => []
|
||||
},
|
||||
modelValue: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const colorVal = ref(props.modelValue)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: string) => {
|
||||
if (val === unref(colorVal)) return
|
||||
colorVal.value = val
|
||||
}
|
||||
)
|
||||
|
||||
// 监听
|
||||
watch(
|
||||
() => colorVal.value,
|
||||
(val: string) => {
|
||||
emit('update:modelValue', val)
|
||||
emit('change', val)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls" class="flex flex-wrap space-x-14px">
|
||||
<span
|
||||
v-for="(item, i) in schema"
|
||||
:key="`radio-${i}`"
|
||||
class="w-20px h-20px cursor-pointer rounded-2px border-solid border-gray-300 border-2px text-center leading-20px mb-5px"
|
||||
:class="{ 'is-active': colorVal === item }"
|
||||
:style="{
|
||||
background: item
|
||||
}"
|
||||
@click="colorVal = item"
|
||||
>
|
||||
<Icon v-if="colorVal === item" color="#fff" icon="ep:check" :size="16" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-color-radio-picker';
|
||||
|
||||
.@{prefix-cls} {
|
||||
.is-active {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,215 @@
|
||||
<script setup lang="ts">
|
||||
import { ElSwitch, ElMessage } from 'element-plus'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { setCssVar } from '@/utils'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('interface-display')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 面包屑
|
||||
const breadcrumb = ref(appStore.getBreadcrumb)
|
||||
|
||||
const breadcrumbChange = (show: boolean) => {
|
||||
appStore.setBreadcrumb(show)
|
||||
}
|
||||
|
||||
// 面包屑图标
|
||||
const breadcrumbIcon = ref(appStore.getBreadcrumbIcon)
|
||||
|
||||
const breadcrumbIconChange = (show: boolean) => {
|
||||
appStore.setBreadcrumbIcon(show)
|
||||
}
|
||||
|
||||
// 折叠图标
|
||||
const hamburger = ref(appStore.getHamburger)
|
||||
|
||||
const hamburgerChange = (show: boolean) => {
|
||||
appStore.setHamburger(show)
|
||||
}
|
||||
|
||||
// 全屏图标
|
||||
const screenfull = ref(appStore.getScreenfull)
|
||||
|
||||
const screenfullChange = (show: boolean) => {
|
||||
appStore.setScreenfull(show)
|
||||
}
|
||||
|
||||
// 尺寸图标
|
||||
const size = ref(appStore.getSize)
|
||||
|
||||
const sizeChange = (show: boolean) => {
|
||||
appStore.setSize(show)
|
||||
}
|
||||
|
||||
// 多语言图标
|
||||
const locale = ref(appStore.getLocale)
|
||||
|
||||
const localeChange = (show: boolean) => {
|
||||
appStore.setLocale(show)
|
||||
}
|
||||
|
||||
// 标签页
|
||||
const tagsView = ref(appStore.getTagsView)
|
||||
|
||||
const tagsViewChange = (show: boolean) => {
|
||||
// 切换标签栏显示时,同步切换标签栏的高度
|
||||
setCssVar('--tags-view-height', show ? '35px' : '0px')
|
||||
appStore.setTagsView(show)
|
||||
}
|
||||
|
||||
// 标签页图标
|
||||
const tagsViewIcon = ref(appStore.getTagsViewIcon)
|
||||
|
||||
const tagsViewIconChange = (show: boolean) => {
|
||||
appStore.setTagsViewIcon(show)
|
||||
}
|
||||
|
||||
// logo
|
||||
const logo = ref(appStore.getLogo)
|
||||
|
||||
const logoChange = (show: boolean) => {
|
||||
appStore.setLogo(show)
|
||||
}
|
||||
|
||||
// 菜单手风琴
|
||||
const uniqueOpened = ref(appStore.getUniqueOpened)
|
||||
|
||||
const uniqueOpenedChange = (uniqueOpened: boolean) => {
|
||||
appStore.setUniqueOpened(uniqueOpened)
|
||||
}
|
||||
|
||||
// 固定头部
|
||||
const fixedHeader = ref(appStore.getFixedHeader)
|
||||
|
||||
const fixedHeaderChange = (show: boolean) => {
|
||||
appStore.setFixedHeader(show)
|
||||
}
|
||||
|
||||
// 灰色模式
|
||||
const greyMode = ref(appStore.getGreyMode)
|
||||
|
||||
const greyModeChange = (show: boolean) => {
|
||||
appStore.setGreyMode(show)
|
||||
}
|
||||
|
||||
// 动态路由
|
||||
const dynamicRouter = ref(!!appStore.getDynamicRouter)
|
||||
|
||||
const dynamicRouterChange = (show: boolean) => {
|
||||
ElMessage.info(t('setting.reExperienced'))
|
||||
appStore.setDynamicRouter(show)
|
||||
}
|
||||
|
||||
// 服务端动态路由
|
||||
const serverDynamicRouter = ref(appStore.getServerDynamicRouter)
|
||||
|
||||
const serverDynamicRouterChange = (show: boolean) => {
|
||||
ElMessage.info(t('setting.reExperienced'))
|
||||
appStore.setServerDynamicRouter(show)
|
||||
}
|
||||
|
||||
// 固定菜单
|
||||
const fixedMenu = ref(appStore.getFixedMenu)
|
||||
|
||||
const fixedMenuChange = (show: boolean) => {
|
||||
appStore.setFixedMenu(show)
|
||||
}
|
||||
|
||||
const layout = computed(() => appStore.getLayout)
|
||||
|
||||
watch(
|
||||
() => layout.value,
|
||||
(n) => {
|
||||
if (n === 'top') {
|
||||
appStore.setCollapse(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.breadcrumb') }}</span>
|
||||
<ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
|
||||
<ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
|
||||
<ElSwitch v-model="hamburger" @change="hamburgerChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.screenfullIcon') }}</span>
|
||||
<ElSwitch v-model="screenfull" @change="screenfullChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.sizeIcon') }}</span>
|
||||
<ElSwitch v-model="size" @change="sizeChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.localeIcon') }}</span>
|
||||
<ElSwitch v-model="locale" @change="localeChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.tagsView') }}</span>
|
||||
<ElSwitch v-model="tagsView" @change="tagsViewChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
|
||||
<ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.logo') }}</span>
|
||||
<ElSwitch v-model="logo" @change="logoChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.uniqueOpened') }}</span>
|
||||
<ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.fixedHeader') }}</span>
|
||||
<ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.greyMode') }}</span>
|
||||
<ElSwitch v-model="greyMode" @change="greyModeChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.dynamicRouter') }}</span>
|
||||
<ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.serverDynamicRouter') }}</span>
|
||||
<ElSwitch v-model="serverDynamicRouter" @change="serverDynamicRouterChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.fixedMenu') }}</span>
|
||||
<ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { computed } from 'vue'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('layout-radio-picker')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const layout = computed(() => appStore.getLayout)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls" class="flex flex-wrap space-x-14px">
|
||||
<div
|
||||
:class="[
|
||||
`${prefixCls}__classic`,
|
||||
'relative w-56px h-48px cursor-pointer bg-gray-300',
|
||||
{
|
||||
'is-acitve': layout === 'classic'
|
||||
}
|
||||
]"
|
||||
@click="appStore.setLayout('classic')"
|
||||
></div>
|
||||
<div
|
||||
:class="[
|
||||
`${prefixCls}__top-left`,
|
||||
'relative w-56px h-48px cursor-pointer bg-gray-300',
|
||||
{
|
||||
'is-acitve': layout === 'topLeft'
|
||||
}
|
||||
]"
|
||||
@click="appStore.setLayout('topLeft')"
|
||||
></div>
|
||||
<div
|
||||
:class="[
|
||||
`${prefixCls}__top`,
|
||||
'relative w-56px h-48px cursor-pointer bg-gray-300',
|
||||
{
|
||||
'is-acitve': layout === 'top'
|
||||
}
|
||||
]"
|
||||
@click="appStore.setLayout('top')"
|
||||
></div>
|
||||
<div
|
||||
:class="[
|
||||
`${prefixCls}__cut-menu`,
|
||||
'relative w-56px h-48px cursor-pointer bg-gray-300',
|
||||
{
|
||||
'is-acitve': layout === 'cutMenu'
|
||||
}
|
||||
]"
|
||||
@click="appStore.setLayout('cutMenu')"
|
||||
>
|
||||
<div class="absolute h-full w-[33%] top-0 left-[10%] bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-layout-radio-picker';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&__classic {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
background-color: #273352;
|
||||
border-radius: 4px 0 0 4px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: #fff;
|
||||
border-radius: 4px 4px 0;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&__top-left {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 33%;
|
||||
background-color: #273352;
|
||||
border-radius: 4px 4px 0 0;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 4px 0 0 4px;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&__top {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 33%;
|
||||
background-color: #273352;
|
||||
border-radius: 4px 4px 0 0;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&__cut-menu {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 33%;
|
||||
background-color: #273352;
|
||||
border-radius: 4px 4px 0 0;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 10%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 4px 0 0 4px;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.is-acitve {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import SizeDropdown from './src/SizeDropdown.vue'
|
||||
|
||||
export { SizeDropdown }
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem, ComponentSize } from 'element-plus'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('size-dropdown')
|
||||
|
||||
defineProps({
|
||||
color: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const sizeMap = computed(() => appStore.sizeMap)
|
||||
|
||||
const setCurrentSize = (size: ComponentSize) => {
|
||||
appStore.setCurrentSize(size)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDropdown :class="prefixCls" trigger="click" @command="setCurrentSize">
|
||||
<Icon :size="18" icon="mdi:format-size" :color="color" class="cursor-pointer" />
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem v-for="item in sizeMap" :key="item" :command="item">
|
||||
{{ t(`size.${item}`) }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
import TabMenu from './src/TabMenu.vue'
|
||||
|
||||
export { TabMenu }
|
||||
@@ -0,0 +1,246 @@
|
||||
<script lang="tsx">
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { computed, unref, defineComponent, watch, ref, onMounted } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { ElScrollbar } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { Menu } from '@/components/Menu'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { pathResolve } from '@/utils/routerHelper'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { filterMenusPath, initTabMap, tabPathMap } from './helper'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { isUrl } from '@/utils/is'
|
||||
|
||||
const { getPrefixCls, variables } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('tab-menu')
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabMenu',
|
||||
setup() {
|
||||
const { push, currentRoute } = useRouter()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const collapse = computed(() => appStore.getCollapse)
|
||||
|
||||
const fixedMenu = computed(() => appStore.getFixedMenu)
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const routers = computed(() => permissionStore.getRouters)
|
||||
|
||||
const tabRouters = computed(() => unref(routers).filter((v) => !v?.meta?.hidden))
|
||||
|
||||
const setCollapse = () => {
|
||||
appStore.setCollapse(!unref(collapse))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (unref(fixedMenu)) {
|
||||
const path = `/${unref(currentRoute).path.split('/')[1]}`
|
||||
const children = unref(tabRouters).find(
|
||||
(v) =>
|
||||
(v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)) &&
|
||||
v.path === path
|
||||
)?.children
|
||||
|
||||
tabActive.value = path
|
||||
if (children) {
|
||||
permissionStore.setMenuTabRouters(
|
||||
cloneDeep(children).map((v) => {
|
||||
v.path = pathResolve(unref(tabActive), v.path)
|
||||
return v
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => routers.value,
|
||||
(routers: AppRouteRecordRaw[]) => {
|
||||
initTabMap(routers)
|
||||
filterMenusPath(routers, routers)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
const showTitle = ref(true)
|
||||
|
||||
watch(
|
||||
() => collapse.value,
|
||||
(collapse: boolean) => {
|
||||
if (!collapse) {
|
||||
setTimeout(() => {
|
||||
showTitle.value = !collapse
|
||||
}, 200)
|
||||
} else {
|
||||
showTitle.value = !collapse
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 是否显示菜单
|
||||
const showMenu = ref(unref(fixedMenu) ? true : false)
|
||||
|
||||
// tab高亮
|
||||
const tabActive = ref('')
|
||||
|
||||
// tab点击事件
|
||||
const tabClick = (item: AppRouteRecordRaw) => {
|
||||
if (isUrl(item.path)) {
|
||||
window.open(item.path)
|
||||
return
|
||||
}
|
||||
const newPath = item.children ? item.path : item.path.split('/')[0]
|
||||
const oldPath = unref(tabActive)
|
||||
tabActive.value = item.children ? item.path : item.path.split('/')[0]
|
||||
if (item.children) {
|
||||
if (newPath === oldPath || !unref(showMenu)) {
|
||||
showMenu.value = unref(fixedMenu) ? true : !unref(showMenu)
|
||||
}
|
||||
if (unref(showMenu)) {
|
||||
permissionStore.setMenuTabRouters(
|
||||
cloneDeep(item.children).map((v) => {
|
||||
v.path = pathResolve(unref(tabActive), v.path)
|
||||
return v
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
push(item.path)
|
||||
permissionStore.setMenuTabRouters([])
|
||||
showMenu.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 设置高亮
|
||||
const isActive = (currentPath: string) => {
|
||||
const { path } = unref(currentRoute)
|
||||
if (tabPathMap[currentPath].includes(path)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const mouseleave = () => {
|
||||
if (!unref(showMenu) || unref(fixedMenu)) return
|
||||
showMenu.value = false
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div
|
||||
id={`${variables.namespace}-menu`}
|
||||
class={[
|
||||
prefixCls,
|
||||
'relative bg-[var(--left-menu-bg-color)] top-1px z-3000 layout-border__right',
|
||||
{
|
||||
'w-[var(--tab-menu-max-width)]': !unref(collapse),
|
||||
'w-[var(--tab-menu-min-width)]': unref(collapse)
|
||||
}
|
||||
]}
|
||||
onMouseleave={mouseleave}
|
||||
>
|
||||
<ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]">
|
||||
<div>
|
||||
{() => {
|
||||
return unref(tabRouters).map((v) => {
|
||||
const item = (
|
||||
v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)
|
||||
? v
|
||||
: {
|
||||
...(v?.children && v?.children[0]),
|
||||
path: pathResolve(v.path, (v?.children && v?.children[0])?.path as string)
|
||||
}
|
||||
) as AppRouteRecordRaw
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}__item`,
|
||||
'text-center text-12px relative py-12px cursor-pointer',
|
||||
{
|
||||
'is-active': isActive(v.path)
|
||||
}
|
||||
]}
|
||||
onClick={() => {
|
||||
tabClick(item)
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Icon icon={item?.meta?.icon}></Icon>
|
||||
</div>
|
||||
{!unref(showTitle) ? undefined : (
|
||||
<p class="break-words mt-5px px-2px">{t(item.meta?.title)}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}--collapse`,
|
||||
'text-center h-[var(--tab-menu-collapse-height)] leading-[var(--tab-menu-collapse-height)] cursor-pointer'
|
||||
]}
|
||||
onClick={setCollapse}
|
||||
>
|
||||
<Icon icon={unref(collapse) ? 'ep:d-arrow-right' : 'ep:d-arrow-left'}></Icon>
|
||||
</div>
|
||||
<Menu
|
||||
class={[
|
||||
'!absolute top-0 z-3000',
|
||||
{
|
||||
'!left-[var(--tab-menu-min-width)]': unref(collapse),
|
||||
'!left-[var(--tab-menu-max-width)]': !unref(collapse),
|
||||
'!w-[var(--left-menu-max-width)] border-r-1 border-r-solid border-[var(--el-border-color)]':
|
||||
unref(showMenu) || unref(fixedMenu),
|
||||
'!w-0': !unref(showMenu) && !unref(fixedMenu)
|
||||
}
|
||||
]}
|
||||
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
|
||||
></Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-tab-menu';
|
||||
|
||||
.@{prefix-cls} {
|
||||
transition: all var(--transition-time-02);
|
||||
|
||||
&__item {
|
||||
color: var(--left-menu-text-color);
|
||||
transition: all var(--transition-time-02);
|
||||
|
||||
&:hover {
|
||||
color: var(--left-menu-text-active-color);
|
||||
// background-color: var(--left-menu-bg-active-color);
|
||||
}
|
||||
}
|
||||
|
||||
&--collapse {
|
||||
color: var(--left-menu-text-color);
|
||||
background-color: var(--left-menu-bg-light-color);
|
||||
}
|
||||
|
||||
.is-active {
|
||||
color: var(--left-menu-text-active-color);
|
||||
background-color: var(--left-menu-bg-active-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { getAllParentPath } from '@/components/Menu/src/helper'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import { isUrl } from '@/utils/is'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export type TabMapTypes = {
|
||||
[key: string]: string[]
|
||||
}
|
||||
|
||||
export const tabPathMap = reactive<TabMapTypes>({})
|
||||
|
||||
export const initTabMap = (routes: AppRouteRecordRaw[]) => {
|
||||
for (const v of routes) {
|
||||
const meta = (v.meta ?? {}) as RouteMeta
|
||||
if (!meta?.hidden) {
|
||||
tabPathMap[v.path] = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const filterMenusPath = (
|
||||
routes: AppRouteRecordRaw[],
|
||||
allRoutes: AppRouteRecordRaw[]
|
||||
): AppRouteRecordRaw[] => {
|
||||
const res: AppRouteRecordRaw[] = []
|
||||
for (const v of routes) {
|
||||
let data: Nullable<AppRouteRecordRaw> = null
|
||||
const meta = (v.meta ?? {}) as RouteMeta
|
||||
if (!meta.hidden || meta.canTo) {
|
||||
const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
|
||||
|
||||
const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/')
|
||||
|
||||
data = cloneDeep(v)
|
||||
data.path = fullPath
|
||||
if (v.children && data) {
|
||||
data.children = filterMenusPath(v.children, allRoutes)
|
||||
}
|
||||
|
||||
if (data) {
|
||||
res.push(data)
|
||||
}
|
||||
|
||||
if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) {
|
||||
tabPathMap[allParentPath[0]].push(fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Table from './src/Table.vue'
|
||||
import { ElTable } from 'element-plus'
|
||||
import { TableColumn, TableSetProps } from './src/types'
|
||||
|
||||
export type {
|
||||
TableColumn,
|
||||
TableSlotDefault,
|
||||
Pagination,
|
||||
TableSetProps,
|
||||
TableProps
|
||||
} from './src/types'
|
||||
|
||||
export interface TableExpose {
|
||||
setProps: (props: Recordable) => void
|
||||
setColumn: (columnProps: TableSetProps[]) => void
|
||||
addColumn: (column: TableColumn, index?: number) => void
|
||||
delColumn: (field: string) => void
|
||||
elTableRef: ComponentRef<typeof ElTable>
|
||||
}
|
||||
|
||||
export { Table }
|
||||
@@ -0,0 +1,588 @@
|
||||
<script lang="tsx">
|
||||
import {
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElPagination,
|
||||
ComponentSize,
|
||||
ElTooltipProps,
|
||||
ElImage,
|
||||
ElEmpty,
|
||||
ElCard
|
||||
} from 'element-plus'
|
||||
import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { setIndex } from './helper'
|
||||
import type { TableProps, TableColumn, Pagination, TableSetProps } from './types'
|
||||
import { set, get } from 'lodash-es'
|
||||
import { CSSProperties } from 'vue'
|
||||
import { getSlot } from '@/utils/tsxHelper'
|
||||
import TableActions from './components/TableActions.vue'
|
||||
import { createVideoViewer } from '@/components/VideoPlayer'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { BaseButton } from '@/components/Button'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Table',
|
||||
props: {
|
||||
pageSize: propTypes.number.def(10),
|
||||
currentPage: propTypes.number.def(1),
|
||||
// 是否展示表格的工具栏
|
||||
showAction: propTypes.bool.def(false),
|
||||
// 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip,
|
||||
showOverflowTooltip: propTypes.bool.def(true),
|
||||
// 表头
|
||||
columns: {
|
||||
type: Array as PropType<TableColumn[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 是否展示分页
|
||||
pagination: {
|
||||
type: Object as PropType<Pagination>,
|
||||
default: (): Pagination | undefined => undefined
|
||||
},
|
||||
// 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key)
|
||||
reserveSelection: propTypes.bool.def(false),
|
||||
// 加载状态
|
||||
loading: propTypes.bool.def(false),
|
||||
// 是否叠加索引
|
||||
reserveIndex: propTypes.bool.def(false),
|
||||
// 对齐方式
|
||||
align: propTypes.string
|
||||
.validate((v: string) => ['left', 'center', 'right'].includes(v))
|
||||
.def('left'),
|
||||
// 表头对齐方式
|
||||
headerAlign: propTypes.string
|
||||
.validate((v: string) => ['left', 'center', 'right'].includes(v))
|
||||
.def('left'),
|
||||
data: {
|
||||
type: Array as PropType<Recordable[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 图片自动预览字段数组
|
||||
imagePreview: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 视频自动预览字段数组
|
||||
videoPreview: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => []
|
||||
},
|
||||
height: propTypes.oneOfType([Number, String]),
|
||||
maxHeight: propTypes.oneOfType([Number, String]),
|
||||
stripe: propTypes.bool.def(false),
|
||||
border: propTypes.bool.def(true),
|
||||
size: {
|
||||
type: String as PropType<ComponentSize>,
|
||||
validator: (v: ComponentSize) => ['medium', 'small', 'mini'].includes(v)
|
||||
},
|
||||
fit: propTypes.bool.def(true),
|
||||
showHeader: propTypes.bool.def(true),
|
||||
highlightCurrentRow: propTypes.bool.def(false),
|
||||
currentRowKey: propTypes.oneOfType([Number, String]),
|
||||
// row-class-name, 类型为 (row: Recordable, rowIndex: number) => string | string
|
||||
rowClassName: {
|
||||
type: [Function, String] as PropType<(row: Recordable, rowIndex: number) => string | string>,
|
||||
default: ''
|
||||
},
|
||||
rowStyle: {
|
||||
type: [Function, Object] as PropType<
|
||||
(row: Recordable, rowIndex: number) => Recordable | CSSProperties
|
||||
>,
|
||||
default: () => undefined
|
||||
},
|
||||
cellClassName: {
|
||||
type: [Function, String] as PropType<
|
||||
(row: Recordable, column: any, rowIndex: number) => string | string
|
||||
>,
|
||||
default: ''
|
||||
},
|
||||
cellStyle: {
|
||||
type: [Function, Object] as PropType<
|
||||
(row: Recordable, column: any, rowIndex: number) => Recordable | CSSProperties
|
||||
>,
|
||||
default: () => undefined
|
||||
},
|
||||
headerRowClassName: {
|
||||
type: [Function, String] as PropType<(row: Recordable, rowIndex: number) => string | string>,
|
||||
default: ''
|
||||
},
|
||||
headerRowStyle: {
|
||||
type: [Function, Object] as PropType<
|
||||
(row: Recordable, rowIndex: number) => Recordable | CSSProperties
|
||||
>,
|
||||
default: () => undefined
|
||||
},
|
||||
headerCellClassName: {
|
||||
type: [Function, String] as PropType<
|
||||
(row: Recordable, column: any, rowIndex: number) => string | string
|
||||
>,
|
||||
default: ''
|
||||
},
|
||||
headerCellStyle: {
|
||||
type: [Function, Object] as PropType<
|
||||
(row: Recordable, column: any, rowIndex: number) => Recordable | CSSProperties
|
||||
>,
|
||||
default: () => undefined
|
||||
},
|
||||
rowKey: propTypes.string.def('id'),
|
||||
emptyText: propTypes.string.def('No Data'),
|
||||
defaultExpandAll: propTypes.bool.def(false),
|
||||
expandRowKeys: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => []
|
||||
},
|
||||
defaultSort: {
|
||||
type: Object as PropType<{ prop: string; order: string }>,
|
||||
default: () => ({})
|
||||
},
|
||||
tooltipEffect: {
|
||||
type: String as PropType<'dark' | 'light'>,
|
||||
default: 'dark'
|
||||
},
|
||||
tooltipOptions: {
|
||||
type: Object as PropType<
|
||||
Pick<
|
||||
ElTooltipProps,
|
||||
| 'effect'
|
||||
| 'enterable'
|
||||
| 'hideAfter'
|
||||
| 'offset'
|
||||
| 'placement'
|
||||
| 'popperClass'
|
||||
| 'popperOptions'
|
||||
| 'showAfter'
|
||||
| 'showArrow'
|
||||
>
|
||||
>,
|
||||
default: () => ({
|
||||
enterable: true,
|
||||
placement: 'top',
|
||||
showArrow: true,
|
||||
hideAfter: 200,
|
||||
popperOptions: { strategy: 'fixed' }
|
||||
})
|
||||
},
|
||||
showSummary: propTypes.bool.def(false),
|
||||
sumText: propTypes.string.def('Sum'),
|
||||
summaryMethod: {
|
||||
type: Function as PropType<(param: { columns: any[]; data: any[] }) => any[]>,
|
||||
default: () => undefined
|
||||
},
|
||||
spanMethod: {
|
||||
type: Function as PropType<
|
||||
(param: { row: any; column: any; rowIndex: number; columnIndex: number }) => any[]
|
||||
>,
|
||||
default: () => undefined
|
||||
},
|
||||
selectOnIndeterminate: propTypes.bool.def(true),
|
||||
indent: propTypes.number.def(16),
|
||||
lazy: propTypes.bool.def(false),
|
||||
load: {
|
||||
type: Function as PropType<(row: Recordable, treeNode: any, resolve: Function) => void>,
|
||||
default: () => undefined
|
||||
},
|
||||
treeProps: {
|
||||
type: Object as PropType<{ hasChildren?: string; children?: string; label?: string }>,
|
||||
default: () => ({ hasChildren: 'hasChildren', children: 'children', label: 'label' })
|
||||
},
|
||||
tableLayout: {
|
||||
type: String as PropType<'auto' | 'fixed'>,
|
||||
default: 'fixed'
|
||||
},
|
||||
scrollbarAlwaysOn: propTypes.bool.def(false),
|
||||
flexible: propTypes.bool.def(false),
|
||||
// 自定义内容
|
||||
customContent: propTypes.bool.def(false),
|
||||
cardBodyStyle: {
|
||||
type: Object as PropType<CSSProperties>,
|
||||
default: () => ({})
|
||||
},
|
||||
cardBodyClass: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
cardWrapStyle: {
|
||||
type: Object as PropType<CSSProperties>,
|
||||
default: () => ({})
|
||||
},
|
||||
cardWrapClass: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['update:pageSize', 'update:currentPage', 'register', 'refresh'],
|
||||
setup(props, { attrs, emit, slots, expose }) {
|
||||
const elTableRef = ref<ComponentRef<typeof ElTable>>()
|
||||
|
||||
// 注册
|
||||
onMounted(() => {
|
||||
const tableRef = unref(elTableRef)
|
||||
emit('register', tableRef?.$parent, unref(elTableRef))
|
||||
})
|
||||
|
||||
const pageSizeRef = ref(props.pageSize)
|
||||
|
||||
const currentPageRef = ref(props.currentPage)
|
||||
|
||||
// useTable传入的props
|
||||
const outsideProps = ref<TableProps>({})
|
||||
|
||||
const mergeProps = ref<TableProps>({})
|
||||
|
||||
const getProps = computed(() => {
|
||||
const propsObj = { ...props }
|
||||
Object.assign(propsObj, unref(mergeProps))
|
||||
return propsObj
|
||||
})
|
||||
|
||||
const setProps = (props: TableProps = {}) => {
|
||||
mergeProps.value = Object.assign(unref(mergeProps), props)
|
||||
outsideProps.value = { ...props } as any
|
||||
}
|
||||
|
||||
const setColumn = (columnProps: TableSetProps[], columnsChildren?: TableColumn[]) => {
|
||||
const { columns } = unref(getProps)
|
||||
for (const v of columnsChildren || columns) {
|
||||
for (const item of columnProps) {
|
||||
if (v.field === item.field) {
|
||||
set(v, item.path, item.value)
|
||||
} else if (v.children?.length) {
|
||||
setColumn(columnProps, v.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addColumn = (column: TableColumn, index?: number) => {
|
||||
const { columns } = unref(getProps)
|
||||
if (index !== void 0) {
|
||||
columns.splice(index, 0, column)
|
||||
} else {
|
||||
columns.push(column)
|
||||
}
|
||||
}
|
||||
|
||||
const delColumn = (field: string) => {
|
||||
const { columns } = unref(getProps)
|
||||
const index = columns.findIndex((item) => item.field === field)
|
||||
if (index > -1) {
|
||||
columns.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
const changSize = (size: ComponentSize) => {
|
||||
setProps({ size })
|
||||
}
|
||||
|
||||
const confirmSetColumn = (columns: TableColumn[]) => {
|
||||
setProps({ columns })
|
||||
}
|
||||
|
||||
expose({
|
||||
setProps,
|
||||
setColumn,
|
||||
delColumn,
|
||||
addColumn,
|
||||
elTableRef
|
||||
})
|
||||
|
||||
const pagination = computed(() => {
|
||||
return Object.assign(
|
||||
{
|
||||
small: false,
|
||||
background: false,
|
||||
pagerCount: 7,
|
||||
layout: 'sizes, prev, pager, next, jumper, ->, total',
|
||||
pageSizes: [10, 20, 30, 40, 50, 100],
|
||||
disabled: false,
|
||||
hideOnSinglePage: false,
|
||||
total: 10
|
||||
},
|
||||
unref(getProps).pagination
|
||||
)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => unref(getProps).pageSize,
|
||||
(val: number) => {
|
||||
pageSizeRef.value = val
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => unref(getProps).currentPage,
|
||||
(val: number) => {
|
||||
currentPageRef.value = val
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => pageSizeRef.value,
|
||||
(val: number) => {
|
||||
emit('update:pageSize', val)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => currentPageRef.value,
|
||||
(val: number) => {
|
||||
emit('update:currentPage', val)
|
||||
}
|
||||
)
|
||||
|
||||
const getBindValue = computed(() => {
|
||||
const bindValue: Recordable = { ...attrs, ...unref(getProps) }
|
||||
delete bindValue.columns
|
||||
delete bindValue.data
|
||||
delete bindValue.align
|
||||
return bindValue
|
||||
})
|
||||
|
||||
const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
|
||||
const { align, headerAlign, showOverflowTooltip, imagePreview, videoPreview } =
|
||||
unref(getProps)
|
||||
return columnsChildren.map((v) => {
|
||||
if (v.hidden) return null
|
||||
const props = { ...v } as any
|
||||
if (props.children) delete props.children
|
||||
|
||||
const children = v.children
|
||||
|
||||
const slots = {
|
||||
default: (...args: any[]) => {
|
||||
const data = args[0]
|
||||
let isPreview = false
|
||||
isPreview =
|
||||
imagePreview.some((item) => (item as string) === v.field) ||
|
||||
videoPreview.some((item) => (item as string) === v.field)
|
||||
|
||||
return children && children.length
|
||||
? renderTreeTableColumn(children)
|
||||
: props?.slots?.default
|
||||
? props.slots.default(...args)
|
||||
: v?.formatter
|
||||
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
|
||||
: isPreview
|
||||
? renderPreview(get(data.row, v.field), v.field)
|
||||
: get(data.row, v.field)
|
||||
}
|
||||
}
|
||||
if (props?.slots?.header) {
|
||||
slots['header'] = (...args: any[]) => props.slots.header(...args)
|
||||
}
|
||||
|
||||
return (
|
||||
<ElTableColumn
|
||||
showOverflowTooltip={showOverflowTooltip}
|
||||
align={align}
|
||||
headerAlign={headerAlign}
|
||||
{...props}
|
||||
prop={v.field}
|
||||
>
|
||||
{slots}
|
||||
</ElTableColumn>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const renderPreview = (url: string, field: string) => {
|
||||
const { imagePreview, videoPreview } = unref(getProps)
|
||||
return (
|
||||
<div class="flex items-center">
|
||||
{imagePreview.includes(field) ? (
|
||||
<ElImage
|
||||
src={url}
|
||||
fit="cover"
|
||||
class="w-[100%]"
|
||||
lazy
|
||||
preview-src-list={[url]}
|
||||
preview-teleported
|
||||
/>
|
||||
) : videoPreview.includes(field) ? (
|
||||
<BaseButton
|
||||
type="primary"
|
||||
icon={<Icon icon="ep:video-play" />}
|
||||
onClick={() => {
|
||||
createVideoViewer({
|
||||
url
|
||||
})
|
||||
}}
|
||||
>
|
||||
预览
|
||||
</BaseButton>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTableColumn = (columnsChildren?: TableColumn[]) => {
|
||||
const {
|
||||
columns,
|
||||
reserveIndex,
|
||||
pageSize,
|
||||
currentPage,
|
||||
align,
|
||||
headerAlign,
|
||||
showOverflowTooltip,
|
||||
reserveSelection,
|
||||
imagePreview,
|
||||
videoPreview
|
||||
} = unref(getProps)
|
||||
|
||||
return (columnsChildren || columns).map((v) => {
|
||||
if (v.hidden) return null
|
||||
if (v.type === 'index') {
|
||||
return (
|
||||
<ElTableColumn
|
||||
type="index"
|
||||
index={
|
||||
v.index ? v.index : (index) => setIndex(reserveIndex, index, pageSize, currentPage)
|
||||
}
|
||||
align={v.align || align}
|
||||
headerAlign={v.headerAlign || headerAlign}
|
||||
label={v.label}
|
||||
fixed={v.fixed}
|
||||
width="65px"
|
||||
></ElTableColumn>
|
||||
)
|
||||
} else if (v.type === 'selection') {
|
||||
return (
|
||||
<ElTableColumn
|
||||
type="selection"
|
||||
reserveSelection={reserveSelection}
|
||||
align={align}
|
||||
headerAlign={headerAlign}
|
||||
selectable={v.selectable}
|
||||
width="50"
|
||||
></ElTableColumn>
|
||||
)
|
||||
} else {
|
||||
const props = { ...v } as any
|
||||
if (props.children) delete props.children
|
||||
|
||||
const children = v.children
|
||||
|
||||
const slots = {
|
||||
default: (...args: any[]) => {
|
||||
const data = args[0]
|
||||
|
||||
let isPreview = false
|
||||
isPreview =
|
||||
imagePreview.some((item) => (item as string) === v.field) ||
|
||||
videoPreview.some((item) => (item as string) === v.field)
|
||||
|
||||
return children && children.length
|
||||
? renderTreeTableColumn(children)
|
||||
: props?.slots?.default
|
||||
? props.slots.default(...args)
|
||||
: v?.formatter
|
||||
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
|
||||
: isPreview
|
||||
? renderPreview(get(data.row, v.field), v.field)
|
||||
: get(data.row, v.field)
|
||||
}
|
||||
}
|
||||
if (props?.slots?.header) {
|
||||
slots['header'] = (...args: any[]) => props.slots.header(...args)
|
||||
}
|
||||
|
||||
return (
|
||||
<ElTableColumn
|
||||
showOverflowTooltip={showOverflowTooltip}
|
||||
align={align}
|
||||
headerAlign={headerAlign}
|
||||
{...props}
|
||||
prop={v.field}
|
||||
>
|
||||
{slots}
|
||||
</ElTableColumn>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
const tableSlots = {}
|
||||
if (getSlot(slots, 'empty')) {
|
||||
tableSlots['empty'] = (...args: any[]) => getSlot(slots, 'empty', args)
|
||||
}
|
||||
if (getSlot(slots, 'append')) {
|
||||
tableSlots['append'] = (...args: any[]) => getSlot(slots, 'append', args)
|
||||
}
|
||||
|
||||
return (
|
||||
<div v-loading={unref(getProps).loading}>
|
||||
{unref(getProps).customContent ? (
|
||||
<div class="flex flex-wrap">
|
||||
{unref(getProps)?.data?.length ? (
|
||||
unref(getProps)?.data.map((item) => {
|
||||
const cardSlots = {
|
||||
default: () => {
|
||||
return getSlot(slots, 'content', item)
|
||||
}
|
||||
}
|
||||
if (getSlot(slots, 'content-header')) {
|
||||
cardSlots['header'] = () => {
|
||||
return getSlot(slots, 'content-header', item)
|
||||
}
|
||||
}
|
||||
if (getSlot(slots, 'content-footer')) {
|
||||
cardSlots['footer'] = () => {
|
||||
return getSlot(slots, 'content-footer', item)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ElCard
|
||||
shadow="hover"
|
||||
class={unref(getProps).cardWrapClass}
|
||||
style={unref(getProps).cardWrapStyle}
|
||||
bodyClass={unref(getProps).cardBodyClass}
|
||||
bodyStyle={unref(getProps).cardBodyStyle}
|
||||
>
|
||||
{cardSlots}
|
||||
</ElCard>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div class="flex flex-1 justify-center">
|
||||
<ElEmpty description="暂无数据" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{unref(getProps).showAction && !unref(getProps).customContent ? (
|
||||
<TableActions
|
||||
columns={unref(getProps).columns}
|
||||
onChangSize={changSize}
|
||||
onRefresh={refresh}
|
||||
onConfirm={confirmSetColumn}
|
||||
/>
|
||||
) : null}
|
||||
<ElTable ref={elTableRef} data={unref(getProps).data} {...unref(getBindValue)}>
|
||||
{{
|
||||
default: () => renderTableColumn(),
|
||||
...tableSlots
|
||||
}}
|
||||
</ElTable>
|
||||
</>
|
||||
)}
|
||||
{unref(getProps).pagination ? (
|
||||
<ElPagination
|
||||
v-model:pageSize={pageSizeRef.value}
|
||||
v-model:currentPage={currentPageRef.value}
|
||||
class="mt-10px"
|
||||
{...unref(pagination)}
|
||||
></ElPagination>
|
||||
) : undefined}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ElDrawer,
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElText,
|
||||
ElRadioButton,
|
||||
ElRadioGroup
|
||||
} from 'element-plus'
|
||||
import { TableColumn } from '../types'
|
||||
import { PropType, ref, watch, unref } from 'vue'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { DEFAULT_FILTER_COLUMN } from '@/constants'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
|
||||
const modelValue = defineModel<boolean>()
|
||||
|
||||
const props = defineProps({
|
||||
columns: {
|
||||
type: Array as PropType<TableColumn[]>,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['confirm'])
|
||||
|
||||
const oldColumns = ref<TableColumn[]>()
|
||||
|
||||
const settingColumns = ref<TableColumn[]>()
|
||||
|
||||
// 存储不要的列
|
||||
const hiddenColumns = ref<TableColumn[]>([])
|
||||
|
||||
const defaultCheckColumns = ref<string[]>([])
|
||||
const checkColumns = ref<string[]>([])
|
||||
|
||||
const checkAll = ref(false)
|
||||
const isIndeterminate = ref(true)
|
||||
const handleCheckAllChange = (val: boolean) => {
|
||||
checkColumns.value = val ? unref(defaultCheckColumns) : []
|
||||
isIndeterminate.value = false
|
||||
}
|
||||
|
||||
const handleCheckedColumnsChange = (value: string[]) => {
|
||||
const checkedCount = value.length
|
||||
checkAll.value = checkedCount === unref(defaultCheckColumns)?.length
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < unref(defaultCheckColumns)?.length
|
||||
}
|
||||
|
||||
const confirm = () => {
|
||||
const newColumns = cloneDeep(unref(settingColumns))?.map((item) => {
|
||||
const fixed = unref(settingColumns)?.find((col) => col.field === item.field)?.fixed
|
||||
item.hidden = !!!unref(checkColumns)?.includes(item.field)
|
||||
item.fixed = fixed ? fixed : undefined
|
||||
return item
|
||||
})
|
||||
emit('confirm', [...unref(hiddenColumns), ...(newColumns || [])])
|
||||
modelValue.value = false
|
||||
}
|
||||
|
||||
const restore = () => {
|
||||
initColumns([...unref(hiddenColumns), ...(unref(oldColumns) || [])], true)
|
||||
}
|
||||
|
||||
const initColumns = (columns: TableColumn[], isReStore = false) => {
|
||||
const newColumns = columns?.filter((item) => {
|
||||
if (!isReStore) {
|
||||
item.fixed = item.fixed !== void 0 ? item.fixed : undefined
|
||||
}
|
||||
return (item.type && !DEFAULT_FILTER_COLUMN.includes(item.type)) || !item.type
|
||||
})
|
||||
if (!unref(oldColumns)?.length) {
|
||||
oldColumns.value = cloneDeep(newColumns)
|
||||
}
|
||||
settingColumns.value = cloneDeep(newColumns)
|
||||
|
||||
hiddenColumns.value = cloneDeep(
|
||||
columns?.filter((item) => item.type && DEFAULT_FILTER_COLUMN.includes(item.type))
|
||||
)
|
||||
|
||||
defaultCheckColumns.value = unref(settingColumns)?.map((item) => item.field) || []
|
||||
checkColumns.value =
|
||||
unref(settingColumns)
|
||||
?.filter((item) => !item.hidden)
|
||||
?.map((item) => item.field) || []
|
||||
|
||||
if (unref(checkColumns)?.length === unref(defaultCheckColumns)?.length) {
|
||||
checkAll.value = true
|
||||
isIndeterminate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.columns,
|
||||
(columns) => {
|
||||
initColumns(columns)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDrawer v-model="modelValue" title="列设置" size="350px">
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-between">
|
||||
<ElCheckbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAllChange"
|
||||
/>
|
||||
<ElText class="ml-8px!">{{ checkColumns.length }} / {{ settingColumns?.length }}</ElText>
|
||||
</div>
|
||||
<ElText>固定 / 排序</ElText>
|
||||
</div>
|
||||
<div v-if="settingColumns?.length">
|
||||
<VueDraggable
|
||||
v-model="settingColumns"
|
||||
target=".el-checkbox-group"
|
||||
handle=".handle"
|
||||
:animation="150"
|
||||
>
|
||||
<ElCheckboxGroup
|
||||
ref="draggableWrap"
|
||||
v-model="checkColumns"
|
||||
@change="handleCheckedColumnsChange"
|
||||
>
|
||||
<div
|
||||
v-for="item in settingColumns"
|
||||
:key="item.field"
|
||||
class="flex items-center justify-between mt-12px"
|
||||
>
|
||||
<ElCheckbox :label="item.field">
|
||||
{{ item.label }}
|
||||
</ElCheckbox>
|
||||
<div class="flex items-center">
|
||||
<ElRadioGroup size="small" v-model="item.fixed">
|
||||
<ElRadioButton label="left">
|
||||
<Icon icon="ep:arrow-left" />
|
||||
</ElRadioButton>
|
||||
<ElRadioButton :label="undefined">
|
||||
<Icon icon="ep:close" />
|
||||
</ElRadioButton>
|
||||
<ElRadioButton label="right">
|
||||
<Icon icon="ep:arrow-right" />
|
||||
</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
|
||||
<div class="ml-12px cursor-move handle"><Icon icon="ep:rank" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCheckboxGroup>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div>
|
||||
<BaseButton @click="restore">还原</BaseButton>
|
||||
<BaseButton type="primary" @click="confirm">确定</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
</template>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="tsx">
|
||||
import { defineComponent, unref, computed, PropType, ref } from 'vue'
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem, ComponentSize } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { TableColumn } from '../types'
|
||||
import ColumnSetting from './ColumnSetting.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TableActions',
|
||||
components: {
|
||||
ColumnSetting
|
||||
},
|
||||
props: {
|
||||
columns: {
|
||||
type: Array as PropType<TableColumn[]>,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['refresh', 'changSize', 'confirm'],
|
||||
setup(props, { emit }) {
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
const sizeMap = computed(() => appStore.sizeMap)
|
||||
const showSetting = ref(false)
|
||||
|
||||
const refresh = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
const changSize = (size: ComponentSize) => {
|
||||
emit('changSize', size)
|
||||
}
|
||||
|
||||
const confirm = (columns: TableColumn[]) => {
|
||||
emit('confirm', columns)
|
||||
}
|
||||
|
||||
const showColumnSetting = () => {
|
||||
showSetting.value = true
|
||||
}
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div class="text-right h-28px flex items-center justify-end">
|
||||
<div title="刷新" class="w-30px h-20px flex items-center justify-end" onClick={refresh}>
|
||||
<Icon
|
||||
icon="ant-design:sync-outlined"
|
||||
class="cursor-pointer"
|
||||
hover-color="var(--el-color-primary)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ElDropdown trigger="click" onCommand={changSize}>
|
||||
{{
|
||||
default: () => {
|
||||
return (
|
||||
<div title="尺寸" class="w-30px h-20px flex items-center justify-end">
|
||||
<Icon
|
||||
icon="ant-design:column-height-outlined"
|
||||
class="cursor-pointer"
|
||||
hover-color="var(--el-color-primary)"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
dropdown: () => {
|
||||
return (
|
||||
<ElDropdownMenu>
|
||||
{{
|
||||
default: () => {
|
||||
return unref(sizeMap).map((v) => {
|
||||
return (
|
||||
<ElDropdownItem key={v} command={v}>
|
||||
{t(`size.${v}`)}
|
||||
</ElDropdownItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
}}
|
||||
</ElDropdownMenu>
|
||||
)
|
||||
}
|
||||
}}
|
||||
</ElDropdown>
|
||||
|
||||
<div
|
||||
title="列设置"
|
||||
class="w-30px h-20px flex items-center justify-end"
|
||||
onClick={showColumnSetting}
|
||||
>
|
||||
<Icon
|
||||
icon="ant-design:setting-outlined"
|
||||
class="cursor-pointer"
|
||||
hover-color="var(--el-color-primary)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ColumnSetting v-model={showSetting.value} columns={props.columns} onConfirm={confirm} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
export const setIndex = (reserveIndex: boolean, index: number, size: number, current: number) => {
|
||||
const newIndex = index + 1
|
||||
if (reserveIndex) {
|
||||
return size * (current - 1) + newIndex
|
||||
} else {
|
||||
return newIndex
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { TableProps as ElTableProps } from 'element-plus'
|
||||
export interface TableColumn {
|
||||
field: string
|
||||
label?: string
|
||||
type?: string
|
||||
/**
|
||||
* 是否隐藏
|
||||
*/
|
||||
hidden?: boolean
|
||||
children?: TableColumn[]
|
||||
slots?: {
|
||||
default?: (...args: any[]) => JSX.Element | JSX.Element[] | null
|
||||
header?: (...args: any[]) => JSX.Element | null
|
||||
}
|
||||
index?: number | ((index: number) => number)
|
||||
columnKey?: string
|
||||
width?: string | number
|
||||
minWidth?: string | number
|
||||
fixed?: boolean | 'left' | 'right'
|
||||
renderHeader?: (...args: any[]) => JSX.Element | null
|
||||
// sortable?: boolean
|
||||
sortMethod?: (...args: any[]) => number
|
||||
sortBy?: string | string[] | ((...args: any[]) => string | string[])
|
||||
sortOrders?: (string | null)[]
|
||||
resizable?: boolean
|
||||
formatter?: (...args: any[]) => any
|
||||
showOverflowTooltip?: boolean
|
||||
align?: 'left' | 'center' | 'right'
|
||||
headerAlign?: 'left' | 'center' | 'right'
|
||||
className?: string
|
||||
labelClassName?: string
|
||||
selectable?: (...args: any[]) => boolean
|
||||
reserveSelection?: boolean
|
||||
filters?: Array<{ text: string; value: string }>
|
||||
filterPlacement?: string
|
||||
filterMultiple?: boolean
|
||||
filterMethod?: (...args: any[]) => boolean
|
||||
filteredValue?: string[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface TableSlotDefault {
|
||||
row: Recordable
|
||||
column: TableColumn
|
||||
$index: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface Pagination {
|
||||
small?: boolean
|
||||
background?: boolean
|
||||
pageSize?: number
|
||||
defaultPageSize?: number
|
||||
total?: number
|
||||
pageCount?: number
|
||||
pagerCount?: number
|
||||
currentPage?: number
|
||||
defaultCurrentPage?: number
|
||||
layout?: string
|
||||
pageSizes?: number[]
|
||||
popperClass?: string
|
||||
prevText?: string
|
||||
nextText?: string
|
||||
disabled?: boolean
|
||||
hideOnSinglePage?: boolean
|
||||
}
|
||||
|
||||
export interface TableSetProps {
|
||||
field: string
|
||||
path: string
|
||||
value: any
|
||||
}
|
||||
|
||||
export interface TableProps extends Omit<Partial<ElTableProps<any[]>>, 'data'> {
|
||||
pageSize?: number
|
||||
currentPage?: number
|
||||
showAction?: boolean
|
||||
// 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip,
|
||||
showOverflowTooltip?: boolean
|
||||
// 表头
|
||||
columns?: TableColumn[]
|
||||
// 是否展示分页
|
||||
pagination?: Pagination | undefined
|
||||
// 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key)
|
||||
reserveSelection?: boolean
|
||||
// 加载状态
|
||||
loading?: boolean
|
||||
// 是否叠加索引
|
||||
reserveIndex?: boolean
|
||||
// 对齐方式
|
||||
align?: 'left' | 'center' | 'right'
|
||||
// 表头对齐方式
|
||||
headerAlign?: 'left' | 'center' | 'right'
|
||||
preview?: string[]
|
||||
sortable?: boolean
|
||||
data?: Recordable
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import TagsView from './src/TagsView.vue'
|
||||
|
||||
export { TagsView }
|
||||
@@ -0,0 +1,585 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch, computed, unref, ref, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { filterAffixTags } from './helper'
|
||||
import { ContextMenu, ContextMenuExpose } from '@/components/ContextMenu'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useTemplateRefsList } from '@vueuse/core'
|
||||
import { ElScrollbar } from 'element-plus'
|
||||
import { useScrollTo } from '@/hooks/event/useScrollTo'
|
||||
import { useTagsView } from '@/hooks/web/useTagsView'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('tags-view')
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { currentRoute, push } = useRouter()
|
||||
|
||||
const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTagsView()
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const routers = computed(() => permissionStore.getRouters)
|
||||
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
|
||||
const visitedViews = computed(() => tagsViewStore.getVisitedViews)
|
||||
|
||||
const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
|
||||
|
||||
const selectedTag = computed(() => tagsViewStore.getSelectedTag)
|
||||
|
||||
const setSelectTag = tagsViewStore.setSelectedTag
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
|
||||
|
||||
const isDark = computed(() => appStore.getIsDark)
|
||||
|
||||
// 初始化tag
|
||||
const initTags = () => {
|
||||
affixTagArr.value = filterAffixTags(unref(routers))
|
||||
for (const tag of unref(affixTagArr)) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
tagsViewStore.addVisitedView(cloneDeep(tag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增tag
|
||||
const addTags = () => {
|
||||
const { name } = unref(currentRoute)
|
||||
if (name) {
|
||||
setSelectTag(unref(currentRoute))
|
||||
tagsViewStore.addView(unref(currentRoute))
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭选中的tag
|
||||
const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
|
||||
closeCurrent(view, () => {
|
||||
if (isActive(view)) {
|
||||
toLastView()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 去最后一个
|
||||
const toLastView = () => {
|
||||
const visitedViews = tagsViewStore.getVisitedViews
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView) {
|
||||
push(latestView)
|
||||
} else {
|
||||
if (
|
||||
unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
|
||||
unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
|
||||
) {
|
||||
addTags()
|
||||
return
|
||||
}
|
||||
// You can set another route
|
||||
push(permissionStore.getAddRouters[0].path)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭全部
|
||||
const closeAllTags = () => {
|
||||
closeAll(() => {
|
||||
toLastView()
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭其它
|
||||
const closeOthersTags = () => {
|
||||
closeOther()
|
||||
}
|
||||
|
||||
// 重新加载
|
||||
const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
|
||||
refreshPage(view)
|
||||
}
|
||||
|
||||
// 关闭左侧
|
||||
const closeLeftTags = () => {
|
||||
closeLeft()
|
||||
}
|
||||
|
||||
// 关闭右侧
|
||||
const closeRightTags = () => {
|
||||
closeRight()
|
||||
}
|
||||
|
||||
// 滚动到选中的tag
|
||||
const moveToCurrentTag = async () => {
|
||||
await nextTick()
|
||||
for (const v of unref(visitedViews)) {
|
||||
if (v.fullPath === unref(currentRoute).path) {
|
||||
moveToTarget(v)
|
||||
if (v.fullPath !== unref(currentRoute).fullPath) {
|
||||
tagsViewStore.updateVisitedView(unref(currentRoute))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
|
||||
|
||||
const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
|
||||
const wrap$ = unref(scrollbarRef)?.wrapRef
|
||||
let firstTag: Nullable<RouterLinkProps> = null
|
||||
let lastTag: Nullable<RouterLinkProps> = null
|
||||
|
||||
const tagList = unref(tagLinksRefs)
|
||||
// find first tag and last tag
|
||||
if (tagList.length > 0) {
|
||||
firstTag = tagList[0]
|
||||
lastTag = tagList[tagList.length - 1]
|
||||
}
|
||||
if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
|
||||
// 直接滚动到0的位置
|
||||
const { start } = useScrollTo({
|
||||
el: wrap$!,
|
||||
position: 'scrollLeft',
|
||||
to: 0,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
} else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
|
||||
// 滚动到最后的位置
|
||||
const { start } = useScrollTo({
|
||||
el: wrap$!,
|
||||
position: 'scrollLeft',
|
||||
to: wrap$!.scrollWidth - wrap$!.offsetWidth,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
} else {
|
||||
// find preTag and nextTag
|
||||
const currentIndex: number = tagList.findIndex(
|
||||
(item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath
|
||||
)
|
||||
const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
|
||||
|
||||
const prevTag = tgsRefs[currentIndex - 1] as HTMLElement
|
||||
const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
|
||||
|
||||
if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
|
||||
const { start } = useScrollTo({
|
||||
el: wrap$!,
|
||||
position: 'scrollLeft',
|
||||
to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
} else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
|
||||
const { start } = useScrollTo({
|
||||
el: wrap$!,
|
||||
position: 'scrollLeft',
|
||||
to: beforePrevTagOffsetLeft,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 是否是当前tag
|
||||
const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
|
||||
return route.path === unref(currentRoute).path
|
||||
}
|
||||
|
||||
// 所有右键菜单组件的元素
|
||||
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
|
||||
|
||||
// 右键菜单状态改变的时候
|
||||
const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
|
||||
if (visible) {
|
||||
for (const v of unref(itemRefs)) {
|
||||
const elDropdownMenuRef = v.elDropdownMenuRef
|
||||
if (tagItem.fullPath !== v.tagItem.fullPath) {
|
||||
elDropdownMenuRef?.handleClose()
|
||||
setSelectTag(tagItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// elscroll 实例
|
||||
const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
|
||||
|
||||
// 保存滚动位置
|
||||
const scrollLeftNumber = ref(0)
|
||||
|
||||
const scroll = ({ scrollLeft }) => {
|
||||
scrollLeftNumber.value = scrollLeft as number
|
||||
}
|
||||
|
||||
// 移动到某个位置
|
||||
const move = (to: number) => {
|
||||
const wrap$ = unref(scrollbarRef)?.wrapRef
|
||||
const { start } = useScrollTo({
|
||||
el: wrap$!,
|
||||
position: 'scrollLeft',
|
||||
to: unref(scrollLeftNumber) + to,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags()
|
||||
addTags()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
() => {
|
||||
addTags()
|
||||
moveToCurrentTag()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:id="prefixCls"
|
||||
:class="prefixCls"
|
||||
class="flex w-full relative bg-[#fff] dark:bg-[var(--el-bg-color)]"
|
||||
>
|
||||
<span
|
||||
:class="`${prefixCls}__tool ${prefixCls}__tool--first`"
|
||||
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex items-center justify-center cursor-pointer"
|
||||
@click="move(-200)"
|
||||
>
|
||||
<Icon
|
||||
icon="ep:d-arrow-left"
|
||||
color="var(--el-text-color-placeholder)"
|
||||
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
|
||||
/>
|
||||
</span>
|
||||
<div class="overflow-hidden flex-1">
|
||||
<ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
|
||||
<div class="flex h-full">
|
||||
<ContextMenu
|
||||
:ref="itemRefs.set"
|
||||
:schema="[
|
||||
{
|
||||
icon: 'ant-design:sync-outlined',
|
||||
label: t('common.reload'),
|
||||
disabled: selectedTag?.fullPath !== item.fullPath,
|
||||
command: () => {
|
||||
refreshSelectedTag(item)
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:close-outlined',
|
||||
label: t('common.closeTab'),
|
||||
disabled: !!visitedViews?.length && selectedTag?.meta.affix,
|
||||
command: () => {
|
||||
closeSelectedTag(item)
|
||||
}
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ant-design:vertical-right-outlined',
|
||||
label: t('common.closeTheLeftTab'),
|
||||
disabled:
|
||||
!!visitedViews?.length &&
|
||||
(item.fullPath === visitedViews[0].fullPath ||
|
||||
selectedTag?.fullPath !== item.fullPath),
|
||||
command: () => {
|
||||
closeLeftTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:vertical-left-outlined',
|
||||
label: t('common.closeTheRightTab'),
|
||||
disabled:
|
||||
!!visitedViews?.length &&
|
||||
(item.fullPath === visitedViews[visitedViews.length - 1].fullPath ||
|
||||
selectedTag?.fullPath !== item.fullPath),
|
||||
command: () => {
|
||||
closeRightTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ant-design:tag-outlined',
|
||||
label: t('common.closeOther'),
|
||||
disabled: selectedTag?.fullPath !== item.fullPath,
|
||||
command: () => {
|
||||
closeOthersTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:line-outlined',
|
||||
label: t('common.closeAll'),
|
||||
command: () => {
|
||||
closeAllTags()
|
||||
}
|
||||
}
|
||||
]"
|
||||
v-for="item in visitedViews"
|
||||
:key="item.fullPath"
|
||||
:tag-item="item"
|
||||
:class="[
|
||||
`${prefixCls}__item`,
|
||||
item?.meta?.affix ? `${prefixCls}__item--affix` : '',
|
||||
{
|
||||
'is-active': isActive(item)
|
||||
}
|
||||
]"
|
||||
@visible-change="visibleChange"
|
||||
>
|
||||
<div>
|
||||
<router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }">
|
||||
<div
|
||||
@click="navigate"
|
||||
class="h-full flex justify-center items-center whitespace-nowrap pl-15px"
|
||||
>
|
||||
<Icon
|
||||
v-if="
|
||||
item?.matched &&
|
||||
item?.matched[1] &&
|
||||
item?.matched[1]?.meta?.icon &&
|
||||
tagsViewIcon
|
||||
"
|
||||
:icon="item?.matched[1]?.meta?.icon"
|
||||
:size="12"
|
||||
class="mr-5px"
|
||||
/>
|
||||
{{ t(item?.meta?.title as string) }}
|
||||
<Icon
|
||||
:class="`${prefixCls}__item--close`"
|
||||
color="#333"
|
||||
icon="ant-design:close-outlined"
|
||||
:size="12"
|
||||
@click.prevent.stop="closeSelectedTag(item)"
|
||||
/>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
<span
|
||||
:class="`${prefixCls}__tool`"
|
||||
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex items-center justify-center cursor-pointer"
|
||||
@click="move(200)"
|
||||
>
|
||||
<Icon
|
||||
icon="ep:d-arrow-right"
|
||||
color="var(--el-text-color-placeholder)"
|
||||
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
:class="`${prefixCls}__tool`"
|
||||
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex items-center justify-center cursor-pointer"
|
||||
@click="refreshSelectedTag(selectedTag)"
|
||||
>
|
||||
<Icon
|
||||
icon="ant-design:reload-outlined"
|
||||
color="var(--el-text-color-placeholder)"
|
||||
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
|
||||
/>
|
||||
</span>
|
||||
<ContextMenu
|
||||
trigger="click"
|
||||
:schema="[
|
||||
{
|
||||
icon: 'ant-design:sync-outlined',
|
||||
label: t('common.reload'),
|
||||
command: () => {
|
||||
refreshSelectedTag(selectedTag)
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:close-outlined',
|
||||
label: t('common.closeTab'),
|
||||
disabled: !!visitedViews?.length && selectedTag?.meta.affix,
|
||||
command: () => {
|
||||
closeSelectedTag(selectedTag!)
|
||||
}
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ant-design:vertical-right-outlined',
|
||||
label: t('common.closeTheLeftTab'),
|
||||
disabled: !!visitedViews?.length && selectedTag?.fullPath === visitedViews[0].fullPath,
|
||||
command: () => {
|
||||
closeLeftTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:vertical-left-outlined',
|
||||
label: t('common.closeTheRightTab'),
|
||||
disabled:
|
||||
!!visitedViews?.length &&
|
||||
selectedTag?.fullPath === visitedViews[visitedViews.length - 1].fullPath,
|
||||
command: () => {
|
||||
closeRightTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ant-design:tag-outlined',
|
||||
label: t('common.closeOther'),
|
||||
command: () => {
|
||||
closeOthersTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:line-outlined',
|
||||
label: t('common.closeAll'),
|
||||
command: () => {
|
||||
closeAllTags()
|
||||
}
|
||||
}
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="`${prefixCls}__tool`"
|
||||
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex items-center justify-center cursor-pointer block"
|
||||
>
|
||||
<Icon
|
||||
icon="ant-design:setting-outlined"
|
||||
color="var(--el-text-color-placeholder)"
|
||||
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
|
||||
/>
|
||||
</span>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-tags-view';
|
||||
|
||||
.@{prefix-cls} {
|
||||
:deep(.@{elNamespace}-scrollbar__view) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__tool {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(~'100% - 1px');
|
||||
border-left: 1px solid var(--el-border-color);
|
||||
content: '';
|
||||
}
|
||||
|
||||
&--first {
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(~'100% - 1px');
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
border-left: none;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
height: calc(~'100% - 6px');
|
||||
padding-right: 25px;
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
|
||||
&--close {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 5px;
|
||||
display: none;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
&:not(.@{prefix-cls}__item--affix):hover {
|
||||
.@{prefix-cls}__item--close {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item:not(.is-active) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__item.is-active {
|
||||
color: var(--el-color-white);
|
||||
background-color: var(--el-color-primary);
|
||||
border: 1px solid var(--el-color-primary);
|
||||
.@{prefix-cls}__item--close {
|
||||
:deep(svg) {
|
||||
color: var(--el-color-white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.@{prefix-cls} {
|
||||
&__tool {
|
||||
&--first {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
&__item:not(.is-active) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__item.is-active {
|
||||
color: var(--el-color-white);
|
||||
background-color: var(--el-color-primary);
|
||||
border: 1px solid var(--el-color-primary);
|
||||
.@{prefix-cls}__item--close {
|
||||
:deep(svg) {
|
||||
color: var(--el-color-white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { pathResolve } from '@/utils/routerHelper'
|
||||
|
||||
export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => {
|
||||
let tags: RouteLocationNormalizedLoaded[] = []
|
||||
routes.forEach((route) => {
|
||||
const meta = route.meta as RouteMeta
|
||||
const tagPath = pathResolve(parentPath, route.path)
|
||||
if (meta?.affix) {
|
||||
tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded)
|
||||
}
|
||||
if (route.children) {
|
||||
const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath)
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return tags
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import ThemeSwitch from './src/ThemeSwitch.vue'
|
||||
|
||||
export { ThemeSwitch }
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { ElSwitch } from 'element-plus'
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const prefixCls = getPrefixCls('theme-switch')
|
||||
|
||||
const Sun = useIcon({ icon: 'emojione-monotone:sun', color: '#fde047' })
|
||||
|
||||
const CrescentMoon = useIcon({ icon: 'emojione-monotone:crescent-moon', color: '#fde047' })
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 初始化获取是否是暗黑主题
|
||||
const isDark = ref(appStore.getIsDark)
|
||||
|
||||
// 设置switch的背景颜色
|
||||
const blackColor = 'var(--el-color-black)'
|
||||
|
||||
const themeChange = (val: boolean) => {
|
||||
appStore.setIsDark(val)
|
||||
emit('change', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElSwitch
|
||||
:class="prefixCls"
|
||||
v-model="isDark"
|
||||
inline-prompt
|
||||
:border-color="blackColor"
|
||||
:inactive-color="blackColor"
|
||||
:active-color="blackColor"
|
||||
:active-icon="Sun"
|
||||
:inactive-icon="CrescentMoon"
|
||||
@change="themeChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.el-switch__core .el-switch__inner .is-icon) {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import UserInfo from './src/UserInfo.vue'
|
||||
|
||||
export { UserInfo }
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import LockDialog from './components/LockDialog.vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import LockPage from './components/LockPage.vue'
|
||||
import { useLockStore } from '@/store/modules/lock'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const lockStore = useLockStore()
|
||||
|
||||
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('user-info')
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const loginOut = () => {
|
||||
userStore.logoutConfirm()
|
||||
}
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
|
||||
// 锁定屏幕
|
||||
const lockScreen = () => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDropdown class="custom-hover" :class="prefixCls" trigger="click">
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
src="@/assets/imgs/avatar.jpg"
|
||||
alt=""
|
||||
class="w-[calc(var(--logo-height)-25px)] rounded-[50%]"
|
||||
/>
|
||||
<span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">{{
|
||||
userStore.getUserInfo?.username
|
||||
}}</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem>
|
||||
<div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem>
|
||||
<div @click="loginOut">{{ t('common.loginOut') }}</div>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
|
||||
<LockDialog v-if="dialogVisible" v-model="dialogVisible" />
|
||||
<teleport to="body">
|
||||
<transition name="fade-bottom" mode="out-in">
|
||||
<LockPage v-if="getIsLock" />
|
||||
</transition>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.fade-bottom-enter-active,
|
||||
.fade-bottom-leave-active {
|
||||
transition:
|
||||
opacity 0.25s,
|
||||
transform 0.3s;
|
||||
}
|
||||
|
||||
.fade-bottom-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10%);
|
||||
}
|
||||
|
||||
.fade-bottom-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { ref } from 'vue'
|
||||
import { Dialog } from '@/components/Dialog'
|
||||
import { Form } from '@/components/Form'
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { reactive, computed } from 'vue'
|
||||
import { useValidator } from '@/hooks/web/useValidator'
|
||||
import { FormSchema } from '@/components/Form'
|
||||
import { ElButton } from 'element-plus'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useLockStore } from '@/store/modules/lock'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('lock-dialog')
|
||||
|
||||
const { required } = useValidator()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const lockStore = useLockStore()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => {
|
||||
console.log('set: ', val)
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
})
|
||||
|
||||
const dialogTitle = ref(t('lock.lockScreen'))
|
||||
|
||||
const rules = reactive({
|
||||
password: [required()]
|
||||
})
|
||||
|
||||
const schema: FormSchema[] = reactive([
|
||||
{
|
||||
label: t('lock.lockPassword'),
|
||||
field: 'password',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'password',
|
||||
showPassword: true
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const { formRegister, formMethods } = useForm()
|
||||
|
||||
const { getFormData, getElFormExpose } = formMethods
|
||||
|
||||
const handleLock = async () => {
|
||||
const formExpose = await getElFormExpose()
|
||||
formExpose?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
dialogVisible.value = false
|
||||
const formData = await getFormData()
|
||||
lockStore.setLockInfo({
|
||||
isLock: true,
|
||||
...formData
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="dialogVisible"
|
||||
width="500px"
|
||||
max-height="170px"
|
||||
:class="prefixCls"
|
||||
:title="dialogTitle"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<img src="@/assets/imgs/avatar.jpg" alt="" class="w-70px h-70px rounded-[50%]" />
|
||||
<span class="text-14px my-10px text-[var(--top-header-text-color)]">Archer</span>
|
||||
</div>
|
||||
<Form :is-col="false" :schema="schema" :rules="rules" @register="formRegister" />
|
||||
<template #footer>
|
||||
<ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:global(.v-lock-dialog) {
|
||||
@media (width <= 767px) {
|
||||
max-width: calc(100vw - 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,272 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElInput, ElButton } from 'element-plus'
|
||||
import { resetRouter } from '@/router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStorage } from '@/hooks/web/useStorage'
|
||||
import { useLockStore } from '@/store/modules/lock'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useNow } from '@/hooks/web/useNow'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { loginOutApi } from '@/api/login'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
|
||||
const { clear } = useStorage()
|
||||
|
||||
const { replace } = useRouter()
|
||||
|
||||
const password = ref('')
|
||||
const loading = ref(false)
|
||||
const errMsg = ref(false)
|
||||
const showDate = ref(true)
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('lock-page')
|
||||
|
||||
const lockStore = useLockStore()
|
||||
|
||||
const { hour, month, minute, meridiem, year, day, week } = useNow(true)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 解锁
|
||||
async function unLock() {
|
||||
if (!password.value) {
|
||||
return
|
||||
}
|
||||
let pwd = password.value
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await lockStore.unLock(pwd)
|
||||
errMsg.value = !res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回登录
|
||||
async function goLogin() {
|
||||
const res = await loginOutApi().catch(() => {})
|
||||
if (res) {
|
||||
clear()
|
||||
tagsViewStore.delAllViews()
|
||||
resetRouter() // 重置静态路由表
|
||||
lockStore.resetLockInfo()
|
||||
replace('/login')
|
||||
}
|
||||
}
|
||||
|
||||
function handleShowForm(show = false) {
|
||||
showDate.value = show
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="prefixCls"
|
||||
class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
|
||||
>
|
||||
<div
|
||||
:class="`${prefixCls}__unlock`"
|
||||
class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
|
||||
@click="handleShowForm(false)"
|
||||
v-show="showDate"
|
||||
>
|
||||
<Icon icon="ep:lock" />
|
||||
<span>{{ t('lock.unlock') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex w-screen h-screen justify-center items-center">
|
||||
<div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
|
||||
<span>{{ hour }}</span>
|
||||
<span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
|
||||
{{ meridiem }}
|
||||
</span>
|
||||
</div>
|
||||
<div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
|
||||
<span> {{ minute }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="fade-slide">
|
||||
<div :class="`${prefixCls}-entry`" v-show="!showDate">
|
||||
<div :class="`${prefixCls}-entry-content`">
|
||||
<div class="flex flex-col items-center">
|
||||
<img src="@/assets/imgs/avatar.jpg" alt="" class="w-70px h-70px rounded-[50%]" />
|
||||
<span class="text-14px my-10px text-[var(--logo-title-text-color)]">Archer</span>
|
||||
</div>
|
||||
<ElInput
|
||||
type="password"
|
||||
:placeholder="t('lock.placeholder')"
|
||||
class="enter-x"
|
||||
v-model="password"
|
||||
/>
|
||||
<span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
|
||||
{{ t('lock.message') }}
|
||||
</span>
|
||||
<div :class="`${prefixCls}-entry__footer enter-x`">
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="small"
|
||||
class="mt-2 mr-2 enter-x"
|
||||
link
|
||||
:disabled="loading"
|
||||
@click="handleShowForm(true)"
|
||||
>
|
||||
{{ t('common.back') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="small"
|
||||
class="mt-2 mr-2 enter-x"
|
||||
link
|
||||
:disabled="loading"
|
||||
@click="goLogin"
|
||||
>
|
||||
{{ t('lock.backToLogin') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
class="mt-2"
|
||||
size="small"
|
||||
link
|
||||
@click="unLock()"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ t('lock.entrySystem') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
|
||||
<div class="text-5xl mb-4 enter-x" v-show="!showDate">
|
||||
{{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
|
||||
</div>
|
||||
<div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-lock-page';
|
||||
|
||||
// Small screen / tablet
|
||||
@screen-sm: 576px;
|
||||
|
||||
// Medium screen / desktop
|
||||
@screen-md: 768px;
|
||||
|
||||
// Large screen / wide desktop
|
||||
@screen-lg: 992px;
|
||||
|
||||
// Extra large screen / full hd
|
||||
@screen-xl: 1200px;
|
||||
|
||||
// Extra extra large screen / large desktop
|
||||
@screen-2xl: 1600px;
|
||||
|
||||
@error-color: #ed6f6f;
|
||||
|
||||
.@{prefix-cls} {
|
||||
z-index: 3000;
|
||||
|
||||
&__unlock {
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
&__hour,
|
||||
&__minute {
|
||||
display: flex;
|
||||
font-weight: 700;
|
||||
color: #bababa;
|
||||
background-color: #141313;
|
||||
border-radius: 30px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (max-width: @screen-md) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: @screen-md) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-sm) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: @screen-lg) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: @screen-xl) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: @screen-2xl) {
|
||||
span:not(.meridiem) {
|
||||
font-size: 320px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-entry {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
backdrop-filter: blur(8px);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-content {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
text-align: center;
|
||||
|
||||
&-img {
|
||||
width: 70px;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&-name {
|
||||
margin-top: 5px;
|
||||
font-weight: 500;
|
||||
color: #bababa;
|
||||
}
|
||||
}
|
||||
|
||||
&__err-msg {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { VNode, createVNode, render } from 'vue'
|
||||
import VideoPlayer from './src/VideoPlayer.vue'
|
||||
import { isClient } from '@/utils/is'
|
||||
import { VideoPlayerViewer } from '@/components/VideoPlayerViewer'
|
||||
import { toAnyString } from '@/utils'
|
||||
|
||||
export { VideoPlayer }
|
||||
|
||||
let instance: Nullable<VNode> = null
|
||||
|
||||
export function createVideoViewer(options: { url: string; poster?: string; show?: boolean }) {
|
||||
if (!isClient) return
|
||||
const { url, poster } = options
|
||||
|
||||
const propsData: Partial<{ url: string; poster?: string; show?: boolean; id?: string }> = {}
|
||||
const container = document.createElement('div')
|
||||
const id = toAnyString()
|
||||
container.id = id
|
||||
propsData.url = url
|
||||
propsData.poster = poster
|
||||
propsData.show = true
|
||||
propsData.id = id
|
||||
|
||||
document.body.appendChild(container)
|
||||
instance = createVNode(VideoPlayerViewer, propsData)
|
||||
render(instance, container)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import Player from 'xgplayer'
|
||||
import { ref, unref, onMounted, watch, onBeforeUnmount, nextTick } from 'vue'
|
||||
import 'xgplayer/dist/index.min.css'
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
poster: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const playerRef = ref<Player>()
|
||||
|
||||
const videoEl = ref<HTMLDivElement>()
|
||||
|
||||
const intiPlayer = () => {
|
||||
if (!unref(videoEl)) return
|
||||
new Player({
|
||||
autoplay: false,
|
||||
...props,
|
||||
el: unref(videoEl)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
intiPlayer()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
async (newProps) => {
|
||||
await nextTick()
|
||||
if (newProps) {
|
||||
unref(playerRef)?.setConfig(newProps)
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unref(playerRef)?.destroy()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
playerExpose: () => unref(playerRef)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="videoEl"></div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
import VideoPlayerViewer from './src/VideoPlayerViewer.vue'
|
||||
|
||||
export { VideoPlayerViewer }
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { VideoPlayer } from '@/components/VideoPlayer'
|
||||
import { ElOverlay } from 'element-plus'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { Icon } from '@/components/Icon'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
poster: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const visible = ref(props.show)
|
||||
|
||||
const close = async () => {
|
||||
visible.value = false
|
||||
await nextTick()
|
||||
const wrap = document.getElementById(props.id)
|
||||
if (!wrap) return
|
||||
document.body.removeChild(wrap)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ElOverlay v-show="visible" @click="close">
|
||||
<div class="w-full h-full flex justify-center items-center relative" @click="close">
|
||||
<div
|
||||
class="w-44px h-44px color-[#fff] bg-[var(--el-text-color-regular)] rounded-full border-[#fff] flex justify-center items-center cursor-pointer absolute top-40px right-40px"
|
||||
@click="close"
|
||||
>
|
||||
<Icon icon="ep:close" :size="24" />
|
||||
</div>
|
||||
<VideoPlayer :url="url" :poster="poster" />
|
||||
</div>
|
||||
</ElOverlay>
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { App } from 'vue'
|
||||
import { Icon } from './Icon'
|
||||
import { BaseButton } from './Button'
|
||||
|
||||
export const setupGlobCom = (app: App<Element>): void => {
|
||||
app.component('Icon', Icon)
|
||||
app.component('BaseButton', BaseButton)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 请求成功状态码
|
||||
*/
|
||||
export const SUCCESS_CODE = 0
|
||||
|
||||
/**
|
||||
* 请求contentType
|
||||
*/
|
||||
export const CONTENT_TYPE: AxiosContentType = 'application/json'
|
||||
|
||||
/**
|
||||
* 请求超时时间
|
||||
*/
|
||||
export const REQUEST_TIMEOUT = 60000
|
||||
|
||||
/**
|
||||
* 不重定向白名单
|
||||
*/
|
||||
export const NO_REDIRECT_WHITE_LIST = ['/login']
|
||||
|
||||
/**
|
||||
* 不重置路由白名单
|
||||
*/
|
||||
export const NO_RESET_WHITE_LIST = ['Redirect', 'RedirectParent', 'Login', 'NoFind', 'Root']
|
||||
|
||||
/**
|
||||
* 表格默认过滤列设置字段
|
||||
*/
|
||||
export const DEFAULT_FILTER_COLUMN = ['expand', 'selection']
|
||||
|
||||
/**
|
||||
* 是否根据headers->content-type自动转换数据格式
|
||||
*/
|
||||
export const TRANSFORM_REQUEST_DATA = true
|
||||
Loaded 100 of 238 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user