Element Plus左侧侧边栏按照屏幕宽度来确定显示和隐藏,如果太小的话,侧边栏消失,菜单会变成一个小按钮,点击按钮以模态框弹出

Element Plus左侧侧边栏按照屏幕宽度来确定显示和隐藏,如果太小的话,侧边栏消失,菜单会变成一个小按钮,点击按钮以模态框弹出

核心代码

头部的组件(子组件)

用媒体查询 @media

父组件

代码

管理后台管理的头部Header

html 复制代码
<template>
	<!-- 管理后台管理的头部Header -->
	<div class="header-container">
		<div class="l_content">
			<!-- 小屏菜单按钮 -->
			<el-button :icon="Operation" @click="click" class="menu-btn" type="primary"/>
			<span>市场部BI项目管理系统</span>
		</div>
		<div class="r_content">
			<el-dropdown @command="handleCommand">
				<span class="el-dropdown-link">
					<h4>{{name}}</h4>
					<el-icon class="el-icon--right">
						<arrow-down />
					</el-icon>
				</span>
				<template #dropdown>
					<el-dropdown-menu>
						<el-dropdown-item command="1">退出登录</el-dropdown-item>
						<el-dropdown-item command="2">修改密码</el-dropdown-item>
					</el-dropdown-menu>
				</template>
			</el-dropdown>
		</div>
	</div>

	<!-- 修改密码的dialog -->
	<div>
		<el-dialog v-model="dialogFormVisible" title="修改密码" width="680"
			@close="handleADialogClose(ruleModifyPassFormRef)">
			<el-form :model="modifyPassRuleForm" ref="ruleModifyPassFormRef" :rules="rules" label-width="auto">
				<el-form-item label="员工号:" :label-width="formLabelWidth" prop="emp_Num">
					<el-input v-model="modifyPassRuleForm.emp_Num" autocomplete="off" disabled />
				</el-form-item>
				<el-form-item label="旧密码" :label-width="formLabelWidth" prop="oldPassword">
					<el-input v-model="modifyPassRuleForm.oldPassword" type="password" show-password
						placeholder="请输入旧密码" />
				</el-form-item>
				<el-form-item label="新密码" :label-width="formLabelWidth" prop="newPassword">
					<el-input v-model="modifyPassRuleForm.newPassword" type="password" show-password
						placeholder="请输入新密码" />
				</el-form-item>
				<el-form-item label="再次输入新密码" :label-width="formLabelWidth" prop="newPasswordAgain">
					<el-input v-model="modifyPassRuleForm.newPasswordAgain" type="password" show-password
						placeholder="请再次输入新密码" />
				</el-form-item>
			</el-form>
			<template #footer>
				<div class="dialog-footer">
					<el-button @click="CancelModifyPass(ruleModifyPassFormRef)">取消</el-button>
					<el-button type="primary" @click="ConfirmModifyPass(ruleModifyPassFormRef)">
						确认
					</el-button>
				</div>
			</template>
		</el-dialog>
	</div>
</template>

