@ -1,186 +1,129 @@
< template >
< div class = "login" >
<!-- 左侧动态背景区 � ? -- >
< div class = "login-left" v-once >
<!-- 网格背景 -- >
< div class = "grid-overlay" > < / div >
<!-- 地图网格 � ? SVG -- >
< svg class = "map-grid" viewBox = "0 0 100 100" preserveAspectRatio = "none" >
< line x1 = "20" y1 = "0" x2 = "20" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "40" y1 = "0" x2 = "40" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "60" y1 = "0" x2 = "60" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "80" y1 = "0" x2 = "80" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "0" y1 = "20" x2 = "100" y2 = "20" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "0" y1 = "40" x2 = "100" y2 = "40" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "0" y1 = "60" x2 = "100" y2 = "60" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "0" y1 = "80" x2 = "100" y2 = "80" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< / svg >
<!-- 地图轮廓 SVG -- >
< svg class = "map-contours" viewBox = "0 0 800 600" preserveAspectRatio = "xMidYMid slice" >
< path d = "M100,100 Q200,50 300,100 T500,100 T700,150" stroke = "rgba(255,255,255,0.4)" fill = "none" stroke -width = " 1.5 " / >
< path d = "M50,200 Q150,150 250,200 T450,200 T650,250 T750,200" stroke = "rgba(255,255,255,0.3)" fill = "none" stroke -width = " 1 " / >
< path d = "M100,300 Q250,250 400,300 T700,350" stroke = "rgba(255,255,255,0.35)" fill = "none" stroke -width = " 1 " / >
< path d = "M50,400 Q200,350 350,400 T550,400 T750,450" stroke = "rgba(255,255,255,0.25)" fill = "none" stroke -width = " 1 " / >
< path d = "M100,500 Q300,450 500,500 T700,500" stroke = "rgba(255,255,255,0.3)" fill = "none" stroke -width = " 1 " / >
< path d = "M200,50 Q350,100 500,50 T800,100" stroke = "rgba(76,175,80,0.3)" fill = "none" stroke -width = " 1 " / >
< path d = "M0,150 Q200,200 400,150 T800,200" stroke = "rgba(76,175,80,0.25)" fill = "none" stroke -width = " 1 " / >
< / svg >
<!-- 连接 � ? SVG -- >
< svg class = "connection-lines" viewBox = "0 0 100 100" preserveAspectRatio = "none" >
< line x1 = "12" y1 = "15" x2 = "50" y2 = "50" stroke = "rgba(76,175,80,0.6)" stroke -width = " 0.15 " / >
< line x1 = "78" y1 = "25" x2 = "50" y2 = "50" stroke = "rgba(33,150,243,0.6)" stroke -width = " 0.15 " / >
< line x1 = "18" y1 = "55" x2 = "50" y2 = "50" stroke = "rgba(255,193,7,0.6)" stroke -width = " 0.15 " / >
< line x1 = "70" y1 = "70" x2 = "50" y2 = "50" stroke = "rgba(76,175,80,0.6)" stroke -width = " 0.15 " / >
< line x1 = "88" y1 = "40" x2 = "50" y2 = "50" stroke = "rgba(255,87,34,0.6)" stroke -width = " 0.15 " / >
< line x1 = "35" y1 = "82" x2 = "50" y2 = "50" stroke = "rgba(33,150,243,0.6)" stroke -width = " 0.15 " / >
< line x1 = "5" y1 = "30" x2 = "50" y2 = "50" stroke = "rgba(156,39,176,0.6)" stroke -width = " 0.15 " / >
< line x1 = "85" y1 = "85" x2 = "50" y2 = "50" stroke = "rgba(76,175,80,0.6)" stroke -width = " 0.15 " / >
< line x1 = "12" y1 = "15" x2 = "78" y2 = "25" stroke = "rgba(255,255,255,0.2)" stroke -width = " 0.05 " stroke -dasharray = " 0.5 , 0.5 " / >
< line x1 = "18" y1 = "55" x2 = "70" y2 = "70" stroke = "rgba(255,255,255,0.2)" stroke -width = " 0.05 " stroke -dasharray = " 0.5 , 0.5 " / >
< / svg >
<!-- 十字准星 SVG -- >
< svg class = "crosshair" width = "600" height = "600" viewBox = "0 0 200 200" >
< circle cx = "100" cy = "100" r = "90" fill = "none" stroke = "rgba(76,175,80,0.2)" stroke -width = " 1 " stroke -dasharray = " 5 , 3 " / >
< circle cx = "100" cy = "100" r = "80" fill = "none" stroke = "rgba(76,175,80,0.15)" stroke -width = " 0.5 " / >
< line x1 = "100" y1 = "20" x2 = "100" y2 = "80" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.5 " / >
< line x1 = "100" y1 = "120" x2 = "100" y2 = "180" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.5 " / >
< line x1 = "20" y1 = "100" x2 = "80" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.5 " / >
< line x1 = "120" y1 = "100" x2 = "180" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.5 " / >
< path d = "M 30 30 L 30 50 M 30 30 L 50 30" stroke = "rgba(76,175,80,0.4)" stroke -width = " 1 " fill = "none" / >
< path d = "M 170 30 L 170 50 M 170 30 L 150 30" stroke = "rgba(76,175,80,0.4)" stroke -width = " 1 " fill = "none" / >
< path d = "M 30 170 L 30 150 M 30 170 L 50 170" stroke = "rgba(76,175,80,0.4)" stroke -width = " 1 " fill = "none" / >
< path d = "M 170 170 L 170 150 M 170 170 L 150 170" stroke = "rgba(76,175,80,0.4)" stroke -width = " 1 " fill = "none" / >
< / svg >
<!-- 脉冲波纹 -- >
< div class = "ripple" > < / div >
< div class = "ripple" > < / div >
< div class = "ripple" > < / div >
< div class = "ripple" > < / div >
<!-- 定位 � ? -- >
< div class = "location-pins" >
< div class = "pin" >
< svg viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" / >
< / svg >
< / div >
< div class = "pin" >
< svg viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" / >
< / svg >
< / div >
< div class = "pin" >
< svg viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" / >
< / svg >
< / div >
< div class = "pin" >
< svg viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" / >
< / svg >
< / div >
< div class = "pin" >
< svg viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" / >
< / svg >
< / div >
< div class = "pin" >
< svg viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" / >
< / svg >
< / div >
< div class = "pin" >
< svg viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" / >
< / svg >
< div class = "login-left" >
< template v-if ="hasCustomLeftImage" >
< img class = "login-left-image" :src ="resolvedLoginLeftImage" alt = "login-left" @error ="handleLeftImageError" >
< div class = "login-left-image-overlay" > < / div >
< / template >
< template v-else >
< div class = "grid-overlay" > < / div >
< svg class = "map-grid" viewBox = "0 0 100 100" preserveAspectRatio = "none" >
< line x1 = "20" y1 = "0" x2 = "20" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "40" y1 = "0" x2 = "40" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "60" y1 = "0" x2 = "60" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "80" y1 = "0" x2 = "80" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "0" y1 = "20" x2 = "100" y2 = "20" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "0" y1 = "40" x2 = "100" y2 = "40" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "0" y1 = "60" x2 = "100" y2 = "60" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< line x1 = "0" y1 = "80" x2 = "100" y2 = "80" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.1 " / >
< / svg >
< svg class = "map-contours" viewBox = "0 0 800 600" preserveAspectRatio = "xMidYMid slice" >
< path d = "M100,100 Q200,50 300,100 T500,100 T700,150" stroke = "rgba(255,255,255,0.4)" fill = "none" stroke -width = " 1.5 " / >
< path d = "M50,200 Q150,150 250,200 T450,200 T650,250 T750,200" stroke = "rgba(255,255,255,0.3)" fill = "none" stroke -width = " 1 " / >
< path d = "M100,300 Q250,250 400,300 T700,350" stroke = "rgba(255,255,255,0.35)" fill = "none" stroke -width = " 1 " / >
< path d = "M50,400 Q200,350 350,400 T550,400 T750,450" stroke = "rgba(255,255,255,0.25)" fill = "none" stroke -width = " 1 " / >
< path d = "M100,500 Q300,450 500,500 T700,500" stroke = "rgba(255,255,255,0.3)" fill = "none" stroke -width = " 1 " / >
< path d = "M200,50 Q350,100 500,50 T800,100" stroke = "rgba(76,175,80,0.3)" fill = "none" stroke -width = " 1 " / >
< path d = "M0,150 Q200,200 400,150 T800,200" stroke = "rgba(76,175,80,0.25)" fill = "none" stroke -width = " 1 " / >
< / svg >
< svg class = "connection-lines" viewBox = "0 0 100 100" preserveAspectRatio = "none" >
< line x1 = "12" y1 = "15" x2 = "50" y2 = "50" stroke = "rgba(76,175,80,0.6)" stroke -width = " 0.15 " / >
< line x1 = "78" y1 = "25" x2 = "50" y2 = "50" stroke = "rgba(33,150,243,0.6)" stroke -width = " 0.15 " / >
< line x1 = "18" y1 = "55" x2 = "50" y2 = "50" stroke = "rgba(255,193,7,0.6)" stroke -width = " 0.15 " / >
< line x1 = "70" y1 = "70" x2 = "50" y2 = "50" stroke = "rgba(76,175,80,0.6)" stroke -width = " 0.15 " / >
< line x1 = "88" y1 = "40" x2 = "50" y2 = "50" stroke = "rgba(255,87,34,0.6)" stroke -width = " 0.15 " / >
< line x1 = "35" y1 = "82" x2 = "50" y2 = "50" stroke = "rgba(33,150,243,0.6)" stroke -width = " 0.15 " / >
< line x1 = "5" y1 = "30" x2 = "50" y2 = "50" stroke = "rgba(156,39,176,0.6)" stroke -width = " 0.15 " / >
< line x1 = "85" y1 = "85" x2 = "50" y2 = "50" stroke = "rgba(76,175,80,0.6)" stroke -width = " 0.15 " / >
< line x1 = "12" y1 = "15" x2 = "78" y2 = "25" stroke = "rgba(255,255,255,0.2)" stroke -width = " 0.05 " stroke -dasharray = " 0.5 , 0.5 " / >
< line x1 = "18" y1 = "55" x2 = "70" y2 = "70" stroke = "rgba(255,255,255,0.2)" stroke -width = " 0.05 " stroke -dasharray = " 0.5 , 0.5 " / >
< / svg >
< svg class = "crosshair" width = "600" height = "600" viewBox = "0 0 200 200" >
< circle cx = "100" cy = "100" r = "90" fill = "none" stroke = "rgba(76,175,80,0.2)" stroke -width = " 1 " stroke -dasharray = " 5 , 3 " / >
< circle cx = "100" cy = "100" r = "80" fill = "none" stroke = "rgba(76,175,80,0.15)" stroke -width = " 0.5 " / >
< line x1 = "100" y1 = "20" x2 = "100" y2 = "80" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.5 " / >
< line x1 = "100" y1 = "120" x2 = "100" y2 = "180" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.5 " / >
< line x1 = "20" y1 = "100" x2 = "80" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.5 " / >
< line x1 = "120" y1 = "100" x2 = "180" y2 = "100" stroke = "rgba(76,175,80,0.3)" stroke -width = " 0.5 " / >
< path d = "M 30 30 L 30 50 M 30 30 L 50 30" stroke = "rgba(76,175,80,0.4)" stroke -width = " 1 " fill = "none" / >
< path d = "M 170 30 L 170 50 M 170 30 L 150 30" stroke = "rgba(76,175,80,0.4)" stroke -width = " 1 " fill = "none" / >
< path d = "M 30 170 L 30 150 M 30 170 L 50 170" stroke = "rgba(76,175,80,0.4)" stroke -width = " 1 " fill = "none" / >
< path d = "M 170 170 L 170 150 M 170 170 L 150 170" stroke = "rgba(76,175,80,0.4)" stroke -width = " 1 " fill = "none" / >
< / svg >
< div class = "ripple" > < / div >
< div class = "ripple" > < / div >
< div class = "ripple" > < / div >
< div class = "ripple" > < / div >
< div class = "location-pins" >
< div class = "pin" v-for ="index in 8" :key ="index" >
< svg viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" / >
< / svg >
< / div >
< / div >
< div class = "pin" >
< svg viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" / >
< div class = "orbit-ring" > < / div >
< div class = "orbit-ring" > < / div >
< div class = "orbit-ring" > < / div >
< div class = "orbit-ring" > < / div >
< div ref = "particles" class = "particles" > < / div >
< div class = "center-marker" >
< svg class = "marker-icon" viewBox = "0 0 100 100" >
< circle cx = "50" cy = "50" r = "48" fill = "none" stroke = "rgba(255,255,255,0.15)" stroke -width = " 1 " / >
< circle cx = "50" cy = "50" r = "40" fill = "none" stroke = "rgba(76,175,80,0.2)" stroke -width = " 1.5 " / >
< path d = "M50 10 C32 10 20 24 20 42 C20 65 50 90 50 90 C80 65 80 42 80 42 C80 24 68 10 50 10 Z"
fill = "rgba(76,175,80,0.85)"
stroke = "rgba(255,255,255,0.7)"
stroke - width = "2.5" / >
< circle cx = "50" cy = "38" r = "12" fill = "rgba(255,255,255,0.95)" / >
< circle cx = "50" cy = "38" r = "7" fill = "rgba(76,175,80,1)" / >
< circle cx = "50" cy = "38" r = "18" fill = "none" stroke = "rgba(76,175,80,0.6)" stroke -width = " 2 " >
< animate attributeName = "r" from = "18" to = "35" dur = "2s" repeatCount = "indefinite" / >
< animate attributeName = "opacity" from = "0.6" to = "0" dur = "2s" repeatCount = "indefinite" / >
< / circle >
< circle cx = "50" cy = "38" r = "18" fill = "none" stroke = "rgba(76,175,80,0.4)" stroke -width = " 1.5 " >
< animate attributeName = "r" from = "18" to = "45" dur = "2s" begin = "0.5s" repeatCount = "indefinite" / >
< animate attributeName = "opacity" from = "0.4" to = "0" dur = "2s" begin = "0.5s" repeatCount = "indefinite" / >
< / circle >
< circle cx = "50" cy = "38" r = "18" fill = "none" stroke = "rgba(76,175,80,0.3)" stroke -width = " 1 " >
< animate attributeName = "r" from = "18" to = "55" dur = "2s" begin = "1s" repeatCount = "indefinite" / >
< animate attributeName = "opacity" from = "0.3" to = "0" dur = "2s" begin = "1s" repeatCount = "indefinite" / >
< / circle >
< / svg >
< / div >
< / div >
<!-- 装饰圆环 -- >
< div class = "orbit-ring" > < / div >
< div class = "orbit-ring" > < / div >
< div class = "orbit-ring" > < / div >
< div class = "orbit-ring" > < / div >
<!-- 粒子效果 -- >
< div ref = "particles" class = "particles" > < / div >
<!-- 中央超大定位标记 -- >
< div class = "center-marker" >
< svg class = "marker-icon" viewBox = "0 0 100 100" >
< circle cx = "50" cy = "50" r = "48" fill = "none" stroke = "rgba(255,255,255,0.15)" stroke -width = " 1 " / >
< circle cx = "50" cy = "50" r = "40" fill = "none" stroke = "rgba(76,175,80,0.2)" stroke -width = " 1.5 " / >
< path d = "M50 10 C32 10 20 24 20 42 C20 65 50 90 50 90 C50 90 80 65 80 42 C80 24 68 10 50 10 Z"
fill = "rgba(76,175,80,0.85)"
stroke = "rgba(255,255,255,0.7)"
stroke - width = "2.5" / >
< circle cx = "50" cy = "38" r = "12" fill = "rgba(255,255,255,0.95)" / >
< circle cx = "50" cy = "38" r = "7" fill = "rgba(76,175,80,1)" / >
< circle cx = "50" cy = "38" r = "18" fill = "none" stroke = "rgba(76,175,80,0.6)" stroke -width = " 2 " >
< animate attributeName = "r" from = "18" to = "35" dur = "2s" repeatCount = "indefinite" / >
< animate attributeName = "opacity" from = "0.6" to = "0" dur = "2s" repeatCount = "indefinite" / >
< / circle >
< circle cx = "50" cy = "38" r = "18" fill = "none" stroke = "rgba(76,175,80,0.4)" stroke -width = " 1.5 " >
< animate attributeName = "r" from = "18" to = "45" dur = "2s" begin = "0.5s" repeatCount = "indefinite" / >
< animate attributeName = "opacity" from = "0.4" to = "0" dur = "2s" begin = "0.5s" repeatCount = "indefinite" / >
< / circle >
< circle cx = "50" cy = "38" r = "18" fill = "none" stroke = "rgba(76,175,80,0.3)" stroke -width = " 1 " >
< animate attributeName = "r" from = "18" to = "55" dur = "2s" begin = "1s" repeatCount = "indefinite" / >
< animate attributeName = "opacity" from = "0.3" to = "0" dur = "2s" begin = "1s" repeatCount = "indefinite" / >
< / circle >
< / svg >
< / div >
< / template >
< / div >
<!-- 右侧登录表单区域 -- >
< div class = "login-right" >
< el -form ref = "loginForm" :model ="loginForm" :rules ="loginRules" class = "login-form" >
< h3 class = "title" > { { $t ( "login.title" ) } } < / h3 >
< h3 class = "title" > { { loginTitle } } < / h3 >
< el -form -item prop = "username" >
< el -input v-model ="loginForm.username" type="text" auto-complete="off" :placeholder="$t('login.usernamePlaceholder')" >
< svg -icon slot = "prefix" icon -class = " user " class = "el-input__icon input-icon" / >
< / e l - i n p u t >
< / e l - f o r m - i t e m >
< el -form -item prop = "password" >
< el -input v -model = " loginForm.password " type = "password" auto -complete = " off " :placeholder ="$t('login.passwordPlaceholder')"
@ keyup . enter . native = "handleLogin" >
< el -input v-model ="loginForm.password" type="password" auto-complete="off" :placeholder="$t('login.passwordPlaceholder')" @keyup.enter.native="handleLogin" >
< svg -icon slot = "prefix" icon -class = " password " class = "el-input__icon input-icon" / >
< / e l - i n p u t >
< / e l - f o r m - i t e m >
< el -form -item prop = "code" >
< el -input v -model = " loginForm.code " auto -complete = " off " :placeholder ="$t('login.googleCodePlaceholder')" style = "width: 100%"
@ keyup . enter . native = "handleLogin" >
< el -input v-model ="loginForm.code" auto-complete="off" :placeholder="$t('login.googleCodePlaceholder')" style="width: 100%" @keyup.enter.native="handleLogin" >
< svg -icon slot = "prefix" icon -class = " validCode " class = "el-input__icon input-icon" / >
< / e l - i n p u t >
<!-- < div class = "login-code" >
< img :src ="codeUrl" @click ="getCode" class = "login-code-img" / >
< / div > -- >
< / e l - f o r m - i t e m >
<!-- 人机验证 -- >
<!-- < el -form -item prop = 'validateCode' >
< el -row :span ="24" >
< el -col :span ="24" >
< reCaptcha :sitekey ="key" @ getValidateCode = 'getValidateCode' v-model ="loginForm.validateCode" > < / reCaptcha >
< / e l - c o l >
< / e l - r o w >
< / e l - f o r m - i t e m > - - >
<!-- < el -checkbox v-model ="loginForm.rememberMe" style="margin:0px 0px 25px 0px;" > 记住密码 < / el -checkbox > -- >
< el -form -item style = "width:100%;" >
< el -button :loading ="loading" size = "medium" type = "primary" style = "width:100%;"
@ click . native . prevent = "handleLogin" >
< span v-if ="!loading" > {{ $ t ( " login.login " ) }} < / span >
< span v-else > {{ $ t ( " login.loggingIn " ) }} < / span >
< el -button :loading ="loading" size = "medium" type = "primary" style = "width:100%;" @click.native.prevent ="handleLogin" >
< span v-if ="!loading" > {{ $ t ( ' login.login ' ) }} < / span >
< span v-else > {{ $ t ( ' login.loggingIn ' ) }} < / span >
< / e l - b u t t o n >
< / e l - f o r m - i t e m >
< div class = "login-form-lang" >
@ -203,51 +146,53 @@
< / div >
< / e l - f o r m >
<!-- 底部 -- >
< div class = "el-login-footer" >
< span class = "login-footer-text" > Copyright © 2026 GeoTag All Rights Reserved < / span >
< span class = "login-footer-text" > { { footerText } } < / span >
< / div >
< / div >
< / div >
< / template >
< script >
import Cookies from "js-cookie" ;
import Cookies from 'js-cookie'
import { encrypt , decrypt } from '@/utils/jsencrypt'
import { getLanguage , setLanguage , languageOptions } from '@/utils/language'
import { extractBusinessNoFromPath , resolveBrandAsset } from '@/utils/brand'
const DEFAULT_COPYRIGHT_TEXT = 'Copyright © 2026 GeoTag All Rights Reserved'
export default {
name : "Login" ,
name : 'Login' ,
data ( ) {
var checkCode = ( rule , value , callback ) => {
const checkCode = ( rule , value , callback ) => {
if ( value == false ) {
callback ( new Error ( this . $t ( "login.humanVerifyRequired" ) ) ) ;
callback ( new Error ( this . $t ( 'login.humanVerifyRequired' ) ) )
} else {
callback ( ) ;
callback ( )
}
} ;
}
return {
codeUrl : "" ,
cookiePassword : "" ,
codeUrl : '' ,
cookiePassword : '' ,
key : '6LcBoGUaAAAAABUnZINfh4j6FgqpQR-yHakZepIR' ,
languageOptions : languageOptions ,
languageOptions ,
loginForm : {
username : "" ,
password : "" ,
username : '' ,
password : '' ,
rememberMe : false ,
code : "" ,
uuid : "" ,
code : '' ,
uuid : '' ,
getValidateCode : false
} ,
loginRules : {
username : [
{ required : true , trigger : "blur" , message : this . $t ( "login.usernameRequired" ) }
{ required : true , trigger : 'blur' , message : this . $t ( 'login.usernameRequired' ) }
] ,
password : [
{ required : true , trigger : "blur" , message : this . $t ( "login.passwordRequired" ) }
{ required : true , trigger : 'blur' , message : this . $t ( 'login.passwordRequired' ) }
] ,
code : [
{ required : true , trigger : "change" , message : this . $t ( "login.codeRequired" ) }
{ required : true , trigger : 'change' , message : this . $t ( 'login.codeRequired' ) }
] ,
validateCode : [
{ validator : checkCode , trigger : 'change' }
@ -256,85 +201,123 @@ export default {
loading : false ,
redirect : undefined ,
particleTask : null ,
particleTaskType : ""
} ;
particleTaskType : '' ,
leftImageLoadError : false
}
} ,
computed : {
currentLanguage ( ) {
return this . $store . state . settings . language || getLanguage ( )
} ,
currentBrand ( ) {
return this . $store . getters . brand || { }
} ,
loginTitle ( ) {
return this . currentBrand . loginSystemName || this . $t ( 'login.title' )
} ,
footerText ( ) {
return this . currentBrand . copyrightText || DEFAULT_COPYRIGHT_TEXT
} ,
resolvedLoginLeftImage ( ) {
if ( this . leftImageLoadError ) {
return ''
}
return resolveBrandAsset ( this . currentBrand . loginLeftImageUrl )
} ,
hasCustomLeftImage ( ) {
return ! ! this . resolvedLoginLeftImage
}
} ,
watch : {
$route : {
handler : function ( route ) {
this . redirect = route . query && route . query . redirect ;
handler ( route ) {
this . redirect = route . query && route . query . redirect
this . loadBrandConfig ( route )
} ,
immediate : true
} ,
hasCustomLeftImage ( ) {
this . syncLoginVisuals ( )
} ,
'currentBrand.loginLeftImageUrl' ( ) {
this . leftImageLoadError = false
}
} ,
created ( ) {
/ / t h i s . g e t C o o k i e ( ) ;
} ,
mounted ( ) {
this . scheduleParticles ( ) ;
this . syncLoginVisuals ( )
} ,
beforeDestroy ( ) {
this . cancelParticleTask ( ) ;
this . clearParticles ( ) ;
this . cancelParticleTask ( )
this . clearParticles ( )
} ,
methods : {
loadBrandConfig ( route ) {
const businessNo = extractBusinessNoFromPath ( route . path ) || route . params . businessNo || ''
if ( ! businessNo ) {
this . syncLoginVisuals ( )
return
}
this . leftImageLoadError = false
this . $store . dispatch ( 'brand/loadBrandByBusinessNo' , businessNo ) . catch ( ( ) => {
this . syncLoginVisuals ( )
} )
} ,
syncLoginVisuals ( ) {
this . cancelParticleTask ( )
this . clearParticles ( )
if ( ! this . hasCustomLeftImage ) {
this . scheduleParticles ( )
}
} ,
scheduleParticles ( ) {
const run = ( ) => this . $nextTick ( ( ) => this . initParticles ( ) ) ;
if ( typeof window !== "undefined" && typeof window . requestIdleCallback === "function" ) {
this . particleTaskType = "idle" ;
this . particleTask = window . requestIdleCallback ( run , { timeout : 180 } ) ;
return ;
const run = ( ) => this . $nextTick ( ( ) => this . initParticles ( ) )
if ( typeof window !== 'undefined' && typeof window . requestIdleCallback === 'function' ) {
this . particleTaskType = 'idle'
this . particleTask = window . requestIdleCallback ( run , { timeout : 180 } )
return
}
this . particleTaskType = "timeout" ;
this . particleTask = window . setTimeout ( run , 32 ) ;
this . particleTaskType = 'timeout'
this . particleTask = window . setTimeout ( run , 32 )
} ,
cancelParticleTask ( ) {
if ( typeof window === "undefined" || this . particleTask == null ) {
return ;
if ( typeof window === 'undefined' || this . particleTask == null ) {
return
}
if ( this . particleTaskType === "idle" && typeof window . cancelIdleCallback === "function" ) {
window . cancelIdleCallback ( this . particleTask ) ;
if ( this . particleTaskType === 'idle' && typeof window . cancelIdleCallback === 'function' ) {
window . cancelIdleCallback ( this . particleTask )
} else {
window . clearTimeout ( this . particleTask ) ;
window . clearTimeout ( this . particleTask )
}
this . particleTask = null ;
this . particleTaskType = "" ;
this . particleTask = null
this . particleTaskType = ''
} ,
initParticles ( ) {
const particlesContainer = this . $refs . particles ;
if ( ! particlesContainer || particlesContainer . childElementCount > 0 ) {
return ;
const particlesContainer = this . $refs . particles
if ( ! particlesContainer || particlesContainer . childElementCount > 0 || this . hasCustomLeftImage ) {
return
}
const fragment = document . createDocumentFragment ( ) ;
const fragment = document . createDocumentFragment ( )
for ( let i = 0 ; i < 40 ; i ++ ) {
const particle = document . createElement ( 'span' ) ;
particle . className = 'particle' ;
particle . style . left = Math . random ( ) * 100 + '%' ;
particle . style . animationDelay = Math . random ( ) * 25 + 's' ;
particle . style . animationDuration = ( 20 + Math . random ( ) * 10 ) + 's' ;
particle . style . width = ( 2 + Math . random ( ) * 3 ) + 'px' ;
particle . style . height = particle . style . width ;
fragment . appendChild ( particle ) ;
const particle = document . createElement ( 'span' )
particle . className = 'particle'
particle . style . left = ` ${ Math . random ( ) * 100 } % `
particle . style . animationDelay = ` ${ Math . random ( ) * 25 } s `
particle . style . animationDuration = ` ${ 20 + Math . random ( ) * 10 } s `
particle . style . width = ` ${ 2 + Math . random ( ) * 3 } px `
particle . style . height = particle . style . width
fragment . appendChild ( particle )
}
particlesContainer . appendChild ( fragment ) ;
particlesContainer . appendChild ( fragment )
} ,
clearParticles ( ) {
const particlesContainer = this . $refs . particles ;
const particlesContainer = this . $refs . particles
if ( particlesContainer ) {
particlesContainer . textContent = "" ;
particlesContainer . textContent = ''
}
} ,
icoCreate ( icoUrl ) {
var link = document . querySelector ( "link[rel*='icon']" ) || document . createElement ( 'link' ) ;
link . type = 'image/x-icon' ;
link . rel = 'shortcut icon' ;
link . href = icoUrl
document . getElementsByTagName ( 'head' ) [ 0 ] . appendChild ( link ) ;
handleLeftImageError ( ) {
this . leftImageLoadError = true
this . syncLoginVisuals ( )
} ,
changeLanguage ( lang ) {
const normalized = setLanguage ( lang )
@ -348,40 +331,42 @@ export default {
this . loginForm . validateCode = value
} ,
getCookie ( ) {
const username = Cookies . get ( "username" ) ;
const password = Cookies . get ( "password" ) ;
const username = Cookies . get ( 'username' )
const password = Cookies . get ( 'password' )
const rememberMe = Cookies . get ( 'rememberMe' )
this . loginForm = {
username : username === undefined ? this . loginForm . username : username ,
password : password === undefined ? this . loginForm . password : decrypt ( password ) ,
rememberMe : rememberMe === undefined ? false : Boolean ( rememberMe )
} ;
}
} ,
handleLogin ( ) {
this . $refs . loginForm . validate ( valid => {
if ( valid ) {
this . loading = true ;
if ( this . loginForm . rememberMe ) {
Cookies . set ( "username" , this . loginForm . username , { expires : 30 } ) ;
Cookies . set ( "password" , encrypt ( this . loginForm . password ) , { expires : 30 } ) ;
Cookies . set ( 'rememberMe' , this . loginForm . rememberMe , { expires : 30 } ) ;
} else {
Cookies . remove ( "username" ) ;
Cookies . remove ( "password" ) ;
Cookies . remove ( 'rememberMe' ) ;
}
this . $store . dispatch ( "Login" , this . loginForm ) . then ( ( ) => {
const targetPath = this . redirect && this . redirect !== "/no-permission" ? this . redirect : "/index" ;
this . $router . push ( { path : targetPath } ) . catch ( ( ) => { } ) ;
} ) . catch ( ( ) => {
this . loading = false ;
} ) ;
this . $refs . loginForm . validate ( ( valid ) => {
if ( ! valid ) {
return
}
} ) ;
this . loading = true
if ( this . loginForm . rememberMe ) {
Cookies . set ( 'username' , this . loginForm . username , { expires : 30 } )
Cookies . set ( 'password' , encrypt ( this . loginForm . password ) , { expires : 30 } )
Cookies . set ( 'rememberMe' , this . loginForm . rememberMe , { expires : 30 } )
} else {
Cookies . remove ( 'username' )
Cookies . remove ( 'password' )
Cookies . remove ( 'rememberMe' )
}
this . $store . dispatch ( 'Login' , this . loginForm ) . then ( ( ) => {
return this . $store . dispatch ( 'brand/loadCurrentBrand' ) . catch ( ( ) => null )
} ) . then ( ( ) => {
const targetPath = this . redirect && this . redirect !== '/no-permission' ? this . redirect : '/index'
this . $router . push ( { path : targetPath } ) . catch ( ( ) => { } )
} ) . catch ( ( ) => {
this . loading = false
} )
} )
}
}
} ;
}
< / script >
< style rel = "stylesheet/scss" lang = "scss" >
@ -391,7 +376,6 @@ export default {
height : 100 % ;
}
/* 左侧动态背景区�?*/
. login - left {
flex : 0 0 60 % ;
position : relative ;
@ -400,20 +384,32 @@ export default {
contain : layout paint ;
}
/* 网格背景 */
. login - left - image {
position : absolute ;
inset : 0 ;
width : 100 % ;
height : 100 % ;
object - fit : cover ;
}
. login - left - image - overlay {
position : absolute ;
inset : 0 ;
background : linear - gradient ( 135 deg , rgba ( 10 , 25 , 41 , 0.18 ) 0 % , rgba ( 13 , 60 , 97 , 0.38 ) 55 % , rgba ( 1 , 87 , 155 , 0.18 ) 100 % ) ;
}
. grid - overlay {
position : absolute ;
top : 0 ;
left : 0 ;
width : 100 % ;
height : 100 % ;
background - image :
background - image :
linear - gradient ( rgba ( 76 , 175 , 80 , 0.08 ) 1 px , transparent 1 px ) ,
linear - gradient ( 90 deg , rgba ( 76 , 175 , 80 , 0.08 ) 1 px , transparent 1 px ) ;
background - size : 60 px 60 px ;
}
/* 地图轮廓 */
. map - contours {
position : absolute ;
top : 50 % ;
@ -424,7 +420,6 @@ export default {
opacity : 0.15 ;
}
/* 定位点动�?*/
. location - pins {
position : absolute ;
top : 0 ;
@ -461,7 +456,6 @@ export default {
50 % { transform : scale ( 1.15 ) ; opacity : 1 ; }
}
/* 连接�?*/
. connection - lines {
position : absolute ;
top : 0 ;
@ -471,7 +465,6 @@ export default {
opacity : 0.2 ;
}
/* 中央大定位标�?*/
. center - marker {
position : absolute ;
top : 50 % ;
@ -493,7 +486,6 @@ export default {
50 % { transform : translateY ( - 20 px ) ; }
}
/* 装饰圆环 */
. orbit - ring {
position : absolute ;
top : 50 % ;
@ -516,7 +508,6 @@ export default {
to { transform : translate ( - 50 % , - 50 % ) rotate ( 360 deg ) ; }
}
/* 粒子效果 */
. particles {
position : absolute ;
top : 0 ;
@ -545,7 +536,6 @@ export default {
100 % { transform : translateY ( - 100 vh ) rotate ( 720 deg ) ; opacity : 0 ; }
}
/* 十字准星 */
. crosshair {
position : absolute ;
top : 50 % ;
@ -554,7 +544,6 @@ export default {
pointer - events : none ;
}
/* 地图网格�?*/
. map - grid {
position : absolute ;
top : 0 ;
@ -564,7 +553,6 @@ export default {
opacity : 0.1 ;
}
/* 脉冲波纹 */
. ripple {
position : absolute ;
top : 50 % ;
@ -595,7 +583,6 @@ export default {
}
}
/* 右侧登录表单区域 */
. login - right {
flex : 0 0 40 % ;
display : flex ;
@ -653,23 +640,6 @@ export default {
cursor : pointer ;
}
. login - tip {
font - size : 13 px ;
text - align : center ;
color : # bfbfbf ;
}
. login - code {
width : 33 % ;
height : 38 px ;
float : right ;
img {
cursor : pointer ;
vertical - align : middle ;
}
}
. el - login - footer {
height : 40 px ;
line - height : 40 px ;
@ -681,24 +651,28 @@ export default {
font - family : Arial ;
font - size : 12 px ;
letter - spacing : 1 px ;
padding : 0 18 px ;
box - sizing : border - box ;
}
. login - footer - text {
pointer - events : none ;
display : inline - block ;
max - width : 100 % ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. login - code - img {
height : 38 px ;
}
/* 响应�?*/
@ media ( max - width : 1024 px ) {
. login - left {
flex : 0 0 50 % ;
}
. login - right {
flex : 0 0 50 % ;
}
. login - form {
width : 350 px ;
}
@ -708,13 +682,14 @@ export default {
. login - left {
display : none ;
}
. login - right {
flex : 0 0 100 % ;
}
. login - form {
width : 90 % ;
max - width : 400 px ;
}
}
< / style >