소스 검색

教师端登录 /tlogin 学生端登录 /slogin

于添 7 달 전
부모
커밋
a0a3031a16

+ 1 - 0
.env.production

@@ -6,6 +6,7 @@ VITE_TITLE = smilingFace
 
 # 接口地址
 VITE_API_BASEURL = http://192.168.1.245:19003
+#VITE_API_BASEURL = http://192.168.31.56:19003
 VITE_FILEURL = http://192.168.1.245:10005/education/
 # 本地端口
 VITE_PORT = 9000

+ 1 - 0
src/components/UpLoadBreakPoint/index.vue

@@ -794,6 +794,7 @@
 		fileList.value = []
 	}
 	const open = () => {
+
 		clearFileList()
 	}
 

+ 24 - 20
src/components/UpLoadDoc/index.vue

@@ -1,5 +1,6 @@
 <template>
 	<div class="cover-upload-row">
+		<span v-if="form.id != ''" style="cursor: pointer; color: #0d84ff">{{ form.name }}</span>
 		<a-upload
 			:key="form.id"
 			:show-upload-list="false"
@@ -9,11 +10,14 @@
 			:action="action"
 			:headers="headers"
 		>
-				<a-button v-if="form.id == ''">
-					<CloudUploadOutlined/>
-					上传文件
-				</a-button>
-				<span v-if="form.id != ''" style="cursor: pointer;">{{form.name}}</span>
+			<a-button v-if="form.id == ''" style="margin-left: 20px">
+				<CloudUploadOutlined/>
+				上传文件
+			</a-button>
+			<a-button v-if="form.id != ''" style="margin-left: 20px">
+				<CloudUploadOutlined/>
+				更换文件
+			</a-button>
 		</a-upload>
 		<span class="upload-tip">支持ppt、word等格式文件上传,文件大小不超过10MB</span>
 	</div>
@@ -26,7 +30,8 @@ import {message} from 'ant-design-vue'
 import {PictureOutlined, CloudUploadOutlined} from '@ant-design/icons-vue'
 import tool from "@/utils/tool";
 import sysConfig from '@/config/index'
-const action = ref(sysConfig.API_URL+'/api/webapp/dev/file/uploadMinioReturnId')
+
+const action = ref(sysConfig.API_URL + '/api/webapp/dev/file/uploadMinioReturnId')
 const headers = ref({
 	token: tool.data.get('TOKEN')
 })