<script lang="ts" setup>
	import axios from 'axios'
	import { ArrowDown } from '@element-plus/icons-vue'
	import { computed, reactive, ref, onMounted } from 'vue'
	import router from '@/router' //引入我们配置的路由模块
	import {
		ElMessage,//消息提示
		ElMessageBox
	} from 'element-plus'
	import { Operation } from '@element-plus/icons-vue'
	import type { FormInstance, FormRules } from 'element-plus'
	import { useEnvironment } from '@/tools/useEnvironment.js' //引入我们的环境变量
	const drawerVisible = ref(false) // 抽屉显示
	//环境变量对象
	const env = reactive({
		VITE_ENV: '',
		baseUrl: ''
	})

	//计算属性,计算现实的登录名称
	const name = computed(() => {
		let admin = sessionStorage.getItem('admin')
		let emp_Num = sessionStorage.getItem('emp_Num')

		if (admin == '1') {
			return '管理员:' + emp_Num
		}
		else {
			return '普通用户:' + emp_Num
		}
	})

	//修改密码对话框是否可见
	const dialogFormVisible = ref(false)

	//表单的宽度
	const formLabelWidth = '140px'
	//创建表单的引用对象
	const ruleModifyPassFormRef = ref<FormInstance>()

	//修改密码的表单
	const modifyPassRuleForm = reactive<RuleForm>({
		emp_Num: '',
		oldPassword: '',
		newPassword: '',
		newPasswordAgain: ''
	})

	//验证旧密码和新密码
	const validatePass = (rule : any, value : any, callback : any) => {
		if (value === '') {
			callback(new Error('新密码必输!请输入新密码'))
		} else if (value === modifyPassRuleForm.oldPassword) {
			callback(new Error("新密码应该与旧密码不相同!"))
		} else {
			callback()
		}
	}

	//验证再次输入的密码
	const validatePass2 = (rule : any, value : any, callback : any) => {
		if (value === '') {
			callback(new Error('请再次输入密码'))
		} else if (value !== modifyPassRuleForm.newPassword) {
			callback(new Error("两次新密码不匹配!"))
		} else {
			callback()
		}
	}

	//规则
	const rules = reactive<FormRules<RuleForm>>({
		emp_Num: [
			{ required: true, message: '账号必输!请输入账号', trigger: 'blur' },
		],
		oldPassword: [
			{ required: true, message: '旧密码必输!请输入旧密码', trigger: ['blur', 'change'] },
		],
		newPassword: [
			{ required: true, validator: validatePass, trigger: ['blur', 'change'] },
		],
		newPasswordAgain: [
			{ required: true, validator: validatePass2, trigger: ['blur', 'change'] },
		],
	})

	//修改密码的数据结构
	const modifyPassData = reactive({
		emp_Num: '',
		oldPassword: '',
		newPassword: '',
	})

	//下拉框点击命令
	const handleCommand = (command : string | number | object) => {
		if (command == '1') {
			//清除所有的sessionStorage
			sessionStorage.clear()
			router.push('/')
		}
		else if (command == '2') {
			dialogFormVisible.value = true
		}
	}

	//取消修改
	const CancelModifyPass = (formEl : FormInstance | undefined) => {
		ElMessage.info('取消修改密码')
		//重置表单
		formEl.resetFields()
		dialogFormVisible.value = false
	}

	//确认修改
	const ConfirmModifyPass = async (formEl : FormInstance | undefined) => {
		if (!formEl) return  //如果是空的,就直接返回
		await formEl.validate((valid, fields) => {  //如果不是空的,等待此函数处理完成
			if (valid) {
				let token = sessionStorage.getItem('token')
				modifyPassData.emp_Num = modifyPassRuleForm.emp_Num
				modifyPassData.oldPassword = modifyPassRuleForm.oldPassword
				modifyPassData.newPassword = modifyPassRuleForm.newPassword
				axios.post(`${env.baseUrl}/User/ModifyPass`, JSON.stringify(modifyPassData), {
					headers: {
						'Content-Type': 'application/json',
						'Authorization': `Bearer ${token}`
					},
				}).then(function (response) {
					if (response.status == 200 && response.data.code == 'S') {
						ElMessage.success(response.data.msg)
					}
					else {
						ElMessage.error(response.data.msg)
					}
				}).catch(function (error) {
					console.log(error)
					//未授权。也就是Token失效,需要重新登录系统
					if (error.status == 401) {
						ElMessageBox.confirm(
							'用户登录的Token失效,请重新登录',
							'错误信息',
							{
								confirmButtonText: '确定',
								cancelButtonText: '取消',
								type: 'warning',
							}
						).then(() => {
							// 确认按钮的回调函数
							router.push('/')  //跳转登录界面
						}).catch(() => {
							// 取消按钮的回调函数或者关闭模态框
							console.log('Canceled');
						});
					}
					if (error.response != undefined && error.response.data.msg != null && error.response.data.msg != undefined) {
						ElMessage.error('修改失败,出现异常:' + error.response.data.msg)
					}
					else {
						ElMessage.error('修改失败,出现异常:' + error.message)
					}
				})
				//重置表单
				formEl.resetFields()
				dialogFormVisible.value = false
			}
		})
	}

	//添加用户对话框关闭之后
	const handleADialogClose = (formEl : FormInstance | undefined) => {
		//重置表单
		formEl.resetFields()
	}

	//挂载的钩子函数
	onMounted(() => {
		//获取环境变量
		const { VITE_ENV, baseUrl } = useEnvironment()
		env.VITE_ENV = VITE_ENV.value
		env.baseUrl = baseUrl.value
		let emp_Num = sessionStorage.getItem('emp_Num')
		modifyPassRuleForm.emp_Num = emp_Num
	})
	
	// 声明组件会触发的两个事件:'eventA' 和 'eventB'
	const emit = defineEmits(['eventA', 'eventB'])
	
	//按钮被点击时
	const click = ()=>{
		drawerVisible.value = true
		// 触发 'eventA' 事件,并传递数据
		emit('eventA',drawerVisible.value)
	}
