initial commit
42
src/App.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<ConfigProvider
|
||||
:locale="zhCN"
|
||||
:renderEmpty="renderEmpty"
|
||||
:transformCellText="transformCellText"
|
||||
v-bind="lockOn"
|
||||
>
|
||||
<router-view />
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { ConfigProvider } from 'ant-design-vue';
|
||||
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
|
||||
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN';
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/zh-cn';
|
||||
|
||||
import { useConfigProvider, useInitAppConfigStore, useListenerNetWork } from './useApp';
|
||||
import { useLockPage } from '/@/hooks/web/useLockPage';
|
||||
moment.locale('zh-cn');
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: { ConfigProvider },
|
||||
setup() {
|
||||
useInitAppConfigStore();
|
||||
useListenerNetWork();
|
||||
createBreakpointListen();
|
||||
const { renderEmpty, transformCellText } = useConfigProvider();
|
||||
const { on: lockOn } = useLockPage();
|
||||
|
||||
return {
|
||||
renderEmpty,
|
||||
transformCellText,
|
||||
zhCN,
|
||||
lockOn,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
18
src/api/sys/menu.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
import { getMenuListByIdParams, getMenuListByIdParamsResultModel } from './model/menuModel';
|
||||
|
||||
enum Api {
|
||||
GetMenuListById = '/getMenuListById',
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 根据id获取用户菜单
|
||||
*/
|
||||
export function getMenuListById(params: getMenuListByIdParams) {
|
||||
return defHttp.request<getMenuListByIdParamsResultModel>({
|
||||
url: Api.GetMenuListById,
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
23
src/api/sys/model/menuModel.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { RouteMeta } from '/@/router/types';
|
||||
export interface RouteItem {
|
||||
path: string;
|
||||
component: any;
|
||||
meta: RouteMeta;
|
||||
name?: string;
|
||||
alias?: string | string[];
|
||||
redirect?: string;
|
||||
caseSensitive?: boolean;
|
||||
children?: RouteItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取菜单接口
|
||||
*/
|
||||
export interface getMenuListByIdParams {
|
||||
id: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取菜单返回值
|
||||
*/
|
||||
export type getMenuListByIdParamsResultModel = RouteItem[];
|
43
src/api/sys/model/userModel.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @description: Login interface parameters
|
||||
*/
|
||||
export interface LoginParams {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Get user information
|
||||
*/
|
||||
export interface GetUserInfoByUserIdParams {
|
||||
userId: string | number;
|
||||
}
|
||||
|
||||
export interface RoleInfo {
|
||||
roleName: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Login interface return value
|
||||
*/
|
||||
export interface LoginResultModel {
|
||||
userId: string | number;
|
||||
token: string;
|
||||
role: RoleInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Get user information return value
|
||||
*/
|
||||
export interface GetUserInfoByUserIdModel {
|
||||
role: RoleInfo;
|
||||
// 用户id
|
||||
userId: string | number;
|
||||
// 用户名
|
||||
username: string;
|
||||
// 真实名字
|
||||
realName: string;
|
||||
// 介绍
|
||||
desc?: string;
|
||||
}
|
48
src/api/sys/user.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import {
|
||||
LoginParams,
|
||||
LoginResultModel,
|
||||
GetUserInfoByUserIdParams,
|
||||
GetUserInfoByUserIdModel,
|
||||
} from './model/userModel';
|
||||
|
||||
enum Api {
|
||||
Login = '/login',
|
||||
GetUserInfoById = '/getUserInfoById',
|
||||
GetPermCodeByUserId = '/getPermCodeByUserId',
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: user login api
|
||||
*/
|
||||
export function loginApi(params: LoginParams) {
|
||||
return defHttp.request<LoginResultModel>(
|
||||
{
|
||||
url: Api.Login,
|
||||
method: 'POST',
|
||||
params,
|
||||
},
|
||||
{
|
||||
errorMessageMode: 'modal',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: getUserInfoById
|
||||
*/
|
||||
export function getUserInfoById(params: GetUserInfoByUserIdParams) {
|
||||
return defHttp.request<GetUserInfoByUserIdModel>({
|
||||
url: Api.GetUserInfoById,
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function getPermCodeByUserId(params: GetUserInfoByUserIdParams) {
|
||||
return defHttp.request<string[]>({
|
||||
url: Api.GetPermCodeByUserId,
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
BIN
src/assets/images/dashboard/wokb/approve.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/images/dashboard/wokb/attendance.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/images/dashboard/wokb/datashow1.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
src/assets/images/dashboard/wokb/datashow2.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/images/dashboard/wokb/datashow3.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
src/assets/images/dashboard/wokb/datashow4.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/assets/images/dashboard/wokb/leave.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src/assets/images/dashboard/wokb/meal.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/images/dashboard/wokb/overtime.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/assets/images/dashboard/wokb/performance.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/images/dashboard/wokb/stamp.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/images/dashboard/wokb/travel.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/images/dashboard/wokb/wokb.png
Normal file
After Width: | Height: | Size: 190 KiB |
BIN
src/assets/images/exception/404.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/exception/500.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/exception/net-work.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
src/assets/images/header.jpg
Normal file
After Width: | Height: | Size: 1.0 MiB |
39
src/assets/images/layout/menu-mix.svg
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
|
||||
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
|
||||
<g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
|
||||
<g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
|
||||
<mask id="mask-3" fill="white">
|
||||
<use xlink:href="#path-2"></use>
|
||||
</mask>
|
||||
<g id="Rectangle-18">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
|
||||
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
|
||||
</g>
|
||||
<rect id="Rectangle-18" fill="#fff" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
|
||||
<rect id="Rectangle-11" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
39
src/assets/images/layout/menu-sidebar.svg
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
|
||||
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
</defs>
|
||||
<g width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
|
||||
<g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
|
||||
<g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
|
||||
<mask id="mask-3" fill="white">
|
||||
<use xlink:href="#path-2"></use>
|
||||
</mask>
|
||||
<g id="Rectangle-18">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
|
||||
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
|
||||
</g>
|
||||
<rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
|
||||
<rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
39
src/assets/images/layout/menu-top.svg
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<defs>
|
||||
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
|
||||
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
</defs>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="setting-copy-2" transform="translate(-1254.000000, -337.000000)">
|
||||
<g id="Group-8" transform="translate(1167.000000, 0.000000)">
|
||||
<g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 338.000000)">
|
||||
<mask id="mask-3" fill="white">
|
||||
<use xlink:href="#path-2"></use>
|
||||
</mask>
|
||||
<g id="Rectangle-18">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
|
||||
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
|
||||
</g>
|
||||
<rect id="Rectangle-11" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
67
src/assets/images/loading.svg
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg viewBox="0 0 200 200" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<style type="text/css">
|
||||
.left-linear {
|
||||
fill: url(#left-linear);
|
||||
}
|
||||
|
||||
.right-linear {
|
||||
fill: url(#right-linear);
|
||||
}
|
||||
|
||||
.top {
|
||||
fill: #64acff;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
fill: #9dbfe4;
|
||||
}
|
||||
@keyframes load {
|
||||
0% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.load {
|
||||
animation: load 1.4s linear infinite;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tip {
|
||||
display: block;
|
||||
min-width: 100px;
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
<circle cx="97" cy="97" r="81" stroke-width="16" stroke="#327fd8" fill="none"></circle>
|
||||
<g class="load">
|
||||
<!--右半圆环-->
|
||||
<linearGradient id="left-linear" gradientUnits="userSpaceOnUse" x1="50" y1="0" x2="100" y2="180">
|
||||
<stop offset="0" style="stop-color: #64acff;" />
|
||||
<stop offset="1" style="stop-color: #9DBFE4;" />
|
||||
</linearGradient>
|
||||
<path class="left-linear" d="M20,100c0-44.1,35.9-80,80-80V0C44.8,0,0,44.8,0,100s44.8,100,100,100v-20C55.9,180,20,144.1,20,100z" />
|
||||
<!--左半圆环-->
|
||||
<circle class="bottom" cx="100" cy="190" r="10" />
|
||||
<linearGradient id="right-linear" gradientUnits="userSpaceOnUse" x1="100" y1="120" x2="100" y2="180">
|
||||
<stop offset="0" style="stop-color: transparent;" />
|
||||
<stop offset="1" style="stop-color: transparent;" />
|
||||
</linearGradient>
|
||||
<path class="right-linear" d="M100,0v20c44.1,0,80,35.9,80,80c0,44.1-35.9,80-80,80v20c55.2,0,100-44.8,100-100S155.2,0,100,0z" />
|
||||
<!--左半圆环-->
|
||||
<circle class="top" cx="100" cy="10" r="10" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/images/lock-page.jpg
Normal file
After Width: | Height: | Size: 218 KiB |
BIN
src/assets/images/lock-page.png
Normal file
After Width: | Height: | Size: 372 KiB |
BIN
src/assets/images/login/login-bg.png
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
src/assets/images/login/login-in.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
src/assets/images/logo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/no-data.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/page_null.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
src/assets/images/qq.jpeg
Normal file
After Width: | Height: | Size: 110 KiB |
BIN
src/assets/images/sidebar/dark-mini.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
src/assets/images/sidebar/dark.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src/assets/images/sidebar/light-mini.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/images/sidebar/light.png
Normal file
After Width: | Height: | Size: 24 KiB |
1
src/assets/svg/preview/p-rotate.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595306944988" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1820" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M1464.3 279.7" p-id="1821" fill="#ffffff"></path><path d="M512 960c-60.5 0-119.1-11.9-174.4-35.2-53.4-22.6-101.3-54.9-142.4-96s-73.4-89-96-142.4C75.9 631.1 64 572.5 64 512s11.9-119.1 35.2-174.4c22.6-53.4 54.9-101.3 96-142.4s89-73.4 142.4-96C392.9 75.9 451.5 64 512 64s119.1 11.9 174.4 35.2c53.4 22.6 101.3 54.9 142.4 96s73.4 89 96 142.4C948.1 392.9 960 451.5 960 512c0 19.1-15.5 34.6-34.6 34.6s-34.6-15.5-34.6-34.6c0-51.2-10-100.8-29.8-147.4-19.1-45.1-46.4-85.6-81.2-120.4C745 209.4 704.5 182 659.4 163c-46.7-19.7-96.3-29.8-147.4-29.8-51.2 0-100.8 10-147.4 29.8-45.1 19.1-85.6 46.4-120.4 81.2S182 319.5 163 364.6c-19.7 46.7-29.8 96.3-29.8 147.4 0 51.2 10 100.8 29.8 147.4 19.1 45.1 46.4 85.6 81.2 120.4C279 814.6 319.5 842 364.6 861c46.7 19.7 96.3 29.8 147.4 29.8 64.6 0 128.4-16.5 184.4-47.8 54.4-30.4 100.9-74.1 134.6-126.6 10.3-16.1 31.7-20.8 47.8-10.4 16.1 10.3 20.8 31.7 10.4 47.8-39.8 62-94.8 113.7-159.1 149.6-66.2 37-141.7 56.6-218.1 56.6z" p-id="1822" fill="#ffffff"></path><path d="M924 552c-19.8 0-36-16.2-36-36V228c0-19.8 16.2-36 36-36s36 16.2 36 36v288c0 19.8-16.2 36-36 36zM275.4 575.5c9.5-2.5 19.1 2.9 22.3 12.2 3.5 10.2 9.9 17.7 19.1 22.6 7.1 3.9 15.1 5.8 24 5.8 16.6 0 30.8-6.9 42.5-20.8 11.7-13.8 20-32.7 24.9-75.1-7.7 12.2-17.3 20.8-28.7 25.8-11.4 5-23.7 7.4-36.8 7.4-26.7 0-47.7-8.3-63.3-24.9-15.5-16.6-23.3-37.9-23.3-64.1 0-25.1 7.7-47.1 23-66.2 15.3-19 37.9-28.6 67.8-28.6 40.3 0 68.1 18.1 83.4 54.4 8.5 19.9 12.7 44.9 12.7 74.9 0 33.8-5.1 63.8-15.3 89.9-16.9 43.5-45.5 65.2-85.8 65.2-27 0-47.6-7.1-61.6-21.2-10-10.1-16.4-22-19.3-35.8-2-9.6 4-19.1 13.5-21.6l0.9 0.1z m103-74.4c9.4-7.5 14.1-20.6 14.1-39.3 0-16.8-4.2-29.3-12.7-37.5S360.6 412 347.5 412c-14 0-25.2 4.7-33.4 14.1-8.2 9.4-12.4 22-12.4 37.7 0 14.9 3.6 26.7 10.9 35.5 7.2 8.8 18.8 13.1 34.6 13.1 11.4 0 21.8-3.8 31.2-11.3zM646.6 414.4c12.4 22.8 18.5 54 18.5 93.7 0 37.6-5.6 68.7-16.8 93.3-16.2 35.3-42.8 52.9-79.6 52.9-33.2 0-57.9-14.4-74.2-43.3-13.5-24.1-20.3-56.4-20.3-97 0-31.4 4.1-58.4 12.2-80.9 15.2-42 42.7-63 82.5-63 35.9 0 61.8 14.8 77.7 44.3z m-40.2 173.3c9.4-13.9 14-39.9 14-78 0-27.4-3.4-50-10.1-67.7-6.8-17.7-19.9-26.6-39.4-26.6-17.9 0-31 8.4-39.3 25.2-8.3 16.8-12.4 41.6-12.4 74.3 0 24.6 2.6 44.4 7.9 59.4 8.1 22.8 22 34.3 41.6 34.3 15.7 0 28.3-7 37.7-20.9zM803.3 387.2c11.2 11.3 16.8 25 16.8 41.2 0 16.7-5.8 30.7-17.5 41.8C791 481.4 777.4 487 762 487c-17.1 0-31.2-5.8-42.1-17.4-10.9-11.6-16.4-25.1-16.4-40.6 0-16.5 5.8-30.4 17.3-41.7 11.5-11.3 25.3-17 41.2-17 16.3 0 30.1 5.7 41.3 16.9zM739.5 451c6.2 6.2 13.7 9.3 22.5 9.3 8.4 0 15.8-3.1 22.1-9.3 6.3-6.2 9.4-13.7 9.4-22.6 0-8.5-3.1-15.9-9.3-22.1-6.2-6.2-13.6-9.3-22.2-9.3s-16.1 3.1-22.4 9.3c-6.3 6.2-9.4 13.7-9.4 22.6-0.1 8.4 3 15.8 9.3 22.1z" p-id="1823" fill="#ffffff"></path></svg>
|
After Width: | Height: | Size: 3.0 KiB |
1
src/assets/svg/preview/resume.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595307154239" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7317" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M316 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8zM512 622c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39zM512 482c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39z" p-id="7318" fill="#ffffff"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" p-id="7319" fill="#ffffff"></path><path d="M648 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8z" p-id="7320" fill="#ffffff"></path></svg>
|
After Width: | Height: | Size: 995 B |
1
src/assets/svg/preview/scale.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595307195033" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8116" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M887.081 904.791a25.8 25.8 0 0 1-18.376-7.619L705.618 734.075l-4.163 3.369c-58.255 47.18-131.522 73.16-206.32 73.16-181.07 0-328.377-147.308-328.377-328.367 0-181.068 147.308-328.376 328.377-328.376 181.063 0 328.376 147.308 328.376 328.376 0 77.072-27.412 152.07-77.169 211.17l-3.522 4.173 162.719 162.744a25.846 25.846 0 0 1 7.639 18.432 26.081 26.081 0 0 1-26.051 26.045l-0.046-0.01zM495.13 205.957c-152.336 0-276.27 123.935-276.27 276.27 0 152.33 123.934 276.27 276.27 276.27 152.34 0 276.275-123.94 276.275-276.27 0-152.335-123.935-276.27-276.275-276.27z" fill="#ffffff" p-id="8117"></path><path d="M626.545 508.355h-262.83a26.127 26.127 0 0 1 0-52.255h262.83a26.127 26.127 0 0 1 0 52.255z" fill="#ffffff" p-id="8118"></path><path d="M495.13 639.77a26.127 26.127 0 0 1-26.128-26.128v-262.83a26.127 26.127 0 0 1 52.255 0v262.835a26.127 26.127 0 0 1-26.127 26.123z" fill="#ffffff" p-id="8119"></path></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/svg/preview/unrotate.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595306911635" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1352" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M924.8 337.6c-22.6-53.4-54.9-101.3-96-142.4s-89-73.4-142.4-96C631.1 75.9 572.5 64 512 64S392.9 75.9 337.6 99.2c-53.4 22.6-101.3 54.9-142.4 96-22.4 22.4-42.2 46.8-59.2 73.1V228c0-19.8-16.2-36-36-36s-36 16.2-36 36v288c0 19.8 16.2 36 36 36s36-16.2 36-36v-50.2c4.2-34.8 13.2-68.7 27-101.2 19.1-45.1 46.4-85.6 81.2-120.4C279 209.4 319.5 182 364.6 163c46.7-19.7 96.3-29.8 147.4-29.8 51.2 0 100.8 10 147.4 29.8 45.1 19.1 85.6 46.4 120.4 81.2C814.6 279 842 319.5 861 364.6c19.7 46.7 29.8 96.3 29.8 147.4 0 51.2-10 100.8-29.8 147.4-19.1 45.1-46.4 85.6-81.2 120.4C745 814.6 704.5 842 659.4 861c-46.7 19.7-96.3 29.8-147.4 29.8-64.6 0-128.4-16.5-184.4-47.8-54.4-30.4-100.9-74.1-134.6-126.6-10.3-16.1-31.7-20.8-47.8-10.4-16.1 10.3-20.8 31.7-10.4 47.8 39.8 62 94.8 113.7 159.1 149.6 66.2 37 141.7 56.6 218.1 56.6 60.5 0 119.1-11.9 174.4-35.2 53.4-22.6 101.3-54.9 142.4-96 41.1-41.1 73.4-89 96-142.4C948.1 631.1 960 572.5 960 512s-11.9-119.1-35.2-174.4z" p-id="1353" fill="#ffffff"></path><path d="M275.4 575.5c9.5-2.5 19.1 2.9 22.3 12.2 3.5 10.2 9.9 17.7 19.1 22.6 7.1 3.9 15.1 5.8 24 5.8 16.6 0 30.8-6.9 42.5-20.8 11.7-13.8 20-32.7 24.9-75.1-7.7 12.2-17.3 20.8-28.7 25.8-11.4 5-23.7 7.4-36.8 7.4-26.7 0-47.7-8.3-63.3-24.9-15.5-16.6-23.3-37.9-23.3-64.1 0-25.1 7.7-47.1 23-66.2 15.3-19 37.9-28.6 67.8-28.6 40.3 0 68.1 18.1 83.4 54.4 8.5 19.9 12.7 44.9 12.7 74.9 0 33.8-5.1 63.8-15.3 89.9-16.9 43.5-45.5 65.2-85.8 65.2-27 0-47.6-7.1-61.6-21.2-10-10.1-16.4-22-19.3-35.8-2-9.6 4-19.1 13.5-21.6l0.9 0.1z m103-74.4c9.4-7.5 14.1-20.6 14.1-39.3 0-16.8-4.2-29.3-12.7-37.5S360.6 412 347.5 412c-14 0-25.2 4.7-33.4 14.1-8.2 9.4-12.4 22-12.4 37.7 0 14.9 3.6 26.7 10.9 35.5 7.2 8.8 18.8 13.1 34.6 13.1 11.4 0 21.8-3.8 31.2-11.3zM646.6 414.4c12.4 22.8 18.5 54 18.5 93.7 0 37.6-5.6 68.7-16.8 93.3-16.2 35.3-42.8 52.9-79.6 52.9-33.2 0-57.9-14.4-74.2-43.3-13.5-24.1-20.3-56.4-20.3-97 0-31.4 4.1-58.4 12.2-80.9 15.2-42 42.7-63 82.5-63 35.9 0 61.8 14.8 77.7 44.3z m-40.2 173.3c9.4-13.9 14-39.9 14-78 0-27.4-3.4-50-10.1-67.7-6.8-17.7-19.9-26.6-39.4-26.6-17.9 0-31 8.4-39.3 25.2-8.3 16.8-12.4 41.6-12.4 74.3 0 24.6 2.6 44.4 7.9 59.4 8.1 22.8 22 34.3 41.6 34.3 15.7 0 28.3-7 37.7-20.9zM803.3 387.2c11.2 11.3 16.8 25 16.8 41.2 0 16.7-5.8 30.7-17.5 41.8C791 481.4 777.4 487 762 487c-17.1 0-31.2-5.8-42.1-17.4-10.9-11.6-16.4-25.1-16.4-40.6 0-16.5 5.8-30.4 17.3-41.7 11.5-11.3 25.3-17 41.2-17 16.3 0 30.1 5.7 41.3 16.9zM739.5 451c6.2 6.2 13.7 9.3 22.5 9.3 8.4 0 15.8-3.1 22.1-9.3 6.3-6.2 9.4-13.7 9.4-22.6 0-8.5-3.1-15.9-9.3-22.1-6.2-6.2-13.6-9.3-22.2-9.3s-16.1 3.1-22.4 9.3c-6.3 6.2-9.4 13.7-9.4 22.6-0.1 8.4 3 15.8 9.3 22.1z" p-id="1354" fill="#ffffff"></path></svg>
|
After Width: | Height: | Size: 2.9 KiB |
1
src/assets/svg/preview/unscale.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595308005241" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9878" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M750.3 198.7C598 46.4 351.1 46.4 198.7 198.7s-152.3 399.2 0 551.5C345.1 896.6 578.8 902.3 732 767.3l172.1 172.1 35.4-35.4-172.1-171.9c135-153.2 129.3-387-17.1-533.4z m39.3 403.8c-17.1 42.1-42.2 80-74.7 112.4-32.5 32.5-70.3 57.6-112.4 74.7-40.7 16.5-83.8 24.9-128 24.9s-87.2-8.4-128-24.9c-42.1-17.1-80-42.2-112.4-74.7s-57.6-70.3-74.7-112.4c-16.5-40.7-24.9-83.8-24.9-128s8.4-87.2 24.9-128c17.1-42.1 42.2-80 74.7-112.4s70.3-57.6 112.4-74.7c40.7-16.5 83.8-24.9 128-24.9s87.2 8.4 128 24.9c42.1 17.1 80 42.2 112.4 74.7 32.5 32.5 57.6 70.3 74.7 112.4 16.5 40.7 24.9 83.8 24.9 128s-8.4 87.3-24.9 128zM671 502H271v-50h400v50z" fill="#ffffff" p-id="9879"></path></svg>
|
After Width: | Height: | Size: 1.0 KiB |
59
src/components/Authority/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { defineComponent, PropType, computed, unref } from 'vue';
|
||||
|
||||
import { PermissionModeEnum } from '/@/enums/appEnum';
|
||||
import { RoleEnum } from '/@/enums/roleEnum';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import { appStore } from '/@/store/modules/app';
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Authority',
|
||||
props: {
|
||||
// 指定角色可见
|
||||
value: {
|
||||
type: [Number, Array, String] as PropType<RoleEnum | RoleEnum[]>,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const getModeRef = computed(() => {
|
||||
return appStore.getProjectConfig.permissionMode;
|
||||
});
|
||||
/**
|
||||
* 渲染角色按钮
|
||||
*/
|
||||
function renderRoleAuth() {
|
||||
const { value } = props;
|
||||
if (!value) {
|
||||
return getSlot(slots, 'default');
|
||||
}
|
||||
const { hasPermission } = usePermission();
|
||||
return hasPermission(value) ? getSlot(slots, 'default') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染编码按钮
|
||||
* 这里只判断是否包含,具体实现可以根据项目自行写逻辑
|
||||
*/
|
||||
function renderCodeAuth() {
|
||||
const { value } = props;
|
||||
if (!value) {
|
||||
return getSlot(slots, 'default');
|
||||
}
|
||||
const { hasPermission } = usePermission();
|
||||
return hasPermission(value) ? getSlot(slots, 'default') : null;
|
||||
}
|
||||
return () => {
|
||||
const mode = unref(getModeRef);
|
||||
// 基于角色渲染
|
||||
if (mode === PermissionModeEnum.ROLE) {
|
||||
return renderRoleAuth();
|
||||
}
|
||||
// 基于后台编码渲染
|
||||
if (mode === PermissionModeEnum.BACK) {
|
||||
return renderCodeAuth();
|
||||
}
|
||||
return getSlot(slots, 'default');
|
||||
};
|
||||
},
|
||||
});
|
4
src/components/Basic/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as BasicArrow } from './src/BasicArrow.vue';
|
||||
export { default as BasicHelp } from './src/BasicHelp';
|
||||
export { default as BasicTitle } from './src/BasicTitle.vue';
|
||||
export { default as BasicEmpty } from './src/BasicEmpty.vue';
|
53
src/components/Basic/src/BasicArrow.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<span :class="getClass">
|
||||
<RightOutlined />
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { defineComponent, computed } from 'vue';
|
||||
|
||||
import { RightOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BaseArrow',
|
||||
components: { RightOutlined },
|
||||
props: {
|
||||
// Expand contract, expand by default
|
||||
expand: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const getClass = computed(() => {
|
||||
const preCls = 'base-arrow';
|
||||
const cls = [preCls];
|
||||
|
||||
props.expand && cls.push(`${preCls}__active`);
|
||||
return cls;
|
||||
});
|
||||
|
||||
return {
|
||||
getClass,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.base-arrow {
|
||||
transform: rotate(-90deg) !important;
|
||||
transition: all 0.3s ease 0.1s;
|
||||
transform-origin: center center;
|
||||
|
||||
&.right {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
&__active {
|
||||
transform: rotate(90deg) !important;
|
||||
transition: all 0.3s ease 0.1s !important;
|
||||
}
|
||||
}
|
||||
</style>
|
28
src/components/Basic/src/BasicEmpty.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<Empty :image="image" :description="description" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { Empty } from 'ant-design-vue';
|
||||
|
||||
import emptySrc from '/@/assets/images/page_null.png';
|
||||
|
||||
export default defineComponent({
|
||||
extends: Empty as any,
|
||||
components: { Empty },
|
||||
props: {
|
||||
description: {
|
||||
type: String,
|
||||
default: '暂无内容',
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
default: emptySrc,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
18
src/components/Basic/src/BasicHelp.less
Normal file
@@ -0,0 +1,18 @@
|
||||
@import (reference) '../../../design/index.less';
|
||||
|
||||
.base-help {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
color: @text-color-help-dark;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
107
src/components/Basic/src/BasicHelp.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
|
||||
import { getPopupContainer } from '/@/utils';
|
||||
|
||||
import { isString, isArray } from '/@/utils/is';
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||
import './BasicHelp.less';
|
||||
export default defineComponent({
|
||||
name: 'BaseHelp',
|
||||
props: {
|
||||
// max-width
|
||||
maxWidth: {
|
||||
type: String as PropType<string>,
|
||||
default: '600px',
|
||||
},
|
||||
// Whether to display the serial number
|
||||
showIndex: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
// Text list
|
||||
text: {
|
||||
type: [Array, String] as PropType<string[] | string>,
|
||||
},
|
||||
// color
|
||||
color: {
|
||||
type: String as PropType<string>,
|
||||
default: '#ffffff',
|
||||
},
|
||||
fontSize: {
|
||||
type: String as PropType<string>,
|
||||
default: '14px',
|
||||
},
|
||||
absolute: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
// 定位
|
||||
position: {
|
||||
type: [Object] as PropType<any>,
|
||||
default: () => ({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}),
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const getOverlayStyleRef = computed(() => {
|
||||
return {
|
||||
maxWidth: props.maxWidth,
|
||||
};
|
||||
});
|
||||
const getWrapStyleRef = computed(() => {
|
||||
return {
|
||||
color: props.color,
|
||||
fontSize: props.fontSize,
|
||||
};
|
||||
});
|
||||
const getMainStyleRef = computed(() => {
|
||||
return props.absolute ? props.position : {};
|
||||
});
|
||||
|
||||
/**
|
||||
* @description: 渲染内容
|
||||
*/
|
||||
const renderTitle = () => {
|
||||
const list = props.text;
|
||||
if (isString(list)) {
|
||||
return <p>{list}</p>;
|
||||
}
|
||||
if (isArray(list)) {
|
||||
return list.map((item, index) => {
|
||||
return (
|
||||
<p key={item}>
|
||||
{props.showIndex ? `${index + 1}. ` : ''}
|
||||
{item}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return () => (
|
||||
<Tooltip
|
||||
title={(<div style={unref(getWrapStyleRef)}>{renderTitle()}</div>) as any}
|
||||
placement="right"
|
||||
overlayStyle={unref(getOverlayStyleRef)}
|
||||
autoAdjustOverflow={true}
|
||||
overlayClassName="base-help__wrap"
|
||||
getPopupContainer={() => getPopupContainer()}
|
||||
>
|
||||
{{
|
||||
default: () => (
|
||||
<span class="base-help" style={unref(getMainStyleRef)}>
|
||||
{getSlot(slots) || <InfoCircleOutlined />}
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
});
|
58
src/components/Basic/src/BasicTitle.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<span class="base-title" :class="{ 'show-span': showSpan && $slots.default }">
|
||||
<slot />
|
||||
<BaseHelp class="base-title__help" v-if="helpMessage" :text="helpMessage" />
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BaseTitle',
|
||||
props: {
|
||||
helpMessage: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
default: '',
|
||||
},
|
||||
showSpan: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@import (reference) '../../../design/index.less';
|
||||
|
||||
.base-title {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding-left: 7px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
color: @text-color-base;
|
||||
|
||||
.unselect();
|
||||
|
||||
&.show-span::before {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
background: @primary-color;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&__help {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
100
src/components/Breadcrumb/Breadcrumb.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div ref="breadcrumbRef" class="breadcrumb">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent, provide, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Breadcrumb',
|
||||
props: {
|
||||
separator: {
|
||||
type: String as PropType<string>,
|
||||
default: '/',
|
||||
},
|
||||
separatorClass: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const breadcrumbRef = ref<Nullable<HTMLElement>>(null);
|
||||
|
||||
provide('breadcrumb', props);
|
||||
|
||||
return {
|
||||
breadcrumbRef,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import (reference) '../../design/index.less';
|
||||
|
||||
.breadcrumb {
|
||||
height: @header-height;
|
||||
padding-right: 20px;
|
||||
font-size: 14px;
|
||||
line-height: @header-height;
|
||||
// line-height: 1;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
display: table;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
margin: 0 9px;
|
||||
font-weight: 700;
|
||||
color: @breadcrumb-item-normal-color;
|
||||
|
||||
&[class*='icon'] {
|
||||
margin: 0 6px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&__inner {
|
||||
color: @breadcrumb-item-normal-color;
|
||||
|
||||
&.is-link,
|
||||
a {
|
||||
font-weight: 700;
|
||||
color: @text-color-base;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
a:hover,
|
||||
&.is-link:hover {
|
||||
color: @primary-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__item:last-child .breadcrumb__inner,
|
||||
&__item:last-child &__inner a,
|
||||
&__item:last-child &__inner a:hover,
|
||||
&__item:last-child &__inner:hover {
|
||||
font-weight: 400;
|
||||
color: @breadcrumb-item-normal-color;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&__item:last-child &__separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
62
src/components/Breadcrumb/BreadcrumbItem.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<span class="breadcrumb__item">
|
||||
<span ref="linkRef" :class="['breadcrumb__inner', to || isLink ? 'is-link' : '']">
|
||||
<slot />
|
||||
</span>
|
||||
<i v-if="separatorClass" class="breadcrumb__separator" :class="separatorClass"></i>
|
||||
<span v-else class="breadcrumb__separator">{{ separator }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, ref, onMounted, unref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useEvent } from '/@/hooks/event/useEvent';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BreadcrumbItem',
|
||||
props: {
|
||||
to: {
|
||||
type: [String, Object],
|
||||
default: '',
|
||||
},
|
||||
replace: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLink: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const linkRef = ref<Nullable<HTMLElement>>(null);
|
||||
const parent = inject('breadcrumb') as {
|
||||
separator: string;
|
||||
separatorClass: string;
|
||||
};
|
||||
const { push, replace } = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
const link = unref(linkRef);
|
||||
if (!link) return;
|
||||
useEvent({
|
||||
el: link,
|
||||
listener: () => {
|
||||
const { to } = props;
|
||||
if (!props.to) return;
|
||||
props.replace ? replace(to) : push(to);
|
||||
},
|
||||
name: 'click',
|
||||
wait: 0,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
linkRef,
|
||||
separator: parent.separator && parent.separator,
|
||||
separatorClass: parent.separatorClass && parent.separatorClass,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
88
src/components/Button/index.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<Button v-bind="getBindValue" :class="[getColor, $attrs.class]">
|
||||
<template v-slot:[item] v-for="item in Object.keys($slots)">
|
||||
<slot :name="item" />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
import { Button } from 'ant-design-vue';
|
||||
// import { extendSlots } from '/@/utils/helper/tsxHelper';
|
||||
import { useThrottle } from '/@/hooks/core/useThrottle';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
export default defineComponent({
|
||||
name: 'AButton',
|
||||
inheritAttrs: false,
|
||||
components: { Button },
|
||||
props: {
|
||||
// 按钮类型
|
||||
type: {
|
||||
type: String as PropType<'primary' | 'default' | 'danger' | 'dashed' | 'link'>,
|
||||
default: 'default',
|
||||
},
|
||||
// 节流防抖类型 throttle debounce
|
||||
throttle: {
|
||||
type: String as PropType<'throttle' | 'debounce'>,
|
||||
default: 'throttle',
|
||||
},
|
||||
color: {
|
||||
type: String as PropType<'error' | 'warning' | 'success'>,
|
||||
},
|
||||
// 防抖节流时间
|
||||
throttleTime: {
|
||||
type: Number as PropType<number>,
|
||||
default: 0,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { attrs }) {
|
||||
const getListeners = computed(() => {
|
||||
const { throttle, throttleTime = 0 } = props;
|
||||
// 是否开启节流防抖
|
||||
const throttleType = throttle!.toLowerCase();
|
||||
const isDebounce = throttleType === 'debounce';
|
||||
const openThrottle = ['throttle', 'debounce'].includes(throttleType) && throttleTime > 0;
|
||||
|
||||
const on: {
|
||||
onClick?: Fn;
|
||||
} = {};
|
||||
|
||||
if (attrs.onClick && isFunction(attrs.onClick) && openThrottle) {
|
||||
const [handler] = useThrottle(attrs.onClick as any, throttleTime!, {
|
||||
debounce: isDebounce,
|
||||
immediate: true,
|
||||
});
|
||||
on.onClick = handler;
|
||||
}
|
||||
|
||||
return {
|
||||
...attrs,
|
||||
...on,
|
||||
};
|
||||
});
|
||||
|
||||
const getColor = computed(() => {
|
||||
const res: string[] = [];
|
||||
const { color, disabled } = props;
|
||||
color && res.push(`ant-btn-${color}`);
|
||||
disabled && res.push('is-disabled');
|
||||
return res;
|
||||
});
|
||||
|
||||
const getBindValue = computed((): any => {
|
||||
return { ...unref(getListeners), ...props };
|
||||
});
|
||||
return { getBindValue, getColor };
|
||||
},
|
||||
});
|
||||
</script>
|
66
src/components/Button/types.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { VNodeChild } from 'vue';
|
||||
|
||||
export interface BasicButtonProps {
|
||||
/**
|
||||
* can be set to primary ghost dashed danger(added in 2.7) or omitted (meaning default)
|
||||
* @default 'default'
|
||||
* @type string
|
||||
*/
|
||||
type?: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default';
|
||||
|
||||
/**
|
||||
* set the original html type of button
|
||||
* @default 'button'
|
||||
* @type string
|
||||
*/
|
||||
htmlType?: 'button' | 'submit' | 'reset' | 'menu';
|
||||
|
||||
/**
|
||||
* set the icon of button
|
||||
* @type string
|
||||
*/
|
||||
icon?: VNodeChild | JSX.Element;
|
||||
|
||||
/**
|
||||
* can be set to circle or circle-outline or omitted
|
||||
* @type string
|
||||
*/
|
||||
shape?: 'circle' | 'circle-outline';
|
||||
|
||||
/**
|
||||
* can be set to small large or omitted
|
||||
* @default 'default'
|
||||
* @type string
|
||||
*/
|
||||
size?: 'small' | 'large' | 'default';
|
||||
|
||||
/**
|
||||
* set the loading status of button
|
||||
* @default false
|
||||
* @type boolean | { delay: number }
|
||||
*/
|
||||
loading?: boolean | { delay: number };
|
||||
|
||||
/**
|
||||
* disabled state of button
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* make background transparent and invert text and border colors, added in 2.7
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
ghost?: boolean;
|
||||
|
||||
/**
|
||||
* option to fit button width to its parent width
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
block?: boolean;
|
||||
|
||||
onClick?: (e?: Event) => void;
|
||||
}
|
21
src/components/ClickOutSide/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div ref="wrapRef"><slot /></div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
import { useClickOutside } from '/@/hooks/web/useClickOutside';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ClickOutSide',
|
||||
|
||||
setup(_, { emit }) {
|
||||
const wrapRef = ref<Nullable<HTMLDivElement | null>>(null);
|
||||
useClickOutside(wrapRef as Ref<HTMLDivElement>, () => {
|
||||
emit('clickOutside');
|
||||
});
|
||||
return { wrapRef };
|
||||
},
|
||||
});
|
||||
</script>
|
5
src/components/Container/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as ScrollContainer } from './src/ScrollContainer.vue';
|
||||
export { default as CollapseContainer } from './src/collapse/CollapseContainer.vue';
|
||||
export { default as LazyContainer } from './src/LazyContainer';
|
||||
|
||||
export * from './src/types.d';
|
27
src/components/Container/src/LazyContainer.less
Normal file
@@ -0,0 +1,27 @@
|
||||
.lazy-container-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.lazy-container-enter-to {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.lazy-container-enter-from,
|
||||
.lazy-container-enter-active {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
transition: opacity 0.3s 0.2s;
|
||||
}
|
||||
|
||||
.lazy-container-leave {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.lazy-container-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.lazy-container-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
200
src/components/Container/src/LazyContainer.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
onMounted,
|
||||
ref,
|
||||
unref,
|
||||
onUnmounted,
|
||||
TransitionGroup,
|
||||
} from 'vue';
|
||||
|
||||
import { Skeleton } from 'ant-design-vue';
|
||||
import { useRaf } from '/@/hooks/event/useRaf';
|
||||
import { useTimeout } from '/@/hooks/core/useTimeout';
|
||||
import { getListeners, getSlot } from '/@/utils/helper/tsxHelper';
|
||||
|
||||
import './LazyContainer.less';
|
||||
|
||||
interface State {
|
||||
isInit: boolean;
|
||||
loading: boolean;
|
||||
intersectionObserverInstance: IntersectionObserver | null;
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'LazyContainer',
|
||||
emits: ['before-init', 'init'],
|
||||
props: {
|
||||
// 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
|
||||
timeout: {
|
||||
type: Number as PropType<number>,
|
||||
default: 8000,
|
||||
// default: 8000,
|
||||
},
|
||||
// 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
|
||||
viewport: {
|
||||
type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>,
|
||||
default: () => null,
|
||||
},
|
||||
// 预加载阈值, css单位
|
||||
threshold: {
|
||||
type: String as PropType<string>,
|
||||
default: '0px',
|
||||
},
|
||||
|
||||
// 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向
|
||||
direction: {
|
||||
type: String as PropType<'vertical' | 'horizontal'>,
|
||||
default: 'vertical',
|
||||
},
|
||||
// 包裹组件的外层容器的标签名
|
||||
tag: {
|
||||
type: String as PropType<string>,
|
||||
default: 'div',
|
||||
},
|
||||
|
||||
maxWaitingTime: {
|
||||
type: Number as PropType<number>,
|
||||
default: 80,
|
||||
},
|
||||
|
||||
// 是否在不可见的时候销毁
|
||||
autoDestory: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
|
||||
// transition name
|
||||
transitionName: {
|
||||
type: String as PropType<string>,
|
||||
default: 'lazy-container',
|
||||
},
|
||||
},
|
||||
setup(props, { attrs, emit, slots }) {
|
||||
const elRef = ref<any>(null);
|
||||
const state = reactive<State>({
|
||||
isInit: false,
|
||||
loading: false,
|
||||
intersectionObserverInstance: null,
|
||||
});
|
||||
|
||||
// If there is a set delay time, it will be executed immediately
|
||||
function immediateInit() {
|
||||
const { timeout } = props;
|
||||
timeout &&
|
||||
useTimeout(() => {
|
||||
init();
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
function init() {
|
||||
// At this point, the skeleton component is about to be switched
|
||||
emit('before-init');
|
||||
// At this point you can prepare to load the resources of the lazy-loaded component
|
||||
state.loading = true;
|
||||
|
||||
requestAnimationFrameFn(() => {
|
||||
state.isInit = true;
|
||||
emit('init');
|
||||
});
|
||||
}
|
||||
function requestAnimationFrameFn(callback: () => any) {
|
||||
// Prevent waiting too long without executing the callback
|
||||
// Set the maximum waiting time
|
||||
useTimeout(() => {
|
||||
if (state.isInit) {
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}, props.maxWaitingTime || 80);
|
||||
|
||||
const { requestAnimationFrame } = useRaf();
|
||||
|
||||
return requestAnimationFrame;
|
||||
}
|
||||
function initIntersectionObserver() {
|
||||
const { timeout, direction, threshold, viewport } = props;
|
||||
if (timeout) {
|
||||
return;
|
||||
}
|
||||
// According to the scrolling direction to construct the viewport margin, used to load in advance
|
||||
let rootMargin;
|
||||
switch (direction) {
|
||||
case 'vertical':
|
||||
rootMargin = `${threshold} 0px`;
|
||||
break;
|
||||
case 'horizontal':
|
||||
rootMargin = `0px ${threshold}`;
|
||||
break;
|
||||
}
|
||||
try {
|
||||
// Observe the intersection of the viewport and the component container
|
||||
state.intersectionObserverInstance = new window.IntersectionObserver(intersectionHandler, {
|
||||
rootMargin,
|
||||
root: viewport,
|
||||
threshold: [0, Number.MIN_VALUE, 0.01],
|
||||
});
|
||||
|
||||
const el = unref(elRef);
|
||||
|
||||
state.intersectionObserverInstance.observe(el.$el);
|
||||
} catch (e) {
|
||||
init();
|
||||
}
|
||||
}
|
||||
// Cross-condition change handling function
|
||||
function intersectionHandler(entries: any[]) {
|
||||
const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
|
||||
if (isIntersecting) {
|
||||
init();
|
||||
if (state.intersectionObserverInstance) {
|
||||
const el = unref(elRef);
|
||||
state.intersectionObserverInstance.unobserve(el.$el);
|
||||
}
|
||||
}
|
||||
// else {
|
||||
// const { autoDestory } = props;
|
||||
// autoDestory && destory();
|
||||
// }
|
||||
}
|
||||
// function destory() {
|
||||
// emit('beforeDestory');
|
||||
// state.loading = false;
|
||||
// nextTick(() => {
|
||||
// emit('destory');
|
||||
// });
|
||||
// }
|
||||
|
||||
immediateInit();
|
||||
onMounted(() => {
|
||||
initIntersectionObserver();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
// Cancel the observation before the component is destroyed
|
||||
if (state.intersectionObserverInstance) {
|
||||
const el = unref(elRef);
|
||||
state.intersectionObserverInstance.unobserve(el.$el);
|
||||
}
|
||||
});
|
||||
|
||||
function renderContent() {
|
||||
const { isInit, loading } = state;
|
||||
if (isInit) {
|
||||
return <div key="component">{getSlot(slots, 'default', { loading })}</div>;
|
||||
}
|
||||
if (slots.skeleton) {
|
||||
return <div key="skeleton">{getSlot(slots, 'skeleton') || <Skeleton />}</div>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return () => {
|
||||
const { tag, transitionName } = props;
|
||||
return (
|
||||
<TransitionGroup ref={elRef} name={transitionName} tag={tag} {...getListeners(attrs)}>
|
||||
{() => renderContent()}
|
||||
</TransitionGroup>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
80
src/components/Container/src/ScrollContainer.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<Scrollbar
|
||||
ref="scrollbarRef"
|
||||
:wrapClass="`scrollbar__wrap`"
|
||||
:viewClass="`scrollbar__view`"
|
||||
class="scroll-container"
|
||||
>
|
||||
<slot />
|
||||
</Scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// component
|
||||
import { defineComponent, ref, unref, nextTick } from 'vue';
|
||||
import { Scrollbar } from '/@/components/Scrollbar';
|
||||
|
||||
// hook
|
||||
import { useScrollTo } from '/@/hooks/event/useScrollTo';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ScrollContainer',
|
||||
components: { Scrollbar },
|
||||
setup() {
|
||||
const scrollbarRef = ref<RefInstanceType<any>>(null);
|
||||
|
||||
function scrollTo(to: number, duration = 500) {
|
||||
const scrollbar = unref(scrollbarRef);
|
||||
if (!scrollbar) return;
|
||||
nextTick(() => {
|
||||
const { start } = useScrollTo({
|
||||
el: unref(scrollbar.$.wrap),
|
||||
to,
|
||||
duration,
|
||||
});
|
||||
start();
|
||||
});
|
||||
}
|
||||
|
||||
function getScrollWrap() {
|
||||
const scrollbar = unref(scrollbarRef);
|
||||
if (!scrollbar) return null;
|
||||
return scrollbar.$.wrap;
|
||||
}
|
||||
|
||||
function scrollBottom() {
|
||||
const scrollbar = unref(scrollbarRef);
|
||||
if (!scrollbar) return;
|
||||
nextTick(() => {
|
||||
const scrollHeight = scrollbar.$.wrap.scrollHeight as number;
|
||||
const { start } = useScrollTo({
|
||||
el: unref(scrollbar.$.wrap),
|
||||
to: scrollHeight,
|
||||
});
|
||||
start();
|
||||
});
|
||||
}
|
||||
return {
|
||||
scrollbarRef,
|
||||
scrollTo,
|
||||
scrollBottom,
|
||||
getScrollWrap,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.scroll-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.scrollbar__wrap {
|
||||
margin-bottom: 18px !important;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.scrollbar__view {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
117
src/components/Container/src/collapse/CollapseContainer.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="collapse-container p-2 bg:white rounded-sm">
|
||||
<CollapseHeader v-bind="$props" :show="show" @expand="handleExpand" />
|
||||
<CollapseTransition :enable="canExpan">
|
||||
<Skeleton v-if="loading" />
|
||||
<div class="collapse-container__body" v-else v-show="show">
|
||||
<LazyContainer :timeout="lazyTime" v-if="lazy">
|
||||
<slot />
|
||||
<template v-slot:skeleton>
|
||||
<slot name="lazySkeleton" />
|
||||
</template>
|
||||
</LazyContainer>
|
||||
<slot />
|
||||
</div>
|
||||
</CollapseTransition>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { defineComponent, ref, unref } from 'vue';
|
||||
// component
|
||||
import { CollapseTransition } from '/@/components/Transition/index';
|
||||
import CollapseHeader from './CollapseHeader.vue';
|
||||
import { Skeleton } from 'ant-design-vue';
|
||||
|
||||
import LazyContainer from '../LazyContainer';
|
||||
|
||||
import { triggerWindowResize } from '/@/utils/event/triggerWindowResizeEvent';
|
||||
// hook
|
||||
import { useTimeout } from '/@/hooks/core/useTimeout';
|
||||
export default defineComponent({
|
||||
components: { Skeleton, LazyContainer, CollapseHeader, CollapseTransition },
|
||||
name: 'CollapseContainer',
|
||||
props: {
|
||||
// 标题
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
// 是否可以展开
|
||||
canExpan: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
// 标题右侧温馨提醒
|
||||
helpMessage: {
|
||||
type: [Array, String] as PropType<string[] | string>,
|
||||
default: '',
|
||||
},
|
||||
// 展开收缩的时候是否触发window.resize,
|
||||
// 可以适应表格和表单,当表单收缩起来,表格触发resize 自适应高度
|
||||
triggerWindowResize: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
// 延时加载
|
||||
lazy: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
// 延时加载时间
|
||||
lazyTime: {
|
||||
type: Number as PropType<number>,
|
||||
default: 3000,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const showRef = ref(true);
|
||||
/**
|
||||
* @description: 处理开展事件
|
||||
*/
|
||||
function handleExpand() {
|
||||
const hasShow = !unref(showRef);
|
||||
showRef.value = hasShow;
|
||||
|
||||
if (props.triggerWindowResize) {
|
||||
// 这里200毫秒是因为展开有动画,
|
||||
useTimeout(triggerWindowResize, 200);
|
||||
}
|
||||
}
|
||||
return {
|
||||
show: showRef,
|
||||
handleExpand,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.collapse-container {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&.no-shadow {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
margin-bottom: 10px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
32
src/components/Container/src/collapse/CollapseHeader.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="collapse-container__header">
|
||||
<BasicTitle :helpMessage="$attrs.helpMessage">
|
||||
<template v-if="$attrs.title">
|
||||
{{ $attrs.title }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot name="title" />
|
||||
</template>
|
||||
</BasicTitle>
|
||||
|
||||
<div class="collapse-container__action">
|
||||
<slot name="action" />
|
||||
<BasicArrow v-if="$attrs.canExpan" :expand="$attrs.show" @click="handleExpand" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { BasicArrow } from '/@/components/Basic';
|
||||
import { BasicTitle } from '/@/components/Basic';
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
components: { BasicArrow, BasicTitle },
|
||||
setup(_, { emit }) {
|
||||
function handleExpand() {
|
||||
emit('expand');
|
||||
}
|
||||
return { handleExpand };
|
||||
},
|
||||
});
|
||||
</script>
|
17
src/components/Container/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
export type ScrollType = 'default' | 'main';
|
||||
|
||||
export interface CollapseContainerOptions {
|
||||
canExpand?: boolean;
|
||||
title?: string;
|
||||
helpMessage?: Array<any> | string;
|
||||
}
|
||||
export interface ScrollContainerOptions {
|
||||
enableScroll?: boolean;
|
||||
type?: ScrollType;
|
||||
}
|
||||
|
||||
export type ScrollActionType = RefType<{
|
||||
scrollBottom: () => void;
|
||||
getScrollWrap: () => Nullable<HTMLElement>;
|
||||
scrollTo: (top: number) => void;
|
||||
}>;
|
65
src/components/ContextMenu/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import contextMenuVue from './src/index';
|
||||
import { isClient } from '/@/utils/is';
|
||||
import { Options, Props } from './src/types';
|
||||
import { createApp } from 'vue';
|
||||
const menuManager: {
|
||||
doms: Element[];
|
||||
resolve: Fn;
|
||||
} = {
|
||||
doms: [],
|
||||
resolve: () => {},
|
||||
};
|
||||
export const createContextMenu = function (options: Options) {
|
||||
const { event } = options || {};
|
||||
try {
|
||||
event.preventDefault();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
if (!isClient) return;
|
||||
return new Promise((resolve) => {
|
||||
const wrapDom = document.createElement('div');
|
||||
const propsData: Partial<Props> = {};
|
||||
if (options.styles !== undefined) propsData.styles = options.styles;
|
||||
if (options.items !== undefined) propsData.items = options.items;
|
||||
if (options.event !== undefined) {
|
||||
propsData.customEvent = event;
|
||||
propsData.axis = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
createApp(contextMenuVue, propsData).mount(wrapDom);
|
||||
const bodyClick = function () {
|
||||
menuManager.resolve('');
|
||||
};
|
||||
const contextMenuDom = wrapDom.children[0];
|
||||
menuManager.doms.push(contextMenuDom);
|
||||
const remove = function () {
|
||||
menuManager.doms.forEach((dom: Element) => {
|
||||
try {
|
||||
document.body.removeChild(dom);
|
||||
} catch (error) {}
|
||||
});
|
||||
document.body.removeEventListener('click', bodyClick);
|
||||
document.body.removeEventListener('scroll', bodyClick);
|
||||
try {
|
||||
(wrapDom as any) = null;
|
||||
} catch (error) {}
|
||||
};
|
||||
menuManager.resolve = function (...arg: any) {
|
||||
resolve(arg[0]);
|
||||
remove();
|
||||
};
|
||||
remove();
|
||||
document.body.appendChild(contextMenuDom);
|
||||
document.body.addEventListener('click', bodyClick);
|
||||
document.body.addEventListener('scroll', bodyClick);
|
||||
});
|
||||
};
|
||||
export const unMountedContextMenu = function () {
|
||||
if (menuManager) {
|
||||
menuManager.resolve('');
|
||||
menuManager.doms = [];
|
||||
}
|
||||
};
|
||||
|
||||
export * from './src/types';
|
49
src/components/ContextMenu/src/index.less
Normal file
@@ -0,0 +1,49 @@
|
||||
@import (reference) '../../../design/index.less';
|
||||
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1500;
|
||||
display: block;
|
||||
width: 156px;
|
||||
min-width: 10rem;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.1),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.06);
|
||||
background-clip: padding-box;
|
||||
user-select: none;
|
||||
|
||||
&.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&__item {
|
||||
a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
|
||||
&:hover {
|
||||
color: @text-color-base;
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
a {
|
||||
color: @disabled-color;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
color: @disabled-color;
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
src/components/ContextMenu/src/index.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onMounted,
|
||||
reactive,
|
||||
computed,
|
||||
ref,
|
||||
unref,
|
||||
onUnmounted,
|
||||
} from 'vue';
|
||||
import { props } from './props';
|
||||
import Icon from '/@/components/Icon';
|
||||
import type { ContextMenuItem } from './types';
|
||||
import './index.less';
|
||||
const prefixCls = 'context-menu';
|
||||
export default defineComponent({
|
||||
name: 'ContextMenu',
|
||||
props,
|
||||
setup(props) {
|
||||
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
|
||||
const state = reactive({
|
||||
show: false,
|
||||
});
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
state.show = true;
|
||||
});
|
||||
});
|
||||
onUnmounted(() => {
|
||||
const el = unref(wrapRef);
|
||||
el && document.body.removeChild(el);
|
||||
});
|
||||
const getStyle = computed(() => {
|
||||
const { axis, items, styles, width } = props;
|
||||
const { x, y } = axis || { x: 0, y: 0 };
|
||||
const menuHeight = (items || []).length * 40;
|
||||
const menuWidth = width;
|
||||
const body = document.body;
|
||||
return {
|
||||
...(styles as any),
|
||||
width: `${width}px`,
|
||||
left: (body.clientWidth < x + menuWidth ? x - menuWidth : x) + 'px',
|
||||
top: (body.clientHeight < y + menuHeight ? y - menuHeight : y) + 'px',
|
||||
};
|
||||
});
|
||||
function handleAction(item: ContextMenuItem, e: MouseEvent) {
|
||||
const { handler, disabled } = item;
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
state.show = false;
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
handler && handler();
|
||||
}
|
||||
function renderContent(item: ContextMenuItem) {
|
||||
const { icon, label } = item;
|
||||
|
||||
const { showIcon } = props;
|
||||
return (
|
||||
<span style="display: inline-block; width: 100%;">
|
||||
{showIcon && icon && <Icon class="mr-2" icon={icon} />}
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
function renderMenuItem(items: ContextMenuItem[]) {
|
||||
return items.map((item) => {
|
||||
const { disabled, label } = item;
|
||||
|
||||
return (
|
||||
<li class={`${prefixCls}__item ${disabled ? 'disabled' : ''}`} key={label}>
|
||||
<a onClick={handleAction.bind(null, item)}>{renderContent(item)}</a>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
const { items } = props;
|
||||
return (
|
||||
<ul class={[prefixCls, !state.show && 'hidden']} ref={wrapRef} style={unref(getStyle)}>
|
||||
{renderMenuItem(items)}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
40
src/components/ContextMenu/src/props.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { PropType } from 'vue';
|
||||
import type { Axis, ContextMenuItem } from './types';
|
||||
export const props = {
|
||||
width: {
|
||||
type: Number as PropType<number>,
|
||||
default: 180,
|
||||
},
|
||||
customEvent: {
|
||||
type: Object as PropType<Event>,
|
||||
default: null,
|
||||
},
|
||||
// 样式
|
||||
styles: {
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
showIcon: {
|
||||
// 是否显示icon
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
axis: {
|
||||
// 鼠标右键点击的位置
|
||||
type: Object as PropType<Axis>,
|
||||
default() {
|
||||
return { x: 0, y: 0 };
|
||||
},
|
||||
},
|
||||
items: {
|
||||
// 最重要的列表,没有的话直接不显示
|
||||
type: Array as PropType<ContextMenuItem[]>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
type: Function as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
};
|
30
src/components/ContextMenu/src/types.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface Axis {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface ContextMenuItem {
|
||||
label: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
handler?: Fn;
|
||||
divider?: boolean;
|
||||
children?: ContextMenuItem[];
|
||||
}
|
||||
export interface Options {
|
||||
event: MouseEvent;
|
||||
icon?: string;
|
||||
styles?: any;
|
||||
items?: ContextMenuItem[];
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
resolve?: (...arg: any) => void;
|
||||
event?: MouseEvent;
|
||||
styles?: any;
|
||||
items: ContextMenuItem[];
|
||||
customEvent?: MouseEvent;
|
||||
axis?: Axis;
|
||||
width?: number;
|
||||
showIcon?: boolean;
|
||||
};
|
3
src/components/Description/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as Description } from './src/index';
|
||||
export * from './src/types';
|
||||
export { useDescription } from './src/useDescription';
|
144
src/components/Description/src/index.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { defineComponent, computed, ref, unref } from 'vue';
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
import { CollapseContainer, CollapseContainerOptions } from '/@/components/Container/index';
|
||||
import type { DescOptions, DescInstance, DescItem } from './types';
|
||||
import descProps from './props';
|
||||
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { deepMerge } from '/@/utils';
|
||||
|
||||
const prefixCls = 'description';
|
||||
export default defineComponent({
|
||||
props: descProps,
|
||||
emits: ['register'],
|
||||
setup(props, { attrs, slots, emit }) {
|
||||
// props来自设置
|
||||
const propsRef = ref<Partial<DescOptions> | null>(null);
|
||||
// 自定义title组件:获得title
|
||||
const getMergeProps = computed(() => {
|
||||
return {
|
||||
...props,
|
||||
...unref(propsRef),
|
||||
};
|
||||
});
|
||||
const getProps = computed(() => {
|
||||
const opt = {
|
||||
...props,
|
||||
...(unref(propsRef) || {}),
|
||||
title: undefined,
|
||||
};
|
||||
return opt;
|
||||
});
|
||||
/**
|
||||
* @description: 是否使用标题
|
||||
*/
|
||||
const useWrapper = computed(() => {
|
||||
return !!unref(getMergeProps).title;
|
||||
});
|
||||
/**
|
||||
* @description: 获取配置Collapse
|
||||
*/
|
||||
const getCollapseOptions = computed(
|
||||
(): CollapseContainerOptions => {
|
||||
return {
|
||||
// 默认不能展开
|
||||
canExpand: false,
|
||||
...unref(getProps).collapseOptions,
|
||||
};
|
||||
}
|
||||
);
|
||||
/**
|
||||
* @description:设置desc
|
||||
*/
|
||||
function setDescProps(descProps: Partial<DescOptions>): void {
|
||||
// 保留上一次的setDrawerProps
|
||||
const mergeProps = deepMerge(unref(propsRef) || {}, descProps);
|
||||
propsRef.value = cloneDeep(mergeProps);
|
||||
}
|
||||
const methods: DescInstance = {
|
||||
setDescProps,
|
||||
};
|
||||
emit('register', methods);
|
||||
|
||||
// 防止换行
|
||||
function renderLabel({ label, labelMinWidth, labelStyle }: DescItem) {
|
||||
if (!labelStyle && !labelMinWidth) {
|
||||
return label;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...labelStyle,
|
||||
|
||||
minWidth: `${labelMinWidth}px`,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderItem() {
|
||||
const { schema } = unref(getProps);
|
||||
return unref(schema).map((item) => {
|
||||
const { render, field, span, show, contentMinWidth } = item;
|
||||
const { data } = unref(getProps) as any;
|
||||
if (show && isFunction(show) && !show(data)) {
|
||||
return null;
|
||||
}
|
||||
const getContent = () =>
|
||||
isFunction(render)
|
||||
? render(data && data[field], data)
|
||||
: unref(data) && unref(data)[field];
|
||||
|
||||
const width = contentMinWidth;
|
||||
return (
|
||||
<Descriptions.Item label={renderLabel(item)} key={field} span={span}>
|
||||
{() =>
|
||||
contentMinWidth ? (
|
||||
<div
|
||||
style={{
|
||||
minWidth: `${width}px`,
|
||||
}}
|
||||
>
|
||||
{getContent()}
|
||||
</div>
|
||||
) : (
|
||||
getContent()
|
||||
)
|
||||
}
|
||||
</Descriptions.Item>
|
||||
);
|
||||
});
|
||||
}
|
||||
const renderDesc = () => {
|
||||
return (
|
||||
<Descriptions class={`${prefixCls}`} {...{ ...attrs, ...unref(getProps) }}>
|
||||
{() => renderItem()}
|
||||
</Descriptions>
|
||||
);
|
||||
};
|
||||
const renderContainer = () => {
|
||||
const content = props.useCollapse ? renderDesc() : <div>{renderDesc()}</div>;
|
||||
// 减少dom层级
|
||||
return props.useCollapse ? (
|
||||
<CollapseContainer
|
||||
title={unref(getMergeProps).title}
|
||||
canExpan={unref(getCollapseOptions).canExpand}
|
||||
helpMessage={unref(getCollapseOptions).helpMessage}
|
||||
>
|
||||
{{
|
||||
default: () => content,
|
||||
action: () => getSlot(slots, 'action'),
|
||||
}}
|
||||
</CollapseContainer>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
};
|
||||
|
||||
return () => (unref(useWrapper) ? renderContainer() : renderDesc());
|
||||
},
|
||||
});
|
39
src/components/Description/src/props.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { PropType } from 'vue';
|
||||
import type { DescItem } from './types';
|
||||
|
||||
export default {
|
||||
useCollapse: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<'small' | 'default' | 'middle' | undefined>,
|
||||
default: 'small',
|
||||
},
|
||||
bordered: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
column: {
|
||||
type: [Number, Object] as PropType<number | any>,
|
||||
default: () => {
|
||||
return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 };
|
||||
},
|
||||
},
|
||||
collapseOptions: {
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
schema: {
|
||||
type: Array as PropType<Array<DescItem>>,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
};
|
95
src/components/Description/src/types.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { VNode } from 'vue';
|
||||
import type { CollapseContainerOptions } from '/@/components/Container/index';
|
||||
|
||||
export interface DescItem {
|
||||
// 最小宽度
|
||||
labelMinWidth?: number;
|
||||
|
||||
contentMinWidth?: number;
|
||||
|
||||
labelStyle?: any;
|
||||
|
||||
field: string;
|
||||
label: string;
|
||||
// 和并列
|
||||
span?: number;
|
||||
show?: (...arg: any) => boolean;
|
||||
// render
|
||||
render?: (val: string, data: any) => VNode | undefined | Element | string | number;
|
||||
}
|
||||
|
||||
export interface DescOptions {
|
||||
// 是否包含collapse组件
|
||||
useCollapse?: boolean;
|
||||
/**
|
||||
* item配置
|
||||
* @type DescItem
|
||||
*/
|
||||
schema: DescItem[];
|
||||
/**
|
||||
* 数据
|
||||
* @type object
|
||||
*/
|
||||
data: object;
|
||||
/**
|
||||
* 内置的CollapseContainer组件配置
|
||||
* @type CollapseContainerOptions
|
||||
*/
|
||||
collapseOptions?: CollapseContainerOptions;
|
||||
/**
|
||||
* descriptions size type
|
||||
* @default 'default'
|
||||
* @type string
|
||||
*/
|
||||
size?: 'default' | 'middle' | 'small';
|
||||
|
||||
/**
|
||||
* custom prefixCls
|
||||
* @type string
|
||||
*/
|
||||
prefixCls?: string;
|
||||
|
||||
/**
|
||||
* whether descriptions have border
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
bordered?: boolean;
|
||||
|
||||
/**
|
||||
* custom title
|
||||
* @type any
|
||||
*/
|
||||
title?: any;
|
||||
|
||||
/**
|
||||
* the number of descriptionsitem in one line
|
||||
* @default 3
|
||||
* @type number | object
|
||||
*/
|
||||
column?: number | object;
|
||||
|
||||
/**
|
||||
* descriptions layout
|
||||
* @default 'horizontal'
|
||||
* @type string
|
||||
*/
|
||||
layout?: 'horizontal' | 'vertical';
|
||||
|
||||
/**
|
||||
* whether have colon in descriptionsitem
|
||||
* @default true
|
||||
* @type boolean
|
||||
*/
|
||||
colon?: boolean;
|
||||
}
|
||||
|
||||
export interface DescInstance {
|
||||
setDescProps(descProps: Partial<DescOptions>): void;
|
||||
}
|
||||
|
||||
export type Register = (descInstance: DescInstance) => void;
|
||||
/**
|
||||
* @description:
|
||||
*/
|
||||
export type UseDescReturnType = [Register, DescInstance];
|
27
src/components/Description/src/useDescription.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ref, getCurrentInstance, unref } from 'vue';
|
||||
import { isProdMode } from '/@/utils/env';
|
||||
|
||||
import type { DescOptions, DescInstance, UseDescReturnType } from './types';
|
||||
|
||||
export function useDescription(props?: Partial<DescOptions>): UseDescReturnType {
|
||||
if (!getCurrentInstance()) {
|
||||
throw new Error('Please put useDescription function in the setup function!');
|
||||
}
|
||||
const descRef = ref<DescInstance | null>(null);
|
||||
const loadedRef = ref(false);
|
||||
|
||||
function getDescription(instance: DescInstance) {
|
||||
if (unref(loadedRef) && isProdMode()) {
|
||||
return;
|
||||
}
|
||||
descRef.value = instance;
|
||||
props && instance.setDescProps(props);
|
||||
loadedRef.value = true;
|
||||
}
|
||||
const methods: DescInstance = {
|
||||
setDescProps: (descProps: Partial<DescOptions>): void => {
|
||||
unref(descRef)!.setDescProps(descProps);
|
||||
},
|
||||
};
|
||||
return [getDescription, methods];
|
||||
}
|
4
src/components/Drawer/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as BasicDrawer } from './src/BasicDrawer';
|
||||
|
||||
export { useDrawer, useDrawerInner } from './src/useDrawer';
|
||||
export * from './src/types';
|
279
src/components/Drawer/src/BasicDrawer.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
import { Drawer, Row, Col, Button } from 'ant-design-vue';
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
computed,
|
||||
watchEffect,
|
||||
watch,
|
||||
unref,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
toRaw,
|
||||
} from 'vue';
|
||||
import { BasicTitle } from '/@/components/Basic';
|
||||
import { ScrollContainer, ScrollContainerOptions } from '/@/components/Container/index';
|
||||
import { FullLoading } from '/@/components/Loading/index';
|
||||
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||
|
||||
import { DrawerInstance, DrawerProps, DrawerType } from './types';
|
||||
|
||||
import { basicProps } from './props';
|
||||
import { isFunction, isNumber } from '/@/utils/is';
|
||||
import { LeftOutlined } from '@ant-design/icons-vue';
|
||||
// import { appStore } from '/@/store/modules/app';
|
||||
// import { useRouter } from 'vue-router';
|
||||
import { buildUUID } from '/@/utils/uuid';
|
||||
import { deepMerge } from '/@/utils';
|
||||
import './index.less';
|
||||
|
||||
const prefixCls = 'basic-drawer';
|
||||
export default defineComponent({
|
||||
// inheritAttrs: false,
|
||||
props: basicProps,
|
||||
emits: ['visible-change', 'ok', 'close', 'register'],
|
||||
setup(props, { slots, emit, attrs }) {
|
||||
// const { currentRoute } = useRouter();
|
||||
const scrollRef = ref<any>(null);
|
||||
/**
|
||||
* @description: 获取配置ScrollContainer
|
||||
*/
|
||||
const getScrollOptions = computed(
|
||||
(): ScrollContainerOptions => {
|
||||
return {
|
||||
...(props.scrollOptions as any),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const visibleRef = ref(false);
|
||||
const propsRef = ref<Partial<DrawerProps> | null>(null);
|
||||
|
||||
// 自定义title组件:获得title
|
||||
const getMergeProps = computed((): any => {
|
||||
return deepMerge(toRaw(props), unref(propsRef));
|
||||
});
|
||||
|
||||
const getProps = computed(() => {
|
||||
const opt: any = {
|
||||
// @ts-ignore
|
||||
placement: 'right',
|
||||
...attrs,
|
||||
...props,
|
||||
...(unref(propsRef) as any),
|
||||
visible: unref(visibleRef),
|
||||
};
|
||||
opt.title = undefined;
|
||||
|
||||
if (opt.drawerType === DrawerType.DETAIL) {
|
||||
if (!opt.width) {
|
||||
opt.width = '100%';
|
||||
}
|
||||
opt.wrapClassName = opt.wrapClassName
|
||||
? `${opt.wrapClassName} ${prefixCls}__detail`
|
||||
: `${prefixCls}__detail`;
|
||||
// opt.maskClosable = false;
|
||||
if (!opt.getContainer) {
|
||||
opt.getContainer = `.default-layout__main`;
|
||||
}
|
||||
}
|
||||
return opt;
|
||||
});
|
||||
watchEffect(() => {
|
||||
visibleRef.value = props.visible;
|
||||
});
|
||||
watch(
|
||||
() => visibleRef.value,
|
||||
(visible) => {
|
||||
// appStore.commitLockMainScrollState(visible);
|
||||
nextTick(() => {
|
||||
emit('visible-change', visible);
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: false,
|
||||
}
|
||||
);
|
||||
|
||||
// watch(
|
||||
// () => currentRoute.value.path,
|
||||
// () => {
|
||||
// if (unref(visibleRef)) {
|
||||
// visibleRef.value = false;
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
function scrollBottom() {
|
||||
const scroll = unref(scrollRef);
|
||||
if (scroll) {
|
||||
scroll.scrollBottom();
|
||||
}
|
||||
}
|
||||
function scrollTo(to: number) {
|
||||
const scroll = unref(scrollRef);
|
||||
if (scroll) {
|
||||
scroll.scrollTo(to);
|
||||
}
|
||||
}
|
||||
function getScrollWrap() {
|
||||
const scroll = unref(scrollRef);
|
||||
if (scroll) {
|
||||
return scroll.getScrollWrap();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// 取消事件
|
||||
async function onClose(e: any) {
|
||||
const { closeFunc } = unref(getProps);
|
||||
emit('close', e);
|
||||
if (closeFunc && isFunction(closeFunc)) {
|
||||
const res = await closeFunc();
|
||||
res && (visibleRef.value = false);
|
||||
return;
|
||||
}
|
||||
visibleRef.value = false;
|
||||
}
|
||||
|
||||
function setDrawerProps(props: Partial<DrawerProps>): void {
|
||||
// 保留上一次的setDrawerProps
|
||||
propsRef.value = deepMerge(unref(propsRef) || {}, props);
|
||||
if (Reflect.has(props, 'visible')) {
|
||||
visibleRef.value = !!props.visible;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部按钮自定义实现,
|
||||
const getFooterHeight = computed(() => {
|
||||
const { footerHeight, showFooter }: DrawerProps = unref(getProps);
|
||||
if (showFooter && footerHeight) {
|
||||
return isNumber(footerHeight) ? `${footerHeight}px` : `${footerHeight.replace('px', '')}px`;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
function renderFooter() {
|
||||
const {
|
||||
showCancelBtn,
|
||||
cancelButtonProps,
|
||||
cancelText,
|
||||
showOkBtn,
|
||||
okType,
|
||||
okText,
|
||||
okButtonProps,
|
||||
confirmLoading,
|
||||
showFooter,
|
||||
}: DrawerProps = unref(getProps);
|
||||
|
||||
return (
|
||||
getSlot(slots, 'footer') ||
|
||||
(showFooter && (
|
||||
<div class={`${prefixCls}__footer`}>
|
||||
{getSlot(slots, 'insertFooter')}
|
||||
|
||||
{showCancelBtn && (
|
||||
<Button {...cancelButtonProps} onClick={onClose} class="mr-2">
|
||||
{() => cancelText}
|
||||
</Button>
|
||||
)}
|
||||
{getSlot(slots, 'centerFooter')}
|
||||
{showOkBtn && (
|
||||
<Button
|
||||
type={okType}
|
||||
{...okButtonProps}
|
||||
loading={confirmLoading}
|
||||
onClick={() => {
|
||||
emit('ok');
|
||||
}}
|
||||
>
|
||||
{() => okText}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{getSlot(slots, 'appendFooter')}
|
||||
</div>
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
const { title } = unref(getMergeProps);
|
||||
return props.drawerType === DrawerType.DETAIL ? (
|
||||
getSlot(slots, 'title') || (
|
||||
<Row type="flex" align="middle" class={`${prefixCls}__detail-header`}>
|
||||
{() => (
|
||||
<>
|
||||
{props.showDetailBack && (
|
||||
<Col class="mx-2">
|
||||
{() => (
|
||||
<Button size="small" type="link" onClick={onClose}>
|
||||
{() => <LeftOutlined />}
|
||||
</Button>
|
||||
)}
|
||||
</Col>
|
||||
)}
|
||||
{title && (
|
||||
<Col style="flex:1" class={[`${prefixCls}__detail-title`, 'ellipsis', 'px-2']}>
|
||||
{() => title}
|
||||
</Col>
|
||||
)}
|
||||
{getSlot(slots, 'titleToolbar')}
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
) : (
|
||||
<BasicTitle>{() => title || getSlot(slots, 'title')}</BasicTitle>
|
||||
);
|
||||
}
|
||||
|
||||
const currentInstance = getCurrentInstance() as any;
|
||||
if (getCurrentInstance()) {
|
||||
currentInstance.scrollBottom = scrollBottom;
|
||||
currentInstance.scrollTo = scrollTo;
|
||||
currentInstance.getScrollWrap = getScrollWrap;
|
||||
}
|
||||
const drawerInstance: DrawerInstance = {
|
||||
setDrawerProps: setDrawerProps,
|
||||
};
|
||||
|
||||
const uuid = buildUUID();
|
||||
emit('register', drawerInstance, uuid);
|
||||
|
||||
return () => {
|
||||
const footerHeight = unref(getFooterHeight);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
class={prefixCls}
|
||||
onClose={onClose}
|
||||
{...{
|
||||
...attrs,
|
||||
...unref(getProps),
|
||||
}}
|
||||
>
|
||||
{{
|
||||
title: () => renderHeader(),
|
||||
default: () => (
|
||||
<>
|
||||
<FullLoading
|
||||
absolute
|
||||
class={[!unref(getProps).loading ? 'hidden' : '']}
|
||||
tip="加载中..."
|
||||
/>
|
||||
<ScrollContainer
|
||||
ref={scrollRef}
|
||||
{...{ ...attrs, ...unref(getScrollOptions) }}
|
||||
style={{
|
||||
height: `calc(100% - ${footerHeight})`,
|
||||
}}
|
||||
>
|
||||
{() => getSlot(slots, 'default')}
|
||||
</ScrollContainer>
|
||||
{renderFooter()}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
63
src/components/Drawer/src/index.less
Normal file
@@ -0,0 +1,63 @@
|
||||
@import (reference) '../../../design/index.less';
|
||||
@header-height: 50px;
|
||||
@footer-height: 60px;
|
||||
|
||||
.basic-drawer {
|
||||
.ant-drawer-wrapper-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
&:hover {
|
||||
color: @error-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
height: calc(100% - @header-height);
|
||||
padding: 0;
|
||||
background-color: @background-color-dark;
|
||||
|
||||
.scrollbar__wrap {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__detail {
|
||||
position: absolute;
|
||||
|
||||
&-header {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-drawer-header {
|
||||
width: 100%;
|
||||
height: @header-height;
|
||||
padding: 0;
|
||||
border-top: 1px solid @border-color-base;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ant-drawer-title {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
height: @header-height;
|
||||
line-height: @header-height;
|
||||
}
|
||||
|
||||
.scrollbar__wrap {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
height: @footer-height;
|
||||
padding: 0 26px;
|
||||
line-height: @footer-height;
|
||||
text-align: right;
|
||||
background: #fff;
|
||||
border-top: 1px solid @border-color-base;
|
||||
}
|
||||
}
|
85
src/components/Drawer/src/props.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { PropType } from 'vue';
|
||||
import { DrawerType } from './types';
|
||||
// import {DrawerProps} from './types'
|
||||
export const footerProps = {
|
||||
confirmLoading: Boolean as PropType<boolean>,
|
||||
/**
|
||||
* @description: 显示关闭按钮
|
||||
*/
|
||||
showCancelBtn: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
cancelButtonProps: Object as PropType<any>,
|
||||
cancelText: {
|
||||
type: String as PropType<string>,
|
||||
default: '关闭',
|
||||
},
|
||||
/**
|
||||
* @description: 显示确认按钮
|
||||
*/
|
||||
showOkBtn: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
okButtonProps: Object as PropType<any>,
|
||||
okText: {
|
||||
type: String as PropType<string>,
|
||||
default: '保存',
|
||||
},
|
||||
okType: {
|
||||
type: String as PropType<string>,
|
||||
default: 'primary',
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
footerHeight: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: 60,
|
||||
},
|
||||
};
|
||||
export const basicProps = {
|
||||
drawerType: {
|
||||
type: Number as PropType<number>,
|
||||
default: DrawerType.DEFAULT,
|
||||
},
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
showDetailBack: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
maskClosable: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
getContainer: {
|
||||
type: [Object, String] as PropType<any>,
|
||||
},
|
||||
scrollOptions: {
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
closeFunc: {
|
||||
type: [Function, Object] as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
triggerWindowResize: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
destroyOnClose: Boolean as PropType<boolean>,
|
||||
...footerProps,
|
||||
};
|
194
src/components/Drawer/src/types.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import type { Button } from 'ant-design-vue/types/button/button';
|
||||
import type { CSSProperties, VNodeChild } from 'vue';
|
||||
import type { ScrollContainerOptions } from '/@/components/Container/index';
|
||||
|
||||
export interface DrawerInstance {
|
||||
setDrawerProps: (props: Partial<DrawerProps> | boolean) => void;
|
||||
}
|
||||
export interface ReturnMethods extends DrawerInstance {
|
||||
openDrawer: (visible?: boolean) => void;
|
||||
transferDrawerData: (data: any) => void;
|
||||
}
|
||||
export type RegisterFn = (drawerInstance: DrawerInstance, uuid?: string) => void;
|
||||
|
||||
export interface ReturnInnerMethods extends DrawerInstance {
|
||||
closeDrawer: () => void;
|
||||
changeLoading: (loading: boolean) => void;
|
||||
changeOkLoading: (loading: boolean) => void;
|
||||
receiveDrawerDataRef: any;
|
||||
}
|
||||
|
||||
export type UseDrawerReturnType = [RegisterFn, ReturnMethods];
|
||||
export type UseDrawerInnerReturnType = [RegisterFn, ReturnInnerMethods];
|
||||
export enum DrawerType {
|
||||
DETAIL,
|
||||
DEFAULT,
|
||||
}
|
||||
|
||||
export interface DrawerFooterProps {
|
||||
showOkBtn: boolean;
|
||||
showCancelBtn: boolean;
|
||||
/**
|
||||
* Text of the Cancel button
|
||||
* @default 'cancel'
|
||||
* @type string
|
||||
*/
|
||||
cancelText: string;
|
||||
/**
|
||||
* Text of the OK button
|
||||
* @default 'OK'
|
||||
* @type string
|
||||
*/
|
||||
okText: string;
|
||||
|
||||
/**
|
||||
* Button type of the OK button
|
||||
* @default 'primary'
|
||||
* @type string
|
||||
*/
|
||||
okType: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default';
|
||||
/**
|
||||
* The ok button props, follow jsx rules
|
||||
* @type object
|
||||
*/
|
||||
okButtonProps: { props: Button; on: {} };
|
||||
|
||||
/**
|
||||
* The cancel button props, follow jsx rules
|
||||
* @type object
|
||||
*/
|
||||
cancelButtonProps: { props: Button; on: {} };
|
||||
/**
|
||||
* Whether to apply loading visual effect for OK button or not
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
confirmLoading: boolean;
|
||||
|
||||
showFooter: boolean;
|
||||
footerHeight: string | number;
|
||||
}
|
||||
export interface DrawerProps extends DrawerFooterProps {
|
||||
drawerType: DrawerType;
|
||||
loading?: boolean;
|
||||
showDetailBack?: boolean;
|
||||
visible?: boolean;
|
||||
/**
|
||||
* 内置的ScrollContainer组件配置
|
||||
* @type ScrollContainerOptions
|
||||
*/
|
||||
scrollOptions?: ScrollContainerOptions;
|
||||
closeFunc?: () => Promise<void>;
|
||||
triggerWindowResize?: boolean;
|
||||
/**
|
||||
* Whether a close (x) button is visible on top right of the Drawer dialog or not.
|
||||
* @default true
|
||||
* @type boolean
|
||||
*/
|
||||
closable?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to unmount child components on closing drawer or not.
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
destroyOnClose?: boolean;
|
||||
|
||||
/**
|
||||
* Return the mounted node for Drawer.
|
||||
* @default 'body'
|
||||
* @type any ( HTMLElement| () => HTMLElement | string)
|
||||
*/
|
||||
getContainer?: () => HTMLElement | string;
|
||||
|
||||
/**
|
||||
* Whether to show mask or not.
|
||||
* @default true
|
||||
* @type boolean
|
||||
*/
|
||||
mask?: boolean;
|
||||
|
||||
/**
|
||||
* Clicking on the mask (area outside the Drawer) to close the Drawer or not.
|
||||
* @default true
|
||||
* @type boolean
|
||||
*/
|
||||
maskClosable?: boolean;
|
||||
|
||||
/**
|
||||
* Style for Drawer's mask element.
|
||||
* @default {}
|
||||
* @type object
|
||||
*/
|
||||
maskStyle?: CSSProperties;
|
||||
|
||||
/**
|
||||
* The title for Drawer.
|
||||
* @type any (string | slot)
|
||||
*/
|
||||
title?: VNodeChild | JSX.Element;
|
||||
|
||||
/**
|
||||
* The class name of the container of the Drawer dialog.
|
||||
* @type string
|
||||
*/
|
||||
wrapClassName?: string;
|
||||
|
||||
/**
|
||||
* Style of wrapper element which **contains mask** compare to `drawerStyle`
|
||||
* @type object
|
||||
*/
|
||||
wrapStyle?: CSSProperties;
|
||||
|
||||
/**
|
||||
* Style of the popup layer element
|
||||
* @type object
|
||||
*/
|
||||
drawerStyle?: CSSProperties;
|
||||
|
||||
/**
|
||||
* Style of floating layer, typically used for adjusting its position.
|
||||
* @type object
|
||||
*/
|
||||
bodyStyle?: CSSProperties;
|
||||
headerStyle?: CSSProperties;
|
||||
|
||||
/**
|
||||
* Width of the Drawer dialog.
|
||||
* @default 256
|
||||
* @type string | number
|
||||
*/
|
||||
width?: string | number;
|
||||
|
||||
/**
|
||||
* placement is top or bottom, height of the Drawer dialog.
|
||||
* @type string | number
|
||||
*/
|
||||
height?: string | number;
|
||||
|
||||
/**
|
||||
* The z-index of the Drawer.
|
||||
* @default 1000
|
||||
* @type number
|
||||
*/
|
||||
zIndex?: number;
|
||||
|
||||
/**
|
||||
* The placement of the Drawer.
|
||||
* @default 'right'
|
||||
* @type string
|
||||
*/
|
||||
placement?: 'top' | 'right' | 'bottom' | 'left';
|
||||
afterVisibleChange?: (visible?: boolean) => void;
|
||||
keyboard?: boolean;
|
||||
|
||||
/**
|
||||
* Specify a callback that will be called when a user clicks mask, close button or Cancel button.
|
||||
*/
|
||||
onClose?: (e?: Event) => void;
|
||||
}
|
||||
export interface DrawerActionType {
|
||||
scrollBottom: () => void;
|
||||
scrollTo: (to: number) => void;
|
||||
getScrollWrap: () => Element | null;
|
||||
}
|
100
src/components/Drawer/src/useDrawer.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type {
|
||||
UseDrawerReturnType,
|
||||
DrawerInstance,
|
||||
ReturnMethods,
|
||||
DrawerProps,
|
||||
UseDrawerInnerReturnType,
|
||||
} from './types';
|
||||
import { ref, getCurrentInstance, onUnmounted, unref, reactive, computed } from 'vue';
|
||||
import { isProdMode } from '/@/utils/env';
|
||||
|
||||
const dataTransferRef = reactive<any>({});
|
||||
/**
|
||||
* @description: 适用于将drawer独立出去,外面调用
|
||||
*/
|
||||
export function useDrawer(): UseDrawerReturnType {
|
||||
if (!getCurrentInstance()) {
|
||||
throw new Error('Please put useDrawer function in the setup function!');
|
||||
}
|
||||
const drawerRef = ref<DrawerInstance | null>(null);
|
||||
const loadedRef = ref<boolean | null>(false);
|
||||
const uidRef = ref<string>('');
|
||||
|
||||
function getDrawer(drawerInstance: DrawerInstance, uuid: string) {
|
||||
uidRef.value = uuid;
|
||||
isProdMode() &&
|
||||
onUnmounted(() => {
|
||||
drawerRef.value = null;
|
||||
loadedRef.value = null;
|
||||
dataTransferRef[unref(uidRef)] = null;
|
||||
});
|
||||
if (unref(loadedRef) && isProdMode() && drawerInstance === unref(drawerRef)) {
|
||||
return;
|
||||
}
|
||||
drawerRef.value = drawerInstance;
|
||||
loadedRef.value = true;
|
||||
}
|
||||
const getInstance = () => {
|
||||
const instance = unref(drawerRef);
|
||||
if (!instance) {
|
||||
throw new Error('instance is undefined!');
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
const methods: ReturnMethods = {
|
||||
setDrawerProps: (props: Partial<DrawerProps>): void => {
|
||||
getInstance().setDrawerProps(props);
|
||||
},
|
||||
openDrawer: (visible = true): void => {
|
||||
getInstance().setDrawerProps({
|
||||
visible: visible,
|
||||
});
|
||||
},
|
||||
transferDrawerData(val: any) {
|
||||
dataTransferRef[unref(uidRef)] = val;
|
||||
},
|
||||
};
|
||||
|
||||
return [getDrawer, methods];
|
||||
}
|
||||
export const useDrawerInner = (): UseDrawerInnerReturnType => {
|
||||
const drawerInstanceRef = ref<DrawerInstance | null>(null);
|
||||
const currentInstall = getCurrentInstance();
|
||||
const uidRef = ref<string>('');
|
||||
|
||||
if (!currentInstall) {
|
||||
throw new Error('instance is undefined!');
|
||||
}
|
||||
const getInstance = () => {
|
||||
const instance = unref(drawerInstanceRef);
|
||||
if (!instance) {
|
||||
throw new Error('instance is undefined!');
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
const register = (modalInstance: DrawerInstance, uuid: string) => {
|
||||
uidRef.value = uuid;
|
||||
drawerInstanceRef.value = modalInstance;
|
||||
currentInstall.emit('register', modalInstance);
|
||||
};
|
||||
return [
|
||||
register,
|
||||
{
|
||||
receiveDrawerDataRef: computed(() => {
|
||||
return dataTransferRef[unref(uidRef)];
|
||||
}),
|
||||
changeLoading: (loading = true) => {
|
||||
getInstance().setDrawerProps({ loading });
|
||||
},
|
||||
changeOkLoading: (loading = true) => {
|
||||
getInstance().setDrawerProps({ confirmLoading: loading });
|
||||
},
|
||||
closeDrawer: () => {
|
||||
getInstance().setDrawerProps({ visible: false });
|
||||
},
|
||||
setDrawerProps: (props: Partial<DrawerProps>) => {
|
||||
getInstance().setDrawerProps(props);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
55
src/components/Dropdown/Dropdown.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
import { Dropdown, Menu } from 'ant-design-vue';
|
||||
|
||||
import Icon from '/@/components/Icon/index';
|
||||
|
||||
import { basicDropdownProps } from './props';
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Dropdown',
|
||||
props: basicDropdownProps,
|
||||
setup(props, { slots, emit, attrs }) {
|
||||
const getMenuList = computed(() => props.dropMenuList);
|
||||
|
||||
function handleClickMenu({ key }: any) {
|
||||
const menu = unref(getMenuList)[key];
|
||||
emit('menuEvent', menu);
|
||||
}
|
||||
|
||||
function renderMenus() {
|
||||
return (
|
||||
<Menu onClick={handleClickMenu}>
|
||||
{() => (
|
||||
<>
|
||||
{unref(getMenuList).map((item, index) => {
|
||||
const { disabled, icon, text, divider } = item;
|
||||
|
||||
return [
|
||||
<Menu.Item key={`${index}`} disabled={disabled}>
|
||||
{() => (
|
||||
<>
|
||||
{icon && <Icon icon={icon} />}
|
||||
<span class="ml-1">{text}</span>
|
||||
</>
|
||||
)}
|
||||
</Menu.Item>,
|
||||
divider && <Menu.Divider key={`d-${index}`} />,
|
||||
];
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
return () => (
|
||||
<Dropdown trigger={props.trigger as any} {...attrs}>
|
||||
{{
|
||||
default: () => <span>{getSlot(slots)}</span>,
|
||||
overlay: () => renderMenus(),
|
||||
}}
|
||||
</Dropdown>
|
||||
);
|
||||
},
|
||||
});
|
2
src/components/Dropdown/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Dropdown } from './Dropdown';
|
||||
export * from './types';
|
69
src/components/Dropdown/props.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { PropType } from 'vue';
|
||||
/**
|
||||
* @description: 基础表格参数配置
|
||||
*/
|
||||
export const dropdownProps = {
|
||||
/**
|
||||
* the trigger mode which executes the drop-down action
|
||||
* @default ['hover']
|
||||
* @type string[]
|
||||
*/
|
||||
trigger: {
|
||||
type: [Array] as PropType<string[]>,
|
||||
default: () => {
|
||||
return ['contextmenu'];
|
||||
},
|
||||
},
|
||||
|
||||
// /**
|
||||
// * the dropdown menu
|
||||
// * @type () => Menu
|
||||
// */
|
||||
// overlay: {
|
||||
// type: null,
|
||||
// },
|
||||
|
||||
// /**
|
||||
// * Class name of the dropdown root element
|
||||
// * @type string
|
||||
// */
|
||||
// overlayClassName: String,
|
||||
|
||||
// /**
|
||||
// * Style of the dropdown root element
|
||||
// * @type object
|
||||
// */
|
||||
// overlayStyle: Object,
|
||||
|
||||
// /**
|
||||
// * whether the dropdown menu is visible
|
||||
// * @type boolean
|
||||
// */
|
||||
// visible: Boolean,
|
||||
|
||||
// /**
|
||||
// * whether the dropdown menu is disabled
|
||||
// * @type boolean
|
||||
// */
|
||||
// disabled: Boolean,
|
||||
|
||||
// /**
|
||||
// * to set the ontainer of the dropdown menu. The default is to create a div element in body, you can reset it to the scrolling area and make a relative reposition.
|
||||
// * @default () => document.body
|
||||
// * @type Function
|
||||
// */
|
||||
// getPopupContainer: Function,
|
||||
|
||||
// /**
|
||||
// * placement of pop menu: bottomLeft bottomCenter bottomRight topLeft topCenter topRight
|
||||
// * @default 'bottomLeft'
|
||||
// * @type string
|
||||
// */
|
||||
// placement: String,
|
||||
};
|
||||
export const basicDropdownProps = Object.assign({}, dropdownProps, {
|
||||
dropMenuList: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
8
src/components/Dropdown/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface DropMenu {
|
||||
to?: string;
|
||||
icon?: string;
|
||||
event: string | number;
|
||||
text: string;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
}
|
7
src/components/Form/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { default as BasicForm } from './src/BasicForm.vue';
|
||||
|
||||
export * from './src/types/form';
|
||||
export * from './src/types/formItem';
|
||||
|
||||
export { useComponentRegister } from './src/hooks/useComponentRegister';
|
||||
export { useForm } from './src/hooks/useForm';
|
463
src/components/Form/src/BasicForm.vue
Normal file
@@ -0,0 +1,463 @@
|
||||
<template>
|
||||
<Form v-bind="$attrs" ref="formElRef" :model="formModel">
|
||||
<Row :class="getProps.compact ? 'compact-form-row' : ''">
|
||||
<slot name="formHeader" />
|
||||
<template v-for="schema in getSchema" :key="schema.field">
|
||||
<FormItem
|
||||
:schema="schema"
|
||||
:formProps="getProps"
|
||||
:allDefaultValues="getAllDefaultValues"
|
||||
:formModel="formModel"
|
||||
>
|
||||
<template v-slot:[item] v-for="item in Object.keys($slots)">
|
||||
<slot :name="item" />
|
||||
</template>
|
||||
</FormItem>
|
||||
</template>
|
||||
<FormAction
|
||||
v-bind="{ ...getActionPropsRef, ...advanceState }"
|
||||
@toggle-advanced="handleToggleAdvanced"
|
||||
/>
|
||||
<slot name="formFooter" />
|
||||
</Row>
|
||||
</Form>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { FormActionType, FormProps, FormSchema } from './types/form';
|
||||
import type { Form as FormType, ValidateFields } from 'ant-design-vue/types/form/form';
|
||||
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
ref,
|
||||
computed,
|
||||
unref,
|
||||
toRaw,
|
||||
watch,
|
||||
toRef,
|
||||
onMounted,
|
||||
} from 'vue';
|
||||
import { Form, Row } from 'ant-design-vue';
|
||||
import FormItem from './FormItem';
|
||||
import { basicProps } from './props';
|
||||
import { deepMerge, unique } from '/@/utils';
|
||||
import FormAction from './FormAction';
|
||||
|
||||
import { dateItemType } from './helper';
|
||||
import moment from 'moment';
|
||||
import { isArray, isBoolean, isFunction, isNumber, isObject, isString } from '/@/utils/is';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useBreakpoint } from '/@/hooks/event/useBreakpoint';
|
||||
import { useThrottle } from '/@/hooks/core/useThrottle';
|
||||
import { useFormValues } from './hooks/useFormValues';
|
||||
import type { ColEx } from './types';
|
||||
import { NamePath } from 'ant-design-vue/types/form/form-item';
|
||||
const BASIC_COL_LEN = 24;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicForm',
|
||||
inheritAttrs: false,
|
||||
components: { FormItem, Form, Row, FormAction },
|
||||
props: basicProps,
|
||||
emits: ['advanced-change', 'reset', 'submit', 'register'],
|
||||
setup(props, { emit }) {
|
||||
let formModel = reactive({});
|
||||
const advanceState = reactive({
|
||||
isAdvanced: true,
|
||||
hideAdvanceBtn: false,
|
||||
isLoad: false,
|
||||
actionSpan: 6,
|
||||
});
|
||||
const propsRef = ref<Partial<FormProps>>({});
|
||||
const schemaRef = ref<FormSchema[] | null>(null);
|
||||
const formElRef = ref<Nullable<FormType>>(null);
|
||||
|
||||
const getMergePropsRef = computed(
|
||||
(): FormProps => {
|
||||
return deepMerge(toRaw(props), unref(propsRef));
|
||||
}
|
||||
);
|
||||
// 获取表单基本配置
|
||||
const getProps = computed(
|
||||
(): FormProps => {
|
||||
const resetAction = {
|
||||
onClick: resetFields,
|
||||
};
|
||||
const submitAction = {
|
||||
onClick: handleSubmit,
|
||||
};
|
||||
return {
|
||||
...unref(getMergePropsRef),
|
||||
resetButtonOptions: deepMerge(
|
||||
resetAction,
|
||||
unref(getMergePropsRef).resetButtonOptions || {}
|
||||
) as any,
|
||||
submitButtonOptions: deepMerge(
|
||||
submitAction,
|
||||
unref(getMergePropsRef).submitButtonOptions || {}
|
||||
) as any,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const getActionPropsRef = computed(() => {
|
||||
const {
|
||||
resetButtonOptions,
|
||||
submitButtonOptions,
|
||||
showActionButtonGroup,
|
||||
showResetButton,
|
||||
showSubmitButton,
|
||||
showAdvancedButton,
|
||||
actionColOptions,
|
||||
} = unref(getProps);
|
||||
return {
|
||||
resetButtonOptions,
|
||||
submitButtonOptions,
|
||||
show: showActionButtonGroup,
|
||||
showResetButton,
|
||||
showSubmitButton,
|
||||
showAdvancedButton,
|
||||
actionColOptions,
|
||||
};
|
||||
});
|
||||
|
||||
const getSchema = computed((): FormSchema[] => {
|
||||
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
|
||||
for (const schema of schemas) {
|
||||
const { defaultValue, component } = schema;
|
||||
if (defaultValue && dateItemType.includes(component!)) {
|
||||
schema.defaultValue = moment(defaultValue);
|
||||
}
|
||||
}
|
||||
return schemas as FormSchema[];
|
||||
});
|
||||
|
||||
const getAllDefaultValues = computed(() => {
|
||||
const schemas = unref(getSchema);
|
||||
const obj: any = {};
|
||||
schemas.forEach((item) => {
|
||||
if (item.defaultValue) {
|
||||
obj[item.field] = item.defaultValue;
|
||||
(formModel as any)[item.field] = item.defaultValue;
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
const getEmptySpanRef = computed((): number => {
|
||||
if (!advanceState.isAdvanced) {
|
||||
return 0;
|
||||
}
|
||||
const emptySpan = unref(getMergePropsRef).emptySpan || 0;
|
||||
|
||||
if (isNumber(emptySpan)) {
|
||||
return emptySpan;
|
||||
}
|
||||
if (isObject(emptySpan)) {
|
||||
const { span = 0 } = emptySpan;
|
||||
const screen = unref(screenRef) as string;
|
||||
|
||||
const screenSpan = (emptySpan as any)[screen.toLowerCase()];
|
||||
return screenSpan || span || 0;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const { realWidthRef, screenEnum, screenRef } = useBreakpoint();
|
||||
const [throttleUpdateAdvanced] = useThrottle(updateAdvanced, 30, { immediate: true });
|
||||
watch(
|
||||
[() => unref(getSchema), () => advanceState.isAdvanced, () => unref(realWidthRef)],
|
||||
() => {
|
||||
const { showAdvancedButton } = unref(getProps);
|
||||
if (showAdvancedButton) {
|
||||
throttleUpdateAdvanced();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
function updateAdvanced() {
|
||||
let itemColSum = 0;
|
||||
let realItemColSum = 0;
|
||||
for (const schema of unref(getSchema)) {
|
||||
const { show, colProps } = schema;
|
||||
let isShow = true;
|
||||
|
||||
if (isBoolean(show)) {
|
||||
isShow = show;
|
||||
}
|
||||
|
||||
if (isFunction(show)) {
|
||||
isShow = show({
|
||||
schema: schema,
|
||||
model: formModel,
|
||||
field: schema.field,
|
||||
values: {
|
||||
...getAllDefaultValues,
|
||||
...formModel,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (isShow && colProps) {
|
||||
const { itemColSum: sum, isAdvanced } = getAdvanced(colProps, itemColSum);
|
||||
|
||||
itemColSum = sum || 0;
|
||||
if (isAdvanced) {
|
||||
realItemColSum = itemColSum;
|
||||
}
|
||||
schema.isAdvanced = isAdvanced;
|
||||
}
|
||||
}
|
||||
advanceState.actionSpan = (realItemColSum % BASIC_COL_LEN) + unref(getEmptySpanRef);
|
||||
getAdvanced(props.actionColOptions || { span: BASIC_COL_LEN }, itemColSum, true);
|
||||
emit('advanced-change');
|
||||
}
|
||||
function getAdvanced(itemCol: Partial<ColEx>, itemColSum = 0, isLastAction = false) {
|
||||
const width = unref(realWidthRef);
|
||||
|
||||
const mdWidth =
|
||||
parseInt(itemCol.md as string) ||
|
||||
parseInt(itemCol.xs as string) ||
|
||||
parseInt(itemCol.sm as string) ||
|
||||
(itemCol.span as number) ||
|
||||
BASIC_COL_LEN;
|
||||
const lgWidth = parseInt(itemCol.lg as string) || mdWidth;
|
||||
const xlWidth = parseInt(itemCol.xl as string) || lgWidth;
|
||||
const xxlWidth = parseInt(itemCol.xxl as string) || xlWidth;
|
||||
if (width <= screenEnum.LG) {
|
||||
itemColSum += mdWidth;
|
||||
} else if (width < screenEnum.XL) {
|
||||
itemColSum += lgWidth;
|
||||
} else if (width < screenEnum.XXL) {
|
||||
itemColSum += xlWidth;
|
||||
} else {
|
||||
itemColSum += xxlWidth;
|
||||
}
|
||||
if (isLastAction) {
|
||||
advanceState.hideAdvanceBtn = false;
|
||||
if (itemColSum <= BASIC_COL_LEN * 2) {
|
||||
// 小于等于2行时,不显示收起展开按钮
|
||||
advanceState.hideAdvanceBtn = true;
|
||||
advanceState.isAdvanced = true;
|
||||
} else if (
|
||||
itemColSum > BASIC_COL_LEN * 2 &&
|
||||
itemColSum <= BASIC_COL_LEN * (props.autoAdvancedLine || 3)
|
||||
) {
|
||||
advanceState.hideAdvanceBtn = false;
|
||||
|
||||
// 大于3行默认收起
|
||||
} else if (!advanceState.isLoad) {
|
||||
advanceState.isLoad = true;
|
||||
advanceState.isAdvanced = !advanceState.isAdvanced;
|
||||
}
|
||||
return { isAdvanced: advanceState.isAdvanced, itemColSum };
|
||||
}
|
||||
if (itemColSum > BASIC_COL_LEN) {
|
||||
return { isAdvanced: advanceState.isAdvanced, itemColSum };
|
||||
} else {
|
||||
// 第一行始终显示
|
||||
return { isAdvanced: true, itemColSum };
|
||||
}
|
||||
}
|
||||
|
||||
async function resetFields(): Promise<any> {
|
||||
const { resetFunc } = unref(getProps);
|
||||
resetFunc && isFunction(resetFunc) && (await resetFunc());
|
||||
const formEl = unref(formElRef);
|
||||
if (!formEl) return;
|
||||
Object.keys(formModel).forEach((key) => {
|
||||
(formModel as any)[key] = undefined;
|
||||
});
|
||||
const values = formEl.resetFields();
|
||||
emit('reset', toRaw(formModel));
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 设置表单值
|
||||
*/
|
||||
async function setFieldsValue(values: any): Promise<void> {
|
||||
const fields = unref(getSchema)
|
||||
.map((item) => item.field)
|
||||
.filter(Boolean);
|
||||
const formEl = unref(formElRef);
|
||||
Object.keys(values).forEach((key) => {
|
||||
const element = values[key];
|
||||
if (fields.includes(key) && element !== undefined && element !== null) {
|
||||
// 时间
|
||||
(formModel as any)[key] = itemIsDateType(key) ? moment(element) : element;
|
||||
if (formEl) {
|
||||
formEl.validateFields([key]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 表单提交
|
||||
*/
|
||||
async function handleSubmit(e?: Event): Promise<void> {
|
||||
e && e.preventDefault();
|
||||
const { submitFunc } = unref(getProps);
|
||||
if (submitFunc && isFunction(submitFunc)) {
|
||||
await submitFunc();
|
||||
return;
|
||||
}
|
||||
const formEl = unref(formElRef);
|
||||
if (!formEl) return;
|
||||
try {
|
||||
const values = await formEl.validate();
|
||||
const res = handleFormValues(values);
|
||||
emit('submit', res);
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 根据字段名删除
|
||||
*/
|
||||
function removeSchemaByFiled(fields: string | string[]): void {
|
||||
const schemaList: FormSchema[] = cloneDeep(unref(getSchema));
|
||||
if (!fields) {
|
||||
return;
|
||||
}
|
||||
let fieldList: string[] = fields as string[];
|
||||
if (isString(fields)) {
|
||||
fieldList = [fields];
|
||||
}
|
||||
for (const field of fieldList) {
|
||||
_removeSchemaByFiled(field, schemaList);
|
||||
}
|
||||
schemaRef.value = schemaList as any;
|
||||
}
|
||||
/**
|
||||
* @description: 根据字段名删除
|
||||
*/
|
||||
function _removeSchemaByFiled(field: string, schemaList: FormSchema[]): void {
|
||||
if (isString(field)) {
|
||||
const index = schemaList.findIndex((schema) => schema.field === field);
|
||||
if (index !== -1) {
|
||||
schemaList.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @description: 往某个字段后面插入,如果没有插入最后一个
|
||||
*/
|
||||
function appendSchemaByField(schema: FormSchema, prefixField?: string) {
|
||||
const schemaList: FormSchema[] = cloneDeep(unref(getSchema));
|
||||
|
||||
const index = schemaList.findIndex((schema) => schema.field === prefixField);
|
||||
const hasInList = schemaList.find((item) => item.field === schema.field);
|
||||
|
||||
if (hasInList) {
|
||||
return;
|
||||
}
|
||||
if (!prefixField || index === -1) {
|
||||
schemaList.push(schema);
|
||||
schemaRef.value = schemaList as any;
|
||||
return;
|
||||
}
|
||||
if (index !== -1) {
|
||||
schemaList.splice(index + 1, 0, schema);
|
||||
}
|
||||
schemaRef.value = schemaList as any;
|
||||
}
|
||||
|
||||
function updateSchema(data: Partial<FormSchema> | Partial<FormSchema>[]) {
|
||||
let updateData: Partial<FormSchema>[] = [];
|
||||
if (isObject(data)) {
|
||||
updateData.push(data as FormSchema);
|
||||
}
|
||||
if (isArray(data)) {
|
||||
updateData = [...data];
|
||||
}
|
||||
const hasField = updateData.every((item) => Reflect.has(item, 'field') && item.field);
|
||||
if (!hasField) {
|
||||
throw new Error('Must pass in the `field` field!');
|
||||
}
|
||||
const schema: FormSchema[] = [];
|
||||
updateData.forEach((item) => {
|
||||
unref(getSchema).forEach((val) => {
|
||||
if (val.field === item.field) {
|
||||
const newScheam = deepMerge(val, item);
|
||||
schema.push(newScheam as FormSchema);
|
||||
} else {
|
||||
schema.push(val);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
schemaRef.value = unique(schema, 'field') as any;
|
||||
}
|
||||
|
||||
function handleToggleAdvanced() {
|
||||
advanceState.isAdvanced = !advanceState.isAdvanced;
|
||||
}
|
||||
|
||||
const handleFormValues = useFormValues(
|
||||
toRef(props, 'transformDateFunc'),
|
||||
toRef(props, 'fieldMapToTime')
|
||||
);
|
||||
function getFieldsValue(): any {
|
||||
const formEl = unref(formElRef);
|
||||
if (!formEl) return;
|
||||
return handleFormValues(toRaw(unref(formModel)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否是时间
|
||||
*/
|
||||
function itemIsDateType(key: string) {
|
||||
return unref(getSchema).some((item) => {
|
||||
return item.field === key ? dateItemType.includes(item.component!) : false;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @description:设置表单
|
||||
*/
|
||||
function setProps(formProps: Partial<FormProps>): void {
|
||||
const mergeProps = deepMerge(unref(propsRef) || {}, formProps);
|
||||
propsRef.value = mergeProps;
|
||||
}
|
||||
|
||||
function validateFields(nameList?: NamePath[] | undefined) {
|
||||
if (!formElRef.value) return;
|
||||
return formElRef.value.validateFields(nameList);
|
||||
}
|
||||
function validate(nameList?: NamePath[] | undefined) {
|
||||
if (!formElRef.value) return;
|
||||
return formElRef.value.validate(nameList);
|
||||
}
|
||||
|
||||
function clearValidate(name: string | string[]) {
|
||||
if (!formElRef.value) return;
|
||||
formElRef.value.clearValidate(name);
|
||||
}
|
||||
|
||||
const methods: Partial<FormActionType> = {
|
||||
getFieldsValue,
|
||||
setFieldsValue,
|
||||
resetFields,
|
||||
updateSchema,
|
||||
setProps,
|
||||
removeSchemaByFiled,
|
||||
appendSchemaByField,
|
||||
clearValidate,
|
||||
validateFields: validateFields as ValidateFields,
|
||||
validate: validate as ValidateFields,
|
||||
};
|
||||
onMounted(() => {
|
||||
emit('register', methods);
|
||||
});
|
||||
return {
|
||||
handleToggleAdvanced,
|
||||
formModel,
|
||||
getActionPropsRef,
|
||||
getAllDefaultValues,
|
||||
advanceState,
|
||||
getProps,
|
||||
formElRef,
|
||||
getSchema,
|
||||
...methods,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
141
src/components/Form/src/FormAction.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { defineComponent, unref, computed, PropType } from 'vue';
|
||||
import { Form, Col } from 'ant-design-vue';
|
||||
import type { ColEx } from './types/index';
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||
import Button from '/@/components/Button/index.vue';
|
||||
import { UpOutlined, DownOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicFormAction',
|
||||
emits: ['toggle-advanced'],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showResetButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showSubmitButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showAdvancedButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
resetButtonOptions: {
|
||||
type: Object as PropType<any>,
|
||||
default: {},
|
||||
},
|
||||
submitButtonOptions: {
|
||||
type: Object as PropType<any>,
|
||||
default: {},
|
||||
},
|
||||
actionColOptions: {
|
||||
type: Object as PropType<any>,
|
||||
default: {},
|
||||
},
|
||||
actionSpan: {
|
||||
type: Number,
|
||||
default: 6,
|
||||
},
|
||||
isAdvanced: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideAdvanceBtn: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { slots, emit }) {
|
||||
const getResetBtnOptionsRef = computed(() => {
|
||||
return {
|
||||
text: '重置',
|
||||
...props.resetButtonOptions,
|
||||
};
|
||||
});
|
||||
const getSubmitBtnOptionsRef = computed(() => {
|
||||
return {
|
||||
text: '查询',
|
||||
// htmlType: 'submit',
|
||||
...props.submitButtonOptions,
|
||||
};
|
||||
});
|
||||
|
||||
const actionColOpt = computed(() => {
|
||||
const { showAdvancedButton, actionSpan: span, actionColOptions } = props;
|
||||
const actionSpan = 24 - span;
|
||||
const advancedSpanObj = showAdvancedButton ? { span: actionSpan < 6 ? 24 : actionSpan } : {};
|
||||
const actionColOpt: Partial<ColEx> = {
|
||||
span: showAdvancedButton ? 6 : 4,
|
||||
...actionColOptions,
|
||||
...advancedSpanObj,
|
||||
};
|
||||
return actionColOpt;
|
||||
});
|
||||
|
||||
function toggleAdvanced() {
|
||||
emit('toggle-advanced');
|
||||
}
|
||||
return () => {
|
||||
if (!props.show) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
showAdvancedButton,
|
||||
hideAdvanceBtn,
|
||||
isAdvanced,
|
||||
showResetButton,
|
||||
showSubmitButton,
|
||||
} = props;
|
||||
return (
|
||||
<>
|
||||
<Col {...unref(actionColOpt)} style={{ textAlign: 'right' }}>
|
||||
{() => (
|
||||
<Form.Item>
|
||||
{() => (
|
||||
<>
|
||||
{getSlot(slots, 'advanceBefore')}
|
||||
{showAdvancedButton && !hideAdvanceBtn && (
|
||||
<Button type="default" class="mr-2" onClick={toggleAdvanced}>
|
||||
{() => (
|
||||
<>
|
||||
{isAdvanced ? '收起' : '展开'}
|
||||
{isAdvanced ? (
|
||||
<UpOutlined class="advanced-icon" />
|
||||
) : (
|
||||
<DownOutlined class="advanced-icon" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{getSlot(slots, 'resetBefore')}
|
||||
{showResetButton && (
|
||||
<Button type="default" class="mr-2" {...unref(getResetBtnOptionsRef)}>
|
||||
{() => unref(getResetBtnOptionsRef).text}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{getSlot(slots, 'submitBefore')}
|
||||
{showSubmitButton && (
|
||||
<Button type="primary" {...unref(getSubmitBtnOptionsRef)}>
|
||||
{() => unref(getSubmitBtnOptionsRef).text}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{getSlot(slots, 'submitAfter')}
|
||||
</>
|
||||
)}
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
267
src/components/Form/src/FormItem.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import { defineComponent, computed, unref, toRef } from 'vue';
|
||||
import { Form, Col } from 'ant-design-vue';
|
||||
import { componentMap } from './componentMap';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
import type { FormProps } from './types/form';
|
||||
import type { FormSchema } from './types/form';
|
||||
import { isBoolean, isFunction } from '/@/utils/is';
|
||||
import { useItemLabelWidth } from './hooks/useLabelWidth';
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||
import { BasicHelp } from '/@/components/Basic';
|
||||
import { createPlaceholderMessage } from './helper';
|
||||
import { upperFirst, cloneDeep } from 'lodash-es';
|
||||
import { ValidationRule } from 'ant-design-vue/types/form/form';
|
||||
export default defineComponent({
|
||||
name: 'BasicFormItem',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
schema: {
|
||||
type: Object as PropType<FormSchema>,
|
||||
default: () => {},
|
||||
},
|
||||
formProps: {
|
||||
type: Object as PropType<FormProps>,
|
||||
default: {},
|
||||
},
|
||||
allDefaultValues: {
|
||||
type: Object as PropType<any>,
|
||||
default: {},
|
||||
},
|
||||
formModel: {
|
||||
type: Object as PropType<any>,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const itemLabelWidthRef = useItemLabelWidth(toRef(props, 'schema'), toRef(props, 'formProps'));
|
||||
|
||||
const getValuesRef = computed(() => {
|
||||
const { allDefaultValues, formModel, schema } = props;
|
||||
const { mergeDynamicData } = props.formProps;
|
||||
return {
|
||||
field: schema.field,
|
||||
model: formModel,
|
||||
values: {
|
||||
...mergeDynamicData,
|
||||
...allDefaultValues,
|
||||
...formModel,
|
||||
},
|
||||
schema: schema,
|
||||
};
|
||||
});
|
||||
const getShowRef = computed(() => {
|
||||
const { show, ifShow, isAdvanced } = props.schema;
|
||||
const { showAdvancedButton } = props.formProps;
|
||||
const itemIsAdvanced = showAdvancedButton ? !!isAdvanced : true;
|
||||
let isShow = true;
|
||||
let isIfShow = true;
|
||||
|
||||
if (isBoolean(show)) {
|
||||
isShow = show;
|
||||
}
|
||||
if (isBoolean(ifShow)) {
|
||||
isIfShow = ifShow;
|
||||
}
|
||||
if (isFunction(show)) {
|
||||
isShow = show(unref(getValuesRef));
|
||||
}
|
||||
if (isFunction(ifShow)) {
|
||||
isIfShow = ifShow(unref(getValuesRef));
|
||||
}
|
||||
isShow = isShow && itemIsAdvanced;
|
||||
return { isShow, isIfShow };
|
||||
});
|
||||
|
||||
const getDisableRef = computed(() => {
|
||||
const { disabled: globDisabled } = props.formProps;
|
||||
const { dynamicDisabled } = props.schema;
|
||||
let disabled = !!globDisabled;
|
||||
if (isBoolean(dynamicDisabled)) {
|
||||
disabled = dynamicDisabled;
|
||||
}
|
||||
|
||||
if (isFunction(dynamicDisabled)) {
|
||||
disabled = dynamicDisabled(unref(getValuesRef));
|
||||
}
|
||||
|
||||
return disabled;
|
||||
});
|
||||
|
||||
function handleRules(): ValidationRule[] {
|
||||
const {
|
||||
rules: defRules = [],
|
||||
component,
|
||||
rulesMessageJoinLabel,
|
||||
label,
|
||||
dynamicRules,
|
||||
} = props.schema;
|
||||
|
||||
if (isFunction(dynamicRules)) {
|
||||
return dynamicRules(unref(getValuesRef));
|
||||
}
|
||||
|
||||
const rules: ValidationRule[] = cloneDeep(defRules);
|
||||
const requiredRuleIndex: number = rules.findIndex(
|
||||
(rule) => Reflect.has(rule, 'required') && !Reflect.has(rule, 'validator')
|
||||
);
|
||||
const { rulesMessageJoinLabel: globalRulesMessageJoinLabel } = props.formProps;
|
||||
if (requiredRuleIndex !== -1) {
|
||||
const rule = rules[requiredRuleIndex];
|
||||
if (rule.required && component) {
|
||||
const joinLabel = Reflect.has(props.schema, 'rulesMessageJoinLabel')
|
||||
? rulesMessageJoinLabel
|
||||
: globalRulesMessageJoinLabel;
|
||||
rule.message =
|
||||
rule.message || createPlaceholderMessage(component) + `${joinLabel ? label : ''}`;
|
||||
if (component.includes('Input') || component.includes('Textarea')) {
|
||||
rule.whitespace = true;
|
||||
}
|
||||
if (
|
||||
component.includes('DatePicker') ||
|
||||
component.includes('MonthPicker') ||
|
||||
component.includes('WeekPicker') ||
|
||||
component.includes('TimePicker')
|
||||
) {
|
||||
rule.type = 'object';
|
||||
}
|
||||
if (component.includes('RangePicker')) {
|
||||
rule.type = 'array';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 最大输入长度规则校验
|
||||
const characterInx = rules.findIndex((val) => val.max);
|
||||
if (characterInx !== -1 && !rules[characterInx].validator) {
|
||||
rules[characterInx].message =
|
||||
rules[characterInx].message || `字符数应小于${rules[characterInx].max}位`;
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
function renderComponent() {
|
||||
const {
|
||||
componentProps,
|
||||
renderComponentContent,
|
||||
component,
|
||||
field,
|
||||
changeEvent = 'change',
|
||||
} = props.schema;
|
||||
|
||||
const isCheck = component && ['Switch'].includes(component);
|
||||
|
||||
const eventKey = `on${upperFirst(changeEvent)}`;
|
||||
const on = {
|
||||
[eventKey]: (e: any) => {
|
||||
if (propsData[eventKey]) {
|
||||
propsData[eventKey](e);
|
||||
}
|
||||
if (e && e.target) {
|
||||
(props.formModel as any)[field] = e.target.value;
|
||||
} else {
|
||||
(props.formModel as any)[field] = e;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const Comp = componentMap.get(component);
|
||||
|
||||
const { autoSetPlaceHolder, size } = props.formProps;
|
||||
const propsData: any = {
|
||||
allowClear: true,
|
||||
getPopupContainer: (trigger: Element) => trigger.parentNode,
|
||||
size,
|
||||
...componentProps,
|
||||
disabled: unref(getDisableRef),
|
||||
};
|
||||
|
||||
const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
|
||||
let placeholder;
|
||||
// RangePicker place为数组
|
||||
if (isCreatePlaceholder && component !== 'RangePicker' && component) {
|
||||
placeholder =
|
||||
(componentProps && componentProps.placeholder) || createPlaceholderMessage(component);
|
||||
}
|
||||
propsData.placeholder = placeholder;
|
||||
propsData.codeField = field;
|
||||
propsData.formValues = unref(getValuesRef);
|
||||
|
||||
const bindValue = {
|
||||
[isCheck ? 'checked' : 'value']: (props.formModel as any)[field],
|
||||
};
|
||||
return (
|
||||
<Comp {...propsData} {...on} {...bindValue}>
|
||||
{{
|
||||
...(renderComponentContent
|
||||
? renderComponentContent(unref(getValuesRef))
|
||||
: {
|
||||
default: () => '',
|
||||
}),
|
||||
}}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
|
||||
function renderLabelHelpMessage() {
|
||||
const { label, helpMessage, helpComponentProps } = props.schema;
|
||||
if (!helpMessage || (Array.isArray(helpMessage) && helpMessage.length === 0)) {
|
||||
return label;
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
{label}
|
||||
<BasicHelp class="mx-1" text={helpMessage} {...helpComponentProps} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
function renderItem() {
|
||||
const { itemProps, slot, render, field } = props.schema;
|
||||
const { labelCol, wrapperCol } = unref(itemLabelWidthRef);
|
||||
const { colon } = props.formProps;
|
||||
const getContent = () => {
|
||||
return slot
|
||||
? getSlot(slots, slot)
|
||||
: render
|
||||
? render(unref(getValuesRef))
|
||||
: renderComponent();
|
||||
};
|
||||
return (
|
||||
<Form.Item
|
||||
name={field}
|
||||
colon={colon}
|
||||
{...itemProps}
|
||||
label={renderLabelHelpMessage()}
|
||||
rules={handleRules()}
|
||||
labelCol={labelCol}
|
||||
wrapperCol={wrapperCol}
|
||||
>
|
||||
{() => getContent()}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
const { colProps = {}, colSlot, renderColContent, component } = props.schema;
|
||||
if (!componentMap.has(component)) return null;
|
||||
const { baseColProps = {} } = props.formProps;
|
||||
|
||||
const realColProps = { ...baseColProps, ...colProps };
|
||||
|
||||
const { isIfShow, isShow } = unref(getShowRef);
|
||||
|
||||
const getContent = () => {
|
||||
return colSlot
|
||||
? getSlot(slots, colSlot)
|
||||
: renderColContent
|
||||
? renderColContent(unref(getValuesRef))
|
||||
: renderItem();
|
||||
};
|
||||
return (
|
||||
isIfShow && (
|
||||
<Col {...realColProps} class={!isShow ? 'hidden' : ''}>
|
||||
{() => getContent()}
|
||||
</Col>
|
||||
)
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
59
src/components/Form/src/componentMap.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Component } from 'vue';
|
||||
/**
|
||||
* 组件列表,在这里注册才可以在表单使用
|
||||
*/
|
||||
import {
|
||||
Input,
|
||||
Select,
|
||||
Radio,
|
||||
Checkbox,
|
||||
AutoComplete,
|
||||
Cascader,
|
||||
DatePicker,
|
||||
InputNumber,
|
||||
Switch,
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
Transfer,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { ComponentType } from './types/index';
|
||||
|
||||
const componentMap = new Map<ComponentType, any>();
|
||||
|
||||
componentMap.set('Input', Input);
|
||||
componentMap.set('InputGroup', Input.Group);
|
||||
componentMap.set('InputPassword', Input.Password);
|
||||
componentMap.set('InputSearch', Input.Search);
|
||||
componentMap.set('InputTextArea', Input.TextArea);
|
||||
componentMap.set('InputNumber', InputNumber);
|
||||
componentMap.set('AutoComplete', AutoComplete);
|
||||
|
||||
componentMap.set('Select', Select);
|
||||
componentMap.set('SelectOptGroup', Select.OptGroup);
|
||||
componentMap.set('SelectOption', Select.Option);
|
||||
componentMap.set('TreeSelect', TreeSelect);
|
||||
componentMap.set('Transfer', Transfer);
|
||||
componentMap.set('Radio', Radio);
|
||||
componentMap.set('Switch', Switch);
|
||||
componentMap.set('RadioButton', Radio.Button);
|
||||
componentMap.set('RadioGroup', Radio.Group);
|
||||
componentMap.set('Checkbox', Checkbox);
|
||||
componentMap.set('CheckboxGroup', Checkbox.Group);
|
||||
componentMap.set('Cascader', Cascader);
|
||||
|
||||
componentMap.set('DatePicker', DatePicker);
|
||||
componentMap.set('MonthPicker', DatePicker.MonthPicker);
|
||||
componentMap.set('RangePicker', DatePicker.RangePicker);
|
||||
componentMap.set('WeekPicker', DatePicker.WeekPicker);
|
||||
componentMap.set('TimePicker', TimePicker);
|
||||
|
||||
export function add(compName: ComponentType, component: Component) {
|
||||
componentMap.set(compName, component);
|
||||
}
|
||||
|
||||
export function del(compName: ComponentType) {
|
||||
componentMap.delete(compName);
|
||||
}
|
||||
|
||||
export { componentMap };
|
30
src/components/Form/src/helper.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ComponentType } from './types/index';
|
||||
/**
|
||||
* @description: 生成placeholder
|
||||
*/
|
||||
export function createPlaceholderMessage(component: ComponentType) {
|
||||
if (component.includes('Input') || component.includes('Complete')) {
|
||||
return '请输入';
|
||||
}
|
||||
if (component.includes('Picker') && !component.includes('Range')) {
|
||||
return '请选择';
|
||||
}
|
||||
if (
|
||||
component.includes('Select') ||
|
||||
component.includes('Cascader') ||
|
||||
component.includes('Checkbox') ||
|
||||
component.includes('Radio') ||
|
||||
component.includes('Switch')
|
||||
) {
|
||||
// return `请选择${label}`;
|
||||
return '请选择';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function genType() {
|
||||
return ['DatePicker', 'MonthPicker', 'RangePicker', 'WeekPicker', 'TimePicker'];
|
||||
}
|
||||
/**
|
||||
* 时间字段
|
||||
*/
|
||||
export const dateItemType = genType();
|
10
src/components/Form/src/hooks/useComponentRegister.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
|
||||
import { add, del } from '../componentMap';
|
||||
|
||||
import { ComponentType } from '../types/index';
|
||||
export function useComponentRegister(compName: ComponentType, comp: any) {
|
||||
add(compName, comp);
|
||||
tryOnUnmounted(() => {
|
||||
del(compName);
|
||||
});
|
||||
}
|
69
src/components/Form/src/hooks/useForm.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ref, onUnmounted, unref } from 'vue';
|
||||
|
||||
import { isInSetup } from '/@/utils/helper/vueHelper';
|
||||
|
||||
import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form';
|
||||
import { isProdMode } from '/@/utils/env';
|
||||
import type { NamePath } from 'ant-design-vue/types/form/form-item';
|
||||
import type { ValidateFields } from 'ant-design-vue/types/form/form';
|
||||
|
||||
export function useForm(props?: Partial<FormProps>): UseFormReturnType {
|
||||
isInSetup();
|
||||
const formRef = ref<FormActionType | null>(null);
|
||||
const loadedRef = ref<boolean | null>(false);
|
||||
function getForm() {
|
||||
const form = unref(formRef);
|
||||
if (!form) {
|
||||
throw new Error('formRef is Null');
|
||||
}
|
||||
return form as FormActionType;
|
||||
}
|
||||
function register(instance: FormActionType) {
|
||||
isProdMode() &&
|
||||
onUnmounted(() => {
|
||||
formRef.value = null;
|
||||
loadedRef.value = null;
|
||||
});
|
||||
if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return;
|
||||
formRef.value = instance;
|
||||
props && instance.setProps(props);
|
||||
loadedRef.value = true;
|
||||
}
|
||||
|
||||
const methods: FormActionType = {
|
||||
setProps: (formProps: Partial<FormProps>) => {
|
||||
getForm().setProps(formProps);
|
||||
},
|
||||
updateSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => {
|
||||
getForm().updateSchema(data);
|
||||
},
|
||||
clearValidate: (name?: string | string[]) => {
|
||||
getForm().clearValidate(name);
|
||||
},
|
||||
resetFields: async () => {
|
||||
await getForm().resetFields();
|
||||
},
|
||||
removeSchemaByFiled: (field: string | string[]) => {
|
||||
getForm().removeSchemaByFiled(field);
|
||||
},
|
||||
getFieldsValue: () => {
|
||||
return getForm().getFieldsValue();
|
||||
},
|
||||
setFieldsValue: <T>(values: T) => {
|
||||
getForm().setFieldsValue<T>(values);
|
||||
},
|
||||
appendSchemaByField: (schema: FormSchema, prefixField?: string | undefined) => {
|
||||
getForm().appendSchemaByField(schema, prefixField);
|
||||
},
|
||||
submit: async (): Promise<any> => {
|
||||
return getForm().submit();
|
||||
},
|
||||
validate: ((async (nameList?: NamePath[]): Promise<any> => {
|
||||
return getForm().validate(nameList);
|
||||
}) as any) as ValidateFields,
|
||||
validateFields: ((async (nameList?: NamePath[]): Promise<any> => {
|
||||
return getForm().validate(nameList);
|
||||
}) as any) as ValidateFields,
|
||||
} as FormActionType;
|
||||
return [register, methods];
|
||||
}
|
62
src/components/Form/src/hooks/useFormValues.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { isArray, isFunction, isObject, isString } from '/@/utils/is';
|
||||
import moment from 'moment';
|
||||
import { unref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type { FieldMapToTime } from '../types/form';
|
||||
|
||||
export function useFormValues(
|
||||
transformDateFuncRef: Ref<Fn>,
|
||||
fieldMapToTimeRef: Ref<FieldMapToTime>
|
||||
) {
|
||||
// 处理表单值
|
||||
function handleFormValues(values: any) {
|
||||
if (!isObject(values)) {
|
||||
return {};
|
||||
}
|
||||
const resMap: any = {};
|
||||
for (const item of Object.entries(values)) {
|
||||
let [, value] = item;
|
||||
const [key] = item;
|
||||
if ((isArray(value) && value.length === 0) || isFunction(value)) {
|
||||
continue;
|
||||
}
|
||||
const transformDateFunc = unref(transformDateFuncRef);
|
||||
if (isObject(value)) {
|
||||
value = transformDateFunc(value);
|
||||
}
|
||||
if (isArray(value) && value[0]._isAMomentObject && value[1]._isAMomentObject) {
|
||||
value = value.map((item) => transformDateFunc(item));
|
||||
}
|
||||
// 去除空格
|
||||
if (isString(value)) {
|
||||
value = value.trim();
|
||||
}
|
||||
resMap[key] = value;
|
||||
}
|
||||
return handleRangeTimeValue(resMap);
|
||||
}
|
||||
/**
|
||||
* @description: 处理时间区间参数
|
||||
*/
|
||||
function handleRangeTimeValue(values: any) {
|
||||
const fieldMapToTime = unref(fieldMapToTimeRef);
|
||||
|
||||
if (!fieldMapToTime || !Array.isArray(fieldMapToTime)) {
|
||||
return values;
|
||||
}
|
||||
|
||||
for (const [field, [startTimeKey, endTimeKey, format = 'YYYY-MM-DD']] of fieldMapToTime) {
|
||||
if (!field || !startTimeKey || !endTimeKey || !values[field]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [startTime, endTime]: string[] = values[field];
|
||||
|
||||
values[startTimeKey] = moment(startTime).format(format);
|
||||
values[endTimeKey] = moment(endTime).format(format);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
return handleFormValues;
|
||||
}
|
43
src/components/Form/src/hooks/useLabelWidth.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Ref } from 'vue';
|
||||
import type { FormProps, FormSchema } from '../types/form';
|
||||
|
||||
import { computed, unref } from 'vue';
|
||||
import { isNumber } from '/@/utils/is';
|
||||
|
||||
// export function useGlobalLabelWidth(propsRef: ComputedRef<FormProps>) {
|
||||
// return computed(() => {
|
||||
// const { labelWidth, labelCol, wrapperCol } = unref(propsRef);
|
||||
// if (!labelWidth) {
|
||||
// return { labelCol, wrapperCol };
|
||||
// }
|
||||
|
||||
// const width = isNumber(labelWidth) ? `${labelWidth}px` : labelWidth;
|
||||
// return {
|
||||
// labelCol: { style: { width }, span: 1, ...labelCol },
|
||||
// wrapperCol: { style: { width: `calc(100% - ${width})` }, span: 23, ...wrapperCol },
|
||||
// };
|
||||
// });
|
||||
// }
|
||||
|
||||
export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<FormProps>) {
|
||||
return computed((): any => {
|
||||
const schemaItem = unref(schemaItemRef);
|
||||
const { labelCol = {}, wrapperCol = {} } = schemaItem.itemProps || {};
|
||||
const { labelWidth, disabledLabelWidth } = schemaItem;
|
||||
|
||||
const { labelWidth: globalLabelWidth } = unref(propsRef) as any;
|
||||
// 如果全局有设置labelWidth, 则所有item使用
|
||||
if ((!globalLabelWidth && !labelWidth) || disabledLabelWidth) {
|
||||
return { labelCol, wrapperCol };
|
||||
}
|
||||
let width = labelWidth || globalLabelWidth;
|
||||
|
||||
if (width) {
|
||||
width = isNumber(width) ? `${width}px` : width;
|
||||
}
|
||||
return {
|
||||
labelCol: { style: { width }, span: 1, ...labelCol },
|
||||
wrapperCol: { style: { width: `calc(100% - ${width})` }, span: 23, ...wrapperCol },
|
||||
};
|
||||
});
|
||||
}
|
107
src/components/Form/src/props.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { FieldMapToTime, FormSchema } from './types/form';
|
||||
import type { PropType } from 'vue';
|
||||
import type { ColEx } from './types';
|
||||
|
||||
export const basicProps = {
|
||||
// 标签宽度 固定宽度
|
||||
labelWidth: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 0,
|
||||
},
|
||||
fieldMapToTime: {
|
||||
type: Array as PropType<FieldMapToTime>,
|
||||
default: () => [],
|
||||
},
|
||||
compact: Boolean as PropType<boolean>,
|
||||
// 表单配置规则
|
||||
schemas: {
|
||||
type: [Array] as PropType<FormSchema[]>,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
mergeDynamicData: {
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
baseColProps: {
|
||||
type: Object as PropType<any>,
|
||||
},
|
||||
autoSetPlaceHolder: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<'default' | 'small' | 'large'>,
|
||||
default: 'default',
|
||||
},
|
||||
// 禁用表单
|
||||
disabled: Boolean as PropType<boolean>,
|
||||
emptySpan: {
|
||||
type: [Number, Object] as PropType<number>,
|
||||
default: 0,
|
||||
},
|
||||
// 是否显示收起展开按钮
|
||||
showAdvancedButton: { type: Boolean as PropType<boolean>, default: false },
|
||||
// 转化时间
|
||||
transformDateFunc: {
|
||||
type: Function as PropType<Fn>,
|
||||
default: (date: any) => {
|
||||
return date._isAMomentObject ? date.format('YYYY-MM-DD HH:mm:ss') : date;
|
||||
},
|
||||
},
|
||||
rulesMessageJoinLabel: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 超过3行自动折叠
|
||||
autoAdvancedLine: {
|
||||
type: Number as PropType<number>,
|
||||
default: 3,
|
||||
},
|
||||
|
||||
// 是否显示操作按钮
|
||||
showActionButtonGroup: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
// 操作列Col配置
|
||||
actionColOptions: Object as PropType<ColEx>,
|
||||
// 显示重置按钮
|
||||
showResetButton: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
// 重置按钮配置
|
||||
resetButtonOptions: Object as PropType<any>,
|
||||
|
||||
// 显示确认按钮
|
||||
showSubmitButton: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
// 确认按钮配置
|
||||
submitButtonOptions: Object as PropType<any>,
|
||||
|
||||
// 自定义重置函数
|
||||
resetFunc: Function as PropType<Fn>,
|
||||
submitFunc: Function as PropType<Fn>,
|
||||
|
||||
// 以下为默认props
|
||||
hideRequiredMark: Boolean as PropType<boolean>,
|
||||
|
||||
labelCol: Object as PropType<ColEx>,
|
||||
|
||||
layout: {
|
||||
type: String as PropType<'horizontal' | 'vertical' | 'inline'>,
|
||||
default: 'horizontal',
|
||||
},
|
||||
|
||||
wrapperCol: Object as PropType<any>,
|
||||
|
||||
colon: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
|
||||
labelAlign: String as PropType<string>,
|
||||
};
|
159
src/components/Form/src/types/form.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { Form, ValidationRule } from 'ant-design-vue/types/form/form';
|
||||
import type { VNode } from 'vue';
|
||||
import type { BasicButtonProps } from '/@/components/Button/types';
|
||||
import type { FormItem } from './formItem';
|
||||
import type { ColEx, ComponentType } from './index';
|
||||
|
||||
export type FieldMapToTime = [string, [string, string], string?][];
|
||||
|
||||
export interface RenderCallbackParams {
|
||||
schema: FormSchema;
|
||||
values: any;
|
||||
model: any;
|
||||
field: string;
|
||||
}
|
||||
export interface FormActionType extends Form {
|
||||
submit(): Promise<void>;
|
||||
setFieldsValue<T>(values: T): void;
|
||||
resetFields(): Promise<any>;
|
||||
getFieldsValue: () => any;
|
||||
clearValidate: (name?: string | string[]) => void;
|
||||
updateSchema(data: Partial<FormSchema> | Partial<FormSchema>[]): void;
|
||||
setProps(formProps: Partial<FormProps>): void;
|
||||
removeSchemaByFiled(field: string | string[]): void;
|
||||
appendSchemaByField(schema: FormSchema, prefixField?: string): void;
|
||||
}
|
||||
export type RegisterFn = (formInstance: FormActionType) => void;
|
||||
|
||||
export type UseFormReturnType = [RegisterFn, FormActionType];
|
||||
|
||||
export interface FormProps {
|
||||
// 整个表单所有项宽度
|
||||
labelWidth?: number | string;
|
||||
|
||||
// 整个表单通用Col配置
|
||||
labelCol?: Partial<ColEx>;
|
||||
// 整个表单通用Col配置
|
||||
wrapperCol?: Partial<ColEx>;
|
||||
|
||||
// 通用col配置
|
||||
baseColProps?: any;
|
||||
|
||||
// 表单配置规则
|
||||
schemas?: FormSchema[];
|
||||
// 用于合并到动态控制表单项的 函数values
|
||||
mergeDynamicData?: any;
|
||||
// 紧凑模式,用于搜索表单
|
||||
compact?: boolean;
|
||||
// 空白行span
|
||||
emptySpan?: number | Partial<ColEx>;
|
||||
// 表单内部组件大小
|
||||
size: 'default' | 'small' | 'large';
|
||||
// 是否禁用
|
||||
disabled?: boolean;
|
||||
// 时间区间字段映射成多个
|
||||
fieldMapToTime?: FieldMapToTime;
|
||||
// 自动设置placeholder
|
||||
autoSetPlaceHolder: boolean;
|
||||
// 校验信息是否加入label
|
||||
rulesMessageJoinLabel?: boolean;
|
||||
// 是否显示收起展开按钮
|
||||
showAdvancedButton?: boolean;
|
||||
// 超过指定行数自动收起
|
||||
autoAdvancedLine?: number;
|
||||
// 是否显示操作按钮
|
||||
showActionButtonGroup: boolean;
|
||||
|
||||
// 重置按钮配置
|
||||
resetButtonOptions: Partial<BasicButtonProps>;
|
||||
|
||||
// 确认按钮配置
|
||||
submitButtonOptions: Partial<BasicButtonProps>;
|
||||
|
||||
// 操作列配置
|
||||
actionColOptions: Partial<ColEx>;
|
||||
|
||||
// 显示重置按钮
|
||||
showResetButton: boolean;
|
||||
// 显示确认按钮
|
||||
showSubmitButton: boolean;
|
||||
|
||||
resetFunc: () => Promise<void>;
|
||||
submitFunc: () => Promise<void>;
|
||||
transformDateFunc: (date: any) => string;
|
||||
colon?: boolean;
|
||||
}
|
||||
export interface FormSchema {
|
||||
// 字段名
|
||||
field: string;
|
||||
changeEvent?: string;
|
||||
// 标签名
|
||||
label: string;
|
||||
// 文本右侧帮助文本
|
||||
helpMessage?: string | string[];
|
||||
// BaseHelp组件props
|
||||
helpComponentProps?: Partial<HelpComponentProps>;
|
||||
// label宽度,有传的话 itemProps配置的 labelCol 和WrapperCol会失效
|
||||
labelWidth?: string | number;
|
||||
// 禁用调有formModel全局设置的labelWidth,自己手动设置 labelCol和wrapperCol
|
||||
disabledLabelWidth?: boolean;
|
||||
// 组件
|
||||
component: ComponentType;
|
||||
// 组件参数
|
||||
componentProps?: any;
|
||||
|
||||
// 校验规则
|
||||
rules?: ValidationRule[];
|
||||
// 校验信息是否加入label
|
||||
rulesMessageJoinLabel?: boolean;
|
||||
|
||||
// 参考formModelItem
|
||||
itemProps?: Partial<FormItem>;
|
||||
|
||||
// formModelItem外层的col配置
|
||||
colProps?: Partial<ColEx>;
|
||||
|
||||
// 默认值
|
||||
defaultValue?: any;
|
||||
isAdvanced?: boolean;
|
||||
|
||||
// 配合详情组件
|
||||
span?: number;
|
||||
|
||||
ifShow?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
|
||||
|
||||
show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
|
||||
|
||||
// 渲染form-item标签内的内容
|
||||
render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string;
|
||||
|
||||
// 渲染 col内容,需要外层包裹 form-item
|
||||
renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[];
|
||||
|
||||
renderComponentContent?: (renderCallbackParams: RenderCallbackParams) => any;
|
||||
|
||||
// 自定义slot, 在 from-item内
|
||||
slot?: string;
|
||||
|
||||
// 自定义slot,类似renderColContent
|
||||
colSlot?: string;
|
||||
|
||||
dynamicDisabled?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
|
||||
|
||||
dynamicRules?: (renderCallbackParams: RenderCallbackParams) => ValidationRule[];
|
||||
}
|
||||
export interface HelpComponentProps {
|
||||
maxWidth: string;
|
||||
// 是否显示序号
|
||||
showIndex: boolean;
|
||||
// 文本列表
|
||||
text: any;
|
||||
// 颜色
|
||||
color: string;
|
||||
// 字体大小
|
||||
fontSize: string;
|
||||
icon: string;
|
||||
absolute: boolean;
|
||||
// 定位
|
||||
position: any;
|
||||
}
|
91
src/components/Form/src/types/formItem.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { NamePath } from 'ant-design-vue/types/form/form-item';
|
||||
import type { Col } from 'ant-design-vue/types/grid/col';
|
||||
import type { VNodeChild } from 'vue';
|
||||
|
||||
export interface FormItem {
|
||||
/**
|
||||
* Used with label, whether to display : after label text.
|
||||
* @default true
|
||||
* @type boolean
|
||||
*/
|
||||
colon?: boolean;
|
||||
|
||||
/**
|
||||
* The extra prompt message. It is similar to help. Usage example: to display error message and prompt message at the same time.
|
||||
* @type any (string | slot)
|
||||
*/
|
||||
extra?: string | VNodeChild | JSX.Element;
|
||||
|
||||
/**
|
||||
* Used with validateStatus, this option specifies the validation status icon. Recommended to be used only with Input.
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
hasFeedback?: boolean;
|
||||
|
||||
/**
|
||||
* The prompt message. If not provided, the prompt message will be generated by the validation rule.
|
||||
* @type any (string | slot)
|
||||
*/
|
||||
help?: string | VNodeChild | JSX.Element;
|
||||
|
||||
/**
|
||||
* Label test
|
||||
* @type any (string | slot)
|
||||
*/
|
||||
label?: string | VNodeChild | JSX.Element;
|
||||
|
||||
/**
|
||||
* The layout of label. You can set span offset to something like {span: 3, offset: 12} or sm: {span: 3, offset: 12} same as with <Col>
|
||||
* @type Col
|
||||
*/
|
||||
labelCol?: Col;
|
||||
|
||||
/**
|
||||
* Whether provided or not, it will be generated by the validation rule.
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
required?: boolean;
|
||||
|
||||
/**
|
||||
* The validation status. If not provided, it will be generated by validation rule. options: 'success' 'warning' 'error' 'validating'
|
||||
* @type string
|
||||
*/
|
||||
validateStatus?: '' | 'success' | 'warning' | 'error' | 'validating';
|
||||
|
||||
/**
|
||||
* The layout for input controls, same as labelCol
|
||||
* @type Col
|
||||
*/
|
||||
wrapperCol?: Col;
|
||||
/**
|
||||
* Set sub label htmlFor.
|
||||
*/
|
||||
htmlFor?: string;
|
||||
/**
|
||||
* text align of label
|
||||
*/
|
||||
labelAlign?: 'left' | 'right';
|
||||
/**
|
||||
* a key of model. In the use of validate and resetFields method, the attribute is required
|
||||
*/
|
||||
name?: NamePath;
|
||||
/**
|
||||
* validation rules of form
|
||||
*/
|
||||
rules?: object | object[];
|
||||
/**
|
||||
* Whether to automatically associate form fields. In most cases, you can use automatic association.
|
||||
* If the conditions for automatic association are not met, you can manually associate them. See the notes below.
|
||||
*/
|
||||
autoLink?: boolean;
|
||||
/**
|
||||
* Whether stop validate on first rule of error for this field.
|
||||
*/
|
||||
validateFirst?: boolean;
|
||||
/**
|
||||
* When to validate the value of children node
|
||||
*/
|
||||
validateTrigger?: string | string[] | false;
|
||||
}
|
113
src/components/Form/src/types/index.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { ColSpanType } from 'ant-design-vue/types/grid/col';
|
||||
|
||||
export interface ColEx {
|
||||
style: object;
|
||||
/**
|
||||
* raster number of cells to occupy, 0 corresponds to display: none
|
||||
* @default none (0)
|
||||
* @type ColSpanType
|
||||
*/
|
||||
span?: ColSpanType;
|
||||
|
||||
/**
|
||||
* raster order, used in flex layout mode
|
||||
* @default 0
|
||||
* @type ColSpanType
|
||||
*/
|
||||
order?: ColSpanType;
|
||||
|
||||
/**
|
||||
* the layout fill of flex
|
||||
* @default none
|
||||
* @type ColSpanType
|
||||
*/
|
||||
flex?: ColSpanType;
|
||||
|
||||
/**
|
||||
* the number of cells to offset Col from the left
|
||||
* @default 0
|
||||
* @type ColSpanType
|
||||
*/
|
||||
offset?: ColSpanType;
|
||||
|
||||
/**
|
||||
* the number of cells that raster is moved to the right
|
||||
* @default 0
|
||||
* @type ColSpanType
|
||||
*/
|
||||
push?: ColSpanType;
|
||||
|
||||
/**
|
||||
* the number of cells that raster is moved to the left
|
||||
* @default 0
|
||||
* @type ColSpanType
|
||||
*/
|
||||
pull?: ColSpanType;
|
||||
|
||||
/**
|
||||
* <576px and also default setting, could be a span value or an object containing above props
|
||||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
|
||||
*/
|
||||
xs?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
|
||||
|
||||
/**
|
||||
* ≥576px, could be a span value or an object containing above props
|
||||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
|
||||
*/
|
||||
sm?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
|
||||
|
||||
/**
|
||||
* ≥768px, could be a span value or an object containing above props
|
||||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
|
||||
*/
|
||||
md?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
|
||||
|
||||
/**
|
||||
* ≥992px, could be a span value or an object containing above props
|
||||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
|
||||
*/
|
||||
lg?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
|
||||
|
||||
/**
|
||||
* ≥1200px, could be a span value or an object containing above props
|
||||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
|
||||
*/
|
||||
xl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
|
||||
|
||||
/**
|
||||
* ≥1600px, could be a span value or an object containing above props
|
||||
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
|
||||
*/
|
||||
xxl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
|
||||
}
|
||||
|
||||
export type ComponentType =
|
||||
| 'Input'
|
||||
| 'InputGroup'
|
||||
| 'InputPassword'
|
||||
| 'InputSearch'
|
||||
| 'InputTextArea'
|
||||
| 'InputNumber'
|
||||
| 'InputCountDown'
|
||||
| 'Select'
|
||||
| 'DictSelect'
|
||||
| 'SelectOptGroup'
|
||||
| 'SelectOption'
|
||||
| 'TreeSelect'
|
||||
| 'Transfer'
|
||||
| 'Radio'
|
||||
| 'RadioButton'
|
||||
| 'RadioGroup'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'AutoComplete'
|
||||
| 'Cascader'
|
||||
| 'DatePicker'
|
||||
| 'MonthPicker'
|
||||
| 'RangePicker'
|
||||
| 'WeekPicker'
|
||||
| 'TimePicker'
|
||||
| 'ImageUpload'
|
||||
| 'Switch'
|
||||
| 'StrengthMeter'
|
||||
| 'Render';
|
7
src/components/Icon/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
### `Icon.vue`
|
||||
|
||||
```html
|
||||
<Icon icon="mdi:account" />
|
||||
```
|
||||
|
||||
The icon id follows the rules in [Iconify](https://iconify.design/) which you can use any icons from the supported icon sets.
|
14
src/components/Icon/index.less
Normal file
@@ -0,0 +1,14 @@
|
||||
@import (reference) '../../design/index.less';
|
||||
|
||||
.app-iconify {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
span.iconify {
|
||||
display: block;
|
||||
min-width: 1em;
|
||||
min-height: 1em;
|
||||
background: @iconify-bg-color;
|
||||
border-radius: 100%;
|
||||
}
|
76
src/components/Icon/index.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent, ref, watch, onMounted, nextTick, unref, computed } from 'vue';
|
||||
import Iconify from '@purge-icons/generated';
|
||||
import { isString } from '/@/utils/is';
|
||||
import './index.less';
|
||||
export default defineComponent({
|
||||
name: 'GIcon',
|
||||
props: {
|
||||
// icon name
|
||||
icon: {
|
||||
type: String as PropType<string>,
|
||||
required: true,
|
||||
},
|
||||
// icon color
|
||||
color: {
|
||||
type: String as PropType<string>,
|
||||
},
|
||||
// icon size
|
||||
size: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: 14,
|
||||
},
|
||||
prefix: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props, { attrs }) {
|
||||
const elRef = ref<Nullable<HTMLElement>>(null);
|
||||
|
||||
const getIconRef = computed(() => {
|
||||
const { icon, prefix } = props;
|
||||
return `${prefix ? prefix + ':' : ''}${icon}`;
|
||||
});
|
||||
const update = async () => {
|
||||
const el = unref(elRef);
|
||||
if (el) {
|
||||
await nextTick();
|
||||
const icon = unref(getIconRef);
|
||||
|
||||
const svg = Iconify.renderSVG(icon, {});
|
||||
|
||||
if (svg) {
|
||||
el.textContent = '';
|
||||
el.appendChild(svg);
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'iconify';
|
||||
span.dataset.icon = icon;
|
||||
el.textContent = '';
|
||||
el.appendChild(span);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.icon, update, { flush: 'post' });
|
||||
|
||||
const wrapStyleRef = computed((): any => {
|
||||
const { size, color } = props;
|
||||
let fs = size;
|
||||
if (isString(size)) {
|
||||
fs = parseInt(size, 10);
|
||||
}
|
||||
return {
|
||||
fontSize: `${fs}px`,
|
||||
color,
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(update);
|
||||
|
||||
return () => (
|
||||
<div ref={elRef} class={[attrs.class, 'app-iconify']} style={unref(wrapStyleRef)} />
|
||||
);
|
||||
},
|
||||
});
|
49
src/components/Loading/BasicLoading.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<section class="flex justify-center items-center flex-col">
|
||||
<img
|
||||
src="/@/assets/images/loading.svg"
|
||||
alt=""
|
||||
:height="getLoadingIconSize"
|
||||
:width="getLoadingIconSize"
|
||||
/>
|
||||
<span class="mt-4" v-if="tip"> {{ tip }}</span>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
// components
|
||||
import { defineComponent, computed } from 'vue';
|
||||
|
||||
// hook
|
||||
|
||||
import { SizeEnum, sizeMap } from '/@/enums/sizeEnum';
|
||||
|
||||
import { BasicLoadingProps } from './type';
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
name: 'BasicLoading',
|
||||
props: {
|
||||
tip: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<SizeEnum>,
|
||||
default: SizeEnum.DEFAULT,
|
||||
validator: (v: SizeEnum): boolean => {
|
||||
return [SizeEnum.DEFAULT, SizeEnum.SMALL, SizeEnum.LARGE].includes(v);
|
||||
},
|
||||
},
|
||||
},
|
||||
setup(props: BasicLoadingProps) {
|
||||
const getLoadingIconSize = computed(() => {
|
||||
const { size } = props;
|
||||
return sizeMap.get(size);
|
||||
});
|
||||
|
||||
return { getLoadingIconSize };
|
||||
},
|
||||
});
|
||||
</script>
|