@@ -35,9 +40,9 @@ const headers = ref({
 
 const props = defineProps({
 	count: Number,
-	default : () => 1
+	default: () => 1
 })
-const emit = defineEmits(['update:visible', 'ok','handlerUpSelect','handlerNewSelect','handlerUpDoc'])
+const emit = defineEmits(['update:visible', 'ok', 'handlerUpSelect', 'handlerNewSelect', 'handlerUpDoc'])
 
 const modalVisible = ref(props.visible)
 watch(
@@ -52,18 +57,18 @@ watch(modalVisible, (v) => {
 
 const formRef = ref()
 const file = ref({
-	id : '',
+	id: '',
 	name: '',
 })
 const form = reactive({
 
-	id : '',
+	id: '',
 	title: '',
 	video: '',
 	coverUrl: '',
 	docUrl: '',
 	srtUrl: '',
-	name : ''
+	name: ''
 })
 
 const rules = {
@@ -74,11 +79,11 @@ const rules = {
 
 
 const handleChange = (res) => {
-	console.log('上传完毕',res)
-	if (res.file && res.file.response &&res.file.response.code == 200) {
+	console.log('上传完毕', res)
+	if (res.file && res.file.response && res.file.response.code == 200) {
 		message.success('上传成功')
-		form.id =	res.file.response.data
-		emit('handlerUpDoc',form.id)
+		form.id = res.file.response.data
+		emit('handlerUpDoc', form.id)
 	} else {
 		// message.error('上传失败')
 	}
@@ -88,9 +93,9 @@ const handleChange = (res) => {
 const setData = (item) => {
 	form.id = item.id
 	form.name = item.name
-	emit('handlerUpDoc',form.id)
+	emit('handlerUpDoc', form.id)
 }
-const beforeUploadDoc = (file) =>{
+const beforeUploadDoc = (file) => {
 	const isDoc = /\.(ppt|pptx|doc|docx|pdf)$/i.test(file.name)
 	const isLt10M = file.size / 1024 / 1024 < 10
 	if (!isDoc) {
@@ -106,12 +111,11 @@ const beforeUploadDoc = (file) =>{
 }
 
 
-
 const dummyRequest = ({onSuccess}) => {
 
 }
 const setFile = (fileData) => {
-	console.log("设置了文件",fileData)
+	console.log("设置了文件", fileData)
 	file.value.id = fileData.id
 	file.value.name = fileData.fileName
 }
@@ -128,7 +132,7 @@ const handleCancel = () => {
 }
 
 defineExpose({
-	getData,setFile,setData
+	getData, setFile, setData
 })
 </script>
 

+ 11 - 7
src/components/UpLoadImg/index.vue

@@ -1,5 +1,8 @@
 <template>
 	<div class="cover-upload-row">
+		<div v-if="form.coverUrl != ''" class="cover-upload-box">
+			<a-image  :src="form.coverUrl" class="cover-img"/>
+		</div>
 		<a-upload
 			:show-upload-list="false"
 			:before-upload="beforeUploadImg"
@@ -8,14 +11,15 @@
 			:headers="headers"
 			@change ="handleChange"
 		>
-			<div class="cover-upload-box">
-				<img v-if="form.coverUrl != ''" :src="form.coverUrl" class="cover-img"/>
-				<div v-else class="cover-placeholder">
-					<PictureOutlined style="font-size: 32px; color: #bbb"/>
-				</div>
-			</div>
+<!--			<div  class="cover-upload-box">-->
+<!--				<PictureOutlined  style="font-size: 32px; color: #bbb"/>-->
+<!--			</div>-->
+			<a-button v-if="form.coverUrl == ''" >上传图片</a-button>
+			<a-button v-if="form.coverUrl != ''" >更改图片</a-button>
 		</a-upload>
-		<div class="cover-tip">支持jpg、png等格式文件上传,文件大小不超过10MB</div>
+
+
+		<div class="cover-tip" style="margin-left: 10px">支持jpg、png等格式文件上传,文件大小不超过10MB</div>
 	</div>
 
 </template>

+ 7 - 3
src/components/UpLoadSrt/index.vue

@@ -1,6 +1,8 @@
 <template>
 	<div class="cover-upload-row">
+		<span v-if="form.id != ''" style="cursor: pointer; color: #0d84ff">{{ form.name }}</span>
 		<a-upload
+			:key="form.id"
 			:show-upload-list="false"
 			:before-upload="beforeUploadSrt"
 			accept=".srt"
@@ -8,15 +10,17 @@
 			:action="action"
 			:headers="headers"
 		>
-			<a-button v-if="form.id ==''">
+			<a-button v-if="form.id == ''" style="margin-left: 20px">
 				<CloudUploadOutlined/>
 				上传文件
 			</a-button>
-			<span v-if="form.id !=''" style="cursor: pointer;">{{form.name}}</span>
+			<a-button v-if="form.id != ''" style="margin-left: 20px">
+				<CloudUploadOutlined/>
+				更换文件
+			</a-button>
 		</a-upload>
 		<span class="upload-tip">仅支持srt格式文件上传,文件大小不超过1MB</span>
 	</div>
-
 </template>
 
 <script setup>

+ 10 - 0
src/router/portal.js

@@ -71,6 +71,16 @@ const portal = [
 		name: 'notLook',
 		component: () => import('@/views/notLook/index.vue'),
 		children: []
+	},
+	{
+		name: 'slogin',
+		path: '/slogin',
+		component: () => import('@/views/slogin/login.vue'),
+	},
+	{
+		name: 'tlogin',
+		path: '/tlogin',
+		component: () => import('@/views/tlogin/login.vue'),
 	}
 ]
 /**

+ 9 - 1
src/router/whiteList.js

@@ -125,7 +125,15 @@ const constRouters = [
 		meta: {
 			title: '试卷查看'
 		}
-	}
+	},
+	{
+		path: '/slogin',
+		component: () => import('@/views/slogin/login.vue')
+	},
+	{
+		path: '/tlogin',
+		component: () => import('@/views/tlogin/login.vue')
+	},
 ]
 /**
  * 路由白名单(数组形式)

+ 1 - 1
src/utils/newRequest.js

@@ -102,7 +102,7 @@ service.interceptors.response.use(
 			// 统一成功提示
 			const responseUrl = response.config.url
 			const apiNameArray = [
-				'add',
+				// 'add',
 				'edit',
 				'delete',
 				'update',

+ 1 - 1
src/views/courseAdd/components/StudentDetails.vue

@@ -67,7 +67,7 @@
 						<a-button size="small" @click="onDetail(record)">详情</a-button>
 						<!-- <a-button size="small" @click="onEdit(record)">编辑</a-button> -->
 						<!-- <a-button size="small" @click="onSetting(record)">设置</a-button> -->
-						<a-popconfirm title="删除此短信?" @confirm="onDelete(record)">
+						<a-popconfirm title="删除此条目?" @confirm="onDelete(record)">
 							<a-button size="small" danger>删除</a-button>
 						</a-popconfirm>
 					</a-space>

+ 12 - 3
src/views/courseAdd/components/courseInfo.vue

@@ -75,10 +75,10 @@
 				type="primary"
 				v-if="!courseInfoId"
 				style="margin-right: 10px"
-				@click="() => formRef.validate().then(handleSubmit)"
+				@click="submit"
 				>提交</a-button
 			>
-			<a-button type="primary" v-if="courseInfoId" @click="() => formRef.validate().then(handleEdit)"
+			<a-button type="primary" v-if="courseInfoId" @click="editSub"
 				>编辑提交</a-button
 			>
 		</a-form-item>
@@ -179,7 +179,16 @@
 	const handleChangeCover = (fileId) => {
 		formState.coverImageId = fileId
 	}
-
+	const submit = () => {
+		formRef.value.validate().then(()=>{
+			handleSubmit()
+		})
+	}
+	const editSub = () => {
+		formRef.value.validate().then(()=>{
+			handleEdit()
+		})
+	}
 	// 移除封面文件
 	const handleRemoveCover = () => {
 		formState.coverImageId = null

+ 1 - 1
src/views/courseAdd/components/courseProduction/addClassHours.vue

@@ -9,7 +9,7 @@
 				<a-button type="primary" @click="()=>{emit('handlerUpSelect')}">新上传资源</a-button>
 			</div>
 			<!--			<span  style="margin-top: 12px;  margin-bottom: 12px">{{ file.name }}</span>-->
-			<FileName ref="fileNameRef" v-model:value="form.video"></FileName>
+			<FileName ref="fileNameRef" style="margin-top: 30px;" v-model:value="form.video"></FileName>
 		</a-form-item>
 		<a-form-item label="上传封面:" required name="coverUrl">
 			<div class="cover-upload-row">

+ 4 - 3
src/views/courseAdd/components/courseProduction/addDialog.vue

@@ -16,7 +16,7 @@
 				<a-tab-pane key="2" tab="作业">
 					<exList v-if="activeKey == '2'" ref="exListRef" @handlerEx="handlerEx"></exList>
 				</a-tab-pane>
-				<a-tab-pane key="3" tab="考试">
+				<a-tab-pane key="3" tab="章节测验">
 					<exLists v-if="activeKey == '3'" ref="exListsRef" @handlerExs="handlerExs"></exLists>
 				</a-tab-pane>
 			</a-tabs>
@@ -129,10 +129,11 @@ const handleChange = (activeKey) => {
 				}
 				if (modeTag.value == 'edit') {
 					console.log('走没走3', exListsRefData.value)
+					console.log('走没走33', exListsRefData.value[0])
 					if(exListsRefData.value && exListsRefData.value[0]&& exListsRefData.value[0].relateId){
 						exListsRef.value.edit(exListsRefData.value[0].relateId)
-					}else if(exListRefData.value && exListRefData.value[0]&& exListRefData.value[0].id){
-						exListRef.value.edit(exListRefData.value[0].id)
+					}else if(exListsRefData.value && exListsRefData.value[0]&& exListsRefData.value[0].id){
+						exListsRef.value.edit(exListsRefData.value[0].id)
 					}
 					console.log('走没走4', exListsRefData.value)
 					if(exListsRefData.value  == null){

+ 1 - 1
src/views/courseAdd/components/courseProduction/fileName.vue

@@ -1,5 +1,5 @@
 <template>
-	<span  style="margin-top: 12px;  margin-bottom: 12px">{{ formRef.name }}</span>
+	<span  style="margin-top: 12px; color: #0d84ff; margin-bottom: 12px">{{ formRef.name }}</span>
 </template>
 
 <script setup>

+ 2 - 0
src/views/courseAdd/components/courseProduction/resourceUpload.vue

@@ -221,6 +221,7 @@ const handleUploadCancel = () => {
 const open = () => {
 	uploadModalVisible.value = true
 
+	formState.userfileIds = null
 	nextTick(() => {
 		UpLoadBreakPointRef.value.open()
 	})
@@ -491,6 +492,7 @@ const handleUploadOk = async () => {
 				userRelateIds: formState.authType == 1 ? formState.userRelateIds : null
 			}
 			console.log(formData, '上传数据')
+
 			resourceAuditApi
 				.add(formData)
 				.then((res) => {

+ 13 - 3
src/views/courseDetails/index.vue

@@ -15,7 +15,7 @@
 				</div>
 				<div class="action-box">
 					<div class="btn-group">
-						<a-button @click="editVisible = true"> <EditOutlined /> 编辑课程</a-button>
+						<a-button @click="handleEdit(course)"> <EditOutlined /> 编辑课程</a-button>
 						<a-button v-if="course.putawayStatus == '1'" @click="handleTakeOffCourse(0)">
 							<DownOutlined /> 下架课程
 						</a-button>
@@ -86,9 +86,12 @@
 	import LearningStatistics from './components/tab/LearningStatistics.vue'
 	import CourseAdd from '../courseAdd/index.vue'
 	import { getCourseDetail, getChapterAllList, updateCourseStatus, deleteCourse } from '@/api/course/courseDetail.js'
-	import { useRoute } from 'vue-router'
 	import sysConfig from '@/config/index'
+	import { useRouter, useRoute } from 'vue-router'
+
+	const router = useRouter()
 	const route = useRoute()
+
 	const course = ref({
 		courseId: '',
 		courseName: '',
@@ -150,7 +153,14 @@
 			})
 		})
 	})
-
+	const handleEdit = (item) => {
+		router.push({
+			path: '/portal/courseAdd',
+			query: {
+				id: item.courseId
+			}
+		})
+	}
 	function onPageChange(page) {
 		currentPage.value = page
 	}

+ 36 - 4
src/views/courseManagement/components/ListView.vue

@@ -14,7 +14,26 @@
 			<template v-if="column.dataIndex === 'action'">
 				<a-button size="small" @click="handleDetail(record)" style="margin-right: 5px">详情</a-button>
 				<a-button size="small" @click="handleEdit(record)" style="margin-right: 5px">编辑</a-button>
-				<a-button size="small" @click="handleShelf(record)" style="margin-right: 5px">上架</a-button>
+				<a-popover v-model:visible="popoverVisible[record.courseId]" title="确定下架?" trigger="click">
+					<template #content>
+						<a-button style="margin-right: 10px" type="primary" @click="handleShelf(record,0)">确定
+						</a-button>
+						<a-button @click="()=>{	popoverVisible[record.courseId] = false}">取消</a-button>
+					</template>
+<!--					<a-button size="small" style="margin-right: 5px">选择</a-button>-->
+					<a-button v-if="record.putawayStatus == 1" size="small" style="margin-right: 5px">下架</a-button>
+				</a-popover>
+				<a-popover v-model:visible="popoverVisibles[record.courseId]" title="确定上架?" trigger="click">
+					<template #content>
+						<a-button style="margin-right: 10px" type="primary" @click="handleShelf(record,1)">确定
+						</a-button>
+						<a-button @click="()=>{	popoverVisibles[record.courseId] = false}">取消</a-button>
+					</template>
+					<!--					<a-button size="small" style="margin-right: 5px">选择</a-button>-->
+					<a-button v-if="record.putawayStatus == 0" size="small"  style="margin-right: 5px">上架</a-button>
+				</a-popover>
+
+
 				<a-button size="small" @click="handleDelete(record)" style="margin-right: 5px">删除</a-button>
 			</template>
 		</template>
@@ -37,6 +56,7 @@ import {EyeOutlined, EditOutlined, SnippetsOutlined, DeleteOutlined} from '@ant-
 import {list} from '@/api/courseinfo'
 import {useRouter} from 'vue-router'
 import collegeApi from '@/api/college'
+import {updateCourseStatus} from '@/api/course/courseDetail'
 
 const router = useRouter()
 
@@ -45,6 +65,8 @@ const emit = defineEmits(['handleEdit'])
 const releaseVisible = ref(false)
 const loading = ref(false) // 列表loading
 const dataSources = ref([])
+const popoverVisible = ref({})
+const popoverVisibles = ref({})
 const formState = ref({
 	name: '',
 	loacl: ''
@@ -63,8 +85,8 @@ const columns = [
 		width: '10%'
 	},
 	{
-		title: '院系类型',
-		dataIndex: 'collegeAllIdName',
+		title: '院系',
+		dataIndex: 'collegeTwoIdName',
 		sorter: true,
 		width: '25%'
 	},
@@ -135,9 +157,19 @@ const handleEdit = (record) => {
 }
 
 // 上架按钮点击事件
-const handleShelf = (record) => {
+const handleShelf = (record,num) => {
 	console.log('上架记录', record)
+	popoverVisible.value[record.courseId] = false
+	popoverVisibles.value[record.courseId] = false
 	// 在这里添加上架记录的逻辑
+	// {
+	// 	"courseId": "1948183431150227458",
+	// 	"putawayStatus": 1
+	// }
+	updateCourseStatus({courseId : record.courseId,putawayStatus : num}).then((res)=>{
+		getList()
+	})
+
 }
 
 // 删除按钮点击事件

+ 43 - 14
src/views/courseManagement/components/QueryView.vue

@@ -2,20 +2,27 @@
 	<div style="display: flex; justify-content: space-between; align-items: center">
 		<div>
 			<a-form layout="inline" :model="formState">
-				<a-form-item label="" style="width: 200px">
+				<a-form-item label="" style="width: 20%">
 					<a-input v-model:value="formState.courseName" placeholder="请输入课程名称" allowClear />
 				</a-form-item>
-				<a-form-item label="" style="width: 200px">
-					<a-cascader
-						v-model:value="formState.loacl"
-						:options="options"
-						placeholder="选择院校/专业"
-						change-on-select
+				<a-form-item label="" style="width: 15%">
+<!--					<a-cascader-->
+<!--						v-model:value="formState.loacl"-->
+<!--						:options="options"-->
+<!--						placeholder="选择院校"-->
+<!--						change-on-select-->
+<!--						allowClear-->
+<!--						:field-names="{ label: 'name', value: 'id', children: 'children' }"-->
+<!--					/>-->
+					<a-select
+						v-model:value="formState.collegeTwoId"
+						:fieldNames="{ label: 'name', value: 'id' }"
+						:options="collegeMajorOptions"
+						placeholder="请选择院系"
 						allowClear
-						:field-names="{ label: 'name', value: 'id', children: 'children' }"
 					/>
 				</a-form-item>
-				<a-form-item label="" style="width: 200px">
+				<a-form-item label="" style="width: 20%">
 <!--					<a-input v-model:value="formState.type" placeholder="选择课程类型" allowClear />-->
 					<a-select
 						ref="select"
@@ -26,7 +33,7 @@
 						allowClear
 					></a-select>
 				</a-form-item>
-				<a-form-item label="" style="width: 300px">
+				<a-form-item label="" style="width: 30%">
 					<a-range-picker 	v-model:value="formState.time" allowClear />
 				</a-form-item>
 			</a-form>
@@ -49,9 +56,11 @@
 	import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
 	import { useRouter } from 'vue-router'
 	import collegeApi from '@/api/college'
+	import resourceAuditApi from '@/api/resourceAudit.js'
 	import tool from '@/utils/tool'
 	const emit = defineEmits([])
 	const router = useRouter()
+	const collegeMajorOptions = ref([]) //院系
 	//发布按钮状态
 	const releaseVisible = ref(false)
 	const loading = ref(false) // 列表loading
@@ -96,7 +105,18 @@
 
 	// 上传资源模态框
 	const uploadModalVisible = ref(false)
-
+	//院系组织查询
+	const getOrgTreeSelector = () => {
+		resourceAuditApi
+			.orgList()
+			.then((res) => {
+				console.log(res.data, '获取学院')
+				collegeMajorOptions.value = res.data
+			})
+			.catch((err) => {
+				console.log(err)
+			})
+	}
 	const loadData = (selectedOptions) => {
 		const targetOption = selectedOptions[selectedOptions.length - 1]
 		targetOption.loading = true
@@ -118,9 +138,18 @@
 		}, 1000)
 	}
 	const getList = () => {
-		collegeApi.treeAll().then((data) => {
-			options.value = data
-		})
+		// collegeApi.treeAll().then((data) => {
+		// 	options.value = data
+		// })
+
+		resourceAuditApi
+			.orgList()
+			.then((res) => {
+				collegeMajorOptions.value = res.data
+			})
+			.catch((err) => {
+				console.log(err)
+			})
 	}
 	const handleSearch = () => {
 		console.log('执行查询操作', formState.value,COURSE_TYPE)

+ 77 - 0
src/views/slogin/callback.vue

@@ -0,0 +1,77 @@
+<template>
+	<div class="login_background">
+		<div class="login_background_front"></div>
+		<div class="login_main">
+			<div class="login-form">
+				<a-card>
+					<div class="login-header">
+						<h2>三方登录</h2>
+					</div>
+					<a-spin tip="正在登录中...">
+						<div class="h-[300px]">
+							<a-skeleton />
+						</div>
+					</a-spin>
+				</a-card>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup name="loginCallback">
+	import { message } from 'ant-design-vue'
+	import tool from '@/utils/tool'
+	import router from '@/router'
+	import thirdApi from '@/api/auth/thirdApi'
+	import loginApi from '@/api/auth/loginApi'
+	import userCenterApi from '@/api/sys/userCenterApi'
+	import dictApi from '@/api/dev/dictApi'
+	import { onMounted } from 'vue'
+
+	onMounted(() => {
+		// 获取当前url
+		const url = new URL(window.location.href)
+		let argLength = 0
+		const params = {}
+		url.searchParams.forEach((value, key) => {
+			argLength += 1
+			params[key] = value
+		})
+		// 当然了,不可能只有一个参数
+		if (argLength < 2) {
+			window.location.href = '/login'
+			return
+		}
+
+		thirdApi
+			.thirdCallback(params)
+			.then((data) => {
+				tool.data.set('TOKEN', data)
+				// 获取登录的用户信息
+				loginApi.getLoginUser().then((loginUser) => {
+					tool.data.set('USER_INFO', loginUser)
+				})
+				userCenterApi.userLoginMenu().then((menu) => {
+					const indexMenu = menu[0].children[0].path
+					tool.data.set('MENU', menu)
+					// 重置系统默认应用
+					tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id)
+					router.replace({
+						path: indexMenu
+					})
+					message.success('登录成功')
+					dictApi.dictTree().then((dictData) => {
+						// 设置字典到store中
+						tool.data.set('DICT_TYPE_TREE_DATA', dictData)
+					})
+				})
+			})
+			.catch(() => {
+				window.location.href = '/login'
+			})
+	})
+</script>
+
+<style lang="less" scoped>
+	@import 'login';
+</style>

+ 152 - 0
src/views/slogin/login.less

@@ -0,0 +1,152 @@
+.login-icon-gray {
+  color: rgba(0, 0, 0, 0.25);
+}
+.login-validCode-img {
+  border: 1px solid var(--border-color-split);
+  cursor: pointer;
+  width: 100%;
+  height: 40px;
+}
+.login_background {
+  width: 100%;
+	min-height: 100vh;
+  overflow: hidden;
+  background-size: cover;
+  background-position: center;
+  background-image: url(/img/login_background.png);
+}
+.login_background_front {
+  width: 450px;
+  height: 450px;
+  margin-left: 100px;
+  margin-top: 15%;
+  overflow: hidden;
+  /*position: relative;*/
+  background-size: cover;
+  background-position: center;
+  background-image: url(/img/login_background_front.png);
+  animation-name: myfirst;
+  animation-duration: 5s;
+  animation-timing-function: linear;
+  animation-delay: 1s;
+  animation-iteration-count: infinite;
+  animation-direction: alternate;
+  animation-play-state: running;
+}
+@keyframes myfirst {
+  0% {
+    left: 0px;
+    top: 0px;
+  }
+  50% {
+    left: 50px;
+    top: 0px;
+  }
+  100% {
+    left: 0px;
+    top: 0px;
+  }
+}
+@-webkit-keyframes myfirst /* Safari and Chrome */ {
+  0% {
+    left: 0px;
+    top: 0px;
+  }
+  50% {
+    left: 50px;
+    top: 0px;
+  }
+  100% {
+    left: 0px;
+    top: 0px;
+  }
+}
+.login_adv__title h2 {
+  font-size: 40px;
+}
+.login_adv__title h4 {
+  font-size: 18px;
+  margin-top: 10px;
+  font-weight: normal;
+}
+.login_adv__title p {
+  font-size: 14px;
+  margin-top: 10px;
+  line-height: 1.8;
+  color: rgba(255, 255, 255, 0.6);
+}
+.login_adv__title div {
+  margin-top: 10px;
+  display: flex;
+  align-items: center;
+}
+.login_adv__title div span {
+  margin-right: 15px;
+}
+.login_adv__title div i {
+  font-size: 40px;
+}
+.login_adv__title div i.add {
+  font-size: 20px;
+  color: rgba(255, 255, 255, 0.6);
+}
+/*background-image:linear-gradient(transparent, #000);*/
+.login_main {
+  flex: 1;
+  overflow: auto;
+  display: flex;
+}
+.login-form {
+  top: 15%;
+  right: 15%;
+  position: absolute;
+  width: 450px;
+  margin-left: 10%;
+  margin-top: 20px;
+  padding: 10px 0;
+}
+.login-header {
+  margin-bottom: 20px;
+}
+.login-header .logo {
+  display: flex;
+  align-items: center;
+}
+.login-header .logo img {
+  width: 35px;
+  height: 35px;
+  vertical-align: bottom;
+  margin-right: 10px;
+}
+.login-header .logo label {
+  font-size: 24px;
+}
+.login-header h2 {
+  font-size: 24px;
+  font-weight: bold;
+  margin-top: 40px;
+}
+.login_config {
+  position: absolute;
+  top: 20px;
+  right: 20px;
+}
+@media (max-width: 1200px) {
+  .login-form {
+    width: 340px;
+  }
+}
+@media (max-width: 1000px) {
+  .login_main {
+    display: block;
+  }
+  .login_background_front {
+    display: none;
+  }
+  .login-form {
+    width: 100%;
+    padding: 20px 40px;
+    right: 0 !important;
+    top: 0 !important;
+  }
+}

+ 203 - 0
src/views/slogin/login.vue

@@ -0,0 +1,203 @@
+<template>
+	<div class="login_background">
+		<div style="display: flex;width: 100%; height: 100%; justify-content: center; align-items: center">
+				<a-card>
+					<div style="margin-bottom: 20px">
+						<span style="font-size: 20px;">学生登录</span>
+					</div>
+
+					<a-form ref="loginForm" :model="ruleForm" :rules="rules">
+						<a-form-item name="account">
+							<a-input
+								v-model:value="ruleForm.account"
+								:placeholder="$t('login.accountPlaceholder')"
+								size="large"
+								@keyup.enter="login"
+							>
+								<template #prefix>
+									<UserOutlined class="login-icon-gray" />
+								</template>
+							</a-input>
+						</a-form-item>
+						<a-form-item name="password">
+							<a-input-password
+								v-model:value="ruleForm.password"
+								:placeholder="$t('login.PWPlaceholder')"
+								size="large"
+								autocomplete="off"
+								@keyup.enter="login"
+							>
+								<template #prefix>
+									<LockOutlined class="login-icon-gray" />
+								</template>
+							</a-input-password>
+						</a-form-item>
+						<a-form-item name="validCode" v-if="captchaOpen === 'true'">
+							<a-row :gutter="8">
+								<a-col :span="17">
+									<a-input
+										v-model:value="ruleForm.validCode"
+										:placeholder="$t('login.validLaceholder')"
+										size="large"
+										@keyup.enter="login"
+									>
+										<template #prefix>
+											<verified-outlined class="login-icon-gray" />
+										</template>
+									</a-input>
+								</a-col>
+								<a-col :span="7">
+									<img :src="validCodeBase64" class="login-validCode-img" @click="loginCaptcha" />
+								</a-col>
+							</a-row>
+						</a-form-item>
+
+						<a-form-item>
+							<a-button style="position: absolute; width: 100%; buttom:0px   " type="primary"  :loading="loading" round size="large" @click="login"
+							>{{ $t('login.signIn') }}
+							</a-button>
+						</a-form-item>
+					</a-form>
+				</a-card>
+		</div>
+	</div>
+</template>
+
+<script>
+	import loginApi from '@/api/auth/loginApi'
+	import phoneLoginForm from './phoneLoginForm.vue'
+	import threeLogin from './threeLogin.vue'
+	import smCrypto from '@/utils/smCrypto'
+	import { required } from '@/utils/formRules'
+	import { afterLogin } from './util'
+	import config from '@/config'
+	import configApi from '@/api/dev/configApi'
+	import tool from '@/utils/tool'
+	import { globalStore, iframeStore, keepAliveStore, viewTagsStore } from '@/store'
+	import { mapActions, mapState } from 'pinia'
+
+	export default {
+		name: 'Login',
+		components: {
+			phoneLoginForm,
+			threeLogin
+		},
+		data() {
+			return {
+				activeKey: 'userAccount',
+				captchaOpen: config.SYS_BASE_CONFIG.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN,
+				validCodeBase64: '',
+				ruleForm: {
+					account: 'superAdmin',
+					password: '123456',
+					validCode: '',
+					validCodeReqNo: '',
+					autologin: false
+				},
+				rules: {
+					account: [required(this.$t('login.accountError'), 'blur')],
+					password: [required(this.$t('login.PWError'), 'blur')]
+				},
+				loading: false,
+				config: {
+					lang: tool.data.get('APP_LANG') || this.$CONFIG.LANG,
+					theme: tool.data.get('APP_THEME') || 'default'
+				},
+				lang: [
+					{
+						name: '简体中文',
+						value: 'zh-cn'
+					},
+					{
+						name: 'English',
+						value: 'en'
+					}
+				]
+			}
+		},
+		computed: {
+			...mapState(globalStore, ['sysBaseConfig']),
+		},
+		watch: {
+			'config.theme': function (val) {
+				document.body.setAttribute('data-theme', val)
+			},
+			'config.lang': function (val) {
+				this.$i18n.locale = val
+				tool.data.set('APP_LANG', val)
+			}
+		},
+		created() {
+			this.clearViewTags()
+			this.clearKeepLive()
+			this.clearIframeList()
+		},
+		mounted() {
+			let formData = ref(config.SYS_BASE_CONFIG)
+			configApi.configSysBaseList().then((data) => {
+				if (data) {
+					data.forEach((item) => {
+						formData.value[item.configKey] = item.configValue
+					})
+					this.captchaOpen = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN
+					tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
+					this.setSysBaseConfig(formData.value)
+					this.refreshSwitch()
+				}
+			})
+		},
+		methods: {
+			...mapActions(keepAliveStore, ['clearKeepLive']),
+			...mapActions(viewTagsStore, ['clearViewTags']),
+			...mapActions(iframeStore, ['clearIframeList']),
+			...mapActions(globalStore, ['setSysBaseConfig']),
+			// 通过开关加载内容
+			refreshSwitch() {
+				// 判断是否开启验证码
+				if (this.captchaOpen === 'true') {
+					// 加载验证码
+					this.loginCaptcha()
+					// 加入校验
+					this.rules.validCode = [required(this.$t('login.validError'), 'blur')]
+				}
+			},
+			// 获取验证码
+			loginCaptcha() {
+				loginApi.getPicCaptcha().then((data) => {
+					this.validCodeBase64 = data.validCodeBase64
+					this.ruleForm.validCodeReqNo = data.validCodeReqNo
+				})
+			},
+			// 用户名密码登录
+			async login() {
+				this.$refs.loginForm.validate().then(async () => {
+					this.loading = true
+					const loginData = {
+						account: this.ruleForm.account,
+						// 密码进行SM2加密,传输过程中看到的只有密文,后端存储使用hash
+						password: smCrypto.doSm2Encrypt(this.ruleForm.password),
+						validCode: this.ruleForm.validCode,
+						validCodeReqNo: this.ruleForm.validCodeReqNo,
+						//根据平台不同传不同参0后台(管理员)1老师2学生
+						eduIdentity : 2
+					}
+					// 获取token
+					try {
+						const loginToken = await loginApi.login(loginData)
+						afterLogin(loginToken)
+					} catch (err) {
+						this.loading = false
+						this.loginCaptcha()
+					}
+				})
+			},
+			configLang(key) {
+				this.config.lang = key
+			}
+		}
+	}
+</script>
+
+<style lang="less">
+	@import 'login';
+</style>

+ 168 - 0
src/views/slogin/phoneLoginForm.vue

@@ -0,0 +1,168 @@
+<template>
+	<a-form ref="phoneLoginFormRef" :model="phoneFormData" :rules="formRules">
+		<a-form-item name="phone">
+			<a-input v-model:value="phoneFormData.phone" :placeholder="$t('login.phonePlaceholder')" size="large">
+				<template #prefix>
+					<mobile-outlined class="text-black text-opacity-25" />
+				</template>
+			</a-input>
+		</a-form-item>
+		<a-form-item name="phoneValidCode">
+			<a-row :gutter="8">
+				<a-col :span="17">
+					<a-input
+						v-model:value="phoneFormData.phoneValidCode"
+						:placeholder="$t('login.smsCodePlaceholder')"
+						size="large"
+					>
+						<template #prefix>
+							<mail-outlined class="text-black text-opacity-25" />
+						</template>
+					</a-input>
+				</a-col>
+				<a-col :span="7">
+					<a-button size="large" style="width: 100%" @click="getPhoneValidCode" :disabled="state.smsSendBtn">
+						{{ (!state.smsSendBtn && $t('login.getSmsCode')) || state.time + ' s' }}
+					</a-button>
+				</a-col>
+			</a-row>
+		</a-form-item>
+		<a-form-item>
+			<a-button type="primary" style="width: 100%" :loading="loading" round size="large" @click="submitLogin">
+				{{ $t('login.signIn') }}
+			</a-button>
+		</a-form-item>
+	</a-form>
+	<a-modal
+		v-model:visible="visible"
+		:width="400"
+		:title="$t('login.machineValidation')"
+		@cancel="handleCancel"
+		@ok="handleOk"
+	>
+		<a-form ref="phoneLoginFormModalRef" :model="phoneFormModalData" :rules="formModalRules">
+			<a-form-item name="validCode">
+				<a-row :gutter="8">
+					<a-col :span="17">
+						<a-input
+							v-model:value="phoneFormModalData.validCode"
+							:placeholder="$t('login.validLaceholder')"
+							size="large"
+						>
+							<template #prefix>
+								<verified-outlined class="text-black text-opacity-25" />
+							</template>
+						</a-input>
+					</a-col>
+					<a-col :span="7">
+						<img
+							:src="validCodeBase64"
+							style="border: 1px solid var(--border-color-split); cursor: pointer; width: 100%; height: 40px"
+							@click="getPhonePicCaptcha"
+						/>
+					</a-col>
+				</a-row>
+			</a-form-item>
+		</a-form>
+	</a-modal>
+</template>
+
+<script setup name="smsLoginForm">
+	import { message } from 'ant-design-vue'
+	import { required, rules } from '@/utils/formRules'
+	import loginApi from '@/api/auth/loginApi'
+	import { afterLogin } from './util'
+
+	const phoneLoginFormRef = ref()
+	const phoneFormData = ref({})
+	const loading = ref(false)
+	let state = ref({
+		time: 60,
+		smsSendBtn: false
+	})
+	let formRules = ref({})
+	const phoneValidCodeReqNo = ref('')
+
+	// 点击获取短信验证码
+	const getPhoneValidCode = () => {
+		formRules.value.phone = [required('请输入11位手机号'), rules.phone]
+		delete formRules.value.phoneValidCode
+		phoneLoginFormRef.value.validate().then(() => {
+			// 显示弹框
+			visible.value = true
+			// 获取内部图片验证码
+			getPhonePicCaptcha()
+		})
+	}
+	// 点击登录按钮
+	const submitLogin = async () => {
+		formRules.value.phone = [required('请输入11位手机号'), rules.phone]
+		formRules.value.phoneValidCode = [required('请输入短信验证码'), rules.number]
+
+		const validate = await phoneLoginFormRef.value.validate().catch(() => {})
+		if (!validate) return false
+
+		phoneFormData.value.validCode = phoneFormData.value.phoneValidCode
+		// delete phoneFormData.value.phoneValidCode
+		phoneFormData.value.validCodeReqNo = phoneValidCodeReqNo.value
+
+		loading.value = true
+		try {
+			const token = await loginApi.loginByPhone(phoneFormData.value)
+			afterLogin(token)
+		} catch (err) {
+			loading.value = false
+		}
+	}
+
+	// 弹框的
+	const visible = ref(false)
+	const phoneLoginFormModalRef = ref()
+	const phoneFormModalData = ref({})
+	const validCodeBase64 = ref('')
+	const validCodeReqNo = ref('')
+	const formModalRules = {
+		validCode: [required('请输入图形验证码'), rules.lettersNum]
+	}
+	const getPhonePicCaptcha = () => {
+		loginApi.getPicCaptcha().then((data) => {
+			validCodeBase64.value = data.validCodeBase64
+			phoneFormModalData.value.validCodeReqNo = data.validCodeReqNo
+		})
+	}
+	const handleCancel = () => {
+		visible.value = false
+	}
+	const handleOk = () => {
+		// 获取到里面的验证码,并发送短信
+		phoneLoginFormModalRef.value.validate().then(() => {
+			visible.value = false
+			// 发送短信,首先拿到刚刚输入的手机号
+			phoneFormModalData.value.phone = phoneFormData.value.phone
+			// 禁用发送按钮,并设置为倒计时
+			state.value.smsSendBtn = true
+			const interval = window.setInterval(() => {
+				if (state.value.time-- <= 0) {
+					state.value.time = 60
+					state.value.smsSendBtn = false
+					window.clearInterval(interval)
+				}
+			}, 1000)
+			const hide = message.loading('验证码发送中..', 0)
+
+			loginApi
+				.getPhoneValidCode(phoneFormModalData.value)
+				.then((data) => {
+					phoneValidCodeReqNo.value = data
+					visible.value = false
+					setTimeout(hide, 500)
+					phoneFormModalData.value.validCode = ''
+				})
+				.catch(() => {
+					setTimeout(hide, 100)
+					clearInterval(interval)
+					state.value.smsSendBtn = false
+				})
+		})
+	}
+</script>

+ 24 - 0
src/views/slogin/threeLogin.vue

@@ -0,0 +1,24 @@
+<template>
+	<a-divider>{{ $t('login.signInOther') }}</a-divider>
+	<div class="login-oauth layout-center">
+		<a-space align="start">
+			<a @click="getLoginRenderUrl('gitee')"><GiteeIcon /></a>
+			<a-button type="primary" shape="circle">
+				<wechat-filled />
+			</a-button>
+		</a-space>
+	</div>
+</template>
+
+<script setup name="threeLogin">
+	import thirdApi from '@/api/auth/thirdApi'
+
+	const getLoginRenderUrl = (platform) => {
+		const param = {
+			platform: platform
+		}
+		thirdApi.thirdRender(param).then((data) => {
+			window.location.href = data.authorizeUrl
+		})
+	}
+</script>

+ 56 - 0
src/views/slogin/util.js

@@ -0,0 +1,56 @@
+import loginApi from '@/api/auth/loginApi'
+import userCenterApi from '@/api/sys/userCenterApi'
+import dictApi from '@/api/dev/dictApi'
+import router from '@/router'
+import tool from '@/utils/tool'
+import { message } from 'ant-design-vue'
+import { useGlobalStore, useMyResourceStore } from '@/store'
+import routerUtil from '@/utils/routerUtil'
+
+export const afterLogin = async (loginToken) => {
+	tool.data.set('TOKEN', loginToken)
+	//cookie里添加 token
+	tool.cookie.set('Token', loginToken)
+	// 获取登录的用户信息
+	const loginUser = await loginApi.getLoginUser()
+	const globalStore = useGlobalStore()
+	const myResourceStore = useMyResourceStore()
+	globalStore.setUserInfo(loginUser)
+	myResourceStore.getUserInfo()
+	tool.data.set('USER_INFO', loginUser)
+
+	// 获取用户的菜单
+	const menu = await userCenterApi.userLoginMenu()
+	let indexMenu = routerUtil.getIndexMenu(menu).path
+	tool.data.set('MENU', menu)
+	// 重置系统默认应用
+	tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id)
+	message.success('登录成功')
+	if (!!tool.data.get('LAST_VIEWS_PATH')) {
+		// 如果有缓存,将其登录跳转到最后访问的路由
+		indexMenu = tool.data.get('LAST_VIEWS_PATH')
+	}
+	// 如果存在退出后换新账号登录,进行重新匹配,匹配无果则默认首页
+	if (menu) {
+		let routerTag = 0
+		menu.forEach((item) => {
+			if (item.children) {
+				if (JSON.stringify(item.children).indexOf(indexMenu) > -1) {
+					routerTag++
+				}
+			}
+		})
+		if (routerTag === 0) {
+			// 取首页
+			indexMenu = routerUtil.getIndexMenu(menu).path
+		}
+	}
+	dictApi.dictTree().then((data) => {
+		// 设置字典到store中
+		tool.data.set('DICT_TYPE_TREE_DATA', data)
+	})
+	await router.replace({
+		path: "portal"
+	})
+	location.reload()
+}

+ 77 - 0
src/views/tlogin/callback.vue

@@ -0,0 +1,77 @@
+<template>
+	<div class="login_background">
+		<div class="login_background_front"></div>
+		<div class="login_main">
+			<div class="login-form">
+				<a-card>
+					<div class="login-header">
+						<h2>三方登录</h2>
+					</div>
+					<a-spin tip="正在登录中...">
+						<div class="h-[300px]">
+							<a-skeleton />
+						</div>
+					</a-spin>
+				</a-card>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup name="loginCallback">
+	import { message } from 'ant-design-vue'
+	import tool from '@/utils/tool'
+	import router from '@/router'
+	import thirdApi from '@/api/auth/thirdApi'
+	import loginApi from '@/api/auth/loginApi'
+	import userCenterApi from '@/api/sys/userCenterApi'
+	import dictApi from '@/api/dev/dictApi'
+	import { onMounted } from 'vue'
+
+	onMounted(() => {
+		// 获取当前url
+		const url = new URL(window.location.href)
+		let argLength = 0
+		const params = {}
+		url.searchParams.forEach((value, key) => {
+			argLength += 1
+			params[key] = value
+		})
+		// 当然了,不可能只有一个参数
+		if (argLength < 2) {
+			window.location.href = '/login'
+			return
+		}
+
+		thirdApi
+			.thirdCallback(params)
+			.then((data) => {
+				tool.data.set('TOKEN', data)
+				// 获取登录的用户信息
+				loginApi.getLoginUser().then((loginUser) => {
+					tool.data.set('USER_INFO', loginUser)
+				})
+				userCenterApi.userLoginMenu().then((menu) => {
+					const indexMenu = menu[0].children[0].path
+					tool.data.set('MENU', menu)
+					// 重置系统默认应用
+					tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id)
+					router.replace({
+						path: indexMenu
+					})
+					message.success('登录成功')
+					dictApi.dictTree().then((dictData) => {
+						// 设置字典到store中
+						tool.data.set('DICT_TYPE_TREE_DATA', dictData)
+					})
+				})
+			})
+			.catch(() => {
+				window.location.href = '/login'
+			})
+	})
+</script>
+
+<style lang="less" scoped>
+	@import 'login';
+</style>

+ 152 - 0
src/views/tlogin/login.less

@@ -0,0 +1,152 @@
+.login-icon-gray {
+  color: rgba(0, 0, 0, 0.25);
+}
+.login-validCode-img {
+  border: 1px solid var(--border-color-split);
+  cursor: pointer;
+  width: 100%;
+  height: 40px;
+}
+.login_background {
+  width: 100%;
+	min-height: 100vh;
+  overflow: hidden;
+  background-size: cover;
+  background-position: center;
+  background-image: url(/img/login_background.png);
+}
+.login_background_front {
+  width: 450px;
+  height: 450px;
+  margin-left: 100px;
+  margin-top: 15%;
+  overflow: hidden;
+  /*position: relative;*/
+  background-size: cover;
+  background-position: center;
+  background-image: url(/img/login_background_front.png);
+  animation-name: myfirst;
+  animation-duration: 5s;
+  animation-timing-function: linear;
+  animation-delay: 1s;
+  animation-iteration-count: infinite;
+  animation-direction: alternate;
+  animation-play-state: running;
+}
+@keyframes myfirst {
+  0% {
+    left: 0px;
+    top: 0px;
+  }
+  50% {
+    left: 50px;
+    top: 0px;
+  }
+  100% {
+    left: 0px;
+    top: 0px;
+  }
+}
+@-webkit-keyframes myfirst /* Safari and Chrome */ {
+  0% {
+    left: 0px;
+    top: 0px;
+  }
+  50% {
+    left: 50px;
+    top: 0px;
+  }
+  100% {
+    left: 0px;
+    top: 0px;
+  }
+}
+.login_adv__title h2 {
+  font-size: 40px;
+}
+.login_adv__title h4 {
+  font-size: 18px;
+  margin-top: 10px;
+  font-weight: normal;
+}
+.login_adv__title p {
+  font-size: 14px;
+  margin-top: 10px;
+  line-height: 1.8;
+  color: rgba(255, 255, 255, 0.6);
+}
+.login_adv__title div {
+  margin-top: 10px;
+  display: flex;
+  align-items: center;
+}
+.login_adv__title div span {
+  margin-right: 15px;
+}
+.login_adv__title div i {
+  font-size: 40px;
+}
+.login_adv__title div i.add {
+  font-size: 20px;
+  color: rgba(255, 255, 255, 0.6);
+}
+/*background-image:linear-gradient(transparent, #000);*/
+.login_main {
+  flex: 1;
+  overflow: auto;
+  display: flex;
+}
+.login-form {
+  top: 15%;
+  right: 15%;
+  position: absolute;
+  width: 450px;
+  margin-left: 10%;
+  margin-top: 20px;
+  padding: 10px 0;
+}
+.login-header {
+  margin-bottom: 20px;
+}
+.login-header .logo {
+  display: flex;
+  align-items: center;
+}
+.login-header .logo img {
+  width: 35px;
+  height: 35px;
+  vertical-align: bottom;
+  margin-right: 10px;
+}
+.login-header .logo label {
+  font-size: 24px;
+}
+.login-header h2 {
+  font-size: 24px;
+  font-weight: bold;
+  margin-top: 40px;
+}
+.login_config {
+  position: absolute;
+  top: 20px;
+  right: 20px;
+}
+@media (max-width: 1200px) {
+  .login-form {
+    width: 340px;
+  }
+}
+@media (max-width: 1000px) {
+  .login_main {
+    display: block;
+  }
+  .login_background_front {
+    display: none;
+  }
+  .login-form {
+    width: 100%;
+    padding: 20px 40px;
+    right: 0 !important;
+    top: 0 !important;
+  }
+}

+ 203 - 0
src/views/tlogin/login.vue

@@ -0,0 +1,203 @@
+<template>
+	<div class="login_background">
+		<div style="display: flex;width: 100%; height: 100%; justify-content: center; align-items: center">
+				<a-card>
+					<div style="margin-bottom: 20px">
+						<span style="font-size: 20px;">教师登录</span>
+					</div>
+
+					<a-form ref="loginForm" :model="ruleForm" :rules="rules">
+						<a-form-item name="account">
+							<a-input
+								v-model:value="ruleForm.account"
+								:placeholder="$t('login.accountPlaceholder')"
+								size="large"
+								@keyup.enter="login"
+							>
+								<template #prefix>
+									<UserOutlined class="login-icon-gray" />
+								</template>
+							</a-input>
+						</a-form-item>
+						<a-form-item name="password">
+							<a-input-password
+								v-model:value="ruleForm.password"
+								:placeholder="$t('login.PWPlaceholder')"
+								size="large"
+								autocomplete="off"
+								@keyup.enter="login"
+							>
+								<template #prefix>
+									<LockOutlined class="login-icon-gray" />
+								</template>
+							</a-input-password>
+						</a-form-item>
+						<a-form-item name="validCode" v-if="captchaOpen === 'true'">
+							<a-row :gutter="8">
+								<a-col :span="17">
+									<a-input
+										v-model:value="ruleForm.validCode"
+										:placeholder="$t('login.validLaceholder')"
+										size="large"
+										@keyup.enter="login"
+									>
+										<template #prefix>
+											<verified-outlined class="login-icon-gray" />
+										</template>
+									</a-input>
+								</a-col>
+								<a-col :span="7">
+									<img :src="validCodeBase64" class="login-validCode-img" @click="loginCaptcha" />
+								</a-col>
+							</a-row>
+						</a-form-item>
+
+						<a-form-item>
+							<a-button style="position: absolute; width: 100%; buttom:0px   " type="primary"  :loading="loading" round size="large" @click="login"
+							>{{ $t('login.signIn') }}
+							</a-button>
+						</a-form-item>
+					</a-form>
+				</a-card>
+		</div>
+	</div>
+</template>
+
+<script>
+	import loginApi from '@/api/auth/loginApi'
+	import phoneLoginForm from './phoneLoginForm.vue'
+	import threeLogin from './threeLogin.vue'
+	import smCrypto from '@/utils/smCrypto'
+	import { required } from '@/utils/formRules'
+	import { afterLogin } from './util'
+	import config from '@/config'
+	import configApi from '@/api/dev/configApi'
+	import tool from '@/utils/tool'
+	import { globalStore, iframeStore, keepAliveStore, viewTagsStore } from '@/store'
+	import { mapActions, mapState } from 'pinia'
+
+	export default {
+		name: 'Login',
+		components: {
+			phoneLoginForm,
+			threeLogin
+		},
+		data() {
+			return {
+				activeKey: 'userAccount',
+				captchaOpen: config.SYS_BASE_CONFIG.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN,
+				validCodeBase64: '',
+				ruleForm: {
+					account: 'superAdmin',
+					password: '123456',
+					validCode: '',
+					validCodeReqNo: '',
+					autologin: false
+				},
+				rules: {
+					account: [required(this.$t('login.accountError'), 'blur')],
+					password: [required(this.$t('login.PWError'), 'blur')]
+				},
+				loading: false,
+				config: {
+					lang: tool.data.get('APP_LANG') || this.$CONFIG.LANG,
+					theme: tool.data.get('APP_THEME') || 'default'
+				},
+				lang: [
+					{
+						name: '简体中文',
+						value: 'zh-cn'
+					},
+					{
+						name: 'English',
+						value: 'en'
+					}
+				]
+			}
+		},
+		computed: {
+			...mapState(globalStore, ['sysBaseConfig']),
+		},
+		watch: {
+			'config.theme': function (val) {
+				document.body.setAttribute('data-theme', val)
+			},
+			'config.lang': function (val) {
+				this.$i18n.locale = val
+				tool.data.set('APP_LANG', val)
+			}
+		},
+		created() {
+			this.clearViewTags()
+			this.clearKeepLive()
+			this.clearIframeList()
+		},
+		mounted() {
+			let formData = ref(config.SYS_BASE_CONFIG)
+			configApi.configSysBaseList().then((data) => {
+				if (data) {
+					data.forEach((item) => {
+						formData.value[item.configKey] = item.configValue
+					})
+					this.captchaOpen = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN
+					tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
+					this.setSysBaseConfig(formData.value)
+					this.refreshSwitch()
+				}
+			})
+		},
+		methods: {
+			...mapActions(keepAliveStore, ['clearKeepLive']),
+			...mapActions(viewTagsStore, ['clearViewTags']),
+			...mapActions(iframeStore, ['clearIframeList']),
+			...mapActions(globalStore, ['setSysBaseConfig']),
+			// 通过开关加载内容
+			refreshSwitch() {
+				// 判断是否开启验证码
+				if (this.captchaOpen === 'true') {
+					// 加载验证码
+					this.loginCaptcha()
+					// 加入校验
+					this.rules.validCode = [required(this.$t('login.validError'), 'blur')]
+				}
+			},
+			// 获取验证码
+			loginCaptcha() {
+				loginApi.getPicCaptcha().then((data) => {
+					this.validCodeBase64 = data.validCodeBase64
+					this.ruleForm.validCodeReqNo = data.validCodeReqNo
+				})
+			},
+			// 用户名密码登录
+			async login() {
+				this.$refs.loginForm.validate().then(async () => {
+					this.loading = true
+					const loginData = {
+						account: this.ruleForm.account,
+						// 密码进行SM2加密,传输过程中看到的只有密文,后端存储使用hash
+						password: smCrypto.doSm2Encrypt(this.ruleForm.password),
+						validCode: this.ruleForm.validCode,
+						validCodeReqNo: this.ruleForm.validCodeReqNo,
+						//根据平台不同传不同参0后台(管理员)1老师2学生
+						eduIdentity : 1
+					}
+					// 获取token
+					try {
+						const loginToken = await loginApi.login(loginData)
+						afterLogin(loginToken)
+					} catch (err) {
+						this.loading = false
+						this.loginCaptcha()
+					}
+				})
+			},
+			configLang(key) {
+				this.config.lang = key
+			}
+		}
+	}
+</script>
+
+<style lang="less">
+	@import 'login';
+</style>

+ 168 - 0
src/views/tlogin/phoneLoginForm.vue

@@ -0,0 +1,168 @@
+<template>
+	<a-form ref="phoneLoginFormRef" :model="phoneFormData" :rules="formRules">
+		<a-form-item name="phone">
+			<a-input v-model:value="phoneFormData.phone" :placeholder="$t('login.phonePlaceholder')" size="large">
+				<template #prefix>
+					<mobile-outlined class="text-black text-opacity-25" />
+				</template>
+			</a-input>
+		</a-form-item>
+		<a-form-item name="phoneValidCode">
+			<a-row :gutter="8">
+				<a-col :span="17">
+					<a-input
+						v-model:value="phoneFormData.phoneValidCode"
+						:placeholder="$t('login.smsCodePlaceholder')"
+						size="large"
+					>
+						<template #prefix>
+							<mail-outlined class="text-black text-opacity-25" />
+						</template>
+					</a-input>
+				</a-col>
+				<a-col :span="7">
+					<a-button size="large" style="width: 100%" @click="getPhoneValidCode" :disabled="state.smsSendBtn">
+						{{ (!state.smsSendBtn && $t('login.getSmsCode')) || state.time + ' s' }}
+					</a-button>
+				</a-col>
+			</a-row>
+		</a-form-item>
+		<a-form-item>
+			<a-button type="primary" style="width: 100%" :loading="loading" round size="large" @click="submitLogin">
+				{{ $t('login.signIn') }}
+			</a-button>
+		</a-form-item>
+	</a-form>
+	<a-modal
+		v-model:visible="visible"
+		:width="400"
+		:title="$t('login.machineValidation')"
+		@cancel="handleCancel"
+		@ok="handleOk"
+	>
+		<a-form ref="phoneLoginFormModalRef" :model="phoneFormModalData" :rules="formModalRules">
+			<a-form-item name="validCode">
+				<a-row :gutter="8">
+					<a-col :span="17">
+						<a-input
+							v-model:value="phoneFormModalData.validCode"
+							:placeholder="$t('login.validLaceholder')"
+							size="large"
+						>
+							<template #prefix>
+								<verified-outlined class="text-black text-opacity-25" />
+							</template>
+						</a-input>
+					</a-col>
+					<a-col :span="7">
+						<img
+							:src="validCodeBase64"
+							style="border: 1px solid var(--border-color-split); cursor: pointer; width: 100%; height: 40px"
+							@click="getPhonePicCaptcha"
+						/>
+					</a-col>
+				</a-row>
+			</a-form-item>
+		</a-form>
+	</a-modal>
+</template>
+
+<script setup name="smsLoginForm">
+	import { message } from 'ant-design-vue'
+	import { required, rules } from '@/utils/formRules'
+	import loginApi from '@/api/auth/loginApi'
+	import { afterLogin } from './util'
+
+	const phoneLoginFormRef = ref()
+	const phoneFormData = ref({})
+	const loading = ref(false)
+	let state = ref({
+		time: 60,
+		smsSendBtn: false
+	})
+	let formRules = ref({})
+	const phoneValidCodeReqNo = ref('')
+
+	// 点击获取短信验证码
+	const getPhoneValidCode = () => {
+		formRules.value.phone = [required('请输入11位手机号'), rules.phone]
+		delete formRules.value.phoneValidCode
+		phoneLoginFormRef.value.validate().then(() => {
+			// 显示弹框
+			visible.value = true
+			// 获取内部图片验证码
+			getPhonePicCaptcha()
+		})
+	}
+	// 点击登录按钮
+	const submitLogin = async () => {
+		formRules.value.phone = [required('请输入11位手机号'), rules.phone]
+		formRules.value.phoneValidCode = [required('请输入短信验证码'), rules.number]
+
+		const validate = await phoneLoginFormRef.value.validate().catch(() => {})
+		if (!validate) return false
+
+		phoneFormData.value.validCode = phoneFormData.value.phoneValidCode
+		// delete phoneFormData.value.phoneValidCode
+		phoneFormData.value.validCodeReqNo = phoneValidCodeReqNo.value
+
+		loading.value = true
+		try {
+			const token = await loginApi.loginByPhone(phoneFormData.value)
+			afterLogin(token)
+		} catch (err) {
+			loading.value = false
+		}
+	}
+
+	// 弹框的
+	const visible = ref(false)
+	const phoneLoginFormModalRef = ref()
+	const phoneFormModalData = ref({})
+	const validCodeBase64 = ref('')
+	const validCodeReqNo = ref('')
+	const formModalRules = {
+		validCode: [required('请输入图形验证码'), rules.lettersNum]
+	}
+	const getPhonePicCaptcha = () => {
+		loginApi.getPicCaptcha().then((data) => {
+			validCodeBase64.value = data.validCodeBase64
+			phoneFormModalData.value.validCodeReqNo = data.validCodeReqNo
+		})
+	}
+	const handleCancel = () => {
+		visible.value = false
+	}
+	const handleOk = () => {
+		// 获取到里面的验证码,并发送短信
+		phoneLoginFormModalRef.value.validate().then(() => {
+			visible.value = false
+			// 发送短信,首先拿到刚刚输入的手机号
+			phoneFormModalData.value.phone = phoneFormData.value.phone
+			// 禁用发送按钮,并设置为倒计时
+			state.value.smsSendBtn = true
+			const interval = window.setInterval(() => {
+				if (state.value.time-- <= 0) {
+					state.value.time = 60
+					state.value.smsSendBtn = false
+					window.clearInterval(interval)
+				}
+			}, 1000)
+			const hide = message.loading('验证码发送中..', 0)
+
+			loginApi
+				.getPhoneValidCode(phoneFormModalData.value)
+				.then((data) => {
+					phoneValidCodeReqNo.value = data
+					visible.value = false
+					setTimeout(hide, 500)
+					phoneFormModalData.value.validCode = ''
+				})
+				.catch(() => {
+					setTimeout(hide, 100)
+					clearInterval(interval)
+					state.value.smsSendBtn = false
+				})
+		})
+	}
+</script>

+ 24 - 0
src/views/tlogin/threeLogin.vue

@@ -0,0 +1,24 @@
+<template>
+	<a-divider>{{ $t('login.signInOther') }}</a-divider>
+	<div class="login-oauth layout-center">
+		<a-space align="start">
+			<a @click="getLoginRenderUrl('gitee')"><GiteeIcon /></a>
+			<a-button type="primary" shape="circle">
+				<wechat-filled />
+			</a-button>
+		</a-space>
+	</div>
+</template>
+
+<script setup name="threeLogin">
+	import thirdApi from '@/api/auth/thirdApi'
+
+	const getLoginRenderUrl = (platform) => {
+		const param = {
+			platform: platform
+		}
+		thirdApi.thirdRender(param).then((data) => {
+			window.location.href = data.authorizeUrl
+		})
+	}
+</script>

+ 56 - 0
src/views/tlogin/util.js

@@ -0,0 +1,56 @@
+import loginApi from '@/api/auth/loginApi'
+import userCenterApi from '@/api/sys/userCenterApi'
+import dictApi from '@/api/dev/dictApi'
+import router from '@/router'
+import tool from '@/utils/tool'
+import { message } from 'ant-design-vue'
+import { useGlobalStore, useMyResourceStore } from '@/store'
+import routerUtil from '@/utils/routerUtil'
+
+export const afterLogin = async (loginToken) => {
+	tool.data.set('TOKEN', loginToken)
+	//cookie里添加 token
+	tool.cookie.set('Token', loginToken)
+	// 获取登录的用户信息
+	const loginUser = await loginApi.getLoginUser()
+	const globalStore = useGlobalStore()
+	const myResourceStore = useMyResourceStore()
+	globalStore.setUserInfo(loginUser)
+	myResourceStore.getUserInfo()
+	tool.data.set('USER_INFO', loginUser)
+
+	// 获取用户的菜单
+	const menu = await userCenterApi.userLoginMenu()
+	let indexMenu = routerUtil.getIndexMenu(menu).path
+	tool.data.set('MENU', menu)
+	// 重置系统默认应用
+	tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id)
+	message.success('登录成功')
+	if (!!tool.data.get('LAST_VIEWS_PATH')) {
+		// 如果有缓存,将其登录跳转到最后访问的路由
+		indexMenu = tool.data.get('LAST_VIEWS_PATH')
+	}
+	// 如果存在退出后换新账号登录,进行重新匹配,匹配无果则默认首页
+	if (menu) {
+		let routerTag = 0
+		menu.forEach((item) => {
+			if (item.children) {
+				if (JSON.stringify(item.children).indexOf(indexMenu) > -1) {
+					routerTag++
+				}
+			}
+		})
+		if (routerTag === 0) {
+			// 取首页
+			indexMenu = routerUtil.getIndexMenu(menu).path
+		}
+	}
+	dictApi.dictTree().then((data) => {
+		// 设置字典到store中
+		tool.data.set('DICT_TYPE_TREE_DATA', data)
+	})
+	await router.replace({
+		path: "portal"
+	})
+	location.reload()
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
stats.html


+ 1 - 1
vite.config.js

@@ -43,7 +43,7 @@ export default defineConfig(({ command, mode }) => {
 			port: envConfig.VITE_PORT,
 			proxy: {
 				'/api': {
-					target: 'http://192.168.31.80:19003',
+					target: 'http://192.168.1.245:9003',
 					ws: false,
 					changeOrigin: true
 					// rewrite: (path) => path.replace(/^\/api/, '')

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.