Yue 1 неделя назад
Родитель
Сommit
ef2683c42f
22 измененных файлов с 917 добавлено и 238 удалено
  1. 1 0
      .gitignore
  2. 7 0
      SERVER/VberAdminPlusV3/.script/docker/docker-compose.yml
  3. 7 0
      SERVER/VberAdminPlusV3/.script/docker/docker-compose_liunx.yml
  4. 5 3
      SERVER/VberAdminPlusV3/.script/sql/admin.sql
  5. 2 0
      SERVER/VberAdminPlusV3/vber-admin/src/main/java/com/vber/web/controller/IndexController.java
  6. 4 0
      SERVER/VberAdminPlusV3/vber-admin/src/main/java/com/vber/web/domain/vo/AppConfigVo.java
  7. 1 0
      SERVER/VberAdminPlusV3/vber-admin/src/main/resources/application.yml
  8. 5 0
      SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/config/VbConfig.java
  9. 5 0
      SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/constant/ConfigKeyConstants.java
  10. 7 1
      SERVER/VberAdminPlusV3/vber-common/vber-common-web/src/main/java/com/vber/common/web/config/ResourcesConfig.java
  11. 15 1
      UI/VAP_V3.VUE/src/components/dict/DictSelect.vue
  12. 263 0
      UI/VAP_V3.VUE/src/components/split-panel/VbSplitPanel.vue
  13. 1 0
      UI/VAP_V3.VUE/src/core/types/AppConfig.ts
  14. 1 1
      UI/VAP_V3.VUE/src/router/_staticRouter.ts
  15. 1 1
      UI/VAP_V3.VUE/src/router/index.ts
  16. 1 1
      UI/VAP_V3.VUE/src/router/menuMap/index.ts
  17. 4 0
      UI/VAP_V3.VUE/src/stores/_app.ts
  18. 6 6
      UI/VAP_V3.VUE/src/views/account/login.vue
  19. 3 20
      UI/VAP_V3.VUE/src/views/home/_home.vue
  20. 24 0
      UI/VAP_V3.VUE/src/views/home/index.vue
  21. 554 0
      UI/VAP_V3.VUE/src/views/home/m-home.vue
  22. 0 204
      UI/VAP_V3.VUE/src/views/mobile-home.vue

+ 1 - 0
.gitignore

@@ -72,3 +72,4 @@ build/
 
 .data/
 
+/UI/**/.lingma

+ 7 - 0
SERVER/VberAdminPlusV3/.script/docker/docker-compose.yml

@@ -26,6 +26,7 @@ services:
       --explicit_defaults_for_timestamp=true
       --lower_case_table_names=1
     privileged: true
+    restart: always
     networks:
       - vap3-net
 
@@ -44,6 +45,7 @@ services:
       - ../../.data/redis/data/:/redis/data/:rw
     command: "redis-server /redis/config/redis.conf"
     privileged: true
+    restart: always
     networks:
       - vap3-net
 
@@ -79,6 +81,7 @@ services:
   #      - /docker/minio/config:/root/.minio/
   #    command: server --address ':9000' --console-address ':9001' /data  # 指定容器中的目录 /data
   #    privileged: true
+  #    restart:  always
   #    networks:
   #      - vap3-net
   #
@@ -101,6 +104,7 @@ services:
       # 日志目录
       - ../../.data/nginx/log:/var/log/nginx
     privileged: true
+    restart: always
     networks:
       - vap3-net
 
@@ -129,6 +133,7 @@ services:
       - vber_redis
       - vber_mysql
     privileged: true
+    restart: always
     networks:
       - vap3-net
 
@@ -156,6 +161,7 @@ services:
       - vber_redis
       - vber_mysql
     privileged: true
+    restart: always
     networks:
       - vap3-net
 
@@ -188,6 +194,7 @@ services:
 #    volumes:
 #      - ../../.data/job/logs/:/vber/job/logs
 #    privileged: true
+#    restart:  always
 #    networks:
 #      - vap3-net
 

+ 7 - 0
SERVER/VberAdminPlusV3/.script/docker/docker-compose_liunx.yml

@@ -26,6 +26,7 @@ services:
       --explicit_defaults_for_timestamp=true
       --lower_case_table_names=1
     privileged: true
+    restart: always
     networks:
       - vap3-net
 
@@ -44,6 +45,7 @@ services:
       - /home/docker/vap3/data/redis/data/:/redis/data/:rw
     command: "redis-server /redis/config/redis.conf"
     privileged: true
+    restart: always
     networks:
       - vap3-net
 
@@ -79,6 +81,7 @@ services:
   #      - /home/docker/vap3/minio/config:/root/.minio/
   #    command: server --address ':9000' --console-address ':9001' /data  # 指定容器中的目录 /data
   #    privileged: true
+  #    restart:  always
   #    networks:
   #      - vap3-net
   #
@@ -101,6 +104,7 @@ services:
       # 日志目录
       - /home/docker/vap3/logs/nginx:/var/log/nginx
     privileged: true
