Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
23c17f34a9 | |||
735024b7ce | |||
8609d8bee5 | |||
dc0dfbc037 | |||
a361be9609 | |||
132f14e850 | |||
6933748af3 |
@ -56,7 +56,8 @@ public class SecurityConfiguration {
|
|||||||
.requestMatchers("/api/auth/**", "/error").permitAll()
|
.requestMatchers("/api/auth/**", "/error").permitAll()
|
||||||
.requestMatchers("/images/**").permitAll()
|
.requestMatchers("/images/**").permitAll()
|
||||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||||
.anyRequest().hasAnyRole(Const.ROLE_DEFAULT)
|
.requestMatchers("/api/admin/**").hasRole(Const.ROLE_ADMIN)
|
||||||
|
.anyRequest().hasAnyRole(Const.ROLE_DEFAULT, Const.ROLE_ADMIN)
|
||||||
)
|
)
|
||||||
.formLogin(conf -> conf
|
.formLogin(conf -> conf
|
||||||
.loginProcessingUrl("/api/auth/login")
|
.loginProcessingUrl("/api/auth/login")
|
||||||
@ -104,6 +105,10 @@ public class SecurityConfiguration {
|
|||||||
} else if(exceptionOrAuthentication instanceof Authentication authentication){
|
} else if(exceptionOrAuthentication instanceof Authentication authentication){
|
||||||
User user = (User) authentication.getPrincipal();
|
User user = (User) authentication.getPrincipal();
|
||||||
Account account = service.findAccountByNameOrEmail(user.getUsername());
|
Account account = service.findAccountByNameOrEmail(user.getUsername());
|
||||||
|
if(account.isBanned()) {
|
||||||
|
writer.write(RestBean.forbidden("登录失败,此账户已被封禁").asJsonString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
String jwt = utils.createJwt(user, account.getUsername(), account.getId());
|
String jwt = utils.createJwt(user, account.getUsername(), account.getId());
|
||||||
if(jwt == null) {
|
if(jwt == null) {
|
||||||
writer.write(RestBean.forbidden("登录验证频繁,请稍后再试").asJsonString());
|
writer.write(RestBean.forbidden("登录验证频繁,请稍后再试").asJsonString());
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package com.example.controller;
|
package com.example.controller;
|
||||||
|
|
||||||
import com.example.entity.RestBean;
|
import com.example.entity.RestBean;
|
||||||
|
import com.example.entity.dto.Account;
|
||||||
import com.example.entity.dto.Interact;
|
import com.example.entity.dto.Interact;
|
||||||
import com.example.entity.vo.request.AddCommentVO;
|
import com.example.entity.vo.request.AddCommentVO;
|
||||||
import com.example.entity.vo.request.TopicCreateVO;
|
import com.example.entity.vo.request.TopicCreateVO;
|
||||||
import com.example.entity.vo.request.TopicUpdateVO;
|
import com.example.entity.vo.request.TopicUpdateVO;
|
||||||
import com.example.entity.vo.response.*;
|
import com.example.entity.vo.response.*;
|
||||||
|
import com.example.service.AccountService;
|
||||||
import com.example.service.TopicService;
|
import com.example.service.TopicService;
|
||||||
import com.example.service.WeatherService;
|
import com.example.service.WeatherService;
|
||||||
import com.example.utils.Const;
|
import com.example.utils.Const;
|
||||||
@ -32,6 +34,9 @@ public class ForumController {
|
|||||||
@Resource
|
@Resource
|
||||||
ControllerUtils utils;
|
ControllerUtils utils;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
AccountService accountService;
|
||||||
|
|
||||||
@GetMapping("/weather")
|
@GetMapping("/weather")
|
||||||
public RestBean<WeatherVO> weather(double longitude, double latitude){
|
public RestBean<WeatherVO> weather(double longitude, double latitude){
|
||||||
WeatherVO vo = service.fetchWeather(longitude, latitude);
|
WeatherVO vo = service.fetchWeather(longitude, latitude);
|
||||||
@ -51,6 +56,10 @@ public class ForumController {
|
|||||||
@PostMapping("/create-topic")
|
@PostMapping("/create-topic")
|
||||||
public RestBean<Void> createTopic(@Valid @RequestBody TopicCreateVO vo,
|
public RestBean<Void> createTopic(@Valid @RequestBody TopicCreateVO vo,
|
||||||
@RequestAttribute(Const.ATTR_USER_ID) int id) {
|
@RequestAttribute(Const.ATTR_USER_ID) int id) {
|
||||||
|
Account account = accountService.findAccountById(id);
|
||||||
|
if(account.isMute()) {
|
||||||
|
return RestBean.forbidden("您已被禁言,无法创建新的主题");
|
||||||
|
}
|
||||||
return utils.messageHandle(() -> topicService.createTopic(id, vo));
|
return utils.messageHandle(() -> topicService.createTopic(id, vo));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +103,10 @@ public class ForumController {
|
|||||||
@PostMapping("/add-comment")
|
@PostMapping("/add-comment")
|
||||||
public RestBean<Void> addComment(@Valid @RequestBody AddCommentVO vo,
|
public RestBean<Void> addComment(@Valid @RequestBody AddCommentVO vo,
|
||||||
@RequestAttribute(Const.ATTR_USER_ID) int id){
|
@RequestAttribute(Const.ATTR_USER_ID) int id){
|
||||||
|
Account account = accountService.findAccountById(id);
|
||||||
|
if(account.isMute()) {
|
||||||
|
return RestBean.forbidden("您已被禁言,无法创建新的回复");
|
||||||
|
}
|
||||||
return utils.messageHandle(() -> topicService.createComment(id, vo));
|
return utils.messageHandle(() -> topicService.createComment(id, vo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
package com.example.controller.admin;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.example.entity.RestBean;
|
||||||
|
import com.example.entity.dto.Account;
|
||||||
|
import com.example.entity.dto.AccountDetails;
|
||||||
|
import com.example.entity.dto.AccountPrivacy;
|
||||||
|
import com.example.entity.vo.response.AccountVO;
|
||||||
|
import com.example.service.AccountDetailsService;
|
||||||
|
import com.example.service.AccountPrivacyService;
|
||||||
|
import com.example.service.AccountService;
|
||||||
|
import com.example.utils.Const;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/user")
|
||||||
|
public class AccountAdminController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
AccountService service;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
AccountDetailsService detailsService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
AccountPrivacyService privacyService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
StringRedisTemplate template;
|
||||||
|
|
||||||
|
@Value("${spring.security.jwt.expire}")
|
||||||
|
private int expire;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
public RestBean<JSONObject> accountList(int page, int size) {
|
||||||
|
JSONObject object = new JSONObject();
|
||||||
|
List<AccountVO> list = service.page(Page.of(page, size))
|
||||||
|
.getRecords()
|
||||||
|
.stream()
|
||||||
|
.map(a -> a.asViewObject(AccountVO.class))
|
||||||
|
.toList();
|
||||||
|
object.put("total", service.count());
|
||||||
|
object.put("list", list);
|
||||||
|
return RestBean.success(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/detail")
|
||||||
|
public RestBean<JSONObject> accountDetail(int id) {
|
||||||
|
JSONObject object = new JSONObject();
|
||||||
|
object.put("detail", detailsService.findAccountDetailsById(id));
|
||||||
|
object.put("privacy", privacyService.accountPrivacy(id));
|
||||||
|
return RestBean.success(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
public RestBean<Void> saveAccount(@RequestBody JSONObject object) {
|
||||||
|
int id = object.getInteger("id");
|
||||||
|
Account account = service.findAccountById(id);
|
||||||
|
Account save = object.toJavaObject(Account.class);
|
||||||
|
handleBanned(account, save);
|
||||||
|
BeanUtils.copyProperties(save, account, "password", "registerTime");
|
||||||
|
service.saveOrUpdate(account);
|
||||||
|
AccountDetails details = detailsService.findAccountDetailsById(id);
|
||||||
|
AccountDetails saveDetails = object.getJSONObject("detail").toJavaObject(AccountDetails.class);
|
||||||
|
BeanUtils.copyProperties(saveDetails, details);
|
||||||
|
detailsService.saveOrUpdate(details);
|
||||||
|
AccountPrivacy privacy = privacyService.accountPrivacy(id);
|
||||||
|
AccountPrivacy savePrivacy = object.getJSONObject("privacy").toJavaObject(AccountPrivacy.class);
|
||||||
|
BeanUtils.copyProperties(savePrivacy, privacy);
|
||||||
|
privacyService.saveOrUpdate(savePrivacy);
|
||||||
|
return RestBean.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleBanned(Account old, Account current) {
|
||||||
|
String key = Const.BANNED_BLOCK + old.getId();
|
||||||
|
if(!old.isBanned() && current.isBanned()) {
|
||||||
|
template.opsForValue().set(key, "true", expire, TimeUnit.HOURS);
|
||||||
|
} else if(old.isBanned() && !current.isBanned()) {
|
||||||
|
template.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
|||||||
import com.example.entity.BaseData;
|
import com.example.entity.BaseData;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ import java.util.Date;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("db_account")
|
@TableName("db_account")
|
||||||
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class Account implements BaseData {
|
public class Account implements BaseData {
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
@ -24,4 +26,6 @@ public class Account implements BaseData {
|
|||||||
String role;
|
String role;
|
||||||
String avatar;
|
String avatar;
|
||||||
Date registerTime;
|
Date registerTime;
|
||||||
|
boolean mute;
|
||||||
|
boolean banned;
|
||||||
}
|
}
|
||||||
|
@ -12,4 +12,6 @@ public class AccountVO {
|
|||||||
String role;
|
String role;
|
||||||
String avatar;
|
String avatar;
|
||||||
Date registerTime;
|
Date registerTime;
|
||||||
|
boolean mute;
|
||||||
|
boolean banned;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import jakarta.servlet.FilterChain;
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@ -27,6 +28,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
@Resource
|
@Resource
|
||||||
JwtUtils utils;
|
JwtUtils utils;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
StringRedisTemplate template;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request,
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
@ -35,11 +39,15 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
DecodedJWT jwt = utils.resolveJwt(authorization);
|
DecodedJWT jwt = utils.resolveJwt(authorization);
|
||||||
if(jwt != null) {
|
if(jwt != null) {
|
||||||
UserDetails user = utils.toUser(jwt);
|
UserDetails user = utils.toUser(jwt);
|
||||||
UsernamePasswordAuthenticationToken authentication =
|
if(!template.hasKey(Const.BANNED_BLOCK + utils.toId(jwt))) {
|
||||||
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt));
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt));
|
||||||
|
} else {
|
||||||
|
utils.invalidateJwt(authorization);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
if(this.existsAccountByUsername(username)) return "该用户名已被他人使用,请重新更换";
|
if(this.existsAccountByUsername(username)) return "该用户名已被他人使用,请重新更换";
|
||||||
String password = passwordEncoder.encode(info.getPassword());
|
String password = passwordEncoder.encode(info.getPassword());
|
||||||
Account account = new Account(null, info.getUsername(),
|
Account account = new Account(null, info.getUsername(),
|
||||||
password, email, Const.ROLE_DEFAULT, null, new Date());
|
password, email, Const.ROLE_DEFAULT, null, new Date(), false, false);
|
||||||
if(!this.save(account)) {
|
if(!this.save(account)) {
|
||||||
return "内部错误,注册失败";
|
return "内部错误,注册失败";
|
||||||
} else {
|
} else {
|
||||||
|
@ -10,6 +10,7 @@ public final class Const {
|
|||||||
//请求频率限制
|
//请求频率限制
|
||||||
public final static String FLOW_LIMIT_COUNTER = "flow:counter:";
|
public final static String FLOW_LIMIT_COUNTER = "flow:counter:";
|
||||||
public final static String FLOW_LIMIT_BLOCK = "flow:block:";
|
public final static String FLOW_LIMIT_BLOCK = "flow:block:";
|
||||||
|
public final static String BANNED_BLOCK = "banned:block:";
|
||||||
//邮件验证码
|
//邮件验证码
|
||||||
public final static String VERIFY_EMAIL_LIMIT = "verify:email:limit:";
|
public final static String VERIFY_EMAIL_LIMIT = "verify:email:limit:";
|
||||||
public final static String VERIFY_EMAIL_DATA = "verify:email:data:";
|
public final static String VERIFY_EMAIL_DATA = "verify:email:data:";
|
||||||
@ -22,6 +23,7 @@ public final class Const {
|
|||||||
public final static String MQ_MAIL = "mail";
|
public final static String MQ_MAIL = "mail";
|
||||||
//用户角色
|
//用户角色
|
||||||
public final static String ROLE_DEFAULT = "user";
|
public final static String ROLE_DEFAULT = "user";
|
||||||
|
public final static String ROLE_ADMIN = "admin";
|
||||||
//论坛相关
|
//论坛相关
|
||||||
public final static String FORUM_WEATHER_CACHE = "weather:cache:";
|
public final static String FORUM_WEATHER_CACHE = "weather:cache:";
|
||||||
public final static String FORUM_IMAGE_COUNTER = "forum:image:";
|
public final static String FORUM_IMAGE_COUNTER = "forum:image:";
|
||||||
|
@ -7,7 +7,7 @@ spring:
|
|||||||
mail:
|
mail:
|
||||||
host: smtp.163.com
|
host: smtp.163.com
|
||||||
username: javastudy111@163.com
|
username: javastudy111@163.com
|
||||||
password: QGLYGBVECIASGPWQ
|
password: SFxfE39P9Wf2icKi
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
addresses: localhost
|
addresses: localhost
|
||||||
username: admin
|
username: admin
|
||||||
@ -44,4 +44,4 @@ spring:
|
|||||||
username: 'minio'
|
username: 'minio'
|
||||||
password: 'password'
|
password: 'password'
|
||||||
weather:
|
weather:
|
||||||
key: 7abbc24d3b4443b597a3b3c676e0f221
|
key: 115bcaf74fb24f73844365290d85862b
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useDark, useToggle } from '@vueuse/core'
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
|
import {onMounted, provide, ref} from "vue";
|
||||||
|
import {isUnauthorized} from "@/net";
|
||||||
|
import {apiUserInfo} from "@/net/api/user";
|
||||||
|
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||||
|
|
||||||
useDark({
|
useDark({
|
||||||
selector: 'html',
|
selector: 'html',
|
||||||
@ -12,18 +16,26 @@ useDark({
|
|||||||
onChanged(dark) { useToggle(dark) }
|
onChanged(dark) { useToggle(dark) }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
provide('userLoading', loading)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(!isUnauthorized()) {
|
||||||
|
apiUserInfo(loading)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<el-config-provider :locale="zhCn">
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</el-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
header {
|
.wrapper {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {get} from "@/net";
|
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import LightCard from "@/components/LightCard.vue";
|
import LightCard from "@/components/LightCard.vue";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import TopicTag from "@/components/TopicTag.vue";
|
import TopicTag from "@/components/TopicTag.vue";
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
|
import {apiForumCollect, apiForumCollectDelete} from "@/net/api/forum";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
show: Boolean
|
show: Boolean
|
||||||
@ -15,11 +15,11 @@ const emit = defineEmits(['close'])
|
|||||||
const list = ref([])
|
const list = ref([])
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
get('/api/forum/collects', data => list.value = data)
|
apiForumCollect(data => list.value = data)
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteCollect(index, tid) {
|
function deleteCollect(index, tid) {
|
||||||
get(`/api/forum/interact?tid=${tid}&type=collect&state=false`, () => {
|
apiForumCollectDelete(tid, () => {
|
||||||
ElMessage.success('已取消收藏!')
|
ElMessage.success('已取消收藏!')
|
||||||
list.value.splice(index, 1)
|
list.value.splice(index, 1)
|
||||||
})
|
})
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
import {Delta, QuillEditor} from "@vueup/vue-quill";
|
import {Delta, QuillEditor} from "@vueup/vue-quill";
|
||||||
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import {post} from "@/net";
|
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
|
import {apiForumCommentSubmit} from "@/net/api/forum";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
@ -22,7 +22,7 @@ function submitComment() {
|
|||||||
ElMessage.warning('评论字数已经超出最大限制,请缩减评论内容!')
|
ElMessage.warning('评论字数已经超出最大限制,请缩减评论内容!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
post('/api/forum/add-comment', {
|
apiForumCommentSubmit({
|
||||||
tid: props.tid,
|
tid: props.tid,
|
||||||
quote: props.quote ? props.quote.id : -1,
|
quote: props.quote ? props.quote.id : -1,
|
||||||
content: JSON.stringify(content.value)
|
content: JSON.stringify(content.value)
|
||||||
|
@ -6,10 +6,11 @@ import ImageResize from "quill-image-resize-vue";
|
|||||||
import { ImageExtend, QuillWatch } from "quill-image-super-solution-module";
|
import { ImageExtend, QuillWatch } from "quill-image-super-solution-module";
|
||||||
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {accessHeader, post} from "@/net";
|
import {accessHeader} from "@/net";
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
import ColorDot from "@/components/ColorDot.vue";
|
import ColorDot from "@/components/ColorDot.vue";
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
|
import {apiForumTopicCreate} from "@/net/api/forum";
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
submit: {
|
submit: {
|
||||||
default: (editor, success) => {
|
default: (editor, success) => {
|
||||||
post('/api/forum/create-topic', {
|
apiForumTopicCreate({
|
||||||
type: editor.type.id,
|
type: editor.type.id,
|
||||||
title: editor.title,
|
title: editor.title,
|
||||||
content: editor.text
|
content: editor.text
|
||||||
|
97
my-project-frontend/src/components/UserInfo.vue
Normal file
97
my-project-frontend/src/components/UserInfo.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<script setup>
|
||||||
|
import {Back, Message, Operation, Right} from "@element-plus/icons-vue";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import {isRoleAdmin, logout} from "@/net";
|
||||||
|
import router from "@/router";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const isAdminPage = computed(() => route.fullPath.startsWith("/admin"))
|
||||||
|
|
||||||
|
function userLogout() {
|
||||||
|
logout(() => router.push("/"))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="user-info">
|
||||||
|
<template v-if="isRoleAdmin()">
|
||||||
|
<el-button type="primary" size="small"
|
||||||
|
@click="router.push('/index')"
|
||||||
|
v-if="isAdminPage">
|
||||||
|
回到用户端
|
||||||
|
<el-icon style="margin-left: 5px">
|
||||||
|
<Right/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" size="small"
|
||||||
|
@click="router.push('/admin')"
|
||||||
|
v-else>
|
||||||
|
前往管理端
|
||||||
|
<el-icon style="margin-left: 5px">
|
||||||
|
<Right/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<slot/>
|
||||||
|
<div class="profile">
|
||||||
|
<div>{{ store.user.username }}</div>
|
||||||
|
<div>{{ store.user.email }}</div>
|
||||||
|
</div>
|
||||||
|
<el-dropdown>
|
||||||
|
<el-avatar :src="store.avatarUrl"/>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-icon>
|
||||||
|
<Operation/>
|
||||||
|
</el-icon>
|
||||||
|
个人设置
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-icon>
|
||||||
|
<Message/>
|
||||||
|
</el-icon>
|
||||||
|
消息列表
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="userLogout" divided>
|
||||||
|
<el-icon>
|
||||||
|
<Back/>
|
||||||
|
</el-icon>
|
||||||
|
退出登录
|
||||||
|
</el-dropdown-item>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-info {
|
||||||
|
width: 320px;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.el-avatar:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
font-size: 10px;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
48
my-project-frontend/src/net/api/forum.js
Normal file
48
my-project-frontend/src/net/api/forum.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import {get, post} from "@/net";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
|
||||||
|
export const apiForumTypes = (success) =>
|
||||||
|
get('/api/forum/types', success)
|
||||||
|
|
||||||
|
export const apiForumTopic = (tid, success) =>
|
||||||
|
get(`api/forum/topic?tid=${tid}`, success)
|
||||||
|
|
||||||
|
export const apiForumInteract = (tid, type, topic, message) => {
|
||||||
|
get(`/api/forum/interact?tid=${tid}&type=${type}&state=${!topic[type]}`, () => {
|
||||||
|
topic[type] = !topic[type]
|
||||||
|
if(topic[type])
|
||||||
|
ElMessage.success(`${message}成功!`)
|
||||||
|
else
|
||||||
|
ElMessage.success(`已取消${message}!`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiForumUpdateTopic = (data, success) =>
|
||||||
|
post('/api/forum/update-topic', data, success)
|
||||||
|
|
||||||
|
export const apiForumComments = (tid, page, success) =>
|
||||||
|
get(`/api/forum/comments?tid=${tid}&page=${page}`, success)
|
||||||
|
|
||||||
|
export const apiForumCommentDelete = (id, success) =>
|
||||||
|
get(`/api/forum/delete-comment?id=${id}`, success)
|
||||||
|
|
||||||
|
export const apiForumCommentSubmit = (data, success) =>
|
||||||
|
post('/api/forum/add-comment', data, success)
|
||||||
|
|
||||||
|
export const apiForumTopicCreate = (data, success) =>
|
||||||
|
post('/api/forum/create-topic', data, success)
|
||||||
|
|
||||||
|
export const apiForumTopTopics = (success) =>
|
||||||
|
get('/api/forum/top-topic', success)
|
||||||
|
|
||||||
|
export const apiForumTopicList = (page, type, success) =>
|
||||||
|
get(`/api/forum/list-topic?page=${page}&type=${type}`, success)
|
||||||
|
|
||||||
|
export const apiForumWeather = (longitude, latitude, success) =>
|
||||||
|
get(`/api/forum/weather?longitude=${longitude}&latitude=${latitude}`, success)
|
||||||
|
|
||||||
|
export const apiForumCollect = (success) =>
|
||||||
|
get('/api/forum/collects', success)
|
||||||
|
|
||||||
|
export const apiForumCollectDelete = (tid, success) =>
|
||||||
|
get(`/api/forum/interact?tid=${tid}&type=collect&state=false`, success)
|
88
my-project-frontend/src/net/api/user.js
Normal file
88
my-project-frontend/src/net/api/user.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import {useStore} from "@/store";
|
||||||
|
import {get, post} from "@/net";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
|
export const apiUserInfo = (loadingRef) => {
|
||||||
|
loadingRef.value = true
|
||||||
|
const store = useStore();
|
||||||
|
get('/api/user/info', (data) => {
|
||||||
|
store.user = data
|
||||||
|
loadingRef.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiAuthRegister = (data) => {
|
||||||
|
post('/api/auth/register', data, () => {
|
||||||
|
ElMessage.success('注册成功,欢迎加入我们')
|
||||||
|
router.push("/")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiAuthAskCode = (email, coldTime, type = 'register') => {
|
||||||
|
coldTime.value = 60
|
||||||
|
get(`/api/auth/ask-code?email=${email}&type=${type}`, () => {
|
||||||
|
ElMessage.success(`验证码已发送到邮箱: ${email},请注意查收`)
|
||||||
|
const handle = setInterval(() => {
|
||||||
|
coldTime.value--
|
||||||
|
if(coldTime.value === 0) {
|
||||||
|
clearInterval(handle)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}, (message) => {
|
||||||
|
ElMessage.warning(message)
|
||||||
|
coldTime.value = 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiAuthRestConfirm = (data, activeRef) =>
|
||||||
|
post('/api/auth/reset-confirm', data, () => activeRef.value++)
|
||||||
|
|
||||||
|
export const apiAuthResetPassword = (data) => {
|
||||||
|
post('/api/auth/reset-password', data, () => {
|
||||||
|
ElMessage.success('密码重置成功,请重新登录')
|
||||||
|
router.push('/')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiUserPrivacy = (success) =>
|
||||||
|
get('/api/user/privacy', success)
|
||||||
|
|
||||||
|
export const apiUserPrivacySave = (data, loadingRef) => {
|
||||||
|
loadingRef.value = true
|
||||||
|
post('/api/user/save-privacy', data, () => {
|
||||||
|
ElMessage.success('隐私设置修改成功!')
|
||||||
|
loadingRef.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiUserChangePassword = (data, success) =>
|
||||||
|
post('/api/user/change-password', data, success)
|
||||||
|
|
||||||
|
export const apiUserDetail = (success) =>
|
||||||
|
get('/api/user/details', success)
|
||||||
|
|
||||||
|
export const apiUserDetailSave = (form, success, failure) =>
|
||||||
|
post('/api/user/save-details', form, success, failure)
|
||||||
|
|
||||||
|
export const apiUserModifyEmail = (form, success) =>
|
||||||
|
post('/api/user/modify-email', form, success)
|
||||||
|
|
||||||
|
export const apiNotificationList = (success) =>
|
||||||
|
get('/api/notification/list', success)
|
||||||
|
|
||||||
|
export const apiNotificationDeleteAll = (success) =>
|
||||||
|
get(`/api/notification/delete-all`, success)
|
||||||
|
|
||||||
|
export const apiNotificationDelete = (id, success) =>
|
||||||
|
get(`/api/notification/delete?id=${id}`, success)
|
||||||
|
|
||||||
|
export const apiUserList = (page, size, success) =>
|
||||||
|
get(`api/admin/user/list?page=${page}&size=${size}`, success)
|
||||||
|
|
||||||
|
export const apiUserDetailTotal = (id, success) =>
|
||||||
|
get(`api/admin/user/detail?id=${id}`, success)
|
||||||
|
|
||||||
|
export const apiUserSave = (data, success) =>
|
||||||
|
post('/api/admin/user/save', data, success)
|
||||||
|
|
@ -6,7 +6,7 @@ const authItemName = "authorize"
|
|||||||
|
|
||||||
const accessHeader = () => {
|
const accessHeader = () => {
|
||||||
return {
|
return {
|
||||||
'Authorization': `Bearer ${takeAccessToken()}`
|
'Authorization': `Bearer ${takeAccessToken()?.token}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,14 +34,11 @@ function takeAccessToken() {
|
|||||||
ElMessage.warning("登录状态已过期,请重新登录!")
|
ElMessage.warning("登录状态已过期,请重新登录!")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return authObj.token
|
return authObj
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeAccessToken(remember, token, expire){
|
function storeAccessToken(remember, token, expire, role){
|
||||||
const authObj = {
|
const authObj = { token, expire, role }
|
||||||
token: token,
|
|
||||||
expire: expire
|
|
||||||
}
|
|
||||||
const str = JSON.stringify(authObj)
|
const str = JSON.stringify(authObj)
|
||||||
if(remember)
|
if(remember)
|
||||||
localStorage.setItem(authItemName, str)
|
localStorage.setItem(authItemName, str)
|
||||||
@ -90,7 +87,7 @@ function login(username, password, remember, success, failure = defaultFailure){
|
|||||||
}, {
|
}, {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
}, (data) => {
|
}, (data) => {
|
||||||
storeAccessToken(remember, data.token, data.expire)
|
storeAccessToken(remember, data.token, data.expire, data.role)
|
||||||
ElMessage.success(`登录成功,欢迎 ${data.username} 来到我们的系统`)
|
ElMessage.success(`登录成功,欢迎 ${data.username} 来到我们的系统`)
|
||||||
success(data)
|
success(data)
|
||||||
}, failure)
|
}, failure)
|
||||||
@ -112,8 +109,12 @@ function get(url, success, failure = defaultFailure) {
|
|||||||
internalGet(url, accessHeader(), success, failure)
|
internalGet(url, accessHeader(), success, failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
function unauthorized() {
|
function isUnauthorized() {
|
||||||
return !takeAccessToken()
|
return !takeAccessToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
export { post, get, login, logout, unauthorized, accessHeader }
|
function isRoleAdmin() {
|
||||||
|
return takeAccessToken()?.role === 'admin'
|
||||||
|
}
|
||||||
|
|
||||||
|
export { post, get, login, logout, isUnauthorized, isRoleAdmin, accessHeader }
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { unauthorized } from "@/net";
|
import {isRoleAdmin, isUnauthorized} from "@/net";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@ -53,15 +53,36 @@ const router = createRouter({
|
|||||||
component: () => import('@/views/settings/PrivacySetting.vue')
|
component: () => import('@/views/settings/PrivacySetting.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}, {
|
||||||
|
path: '/admin',
|
||||||
|
name: 'admin',
|
||||||
|
component: () => import('@/views/AdminView.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'admin-welcome',
|
||||||
|
component: () => import('@/views/admin/WelcomeAdmin.vue')
|
||||||
|
}, {
|
||||||
|
path: 'user',
|
||||||
|
name: 'admin-user',
|
||||||
|
component: () => import('@/views/admin/UserAdmin.vue')
|
||||||
|
}, {
|
||||||
|
path: 'forum',
|
||||||
|
name: 'admin-forum',
|
||||||
|
component: () => import('@/views/admin/ForumAdmin.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
const isUnauthorized = unauthorized()
|
const unauthorized = isUnauthorized(), admin = isRoleAdmin()
|
||||||
if(to.name.startsWith('welcome') && !isUnauthorized) {
|
if(to.name.startsWith('welcome') && !unauthorized) {
|
||||||
next('/index')
|
next('/index')
|
||||||
} else if(to.fullPath.startsWith('/index') && isUnauthorized) {
|
} else if(to.fullPath.startsWith('/admin') && !admin) {
|
||||||
|
next('/index')
|
||||||
|
}else if(to.fullPath.startsWith('/index') && unauthorized) {
|
||||||
next('/')
|
next('/')
|
||||||
} else {
|
} else {
|
||||||
next()
|
next()
|
||||||
|
190
my-project-frontend/src/views/AdminView.vue
Normal file
190
my-project-frontend/src/views/AdminView.vue
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
ChatDotSquare, Collection,
|
||||||
|
DataLine,
|
||||||
|
Document,
|
||||||
|
Files,
|
||||||
|
Location,
|
||||||
|
Monitor, Notification, Position, School,
|
||||||
|
Umbrella,
|
||||||
|
User
|
||||||
|
} from "@element-plus/icons-vue";
|
||||||
|
import UserInfo from "@/components/UserInfo.vue";
|
||||||
|
import {inject, onMounted, ref} from "vue";
|
||||||
|
import router from "@/router";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
|
const adminMenu = [
|
||||||
|
{
|
||||||
|
title: '校园论坛管理', icon: Location, sub: [
|
||||||
|
{ title: '用户管理', icon: User, index: '/admin/user' },
|
||||||
|
{ title: '帖子广场管理', icon: ChatDotSquare, index: '/admin/forum' },
|
||||||
|
{ title: '失物招领管理', icon: Bell },
|
||||||
|
{ title: '校园活动管理', icon: Notification },
|
||||||
|
{ title: '表白墙管理', icon: Umbrella },
|
||||||
|
{ title: '合作机构管理', icon: School }
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
title: '探索与发现管理', icon: Position, sub: [
|
||||||
|
{ title: '成绩管理', icon: Document },
|
||||||
|
{ title: '课程表管理', icon: Files },
|
||||||
|
{ title: '教务通知管理', icon: Monitor },
|
||||||
|
{ title: '在线图书馆管理', icon: Collection },
|
||||||
|
{ title: '预约教室管理', icon: DataLine }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const loading = inject('userLoading')
|
||||||
|
const pageTabs = ref([])
|
||||||
|
|
||||||
|
function handleTabClick({ props }) {
|
||||||
|
router.push(props.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabClose(name) {
|
||||||
|
const index = pageTabs.value.findIndex(tab => tab.name === name)
|
||||||
|
const isCurrent = name === route.fullPath
|
||||||
|
pageTabs.value.splice(index, 1)
|
||||||
|
if(pageTabs.value.length > 0) {
|
||||||
|
//删除后,标签列表中还有剩余的Tab且关闭的是当前的,则自动进行切换,默认切换到上一个,如果没有上一个,则切换到下一个
|
||||||
|
if(isCurrent) {
|
||||||
|
router.push(pageTabs.value[Math.max(0, index - 1)].name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
router.push('/admin')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAdminTab(menu) {
|
||||||
|
if(!menu.index) return
|
||||||
|
if(pageTabs.value.findIndex(tab => tab.name === menu.index) < 0) {
|
||||||
|
pageTabs.value.push({
|
||||||
|
title: menu.title,
|
||||||
|
name: menu.index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const initPage = adminMenu
|
||||||
|
.flatMap(menu => menu.sub)
|
||||||
|
.find(sub => sub.index === route.fullPath)
|
||||||
|
if(initPage) {
|
||||||
|
addAdminTab(initPage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="admin-content" v-loading="loading" element-loading-text="正在进入,请稍后...">
|
||||||
|
<el-container style="height: 100%">
|
||||||
|
<el-aside width="230px" class="admin-content-aside">
|
||||||
|
<div class="logo-box">
|
||||||
|
<el-image class="logo" src="https://element-plus.org/images/element-plus-logo.svg"/>
|
||||||
|
</div>
|
||||||
|
<el-scrollbar style="height: calc(100vh - 57px)">
|
||||||
|
<el-menu
|
||||||
|
router
|
||||||
|
:default-active="$route.path"
|
||||||
|
:default-openeds="['1', '2']"
|
||||||
|
style="min-height: calc(100vh - 57px);border: none">
|
||||||
|
<el-sub-menu :index="(index + 1).toString()"
|
||||||
|
v-for="(menu, index) in adminMenu">
|
||||||
|
<template #title>
|
||||||
|
<el-icon>
|
||||||
|
<component :is="menu.icon"/>
|
||||||
|
</el-icon>
|
||||||
|
<span><b>{{ menu.title }}</b></span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item :index="subMenu.index"
|
||||||
|
@click="addAdminTab(subMenu)"
|
||||||
|
v-for="subMenu in menu.sub">
|
||||||
|
<template #title>
|
||||||
|
<el-icon>
|
||||||
|
<component :is="subMenu.icon"/>
|
||||||
|
</el-icon>
|
||||||
|
{{ subMenu.title }}
|
||||||
|
</template>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-aside>
|
||||||
|
<el-container>
|
||||||
|
<el-header class="admin-content-header">
|
||||||
|
<div style="flex: 1">
|
||||||
|
<el-tabs type="card"
|
||||||
|
:model-value="route.fullPath"
|
||||||
|
closable
|
||||||
|
@tab-remove="handleTabClose"
|
||||||
|
@tab-click="handleTabClick">
|
||||||
|
<el-tab-pane v-for="tab in pageTabs"
|
||||||
|
:label="tab.title"
|
||||||
|
:name="tab.name"
|
||||||
|
:key="tab.name"/>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
<user-info/>
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<keep-alive>
|
||||||
|
<component :is="Component" />
|
||||||
|
</keep-alive>
|
||||||
|
</router-view>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-content {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
.admin-content-aside {
|
||||||
|
border-right: solid 1px var(--el-border-color);
|
||||||
|
|
||||||
|
.logo-box {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px 0 10px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-content-header {
|
||||||
|
border-bottom: solid 1px var(--el-border-color);
|
||||||
|
height: 55px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
:deep(.el-tabs__header) {
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__nav) {
|
||||||
|
gap: 10px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__item) {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: solid 1px var(--el-border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,23 +1,45 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {get, logout} from '@/net'
|
import {inject, reactive, ref} from "vue";
|
||||||
import router from "@/router";
|
|
||||||
import {useStore} from "@/store";
|
|
||||||
import {reactive, ref} from "vue";
|
|
||||||
import {
|
import {
|
||||||
Back,
|
|
||||||
Bell,
|
Bell,
|
||||||
ChatDotSquare, Check, Collection, DataLine,
|
ChatDotSquare, Check, Collection, DataLine,
|
||||||
Document, Files,
|
Document, Files,
|
||||||
Location, Lock, Message, Monitor,
|
Location, Lock, Monitor,
|
||||||
Notification, Operation,
|
Notification, Operation,
|
||||||
Position,
|
Position,
|
||||||
School, Search,
|
School, Search,
|
||||||
Umbrella, User
|
Umbrella, User
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
import LightCard from "@/components/LightCard.vue";
|
import LightCard from "@/components/LightCard.vue";
|
||||||
|
import UserInfo from "@/components/UserInfo.vue";
|
||||||
|
import {apiNotificationDelete, apiNotificationDeleteAll, apiNotificationList} from "@/net/api/user";
|
||||||
|
|
||||||
const store = useStore()
|
const userMenu = [
|
||||||
const loading = ref(true)
|
{
|
||||||
|
title: '校园论坛', icon: Location, sub: [
|
||||||
|
{ title: '帖子广场', icon: ChatDotSquare, index: '/index' },
|
||||||
|
{ title: '失物招领', icon: Bell },
|
||||||
|
{ title: '校园活动', icon: Notification },
|
||||||
|
{ title: '表白墙', icon: Umbrella },
|
||||||
|
{ title: '海文考研', icon: School }
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
title: '探索与发现', icon: Position, sub: [
|
||||||
|
{ title: '成绩查询', icon: Document },
|
||||||
|
{ title: '班级课程表', icon: Files },
|
||||||
|
{ title: '教务通知', icon: Monitor },
|
||||||
|
{ title: '在线图书馆', icon: Collection },
|
||||||
|
{ title: '预约教室', icon: DataLine }
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
title: '个人设置', icon: Operation, sub: [
|
||||||
|
{ title: '个人信息设置', icon: User, index: '/index/user-setting' },
|
||||||
|
{ title: '账号安全设置', icon: Lock, index: '/index/privacy-setting' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const loading = inject('userLoading')
|
||||||
|
|
||||||
const searchInput = reactive({
|
const searchInput = reactive({
|
||||||
type: '1',
|
type: '1',
|
||||||
@ -25,27 +47,19 @@ const searchInput = reactive({
|
|||||||
})
|
})
|
||||||
const notification = ref([])
|
const notification = ref([])
|
||||||
|
|
||||||
get('/api/user/info', (data) => {
|
|
||||||
store.user = data
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
const loadNotification =
|
const loadNotification =
|
||||||
() => get('/api/notification/list', data => notification.value = data)
|
() => apiNotificationList(data => notification.value = data)
|
||||||
loadNotification()
|
loadNotification()
|
||||||
|
|
||||||
function userLogout() {
|
|
||||||
logout(() => router.push("/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmNotification(id, url) {
|
function confirmNotification(id, url) {
|
||||||
get(`/api/notification/delete?id=${id}`, () => {
|
apiNotificationDelete(id, () => {
|
||||||
loadNotification()
|
loadNotification()
|
||||||
window.open(url)
|
window.open(url)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAllNotification() {
|
function deleteAllNotification() {
|
||||||
get(`/api/notification/delete-all`, loadNotification)
|
apiNotificationDeleteAll(loadNotification)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -53,7 +67,9 @@ function deleteAllNotification() {
|
|||||||
<div class="main-content" v-loading="loading" element-loading-text="正在进入,请稍后...">
|
<div class="main-content" v-loading="loading" element-loading-text="正在进入,请稍后...">
|
||||||
<el-container style="height: 100%" v-if="!loading">
|
<el-container style="height: 100%" v-if="!loading">
|
||||||
<el-header class="main-content-header">
|
<el-header class="main-content-header">
|
||||||
<el-image class="logo" src="https://element-plus.org/images/element-plus-logo.svg"/>
|
<div style="width: 320px;height: 32px">
|
||||||
|
<el-image class="logo" src="https://element-plus.org/images/element-plus-logo.svg"/>
|
||||||
|
</div>
|
||||||
<div style="flex: 1;padding: 0 20px;text-align: center">
|
<div style="flex: 1;padding: 0 20px;text-align: center">
|
||||||
<el-input v-model="searchInput.text" style="width: 100%;max-width: 500px"
|
<el-input v-model="searchInput.text" style="width: 100%;max-width: 500px"
|
||||||
placeholder="搜索论坛相关内容...">
|
placeholder="搜索论坛相关内容...">
|
||||||
@ -72,10 +88,10 @@ function deleteAllNotification() {
|
|||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info">
|
<user-info>
|
||||||
<el-popover placement="bottom" :width="350" trigger="click">
|
<el-popover placement="bottom" :width="350" trigger="click">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-badge style="margin-right: 15px" is-dot :hidden="!notification.length">
|
<el-badge is-dot :hidden="!notification.length">
|
||||||
<div class="notification">
|
<div class="notification">
|
||||||
<el-icon><Bell/></el-icon>
|
<el-icon><Bell/></el-icon>
|
||||||
<div style="font-size: 10px">消息</div>
|
<div style="font-size: 10px">消息</div>
|
||||||
@ -101,34 +117,7 @@ function deleteAllNotification() {
|
|||||||
style="width: 100%" plain>清除全部未读消息</el-button>
|
style="width: 100%" plain>清除全部未读消息</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
<div class="profile">
|
</user-info>
|
||||||
<div>{{ store.user.username }}</div>
|
|
||||||
<div>{{ store.user.email }}</div>
|
|
||||||
</div>
|
|
||||||
<el-dropdown>
|
|
||||||
<el-avatar :src="store.avatarUrl"/>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-item>
|
|
||||||
<el-icon>
|
|
||||||
<Operation/>
|
|
||||||
</el-icon>
|
|
||||||
个人设置
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item>
|
|
||||||
<el-icon>
|
|
||||||
<Message/>
|
|
||||||
</el-icon>
|
|
||||||
消息列表
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item @click="userLogout" divided>
|
|
||||||
<el-icon>
|
|
||||||
<Back/>
|
|
||||||
</el-icon>
|
|
||||||
退出登录
|
|
||||||
</el-dropdown-item>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</div>
|
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-aside width="230px">
|
<el-aside width="230px">
|
||||||
@ -138,124 +127,20 @@ function deleteAllNotification() {
|
|||||||
:default-active="$route.path"
|
:default-active="$route.path"
|
||||||
:default-openeds="['1', '2', '3']"
|
:default-openeds="['1', '2', '3']"
|
||||||
style="min-height: calc(100vh - 55px)">
|
style="min-height: calc(100vh - 55px)">
|
||||||
<el-sub-menu index="1">
|
<el-sub-menu :index="(index + 1).toString()"
|
||||||
|
v-for="(menu, index) in userMenu">
|
||||||
<template #title>
|
<template #title>
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Location/>
|
<component :is="menu.icon"/>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span><b>校园论坛</b></span>
|
<span><b>{{ menu.title }}</b></span>
|
||||||
</template>
|
</template>
|
||||||
<el-menu-item index="/index">
|
<el-menu-item :index="subMenu.index" v-for="subMenu in menu.sub">
|
||||||
<template #title>
|
<template #title>
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<ChatDotSquare/>
|
<component :is="subMenu.icon"/>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
帖子广场
|
{{ subMenu.title }}
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item>
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<Bell/>
|
|
||||||
</el-icon>
|
|
||||||
失物招领
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item>
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<Notification/>
|
|
||||||
</el-icon>
|
|
||||||
校园活动
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item>
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<Umbrella/>
|
|
||||||
</el-icon>
|
|
||||||
表白墙
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item>
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<School/>
|
|
||||||
</el-icon>
|
|
||||||
海文考研
|
|
||||||
<el-tag style="margin-left: 10px" size="small">合作机构</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
</el-sub-menu>
|
|
||||||
<el-sub-menu index="2">
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<Position/>
|
|
||||||
</el-icon>
|
|
||||||
<span><b>探索与发现</b></span>
|
|
||||||
</template>
|
|
||||||
<el-menu-item>
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<Document/>
|
|
||||||
</el-icon>
|
|
||||||
成绩查询
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item>
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<Files/>
|
|
||||||
</el-icon>
|
|
||||||
班级课程表
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item>
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<Monitor/>
|
|
||||||
</el-icon>
|
|
||||||
教务通知
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item>
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<Collection/>
|
|
||||||
</el-icon>
|
|
||||||
在线图书馆
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item>
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<DataLine/>
|
|
||||||
</el-icon>
|
|
||||||
预约教室
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
</el-sub-menu>
|
|
||||||
<el-sub-menu index="3">
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<Operation/>
|
|
||||||
</el-icon>
|
|
||||||
<span><b>个人设置</b></span>
|
|
||||||
</template>
|
|
||||||
<el-menu-item index="/index/user-setting">
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<User/>
|
|
||||||
</el-icon>
|
|
||||||
个人信息设置
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/index/privacy-setting">
|
|
||||||
<template #title>
|
|
||||||
<el-icon>
|
|
||||||
<Lock/>
|
|
||||||
</el-icon>
|
|
||||||
账号安全设置
|
|
||||||
</template>
|
</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
|
13
my-project-frontend/src/views/admin/ForumAdmin.vue
Normal file
13
my-project-frontend/src/views/admin/ForumAdmin.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
我是论坛管理
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
181
my-project-frontend/src/views/admin/UserAdmin.vue
Normal file
181
my-project-frontend/src/views/admin/UserAdmin.vue
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<script setup>
|
||||||
|
import {EditPen, User} from "@element-plus/icons-vue";
|
||||||
|
import {apiUserDetailTotal, apiUserList, apiUserSave} from "@/net/api/user";
|
||||||
|
import {reactive, watchEffect} from "vue";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const editor = reactive({
|
||||||
|
id: 0,
|
||||||
|
display: false,
|
||||||
|
temp: {},
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const userTable = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0,
|
||||||
|
data: []
|
||||||
|
})
|
||||||
|
|
||||||
|
function userStatus(user) {
|
||||||
|
if(user.mute && user.banned)
|
||||||
|
return '禁言中、封禁中'
|
||||||
|
else if(user.mute)
|
||||||
|
return '禁言中'
|
||||||
|
else if(user.banned)
|
||||||
|
return '封禁中'
|
||||||
|
else
|
||||||
|
return '正常'
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUserEditor(user) {
|
||||||
|
editor.id = user.id
|
||||||
|
editor.display = true
|
||||||
|
editor.loading = true
|
||||||
|
apiUserDetailTotal(editor.id, data => {
|
||||||
|
editor.temp = { ...data, ...user }
|
||||||
|
editor.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUserDetail() {
|
||||||
|
editor.display = false
|
||||||
|
apiUserSave(editor.temp, () => {
|
||||||
|
const user = userTable.data.find(user => user.id === editor.id)
|
||||||
|
Object.assign(user, editor.temp)
|
||||||
|
ElMessage.success('数据保存成功')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => apiUserList(userTable.page, userTable.size, data => {
|
||||||
|
userTable.total = data.total
|
||||||
|
userTable.data = data.list
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="user-admin">
|
||||||
|
<div class="title">
|
||||||
|
<el-icon><User/></el-icon>
|
||||||
|
论坛用户列表
|
||||||
|
</div>
|
||||||
|
<div class="desc">
|
||||||
|
在这里管理论坛的所有用户,包括账号信息、封禁和禁言处理。
|
||||||
|
</div>
|
||||||
|
<el-table :data="userTable.data" height="320">
|
||||||
|
<el-table-column prop="id" label="编号" width="80"/>
|
||||||
|
<el-table-column label="用户名" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="table-username">
|
||||||
|
<el-avatar :size="30" :src="store.avatarUserUrl(row.avatar)"/>
|
||||||
|
<div>{{ row.username }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="角色" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="danger" v-if="row.role === 'admin'">管理员</el-tag>
|
||||||
|
<el-tag v-else>普通用户</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="email" label="电子邮件"/>
|
||||||
|
<el-table-column label="注册时间">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ new Date(row.registerTime).toLocaleString() }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ userStatus(row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" :icon="EditPen"
|
||||||
|
@click="openUserEditor(row)"
|
||||||
|
:disabled="row.role === 'admin'">编辑</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination :total="userTable.total"
|
||||||
|
v-model:current-page="userTable.page"
|
||||||
|
v-model:page-size="userTable.size"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"/>
|
||||||
|
</div>
|
||||||
|
<el-drawer v-model="editor.display">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: bold">
|
||||||
|
<el-icon><EditPen/></el-icon> 编辑用户信息
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 13px">编辑完成后请点击下方保存按钮</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="editor.temp.username"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="电子邮件">
|
||||||
|
<el-input v-model="editor.temp.email"/>
|
||||||
|
</el-form-item>
|
||||||
|
<div style="display: flex;font-size: 14px;gap: 20px">
|
||||||
|
<div>
|
||||||
|
<span style="margin-right: 10px">禁言</span>
|
||||||
|
<el-switch v-model="editor.temp.mute"/>
|
||||||
|
</div>
|
||||||
|
<el-divider style="height: 30px" direction="vertical"/>
|
||||||
|
<div>
|
||||||
|
<span style="margin-right: 10px">账号封禁</span>
|
||||||
|
<el-switch v-model="editor.temp.banned"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 10px;color: #606266;font-size: 14px">
|
||||||
|
注册时间: {{ new Date(editor.temp.registerTime).toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<el-divider/>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div style="text-align: center">
|
||||||
|
<el-button type="success" @click="saveUserDetail">保存</el-button>
|
||||||
|
<el-button type="info" @click="editor.display = false">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.user-admin {
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
color: #bababa;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-username {
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-drawer__header) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
13
my-project-frontend/src/views/admin/WelcomeAdmin.vue
Normal file
13
my-project-frontend/src/views/admin/WelcomeAdmin.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
我是欢迎页
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@ -1,10 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {get} from "@/net";
|
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
|
import {apiForumTypes} from "@/net/api/forum";
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
get('/api/forum/types', data => {
|
apiForumTypes(data => {
|
||||||
const array = []
|
const array = []
|
||||||
array.push({name: '全部', id: 0, color: 'linear-gradient(45deg, white, red, orange, gold, green, blue)'})
|
array.push({name: '全部', id: 0, color: 'linear-gradient(45deg, white, red, orange, gold, green, blue)'})
|
||||||
data.forEach(d => array.push(d))
|
data.forEach(d => array.push(d))
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {get, post} from "@/net";
|
import {reactive, ref} from "vue";
|
||||||
import axios from "axios";
|
|
||||||
import {computed, reactive, ref} from "vue";
|
|
||||||
import {ArrowLeft, ChatSquare, CircleCheck, Delete, EditPen, Female, Male, Plus, Star} from "@element-plus/icons-vue";
|
import {ArrowLeft, ChatSquare, CircleCheck, Delete, EditPen, Female, Male, Plus, Star} from "@element-plus/icons-vue";
|
||||||
import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html';
|
import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html';
|
||||||
import Card from "@/components/Card.vue";
|
import Card from "@/components/Card.vue";
|
||||||
@ -13,6 +11,13 @@ import {ElMessage} from "element-plus";
|
|||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import TopicEditor from "@/components/TopicEditor.vue";
|
import TopicEditor from "@/components/TopicEditor.vue";
|
||||||
import TopicCommentEditor from "@/components/TopicCommentEditor.vue";
|
import TopicCommentEditor from "@/components/TopicCommentEditor.vue";
|
||||||
|
import {
|
||||||
|
apiForumCommentDelete,
|
||||||
|
apiForumComments,
|
||||||
|
apiForumInteract,
|
||||||
|
apiForumTopic,
|
||||||
|
apiForumUpdateTopic
|
||||||
|
} from "@/net/api/forum";
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
@ -33,7 +38,7 @@ const comment = reactive({
|
|||||||
quote: null
|
quote: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const init = () => get(`api/forum/topic?tid=${tid}`, data => {
|
const init = () => apiForumTopic(tid, data => {
|
||||||
topic.data = data
|
topic.data = data
|
||||||
topic.like = data.interact.like
|
topic.like = data.interact.like
|
||||||
topic.collect = data.interact.collect
|
topic.collect = data.interact.collect
|
||||||
@ -48,17 +53,11 @@ function convertToHtml(content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function interact(type, message) {
|
function interact(type, message) {
|
||||||
get(`/api/forum/interact?tid=${tid}&type=${type}&state=${!topic[type]}`, () => {
|
apiForumInteract(tid, type, topic, message)
|
||||||
topic[type] = !topic[type]
|
|
||||||
if(topic[type])
|
|
||||||
ElMessage.success(`${message}成功!`)
|
|
||||||
else
|
|
||||||
ElMessage.success(`已取消${message}!`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTopic(editor) {
|
function updateTopic(editor) {
|
||||||
post('/api/forum/update-topic', {
|
apiForumUpdateTopic({
|
||||||
id: tid,
|
id: tid,
|
||||||
type: editor.type.id,
|
type: editor.type.id,
|
||||||
title: editor.title,
|
title: editor.title,
|
||||||
@ -73,7 +72,7 @@ function updateTopic(editor) {
|
|||||||
function loadComments(page) {
|
function loadComments(page) {
|
||||||
topic.comments = null
|
topic.comments = null
|
||||||
topic.page = page
|
topic.page = page
|
||||||
get(`/api/forum/comments?tid=${tid}&page=${page - 1}`, data => topic.comments = data)
|
apiForumComments(tid, page - 1, data => topic.comments = data)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCommentAdd() {
|
function onCommentAdd() {
|
||||||
@ -82,7 +81,7 @@ function onCommentAdd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteComment(id) {
|
function deleteComment(id) {
|
||||||
get(`/api/forum/delete-comment?id=${id}`, () => {
|
apiForumCommentDelete(id, () => {
|
||||||
ElMessage.success('删除评论成功!')
|
ElMessage.success('删除评论成功!')
|
||||||
loadComments(topic.page)
|
loadComments(topic.page)
|
||||||
})
|
})
|
||||||
|
@ -13,8 +13,7 @@ import {
|
|||||||
Microphone, CircleCheck, Star, FolderOpened, ArrowRightBold
|
Microphone, CircleCheck, Star, FolderOpened, ArrowRightBold
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
import Weather from "@/components/Weather.vue";
|
import Weather from "@/components/Weather.vue";
|
||||||
import {computed, reactive, ref, watch} from "vue";
|
import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||||
import {get} from "@/net";
|
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
import TopicEditor from "@/components/TopicEditor.vue";
|
import TopicEditor from "@/components/TopicEditor.vue";
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
@ -22,6 +21,7 @@ import ColorDot from "@/components/ColorDot.vue";
|
|||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import TopicTag from "@/components/TopicTag.vue";
|
import TopicTag from "@/components/TopicTag.vue";
|
||||||
import TopicCollectList from "@/components/TopicCollectList.vue";
|
import TopicCollectList from "@/components/TopicCollectList.vue";
|
||||||
|
import {apiForumTopicList, apiForumTopTopics, apiForumWeather} from "@/net/api/forum";
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
@ -47,10 +47,10 @@ const today = computed(() => {
|
|||||||
const date = new Date()
|
const date = new Date()
|
||||||
return `${date.getFullYear()} 年 ${date.getMonth() + 1} 月 ${date.getDate()} 日`
|
return `${date.getFullYear()} 年 ${date.getMonth() + 1} 月 ${date.getDate()} 日`
|
||||||
})
|
})
|
||||||
get('/api/forum/top-topic', data => topics.top = data)
|
|
||||||
function updateList(){
|
function updateList(){
|
||||||
if(topics.end) return
|
if(topics.end) return
|
||||||
get(`/api/forum/list-topic?page=${topics.page}&type=${topics.type}`, data => {
|
apiForumTopicList(topics.page, topics.type, data => {
|
||||||
if(data) {
|
if(data) {
|
||||||
data.forEach(d => topics.list.push(d))
|
data.forEach(d => topics.list.push(d))
|
||||||
topics.page++
|
topics.page++
|
||||||
@ -75,14 +75,14 @@ function resetList() {
|
|||||||
navigator.geolocation.getCurrentPosition(position => {
|
navigator.geolocation.getCurrentPosition(position => {
|
||||||
const longitude = position.coords.longitude
|
const longitude = position.coords.longitude
|
||||||
const latitude = position.coords.latitude
|
const latitude = position.coords.latitude
|
||||||
get(`/api/forum/weather?longitude=${longitude}&latitude=${latitude}`, data => {
|
apiForumWeather(longitude, latitude, data => {
|
||||||
Object.assign(weather, data)
|
Object.assign(weather, data)
|
||||||
weather.success = true
|
weather.success = true
|
||||||
})
|
})
|
||||||
}, error => {
|
}, error => {
|
||||||
console.info(error)
|
console.info(error)
|
||||||
ElMessage.warning('位置信息获取超时,请检测网络设置')
|
ElMessage.warning('位置信息获取超时,请检测网络设置')
|
||||||
get(`/api/forum/weather?longitude=116.40529&latitude=39.90499`, data => {
|
apiForumWeather(116.40529, 39.90499, data => {
|
||||||
Object.assign(weather, data)
|
Object.assign(weather, data)
|
||||||
weather.success = true
|
weather.success = true
|
||||||
})
|
})
|
||||||
@ -90,6 +90,10 @@ navigator.geolocation.getCurrentPosition(position => {
|
|||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
enableHighAccuracy: true
|
enableHighAccuracy: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
apiForumTopTopics(data => topics.top = data)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
import Card from "@/components/Card.vue";
|
import Card from "@/components/Card.vue";
|
||||||
import {Setting, Switch, Lock} from "@element-plus/icons-vue";
|
import {Setting, Switch, Lock} from "@element-plus/icons-vue";
|
||||||
import {reactive, ref} from "vue";
|
import {onMounted, reactive, ref} from "vue";
|
||||||
import {get, post} from "@/net";
|
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
|
import {apiUserChangePassword, apiUserPrivacy, apiUserPrivacySave} from "@/net/api/user";
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
password: '',
|
password: '',
|
||||||
new_password: '',
|
new_password: '',
|
||||||
new_password_repeat: ''
|
new_password_repeat: ''
|
||||||
})
|
})
|
||||||
const validatePassword = (rule, value, callback) => {
|
const validatePassword = (_, value, callback) => {
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
callback(new Error('请再次输入密码'))
|
callback(new Error('请再次输入密码'))
|
||||||
} else if (value !== form.new_password) {
|
} else if (value !== form.new_password) {
|
||||||
@ -40,7 +40,7 @@ const onValidate = (prop, isValid) => valid.value = isValid
|
|||||||
function resetPassword(){
|
function resetPassword(){
|
||||||
formRef.value.validate(valid => {
|
formRef.value.validate(valid => {
|
||||||
if(valid) {
|
if(valid) {
|
||||||
post('/api/user/change-password', form, () => {
|
apiUserChangePassword(form, () => {
|
||||||
ElMessage.success('修改密码成功!')
|
ElMessage.success('修改密码成功!')
|
||||||
formRef.value.resetFields();
|
formRef.value.resetFields();
|
||||||
})
|
})
|
||||||
@ -56,24 +56,17 @@ const privacy = reactive({
|
|||||||
email: false,
|
email: false,
|
||||||
gender: false
|
gender: false
|
||||||
})
|
})
|
||||||
get('/api/user/privacy', data => {
|
|
||||||
privacy.phone = data.phone
|
|
||||||
privacy.email = data.email
|
|
||||||
privacy.wx = data.wx
|
|
||||||
privacy.qq = data.qq
|
|
||||||
privacy.gender = data.gender
|
|
||||||
saving.value = false
|
|
||||||
})
|
|
||||||
function savePrivacy(type, status){
|
function savePrivacy(type, status){
|
||||||
saving.value = true
|
apiUserPrivacySave({ type, status }, saving)
|
||||||
post('/api/user/save-privacy', {
|
}
|
||||||
type: type,
|
|
||||||
status: status
|
onMounted(() => {
|
||||||
}, () => {
|
apiUserPrivacy(data => {
|
||||||
ElMessage.success('隐私设置修改成功!')
|
Object.assign(privacy, data)
|
||||||
saving.value = false
|
saving.value = false
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -3,10 +3,11 @@
|
|||||||
import Card from "@/components/Card.vue";
|
import Card from "@/components/Card.vue";
|
||||||
import {Message, Refresh, Select, User} from "@element-plus/icons-vue";
|
import {Message, Refresh, Select, User} from "@element-plus/icons-vue";
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import {computed, reactive, ref} from "vue";
|
import {computed, onMounted, reactive, ref} from "vue";
|
||||||
import {accessHeader, get, post} from "@/net";
|
import {accessHeader} from "@/net";
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import {apiAuthAskCode, apiUserDetail, apiUserDetailSave, apiUserModifyEmail} from "@/net/api/user";
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ const emailForm = reactive({
|
|||||||
email: '',
|
email: '',
|
||||||
code: ''
|
code: ''
|
||||||
})
|
})
|
||||||
const validateUsername = (rule, value, callback) => {
|
const validateUsername = (_, value, callback) => {
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
callback(new Error('请输入用户名'))
|
callback(new Error('请输入用户名'))
|
||||||
} else if (!/^[a-zA-Z0-9\u4e00-\u9fa5]+$/.test(value)) {
|
} else if (!/^[a-zA-Z0-9\u4e00-\u9fa5]+$/.test(value)) {
|
||||||
@ -55,12 +56,12 @@ function saveDetails() {
|
|||||||
baseFormRef.value.validate(isValid => {
|
baseFormRef.value.validate(isValid => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
loading.base = true
|
loading.base = true
|
||||||
post('/api/user/save-details', baseForm, () => {
|
apiUserDetailSave(baseForm, () => {
|
||||||
ElMessage.success('用户信息保存成功')
|
ElMessage.success('用户信息保存成功')
|
||||||
store.user.usernamew = baseForm.username
|
store.user.usernamew = baseForm.username
|
||||||
desc.value = baseForm.desc
|
desc.value = baseForm.desc
|
||||||
loading.base = false
|
loading.base = false
|
||||||
}, (message) => {
|
}, message => {
|
||||||
ElMessage.warning(message)
|
ElMessage.warning(message)
|
||||||
loading.base = false
|
loading.base = false
|
||||||
})
|
})
|
||||||
@ -68,17 +69,6 @@ function saveDetails() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get('/api/user/details', data => {
|
|
||||||
baseForm.username = store.user.username
|
|
||||||
baseForm.gender = data.gender
|
|
||||||
baseForm.phone = data.phone
|
|
||||||
baseForm.wx = data.wx
|
|
||||||
baseForm.qq = data.qq
|
|
||||||
baseForm.desc = desc.value = data.desc
|
|
||||||
emailForm.email = store.user.email
|
|
||||||
loading.form = false
|
|
||||||
})
|
|
||||||
|
|
||||||
const coldTime = ref(0)
|
const coldTime = ref(0)
|
||||||
const isEmailValid = ref(true)
|
const isEmailValid = ref(true)
|
||||||
const onValidate = (prop, isValid) => {
|
const onValidate = (prop, isValid) => {
|
||||||
@ -89,19 +79,7 @@ const onValidate = (prop, isValid) => {
|
|||||||
function sendEmailCode() {
|
function sendEmailCode() {
|
||||||
emailFormRef.value.validate(isValid => {
|
emailFormRef.value.validate(isValid => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
coldTime.value = 60
|
apiAuthAskCode(emailForm.email, coldTime, 'modify')
|
||||||
get(`/api/auth/ask-code?email=${emailForm.email}&type=modify`, () => {
|
|
||||||
ElMessage.success(`验证码已成功发送到邮箱:${emailForm.email},请注意查收`)
|
|
||||||
const handle = setInterval(() => {
|
|
||||||
coldTime.value--
|
|
||||||
if (coldTime.value === 0) {
|
|
||||||
clearInterval(handle)
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}, (message) => {
|
|
||||||
ElMessage.warning(message)
|
|
||||||
coldTime.value = 0
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -109,7 +87,7 @@ function sendEmailCode() {
|
|||||||
function modifyEmail() {
|
function modifyEmail() {
|
||||||
emailFormRef.value.validate(isValid => {
|
emailFormRef.value.validate(isValid => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
post('/api/user/modify-email', emailForm, () => {
|
apiUserModifyEmail(emailForm, () => {
|
||||||
ElMessage.success('邮件修改成功')
|
ElMessage.success('邮件修改成功')
|
||||||
store.user.email = emailForm.email
|
store.user.email = emailForm.email
|
||||||
emailForm.code = ''
|
emailForm.code = ''
|
||||||
@ -133,6 +111,16 @@ function uploadSuccess(response){
|
|||||||
ElMessage.success('头像上传成功')
|
ElMessage.success('头像上传成功')
|
||||||
store.user.avatar = response.data
|
store.user.avatar = response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
apiUserDetail(data => {
|
||||||
|
baseForm.username = store.user.username
|
||||||
|
baseForm.desc = desc.value = data.desc
|
||||||
|
Object.assign(baseForm, data)
|
||||||
|
emailForm.email = store.user.email
|
||||||
|
loading.form = false
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -80,9 +80,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {reactive, ref} from "vue";
|
import {reactive, ref} from "vue";
|
||||||
import {EditPen, Lock, Message} from "@element-plus/icons-vue";
|
import {EditPen, Lock, Message} from "@element-plus/icons-vue";
|
||||||
import {get, post} from "@/net";
|
import {apiAuthAskCode, apiAuthResetPassword, apiAuthRestConfirm} from "@/net/api/user";
|
||||||
import {ElMessage} from "element-plus";
|
|
||||||
import router from "@/router";
|
|
||||||
|
|
||||||
const active = ref(0)
|
const active = ref(0)
|
||||||
|
|
||||||
@ -129,29 +127,15 @@ const onValidate = (prop, isValid) => {
|
|||||||
isEmailValid.value = isValid
|
isEmailValid.value = isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateEmail = () => {
|
const validateEmail = () => apiAuthAskCode(form.email, coldTime, 'reset')
|
||||||
coldTime.value = 60
|
|
||||||
get(`/api/auth/ask-code?email=${form.email}&type=reset`, () => {
|
|
||||||
ElMessage.success(`验证码已发送到邮箱: ${form.email},请注意查收`)
|
|
||||||
const handle = setInterval(() => {
|
|
||||||
coldTime.value--
|
|
||||||
if(coldTime.value === 0) {
|
|
||||||
clearInterval(handle)
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}, (message) => {
|
|
||||||
ElMessage.warning(message)
|
|
||||||
coldTime.value = 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmReset = () => {
|
const confirmReset = () => {
|
||||||
formRef.value.validate((isValid) => {
|
formRef.value.validate((isValid) => {
|
||||||
if(isValid) {
|
if(isValid) {
|
||||||
post('/api/auth/reset-confirm', {
|
apiAuthRestConfirm({
|
||||||
email: form.email,
|
email: form.email,
|
||||||
code: form.code
|
code: form.code
|
||||||
}, () => active.value++)
|
}, active)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -159,13 +143,10 @@ const confirmReset = () => {
|
|||||||
const doReset = () => {
|
const doReset = () => {
|
||||||
formRef.value.validate((isValid) => {
|
formRef.value.validate((isValid) => {
|
||||||
if(isValid) {
|
if(isValid) {
|
||||||
post('/api/auth/reset-password', {
|
apiAuthResetPassword({
|
||||||
email: form.email,
|
email: form.email,
|
||||||
code: form.code,
|
code: form.code,
|
||||||
password: form.password
|
password: form.password
|
||||||
}, () => {
|
|
||||||
ElMessage.success('密码重置成功,请重新登录')
|
|
||||||
router.push('/')
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -51,8 +51,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {User, Lock} from '@element-plus/icons-vue'
|
import {User, Lock} from '@element-plus/icons-vue'
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import {reactive, ref} from "vue";
|
import {inject, reactive, ref} from "vue";
|
||||||
import {login} from '@/net'
|
import {login} from '@/net'
|
||||||
|
import {apiUserInfo} from "@/net/api/user";
|
||||||
|
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
@ -70,10 +71,15 @@ const rules = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loading = inject('userLoading')
|
||||||
|
|
||||||
function userLogin() {
|
function userLogin() {
|
||||||
formRef.value.validate((isValid) => {
|
formRef.value.validate((isValid) => {
|
||||||
if(isValid) {
|
if(isValid) {
|
||||||
login(form.username, form.password, form.remember, () => router.push("/index"))
|
login(form.username, form.password, form.remember, () => {
|
||||||
|
apiUserInfo(loading)
|
||||||
|
router.push("/index")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ import {EditPen, Lock, Message, User} from "@element-plus/icons-vue";
|
|||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import {reactive, ref} from "vue";
|
import {reactive, ref} from "vue";
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
import {get, post} from "@/net";
|
import {apiAuthAskCode, apiAuthRegister} from "@/net/api/user";
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
@ -131,14 +131,11 @@ const onValidate = (prop, isValid) => {
|
|||||||
const register = () => {
|
const register = () => {
|
||||||
formRef.value.validate((isValid) => {
|
formRef.value.validate((isValid) => {
|
||||||
if(isValid) {
|
if(isValid) {
|
||||||
post('/api/auth/register', {
|
apiAuthRegister({
|
||||||
username: form.username,
|
username: form.username,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
email: form.email,
|
email: form.email,
|
||||||
code: form.code
|
code: form.code
|
||||||
}, () => {
|
|
||||||
ElMessage.success('注册成功,欢迎加入我们')
|
|
||||||
router.push("/")
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ElMessage.warning('请完整填写注册表单内容!')
|
ElMessage.warning('请完整填写注册表单内容!')
|
||||||
@ -146,21 +143,7 @@ const register = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateEmail = () => {
|
const validateEmail = () => apiAuthAskCode(form.email, coldTime)
|
||||||
coldTime.value = 60
|
|
||||||
get(`/api/auth/ask-code?email=${form.email}&type=register`, () => {
|
|
||||||
ElMessage.success(`验证码已发送到邮箱: ${form.email},请注意查收`)
|
|
||||||
const handle = setInterval(() => {
|
|
||||||
coldTime.value--
|
|
||||||
if(coldTime.value === 0) {
|
|
||||||
clearInterval(handle)
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}, undefined, (message) => {
|
|
||||||
ElMessage.warning(message)
|
|
||||||
coldTime.value = 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user