</script>

<style>
	.header-container {
		/* flex布局 */
		display: flex;
		justify-content: space-between;
		align-items: center;
	}

	.l_content {
		margin-top: 10px;
		margin-left:10px;
		font-size: 20px;
		font-weight: bold;
		display: flex;
		align-items: center; /* 交叉轴(Y轴)居中 */
	}
	.l-content .el-button{
		padding: 0 8px;
	}
	.l_content span{
		margin-left:20px;
	}

	.r_content {
		margin-right: 30px;
	}

	.user {
		width: 40px;
		height: 40px;
		border-radius: 50%;
	}

	.el-dropdown-link {
		cursor: pointer;
		/* color: var(--el-color-primary); */
		color: white;
		display: flex;
		align-items: center;
	}
	
	/* 小屏显示菜单按钮 */
	.menu-btn {
	  display: none;
	}
	
	@media (max-width: 930px) {
	  .menu-btn {
	    display: inline-block;
	  }
	}
</style>

父组件监听子组件

html 复制代码
<template>
	<!-- 后台管理首页 -->
	<div class="common-layout">
		<el-container>
			<el-header>
				<!-- 父组件监听子组件的事件 -->
				<BackIndexHeader @eventA="handleEventA" />
			</el-header>
			<el-container class="back-admin-index-con">
				<el-aside width="290px" style="background-color: #242f41;" :class="{ 'hide-on-small': isHide }">
					<el-scrollbar>
						<el-menu active-text-color="#44a1f7" background-color="#242f41" class="el-menu-vertical-demo"
							:default-active="activeMenu" text-color="#fff" @open="handleOpen" @close="handleClose"
							@select="handleSelect" ref="menuRef">
							<el-sub-menu index="1">
								<template #title>
									<el-icon>
										<Document />
									</el-icon>
									<span>文件导入</span>
								</template>
								<!-- <el-menu-item index="1-0">Test表文件导入</el-menu-item> -->
								<el-menu-item index="1-1">我司交付车型终端销量情况导入</el-menu-item>
								<el-menu-item index="1-2">不同级别乘用车批售量导入</el-menu-item>
								<el-menu-item index="1-3">新能源乘用车市场销量表现不同价格段零售量导入</el-menu-item>
								<el-menu-item index="1-4">乘用车市场不同价格段零售量导入</el-menu-item>
								<el-sub-menu index="1-5" v-show="isAdmin">
									<template #title>
										<span>东南亚新能源汽车市场情况</span>
									</template>
									<el-menu-item index="1-5-1">泰国(TAIA)市场销量</el-menu-item>
									<el-menu-item index="1-5-2">印度尼西亚(GAIKINDO)市场销量</el-menu-item>
									<el-menu-item index="1-5-3">越南(VAMA)市场销量</el-menu-item>
									<el-menu-item index="1-5-4">马来西亚(MAA)市场销量</el-menu-item>
								</el-sub-menu>

								<el-menu-item index="1-6">欧洲新能源汽车市场情况导入</el-menu-item>
								<el-sub-menu index="1-7" v-show="isAdmin">
									<template #title>
										<span>国内动力电池单车平均带电量</span>
									</template>
									<el-menu-item index="1-7-1">新能源汽车月度单台平均装车电量</el-menu-item>
									<el-menu-item index="1-7-2">纯电动乘用车续航里程</el-menu-item>
								</el-sub-menu>
								<el-menu-item index="1-8">国内动力电池企业装车量市场分析导入</el-menu-item>
							</el-sub-menu>
							<el-menu-item index="2">
								<el-icon>
									<User />
								</el-icon>
								<template #title>用户管理</template>
							</el-menu-item>
							<el-sub-menu index="3" v-show="isAdmin">
								<template #title>
									<el-icon>
										<Service />
									</el-icon>
									<span>系统管理</span>
								</template>
								<el-menu-item index="3-1">组管理</el-menu-item>
								<el-menu-item index="3-2">用户权限分配</el-menu-item>
							</el-sub-menu>
						</el-menu>
					</el-scrollbar>
				</el-aside>
				<el-main>
					<router-view />
				</el-main>
			</el-container>
		</el-container>


		<!-- 小屏抽屉 -->
		<el-drawer v-model="drawerVisible" direction="ltr" size="400px" title="菜单">
			<el-scrollbar>
				<el-menu active-text-color="#44a1f7" class="el-menu-vertical-demo" :default-active="activeMenu"
					default-active="1-1" @open="handleOpen" @close="handleClose" @select="handleSelect" ref="menuRef">
					<el-sub-menu index="1">
						<template #title>
							<el-icon>
								<Document />
							</el-icon>
							<span>文件导入</span>
						</template>
						<el-menu-item index="1-1">我司交付车型终端销量情况导入</el-menu-item>
						<el-menu-item index="1-2">不同级别乘用车批售量导入</el-menu-item>
						<el-menu-item index="1-3">新能源乘用车市场销量表现不同价格段零售量导入</el-menu-item>
						<el-menu-item index="1-4">乘用车市场不同价格段零售量导入</el-menu-item>
						<el-sub-menu index="1-5" v-show="isAdmin">
							<template #title>
								<span>东南亚新能源汽车市场情况</span>
							</template>
							<el-menu-item index="1-5-1">泰国(TAIA)市场销量</el-menu-item>
							<el-menu-item index="1-5-2">印度尼西亚(GAIKINDO)市场销量</el-menu-item>
							<el-menu-item index="1-5-3">越南(VAMA)市场销量</el-menu-item>
							<el-menu-item index="1-5-4">马来西亚(MAA)市场销量</el-menu-item>
						</el-sub-menu>
						<el-menu-item index="1-6">欧洲新能源汽车市场情况导入</el-menu-item>
						<el-sub-menu index="1-7" v-show="isAdmin">
							<template #title>
								<span>国内动力电池单车平均带电量</span>
							</template>
							<el-menu-item index="1-7-1">新能源汽车月度单台平均装车电量</el-menu-item>
							<el-menu-item index="1-7-2">纯电动乘用车续航里程</el-menu-item>
						</el-sub-menu>
						<el-menu-item index="1-8">国内动力电池企业装车量市场分析导入</el-menu-item>
					</el-sub-menu>
					<el-menu-item index="2">
						<el-icon>
							<User />
						</el-icon>
						<template #title>用户管理</template>
					</el-menu-item>
					<el-sub-menu index="3" v-show="isAdmin">
						<template #title>
							<el-icon>
								<Service />
							</el-icon>
							<span>系统管理</span>
						</template>
						<el-menu-item index="3-1">组管理</el-menu-item>
						<el-menu-item index="3-2">用户权限分配</el-menu-item>
					</el-sub-menu>
				</el-menu>
			</el-scrollbar>
		</el-drawer>
	</div>
