Compare commits

..

7 Commits
dev ... main

41 changed files with 448 additions and 647 deletions

View File

@ -80,7 +80,7 @@
<dependency> <dependency>
<groupId>com.alibaba.fastjson2</groupId> <groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId> <artifactId>fastjson2</artifactId>
<version>2.0.53</version> <version>2.0.25</version>
</dependency> </dependency>
<!-- Jwt令牌生成校验框架 --> <!-- Jwt令牌生成校验框架 -->
<dependency> <dependency>

View File

@ -1,9 +1,7 @@
package com.example.config; package com.example.config;
import org.springframework.amqp.core.*; import org.springframework.amqp.core.Queue;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.core.QueueBuilder;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -12,42 +10,10 @@ import org.springframework.context.annotation.Configuration;
*/ */
@Configuration @Configuration
public class RabbitConfiguration { public class RabbitConfiguration {
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean("errorQueue")
public Queue dlQueue(){
return QueueBuilder
.durable("error")
.ttl(24 * 60 * 60 * 1000)
.build();
}
@Bean("errorExchange")
public Exchange dlExchange(){
return ExchangeBuilder.directExchange("dlx.direct").build();
}
@Bean("dlBinding") //死信交换机和死信队列进绑定
public Binding dlBinding(@Qualifier("errorExchange") Exchange exchange,
@Qualifier("errorQueue") Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("error-message")
.noargs();
}
@Bean("mailQueue") @Bean("mailQueue")
public Queue queue(){ public Queue queue(){
return QueueBuilder return QueueBuilder
.durable("mail") .durable("mail")
.deadLetterExchange("dlx.direct")
.deadLetterRoutingKey("error-message")
.ttl(3 * 60 * 1000)
.build(); .build();
} }
} }

View File

@ -106,7 +106,7 @@ public class SecurityConfiguration {
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()) { if(account.isBanned()) {
writer.write(RestBean.forbidden("登录失败,此账户已被封禁,请俩系管理员").asJsonString()); writer.write(RestBean.forbidden("登录失败,此账户已被封禁").asJsonString());
return; return;
} }
String jwt = utils.createJwt(user, account.getUsername(), account.getId()); String jwt = utils.createJwt(user, account.getUsername(), account.getId());

View File

@ -1,5 +1,6 @@
package com.example.config; package com.example.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;

View File

@ -32,10 +32,10 @@ public class ForumController {
TopicService topicService; TopicService topicService;
@Resource @Resource
AccountService accountService; ControllerUtils utils;
@Resource @Resource
ControllerUtils utils; AccountService accountService;
@GetMapping("/weather") @GetMapping("/weather")
public RestBean<WeatherVO> weather(double longitude, double latitude){ public RestBean<WeatherVO> weather(double longitude, double latitude){

View File

@ -68,19 +68,23 @@ public class AccountAdminController {
handleBanned(account, save); handleBanned(account, save);
BeanUtils.copyProperties(save, account, "password", "registerTime"); BeanUtils.copyProperties(save, account, "password", "registerTime");
service.saveOrUpdate(account); service.saveOrUpdate(account);
AccountDetails details = detailsService.findAccountDetailsById(id);
AccountDetails saveDetails = object.getJSONObject("detail").toJavaObject(AccountDetails.class); AccountDetails saveDetails = object.getJSONObject("detail").toJavaObject(AccountDetails.class);
detailsService.saveOrUpdate(saveDetails); BeanUtils.copyProperties(saveDetails, details);
detailsService.saveOrUpdate(details);
AccountPrivacy privacy = privacyService.accountPrivacy(id);
AccountPrivacy savePrivacy = object.getJSONObject("privacy").toJavaObject(AccountPrivacy.class); AccountPrivacy savePrivacy = object.getJSONObject("privacy").toJavaObject(AccountPrivacy.class);
BeanUtils.copyProperties(savePrivacy, privacy);
privacyService.saveOrUpdate(savePrivacy); privacyService.saveOrUpdate(savePrivacy);
return RestBean.success(); return RestBean.success();
} }
private void handleBanned(Account old, Account current) { private void handleBanned(Account old, Account current) {
String key = Const.BANNED_BLOCK + old.getId(); String key = Const.BANNED_BLOCK + old.getId();
if(old.isBanned() && !current.isBanned()) { if(!old.isBanned() && current.isBanned()) {
template.delete(key);
} else if(!old.isBanned() && current.isBanned()) {
template.opsForValue().set(key, "true", expire, TimeUnit.HOURS); template.opsForValue().set(key, "true", expire, TimeUnit.HOURS);
} else if(old.isBanned() && !current.isBanned()) {
template.delete(key);
} }
} }
} }

View File

@ -1,30 +0,0 @@
package com.example.entity;
import lombok.Getter;
import lombok.ToString;
import java.util.HashMap;
import java.util.Map;
@Getter
@ToString
public class QueueMessage {
private String messageType;
private final Map<String, Object> data = new HashMap<>();
public static QueueMessage create(String messageType) {
QueueMessage queueMessage = new QueueMessage();
queueMessage.messageType = messageType;
return queueMessage;
}
public QueueMessage put(String key, Object value) {
data.put(key, value);
return this;
}
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) data.get(key);
}
}

