web-模块、权限、成就

This commit is contained in:
2025-10-25 17:45:47 +08:00
parent f7057a0cc9
commit 5d211faee1
32 changed files with 4024 additions and 876 deletions

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 386 B

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 979 B

After

Width:  |  Height:  |  Size: 979 B

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 377 B

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1,414 @@
<svg width="120" height="115" viewBox="0 0 120 115" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_647_3457)">
<path d="M77.3358 54.2863L130.062 81.7L119.574 99.8654L69.47 67.9104L77.3358 54.2863Z" fill="url(#paint0_linear_647_3457)"/>
<path d="M68.5782 68.9654L71.2002 128.334H50.2245L52.8465 68.9654H68.5782Z" fill="url(#paint1_linear_647_3457)"/>
<path d="M52.4374 37.3686L49.8154 -22L70.7911 -22L68.1691 37.3686H52.4374Z" fill="url(#paint2_linear_647_3457)"/>
<path d="M51.592 68.099L1.48828 100.054L-8.99955 81.8885L43.7261 54.4749L51.592 68.099Z" fill="url(#paint3_linear_647_3457)"/>
<path d="M77.3358 52.0864L130.062 24.6729L119.574 6.50741L69.47 38.4624L77.3358 52.0864Z" fill="url(#paint4_linear_647_3457)"/>
<path d="M51.592 38.2748L1.48828 6.31982L-8.99955 24.4853L43.7261 51.8989L51.592 38.2748Z" fill="url(#paint5_linear_647_3457)"/>
<g opacity="0.3" filter="url(#filter0_f_647_3457)">
<circle cx="40.7228" cy="9.75311" r="2.07342" fill="#FFEBC4"/>
</g>
<g filter="url(#filter1_f_647_3457)">
<circle cx="31.99" cy="84.8855" r="0.69114" fill="#FFEBC4"/>
</g>
<g opacity="0.3" filter="url(#filter2_f_647_3457)">
<circle cx="37.7068" cy="21.7131" r="0.69114" fill="#FFEBC4"/>
</g>
<g filter="url(#filter3_f_647_3457)">
<circle cx="95.9782" cy="62.2856" r="0.69114" fill="#FFEBC4"/>
</g>
<g filter="url(#filter4_f_647_3457)">
<circle cx="12.6358" cy="56.1849" r="2.3038" fill="#FFDDAA"/>
</g>
<g opacity="0.6" filter="url(#filter5_f_647_3457)">
<circle cx="117.824" cy="46.2827" r="1.84304" fill="#FFDDAA"/>
</g>
<g opacity="0.8" filter="url(#filter6_f_647_3457)">
<circle cx="41.5833" cy="106.46" r="1.84304" fill="#FFDDAA"/>
</g>
<g filter="url(#filter7_dd_647_3457)">
<path d="M32.2344 40.5559L34.3888 29.5201L2.98719 23.3898C1.64066 30.2871 5.70884 35.3775 9.67068 36.1509L32.2344 40.5559Z" fill="url(#paint6_linear_647_3457)"/>
<path d="M31.9141 40.7712L34.0685 29.7355L3.46862 23.7616C2.12209 30.659 6.19027 35.7493 10.1521 36.5228L31.9141 40.7712Z" stroke="url(#paint7_linear_647_3457)" stroke-width="0.624672"/>
<path d="M32.4414 50.8618L34.5959 39.826L8.53918 34.7391C7.19265 41.6365 11.2608 46.7268 15.2227 47.5003L32.4414 50.8618Z" fill="url(#paint8_linear_647_3457)"/>
<path d="M32.4414 50.8618L34.5959 39.826L8.53918 34.7391C7.19265 41.6365 11.2608 46.7268 15.2227 47.5003L32.4414 50.8618Z" stroke="url(#paint9_linear_647_3457)" stroke-width="0.624672"/>
<path d="M39.7236 62.4668L41.8781 51.431L15.8214 46.3441C14.4749 53.2415 18.5431 58.3318 22.5049 59.1053L39.7236 62.4668Z" fill="url(#paint10_linear_647_3457)"/>
<path d="M39.7236 62.4668L41.8781 51.431L15.8214 46.3441C14.4749 53.2415 18.5431 58.3318 22.5049 59.1053L39.7236 62.4668Z" stroke="url(#paint11_linear_647_3457)" stroke-width="0.624672"/>
<g filter="url(#filter8_d_647_3457)">
<path d="M33.7939 90.1085V65.9517H47.817V98.2184L34.2206 90.8262C33.9577 90.6832 33.7939 90.4078 33.7939 90.1085Z" fill="#ECD8BA"/>
<path d="M33.7939 90.1085V65.9517H47.817V98.2184L34.2206 90.8262C33.9577 90.6832 33.7939 90.4078 33.7939 90.1085Z" stroke="url(#paint12_linear_647_3457)" stroke-width="0.272293"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.3383 89.4068V78.2048H35.1561V89.5585L35.2322 89.5712C35.4841 89.6132 35.7641 89.7622 35.9928 89.98C36.2213 90.1976 36.3904 90.476 36.4323 90.7695L36.4393 90.8182L43.8096 94.2701V81.9424H43.6274V93.9836L36.6046 90.6945C36.5446 90.3697 36.3563 90.0745 36.1184 89.8481C35.8903 89.6308 35.6099 89.4702 35.3383 89.4068Z" fill="url(#paint13_linear_647_3457)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.6163 93.3173C43.8128 92.9852 43.824 92.514 43.8088 92.2097L43.6268 92.2188C43.6419 92.5218 43.6259 92.9433 43.4595 93.2245C43.3789 93.3609 43.2647 93.4613 43.1004 93.5025C42.9333 93.5445 42.7023 93.5282 42.3845 93.4046L42.3185 93.5744C42.6565 93.7059 42.9281 93.7336 43.1448 93.6792C43.3644 93.624 43.5155 93.4876 43.6163 93.3173Z" fill="#D4B195"/>
<g filter="url(#filter9_d_647_3457)">
<path d="M86.7559 90.1085V65.9517H72.7328V98.2184L86.3292 90.8262C86.5921 90.6832 86.7559 90.4078 86.7559 90.1085Z" fill="#ECD8BA"/>
<path d="M86.7559 90.1085V65.9517H72.7328V98.2184L86.3292 90.8262C86.5921 90.6832 86.7559 90.4078 86.7559 90.1085Z" stroke="url(#paint14_linear_647_3457)" stroke-width="0.272293"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M85.2125 89.4068V78.2048H85.3947V89.5585L85.3186 89.5712C85.0667 89.6132 84.7867 89.7622 84.558 89.98C84.3295 90.1976 84.1604 90.476 84.1185 90.7695L84.1115 90.8182L76.7412 94.2701V81.9424H76.9234V93.9836L83.9462 90.6945C84.0061 90.3697 84.1945 90.0745 84.4323 89.8481C84.6605 89.6308 84.9409 89.4702 85.2125 89.4068Z" fill="url(#paint15_linear_647_3457)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M76.9335 93.3173C76.7371 92.9852 76.7258 92.514 76.7411 92.2097L76.923 92.2188C76.9079 92.5218 76.9239 92.9433 77.0903 93.2245C77.171 93.3609 77.2851 93.4613 77.4494 93.5025C77.6165 93.5445 77.8475 93.5282 78.1653 93.4046L78.2313 93.5744C77.8933 93.7059 77.6217 93.7336 77.405 93.6792C77.1854 93.624 77.0343 93.4876 76.9335 93.3173Z" fill="#D4B195"/>
<path d="M88.1299 40.9106L85.9754 29.8749L117.377 23.7445C118.724 30.6419 114.655 35.7322 110.694 36.5057L88.1299 40.9106Z" fill="url(#paint16_linear_647_3457)"/>
<path d="M88.4502 41.127L86.2957 30.0912L116.896 24.1173C118.242 31.0147 114.174 36.105 110.212 36.8785L88.4502 41.127Z" stroke="url(#paint17_linear_647_3457)" stroke-width="0.624672"/>
<path d="M87.9229 51.217L85.7684 40.1813L111.825 35.0944C113.172 41.9917 109.103 47.0821 105.142 47.8555L87.9229 51.217Z" fill="url(#paint18_linear_647_3457)"/>
<path d="M87.9229 51.217L85.7684 40.1813L111.825 35.0944C113.172 41.9917 109.103 47.0821 105.142 47.8555L87.9229 51.217Z" stroke="url(#paint19_linear_647_3457)" stroke-width="0.624672"/>
<path d="M80.6416 62.8218L78.4871 51.786L104.544 46.6991C105.89 53.5965 101.822 58.6868 97.8603 59.4603L80.6416 62.8218Z" fill="url(#paint20_linear_647_3457)"/>
<path d="M80.6416 62.8218L78.4871 51.786L104.544 46.6991C105.89 53.5965 101.822 58.6868 97.8603 59.4603L80.6416 62.8218Z" stroke="url(#paint21_linear_647_3457)" stroke-width="0.624672"/>
<g filter="url(#filter10_d_647_3457)">
<path d="M45.2314 64.5894H75.1837V102.625L60.3741 110.153C60.1498 110.267 59.8845 110.267 59.6606 110.152L45.2314 102.756V64.5894Z" fill="url(#paint22_linear_647_3457)"/>
<path d="M45.2314 64.5894H75.1837V102.625L60.3741 110.153C60.1498 110.267 59.8845 110.267 59.6606 110.152L45.2314 102.756V64.5894Z" stroke="url(#paint23_linear_647_3457)" stroke-width="0.544586"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.5646 107.752L60.3747 109.882C60.1504 109.996 59.8851 109.995 59.6612 109.88L55.3066 107.648V79.8384H64.5646V107.752Z" fill="url(#paint24_linear_647_3457)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M46.8652 85.0117H47.2991V102.035L60.1001 108.652L73.1161 102.035V85.0117H73.55V102.189L60.0982 108.974L46.8652 102.188V85.0117Z" fill="url(#paint25_linear_647_3457)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M60.1888 102.71V107.651H59.9521V102.71H60.1888Z" fill="url(#paint26_linear_647_3457)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.3592 105.898C58.8881 106.441 58.3141 106.562 57.9189 106.496L57.9579 106.263C58.2727 106.315 58.7639 106.223 59.1804 105.743C59.5985 105.26 59.9503 104.376 59.9503 102.829H60.1869C60.1869 104.407 59.8286 105.356 59.3592 105.898Z" fill="url(#paint27_linear_647_3457)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M60.7795 105.898C61.2506 106.441 61.8245 106.562 62.2197 106.496L62.1808 106.263C61.8659 106.315 61.3748 106.223 60.9583 105.743C60.5402 105.26 60.1884 104.376 60.1884 102.829H59.9517C59.9517 104.407 60.31 105.356 60.7795 105.898Z" fill="url(#paint28_linear_647_3457)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M60.0202 107.759L57.3477 106.261L57.9394 106.261L60.0697 107.521L62.1998 106.25H62.7221L60.1185 107.759C60.0873 107.773 60.0514 107.773 60.0202 107.759Z" fill="url(#paint29_linear_647_3457)"/>
<g filter="url(#filter11_d_647_3457)">
<path d="M57.9955 14.8737L28.5879 30.8712C27.0982 31.6815 26.1709 33.2416 26.1709 34.9374L26.1709 71.5059C26.1709 73.2017 27.0982 74.7618 28.5879 75.5722L57.9955 91.5696C59.3748 92.3199 61.0403 92.3199 62.4195 91.5696L91.8272 75.5722C93.3169 74.7618 94.2442 73.2017 94.2442 71.5059L94.2442 34.9374C94.2442 33.2416 93.3169 31.6815 91.8272 30.8712L62.4195 14.8737C61.0403 14.1234 59.3748 14.1234 57.9955 14.8737Z" fill="url(#paint30_linear_647_3457)"/>
<path d="M57.9643 20.8602L33.7536 34.0305C32.2695 34.8378 31.3457 36.392 31.3457 38.0815L31.3457 68.2254C31.3457 69.9148 32.2695 71.469 33.7536 72.2763L57.9643 85.4467C59.3383 86.1942 60.9976 86.1942 62.3717 85.4467L86.5824 72.2763C88.0664 71.469 88.9902 69.9148 88.9902 68.2254L88.9902 38.0815C88.9902 36.392 88.0664 34.8378 86.5824 34.0305L62.3717 20.8602C60.9976 20.1127 59.3383 20.1127 57.9643 20.8602Z" fill="url(#paint31_linear_647_3457)"/>
<g filter="url(#filter12_f_647_3457)">
<path d="M57.9643 20.8602L33.7536 34.0305C32.2695 34.8378 31.3457 36.392 31.3457 38.0815L31.3457 68.2254C31.3457 69.9148 32.2695 71.469 33.7536 72.2763L57.9643 85.4467C59.3383 86.1942 60.9976 86.1942 62.3717 85.4467L86.5824 72.2763C88.0664 71.469 88.9902 69.9148 88.9902 68.2254L88.9902 38.0815C88.9902 36.392 88.0664 34.8378 86.5824 34.0305L62.3717 20.8602C60.9976 20.1127 59.3383 20.1127 57.9643 20.8602Z" stroke="url(#paint32_linear_647_3457)" stroke-width="0.272293"/>
</g>
<g opacity="0.5">
<mask id="mask0_647_3457" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="33" y="22" width="54" height="62">
<path d="M58.2497 23.5223L36.0878 35.5673C34.6753 36.335 33.7959 37.8138 33.7959 39.4215L33.7959 66.9906C33.7959 68.5982 34.6753 70.0771 36.0878 70.8448L58.2497 82.8898C59.556 83.5998 61.133 83.5998 62.4392 82.8898L84.6012 70.8448C86.0137 70.0771 86.893 68.5982 86.893 66.9906L86.8931 39.4215C86.8931 37.8138 86.0137 36.335 84.6012 35.5673L62.4392 23.5223C61.133 22.8123 59.556 22.8123 58.2497 23.5223Z" fill="#A6D3DE"/>
</mask>
<g mask="url(#mask0_647_3457)">
<path d="M58.2497 23.5223L36.0878 35.5673C34.6753 36.335 33.7959 37.8138 33.7959 39.4215L33.7959 66.9906C33.7959 68.5982 34.6753 70.0771 36.0878 70.8448L58.2497 82.8898C59.556 83.5998 61.133 83.5998 62.4392 82.8898L84.6012 70.8448C86.0137 70.0771 86.893 68.5982 86.893 66.9906L86.8931 39.4215C86.8931 37.8138 86.0137 36.335 84.6012 35.5673L62.4392 23.5223C61.133 22.8123 59.556 22.8123 58.2497 23.5223Z" fill="url(#paint33_radial_647_3457)"/>
<path opacity="0.5" d="M55.9081 85.2464L54.8535 21.1636H65.8391L64.7845 85.2464H55.9081Z" fill="url(#paint34_linear_647_3457)"/>
<path opacity="0.5" d="M55.7672 84.6778L22.8125 29.7078L32.3263 24.215L63.4544 80.2396L55.7672 84.6778Z" fill="url(#paint35_linear_647_3457)"/>
<path opacity="0.5" d="M54.8495 80.239L85.9775 24.2144L95.4914 29.7072L62.5366 84.6772L54.8495 80.239Z" fill="url(#paint36_linear_647_3457)"/>
<rect x="33.7959" y="49.543" width="53.0972" height="34.4826" fill="url(#paint37_linear_647_3457)"/>
<g filter="url(#filter13_i_647_3457)">
<path d="M58.2497 23.5223L36.0878 35.5673C34.6753 36.335 33.7959 37.8138 33.7959 39.4215L33.7959 66.9906C33.7959 68.5982 34.6753 70.0771 36.0878 70.8448L58.2497 82.8898C59.556 83.5998 61.133 83.5998 62.4392 82.8898L84.6012 70.8448C86.0137 70.0771 86.893 68.5982 86.893 66.9906L86.8931 39.4215C86.8931 37.8138 86.0137 36.335 84.6012 35.5673L62.4392 23.5223C61.133 22.8123 59.556 22.8123 58.2497 23.5223Z" fill="#6052B4" fill-opacity="0.01"/>
</g>
</g>
</g>
</g>
</g>
<g clip-path="url(#clip1_647_3457)">
<g filter="url(#filter14_d_647_3457)">
<path d="M60.5274 40.4265L45.2393 49.3446L60.5274 58.2627L73.2675 50.831V60.1737H75.8155V49.3446L60.5274 40.4265ZM50.3341 55.0655V60.8108C52.6584 63.9053 56.359 65.9069 60.5271 65.9069C64.6952 65.9069 68.3958 63.9053 70.7201 60.8108L70.7196 55.0666L60.5278 61.0119L50.3341 55.0655Z" fill="url(#paint38_linear_647_3457)"/>
</g>
</g>
</g>
<defs>
<filter id="filter0_f_647_3457" x="33.1203" y="2.15057" width="15.2047" height="15.205" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.76456" result="effect1_foregroundBlur_647_3457"/>
</filter>
<filter id="filter1_f_647_3457" x="30.8381" y="83.7336" width="2.30336" height="2.30384" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.23038" result="effect1_foregroundBlur_647_3457"/>
</filter>
<filter id="filter2_f_647_3457" x="34.8373" y="18.8436" width="5.73853" height="5.73901" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="1.08917" result="effect1_foregroundBlur_647_3457"/>
</filter>
<filter id="filter3_f_647_3457" x="93.6534" y="59.9607" width="4.64935" height="4.64984" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.816879" result="effect1_foregroundBlur_647_3457"/>
</filter>
<filter id="filter4_f_647_3457" x="4.88617" y="48.4352" width="15.4991" height="15.4994" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.72293" result="effect1_foregroundBlur_647_3457"/>
</filter>
<filter id="filter5_f_647_3457" x="111.374" y="39.8321" width="12.9017" height="12.9012" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.3038" result="effect1_foregroundBlur_647_3457"/>
</filter>
<filter id="filter6_f_647_3457" x="35.1326" y="100.009" width="12.9017" height="12.9012" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.3038" result="effect1_foregroundBlur_647_3457"/>
</filter>
<filter id="filter7_dd_647_3457" x="-22.2818" y="-9.02824" width="164.928" height="146.212" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="4.16773"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.991667 0 0 0 0 0.801597 0 0 0 0 0.516493 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3457"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1.66709"/>
<feGaussianBlur stdDeviation="12.5032"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.722667 0 0 0 0 0.466667 0 0 0 0.6 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_647_3457" result="effect2_dropShadow_647_3457"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_647_3457" result="shape"/>
</filter>
<filter id="filter8_d_647_3457" x="31.4799" y="63.6371" width="18.6516" height="36.9885" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.08917"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.7 0 0 0 0 0.561568 0 0 0 0 0.32375 0 0 0 0.78 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3457"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_647_3457" result="shape"/>
</filter>
<filter id="filter9_d_647_3457" x="70.4183" y="63.6371" width="18.6516" height="36.9885" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.08917"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.7 0 0 0 0 0.561568 0 0 0 0 0.32375 0 0 0 0.78 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3457"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_647_3457" result="shape"/>
</filter>
<filter id="filter10_d_647_3457" x="42.7806" y="62.1388" width="34.8538" height="50.55" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.08917"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.7 0 0 0 0 0.561568 0 0 0 0 0.32375 0 0 0 0.78 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3457"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_647_3457" result="shape"/>
</filter>
<filter id="filter11_d_647_3457" x="24.5371" y="14.311" width="71.3408" height="81.6334" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2.17834"/>
<feGaussianBlur stdDeviation="0.816879"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.4375 0 0 0 0 0.287401 0 0 0 0 0.111198 0 0 0 0.6 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3457"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_647_3457" result="shape"/>
</filter>
<filter id="filter12_f_647_3457" x="31.1283" y="20.0816" width="58.0794" height="66.1436" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.040844" result="effect1_foregroundBlur_647_3457"/>
</filter>
<filter id="filter13_i_647_3457" x="33.7959" y="22.9897" width="53.0967" height="60.4326" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.83094"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.458333 0 0 0 0 0.256743 0 0 0 0 0.0706597 0 0 0 1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_647_3457"/>
</filter>
<filter id="filter14_d_647_3457" x="43.3683" y="39.4418" width="34.5151" height="29.4194" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="0.0984731" dy="0.984731"/>
<feGaussianBlur stdDeviation="0.984731"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.795833 0 0 0 0 0.519745 0 0 0 0 0.195642 0 0 0 0.42 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3457"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_647_3457" result="shape"/>
</filter>
<linearGradient id="paint0_linear_647_3457" x1="124.818" y1="90.7827" x2="73.4029" y2="61.0984" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF6D6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFEFC5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint1_linear_647_3457" x1="60.7124" y1="128.334" x2="60.7124" y2="68.9654" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF6D6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFEFC5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint2_linear_647_3457" x1="60.3033" y1="-22" x2="60.3033" y2="37.3686" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF6D6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFEFC5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint3_linear_647_3457" x1="-3.75563" y1="90.9712" x2="47.6591" y2="61.2869" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF6D6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFEFC5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint4_linear_647_3457" x1="124.818" y1="15.5901" x2="73.4029" y2="45.2744" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF6D6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFEFC5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint5_linear_647_3457" x1="-3.75563" y1="15.4025" x2="47.6591" y2="45.0868" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF6D6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFEFC5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint6_linear_647_3457" x1="16.7733" y1="37.5375" x2="9.24885" y2="26.3625" gradientUnits="userSpaceOnUse">
<stop stop-color="#D2A86B"/>
<stop offset="0.875" stop-color="#FFF5DE"/>
</linearGradient>
<linearGradient id="paint7_linear_647_3457" x1="5.00137" y1="24.0609" x2="11.7368" y2="36.8322" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF5DE"/>
<stop offset="0.378992" stop-color="#F5E3C4"/>
<stop offset="0.480868" stop-color="#E6CA9E"/>
<stop offset="0.729174" stop-color="#D2A86B"/>
</linearGradient>
<linearGradient id="paint8_linear_647_3457" x1="22.3253" y1="48.8869" x2="14.8008" y2="37.7119" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0C08E"/>
<stop offset="0.875" stop-color="#FFF5DE"/>
</linearGradient>
<linearGradient id="paint9_linear_647_3457" x1="10.0719" y1="35.0384" x2="25.2161" y2="53.0168" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF5DE"/>
<stop offset="0.378992" stop-color="#FFF5DE"/>
<stop offset="0.480868" stop-color="#EAD2A9"/>
<stop offset="0.729174" stop-color="#D2A86B"/>
</linearGradient>
<linearGradient id="paint10_linear_647_3457" x1="29.6075" y1="60.4919" x2="22.0831" y2="49.3168" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0C08E"/>
<stop offset="0.875" stop-color="#FFF5DE"/>
</linearGradient>
<linearGradient id="paint11_linear_647_3457" x1="17.3542" y1="46.6434" x2="32.4983" y2="64.6217" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF5DE"/>
<stop offset="0.378992" stop-color="#FFF5DE"/>
<stop offset="0.480868" stop-color="#EAD2A9"/>
<stop offset="0.729174" stop-color="#D2A86B"/>
</linearGradient>
<linearGradient id="paint12_linear_647_3457" x1="33.7939" y1="87.943" x2="48.191" y2="91.1959" gradientUnits="userSpaceOnUse">
<stop stop-color="#8D664F"/>
<stop offset="0.208702" stop-color="#C49F7D"/>
<stop offset="0.648432" stop-color="#FFF1DC"/>
<stop offset="0.827985" stop-color="#B38368"/>
<stop offset="0.971703" stop-color="#8C5F4C"/>
</linearGradient>
<linearGradient id="paint13_linear_647_3457" x1="35.5645" y1="87.4628" x2="40.7723" y2="85.9103" gradientUnits="userSpaceOnUse">
<stop stop-color="#C7A383"/>
<stop offset="0.309893" stop-color="#D5B198"/>
<stop offset="0.523799" stop-color="#CBA68C"/>
<stop offset="0.77229" stop-color="#BB9478"/>
<stop offset="1" stop-color="#D7B49B"/>
</linearGradient>
<linearGradient id="paint14_linear_647_3457" x1="86.7559" y1="87.943" x2="72.3588" y2="91.1959" gradientUnits="userSpaceOnUse">
<stop stop-color="#8D664F"/>
<stop offset="0.208702" stop-color="#C49F7D"/>
<stop offset="0.648432" stop-color="#FFF1DC"/>
<stop offset="0.827985" stop-color="#B38368"/>
<stop offset="0.971703" stop-color="#8C5F4C"/>
</linearGradient>
<linearGradient id="paint15_linear_647_3457" x1="84.9863" y1="87.4628" x2="79.7785" y2="85.9103" gradientUnits="userSpaceOnUse">
<stop stop-color="#C7A383"/>
<stop offset="0.309893" stop-color="#D5B198"/>
<stop offset="0.523799" stop-color="#CBA68C"/>
<stop offset="0.77229" stop-color="#BB9478"/>
<stop offset="1" stop-color="#D7B49B"/>
</linearGradient>
<linearGradient id="paint16_linear_647_3457" x1="103.591" y1="37.8923" x2="111.115" y2="26.7172" gradientUnits="userSpaceOnUse">
<stop stop-color="#D2A86B"/>
<stop offset="0.875" stop-color="#FFF5DE"/>
</linearGradient>
<linearGradient id="paint17_linear_647_3457" x1="115.363" y1="24.4166" x2="108.627" y2="37.1879" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF5DE"/>
<stop offset="0.378992" stop-color="#F5E3C4"/>
<stop offset="0.480868" stop-color="#E6CA9E"/>
<stop offset="0.729174" stop-color="#D2A86B"/>
</linearGradient>
<linearGradient id="paint18_linear_647_3457" x1="98.039" y1="49.2421" x2="105.563" y2="38.0671" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0C08E"/>
<stop offset="0.875" stop-color="#FFF5DE"/>
</linearGradient>
<linearGradient id="paint19_linear_647_3457" x1="110.292" y1="35.3936" x2="95.1482" y2="53.372" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF5DE"/>
<stop offset="0.378992" stop-color="#FFF5DE"/>
<stop offset="0.480868" stop-color="#EAD2A9"/>
<stop offset="0.729174" stop-color="#D2A86B"/>
</linearGradient>
<linearGradient id="paint20_linear_647_3457" x1="90.7577" y1="60.8469" x2="98.2822" y2="49.6718" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0C08E"/>
<stop offset="0.875" stop-color="#FFF5DE"/>
</linearGradient>
<linearGradient id="paint21_linear_647_3457" x1="103.011" y1="46.9983" x2="87.8669" y2="64.9767" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF5DE"/>
<stop offset="0.378992" stop-color="#FFF5DE"/>
<stop offset="0.480868" stop-color="#EAD2A9"/>
<stop offset="0.729174" stop-color="#D2A86B"/>
</linearGradient>
<linearGradient id="paint22_linear_647_3457" x1="60.2076" y1="84.739" x2="60.2076" y2="110.335" gradientUnits="userSpaceOnUse">
<stop stop-color="#B58D70"/>
<stop offset="0.2286" stop-color="#B58D70"/>
<stop offset="0.47675" stop-color="#D8B59B"/>
<stop offset="0.88344" stop-color="#B58D70"/>
<stop offset="1" stop-color="#B58D70"/>
</linearGradient>
<linearGradient id="paint23_linear_647_3457" x1="45.2314" y1="95.7669" x2="74.1961" y2="105.627" gradientUnits="userSpaceOnUse">
<stop stop-color="#8D664F"/>
<stop offset="0.208702" stop-color="#FFF5E6"/>
<stop offset="0.648432" stop-color="#ECD8BA"/>
<stop offset="0.780206" stop-color="#D0A568"/>
<stop offset="0.971703" stop-color="#8C5F4C"/>
</linearGradient>
<linearGradient id="paint24_linear_647_3457" x1="59.9356" y1="79.8384" x2="59.9356" y2="109.967" gradientUnits="userSpaceOnUse">
<stop stop-color="#DDBFA9"/>
<stop offset="0.22229" stop-color="#DDBFA9"/>
<stop offset="0.669825" stop-color="#EBD4C4"/>
<stop offset="0.898796" stop-color="#DDBFA9"/>
<stop offset="1" stop-color="#DDBFA9"/>
</linearGradient>
<linearGradient id="paint25_linear_647_3457" x1="72.2907" y1="99.2633" x2="61.3642" y2="90.7969" gradientUnits="userSpaceOnUse">
<stop stop-color="#ECD8BA"/>
<stop offset="0.309893" stop-color="#EBD3AE"/>
<stop offset="0.523799" stop-color="#E2C69C"/>
<stop offset="0.77229" stop-color="#E3C8A0"/>
<stop offset="1" stop-color="#ECD8BA"/>
</linearGradient>
<linearGradient id="paint26_linear_647_3457" x1="60.0705" y1="102.71" x2="60.0705" y2="107.326" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAECD8"/>
<stop offset="0.491891" stop-color="#F9EBD7"/>
<stop offset="1" stop-color="#ECD5B4"/>
</linearGradient>
<linearGradient id="paint27_linear_647_3457" x1="59.0529" y1="102.829" x2="59.0529" y2="106.512" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9EBD7"/>
<stop offset="0.479167" stop-color="#FAEDD9"/>
<stop offset="1" stop-color="#F8E9D4"/>
</linearGradient>
<linearGradient id="paint28_linear_647_3457" x1="61.0857" y1="102.829" x2="61.0857" y2="106.512" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAEDD8"/>
<stop offset="0.479167" stop-color="#FAECD8"/>
<stop offset="1" stop-color="#EAD4B2"/>
</linearGradient>
<linearGradient id="paint29_linear_647_3457" x1="59.7831" y1="106.77" x2="56.7128" y2="107.133" gradientUnits="userSpaceOnUse">
<stop stop-color="#E5CBA6"/>
<stop offset="0.309893" stop-color="#FAEDDA"/>
<stop offset="0.523799" stop-color="#F3E2C9"/>
<stop offset="0.613641" stop-color="#EFDCC0"/>
<stop offset="1" stop-color="#EFDCC0"/>
</linearGradient>
<linearGradient id="paint30_linear_647_3457" x1="45.9122" y1="23.2007" x2="76.5451" y2="84.0582" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF5DE"/>
<stop offset="1" stop-color="#CFA365"/>
</linearGradient>
<linearGradient id="paint31_linear_647_3457" x1="45.6411" y1="28.2386" x2="72.0535" y2="76.9791" gradientUnits="userSpaceOnUse">
<stop stop-color="#D2A86C"/>
<stop offset="1" stop-color="#F8E6C0"/>
</linearGradient>
<linearGradient id="paint32_linear_647_3457" x1="48.5002" y1="26.4687" x2="76.9548" y2="77.6598" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF6E7"/>
<stop offset="1" stop-color="#FFF8EC"/>
</linearGradient>
<radialGradient id="paint33_radial_647_3457" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(60.3445 53.206) rotate(90) scale(30.8223 26.5486)">
<stop stop-color="#FFE4BA"/>
<stop offset="1" stop-color="#DEA44D"/>
</radialGradient>
<linearGradient id="paint34_linear_647_3457" x1="60.3463" y1="21.1636" x2="60.3463" y2="85.2464" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#F2EDED" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint35_linear_647_3457" x1="27.5694" y1="26.9614" x2="59.6108" y2="82.4587" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#F2EDED" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint36_linear_647_3457" x1="90.7345" y1="26.9608" x2="58.6931" y2="82.4581" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#F2EDED" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint37_linear_647_3457" x1="60.3445" y1="64.4956" x2="60.1843" y2="81.8891" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#FFF1E3"/>
</linearGradient>
<linearGradient id="paint38_linear_647_3457" x1="58.1057" y1="34.0394" x2="78.3099" y2="41.4078" gradientUnits="userSpaceOnUse">
<stop offset="0.168737" stop-color="#FDF2D9"/>
<stop offset="0.304755" stop-color="#FFF7E3"/>
<stop offset="0.451944" stop-color="#FFFCF4"/>
<stop offset="0.581036" stop-color="#FFF1D9"/>
<stop offset="0.705947" stop-color="#F3E0BF"/>
<stop offset="0.940592" stop-color="#FFF5DE"/>
</linearGradient>
<clipPath id="clip0_647_3457">
<rect width="123" height="123" fill="white" transform="translate(-1 -8)"/>
</clipPath>
<clipPath id="clip1_647_3457">
<rect width="30.5763" height="30.5763" fill="white" transform="translate(45.2393 37.8784)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,414 @@
<svg width="120" height="115" viewBox="0 0 120 115" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_647_3536)">
<path d="M77.3358 54.2863L130.062 81.7L119.574 99.8654L69.47 67.9104L77.3358 54.2863Z" fill="url(#paint0_linear_647_3536)"/>
<path d="M68.5782 68.9654L71.2002 128.334H50.2245L52.8465 68.9654H68.5782Z" fill="url(#paint1_linear_647_3536)"/>
<path d="M52.4374 37.3686L49.8154 -22L70.7911 -22L68.1691 37.3686H52.4374Z" fill="url(#paint2_linear_647_3536)"/>
<path d="M51.592 68.099L1.48828 100.054L-8.99955 81.8885L43.7261 54.4749L51.592 68.099Z" fill="url(#paint3_linear_647_3536)"/>
<path d="M77.3358 52.0864L130.062 24.6729L119.574 6.50741L69.47 38.4624L77.3358 52.0864Z" fill="url(#paint4_linear_647_3536)"/>
<path d="M51.592 38.2748L1.48828 6.31982L-8.99955 24.4853L43.7261 51.8989L51.592 38.2748Z" fill="url(#paint5_linear_647_3536)"/>
<g opacity="0.3" filter="url(#filter0_f_647_3536)">
<circle cx="40.7228" cy="9.75311" r="2.07342" fill="#FFDAC4"/>
</g>
<g filter="url(#filter1_f_647_3536)">
<circle cx="31.99" cy="84.8855" r="0.69114" fill="#FFDAC4"/>
</g>
<g opacity="0.3" filter="url(#filter2_f_647_3536)">
<circle cx="37.7068" cy="21.7131" r="0.69114" fill="#FFDAC4"/>
</g>
<g filter="url(#filter3_f_647_3536)">
<circle cx="95.9782" cy="62.2856" r="0.69114" fill="#FFDAC4"/>
</g>
<g filter="url(#filter4_f_647_3536)">
<circle cx="12.6358" cy="56.1849" r="2.3038" fill="#FFC8AA"/>
</g>
<g opacity="0.6" filter="url(#filter5_f_647_3536)">
<circle cx="117.824" cy="46.2827" r="1.84304" fill="#FFC8AA"/>
</g>
<g opacity="0.8" filter="url(#filter6_f_647_3536)">
<circle cx="41.5833" cy="106.46" r="1.84304" fill="#FFC8AA"/>
</g>
<g filter="url(#filter7_dd_647_3536)">
<path d="M32.2354 40.5562L34.3898 29.5204L2.98817 23.39C1.64164 30.2874 5.70982 35.3777 9.67165 36.1512L32.2354 40.5562Z" fill="url(#paint6_linear_647_3536)"/>
<path d="M31.9141 40.7715L34.0685 29.7357L3.46862 23.7619C2.12209 30.6592 6.19027 35.7496 10.1521 36.523L31.9141 40.7715Z" stroke="url(#paint7_linear_647_3536)" stroke-width="0.624672"/>
<path d="M32.4424 50.8621L34.5968 39.8263L8.54016 34.7394C7.19363 41.6367 11.2618 46.7271 15.2236 47.5005L32.4424 50.8621Z" fill="url(#paint8_linear_647_3536)"/>
<path d="M32.4424 50.8621L34.5968 39.8263L8.54016 34.7394C7.19362 41.6367 11.2618 46.7271 15.2236 47.5005L32.4424 50.8621Z" stroke="url(#paint9_linear_647_3536)" stroke-width="0.624672"/>
<path d="M39.7246 62.467L41.8791 51.4313L15.8224 46.3444C14.4759 53.2417 18.544 58.3321 22.5059 59.1055L39.7246 62.467Z" fill="url(#paint10_linear_647_3536)"/>
<path d="M39.7246 62.467L41.8791 51.4313L15.8224 46.3444C14.4759 53.2417 18.544 58.3321 22.5059 59.1055L39.7246 62.467Z" stroke="url(#paint11_linear_647_3536)" stroke-width="0.624672"/>
<g filter="url(#filter8_d_647_3536)">
<path d="M33.7939 90.1085V65.9517H47.817V98.2184L34.2206 90.8262C33.9577 90.6832 33.7939 90.4078 33.7939 90.1085Z" fill="#ECC3BA"/>
<path d="M33.7939 90.1085V65.9517H47.817V98.2184L34.2206 90.8262C33.9577 90.6832 33.7939 90.4078 33.7939 90.1085Z" stroke="url(#paint12_linear_647_3536)" stroke-width="0.272293"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.3383 89.4068V78.2048H35.1561V89.5585L35.2322 89.5712C35.4841 89.6132 35.7641 89.7622 35.9928 89.98C36.2213 90.1976 36.3904 90.476 36.4323 90.7695L36.4393 90.8182L43.8096 94.2701V81.9424H43.6274V93.9836L36.6046 90.6945C36.5446 90.3697 36.3563 90.0745 36.1184 89.8481C35.8903 89.6308 35.6099 89.4702 35.3383 89.4068Z" fill="url(#paint13_linear_647_3536)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.6163 93.3173C43.8128 92.9852 43.824 92.514 43.8088 92.2097L43.6268 92.2188C43.6419 92.5218 43.6259 92.9433 43.4595 93.2245C43.3789 93.3609 43.2647 93.4613 43.1004 93.5025C42.9333 93.5445 42.7023 93.5282 42.3845 93.4046L42.3185 93.5744C42.6565 93.7059 42.9281 93.7336 43.1448 93.6792C43.3644 93.624 43.5155 93.4876 43.6163 93.3173Z" fill="#D49D95"/>
<g filter="url(#filter9_d_647_3536)">
<path d="M86.7559 90.1085V65.9517H72.7328V98.2184L86.3292 90.8262C86.5921 90.6832 86.7559 90.4078 86.7559 90.1085Z" fill="#ECC3BA"/>
<path d="M86.7559 90.1085V65.9517H72.7328V98.2184L86.3292 90.8262C86.5921 90.6832 86.7559 90.4078 86.7559 90.1085Z" stroke="url(#paint14_linear_647_3536)" stroke-width="0.272293"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M85.2125 89.4068V78.2048H85.3947V89.5585L85.3186 89.5712C85.0667 89.6132 84.7867 89.7622 84.558 89.98C84.3295 90.1976 84.1604 90.476 84.1185 90.7695L84.1115 90.8182L76.7412 94.2701V81.9424H76.9234V93.9836L83.9462 90.6945C84.0061 90.3697 84.1945 90.0745 84.4323 89.8481C84.6605 89.6308 84.9409 89.4702 85.2125 89.4068Z" fill="url(#paint15_linear_647_3536)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M76.9335 93.3173C76.7371 92.9852 76.7258 92.514 76.7411 92.2097L76.923 92.2188C76.9079 92.5218 76.9239 92.9433 77.0903 93.2245C77.171 93.3609 77.2851 93.4613 77.4494 93.5025C77.6165 93.5445 77.8475 93.5282 78.1653 93.4046L78.2313 93.5744C77.8933 93.7059 77.6217 93.7336 77.405 93.6792C77.1854 93.624 77.0343 93.4876 76.9335 93.3173Z" fill="#D49D95"/>
<path d="M88.1289 40.9109L85.9745 29.8751L117.376 23.7447C118.723 30.6421 114.654 35.7325 110.693 36.5059L88.1289 40.9109Z" fill="url(#paint16_linear_647_3536)"/>
<path d="M88.4502 41.1272L86.2957 30.0914L116.896 24.1176C118.242 31.0149 114.174 36.1053 110.212 36.8787L88.4502 41.1272Z" stroke="url(#paint17_linear_647_3536)" stroke-width="0.624672"/>
<path d="M87.9229 51.2173L85.7684 40.1815L111.825 35.0946C113.172 41.992 109.103 47.0823 105.142 47.8558L87.9229 51.2173Z" fill="url(#paint18_linear_647_3536)"/>
<path d="M87.9229 51.2173L85.7684 40.1815L111.825 35.0946C113.172 41.992 109.103 47.0823 105.142 47.8558L87.9229 51.2173Z" stroke="url(#paint19_linear_647_3536)" stroke-width="0.624672"/>
<path d="M80.6416 62.822L78.4871 51.7863L104.544 46.6993C105.89 53.5967 101.822 58.6871 97.8603 59.4605L80.6416 62.822Z" fill="url(#paint20_linear_647_3536)"/>
<path d="M80.6416 62.822L78.4871 51.7863L104.544 46.6993C105.89 53.5967 101.822 58.6871 97.8603 59.4605L80.6416 62.822Z" stroke="url(#paint21_linear_647_3536)" stroke-width="0.624672"/>
<g filter="url(#filter10_d_647_3536)">
<path d="M45.2314 64.5894H75.1837V102.625L60.3741 110.153C60.1498 110.267 59.8845 110.267 59.6606 110.152L45.2314 102.756V64.5894Z" fill="url(#paint22_linear_647_3536)"/>
<path d="M45.2314 64.5894H75.1837V102.625L60.3741 110.153C60.1498 110.267 59.8845 110.267 59.6606 110.152L45.2314 102.756V64.5894Z" stroke="url(#paint23_linear_647_3536)" stroke-width="0.544586"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.5646 107.752L60.3747 109.882C60.1504 109.996 59.8851 109.995 59.6612 109.88L55.3066 107.648V79.8384H64.5646V107.752Z" fill="url(#paint24_linear_647_3536)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M46.8652 85.0117H47.2991V102.035L60.1001 108.652L73.1161 102.035V85.0117H73.55V102.189L60.0982 108.974L46.8652 102.188V85.0117Z" fill="url(#paint25_linear_647_3536)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M60.1888 102.71V107.651H59.9521V102.71H60.1888Z" fill="url(#paint26_linear_647_3536)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.3592 105.898C58.8881 106.441 58.3141 106.562 57.9189 106.496L57.9579 106.263C58.2727 106.315 58.7639 106.223 59.1804 105.743C59.5985 105.26 59.9503 104.376 59.9503 102.829H60.1869C60.1869 104.407 59.8286 105.356 59.3592 105.898Z" fill="url(#paint27_linear_647_3536)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M60.7795 105.898C61.2506 106.441 61.8245 106.562 62.2197 106.496L62.1808 106.263C61.8659 106.315 61.3748 106.223 60.9583 105.743C60.5402 105.26 60.1884 104.376 60.1884 102.829H59.9517C59.9517 104.407 60.31 105.356 60.7795 105.898Z" fill="url(#paint28_linear_647_3536)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M60.0202 107.759L57.3477 106.261L57.9394 106.261L60.0697 107.521L62.1998 106.25H62.7221L60.1185 107.759C60.0873 107.773 60.0514 107.773 60.0202 107.759Z" fill="url(#paint29_linear_647_3536)"/>
<g filter="url(#filter11_d_647_3536)">
<path d="M57.9955 14.8737L28.5879 30.8712C27.0982 31.6815 26.1709 33.2416 26.1709 34.9374L26.1709 71.5059C26.1709 73.2017 27.0982 74.7618 28.5879 75.5722L57.9955 91.5696C59.3748 92.3199 61.0403 92.3199 62.4195 91.5696L91.8272 75.5722C93.3169 74.7618 94.2442 73.2017 94.2442 71.5059L94.2442 34.9374C94.2442 33.2416 93.3169 31.6815 91.8272 30.8712L62.4195 14.8737C61.0403 14.1234 59.3748 14.1234 57.9955 14.8737Z" fill="url(#paint30_linear_647_3536)"/>
<path d="M57.9643 20.8602L33.7536 34.0305C32.2695 34.8378 31.3457 36.392 31.3457 38.0815L31.3457 68.2254C31.3457 69.9148 32.2695 71.469 33.7536 72.2763L57.9643 85.4467C59.3383 86.1942 60.9976 86.1942 62.3717 85.4467L86.5824 72.2763C88.0664 71.469 88.9902 69.9148 88.9902 68.2254L88.9902 38.0815C88.9902 36.392 88.0664 34.8378 86.5824 34.0305L62.3717 20.8602C60.9976 20.1127 59.3383 20.1127 57.9643 20.8602Z" fill="url(#paint31_linear_647_3536)"/>
<g filter="url(#filter12_f_647_3536)">
<path d="M57.9643 20.8602L33.7536 34.0305C32.2695 34.8378 31.3457 36.392 31.3457 38.0815L31.3457 68.2254C31.3457 69.9148 32.2695 71.469 33.7536 72.2763L57.9643 85.4467C59.3383 86.1942 60.9976 86.1942 62.3717 85.4467L86.5824 72.2763C88.0664 71.469 88.9902 69.9148 88.9902 68.2254L88.9902 38.0815C88.9902 36.392 88.0664 34.8378 86.5824 34.0305L62.3717 20.8602C60.9976 20.1127 59.3383 20.1127 57.9643 20.8602Z" stroke="url(#paint32_linear_647_3536)" stroke-width="0.272293"/>
</g>
<g opacity="0.5">
<mask id="mask0_647_3536" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="33" y="22" width="54" height="62">
<path d="M58.2497 23.5223L36.0878 35.5673C34.6753 36.335 33.7959 37.8138 33.7959 39.4215L33.7959 66.9906C33.7959 68.5982 34.6753 70.0771 36.0878 70.8448L58.2497 82.8898C59.556 83.5998 61.133 83.5998 62.4392 82.8898L84.6012 70.8448C86.0137 70.0771 86.893 68.5982 86.893 66.9906L86.8931 39.4215C86.8931 37.8138 86.0137 36.335 84.6012 35.5673L62.4392 23.5223C61.133 22.8123 59.556 22.8123 58.2497 23.5223Z" fill="#A6D3DE"/>
</mask>
<g mask="url(#mask0_647_3536)">
<path d="M58.2497 23.5223L36.0878 35.5673C34.6753 36.335 33.7959 37.8138 33.7959 39.4215L33.7959 66.9906C33.7959 68.5982 34.6753 70.0771 36.0878 70.8448L58.2497 82.8898C59.556 83.5998 61.133 83.5998 62.4392 82.8898L84.6012 70.8448C86.0137 70.0771 86.893 68.5982 86.893 66.9906L86.8931 39.4215C86.8931 37.8138 86.0137 36.335 84.6012 35.5673L62.4392 23.5223C61.133 22.8123 59.556 22.8123 58.2497 23.5223Z" fill="url(#paint33_radial_647_3536)"/>
<path opacity="0.5" d="M55.9081 85.2464L54.8535 21.1636H65.8391L64.7845 85.2464H55.9081Z" fill="url(#paint34_linear_647_3536)"/>
<path opacity="0.5" d="M55.7672 84.6778L22.8125 29.7078L32.3263 24.215L63.4544 80.2396L55.7672 84.6778Z" fill="url(#paint35_linear_647_3536)"/>
<path opacity="0.5" d="M54.8495 80.239L85.9775 24.2144L95.4914 29.7072L62.5366 84.6772L54.8495 80.239Z" fill="url(#paint36_linear_647_3536)"/>
<rect x="33.7959" y="49.543" width="53.0972" height="34.4826" fill="url(#paint37_linear_647_3536)"/>
<g filter="url(#filter13_i_647_3536)">
<path d="M58.2497 23.5223L36.0878 35.5673C34.6753 36.335 33.7959 37.8138 33.7959 39.4215L33.7959 66.9906C33.7959 68.5982 34.6753 70.0771 36.0878 70.8448L58.2497 82.8898C59.556 83.5998 61.133 83.5998 62.4392 82.8898L84.6012 70.8448C86.0137 70.0771 86.893 68.5982 86.893 66.9906L86.8931 39.4215C86.8931 37.8138 86.0137 36.335 84.6012 35.5673L62.4392 23.5223C61.133 22.8123 59.556 22.8123 58.2497 23.5223Z" fill="#6052B4" fill-opacity="0.01"/>
</g>
</g>
</g>
</g>
</g>
<g clip-path="url(#clip1_647_3536)">
<g filter="url(#filter14_d_647_3536)">
<path d="M60.5274 40.4265L45.2393 49.3446L60.5274 58.2627L73.2675 50.831V60.1737H75.8155V49.3446L60.5274 40.4265ZM50.3341 55.0655V60.8108C52.6584 63.9053 56.359 65.9069 60.5271 65.9069C64.6952 65.9069 68.3958 63.9053 70.7201 60.8108L70.7196 55.0666L60.5278 61.0119L50.3341 55.0655Z" fill="url(#paint38_linear_647_3536)"/>
</g>
</g>
</g>
<defs>
<filter id="filter0_f_647_3536" x="33.1203" y="2.15057" width="15.2047" height="15.205" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.76456" result="effect1_foregroundBlur_647_3536"/>
</filter>
<filter id="filter1_f_647_3536" x="30.8381" y="83.7336" width="2.30336" height="2.30384" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.23038" result="effect1_foregroundBlur_647_3536"/>
</filter>
<filter id="filter2_f_647_3536" x="34.8373" y="18.8436" width="5.73853" height="5.73901" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="1.08917" result="effect1_foregroundBlur_647_3536"/>
</filter>
<filter id="filter3_f_647_3536" x="93.6534" y="59.9607" width="4.64935" height="4.64984" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.816879" result="effect1_foregroundBlur_647_3536"/>
</filter>
<filter id="filter4_f_647_3536" x="4.88617" y="48.4352" width="15.4991" height="15.4994" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.72293" result="effect1_foregroundBlur_647_3536"/>
</filter>
<filter id="filter5_f_647_3536" x="111.374" y="39.8321" width="12.9017" height="12.9012" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.3038" result="effect1_foregroundBlur_647_3536"/>
</filter>
<filter id="filter6_f_647_3536" x="35.1326" y="100.009" width="12.9017" height="12.9012" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.3038" result="effect1_foregroundBlur_647_3536"/>
</filter>
<filter id="filter7_dd_647_3536" x="-22.2808" y="-9.02824" width="164.926" height="146.212" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="4.16773"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.991667 0 0 0 0 0.801597 0 0 0 0 0.516493 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3536"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1.66709"/>
<feGaussianBlur stdDeviation="12.5032"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.722667 0 0 0 0 0.466667 0 0 0 0.6 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_647_3536" result="effect2_dropShadow_647_3536"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_647_3536" result="shape"/>
</filter>
<filter id="filter8_d_647_3536" x="31.4799" y="63.6371" width="18.6516" height="36.9885" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.08917"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.7 0 0 0 0 0.561568 0 0 0 0 0.32375 0 0 0 0.78 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3536"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_647_3536" result="shape"/>
</filter>
<filter id="filter9_d_647_3536" x="70.4183" y="63.6371" width="18.6516" height="36.9885" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.08917"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.7 0 0 0 0 0.561568 0 0 0 0 0.32375 0 0 0 0.78 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3536"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_647_3536" result="shape"/>
</filter>
<filter id="filter10_d_647_3536" x="42.7806" y="62.1388" width="34.8538" height="50.55" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.08917"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.7 0 0 0 0 0.561568 0 0 0 0 0.32375 0 0 0 0.78 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3536"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_647_3536" result="shape"/>
</filter>
<filter id="filter11_d_647_3536" x="24.5371" y="14.311" width="71.3408" height="81.6334" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2.17834"/>
<feGaussianBlur stdDeviation="0.816879"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.4375 0 0 0 0 0.287401 0 0 0 0 0.111198 0 0 0 0.6 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3536"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_647_3536" result="shape"/>
</filter>
<filter id="filter12_f_647_3536" x="31.1283" y="20.0816" width="58.0794" height="66.1436" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.040844" result="effect1_foregroundBlur_647_3536"/>
</filter>
<filter id="filter13_i_647_3536" x="33.7959" y="22.9897" width="53.0967" height="60.4326" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.83094"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.458333 0 0 0 0 0.256743 0 0 0 0 0.0706597 0 0 0 1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_647_3536"/>
</filter>
<filter id="filter14_d_647_3536" x="43.3683" y="39.4418" width="34.5151" height="29.4194" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="0.0984731" dy="0.984731"/>
<feGaussianBlur stdDeviation="0.984731"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.795833 0 0 0 0 0.519745 0 0 0 0 0.195642 0 0 0 0.42 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_647_3536"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_647_3536" result="shape"/>
</filter>
<linearGradient id="paint0_linear_647_3536" x1="124.818" y1="90.7827" x2="73.4029" y2="61.0984" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEFD6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFD9C5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint1_linear_647_3536" x1="60.7124" y1="128.334" x2="60.7124" y2="68.9654" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEFD6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFD9C5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint2_linear_647_3536" x1="60.3033" y1="-22" x2="60.3033" y2="37.3686" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEFD6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFD9C5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint3_linear_647_3536" x1="-3.75563" y1="90.9712" x2="47.6591" y2="61.2869" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEFD6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFD9C5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint4_linear_647_3536" x1="124.818" y1="15.5901" x2="73.4029" y2="45.2744" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEFD6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFD9C5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint5_linear_647_3536" x1="-3.75563" y1="15.4025" x2="47.6591" y2="45.0868" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEFD6" stop-opacity="0"/>
<stop offset="1" stop-color="#FFD9C5" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint6_linear_647_3536" x1="16.7743" y1="37.5378" x2="9.24983" y2="26.3627" gradientUnits="userSpaceOnUse">
<stop stop-color="#D2836B"/>
<stop offset="0.875" stop-color="#FFE6DE"/>
</linearGradient>
<linearGradient id="paint7_linear_647_3536" x1="5.00137" y1="24.0611" x2="11.7368" y2="36.8324" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFE9DE"/>
<stop offset="0.378992" stop-color="#F5D3C4"/>
<stop offset="0.480868" stop-color="#E6B69E"/>
<stop offset="0.729174" stop-color="#D28A6B"/>
</linearGradient>
<linearGradient id="paint8_linear_647_3536" x1="22.3263" y1="48.8871" x2="14.8018" y2="37.7121" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0AB8E"/>
<stop offset="0.875" stop-color="#FFECDE"/>
</linearGradient>
<linearGradient id="paint9_linear_647_3536" x1="10.0729" y1="35.0386" x2="25.217" y2="53.017" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEADE"/>
<stop offset="0.378992" stop-color="#FFE9DE"/>
<stop offset="0.480868" stop-color="#EAC0A9"/>
<stop offset="0.729174" stop-color="#D27E6B"/>
</linearGradient>
<linearGradient id="paint10_linear_647_3536" x1="29.6085" y1="60.4921" x2="22.084" y2="49.3171" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0AB8E"/>
<stop offset="0.875" stop-color="#FFECDE"/>
</linearGradient>
<linearGradient id="paint11_linear_647_3536" x1="17.3551" y1="46.6436" x2="32.4993" y2="64.622" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEADE"/>
<stop offset="0.378992" stop-color="#FFE9DE"/>
<stop offset="0.480868" stop-color="#EAC0A9"/>
<stop offset="0.729174" stop-color="#D27E6B"/>
</linearGradient>
<linearGradient id="paint12_linear_647_3536" x1="33.7939" y1="87.943" x2="48.191" y2="91.1959" gradientUnits="userSpaceOnUse">
<stop stop-color="#8D574F"/>
<stop offset="0.208702" stop-color="#C4887D"/>
<stop offset="0.648432" stop-color="#FFE4DC"/>
<stop offset="0.827985" stop-color="#B36D68"/>
<stop offset="0.971703" stop-color="#8C574C"/>
</linearGradient>
<linearGradient id="paint13_linear_647_3536" x1="35.5645" y1="87.4628" x2="40.7723" y2="85.9103" gradientUnits="userSpaceOnUse">
<stop stop-color="#C79483"/>
<stop offset="0.309893" stop-color="#D5A598"/>
<stop offset="0.523799" stop-color="#CB9D8C"/>
<stop offset="0.77229" stop-color="#BB8178"/>
<stop offset="1" stop-color="#D7A69B"/>
</linearGradient>
<linearGradient id="paint14_linear_647_3536" x1="86.7559" y1="87.943" x2="72.3588" y2="91.1959" gradientUnits="userSpaceOnUse">
<stop stop-color="#8D574F"/>
<stop offset="0.208702" stop-color="#C4887D"/>
<stop offset="0.648432" stop-color="#FFE4DC"/>
<stop offset="0.827985" stop-color="#B36D68"/>
<stop offset="0.971703" stop-color="#8C574C"/>
</linearGradient>
<linearGradient id="paint15_linear_647_3536" x1="84.9863" y1="87.4628" x2="79.7785" y2="85.9103" gradientUnits="userSpaceOnUse">
<stop stop-color="#C79483"/>
<stop offset="0.309893" stop-color="#D5A598"/>
<stop offset="0.523799" stop-color="#CB9D8C"/>
<stop offset="0.77229" stop-color="#BB8178"/>
<stop offset="1" stop-color="#D7A69B"/>
</linearGradient>
<linearGradient id="paint16_linear_647_3536" x1="103.59" y1="37.8925" x2="111.114" y2="26.7175" gradientUnits="userSpaceOnUse">
<stop stop-color="#D2836B"/>
<stop offset="0.875" stop-color="#FFE6DE"/>
</linearGradient>
<linearGradient id="paint17_linear_647_3536" x1="115.363" y1="24.4168" x2="108.627" y2="37.1881" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFE9DE"/>
<stop offset="0.378992" stop-color="#F5D3C4"/>
<stop offset="0.480868" stop-color="#E6B69E"/>
<stop offset="0.729174" stop-color="#D28A6B"/>
</linearGradient>
<linearGradient id="paint18_linear_647_3536" x1="98.039" y1="49.2424" x2="105.563" y2="38.0673" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0AB8E"/>
<stop offset="0.875" stop-color="#FFECDE"/>
</linearGradient>
<linearGradient id="paint19_linear_647_3536" x1="110.292" y1="35.3938" x2="95.1482" y2="53.3722" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEADE"/>
<stop offset="0.378992" stop-color="#FFE9DE"/>
<stop offset="0.480868" stop-color="#EAC0A9"/>
<stop offset="0.729174" stop-color="#D27E6B"/>
</linearGradient>
<linearGradient id="paint20_linear_647_3536" x1="90.7577" y1="60.8471" x2="98.2822" y2="49.6721" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0AB8E"/>
<stop offset="0.875" stop-color="#FFECDE"/>
</linearGradient>
<linearGradient id="paint21_linear_647_3536" x1="103.011" y1="46.9986" x2="87.8669" y2="64.977" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEADE"/>
<stop offset="0.378992" stop-color="#FFE9DE"/>
<stop offset="0.480868" stop-color="#EAC0A9"/>
<stop offset="0.729174" stop-color="#D27E6B"/>
</linearGradient>
<linearGradient id="paint22_linear_647_3536" x1="60.2076" y1="84.739" x2="60.2076" y2="110.335" gradientUnits="userSpaceOnUse">
<stop stop-color="#B57C70"/>
<stop offset="0.2286" stop-color="#B57C70"/>
<stop offset="0.47675" stop-color="#D8A79B"/>
<stop offset="0.88344" stop-color="#B58070"/>
<stop offset="1" stop-color="#B57670"/>
</linearGradient>
<linearGradient id="paint23_linear_647_3536" x1="45.2314" y1="95.7669" x2="74.1961" y2="105.627" gradientUnits="userSpaceOnUse">
<stop stop-color="#8D5C4F"/>
<stop offset="0.208702" stop-color="#FFEFE6"/>
<stop offset="0.648432" stop-color="#ECCABA"/>
<stop offset="0.780206" stop-color="#D08768"/>
<stop offset="0.971703" stop-color="#8C574C"/>
</linearGradient>
<linearGradient id="paint24_linear_647_3536" x1="59.9356" y1="79.8384" x2="59.9356" y2="109.967" gradientUnits="userSpaceOnUse">
<stop stop-color="#DDB3A9"/>
<stop offset="0.22229" stop-color="#DDB3A9"/>
<stop offset="0.669825" stop-color="#EBCEC4"/>
<stop offset="0.898796" stop-color="#DDB6A9"/>
<stop offset="1" stop-color="#DDB2A9"/>
</linearGradient>
<linearGradient id="paint25_linear_647_3536" x1="72.2907" y1="99.2633" x2="61.3642" y2="90.7969" gradientUnits="userSpaceOnUse">
<stop stop-color="#ECC7BA"/>
<stop offset="0.309893" stop-color="#EBC4AE"/>
<stop offset="0.523799" stop-color="#E2B39C"/>
<stop offset="0.77229" stop-color="#E3B7A0"/>
<stop offset="1" stop-color="#ECCCBA"/>
</linearGradient>
<linearGradient id="paint26_linear_647_3536" x1="60.0705" y1="102.71" x2="60.0705" y2="107.326" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAE3D8"/>
<stop offset="0.491891" stop-color="#F9E4D7"/>
<stop offset="1" stop-color="#ECC8B4"/>
</linearGradient>
<linearGradient id="paint27_linear_647_3536" x1="59.0529" y1="102.829" x2="59.0529" y2="106.512" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9E2D7"/>
<stop offset="0.479167" stop-color="#FAE7D9"/>
<stop offset="1" stop-color="#F8DFD4"/>
</linearGradient>
<linearGradient id="paint28_linear_647_3536" x1="61.0857" y1="102.829" x2="61.0857" y2="106.512" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAE6D8"/>
<stop offset="0.479167" stop-color="#FAE5D8"/>
<stop offset="1" stop-color="#EAC6B2"/>
</linearGradient>
<linearGradient id="paint29_linear_647_3536" x1="59.7831" y1="106.77" x2="56.7128" y2="107.133" gradientUnits="userSpaceOnUse">
<stop stop-color="#E5B4A6"/>
<stop offset="0.309893" stop-color="#FAE3DA"/>
<stop offset="0.523799" stop-color="#F3CFC9"/>
<stop offset="0.613641" stop-color="#EFCBC0"/>
<stop offset="1" stop-color="#EFCCC0"/>
</linearGradient>
<linearGradient id="paint30_linear_647_3536" x1="45.9122" y1="23.2007" x2="76.5451" y2="84.0582" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEBDE"/>
<stop offset="1" stop-color="#CF8365"/>
</linearGradient>
<linearGradient id="paint31_linear_647_3536" x1="45.6411" y1="28.2386" x2="72.0535" y2="76.9791" gradientUnits="userSpaceOnUse">
<stop stop-color="#D27F6C"/>
<stop offset="1" stop-color="#F8C8C0"/>
</linearGradient>
<linearGradient id="paint32_linear_647_3536" x1="48.5002" y1="26.4687" x2="76.9548" y2="77.6598" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEDE7"/>
<stop offset="1" stop-color="#FFF3EC"/>
</linearGradient>
<radialGradient id="paint33_radial_647_3536" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(60.3445 53.206) rotate(90) scale(30.8223 26.5486)">
<stop stop-color="#FFD4BA"/>
<stop offset="1" stop-color="#DE6C4D"/>
</radialGradient>
<linearGradient id="paint34_linear_647_3536" x1="60.3463" y1="21.1636" x2="60.3463" y2="85.2464" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#F2EDED" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint35_linear_647_3536" x1="27.5694" y1="26.9614" x2="59.6108" y2="82.4587" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#F2EDED" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint36_linear_647_3536" x1="90.7345" y1="26.9608" x2="58.6931" y2="82.4581" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#F2EDED" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint37_linear_647_3536" x1="60.3445" y1="64.4956" x2="60.1843" y2="81.8891" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#FFE9E3"/>
</linearGradient>
<linearGradient id="paint38_linear_647_3536" x1="58.1057" y1="34.0394" x2="78.3099" y2="41.4078" gradientUnits="userSpaceOnUse">
<stop offset="0.168737" stop-color="#FDE7D9"/>
<stop offset="0.304755" stop-color="#FFF0E3"/>
<stop offset="0.451944" stop-color="#FFF8F4"/>
<stop offset="0.581036" stop-color="#FFE7D9"/>
<stop offset="0.705947" stop-color="#F3CFBF"/>
<stop offset="0.940592" stop-color="#FFE8DE"/>
</linearGradient>
<clipPath id="clip0_647_3536">
<rect width="123" height="123" fill="white" transform="translate(-1 -8)"/>
</clipPath>
<clipPath id="clip1_647_3536">
<rect width="30.5763" height="30.5763" fill="white" transform="translate(45.2393 37.8784)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,233 @@
/**
* @description 成就相关API
* @author yslg
* @since 2025-10-24
*/
import { api } from '@/apis/index';
import type { Achievement, UserAchievement, UserAchievementProgress, AchievementVO, AchievementEvent, ResultDomain, PageParam } from '@/types';
/**
* 成就API服务
*/
export const achievementApi = {
// ==================== 成就定义管理(管理员)====================
/**
* 创建成就
* @param achievement 成就信息
* @returns Promise<ResultDomain<Achievement>>
*/
async createAchievement(achievement: Achievement): Promise<ResultDomain<Achievement>> {
const response = await api.post<Achievement>('/achievements/achievement', achievement);
return response.data;
},
/**
* 更新成就
* @param achievement 成就信息
* @returns Promise<ResultDomain<Achievement>>
*/
async updateAchievement(achievement: Achievement): Promise<ResultDomain<Achievement>> {
const response = await api.put<Achievement>('/achievements/achievement', achievement);
return response.data;
},
/**
* 删除成就
* @param achievement 成就信息包含achievementID
* @returns Promise<ResultDomain<void>>
*/
async deleteAchievement(achievement: Achievement): Promise<ResultDomain<void>> {
const response = await api.delete<void>('/achievements/achievement', achievement);
return response.data;
},
/**
* 获取所有成就列表
* @param filter 过滤条件type: 成就类型, level: 成就等级)
* @returns Promise<ResultDomain<Achievement>>
*/
async getAllAchievements(filter?: Partial<Achievement>): Promise<ResultDomain<Achievement>> {
const response = await api.post<Achievement>('/achievements/list', filter || {});
return response.data;
},
/**
* 分页查询成就
* @param filter 过滤条件
* @param pageParam 分页参数
* @returns Promise<ResultDomain<Achievement>>
*/
async getAchievementPage(filter?: Partial<Achievement>, pageParam?: PageParam): Promise<ResultDomain<Achievement>> {
const response = await api.post<Achievement>('/achievements/page', { filter, pageParam });
return response.data;
},
/**
* 获取成就详情
* @param achievementID 成就ID
* @returns Promise<ResultDomain<Achievement>>
*/
async getAchievementDetail(achievementID: string): Promise<ResultDomain<Achievement>> {
const response = await api.get<Achievement>(`/achievements/detail/${achievementID}`);
return response.data;
},
// ==================== 用户成就查询 ====================
/**
* 获取用户已获得的成就
* @param userID 用户ID
* @param type 成就类型(可选)
* @returns Promise<ResultDomain<UserAchievement>>
*/
async getUserAchievements(userID: string, type?: number): Promise<ResultDomain<UserAchievement>> {
const params: Record<string, any> = {};
if (type !== undefined) {
params.type = type;
}
const response = await api.get<UserAchievement>(`/achievements/user/${userID}`, params);
return response.data;
},
/**
* 获取当前用户的成就列表(含进度信息)
* 返回所有成就,包括:
* - 已获得的成就:显示获得时间
* - 未获得的成就:显示当前进度
* @param type 成就类型(可选)
* @returns Promise<ResultDomain<AchievementVO>>
*/
async getMyAchievements(type?: number): Promise<ResultDomain<AchievementVO>> {
const params: Record<string, any> = {};
if (type !== undefined) {
params.type = type;
}
const response = await api.get<AchievementVO>('/achievements/my', params);
return response.data;
},
/**
* 检查用户是否已获得成就
* @param userID 用户ID
* @param achievementID 成就ID
* @returns Promise<ResultDomain<boolean>>
*/
async hasAchievement(userID: string, achievementID: string): Promise<ResultDomain<boolean>> {
const response = await api.get<boolean>(`/achievements/check/${userID}/${achievementID}`);
return response.data;
},
// ==================== 成就授予(管理员)====================
/**
* 手动授予用户成就
* @param userID 用户ID
* @param achievementID 成就ID
* @returns Promise<ResultDomain<UserAchievement>>
*/
async grantAchievement(userID: string, achievementID: string): Promise<ResultDomain<UserAchievement>> {
const response = await api.post<UserAchievement>('/achievements/grant', null, {
params: { userID, achievementID }
});
return response.data;
},
/**
* 撤销用户成就
* @param userID 用户ID
* @param achievementID 成就ID
* @returns Promise<ResultDomain<void>>
*/
async revokeAchievement(userID: string, achievementID: string): Promise<ResultDomain<void>> {
const response = await api.delete<void>('/achievements/revoke', null, {
params: { userID, achievementID }
});
return response.data;
},
// ==================== 成就进度查询 ====================
/**
* 获取用户成就进度
* @param userID 用户ID
* @param achievementID 成就ID可选为空则获取所有进度
* @returns Promise<ResultDomain<UserAchievementProgress>>
*/
async getUserAchievementProgress(userID: string, achievementID?: string): Promise<ResultDomain<UserAchievementProgress>> {
const params: Record<string, any> = {};
if (achievementID) {
params.achievementID = achievementID;
}
const response = await api.get<UserAchievementProgress>(`/achievements/progress/${userID}`, params);
return response.data;
},
// ==================== 成就检测 ====================
/**
* 处理成就事件(内部接口,由其他服务调用)
* @param event 成就事件
* @returns Promise<ResultDomain<UserAchievement>>
*/
async processAchievementEvent(event: AchievementEvent): Promise<ResultDomain<UserAchievement>> {
const response = await api.post<UserAchievement>('/achievements/event/process', event);
return response.data;
},
/**
* 检查用户是否满足成就条件
* @param userID 用户ID
* @param achievementID 成就ID
* @returns Promise<ResultDomain<boolean>>
*/
async checkAchievementCondition(userID: string, achievementID: string): Promise<ResultDomain<boolean>> {
const response = await api.get<boolean>(`/achievements/condition/check/${userID}/${achievementID}`);
return response.data;
},
/**
* 批量检查用户可获得的成就
* @param userID 用户ID
* @returns Promise<ResultDomain<Achievement>>
*/
async checkAvailableAchievements(userID: string): Promise<ResultDomain<Achievement>> {
const response = await api.get<Achievement>(`/achievements/available/${userID}`);
return response.data;
},
// ==================== 成就统计 ====================
/**
* 获取用户成就统计
* @param userID 用户ID
* @returns Promise<ResultDomain<Record<string, any>>>
*/
async getUserAchievementStatistics(userID: string): Promise<ResultDomain<Record<string, any>>> {
const response = await api.get<Record<string, any>>(`/achievements/statistics/${userID}`);
return response.data;
},
/**
* 获取成就排行榜
* @param limit 排行榜条数默认10条
* @returns Promise<ResultDomain<Record<string, any>>>
*/
async getAchievementRanking(limit = 10): Promise<ResultDomain<Record<string, any>>> {
const response = await api.get<Record<string, any>>('/achievements/ranking', { limit });
return response.data;
},
/**
* 获取最近获得成就的用户
* @param pageParam 分页参数
* @param filter 过滤条件
* @param limit 查询条数默认10条
* @returns Promise<ResultDomain<UserAchievement>>
*/
async getRecentAchievers(pageParam: PageParam, filter?:Achievement ): Promise<ResultDomain<UserAchievement>> {
const response = await api.post<UserAchievement>(`/achievements/recent`, {pageParam, filter });
return response.data;
}
};

View File

@@ -0,0 +1 @@
export * from './achievement';

View File

@@ -178,9 +178,6 @@ request.interceptors.response.use(
} }
); );
/**
* API封装
*/
/** /**
* API封装 * API封装
*/ */

View File

@@ -12,4 +12,5 @@ export { menuApi } from './menu';
export { permissionApi } from './permission'; export { permissionApi } from './permission';
export { authApi } from './auth'; export { authApi } from './auth';
export { fileApi } from './file'; export { fileApi } from './file';
export { moduleApi } from './module';

View File

@@ -0,0 +1,197 @@
/**
* @description 系统模块相关API
* @author yslg
* @since 2025-10-25
*/
import { api } from '@/apis/index';
import type { SysModule, ResultDomain, PageParam, SysPermission } from '@/types';
/**
* 系统模块API服务
*/
export const moduleApi = {
baseUrl: '/system/modules',
/**
* 查询模块列表
* @param filter 过滤条件
* @returns Promise<ResultDomain<SysModule>>
*/
async getModuleList(filter?: Partial<SysModule>): Promise<ResultDomain<SysModule>> {
const response = await api.post<SysModule>(`${this.baseUrl}/list`, filter);
return response.data;
},
/**
* 根据模块ID查询模块信息
* @param moduleID 模块ID
* @returns Promise<ResultDomain<SysModule>>
*/
async getModuleById(moduleID: string): Promise<ResultDomain<SysModule>> {
const response = await api.get<SysModule>(`${this.baseUrl}/${moduleID}`);
return response.data;
},
/**
* 根据模块代码查询模块信息
* @param code 模块代码
* @returns Promise<ResultDomain<SysModule>>
*/
async getModuleByCode(code: string): Promise<ResultDomain<SysModule>> {
const response = await api.get<SysModule>(`${this.baseUrl}/code/${code}`);
return response.data;
},
/**
* 查询启用的模块列表
* @returns Promise<ResultDomain<SysModule>>
*/
async getActiveModules(): Promise<ResultDomain<SysModule>> {
const response = await api.get<SysModule>(`${this.baseUrl}/active`);
return response.data;
},
/**
* 创建模块
* @param module 模块信息
* @returns Promise<ResultDomain<SysModule>>
*/
async createModule(module: SysModule): Promise<ResultDomain<SysModule>> {
const response = await api.post<SysModule>(`${this.baseUrl}/module`, module);
return response.data;
},
/**
* 更新模块
* @param module 模块信息
* @returns Promise<ResultDomain<SysModule>>
*/
async updateModule(module: SysModule): Promise<ResultDomain<SysModule>> {
const response = await api.put<SysModule>(`${this.baseUrl}/module`, module);
return response.data;
},
/**
* 删除模块
* @param moduleID 模块ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteModule(moduleID: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/module`, { moduleID });
return response.data;
},
/**
* 批量删除模块
* @param moduleIDs 模块ID列表
* @returns Promise<ResultDomain<boolean>>
*/
async batchDeleteModules(moduleIDs: string[]): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/batch`, { moduleIDs });
return response.data;
},
/**
* 更新模块状态
* @param moduleID 模块ID
* @param status 状态0禁用 1启用
* @returns Promise<ResultDomain<boolean>>
*/
async updateModuleStatus(moduleID: string, status: number): Promise<ResultDomain<boolean>> {
const response = await api.put<boolean>(`${this.baseUrl}/${moduleID}/status/${status}`);
return response.data;
},
/**
* 更新模块排序
* @param moduleID 模块ID
* @param orderNum 排序号
* @returns Promise<ResultDomain<boolean>>
*/
async updateModuleOrder(moduleID: string, orderNum: number): Promise<ResultDomain<boolean>> {
const response = await api.put<boolean>(`${this.baseUrl}/${moduleID}/order/${orderNum}`);
return response.data;
},
/**
* 分页查询模块列表
* @param filter 过滤条件
* @param pageParam 分页参数
* @returns Promise<ResultDomain<SysModule>>
*/
async getModuleListPage(filter?: Partial<SysModule>, pageParam?: PageParam): Promise<ResultDomain<SysModule>> {
const response = await api.post<SysModule>(`${this.baseUrl}/page`, {
filter,
pageParam: {pageNumber: pageParam?.page || 1, pageSize: pageParam?.size || 10}
});
return response.data;
},
/**
* 统计模块数量
* @param filter 过滤条件
* @returns Promise<ResultDomain<number>>
*/
async countModules(filter?: Partial<SysModule>): Promise<ResultDomain<number>> {
const response = await api.post<number>(`${this.baseUrl}/count`, filter);
return response.data;
},
/**
* 检查模块代码是否存在
* @param code 模块代码
* @param excludeID 排除的模块ID
* @returns Promise<ResultDomain<boolean>>
*/
async checkModuleCodeExists(code: string, excludeID?: string): Promise<ResultDomain<boolean>> {
const response = await api.get<boolean>(`${this.baseUrl}/check-code`, {
code,
excludeID
});
return response.data;
},
/**
* 在模块中创建权限
* @param moduleID 模块ID
* @param permission 权限信息
* @returns Promise<ResultDomain<SysPermission>>
*/
async createPermissionInModule(moduleID: string, permission: SysPermission): Promise<ResultDomain<SysPermission>> {
const response = await api.post<SysPermission>(`${this.baseUrl}/${moduleID}/permissions`, permission);
return response.data;
},
/**
* 更新模块中的权限
* @param moduleID 模块ID
* @param permission 权限信息
* @returns Promise<ResultDomain<SysPermission>>
*/
async updatePermissionInModule(moduleID: string, permission: SysPermission): Promise<ResultDomain<SysPermission>> {
const response = await api.put<SysPermission>(`${this.baseUrl}/${moduleID}/permissions`, permission);
return response.data;
},
/**
* 删除模块中的权限
* @param moduleID 模块ID
* @param permissionID 权限ID
* @returns Promise<ResultDomain<boolean>>
*/
async deletePermissionInModule(moduleID: string, permissionID: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/${moduleID}/permissions/${permissionID}`);
return response.data;
},
/**
* 获取模块的权限列表
* @param moduleID 模块ID
* @returns Promise<ResultDomain<SysPermission>>
*/
async getModulePermissions(moduleID: string): Promise<ResultDomain<SysPermission>> {
const response = await api.get<SysPermission>(`${this.baseUrl}/${moduleID}/permissions`);
return response.data;
}
};

