This commit is contained in:
2025-06-19 17:32:00 +08:00
commit f6df695404
516 changed files with 119052 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
import { useThemeMode } from '@/hooks/useSystem/index';
const { defineMode } = useThemeMode();
defineMode();
</script>
<template>
<router-view />
</template>
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
@font-face {
font-family: Alibaba_PuHuiTi;
src: url('./AlibabaPuHuiTi-Regular.ttf');
font-style: normal;
}
* {
font-family: Alibaba_PuHuiTi;
}
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>arrow-down</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控" transform="translate(-1869.000000, -1004.000000)" fill-rule="nonzero">
<g id="arrow-down" transform="translate(1879.000000, 1014.000000) scale(1, -1) translate(-1879.000000, -1014.000000) translate(1869.000000, 1004.000000)">
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M10,13.9583398 C9.83333984,13.9583398 9.66666016,13.9166602 9.54166016,13.7916602 L2.875,7.125 C2.625,6.875 2.625,6.5 2.875,6.25 C3.125,6 3.5,6 3.75,6.25 L10,12.4583398 L16.2083398,6.20833984 C16.4583398,5.95833984 16.8333398,5.95833984 17.0833398,6.20833984 C17.3333398,6.45833984 17.3333398,6.83333984 17.0833398,7.08333984 L10.4166602,13.75 C10.3333398,13.9166602 10.1666602,13.9583398 10,13.9583398 Z" id="路径" fill="#666666"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>arrow-down</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控展开列表" transform="translate(-1870.000000, -625.000000)" fill-rule="nonzero">
<g id="arrow-down" transform="translate(1870.000000, 625.000000)">
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M10,13.9583398 C9.83333984,13.9583398 9.66666016,13.9166602 9.54166016,13.7916602 L2.875,7.125 C2.625,6.875 2.625,6.5 2.875,6.25 C3.125,6 3.5,6 3.75,6.25 L10,12.4583398 L16.2083398,6.20833984 C16.4583398,5.95833984 16.8333398,5.95833984 17.0833398,6.20833984 C17.3333398,6.45833984 17.3333398,6.83333984 17.0833398,7.08333984 L10.4166602,13.75 C10.3333398,13.9166602 10.1666602,13.9583398 10,13.9583398 Z" id="路径" fill="#666666"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26px" height="26px" viewBox="0 0 26 26" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 4</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="轨迹回放" transform="translate(-1850.000000, -138.000000)">
<g id="编组-4" transform="translate(1850.000000, 138.000000)">
<circle id="椭圆形备份-7" stroke="#2C6AF3" fill="#FFFFFF" cx="13" cy="13" r="12.5"></circle>
<g id="关闭" transform="translate(6.000000, 6.000000)" fill="#2C6AF3" fill-rule="nonzero">
<rect id="矩形" opacity="0" x="0" y="0" width="14" height="14"></rect>
<path d="M7.50575,6.887125 L12.105625,2.2876875 C12.2161317,2.17718079 12.2592895,2.01611374 12.2188412,1.86515876 C12.178393,1.71420379 12.0604837,1.59629453 11.9095287,1.55584626 C11.7585738,1.515398 11.5975067,1.55855578 11.487,1.6690625 L6.887125,6.2685 L2.2876875,1.6690625 C2.17718079,1.55855579 2.01611374,1.515398 1.86515877,1.55584627 C1.71420379,1.59629453 1.59629453,1.71420379 1.55584627,1.86515877 C1.515398,2.01611374 1.55855579,2.17718079 1.6690625,2.2876875 L6.2685,6.887125 L1.6690625,11.4865625 C1.49823392,11.6573911 1.49823392,11.9343589 1.6690625,12.1051875 C1.83989108,12.2760161 2.11685892,12.2760161 2.2876875,12.1051875 L6.887125,7.50575 L11.4865625,12.105625 C11.5970692,12.2161317 11.7581363,12.2592895 11.9090912,12.2188412 C12.0600462,12.178393 12.1779555,12.0604837 12.2184037,11.9095287 C12.258852,11.7585738 12.2156942,11.5975067 12.1051875,11.487 L7.50575,6.887125 Z" id="路径"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>车辆 (3)</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控" transform="translate(-1207.000000, -592.000000)" fill="#FFFFFF" fill-rule="nonzero">
<g id="编组-7" transform="translate(1018.000000, 207.000000)">
<g id="车辆-(3)" transform="translate(189.000000, 385.000000)">
<rect id="矩形" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M8.91,19.3333588 C8.83,19.3333588 8.74666016,19.3133398 8.67333984,19.2733398 L5.16,17.3933398 L1.82333984,19.27 C1.66666016,19.3566602 1.48,19.3533398 1.32666016,19.2666797 C1.17388392,19.177339 1.07999053,19.0136413 1.07999053,18.8366602 L1.07999053,2.50333984 C1.07667969,1.49 1.94666016,0.666660156 3.01666016,0.666660156 L14.8066602,0.666660156 C15.8766602,0.666660156 16.7433398,1.49 16.7433398,2.5 L16.7433398,12.0833398 C16.7433398,12.36 16.52,12.5833398 16.2433398,12.5833398 C15.9666797,12.5833398 15.7433398,12.36 15.7433398,12.0833398 L15.7433398,2.5 C15.7433398,2.04 15.3233398,1.66666016 14.8066602,1.66666016 L3.01666016,1.66666016 C2.5,1.66666016 2.07666016,2.04 2.07666016,2.5 L2.07666016,17.98 L4.91,16.39 C5.06,16.3066602 5.24,16.3033398 5.39,16.3866797 L8.89332031,18.2633398 L9.74666016,17.7533398 C9.98435852,17.6130665 10.2906839,17.6903841 10.4333398,17.9266602 C10.5733398,18.1633398 10.4966602,18.47 10.26,18.6133398 L9.16332031,19.2666602 C9.09,19.31 9,19.3333588 8.91,19.3333588 L8.91,19.3333588 Z" id="路径" stroke="#FFFFFF" stroke-width="0.5"></path>
<path d="M13.0433398,5.68039426 L4.77666016,5.68039426 C4.5,5.68039426 4.27666016,5.33564708 4.27666016,4.90852721 C4.27666016,4.48140734 4.5,4.13666016 4.77666016,4.13666016 L13.0433594,4.13666016 C13.3200195,4.13666016 13.5433594,4.48143749 13.5433594,4.90852721 C13.5433594,5.33561693 13.3200195,5.68039426 13.0433594,5.68039426 L13.0433398,5.68039426 Z M13.0433398,10.1366602 L4.77666016,10.1366602 C4.5,10.1366602 4.27666016,9.79188282 4.27666016,9.3647931 C4.27666016,8.93770338 4.5,8.59292605 4.77666016,8.59292605 L13.0433594,8.59292605 C13.3200195,8.59292605 13.5433594,8.93767323 13.5433594,9.3647931 C13.5433594,9.79191297 13.3200195,10.1366602 13.0433594,10.1366602 L13.0433398,10.1366602 Z M11.39,16.9333398 C11.6466602,16.9333398 11.8566602,16.7233398 11.8566602,16.4666602 C11.8566602,16.2099805 11.6466797,16 11.39,16 C11.1333203,16 10.9233398,16.21 10.9233398,16.4666602 C10.9233398,16.7233203 11.1333398,16.9333398 11.39,16.9333398 L11.39,16.9333398 Z M16.5,16.9333398 C16.7566602,16.9333398 16.9666602,16.7233398 16.9666602,16.4666602 C16.9666602,16.2099805 16.76,16 16.5,16 C16.2433398,16 16.0333398,16.21 16.0333398,16.4666602 C16.0333398,16.7233203 16.2433398,16.9333398 16.5,16.9333398 L16.5,16.9333398 Z" id="形状"></path>
<path d="M16.4066602,19.2966797 C15.76,19.2966797 15.2133398,18.9 15.0566602,18.3633398 L12.8333398,18.3633398 C12.6766602,18.9 12.13,19.2966797 11.4833398,19.2966797 C10.8366797,19.2966797 10.29,18.9 10.1333398,18.3633398 C9.49,18.36 8.97,17.8866602 8.97,17.3033398 L8.97,15.57 C8.97,14.97 9.35,14.4166797 9.95,14.14 L10.4033203,12.5233398 C10.5566602,11.97 11.1133398,11.58 11.7566602,11.58 L16.1633398,11.58 C16.8133398,11.58 17.37,11.9733398 17.52,12.5333203 L17.95,14.14 C18.5433398,14.42 18.9233398,14.9733398 18.9233398,15.57 L18.9233398,17.3033398 C18.9233398,17.8866602 18.4033398,18.36 17.76,18.3633398 C17.6033398,18.9 17.0566797,19.2966797 16.4066797,19.2966797 L16.4066602,19.2966797 Z M12.38,17.3633398 L15.5133398,17.3633398 C15.79,17.3633398 16.0133398,17.5866602 16.0133398,17.8633398 L16.0133398,18.05 C16.0133398,18.1666602 16.1833398,18.2966797 16.41,18.2966797 C16.6366602,18.2966797 16.8066797,18.1666602 16.8066797,18.05 L16.8066797,17.8633398 C16.8066797,17.5866602 17.03,17.3633398 17.3066797,17.3633398 L17.7533398,17.3633398 C17.8533398,17.3633398 17.9166602,17.3166602 17.9266602,17.2966602 L17.9266602,15.5733398 C17.9266602,15.3566602 17.7566602,15.1433398 17.4933398,15.0333398 L17.34,14.97 C17.1966602,14.91 17.09,14.7866602 17.05,14.6366602 L16.5566602,12.7966602 C16.53,12.6933398 16.3766602,12.5833398 16.1666602,12.5833398 L11.7533398,12.5833398 C11.5466602,12.5833398 11.3933398,12.69 11.3633398,12.7933398 L10.8466602,14.64 C10.8066602,14.7866797 10.7,14.9066602 10.5566602,14.9666602 L10.4033398,15.03 C10.14,15.14 9.96666016,15.3533398 9.96666016,15.57 L9.96666016,17.3033398 C9.97333984,17.3133398 10.0366602,17.3633398 10.1366602,17.3633398 L10.5833398,17.3633398 C10.86,17.3633398 11.0833398,17.5866602 11.0833398,17.8633398 L11.0833398,18.05 C11.0833398,18.1666602 11.2533398,18.2966797 11.48,18.2966797 C11.7066602,18.2966797 11.8733398,18.1666602 11.8733398,18.05 L11.8733398,17.8633398 C11.88,17.59 12.1033398,17.3633398 12.38,17.3633398 L12.38,17.3633398 Z" id="形状"></path>
<path d="M16.1833398,15.5666602 L11.71,15.5666602 C11.5533398,15.5666602 11.4066797,15.4933398 11.3133203,15.37 C11.22,15.2466602 11.1866602,15.0866602 11.2266602,14.9366602 L11.5966602,13.55 C11.7,13.16 12.08,12.8866602 12.5166602,12.8866602 L15.38,12.8866602 C15.8166602,12.8866602 16.1966602,13.16 16.3,13.55 L16.6666602,14.94 C16.7066602,15.09 16.6733398,15.25 16.58,15.3733398 C16.4866602,15.4966797 16.34,15.5666602 16.1833203,15.5666602 L16.1833398,15.5666602 Z M12.36,14.5666602 L15.5333398,14.5666602 L15.3533398,13.89 L12.54,13.89 L12.36,14.5666602 Z" id="形状"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>车辆管理</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控" transform="translate(-265.000000, -293.000000)" fill="#B7B7B7" fill-rule="nonzero">
<g id="车辆管理" transform="translate(265.000000, 293.000000)">
<rect id="矩形" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M18.2386719,7.15078125 L16.6105469,7.15078125 C16.5357184,7.15163115 16.4623812,7.17181572 16.3976563,7.209375 L15.6640625,4.97695312 C15.4296875,3.70742187 14.54375,3.09570312 13.4570313,3.09570312 L6.56054688,3.09570312 C5.31191406,3.09570312 4.55390625,3.91757812 4.35351562,4.97617188 L3.61660156,7.21542969 C3.5485776,7.17401 3.47065258,7.15167824 3.39101562,7.15078125 L1.76210937,7.15078125 C1.64127721,7.15129805 1.52560237,7.19980807 1.44054584,7.28563385 C1.3554893,7.37145963 1.30802268,7.48756652 1.30858866,7.60839844 L1.30858866,8.00605469 C1.30772852,8.25751573 1.51065016,8.46218518 1.76210938,8.46347656 L2.37910156,8.56992187 C2.18378906,8.95410156 2.07285156,9.39355469 2.07285156,9.90449219 L1.84511719,12.5390625 C1.84543543,12.6168988 1.8504576,12.6946447 1.86015625,12.771875 C1.85056163,12.8207823 1.84552702,12.8704746 1.84511719,12.9203125 L1.84511719,16.0878906 C1.84418277,16.3034832 1.92893149,16.5106167 2.08071918,16.663723 C2.23250687,16.8168293 2.43889956,16.9033665 2.65449219,16.9042969 L3.88964844,16.9042969 C4.10511982,16.9032635 4.31135231,16.816662 4.46296258,16.6635499 C4.61457285,16.5104378 4.69913734,16.3033617 4.69804687,16.0878906 L4.69804687,15.0697266 L15.3167969,15.0697266 L15.3167969,16.0878906 C15.3158625,16.3034832 15.4006112,16.5106167 15.5523989,16.663723 C15.7041866,16.8168293 15.9105792,16.9033665 16.1261719,16.9042969 L17.3615234,16.9042969 C17.5770983,16.9033666 17.783471,16.8168232 17.9352277,16.6637112 C18.0869843,16.5105992 18.1716895,16.3034652 18.1707031,16.0878906 L18.1707031,12.9195313 C18.1702576,12.8696953 18.1652233,12.8200063 18.1556641,12.7710938 C18.1649744,12.693827 18.1699951,12.6161037 18.1707031,12.5382813 L17.9419922,9.90449219 C17.9419922,9.39179688 17.8308594,8.95253906 17.6351562,8.56679688 L18.2386719,8.46347656 C18.4900374,8.46207802 18.6927042,8.25722545 18.6914184,8.00585938 L18.6914184,7.60839844 C18.6922898,7.48760749 18.6450313,7.37143796 18.5600763,7.28556668 C18.4751213,7.19969539 18.3594654,7.15119348 18.2386719,7.15078125 Z M4.896875,6.68203125 L5.43574219,5.23378906 L5.44355469,5.2 C5.49726562,4.90703125 5.6171875,4.85527344 5.79375,4.62871094 L14.2324219,4.62871094 C14.4115234,4.86132812 14.5226562,4.92050781 14.5714844,5.19667969 L15.1183594,6.68105469 L15.31875,7.48789063 C15.2738281,8.07871094 14.5708984,8.54472656 13.9857422,8.54472656 L6.0296875,8.54472656 C5.44375,8.54472656 4.740625,8.07871094 4.69570312,7.48789063 L4.896875,6.68203125 Z M5.27480469,13.5691406 C4.59804688,13.5691406 4.04980469,13.0152344 4.04980469,12.3322266 C4.04980469,11.6492187 4.59804688,11.0953125 5.27480469,11.0953125 C5.9515625,11.0953125 6.49960937,11.6482422 6.49960937,12.3322266 C6.49960937,13.0162109 5.95136719,13.5691406 5.27480469,13.5691406 Z M12.1183627,12.65625 C12.1187746,12.7534586 12.0805029,12.8468394 12.0119856,12.9157964 C11.9434684,12.9847533 11.8503342,13.0236215 11.753125,13.0238281 L8.315625,13.0238281 C8.11373991,13.0226448 7.95097956,12.8581362 7.95194878,12.65625 L7.95194878,11.6210938 C7.95164187,11.5241419 7.98987094,11.4310401 8.05822485,11.3622833 C8.12657877,11.2935264 8.21945422,11.2547508 8.31640625,11.2544922 L11.7539063,11.2544922 C11.9557144,11.255246 12.1187918,11.4192847 12.1183627,11.6210938 L12.1183627,12.65625 Z M14.7410156,13.5689453 C14.0642578,13.5689453 13.5154297,13.0150391 13.5154297,12.3320312 C13.5154297,11.6490234 14.0642578,11.0951172 14.7410156,11.0951172 C15.4177734,11.0951172 15.9660156,11.6480469 15.9660156,12.3320312 C15.9660156,13.0160156 15.4177734,13.5689453 14.7410156,13.5689453 Z" id="形状"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>车辆管理</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控" transform="translate(-265.000000, -327.000000)" fill="#53CB24" fill-rule="nonzero">
<g id="车辆管理" transform="translate(265.000000, 327.000000)">
<rect id="矩形" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M18.2386719,7.15078125 L16.6105469,7.15078125 C16.5357184,7.15163115 16.4623812,7.17181572 16.3976563,7.209375 L15.6640625,4.97695312 C15.4296875,3.70742187 14.54375,3.09570312 13.4570313,3.09570312 L6.56054688,3.09570312 C5.31191406,3.09570312 4.55390625,3.91757812 4.35351562,4.97617188 L3.61660156,7.21542969 C3.5485776,7.17401 3.47065258,7.15167824 3.39101562,7.15078125 L1.76210937,7.15078125 C1.64127721,7.15129805 1.52560237,7.19980807 1.44054584,7.28563385 C1.3554893,7.37145963 1.30802268,7.48756652 1.30858866,7.60839844 L1.30858866,8.00605469 C1.30772852,8.25751573 1.51065016,8.46218518 1.76210938,8.46347656 L2.37910156,8.56992187 C2.18378906,8.95410156 2.07285156,9.39355469 2.07285156,9.90449219 L1.84511719,12.5390625 C1.84543543,12.6168988 1.8504576,12.6946447 1.86015625,12.771875 C1.85056163,12.8207823 1.84552702,12.8704746 1.84511719,12.9203125 L1.84511719,16.0878906 C1.84418277,16.3034832 1.92893149,16.5106167 2.08071918,16.663723 C2.23250687,16.8168293 2.43889956,16.9033665 2.65449219,16.9042969 L3.88964844,16.9042969 C4.10511982,16.9032635 4.31135231,16.816662 4.46296258,16.6635499 C4.61457285,16.5104378 4.69913734,16.3033617 4.69804687,16.0878906 L4.69804687,15.0697266 L15.3167969,15.0697266 L15.3167969,16.0878906 C15.3158625,16.3034832 15.4006112,16.5106167 15.5523989,16.663723 C15.7041866,16.8168293 15.9105792,16.9033665 16.1261719,16.9042969 L17.3615234,16.9042969 C17.5770983,16.9033666 17.783471,16.8168232 17.9352277,16.6637112 C18.0869843,16.5105992 18.1716895,16.3034652 18.1707031,16.0878906 L18.1707031,12.9195313 C18.1702576,12.8696953 18.1652233,12.8200063 18.1556641,12.7710938 C18.1649744,12.693827 18.1699951,12.6161037 18.1707031,12.5382813 L17.9419922,9.90449219 C17.9419922,9.39179688 17.8308594,8.95253906 17.6351562,8.56679688 L18.2386719,8.46347656 C18.4900374,8.46207802 18.6927042,8.25722545 18.6914184,8.00585938 L18.6914184,7.60839844 C18.6922898,7.48760749 18.6450313,7.37143796 18.5600763,7.28556668 C18.4751213,7.19969539 18.3594654,7.15119348 18.2386719,7.15078125 Z M4.896875,6.68203125 L5.43574219,5.23378906 L5.44355469,5.2 C5.49726562,4.90703125 5.6171875,4.85527344 5.79375,4.62871094 L14.2324219,4.62871094 C14.4115234,4.86132812 14.5226562,4.92050781 14.5714844,5.19667969 L15.1183594,6.68105469 L15.31875,7.48789063 C15.2738281,8.07871094 14.5708984,8.54472656 13.9857422,8.54472656 L6.0296875,8.54472656 C5.44375,8.54472656 4.740625,8.07871094 4.69570312,7.48789063 L4.896875,6.68203125 Z M5.27480469,13.5691406 C4.59804688,13.5691406 4.04980469,13.0152344 4.04980469,12.3322266 C4.04980469,11.6492187 4.59804688,11.0953125 5.27480469,11.0953125 C5.9515625,11.0953125 6.49960937,11.6482422 6.49960937,12.3322266 C6.49960937,13.0162109 5.95136719,13.5691406 5.27480469,13.5691406 Z M12.1183627,12.65625 C12.1187746,12.7534586 12.0805029,12.8468394 12.0119856,12.9157964 C11.9434684,12.9847533 11.8503342,13.0236215 11.753125,13.0238281 L8.315625,13.0238281 C8.11373991,13.0226448 7.95097956,12.8581362 7.95194878,12.65625 L7.95194878,11.6210938 C7.95164187,11.5241419 7.98987094,11.4310401 8.05822485,11.3622833 C8.12657877,11.2935264 8.21945422,11.2547508 8.31640625,11.2544922 L11.7539063,11.2544922 C11.9557144,11.255246 12.1187918,11.4192847 12.1183627,11.6210938 L12.1183627,12.65625 Z M14.7410156,13.5689453 C14.0642578,13.5689453 13.5154297,13.0150391 13.5154297,12.3320312 C13.5154297,11.6490234 14.0642578,11.0951172 14.7410156,11.0951172 C15.4177734,11.0951172 15.9660156,11.6480469 15.9660156,12.3320312 C15.9660156,13.0160156 15.4177734,13.5689453 14.7410156,13.5689453 Z" id="形状"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>车辆管理</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控" transform="translate(-265.000000, -463.000000)" fill="#EA4955" fill-rule="nonzero">
<g id="车辆管理" transform="translate(265.000000, 463.000000)">
<rect id="矩形" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M18.2386719,7.15078125 L16.6105469,7.15078125 C16.5357184,7.15163115 16.4623812,7.17181572 16.3976563,7.209375 L15.6640625,4.97695312 C15.4296875,3.70742187 14.54375,3.09570312 13.4570313,3.09570312 L6.56054688,3.09570312 C5.31191406,3.09570312 4.55390625,3.91757812 4.35351562,4.97617188 L3.61660156,7.21542969 C3.5485776,7.17401 3.47065258,7.15167824 3.39101562,7.15078125 L1.76210937,7.15078125 C1.64127721,7.15129805 1.52560237,7.19980807 1.44054584,7.28563385 C1.3554893,7.37145963 1.30802268,7.48756652 1.30858866,7.60839844 L1.30858866,8.00605469 C1.30772852,8.25751573 1.51065016,8.46218518 1.76210938,8.46347656 L2.37910156,8.56992187 C2.18378906,8.95410156 2.07285156,9.39355469 2.07285156,9.90449219 L1.84511719,12.5390625 C1.84543543,12.6168988 1.8504576,12.6946447 1.86015625,12.771875 C1.85056163,12.8207823 1.84552702,12.8704746 1.84511719,12.9203125 L1.84511719,16.0878906 C1.84418277,16.3034832 1.92893149,16.5106167 2.08071918,16.663723 C2.23250687,16.8168293 2.43889956,16.9033665 2.65449219,16.9042969 L3.88964844,16.9042969 C4.10511982,16.9032635 4.31135231,16.816662 4.46296258,16.6635499 C4.61457285,16.5104378 4.69913734,16.3033617 4.69804687,16.0878906 L4.69804687,15.0697266 L15.3167969,15.0697266 L15.3167969,16.0878906 C15.3158625,16.3034832 15.4006112,16.5106167 15.5523989,16.663723 C15.7041866,16.8168293 15.9105792,16.9033665 16.1261719,16.9042969 L17.3615234,16.9042969 C17.5770983,16.9033666 17.783471,16.8168232 17.9352277,16.6637112 C18.0869843,16.5105992 18.1716895,16.3034652 18.1707031,16.0878906 L18.1707031,12.9195313 C18.1702576,12.8696953 18.1652233,12.8200063 18.1556641,12.7710938 C18.1649744,12.693827 18.1699951,12.6161037 18.1707031,12.5382813 L17.9419922,9.90449219 C17.9419922,9.39179688 17.8308594,8.95253906 17.6351562,8.56679688 L18.2386719,8.46347656 C18.4900374,8.46207802 18.6927042,8.25722545 18.6914184,8.00585938 L18.6914184,7.60839844 C18.6922898,7.48760749 18.6450313,7.37143796 18.5600763,7.28556668 C18.4751213,7.19969539 18.3594654,7.15119348 18.2386719,7.15078125 Z M4.896875,6.68203125 L5.43574219,5.23378906 L5.44355469,5.2 C5.49726562,4.90703125 5.6171875,4.85527344 5.79375,4.62871094 L14.2324219,4.62871094 C14.4115234,4.86132812 14.5226562,4.92050781 14.5714844,5.19667969 L15.1183594,6.68105469 L15.31875,7.48789063 C15.2738281,8.07871094 14.5708984,8.54472656 13.9857422,8.54472656 L6.0296875,8.54472656 C5.44375,8.54472656 4.740625,8.07871094 4.69570312,7.48789063 L4.896875,6.68203125 Z M5.27480469,13.5691406 C4.59804688,13.5691406 4.04980469,13.0152344 4.04980469,12.3322266 C4.04980469,11.6492187 4.59804688,11.0953125 5.27480469,11.0953125 C5.9515625,11.0953125 6.49960937,11.6482422 6.49960937,12.3322266 C6.49960937,13.0162109 5.95136719,13.5691406 5.27480469,13.5691406 Z M12.1183627,12.65625 C12.1187746,12.7534586 12.0805029,12.8468394 12.0119856,12.9157964 C11.9434684,12.9847533 11.8503342,13.0236215 11.753125,13.0238281 L8.315625,13.0238281 C8.11373991,13.0226448 7.95097956,12.8581362 7.95194878,12.65625 L7.95194878,11.6210938 C7.95164187,11.5241419 7.98987094,11.4310401 8.05822485,11.3622833 C8.12657877,11.2935264 8.21945422,11.2547508 8.31640625,11.2544922 L11.7539063,11.2544922 C11.9557144,11.255246 12.1187918,11.4192847 12.1183627,11.6210938 L12.1183627,12.65625 Z M14.7410156,13.5689453 C14.0642578,13.5689453 13.5154297,13.0150391 13.5154297,12.3320312 C13.5154297,11.6490234 14.0642578,11.0951172 14.7410156,11.0951172 C15.4177734,11.0951172 15.9660156,11.6480469 15.9660156,12.3320312 C15.9660156,13.0160156 15.4177734,13.5689453 14.7410156,13.5689453 Z" id="形状"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>轨迹-01</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控" transform="translate(-1048.000000, -592.000000)" fill="#FFFFFF" fill-rule="nonzero">
<g id="编组-7" transform="translate(1018.000000, 207.000000)">
<g id="轨迹-01" transform="translate(30.000000, 385.000000)">
<rect id="矩形" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M16.48,14.88 L3.52,14.88 C3.2,14.88 2.92,14.64 2.92,14.28 L2.92,14.04 C2.92,13.72 3.16,13.44 3.52,13.44 L6.04,13.44 C6.44,13.44 6.8,13.12 6.8,12.68 C6.8,12.24 6.48,12 6.08,12 L3.52,12 C2.36,12 1.44,12.92 1.44,14.08 L1.44,14.32 C1.44,15.48 2.36,16.4 3.52,16.4 L16.44,16.4 C16.76,16.4 17.04,16.64 17.04,17 L17.04,17.2 C17.04,17.52 16.8,17.8 16.44,17.8 L5.76,17.8 C5.36,17.8 5,18.12 5,18.56 C5,18.96 5.32,19.32 5.76,19.32 L16.44,19.32 C17.6,19.32 18.52,18.4 18.52,17.24 L18.52,17 C18.56,15.8 17.6,14.88 16.48,14.88 Z M10,0.72 C7.32,0.72 5.16,2.88 5.16,5.56 C5.16,8.24 8.44,12.96 10,12.96 C12,12.96 14.84,8.24 14.84,5.56 C14.84,2.88 12.64,0.72 10,0.72 Z M10,8 C8.64,8 7.52,6.88 7.52,5.52 C7.52,4.16 8.64,3.04 10,3.04 C11.36,3.04 12.48,4.16 12.48,5.52 C12.48,6.88 11.36,8 10,8 L10,8 Z" id="形状"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>单列列表</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控" transform="translate(-252.000000, -1006.000000)" fill-rule="nonzero">
<g id="编组-3" transform="translate(248.000000, 1002.000000)">
<g id="单列列表" transform="translate(4.500000, 4.500000)">
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="15" height="15"></rect>
<path d="M12.4379883,1.875 L2.56201172,1.875 C2.18261719,1.875 1.875,2.18261719 1.875,2.56201172 L1.875,3.06298828 C1.875,3.44238281 2.18261719,3.75 2.56201172,3.75 L12.4379883,3.75 C12.8173828,3.75 13.125,3.44238281 13.125,3.06298828 L13.125,2.56201172 C13.125,2.18261719 12.8173828,1.875 12.4379883,1.875 L12.4379883,1.875 Z M12.4379883,11.25 L2.56201172,11.25 C2.18261719,11.25 1.875,11.5576172 1.875,11.9370117 L1.875,12.4379883 C1.875,12.8173828 2.18261719,13.125 2.56201172,13.125 L12.4379883,13.125 C12.8173828,13.125 13.125,12.8173828 13.125,12.4379883 L13.125,11.9370117 C13.125,11.5576172 12.8173828,11.25 12.4379883,11.25 Z M12.4379883,6.5625 L2.56201172,6.5625 C2.18261719,6.5625 1.875,6.87011719 1.875,7.24951172 L1.875,7.75048828 C1.875,8.12988281 2.18261719,8.4375 2.56201172,8.4375 L12.4379883,8.4375 C12.8173828,8.4375 13.125,8.12988281 13.125,7.75048828 L13.125,7.24951172 C13.125,6.87011719 12.8173828,6.5625 12.4379883,6.5625 L12.4379883,6.5625 Z" id="形状" fill="#9CB1E3"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>轨迹-01</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控展开列表" transform="translate(-233.000000, -717.000000)" fill="#2C6AF3" fill-rule="nonzero">
<g id="轨迹-01" transform="translate(233.000000, 717.000000)">
<rect id="矩形" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M16.48,14.88 L3.52,14.88 C3.2,14.88 2.92,14.64 2.92,14.28 L2.92,14.04 C2.92,13.72 3.16,13.44 3.52,13.44 L6.04,13.44 C6.44,13.44 6.8,13.12 6.8,12.68 C6.8,12.24 6.48,12 6.08,12 L3.52,12 C2.36,12 1.44,12.92 1.44,14.08 L1.44,14.32 C1.44,15.48 2.36,16.4 3.52,16.4 L16.44,16.4 C16.76,16.4 17.04,16.64 17.04,17 L17.04,17.2 C17.04,17.52 16.8,17.8 16.44,17.8 L5.76,17.8 C5.36,17.8 5,18.12 5,18.56 C5,18.96 5.32,19.32 5.76,19.32 L16.44,19.32 C17.6,19.32 18.52,18.4 18.52,17.24 L18.52,17 C18.56,15.8 17.6,14.88 16.48,14.88 Z M10,0.72 C7.32,0.72 5.16,2.88 5.16,5.56 C5.16,8.24 8.44,12.96 10,12.96 C12,12.96 14.84,8.24 14.84,5.56 C14.84,2.88 12.64,0.72 10,0.72 Z M10,8 C8.64,8 7.52,6.88 7.52,5.52 C7.52,4.16 8.64,3.04 10,3.04 C11.36,3.04 12.48,4.16 12.48,5.52 C12.48,6.88 11.36,8 10,8 L10,8 Z" id="形状"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>关闭</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控" transform="translate(-1309.000000, -216.000000)" fill="#FFFFFF" fill-rule="nonzero">
<g id="编组-7" transform="translate(1018.000000, 207.000000)">
<g id="关闭" transform="translate(291.000000, 9.000000)">
<rect id="矩形" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M9.98697266,2.48697266 C11.0000195,2.48697266 11.9820898,2.68509766 12.9059375,3.07583984 C13.7988867,3.45351562 14.6010937,3.99447266 15.2902734,4.68367187 C15.9794727,5.37285156 16.5204102,6.17505859 16.8981055,7.06800781 C17.2888477,7.99183594 17.4869727,8.97392578 17.4869727,9.98697266 C17.4869727,11.0000195 17.2888477,11.9820898 16.8981055,12.9059375 C16.5204297,13.7988867 15.9794727,14.6010938 15.2902734,15.2902734 C14.6010937,15.9794727 13.7988867,16.5204102 12.9059375,16.8981055 C11.9821094,17.2888477 11.0000195,17.4869727 9.98697266,17.4869727 C8.97392578,17.4869727 7.99185547,17.2888477 7.06800781,16.8981055 C6.17505859,16.5204297 5.37285156,15.9794727 4.68367187,15.2902734 C3.99447266,14.6010938 3.45353516,13.7988867 3.07583984,12.9059375 C2.68509766,11.9821094 2.48697266,11.0000195 2.48697266,9.98697266 C2.48697266,8.97392578 2.68509766,7.99185547 3.07583984,7.06800781 C3.45351562,6.17505859 3.99447266,5.37285156 4.68367187,4.68367187 C5.37285156,3.99447266 6.17505859,3.45353516 7.06800781,3.07583984 C7.99185547,2.68509766 8.97392578,2.48697266 9.98697266,2.48697266 M9.98697266,1.23697266 C5.15447266,1.23697266 1.23697266,5.15447266 1.23697266,9.98697266 C1.23697266,14.8194727 5.15447266,18.7369727 9.98697266,18.7369727 C14.8194727,18.7369727 18.7369727,14.8194727 18.7369727,9.98697266 C18.7369727,5.15447266 14.8194727,1.23697266 9.98697266,1.23697266 L9.98697266,1.23697266 Z" id="形状"></path>
<path d="M10.8838672,10 L13.7622461,7.12162109 C14.0063281,6.87753906 14.0063281,6.48181641 13.7622461,6.23773438 C13.5181836,5.99365234 13.1224414,5.99365234 12.8783789,6.23773438 L10,9.11613281 L7.12162109,6.23775391 C6.87753906,5.99367187 6.48181641,5.99367187 6.23773438,6.23775391 C5.99365234,6.48183594 5.99365234,6.87755859 6.23773438,7.12164062 L9.11613281,10 L6.23775391,12.8783789 C5.99367187,13.1224414 5.99367187,13.5181836 6.23775391,13.7622461 C6.35978516,13.8842969 6.51974609,13.9453125 6.6796875,13.9453125 C6.83962891,13.9453125 6.99958984,13.8842969 7.12162109,13.7622461 L10,10.8838672 L12.8783789,13.7622461 C13.0004102,13.8842969 13.1603711,13.9453125 13.3203125,13.9453125 C13.4802539,13.9453125 13.6402148,13.8842969 13.7622461,13.7622461 C14.0063281,13.5181836 14.0063281,13.1224414 13.7622461,12.8783789 L10.8838672,10 Z" id="路径"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26px" height="26px" viewBox="0 0 26 26" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 4备份</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="轨迹回放" transform="translate(-1853.000000, -135.000000)">
<g id="编组-9" transform="translate(1626.000000, 123.000000)">
<g id="编组-4备份" transform="translate(227.000000, 12.000000)">
<circle id="椭圆形" stroke="#2C6AF3" fill="#FFFFFF" cx="13" cy="13" r="12.5"></circle>
<rect id="矩形" fill="#2C6AF3" x="8" y="8" width="10" height="10" rx="2"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 818 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon_zhanneixin_solid备份 3</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="全局监控展开列表" transform="translate(-269.000000, -717.000000)" fill="#2C6AF3" fill-rule="nonzero">
<g id="icon_zhanneixin_solid备份-3" transform="translate(269.000000, 717.000000)">
<rect id="矩形" opacity="0" x="0" y="0" width="19.9610136" height="19.9610136"></rect>
<path d="M18.5425341,2.50136452 L1.51578947,9.19703704 C1.36900833,9.26929645 1.26910226,9.41129984 1.25067515,9.57386235 C1.23224803,9.73642486 1.29783434,9.89718749 1.42471735,10.0004678 L4.13692008,12.1213255 L15.5546199,5.31711501 L5.89598441,13.4562183 L5.89598441,16.7547758 C5.88552611,16.8551277 5.93418424,16.952444 6.02074074,17.0042885 C6.10423606,17.0538017 6.20886907,17.050395 6.28896686,16.9955556 L8.48592593,15.3163353 L11.4152047,17.3847953 C11.5061435,17.4561425 11.6249463,17.4814687 11.737076,17.4534113 C11.85009,17.4262945 11.9449174,17.3497911 11.9953216,17.2450682 L18.697232,2.71968811 C18.757115,2.60366472 18.6548148,2.46393762 18.5425341,2.50136452 Z" id="路径"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>时间</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="轨迹回放" transform="translate(-432.000000, -275.000000)" fill="#2C6AF3" fill-rule="nonzero">
<g id="时间" transform="translate(432.000000, 275.000000)">
<rect id="矩形" opacity="0" x="0" y="0" width="14" height="14"></rect>
<path d="M6.99088086,1.74088086 C7.70001367,1.74088086 8.38746289,1.87956836 9.03415625,2.15308789 C9.6592207,2.41746094 10.2207656,2.79613086 10.7031914,3.27857031 C11.1856309,3.76099609 11.5642871,4.32254102 11.8286738,4.94760547 C12.1021934,5.59428516 12.2408809,6.28174805 12.2408809,6.99088086 C12.2408809,7.70001367 12.1021934,8.38746289 11.8286738,9.03415625 C11.5643008,9.6592207 11.1856309,10.2207656 10.7031914,10.7031914 C10.2207656,11.1856309 9.6592207,11.5642871 9.03415625,11.8286738 C8.38747656,12.1021934 7.70001367,12.2408809 6.99088086,12.2408809 C6.28174805,12.2408809 5.59429883,12.1021934 4.94760547,11.8286738 C4.32254102,11.5643008 3.76099609,11.1856309 3.27857031,10.7031914 C2.79613086,10.2207656 2.41747461,9.6592207 2.15308789,9.03415625 C1.87956836,8.38747656 1.74088086,7.70001367 1.74088086,6.99088086 C1.74088086,6.28174805 1.87956836,5.59429883 2.15308789,4.94760547 C2.41746094,4.32254102 2.79613086,3.76099609 3.27857031,3.27857031 C3.76099609,2.79613086 4.32254102,2.41747461 4.94760547,2.15308789 C5.59429883,1.87956836 6.28174805,1.74088086 6.99088086,1.74088086 M6.99088086,0.865880859 C3.60813086,0.865880859 0.865880859,3.60813086 0.865880859,6.99088086 C0.865880859,10.3736309 3.60813086,13.1158809 6.99088086,13.1158809 C10.3736309,13.1158809 13.1158809,10.3736309 13.1158809,6.99088086 C13.1158809,3.60813086 10.3736309,0.865880859 6.99088086,0.865880859 L6.99088086,0.865880859 Z" id="形状"></path>
<path d="M9.60935938,7.42838086 L6.99088086,7.42838086 C6.74925781,7.42838086 6.55338086,7.23251758 6.55338086,6.99088086 L6.55338086,3.07074414 C6.55338086,2.82912109 6.74925781,2.63324414 6.99088086,2.63324414 C7.23250391,2.63324414 7.42838086,2.82912109 7.42838086,3.07074414 L7.42838086,6.55338086 L9.6093457,6.55338086 C9.85096875,6.55338086 10.0468457,6.74925781 10.0468457,6.99088086 C10.0468457,7.23250391 9.85098242,7.42838086 9.60935938,7.42838086 Z" id="路径"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+39
View 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">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Group 5</title>
<desc>Created with Sketch.</desc>
<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="配置面板" 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="-1" y="0" width="49" height="10"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+40
View File
@@ -0,0 +1,40 @@
<?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">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Group 5 Copy 5</title>
<desc>Created with Sketch.</desc>
<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="配置面板" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="setting-copy-2" transform="translate(-1190.000000, -136.000000)">
<g id="Group-8" 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="-1" y="0" width="49" height="10"></rect>
<rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="44"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26px" height="26px" viewBox="0 0 26 26" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 5</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="轨迹回放点击行驶轨迹" transform="translate(-1853.000000, -135.000000)">
<g id="编组-5" transform="translate(1853.000000, 135.000000)">
<circle id="椭圆形" fill="#2C6AF3" cx="13" cy="13" r="13"></circle>
<g id="暂停" transform="translate(3.000000, 3.000000)" fill="#FFFFFF" fill-rule="nonzero">
<rect id="矩形" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M5.25335938,2.23275391 C6.68328125,2.23275391 7.84242188,3.39191406 7.84242188,4.82183594 L7.84242188,15.1781445 C7.84242188,16.6080859 6.68328125,17.7672266 5.25335938,17.7672266 C3.8234375,17.7672266 2.66429687,16.6080859 2.66429687,15.1781641 L2.66429687,4.82183594 C2.66429687,3.39189453 3.8234375,2.23275391 5.25335938,2.23275391 L5.25335938,2.23275391 Z M14.7466406,2.23275391 C16.1765625,2.23275391 17.3357031,3.39191406 17.3357031,4.82183594 L17.3357031,15.1781445 C17.3357031,16.6080859 16.1765625,17.7672266 14.7466406,17.7672266 C13.3167188,17.7672266 12.1575781,16.6080859 12.1575781,15.1781641 L12.1575781,4.82183594 C12.1575781,3.39189453 13.3167188,2.23275391 14.7466406,2.23275391 L14.7466406,2.23275391 Z" id="形状"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26px" height="26px" viewBox="0 0 26 26" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 4</title>
<g id="web" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="轨迹回放" transform="translate(-1816.000000, -138.000000)">
<g id="编组-4" transform="translate(1816.000000, 138.000000)">
<circle id="椭圆形" fill="#2C6AF3" cx="13" cy="13" r="13"></circle>
<g id="播放器-播放_44" transform="translate(4.000000, 3.000000)" fill-rule="nonzero">
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="20" height="20"></rect>
<path d="M14.5454492,10.7727344 L6.36363281,15.5454492 C5.77273438,15.909082 5,15.4545508 5,14.7727344 L5,5.22726562 C5,4.54544922 5.77273438,4.09091797 6.36363281,4.45455078 L14.5454688,9.22726562 C15.1363672,9.54544922 15.1363672,10.4545508 14.5454688,10.7727344 L14.5454492,10.7727344 Z" id="路径" fill="#FFFFFF"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