+    restart: always
     networks:
       - vap3-net
 
@@ -129,6 +133,7 @@ services:
       - vber_redis
       - vber_mysql
     privileged: true
+    restart: always
     networks:
       - vap3-net
 
@@ -156,6 +161,7 @@ services:
       - vber_redis
       - vber_mysql
     privileged: true
+    restart: always
     networks:
       - vap3-net
 
@@ -188,6 +194,7 @@ services:
 #    volumes:
 #      - /home/docker/vap3/logs/job/:/vber/job/logs
 #    privileged: true
+#    restart:  always
 #    networks:
 #      - vap3-net
 

+ 5 - 3
SERVER/VberAdminPlusV3/.script/sql/admin.sql

@@ -982,11 +982,13 @@ VALUES (1, '000000', 1, '男', '0', 'sys_user_sex', '', 'primary', 'Y', 100, 1,
 -- ----------------------------
 INSERT INTO sys_config (config_id, tenant_id, config_name, config_key, config_value, config_type, create_org, create_by,
                         create_time, update_by, update_time, remark)
-VALUES (1, '000000', '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', 100, 1, SYSDATE(), NULL, NULL,
+VALUES (1, '000000', '平台名称', 'sys.platform.name', '玮博科技', 'Y', 100, 1, SYSDATE(), NULL, NULL,
+        ''),
+       (2, '000000', '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', 100, 1, SYSDATE(), NULL, NULL,
         '初始化密码 123456'),
-       (2, '000000', '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 100, 1, SYSDATE(), NULL,
+       (3, '000000', '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 100, 1, SYSDATE(), NULL,
         NULL, '是否开启注册用户功能(true开启,false关闭)'),
-       (3, '000000', 'OSS预览列表资源开关', 'sys.oss.previewListResource', 'true', 'Y', 100, 1, SYSDATE(), NULL, NULL,
+       (4, '000000', 'OSS预览列表资源开关', 'sys.oss.previewListResource', 'true', 'Y', 100, 1, SYSDATE(), NULL, NULL,
         'true:开启, false:关闭');
 
 

+ 2 - 0
SERVER/VberAdminPlusV3/vber-admin/src/main/java/com/vber/web/controller/IndexController.java

@@ -49,6 +49,8 @@ public class IndexController {
         String registerUser = sysConfigService.selectConfigByKey(ConfigKeyConstants.SYS_ACCOUNT_REGISTER_USER);
         configVo.setRegisterEnabled(StringUtils.isNotEmpty(registerUser) ? Boolean.valueOf("true".equals(registerUser)) : vbConfig.getRegisterEnabled());
         String previewListResource = sysConfigService.selectConfigByKey(ConfigKeyConstants.SYS_OSS_PREVIEW_LIST_RESOURCE);
+        String platformName = sysConfigService.selectConfigByKey(ConfigKeyConstants.SYS_PLATFORM_NAME);
+        configVo.setPlatformName(platformName.isBlank() ? vbConfig.getPlatformName() : platformName);
         configVo.setOssPreviewEnabled(StringUtils.isNotEmpty(previewListResource) ? Boolean.valueOf("true".equals(previewListResource)) : vbConfig.getOssPreviewEnabled());
         configVo.setWorkflowEnabled(vbConfig.getWorkflowEnabled());
         return R.ok(configVo);

+ 4 - 0
SERVER/VberAdminPlusV3/vber-admin/src/main/java/com/vber/web/domain/vo/AppConfigVo.java

@@ -12,6 +12,10 @@ public class AppConfigVo {
      * 项目名称
      */
     private String name;
+    /**
+     * 平台名称
+     */
+    private String platformName;
     /**
      * 版本
      */

+ 1 - 0
SERVER/VberAdminPlusV3/vber-admin/src/main/resources/application.yml

@@ -2,6 +2,7 @@
 vb:
   # 名称
   name: VberAdminPlusV3
+  platformName: "玮博科技"
   # 版本
   version: ${revision}
   # 版权年份

+ 5 - 0
SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/config/VbConfig.java

@@ -23,6 +23,11 @@ public class VbConfig {
      * 项目名称
      */
     private String name;
+
+    /**
+     * 平台名称
+     */
+    private String platformName;
     /**
      * 模式
      */

+ 5 - 0
SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/constant/ConfigKeyConstants.java

@@ -1,6 +1,11 @@
 package com.vber.common.core.constant;
 
 public interface ConfigKeyConstants {
+
+    /**
+     * 平台名称
+     */
+    String SYS_PLATFORM_NAME = "sys.platform.name";
     /**
      * 用户初始密码
      */

+ 7 - 1
SERVER/VberAdminPlusV3/vber-common/vber-common-web/src/main/java/com/vber/common/web/config/ResourcesConfig.java

@@ -5,6 +5,7 @@ import cn.hutool.core.date.DateUtil;
 import com.vber.common.core.utils.ObjectUtils;
 import com.vber.common.web.handler.GlobalExceptionHandler;
 import com.vber.common.web.interceptor.PlusWebInvokeTimeInterceptor;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.context.annotation.Bean;
 import org.springframework.format.FormatterRegistry;
@@ -25,10 +26,15 @@ import java.util.Date;
 @AutoConfiguration
 public class ResourcesConfig implements WebMvcConfigurer {
 
+    @Value("${vb.mode}")
+    private String mode;
+
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         // 全局访问性能拦截
-        registry.addInterceptor(new PlusWebInvokeTimeInterceptor());
+        if ("dev".equals(mode)) {
+            registry.addInterceptor(new PlusWebInvokeTimeInterceptor());
+        }
     }
 
     @Override

+ 15 - 1
UI/VAP_V3.VUE/src/components/dict/DictSelect.vue

@@ -9,6 +9,7 @@ const props = withDefaults(
 		placeholder?: string
 		type?: "select" | "checkbox" | "radio"
 		valueIsNumber?: boolean
+		exclude?: string[] | string // 需要排除的值,统一转成string处理
 		listeners?: any
 	}>(),
 	{
@@ -30,7 +31,20 @@ function onChange(v: any) {
 }
 
 function getData() {
-	return appStore.dictStore.getDict(props.dictType)
+	return new Promise((resolve, reject) => {
+		appStore.dictStore
+			.getDict(props.dictType)
+			.then((res: any[]) => {
+				if (props.exclude) {
+					const exList = typeof props.exclude === "string" ? [props.exclude] : props.exclude
+					res = res.filter((item: any) => !exList.includes(item.value + ""))
+				}
+				resolve(res)
+			})
+			.catch((err) => {
+				reject(err)
+			})
+	})
 }
 </script>
 <template>

+ 263 - 0
UI/VAP_V3.VUE/src/components/split-panel/VbSplitPanel.vue

@@ -0,0 +1,263 @@
+<script setup lang="ts">
+const props = withDefaults(
+	defineProps<{
+		mode?: "h" | "v" | "horizontal" | "vertical" // 拖拽方向
+		size?: number | string // 初始尺寸,支持像素或百分比(如 '50%')
+		minSize?: number | string // 最小尺寸,支持像素或百分比(如 '10%')
+		maxSize?: number | string // 最大尺寸,支持像素或百分比(如 '80%')
+		containerSize?: string | number // 容器尺寸
+		panelStyle?: object // 面板样式,支持内联样式
+		panelItemStyle?: object // 面板项样式,支持内联样式
+		firstPanelItemStyle?: object // 第1个面板项样式,支持内联样式
+		secondPanelItemStyle?: object // 第2个面板项样式,支持内联样式
+		handlerBg?: string // 拖拽条背景色
+		handlerHoverBg?: string // 拖拽条悬停背景色
+		handlerWidth?: number // 拖拽条宽度
+		handlerMargin?: number // 拖拽条边距
+	}>(),
+	{
+		mode: "h",
+		size: 200,
+		minSize: 100,
+		maxSize: 2000,
+		containerSize: "100%",
+		handlerBg: "#e5e7eb",
+		handlerHoverBg: "#409eff",
+		handlerWidth: 6,
+		handlerMargin: 2
+	}
+)
+const emits = defineEmits<{
+	(e: "dragComplete", n1: number, n2: number): void
+}>()
+
+const panelRef = ref(null)
+const isDragging = ref(false)
+const parentSize = ref(0)
+const firstPanelSize = ref<number>(
+	typeof props.size === "number" ? props.size : parseFloat(props.size as string) || 200
+)
+
+const secondPanelSize = computed(() =>
+	Math.floor(
+		parentSize.value - firstPanelSize.value - (props.handlerWidth || 6) - (props.handlerMargin || 2)
+	)
+)
+
+const validatePositive = (value, name) => {
+	const num = Number(value)
+	if (!isNaN(num) && num < 0) {
+		throw new Error(`VbSplitPanel:${name} 不能小于 0,当前值:${value}`)
+	}
+}
+
+const realMode = computed(() => {
+	const m = props.mode
+	return m === "h" || m === "horizontal" ? "horizontal" : "vertical"
+})
+
+// 解析尺寸值,将百分比转换为像素值
+const parseSizeValue = (value: number | string, parentSize: number): number => {
+	if (typeof value === "string" && value.endsWith("%")) {
+		const percentage = parseFloat(value)
+
+		if (!isNaN(percentage)) {
+			if (percentage > 100) {
+				console.warn(`VbSplitPanel:${value} 超出范围,已设置为 90%`)
+				return Math.floor((parentSize * 90) / 100)
+			} else if (percentage < 0) {
+				console.warn(`VbSplitPanel:${value} 不能小于 0,已设置为 10%`)
+				return Math.floor((parentSize * 10) / 100)
+			} else {
+				return Math.floor((parentSize * percentage) / 100)
+			}
+		}
+	}
+	return typeof value === "number" ? value : parseFloat(value) || 0
+}
+
+// 获取父元素拖拽方向尺寸
+const getParentSize = () => {
+	if (!panelRef.value?.parentElement) return
+	const parent = panelRef.value.parentElement
+	parent.style.userSelect = "none"
+	const size = realMode.value === "horizontal" ? parent.offsetWidth : parent.offsetHeight
+
+	validatePositive(size, `拖拽父元素${realMode.value === "horizontal" ? "水平" : "垂直"}方向尺寸`)
+	parentSize.value = Math.floor(size)
+}
+
+// 容器样式
+const containerStyle = computed(() => {
+	const isH = realMode.value === "horizontal"
+	const sizeProp = isH ? "width" : "height"
+	const crossProp = isH ? "height" : "width"
+	const val = props.containerSize
+
+	validatePositive(val, "size")
+
+	let finalSize
+	if (val === "auto") {
+		if (parentSize.value < 0) {
+			throw new Error(`SplitPanel:size=auto,但父元素${sizeProp}为 0,无法渲染`)
+		}
+		finalSize = `${parentSize.value}px`
+	} else if (!isNaN(Number(val))) {
+		finalSize = `${val}px`
+	} else {
+		finalSize = val
+	}
+
+	return {
+		...(props.panelStyle || {}),
+		"--split-handler-bg": props.handlerBg || "#e5e7eb",
+		"--split-handler-hover-bg": props.handlerHoverBg || "#409eff",
+		"--split-handler-width": (props.handlerWidth || 6) + "px",
+		"--split-handler-margin": (props.handlerMargin || 2) + "px",
+		[sizeProp]: finalSize,
+		[crossProp]: "100%",
+		display: "flex",
+		overflow: "hidden"
+	}
+})
+
+const modeClass = computed(() => realMode.value)
+const handlerClass = computed(() => (realMode.value === "horizontal" ? "h-handler" : "v-handler"))
+
+const firstPanelStyle = computed(() => {
+	const pixelSize = parseSizeValue(firstPanelSize.value, parentSize.value)
+	return {
+		...(props.panelItemStyle || {}),
+		...(props.firstPanelItemStyle || {}),
+		...(realMode.value === "horizontal"
+			? { width: `${pixelSize}px` }
+			: { height: `${pixelSize}px` }),
+		flex: "none"
+	}
+})
+const secondPanelStyle = computed(() => {
+	return {
+		...(props.panelItemStyle || {}),
+		...(props.secondPanelItemStyle || {}),
+		...(realMode.value === "horizontal"
+			? { width: `${secondPanelSize.value}px` }
+			: { height: `${secondPanelSize.value}px` }),
+		flex: 1
+	}
+})
+
+// 拖拽逻辑
+const startDrag = (e) => {
+	e.preventDefault()
+	isDragging.value = true
+	const startPos = realMode.value === "horizontal" ? e.clientX : e.clientY
+	// 确保 start 是数值类型
+	const start =
+		typeof firstPanelSize.value === "number"
+			? firstPanelSize.value
+			: parseFloat(firstPanelSize.value as string) || 200
+	const onMove = (e) => {
+		if (!isDragging.value) return
+		const pos = realMode.value === "horizontal" ? e.clientX : e.clientY
+		let size = start + (pos - startPos)
+
+		// 解析 minSize 和 maxSize 为像素值
+		const minPixelSize = parseSizeValue(props.minSize, parentSize.value)
+		const maxPixelSize = parseSizeValue(props.maxSize, parentSize.value)
+
+		size = Math.max(minPixelSize, Math.min(maxPixelSize, size))
+		size = Math.floor(Math.min(size, parentSize.value - minPixelSize))
+		firstPanelSize.value = size
+	}
+
+	const stop = () => {
+		isDragging.value = false
+		document.removeEventListener("mousemove", onMove)
+		document.removeEventListener("mouseup", stop)
+
+		nextTick(() => {
+			emits("dragComplete", firstPanelSize.value, secondPanelSize.value)
+		})
+	}
+
+	document.addEventListener("mousemove", onMove)
+	document.addEventListener("mouseup", stop)
+}
+
+onMounted(() => {
+	getParentSize()
+	// 在父元素尺寸获取后,重新计算初始尺寸
+	if (parentSize.value > 0) {
+		firstPanelSize.value = parseSizeValue(props.size, parentSize.value)
+	}
+})
+
+watch(() => [props.mode, props.containerSize], getParentSize)
+
+// 监听 size 属性变化
+watch(
+	() => props.size,
+	(newSize) => {
+		if (parentSize.value > 0) {
+			firstPanelSize.value = parseSizeValue(newSize, parentSize.value)
+		}
+	}
+)
+</script>
+
+<template>
+	<div class="split-panel" ref="panelRef" :class="modeClass" :style="containerStyle">
+		<div class="panel-item" :style="firstPanelStyle">
+			<slot name="first"></slot>
+		</div>
+
+		<div class="split-handler" :class="handlerClass" @mousedown="startDrag"></div>
+
+		<div class="panel-item" :style="secondPanelStyle">
+			<slot name="second"></slot>
+		</div>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.split-panel {
+	position: relative;
+	background: transparent;
+	--split-handler-bg: #e5e7eb;
+	--split-handler-hover-bg: #409eff;
+	--split-handler-width: 6px;
+	--split-handler-margin: 2px;
+
+	&.horizontal {
+		flex-direction: row;
+	}
+	&.vertical {
+		flex-direction: column;
+	}
+	.panel-item {
+		background: transparent;
+		overflow: auto;
+	}
+	.split-handler {
+		background: var(--split-handler-bg);
+		transition: background 0.2s;
+		z-index: 10;
+		flex-shrink: 0;
+		&.h-handler {
+			width: var(--split-handler-width);
+			height: 100%;
+			cursor: col-resize;
+			margin: 0 var(--split-handler-margin);
+		}
+		&.v-handler {
+			height: var(--split-handler-width);
+			width: 100%;
+			cursor: row-resize;
+			margin: var(--split-handler-margin) 0;
+		}
+		&:hover {
+			background: var(--split-handler-hover-bg);
+		}
+	}
+}
+</style>

+ 1 - 0
UI/VAP_V3.VUE/src/core/types/AppConfig.ts

@@ -3,6 +3,7 @@
  */
 export type AppConfig = {
 	name: string
+	platformName: string
 	version: string
 	copyrightYear: string
 	tenantEnabled: boolean

+ 1 - 1
UI/VAP_V3.VUE/src/router/_staticRouter.ts

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

+ 1 - 1
UI/VAP_V3.VUE/src/router/index.ts

@@ -26,7 +26,7 @@ export const constantRoutes: RouteRecordRaw[] = [
 			{
 				path: "/home",
 				name: "home",
-				component: () => import("@/views/home.vue"),
+				component: () => import("@/views/home/index.vue"),
 				meta: {
 					title: "首页",
 					affix: true

+ 1 - 1
UI/VAP_V3.VUE/src/router/menuMap/index.ts

@@ -7,7 +7,7 @@ export const MenuRouteMaps: MenuRouteMap[] = [
 	},
 	{
 		path: "/home/index",
-		component: () => import("@v/home.vue")
+		component: () => import("@v/home/index.vue")
 	},
 	...system
 ]

+ 4 - 0
UI/VAP_V3.VUE/src/stores/_app.ts

@@ -28,6 +28,9 @@ export const useAppConfigStore = defineStore("appConfig", () => {
 	function getConfig(): AppConfig {
 		return config.value
 	}
+	function getPlatformName() {
+		return getConfig().platformName
+	}
 
 	function isTenant() {
 		return getConfig().tenantEnabled
@@ -52,6 +55,7 @@ export const useAppConfigStore = defineStore("appConfig", () => {
 	return {
 		loadConfig,
 		getConfig,
+		getPlatformName,
 		isTenant,
 		isRegister,
 		isCaptcha,

+ 6 - 6
UI/VAP_V3.VUE/src/views/account/login.vue

@@ -13,7 +13,7 @@ const copyright = computed(() => {
 	return appStore.appConfigStore.getConfig().copyrightYear || "2020"
 })
 
-const title = ref(import.meta.env.VITE_APP_TITLE || "后台管理系统")
+const title = ref("玮博科技管理平台")
 
 const tenantOptions = ref<TenantVO[]>([])
 const loginForm = ref<LoginData>({
@@ -137,12 +137,12 @@ const initTenant = async () => {
 }
 function init() {
 	// 移除设备检测跳转逻辑,使用响应式设计适配所有设备
-	
+
 	appStore.appConfigStore
 		.loadConfig()
 		.then(() => {
 			console.log("---", appStore.appConfigStore.getConfig())
-			title.value = appStore.appConfigStore.getConfig().name
+			title.value = (appStore.appConfigStore.getPlatformName() || "玮博科技") + "管理平台"
 			register.value = appStore.appConfigStore.isRegister()
 			if (appStore.appConfigStore.isTenant()) {
 				tenantEnabled.value = true
@@ -371,13 +371,13 @@ watch(
 	.login {
 		padding: 10px;
 	}
-	
+
 	.login-form {
 		padding: 20px 20px 5px 20px;
 	}
-	
+
 	.el-login-footer {
 		font-size: 10px;
 	}
 }
-</style>
+</style>

+ 3 - 20
UI/VAP_V3.VUE/src/views/home.vue → UI/VAP_V3.VUE/src/views/home/_home.vue

@@ -1,31 +1,14 @@
 <script setup lang="ts">
-import { onMounted, onBeforeUnmount, ref } from "vue"
-import { useRouter } from "vue-router"
+import appStore from "@s"
 
-const name = ref("中科轼峰")
-const router = useRouter()
-
-onMounted(() => {
-	// 如果是移动设备,跳转到移动端主页
-	if (isMobile()) {
-		router.replace("/m-home")
-		return
-	}
-
-	document.querySelector("body")?.classList.add("is-home")
-})
-
-onBeforeUnmount(() => {
-	document.querySelector("body")?.classList.remove("is-home")
-})
+const name = ref(appStore.appConfigStore.getPlatformName())
 </script>
-
 <template>
 	<div class="home-page">
 		<div class="inner-header">
 			<span class="title">
 				<span class="me-5 fw-bold">{{ name }}</span>
-				<small>系统管理平台</small>
+				<small>管理平台</small>
 			</span>
 		</div>
 		<!--Waves Container-->

+ 24 - 0
UI/VAP_V3.VUE/src/views/home/index.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import { useRouter } from "vue-router"
+import Home from "./_home.vue"
+
+const router = useRouter()
+
+onMounted(() => {
+	// 如果是移动设备,跳转到移动端主页
+	if (isMobile()) {
+		router.replace("/m-home")
+		return
+	}
+	document.querySelector("body")?.classList.add("is-home")
+})
+onBeforeUnmount(() => {
+	document.querySelector("body")?.classList.remove("is-home")
+})
+</script>
+
+<template>
+	<div>
+		<Home />
+	</div>
+</template>

+ 554 - 0
UI/VAP_V3.VUE/src/views/home/m-home.vue

@@ -0,0 +1,554 @@
+<script setup lang="ts">
+import appStore from "@s"
+import { useDebounceFn, useResizeObserver } from "@vueuse/core"
+import { ref, onBeforeUnmount, onMounted, computed } from "vue"
+import VbQrScan from "@/components/qrcode/VbQrScan.vue"
+import router from "@r"
+
+const pageRef = ref<HTMLDivElement | null>(null)
+const name = ref(appStore.appConfigStore.getPlatformName())
+const showScanner = ref(false)
+const scanResult = ref("")
+const currentTime = ref(new Date())
+
+const copyright = computed(() => {
+	return appStore.appConfigStore.getConfig().copyrightYear || "2020"
+})
+
+// 更新当前时间
+onMounted(() => {
+	const timer = setInterval(() => {
+		currentTime.value = new Date()
+	}, 1000)
+
+	onBeforeUnmount(() => {
+		clearInterval(timer)
+		document.querySelector("body")?.classList.remove("is-mobile-home")
+	})
+})
+
+function handleScan() {
+	showScanner.value = true
+}
+
+function handleScanResult(result: string) {
+	scanResult.value = result
+	if (result.startsWith("vb@")) {
+		const arr = result.split("@")
+		if (arr.length > 2) {
+			router.push(arr[2])
+			return
+		}
+	}
+	message.msgError("无效二维码")
+}
+
+function handleCloseScanner() {
+	showScanner.value = false
+}
+
+// 格式化时间显示
+const formattedTime = computed(() => {
+	return currentTime.value.toLocaleTimeString("zh-CN", {
+		hour: "2-digit",
+		minute: "2-digit"
+	})
+})
+
+const formattedDate = computed(() => {
+	return currentTime.value.toLocaleDateString("zh-CN", {
+		year: "numeric",
+		month: "long",
+		day: "numeric",
+		weekday: "long"
+	})
+})
+
+// 问候语
+const greeting = computed(() => {
+	const hour = currentTime.value.getHours()
+	if (hour < 6) return "夜深了"
+	if (hour < 9) return "早上好"
+	if (hour < 12) return "上午好"
+	if (hour < 14) return "中午好"
+	if (hour < 18) return "下午好"
+	if (hour < 22) return "晚上好"
+	return "夜深了"
+})
+
+function handleResize() {
+	if (pageRef.value) {
+		pageRef.value.style.width = window.innerWidth + "px"
+		pageRef.value.style.height = window.innerHeight + "px"
+	}
+}
+
+onMounted(() => {
+	handleResize()
+	useResizeObserver(pageRef.value, useDebounceFn(handleResize, 200, { maxWait: 500 }))
+})
+</script>
+
+<template>
+	<div ref="pageRef" class="mobile-home-page">
+		<!-- 动态背景 -->
+		<div class="background-decoration">
+			<div class="circle circle-1"></div>
+			<div class="circle circle-2"></div>
+			<div class="circle circle-3"></div>
+		</div>
+
+		<div class="home-content">
+			<!-- 头部区域 -->
+			<header class="header-section">
+				<div class="time-display">
+					<div class="time">{{ formattedTime }}</div>
+					<div class="date">{{ formattedDate }}</div>
+				</div>
+				<div class="company-name">
+					<h1>{{ name }}</h1>
+					<p class="tagline">移动工作平台</p>
+				</div>
+			</header>
+
+			<!-- 主要内容区 -->
+			<main class="main-content">
+				<!-- 用户信息卡片 -->
+				<div class="user-card">
+					<div class="user-avatar">
+						<vb-symbol
+							:text="appStore.authStore.user.nickName"
+							:src="appStore.authStore.user.avatar"
+							:size="80"
+							shape="circle" />
+					</div>
+					<div class="user-info">
+						<div class="greeting">{{ greeting }}</div>
+						<div class="nickname">{{ appStore.authStore.user.nickName }}</div>
+						<div class="status">在线工作中</div>
+					</div>
+				</div>
+
+				<!-- 快捷操作区 -->
+				<div class="quick-actions">
+					<div class="action-card primary" @click="handleScan">
+						<div class="action-icon">
+							<i class="bi bi-qr-code-scan"></i>
+						</div>
+						<div class="action-info">
+							<div class="action-title">扫一扫</div>
+							<div class="action-desc">扫描二维码快速访问</div>
+						</div>
+						<div class="action-arrow">
+							<i class="bi bi-chevron-right"></i>
+						</div>
+					</div>
+
+					<!-- <div class="action-card">
+						<div class="action-icon">
+							<i class="bi bi-calendar-check"></i>
+						</div>
+						<div class="action-info">
+							<div class="action-title">工作台</div>
+							<div class="action-desc">查看待办事项</div>
+						</div>
+						<div class="action-arrow">
+							<i class="bi bi-chevron-right"></i>
+						</div>
+					</div>
+
+					<div class="action-card">
+						<div class="action-icon">
+							<i class="bi bi-bell"></i>
+						</div>
+						<div class="action-info">
+							<div class="action-title">消息通知</div>
+							<div class="action-desc">查看系统消息</div>
+						</div>
+						<div class="action-badge">3</div>
+						<div class="action-arrow">
+							<i class="bi bi-chevron-right"></i>
+						</div>
+					</div> -->
+				</div>
+
+				<!-- 数据统计区 -->
+				<!-- <div class="stats-section">
+					<h3 class="section-title">今日概览</h3>
+					<div class="stats-grid">
+						<div class="stat-item">
+							<div class="stat-value">12</div>
+							<div class="stat-label">待处理</div>
+						</div>
+						<div class="stat-item">
+							<div class="stat-value">8</div>
+							<div class="stat-label">已完成</div>
+						</div>
+						<div class="stat-item">
+							<div class="stat-value">3</div>
+							<div class="stat-label">进行中</div>
+						</div>
+						<div class="stat-item">
+							<div class="stat-value">1</div>
+							<div class="stat-label">预警</div>
+						</div>
+					</div>
+				</div> -->
+			</main>
+
+			<!-- 底部区域 -->
+			<footer class="footer-section">
+				<p>Copyright ©{{ copyright }}-{{ new Date().getFullYear() }} VAP All Rights Reserved.</p>
+			</footer>
+		</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;
+	position: relative;
+	background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+
+	.background-decoration {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		overflow: hidden;
+		pointer-events: none;
+
+		.circle {
+			position: absolute;
+			border-radius: 50%;
+			background: rgba(255, 255, 255, 0.05);
+			animation: float 20s infinite ease-in-out;
+
+			&.circle-1 {
+				width: 300px;
+				height: 300px;
+				top: -100px;
+				right: -100px;
+				animation-delay: 0s;
+			}
+
+			&.circle-2 {
+				width: 200px;
+				height: 200px;
+				bottom: -50px;
+				left: -50px;
+				animation-delay: 5s;
+			}
+
+			&.circle-3 {
+				width: 150px;
+				height: 150px;
+				top: 50%;
+				left: 50%;
+				animation-delay: 10s;
+			}
+		}
+	}
+}
+
+.home-content {
+	width: 100%;
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+	position: relative;
+	z-index: 1;
+}
+
+.header-section {
+	padding: 40px 20px 20px;
+	text-align: center;
+
+	.time-display {
+		margin-bottom: 20px;
+
+		.time {
+			font-size: 3rem;
+			font-weight: 200;
+			color: rgba(255, 255, 255, 0.95);
+			letter-spacing: 2px;
+			text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+		}
+
+		.date {
+			font-size: 0.9rem;
+			color: rgba(255, 255, 255, 0.7);
+			margin-top: 5px;
+			letter-spacing: 1px;
+		}
+	}
+
+	.company-name {
+		h1 {
+			font-size: 1.8rem;
+			font-weight: 700;
+			color: white;
+			margin-bottom: 5px;
+			text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+		}
+
+		.tagline {
+			font-size: 0.9rem;
+			color: rgba(255, 255, 255, 0.8);
+			letter-spacing: 3px;
+		}
+	}
+}
+
+.main-content {
+	flex: 1;
+	overflow-y: auto;
+	padding: 0 20px 20px;
+
+	&::-webkit-scrollbar {
+		display: none;
+	}
+}
+
+.user-card {
+	background: rgba(255, 255, 255, 0.15);
+	backdrop-filter: blur(10px);
+	border-radius: 20px;
+	padding: 25px;
+	display: flex;
+	align-items: center;
+	gap: 20px;
+	margin-bottom: 25px;
+	box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
+	border: 1px solid rgba(255, 255, 255, 0.2);
+
+	.user-avatar {
+		flex-shrink: 0;
+		border: 3px solid rgba(255, 255, 255, 0.3);
+		border-radius: 50%;
+		background: rgba(255, 255, 255, 0.1);
+
+		:deep(.symbol) {
+			border-radius: 50%;
+		}
+	}
+
+	.user-info {
+		flex: 1;
+
+		.greeting {
+			font-size: 0.85rem;
+			color: rgba(255, 255, 255, 0.7);
+			margin-bottom: 5px;
+		}
+
+		.nickname {
+			font-size: 1.4rem;
+			font-weight: 600;
+			color: white;
+			margin-bottom: 5px;
+			text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+		}
+
+		.status {
+			font-size: 0.75rem;
+			color: #4ade80;
+			display: flex;
+			align-items: center;
+			gap: 5px;
+
+			&::before {
+				content: "";
+				width: 6px;
+				height: 6px;
+				background: #4ade80;
+				border-radius: 50%;
+				display: inline-block;
+				animation: pulse 2s infinite;
+			}
+		}
+	}
+}
+
+.quick-actions {
+	margin-bottom: 25px;
+}
+
+.action-card {
+	background: rgba(255, 255, 255, 0.15);
+	backdrop-filter: blur(10px);
+	border-radius: 16px;
+	padding: 18px;
+	display: flex;
+	align-items: center;
+	gap: 15px;
+	margin-bottom: 12px;
+	cursor: pointer;
+	transition: all 0.3s ease;
+	border: 1px solid rgba(255, 255, 255, 0.1);
+	position: relative;
+
+	&:active {
+		transform: scale(0.98);
+		background: rgba(255, 255, 255, 0.2);
+	}
+
+	&.primary {
+		background: linear-gradient(135deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.1));
+		border-color: rgba(255, 255, 255, 0.3);
+	}
+
+	.action-icon {
+		width: 45px;
+		height: 45px;
+		border-radius: 12px;
+		background: rgba(255, 255, 255, 0.2);
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 1.5rem;
+		color: white;
+		flex-shrink: 0;
+		i {
+			font-size: 1.5rem;
+			color: #eee;
+		}
+	}
+
+	.action-info {
+		flex: 1;
+
+		.action-title {
+			font-size: 1rem;
+			font-weight: 600;
+			color: white;
+			margin-bottom: 3px;
+		}
+
+		.action-desc {
+			font-size: 0.8rem;
+			color: rgba(255, 255, 255, 0.6);
+		}
+	}
+
+	.action-arrow {
+		color: rgba(255, 255, 255, 0.5);
+		font-size: 1.2rem;
+	}
+
+	.action-badge {
+		position: absolute;
+		top: 10px;
+		right: 45px;
+		background: #ef4444;
+		color: white;
+		font-size: 0.7rem;
+		font-weight: 600;
+		padding: 2px 8px;
+		border-radius: 10px;
+		animation: bounce 2s infinite;
+	}
+}
+
+.stats-section {
+	background: rgba(255, 255, 255, 0.1);
+	backdrop-filter: blur(10px);
+	border-radius: 16px;
+	padding: 20px;
+	border: 1px solid rgba(255, 255, 255, 0.1);
+
+	.section-title {
+		font-size: 1rem;
+		font-weight: 600;
+		color: white;
+		margin-bottom: 15px;
+		text-align: left;
+	}
+
+	.stats-grid {
+		display: grid;
+		grid-template-columns: repeat(4, 1fr);
+		gap: 15px;
+
+		.stat-item {
+			text-align: center;
+
+			.stat-value {
+				font-size: 1.6rem;
+				font-weight: 700;
+				color: #fbbf24;
+				margin-bottom: 5px;
+				text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+			}
+
+			.stat-label {
+				font-size: 0.75rem;
+				color: rgba(255, 255, 255, 0.6);
+			}
+		}
+	}
+}
+
+.footer-section {
+	padding: 20px;
+	text-align: center;
+
+	p {
+		font-size: 0.75rem;
+		color: rgba(255, 255, 255, 0.5);
+	}
+}
+
+@keyframes shine {
+	0% {
+		background-position: 0% 50%;
+	}
+	100% {
+		background-position: 200% 50%;
+	}
+}
+
+@keyframes float {
+	0%,
+	100% {
+		transform: translate(0, 0) rotate(0deg);
+	}
+	33% {
+		transform: translate(30px, -30px) rotate(120deg);
+	}
+	66% {
+		transform: translate(-20px, 20px) rotate(240deg);
+	}
+}
+
+@keyframes pulse {
+	0%,
+	100% {
+		opacity: 1;
+	}
+	50% {
+		opacity: 0.5;
+	}
+}
+
+@keyframes bounce {
+	0%,
+	100% {
+		transform: translateY(0);
+	}
+	50% {
+		transform: translateY(-5px);
+	}
+}
+</style>

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

@@ -1,204 +0,0 @@
-<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>