View File

@@ -1,46 +0,0 @@
/**
* @description 用户成就相关API
* @author yslg
* @since 2025-10-15
*/
import { api } from '@/apis/index';
import type { UserAchievement, Achievement, ResultDomain } from '@/types';
/**
* 用户成就API服务
*/
export const userAchievementApi = {
/**
* 获取用户成就列表
* @param userID 用户ID
* @returns Promise<ResultDomain<UserAchievement>>
*/
async getUserAchievements(userID: string): Promise<ResultDomain<UserAchievement>> {
const response = await api.get<UserAchievement>('/usercenter/achievement/user-list', { userID });
return response.data;
},
/**
* 获取所有成就列表
* @returns Promise<ResultDomain<Achievement>>
*/
async getAllAchievements(): Promise<ResultDomain<Achievement>> {
const response = await api.get<Achievement>('/usercenter/achievement/list');
return response.data;
},
/**
* 检查用户成就进度
* @param userID 用户ID
* @param achievementID 成就ID
* @returns Promise<ResultDomain<{ progress: number; isCompleted: boolean }>>
*/
async checkAchievementProgress(userID: string, achievementID: string): Promise<ResultDomain<{ progress: number; isCompleted: boolean }>> {
const response = await api.get<{ progress: number; isCompleted: boolean }>('/usercenter/achievement/progress', {
userID,
achievementID
});
return response.data;
}
};

