Browse Source

add 添加二维码扫描组件,登录页面响应式设计

Yue 1 month ago
parent
commit
52f250699e

+ 2 - 0
UI/VB.VUE/package.json

@@ -33,6 +33,7 @@
 		"element-plus": "^2.3.9",
 		"file-saver": "^2.0.5",
 		"jsencrypt": "^3.3.2",
+		"jsqr": "^1.4.0",
 		"min-dash": "^4.1.1",
 		"nprogress": "^0.2.0",
 		"object-path": "^0.11.8",
@@ -64,6 +65,7 @@
 		"@types/object-path": "^0.11.1",
 		"@types/prismjs": "^1.26.0",
 		"@typescript-eslint/eslint-plugin": "^6.1.0",
+		"@vitejs/plugin-basic-ssl": "^2.1.0",
 		"@vitejs/plugin-vue": "^4.2.3",
 		"@vue/eslint-config-prettier": "^7.0.0",
 		"@vue/eslint-config-typescript": "^11.0.0",

+ 21 - 0
UI/VB.VUE/pnpm-lock.yaml

@@ -65,6 +65,9 @@ importers:
       jsencrypt:
         specifier: ^3.3.2
         version: 3.3.2
+      jsqr:
+        specifier: ^1.4.0
+        version: 1.4.0
       min-dash:
         specifier: ^4.1.1
         version: 4.2.3
@@ -153,6 +156,9 @@ importers:
       '@typescript-eslint/eslint-plugin':
         specifier: ^6.1.0
         version: 6.21.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)
+      '@vitejs/plugin-basic-ssl':
+        specifier: ^2.1.0
+        version: 2.1.0(vite@4.5.14(@types/node@20.19.0)(lightningcss@1.30.1)(sass@1.63.4)(terser@5.42.0))
       '@vitejs/plugin-vue':
         specifier: ^4.2.3
         version: 4.6.2(vite@4.5.14(@types/node@20.19.0)(lightningcss@1.30.1)(sass@1.63.4)(terser@5.42.0))(vue@3.5.16(typescript@5.8.3))
@@ -1404,6 +1410,12 @@ packages:
   '@ungap/structured-clone@1.3.0':
     resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
 
+  '@vitejs/plugin-basic-ssl@2.1.0':
+    resolution: {integrity: sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==}
+    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+    peerDependencies:
+      vite: ^6.0.0 || ^7.0.0
+
   '@vitejs/plugin-vue@4.6.2':
     resolution: {integrity: sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==}
     engines: {node: ^14.18.0 || >=16.0.0}
@@ -3096,6 +3108,9 @@ packages:
     resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
     engines: {'0': node >= 0.2.0}
 
+  jsqr@1.4.0:
+    resolution: {integrity: sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==}
+
   keyv@4.5.4:
     resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
 
@@ -6208,6 +6223,10 @@ snapshots:
 
   '@ungap/structured-clone@1.3.0': {}
 
+  '@vitejs/plugin-basic-ssl@2.1.0(vite@4.5.14(@types/node@20.19.0)(lightningcss@1.30.1)(sass@1.63.4)(terser@5.42.0))':
+    dependencies:
+      vite: 4.5.14(@types/node@20.19.0)(lightningcss@1.30.1)(sass@1.63.4)(terser@5.42.0)
+
   '@vitejs/plugin-vue@4.6.2(vite@4.5.14(@types/node@20.19.0)(lightningcss@1.30.1)(sass@1.63.4)(terser@5.42.0))(vue@3.5.16(typescript@5.8.3))':
     dependencies:
       vite: 4.5.14(@types/node@20.19.0)(lightningcss@1.30.1)(sass@1.63.4)(terser@5.42.0)
@@ -8142,6 +8161,8 @@ snapshots:
 
   jsonparse@1.3.1: {}
 
+  jsqr@1.4.0: {}
+
   keyv@4.5.4:
     dependencies:
       json-buffer: 3.0.1

+ 425 - 0
UI/VB.VUE/src/components/qrcode/VbQrScan.vue

