Compare commits
3 Commits
deabd5f7f5
...
09b326c07a
| Author | SHA1 | Date | |
|---|---|---|---|
| 09b326c07a | |||
| 49963bb49c | |||
| bf2a80917c |
@@ -1,16 +1,22 @@
|
|||||||
package com.yinlihupo.enlish.service.controller;
|
package com.yinlihupo.enlish.service.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||||
import com.yinlihupo.enlish.service.model.vo.user.FindUserInfoRspVO;
|
import com.yinlihupo.enlish.service.model.vo.user.FindUserInfoRspVO;
|
||||||
|
import com.yinlihupo.enlish.service.model.vo.user.UpdateUserInfoReqVO;
|
||||||
import com.yinlihupo.enlish.service.service.UserService;
|
import com.yinlihupo.enlish.service.service.UserService;
|
||||||
|
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||||
import com.yinlihupo.framework.common.response.Response;
|
import com.yinlihupo.framework.common.response.Response;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/user/")
|
@RequestMapping("/user/")
|
||||||
|
@Slf4j
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -27,4 +33,23 @@ public class UserController {
|
|||||||
return Response.success(findUserInfoRspVO);
|
return Response.success(findUserInfoRspVO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("update-user-info")
|
||||||
|
@ApiOperationLog(description = "修改密码")
|
||||||
|
public Response<String> updatePassword(@RequestBody UpdateUserInfoReqVO updateUserInfoReqVO) {
|
||||||
|
try {
|
||||||
|
String code = updateUserInfoReqVO.getCode();
|
||||||
|
String newPassword = updateUserInfoReqVO.getNewPassword();
|
||||||
|
String phone = updateUserInfoReqVO.getPhone();
|
||||||
|
String name = updateUserInfoReqVO.getName();
|
||||||
|
userService.updateUserInfo(newPassword, code, phone, name);
|
||||||
|
|
||||||
|
StpUtil.logout();
|
||||||
|
|
||||||
|
return Response.success();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("修改密码失败 {}", e.getMessage());
|
||||||
|
return Response.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ public interface UserDOMapper {
|
|||||||
|
|
||||||
void insert(UserDO userDO);
|
void insert(UserDO userDO);
|
||||||
|
|
||||||
|
void updatePassword(@Param("id") Long id, @Param("password") String password);
|
||||||
|
|
||||||
|
void updateUserInfo(@Param("id") Long id, @Param("name") String name, @Param("password") String password, @Param("phone") String phone);
|
||||||
|
|
||||||
UserDO selectById(Long id);
|
UserDO selectById(Long id);
|
||||||
|
|
||||||
List<UserDO> selectUserDOList(@Param("name") String name, @Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
List<UserDO> selectUserDOList(@Param("name") String name, @Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yinlihupo.enlish.service.model.vo.user;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Data
|
||||||
|
public class UpdateUserInfoReqVO {
|
||||||
|
|
||||||
|
private String newPassword;
|
||||||
|
private String name;
|
||||||
|
private String phone;
|
||||||
|
private String code;
|
||||||
|
}
|
||||||
@@ -13,4 +13,6 @@ public interface UserService {
|
|||||||
Integer findUserTotal();
|
Integer findUserTotal();
|
||||||
|
|
||||||
void createUser(UserDO userDO);
|
void createUser(UserDO userDO);
|
||||||
|
|
||||||
|
void updateUserInfo(String password, String reqCode, String phone, String name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -83,7 +84,9 @@ public class LoginServiceImpl implements LoginService {
|
|||||||
@Override
|
@Override
|
||||||
public void sendVerificationCode(String phone) {
|
public void sendVerificationCode(String phone) {
|
||||||
String code = RandomUtil.randomNumbers(6);
|
String code = RandomUtil.randomNumbers(6);
|
||||||
redisTemplate.opsForValue().set(UserRedisConstants.buildUserLoginCode(phone), code, Duration.ofSeconds(60));
|
String key = UserRedisConstants.buildUserLoginCode(phone);
|
||||||
|
redisTemplate.opsForValue().set(key, code);
|
||||||
|
redisTemplate.expire(key, 5, TimeUnit.MINUTES);
|
||||||
String signName = "短信测试";
|
String signName = "短信测试";
|
||||||
String templateCode = "SMS_154950909";
|
String templateCode = "SMS_154950909";
|
||||||
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package com.yinlihupo.enlish.service.service.user;
|
package com.yinlihupo.enlish.service.service.user;
|
||||||
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||||
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
|
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
|
||||||
import com.yinlihupo.enlish.service.service.UserService;
|
import com.yinlihupo.enlish.service.service.UserService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -17,8 +21,10 @@ public class UserServiceImpl implements UserService {
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private UserDOMapper userDOMapper;
|
private UserDOMapper userDOMapper;
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
@Resource
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDO findUser() {
|
public UserDO findUser() {
|
||||||
@@ -43,4 +49,21 @@ public class UserServiceImpl implements UserService {
|
|||||||
userDOMapper.insert(userDO);
|
userDOMapper.insert(userDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateUserInfo(String password, String reqCode, String phone, String name) {
|
||||||
|
long id = Integer.parseInt(String.valueOf(StpUtil.getLoginId()));
|
||||||
|
UserDO userDO = userDOMapper.selectById(id);
|
||||||
|
|
||||||
|
String key = UserRedisConstants.buildUserLoginCode(userDO.getPhone());
|
||||||
|
String code = Objects.requireNonNull(redisTemplate.opsForValue().get(key)).toString();
|
||||||
|
if (code == null || !code.equals(reqCode)) {
|
||||||
|
throw new RuntimeException("验证码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password != null) {
|
||||||
|
password = passwordEncoder.encode(password);
|
||||||
|
}
|
||||||
|
userDOMapper.updateUserInfo(id, name, password, phone);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ sa-token:
|
|||||||
is-share: true
|
is-share: true
|
||||||
# 是否输出操作日志
|
# 是否输出操作日志
|
||||||
is-log: true
|
is-log: true
|
||||||
logging:
|
#logging:
|
||||||
level:
|
# level:
|
||||||
com.yinlihupo.enlish.service.domain.mapper: debug
|
# com.yinlihupo.enlish.service.domain.mapper: debug
|
||||||
@@ -20,6 +20,28 @@
|
|||||||
values (#{phone}, #{name}, #{password})
|
values (#{phone}, #{name}, #{password})
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<update id="updatePassword">
|
||||||
|
update user
|
||||||
|
set password = #{password}
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="updateUserInfo">
|
||||||
|
update user
|
||||||
|
<set>
|
||||||
|
<if test="password != null">
|
||||||
|
password = #{password},
|
||||||
|
</if>
|
||||||
|
<if test="name != null">
|
||||||
|
`name` = #{name},
|
||||||
|
</if>
|
||||||
|
<if test="phone != null">
|
||||||
|
phone = #{phone},
|
||||||
|
</if>
|
||||||
|
</set>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
<select id="selectByPhone" resultMap="BaseResultMap">
|
<select id="selectByPhone" resultMap="BaseResultMap">
|
||||||
select *
|
select *
|
||||||
from user
|
from user
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M5 6C5 4.89543 5.89543 4 7 4H12C13.6569 4 15 5.34315 15 7V17C15 18.6569 13.6569 20 12 20H7C5.89543 20 5 19.1046 5 18V6ZM7 6V10H12C12.5523 10 13 9.55228 13 9C13 8.44772 12.5523 8 12 8H8.5C7.67157 8 7 7.32843 7 6.5V6ZM7 18L7 14H10C10.5523 14 11 13.5523 11 13C11 12.4477 10.5523 12 10 12H7V17.5C7 17.7761 7.22386 18 7.5 18H12C12.5523 18 13 17.5523 13 17V17C13 16.4477 12.5523 16 12 16H8.5C7.67157 16 7 16.6716 7 17.5V18Z"
|
||||||
|
fill="#0056D2" />
|
||||||
|
<circle cx="17.5" cy="6.5" r="2.5" fill="#FFAB00" />
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 865 B |
@@ -15,3 +15,7 @@ export function getVerificationCode(data) {
|
|||||||
export function getUserInfo() {
|
export function getUserInfo() {
|
||||||
return axios.post("/user/info")
|
return axios.post("/user/info")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateUserInfo(data) {
|
||||||
|
return axios.post("/user/update-user-info", data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,14 @@
|
|||||||
<nav class="fluent-nav px-4 lg:px-6 py-2.5">
|
<nav class="fluent-nav px-4 lg:px-6 py-2.5">
|
||||||
<div class="flex flex-wrap justify-between items-center">
|
<div class="flex flex-wrap justify-between items-center">
|
||||||
<a href="#" class="flex items-center">
|
<a href="#" class="flex items-center">
|
||||||
<img src="https://flowbite.com/docs/images/logo.svg" class="mr-3 h-6 sm:h-9" alt="Flowbite Logo" width="24" height="24" />
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"
|
||||||
<span class="self-center text-xl font-semibold whitespace-nowrap">Flowbite</span>
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M5 6C5 4.89543 5.89543 4 7 4H12C13.6569 4 15 5.34315 15 7V17C15 18.6569 13.6569 20 12 20H7C5.89543 20 5 19.1046 5 18V6ZM7 6V10H12C12.5523 10 13 9.55228 13 9C13 8.44772 12.5523 8 12 8H8.5C7.67157 8 7 7.32843 7 6.5V6ZM7 18L7 14H10C10.5523 14 11 13.5523 11 13C11 12.4477 10.5523 12 10 12H7V17.5C7 17.7761 7.22386 18 7.5 18H12C12.5523 18 13 17.5523 13 17V17C13 16.4477 12.5523 16 12 16H8.5C7.67157 16 7 16.6716 7 17.5V18Z"
|
||||||
|
fill="#0056D2" />
|
||||||
|
<circle cx="17.5" cy="6.5" r="2.5" fill="#FFAB00" />
|
||||||
|
</svg>
|
||||||
|
<span class="self-center text-xl font-semibold whitespace-nowrap">英语教育</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="flex items-center lg:order-2">
|
<div class="flex items-center lg:order-2">
|
||||||
<template v-if="userName">
|
<template v-if="userName">
|
||||||
@@ -20,10 +26,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="menuOpen" class="fluent-card absolute right-0 mt-2 z-50">
|
<div v-if="menuOpen" class="fluent-card absolute right-0 mt-2 z-50">
|
||||||
<router-link to="/admid" @click="menuOpen = false" class="block px-4 py-2 fluent-link">
|
<router-link to="/admid" @click="menuOpen = false"
|
||||||
|
class="block px-4 py-2 fluent-link">
|
||||||
后台
|
后台
|
||||||
</router-link>
|
</router-link>
|
||||||
<button @click="handleLogout" class="w-full text-left block px-4 py-2 fluent-link">
|
<button @click="handleLogout"
|
||||||
|
class="w-full text-left block px-4 py-2 fluent-link">
|
||||||
登出
|
登出
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +41,8 @@
|
|||||||
class="inline-flex items-center p-2 ml-1 text-sm rounded-lg lg:hidden fluent-btn"
|
class="inline-flex items-center p-2 ml-1 text-sm rounded-lg lg:hidden fluent-btn"
|
||||||
aria-controls="mobile-menu-2" aria-expanded="false">
|
aria-controls="mobile-menu-2" aria-expanded="false">
|
||||||
<span class="sr-only">Open main menu</span>
|
<span class="sr-only">Open main menu</span>
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd"
|
<path fill-rule="evenodd"
|
||||||
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
||||||
clip-rule="evenodd"></path>
|
clip-rule="evenodd"></path>
|
||||||
@@ -46,7 +55,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1" id="mobile-menu-2">
|
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1"
|
||||||
|
id="mobile-menu-2">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,9 +83,14 @@ async function refreshUser() {
|
|||||||
try {
|
try {
|
||||||
const r = await getUserInfo()
|
const r = await getUserInfo()
|
||||||
const d = r?.data
|
const d = r?.data
|
||||||
userName.value = d?.success ? (d?.data?.name || '') : ''
|
console.log("header" + d.success)
|
||||||
|
if (d?.success) {
|
||||||
|
userName.value = d?.data?.name || ''
|
||||||
|
} else {
|
||||||
|
handleLogout()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
userName.value = ''
|
handleLogout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
@@ -86,7 +101,7 @@ async function handleLogout() {
|
|||||||
userName.value = ''
|
userName.value = ''
|
||||||
menuOpen.value = false
|
menuOpen.value = false
|
||||||
showMessage('已退出登录', 'success')
|
showMessage('已退出登录', 'success')
|
||||||
router.push('/')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onDocClick(e) {
|
function onDocClick(e) {
|
||||||
@@ -111,10 +126,12 @@ onBeforeUnmount(() => {
|
|||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .fluent-nav {
|
:global(.dark) .fluent-nav {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fluent-card {
|
.fluent-card {
|
||||||
background: rgba(255, 255, 255, 0.6);
|
background: rgba(255, 255, 255, 0.6);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
@@ -123,14 +140,17 @@ onBeforeUnmount(() => {
|
|||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
transition: box-shadow 200ms ease, transform 200ms ease;
|
transition: box-shadow 200ms ease, transform 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .fluent-card {
|
:global(.dark) .fluent-card {
|
||||||
background: rgba(55, 65, 81, 0.4);
|
background: rgba(55, 65, 81, 0.4);
|
||||||
border-color: rgba(148, 163, 184, 0.25);
|
border-color: rgba(148, 163, 184, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fluent-card:hover {
|
.fluent-card:hover {
|
||||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16);
|
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fluent-btn {
|
.fluent-btn {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
background: rgba(255, 255, 255, 0.6);
|
background: rgba(255, 255, 255, 0.6);
|
||||||
@@ -139,32 +159,39 @@ onBeforeUnmount(() => {
|
|||||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
|
||||||
transition: background 200ms ease, box-shadow 200ms ease, transform 200ms ease;
|
transition: background 200ms ease, box-shadow 200ms ease, transform 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fluent-btn:hover {
|
.fluent-btn:hover {
|
||||||
background: rgba(255, 255, 255, 0.7);
|
background: rgba(255, 255, 255, 0.7);
|
||||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12);
|
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .fluent-btn {
|
:global(.dark) .fluent-btn {
|
||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
background: rgba(55, 65, 81, 0.4);
|
background: rgba(55, 65, 81, 0.4);
|
||||||
border-color: rgba(148, 163, 184, 0.25);
|
border-color: rgba(148, 163, 184, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fluent-link {
|
.fluent-link {
|
||||||
color: #2563eb;
|
color: #2563eb;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
transition: color 200ms ease, background 200ms ease, box-shadow 200ms ease;
|
transition: color 200ms ease, background 200ms ease, box-shadow 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fluent-link:hover {
|
.fluent-link:hover {
|
||||||
color: #1d4ed8;
|
color: #1d4ed8;
|
||||||
background: rgba(255, 255, 255, 0.35);
|
background: rgba(255, 255, 255, 0.35);
|
||||||
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.25);
|
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .fluent-link:hover {
|
:global(.dark) .fluent-link:hover {
|
||||||
background: rgba(55, 65, 81, 0.35);
|
background: rgba(55, 65, 81, 0.35);
|
||||||
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.25);
|
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fluent-card {
|
.fluent-card {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.el-header) {
|
:global(.el-header) {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function sortData(arr) {
|
|||||||
|
|
||||||
function toSource(arr) {
|
function toSource(arr) {
|
||||||
return sortData(arr).map(it => ({
|
return sortData(arr).map(it => ({
|
||||||
startTime: it.startTime,
|
startTime: it.startTime.replace('T', ' '),
|
||||||
totalCount: Number(it.totalCount) || 0,
|
totalCount: Number(it.totalCount) || 0,
|
||||||
planId: it.planId ?? null,
|
planId: it.planId ?? null,
|
||||||
id: it.id ?? null
|
id: it.id ?? null
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|||||||
@@ -1,72 +1,94 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-screen h-screen overflow-hidden flex">
|
<div
|
||||||
<div class="flex-1 bg-black">
|
class="min-h-screen relative flex items-center justify-center bg-fixed bg-cover bg-center bg-[url('https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg')]">
|
||||||
<el-image fit="cover" src="https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg"
|
<div class="absolute inset-0 bg-gradient-to-br from-[rgba(30,20,50,0.4)] to-[rgba(10,10,20,0.6)]"></div>
|
||||||
style="width: 100%; height: 100%" />
|
|
||||||
|
<div
|
||||||
|
class="relative z-10 w-[400px] max-w-[92%] bg-white/10 backdrop-blur-2xl border border-white/20 rounded-2xl p-8 shadow-2xl text-white">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<h2 class="text-2xl font-semibold tracking-wide mb-1">Welcome Back</h2>
|
||||||
|
<p class="text-sm text-white/80">智慧英语 · 让学习更简单</p>
|
||||||
</div>
|
</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">
|
<div class="flex justify-center mb-6 border-b border-white/10">
|
||||||
<button type="button" @click="switchMode('login')" class="w-full px-3 py-1.5 rounded text-sm"
|
<button class="px-5 py-2 text-white/70 hover:text-white transition"
|
||||||
:class="mode === 'login' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
:class="mode === 'login' ? 'font-bold text-white border-b-2 border-white' : ''"
|
||||||
登录
|
@click="switchMode('login')">登录</button>
|
||||||
</button>
|
<button class="px-5 py-2 text-white/70 hover:text-white transition"
|
||||||
<button type="button" @click="switchMode('register')" class="w-full px-3 py-1.5 rounded text-sm"
|
:class="mode === 'register' ? 'font-bold text-white border-b-2 border-white' : ''"
|
||||||
:class="mode === 'register' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
@click="switchMode('register')">注册</button>
|
||||||
注册
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
|
||||||
<h1 class="text-2xl font-bold" v-if="mode === 'login'">登录</h1>
|
<el-form :model="form" :rules="rules" ref="formRef" class="mt-2">
|
||||||
<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-form-item prop="phone">
|
||||||
<el-input v-model="form.phone" maxlength="11" placeholder="手机号" />
|
<div class="input-glass relative mb-4 w-full">
|
||||||
|
<el-input v-model="form.phone" maxlength="11" placeholder="请输入手机号 / 账号" />
|
||||||
|
<i class="fas fa-user absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<transition name="tabfade" mode="out-in">
|
||||||
|
<div :key="mode">
|
||||||
<template v-if="mode === 'login'">
|
<template v-if="mode === 'login'">
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
<el-input v-model="form.password" type="password" maxlength="20" placeholder="密码" />
|
<div class="input-glass relative mb-4 w-full">
|
||||||
|
<el-input v-model="form.password" type="password" maxlength="20" placeholder="请输入密码" />
|
||||||
|
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-form-item prop="name">
|
<el-form-item prop="name">
|
||||||
|
<div class="input-glass relative mb-4 w-full">
|
||||||
<el-input v-model="form.name" maxlength="20" placeholder="姓名" />
|
<el-input v-model="form.name" maxlength="20" placeholder="姓名" />
|
||||||
|
<i class="fas fa-id-card absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
|
<div class="input-glass relative mb-4 w-full">
|
||||||
<el-input v-model="form.password" type="password" maxlength="20" placeholder="设置密码" />
|
<el-input v-model="form.password" type="password" maxlength="20" placeholder="设置密码" />
|
||||||
|
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="password_repeat">
|
<el-form-item prop="password_repeat">
|
||||||
|
<div class="input-glass relative mb-4 w-full">
|
||||||
<el-input v-model="form.password_repeat" type="password" maxlength="20" placeholder="重复密码" />
|
<el-input v-model="form.password_repeat" type="password" maxlength="20" placeholder="重复密码" />
|
||||||
|
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="invitationCode">
|
<el-form-item prop="invitationCode">
|
||||||
|
<div class="input-glass relative mb-4 w-full">
|
||||||
<el-input v-model="form.invitationCode" maxlength="6" placeholder="邀请码" />
|
<el-input v-model="form.invitationCode" maxlength="6" placeholder="邀请码" />
|
||||||
|
<i class="fas fa-ticket absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="code">
|
<el-form-item prop="code">
|
||||||
<div class="flex items-center gap-2 w-full">
|
<div class="relative mb-2 flex items-center gap-2">
|
||||||
|
<div class="input-glass relative flex-1">
|
||||||
<el-input v-model="form.code" maxlength="6" placeholder="验证码" />
|
<el-input v-model="form.code" maxlength="6" placeholder="验证码" />
|
||||||
<el-button type="primary" @click="sendCode" :disabled="codeDisabled || !form.phone">
|
<i class="fas fa-shield absolute left-4 top-1/2 -translate-y-1/2 text-white/80 text-lg pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
|
<el-button class="rounded-lg bg-gradient-to-r from-blue-600 to-blue-400 text-white px-3 py-2"
|
||||||
|
@click="sendCode" :disabled="codeDisabled || !form.phone">
|
||||||
{{ codeBtnText }}
|
{{ codeBtnText }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="mt-6">
|
|
||||||
<el-button :type="mode === 'login' ? 'success' : 'warning'" plain class="w-full" :disabled="loading"
|
<button
|
||||||
@click="userLogin">
|
class="w-full mt-3 px-4 py-3 rounded-xl bg-gradient-to-r from-blue-600 to-blue-400 text-white font-semibold shadow-lg hover:-translate-y-0.5 hover:shadow-xl transition disabled:opacity-70 disabled:cursor-not-allowed"
|
||||||
|
:disabled="loading" @click="userLogin">
|
||||||
<span v-if="!loading && mode === 'login'">立即登录</span>
|
<span v-if="!loading && mode === 'login'">立即登录</span>
|
||||||
<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>
|
||||||
<span v-if="loading && mode === 'register'">处理中...</span>
|
<span v-if="loading && mode === 'register'">处理中...</span>
|
||||||
</el-button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="absolute bottom-2 left-6 text-white drop-shadow">
|
|
||||||
<h2 class="text-xl">欢迎来到智慧英语</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -200,4 +222,26 @@ async function sendCode() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.input-glass :deep(.el-input__wrapper) {
|
||||||
|
@apply w-full bg-white/10 border border-white/10 rounded-xl pl-10 transition;
|
||||||
|
}
|
||||||
|
.input-glass :deep(.el-input__wrapper:hover),
|
||||||
|
.input-glass :deep(.el-input__wrapper.is-focus) {
|
||||||
|
@apply bg-white/15 border-white/50;
|
||||||
|
}
|
||||||
|
.input-glass :deep(.el-input__inner) {
|
||||||
|
@apply text-white placeholder:text-white/70;
|
||||||
|
}
|
||||||
|
.tabfade-enter-active, .tabfade-leave-active {
|
||||||
|
transition: all .25s ease;
|
||||||
|
}
|
||||||
|
.tabfade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px) scale(.98);
|
||||||
|
}
|
||||||
|
.tabfade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px) scale(.98);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,54 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header>
|
|
||||||
<Header></Header>
|
|
||||||
</el-header>
|
|
||||||
|
|
||||||
<el-container>
|
|
||||||
<el-aside width="200px" class="pt-4">
|
|
||||||
<Sidebar />
|
|
||||||
</el-aside>
|
|
||||||
|
|
||||||
<el-main class="p-4">
|
<el-container class="pt-4">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
|
<el-main class="p-2">
|
||||||
|
<div class="panel-shell p-6">
|
||||||
<div class="text-lg font-semibold mb-4">TTS</div>
|
<div class="text-lg font-semibold mb-4">TTS</div>
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mb-4">
|
||||||
<el-input v-model="planIdInput" placeholder="planId" style="max-width: 220px" />
|
<!-- <el-input v-model="planIdInput" placeholder="planId" class="w-full sm:w-auto" style="max-width: 220px" /> -->
|
||||||
<el-button type="primary" :loading="loadingWords" @click="onLoadWords">加载词汇</el-button>
|
<el-button type="primary" :loading="loadingWords" @click="onLoadWords">加载词汇</el-button>
|
||||||
<el-select v-model="voice" placeholder="选择声线" style="max-width: 160px">
|
<!-- <el-select v-model="voice" placeholder="选择声线" class="w-full sm:w-auto" style="max-width: 160px">
|
||||||
<el-option label="alloy" value="alloy" />
|
<el-option label="alloy" value="alloy" />
|
||||||
<el-option label="verse" value="verse" />
|
<el-option label="verse" value="verse" />
|
||||||
<el-option label="nova" value="nova" />
|
<el-option label="nova" value="nova" />
|
||||||
</el-select>
|
</el-select> -->
|
||||||
<el-select v-model="format" placeholder="格式" style="max-width: 120px">
|
<!-- <el-select v-model="format" placeholder="格式" class="w-full sm:w-auto" style="max-width: 120px">
|
||||||
<el-option label="mp3" value="mp3" />
|
<el-option label="mp3" value="mp3" />
|
||||||
<el-option label="wav" value="wav" />
|
<el-option label="wav" value="wav" />
|
||||||
<el-option label="ogg" value="ogg" />
|
<el-option label="ogg" value="ogg" />
|
||||||
</el-select>
|
</el-select> -->
|
||||||
<el-button type="success" :disabled="words.length === 0" :loading="generatingAll"
|
<el-button type="success" :disabled="words.length === 0" :loading="generatingAll" class="!ml-0"
|
||||||
@click="onGenerateAll">生成全部音频</el-button>
|
@click="onGenerateAll">生成音频</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="tableData" border class="w-full" v-loading="loadingWords">
|
<div class="sm:hidden">
|
||||||
<el-table-column prop="word" label="词汇/短语" min-width="260" />
|
<div v-for="row in tableData" :key="row.word" class="panel-shell p-4 mb-3">
|
||||||
<el-table-column label="状态" width="160">
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="font-medium">{{ row.word }}</div>
|
||||||
|
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
|
||||||
|
{{ row.audioUrl ? '已生成' : '未生成' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<el-button size="small" type="primary" :loading="row.loading" @click="onGenerateOne(row)">生成音频</el-button>
|
||||||
|
<el-button size="small" :disabled="!row.audioUrl" @click="onPlay(row)">播放</el-button>
|
||||||
|
<el-button size="small" :disabled="!row.audioUrl" @click="onDownload(row)">下载</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:block overflow-x-auto">
|
||||||
|
<el-table :data="tableData" border class="min-w-[640px]" v-loading="loadingWords" size="small">
|
||||||
|
<el-table-column prop="word" label="词汇/短语" min-width="200" />
|
||||||
|
<el-table-column label="状态" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
|
<el-tag :type="row.audioUrl ? 'success' : 'info'" effect="plain">
|
||||||
{{ row.audioUrl ? '已生成' : '未生成' }}
|
{{ row.audioUrl ? '已生成' : '未生成' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="360" fixed="right">
|
<el-table-column label="操作" width="240" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" type="primary" :loading="row.loading"
|
<el-button size="small" type="primary" :loading="row.loading" @click="onGenerateOne(row)">生成音频</el-button>
|
||||||
@click="onGenerateOne(row)">生成音频</el-button>
|
<el-button size="small" class="ml-2" :disabled="!row.audioUrl" @click="onPlay(row)">播放</el-button>
|
||||||
<el-button size="small" class="ml-2" :disabled="!row.audioUrl"
|
<el-button size="small" class="ml-2" :disabled="!row.audioUrl" @click="onDownload(row)">下载</el-button>
|
||||||
@click="onPlay(row)">播放</el-button>
|
|
||||||
<el-button size="small" class="ml-2" :disabled="!row.audioUrl"
|
|
||||||
@click="onDownload(row)">下载</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</div>
|
||||||
<div class="mt-3 text-sm text-gray-500">
|
<div class="mt-3 text-sm text-gray-500">
|
||||||
共 {{ words.length }} 条
|
共 {{ words.length }} 条
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,6 +48,40 @@
|
|||||||
<el-alert type="success" :closable="false" :title="`邀请码:${inviteCode}`" show-icon />
|
<el-alert type="success" :closable="false" :title="`邀请码:${inviteCode}`" show-icon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-shell p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<span class="text-lg font-semibold">修改用户信息</span>
|
||||||
|
</div>
|
||||||
|
<el-form :model="pwForm" :rules="pwRules" ref="pwFormRef" label-width="120px">
|
||||||
|
<el-form-item label="姓名" prop="name">
|
||||||
|
<el-input v-model="pwForm.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input v-model="pwForm.newPassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-input v-model="pwForm.confirmPassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="pwForm.phone" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="验证码" prop="code">
|
||||||
|
<el-input v-model="pwForm.code">
|
||||||
|
<template #append>
|
||||||
|
<el-button :disabled="codeCountdown > 0 || codeSending || !pwForm.phone"
|
||||||
|
@click="sendCode">
|
||||||
|
{{ codeCountdown > 0 ? `${codeCountdown}s` : '获取验证码' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="pwLoading" @click="submitPw">修改用户信息</el-button>
|
||||||
|
<el-button class="ml-2" @click="resetPw">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
||||||
@@ -76,8 +110,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Header from '@/layouts/components/Header.vue'
|
import Header from '@/layouts/components/Header.vue'
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
||||||
|
import { updateUserInfo, getVerificationCode } from '@/api/user'
|
||||||
import { showMessage } from '@/composables/util.js'
|
import { showMessage } from '@/composables/util.js'
|
||||||
import Sidebar from '@/layouts/components/Sidebar.vue'
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
|
||||||
@@ -203,4 +238,109 @@ function submitInvite() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pwForm = reactive({ name: '', phone: '', newPassword: '', confirmPassword: '', code: '' })
|
||||||
|
const pwFormRef = ref()
|
||||||
|
const pwLoading = ref(false)
|
||||||
|
const pwRules = {
|
||||||
|
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
|
||||||
|
newPassword: [],
|
||||||
|
confirmPassword: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (pwForm.newPassword) {
|
||||||
|
if (!value) {
|
||||||
|
callback(new Error('请确认密码'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value !== pwForm.newPassword) {
|
||||||
|
callback(new Error('两次输入的密码不一致'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPw() {
|
||||||
|
if (codeTimer) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
codeCountdown.value = 0
|
||||||
|
codeSending.value = false
|
||||||
|
pwForm.name = ''
|
||||||
|
pwForm.phone = ''
|
||||||
|
pwForm.newPassword = ''
|
||||||
|
pwForm.confirmPassword = ''
|
||||||
|
pwForm.code = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeCountdown = ref(0)
|
||||||
|
const codeSending = ref(false)
|
||||||
|
let codeTimer = null
|
||||||
|
|
||||||
|
async function sendCode() {
|
||||||
|
if (!pwForm.phone) {
|
||||||
|
showMessage('请输入手机号', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (codeSending.value || codeCountdown.value > 0) return
|
||||||
|
codeSending.value = true
|
||||||
|
try {
|
||||||
|
const r = await getVerificationCode({ phone: pwForm.phone })
|
||||||
|
const d = r?.data
|
||||||
|
if (d?.success) {
|
||||||
|
showMessage('验证码已发送', 'success')
|
||||||
|
codeCountdown.value = 60
|
||||||
|
codeTimer = setInterval(() => {
|
||||||
|
if (codeCountdown.value > 0) {
|
||||||
|
codeCountdown.value -= 1
|
||||||
|
} else {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
showMessage(d?.message || '发送验证码失败', 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
codeSending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (codeTimer) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function submitPw() {
|
||||||
|
pwFormRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
pwLoading.value = true
|
||||||
|
try {
|
||||||
|
const r = await updateUserInfo({
|
||||||
|
newPassword: pwForm.newPassword || '',
|
||||||
|
name: pwForm.name || '',
|
||||||
|
phone: pwForm.phone,
|
||||||
|
code: pwForm.code
|
||||||
|
})
|
||||||
|
const d = r?.data
|
||||||
|
if (d?.success) {
|
||||||
|
showMessage('用户信息修改成功', 'success')
|
||||||
|
resetPw()
|
||||||
|
} else {
|
||||||
|
showMessage(d?.message || '用户信息修改失败', 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pwLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<Sidebar></Sidebar>
|
<Sidebar></Sidebar>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
<el-main class="">
|
<el-main class="h-full">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div class="lg:col-span-1 flex flex-col gap-6">
|
<div class="lg:col-span-1 flex flex-col gap-6">
|
||||||
<div class="panel-shell p-6">
|
<div class="panel-shell p-6">
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-container>
|
<el-container class="pt-4">
|
||||||
<el-aside width="200px" class="pt-4">
|
<el-main class="h-full">
|
||||||
<Sidebar />
|
|
||||||
</el-aside>
|
|
||||||
<el-main class="p-4">
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="panel-shell p-6">
|
||||||
<div class="text-lg font-semibold mb-4">学生详情</div>
|
<div class="text-lg font-semibold mb-4">学生详情</div>
|
||||||
<template v-if="detail">
|
<template v-if="detail">
|
||||||
<el-descriptions :column="1" border>
|
<el-descriptions :column="1" border>
|
||||||
@@ -19,46 +16,43 @@
|
|||||||
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
|
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
|
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
|
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade
|
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade }}</el-descriptions-item>
|
||||||
}}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-empty description="请从班级页跳转" />
|
<el-empty description="请从班级页跳转" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-shell p-6">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<div class="text-lg font-semibold mb-4">学生词汇统计</div>
|
<div class="text-lg font-semibold mb-4">学生词汇统计</div>
|
||||||
<template v-if="wordStat">
|
<template v-if="wordStat">
|
||||||
<el-descriptions :column="1" border>
|
<el-descriptions :column="1" border>
|
||||||
<el-descriptions-item label="已掌握">{{ wordStat.masteredWordCount
|
<el-descriptions-item label="已掌握">{{ wordStat.masteredWordCount }}</el-descriptions-item>
|
||||||
}}</el-descriptions-item>
|
<el-descriptions-item label="未掌握">{{ wordStat.unmasteredWordCount }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="未掌握">{{ wordStat.unmasteredWordCount
|
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount }}</el-descriptions-item>
|
||||||
}}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount
|
|
||||||
}}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-empty description="暂无统计" />
|
<el-empty description="暂无统计" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-shell p-6">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<div class="text-md font-semibold mb-3">学生考试记录</div>
|
<div class="text-md font-semibold mb-3">学生考试记录</div>
|
||||||
<ExamHistoryChart :data="history" />
|
<ExamHistoryChart :data="history" />
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="panel-shell p-6">
|
||||||
<div class="text-md font-semibold mb-3">学生学案记录</div>
|
<div class="text-md font-semibold mb-3">学生学案记录</div>
|
||||||
<PlanHistoryChart :student-id="route.params.id" />
|
<PlanHistoryChart :student-id="route.params.id" />
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
<div class="panel-shell p-6 lg:col-span-2">
|
||||||
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
|
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
|
||||||
<WordMasteryHeatmap :student-id="route.params.id" :columns="50" />
|
<WordMasteryHeatmap :student-id="route.params.id" :columns="50" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-shell p-6 lg:col-span-2">
|
||||||
|
<div class="text-md font-semibold mb-3">学情分析</div>
|
||||||
<StudyAnalysis :student-id="route.params.id" />
|
<StudyAnalysis :student-id="route.params.id" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
</el-container>
|
</el-container>
|
||||||
@@ -105,6 +99,10 @@ async function fetchExamHistory() {
|
|||||||
history.value = Array.isArray(d?.data) ? d.data.slice().sort((a, b) => {
|
history.value = Array.isArray(d?.data) ? d.data.slice().sort((a, b) => {
|
||||||
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
||||||
}) : []
|
}) : []
|
||||||
|
// 遍历 history 中的 startDate 去掉其中的 T
|
||||||
|
history.value.forEach(item => {
|
||||||
|
item.startDate = item.startDate.replace('T', ' ')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchWordStat() {
|
async function fetchWordStat() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-layout">
|
<div class="common-layout">
|
||||||
<el-container>
|
<el-container class="min-h-screen">
|
||||||
<el-header>
|
<el-header>
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|||||||
Reference in New Issue
Block a user