View File

@@ -8,5 +8,4 @@
export { userCollectionApi } from './collection'; export { userCollectionApi } from './collection';
export { userBrowseRecordApi } from './browse-record'; export { userBrowseRecordApi } from './browse-record';
export { userPointsApi } from './points'; export { userPointsApi } from './points';
export { userAchievementApi } from './achievement';
export { userProfileApi } from './profile'; export { userProfileApi } from './profile';

View File

@@ -51,6 +51,6 @@ export const APP_CONFIG = {
refreshThreshold: 5 * 60 * 1000 // 提前5分钟刷新 refreshThreshold: 5 * 60 * 1000 // 提前5分钟刷新
} }
}; };
export const PUBLIC_IMG_PATH = '/schoolNewsWeb/img';
export default APP_CONFIG; export default APP_CONFIG;

View File

@@ -0,0 +1,114 @@
import { BaseDTO } from '../base';
import { AchievementEventType } from '../enums';
/**
* 成就实体
*/
export interface Achievement extends BaseDTO {
/** 成就唯一标识 */
achievementID?: string;
/** 成就名称 */
name?: string;
/** 成就描述 */
description?: string;
/** 成就图标 */
icon?: string;
/** 成就类型1勋章 2等级 */
type?: number;
/** 成就等级 */
level?: number;
/** 获取条件类型1学习时长 2资源数量 3课程数量 4连续学习天数 */
conditionType?: number;
/** 条件值 */
conditionValue?: number;
/** 获得积分 */
points?: number;
/** 排序号 */
orderNum?: number;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 用户成就实体
*/
export interface UserAchievement extends BaseDTO {
/** 用户ID */
userID?: string;
/** 成就ID */
achievementID?: string;
/** 获得时间 */
obtainTime?: string;
}
/**
* 用户成就进度实体
*/
export interface UserAchievementProgress extends BaseDTO {
/** 用户ID */
userID?: string;
/** 成就ID */
achievementID?: string;
/** 当前进度值 */
currentValue?: number;
/** 目标进度值 */
targetValue?: number;
/** 进度百分比 (0-100) */
progressPercentage?: number;
/** 是否已完成 */
completed?: boolean;
/** 最后更新时间 */
lastUpdateTime?: string;
}
/**
* 成就视图对象 - 包含成就信息、获得状态和进度信息
*/
export interface AchievementVO extends Achievement {
// ==================== 用户成就表的字段 ====================
/** 用户成就记录ID */
userAchievementID?: string;
/** 用户ID */
userID?: string;
/** 获得时间 */
obtainTime?: string;
// ==================== 成就进度表的字段 ====================
/** 进度记录ID */
progressID?: string;
/** 当前进度值 */
currentValue?: number;
/** 目标进度值 */
targetValue?: number;
/** 进度百分比 (0-100) */
progressPercentage?: number;
/** 是否已完成 */
completed?: boolean;
/** 最后更新时间 */
lastUpdateTime?: string;
// ==================== 扩展字段 ====================
/** 是否已获得该成就 */
obtained?: boolean;
}
/**
* 成就事件实体
* 用于触发成就检测和授予
* @since 2025-10-24
*/
export interface AchievementEvent {
/** 用户ID */
userID: string;
/** 事件类型 */
eventType: AchievementEventType;
/** 事件值(如学习时长、资源数量等) */
eventValue?: number;
/** 事件时间 */
eventTime?: string;
/** 额外数据 */
extraData?: Record<string, any>;
}

View File

@@ -0,0 +1 @@
export * from './achievement';

View File

@@ -0,0 +1,186 @@
/**
* @description 成就相关枚举辅助工具
* @author yslg
* @since 2025-10-25
*/
import { AchievementType, AchievementConditionType, AchievementEventType } from './index';
/**
* 成就类型描述映射
*/
export const AchievementTypeDescriptions: Record<AchievementType, string> = {
[AchievementType.BADGE]: '勋章',
[AchievementType.LEVEL]: '等级'
};
/**
* 成就条件类型描述映射
*/
export const AchievementConditionTypeDescriptions: Record<AchievementConditionType, string> = {
[AchievementConditionType.LEARNING_TIME]: '学习时长',
[AchievementConditionType.RESOURCE_VIEW_COUNT]: '浏览资源数量',
[AchievementConditionType.COURSE_COMPLETE_COUNT]: '完成课程数量',
[AchievementConditionType.CONTINUOUS_LOGIN_DAYS]: '连续登录天数',
[AchievementConditionType.RESOURCE_COLLECT_COUNT]: '收藏资源数量',
[AchievementConditionType.TASK_COMPLETE_COUNT]: '完成任务数量',
[AchievementConditionType.POINTS_EARNED]: '获得积分数量',
[AchievementConditionType.COMMENT_COUNT]: '发表评论数量',
[AchievementConditionType.CHAPTER_COMPLETE_COUNT]: '完成章节数量',
[AchievementConditionType.TOTAL_LOGIN_DAYS]: '累计登录天数'
};
/**
* 成就事件类型描述映射
*/
export const AchievementEventTypeDescriptions: Record<AchievementEventType, string> = {
// 学习相关事件
[AchievementEventType.LEARNING_TIME_UPDATED]: '学习时长更新',
[AchievementEventType.COURSE_COMPLETED]: '课程完成',
[AchievementEventType.COURSE_STARTED]: '开始学习课程',
[AchievementEventType.CHAPTER_COMPLETED]: '章节完成',
// 资源相关事件
[AchievementEventType.RESOURCE_VIEWED]: '浏览资源',
[AchievementEventType.RESOURCE_COLLECTED]: '收藏资源',
[AchievementEventType.RESOURCE_SHARED]: '分享资源',
// 任务相关事件
[AchievementEventType.TASK_COMPLETED]: '任务完成',
[AchievementEventType.TASK_ITEM_COMPLETED]: '任务项完成',
// 互动相关事件
[AchievementEventType.COMMENT_POSTED]: '发表评论',
[AchievementEventType.LIKE_GIVEN]: '点赞',
// 登录相关事件
[AchievementEventType.USER_LOGIN]: '用户登录',
[AchievementEventType.CONTINUOUS_LOGIN]: '连续登录',
// 积分相关事件
[AchievementEventType.POINTS_EARNED_EVENT]: '获得积分',
// 测试相关事件
[AchievementEventType.TEST_PASSED]: '测试通过',
[AchievementEventType.TEST_PERFECT_SCORE]: '测试满分'
};
/**
* 成就枚举辅助类
*/
export class AchievementEnumHelper {
/**
* 获取成就类型描述
* @param type 成就类型
* @returns 描述文本
*/
static getAchievementTypeDescription(type: AchievementType): string {
return AchievementTypeDescriptions[type] || '未知类型';
}
/**
* 获取成就条件类型描述
* @param type 条件类型
* @returns 描述文本
*/
static getConditionTypeDescription(type: AchievementConditionType): string {
return AchievementConditionTypeDescriptions[type] || '未知条件';
}
/**
* 获取成就事件类型描述
* @param type 事件类型
* @returns 描述文本
*/
static getEventTypeDescription(type: AchievementEventType): string {
return AchievementEventTypeDescriptions[type] || '未知事件';
}
/**
* 根据code获取成就条件类型
* @param code 条件类型代码
* @returns 条件类型枚举值或null
*/
static getConditionTypeFromCode(code: number): AchievementConditionType | null {
const types = Object.values(AchievementConditionType).filter(
(value): value is AchievementConditionType => typeof value === 'number'
);
return types.find(type => type === code) || null;
}
/**
* 根据code获取成就事件类型
* @param code 事件类型代码
* @returns 事件类型枚举值或null
*/
static getEventTypeFromCode(code: string): AchievementEventType | null {
const types = Object.values(AchievementEventType).filter(
(value): value is AchievementEventType => typeof value === 'string'
);
return types.find(type => type === code) || null;
}
/**
* 获取所有成就条件类型选项(用于下拉框等)
* @returns 选项数组
*/
static getAllConditionTypeOptions(): Array<{ value: AchievementConditionType; label: string }> {
return Object.entries(AchievementConditionTypeDescriptions).map(([value, label]) => ({
value: Number(value) as AchievementConditionType,
label
}));
}
/**
* 获取所有成就类型选项(用于下拉框等)
* @returns 选项数组
*/
static getAllAchievementTypeOptions(): Array<{ value: AchievementType; label: string }> {
return Object.entries(AchievementTypeDescriptions).map(([value, label]) => ({
value: Number(value) as AchievementType,
label
}));
}
/**
* 获取所有成就事件类型选项(用于下拉框等)
* @returns 选项数组
*/
static getAllEventTypeOptions(): Array<{ value: AchievementEventType; label: string }> {
return Object.entries(AchievementEventTypeDescriptions).map(([value, label]) => ({
value: value as AchievementEventType,
label
}));
}
/**
* 格式化条件值显示
* @param conditionType 条件类型
* @param conditionValue 条件值
* @returns 格式化后的显示文本
*/
static formatConditionValue(conditionType: AchievementConditionType, conditionValue: number): string {
const typeDesc = this.getConditionTypeDescription(conditionType);
switch (conditionType) {
case AchievementConditionType.LEARNING_TIME:
return `${typeDesc}达到${conditionValue}分钟`;
case AchievementConditionType.RESOURCE_VIEW_COUNT:
return `${typeDesc}达到${conditionValue}`;
case AchievementConditionType.COURSE_COMPLETE_COUNT:
return `${typeDesc}达到${conditionValue}`;
case AchievementConditionType.CONTINUOUS_LOGIN_DAYS:
return `${typeDesc}达到${conditionValue}`;
case AchievementConditionType.RESOURCE_COLLECT_COUNT:
return `${typeDesc}达到${conditionValue}`;
case AchievementConditionType.TASK_COMPLETE_COUNT:
return `${typeDesc}达到${conditionValue}`;
case AchievementConditionType.POINTS_EARNED:
return `${typeDesc}达到${conditionValue}`;
case AchievementConditionType.COMMENT_COUNT:
return `${typeDesc}达到${conditionValue}`;
case AchievementConditionType.CHAPTER_COMPLETE_COUNT:
return `${typeDesc}达到${conditionValue}`;
case AchievementConditionType.TOTAL_LOGIN_DAYS:
return `${typeDesc}达到${conditionValue}`;
default:
return `${typeDesc}达到${conditionValue}`;
}
}
}

View File

@@ -210,3 +210,90 @@ export enum TaskItemType {
/** 课程类型 */ /** 课程类型 */
COURSE = 2 COURSE = 2
} }
/**
* 成就类型枚举
*/
export enum AchievementType {
/** 勋章 */
BADGE = 1,
/** 等级 */
LEVEL = 2
}
/**
* 成就条件类型枚举
*/
export enum AchievementConditionType {
/** 学习时长(分钟) */
LEARNING_TIME = 1,
/** 浏览资源数量 */
RESOURCE_VIEW_COUNT = 2,
/** 完成课程数量 */
COURSE_COMPLETE_COUNT = 3,
/** 连续登录天数 */
CONTINUOUS_LOGIN_DAYS = 4,
/** 收藏资源数量 */
RESOURCE_COLLECT_COUNT = 5,
/** 完成任务数量 */
TASK_COMPLETE_COUNT = 6,
/** 获得积分数量 */
POINTS_EARNED = 7,
/** 发表评论数量 */
COMMENT_COUNT = 8,
/** 完成章节数量 */
CHAPTER_COMPLETE_COUNT = 9,
/** 累计登录天数 */
TOTAL_LOGIN_DAYS = 10
}
/**
* 成就事件类型枚举
*/
export enum AchievementEventType {
// ==================== 学习相关事件 ====================
/** 学习时长更新 */
LEARNING_TIME_UPDATED = 'learning_time_updated',
/** 课程完成 */
COURSE_COMPLETED = 'course_completed',
/** 开始学习课程 */
COURSE_STARTED = 'course_started',
/** 章节完成 */
CHAPTER_COMPLETED = 'chapter_completed',
// ==================== 资源相关事件 ====================
/** 浏览资源 */
RESOURCE_VIEWED = 'resource_viewed',
/** 收藏资源 */
RESOURCE_COLLECTED = 'resource_collected',
/** 分享资源 */
RESOURCE_SHARED = 'resource_shared',
// ==================== 任务相关事件 ====================
/** 任务完成 */
TASK_COMPLETED = 'task_completed',
/** 任务项完成 */
TASK_ITEM_COMPLETED = 'task_item_completed',
// ==================== 互动相关事件 ====================
/** 发表评论 */
COMMENT_POSTED = 'comment_posted',
/** 点赞 */
LIKE_GIVEN = 'like_given',
// ==================== 登录相关事件 ====================
/** 用户登录 */
USER_LOGIN = 'user_login',
/** 连续登录 */
CONTINUOUS_LOGIN = 'continuous_login',
// ==================== 积分相关事件 ====================
/** 获得积分 */
POINTS_EARNED_EVENT = 'points_earned',
// ==================== 测试相关事件 ====================
/** 测试通过 */
TEST_PASSED = 'test_passed',
/** 测试满分 */
TEST_PERFECT_SCORE = 'test_perfect_score'
}