+46
View File
@@ -0,0 +1,46 @@
@charset "UTF-8";
* {
box-sizing: border-box;
font-family: Alibaba_PuHuiTi, sans-serif, -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, sans-serif;
}
html {
overflow: scroll;
overflow: overlay;
}
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
/* 滚动条的宽高 */
/*定义滚动条轨道*/
/* 滚动条的滑块 */
}
html *::-webkit-scrollbar,
body *::-webkit-scrollbar,
#app *::-webkit-scrollbar {
width: 8px;
height: 6px;
}
html *::-webkit-scrollbar-track,
body *::-webkit-scrollbar-track,
#app *::-webkit-scrollbar-track {
background-color: transparent;
border-radius: 2em;
}
html *::-webkit-scrollbar-thumb,
body *::-webkit-scrollbar-thumb,
#app *::-webkit-scrollbar-thumb {
opacity: 1;
background-color: #cecece;
-webkit-transition: opacity 800ms linear;
transition: opacity 800ms linear;
z-index: 200;
}
.amap-copyright, .amap-logo {
display: none !important;
}/*# sourceMappingURL=base.theme.css.map */
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["base.theme.css","../theme/base.theme.scss"],"names":[],"mappings":"AAAA,gBAAgB;ACmBhB;EACE,sBAAA;EACA,gMApBA;ADGF;;ACoBA;EACE,gBAAA;EACA,iBAAA;ADjBF;;ACoBA;;;EAGE,YAAA;EACA,SAAA;EACA,UAAA;EACA,WAAA;EAMA,UAAA;EAQA,WAAA;AD7BF;ACgBE;;;EACE,UAvBa;EAwBb,WAvBc;ADWlB;ACgBE;;;EACE,6BAAA;EAGA,kBAAA;ADZJ;ACgBE;;;EACE,UAAA;EACA,yBApCa;EAqCb,wCAAA;EAAA,gCAAA;EACA,YAAA;ADZJ;;ACgBA;EACE,wBAAA;ADbF","file":"base.theme.css"}
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+71
View File
@@ -0,0 +1,71 @@
$main-fontFamily:
Alibaba_PuHuiTi,
sans-serif,
-apple-system,
BlinkMacSystemFont,
Helvetica Neue,
PingFang SC,
Microsoft YaHei,
Source Han Sans SC,
Noto Sans CJK SC,
WenQuanYi Micro Hei,
sans-serif;
// 滚动条配置
$scroller-width: 6px;
$scroller-height: 6px;
$scroller-color: #9093994c;
* {
box-sizing: border-box;
font-family: $main-fontFamily;
}
html {
overflow: scroll;
overflow: overlay;
}
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
/* 滚动条的宽高 */
*::-webkit-scrollbar {
width: $scroller-width;
height: $scroller-height;
}
/*定义滚动条轨道*/
*::-webkit-scrollbar-track {
background-color: transparent;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}
/* 滚动条的滑块 */
*::-webkit-scrollbar-thumb {
opacity: 1;
background-color: $scroller-color;
transition: opacity 800ms linear;
border-radius: 12px;
z-index: 200;
cursor: pointer;
&:hover {
background-color: #90939981;
}
}
}
.amap-copyright, .amap-logo {
display: none !important;
}
.host-main-header__user-popover {
z-index: 9999 !important;
}
+40
View File
@@ -0,0 +1,40 @@
// 暗黑模式
@forward 'element-plus/theme-chalk/src/dark/var.scss' with (
$bg-color: (
'': #3d3d3d,
'overlay': #313131
),
$colors: (
'primary': (
'base': #2c6af3
)
)
);
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;
html.dark {
.el-dialog {
--el-dialog-bg-color: #333;
}
.el-table {
--el-table-header-text-color: #fff;
}
.host-main-body__aside .el-sub-menu {
background: #333;
}
}
.el-popper.is-dark {
background-color: #061a3c;
border-color: rgba(23, 154, 255, 0.4);
}
.el-popper.is-dark .el-popper__arrow::before {
background-color: #061a3c;
border-color: rgba(23, 154, 255, 0.4);
}
+107
View File
@@ -0,0 +1,107 @@
// @style/element.style.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #2c6af3
)
),
$fill-color: (
'light': #f4f7ff
),
$bg-color: (
'': '#f5f7fb',
'overlay': '#fff'
),
$dialog: (
'bg-color': '#fff'
)
);
// 导入所有样式:
@use 'element-plus/theme-chalk/src/index.scss' as *;
.el-dialog {
max-width: 90%;
}
.el-popover.el-popper {
min-width: 80px;
}
.el-table__body-wrapper tr td.el-table-fixed-column--left,
.el-table__body-wrapper tr td.el-table-fixed-column--right,
.el-table__body-wrapper tr th.el-table-fixed-column--left,
.el-table__body-wrapper tr th.el-table-fixed-column--right,
.el-table__footer-wrapper tr td.el-table-fixed-column--left,
.el-table__footer-wrapper tr td.el-table-fixed-column--right,
.el-table__footer-wrapper tr th.el-table-fixed-column--left,
.el-table__footer-wrapper tr th.el-table-fixed-column--right {
background-color: var(--el-bg-color-overlay);
}
.el-table {
--el-table-header-text-color: rgba(96, 98, 102, 1);
}
html,
body {
--system-margin-padding-width: 8px;
.el-card__body {
padding: var(--system-margin-padding-width);
}
.gci-search .el-form-item {
margin-bottom: var(--system-margin-padding-width);
}
}
.gci-search.gci-search-button__no-margin .el-button {
margin-bottom: 0;
}
.gci-search > .el-card__body {
padding-bottom: 0px;
}
.gci-search.el-card {
margin-bottom: var(--system-margin-padding-width);
}
.gci-search .el-button {
margin-bottom: var(--system-margin-padding-width);
}
.data-table-grid-header {
margin-bottom: var(--system-margin-padding-width);
}
.el-dialog__body {
overflow-x: hidden;
}
.el-input-number__decrease,
.el-input-number__increase {
background: #fafafa;
}
.el-popconfirm__action .el-button.is-text {
border: 1px solid #dcdfe6;
}
.host-main-body__aside .el-sub-menu {
background: #fff;
}
.el-button.is-link.is-disabled {
color: #ced1d8;
}
.el-table thead.is-group th.el-table__cell {
background: var(--el-table-header-bg-color);
}
.el-message {
white-space: pre;
align-items: flex-start;
line-height: 1em;
}
+108
View File
@@ -0,0 +1,108 @@
<template>
<el-tree
v-loading="pending"
:data="treeData"
default-expand-all
highlight-current
:props="defaultProps"
@current-change="handleNodeClick"
>
<template #default="{ data }">
<img
v-if="typeof data.isOnline === 'number'"
:src="
data.alarmStatus === 1
? carStateImageMap[2]
: carStateImageMap[data.isOnline]
"
class="mr-1"
alt="car_state_logo"
/>
<div
class="flex w-full"
:class="{
'text-[#1D2129]': data.children instanceof Array,
'text-[#175EFF]': nowSelectNode && nowSelectNode === data.plateNumber
}"
>
<div>{{ data.plateNumber || data.projectName || '-' }}</div>
<div
v-if="data.children && data.children instanceof Array"
class="ml-auto text-[#888] mr-1"
>
{{ data.children.filter(item => item.isOnline === 1).length }}/{{
data.children.length
}}
</div>
</div>
</template>
</el-tree>
</template>
<script lang="ts" setup>
import useRequest from '@/hooks/useRequest';
import { getCarMonitorTree } from '@/services/api/vehicleMonitoring/monitoring/index';
import { getImages } from '@/utils/utils';
import { computed, watch } from 'vue';
const carStateImageMap = {
0: getImages('logo_car_state_0', 'svg'),
1: getImages('logo_car_state_1', 'svg'),
2: getImages('logo_car_state_2', 'svg')
};
const props = defineProps<{
modelValue: string | undefined;
plateNumber?: string;
}>();
const Emits = defineEmits(['update:modelValue', 'change']);
const nowSelectNode = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const handleNodeClick = (data: any) => {
if (data.children instanceof Array) {
nowSelectNode.value = undefined;
} else {
nowSelectNode.value = data.plateNumber || '';
Emits('change', data.plateNumber);
}
};
const defaultProps = {
children: 'children',
label: 'plateNumber'
};
// 树形图
const {
pending,
data: treeData,
run: getGlobalTreeData
} = useRequest(
async () => {
const response = await getCarMonitorTree(props.plateNumber);
return response.data.result || [];
},
{
initialData: []
}
);
watch(
() => props.plateNumber,
() => {
getGlobalTreeData();
},
{
immediate: true
}
);
</script>
+52
View File
@@ -0,0 +1,52 @@
<script setup lang="ts" name="CarSelect">
import CarTree from './CarTree.vue';
import { computed, ref } from 'vue';
const props = defineProps<{
modelValue: string | undefined;
}>();
const Emits = defineEmits(['update:modelValue', 'confirm', 'cancel']);
const inputRef = ref();
const selectCar = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const searchValue = ref(props.modelValue);
const popoverRef = ref();
const changeSearchValue = (value: string) => {
searchValue.value = value;
popoverRef.value.hide();
};
</script>
<template>
<el-popover
ref="popoverRef"
placement="bottom"
:width="310"
trigger="click"
:show-arrow="false"
>
<template #reference>
<el-input
ref="inputRef"
v-model="searchValue"
placeholder="请输入车牌号"
suffix-icon="Search"
></el-input>
</template>
<CarTree
v-model="selectCar"
:plate-number="searchValue"
@change="changeSearchValue"
></CarTree>
</el-popover>
</template>
+180
View File
@@ -0,0 +1,180 @@
<template>
<CustomDialog
v-model="dialogVisible"
destroy-on-close
:width="Number(width)"
:top="top"
class="check-dialog"
:show-footer="false"
:title="checkOptions.title"
@cancel="handleClose"
>
<template #header="{ close, titleId, titleClass }">
<div class="flex w-full items-center">
<div :id="titleId" class="text-sm font-bold" :class="titleClass">
查看{{ checkOptions.title }}
</div>
<div class="ml-auto cursor-pointer" @click="close">
<el-icon><Close /></el-icon>
</div>
</div>
</template>
<template v-for="(option, index) in checkOptions.data" :key="index">
<div class="group">
<div v-show="option.childTitle" class="group-title">
{{ option.childTitle }}
</div>
<el-row>
<el-col
v-for="(item, idx) in option.childData"
:key="idx"
:span="item.span"
class="item"
>
<template v-if="item.slotName" #default>
<slot :name="item.slotName" :value="form"></slot>
</template>
<div>
<span class="item-title">{{ item.label }}:</span>
<span
v-if="form[item.prop] !== null && form[item.prop] !== ''"
class="item-content"
>{{ translateFn(form[item.prop], item.translate) }}</span
>
<span v-else class="item-content">/</span>
</div>
</el-col>
</el-row>
</div>
</template>
<slot name="default" :value="form"></slot>
</CustomDialog>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import CustomDialog from '@/components/CustomDialog/index.vue';
// import cloneDeep from 'lodash/cloneDeep';
const props = withDefaults(
defineProps<{
checkOptions: any; // 表单配置
checkData: any; // 表单字段
width?: number | string;
top?: string;
}>(),
{
checkOptions: {},
checkData: {},
width: '53%',
top: '15vh'
}
);
let form = reactive(props.checkData);
// 控制弹窗显隐
const dialogVisible = ref(false);
const openDialog = (type: string) => {
dialogVisible.value = true;
};
// 关闭后调用
const handleClose = () => {
dialogVisible.value = false;
emit('close');
};
// 翻译
const translateFn = (target, translate) => {
if (translate) {
return translate.fn(target, translate.options);
} else return target;
};
onMounted(() => {
// console.log(form);
});
const emit = defineEmits(['close']);
// 暴露方法和数据
defineExpose({ openDialog });
</script>
<script lang="ts">
export default {
name: 'Check'
};
</script>
<style lang="scss">
.check-dialog > .el-dialog__header {
margin-right: 0px;
padding: 12px 20px;
background: var(--el-dialog-header-bg-color);
overflow: hidden;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
}
.check-dialog > .el-dialog__footer {
margin-right: 0px;
padding: 12px 20px;
border-top: 1px solid var(--el-dialog-footer-border-color);
}
.check-dialog .el-dialog {
--el-dialog-header-bg-color: #f9f9f9;
--el-text-color-primary: #6c6c6c;
--el-dialog-footer-border-color: #e0e0e0;
}
html.dark {
.check-dialog .el-dialog {
--el-dialog-header-bg-color: #4a4a4a;
--el-text-color-primary: #dedede;
--el-dialog-footer-border-color: #7a7a7a;
}
}
.check-dialog {
.group {
.group-title {
width: 100%;
height: 22px;
font-size: 16px;
font-weight: 600;
color: #2c6af3;
line-height: 22px;
margin-left: 8px;
margin-bottom: 12px;
&::before {
content: '';
display: inline-block;
width: 4px;
height: 16px;
background: linear-gradient(180deg, #327dfe 0%, #2c6af3 100%);
border-radius: 2px;
position: relative;
left: -6px;
top: 2px;
}
}
}
.item {
margin-bottom: 8px;
}
.item-title {
height: 22px;
line-height: 22px;
// display: inline-block;
font-size: 14px;
font-weight: 400;
color: #888888;
}
.item-content {
height: 22px;
line-height: 22px;
// display: inline-block;
margin-left: 4px;
font-size: 14px;
font-weight: 400;
color: #000;
word-break: break-all;
}
}
</style>
+170
View File
@@ -0,0 +1,170 @@
<script lang="ts" setup name="CustomDialog">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: Boolean,
required: true
},
title: {
type: String,
default: '弹窗'
},
destroyOnClose: {
type: Boolean,
default: true
},
cancelText: {
type: String,
default: '取 消'
},
confirmText: {
type: String,
default: '确 认'
},
loading: {
type: Boolean,
default: false
},
width: {
type: Number,
default: 700
},
disabled: {
type: Boolean,
default: false
},
showFooter: {
type: Boolean,
default: true
},
noPadding: {
type: Boolean,
default: false
},
noPaddingTop: {
type: Boolean,
default: false
},
draggable: {
type: Boolean,
default: false
},
closeOnClickModal: {
type: Boolean,
default: true
}
});
const Emits = defineEmits(['update:modelValue', 'confirm', 'cancel']);
const dialogVisible = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const confirmClick = () => {
Emits('confirm');
};
const cancelClick = () => {
Emits('cancel');
dialogVisible.value = false;
};
</script>
<template>
<el-dialog
v-model="dialogVisible"
:draggable="draggable"
:destroy-on-close="destroyOnClose"
:width="width"
:append-to-body="true"
:lock-scroll="false"
class="gci-component__custom-dialog"
:class="{
'gci-component__custom-dialog-nopadding': noPadding,
'gci-component__custom-dialog-nopaddingTop': noPaddingTop
}"
:before-close="cancelClick"
:show-close="false"
:close-on-click-modal="false"
style="--el-dialog-title-font-size: 1rem; --el-dialog-border-radius: 8px"
>
<template #header="{ close, titleId, titleClass }">
<div class="flex w-full items-center">
<div :id="titleId" class="text-sm font-bold" :class="titleClass">
{{ title }}
</div>
<div class="ml-auto cursor-pointer" @click="close">
<el-icon><Close /></el-icon>
</div>
</div>
</template>
<slot></slot>
<template v-if="showFooter" #footer>
<slot name="footer">
<div class="flex items-center">
<div class="mr-auto">
<slot name="footer-suffix"></slot>
</div>
<span class="dialog-footer">
<el-button @click="cancelClick">{{ cancelText }}</el-button>
<el-button
type="primary"
:loading="loading"
:disabled="disabled"
@click="confirmClick"
>{{ confirmText }}</el-button
>
</span>
</div>
</slot>
</template>
</el-dialog>
</template>
<style lang="scss">
.gci-component__custom-dialog > .el-dialog__header {
margin-right: 0px;
padding: 12px 20px;
background: var(--el-dialog-header-bg-color);
overflow: hidden;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
}
.gci-component__custom-dialog > .el-dialog__footer {
margin-right: 0px;
padding: 12px 20px;
border-top: 1px solid var(--el-dialog-footer-border-color);
}
.gci-component__custom-dialog.el-dialog {
--el-dialog-header-bg-color: #f9f9f9;
--el-text-color-primary: rgba(96, 98, 102, 1);
--el-dialog-footer-border-color: #e0e0e0;
}
html.dark {
.gci-component__custom-dialog.el-dialog {
--el-dialog-header-bg-color: #4a4a4a;
--el-text-color-primary: #dedede;
--el-dialog-footer-border-color: #7a7a7a;
}
}
.gci-component__custom-dialog-nopadding .el-dialog__body {
padding: 0;
}
.gci-component__custom-dialog-nopaddingTop .el-dialog__body {
padding-top: 0;
}
</style>
+69
View File
@@ -0,0 +1,69 @@
<script setup lang="ts" name="UserSelect">
import useRequest from '@/hooks/useRequest';
import { getCustomerSelectList } from '@/services/api/base/ClientManage/index.ts';
import { computed } from 'vue';
/**
* 客户选择下拉
*/
const props = defineProps<{
modelValue: Array<string> | string | number | undefined;
placeholder?: string;
multiple?: boolean;
}>();
const Emits = defineEmits(['update:modelValue']);
const selectedValue = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
// const queryParams = reactive({
// project: undefined
// });
const {
data: customerOptions,
pending,
run: getCustomerOptions
} = useRequest(
async () => {
const response = await getCustomerSelectList();
return response.data.result.records || [];
},
{
initialData: []
}
);
getCustomerOptions();
</script>
<template>
<el-select
v-model="selectedValue"
style="width: 100%"
clearable
:multiple="multiple"
filterable
autocomplete="off"
:placeholder="placeholder || '请选择'"
:loading="pending"
v-bind="$attrs"
>
<el-option
v-for="item in customerOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</template>
+553
View File
@@ -0,0 +1,553 @@
<script setup lang="ts">
import DictSelect from '@/components/DictSelect/index.vue';
import DictCheckbox from '@/components/DictCheckBox/index.vue';
import DictRadio from '@/components/DictRadio/index.vue';
import RemoteSelect from '@/components/RemoteSelect/index.vue';
import UploadImage from '@/components/UploadImage/index.vue';
import UploadFiles from '@/components/UploadFiles/index.vue';
import type { FormGridConfig } from './types';
import { computed, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
const props = defineProps<{
modelValue: any;
configs: Array<FormGridConfig>;
labelWidth?: string;
labelPosition?: 'left' | 'right' | 'top';
disabled?: boolean;
formName?: string;
type?: 'text' | 'form';
}>();
const Emits = defineEmits(['update:modelValue']);
const formValue = computed({
get: () => {
return props.modelValue || {};
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const rules = ref({} as any);
const formRef = ref();
/**
* 选择类型
*/
const selectType = [
'select',
'remoteSelect',
'dictSelect',
'dictCheckbox',
'dictRadio',
'checkbox',
'radio',
'date',
'datetime',
'datetimerange',
'daterange',
'year',
'yearrange',
'month',
'monthrange',
'week',
'weekrange'
];
watch(
() => props.configs,
newConfigs => {
rules.value = {};
newConfigs.forEach(item => {
if (item.required) {
rules.value[item.key] = item.rules || {
required: true,
message:
item.requiredText ||
item.placeholder ||
(item.type && selectType.includes(item.type)
? '请选择'
: '请输入') + item.name,
trigger: ['blur', 'change']
};
}
});
if (formRef.value) {
// formRef.value.resetFields();
formRef.value.clearValidate();
}
},
{
immediate: true,
deep: true
}
);
const formValidate = () => {
return new Promise((resolve, reject) => {
if (typeof formValue.value === 'object') {
for (const key in formValue.value) {
if (typeof formValue.value[key] === 'string') {
formValue.value[key] = formValue.value[key].trim();
}
}
}
formRef.value?.validate((valid, fields) => {
if (valid) {
resolve(formValue);
} else {
console.error(valid, fields);
if (typeof fields === 'object') {
const errorMessages = [] as Array<string>;
for (const key in fields) {
if (Object.prototype.hasOwnProperty.call(fields, key)) {
const rules = fields[key];
if (rules instanceof Array) {
rules.forEach(rule => {
errorMessages.push(rule.message);
});
}
}
}
ElMessage.warning(errorMessages.join('\n'));
} else {
ElMessage.warning({
message: '请完善' + (props.formName || '') + '表单'
});
}
reject(valid);
}
});
});
};
/**
* 时间类型
*/
const timeType = [
'date',
'datetime',
'daterange',
'year',
'month',
'week',
'datetimerange'
];
/**
* 时间格式化
*/
const timeFormat = {
date: 'YYYY-MM-DD',
datetime: 'YYYY-MM-DD HH:mm:ss',
daterange: 'YYYY-MM-DD',
year: 'YYYY',
month: 'MM',
datetimerange: 'YYYY-MM-DD HH:mm:ss'
};
const formReset = () => {
formRef.value?.resetFields();
};
const formItemRefs = {} as any;
const setFormItemRef = (itemKey, itemRef) => {
formItemRefs[itemKey] = itemRef;
};
/**
* 重新校验表单
* @param itemKey
*/
const reValidateKey = (itemKey: string) => {
if (formItemRefs[itemKey]) {
formItemRefs[itemKey].clearValidate();
}
};
/**
* 字典类型组件
*/
const dictTextType = [
'select',
'remoteSelect',
'dictSelect',
'dictCheckbox',
'dictRadio'
];
const localOptionType = ['select', 'radio', 'checkbox'];
/**
* 特殊组件
*/
const ignoreType = ['uploadImage', 'uploadFiles'];
defineExpose({
formValidate,
formReset
});
</script>
<template>
<el-form
ref="formRef"
:model="formValue"
:rules="rules"
:label-width="labelWidth"
:label-position="labelPosition"
:class="{
'data-form-grid__text': type === 'text'
}"
scroll-to-error
:validate-on-rule-change="false"
@submit.prevent
>
<el-row :gutter="24">
<el-col
v-for="item in configs"
:key="item.key"
v-auth="item.auth"
:sm="24"
:md="
item.mdSpan ||
(item.type === 'title'
? 24
: item.span && item.span > 12
? item.span
: 12)
"
:lg="item.span || 24"
>
<template v-if="!item.hidden">
<template v-if="item.type === 'null' || item.type === 'slot'">
<div v-auth="item.auth">
<slot v-if="item.type === 'slot'" :name="item.key"></slot>
</div>
</template>
<template v-else-if="item.type === 'title'">
<div
v-auth="item.auth"
class="mb-4 text-primary-light-color font-bold flex items-center"
:style="{
marginBottom:
type === 'text' || item.disabledText ? '8px' : undefined
}"
>
{{ item.name }}
<slot
v-if="item.options && item.options[0].hasSuffix"
:name="item.key"
></slot>
</div>
</template>
<el-form-item
v-else
:ref="el => setFormItemRef(item.key, el)"
v-auth="item.auth"
:label="item.name + ''"
:prop="item.key"
:class="{
'data-form-grid__text-item': type === 'text' || item.disabledText
}"
:label-width="item.labelWidth"
>
<slot :name="item.key" :value-key="item.key">
<template
v-if="
(type !== 'text' && !item.disabledText) ||
ignoreType.includes(item.type as any)
"
>
<template v-if="item.type === 'checkbox' && item.options">
<el-checkbox-group
v-model="formValue[item.key]"
v-auth="item.auth"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
>
<el-checkbox
v-for="option in item.options"
:key="option.value"
:label="option.value"
>{{ option.label }}</el-checkbox
>
</el-checkbox-group>
</template>
<template v-else-if="item.type === 'radio' && item.options">
<el-radio-group
v-model="formValue[item.key]"
v-auth="item.auth"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
>
<el-radio
v-for="option in item.options"
:key="option.value"
:label="option.value"
>{{ option.label }}</el-radio
>
</el-radio-group>
</template>
<template
v-else-if="
item.type === 'dictRadio' &&
item.options &&
item.options[0].dictCode
"
>
<DictRadio
v-model="formValue[item.key]"
v-auth="item.auth"
:dict-code="item.options[0].dictCode"
:text-key="item.options[0].label"
:value-key="item.options[0].value"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
></DictRadio>
</template>
<template
v-else-if="
item.type === 'dictCheckbox' &&
item.options &&
item.options[0].dictCode
"
>
<DictCheckbox
v-model="formValue[item.key]"
v-auth="item.auth"
:dict-code="item.options[0].dictCode"
:text-key="item.options[0].label"
:value-key="item.options[0].value"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
></DictCheckbox>
</template>
<template v-else-if="item.type === 'select' && item.options">
<el-select
v-model="formValue[item.key]"
v-auth="item.auth"
:placeholder="
item.placeholder ? item.placeholder : '请选择' + item.name
"
class="w-full"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
>
<el-option
v-for="option in item.options"
:key="option.value"
:label="option.label"
:value="option.value"
></el-option>
</el-select>
</template>
<template
v-else-if="
item.type === 'dictSelect' &&
item.options &&
item.options[0].dictCode
"
>
<DictSelect
v-model="formValue[item.key]"
v-auth="item.auth"
:dict-code="item.options[0].dictCode"
:text-key="item.options[0].label"
:value-key="item.options[0].value"
:disabled="disabled || item.disabledText"
:placeholder="
item.placeholder ? item.placeholder : '请选择' + item.name
"
class="w-full"
clearable
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
></DictSelect>
</template>
<template
v-else-if="item.type === 'remoteSelect' && item.options"
>
<RemoteSelect
v-model="formValue[item.key]"
v-auth="item.auth"
:url="item.options[0].url"
:option-label="item.options[0].label"
:option-value="item.options[0].value"
:placeholder="
item.placeholder ? item.placeholder : '请选择' + item.name
"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
></RemoteSelect>
</template>
<template
v-else-if="item.type === 'uploadImage' && item.options"
>
<UploadImage
v-model="formValue[item.key]"
v-auth="item.auth"
:url="item.options[0].url || ''"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
@change="reValidateKey(item.key)"
v-on="item.eventOptions || {}"
></UploadImage>
</template>
<template
v-else-if="item.type === 'uploadFiles' && item.options"
>
<UploadFiles
v-model="formValue[item.key]"
v-auth="item.auth"
:url="item.options[0].url || ''"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
:accept="item.accept || ''"
@change="reValidateKey(item.key)"
v-on="item.eventOptions || {}"
></UploadFiles>
</template>
<template v-else-if="item.type && timeType.includes(item.type)">
<el-date-picker
v-model="formValue[item.key]"
v-auth="item.auth"
:type="item.type"
class="w-full"
:placeholder="
item.placeholder ? item.placeholder : '请选择日期'
"
:format="timeFormat[item.type] || 'YYYY-MM-DD'"
:value-format="timeFormat[item.type] || 'YYYY-MM-DD'"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
/>
</template>
<template v-else-if="item.type === 'textarea'">
<el-input
v-model="formValue[item.key]"
v-auth="item.auth"
type="textarea"
:placeholder="
item.placeholder ? item.placeholder : '请输入' + item.name
"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
></el-input>
</template>
<template v-else-if="item.type === 'numberInput'">
<el-input
v-model.number="formValue[item.key]"
v-auth="item.auth"
type="number"
style="width: 100%"
:placeholder="
item.placeholder ? item.placeholder : '请输入' + item.name
"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
></el-input>
</template>
<el-input
v-else
v-model="formValue[item.key]"
v-auth="item.auth"
:type="item.type !== 'input' ? item.type : undefined"
:placeholder="
item.placeholder ? item.placeholder : '请输入' + item.name
"
:disabled="disabled || item.disabledText"
v-bind="item.componentOptions"
v-on="item.eventOptions || {}"
></el-input>
</template>
<template v-else>
<div
v-if="dictTextType.includes(item.type as any)"
v-auth="item.auth"
class="text-[#888]"
style="white-space: break-spaces"
>
{{
typeof formValue[
item.dictTextName
? item.dictTextName
: `${item.key}_dictText`
] === 'undefined' || formValue[item.key] === null
? '-'
: formValue[
item.dictTextName
? item.dictTextName
: `${item.key}_dictText`
]
}}
</div>
<div
v-else-if="
item.type &&
localOptionType.includes(item.type) &&
item.options
"
>
{{
typeof formValue[item.key] === 'undefined'
? '-'
: item.options.find(opItem => {
return opItem.value === formValue[item.key];
})?.label
}}
</div>
<div
v-else
v-auth="item.auth"
class="text-[#888]"
style="white-space: break-spaces"
>
{{
typeof formValue[item.key] === 'undefined' ||
formValue[item.key] === null
? '-'
: formValue[item.key] +
(item.componentOptions &&
item.componentOptions.suffixIcon
? ' ' + item.componentOptions.suffixIcon.render()
: '')
}}
</div>
</template>
</slot>
</el-form-item>
</template>
</el-col>
</el-row>
</el-form>
</template>
<style scoped>
:deep(.el-date-editor.el-input) {
width: 100%;
}
.data-form-grid__text :deep(.el-form-item) {
margin-bottom: 8px;
}
.data-form-grid__text-item {
margin-bottom: 8px;
}
</style>
+56
View File
@@ -0,0 +1,56 @@
export interface FormGridConfig {
required?: boolean; // 是否必填
requiredText?: string; // 必填校验显示字符
name: string; // 显示名称
key: string; // 取值key
span?: number; // 栅格占据
mdSpan?: number;
type?: // 类型
| 'select'
| 'remoteSelect'
| 'dictSelect'
| 'dictCheckbox'
| 'dictRadio'
| 'checkbox'
| 'radio'
| 'input'
| 'numberInput'
| 'uploadImage'
| 'uploadFiles'
| 'textarea'
| 'date'
| 'datetime'
| 'datetimerange'
| 'daterange'
| 'year'
| 'yearrange'
| 'month'
| 'monthrange'
| 'week'
| 'weekrange'
| 'title'
| 'null'
| 'itemSlot'
| 'slot';
placeholder?: string; // 悬浮显示内容
labelWidth?: string; // 显示名称宽度
disabledText?: boolean;
dictTextName?: string; // 查看禁用文本取值
hidden?: boolean;
textFormatter?: (data) => string;
options?: Array<{
// 选项,字典类型则取第一个option
label?: string;
value?: any;
dictCode?: string;
url?: string;
hasSuffix?: boolean;
}>;
rules?: any;
auth?: { action: string; type?: any; description?: string } | string;
componentOptions?: any;
eventOptions?: any;
accept?: string;
}
export default FormGridConfig;
+85
View File
@@ -0,0 +1,85 @@
<template>
<div>
<el-pagination
v-model:current-page="data_currentPage"
v-model:page-size="data_pageSize"
:page-sizes="[5, 10, 20, 50, 100, 200, 500]"
:small="small"
:pager-count="5"
:disabled="disabled"
:background="background"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script lang="ts" name="DataPagination" setup>
import { computed } from 'vue';
const props = defineProps({
// 当前页
currentPage: {
type: Number,
default: 1
},
// 每页占据数量
pageSize: {
type: Number,
default: 10
},
// 是否为 small 样式
small: {
type: Boolean,
default: false
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否有深色背景
background: {
type: Boolean,
default: true
},
// 总计数目
total: {
type: Number,
default: 0
}
});
// 触发方法返给上一级
const Emits = defineEmits([
'size-change',
'current-change',
'update:currentPage',
'update:pageSize'
]);
const handleSizeChange = (val: number) => {
Emits('size-change', val);
};
const handleCurrentChange = (val: number) => {
Emits('current-change', val);
};
// 监听数据 - 以支持 v-model
const data_currentPage = computed({
get: () => props.currentPage,
set: val => {
Emits('update:currentPage', val);
}
});
const data_pageSize = computed({
get: () => props.pageSize,
set: val => {
Emits('update:pageSize', val);
}
});
</script>
<style lang="scss" scoped></style>
+260
View File
@@ -0,0 +1,260 @@
<script lang="ts" setup name="DataTable">
import { nextTick, ref, watch } from 'vue';
import { ColumnType, SpanMethodProps, CellMethodProps } from './types';
import cloneDeep from 'lodash/cloneDeep';
/**
* DataTable -- 基于 element-plus 封装
* 2023年8月23日
*
* @author Charles Chan 陈裕峰
*/
const props = withDefaults(
defineProps<{
data?: Array<any>;
columns?: Array<ColumnType>;
height?: string | number;
maxHeight?: string | number;
rowKey?: string | ((row: any) => any);
treeOptions?: {
defaultExpandAll: boolean;
lazy?: boolean;
load?: (row: any, treeNode: any, resolve: any) => void;
props?: {
hasChildren: string;
children: string;
};
};
defaultSelections?: Array<any>;
stripe?: boolean;
spanMethod?: (spanArgs: SpanMethodProps) => any;
noMutiple?: boolean;
tableAttrs?: any;
cellMouseEnter?: (spanArgs: CellMethodProps) => any;
cellMouseLeave?: (spanArgs: CellMethodProps) => any;
rowClassName?: (row: any, rowIndex: number) => any;
eventMethods?: any;
emptyStyle?: any;
}>(),
{
data: () => [],
columns: () => [],
height: undefined,
rowKey: undefined,
maxHeight: '100vh',
treeOptions: () => ({
defaultExpandAll: false
}),
defaultSelections: () => [],
stripe: true,
spanMethod: undefined,
tableAttrs: () => ({}),
cellMouseEnter: undefined,
cellMouseLeave: undefined,
rowClassName: undefined,
eventMethods: () => ({}),
emptyStyle: undefined
}
);
const tableRef = ref();
const keySelections = ref(cloneDeep(props.defaultSelections) as Array<any>);
const totalKeySelections = ref([] as Array<any>);
const resetKeySelection = () => {
totalKeySelections.value = [];
keySelections.value = cloneDeep(props.defaultSelections) as Array<any>;
if (typeof props.rowKey === 'function') {
props.data.forEach(item => {
const keyValueFunction = props.rowKey as any;
if (keySelections.value.includes(keyValueFunction(item))) {
tableRef.value.toggleRowSelection(item, true);
}
});
}
};
const Emits = defineEmits([
'selection-change',
'key-selection-change',
'page-selection-change'
]);
// 选择项改变时,调用方法
const emitSelectionChange = (selection: Array<any>) => {
Emits('selection-change', selection);
};
const selectChange = (selection, row) => {
if (typeof props.rowKey === 'function') {
const keyValueFunction = props.rowKey;
const selectState = selection.includes(row);
if (selectState && !keySelections.value.includes(keyValueFunction(row))) {
if (props.noMutiple) {
tableRef.value.clearSelection();
keySelections.value = [keyValueFunction(row)];
totalKeySelections.value = [cloneDeep(row)];
tableRef.value.toggleRowSelection(row, true);
return;
}
keySelections.value.push(keyValueFunction(row));
totalKeySelections.value.push(cloneDeep(row));
} else if (
!selectState &&
keySelections.value.includes(keyValueFunction(row))
) {
if (props.noMutiple) {
tableRef.value.clearSelection();
keySelections.value = [];
totalKeySelections.value = [];
return;
}
keySelections.value.splice(
keySelections.value.indexOf(keyValueFunction(row)),
1
);
totalKeySelections.value.splice(
totalKeySelections.value.findIndex(
item => keyValueFunction(item) === keyValueFunction(row)
),
1
);
}
}
};
const allSelectChange = selection => {
if (props.noMutiple && typeof props.rowKey === 'function') {
tableRef.value.clearSelection();
keySelections.value = [];
totalKeySelections.value = [];
return;
}
if (props.data instanceof Array) {
props.data.forEach(row => {
selectChange(selection, row);
});
}
};
watch(
() => props.data,
newValue => {
if (typeof props.rowKey === 'function') {
const keyValueFunction = props.rowKey;
const nowPageSelections = newValue.filter(item =>
keySelections.value.includes(keyValueFunction(item))
);
nextTick(() => {
nowPageSelections.forEach(item => {
tableRef.value.toggleRowSelection(item, true);
});
});
}
},
{
immediate: true,
deep: true
}
);
watch(
() => keySelections.value,
newValue => {
Emits('key-selection-change', newValue);
Emits('page-selection-change', totalKeySelections.value);
},
{
immediate: true,
deep: true
}
);
defineExpose({
tableRef,
keySelections,
totalKeySelections,
resetKeySelection
});
</script>
<template>
<div>
<el-table
ref="tableRef"
:data="data"
:height="height"
:max-height="maxHeight"
scrollbar-always-on
:stripe="stripe"
:default-expand-all="treeOptions.defaultExpandAll"
:row-key="rowKey"
:lazy="treeOptions.lazy"
:load="treeOptions.load"
:span-method="spanMethod"
:tree-props="treeOptions.props"
v-bind="tableAttrs"
:row-class-name="rowClassName"
@select-all="allSelectChange"
@select="selectChange"
@selection-change="emitSelectionChange"
@cell-mouse-enter="cellMouseEnter"
@cell-mouse-leave="cellMouseLeave"
v-on="eventMethods || {}"
>
<el-table-column
v-for="item in columns"
:key="item.prop"
:fixed="item.fixed"
:type="item.type"
:show-overflow-tooltip="!item.slotName"
:property="item.prop"
:label="item.name"
:min-width="item.type === 'selection' ? undefined : item.width || 120"
:width="
item.type === 'selection' ? 50 : item.fixed ? item.width : undefined
"
:align="item.align || 'center'"
:render-header="item.renderHeader"
:formatter="item.formatter"
:selectable="item.selectable"
:sortable="item.sortable"
v-bind="item.columnOptions"
v-on="item.eventOptions || {}"
>
<template v-if="item.slotName" #default="scope">
<slot
:name="item.slotName"
:row="scope.row"
:index="scope.$index"
></slot>
</template>
</el-table-column>
<template #empty>
<slot name="empty">
<el-empty :style="emptyStyle" description="没有发现相关数据" />
</slot>
</template>
<slot></slot>
</el-table>
</div>
</template>
<style lang="scss" scoped>
// 合并单元格的背景颜色
.el-table {
:deep(.el-table__body .el-table__row.hover-bg td) {
background-color: #f5f7fb;
}
}
</style>
+29
View File
@@ -0,0 +1,29 @@
export interface ColumnType {
prop: string; // 表头返回字段参数
name?: string; // 表头显示名称
type?: string; // 可以设置为 selection
width?: number | string; // 宽度占比
fixed?: boolean | string; // 固定位置
align?: string; // 文本居中布局
slotName?: string; // 添加插槽名自定义
formatter?: (row?: any, column?: any, cellValue?: any, index?: number) => any; // 格式化显示数据
renderHeader?: (column?: any) => any; // 表头自定义渲染
selectable?: (row?: any, index?: number) => any;
sortable?: boolean | 'custom';
columnOptions?: any;
eventOptions?: any;
}
export interface SpanMethodProps {
row: any;
column: any;
rowIndex: number;
columnIndex: number;
}
export interface CellMethodProps {
row: any;
column: any;
cell: any;
event: any;
}
+320
View File
@@ -0,0 +1,320 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import {
ColumnType,
SpanMethodProps,
CellMethodProps
} from '@/components/DataTable/types';
import useRequest from '@/hooks/useRequest';
import DataPagination from '@/components/DataPagination/index.vue';
import DataTable from '@/components/DataTable/index.vue';
import { getTableData } from '@/services/api/table/index';
const dataTableRef = ref();
const props = withDefaults(
defineProps<{
url?: string; // 接口
data?: Array<any>;
loading?: boolean;
columns?: Array<ColumnType>; // 表格设置
queryParams?: any; // 接口额外请求参数
keepAliveParams?: any; // 默认参数
height?: string | number; // 高度
maxHeight?: string | number; // 最大高度
rowKey?: string | ((row: any) => any); // 列表唯一值,tree和保留分页多选时候必传
treeOptions?: {
//树形选项
defaultExpandAll: boolean;
lazy?: boolean;
load?: (row: any, treeNode: any, resolve: any) => void;
props?: {
hasChildren: string;
children: string;
};
};
dataKey?: string;
defaultSelections?: Array<any>;
stripe?: boolean;
spanMethod?: (spanArgs: SpanMethodProps) => any;
noMutiple?: boolean;
hasFooter?: boolean;
tableAttrs?: any;
cellMouseEnter?: (spanArgs: CellMethodProps) => any;
cellMouseLeave?: (spanArgs: CellMethodProps) => any;
rowClassName?: (row: any, rowIndex: number) => any;
eventMethods?: any;
}>(),
{
url: undefined,
queryParams: {},
keepAliveParams: {},
columns: () => [],
height: undefined,
maxHeight: undefined,
stripe: true,
rowKey: undefined,
treeOptions: () => ({
defaultExpandAll: false
}),
dataKey: undefined,
data: undefined,
defaultSelections: () => [],
spanMethod: undefined,
hasFooter: true,
tableAttrs: () => ({}),
cellMouseEnter: undefined,
cellMouseLeave: undefined,
rowClassName: undefined,
eventMethods: undefined
}
);
const Emits = defineEmits([
'update:queryParams',
'selection-change',
'key-selection-change',
'page-selection-change',
'data-change',
'loading'
]);
// 请求参数分页传参
const paginations = reactive({
pageNo: 1,
pageSize: 10
});
export interface PageReponse {
size: number;
total: number;
pages: number;
current: number;
}
// 接口返回的页码设置
const pageSetting: PageReponse = reactive({
size: 10,
total: 1,
pages: 1,
current: 1
});
/**
* 列表数据请求
*/
const {
pending,
data: tableData,
run: searchUserList
} = useRequest(
async ({ pageNo = 1, pageSize = 10, ...queryParams }) => {
if (props.data instanceof Array) {
paginations.pageNo = pageNo;
paginations.pageSize = pageSize;
pageSetting.current = pageNo;
pageSetting.pages = props.data.length % pageSize;
pageSetting.total = props.data.length;
pageSetting.size = pageSize;
return props.data.slice(
(pageNo - 1) * pageSize,
(pageNo - 1) * pageSize + pageSize
);
} else if (typeof props.url === 'string') {
paginations.pageNo = pageNo;
paginations.pageSize = pageSize;
const response = await getTableData(props.url, {
pageNo,
pageSize,
...queryParams,
...props.keepAliveParams
});
Emits('update:queryParams');
pageSetting.current = response.data?.result.current || 1;
pageSetting.pages = response.data?.result.pages || 1;
pageSetting.total = response.data?.result.total || 0;
pageSetting.size = response.data?.result.size || paginations.pageSize;
return props.dataKey
? response.data?.[props.dataKey]
: response.data?.result.records;
}
},
{ initialData: [] }
);
watch(
() => props.data,
newValue => {
if (newValue instanceof Array) {
loadTableData();
}
},
{
deep: true
}
);
const slotNames = computed(
() =>
props.columns
.filter(item => typeof item.slotName === 'string')
.map(item => item.slotName) as Array<string>
);
/**
* 加载表单数据
*/
const loadTableData = () => {
searchUserList({
...paginations,
...props.queryParams
});
};
/**
* 重置表单搜索
*/
const resetTableData = () => {
paginations.pageNo = 1;
paginations.pageSize = 10;
searchUserList({
...paginations,
...props.queryParams
});
if (dataTableRef.value) {
resetKeySelection();
}
};
/**
* 重置表单选择框
*/
const resetKeySelection = () => {
dataTableRef.value.resetKeySelection();
};
/**
* 切换分页功能
* @param page
*/
const changeCurrentPage = (page: number) => {
paginations.pageNo = page;
loadTableData();
};
const changePageSize = (size: number) => {
paginations.pageSize = size;
paginations.pageNo = 1;
loadTableData();
};
/**
* 选择项变化
* @param selections
*/
const emitSelectionChange = (selections: Array<any>) => {
Emits('selection-change', selections);
};
loadTableData();
const keySelections = ref([] as Array<string>);
const totalKeySelections = ref([] as Array<any>);
const emitKeySelectionChange = newKeySelections => {
keySelections.value = newKeySelections || [];
Emits('key-selection-change', keySelections.value);
};
const emitPageSelectionChange = newPageSelections => {
totalKeySelections.value = newPageSelections || [];
Emits('page-selection-change', totalKeySelections.value);
};
watch(
() => pending.value,
newValue => {
Emits('loading', !!newValue);
}
);
watch(
() => tableData.value,
newValue => {
Emits('data-change', newValue);
},
{
immediate: true,
deep: true
}
);
defineExpose({
pending,
tableData,
loadTableData,
resetTableData,
keySelections,
totalKeySelections,
resetKeySelection
});
</script>
<template>
<div>
<header>
<slot name="header"></slot>
</header>
<section style="position: relative">
<DataTable
ref="dataTableRef"
v-loading="pending || loading"
:data="tableData"
:default-selections="defaultSelections"
:columns="columns"
:height="height"
:row-key="rowKey"
:no-mutiple="noMutiple"
:max-height="maxHeight"
:span-method="spanMethod"
:stripe="stripe"
:tree-options="treeOptions"
:table-attrs="tableAttrs"
:row-class-name="rowClassName"
:cell-mouse-enter="cellMouseEnter"
:cell-mouse-leave="cellMouseLeave"
:event-methods="eventMethods"
@key-selection-change="emitKeySelectionChange"
@page-selection-change="emitPageSelectionChange"
@selection-change="emitSelectionChange"
>
<template
v-for="item in slotNames"
:key="item"
#[item]="{ row, index }"
>
<slot :name="item" :row="row" :index="index"></slot>
</template>
</DataTable>
<slot name="middle"></slot>
</section>
<footer v-if="hasFooter">
<DataPagination
v-model:current-page="pageSetting.current"
v-model:page-size="pageSetting.size"
:total="pageSetting.total"
style="margin-top: 0.8em"
class="flex justify-end"
@current-change="changeCurrentPage"
@size-change="changePageSize"
></DataPagination>
</footer>
</div>
</template>
@@ -0,0 +1,63 @@
<script setup lang="ts" name="DepartmentTreeSelect">
import { computed, watch } from 'vue';
import { getDepartmentSubTrees } from '@/services/api/system/department';
import type { DepartmentTree } from '@/services/api/system/department/types';
import useRequest from '@/hooks/useRequest';
const props = defineProps<{
modelValue?: string | string[];
options?: DepartmentTree[];
orgCode?: string;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value?: string | string[]);
}>();
const modelValue = computed({
get: () => props.modelValue,
set: nv => {
emit('update:modelValue', nv);
}
});
const { data: departmentTrees, run: getDepartmentTrees } = useRequest(
async () => {
const response = await getDepartmentSubTrees(props.orgCode);
return response.data.result || [];
},
{ initialData: [] }
);
const options = computed(() => props.options ?? departmentTrees.value);
if (!props.options) {
getDepartmentTrees();
}
watch(
() => props.orgCode,
(newValue, oldValue) => {
if (newValue !== oldValue) {
getDepartmentTrees();
}
}
);
</script>
<template>
<el-tree-select
v-model="modelValue"
:data="options"
placeholder="请选择部门/机构"
:expand-on-click-node="false"
node-key="orgCode"
check-strictly
default-expand-all
:props="{
label: 'departName',
children: 'children'
}"
v-bind="$attrs"
/>
</template>
@@ -0,0 +1,54 @@
<script setup lang="ts" name="DepartmentTreeSelect">
import { computed } from 'vue';
import { getDepartmentTrees as getDepartmentTreesAPI } from '@/services/api/system/department';
import type { DepartmentTree } from '@/services/api/system/department/types';
import useRequest from '@/hooks/useRequest';
const props = defineProps<{
modelValue?: string | string[];
options?: DepartmentTree[];
orgCategory?: number;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value?: string | string[]);
}>();
const modelValue = computed({
get: () => props.modelValue,
set: nv => {
emit('update:modelValue', nv);
}
});
const { data: departmentTrees, run: getDepartmentTrees } = useRequest(
async () => {
const response = await getDepartmentTreesAPI(undefined, props.orgCategory);
return response.data.result || [];
},
{ initialData: [] }
);
const options = computed(() => props.options ?? departmentTrees.value);
if (!props.options) {
getDepartmentTrees();
}
</script>
<template>
<el-tree-select
v-model="modelValue"
:data="options"
placeholder="请选择部门/机构"
:expand-on-click-node="false"
node-key="id"
check-strictly
default-expand-all
:props="{
label: 'departName',
children: 'children'
}"
v-bind="$attrs"
/>
</template>
+113
View File
@@ -0,0 +1,113 @@
<script setup lang="ts" name="DialogFormGrid">
import CustomDialog from '@/components/CustomDialog/index.vue';
import DataFormGrid from '@/components/DataFormGrid/index.vue';
import type { FormGridConfig } from '@/components/DataFormGrid/types';
import { ElMessage } from 'element-plus';
import { computed, ref } from 'vue';
const formRef = ref();
const props = defineProps<{
type: 'new' | 'edit' | 'detail';
modelValue: boolean;
formValue: any;
configs: Array<FormGridConfig>;
submitApi: any;
width?: number;
title: string;
noMessage?: boolean;
}>();
const Emits = defineEmits(['update:modelValue', 'update:formValue', 'reload']);
const TitleMap = {
new: '新增',
edit: '编辑',
detail: '查看'
};
const dialogTitle = computed(() => TitleMap[props.type] + props.title);
const dialogVisible = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const formData = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.formValue;
},
set: nv => {
Emits('update:formValue', nv);
}
});
const isDisabled = computed(() => props.type === 'detail');
const loading = ref(false);
/**
* 提交表单
*/
const submitForm = async () => {
loading.value = true;
try {
await formRef.value.formValidate();
const response = await props.submitApi(formData.value);
Emits('reload');
if (!props.noMessage) {
ElMessage.success(response.data.message || '操作成功!');
}
loading.value = false;
dialogVisible.value = false;
} catch (error) {
loading.value = false;
console.error(error);
}
};
const formConfigSlots = computed(() => {
return props.configs.filter(
item =>
item.type === 'slot' ||
item.type === 'itemSlot' ||
(item.type === 'title' && item.options && item.options[0].hasSuffix)
);
});
const cancelSubmit = () => {
formData.value = {};
};
</script>
<template>
<CustomDialog
v-model="dialogVisible"
:title="dialogTitle"
:width="width"
destroy-on-close
:loading="loading"
:show-footer="type === 'detail' ? false : true"
@confirm="submitForm"
@cancel="cancelSubmit"
>
<slot></slot>
<DataFormGrid
ref="formRef"
v-model="formData"
v-bind="$attrs"
:configs="configs"
:type="type === 'detail' ? 'text' : 'form'"
:disabled="isDisabled"
>
<template v-for="configSlot in formConfigSlots" #[configSlot.key]>
<slot
:name="configSlot.key"
:value-key="configSlot.key"
:disabled="isDisabled"
:type="type"
></slot>
</template>
</DataFormGrid>
</CustomDialog>
</template>
+61
View File
@@ -0,0 +1,61 @@
<script setup lang="ts">
import { computed } from 'vue';
import useRequest from '@/hooks/useRequest';
import { getDiciByCode } from '@/services/api/system/dict/index';
const props = defineProps<{
dictCode: string; // 字典编码 或 表名
valueKey?: string; // 若dictCode为表名,则需传入返回option取得value 数据库表头key
textKey?: string; // 若dictCode为表名,则需传入返回option显示label 数据库表头key
filter?: string; // 若dictCode为表名,则可输入过滤方法,例如 username='admin'
modelValue: any;
disabled?: boolean; // 禁用
min?: number; // 最少勾选数量
max?: number; // 最多勾选数量
border?: boolean; // 是否显示边框
}>();
const Emits = defineEmits(['update:modelValue']);
const bindValue = computed({
get: () => {
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const { data: options, run: getSelectOptions } = useRequest(
async () => {
const response = await getDiciByCode(
`${props.dictCode}${props.textKey ? `,${props.textKey}` : ''}${
props.valueKey ? `,${props.valueKey}` : ''
}${props.filter ? `,${props.filter}` : ''}`
);
return response.data.result || [];
},
{
initialData: []
}
);
getSelectOptions();
</script>
<template>
<el-checkbox-group
v-model="bindValue"
:disabled="disabled"
:min="min"
:max="max"
>
<el-checkbox
v-for="item in options"
:key="item.value"
:label="item.value"
>{{ item.text }}</el-checkbox
>
</el-checkbox-group>
</template>
+57
View File
@@ -0,0 +1,57 @@
<script setup lang="ts">
import { computed } from 'vue';
import useRequest from '@/hooks/useRequest';
import { getDiciByCode } from '@/services/api/system/dict/index';
const props = defineProps<{
dictCode: string; // 字典编码 或 表名
valueKey?: string; // 若dictCode为表名,则需传入返回option取得value 数据库表头key
textKey?: string; // 若dictCode为表名,则需传入返回option显示label 数据库表头key
filter?: string; // 若dictCode为表名,则可输入过滤方法,例如 username='admin'
modelValue: any;
disabled?: boolean; // 禁用
border?: boolean; // 是否显示边框
}>();
const Emits = defineEmits(['update:modelValue']);
const bindValue = computed({
get: () => {
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const {
pending,
data: options,
run: getSelectOptions
} = useRequest(
async () => {
const response = await getDiciByCode(
`${props.dictCode}${props.textKey ? `,${props.textKey}` : ''}${
props.valueKey ? `,${props.valueKey}` : ''
}${props.filter ? `,${props.filter}` : ''}`
);
return response.data.result || [];
},
{
initialData: []
}
);
getSelectOptions();
</script>
<template>
<el-radio-group v-model="bindValue" v-loading="pending" :disabled="disabled">
<slot :options="options">
<el-radio v-for="item in options" :key="item.value" :label="item.value">{{
item.text
}}</el-radio>
</slot>
</el-radio-group>
</template>
+80
View File
@@ -0,0 +1,80 @@
<script setup lang="ts">
import { computed } from 'vue';
import useRequest from '@/hooks/useRequest';
import { getDiciByCode } from '@/services/api/system/dict/index';
const props = defineProps<{
dictCode: string; // 字典编码 或 表名
valueKey?: string; // 若dictCode为表名,则需传入返回option取得value 数据库表头key
textKey?: string; // 若dictCode为表名,则需传入返回option显示label 数据库表头key
filter?: string; // 若dictCode为表名,则可输入过滤方法,例如 username='admin'
modelValue: any;
placeholder?: string; // 悬浮提示
filterable?: boolean; // 可搜索
multiple?: boolean; // 多选
disabled?: boolean; // 禁用
filterMethod?: () => void; // 可自定义搜索方法
clearable?: boolean; // 可清空
}>();
const Emits = defineEmits(['update:modelValue']);
const bindValue = computed({
get: () => {
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const {
pending,
data: options,
run: getSelectOptions
} = useRequest(
async (forceReload = false) => {
const response = await getDiciByCode(
`${props.dictCode}${props.textKey ? `,${props.textKey}` : ''}${
props.valueKey ? `,${props.valueKey}` : ''
}${props.filter ? `,${props.filter}` : ''}`,
forceReload
);
return response.data.result || [];
},
{
initialData: []
}
);
getSelectOptions();
const forceReloadOptions = async () => {
await getSelectOptions(true);
};
defineExpose({
reload: forceReloadOptions
});
</script>
<template>
<el-select
v-model="bindValue"
:filterable="filterable"
:clearable="clearable"
:multiple="multiple"
:placeholder="placeholder"
:loading="pending"
:filter-method="filterMethod"
:disabled="disabled"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</template>
+445
View File
@@ -0,0 +1,445 @@
<template>
<CustomDialog
v-model="dialogVisible"
confirm-text=" "
:title="
title || `${formType == 'add' ? '新增' : '编辑'}${formOptions.title}`
"
destroy-on-close
:width="width"
:top="top"
:show-footer="showFooter"
:loading="loading"
class="form-dialog"
@cancel="handleClose"
@confirm="submitForm(formRef)"
>
<el-form ref="formRef" :model="form" label-position="right" @submit.prevent>
<template v-for="(option, index) in formOptions.data" :key="index">
<div class="group">
<div v-show="option.childTitle" class="group-title">
{{ option.childTitle }}
</div>
<el-row>
<el-col
v-for="(item, idx) in option.childData"
:key="idx"
:span="item.span"
>
<slot
v-if="item.type === 'slot' && item.slotName"
:name="item.slotName"
:value="form"
></slot>
<el-form-item
v-else
:name="item.prop"
:rules="item.rule"
:prop="item.prop"
:label-width="item.labelWidth || '110px'"
class="form-item"
>
<el-input
v-if="item.type == 'input'"
v-model="form[item.prop]"
autocomplete="off"
placeholder="请输入"
:disabled="item.disabled || disabledKeys.includes(item.prop)"
>
<template v-if="item.append" #append>{{
item.append
}}</template>
</el-input>
<el-input
v-if="item.type == 'password'"
v-model="form[item.prop]"
type="password"
autocomplete="off"
placeholder="请输入"
:disabled="item.disabled || disabledKeys.includes(item.prop)"
>
<template v-if="item.append" #append>{{
item.append
}}</template>
</el-input>
<el-input
v-else-if="item.type == 'number'"
v-model="form[item.prop]"
autocomplete="off"
placeholder="请输入"
:disabled="item.disabled || disabledKeys.includes(item.prop)"
:min="item.min"
:max="item.max"
@input="regStr(item.prop, item.integer)"
@blur="inputNumber(item.prop, item)"
>
<template v-if="item.append" #append>{{
item.append
}}</template>
</el-input>
<el-input
v-else-if="item.type == 'textarea'"
v-model="form[item.prop]"
:rows="item.options ? item.options.row : 2"
type="textarea"
:placeholder="
item.options ? item.options.placeholder : '请输入'
"
:maxlength="item.options ? item.options.maxlength : ''"
:show-word-limit="item.options?.maxlength ? true : false"
:disabled="item.disabled || disabledKeys.includes(item.prop)"
/>
<el-select
v-else-if="item.type === 'select'"
v-model="form[item.prop]"
placeholder="请选择"
style="width: 100%"
:multiple="item.multiple || false"
:collapse-tags="item.collapseTags || false"
:collapse-tags-tooltip="item.collapseTagsTooltip || false"
:max-collapse-tags="item.maxCollapseTags || 1"
filterable
clearable
:disabled="item.disabled || disabledKeys.includes(item.prop)"
>
<el-option
v-for="val in item.options"
:key="val[item.selectItem.value]"
:label="val[item.selectItem.title]"
:value="val[item.selectItem.value]"
/>
</el-select>
<el-date-picker
v-else-if="item.type === 'date' || item.type === 'datetime'"
v-model="form[item.prop]"
:type="item.type"
placeholder="选择日期"
style="width: 100%"
:disabled="item.disabled || disabledKeys.includes(item.prop)"
/>
<el-date-picker
v-else-if="
item.type === 'daterange' || item.type === 'datetimerange'
"
v-model="form[item.prop]"
:type="item.type"
range-separator="到"
start-placeholder="开始日期"
end-placeholder="结束日期"
:disabled="item.disabled || disabledKeys.includes(item.prop)"
/>
<el-radio-group
v-else-if="item.type === 'radio'"
v-model="form[item.prop]"
class="ml-4"
:disabled="item.disabled || disabledKeys.includes(item.prop)"
>
<el-radio
v-for="(i, rIndex) in item.radioItem"
:key="rIndex"
:label="i.value"
>{{ i.title }}</el-radio
>
</el-radio-group>
<DictSelect
v-else-if="item.type === 'dict'"
v-model="form[item.prop]"
:dict-code="item.dict.dictCdoe"
:text-key="item.dict.textKey || ''"
:value-key="item.dict.valueKey || ''"
:filter="item.dict.filter || ''"
:placeholder="item.dict.placeholder"
style="width: 100%"
:disabled="item.disabled || disabledKeys.includes(item.prop)"
filterable
clearable
></DictSelect>
<DictCheckBox
v-else-if="item.type === 'dictCheckBox'"
v-model="form[item.prop]"
:dict-code="item.dict.dictCdoe"
:text-key="item.dict.textKey || ''"
:value-key="item.dict.valueKey || ''"
style="width: 100%"
:disabled="item.disabled || disabledKeys.includes(item.prop)"
:min="item.dict.min"
:max="item.dict.max"
:border="item.dict.border"
></DictCheckBox>
<DictRadio
v-else-if="item.type === 'dictRadio'"
v-model="form[item.prop]"
:dict-code="item.dict.dictCdoe"
:text-key="item.dict.textKey || ''"
:value-key="item.dict.valueKey || ''"
></DictRadio>
<DepartmentTreeSelect
v-else-if="item.type === 'departmentTreeSelect'"
v-model="form[item.prop]"
v-bind="item.componentProps ?? {}"
/>
<template v-if="item.slotName" #default>
<slot
:name="item.slotName"
:value="form"
:disabled="
item.disabled || disabledKeys.includes(item.prop)
"
></slot>
</template>
<template #label>
<span
:class="item && item.rule ? 'requestName' : 'labelName'"
>{{ item.label + '' }}</span
>
</template>
</el-form-item>
</el-col>
</el-row>
</div>
</template>
<slot name="default" :value="form"></slot>
</el-form>
</CustomDialog>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import dayjs from 'dayjs';
import cloneDeep from 'lodash/cloneDeep';
import { ElMessage, type FormInstance } from 'element-plus';
import DictSelect from '@/components/DictSelect/index.vue';
import DictCheckBox from '@/components/DictCheckBox/index.vue';
import CustomDialog from '@/components/CustomDialog/index.vue';
import DepartmentTreeSelect from '@/components/DepartmentTreeSelect/index.vue';
import DictRadio from '@/components/DictRadio/index.vue';
const props = defineProps({
// 表单配置
formOptions: {
type: Object,
default: () => {}
},
// 表单字段
formData: {
type: Object,
default: () => {}
},
disabledKeys: {
type: Array,
default: () => []
},
// margin-top
top: {
type: String,
default: () => '15vh'
},
width: {
type: Number,
default: () => 1000
},
showFooter: {
type: Boolean,
default: () => true
},
title: {
type: String,
default: () => null
},
closeOnClickModal: {
type: Boolean,
default: () => true
},
loading: {
type: Boolean,
default: () => false
}
});
const emit = defineEmits(['submit', 'reset']);
interface FormOptionItem {
label: string;
prop: string;
span: number;
type: string;
append?: string; // input后缀
}
// 表单
const formRef = ref<FormInstance>();
let formType = ''; // 控制form是新增还是编辑
let form = reactive(props.formData);
// 控制弹窗显隐
const dialogVisible = ref(false);
const openDialog = (type: string) => {
dialogVisible.value = true;
formType = type;
};
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
let params = cloneDeep(form);
// 带出数据和当前模式,用于判断调用新增还是编辑
props.formOptions.data.forEach(el => {
el.childData.forEach((item: FormOptionItem) => {
// 修改时间格式
if (params[item.prop]) {
if (item.type === 'daterange') {
params[item.prop][0] = dayjs(params[item.prop][0]).format(
'YYYY-MM-DD'
);
params[item.prop][1] = dayjs(params[item.prop][1]).format(
'YYYY-MM-DD'
);
} else if (item.type === 'datetimerange') {
params[item.prop][0] = dayjs(params[item.prop][0]).format(
'YYYY-MM-DD HH:mm:ss'
);
params[item.prop][1] = dayjs(params[item.prop][1]).format(
'YYYY-MM-DD HH:mm:ss'
);
} else if (item.type === 'date') {
params[item.prop] = dayjs(params[item.prop]).format('YYYY-MM-DD');
} else if (item.type === 'datetime') {
params[item.prop] = dayjs(params[item.prop]).format(
'YYYY-MM-DD HH:mm:ss'
);
}
}
});
});
emit('submit', params, formType);
// handleClose();
} else {
console.log('未完成填写', fields);
}
});
};
// 过滤非数字
const regStr = (prop: any, integer: boolean) => {
let value = form[prop];
if (!integer) {
value = value.replace(/[^\d.]/g, ''); // 只能输入数字和.
value = value.replace(/^\./g, ''); //第一个字符不能是.
value = value.replace(/\.{2,}/g, '.'); // 不能连续输入.
value = value.replace(/(\.\d+)\./g, '$1'); // .后面不能再输入.
value = value.replace(/^0+(\d)/, '$1');
}
if (integer) {
value = value.replace(/[^\d]/g, ''); // 只能输入数字
value = value.replace(/^0+(\d)/, '$1');
}
form[prop] = value;
};
// 数字输入做出限制
const inputNumber = (prop, data) => {
if (form[prop] != '') {
form[prop] = Number(form[prop]);
let num = form[prop];
// 保留小数
if (data.toFixed) {
form[prop] = Number(form[prop].toFixed(data.toFixed));
}
// 最大值
if (data.max && num > data.max) {
form[prop] = data.max;
ElMessage.warning(`输入值不可超过最大值${data.max}`);
}
// 最小值
if (data.min !== undefined && num < data.min) {
form[prop] = data.min;
ElMessage.warning(`输入值不可低于最小值${data.min}`);
}
}
};
// 关闭后调用
const handleClose = () => {
resetForm(formRef.value);
dialogVisible.value = false;
emit('reset');
};
// 重置校验和已填写数据
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
// 表单校验
const validate = async () => {
let res;
await formRef.value?.validate((valid, fields) => {
if (valid) {
res = true;
} else res = false;
});
return res;
};
const validateField = async props => {
let res;
await formRef.value?.validateField(props, (valid, fields) => {
if (valid) {
res = true;
} else res = false;
});
return res;
};
// 暴露方法和数据
defineExpose({ openDialog, form, handleClose, validate, validateField });
</script>
<script lang="ts">
export default {
name: 'Forms'
};
</script>
<style lang="scss">
.form-dialog {
.form-item {
margin-right: 10px;
.el-form-item__label {
padding: 0 6px 0 0;
display: flex;
align-items: center;
}
}
.el-form-item {
margin-bottom: 16px;
}
.el-form-item__label {
padding-right: 0;
}
.el-input-group__append {
background-color: #f5f7fa;
}
.group {
.group-title {
width: 100%;
height: 22px;
font-size: 16px;
font-weight: 600;
color: #2c6af3;
line-height: 22px;
margin-left: 8px;
margin-bottom: 12px;
&::before {
content: '';
display: inline-block;
width: 4px;
height: 16px;
background: linear-gradient(180deg, #327dfe 0%, #2c6af3 100%);
border-radius: 2px;
position: relative;
left: -6px;
top: 2px;
}
}
}
.labelName {
margin-left: 10px;
display: inline-block;
line-height: 1.2;
}
.requestName {
line-height: 1.2;
}
}
</style>
+65
View File
@@ -0,0 +1,65 @@
<script setup lang="ts">
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import { ref, computed } from 'vue';
const iconRefs = ref([] as Array<string>);
for (const [key] of Object.entries(ElementPlusIconsVue)) {
iconRefs.value.push(key);
}
const props = defineProps<{
modelValue?: string;
}>();
const Emits = defineEmits(['update:modelValue']);
const nowSelectIcon = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const changeSelectIcon = (icon: string) => {
if (icon !== nowSelectIcon.value) {
nowSelectIcon.value = icon;
} else {
nowSelectIcon.value = undefined;
}
};
defineExpose({
nowSelectIcon
});
</script>
<template>
<div>
<el-icon
v-for="item in iconRefs"
:key="item"
:size="16"
:color="
nowSelectIcon && item.toLowerCase() === nowSelectIcon.toLowerCase()
? '#fff'
: undefined
"
style="font-size: 1.2em; padding: 4px; width: 1.5em; height: 1.5em"
:class="{
'mr-1': true,
'cursor-pointer': true,
'bg-primary-light-color':
nowSelectIcon && item.toLowerCase() === nowSelectIcon.toLowerCase(),
'rounded-sm': true
}"
@click="changeSelectIcon(item)"
>
<component :is="item"></component>
</el-icon>
</div>
</template>
+43
View File
@@ -0,0 +1,43 @@
<script setup lang="ts">
defineProps<{
title: string;
}>();
</script>
<template>
<div class="info-card w-full">
<header class="info-card__header">
<div class="info-card__prefix mx-2"></div>
<div class="h-full text-base" style="line-height: 34px">{{ title }}</div>
</header>
<section class="min-h-[100px] p-2 pt-0">
<slot></slot>
</section>
</div>
</template>
<style scoped>
.info-card {
background-color: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
overflow: hidden;
width: 100%;
}
.info-card__header {
height: 36px;
width: 100%;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
display: flex;
align-items: center;
}
.info-card__prefix {
width: 4px;
height: 20px;
background: linear-gradient(180deg, #327dfe 0%, #2c6af3 100%);
border-radius: 4px;
}
</style>
+104
View File
@@ -0,0 +1,104 @@
<script setup lang="ts">
defineProps<{
data: any;
configs: Array<{
name: string;
key: string;
formatter?: (data: any, valueKey: string) => any;
alarmType?: any;
}>;
darkmode?: boolean; // 暗黑模式兼容
noStrike?: boolean; // 斑马纹
noColon?: boolean; // 自带冒号
labelWidth?: string;
minWidth?: string;
}>();
</script>
<template>
<div
class="info-table"
:class="{
'dark-mode': darkmode,
'has-strike': !noStrike,
'has-border': noStrike
}"
:style="{
minWidth
}"
>
<div
v-for="item in configs"
:key="item.key"
class="flex items-center min-h-[28px] p-1"
>
<div
class="info-table__label shrink-0"
:style="{
width: labelWidth
}"
>
{{ item.name }}<span v-if="!noColon"></span>
</div>
<div class="ml-auto break-all text-[#2C6AF3]">
<slot :value-key="item.key" :name="item.key" :data="data">
<span
class="info-table-item__value"
:class="{
alarm:
typeof item.alarmType === 'function'
? item.alarmType(data, item.key)
: typeof item.alarmType !== 'undefined' &&
data[item.key] === item.alarmType
}"
>{{
item.formatter
? item.formatter(data, item.key)
: data[item.key] || '-'
}}</span
>
</slot>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.info-table {
font-size: 14px;
&.has-strike {
& > div:nth-child(2n) {
background-color: #f5f7fa;
border-radius: 3px;
}
}
&.has-border {
& > div {
border-bottom: 1px solid #dbdfe6;
}
& > div:last-child {
border-bottom: none;
}
}
}
html.dark {
.info-table.has-strike.dark-mode {
& > div:nth-child(2n) {
background-color: #4a4a4a;
}
}
}
.info-table__label {
width: 84px;
color: #333;
}
.info-table-item__value.alarm {
color: #ea4955;
}
</style>
+720
View File
@@ -0,0 +1,720 @@
<script setup lang="ts">
import { ClickOutside as vClickOutside } from 'element-plus';
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
import usePreferenceStore from '@/stores/preference';
import useAccountStore from '@/stores/account';
import PreferenceSetting from './PreferenceSetting.vue';
import defaultUserAvatar from '@/assets/images/default-user-avatar.webp';
import { onBeforeRouteUpdate, useRoute } from 'vue-router';
import useMenuTab from './useMenuTab';
import SubMenu from './SubMenu.vue';
import { ElMessage } from 'element-plus';
import useRequest from '@/hooks/useRequest';
import PasswordEdit from '@/views/system/user/PasswordEdit.vue';
import CustomDialog from '@/components/CustomDialog/index.vue';
import { changeMyPassword } from '@/services/api/system/user/index';
import { aesEncrypt } from '@/views/login/utils';
import {
getCaptchaImage as getCaptchaImageAPI,
getEncryptString as getEncryptStringAPI
} from '@/services/api/auth';
import cloneDeep from 'lodash/cloneDeep';
const preferenceStore = usePreferenceStore();
const accountStore = useAccountStore();
const menuData = computed(() => {
const getAllowMenus = accountStore.rawPermissions?.menu || [];
return getAllowMenus;
});
const avatar = computed(() => defaultUserAvatar);
// const avatar = computed(() => accountStore.avatar || defaultUserAvatar);
const showPreferenceSetting = ref(false);
const route = useRoute();
const nowRoute = reactive({
path: route.path,
query: route.query,
params: route.params,
fullPath: route.fullPath,
name: route.name,
meta: route.meta
});
onBeforeRouteUpdate(to => {
nowRoute.path = to.path;
nowRoute.name = to.name;
nowRoute.meta = to.meta;
nowRoute.query = to.query;
nowRoute.params = to.params;
nowRoute.fullPath = to.fullPath;
});
// 当前路由路径
const routePath = computed(() => nowRoute.path);
const {
menuTabs,
routeLoading,
keepAliveNames,
closeMenuTab,
closeAllTabs,
closeTabLeft,
closeTabRight,
onMenuTabKeyChange
} = useMenuTab(nowRoute as any);
// 展示系统设置
const showSystemSetting = () => {
showPreferenceSetting.value = true;
};
if (accountStore.token) {
accountStore.getUser().catch(error => {
ElMessage({
message: error.response.data?.message,
type: 'error'
});
});
}
// 修改密码
// accountStore.getUserPermissions();
const passwordEditForm = ref();
const showUserPassword = ref(false);
const changeEditPassword = () => {
showUserPassword.value = true;
};
const { pending: passwordEditLoading, run: submitEditUserPassword } =
useRequest(async () => {
if (passwordEditForm.value) {
const submitParams = await passwordEditForm.value.validateForm();
// 增加AES加密
const newPassword = aesEncrypt(
submitParams.password,
encryptParams.value.key,
encryptParams.value.iv
);
const oldPassword = aesEncrypt(
submitParams.oldPassword,
encryptParams.value.key,
encryptParams.value.iv
);
let parasm = cloneDeep(submitParams);
parasm.oldPassword = oldPassword;
parasm.password = newPassword;
parasm.confirmPassword = newPassword;
const response = await changeMyPassword(parasm);
ElMessage({
message: response.data.message || '操作成功!',
type: 'success'
});
showUserPassword.value = false;
} else {
ElMessage({
message: '请完善表单',
type: 'warning'
});
}
});
if (
accountStore.token &&
accountStore.PermissionsRoute instanceof Array &&
accountStore.PermissionsRoute.length === 0
) {
accountStore.getUserPermissions();
}
const { data: encryptParams, run: getEncryptString } = useRequest(
async () => {
const response = await getEncryptStringAPI();
const result = response.data?.result;
return { iv: result?.iv, key: result?.key };
},
{ initialData: { iv: '', key: '' } }
);
watch(
() => route.fullPath,
() => {
nextTick(() => {
routeLoading.value = false;
});
}
);
onMounted(() => {
routeLoading.value = false;
const horizontalScroll = document.querySelector(
'.host-main-header__menu-horizontal'
);
if (horizontalScroll) {
horizontalScroll.addEventListener('wheel', function (e: any) {
e.preventDefault();
// 滚动页面的水平位置
horizontalScroll.scrollLeft += e.deltaY;
});
}
getEncryptString();
});
/**
* 伸缩菜单
*/
const autoHide = computed(() => {
if (route.query && route.query.autoHide) {
return route.query.autoHide === 'true';
}
return preferenceStore.autoHide;
});
/**
* 伸缩菜单状态记录
*/
const autoHideShowState = ref(false);
/**
* 布局模式
*/
const layoutMenuMode = computed(() => {
if (route.query && route.query.layoutMenuMode) {
return route.query.layoutMenuMode;
}
return preferenceStore.layoutMenuMode;
});
/**
* 标签页隐藏
*/
const tabsHide = computed(() => {
if (route.query && route.query.tabsHide) {
return route.query.tabsHide === 'true';
}
return preferenceStore.tabsHide;
});
const routeViewBackground = computed(() => {
if (route.query && typeof route.query.routeViewBackground === 'string') {
return route.query.routeViewBackground;
}
return undefined;
});
// 标签右键功能
const tabActions = [
{
name: '关闭页面',
icon: 'DocumentDelete',
handler: () => {
closeMenuTab(contextMenuTabId.value);
showTabContextMenu.value = false;
}
},
{
name: '关闭所有',
icon: 'FolderDelete',
handler: () => {
closeAllTabs();
showTabContextMenu.value = false;
}
},
{
name: '关闭左侧',
icon: 'DArrowLeft',
handler: () => {
closeTabLeft(contextMenuTabId.value);
showTabContextMenu.value = false;
}
},
{
name: '关闭右侧',
icon: 'DArrowRight',
handler: () => {
closeTabRight(contextMenuTabId.value);
showTabContextMenu.value = false;
}
}
];
const showTabContextMenu = ref(false);
const contextMenuTabId = ref();
const tabContextMenuPosition = reactive({
x: 0,
y: 0
});
const openContextMenu = evt => {
if (evt.srcElement.id) {
// console.error(evt, evt.srcElement.id);
contextMenuTabId.value = evt.srcElement.id.split('-')[1];
if (contextMenuTabId.value !== '/home') {
showTabContextMenu.value = true;
tabContextMenuPosition.x = evt.clientX;
tabContextMenuPosition.y = evt.clientY + 10;
}
}
};
</script>
<template>
<section class="host-main-layout__container">
<header
class="host-main-header__container bg-primary-light-color dark:bg-primary-dark-color border-b border-b-border-color dark:border-b-border-dark-color"
:class="{
'auto-hide': autoHide,
'auto-hide__visible': autoHideShowState
}"
>
<img class="host-main-header__logo" src="@/assets/images/logo.png" />
<div class="host-main-header__menu-horizontal h-full relative">
<el-menu
v-if="layoutMenuMode === 'horizontal'"
mode="horizontal"
class="w-full h-full"
:default-active="routePath"
router
style="
--el-menu-bg-color: transparent;
--el-menu-hover-bg-color: transparent;
--el-menu-border-color: transparent;
--el-bg-color-overlay: transparent;
--el-menu-text-color: white;
--el-menu-hover-text-color: #f5f5f5;
--el-menu-active-color: white;
"
ellipsis
>
<SubMenu :data="menuData"></SubMenu>
</el-menu>
</div>
<div class="host-main-header__right">
<el-dropdown
:tabindex="3000"
popper-class="host-main-header__user-popover"
>
<div class="host-main-header__user">
<img class="host-main-header__user-avatar" :src="avatar" />
<button
style="white-space: nowrap"
class="host-main-header__user-label host-header-button hidden md:inline-block"
>
欢迎您{{
accountStore.realname || accountStore.username || '访客用户'
}}
</button>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-if="!!accountStore.token"
@click="changeEditPassword"
>修改密码</el-dropdown-item
>
<el-dropdown-item @click="showSystemSetting"
>系统偏好设置</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 弹窗组件 -->
<CustomDialog
v-model="showUserPassword"
confirm-text=" "
title="修改密码"
destroy-on-close
:loading="passwordEditLoading"
@confirm="submitEditUserPassword"
>
<PasswordEdit
ref="passwordEditForm"
type="user"
:data="{
username: accountStore.rawUser?.username
}"
></PasswordEdit>
</CustomDialog>
<el-drawer
v-model="showPreferenceSetting"
title="个人化设置"
:size="360"
:with-header="false"
>
<PreferenceSetting></PreferenceSetting>
</el-drawer>
<button
class="host-header-button ml-2 md:ml-5"
style="white-space: nowrap"
@click="accountStore.logout(true)"
>
<span class="icon iconfont gci-export" style="margin-right: 5px" />
退出登录
</button>
</div>
</header>
<button
v-if="autoHide"
class="host-main-auto-hide__button bg-primary-light-color dark:bg-primary-dark-color border-l border-b border-border-color dark:border-border-dark-color"
:style="{
top: autoHideShowState ? (tabsHide ? '60px' : '103px') : '0px'
}"
@click="autoHideShowState = !autoHideShowState"
>
<span class="host-main-auto-hide__text">
{{ autoHideShowState ? '收起菜单' : '展开菜单' }}
</span>
</button>
<section class="host-main-body__container">
<aside
:class="[
'host-main-body__aside',
{
'host-main-body__aside--visible': layoutMenuMode === 'vertical',
'auto-hide': autoHide,
'auto-hide__visible': autoHideShowState
}
]"
>
<el-menu style="height: 100%" router :default-active="routePath">
<SubMenu :data="menuData"></SubMenu>
</el-menu>
</aside>
<main class="host-main-body__main relative">
<div
v-show="showTabContextMenu"
v-click-outside="() => (showTabContextMenu = false)"
class="host-menu-tab__contextmenu"
:style="{
left: tabContextMenuPosition.x + 'px',
top: tabContextMenuPosition.y + 'px'
}"
>
<div
v-for="tabAction in tabActions"
:key="tabAction.name"
class="py-1 pr-2 pl-1 hover:bg-primary-light-color/80 hover:text-white cursor-pointer flex items-center justify-center"
@click="tabAction.handler"
>
<el-icon><component :is="tabAction.icon" /></el-icon>
<span class="ml-1">
{{ tabAction.name }}
</span>
</div>
</div>
<header
v-if="!tabsHide"
class="host-main-body__tab overflow-hidden bg-white dark:bg-primary-dark-color"
:class="{
'auto-hide': autoHide,
'auto-hide__visible': autoHideShowState
}"
:style="{
width:
autoHide && layoutMenuMode === 'vertical'
? 'calc(100vw - 200px)'
: '100%',
left: autoHide && layoutMenuMode === 'vertical' ? '200px' : '0'
}"
>
<el-tabs
:model-value="routePath"
type="card"
style="width: 100%; margin-top: 2px"
@tab-remove="closeMenuTab"
@tab-change="onMenuTabKeyChange"
@contextmenu.prevent="openContextMenu($event)"
>
<el-tab-pane
v-for="(item, index) in menuTabs"
:key="item.path"
:label="item.meta?.title || '未知菜单标题'"
:name="item.path"
:closable="index !== 0"
>
</el-tab-pane>
</el-tabs>
</header>
<section
v-loading="routeLoading"
class="host-main-body__content bg-[#f1f2f6] dark:bg-[#404040]"
:class="{
'no-tabs': tabsHide,
'auto-hide': autoHide
}"
:style="{
backgroundColor: routeViewBackground
? `#${routeViewBackground}`
: undefined
}"
>
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveNames">
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
<!-- <RouterView :key="$route.fullPath"></RouterView> -->
</section>
</main>
</section>
</section>
</template>
<style lang="scss" scoped>
$header-height: 60px;
$tab-height: 34px;
.host-main-layout__container {
height: 100%;
display: flex;
flex-direction: column;
}
.host-main-header__container {
padding: 0 20px;
height: $header-height;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
}
.host-main-header__container.auto-hide {
position: fixed;
top: -60px;
width: 100vw;
z-index: 2099;
transition: top 0.3s;
}
.host-main-header__container.auto-hide__visible {
top: 0px;
}
.host-main-header__menu-horizontal {
width: calc(100vw - 748px);
margin-left: 32px;
margin-right: auto;
overflow: hidden;
// display: flex;
// align-items: center;
&::-webkit-scrollbar {
display: none;
}
}
.host-main-header__logo {
// width: 150px;
height: 32px;
}
.host-main-header__user-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
}
.host-main-header__right,
.host-main-header__user {
display: flex;
align-items: center;
font-size: 14px;
}
.host-main-header__user-label {
margin-left: 8px;
}
.host-main-body__container {
flex: 1;
display: flex;
overflow: hidden;
}
.host-main-body__aside {
width: 0;
height: 100%;
overflow: auto;
transition: width 0.2s;
border-right: solid 1px var(--el-menu-border-color, #dcdfe6);
&::-webkit-scrollbar {
display: none;
}
&.host-main-body__aside--visible {
// border-right: 1px solid var(--host-border-color);
width: 200px;
}
&:deep(.el-menu) {
border-right: none;
}
}
.host-main-body__aside.auto-hide {
position: fixed;
left: -200px;
top: $header-height;
height: calc(100vh - $header-height);
z-index: 2080;
transition: left 0.3s;
}
.host-main-body__aside.auto-hide__visible {
left: 0px;
}
.host-main-body__main {
flex: 1;
height: 100%;
overflow-y: hidden;
}
.host-main-body__tab {
// height: $tab-height;
width: 100%;
display: flex;
align-items: center;
// background-color: var(--host-content-background-color);
&
:deep(
.n-tabs .n-tabs-nav.n-tabs-nav--card-type .n-tabs-tab.n-tabs-tab--active
) {
background-color: var(--n-tab-color);
border-bottom: 1px solid var(--n-tab-border-color);
}
&
:deep(
.n-tabs .n-tabs-nav.n-tabs-nav--card-type .n-tabs-tab.n-tabs-tab--closable
) {
border-radius: 4px;
}
& :deep(.n-tabs .n-tabs-nav) {
line-height: 1;
}
& :deep(.n-tabs .n-tabs-nav.n-tabs-nav--card-type .n-tabs-pad) {
border-bottom: none;
}
& :deep(.n-tabs .n-tabs-nav.n-tabs-nav--card-type .n-tabs-tab-pad) {
border-bottom: none;
}
}
.host-main-body__tab.auto-hide {
position: fixed;
top: -42px;
transition: top 0.3s;
z-index: 2079;
}
.host-main-body__tab.auto-hide__visible {
top: 60px;
}
.host-main-body__content-iframe {
width: 100%;
height: 100%;
overflow-y: auto;
}
.host-main-body__content {
width: 100%;
height: calc(100% - $tab-height);
overflow-y: auto;
position: relative;
padding: 8px;
// &::-webkit-scrollbar {
// display: none;
// }
}
.host-main-body__content.auto-hide {
height: 100%;
}
.host-main-body__content.no-tabs {
height: 100%;
}
#host-menu-ellipsis {
cursor: pointer;
margin-left: 20px;
}
#host-main-body__tab_box {
height: 32px;
justify-content: center;
// border-top: 1px solid var(--host-border-color);
// border-bottom: 1px solid var(--host-border-color);
box-sizing: content-box;
padding-left: 10px;
}
.host-main-auto-hide__button {
position: fixed;
right: 0px;
// width: 28px;
width: 72px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0px;
border-bottom-left-radius: 8px;
font-size: 12px;
border-top: none;
cursor: pointer;
user-select: none;
transition: top 0.3s;
z-index: 2096;
transition: width 0.3s;
color: #f5f5f5;
.host-main-auto-hide__text {
// white-space: nowrap;
// display: none;
display: inline-block;
}
// &:hover {
// width: 72px;
// .host-main-auto-hide__text {
// display: inline-block;
// }
// }
}
.host-header-button {
color: #f5f5f5;
transition: all 0.3s;
line-height: 16px;
&:hover {
color: #fff;
}
}
host-main-header__user:hover .host-header-button {
color: #fff;
}
:deep(.el-tabs__header) {
margin: 0;
}
:deep(.el-overlay > div.el-drawer) {
max-width: 90%;
}
.host-menu-tab__contextmenu {
min-width: 100px;
border: 1px solid #ccc;
background: #fff;
z-index: 3000;
position: fixed;
list-style-type: none;
// padding: 4px;
margin: 4px 12px;
border-radius: 4px;
font-size: 14px;
color: #4a4a4a;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.2);
}
</style>
@@ -0,0 +1,131 @@
<template>
<section style="position: relative">
<div>
<h4>导航模式</h4>
<ul class="host-preference-list">
<li
:class="[
'host-preference-list__item',
'host-preference-list__item-vertical',
{ 'host-preference-list__item--checked': isMenuVertical }
]"
@click="setVerticalMenuMode"
/>
<li
:class="[
'host-preference-list__item',
'host-preference-list__item-horizontal',
{ 'host-preference-list__item--checked': isMenuHorizontal }
]"
@click="setHorizontalMenuMode"
/>
</ul>
</div>
<div style="margin-top: 32px">
<h4>其他设置</h4>
<div class="host-setting-item">
<div class="host-setting-item-label">暗黑模式</div>
<el-switch
:model-value="isDarkTheme"
size="small"
:disabled="true || preferenceStore.preferenceDisabled"
@change="toggleTheme"
/>
</div>
<div class="host-setting-item">
<div class="host-setting-item-label">伸缩菜单</div>
<el-switch
v-model="preferenceStore.autoHide"
:disabled="preferenceStore.preferenceDisabled"
size="small"
/>
</div>
<div class="host-setting-item">
<div class="host-setting-item-label">隐藏标签页</div>
<el-switch
v-model="preferenceStore.tabsHide"
:disabled="preferenceStore.preferenceDisabled"
size="small"
/>
</div>
</div>
</section>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import usePreferenceStore from '@/stores/preference';
const preferenceStore = usePreferenceStore();
const isMenuHorizontal = computed(
() => preferenceStore.layoutMenuMode === 'horizontal'
);
const isMenuVertical = computed(
() => preferenceStore.layoutMenuMode === 'vertical'
);
const isDarkTheme = computed(() => preferenceStore.theme === 'dark');
const setVerticalMenuMode = () => {
preferenceStore.setLayoutMenuMode('vertical');
};
const setHorizontalMenuMode = () => {
preferenceStore.setLayoutMenuMode('horizontal');
};
const toggleTheme = () => {
preferenceStore.setTheme(isDarkTheme.value ? 'light' : 'dark');
};
</script>
<style lang="scss" scoped>
.host-preference-list {
margin-top: 8px;
display: flex;
align-items: center;
gap: 8px;
line-height: 1;
}
.host-preference-list__item {
width: 48px;
height: 48px;
cursor: pointer;
border-radius: 8px;
background-repeat: no-repeat;
background-size: 100% 100%;
transition: all 0.2s;
&.host-preference-list__item-vertical {
background-image: url('@/assets/images/menu-vertical.svg');
}
&.host-preference-list__item-horizontal {
background-image: url('@/assets/images/menu-horizontal.svg');
}
&.host-preference-list__item--checked {
border: 1px solid #3a97f9;
}
}
.host-setting-item {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0.4em 0;
.host-setting-item-label {
font-size: 0.88em;
width: 120px;
opacity: 0.75;
// text-align: right;
}
}
.host-preference-tip {
position: absolute;
right: 0;
cursor: pointer;
}
.host-preference-tip__item {
font-size: 12px;
}
</style>
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { RouteResponse } from '@/services/api/auth/types';
defineProps<{
data: RouteResponse[];
}>();
</script>
<template>
<template v-for="menuItem in data" :key="menuItem.path">
<el-sub-menu
v-if="
menuItem.children &&
menuItem.children instanceof Array &&
menuItem.children.length > 0 &&
menuItem.hidden !== 1
"
:index="menuItem.path"
popper-class="host-main-header__user-popover"
>
<template #title>
<el-icon v-if="menuItem.meta && menuItem.meta.icon"
><component :is="menuItem.meta.icon"
/></el-icon>
<span>{{ menuItem.meta?.title || '未知菜单标题' }}</span>
</template>
<SubMenu :data="menuItem.children"></SubMenu>
</el-sub-menu>
<el-menu-item v-else-if="menuItem.hidden !== 1" :index="menuItem.path">
<el-icon v-if="menuItem.meta && menuItem.meta.icon"
><component :is="menuItem.meta.icon"
/></el-icon>
<template #title>{{ menuItem.meta?.title || '未知菜单标题' }}</template>
</el-menu-item>
</template>
</template>
+172
View File
@@ -0,0 +1,172 @@
import { computed, ref, watch } from 'vue';
import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router';
import cloneDeep from 'lodash/cloneDeep';
import { getCacheByUrl } from '@/utils/utils';
function useMenuTab(activeMenu: RouteLocationNormalizedLoaded) {
const routeLoading = ref(false);
const router = useRouter();
const menuTabs = ref<RouteLocationNormalizedLoaded[]>([
{
path: '/home',
fullPath: '/home',
meta: {
authRequired: true,
defaultQuery: undefined,
hidden: false,
icon: 'house',
internalOrExternal: false,
title: '首页'
},
name: 'home',
params: {},
query: {}
} as any
]); // 点击过的菜单tab
const keepAliveNames = computed<Array<string>>(() => {
return menuTabs.value.map(tab =>
tab.fullPath && typeof tab.fullPath === 'string'
? getCacheByUrl(tab.fullPath)
: '???'
);
});
// 添加点击过的菜单
const addMenuTab = (activeMenu: RouteLocationNormalizedLoaded) => {
const sameRouteIndex = menuTabs.value.findIndex(
menuTab => menuTab.path === activeMenu.path
);
if (typeof activeMenu.path === 'undefined' || sameRouteIndex !== -1) {
const sameRoute = menuTabs.value.find(
menuTab => menuTab.path === activeMenu.path
);
if (sameRoute?.fullPath !== activeMenu.fullPath) {
menuTabs.value.splice(sameRouteIndex, 1, activeMenu);
return;
}
return;
}
menuTabs.value.push(activeMenu);
};
// 关闭历史打开的tab
const closeMenuTab = (key: string | number) => {
if (menuTabs.value.length === 1) {
return;
}
const closeIndex = menuTabs.value.findIndex(
(menuTab: RouteLocationNormalizedLoaded) => menuTab.path === key
);
let jumpRoute: RouteLocationNormalizedLoaded | undefined;
if (activeMenu.path === key) {
jumpRoute =
closeIndex === 0 ? menuTabs.value[1] : menuTabs.value[closeIndex - 1];
}
menuTabs.value.splice(closeIndex, 1);
if (jumpRoute) {
router.replace(jumpRoute.fullPath);
}
};
const onMenuTabKeyChange = (key: string) => {
const jumpRoute = menuTabs.value.find(menuTab => menuTab.path === key);
if (jumpRoute) {
router.replace(jumpRoute.fullPath);
return;
}
router.replace(key);
};
const closeTabRight = (key: string) => {
let activeIndex = 0;
const closeIndex = menuTabs.value.findIndex(
(menuTab: RouteLocationNormalizedLoaded) => menuTab.path === key
);
if (activeMenu.path !== key) {
activeIndex = menuTabs.value.findIndex(
(menuTab: RouteLocationNormalizedLoaded) =>
menuTab.path === activeMenu.path
);
} else {
activeIndex = closeIndex;
}
if (closeIndex === menuTabs.value.length - 1) {
return;
}
if (activeIndex > closeIndex) {
const jumpRoute = menuTabs.value[closeIndex];
router.replace(jumpRoute.fullPath);
}
menuTabs.value.splice(
closeIndex + 1,
menuTabs.value.length - closeIndex - 1
);
};
const closeTabLeft = (key: string) => {
let activeIndex = 0;
const closeIndex = menuTabs.value.findIndex(
(menuTab: RouteLocationNormalizedLoaded) => menuTab.path === key
);
if (activeMenu.path !== key) {
activeIndex = menuTabs.value.findIndex(
(menuTab: RouteLocationNormalizedLoaded) =>
menuTab.path === activeMenu.path
);
} else {
activeIndex = closeIndex;
}
if (closeIndex === 1) {
return;
}
if (activeIndex < closeIndex) {
const jumpRoute = menuTabs.value[closeIndex];
router.replace(jumpRoute.fullPath);
}
menuTabs.value.splice(1, closeIndex - 1);
};
const closeAllTabs = () => {
menuTabs.value = [menuTabs.value[0]];
router.replace(menuTabs.value[0].path);
};
watch(
() => activeMenu.fullPath,
path => {
if (!path) {
routeLoading.value = false;
return;
}
routeLoading.value = true;
addMenuTab(cloneDeep(activeMenu));
},
{ immediate: true }
);
return {
menuTabs,
routeLoading,
keepAliveNames,
addMenuTab,
closeMenuTab,
closeAllTabs,
closeTabLeft,
closeTabRight,
onMenuTabKeyChange
};
}
export default useMenuTab;
+216
View File
@@ -0,0 +1,216 @@
<script setup lang="ts" name="PageSelectTable">
import DataFormGrid from '@/components/DataFormGrid/index.vue';
import DataTableGrid from '@/components/DataTableGrid/index.vue';
import { computed, nextTick, onMounted, ref } from 'vue';
import type { ColumnType } from '../DataTable/types';
import type { ActionType } from './types.d.ts';
import type { FormGridConfig } from '../DataFormGrid/types';
import cloneDeep from 'lodash/cloneDeep';
const tableRef = ref();
const formRef = ref();
const props = withDefaults(
defineProps<{
url: string;
fields: Array<FormGridConfig>; // 表单搜索配置
columns: Array<ColumnType>; // 列表显示字段
actions?: Array<ActionType>;
defaultQuery?: any;
searchToQuery?: (searchForm?: any) => any;
submitSpan?: number;
fieldOptions?: any;
justTable?: boolean;
height: any;
tableOptions?: any;
tableEvents?: any;
}>(),
{
fields: () => [],
columns: () => [],
actions: () => [],
defaultQuery: () => ({}),
searchToQuery: undefined,
submitSpan: 6,
fieldOptions: () => ({}),
tableOptions: () => ({}),
tableEvents: () => ({}),
height: null
}
);
/**
* 列表插槽项
*/
const colSlots = computed(() => {
return props.columns.filter(item => !!item.slotName) as Array<any>;
});
/**
* 搜索插槽项
*/
const fieldSlots = computed(() => {
return props.fields.filter(
item =>
(item.type === 'slot' ||
item.type === 'itemSlot' ||
(item.type === 'title' && item.options && item.options[0].hasSuffix)) &&
item.key !== 'submitActions'
);
});
/**
* 搜索项
*/
const searchConfigs = computed(() => {
return props.fields.concat([
{
type: 'slot',
key: 'submitActions',
span: props.submitSpan
? props.submitSpan
: props.fields.length > 0
? 6
: 24,
mdSpan: 24,
name: '查询按钮'
}
]);
});
/**
* 请求参数
*/
const searchForm = ref(cloneDeep(props.defaultQuery));
const queryParams = ref(cloneDeep(props.defaultQuery));
/**
* 搜索方法
*/
const loadSearch = async () => {
await formRef.value?.formValidate();
if (props.searchToQuery) {
queryParams.value = props.searchToQuery(searchForm.value);
} else {
queryParams.value = cloneDeep(searchForm.value);
}
nextTick(() => {
tableRef.value.resetTableData();
});
};
// 重置按钮
const resetSearch = () => {
searchForm.value = cloneDeep(props.defaultQuery);
formRef.value?.formReset();
loadSearch();
};
const pageSearchTable = ref();
const searchRef = ref();
const tableHeight = ref();
const calTableHeight = () => {
const pageHeight = pageSearchTable.value.offsetHeight;
const searchHeight = searchRef.value.$el.offsetHeight;
if (pageHeight && searchHeight && pageHeight > 120) {
tableHeight.value = pageHeight - searchHeight - 120;
}
};
onMounted(() => {
if (!props.justTable) {
calTableHeight();
}
});
defineExpose({
loadSearch,
resetSearch,
queryParams,
tableRef
});
</script>
<template>
<div v-if="!justTable" ref="pageSearchTable" class="w-full h-full relative">
<el-card ref="searchRef" class="gci-search">
<DataFormGrid
ref="formRef"
v-model="searchForm"
:configs="searchConfigs"
v-bind="fieldOptions"
>
<template v-for="fieldSlot in fieldSlots" #[fieldSlot.key]="">
<slot :name="fieldSlot.key" :value="searchForm"></slot>
</template>
<template #submitActions>
<div class="w-full flex justify-end">
<el-button icon="Search" type="primary" @click="loadSearch"
>查询</el-button
>
<el-button icon="RefreshRight" @click="resetSearch">重置</el-button>
</div>
</template>
</DataFormGrid>
</el-card>
<el-card class="mt-2">
<DataTableGrid
ref="tableRef"
:query-params="queryParams"
:columns="columns"
:height="height || tableHeight"
:url="url"
v-bind="tableOptions"
v-on="tableEvents"
>
<template #header>
<div class="flex w-full mb-2 min-h-[32px]">
<div
v-for="action in actions"
:key="action.key"
:class="action.class"
>
<slot
:name="action.key"
:handler="action.handler"
:icon="action.icon"
:auth="action.auth"
>
<el-button
v-if="action.show !== false"
v-auth="action.auth"
:type="action.type || 'primary'"
:icon="action.icon"
@click="action.handler"
>
{{ action.name }}
</el-button>
</slot>
</div>
</div>
</template>
<template v-for="colSlot in colSlots" #[colSlot.slotName]="{ row }">
<slot :name="colSlot.slotName" :row="row" :value="colSlot"></slot>
</template>
</DataTableGrid>
</el-card>
</div>
<DataTableGrid
v-else
ref="tableRef"
:query-params="queryParams"
:columns="columns"
:height="height || tableHeight"
:url="url"
>
<template v-for="colSlot in colSlots" #[colSlot.slotName]="{ row }">
<slot :name="colSlot.slotName" :row="row" :value="colSlot"></slot>
</template>
</DataTableGrid>
</template>
+12
View File
@@ -0,0 +1,12 @@
export interface ActionType {
key: string;
name: string;
type?: string;
icon?: string;
class?: string;
handler: () => void;
show?: boolean;
auth?: string;
}
export default ActionType;
+87
View File
@@ -0,0 +1,87 @@
<script setup lang="ts" name="ProjectSelect">
import useRequest from '@/hooks/useRequest';
import {
getProjects,
getProjectsByPage
} from '@/services/api/base/ExternalContacts/index.ts';
import { computed, watch } from 'vue';
const props = withDefaults(
defineProps<{
modelValue: Array<string> | string | number | undefined;
queryParams: {
project?: string;
customerId?: string;
};
usePage?: boolean;
}>(),
{
usePage: false
}
);
const Emits = defineEmits(['update:modelValue']);
const selectedValue = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const {
data: projectOptions,
pending,
run: getProjectOptions
} = useRequest(
async () => {
let api = props.usePage ? getProjectsByPage : getProjects;
const response = await api(props.queryParams);
let result = props.usePage
? response.data.result.records
: response.data.result;
return result || [];
},
{
initialData: []
}
);
getProjectOptions();
watch(
() => props.queryParams.customerId,
(newValue, oldValue) => {
if (newValue !== oldValue) {
selectedValue.value = [];
getProjectOptions();
}
},
{
deep: true
}
);
</script>
<template>
<el-select
v-model="selectedValue"
style="width: 100%"
clearable
multiple
filterable
placeholder="请选择所属项目"
:loading="pending"
v-bind="$attrs"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="item.projectName"
:value="item.id"
/>
</el-select>
</template>
+100
View File
@@ -0,0 +1,100 @@
<script setup lang="ts" name="RmoteSelect">
import useRequest from '@/hooks/useRequest';
import NormalizedResponse from '@/services/http/types';
import axios from '@/services/http';
import { computed } from 'vue';
const props = defineProps<{
modelValue: Array<string> | string | number | undefined;
url: string;
optionLabel: string;
optionValue: string;
isList?: boolean;
multiple?: boolean;
isString?: boolean;
queryUrl?: string;
}>();
const Emits = defineEmits(['update:modelValue', 'change']);
const selectedValue = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
if (props.isString && props.multiple) {
return typeof props.modelValue === 'string' && props.modelValue
? props.modelValue.split(',')
: [];
}
return props.modelValue;
},
set: nv => {
if (props.isString && props.multiple) {
Emits(
'update:modelValue',
nv instanceof Array ? nv.join(',') : undefined
);
} else {
Emits('update:modelValue', nv);
}
if (!props.multiple) {
Emits(
'change',
remoteOptions.value.filter(item => item[props.optionValue] === nv)
);
} else {
Emits(
'change',
remoteOptions.value.filter(item =>
nv instanceof Array ? nv.includes(item[props.optionValue]) : []
)
);
}
}
});
const {
data: remoteOptions,
pending,
run: getRemoteOptions
} = useRequest(
async () => {
const remoteUrl = `${props.url}${
props.isList ? '?pageNo=1&pageSize=-1' : ''
}`;
const response = await axios.get<NormalizedResponse<any>>(
remoteUrl + (props.queryUrl || '')
);
if (props.isList) {
return response.data.result?.records || [];
}
return response.data.result || [];
},
{
initialData: []
}
);
getRemoteOptions();
</script>
<template>
<el-select
v-model="selectedValue"
style="width: 100%"
clearable
:multiple="multiple"
filterable
placeholder="请选择所属项目"
:loading="pending"
v-bind="$attrs"
>
<el-option
v-for="item in remoteOptions"
:key="item[optionValue]"
:label="item[optionLabel]"
:value="item[optionValue]"
/>
</el-select>
</template>
+87
View File
@@ -0,0 +1,87 @@
<script setup lang="ts" name="RoleSelect">
import { ref, watch, computed } from 'vue';
import useRequest from '@/hooks/useRequest';
import { getAllRoles, getUserRole } from '@/services/api/system/role/index';
const props = defineProps<{
id?: string;
modelValue: Array<string> | string | number | undefined;
alwayIds?: Array<string>;
disabled?: boolean;
}>();
const initSelectValue =
typeof props.modelValue === 'string'
? props.modelValue.split(',')
: ([] as Array<string>);
/**
* 用户权限
*/
const selectRolesArr = ref(props.alwayIds ? props.alwayIds : initSelectValue);
const Emits = defineEmits(['update:modelValue']);
const selectedroles = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
const {
pending,
data: rolesOptions,
run: getRoleSelectOptions
} = useRequest(
async () => {
const response = await getAllRoles();
if (props.id) {
const userRoleResponse = await getUserRole(props.id);
selectRolesArr.value = userRoleResponse.data.result || [];
}
return response.data.result || [];
},
{ initialData: [] }
);
getRoleSelectOptions();
watch(
() => selectRolesArr.value,
newValue => {
selectedroles.value = newValue.join(',');
},
{
immediate: true,
deep: true
}
);
</script>
<template>
<el-select
v-model="selectRolesArr"
style="width: 100%"
clearable
:disabled="disabled"
multiple
filterable
:max-collapse-tags="1"
collapse-tags
placeholder="请选择用户角色"
:loading="pending"
>
<el-option
v-for="item in rolesOptions"
:key="item.id"
:label="item.roleName"
:value="item.id"
/>
</el-select>
</template>
+135
View File
@@ -0,0 +1,135 @@
# SchemaForm
基于 element-plus 的配置型表单。
## 用法
通过传入 `formGroups` 配置,实现表单渲染以及表单模型构造。以下是配置说明:
```ts
{
key: 'string', // 必填,循环渲染时使用
title: 'string', // 选填,该 group 标题,不填则不渲染
fields: [ // 必填,该 group 下的表单域
{ // 表单域
label: 'string', // 必填,表单域文案描述
key: 'string', // 必填,表单域在表单模型中的字段名
initialValue: 'any', // 选填,表单域在表单模型中的初始值,类型不限,与自定义组件的值类型相匹配即可
render: (form: FormModel): VNode => {
// 自定义渲染函数,入参是表单模型
}
}
]
}
```
以下是一个例子:
```ts
const formGroups: FormGroup[] = [
{
key: 'group1',
title: 'group1 title',
fields: [
{
label: '排序',
key: 'departOrder',
initialValue: 0,
render: form => <el-input-number v-model={form.departOrder} min={0} />
},
{
label: '手机号',
key: 'mobile',
initialValue: '',
render: form => (
<el-input v-model={form.mobile} placeholder="请输入手机号" />
)
}
]
}
];
```
## 方案权衡与取舍
配置型表单需要考虑如何实现表单域的自定义渲染,`render` 函数是最终的选择,也曾经考虑过指定 `component``componentProps` 的方式。该方案的使用方法大概如下:
```ts
{
label: '手机号',
key: 'mobile',
initialValue: '',
component: 'el-input',
componentProps: {
placeholder: '请输入手机号'
}
}
```
内部核心实现:
```
<component :is="field.component" v-model="form[field.key]" v-bind="field.componentProps">
```
通过动态组件实现,传递 componentProps 以及双向绑定表单模型。
在部分场景下,该方案使用起来比 `render` 简单。但是如果考虑到渲染稍微多元化的情况,它的灵活性却不足。
例如,我们知道 el-select 组件的官方使用方法是:
```
<el-select>
<el-option label="label1" value="value1" />
</el-select>
```
el-select 下需要嵌套 el-option,该方案下就不能使用 el-select 了,需要做点额外工作。其中一种方案可以是对 el-select 封装一下,集成 el-select 与 el-option
```
<template>
<el-select v-bind="$attrs">
<el-option
v-for="option in options"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</template>
```
使用时:
```ts
{
component: 'my-select',
componentProps: {
options: [
{ label: 'label1', value: 'value1' }
]
}
}
```
另一种方案就是直接使用 `render` 函数,它相比前者的灵活性更高,但是它也有缺点,首先是配置相对复杂,另外需要开发者自行实现自定义组件与表单模型的数据双向绑定:
```ts
{
render: form => <el-input v-model={form.mobile} />;
}
```
`render` 方案下,内部使用 jsx 实现渲染。问题是:jsx 不能使用动态组件 `<component />`,因此也没办法内部实现数据双向绑定,需要开发者自行绑定。
综上,`render` 的优缺点:
- 优点:灵活性高
- 缺点:配置相对复杂,需要开发者实现表单模型双向绑定
`component` 的优缺点:
- 优点:部分场景下配置简单,且内部实现了双向绑定,无需开发者关心
- 缺点:复杂场景下灵活性不足,需要开发者封装一个整体组件包裹
既然两者都有优缺点,能不能 "我全都要",同时保留两者方案呢?不是不可以,但是 API 不统一,缺少美感。最终经过权衡取舍,选择 `render` 方案。
+4
View File
@@ -0,0 +1,4 @@
import SchemaForm from './schema-form.vue';
export * from './types';
export { SchemaForm };
+94
View File
@@ -0,0 +1,94 @@
<script lang="tsx">
import { defineComponent, ref, reactive } from 'vue';
import type { PropType } from 'vue';
import type { FormInstance } from 'element-plus';
import type { FormGroup } from './types';
export default defineComponent({
name: 'SchemaForm',
props: {
formGroups: {
type: Array as PropType<FormGroup[]>,
required: true
}
},
setup(props) {
const elFormRef = ref<FormInstance>();
const form = reactive(buildFormModel(props.formGroups));
function buildFormModel(groups: FormGroup[]) {
return groups.reduce(
(prev, group) => {
group.fields.forEach(field => {
const { key, initialValue } = field;
prev[key] = initialValue;
});
return prev;
},
{} as Record<string, any>
);
}
async function validateForm() {
return new Promise<Record<string, any>>((resolve, reject) => {
if (!elFormRef.value) {
reject('<el-form> not found.');
} else {
elFormRef.value.validate((valid, invalidFields) => {
if (valid) {
resolve({ ...form });
} else {
reject(invalidFields);
}
});
}
});
}
function setFormFieldsValue(fields: Record<string, any>) {
Object.keys(fields).forEach(key => {
const value = fields[key];
form[key] = value;
});
}
return {
elFormRef,
form,
validateForm,
setFormFieldsValue
};
},
render() {
const { form, formGroups, $attrs } = this;
return (
<el-form
ref="elFormRef"
model={form}
label-position="right"
label-width="100px"
v-bind={$attrs}
>
{formGroups.map(group => (
<div key={group.key}>
{group.title && <div>{group.title}</div>}
<el-row>
{group.fields.map(field => (
<el-col key={field.key}>
<el-form-item
label={field.label + ':'}
rules={field.rules}
prop={field.key}
>
{field.render(form)}
</el-form-item>
</el-col>
))}
</el-row>
</div>
))}
</el-form>
);
}
});
</script>
+23
View File
@@ -0,0 +1,23 @@
import type { VNode } from 'vue';
import type { FormItemRule } from 'element-plus';
export interface FormInst {
// TODO 类型推断完善
validateForm(): Promise<Record<string, any>>;
setFormFieldsValue(fields: Record<string, any>): void;
}
export interface FormField {
label: string;
key: string;
initialValue?: any;
render: (form: Record<string, any>) => VNode;
rules?: FormItemRule[];
span?: number;
}
export interface FormGroup {
key: string;
fields: FormField[];
title?: string;
}
+316
View File
@@ -0,0 +1,316 @@
<template>
<el-form
ref="selectFormRef"
:model="selectForm"
:label-width="labelWidth"
label-position="right"
>
<div
class="search-container"
:class="{ hide: isHide }"
:style="`height:${height}px`"
>
<el-row class="search-list">
<el-col
v-for="(item, index) in colOption"
:key="index"
:sm="12"
:lg="item.span"
>
<el-form-item
:label="item.label + ''"
:name="item.prop"
:label-width="item.labelWidth"
class="form-item"
>
<el-select
v-if="item.type === 'select'"
v-model="selectForm[item.prop]"
placeholder="请选择"
style="width: 100%"
:multiple="item.multiple || false"
:collapse-tags="item.collapseTags || false"
:collapse-tags-tooltip="item.collapseTagsTooltip || false"
:max-collapse-tags="item.maxCollapseTags || 1"
filterable
:clearable="item.clearable === false ? false : true"
>
<el-option
v-for="val in item.options"
:key="val[item.selectItem?.value]"
:label="val[item.selectItem?.title]"
:value="val[item.selectItem?.value]"
/>
</el-select>
<el-date-picker
v-else-if="item.type === 'date' || item.type === 'datetime'"
v-model="selectForm[item.prop]"
:type="item.type"
placeholder="选择日期"
style="width: 100%"
:clearable="item.clearable === false ? false : true"
/>
<el-date-picker
v-else-if="
item.type === 'daterange' || item.type === 'datetimerange'
"
v-model="selectForm[item.prop]"
:type="item.type"
range-separator="到"
:start-placeholder="
item.type === 'daterange' ? '开始日期' : '开始时间'
"
:end-placeholder="
item.type === 'daterange' ? '结束日期' : '结束时间'
"
v-bind="item.componentOptions"
:default-time="defaultTime"
:clearable="item.clearable === false ? false : true"
v-on="item.eventOptions || {}"
/>
<el-input
v-else-if="item.type === 'input'"
v-model="selectForm[item.prop]"
autocomplete="off"
placeholder="请输入"
:clearable="item.clearable === false ? false : true"
/>
<el-input
v-else-if="item.type == 'number'"
v-model="selectForm[item.prop]"
autocomplete="off"
placeholder="请输入"
type="input"
@input="regStr(item.prop, item.integer)"
/>
<DictSelect
v-else-if="item.type === 'dict'"
v-model="selectForm[item.prop]"
:dict-code="item.dict.dictCdoe"
:text-key="item.dict.textKey || ''"
:value-key="item.dict.valueKey || ''"
:placeholder="item.dict.placeholder"
style="width: 100%"
:clearable="item.clearable === false ? false : true"
></DictSelect>
<template v-if="item.slotName" #default>
<slot :name="item.slotName" :value="selectForm"></slot>
</template>
</el-form-item>
</el-col>
</el-row>
<div class="button-list">
<el-button @click="reset">重置</el-button>
<el-button type="primary" style="margin-left: 8px" @click="onSubmit"
>查询</el-button
>
<el-button
v-if="colOption.length > 4 && showMore"
type="primary"
link
style="margin-left: 8px"
@click="more"
>
{{ isHide ? '展开' : '收起' }}
<el-icon :size="16" style="margin-left: 4px">
<ArrowDownBold v-if="isHide" />
<ArrowUpBold v-else />
</el-icon>
</el-button>
</div>
</div>
</el-form>
</template>
<script lang="ts" setup>
import { ref, defineProps, defineEmits, onMounted, computed } from 'vue';
import dayjs from 'dayjs';
import cloneDeep from 'lodash/cloneDeep';
import type { FormInstance } from 'element-plus';
import DictSelect from '@/components/DictSelect/index.vue';
import type { ColOptionItem } from './types';
const props = withDefaults(
defineProps<{
colOption: ColOptionItem[]; // 表单配置
searchForm?: any; // 表单字段
labelWidth?: string;
showMore?: boolean;
}>(),
{
colOption: () => [],
searchForm: undefined,
labelWidth: '90px',
showMore: true
}
);
// const props = defineProps({
// colOption: {
// type: Array as () => ColOptionItem[],
// default: () => []
// },
// searchForm: {
// type: Object,
// default: () => {}
// }
// });
// 默认查询条件
let oldSelectForm = cloneDeep(props.searchForm);
const emit = defineEmits(['search', 'update:searchForm', 'reset']);
const isHide = ref(true);
const height = ref(0);
const computedHeight = () => {
let number = Math.ceil(props.colOption.length / 4);
height.value = number * 40;
};
// 表单
const selectFormRef = ref<FormInstance>();
// const selectForm = reactive({ ...cloneDeep(props.searchForm) });
// const Emits = defineEmits(['update:searchForm']);
const tempSearchForm = ref({} as any);
const selectForm = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.searchForm || tempSearchForm.value;
},
set: nv => {
tempSearchForm.value = nv;
emit('update:searchForm', nv);
}
});
// 过滤非数字
const regStr = (name: any, integer: boolean) => {
let value = selectForm.value[name];
if (!integer) {
value = value.replace(/[^\d.]/g, ''); // 只能输入数字和.
value = value.replace(/^\./g, ''); //第一个字符不能是.
value = value.replace(/\.{2,}/g, '.'); // 不能连续输入.
value = value.replace(/(\.\d+)\./g, '$1'); // .后面不能再输入.
value = value.replace(/^0+(\d)/, '$1');
}
if (integer) {
value = value.replace(/[^\d]/g, ''); // 只能输入数字
value = value.replace(/^0+(\d)/, '$1');
}
selectForm.value[name] = value;
};
const defaultTime = [
new Date(2000, 1, 1, 0, 0, 0),
new Date(2000, 2, 1, 23, 59, 59)
];
// 查询
const onSubmit = () => {
let params = cloneDeep(selectForm.value);
if (params) {
props.colOption.forEach((item: ColOptionItem) => {
// 修改时间格式
if (params[item.prop]) {
if (item.type === 'daterange') {
params[item.prop][0] = dayjs(params[item.prop][0]).format(
'YYYY-MM-DD'
);
params[item.prop][1] = dayjs(params[item.prop][1]).format(
'YYYY-MM-DD'
);
} else if (item.type === 'datetimerange') {
params[item.prop][0] = dayjs(params[item.prop][0]).format(
'YYYY-MM-DD HH:mm:ss'
);
params[item.prop][1] = dayjs(params[item.prop][1]).format(
'YYYY-MM-DD HH:mm:ss'
);
} else if (item.type === 'date') {
params[item.prop] = dayjs(params[item.prop]).format('YYYY-MM-DD');
} else if (item.type === 'datetime') {
params[item.prop] = dayjs(params[item.prop]).format(
'YYYY-MM-DD HH:mm:ss'
);
}
// 多选下拉 且返回结果是逗号分隔
else if (item.type == 'select' && item.isString) {
params[item.prop] = params[item.prop].join(',');
}
}
});
}
emit('search', params);
};
// 重置
const reset = () => {
let keys: string[] = Object.keys(selectForm.value);
keys.forEach((item: string) => {
selectForm.value[item] = null;
});
if (oldSelectForm) {
let key: string[] = Object.keys(oldSelectForm);
key.forEach((item: string) => {
selectForm.value[item] = oldSelectForm[item];
});
}
let params = cloneDeep(selectForm.value);
emit('search', params);
emit('reset', params);
};
// 更多
const more = () => {
isHide.value = !isHide.value;
};
defineExpose({ onSubmit, reset });
onMounted(() => {
computedHeight();
});
</script>
<script lang="ts">
export default {
name: 'Search'
};
</script>
<style lang="scss" scoped>
.card-title {
font-weight: 600;
}
.form-item {
margin-bottom: 8px;
margin-right: 10px;
:deep label {
line-height: 1.2;
display: flex;
align-items: center;
}
// :deep .el-form-item__content {
// max-width: 248px;
// }
:deep .el-select__tags-text {
max-width: 135px !important;
}
}
.search-container {
display: flex;
height: 40px;
overflow-y: hidden;
transition: height 0.5s ease-in-out;
.search-list {
width: calc(100% - 200px);
}
.button-list {
height: 32px;
width: 200px;
display: flex;
justify-content: right;
align-items: center;
}
}
.hide {
height: 40px !important;
transition: height 0.5s ease-in-out;
}
</style>
+20
View File
@@ -0,0 +1,20 @@
export interface ColOptionItem {
label: string;
prop: string;
span: number;
type?: string;
options?: any;
slotName?: string;
selectItem?: any;
dict?: any;
labelWidth?: string;
componentOptions?: any; // 绑定属性
clearable?: boolean;
multiple?: boolean; // 下拉多选
collapseTags?: boolean;
collapseTagsTooltip?: boolean;
maxCollapseTags?: number;
isString?: boolean; // 下拉多选指的格式 true 逗号分隔 false 数组
integer?: any; // 是否整数
eventOptions?: any; // 绑定事件调用 如input change等
}
+209
View File
@@ -0,0 +1,209 @@
<template>
<div
:class="{
'upload-disabled': isDisabled,
'w-full': true
}"
>
<el-upload
v-model:file-list="FileList"
:action="baseURL + url"
:headers="headers"
:limit="limit || 1"
:on-success="importSuccessData"
list-type="text"
:on-preview="handleFilePreview"
:before-upload="fileValidate"
:on-error="handleUploadError"
v-bind="$attrs"
:disabled="disabled"
:accept="accept"
>
<el-button
icon="Upload"
size="small"
:disabled="disabled && FileList.length === 0"
>上传文件</el-button
>
<template #tip>
<slot name="tips">
<div class="el-upload__tip" style="margin-top: 0px">
单个文件上传大小为
{{ props.limitSize || 500 }} M限制文件数 ({{
FileList.length + '/'
}}{{ limit || 1 }})
</div>
</slot>
</template>
</el-upload>
<div
v-if="disabled && FileList.length === 0"
class="empty text-[#959595] text-left"
>
{{ emptyText || '无文件' }}
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch, nextTick } from 'vue';
import {
ElMessage,
UploadFiles,
type UploadFile,
UploadRawFile
} from 'element-plus';
import { getBaseUrlAndHeaders } from '@/utils/utils';
const { baseURL, headers } = getBaseUrlAndHeaders();
const props = withDefaults(
defineProps<{
url: string;
modelValue: string | undefined;
title?: string;
limit?: number;
limitSize?: number;
disabled?: boolean;
emptyText?: string;
useMessage?: boolean;
accept?: string;
}>(),
{ useMessage: false, accept: '' }
);
const dialogFileUrl = ref('');
const FileList = ref(
props.modelValue
? props.modelValue.split(',').map(item => {
const fileNames = item.split('/');
return {
name: fileNames[fileNames.length - 1],
url: item
};
})
: []
);
const handleRemove = (file: UploadFile) => {
FileList.value.splice(
FileList.value.findIndex(item => item.url === file.url),
1
);
};
const handleFilePreview = (file: UploadFile) => {
dialogFileUrl.value = file.url!;
// console.error(dialogFileUrl.value);
window.open(dialogFileUrl.value, '_blank');
};
const maxSize = (props.limitSize || 500) * 1024 * 1024;
const fileValidate = (rawFlie: UploadRawFile) => {
if (rawFlie.name.includes(',')) {
ElMessage.error('上传文件名禁止包含 "," 字符!');
return false;
}
if (rawFlie.size > maxSize) {
ElMessage.error(`禁止上传大于${props.limitSize || 500}M的文件!`);
return false;
}
return true;
};
const importSuccessData = (response, file: UploadFile, files: UploadFiles) => {
if (response.code == 200) {
ElMessage.success(props.useMessage ? response.message : '文件上传成功');
Emits('success');
const nowFile = files.find(item => item === file);
if (nowFile) {
nowFile.url = response.message;
}
} else {
ElMessage.error(response.message);
handleRemove(file);
}
};
const handleUploadError = () => {
ElMessage.error('文件上传失败!');
};
const isDisabled = computed(() => {
if (props.limit) {
return FileList.value.length >= props.limit || !!props.disabled;
}
// console.error(props.disabled);
return FileList.value.length === 1 || !!props.disabled;
});
const Emits = defineEmits(['update:modelValue', 'change', 'success']);
const imageUrls = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
Emits('change', nv);
}
});
// 文件名称转换
const fileNameChange = (str: any) => {
let res,
name = '',
type = '';
if (str) {
res = str.split('/')[str.split('/').length - 1];
type = res.split('.')[res.split('.').length - 1];
let newName = res.split('_');
if (newName.length > 2) {
newName.splice(newName.length - 1, 1);
console.log(newName);
name = newName.join('_') + '.' + type;
} else if (newName.length == 2) {
name = newName[0] + '.' + type;
} else name = res;
}
return name;
};
watch(
() => FileList.value,
newValue => {
if (newValue instanceof Array) {
nextTick(() => {
newValue.forEach((item: any) => {
if (
!Object.prototype.hasOwnProperty.call(item, 'raw') &&
!item.isChange
) {
item.name = fileNameChange(item.name);
item.isChange = true;
}
});
});
imageUrls.value = newValue.map(item => item.url).join(',');
}
},
{
deep: true
}
);
</script>
<style scoped>
.upload-disabled :deep(.el-upload.el-upload--text) {
display: none;
}
.upload-disabled :deep(.el-upload-list) {
margin-top: 0;
}
.upload-disabled :deep(.el-upload__tip) {
display: none;
}
</style>
+197
View File
@@ -0,0 +1,197 @@
<template>
<div
:class="{
'upload-disabled': isDisable
}"
>
<el-upload
v-model:file-list="FileList"
:action="baseURL + url"
:headers="headers"
:limit="limit || 1"
:on-success="importSuccessData"
:before-upload="fileValidate"
:on-error="handleUploadError"
list-type="picture-card"
v-bind="$attrs"
:disabled="isDisable"
>
<template #file="{ file }">
<div class="w-full relative">
<img
class="el-upload-list__item-thumbnail w-full"
:src="file.url"
alt=""
/>
<span class="el-upload-list__item-actions flex items-center">
<span
class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)"
>
<el-icon><zoom-in /></el-icon>
</span>
<span class="el-upload-list__item-preview">
<a
:href="file.url"
:download="file.name || '环投项目图片下载'"
target="_blank"
>
<el-icon><download /></el-icon>
</a>
</span>
<span
v-if="!disabled"
class="el-upload-list__item-delete"
@click="handleRemove(file)"
>
<el-icon><Delete /></el-icon>
</span>
</span>
</div>
</template>
<template #default>
<el-icon><Plus /></el-icon>
</template>
</el-upload>
<div
v-if="disabled && FileList.length === 0"
class="empty text-[#959595] text-center"
style="margin-top: -32px"
>
</div>
<div class="text-center">{{ title }}</div>
</div>
<!-- <span></span> -->
<el-dialog v-model="dialogVisible" :close-on-click-modal="false">
<div class="flex justify-center mt-6">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { Delete, Plus, ZoomIn } from '@element-plus/icons-vue';
import {
ElMessage,
UploadFiles,
type UploadFile,
UploadRawFile
} from 'element-plus';
import { getBaseUrlAndHeaders } from '@/utils/utils';
const { baseURL, headers } = getBaseUrlAndHeaders();
const props = defineProps<{
url: string;
modelValue: string | undefined;
title?: string;
limit?: number;
disabled?: boolean;
}>();
const dialogImageUrl = ref('');
const dialogVisible = ref(false);
const FileList = ref(
props.modelValue
? props.modelValue.split(',').map(item => {
const fileNames = item.split('/');
return {
name: fileNames[fileNames.length - 1],
url: item
};
})
: []
);
const handleRemove = (file: UploadFile) => {
FileList.value.splice(
FileList.value.findIndex(item => item.url === file.url),
1
);
};
const handlePictureCardPreview = (file: UploadFile) => {
dialogImageUrl.value = file.url!;
dialogVisible.value = true;
};
const importSuccessData = (response, file: UploadFile, files: UploadFiles) => {
if (response.code == 200) {
ElMessage.success('图片上传成功');
const nowFile = files.find(item => item === file);
if (nowFile) {
nowFile.url = response.message;
}
} else {
ElMessage.error(response.message);
handleRemove(file);
}
};
const isDisable = computed(() => {
if (props.limit) {
return FileList.value.length >= props.limit || !!props.disabled;
}
return !!props.disabled;
});
const Emits = defineEmits(['update:modelValue', 'change']);
const imageUrls = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
Emits('change', nv);
}
});
watch(
() => FileList.value,
newValue => {
if (newValue instanceof Array) {
const target = newValue.map(item => {
return item.url;
});
imageUrls.value = target.join(',');
}
},
{
deep: true
}
);
const maxSize = 500 * 1024 * 1024;
const fileValidate = (rawFlie: UploadRawFile) => {
if (rawFlie.name.includes(',')) {
ElMessage.error('上传文件名禁止包含 "," 字符!');
return false;
}
if (rawFlie.size > maxSize) {
ElMessage.error('禁止上传大于500M的文件!');
return false;
}
return true;
};
const handleUploadError = error => {
console.error(error);
ElMessage.error('文件上传失败!');
};
</script>
<style scoped>
.upload-disabled :deep(.el-upload.el-upload--picture-card) {
display: none;
}
</style>
@@ -0,0 +1,60 @@
<template>
<input
v-show="false"
:key="selectedFile"
ref="fileRef"
type="file"
accept=".xlsx"
@change="fileChange"
/>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import axios from '@/services/http/index.ts';
import { ElMessage } from 'element-plus';
const props = defineProps<{
url: string;
}>();
const Emits = defineEmits(['fail', 'success', 'import', 'error']);
const fileRef = ref();
const selectedFile = ref(null) as any;
// 导入文件
const fileChange = (params: any) => {
selectedFile.value = fileRef.value.files?.[0];
if (selectedFile.value) {
Emits('import');
const formData = new FormData();
formData.append('file', selectedFile.value);
axios
.post(props.url, formData)
.then((res: any) => {
if (res.data.code === 200) {
if (res.data.result && res.data.result.msg) {
ElMessage.warning(res.data.result.msg);
Emits('fail', res.data.result);
} else {
ElMessage.success('导入成功');
Emits('success');
}
} else {
Emits('error');
}
// 在这里处理上传成功的情况
selectedFile.value = null;
})
.catch(error => {
console.error('失败', error);
Emits('error');
});
}
};
// 模拟点击上传
const importFilesFn = () => {
fileRef.value.dispatchEvent(new MouseEvent('click'));
};
defineExpose({
importFilesFn
});
</script>
<style scoped></style>
@@ -0,0 +1,215 @@
<template>
<CustomDialog
v-model="importDialog"
title="导入"
destroy-on-close
:show-footer="false"
:width="width"
:no-padding-top="true"
@cancel="close"
>
<div class="tips">
<div class="tips-title">{{ name }}导入提示</div>
<template v-for="(item, index) in tips" :key="index">
<div>{{ index + 1 }}{{ item }}</div>
</template>
</div>
<div class="import-buttonList">
<el-button type="primary" :loading="loading1" @click="downTemplateFn"
>导入模板下载</el-button
>
<el-button
type="primary"
class="import"
:loading="loading2"
@click="importFn"
>导入</el-button
>
<ImportFiles
ref="importFilesRef"
:url="url"
@fail="failFn"
@success="successFn"
@import="importingFN"
@error="errorFn"
></ImportFiles>
</div>
<div v-if="path" class="error">
<div v-show="errorMsg">{{ errorMsg[0] }}</div>
<div v-show="errorMsg[1]" class="errorMsg">{{ errorMsg[1] }}</div>
<div v-show="errorMsg[2]" class="errorMsg">{{ errorMsg[2] }}</div>
<el-button
class="downError"
type="danger"
:disabled="isDownloadError"
:loading="loading3"
@click="downError"
>错误信息下载</el-button
>
<div v-show="isDownloadError" class="errorMsg">
错误信息文件已下载原文件已删除
</div>
</div>
</CustomDialog>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import CustomDialog from '@/components/CustomDialog/index.vue';
import ImportFiles from './importFiles.vue';
import axios from '@/services/http/index.ts';
const props = withDefaults(
defineProps<{
url: string; // 导入地址
templateUrl: string; // 模板地址
errorUrl: string; // 错误信息地址
width: any;
modelValue: boolean;
name: string;
tips: string[];
}>(),
{
url: '',
templateUrl: '',
errorUrl: '',
name: '',
width: 300,
tips: () => []
}
);
const loading1 = ref(false); // 导入模板下载
const loading2 = ref(false); // 导入
const loading3 = ref(false); // 错误信息下载
const importFilesRef = ref();
const Emits = defineEmits(['update:modelValue', 'success']);
let path = ref(''); // 错误文件地址
let errorMsg = ref('');
// 成功回调
const successFn = () => {
importDialog.value = false;
loading2.value = false;
path.value = '';
Emits('success');
};
// 开始导入
const importingFN = () => {
loading2.value = true;
};
// 失败回调
const failFn = (error: any) => {
errorMsg.value = error.msg.split(';');
path.value = error.path;
isDownloadError.value = false;
loading2.value = false;
};
// 错误回调
const errorFn = () => {
loading2.value = false;
};
const importDialog = computed({
get: () => {
// 在每次获取弹窗显示状态时,自动修改宽度
// changeDialogWidth();
return props.modelValue;
},
set: nv => {
Emits('update:modelValue', nv);
}
});
// 关闭
const close = () => {
path.value = '';
errorMsg.value = '';
isDownloadError.value = false;
};
// 点击导入
const importFn = () => {
importFilesRef.value.importFilesFn();
};
// 下载过导入模板
let isDownloadError = ref(false);
// 导入模板下载
const downTemplateFn = async () => {
loading1.value = true;
axios
.get(props.templateUrl, {
responseType: 'blob'
})
.then(res => {
let blob = new Blob([res.data], { type: 'application/vnd.ms-excel' });
let reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = (e: any) => {
let a = document.createElement('a');
a.download = `${props.name}导入模板.xlsx`;
a.href = e.target.result;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
loading1.value = false;
})
.catch(error => {
loading1.value = false;
});
};
// 错误信息下载
const downError = async () => {
loading3.value = true;
axios
.get(props.errorUrl, {
params: {
path: path.value
},
responseType: 'blob'
})
.then(res => {
isDownloadError.value = true;
let blob = new Blob([res.data], { type: 'application/vnd.ms-excel' });
let reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = (e: any) => {
let a = document.createElement('a');
a.download = `${props.name}导入错误模板.xlsx`;
a.href = e.target.result;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
loading3.value = false;
})
.catch(error => {
loading3.value = false;
});
};
</script>
<style scoped lang="scss">
.import-buttonList {
width: 100%;
display: flex;
}
.error {
margin-top: 8px;
div {
margin-top: 4px;
}
}
.errorMsg {
color: #f56c6c;
}
.import {
margin-left: 20px;
}
.downError {
margin-top: 8px;
}
.tips {
margin: 12px 0;
.tips-title {
font-weight: bold;
font-size: 16px;
color: #f56c6c;
}
}
</style>
+16
View File
@@ -0,0 +1,16 @@
export const IS_DEV: boolean = import.meta.env.NODE_ENV === 'development';
export const PUBLIC_PATH: string = import.meta.env.VITE_APP_PUBLIC_PATH;
export const API_URL: string = import.meta.env.VITE_APP_API_URL;
export const API_URL_OUTER: string = import.meta.env.VITE_APP_API_URL_OUTER;
export const API_PATH: string = import.meta.env.VITE_APP_API_PATH;
export const ACCESS_TOKEN_HEADER_NAME = 'X-Access-Token';
export const IS_INNER = 'Is-Inner'; // 是否内网
/* ----------------- localStorage/sessionStorage key ----------------- */
export const ACCESS_TOKEN_STORAGE_KEY = '_access_token';
export const THEME_STORAGE_KEY = '_theme';
export const LAYOUT_MENU_MODE_STORAGE_KEY = '_layout_menu_mode';
export const SYSTEM_TABS_HIDE = '_system_tabs_hide';
export const SYSTEM_AUTO_HIDE = '_system_auto_hide';
export const AMAP_CENTER: [number, number] = [104.066301, 30.572961];
+41
View File
@@ -0,0 +1,41 @@
import mitt from 'mitt';
import { Emitter, EventHandlerMap, EventType } from 'mitt';
const EventBusMap = new Map();
export const useEventBus = <Events extends Record<EventType, unknown>>(
uuid: string,
all?: EventHandlerMap<Events>
): Emitter<any> & {
/**
* 危险操作,将会使其他页面的该 uuid 的 eventBus 失效
* @returns
*/
destroy: () => void;
} => {
if (EventBusMap.get(uuid)) {
return EventBusMap.get(uuid);
}
let EventBus = mitt(all) as any;
EventBusMap.set(uuid, EventBus);
EventBus.destroy = () => {
EventBus.all.clear();
EventBusMap.delete(uuid);
EventBus = null as any;
};
return EventBus;
};
/**
* 生成唯一id
* @returns
*/
export const createUuid = (): string => {
const temp_url = URL.createObjectURL(new Blob());
const uuid = temp_url.toString(); // blob:https://xxx.com/b250d159-e1b6-4a87-9002-885d90033be3
URL.revokeObjectURL(temp_url);
return uuid.substring(uuid.lastIndexOf('/') + 1);
};
+925
View File
@@ -0,0 +1,925 @@
import AMapLoader from '@amap/amap-jsapi-loader';
import '@amap/amap-jsapi-types';
import { shallowRef, watch } from 'vue';
import type {
ControlPlugin,
MapHooks,
EventEffect,
PlaceSearchOptions,
OverlayType,
FeatureType,
PiontData,
MapResult,
ShapeEditorType,
AMapBrowser,
DistrictSearchOptions,
BusSearchOptions
} from './types';
import { AMAP_CENTER } from '@/constants';
import usePreferenceStore from '@/stores/preference';
// import { Scene, type ISceneConfig } from '@antv/l7';
// import { GaodeMap } from '@antv/l7-maps';
/**
* 高德地图类型推断优化 hook
* ps: 务必记得 onMounted -> initMap -> map || Amap -> 其他加载示例
* @param key
* @param code
* @returns
*/
export const useMap = () => {
const key = import.meta.env.VITE_AMAP_KEY;
const code = import.meta.env.VITE_AMAP_TOKEN;
const map = shallowRef();
const preference = usePreferenceStore();
watch(
() => preference.theme,
(newTheme: string) => {
if (map.value) {
if (newTheme === 'dark') {
setMapStyle('amap://styles/dark');
} else {
setMapStyle('amap://styles/normal');
}
}
}
);
// window.addEventListener('resize', () => {
// if (map.value) {
// map.value.resize();
// }
// });
/**
* 初始化地图函数
* @param elementId
* @param plugins
* @param options
* @param hooks
* @returns
*/
const initMap = (
elementId: string,
plugins?: Array<string>,
options: AMap.MapOptions = {
// 设置地图容器id
viewMode: '3D', // 是否为3D地图模式
zoom: 12, // 初始化地图级别
rotation: 0, // 地图平面旋转
pitch: 0, // 地图视角
zooms: [6, 20],
// mapStyle: 'amap://styles/grey',
center: AMAP_CENTER // 初始化地图中心点位置
},
hooks?: MapHooks
): Promise<MapResult> => {
return new Promise((resolve, reject) => {
const targetWindow = window as any;
targetWindow._AMapSecurityConfig = {
securityJsCode: code
};
AMapLoader.load({
key: key, // 申请好的Web端开发者Key,首次调用 load 时必填
version: '2.0', // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins // 需要使用的的插件列表,如比例尺'AMap.Scale'等
})
.then(AMap => {
if (hooks && hooks.beforeMapCreate) {
hooks.beforeMapCreate(AMap);
}
map.value = new AMap.Map(elementId, options);
if (hooks && hooks.afterMapCreated) {
hooks.afterMapCreated(AMap, map);
}
resolve({
AMap,
map
});
})
.catch(error => {
console.error('高德地图初始化失败', error);
reject(error);
});
});
};
/**
* 加载 L7 地图并兼容原生 hook 方法
* @param elementId
* @param plugins
* @param options
* @param hooks
* @returns
*/
// const initL7Map = (
// elementId: string,
// plugins?: Array<string>,
// options: ISceneConfig = {
// id: elementId,
// map: new GaodeMap({
// pitch: 35.210526315789465,
// style: 'darkblue',
// center: [104.288144, 31.239692],
// zoom: 4.4,
// token: key,
// plugin: plugins,
// logoVisible: false
// })
// },
// hooks?: MapHooks
// ) => {
// return new Promise(resolve => {
// const targetWindow = window as any;
// targetWindow._AMapSecurityConfig = {
// securityJsCode: code
// };
// if (hooks && hooks.beforeMapCreate) {
// hooks.beforeMapCreate(AMap);
// }
// const scene = new Scene(options);
// scene.on('loaded', () => {
// map.value = scene.map;
// if (hooks && hooks.afterMapCreated) {
// hooks.afterMapCreated(window.AMap, map);
// }
// resolve({
// AMap: window.AMap,
// map,
// scene
// });
// });
// });
// };
// ================== api 类型推断小工具 =====================
/**
* 创建信息窗体
* @param AMap
* @param options
* @returns
*/
const createInfoWindow = (
AMap: any,
options: string | AMap.InfoOptions
): AMap.InfoWindow => {
if (typeof options === 'string') {
return new AMap.InfoWindow({
isCustom: true, // 使用自定义窗体
content: options, // 信息窗体的内容可以是任意 html 片段
offset: new AMap.Pixel(16, -45)
});
} else {
return new AMap.InfoWindow(options);
}
};
/**
* 生成Marker
* ps 也能自定义marker https://lbs.amap.com/api/javascript-api-v2/guide/amap-marker/custom-marker
* @param AMap
* @param MarkerOptions
* @param MarkClickEvent
*/
const createMarker = (
AMap: any,
MarkerOptions: AMap.LngLatLike | AMap.MarkerOptions,
MarkClickEvent?: EventEffect
): AMap.Marker => {
let marker: AMap.Marker;
if (MarkerOptions instanceof Array) {
marker = new AMap.Marker({
position: MarkerOptions //位置
});
} else {
marker = new AMap.Marker(MarkerOptions);
}
// map.value.add(marker) //添加到地图
if (MarkClickEvent) {
marker.on('click', MarkClickEvent); // 绑定 click 事件
}
return marker;
};
/**
* 批量生成Marker
* @param AMap
* @param MarkerList
* @param MarkClickEvent
* @param MarkerOptions
*/
const batchCreateMarker = (
AMap: any,
MarkerList: AMap.LngLatLike[],
MarkerOptions?: AMap.MarkerOptions,
MarkClickEvent?: EventEffect
): Array<AMap.Marker> => {
let markOptions: AMap.MarkerOptions = {};
if (MarkerOptions) {
markOptions = MarkerOptions;
}
return MarkerList.map(markerPosition => {
return createMarker(
AMap,
Object.assign({
position: markerPosition,
...markOptions
}),
MarkClickEvent
);
});
};
/**
* 创建海量点
* @param AMap
* @param options
* @param data
* @returns
*/
const addMassMarks = (
AMap: any,
options: AMap.MassMarkersOptions,
data: AMap.MassData[]
): AMap.MassMarks => {
const massMarks: AMap.MassMarks = new AMap.MassMarks(options);
massMarks.setData(data);
massMarks.setMap(map.value);
return massMarks;
};
/**
* 创建承载 label 的图层 - 例如 labelMarker 必须添加到 labelsLayer
* @param options
* @returns
*/
const createLabelsLayer = (
AMap: any,
options: AMap.LabelsLayerOptions
): AMap.LabelsLayer => {
return new AMap.LabelsLayer(options);
};
/**
* 创建labelMarker
* @param AMap
* @param options
* @param labelsLayer
* @returns
*/
const createLabelMarker = (
AMap: any,
options: AMap.LabelMarkerOptions,
labelsLayer: AMap.LabelsLayer | any
): AMap.LabelMarker => {
const labelMarker: AMap.LabelMarker = new AMap.LabelMarker(options);
labelsLayer.add(labelMarker);
return labelMarker;
};
/**
* 创建PolyLine
* @param AMap
* @param LineOptions
* @returns
*/
const createPolyLine = (
AMap: any,
LineOptions: AMap.PolylineOptions
): AMap.Polyline => {
return new AMap.Polyline(LineOptions);
};
/**
* 创建贝塞尔曲线
* @param AMap
* @param options
* @returns
*/
const createBezierCurve = (
AMap: any,
options: AMap.BezierCurveOptions
): AMap.BezierCurve => {
return new AMap.BezierCurve(options);
};
/**
* 创建线路编辑器
* @param AMap
* @param line
* @returns
*/
const createPolyLineEditor = (
AMap: any,
line: AMap.Polyline
): Promise<AMap.PolylineEditor> => {
return new Promise(resolve => {
AMap.plugin(['AMap.PolylineEditor'], function () {
// 实例化多边形编辑器,传入地图实例和要进行编辑的多边形实例
const polylineEditor: AMap.PolylineEditor = new AMap.PolylineEditor(
map.value,
line
);
resolve(polylineEditor);
// // 开启编辑模式
// polylineEditor.open();
});
});
};
/**
* 创建线路编辑器
* @param AMap
* @param line
* @returns
*/
const createBezierCurveEditor = (
AMap: any,
line: AMap.BezierCurve
): Promise<AMap.BezierCurveEditor> => {
return new Promise(resolve => {
AMap.plugin(['AMap.BezierCurveEditor'], function () {
// 实例化多边形编辑器,传入地图实例和要进行编辑的多边形实例
const bezierCurveEditor: AMap.BezierCurveEditor =
new AMap.BezierCurveEditor(map.value, line);
resolve(bezierCurveEditor);
});
});
};
/**
* 创建行车路径
* @param AMap
* @param markerList
* @param waypoints
* @returns
*/
const createDrivingLine = (
AMap: any,
StartEndMarkers: AMap.LngLatLike[],
Waypoints: AMap.LngLatLike[]
) => {
return new Promise((reslove, reject) => {
AMap.plugin('AMap.Driving', () => {
const driving = new AMap.Driving({
map: map.value
});
// 根据起终点经纬度规划驾车导航路线
driving.search(
StartEndMarkers[0],
StartEndMarkers[1],
{
waypoints: Waypoints
// waypoints: [new AMap.LngLat(113.301499, 23.109903)]
},
function (status: string, result: any) {
// result 即是对应的驾车导航信息,相关数据结构文档请参考 https://lbs.amap.com/api/javascript-api/reference/route-search#m_DrivingResult
if (status === 'complete') {
// console.log('绘制驾车路线完成')
reslove(result);
} else {
console.error('获取驾车数据失败:' + result);
reject(result);
}
}
);
});
});
};
/**
* 创建点聚合
* @param AMap
* @param styles
* @param points
* @returns
*/
const createMarkerClusterer = (
AMap: any,
styles: AMap.MassMarkersStyleOptions[],
points: PiontData[]
): Promise<AMap.MarkerClusterer> => {
return new Promise(resolve => {
AMap.plugin(['AMap.MarkerClusterer'], () => {
const cluster: AMap.MarkerClusterer = new AMap.MarkerClusterer(
map, // 地图实例
points, // 海量点数据,数据中需包含经纬度信息字段 lnglat
{
styles: styles
}
);
resolve(cluster);
});
});
};
/**
* 获取坐标在地图上的位置信息
* @param AMap
* @param TargetLngLat
* @returns
*/
const getPixelInContainer = (
AMap: any,
TargetLngLat: AMap.LngLatLike
): AMap.Pixel => {
if (TargetLngLat instanceof Array) {
const lnglat = new AMap.LngLat(TargetLngLat[0], TargetLngLat[1]);
return map.value.lngLatToContainer(lnglat);
}
return map.value.lngLatToContainer(TargetLngLat);
};
/**
* 转化其他坐标系为高德地图坐标地址
*
* 地球坐标系——WGS84:常见于 GPS 设备,Google 地图等国际标准的坐标体系。
* 火星坐标系——GCJ-02:中国国内使用的被强制加密后的坐标体系,高德坐标就属于该种坐标体系。
* 百度坐标系——BD-09:百度地图所使用的坐标体系,是在火星坐标系的基础上又进行了一次加密处理。
*
* @param AMap
* @param TargetLngLat
* @returns
*/
const getAmapLngLat = (
AMap: any,
TargetLngLat: [number, number]
): Promise<AMap.LngLat | AMap.LngLat[]> => {
return new Promise((resolve, reject) => {
// 参数说明:需要转换的坐标,需要转换的坐标类型,转换成功后的回调函数
AMap.convertFrom(
TargetLngLat,
'gps',
function (_status: any, result: any) {
if (result.info === 'ok') {
resolve(result.locations); // 转换后的高德坐标 Array.<LngLat>
}
reject(result);
}
);
});
};
/**
* 获取地图覆盖物
* @param overlays
* @returns
*/
const getOverlays = (overlays?: OverlayType | Array<OverlayType>) => {
return map.value.getAllOverlays(overlays);
};
/**
* 移除地图覆盖物
* @param overlays
* @returns
*/
const removeOverlays = (overlays?: OverlayType | Array<OverlayType>) => {
return map.value.remove(overlays);
};
/**
* 获取高德集合应用
* @param AMap
* @returns
*/
const getGeometryUtil = (AMap: any): AMap.IGeometryUtil => {
return AMap.GeometryUtil;
};
/**
* 获取地图浏览器信息
* @param AMap
* @returns
*/
const getAMapBrowser = (AMap: any): AMapBrowser => {
return AMap.browser;
};
/**
* 清空地图所有内容
*/
const clearMap = () => {
if (map.value) {
map.value.clearMap();
}
};
/**
* 创建右侧菜单
* @param AMap
* @param content
* @returns
*/
const createContextMenu = (
AMap: any,
content: string | Array<string>,
options?: any
): AMap.ContextMenu => {
return new AMap.ContextMenu({
isCustom: true,
content: content instanceof Array ? content.join('') : content,
...options
});
};
/**
* 创建多边形区域
* @param AMap
* @param options
* @returns
*/
const createPolygon = (
AMap: any,
options: AMap.PolygonOptions
): AMap.Polygon => {
return new AMap.Polygon(options);
};
/**
* 创建圆形 面
* @param AMap
* @param options
* @returns
*/
const createCircle = (
AMap: any,
options: AMap.CircleOptions & {
borderWeight?: number;
}
): AMap.Circle => {
return new AMap.Circle(options);
};
/**
* 创建矩形 面
* @param AMap
* @param options
* @returns
*/
const createRectangle = (
AMap: any,
options: AMap.RectangleOptions
): AMap.Rectangle => {
return new AMap.Rectangle(options);
};
/**
* 创建圆形 面
* @param AMap
* @param options
* @returns
*/
const createEllipse = (
AMap: any,
options: AMap.EllipseOptions
): AMap.Ellipse => {
return new AMap.Ellipse(options);
};
/**
* 创建区域编辑器
* @param AMap
* @param area
* @returns
*/
const createPolygonEditor = (
AMap: any,
area: AMap.Polygon
): Promise<AMap.PolygonEditor> => {
return new Promise(resolve => {
AMap.plugin(['AMap.PolygonEditor'], function () {
// 实例化多边形编辑器,传入地图实例和要进行编辑的多边形实例
const polylineEditor: AMap.PolygonEditor = new AMap.PolygonEditor(
map.value,
area
);
resolve(polylineEditor);
});
});
};
/**
* 面类型编辑器创建
* @param AMap
* @param shape
* @param editorType
* @returns
*/
const createShapeEditor = (
AMap: any,
shape: AMap.Circle | AMap.Rectangle | AMap.Ellipse,
editorType: ShapeEditorType
): Promise<AMap.CircleEditor | AMap.RectangleEditor | AMap.EllipseEditor> => {
return new Promise((resolve, reject) => {
AMap.plugin([editorType], () => {
switch (editorType) {
case 'AMap.CircleEditor':
resolve(new AMap.CircleEditor(map.value, shape));
break;
case 'AMap.RectangleEditor':
resolve(new AMap.RectangleEditor(map.value, shape));
break;
case 'AMap.EllipseEditor':
resolve(new AMap.EllipseEditor(map.value, shape));
break;
default:
reject('未知类型入参');
break;
}
});
});
};
/**
* 创建区域搜索
* @param AMap
* @param options
* @returns
*/
const createPlaceSearch = (
AMap: any,
options: PlaceSearchOptions = {
pageSize: 5, // 单页显示结果条数
pageIndex: 1, // 页码
city: '020', // 兴趣点城市
citylimit: true, //是否强制限制在设置的城市内搜索
map: map.value, // 展现结果的地图实例
panel: 'panel', // 结果列表将在此容器中进行展示。
autoFitView: true // 是否自动调整地图视野使绘制的 Marker点都处于视口的可见范围
}
) => {
return new Promise(reslove => {
AMap.plugin('AMap.PlaceSearch', () => {
// console.error(result);
reslove(new AMap.PlaceSearch(options));
// return new AMap.PlaceSearch(options)
});
});
};
/**
* 行政区搜索
* @param AMap
* @returns
*/
const createDistrictSearch = (
AMap: any,
options: DistrictSearchOptions
): Promise<any> => {
return new Promise(resolve => {
AMap.plugin('AMap.DistrictSearch', function () {
const districtSearch = new AMap.DistrictSearch(options);
resolve(districtSearch);
});
});
};
/**
* 地理编码 - 正反向
* @param AMap
* @param options
* @param searchVal
* @returns
*/
const createGeocoder = (
AMap: any,
options: {
city?: string | number;
},
searchVal: string | AMap.LngLatLike
) => {
return new Promise(resolve => {
AMap.plugin('AMap.Geocoder', function () {
const geocoder = new AMap.Geocoder(options);
if (typeof searchVal === 'string') {
geocoder.getLocation(
searchVal,
function (status: string, result: any) {
if (status === 'complete' && result.info === 'OK') {
// result中对应详细地理坐标信息
resolve(result);
}
}
);
} else {
geocoder.getAddress(
searchVal,
function (status: string, result: any) {
if (status === 'complete' && result.info === 'OK') {
// result为对应的地理位置详细信息
resolve(result);
}
}
);
}
});
});
};
/**
* 创建公交站点查询
* @param AMap
* @param options
* @returns
*/
const createStationSearch = (AMap: any, options: BusSearchOptions) => {
return new Promise(resolve => {
AMap.plugin(['AMap.StationSearch'], function () {
//实例化公交站点查询类
const stationSearch = new AMap.StationSearch(options);
resolve(stationSearch);
});
});
};
/**
* 创建公交线路查询
* @param AMap
* @param options
* @returns
*/
const createLineSearch = (AMap: any, options: BusSearchOptions) => {
return new Promise(resolve => {
AMap.plugin(['AMap.LineSearch'], function () {
//实例化公交站点查询类
const lineSearch = new AMap.LineSearch(options);
resolve(lineSearch);
});
});
};
/**
* 创建天气查询
* @param AMap
* @param options
* @returns
*/
const createWeatherSearch = (AMap: any): Promise<any> => {
return new Promise(resolve => {
AMap.plugin(['AMap.Weather'], function () {
//实例化公交站点查询类
const weatherSearch = new AMap.Weather();
resolve(weatherSearch);
});
});
};
// ============== Control 控件 ===================
/**
* 添加地图控件
* @param AMap
* @param plugins
*/
const addControls = (AMap: any, plugins: Array<ControlPlugin>) => {
AMap.plugin(plugins, () => {
plugins.forEach(pluginName => {
switch (pluginName) {
case 'AMap.ToolBar':
map.value.addControl(new AMap.ToolBar());
break;
case 'AMap.Scale':
map.value.addControl(new AMap.Scale());
break;
case 'AMap.HawkEye':
map.value.addControl(new AMap.HawkEye({ isOpen: true }));
break;
case 'AMap.MapType':
map.value.addControl(new AMap.MapType());
break;
case 'AMap.Geolocation':
map.value.addControl(new AMap.Geolocation());
break;
default:
break;
}
});
});
};
/**
* 创建鼠标绘制工具
* @param AMap
* @returns
*/
const createMouseTool = (AMap: any): AMap.MouseTool => {
return new AMap.MouseTool(map.value);
};
// ========== Event 事件 ==========
/**
* 添加右点击事件
* @param effect
*/
const addRightClickEvent = (effect: EventEffect) => {
map.value.on('rightclick', effect);
};
// ============= TODO: Layer Hook ==============
/**
* 地图适配合适的视野范围 - 可传入需要覆盖物
* @param coverList
*/
const autoFitView = (coverList?: Array<any>) => {
map.value.setFitView(coverList);
};
/**
* 设置地图样式
* @param style
*/
const setMapStyle = (style: string) => {
map.value.setMapStyle(style);
};
/**
* 设置地图要素显示
* @param features
*/
const setMapFeatures = (features: FeatureType | Array<FeatureType>) => {
map.value.setFeatures(features);
};
return {
map,
initMap, // 初始化地图
// initL7Map,
createInfoWindow, // 创建信息窗体
createMarker, // 创建标记物
batchCreateMarker, // 批量创建标记物
createPolyLine, // 创建折现
createPolyLineEditor, // 创建折现编辑器
createDrivingLine, // 创建行驶路线
createContextMenu, // 创建右键菜单
createPolygon, // 创建多边形
createPolygonEditor, // 创建多边形
createPlaceSearch, // 地区搜索
createLabelsLayer, // labelmarker 图层
createLabelMarker, // 创建 labelmarker
createMarkerClusterer, // 创建点聚合
createBezierCurve, // 创建贝塞尔曲线
createBezierCurveEditor, // 创建贝塞尔曲线编辑器
createRectangle, // 创建矩形
createEllipse, // 创建椭圆形
createCircle, // 创建原型
createShapeEditor, // 创建多边形面编辑器
createDistrictSearch, // 行政区搜索
createGeocoder, // 地理编码 (正反)
createStationSearch, // 公交站点查询
createLineSearch, // 公交线路查询
createWeatherSearch, // 天气查询
createMouseTool, // 鼠标绘制工具
addControls, // 添加控件
addRightClickEvent, // 添加右键触发事件
addMassMarks, // 创建海量点 (直接设置于map)
setMapStyle, // 设置地图样式
setMapFeatures, // 设置地图要素显示
getPixelInContainer, // 通过坐标获取地图像素位
getAmapLngLat, // 转化其他坐标系为高德地图坐标系
getOverlays, // 获取地图覆盖物
getGeometryUtil, // 高德地图内置几何应用
getAMapBrowser, // 获取地图浏览器信息
removeOverlays, // 移除地图覆盖物
clearMap, // 清除地图
autoFitView // 自动适应地图位置
};
};
export default useMap;
+84
View File
@@ -0,0 +1,84 @@
import type { Ref } from 'vue';
// 地图initMap返回结果
export interface MapResult {
AMap: any;
map: Ref<AMap.Map>;
}
// hook生命周期函数
export interface MapHooks {
beforeMapCreate?: BeforeMapCreate;
afterMapCreated?: AfterMapCreated;
}
export type AfterMapCreated = (AMap?: any, Map?: any) => void;
export type BeforeMapCreate = (AMap?: any) => void;
// 控件插件类型
export type ControlPlugin =
| 'AMap.ToolBar'
| 'AMap.Scale'
| 'AMap.HawkEye'
| 'AMap.MapType'
| 'AMap.Geolocation';
// 地图事件
interface MapEvent extends Event {
target: any;
lnglat: AMap.LngLat;
pixel: AMap.Pixel;
type: string;
}
// 地图事件函数
export type EventEffect = (e?: MapEvent) => void;
// 区域搜索选择
export interface PlaceSearchOptions {
pageSize: number; // 单页显示结果条数
pageIndex: number; // 页码
city: string; // 兴趣点城市
citylimit: boolean; //是否强制限制在设置的城市内搜索
map: any; // 展现结果的地图实例
panel: string; // 结果列表将在此容器中进行展示。
autoFitView: boolean; // 是否自动调整地图视野使绘制的 Marker点都处于视口的可见范围
}
// 覆盖物类型
export type OverlayType = 'marker' | 'circle' | 'polyline' | 'polygon';
// 地图要素内容
export type FeatureType = 'bg' | 'point' | 'road' | 'building';
export interface PiontData {
lnglat: AMap.LngLatLike | [number, number];
weight?: number;
}
// 多边形类型
export type ShapeEditorType =
| 'AMap.CircleEditor'
| 'AMap.RectangleEditor'
| 'AMap.EllipseEditor';
// 获取 AMap 浏览器信息
export interface AMapBrowser {
ua: any;
plat: string;
canvas: boolean;
isWebGL: boolean;
}
// 行政区搜索
export interface DistrictSearchOptions {
level: string;
subdistrict: number;
}
export interface BusSearchOptions {
pageIndex: number; //页码,默认值为1
pageSize: number; //单页显示结果条数,默认值为20,最大值为50
city: string; //限定查询城市,可以是城市名(中文/中文全拼)、城市编码,默认值为『全国』
extensions?: string; //是否返回公交线路详细信息,默认值为『base』
}
+11
View File
@@ -0,0 +1,11 @@
import useRequestImpl from './useRequestImpl';
import type { Service, Options } from './types';
function useRequest<P extends any[], D, E extends Error>(
service: Service<P, D>,
options: Options<P, D, E> = {}
) {
return useRequestImpl(service, options);
}
export default useRequest;

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