@@ -0,0 +1,425 @@
+<script setup lang="ts">
+// @ts-ignore
+import jsQR from "jsqr"
+import { ref, onMounted, onUnmounted } from "vue"
+
+const props = withDefaults(
+	defineProps<{
+		/**
+		 * 扫描提示语
+		 * @default "将二维码放入框内,即可自动扫描"
+		 */
+		tipMessage?: string
+		/**
+		 * 是否启用手电筒功能
+		 * @default true
+		 */
+		enableTorch?: boolean
+		/**
+		 * 是否启用连续扫描
+		 * @default false
+		 */
+		continuous?: boolean
+	}>(),
+	{
+		tipMessage: "将二维码放入框内,即可自动扫描",
+		enableTorch: true,
+		continuous: false
+	}
+)
+const emits = defineEmits<{
+	/**
+	 * 扫描成功时触发
+	 * @param result 扫描结果
+	 */
+	(e: "scanSuccess", result: string): void
+	/**
+	 * 关闭扫码组件时触发
+	 */
+	(e: "close"): void
+}>()
+
+const video = ref<HTMLVideoElement | null>(null)
+const canvas = ref<HTMLCanvasElement | null>(null)
+const streamRef = ref<MediaStream | null>(null)
+const isScanning = ref(false)
+const torchSupported = ref(false)
+const torchOn = ref(false)
+const scanningResult = ref("")
+
+let animationFrameId: number | null = null
+
+function init() {
+	startScan()
+}
+
+function startScan() {
+	if (isScanning.value) return
+
+	isScanning.value = true
+	const constraints: MediaStreamConstraints = {
+		video: {
+			facingMode: "environment",
+			width: { ideal: 1280 },
+			height: { ideal: 720 }
+		}
+	}
+
+	navigator.mediaDevices
+		.getUserMedia(constraints)
+		.then((stream) => {
+			streamRef.value = stream
+
+			// 检查是否支持手电筒
+			const track = stream.getVideoTracks()[0]
+			if (props.enableTorch && track && track.getCapabilities) {
+				const capabilities: any = track.getCapabilities()
+				if (capabilities.torch) {
+					torchSupported.value = true
+				}
+			}
+
+			if (video.value) {
+				video.value.srcObject = stream
+				video.value.setAttribute("playsinline", "true") // iOS Safari 兼容
+				video.value.play()
+				animationFrameId = requestAnimationFrame(tick)
+			}
+		})
+		.catch((err) => {
+			console.error("摄像头访问错误:", err)
+			isScanning.value = false
+
+			let errorMessage = "无法访问摄像头"
+			if (err.name === "NotAllowedError") {
+				errorMessage = "用户拒绝了摄像头访问权限"
+			} else if (err.name === "NotFoundError") {
+				errorMessage = "未找到摄像头设备"
+			} else if (err.name === "NotReadableError") {
+				errorMessage = "摄像头正被其他应用占用"
+			} else if (err.name === "OverconstrainedError") {
+				errorMessage = "摄像头不支持所需的分辨率"
+			}
+
+			alert(errorMessage)
+		})
+}
+
+function toggleTorch() {
+	if (!torchSupported.value || !streamRef.value) return
+
+	const track = streamRef.value.getVideoTracks()[0]
+	if (track) {
+		torchOn.value = !torchOn.value
+		track
+			.applyConstraints({
+				advanced: [{ torch: torchOn.value } as any]
+			})
+			.catch((err) => {
+				console.error("切换手电筒失败:", err)
+				torchOn.value = !torchOn.value
+			})
+	}
+}
+
+function closeScanner() {
+	stopScan()
+	emits("close")
+}
+
+function tick() {
+	if (!isScanning.value) return
+
+	if (video.value && video.value.readyState === video.value.HAVE_ENOUGH_DATA) {
+		if (!canvas.value) return
+
+		const ctx = canvas.value.getContext("2d")
+		if (!ctx) return
+
+		canvas.value.width = video.value.videoWidth
+		canvas.value.height = video.value.videoHeight
+
+		ctx.drawImage(video.value, 0, 0, canvas.value.width, canvas.value.height)
+		const imageData = ctx.getImageData(0, 0, canvas.value.width, canvas.value.height)
+
+		const code = jsQR(imageData.data, imageData.width, imageData.height, {
+			inversionAttempts: "dontInvert"
+		})
+
+		if (code) {
+			scanningResult.value = code.data
+			// 根据continuous属性决定是否继续扫描
+			if (!props.continuous) {
+				closeScanner()
+			}
+			emits("scanSuccess", code.data)
+		}
+	}
+
+	if (isScanning.value) {
+		animationFrameId = requestAnimationFrame(tick)
+	}
+}
+
+function stopScan() {
+	isScanning.value = false
+
+	if (animationFrameId) {
+		cancelAnimationFrame(animationFrameId)
+		animationFrameId = null
+	}
+
+	if (streamRef.value) {
+		streamRef.value.getTracks().forEach((track) => {
+			track.stop()
+		})
+		streamRef.value = null
+	}
+
+	if (video.value) {
+		video.value.srcObject = null
+	}
+}
+
+onMounted(init)
+onUnmounted(stopScan)
+</script>
+
+<template>
+	<div class="qr-scanner">
+		<!-- 视频背景 -->
+		<video ref="video" class="scanner-video" autoplay muted playsinline></video>
+
+		<!-- 遮罩层 -->
+		<div class="scanner-overlay">
+			<!-- 顶部半透明遮罩 -->
+			<div class="scanner-mask top-mask"></div>
+
+			<!-- 中间扫描区域 -->
+			<div class="scanner-middle">
+				<!-- 左侧遮罩 -->
+				<div class="scanner-mask side-mask"></div>
+
+				<!-- 扫描框 -->
+				<div class="scanner-frame">
+					<!-- 扫描线 -->
+					<div class="scanner-line"></div>
+
+					<!-- 角落装饰 -->
+					<div class="corner top-left"></div>
+					<div class="corner top-right"></div>
+					<div class="corner bottom-left"></div>
+					<div class="corner bottom-right"></div>
+				</div>
+				<!-- 右侧遮罩 -->
+				<div class="scanner-mask side-mask"></div>
+			</div>
+
+			<!-- 底部半透明遮罩 -->
+			<div class="scanner-mask bottom-mask">
+				<!-- 扫描提示文字 -->
+				<div class="scanner-tip">{{ tipMessage }}</div>
+				<div
+					v-if="props.enableTorch && torchSupported"
+					class="torch-button"
+					:class="{ active: torchOn }"
+					@click="toggleTorch">
+					<i v-if="torchOn" class="bi bi-brightness-high-fill torch-icon"></i>
+					<i v-else class="bi bi-brightness-low torch-icon text-white"></i>
+					<span class="mt-2">{{ torchOn ? "关闭手电筒" : "打开手电筒" }}</span>
+				</div>
+			</div>
+		</div>
+
+		<!-- 关闭按钮 -->
+		<div class="close-button" @click="closeScanner">
+			<i class="close-icon"></i>
+		</div>
+
+		<!-- 隐藏的canvas用于图像处理 -->
+		<canvas ref="canvas" style="display: none"></canvas>
+	</div>
+</template>
+
+<style lang="scss" scoped>
+.qr-scanner {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	z-index: 2000;
+	background: #000;
+	overflow: hidden;
+}
+
+.scanner-video {
+	width: 100%;
+	height: 100%;
+	object-fit: cover;
+	transform: scaleX(-1); // 镜像翻转
+}
+
+.scanner-overlay {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+}
+
+.scanner-mask {
+	background: rgba(0, 0, 0, 0.5);
+}
+
+.top-mask {
+	flex: 1;
+}
+
+.scanner-middle {
+	display: flex;
+	height: 300px;
+}
+
+.side-mask {
+	flex: 1;
+}
+
+.scanner-frame {
+	width: 300px;
+	height: 300px;
+	position: relative;
+	box-sizing: border-box;
+	border: 2px solid rgba(0, 255, 51, 0.6);
+
+	.scanner-line {
+		position: absolute;
+		width: 100%;
+		height: 2px;
+		background: rgba(0, 255, 51, 0.8);
+		box-shadow: 0 0 8px rgba(0, 255, 51, 0.8);
+		animation: scan 2s linear infinite;
+	}
+
+	.corner {
+		position: absolute;
+		width: 20px;
+		height: 20px;
+		border: 2px solid #00ff33;
+
+		&.top-left {
+			top: 0;
+			left: 0;
+			border-right: none;
+			border-bottom: none;
+		}
+
+		&.top-right {
+			top: 0;
+			right: 0;
+			border-left: none;
+			border-bottom: none;
+		}
+
+		&.bottom-left {
+			bottom: 0;
+			left: 0;
+			border-right: none;
+			border-top: none;
+		}
+
+		&.bottom-right {
+			bottom: 0;
+			right: 0;
+			border-left: none;
+			border-top: none;
+		}
+	}
+}
+
+.bottom-mask {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding: 30px;
+	box-sizing: border-box;
+}
+
+.scanner-tip {
+	color: #fff;
+	font-size: 16px;
+	margin-bottom: 20px;
+	text-align: center;
+}
+
+.torch-button {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	color: #fff;
+	cursor: pointer;
+
+	.torch-icon {
+		font-size: 30px;
+	}
+	&.active {
+		color: #00ff33;
+		.torch-icon {
+			color: #00ff33;
+		}
+	}
+}
+
+.close-button {
+	position: absolute;
+	top: 20px;
+	left: 20px;
+	width: 40px;
+	height: 40px;
+	border-radius: 50%;
+	background: rgba(0, 0, 0, 0.5);
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	cursor: pointer;
+	z-index: 10;
+
+	.close-icon {
+		position: relative;
+		width: 20px;
+		height: 20px;
+
+		&::before,
+		&::after {
+			content: "";
+			position: absolute;
+			top: 50%;
+			left: 0;
+			width: 100%;
+			height: 2px;
+			background: #fff;
+			transform-origin: center;
+		}
+
+		&::before {
+			transform: rotate(45deg);
+		}
+
+		&::after {
+			transform: rotate(-45deg);
+		}
+	}
+}
+
+@keyframes scan {
+	0% {
+		top: 0;
+	}
+	100% {
+		top: 100%;
+	}
+}
+</style>