View File

@@ -22,6 +22,11 @@ export * from './menu';
// 权限相关 // 权限相关
export * from './permission'; export * from './permission';
// 系统相关
export * from './module';
export * from './achievement';
// 认证相关 // 认证相关
export * from './auth'; export * from './auth';
@@ -42,6 +47,7 @@ export * from './usercenter';
// 枚举类型 // 枚举类型
export * from './enums'; export * from './enums';
export * from './enums/achievement-enums';
// 常量 // 常量
export * from './constants'; export * from './constants';

View File

@@ -0,0 +1,162 @@
/**
* @description 系统相关类型定义
* @author yslg
* @since 2025-10-25
*/
import { BaseDTO } from '../base';
/**
* 系统模块
*/
export interface SysModule extends BaseDTO {
/** 模块ID */
moduleID?: string;
/** 模块名称 */
name?: string;
/** 模块代码 */
code?: string;
/** 模块描述 */
description?: string;
/** 模块图标 */
icon?: string;
/** 模块排序号 */
orderNum?: number;
/** 模块状态0禁用 1启用 */
status?: number;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 系统配置
*/
export interface SysConfig extends BaseDTO {
/** 配置键 */
configKey?: string;
/** 配置值 */
configValue?: string;
/** 配置名称 */
configName?: string;
/** 配置描述 */
description?: string;
/** 配置类型 */
configType?: string;
/** 是否系统内置 */
isSystem?: boolean;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 系统字典类型
*/
export interface SysDictType extends BaseDTO {
/** 字典类型 */
dictType?: string;
/** 字典名称 */
dictName?: string;
/** 状态0禁用 1启用 */
status?: number;
/** 备注 */
remark?: string;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 系统字典数据
*/
export interface SysDictData extends BaseDTO {
/** 字典排序 */
dictSort?: number;
/** 字典标签 */
dictLabel?: string;
/** 字典键值 */
dictValue?: string;
/** 字典类型 */
dictType?: string;
/** 状态0禁用 1启用 */
status?: number;
/** 是否默认 */
isDefault?: boolean;
/** 备注 */
remark?: string;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 系统通知
*/
export interface SysNotification extends BaseDTO {
/** 通知ID */
notificationID?: string;
/** 通知标题 */
title?: string;
/** 通知内容 */
content?: string;
/** 通知类型 */
type?: number;
/** 接收用户ID */
receiverID?: string;
/** 是否已读 */
isRead?: boolean;
/** 已读时间 */
readTime?: string;
/** 创建者 */
creator?: string;
}
/**
* 系统操作日志
*/
export interface SysOperationLog extends BaseDTO {
/** 操作用户 */
operator?: string;
/** 操作模块 */
module?: string;
/** 操作类型 */
operationType?: string;
/** 操作方法 */
method?: string;
/** 请求参数 */
requestParams?: string;
/** 返回结果 */
responseResult?: string;
/** 操作状态0失败 1成功 */
status?: number;
/** 错误消息 */
errorMsg?: string;
/** 操作IP */
operationIP?: string;
/** 操作时间 */
operationTime?: string;
/** 执行时长(毫秒) */
duration?: number;
}
/**
* 系统访问统计
*/
export interface SysVisitStatistics extends BaseDTO {
/** 统计日期 */
statisticsDate?: string;
/** 访问量 */
visitCount?: number;
/** 独立访客数 */
uniqueVisitors?: number;
/** 页面浏览量 */
pageViews?: number;
/** 平均访问时长(秒) */
avgDuration?: number;
}

View File

@@ -7,6 +7,7 @@
import { BaseDTO } from '../base'; import { BaseDTO } from '../base';
import { SysMenu } from '../menu'; import { SysMenu } from '../menu';
import { SysRole } from '../role'; import { SysRole } from '../role';
import { SysModule } from '../module';
/** /**
* 系统权限 * 系统权限
@@ -16,6 +17,7 @@ export interface SysPermission extends BaseDTO {
id?:string; id?:string;
/** 权限ID */ /** 权限ID */
permissionID?: string; permissionID?: string;
moduleID?: string;
/** 权限名称 */ /** 权限名称 */
name?: string; name?: string;
/** 权限描述 */ /** 权限描述 */
@@ -34,5 +36,5 @@ export interface SysPermission extends BaseDTO {
menus?: SysMenu[]; menus?: SysMenu[];
roles?: SysRole[]; roles?: SysRole[];
permissions?: SysPermission[]; permissions?: SysPermission[];
module?: SysModule;
} }

View File

@@ -68,38 +68,6 @@ export interface PointsRecord extends BaseDTO {
relatedType?: number; relatedType?: number;
} }
/**
* 用户成就实体
*/
export interface UserAchievement extends BaseDTO {
/** 用户ID */
userID?: string;
/** 成就ID */
achievementID?: string;
/** 获得时间 */
achieveTime?: string;
}
/**
* 成就实体
*/
export interface Achievement extends BaseDTO {
/** 成就名称 */
name?: string;
/** 成就描述 */
description?: string;
/** 成就图标 */
icon?: string;
/** 成就类型 */
type?: number;
/** 获得条件 */
condition?: string;
/** 奖励积分 */
rewardPoints?: number;
/** 状态0禁用 1启用 */
status?: number;
}
/** /**
* 个人中心统计信息 * 个人中心统计信息
*/ */

View File

@@ -0,0 +1,618 @@
<template>
<div class="achievement-management">
<div class="header">
<h2>成就管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增成就
</el-button>
</div>
<!-- 筛选条件 -->
<div class="filter-bar">
<div class="filter-item">
<span class="filter-label">成就类型</span>
<el-select v-model="filter.type" placeholder="全部" clearable style="width: 150px">
<el-option
v-for="option in achievementTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-item">
<span class="filter-label">成就等级</span>
<el-input-number
v-model="filter.level"
:min="0"
:max="10"
placeholder="全部"
clearable
style="width: 120px"
/>
</div>
<div class="filter-item">
<span class="filter-label">条件类型</span>
<el-select v-model="filter.conditionType" placeholder="全部" clearable style="width: 180px">
<el-option
v-for="option in conditionTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-actions">
<el-button type="primary" @click="loadAchievementList">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
</div>
</div>
<!-- 成就列表 -->
<el-table
:data="achievementList"
style="width: 100%"
v-loading="loading"
border
stripe
row-key="achievementID"
>
<el-table-column prop="icon" label="图标" width="80" align="center">
<template #default="{ row }">
<el-image
:src="getIconUrl(row.icon)"
style="width: 48px; height: 48px"
fit="contain"
:preview-src-list="[getIconUrl(row.icon)]"
>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
</el-table-column>
<el-table-column prop="name" label="成就名称" min-width="150" />
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="type" label="类型" width="100">
<template #default="{ row }">
<el-tag :type="row.type === 1 ? 'success' : 'primary'">
{{ getAchievementTypeLabel(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="level" label="等级" width="80" align="center" />
<el-table-column prop="conditionType" label="条件类型" width="150">
<template #default="{ row }">
{{ getConditionTypeLabel(row.conditionType) }}
</template>
</el-table-column>
<el-table-column prop="conditionValue" label="条件值" width="100" align="center" />
<el-table-column prop="points" label="积分奖励" width="100" align="center" />
<el-table-column prop="orderNum" label="排序号" width="80" align="center" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
link
>
编辑
</el-button>
<el-button
type="info"
size="small"
@click="handleViewUsers(row)"
link
>
查看获得者
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
link
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑成就对话框 -->
<el-dialog
v-model="achievementDialogVisible"
:title="isEdit ? '编辑成就' : '新增成就'"
width="700px"
@close="resetForm"
>
<el-form
ref="achievementFormRef"
:model="currentAchievement"
:rules="achievementFormRules"
label-width="120px"
>
<el-form-item label="成就名称" prop="name">
<el-input v-model="currentAchievement.name" placeholder="请输入成就名称" />
</el-form-item>
<el-form-item label="成就描述" prop="description">
<el-input
v-model="currentAchievement.description"
type="textarea"
:rows="3"
placeholder="请输入成就描述"
/>
</el-form-item>
<el-form-item label="成就图标" prop="icon">
<el-input
v-model="currentAchievement.icon"
placeholder="请输入图标路径,如:/img/achievement/v1-icon.svg 或完整URL"
>
<template #append>
<el-button @click="showIconUpload = true">上传</el-button>
</template>
</el-input>
<div v-if="currentAchievement.icon" style="margin-top: 10px">
<div style="display: flex; align-items: center; gap: 10px;">
<el-image
:src="getIconUrl(currentAchievement.icon)"
style="width: 80px; height: 80px"
fit="contain"
>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
<div style="font-size: 12px; color: #909399;">
<div>路径: {{ currentAchievement.icon }}</div>
<div>完整URL: {{ getIconUrl(currentAchievement.icon) }}</div>
</div>
</div>
</div>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="成就类型" prop="type">
<el-select v-model="currentAchievement.type" placeholder="请选择成就类型" style="width: 100%">
<el-option
v-for="option in achievementTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="成就等级" prop="level">
<el-input-number
v-model="currentAchievement.level"
:min="1"
:max="10"
placeholder="请输入等级"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="条件类型" prop="conditionType">
<el-select
v-model="currentAchievement.conditionType"
placeholder="请选择条件类型"
style="width: 100%"
>
<el-option
v-for="option in conditionTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="条件值" prop="conditionValue">
<el-input-number
v-model="currentAchievement.conditionValue"
:min="1"
placeholder="请输入条件值"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="条件预览" v-if="currentAchievement.conditionType && currentAchievement.conditionValue">
<el-alert
:title="formatConditionValue(currentAchievement.conditionType, currentAchievement.conditionValue)"
type="info"
:closable="false"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="积分奖励" prop="points">
<el-input-number
v-model="currentAchievement.points"
:min="0"
placeholder="请输入积分奖励"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序号" prop="orderNum">
<el-input-number
v-model="currentAchievement.orderNum"
:min="0"
placeholder="请输入排序号"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="achievementDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveAchievement" :loading="submitting">
{{ isEdit ? '更新' : '创建' }}
</el-button>
</template>
</el-dialog>
<!-- 查看获得者对话框 -->
<el-dialog
v-model="usersDialogVisible"
title="成就获得者列表"
width="900px"
>
<div class="achievement-info-bar">
<el-alert
:title="`成就:${currentAchievement.name} - ${currentAchievement.description}`"
type="info"
:closable="false"
style="margin-bottom: 20px"
/>
</div>
<el-table
:data="achieverList"
v-loading="achieversLoading"
border
stripe
>
<el-table-column prop="userID" label="用户ID" width="200" />
<el-table-column prop="username" label="用户名" min-width="120" />
<el-table-column prop="realName" label="真实姓名" min-width="100" />
<el-table-column prop="obtainTime" label="获得时间" width="180" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button
type="danger"
size="small"
@click="handleRevokeAchievement(row)"
>
撤销成就
</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Picture } from '@element-plus/icons-vue';
import { achievementApi } from '@/apis/achievement';
import type { Achievement, UserAchievement } from '@/types';
import { AchievementEnumHelper } from '@/types/enums/achievement-enums';
import { PUBLIC_IMG_PATH } from '@/config';
// 响应式数据
const loading = ref(false);
const submitting = ref(false);
const achieversLoading = ref(false);
const achievementList = ref<Achievement[]>([]);
const achieverList = ref<UserAchievement[]>([]);
// 对话框控制
const achievementDialogVisible = ref(false);
const usersDialogVisible = ref(false);
const showIconUpload = ref(false);
const isEdit = ref(false);
// 筛选条件
const filter = ref<Partial<Achievement>>({});
// 当前操作的成就
const currentAchievement = ref<Achievement>({});
// 枚举选项
const achievementTypeOptions = AchievementEnumHelper.getAllAchievementTypeOptions();
const conditionTypeOptions = AchievementEnumHelper.getAllConditionTypeOptions();
// 表单引用
const achievementFormRef = ref();
// 表单验证规则
const achievementFormRules = {
name: [
{ required: true, message: '请输入成就名称', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入成就描述', trigger: 'blur' }
],
icon: [
{ required: true, message: '请输入图标URL', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择成就类型', trigger: 'change' }
],
level: [
{ required: true, message: '请输入成就等级', trigger: 'blur' }
],
conditionType: [
{ required: true, message: '请选择条件类型', trigger: 'change' }
],
conditionValue: [
{ required: true, message: '请输入条件值', trigger: 'blur' }
],
points: [
{ required: true, message: '请输入积分奖励', trigger: 'blur' }
],
orderNum: [
{ required: true, message: '请输入排序号', trigger: 'blur' }
]
};
// 获取成就类型标签
function getAchievementTypeLabel(type?: number): string {
if (type === undefined) return '未知';
return AchievementEnumHelper.getAchievementTypeDescription(type);
}
// 获取条件类型标签
function getConditionTypeLabel(type?: number): string {
if (type === undefined) return '未知';
return AchievementEnumHelper.getConditionTypeDescription(type);
}
// 格式化条件值显示
function formatConditionValue(conditionType?: number, conditionValue?: number): string {
if (conditionType === undefined || conditionValue === undefined) return '';
return AchievementEnumHelper.formatConditionValue(conditionType, conditionValue);
}
// 获取图标完整路径
function getIconUrl(icon?: string): string {
if (!icon) return '';
// 如果是http或https开头直接返回
if (icon.startsWith('http://') || icon.startsWith('https://')) {
return icon;
}
// 否则拼接默认成就图标路径
const path = `${PUBLIC_IMG_PATH}/achievement`;
return icon.startsWith('/') ? `${path}${icon}` : `${path}/${icon}`;
}
// 加载成就列表
async function loadAchievementList() {
try {
loading.value = true;
const result = await achievementApi.getAllAchievements(filter.value);
achievementList.value = result.dataList || [];
} catch (error) {
console.error('加载成就列表失败:', error);
ElMessage.error('加载成就列表失败');
} finally {
loading.value = false;
}
}
// 重置筛选条件
function resetFilter() {
filter.value = {};
loadAchievementList();
}
// 新增成就
function handleAdd() {
isEdit.value = false;
currentAchievement.value = {
type: 1,
level: 1,
points: 0,
orderNum: 0
};
achievementDialogVisible.value = true;
}
// 编辑成就
function handleEdit(row: Achievement) {
isEdit.value = true;
currentAchievement.value = { ...row };
achievementDialogVisible.value = true;
}
// 删除成就
async function handleDelete(row: Achievement) {
try {
await ElMessageBox.confirm(
`确定要删除成就 "${row.name}" 吗?删除后无法恢复。`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
await achievementApi.deleteAchievement(row);
ElMessage.success('删除成功');
await loadAchievementList();
} catch (error) {
if (error !== 'cancel') {
console.error('删除成就失败:', error);
ElMessage.error('删除成就失败');
}
}
}
// 保存成就
async function saveAchievement() {
try {
await achievementFormRef.value?.validate();
submitting.value = true;
if (isEdit.value) {
await achievementApi.updateAchievement(currentAchievement.value);
ElMessage.success('更新成功');
} else {
await achievementApi.createAchievement(currentAchievement.value);
ElMessage.success('创建成功');
}
achievementDialogVisible.value = false;
await loadAchievementList();
} catch (error) {
console.error('保存成就失败:', error);
if (error !== false) {
ElMessage.error('保存成就失败');
}
} finally {
submitting.value = false;
}
}
// 查看获得者
async function handleViewUsers(row: Achievement) {
currentAchievement.value = { ...row };
usersDialogVisible.value = true;
try {
achieversLoading.value = true;
const result = await achievementApi.getRecentAchievers(
{ page: 1, size: 100 },
{ achievementID: row.achievementID }
);
achieverList.value = result.dataList || [];
} catch (error) {
console.error('加载获得者列表失败:', error);
ElMessage.error('加载获得者列表失败');
} finally {
achieversLoading.value = false;
}
}
// 撤销成就
async function handleRevokeAchievement(row: UserAchievement) {
try {
await ElMessageBox.confirm(
'确定要撤销该用户的成就吗?',
'确认撤销',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
await achievementApi.revokeAchievement(row.userID || '', row.achievementID || '');
ElMessage.success('撤销成功');
// 刷新获得者列表
await handleViewUsers(currentAchievement.value);
} catch (error) {
if (error !== 'cancel') {
console.error('撤销成就失败:', error);
ElMessage.error('撤销成就失败');
}
}
}
// 重置表单
function resetForm() {
currentAchievement.value = {};
achievementFormRef.value?.clearValidate();
}
// 组件挂载时加载数据
onMounted(() => {
loadAchievementList();
});
</script>
<style scoped lang="scss">
.achievement-management {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #141F38;
}
}
.filter-bar {
display: flex;
align-items: center;
gap: 16px;
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
.filter-item {
display: flex;
align-items: center;
gap: 8px;
.filter-label {
font-size: 14px;
color: #606266;
white-space: nowrap;
}
}
.filter-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
}
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #f5f5f5;
color: #ccc;
font-size: 24px;
}
.achievement-info-bar {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,2 @@
export { default as AchievementManagementView } from './AchievementManagementView.vue';

File diff suppressed because it is too large Load Diff

View File

@@ -1,721 +0,0 @@
<template>
<div class="permission-manage">
<div class="header">
<h2>权限管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增权限
</el-button>
</div>
<el-table
:data="permissionList"
style="width: 100%"
v-loading="loading"
border
stripe
>
<el-table-column prop="name" label="权限名称" min-width="150" />
<el-table-column prop="code" label="权限编码" min-width="200" />
<el-table-column prop="description" label="权限描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="creatorName" label="创建人" width="120" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
link
>
编辑
</el-button>
<el-button
type="primary"
size="small"
@click="handleBindMenu(row)"
link
>
绑定菜单
</el-button>
<el-button
type="primary"
size="small"
@click="handleBindRole(row)"
link
>
绑定角色
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
link
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑权限' : '新增权限'"
width="500px"
@close="resetForm"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="权限名称" prop="name">
<el-input
v-model="formData.name"
placeholder="请输入权限名称"
clearable
/>
</el-form-item>
<el-form-item label="权限编码" prop="code">
<el-input
v-model="formData.code"
placeholder="请输入权限编码"
clearable
/>
</el-form-item>
<el-form-item label="权限描述" prop="description">
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入权限描述"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitting"
>
确定
</el-button>
</template>
</el-dialog>
<!-- 绑定菜单对话框 -->
<el-dialog v-model="bindMenuDialogVisible" title="绑定菜单" width="800px" @close="resetBindList">
<div class="menu-binding-container">
<!-- 权限信息显示 -->
<div class="permission-info" v-if="currentPermission">
<h4>权限信息{{ currentPermission.name }}</h4>
<p>权限编码{{ currentPermission.code }}</p>
</div>
<!-- 菜单绑定状态表格 -->
<el-table :data="menuList" style="width: 100%" border stripe>
<el-table-column width="80" label="绑定状态">
<template #default="{ row }">
<el-tag
:type="isMenuSelected(row.menuID) ? 'success' : 'info'"
size="small"
>
{{ isMenuSelected(row.menuID) ? '已绑定' : '未绑定' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="菜单名称" min-width="150" />
<el-table-column prop="menuID" label="菜单ID" min-width="120" />
<el-table-column prop="url" label="菜单路径" min-width="200" show-overflow-tooltip />
<el-table-column prop="component" label="菜单组件" min-width="180" show-overflow-tooltip />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button
:type="isMenuSelected(row.menuID) ? 'danger' : 'primary'"
size="small"
@click="toggleMenuSelection(row)"
>
{{ isMenuSelected(row.menuID) ? '解绑' : '绑定' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 统计信息 -->
<div class="binding-stats">
<el-alert
:title="`已绑定 ${selectedMenus.length} 个菜单,未绑定 ${menuList.length - selectedMenus.length} 个菜单`"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
<template #footer>
<el-button @click="bindMenuDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveMenuBinding" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
<!-- 绑定角色对话框 -->
<el-dialog v-model="bindRoleDialogVisible" title="绑定角色" width="800px" @close="resetBindList">
<div class="role-binding-container">
<!-- 权限信息显示 -->
<div class="permission-info" v-if="currentPermission">
<h4>权限信息{{ currentPermission.name }}</h4>
<p>权限编码{{ currentPermission.code }}</p>
</div>
<!-- 角色绑定状态表格 -->
<el-table :data="roleList" style="width: 100%" border stripe>
<el-table-column width="80" label="绑定状态">
<template #default="{ row }">
<el-tag
:type="isRoleSelected(row.id) ? 'success' : 'info'"
size="small"
>
{{ isRoleSelected(row.id) ? '已绑定' : '未绑定' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="角色名称" min-width="150" />
<el-table-column prop="id" label="角色ID" min-width="120" />
<el-table-column prop="description" label="角色描述" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button
:type="isRoleSelected(row.id) ? 'danger' : 'primary'"
size="small"
@click="toggleRoleSelection(row)"
>
{{ isRoleSelected(row.id) ? '解绑' : '绑定' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 统计信息 -->
<div class="binding-stats">
<el-alert
:title="`已绑定 ${selectedRoles.length} 个角色,未绑定 ${roleList.length - selectedRoles.length} 个角色`"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
<template #footer>
<el-button @click="bindRoleDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveRoleBinding" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { permissionApi } from '@/apis/system/permission';
import { roleApi } from '@/apis/system/role';
import { menuApi } from '@/apis/system/menu';
import { SysPermission, SysRole, SysMenu } from '@/types';
import { ref, onMounted, reactive } from 'vue';
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
// 数据状态
const permissionList = ref<SysPermission[]>([]);
const loading = ref(false);
const submitting = ref(false);
const bindList = ref<SysPermission>({
menus: [],
roles: []
});
const roleList = ref<SysRole[]>([]);
const menuList = ref<SysMenu[]>([]);
const selectedMenus = ref<string[]>([]);
const selectedRoles = ref<string[]>([]);
const currentPermission = ref<SysPermission | null>(null);
// 对话框状态
const dialogVisible = ref(false);
const isEdit = ref(false);
const formRef = ref<FormInstance>();
const bindMenuDialogVisible = ref(false);
const bindRoleDialogVisible = ref(false);
const queryFilter = ref<SysPermission>({
name: '',
code: '',
description: ''
});
// 表单数据
const formData = reactive<SysPermission>({
name: '',
code: '',
description: ''
});
// 表单验证规则
const formRules: FormRules = {
name: [
{ required: true, message: '请输入权限名称', trigger: 'blur' },
{ min: 2, max: 50, message: '权限名称长度在 2 到 50 个字符', trigger: 'blur' }
],
code: [
{ required: true, message: '请输入权限编码', trigger: 'blur' },
{
pattern: /^[a-zA-Z][a-zA-Z0-9:_-]*$/,
message: '权限编码只能包含字母、数字、冒号、下划线和横线,且必须以字母开头',
trigger: 'blur'
},
{ min: 2, max: 100, message: '权限编码长度在 2 到 100 个字符', trigger: 'blur' }
],
description: [
{ max: 200, message: '权限描述不能超过 200 个字符', trigger: 'blur' }
]
};
// 加载权限列表
async function loadPermissionList() {
try {
loading.value = true;
const result = await permissionApi.getPermissionList(queryFilter.value);
permissionList.value = result.dataList || [];
} catch (error) {
console.error('加载权限列表失败:', error);
ElMessage.error('加载权限列表失败');
} finally {
loading.value = false;
}
}
// 新增权限
function handleAdd() {
isEdit.value = false;
dialogVisible.value = true;
}
// 编辑权限
function handleEdit(row: SysPermission) {
isEdit.value = true;
Object.assign(formData, row);
dialogVisible.value = true;
}
// 查看绑定菜单
async function handleBindMenu(row: SysPermission) {
currentPermission.value = row;
row.bindType = "menu";
try {
// 获取已绑定的菜单
const bindingResult = await permissionApi.getPermissionBindingList(row);
bindList.value.menus = bindingResult.data?.menus || [];
// 获取所有菜单
const menuResult = await menuApi.getAllMenuList();
menuList.value = menuResult.dataList || [];
// 设置已选中的菜单
selectedMenus.value = bindList.value.menus.map(menu => menu.menuID).filter((id): id is string => !!id);
console.log('已绑定的菜单:', bindList.value.menus);
console.log('所有菜单:', menuList.value);
bindMenuDialogVisible.value = true;
} catch (error) {
console.error('获取菜单绑定信息失败:', error);
ElMessage.error('获取菜单绑定信息失败');
}
}
// 查看绑定角色
async function handleBindRole(row: SysPermission) {
currentPermission.value = row;
row.bindType = "role";
try {
// 获取已绑定的角色
const bindingResult = await permissionApi.getPermissionBindingList(row);
bindList.value.roles = bindingResult.data?.roles || [];
// 获取所有角色
const roleResult = await roleApi.getAllRoles();
roleList.value = roleResult.dataList || [];
// 设置已选中的角色
selectedRoles.value = bindList.value.roles.map(role => role.id).filter((id): id is string => !!id);
console.log('已绑定的角色:', bindList.value.roles);
console.log('所有角色:', roleList.value);
bindRoleDialogVisible.value = true;
} catch (error) {
console.error('获取角色绑定信息失败:', error);
ElMessage.error('获取角色绑定信息失败');
}
}
// 删除权限
async function handleDelete(row: SysPermission) {
try {
await ElMessageBox.confirm(
`确定要删除权限 "${row.name}" 吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
await permissionApi.deletePermission(row);
ElMessage.success('删除成功');
await loadPermissionList();
} catch (error) {
if (error !== 'cancel') {
console.error('删除权限失败:', error);
ElMessage.error('删除权限失败');
}
}
}
// 重置绑定列表
function resetBindList() {
bindList.value = {
menus: [],
roles: []
};
selectedMenus.value = [];
selectedRoles.value = [];
currentPermission.value = null;
}
// 检查菜单是否已选中
function isMenuSelected(menuID: string | undefined): boolean {
return menuID ? selectedMenus.value.includes(menuID) : false;
}
// 切换菜单选择状态
function toggleMenuSelection(menu: SysMenu) {
if (!menu.menuID) return;
const index = selectedMenus.value.indexOf(menu.menuID);
if (index > -1) {
selectedMenus.value.splice(index, 1);
} else {
selectedMenus.value.push(menu.menuID);
}
}
// 保存菜单绑定
async function saveMenuBinding() {
if (!currentPermission.value || !currentPermission.value.permissionID) {
ElMessage.error('权限信息不完整');
return;
}
try {
submitting.value = true;
// 获取当前已绑定的菜单ID
const currentBoundMenus = (bindList.value.menus || []).map(menu => menu.menuID).filter((id): id is string => !!id);
// 找出需要绑定的菜单(新增的)
const menusToBind = selectedMenus.value.filter(menuID => !currentBoundMenus.includes(menuID));
// 找出需要解绑的菜单(移除的)
const menusToUnbind = currentBoundMenus.filter(menuID => !selectedMenus.value.includes(menuID));
// 构建需要绑定的菜单对象数组
if (menusToBind.length > 0) {
const menusToBindObjects = menusToBind.map(menuID => {
const menu = menuList.value.find(m => m.menuID === menuID);
return menu || { menuID: menuID };
});
const bindPermission = {
...currentPermission.value,
menus: menusToBindObjects,
permissions: [{permissionID: currentPermission.value.permissionID}]
};
await permissionApi.bindMenu(bindPermission);
}
// 构建需要解绑的菜单对象数组
if (menusToUnbind.length > 0) {
const menusToUnbindObjects = menusToUnbind.map(menuID => {
const menu = menuList.value.find(m => m.menuID === menuID);
return menu || { menuID: menuID };
});
const unbindPermission = {
...currentPermission.value,
menus: menusToUnbindObjects,
permissions: [{permissionID: currentPermission.value.permissionID}]
};
await permissionApi.unbindMenu(unbindPermission);
}
ElMessage.success('菜单绑定保存成功');
bindMenuDialogVisible.value = false;
// 刷新权限列表
await loadPermissionList();
} catch (error) {
console.error('保存菜单绑定失败:', error);
ElMessage.error('保存菜单绑定失败');
} finally {
submitting.value = false;
}
}
// 检查角色是否已选中
function isRoleSelected(roleID: string | undefined): boolean {
return roleID ? selectedRoles.value.includes(roleID) : false;
}
// 切换角色选择状态
function toggleRoleSelection(role: SysRole) {
if (!role.id) return;
const index = selectedRoles.value.indexOf(role.id);
if (index > -1) {
selectedRoles.value.splice(index, 1);
} else {
selectedRoles.value.push(role.id);
}
}
// 保存角色绑定
async function saveRoleBinding() {
if (!currentPermission.value || !currentPermission.value.permissionID) {
ElMessage.error('权限信息不完整');
return;
}
try {
submitting.value = true;
// 获取当前已绑定的角色ID
const currentBoundRoles = (bindList.value.roles || []).map(role => role.id).filter((id): id is string => !!id);
// 找出需要绑定的角色(新增的)
const rolesToBind = selectedRoles.value.filter(roleID => !currentBoundRoles.includes(roleID));
// 找出需要解绑的角色(移除的)
const rolesToUnbind = currentBoundRoles.filter(roleID => !selectedRoles.value.includes(roleID));
// 构建需要绑定的角色对象数组
if (rolesToBind.length > 0) {
const rolesToBindObjects = rolesToBind.map(roleID => {
const role = roleList.value.find(r => r.id === roleID);
return role || { id: roleID };
});
const bindPermission = {
...currentPermission.value,
roles: rolesToBindObjects,
permissions: [{permissionID: currentPermission.value.permissionID}]
};
await permissionApi.bindRole(bindPermission);
}
// 构建需要解绑的角色对象数组
if (rolesToUnbind.length > 0) {
const rolesToUnbindObjects = rolesToUnbind.map(roleID => {
const role = roleList.value.find(r => r.id === roleID);
return role || { id: roleID };
});
const unbindPermission = {
...currentPermission.value,
roles: rolesToUnbindObjects,
permissions: [{permissionID: currentPermission.value.permissionID}]
};
await permissionApi.unbindRole(unbindPermission);
}
ElMessage.success('角色绑定保存成功');
bindRoleDialogVisible.value = false;
// 刷新权限列表
await loadPermissionList();
} catch (error) {
console.error('保存角色绑定失败:', error);
ElMessage.error('保存角色绑定失败');
} finally {
submitting.value = false;
}
}
// 提交表单
async function handleSubmit() {
if (!formRef.value) return;
try {
await formRef.value.validate();
submitting.value = true;
if (isEdit.value) {
await permissionApi.updatePermission(formData);
ElMessage.success('更新成功');
} else {
await permissionApi.addPermission(formData);
ElMessage.success('新增成功');
}
dialogVisible.value = false;
await loadPermissionList();
} catch (error) {
if (error !== false) { // 表单验证失败时error为false
console.error('提交失败:', error);
ElMessage.error(isEdit.value ? '更新失败' : '新增失败');
}
} finally {
submitting.value = false;
}
}
// 重置表单
function resetForm() {
if (formRef.value) {
formRef.value.resetFields();
}
Object.assign(formData, {
name: '',
code: '',
description: ''
});
}
// 页面加载时获取权限列表
onMounted(() => {
loadPermissionList();
});
</script>
<style scoped lang="scss">
.permission-manage {
padding: 20px;
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
color: #303133;
font-size: 20px;
font-weight: 600;
}
}
.el-table {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
}
// 对话框样式优化
:deep(.el-dialog) {
.el-dialog__header {
padding: 20px 20px 10px;
border-bottom: 1px solid #ebeef5;
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.el-dialog__body {
padding: 20px;
}
.el-dialog__footer {
padding: 10px 20px 20px;
border-top: 1px solid #ebeef5;
}
}
// 表单样式优化
:deep(.el-form) {
.el-form-item__label {
font-weight: 500;
color: #606266;
}
.el-input__wrapper {
box-shadow: 0 0 0 1px #dcdfe6 inset;
&:hover {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
&.is-focus {
box-shadow: 0 0 0 1px #409eff inset;
}
}
.el-textarea__inner {
box-shadow: 0 0 0 1px #dcdfe6 inset;
&:hover {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
&:focus {
box-shadow: 0 0 0 1px #409eff inset;
}
}
}
// 按钮样式优化
:deep(.el-button) {
&.is-link {
padding: 0;
margin-right: 12px;
&:last-child {
margin-right: 0;
}
}
}
.menu-binding-container,
.role-binding-container {
.permission-info {
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
h4 {
margin: 0 0 8px 0;
color: #303133;
font-size: 16px;
}
p {
margin: 0;
color: #606266;
font-size: 14px;
}
}
.binding-stats {
margin-top: 20px;
}
}
</style>

View File

@@ -3,59 +3,257 @@
<div class="achievements-header"> <div class="achievements-header">
<h2>我的成就</h2> <h2>我的成就</h2>
<div class="achievement-stats"> <div class="achievement-stats">
<span>已获得 <strong>{{ earnedCount }}</strong> / {{ totalCount }} 个成就</span> <div class="stat-item">
<span class="stat-label">已获得</span>
<span class="stat-value">{{ earnedCount }}</span>
<span class="stat-total"> / {{ totalCount }}</span>
</div>
<div class="stat-item">
<span class="stat-label">完成率</span>
<span class="stat-value">{{ completionRate }}%</span>
</div>
</div> </div>
</div> </div>
<div class="achievements-grid"> <!-- 成就类型筛选 -->
<div class="filter-tabs">
<el-radio-group v-model="selectedType" @change="filterAchievements">
<el-radio-button :label="undefined">全部</el-radio-button>
<el-radio-button
v-for="option in achievementTypeOptions"
:key="option.value"
:label="option.value"
>
{{ option.label }}
</el-radio-button>
</el-radio-group>
<el-checkbox v-model="showOnlyEarned" @change="filterAchievements">
仅显示已获得
</el-checkbox>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<el-skeleton :rows="3" animated />
</div>
<!-- 成就网格 -->
<div v-else class="achievements-grid">
<div <div
class="achievement-item" class="achievement-item"
v-for="achievement in achievements" v-for="achievement in filteredAchievements"
:key="achievement.id" :key="achievement.achievementID"
:class="{ earned: achievement.earned, locked: !achievement.earned }" :class="{ earned: achievement.obtained, locked: !achievement.obtained }"
> >
<div class="achievement-icon"> <div class="achievement-icon">
<img :src="achievement.icon" :alt="achievement.name" /> <el-image
<div class="achievement-badge" v-if="achievement.earned"></div> :src="getIconUrl(achievement.icon)"
:alt="achievement.name"
fit="contain"
>
<template #error>
<div class="image-placeholder">
<el-icon><Trophy /></el-icon>
</div>
</template>
</el-image>
<div class="achievement-badge" v-if="achievement.obtained">
<el-icon><Check /></el-icon>
</div>
<div class="achievement-level" v-if="achievement.level">
Lv.{{ achievement.level }}
</div>
</div> </div>
<div class="achievement-info"> <div class="achievement-info">
<div class="achievement-header">
<h3>{{ achievement.name }}</h3> <h3>{{ achievement.name }}</h3>
<el-tag
:type="achievement.type === 1 ? 'success' : 'primary'"
size="small"
>
{{ getAchievementTypeLabel(achievement.type) }}
</el-tag>
</div>
<p class="achievement-description">{{ achievement.description }}</p> <p class="achievement-description">{{ achievement.description }}</p>
<div class="achievement-progress" v-if="!achievement.earned && achievement.progress">
<div class="progress-bar"> <!-- 条件说明 -->
<div class="progress-fill" :style="{ width: achievement.progress + '%' }"></div> <div class="achievement-condition">
<el-icon><InfoFilled /></el-icon>
<span>{{ formatConditionValue(achievement.conditionType, achievement.conditionValue) }}</span>
</div> </div>
<span class="progress-text">{{ achievement.progress }}%</span>
<!-- 进度条 -->
<div class="achievement-progress" v-if="!achievement.obtained">
<div class="progress-info">
<span class="progress-label">进度</span>
<span class="progress-text">
{{ achievement.currentValue || 0 }} / {{ achievement.targetValue || achievement.conditionValue }}
</span>
</div> </div>
<div class="achievement-date" v-if="achievement.earned"> <el-progress
获得时间{{ achievement.earnedDate }} :percentage="achievement.progressPercentage || 0"
:color="progressColor"
:show-text="false"
/>
</div>
<!-- 获得信息 -->
<div class="achievement-footer" v-if="achievement.obtained">
<div class="achievement-date">
<el-icon><Calendar /></el-icon>
<span>{{ formatDate(achievement.obtainTime) }}</span>
</div>
<div class="achievement-points" v-if="achievement.points">
<el-icon><Star /></el-icon>
<span>+{{ achievement.points }} 积分</span>
</div>
</div>
<!-- 未获得时显示积分奖励 -->
<div class="achievement-reward" v-else-if="achievement.points">
<el-icon><Present /></el-icon>
<span>奖励 {{ achievement.points }} 积分</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 空状态 -->
<el-empty
v-if="!loading && filteredAchievements.length === 0"
description="暂无成就数据"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Trophy, Check, InfoFilled, Calendar, Star, Present } from '@element-plus/icons-vue';
import { achievementApi } from '@/apis/achievement';
import type { AchievementVO } from '@/types';
import { AchievementEnumHelper } from '@/types/enums/achievement-enums';
import { PUBLIC_IMG_PATH } from '@/config';
const achievements = ref<any[]>([]); // 响应式数据
const loading = ref(false);
const achievements = ref<AchievementVO[]>([]);
const selectedType = ref<number | undefined>(undefined);
const showOnlyEarned = ref(false);
// 枚举选项
const achievementTypeOptions = AchievementEnumHelper.getAllAchievementTypeOptions();
// 进度条颜色
const progressColor = [
{ color: '#f56c6c', percentage: 30 },
{ color: '#e6a23c', percentage: 60 },
{ color: '#5cb87a', percentage: 100 }
];
// 已获得数量
const earnedCount = computed(() => { const earnedCount = computed(() => {
return achievements.value.filter(a => a.earned).length; return achievements.value.filter(a => a.obtained).length;
}); });
// 总数量
const totalCount = computed(() => { const totalCount = computed(() => {
return achievements.value.length; return achievements.value.length;
}); });
// 完成率
const completionRate = computed(() => {
if (totalCount.value === 0) return 0;
return Math.round((earnedCount.value / totalCount.value) * 100);
});
// 筛选后的成就列表
const filteredAchievements = computed(() => {
let result = achievements.value;
// 按类型筛选
if (selectedType.value !== undefined) {
result = result.filter(a => a.type === selectedType.value);
}
// 仅显示已获得
if (showOnlyEarned.value) {
result = result.filter(a => a.obtained);
}
// 排序:已获得的在前,按等级排序
return result.sort((a, b) => {
if (a.obtained !== b.obtained) {
return a.obtained ? -1 : 1;
}
return (a.level || 0) - (b.level || 0);
});
});
// 获取成就类型标签
function getAchievementTypeLabel(type?: number): string {
if (type === undefined) return '未知';
return AchievementEnumHelper.getAchievementTypeDescription(type);
}
// 格式化条件值显示
function formatConditionValue(conditionType?: number, conditionValue?: number): string {
if (conditionType === undefined || conditionValue === undefined) return '';
return AchievementEnumHelper.formatConditionValue(conditionType, conditionValue);
}
// 格式化日期
function formatDate(dateStr?: string): string {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
// 获取图标完整路径
function getIconUrl(icon?: string): string {
if (!icon) return '';
// 如果是http或https开头直接返回
if (icon.startsWith('http://') || icon.startsWith('https://')) {
return icon;
}
// 否则拼接默认成就图标路径
const path = `${PUBLIC_IMG_PATH}/achievement`;
return icon.startsWith('/') ? `${path}${icon}` : `${path}/${icon}`;
}
// 筛选成就
function filterAchievements() {
// 触发计算属性重新计算
}
// 加载成就数据
async function loadAchievements() {
try {
loading.value = true;
const result = await achievementApi.getMyAchievements();
achievements.value = result.dataList || [];
} catch (error) {
console.error('加载成就数据失败:', error);
ElMessage.error('加载成就数据失败');
} finally {
loading.value = false;
}
}
onMounted(() => { onMounted(() => {
// TODO: 加载成就数据 loadAchievements();
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.my-achievements { .my-achievements {
padding: 20px 0;
.achievements-header { .achievements-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -63,27 +261,60 @@ onMounted(() => {
margin-bottom: 32px; margin-bottom: 32px;
h2 { h2 {
font-size: 24px; font-size: 28px;
font-weight: 600; font-weight: 600;
color: #141F38; color: #141F38;
margin: 0;
} }
} }
} }
.achievement-stats { .achievement-stats {
font-size: 16px; display: flex;
color: #666; gap: 32px;
strong { .stat-item {
color: #C62828; display: flex;
font-size: 20px; align-items: baseline;
gap: 8px;
.stat-label {
font-size: 14px;
color: #666;
} }
.stat-value {
font-size: 24px;
font-weight: 600;
color: #C62828;
}
.stat-total {
font-size: 16px;
color: #999;
}
}
}
.filter-tabs {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.loading-container {
padding: 40px 0;
} }
.achievements-grid { .achievements-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 20px; gap: 20px;
margin-bottom: 20px;
} }
.achievement-item { .achievement-item {
@@ -91,110 +322,233 @@ onMounted(() => {
gap: 16px; gap: 16px;
padding: 20px; padding: 20px;
border: 2px solid #e0e0e0; border: 2px solid #e0e0e0;
border-radius: 8px; border-radius: 12px;
transition: all 0.3s; transition: all 0.3s;
background: white;
&.earned { &.earned {
border-color: #C62828; border-color: #52c41a;
background: linear-gradient(135deg, #fff5f5, #ffffff); background: linear-gradient(135deg, #f6ffed, #ffffff);
.achievement-icon { .achievement-icon {
img { :deep(.el-image__inner) {
filter: none; filter: none;
} }
} }
} }
&.locked { &.locked {
opacity: 0.6; opacity: 0.75;
.achievement-icon { .achievement-icon {
img { :deep(.el-image__inner) {
filter: grayscale(100%); filter: grayscale(100%);
} }
} }
} }
&:hover.earned { &:hover.earned {
box-shadow: 0 4px 12px rgba(198, 40, 40, 0.2); box-shadow: 0 4px 16px rgba(82, 196, 26, 0.3);
transform: translateY(-2px);
}
&:hover.locked {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
} }
.achievement-icon { .achievement-icon {
position: relative; position: relative;
width: 64px; width: 80px;
height: 64px; height: 80px;
flex-shrink: 0; flex-shrink: 0;
img { :deep(.el-image) {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; }
.image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 8px;
color: #ccc;
font-size: 32px;
} }
} }
.achievement-badge { .achievement-badge {
position: absolute; position: absolute;
bottom: -4px; top: -4px;
right: -4px; right: -4px;
width: 24px; width: 28px;
height: 24px; height: 28px;
background: #4caf50; background: #52c41a;
color: white; color: white;
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 14px; font-size: 16px;
font-weight: 600; font-weight: 600;
border: 3px solid white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.achievement-level {
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
padding: 2px 8px;
background: #1890ff;
color: white;
font-size: 12px;
font-weight: 600;
border-radius: 10px;
border: 2px solid white; border: 2px solid white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
} }
.achievement-info { .achievement-info {
flex: 1; flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.achievement-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
h3 { h3 {
font-size: 16px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: #141F38; color: #141F38;
margin-bottom: 8px; margin: 0;
flex: 1;
}
} }
} }
.achievement-description { .achievement-description {
font-size: 14px; font-size: 14px;
color: #666; color: #666;
line-height: 1.5; line-height: 1.6;
margin-bottom: 12px; margin: 0;
}
.achievement-condition {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #999;
padding: 6px 12px;
background: #fafafa;
border-radius: 6px;
:deep(.el-icon) {
font-size: 14px;
}
} }
.achievement-progress { .achievement-progress {
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px; margin-bottom: 8px;
.progress-bar { .progress-label {
width: 100%; font-size: 13px;
height: 6px; color: #666;
background: #f5f5f5; font-weight: 500;
border-radius: 3px;
overflow: hidden;
margin-bottom: 4px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #C62828, #E53935);
transition: width 0.3s;
} }
.progress-text { .progress-text {
font-size: 12px; font-size: 13px;
color: #999; color: #1890ff;
font-weight: 600;
}
}
:deep(.el-progress) {
.el-progress-bar__outer {
background-color: #f0f0f0;
}
} }
} }
.achievement-date { .achievement-footer {
font-size: 12px; display: flex;
color: #999; justify-content: space-between;
align-items: center;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
.achievement-date,
.achievement-points {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #666;
:deep(.el-icon) {
font-size: 14px;
}
}
.achievement-points {
color: #faad14;
font-weight: 600;
}
}
.achievement-reward {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #faad14;
font-weight: 600;
padding: 6px 12px;
background: #fffbe6;
border-radius: 6px;
:deep(.el-icon) {
font-size: 14px;
}
}
// 响应式设计
@media (max-width: 1200px) {
.achievements-grid {
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
}
@media (max-width: 768px) {
.achievements-grid {
grid-template-columns: 1fr;
}
.achievement-stats {
flex-direction: column;
gap: 16px;
}
.filter-tabs {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
} }
</style> </style>