</template>

<script lang="ts" setup>
	// 
	import {
		ref,
		reactive,
		onMounted,
		onBeforeMount,
		computed,
		onUnmounted
	} from 'vue'
	import {
		Document,
		Menu as IconMenu,
		Location,
		Setting
	} from '@element-plus/icons-vue'
	import {
		ElMessage, //消息提示
		ElMessageBox,
		MenuItemClicked
	} from 'element-plus'
	import router from '@/router' //引入我们配置的路由模块
	import { useRoute } from 'vue-router'
	import BackIndexHeader from './BackIndexHeader.vue';
	import axios from 'axios'

	import { useEnvironment } from '@/tools/useEnvironment.js' //引入我们的环境变量
	const route = useRoute()
	const activePath = computed(() => route.path) //当前选中的路径
	const drawerVisible = ref(false) 	// 抽屉显示
	const activeMenu = ref('1-1') 		//激活的菜单
	const isHide = ref(false) 			//隐藏侧边栏

	//环境变量对象
	const env = reactive({
		VITE_ENV: '',
		baseUrl: ''
	})
	const menuRef = ref(null);

	// const dialogHeight = ref(0)
	const conHeight = ref(0)

	//是否是管理员
	const isAdmin = ref(false)

	//sub-menu 展开的回调
	const handleOpen = (key : string, keyPath : string[]) => {
	}
	//sub-menu 收起的回调
	const handleClose = (key : string, keyPath : string[]) => {
		router.push('/admin')
	}

	//菜单激活回调
	const handleSelect = (index : string, item : MenuItemClicked) => {
		drawerVisible.value = false
		switch (index) {
			case '1-1':
				router.push('/admin/upload-DVT')
				break
			case '1-2':
				router.push('/admin/upload-DGPVW')
				break
			case '1-3':
				router.push('/admin/upload-NPVMR')
				break
			case '1-4':
				router.push('/admin/upload-PVMR')
				break
			case '1-5-1':
				router.push('/admin/upload-Thai')
				break
			case '1-5-2':
				router.push('/admin/upload-Indonesia')
				break
			case '1-5-3':
				router.push('/admin/upload-Vietnam')
				break
			case '1-5-4':
				router.push('/admin/upload-Malaysia')
				break
			case '1-6':
				router.push('/admin/upload-EU')
				break
			case '1-7-1':
				router.push('/admin/upload-NMA')
				break
			case '1-7-2':
				router.push('/admin/upload-PER')
				break
			case '1-8':
				router.push('/admin/upload-DBL')
				break
			case '2':
				router.push('/admin/user-manage')
				break
			case '3-1':
				router.push('/admin/group-manage')
				break
		}
		activeMenu.value = index
	}

	//注册一个钩子,在组件被挂载之前被调用
	onBeforeMount(() => {
		//获取环境变量
		const { VITE_ENV, baseUrl } = useEnvironment()
		env.VITE_ENV = VITE_ENV.value
		env.baseUrl = baseUrl.value

		let token = sessionStorage.getItem('token')
		if (token == null) {
			router.push('/')
			ElMessage.error('您未登录,请登录再使用后台管理系统')
		}
		else {
			//获取用户
			GetUser()
		}
	})

	//获取用户信息
	const GetUser = () => {
		//获取sessionStorage的键值
		let token = sessionStorage.getItem('token')
		let emp_Num = sessionStorage.getItem('emp_Num')
		axios.get(`${env.baseUrl}/User/GetUser`, {
			params: {
				emp_Num: emp_Num
			},
			headers: {
				Authorization: `Bearer ${token}`
			}
		}).then(function (response) {
			if (response.status == 200 && response.data.code == 'S') {
				let user = JSON.parse(response.data.data) //解析json字符串
				//获取组的信息
				if (user[0].u_group != null && user[0].u_group != undefined) {
					GetGroup(user[0].u_group)
				}

			}
			else {
				ElMessage.error(response.data.msg)
			}
		}).catch(function (error) {
			//未授权。也就是Token失效,需要重新登录系统
			if (error.status == 401) {
				ElMessageBox.confirm(
					'用户登录的Token失效,请重新登录',
					'错误信息',
					{
						confirmButtonText: '确定',
						cancelButtonText: '取消',
						type: 'warning',
					}
				).then(() => {
					// 确认按钮的回调函数
					router.push('/')  //跳转登录界面
				}).catch(() => {
					// 取消按钮的回调函数或者关闭模态框
					console.log('Canceled');
				});
			}
			if (error.response != undefined && error.response.data.msg != null && error.response.data.msg != undefined) {
				ElMessage.error('获取用户失败,出现异常:' + error.response.data.msg)
			}
			else {
				ElMessage.error('获取用户失败,出现异常:' + error.message)
			}
		})

	}


	//获取组信息
	const GetGroup = (u_group : string) => {
		//获取sessionStorage的键值
		let token = sessionStorage.getItem('token')
		let admin = sessionStorage.getItem('admin')

		axios.get(`${env.baseUrl}/Group/GetGroup`, {
			params: {
				groupNum: u_group
			},
			headers: {
				Authorization: `Bearer ${token}`
			}
		}).then(function (response) {
			if (response.status == 200 && response.data.code == 'S') {
				let data = JSON.parse(response.data.data) //解析json字符串
				//管理员且是管理组,显示系统管理
				if (admin == 1 && data[0].admin == 1) {
					sessionStorage.setItem('adminGroup', 1)
					isAdmin.value = true
				}
			}
			else {
				ElMessage.error(response.data.msg)
			}
		}).catch(function (error) {
			//未授权。也就是Token失效,需要重新登录系统
			if (error.status == 401) {
				ElMessageBox.confirm(
					'用户登录的Token失效,请重新登录',
					'错误信息',
					{
						confirmButtonText: '确定',
						cancelButtonText: '取消',
						type: 'warning',
					}
				).then(() => {
					// 确认按钮的回调函数
					router.push('/')  //跳转登录界面
				}).catch(() => {
					// 取消按钮的回调函数或者关闭模态框
					console.log('Canceled');
				});
			}
			if (error.response != undefined && error.response.data.msg != null && error.response.data.msg != undefined) {
				ElMessage.error('获取组信息失败,出现异常:' + error.response.data.msg)
			}
			else {
				ElMessage.error('获取组信息失败,出现异常:' + error.message)
			}
		})
	}
	//刷新当前选中的菜单项
	const freshActiveMenuItem = () => {
		switch (activePath.value) {
			case '/admin/upload-DVT':
				activeMenu.value = '1-1'
				break
			case '/admin/upload-DGPVW':
			activeMenu.value = '1-2'
				break
			case '/admin/upload-NPVMR':
				activeMenu.value = '1-3'
				break
			case '/admin/upload-PVMR':
				activeMenu.value = '1-4'
				break
			case '/admin/upload-Thai':
				activeMenu.value = '1-5-1'
				break
			case '/admin/upload-Indonesia':
				activeMenu.value = '1-5-2'
				break
			case '/admin/upload-Vietnam':
				activeMenu.value = '1-5-3'
				break
			case '/admin/upload-Malaysia':
				activeMenu.value = '1-5-4'
				break
			case '/admin/upload-EU':
				activeMenu.value = '1-6'
				break
			case '/admin/upload-NMA':
				activeMenu.value = '1-7-1'
				break
			case '/admin/upload-PER':
				activeMenu.value = '1-7-2'
				break
			case '/admin/upload-DBL':
				activeMenu.value = '1-8'
				break
			case '/admin/user-manage':
				activeMenu.value = '2'
				break
			case '/admin/group-manage':
				activeMenu.value = '3-1'
				break
		}
	}

	// 监听窗口尺寸
	const handleResize = () => {
		const width = window.innerWidth
		isHide.value = width < 930
	}

	//挂载时的钩子函数
	onMounted(() => {
		handleResize()
		freshActiveMenuItem()
		//用于添加窗口大小改变事件(resize)监听器的 JavaScript 方法
		window.addEventListener('resize', handleResize)
	})

	onUnmounted(() => {
		//用于移除窗口大小改变事件(resize)监听器的 JavaScript 方法
		window.removeEventListener('resize', handleResize)
	})

	//监听子组件触发的事件
	const handleEventA = (value) => {
		//将抽屉的可见性打开
		drawerVisible.value = value
		//侧边栏隐藏
		isHide.value = true
	}
