Compare commits
4 Commits
2d76ed507e
...
5ebf40101d
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ebf40101d | |||
| 5858bf2ecc | |||
| bddf6c0936 | |||
| 340bc5b5e3 |
@@ -24,24 +24,12 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
.back();
|
||||
|
||||
SaRouter.match("/**")
|
||||
.notMatch("/class/list")
|
||||
.notMatch("/exam/words/get")
|
||||
.notMatch("/exam/words/detail")
|
||||
.notMatch("/exam/words/student/history")
|
||||
.notMatch("grade/list")
|
||||
.notMatch("/student/list")
|
||||
.notMatch("/student/detail")
|
||||
.notMatch("/studentLessonPlans/list")
|
||||
.notMatch("/studentLessonPlans/history")
|
||||
.notMatch("/student/analyze")
|
||||
.notMatch("/student/mastery/detail")
|
||||
.notMatch("/unit/list")
|
||||
.notMatch("/vocabulary/list")
|
||||
.notMatch("/vocabulary/student/detail")
|
||||
.notMatch("/plan/download")
|
||||
.notMatch("/login/**")
|
||||
.check(r -> StpUtil.checkLogin());
|
||||
|
||||
SaRouter.match("/admin/**")
|
||||
.check(r -> StpUtil.checkRole("root"));
|
||||
|
||||
}))
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns("/error");
|
||||
|
||||
@@ -28,7 +28,11 @@ public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
return userToRole((Long) loginId) ;
|
||||
long l = 0L;
|
||||
if (loginId instanceof String loginIdStr) {
|
||||
l = Long.parseLong(loginIdStr);
|
||||
}
|
||||
return userToRole(l);
|
||||
}
|
||||
|
||||
private List<String> userToRole(Long userId) {
|
||||
|
||||
@@ -4,7 +4,13 @@ public class UserRedisConstants {
|
||||
|
||||
public static final String USER_LOGIN_CODE = "user:login:code:";
|
||||
|
||||
public static final String USER_INVITATION_CODE = "user:invitation:code:";
|
||||
|
||||
public static String buildUserLoginCode(String phone) {
|
||||
return USER_LOGIN_CODE + phone;
|
||||
}
|
||||
|
||||
public static String buildUserInvitationCode(String code) {
|
||||
return USER_INVITATION_CODE + code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.yinlihupo.enlish.service.controller;
|
||||
|
||||
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.RoleDO;
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||
import com.yinlihupo.enlish.service.model.vo.admin.CreateInvitationCodeReqVO;
|
||||
import com.yinlihupo.enlish.service.model.vo.admin.CreateInvitationCodeRspVO;
|
||||
import com.yinlihupo.enlish.service.model.vo.user.CreateUserReqVO;
|
||||
import com.yinlihupo.enlish.service.model.vo.user.FindUserListRepVO;
|
||||
import com.yinlihupo.enlish.service.model.vo.user.FindUserListRspVO;
|
||||
@@ -11,6 +14,7 @@ import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||
import com.yinlihupo.framework.common.response.PageResponse;
|
||||
import com.yinlihupo.framework.common.response.Response;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -19,6 +23,8 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RequestMapping("/admin/")
|
||||
@RestController
|
||||
@@ -30,6 +36,8 @@ public class AdminController {
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@Resource
|
||||
private RoleService roleService;
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@PostMapping("user/list")
|
||||
@ApiOperationLog(description = "查询用户列表")
|
||||
@@ -66,4 +74,18 @@ public class AdminController {
|
||||
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@PostMapping("user/create/invitation/code")
|
||||
@ApiOperationLog(description = "创建邀请码")
|
||||
public Response<CreateInvitationCodeRspVO> createInvitationCode(@RequestBody CreateInvitationCodeReqVO createInvitationCodeReqVO) {
|
||||
|
||||
String code = String.valueOf(new Random().nextInt(1000000));
|
||||
Integer limit = createInvitationCodeReqVO.getLimit();
|
||||
Integer expire = createInvitationCodeReqVO.getExpire();
|
||||
|
||||
redisTemplate.opsForValue().set(UserRedisConstants.buildUserInvitationCode(code), limit);
|
||||
redisTemplate.expire(UserRedisConstants.buildUserInvitationCode(code), expire, TimeUnit.DAYS);
|
||||
|
||||
return Response.success(CreateInvitationCodeRspVO.builder().invitationCode(code).build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public class LoginController {
|
||||
@ApiOperationLog(description = "登录")
|
||||
public Response<String> login(@RequestBody LoginReqVO loginReqVO) {
|
||||
try {
|
||||
loginService.login(loginReqVO.getPhone(), loginReqVO.getName(), loginReqVO.getPassword(), loginReqVO.getCode());
|
||||
loginService.login(loginReqVO.getPhone(), loginReqVO.getName(), loginReqVO.getPassword(), loginReqVO.getCode(), loginReqVO.getInvitationCode());
|
||||
return Response.success(StpUtil.getTokenInfo().getTokenValue());
|
||||
} catch (Exception e) {
|
||||
log.error("注册或登录失败 {}", e.getMessage());
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.yinlihupo.enlish.service.model.vo.admin;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class CreateInvitationCodeReqVO {
|
||||
|
||||
// 限制人数
|
||||
private Integer limit;
|
||||
// 有效期
|
||||
private Integer expire;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.yinlihupo.enlish.service.model.vo.admin;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class CreateInvitationCodeRspVO {
|
||||
|
||||
private String invitationCode;
|
||||
|
||||
}
|
||||
@@ -15,4 +15,5 @@ public class LoginReqVO {
|
||||
private String name;
|
||||
private String password;
|
||||
private String code;
|
||||
private String invitationCode;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.yinlihupo.enlish.service.service;
|
||||
|
||||
public interface LoginService {
|
||||
|
||||
void login(String phone, String name, String reqPassword, String reqCode);
|
||||
void login(String phone, String name, String reqPassword, String reqCode, String invitationCode);
|
||||
|
||||
void sendVerificationCode(String phone);
|
||||
}
|
||||
|
||||
@@ -13,11 +13,14 @@ import com.yinlihupo.enlish.service.service.LoginService;
|
||||
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -28,20 +31,32 @@ public class LoginServiceImpl implements LoginService {
|
||||
@Resource
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@Resource
|
||||
private Client client;
|
||||
|
||||
@Override
|
||||
public void login(String phone, String name, String reqPassword, String reqCode) {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void login(String phone, String name, String reqPassword, String reqCode, String invitationCode) {
|
||||
UserDO userDO = userDOMapper.selectByPhone(phone);
|
||||
log.info("userDO:{}", userDO);
|
||||
|
||||
String code = stringRedisTemplate.opsForValue().get(UserRedisConstants.buildUserLoginCode(phone));
|
||||
String code = JsonUtils.toJsonString(redisTemplate.opsForValue().get(UserRedisConstants.buildUserLoginCode(phone)));
|
||||
if (userDO == null) {
|
||||
if (code == null || !code.equals(reqCode)) {
|
||||
throw new RuntimeException("验证码错误");
|
||||
}
|
||||
|
||||
Object invitationObj = redisTemplate.opsForValue().get(UserRedisConstants.buildUserInvitationCode(invitationCode));
|
||||
if (invitationObj == null) {
|
||||
throw new RuntimeException("邀请码错误");
|
||||
}
|
||||
int invitationLimit = Integer.parseInt(JsonUtils.toJsonString(invitationObj));
|
||||
if (invitationLimit <= 0) {
|
||||
throw new RuntimeException("邀请码已使用完毕");
|
||||
}
|
||||
redisTemplate.opsForValue().set(UserRedisConstants.buildUserInvitationCode(invitationCode), invitationLimit - 1);
|
||||
|
||||
userDO = UserDO.builder()
|
||||
.phone(phone)
|
||||
.name(name)
|
||||
@@ -68,7 +83,7 @@ public class LoginServiceImpl implements LoginService {
|
||||
@Override
|
||||
public void sendVerificationCode(String phone) {
|
||||
String code = RandomUtil.randomNumbers(6);
|
||||
stringRedisTemplate.opsForValue().set(UserRedisConstants.buildUserLoginCode(phone), code, Duration.ofSeconds(60));
|
||||
redisTemplate.opsForValue().set(UserRedisConstants.buildUserLoginCode(phone), code, Duration.ofSeconds(60));
|
||||
String signName = "短信测试";
|
||||
String templateCode = "SMS_154950909";
|
||||
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
||||
|
||||
@@ -48,7 +48,8 @@ public class RoleServiceImpl implements RoleService {
|
||||
List<RoleDO> roleDOs = roleIds.stream().map(roleId2RoleDO::get).toList();
|
||||
List<String> user2RoleKeys = roleDOs.stream().map(RoleDO::getRoleKey).toList();
|
||||
log.info("将用户 {} 的角色同步到 redis 中, {}", userId, roleKeys);
|
||||
redisTemplate.opsForValue().set(RoleConstants.buildUserRoleKey(userId), JsonUtils.toJsonString(user2RoleKeys));
|
||||
// 不要使用 JsonUtils.toJsonString(user2RoleKeys); 会造成二次序列化
|
||||
redisTemplate.opsForValue().set(RoleConstants.buildUserRoleKey(userId), user2RoleKeys);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,3 +8,6 @@ export function createUser(data) {
|
||||
return axios.post('/admin/user/create', data)
|
||||
}
|
||||
|
||||
export function createInvitationCode(data) {
|
||||
return axios.post('/admin/user/create/invitation/code', data)
|
||||
}
|
||||
|
||||
@@ -34,12 +34,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a href="#" @click.prevent="showLogin = true"
|
||||
class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">
|
||||
Login
|
||||
</a>
|
||||
</template>
|
||||
<button data-collapse-toggle="mobile-menu-2" type="button"
|
||||
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||
aria-controls="mobile-menu-2" aria-expanded="false">
|
||||
@@ -65,7 +59,7 @@
|
||||
aria-current="page">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/"
|
||||
<router-link to="/class"
|
||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
||||
班级
|
||||
</router-link>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import '@/assets/main.css'
|
||||
import 'nprogress/nprogress.css'
|
||||
import 'element-plus/dist/index.css'
|
||||
// 导入路由
|
||||
import router from '@/router'
|
||||
// 导入全局路由守卫
|
||||
|
||||
203
enlish-vue/src/pages/Login.vue
Normal file
203
enlish-vue/src/pages/Login.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
|
||||
<template>
|
||||
<div class="w-screen h-screen overflow-hidden flex">
|
||||
<div class="flex-1 bg-black">
|
||||
<el-image fit="cover" src="https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg"
|
||||
style="width: 100%; height: 100%" />
|
||||
</div>
|
||||
<div class="w-[500px] z-10 bg-white dark:bg-gray-800 p-8">
|
||||
<div class="mb-4 grid grid-cols-2 gap-2 justify-center pt-64">
|
||||
<button type="button" @click="switchMode('login')" class="w-full px-3 py-1.5 rounded text-sm"
|
||||
:class="mode === 'login' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
||||
登录
|
||||
</button>
|
||||
<button type="button" @click="switchMode('register')" class="w-full px-3 py-1.5 rounded text-sm"
|
||||
:class="mode === 'register' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h1 class="text-2xl font-bold" v-if="mode === 'login'">登录</h1>
|
||||
<h1 class="text-2xl font-bold" v-else>注册新用户</h1>
|
||||
<p class="text-sm text-gray-500" v-if="mode === 'login'">请输入手机号和密码进行登录</p>
|
||||
<p class="text-sm text-gray-500" v-else>请填写以下信息进行注册</p>
|
||||
</div>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" class="space-y-4">
|
||||
<el-form-item prop="phone">
|
||||
<el-input v-model="form.phone" maxlength="11" placeholder="手机号" />
|
||||
</el-form-item>
|
||||
<template v-if="mode === 'login'">
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" type="password" maxlength="20" placeholder="密码" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-form-item prop="name">
|
||||
<el-input v-model="form.name" maxlength="20" placeholder="姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" type="password" maxlength="20" placeholder="设置密码" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password_repeat">
|
||||
<el-input v-model="form.password_repeat" type="password" maxlength="20" placeholder="重复密码" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="invitationCode">
|
||||
<el-input v-model="form.invitationCode" maxlength="6" placeholder="邀请码" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="code">
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<el-input v-model="form.code" maxlength="6" placeholder="验证码" />
|
||||
<el-button type="primary" @click="sendCode" :disabled="codeDisabled || !form.phone">
|
||||
{{ codeBtnText }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<div class="mt-6">
|
||||
<el-button :type="mode === 'login' ? 'success' : 'warning'" plain class="w-full" :disabled="loading"
|
||||
@click="userLogin">
|
||||
<span v-if="!loading && mode === 'login'">立即登录</span>
|
||||
<span v-if="loading && mode === 'login'">登录中...</span>
|
||||
<span v-if="!loading && mode === 'register'">立即注册</span>
|
||||
<span v-if="loading && mode === 'register'">处理中...</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-2 left-6 text-white drop-shadow">
|
||||
<h2 class="text-xl">欢迎来到智慧英语</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import router from '@/router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { login, getVerificationCode } from '@/api/user'
|
||||
import { setToken } from '@/composables/auth'
|
||||
|
||||
const loading = ref(false)
|
||||
const formRef = ref()
|
||||
const mode = ref('login')
|
||||
const codeDisabled = ref(false)
|
||||
const codeBtnText = ref('发送验证码')
|
||||
let timer = null
|
||||
const form = reactive({
|
||||
phone: '',
|
||||
name: '',
|
||||
password: '',
|
||||
password_repeat: '',
|
||||
code: '',
|
||||
invitationCode: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入合法的手机号', trigger: ['blur', 'change'] },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度为6-20位', trigger: ['blur', 'change'] },
|
||||
],
|
||||
name: [
|
||||
{
|
||||
validator: (_r, v, cb) => {
|
||||
if (mode.value !== 'register') return cb()
|
||||
if (!v?.trim()) return cb(new Error('请输入姓名'))
|
||||
cb()
|
||||
}, trigger: ['blur', 'change']
|
||||
},
|
||||
],
|
||||
password_repeat: [
|
||||
{
|
||||
validator: (_r, v, cb) => {
|
||||
if (mode.value !== 'register') return cb()
|
||||
if (!v) return cb(new Error('请再次输入密码'))
|
||||
if (v !== form.password) return cb(new Error('两次密码不一致'))
|
||||
cb()
|
||||
}, trigger: ['blur', 'change']
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{
|
||||
validator: (_r, v, cb) => {
|
||||
if (mode.value !== 'register') return cb()
|
||||
if (!v?.trim()) return cb(new Error('请输入验证码'))
|
||||
cb()
|
||||
}, trigger: ['blur', 'change']
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
async function userLogin() {
|
||||
if (loading.value) return
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
const payload = mode.value === 'login'
|
||||
? {
|
||||
phone: form.phone.trim(),
|
||||
password: form.password.trim(),
|
||||
}
|
||||
: {
|
||||
phone: form.phone.trim(),
|
||||
name: form.name.trim(),
|
||||
password: form.password.trim(),
|
||||
code: form.code.trim(),
|
||||
invitationCode: form.invitationCode.trim()
|
||||
}
|
||||
const res = await login(payload)
|
||||
const data = res.data
|
||||
if (data?.success) {
|
||||
if (mode.value === 'login') {
|
||||
try { setToken(data.data) } catch { }
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/class')
|
||||
} else {
|
||||
ElMessage.success('注册成功')
|
||||
mode.value = 'login'
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(data?.message || '登录失败')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function switchMode(m) {
|
||||
mode.value = m
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
async function sendCode() {
|
||||
if (!form.phone || codeDisabled.value) return
|
||||
codeDisabled.value = true
|
||||
try {
|
||||
await getVerificationCode({ phone: form.phone.trim() })
|
||||
ElMessage.success('验证码已发送')
|
||||
let count = 60
|
||||
codeBtnText.value = `${count}s`
|
||||
timer = setInterval(() => {
|
||||
count -= 1
|
||||
if (count <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
codeDisabled.value = false
|
||||
codeBtnText.value = '发送验证码'
|
||||
} else {
|
||||
codeBtnText.value = `${count}s`
|
||||
}
|
||||
}, 1000)
|
||||
} catch {
|
||||
codeDisabled.value = false
|
||||
codeBtnText.value = '发送验证码'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -31,6 +31,29 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt-4">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>生成邀请码</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="inviteForm" :rules="inviteRules" ref="inviteFormRef" label-width="120px">
|
||||
<el-form-item label="使用次数限制" prop="limit">
|
||||
<el-input-number v-model="inviteForm.limit" :min="1" :max="9999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="有效期(天)" prop="expire">
|
||||
<el-input-number v-model="inviteForm.expire" :min="1" :max="3650" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="inviteLoading" @click="submitInvite">生成邀请码</el-button>
|
||||
<el-button class="ml-2" @click="resetInvite">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-if="inviteCode" class="mt-2">
|
||||
<el-alert type="success" :closable="false" :title="`邀请码:${inviteCode}`" show-icon />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
||||
<el-form :model="createForm" :rules="rules" ref="createFormRef" label-width="80px">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
@@ -57,7 +80,7 @@
|
||||
<script setup>
|
||||
import Header from '@/layouts/components/Header.vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { getUserList, createUser } from '@/api/admin'
|
||||
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
||||
import { showMessage } from '@/composables/util.js'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -144,4 +167,42 @@ onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
|
||||
const inviteForm = reactive({ limit: 10, expire: 3 })
|
||||
const inviteFormRef = ref()
|
||||
const inviteLoading = ref(false)
|
||||
const inviteCode = ref('')
|
||||
const inviteRules = {
|
||||
limit: [{ required: true, message: '请输入使用次数限制', trigger: 'change' }],
|
||||
expire: [{ required: true, message: '请输入有效期', trigger: 'change' }],
|
||||
}
|
||||
|
||||
function resetInvite() {
|
||||
inviteForm.limit = 10
|
||||
inviteForm.expire = 3
|
||||
inviteCode.value = ''
|
||||
}
|
||||
|
||||
function submitInvite() {
|
||||
inviteFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
inviteLoading.value = true
|
||||
try {
|
||||
const r = await createInvitationCode({ limit: inviteForm.limit, expire: inviteForm.expire })
|
||||
const d = r?.data
|
||||
if (d?.success) {
|
||||
inviteCode.value = d?.data?.invitationCode || ''
|
||||
if (inviteCode.value) {
|
||||
showMessage('邀请码生成成功', 'success')
|
||||
} else {
|
||||
showMessage('生成成功,但未返回邀请码', 'warning')
|
||||
}
|
||||
} else {
|
||||
showMessage(d?.message || '邀请码生成失败', 'error')
|
||||
}
|
||||
} finally {
|
||||
inviteLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 lg:col-span-1 lg:row-span-1">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 lg:col-span-1 lg:row-span-2">
|
||||
<div class="text-lg font-semibold mb-4">学生查询</div>
|
||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||
<el-input v-model="studentName" placeholder="按姓名查询" clearable style="max-width: 220px" />
|
||||
@@ -96,56 +96,12 @@
|
||||
row-key="id" :current-row-key="selectedGradeId" @row-click="onGradeRowClick">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="年级名称" min-width="160" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" size="small"
|
||||
@click.stop="onDeleteGrade(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="gradeTotalCount"
|
||||
:page-size="gradePageSize" :current-page="gradePageNo"
|
||||
@current-change="handleGradePageChange" @size-change="handleGradeSizeChange" />
|
||||
</div>
|
||||
<div class="mt-3 flex justify-end">
|
||||
<el-button type="primary" @click="showAddGradeDialog = true">新增年级</el-button>
|
||||
</div>
|
||||
<AddGradeDialog v-model="showAddGradeDialog" @success="fetchGrades" />
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6" v-loading="unitLoading">
|
||||
<div class="text-lg font-semibold mb-4">单元列表</div>
|
||||
<el-table ref="unitTableRef" :data="units" border class="w-full">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="单元名称" min-width="200" />
|
||||
<el-table-column prop="version" label="版本" min-width="120" />
|
||||
<el-table-column prop="createAt" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" size="small" @click.stop="onDeleteUnit(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next, sizes, total"
|
||||
:total="unitTotalCount"
|
||||
:page-size="unitPageSize"
|
||||
:current-page="unitPageNo"
|
||||
@current-change="handleUnitPageChange"
|
||||
@size-change="handleUnitSizeChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3 flex justify-end">
|
||||
<el-button type="primary" :disabled="!selectedGradeId" @click="showAddUnitDialog = true">新增单元</el-button>
|
||||
</div>
|
||||
<AddUnitDialog
|
||||
v-model="showAddUnitDialog"
|
||||
:default-grade-id="selectedGradeId"
|
||||
@success="fetchUnits"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -169,6 +125,7 @@ import LessonPlanDialog from '@/layouts/components/LessonPlanDialog.vue'
|
||||
import { getUnitList, deleteUnit } from '@/api/unit'
|
||||
import AddUnitDialog from '@/layouts/components/AddUnitDialog.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
const classes = ref([])
|
||||
const pageNo = ref(1)
|
||||
@@ -311,6 +268,11 @@ function onGradeRowClick(row) {
|
||||
}
|
||||
async function onDeleteStudent(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该学生?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await deleteStudent(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
if (selectedStudentIds.value?.length) {
|
||||
@@ -319,11 +281,17 @@ async function onDeleteStudent(row) {
|
||||
}
|
||||
await fetchStudents()
|
||||
} catch (e) {
|
||||
if (e === 'cancel' || e === 'close') return
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
async function onDeleteClass(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该班级?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await deleteClass(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
if (selectedClassId.value === row.id) {
|
||||
@@ -333,6 +301,7 @@ async function onDeleteClass(row) {
|
||||
}
|
||||
await fetchClasses()
|
||||
} catch (e) {
|
||||
if (e === 'cancel' || e === 'close') return
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,13 @@
|
||||
生成学习分析
|
||||
</el-button>
|
||||
</div>
|
||||
<template v-if="analysisHtml">
|
||||
<template v-if="analyzeLoading">
|
||||
<div class="space-y-2">
|
||||
<el-progress :percentage="analyzeProgress" :stroke-width="10" />
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">正在生成学习分析,请稍候…</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="analysisHtml">
|
||||
<div class="leading-7 text-gray-700 dark:text-gray-200" v-html="analysisHtml"></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -87,6 +93,8 @@ const detail = ref(null)
|
||||
const route = useRoute()
|
||||
const history = ref([])
|
||||
const analyzeLoading = ref(false)
|
||||
const analyzeProgress = ref(0)
|
||||
let analyzeTimer = null
|
||||
const analysisText = ref('')
|
||||
const wordStat = ref(null)
|
||||
const md = new MarkdownIt({
|
||||
@@ -125,6 +133,16 @@ async function fetchStudyAnalyze() {
|
||||
const id = route.params.id
|
||||
if (!id) return
|
||||
analyzeLoading.value = true
|
||||
analyzeProgress.value = 0
|
||||
if (analyzeTimer) {
|
||||
clearInterval(analyzeTimer)
|
||||
analyzeTimer = null
|
||||
}
|
||||
analyzeTimer = setInterval(() => {
|
||||
const inc = Math.floor(Math.random() * 8) + 3
|
||||
const next = analyzeProgress.value + inc
|
||||
analyzeProgress.value = next >= 90 ? 90 : next
|
||||
}, 300)
|
||||
try {
|
||||
const res = await getStudentStudyAnalyze({
|
||||
studentId: Number(id)
|
||||
@@ -133,6 +151,11 @@ async function fetchStudyAnalyze() {
|
||||
const raw = typeof d?.data === 'string' ? d.data : ''
|
||||
analysisText.value = raw.replace(/\\n/g, '\n')
|
||||
} finally {
|
||||
analyzeProgress.value = 100
|
||||
if (analyzeTimer) {
|
||||
clearInterval(analyzeTimer)
|
||||
analyzeTimer = null
|
||||
}
|
||||
analyzeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,22 @@ import router from '@/router/index'
|
||||
|
||||
import { showMessage } from '@/composables/util'
|
||||
import { showPageLoading, hidePageLoading } from '@/composables/util'
|
||||
import { getToken } from '@/composables/auth'
|
||||
|
||||
// 全局路由前置守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log('==> 全局路由前置守卫')
|
||||
// 展示页面加载 Loading
|
||||
showPageLoading()
|
||||
const token = getToken()
|
||||
if (!token && to.path !== '/login') {
|
||||
next({ path: '/login' })
|
||||
return
|
||||
}
|
||||
if (token && to.path === '/login') {
|
||||
next({ path: '/class' })
|
||||
return
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
@@ -5,14 +5,15 @@ import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import Admid from '@/pages/admid/admid.vue'
|
||||
import Student from '@/pages/student.vue'
|
||||
import PlanTTS from '@/pages/PlanTTS.vue'
|
||||
import Login from '@/pages/Login.vue'
|
||||
|
||||
// 统一在这里声明所有路由
|
||||
const routes = [
|
||||
{
|
||||
path: '/', // 路由地址
|
||||
component: Class, // 对应组件
|
||||
meta: { // meta 信息
|
||||
title: '班级' // 页面标题
|
||||
path: '/class',
|
||||
component: Class,
|
||||
meta: {
|
||||
title: '班级'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -43,6 +44,13 @@ const routes = [
|
||||
title: '管理员页面' // 页面标题
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: Login,
|
||||
meta: {
|
||||
title: '登录'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/plan/tts',
|
||||
component: PlanTTS,
|
||||
|
||||
Reference in New Issue
Block a user