+ 6 - 1
UI/VB.VUE/src/core/utils/index.ts

@@ -696,7 +696,7 @@ export const getAssetPath = (path: string): string => {
 	return import.meta.env.BASE_URL + path
 }
 
-function blob2Base64(blob: any) {
+export function blob2Base64(blob: any) {
 	return new Promise((resolve, reject) => {
 		const reader = new FileReader()
 		reader.addEventListener("load", () => {
@@ -709,3 +709,8 @@ function blob2Base64(blob: any) {
 		reader.readAsDataURL(blob)
 	})
 }
+
+// 检测是否为移动设备
+export function isMobile() {
+	return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
+}

+ 13 - 0
UI/VB.VUE/src/router/_staticRouter.ts

@@ -39,6 +39,19 @@ export const staticRouter: RouteRecordRaw[] = [
 			noCache: true
 		}
 	},
+	{
+		path: "/",
+		children: [
+			{
+				path: "/m-home",
+				name: "m-home",
+				component: () => import("@/views/mobile-home.vue"),
+				meta: {
+					title: "移动端首页"
+				}
+			}
+		]
+	},
 	{
 		path: "/",
 		children: [

+ 35 - 3
UI/VB.VUE/src/views/account/login.vue

@@ -136,6 +136,8 @@ const initTenant = async () => {
 		})
 }
 function init() {
+	// 移除设备检测跳转逻辑,使用响应式设计适配所有设备
+
 	appStore.appConfigStore
 		.loadConfig()
 		.then(() => {
@@ -189,7 +191,7 @@ watch(
 						:label="item.companyName"
 						:value="item.tenantId"></el-option>
 					<template #prefix>
-						<span class="el-input__icon input-icon"><VbIcon icon-name="home" class="fs-3" /></span>
+						<span class="el-input__icon"><VbIcon icon-name="home" class="fs-3" /></span>
 					</template>
 				</el-select>
 			</el-form-item>
@@ -282,9 +284,11 @@ watch(
 	display: flex;
 	justify-content: center;
 	align-items: center;
-	height: 100%;
+	height: 100vh;
 	background-image: url("/media/auth/bg5.jpg");
 	background-size: cover;
+	padding: 20px;
+	box-sizing: border-box;
 }
 
 .title {
@@ -296,7 +300,8 @@ watch(
 .login-form {
 	border-radius: 6px;
 	background: rgba(255, 255, 255, 0.9);
-	width: 400px;
+	width: 100%;
+	max-width: 400px;
 	padding: 25px 25px 5px 25px;
 
 	.el-input {
@@ -348,4 +353,31 @@ watch(
 	height: 40px;
 	padding-left: 12px;
 }
+
+.link-type {
+	text-decoration: none;
+	color: #337ab7;
+}
+
+// 响应式设计,适配移动端
+@media (max-width: 768px) {
+	.login-form {
+		max-width: 350px;
+		padding: 25px 25px 5px 25px;
+	}
+}
+
+@media (max-width: 480px) {
+	.login {
+		padding: 10px;
+	}
+
+	.login-form {
+		padding: 20px 20px 5px 20px;
+	}
+
+	.el-login-footer {
+		font-size: 10px;
+	}
+}
 </style>

+ 8 - 0
UI/VB.VUE/src/views/home.vue

@@ -1,6 +1,14 @@
 <script setup lang="ts">
+import { useRouter } from "vue-router"
 const name = ref("广明弥勒养殖")
+const router = useRouter()
 onMounted(() => {
+	// 如果是移动设备,跳转到移动端主页
+	if (isMobile()) {
+		router.replace("/m-home")
+		return
+	}
+
 	document.querySelector("body")?.classList.add("is-home")
 })
 onBeforeUnmount(() => {

+ 204 - 0
UI/VB.VUE/src/views/mobile-home.vue

@@ -0,0 +1,204 @@
+<script setup lang="ts">
+import appStore from "@s"
+import { ref, onBeforeUnmount } from "vue"
+import VbQrScan from "@/components/qrcode/VbQrScan.vue"
+
+const name = ref("中科轼峰")
+const showScanner = ref(false)
+const scanResult = ref("")
+
+onBeforeUnmount(() => {
+	document.querySelector("body")?.classList.remove("is-mobile-home")
+})
+
+function handleScan() {
+	// 使用新创建的扫码组件
+	showScanner.value = true
+}
+
+function handleScanResult(result: string) {
+	scanResult.value = result
+	alert("扫描结果: " + result)
+}
+
+function handleCloseScanner() {
+	showScanner.value = false
+}
+</script>
+
+<template>
+	<div class="mobile-home-page">
+		<div class="home-page">
+			<div class="header">
+				<h1 class="animated-title">{{ name }}</h1>
+				<div class="subtitle">移动工作平台</div>
+			</div>
+
+			<div class="content">
+				<div class="avatar-wrapper">
+					<vb-symbol
+						:text="appStore.authStore.user.nickName"
+						:src="appStore.authStore.user.avatar"
+						:size="100"
+						shape="circle" />
+				</div>
+
+				<div class="welcome-card">
+					<div class="welcome-message">
+						<h2>欢迎您,{{ appStore.authStore.user.nickName }} !</h2>
+						<p class="welcome-text">祝您使用愉快</p>
+					</div>
+				</div>
+
+				<div class="actions">
+					<el-button type="primary" round style="" @click="handleScan">
+						<i class="bi bi-qr-code-scan me-3 text-white fs-2"></i>
+						<span class="fs-2">扫一扫</span>
+					</el-button>
+				</div>
+			</div>
+
+			<div class="footer">
+				<p>&copy; {{ new Date().getFullYear() }} 中科轼峰. 保留所有权利.</p>
+			</div>
+		</div>
+		<!-- 扫码组件 -->
+		<VbQrScan
+			v-if="showScanner"
+			:enable-torch="true"
+			:continuous="false"
+			@close="handleCloseScanner"
+			@scanSuccess="handleScanResult" />
+	</div>
+</template>
+
+<style lang="scss" scoped>
+.mobile-home-page {
+	width: 100%;
+	height: 100vh;
+	overflow: hidden;
+	background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+	display: flex;
+	justify-content: center;
+}
+.home-page {
+	width: 100%;
+	max-width: 500px;
+	height: 100%;
+	color: white;
+	display: flex;
+	flex-direction: column;
+	position: relative;
+
+	&::before {
+		content: "";
+		position: absolute;
+		top: -50%;
+		left: -50%;
+		width: 200%;
+		height: 200%;
+		background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 70%);
+		transform: rotate(30deg);
+		z-index: 0;
+	}
+
+	.header {
+		text-align: center;
+		padding: 30px 20px 10px;
+		position: relative;
+		z-index: 1;
+
+		.animated-title {
+			font-size: 2.2rem;
+			font-weight: 800;
+			margin-bottom: 5px;
+			background: linear-gradient(45deg, #ffffff, #ffd700, #ffffff);
+			background-size: 200% auto;
+			-webkit-background-clip: text;
+			-webkit-text-fill-color: transparent;
+			animation: shine 3s linear infinite;
+			text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+			letter-spacing: 1px;
+		}
+
+		.subtitle {
+			font-size: 1.1rem;
+			opacity: 0.9;
+			letter-spacing: 2px;
+			font-weight: 300;
+		}
+	}
+
+	.content {
+		flex: 1;
+		display: flex;
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+		margin-top: -20%;
+		padding: 10px 20px;
+		position: relative;
+		z-index: 1;
+
+		.avatar-wrapper {
+			margin-bottom: -40px;
+			z-index: 2;
+			border: 4px solid rgba(255, 255, 255, 0.3);
+			border-radius: 50%;
+			background: rgba(255, 255, 255, 0.1);
+			backdrop-filter: blur(5px);
+
+			:deep(.symbol) {
+				border-radius: 50%;
+				box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+			}
+		}
+
+		.welcome-card {
+			background: rgba(255, 255, 255, 0.15);
+			backdrop-filter: blur(10px);
+			border-radius: 20px;
+			padding: 60px 20px 30px;
+			text-align: center;
+			margin-bottom: 50px;
+			width: 90%;
+			box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
+			border: 1px solid rgba(255, 255, 255, 0.18);
+
+			.welcome-message {
+				h2 {
+					font-size: 1.6rem;
+					margin-bottom: 10px;
+					font-weight: 600;
+					text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+				}
+
+				.welcome-text {
+					font-size: 1.1rem;
+					opacity: 0.9;
+					letter-spacing: 1px;
+				}
+			}
+		}
+
+		.actions {
+			width: 90%;
+
+			:deep(.el-button) {
+				width: 100%;
+				height: 45px;
+				font-size: 1rem;
+			}
+		}
+	}
+
+	.footer {
+		text-align: center;
+		padding: 15px;
+		font-size: 0.8rem;
+		opacity: 0.7;
+		position: relative;
+		z-index: 1;
+	}
+}
+</style>

+ 7 - 1
UI/VB.VUE/vite.config.ts

@@ -1,6 +1,8 @@
 import { defineConfig, loadEnv } from "vite"
 import { resolve } from "path"
 import createVitePlugins from "./vite/plugins"
+import * as basicSsl from "@vitejs/plugin-basic-ssl"
+
 //import { visualizer } from "rollup-plugin-visualizer"
 // https://vitejs.dev/config/
 export default defineConfig(({ mode, command }) => {
@@ -11,7 +13,9 @@ export default defineConfig(({ mode, command }) => {
 		// 例如 https://www.vber.net/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.vber.net/admin/,则设置 baseUrl 为 /admin/。
 		base: env.VITE_APP_CONTEXT_PATH,
 		plugins: [
-			...createVitePlugins(env, command === "build")
+			...createVitePlugins(env, command === "build"),
+			// @ts-ignore
+			basicSsl.default()
 			//,visualizer()
 		],
 		build: {
@@ -115,6 +119,8 @@ export default defineConfig(({ mode, command }) => {
 			// 设为 true 时若端口已被占用则会直接退出,而不是尝试下一个可用端口
 			strictPort: true,
 			open: true,
+			// 开启https
+			https: true,
 			proxy: {
 				[env.VITE_APP_BASE_API]: {
 					//target: "http://localhost:8080",