View File

@ -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)

View File

@ -1,30 +0,0 @@
package com.example.entity.dto;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("db_verify_email")
public class VerifyEmail {
@TableId(type = IdType.AUTO)
Integer id;
String email;
String type;
String code;
Date time;
boolean success;
public static VerifyEmail success() {
return new VerifyEmail().setSuccess(true);
}
public static VerifyEmail failure() {
return new VerifyEmail().setSuccess(false);
}
}

View File

@ -29,7 +29,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
JwtUtils utils; JwtUtils utils;
@Resource @Resource
private StringRedisTemplate template; StringRedisTemplate template;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, protected void doFilterInternal(HttpServletRequest request,

View File

@ -1,37 +0,0 @@
package com.example.listener;
import com.example.entity.QueueMessage;
import com.example.entity.dto.VerifyEmail;
import com.example.mapper.VerifyEmailMapper;
import com.example.utils.Const;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
@Slf4j
@Component
@RabbitListener(queues = Const.MQ_ERROR)
public class ErrorQueueListener {
@Resource
VerifyEmailMapper mapper;
@RabbitHandler
public void saveErrorToDatabase(QueueMessage message) {
log.error("出现一条错误的队列消息: {}", message);
switch (message.getMessageType()) {
case "email" -> {
VerifyEmail error = VerifyEmail.failure()
.setCode(message.get("code").toString())
.setType(message.get("type"))
.setEmail(message.get("email"))
.setTime(new Date());
mapper.insert(error);
}
}
}
}

View File

@ -1,11 +1,6 @@
package com.example.listener; package com.example.listener;
import com.example.entity.QueueMessage;
import com.example.entity.dto.VerifyEmail;
import com.example.mapper.VerifyEmailMapper;
import com.example.utils.Const;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -13,34 +8,30 @@ import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Date; import java.util.Map;
/** /**
* 用于处理邮件发送的消息队列监听器 * 用于处理邮件发送的消息队列监听器
*/ */
@Slf4j
@Component @Component
@RabbitListener(queues = Const.MQ_MAIL, concurrency = "10") @RabbitListener(queues = "mail")
public class MailQueueListener { public class MailQueueListener {
@Resource @Resource
JavaMailSender sender; JavaMailSender sender;
@Resource
VerifyEmailMapper emailMapper;
@Value("${spring.mail.username}") @Value("${spring.mail.username}")
String username; String username;
/** /**
* 处理邮件发送 * 处理邮件发送
* @param message 邮件信息 * @param data 邮件信息
*/ */
@RabbitHandler @RabbitHandler
public void sendMailMessage(QueueMessage message) { public void sendMailMessage(Map<String, Object> data) {
String email = message.get("email"), type = message.get("type"); String email = data.get("email").toString();
Integer code = message.get("code"); Integer code = (Integer) data.get("code");
SimpleMailMessage mailMessage = switch (type) { SimpleMailMessage message = switch (data.get("type").toString()) {
case "register" -> case "register" ->
createMessage("欢迎注册我们的网站", createMessage("欢迎注册我们的网站",
"您的邮件注册验证码为: "+code+"有效时间3分钟为了保障您的账户安全请勿向他人泄露验证码信息。", "您的邮件注册验证码为: "+code+"有效时间3分钟为了保障您的账户安全请勿向他人泄露验证码信息。",
@ -55,15 +46,8 @@ public class MailQueueListener {
email); email);
default -> null; default -> null;
}; };
if(mailMessage == null) return; if(message == null) return;
log.info("正在向 {} 发送 {} 类型的电子邮件...", email, type); sender.send(message);
sender.send(mailMessage);
VerifyEmail record = VerifyEmail.success()
.setCode(message.get("code").toString())
.setType(message.get("type"))
.setEmail(message.get("email"))
.setTime(new Date());
emailMapper.insert(record);
} }
/** /**

View File

@ -1,9 +0,0 @@
package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.dto.VerifyEmail;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface VerifyEmailMapper extends BaseMapper<VerifyEmail> {
}

View File

@ -2,7 +2,6 @@ package com.example.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.QueueMessage;
import com.example.entity.dto.Account; import com.example.entity.dto.Account;
import com.example.entity.dto.AccountDetails; import com.example.entity.dto.AccountDetails;
import com.example.entity.dto.AccountPrivacy; import com.example.entity.dto.AccountPrivacy;
@ -24,6 +23,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Date; import java.util.Date;
import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -86,9 +86,8 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
return "请求频繁,请稍后再试"; return "请求频繁,请稍后再试";
Random random = new Random(); Random random = new Random();
int code = random.nextInt(899999) + 100000; int code = random.nextInt(899999) + 100000;
QueueMessage message = QueueMessage.create("email"); Map<String, Object> data = Map.of("type",type,"email", email, "code", code);
message.put("type",type).put("email", email).put("code", code); rabbitTemplate.convertAndSend(Const.MQ_MAIL, data);
rabbitTemplate.convertAndSend(Const.MQ_MAIL, message);
stringRedisTemplate.opsForValue() stringRedisTemplate.opsForValue()
.set(Const.VERIFY_EMAIL_DATA + email, String.valueOf(code), 3, TimeUnit.MINUTES); .set(Const.VERIFY_EMAIL_DATA + email, String.valueOf(code), 3, TimeUnit.MINUTES);
return null; return null;

View File

@ -21,7 +21,6 @@ public final class Const {
public final static String ATTR_USER_ID = "userId"; public final static String ATTR_USER_ID = "userId";
//消息队列 //消息队列
public final static String MQ_MAIL = "mail"; public final static String MQ_MAIL = "mail";
public final static String MQ_ERROR = "error";
//用户角色 //用户角色
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 ROLE_ADMIN = "admin";

View File

@ -13,12 +13,6 @@ spring:
username: admin username: admin
password: admin password: admin
virtual-host: / virtual-host: /
listener:
simple:
retry:
enabled: true
max-attempts: 3
initial-interval: 1000ms
datasource: datasource:
url: jdbc:mysql://localhost:3306/study url: jdbc:mysql://localhost:3306/study
username: root username: root

View File

@ -3,7 +3,7 @@ import { useDark, useToggle } from '@vueuse/core'
import {onMounted, provide, ref} from "vue"; import {onMounted, provide, ref} from "vue";
import {isUnauthorized} from "@/net"; import {isUnauthorized} from "@/net";
import {apiUserInfo} from "@/net/api/user"; import {apiUserInfo} from "@/net/api/user";
import zhCn from 'element-plus/es/locale/lang/zh-cn' import zhCn from "element-plus/es/locale/lang/zh-cn";
useDark({ useDark({
selector: 'html', selector: 'html',
@ -12,13 +12,13 @@ useDark({
valueLight: 'light' valueLight: 'light'
}) })
const loading = ref()
provide('userLoading', loading)
useDark({ useDark({
onChanged(dark) { useToggle(dark) } onChanged(dark) { useToggle(dark) }
}) })
const loading = ref(false)
provide('userLoading', loading)
onMounted(() => { onMounted(() => {
if(!isUnauthorized()) { if(!isUnauthorized()) {
apiUserInfo(loading) apiUserInfo(loading)

View File

@ -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)
}) })

View File

@ -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)

View File

@ -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

View File

@ -1,120 +0,0 @@
<script setup>
import {EditPen} from "@element-plus/icons-vue";
import {reactive} from "vue";
import {apiUserDetailTotal, apiUserSave} from "@/net/api/user";
import {ElMessage} from "element-plus";
const editor = reactive({
id: 0,
display: false,
temp: {},
loading: false,
})
function loadUserEditor(user) {
editor.id = user.id
editor.display = true
editor.loading = true
apiUserDetailTotal(editor.id, data => {
editor.temp = { ...data, ...user }
editor.loading = false
})
}
defineExpose({ loadUserEditor })
function saveUserSettings() {
editor.display = false
apiUserSave(editor.temp, () => {
const user = userTable.data.find(user => user.id === editor.id)
Object.assign(user, editor.temp)
ElMessage.success('数据保存成功')
})
}
</script>
<template>
<el-drawer v-model="editor.display" size="380" :close-on-click-modal="false">
<template #header>
<div>
<div style="font-weight: bold">
<el-icon><EditPen/></el-icon>
编辑用户信息
</div>
<div style="font-size: 13px">编辑完成后请点击下方保存按钮</div>
</div>
</template>
<div v-loading="editor.loading" element-loading-text="数据加载中,请稍后..." style="height: 100%">
<el-form label-position="top" v-if="!editor.loading">
<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">
<div>
<span style="margin-right: 10px">禁言</span>
<el-switch v-model="editor.temp.mute"/>
</div>
<el-divider direction="vertical" style="height: 30px;margin: 0 20px"/>
<div>
<span style="margin-right: 10px">账号封禁</span>
<el-switch v-model="editor.temp.banned"/>
</div>
</div>
<div style="margin-top: 20px;color: #606266;font-size: 14px">
注册时间: {{ new Date(editor.temp.registerTime).toLocaleString() }}
</div>
<el-divider direction="horizontal"/>
<el-form-item label="性别">
<el-radio-group v-model="editor.temp.detail.gender">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="editor.temp.detail.phone"/>
</el-form-item>
<el-form-item label="QQ账号">
<el-input v-model="editor.temp.detail.qq"/>
</el-form-item>
<el-form-item label="微信账号">
<el-input v-model="editor.temp.detail.wx"/>
</el-form-item>
<el-form-item label="个人简介">
<el-input type="textarea" :rows="4" v-model="editor.temp.detail.desc"/>
</el-form-item>
<el-divider direction="horizontal"/>
<div style="padding-bottom: 20px">
<div style="margin-bottom: 10px;">隐私设置</div>
<el-checkbox v-model="editor.temp.privacy.phone">
公开展示用户的手机号
</el-checkbox>
<el-checkbox v-model="editor.temp.privacy.email">
公开展示用户的电子邮件地址
</el-checkbox>
<el-checkbox v-model="editor.temp.privacy.wx">
公开展示用户的微信号
</el-checkbox>
<el-checkbox v-model="editor.temp.privacy.qq">
公开展示用户的QQ号
</el-checkbox>
<el-checkbox v-model="editor.temp.privacy.gender">
公开展示用户的性别
</el-checkbox>
</div>
</el-form>
</div>
<template #footer>
<div style="text-align: center">
<el-button type="success" @click="saveUserSettings">保存</el-button>
<el-button type="info" @click="editor.display = false">取消</el-button>
</div>
</template>
</el-drawer>
</template>
<style scoped>
</style>

View File

@ -1,15 +1,16 @@
<script setup> <script setup>
import router from "@/router";
import {Back, Message, Operation, Right} from "@element-plus/icons-vue"; import {Back, Message, Operation, Right} from "@element-plus/icons-vue";
import {useStore} from "@/store"; import {useStore} from "@/store";
import {logout} from "@/net"; import {isRoleAdmin, logout} from "@/net";
import router from "@/router";
defineProps({ import {computed} from "vue";
admin: Boolean import {useRoute} from "vue-router";
})
const route = useRoute()
const store = useStore() const store = useStore()
const isAdminPage = computed(() => route.fullPath.startsWith("/admin"))
function userLogout() { function userLogout() {
logout(() => router.push("/")) logout(() => router.push("/"))
} }
@ -17,10 +18,10 @@ function userLogout() {
<template> <template>
<div class="user-info"> <div class="user-info">
<template v-if="store.isAdmin"> <template v-if="isRoleAdmin()">
<el-button type="primary" size="small" <el-button type="primary" size="small"
@click="router.push('/index')" @click="router.push('/index')"
v-if="admin"> v-if="isAdminPage">
回到用户端 回到用户端
<el-icon style="margin-left: 5px"> <el-icon style="margin-left: 5px">
<Right/> <Right/>
@ -35,7 +36,7 @@ function userLogout() {
</el-icon> </el-icon>
</el-button> </el-button>
</template> </template>
<template/> <slot/>
<div class="profile"> <div class="profile">
<div>{{ store.user.username }}</div> <div>{{ store.user.username }}</div>
<div>{{ store.user.email }}</div> <div>{{ store.user.email }}</div>
@ -68,10 +69,16 @@ function userLogout() {
<style scoped> <style scoped>
.user-info { .user-info {
width: 320px;
display: flex; display: flex;
gap: 20px; gap: 20px;
justify-content: flex-end;
align-items: center; align-items: center;
.el-avatar:hover {
cursor: pointer;
}
.profile { .profile {
text-align: right; text-align: right;

View 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)

View File

@ -1,40 +1,88 @@
import {get, post} from "@/net";
import {useStore} from "@/store"; import {useStore} from "@/store";
import {get, post} from "@/net";
import {ElMessage} from "element-plus";
import router from "@/router";
export const apiUserInfo = (loadingRef) => { export const apiUserInfo = (loadingRef) => {
if(loadingRef) loadingRef.value = true loadingRef.value = true
get('/api/user/info', (data) => {
const store = useStore(); const store = useStore();
get('/api/user/info', (data) => {
store.user = data store.user = data
if(loadingRef) loadingRef.value = false loadingRef.value = false
}) })
} }
export const apiUserChangePassword = (form, success) => export const apiAuthRegister = (data) => {
post('/api/user/change-password', form, success) 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) => export const apiUserPrivacy = (success) =>
get('/api/user/privacy', success) get('/api/user/privacy', success)
export const apiUserPrivacySave = (data, loadingRef, success) => { export const apiUserPrivacySave = (data, loadingRef) => {
loadingRef.value = true loadingRef.value = true
post('/api/user/save-privacy', data, () => { post('/api/user/save-privacy', data, () => {
ElMessage.success('隐私设置修改成功!')
loadingRef.value = false loadingRef.value = false
success()
}) })
} }
export const apiUserDetailSave = (form, success, failure) => export const apiUserChangePassword = (data, success) =>
post('/api/user/save-details', form, success, failure) post('/api/user/change-password', data, success)
export const apiUserDetail = (success) => export const apiUserDetail = (success) =>
get('/api/user/details', 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) => export const apiUserList = (page, size, success) =>
get(`/api/admin/user/list?page=${page}&size=${size}`, success) get(`api/admin/user/list?page=${page}&size=${size}`, success)
export const apiUserDetailTotal = (id, success) => export const apiUserDetailTotal = (id, success) =>
get(`api/admin/user/detail?id=${id}`, success) get(`api/admin/user/detail?id=${id}`, success)
export const apiUserSave = (data, success) => export const apiUserSave = (data, success) =>
post('/api/admin/user/save', data, success) post('/api/admin/user/save', data, success)

View File

@ -113,8 +113,8 @@ function isUnauthorized() {
return !takeAccessToken() return !takeAccessToken()
} }
function isAdminRole() { function isRoleAdmin() {
return takeAccessToken()?.role === 'admin' return takeAccessToken()?.role === 'admin'
} }
export { post, get, login, logout, isUnauthorized, isAdminRole, accessHeader } export { post, get, login, logout, isUnauthorized, isRoleAdmin, accessHeader }

View File

@ -1,6 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import {isAdminRole, isUnauthorized} from "@/net"; import {isRoleAdmin, isUnauthorized} from "@/net";
import {useStore} from "@/store";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -71,10 +70,6 @@ const router = createRouter({
path: 'forum', path: 'forum',
name: 'admin-forum', name: 'admin-forum',
component: () => import('@/views/admin/ForumAdmin.vue') component: () => import('@/views/admin/ForumAdmin.vue')
}, {
path: 'email',
name: 'admin-email',
component: () => import('@/views/admin/EmailAdmin.vue')
} }
] ]
} }
@ -82,7 +77,7 @@ const router = createRouter({
}) })
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const unauthorized = isUnauthorized(), admin = isAdminRole() const unauthorized = isUnauthorized(), admin = isRoleAdmin()
if(to.name.startsWith('welcome') && !unauthorized) { if(to.name.startsWith('welcome') && !unauthorized) {
next('/index') next('/index')
} else if(to.fullPath.startsWith('/admin') && !admin) { } else if(to.fullPath.startsWith('/admin') && !admin) {

View File

@ -17,9 +17,6 @@ export const useStore = defineStore('general', {
} }
} }
}, getters: { }, getters: {
isAdmin() {
return this.user.role === 'admin'
},
avatarUrl() { avatarUrl() {
if(this.user.avatar) if(this.user.avatar)
return `${axios.defaults.baseURL}/images${this.user.avatar}` return `${axios.defaults.baseURL}/images${this.user.avatar}`

View File

@ -1,25 +1,25 @@
<script setup> <script setup>
import { import {
Bell, Bell,
ChatDotSquare, Collection, ChatDotSquare, Collection,
DataLine, DataLine,
Document, Document,
Files, Files,
Location, Message, Location,
Monitor, Notification, Position, School, Monitor, Notification, Position, School,
Umbrella, Umbrella,
User User
} from "@element-plus/icons-vue"; } from "@element-plus/icons-vue";
import UserInfo from "@/components/UserInfo.vue"; import UserInfo from "@/components/UserInfo.vue";
import {inject, onMounted, ref} from "vue"; import {inject, onMounted, ref} from "vue";
import {useRoute} from "vue-router";
import router from "@/router"; import router from "@/router";
import {useRoute} from "vue-router";
const adminMenu = [ const adminMenu = [
{ {
title: '校园论坛管理', icon: Location, sub: [ title: '校园论坛管理', icon: Location, sub: [
{ title: '用户管理', icon: User, index: '/admin/user' }, { title: '用户管理', icon: User, index: '/admin/user' },
{title: '邮件发信管理', icon: Message, index: '/admin/email' },
{ title: '帖子广场管理', icon: ChatDotSquare, index: '/admin/forum' }, { title: '帖子广场管理', icon: ChatDotSquare, index: '/admin/forum' },
{ title: '失物招领管理', icon: Bell }, { title: '失物招领管理', icon: Bell },
{ title: '校园活动管理', icon: Notification }, { title: '校园活动管理', icon: Notification },
@ -28,8 +28,8 @@ const adminMenu = [
] ]
}, { }, {
title: '探索与发现管理', icon: Position, sub: [ title: '探索与发现管理', icon: Position, sub: [
{title: '成绩查询管理', icon: Document}, { title: '成绩管理', icon: Document },
{title: '班级课程表管理', icon: Files}, { title: '课程表管理', icon: Files },
{ title: '教务通知管理', icon: Monitor }, { title: '教务通知管理', icon: Monitor },
{ title: '在线图书馆管理', icon: Collection }, { title: '在线图书馆管理', icon: Collection },
{ title: '预约教室管理', icon: DataLine } { title: '预约教室管理', icon: DataLine }
@ -39,28 +39,27 @@ const adminMenu = [
const route = useRoute() const route = useRoute()
const loading = inject('userLoading') const loading = inject('userLoading')
const pageTabs = ref([]) const pageTabs = ref([])
function handleTabsClick({ props }) { function handleTabClick({ props }) {
router.push(props.name) router.push(props.name)
} }
function handleTabRemove(name) { function handleTabClose(name) {
const index = pageTabs.value.findIndex(tab => tab.name === name) const index = pageTabs.value.findIndex(tab => tab.name === name)
const isCurrent = name === route.fullPath const isCurrent = name === route.fullPath
pageTabs.value.splice(index, 1) pageTabs.value.splice(index, 1)
if(pageTabs.value.length > 0) { if(pageTabs.value.length > 0) {
// //Tab
if(isCurrent) { if(isCurrent) {
router.push(pageTabs.value[Math.max(0, index - 1)].name) // router.push(pageTabs.value[Math.max(0, index - 1)].name)
} }
} else { // } else {
router.push('/admin') router.push('/admin')
} }
} }
function openAdminTab(menu) { function addAdminTab(menu) {
if(!menu.index) return if(!menu.index) return
if(pageTabs.value.findIndex(tab => tab.name === menu.index) < 0) { if(pageTabs.value.findIndex(tab => tab.name === menu.index) < 0) {
pageTabs.value.push({ pageTabs.value.push({
@ -75,7 +74,7 @@ onMounted(() => {
.flatMap(menu => menu.sub) .flatMap(menu => menu.sub)
.find(sub => sub.index === route.fullPath) .find(sub => sub.index === route.fullPath)
if(initPage) { if(initPage) {
openAdminTab(initPage) addAdminTab(initPage)
} }
}) })
</script> </script>
@ -84,15 +83,15 @@ onMounted(() => {
<div class="admin-content" v-loading="loading" element-loading-text="正在进入,请稍后..."> <div class="admin-content" v-loading="loading" element-loading-text="正在进入,请稍后...">
<el-container style="height: 100%"> <el-container style="height: 100%">
<el-aside width="230px" class="admin-content-aside"> <el-aside width="230px" class="admin-content-aside">
<div style="text-align: center;padding: 15px 0 10px;height: 32px"> <div class="logo-box">
<el-image class="logo" src="https://element-plus.org/images/element-plus-logo.svg"/> <el-image class="logo" src="https://element-plus.org/images/element-plus-logo.svg"/>
</div> </div>
<el-scrollbar style="height: calc(100% - 60px)"> <el-scrollbar style="height: calc(100vh - 57px)">
<el-menu <el-menu
router router
:default-active="$route.path" :default-active="$route.path"
:default-openeds="['1', '2', '3']" :default-openeds="['1', '2']"
style="height: calc(100% - 60px);border-right: none"> style="min-height: calc(100vh - 57px);border: none">
<el-sub-menu :index="(index + 1).toString()" <el-sub-menu :index="(index + 1).toString()"
v-for="(menu, index) in adminMenu"> v-for="(menu, index) in adminMenu">
<template #title> <template #title>
@ -102,17 +101,13 @@ onMounted(() => {
<span><b>{{ menu.title }}</b></span> <span><b>{{ menu.title }}</b></span>
</template> </template>
<el-menu-item :index="subMenu.index" <el-menu-item :index="subMenu.index"
@click="openAdminTab(subMenu)" @click="addAdminTab(subMenu)"
v-for="subMenu in menu.sub"> v-for="subMenu in menu.sub">
<template #title> <template #title>
<el-icon> <el-icon>
<component :is="subMenu.icon"/> <component :is="subMenu.icon"/>
</el-icon> </el-icon>
{{ subMenu.title }} {{ subMenu.title }}
<el-tag style="margin-left: 10px" size="small"
:type="subMenu.tag.type"
v-if="subMenu.tag">{{ subMenu.tag.name }}
</el-tag>
</template> </template>
</el-menu-item> </el-menu-item>
</el-sub-menu> </el-sub-menu>
@ -122,21 +117,18 @@ onMounted(() => {
<el-container> <el-container>
<el-header class="admin-content-header"> <el-header class="admin-content-header">
<div style="flex: 1"> <div style="flex: 1">
<el-tabs :model-value="route.fullPath" <el-tabs type="card"
type="card" :model-value="route.fullPath"
closable closable
@tab-remove="handleTabRemove" @tab-remove="handleTabClose"
@tab-click="handleTabsClick"> @tab-click="handleTabClick">
<el-tab-pane <el-tab-pane v-for="tab in pageTabs"
v-for="item in pageTabs" :label="tab.title"
:key="item.name" :name="tab.name"
:label="item.title" :key="tab.name"/>
:name="item.name">
{{ item.content }}
</el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
<user-info admin/> <user-info/>
</el-header> </el-header>
<el-main> <el-main>
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
@ -150,19 +142,24 @@ onMounted(() => {
</div> </div>
</template> </template>
<style lang="less" scoped> <style scoped>
.admin-content { .admin-content {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
}
.admin-content-aside { .admin-content-aside {
border-right: solid 1px var(--el-border-color); border-right: solid 1px var(--el-border-color);
.logo-box {
text-align: center;
padding: 15px 0 10px;
height: 32px;
.logo { .logo {
height: 32px; height: 32px;
} }
} }
}
.admin-content-header { .admin-content-header {
border-bottom: solid 1px var(--el-border-color); border-bottom: solid 1px var(--el-border-color);
@ -184,9 +181,10 @@ onMounted(() => {
:deep(.el-tabs__item) { :deep(.el-tabs__item) {
height: 32px; height: 32px;
padding: 0 15px !important; padding: 0 15px;
border-radius: 6px; border-radius: 6px;
border: solid 1px var(--el-border-color) !important; border: solid 1px var(--el-border-color);
}
} }
} }
</style> </style>

View File

@ -1,5 +1,4 @@
<script setup> <script setup>
import {get} from '@/net'
import {inject, reactive, ref} from "vue"; import {inject, reactive, ref} from "vue";
import { import {
Bell, Bell,
@ -13,14 +12,16 @@ import {
} 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 UserInfo from "@/components/UserInfo.vue";
import {apiNotificationDelete, apiNotificationDeleteAll, apiNotificationList} from "@/net/api/user";
const userMenu = [ const userMenu = [
{ title: '校园论坛', icon: Location, sub: [ {
{ title: '帖子广场', index: '/index', icon: ChatDotSquare }, title: '校园论坛', icon: Location, sub: [
{ title: '帖子广场', icon: ChatDotSquare, index: '/index' },
{ title: '失物招领', icon: Bell }, { title: '失物招领', icon: Bell },
{ title: '校园活动', icon: Notification }, { title: '校园活动', icon: Notification },
{ title: '表白墙', icon: Umbrella }, { title: '表白墙', icon: Umbrella },
{ title: '海文考研', icon: School, tag: { name: '合作机构', type: '' } } { title: '海文考研', icon: School }
] ]
}, { }, {
title: '探索与发现', icon: Position, sub: [ title: '探索与发现', icon: Position, sub: [
@ -47,18 +48,18 @@ const searchInput = reactive({
const notification = ref([]) const notification = ref([])
const loadNotification = const loadNotification =
() => get('/api/notification/list', data => notification.value = data) () => apiNotificationList(data => notification.value = data)
loadNotification() loadNotification()
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>
@ -66,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">
<div style="width: 320px;height: 32px">
<el-image class="logo" src="https://element-plus.org/images/element-plus-logo.svg"/> <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="搜索论坛相关内容...">
@ -138,9 +141,6 @@ function deleteAllNotification() {
<component :is="subMenu.icon"/> <component :is="subMenu.icon"/>
</el-icon> </el-icon>
{{ subMenu.title }} {{ subMenu.title }}
<el-tag style="margin-left: 10px" size="small"
:type="subMenu.tag.type"
v-if="subMenu.tag">{{ subMenu.tag.name }}</el-tag>
</template> </template>
</el-menu-item> </el-menu-item>
</el-sub-menu> </el-sub-menu>
@ -205,17 +205,9 @@ function deleteAllNotification() {
.logo { .logo {
height: 32px; height: 32px;
width: 340px;
text-align: left;
:deep(.el-image__inner) {
width: 120px;
}
} }
.user-info { .user-info {
gap: 20px;
width: 340px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
@ -226,6 +218,7 @@ function deleteAllNotification() {
.profile { .profile {
text-align: right; text-align: right;
margin-right: 20px;
:first-child { :first-child {
font-size: 18px; font-size: 18px;

View File

@ -1,32 +0,0 @@
<script setup>
import {Message} from "@element-plus/icons-vue";
</script>
<template>
<div class="email-admin">
<div class="title">
<el-icon><Message/></el-icon>
邮件发信列表
</div>
<div class="desc">在这里查看所有发送的电子邮件列表失败邮件可以选择重新发送</div>
</div>
</template>
<style lang="less" scoped>
.email-admin {
height: 100%;
display: flex;
flex-direction: column;
.title {
font-weight: bold;
}
.desc {
color: #bababa;
font-size: 13px;
margin-bottom: 20px;
}
}
</style>

View File

@ -4,7 +4,7 @@
<template> <template>
<div> <div>
我是帖子管理 我是论坛管理
</div> </div>
</template> </template>

View File

@ -1,12 +1,19 @@
<script setup> <script setup>
import {EditPen, User} from "@element-plus/icons-vue"; import {EditPen, User} from "@element-plus/icons-vue";
import {apiUserList} from "@/net/api/user"; import {apiUserDetailTotal, apiUserList, apiUserSave} from "@/net/api/user";
import {reactive, ref, watchEffect} from "vue"; import {reactive, watchEffect} from "vue";
import {useStore} from "@/store"; import {useStore} from "@/store";
import UserEditor from "@/components/UserEditor.vue"; import {ElMessage} from "element-plus";
const store = useStore() const store = useStore()
const editor = reactive({
id: 0,
display: false,
temp: {},
loading: false
})
const userTable = reactive({ const userTable = reactive({
page: 1, page: 1,
size: 10, size: 10,
@ -14,8 +21,6 @@ const userTable = reactive({
data: [] data: []
}) })
const editorRef = ref()
function userStatus(user) { function userStatus(user) {
if(user.mute && user.banned) if(user.mute && user.banned)
return '禁言中、封禁中' return '禁言中、封禁中'
@ -27,6 +32,25 @@ function userStatus(user) {
return '正常' 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 => { watchEffect(() => apiUserList(userTable.page, userTable.size, data => {
userTable.total = data.total userTable.total = data.total
userTable.data = data.list userTable.data = data.list
@ -39,8 +63,10 @@ watchEffect(() => apiUserList(userTable.page, userTable.size, data => {
<el-icon><User/></el-icon> <el-icon><User/></el-icon>
论坛用户列表 论坛用户列表
</div> </div>
<div class="desc">在这里管理论坛的所有用户包括账号信息封禁和禁言</div> <div class="desc">
<el-table :data="userTable.data" style="width: 100%;flex: 1"> 在这里管理论坛的所有用户包括账号信息封禁和禁言处理
</div>
<el-table :data="userTable.data" height="320">
<el-table-column prop="id" label="编号" width="80"/> <el-table-column prop="id" label="编号" width="80"/>
<el-table-column label="用户名" width="180"> <el-table-column label="用户名" width="180">
<template #default="{ row }"> <template #default="{ row }">
@ -62,36 +88,69 @@ watchEffect(() => apiUserList(userTable.page, userTable.size, data => {
{{ new Date(row.registerTime).toLocaleString() }} {{ new Date(row.registerTime).toLocaleString() }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" width="100" align="center"> <el-table-column label="状态" align="center">
<template #default="{ row }"> <template #default="{ row }">
<div>{{ userStatus(row) }}</div> {{ userStatus(row) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100" align="center"> <el-table-column label="操作" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button size="small" type="primary" :icon="EditPen" <el-button type="primary" size="small" :icon="EditPen"
@click="editorRef.loadUserEditor(row)" @click="openUserEditor(row)"
:disabled="store.user.id === row.id">编辑</el-button> :disabled="row.role === 'admin'">编辑</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div style="margin-top: 20px;display: flex;justify-content: right"> <div class="pagination">
<el-pagination style="width: fit-content" <el-pagination :total="userTable.total"
:total="userTable.total"
v-model:current-page="userTable.page" v-model:current-page="userTable.page"
v-model:page-size="userTable.size" v-model:page-size="userTable.size"
layout="total, sizes, prev, pager, next, jumper"/> layout="total, sizes, prev, pager, next, jumper"/>
</div> </div>
<user-editor ref="editorRef"/> <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> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.user-admin { .user-admin {
height: 100%;
display: flex;
flex-direction: column;
.title { .title {
font-weight: bold; font-weight: bold;
} }
@ -109,6 +168,12 @@ watchEffect(() => apiUserList(userTable.page, userTable.size, data => {
gap: 15px; gap: 15px;
} }
.pagination {
margin-top: 20px;
display: flex;
justify-content: right;
}
:deep(.el-drawer__header) { :deep(.el-drawer__header) {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -4,7 +4,7 @@
<template> <template>
<div> <div>
我是管理端欢迎页 我是欢迎页
</div> </div>
</template> </template>

View File

@ -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))

View File

@ -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)
}) })

View File

@ -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>

View File

@ -2,7 +2,7 @@
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 {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {apiUserChangePassword, apiUserPrivacy, apiUserPrivacySave} from "@/net/api/user"; import {apiUserChangePassword, apiUserPrivacy, apiUserPrivacySave} from "@/net/api/user";
@ -35,7 +35,7 @@ const rules = {
} }
const formRef = ref() const formRef = ref()
const valid = ref(false) const valid = ref(false)
const onValidate = (_, isValid) => valid.value = isValid const onValidate = (prop, isValid) => valid.value = isValid
function resetPassword(){ function resetPassword(){
formRef.value.validate(valid => { formRef.value.validate(valid => {
@ -57,15 +57,16 @@ const privacy = reactive({
gender: false gender: false
}) })
function savePrivacy(type, status){
apiUserPrivacySave({ type, status }, saving)
}
onMounted(() => {
apiUserPrivacy(data => { apiUserPrivacy(data => {
Object.assign(privacy, data) Object.assign(privacy, data)
saving.value = false saving.value = false
}) })
})
function savePrivacy(type, status){
apiUserPrivacySave({ type, status }, saving,
() => ElMessage.success('隐私设置修改成功!'))
}
</script> </script>
<template> <template>

View File

@ -3,11 +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 {apiUserDetail, apiUserDetailSave} from "@/net/api/user"; import {apiAuthAskCode, apiUserDetail, apiUserDetailSave, apiUserModifyEmail} from "@/net/api/user";
const store = useStore() const store = useStore()
@ -28,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)) {
@ -61,7 +61,7 @@ function saveDetails() {
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
}) })
@ -69,14 +69,6 @@ function saveDetails() {
}) })
} }
apiUserDetail(data => {
baseForm.username = store.user.username
Object.assign(baseForm, data)
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) => {
@ -87,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
})
} }
}) })
} }
@ -107,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 = ''
@ -131,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>

View File

@ -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('/')
}) })
} }
}) })

View File

@ -62,8 +62,6 @@ const form = reactive({
remember: false remember: false
}) })
const loading = inject('userLoading')
const rules = { const rules = {
username: [ username: [
{ required: true, message: '请输入用户名' } { required: true, message: '请输入用户名' }
@ -73,6 +71,8 @@ const rules = {
] ]
} }
const loading = inject('userLoading')
function userLogin() { function userLogin() {
formRef.value.validate((isValid) => { formRef.value.validate((isValid) => {
if(isValid) { if(isValid) {

View File

@ -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>