</script>

<style scoped>
	.el-header {
		background-color: #242f41;
		height: 70px;
		color: white;
		border-bottom: 2px white solid;
	}

	.el-header h1 {
		margin-left: 40px;
	}

	.back-admin-index-con {
		height: 90vh;
	}

	.el-menu {
		border: 0 !important;
	}

	/* 小屏隐藏侧边栏 */
	.hide-on-small {
		display: none;
	}

	.el-menu-item {
		/* 允许元素换行 */
		white-space: normal !important;
		/* 设置行高,否则元素会重叠 */
		line-height: 18px;
	}
</style>

运行结果

相关推荐
看客随心2 小时前
vue + elementPlus大屏项目使用autofit做适配及注意点
前端·javascript·vue.js
网络点点滴3 小时前
Vue3 全局API转移到应用对象
前端·javascript·vue.js
whuhewei3 小时前
useCountDown (React Hooks)倒计时
前端·javascript·react.js
终端鹿3 小时前
插槽(slot):默认插槽、具名插槽、作用域插槽实战
前端·javascript·vue.js
Amumu121383 小时前
工程化: webpack介绍和基础用法
前端·javascript·工程化
SuperEugene3 小时前
前端组件三层架构:页面/业务/基础组件划分,高内聚低耦合|组件化设计基础篇
前端·javascript·vue.js·架构·前端框架·状态模式
前端郭德纲4 小时前
JavaScript原生开发与鸿蒙原生开发对比
开发语言·javascript·harmonyos
百撕可乐4 小时前
NextJS官网实战02:项目的基础骨架搭建
前端·javascript·react.js