初始化最终1.0
|
@ -0,0 +1,14 @@
|
||||||
|
/* eslint-env node */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
'extends': [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 0,//不再强制要求组价命名
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
# zt-sys
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<script src="//unpkg.com/@element-plus/icons-vue"></script>
|
||||||
|
<title>Vite App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"types": ["element-plus/global"]
|
||||||
|
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"name": "zt-sys",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"element-plus": "^2.7.7",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"vue": "^3.4.29",
|
||||||
|
"vue-router": "^4.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
|
"sass": "^1.77.8",
|
||||||
|
"unplugin-auto-import": "^0.18.0",
|
||||||
|
"unplugin-vue-components": "^0.27.2",
|
||||||
|
"vite": "^5.3.1"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "16.18.1"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,11 @@
|
||||||
|
<script setup>
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
// import HelloWorld from './components/HelloWorld.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
|
@ -0,0 +1,28 @@
|
||||||
|
import http from "@/utils/http";
|
||||||
|
//添加收藏
|
||||||
|
export const collect= (questionId)=>{
|
||||||
|
return http({
|
||||||
|
url:"/collect",
|
||||||
|
method:"post",
|
||||||
|
data:{
|
||||||
|
questionId
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//取消收藏
|
||||||
|
export const cancelCollect= (questionId)=>{
|
||||||
|
return http({
|
||||||
|
url:"/collect/cancel",
|
||||||
|
method:"post",
|
||||||
|
data:{
|
||||||
|
questionId
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//获取收藏列表
|
||||||
|
export const getCollectList= ()=>{
|
||||||
|
return http({
|
||||||
|
url:"/collect",
|
||||||
|
method:"get",
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import http from "@/utils/http";
|
||||||
|
//获取题目
|
||||||
|
export const getQuestion= (type)=>{
|
||||||
|
if(type==null||type==undefined){
|
||||||
|
type="refresh";
|
||||||
|
}
|
||||||
|
return http({
|
||||||
|
url:"/question/getQuestion",
|
||||||
|
method:"get",
|
||||||
|
params:{
|
||||||
|
type
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//验证题目是否正确
|
||||||
|
export const checkQuestion= ({
|
||||||
|
questionId,
|
||||||
|
answer
|
||||||
|
})=>{
|
||||||
|
return http({
|
||||||
|
url:"/question/validationAnswer",
|
||||||
|
method:"get",
|
||||||
|
params:{
|
||||||
|
questionId,
|
||||||
|
answer
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//获取题目
|
||||||
|
export const getQuestionByTagName= (tagName)=>{
|
||||||
|
return http({
|
||||||
|
url:"/question/getQuestionByTagName",
|
||||||
|
method:"get",
|
||||||
|
params:{
|
||||||
|
tagName
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//获取题目答案
|
||||||
|
export const validationAnswer = ({questionId,answer})=>{
|
||||||
|
return http({
|
||||||
|
url:"/question/validationAnswer",
|
||||||
|
method:"get",
|
||||||
|
params:{
|
||||||
|
questionId,
|
||||||
|
answer
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import http from "@/utils/http";
|
||||||
|
|
||||||
|
export const getTags =(tagName)=>{
|
||||||
|
return http({
|
||||||
|
url:"/tag/getTags",
|
||||||
|
method:"get",
|
||||||
|
params:{
|
||||||
|
tagName:tagName
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import httpInstance from '@/utils/http'
|
||||||
|
|
||||||
|
export const uploadImage = (file) => {
|
||||||
|
// 创建一个 FormData 对象
|
||||||
|
const formData = new FormData();
|
||||||
|
// 将文件添加到 FormData
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
// 发送 POST 请求
|
||||||
|
return httpInstance({
|
||||||
|
url: `/QiNiu/upload`,
|
||||||
|
method: "post",
|
||||||
|
data: formData, // 将 FormData 对象作为请求体
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import http from "@/utils/http";
|
||||||
|
//登录
|
||||||
|
export const login= (user)=>{
|
||||||
|
return http({
|
||||||
|
url:"/user/login",
|
||||||
|
method:"post",
|
||||||
|
data:user,
|
||||||
|
headers:{
|
||||||
|
isToken:false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//注册
|
||||||
|
export const register= (user)=>{
|
||||||
|
return http({
|
||||||
|
url:"/user/register",
|
||||||
|
method:"post",
|
||||||
|
data:user,
|
||||||
|
headers:{
|
||||||
|
isToken:false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//获取用户信息
|
||||||
|
export const getUserInfo= ()=>{
|
||||||
|
return http({
|
||||||
|
url:"/user",
|
||||||
|
method:"get"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//修改用户信息
|
||||||
|
export const updateUserInfo= (user)=>{
|
||||||
|
return http.request({
|
||||||
|
url:"/user/update",
|
||||||
|
method:"post",
|
||||||
|
data:user
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
'Fira Sans',
|
||||||
|
'Droid Sans',
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
After Width: | Height: | Size: 188 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 316 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 295 KiB |
After Width: | Height: | Size: 109 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 276 B |
|
@ -0,0 +1,4 @@
|
||||||
|
html,body{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
<template>
|
||||||
|
<div class="img-upload">
|
||||||
|
<input v-show="0 != 0" ref="input" type="file" name="image" accept="image/*" @change="upload" :v-model="file">
|
||||||
|
<!-- <el-button type="" @click="uploadClick">上传</el-button> -->
|
||||||
|
<div v-if="imageUrl == ''&&!disabled" class="img-upload-box" @click="uploadClick">
|
||||||
|
<p>+</p>
|
||||||
|
</div>
|
||||||
|
<!-- <img class="img-img" v-if="imageUrl!=''" :src="imageUrl" alt="" style="height: 100%;"> -->
|
||||||
|
<el-image class="img-img" v-if="imageUrl != ''" style="height:100%" :src="imageUrl" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2"
|
||||||
|
:preview-src-list="[imageUrl]" fit="cover" />
|
||||||
|
<el-button v-if="imageUrl != ''&&!disabled" type="primary" class="img-button" @click="uploadClick">修改头像</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.img-upload {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-img:hover + .img-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-button{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100px;
|
||||||
|
height: 30px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.img-upload-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script setup>
|
||||||
|
import { uploadImage } from '@/api/upload/upload';
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
// 抛出事件
|
||||||
|
const emit = defineEmits(['upload']);
|
||||||
|
const props=defineProps({
|
||||||
|
disabled:{
|
||||||
|
type:Boolean,
|
||||||
|
default:false
|
||||||
|
},
|
||||||
|
image:{
|
||||||
|
type:String,
|
||||||
|
default:''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watch(props.image,(newV)=>{
|
||||||
|
if(newV!=null){
|
||||||
|
console.log("传入了图片")
|
||||||
|
imageUrl.value=newV;
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
deep: true})
|
||||||
|
// const
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
//文件对象
|
||||||
|
const file = ref(null);
|
||||||
|
//上传框dom
|
||||||
|
const input = ref(null);
|
||||||
|
//图片url
|
||||||
|
const imageUrl = ref('');
|
||||||
|
//点击开启上传
|
||||||
|
const uploadClick = () => {
|
||||||
|
input.value.click();
|
||||||
|
}
|
||||||
|
//上传
|
||||||
|
const toUploadImage = async () => {
|
||||||
|
let res = await uploadImage(file.value);
|
||||||
|
if (res.code == 200) {
|
||||||
|
//成功
|
||||||
|
return res.url;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const upload = async (e) => {
|
||||||
|
let imgMaxSize = 1024 * 1024 * 4;
|
||||||
|
if (e.target.files[0].size > imgMaxSize) {
|
||||||
|
alert("图片大小不能超过4M");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (['jpeg', 'png', 'gif', 'jpg'].indexOf(e.target.files[0].type.split("/")[1]) < 0) {
|
||||||
|
//用你选择组件的报错弹窗就行,报出以下提醒
|
||||||
|
alert("上传的文件必须是图片格式");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file.value = e.target.files[0];
|
||||||
|
// alert("上传文件");
|
||||||
|
let url = await toUploadImage();
|
||||||
|
if (url != null) {
|
||||||
|
imageUrl.value = url;
|
||||||
|
emit('upload', url);
|
||||||
|
} else {
|
||||||
|
alert("上传失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updateUrl=(img)=>{
|
||||||
|
file.value=null;
|
||||||
|
if(img==null){
|
||||||
|
imageUrl.value='';
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
imageUrl.value=img;
|
||||||
|
}
|
||||||
|
onMounted(()=>{
|
||||||
|
if(props.image!=''){
|
||||||
|
updateUrl(props.image);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//暴露方法
|
||||||
|
defineExpose(
|
||||||
|
{
|
||||||
|
updateUrl
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
|
@ -0,0 +1,126 @@
|
||||||
|
<template>
|
||||||
|
<div class="topicInfoBox">
|
||||||
|
<div class="introduce title">
|
||||||
|
[{{ props.type=="select"?"选择题":"填空题" }}]{{ props.introduce }}
|
||||||
|
</div>
|
||||||
|
<div class="introduce tag">
|
||||||
|
<el-tag v-for="itme in props.tags" :key="itme.name" type="primary">{{ itme.name}}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="introduce hod">
|
||||||
|
{{ props.topicTitle }}
|
||||||
|
</div>
|
||||||
|
<div class="introduce">
|
||||||
|
<el-radio-group v-if="props.type=='select'" v-model="radio">
|
||||||
|
<el-radio
|
||||||
|
v-for="itme in topicSelect"
|
||||||
|
:key="itme.key"
|
||||||
|
:value="itme.key"
|
||||||
|
>{{ itme.key + "." + itme.name }}</el-radio
|
||||||
|
>
|
||||||
|
</el-radio-group>
|
||||||
|
<el-input v-else v-model="radio" type="textarea" :rows="2" placeholder="请输入内容" ></el-input>
|
||||||
|
</div>
|
||||||
|
<div class="introduce">
|
||||||
|
<el-button v-if="radio != null" type="success" @click="tupClick">提交</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="native.isAnswer" class="introduce jg">
|
||||||
|
已作答,<span v-if="!native.correct" style="color: red;">答案错误</span>
|
||||||
|
<span v-else style="color: rgb(17, 255, 0);">答案正确</span><br>
|
||||||
|
<br>
|
||||||
|
<span>你的答案:{{ native.userAnswer }}</span><br>
|
||||||
|
<br>
|
||||||
|
<span>正确答案:{{ ArrayToString(native.correctAnswer) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.topicInfoBox {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: "KaiTi"
|
||||||
|
}
|
||||||
|
.introduce {
|
||||||
|
margin: 10px auto;
|
||||||
|
width: 90%;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.title{
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 17px;
|
||||||
|
margin-top:30px;
|
||||||
|
}
|
||||||
|
.tag{
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.jg{
|
||||||
|
width: 100%;
|
||||||
|
height: 17vw;
|
||||||
|
box-sizing:border-box;
|
||||||
|
padding-top: 19%;
|
||||||
|
padding-left: 23%;
|
||||||
|
font-family: "黑体";
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 17px;
|
||||||
|
// background-color: aqua;
|
||||||
|
background-image: url("@/assets/image/bltt.png");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { useTopicStore } from "@/stores/topic.js";
|
||||||
|
const topStore = useTopicStore();
|
||||||
|
const radio = ref('');
|
||||||
|
const props = defineProps({
|
||||||
|
id: { type: Number, default: null },
|
||||||
|
videoUrl: { type: String, default: null },
|
||||||
|
topicList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
native:{
|
||||||
|
type:Object,
|
||||||
|
default:()=>{
|
||||||
|
return {
|
||||||
|
isAnswer:false,
|
||||||
|
analysis:'解析',
|
||||||
|
correctAnswer:'正确答案',
|
||||||
|
//是否答对
|
||||||
|
correct:false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: { type: String, default: null },
|
||||||
|
topicTitle: { type: String, default: "题目" },
|
||||||
|
introduce: { type: String, default: "介绍" },
|
||||||
|
tags: { type: Array, default: null },
|
||||||
|
});
|
||||||
|
const tupClick=async()=>{
|
||||||
|
let res=await topStore.submitAnswer(radio.value);
|
||||||
|
console.log("res",res);''
|
||||||
|
}
|
||||||
|
const ArrayToString=(arr)=>{
|
||||||
|
if(arr instanceof Array){
|
||||||
|
return arr.join(",");
|
||||||
|
}else if(arr instanceof Object){
|
||||||
|
return arr.toSting();
|
||||||
|
}else{
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const topicSelect = ref([]);
|
||||||
|
onMounted(() => {
|
||||||
|
topicSelect.value = props.topicList;
|
||||||
|
radio.value=props.native.userAnswer?props.native.userAnswer:null;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<div class="word-list">
|
||||||
|
<!-- {{ props.type }}{{ props }} -->
|
||||||
|
<div
|
||||||
|
v-for="word in words"
|
||||||
|
:key="word"
|
||||||
|
class="word-item"
|
||||||
|
@click="handleWordClick(word)"
|
||||||
|
>
|
||||||
|
{{ word }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref ,watch} from 'vue';
|
||||||
|
import { getTags } from "@/api/tag/tag";
|
||||||
|
const emit=defineEmits(['select'])
|
||||||
|
const props=defineProps({
|
||||||
|
type:{
|
||||||
|
type:String,
|
||||||
|
default:''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const getWords=async(type)=>{
|
||||||
|
let res=await getTags(type);
|
||||||
|
if(res.code==200){
|
||||||
|
words.value=res.data.map(itme=>itme.name);
|
||||||
|
if(words.value.length==0){
|
||||||
|
words.value=['暂无数据']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(() => props.type,(newValue)=>{
|
||||||
|
getWords(newValue);
|
||||||
|
console.log("props",newValue);
|
||||||
|
},{
|
||||||
|
//深度监听
|
||||||
|
deep: true, // 启用深度监听
|
||||||
|
})
|
||||||
|
// 包含所有可点击词的数组
|
||||||
|
const words = ref([
|
||||||
|
'高数上', '高数下', '线性代数', '数学分析', '解析几何',
|
||||||
|
'概率论与数理统计', '复变函数', '实变函数', '常微分方程',
|
||||||
|
'近世代数', '数据结构', 'C语言', '软件工程类', '大学物理(简明)',
|
||||||
|
'物理实验', '力学', '热学', '光学', '电磁学', '原子物理学',
|
||||||
|
'物理专业', '高中物理', '初中物理', '高中化学', '初中化学',
|
||||||
|
'高中数学', '初中数学', '小学数学', '高中生物', '初中生物',
|
||||||
|
'有机化学', '无机化学', '分析化学', '物理化学', '结构化学',
|
||||||
|
'高分子化学', '配位化学', '生物化学', '普通生物学', '细胞生物学',
|
||||||
|
'遗传学', '微生物学', '植物学', '动物学', '数学', '物理', '化学', '生物',
|
||||||
|
'法学', '经济学', '其他'
|
||||||
|
]);
|
||||||
|
const handleWordClick = (word) => {
|
||||||
|
console.log(`Clicked word: ${word}`);
|
||||||
|
emit('select',word);
|
||||||
|
// 这里可以抛出一个事件,或者调用其他方法
|
||||||
|
};
|
||||||
|
onMounted(()=>{
|
||||||
|
getWords();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.word-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
/* gap: 10px; */
|
||||||
|
}
|
||||||
|
.word-item {
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #43CF7C;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none; /* 防止用户选择文本 */
|
||||||
|
}
|
||||||
|
.word-item:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div class="video-container">
|
||||||
|
<video
|
||||||
|
ref="videoRef"
|
||||||
|
:src="props.src"
|
||||||
|
width="100%"
|
||||||
|
height="400px"
|
||||||
|
controls
|
||||||
|
@play="handlePlay"
|
||||||
|
@fullscreenchange="handleFullscreenChange"
|
||||||
|
>
|
||||||
|
您的浏览器不支持 video 标签。
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: String
|
||||||
|
});
|
||||||
|
|
||||||
|
const videoSrc = ref(props.src);
|
||||||
|
const videoRef = ref(null);
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
console.log('视频开始播放');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
isFullscreen.value = document.fullscreenElement === videoRef.value;
|
||||||
|
};
|
||||||
|
watch(() => props.src, (newSrc) => {
|
||||||
|
videoSrc.value = newSrc;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('视频组件挂载完成');
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
console.log('视频组件卸载');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px; /* 可以根据需要调整 */
|
||||||
|
/* margin: 0 auto; */
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,115 @@
|
||||||
|
<template>
|
||||||
|
<div class="nav">
|
||||||
|
<div class="logo-box"><span class="loogText">Xuaua</span></div>
|
||||||
|
<div>
|
||||||
|
<el-popover :width="700" trigger="click">
|
||||||
|
<template #reference>
|
||||||
|
<div class="search-box">
|
||||||
|
<div class="selectInput">
|
||||||
|
<div @click="selectClick">
|
||||||
|
<el-icon :size="20" >
|
||||||
|
<Search />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<input type="text" placeholder="搜索关键词" v-model="type" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<TypeSelect @select="selectInput" :type="type"></TypeSelect>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
<div @click="toUser" class="user-avatar">
|
||||||
|
<el-avatar :size="40" :src="useSotr.avatar" />{{ useSotr.username }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
// import {defineProps} from "vue"
|
||||||
|
import TypeSelect from "@/components/TypeSelect.vue";
|
||||||
|
import { debounceRef } from "@/utils/debounceRef";
|
||||||
|
import router from "@/router";
|
||||||
|
import { useTopicStore } from "@/stores/topic.js";
|
||||||
|
import {useUserStore } from "@/stores/user.js";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import "element-plus/theme-chalk/el-message.css";
|
||||||
|
const useSotr=useUserStore();
|
||||||
|
const topStore = useTopicStore();
|
||||||
|
// import { ref } from 'vue'
|
||||||
|
import { Search } from "@element-plus/icons-vue";
|
||||||
|
const type = debounceRef("", 300);
|
||||||
|
const selectInput = (a) => {
|
||||||
|
type.value = a;
|
||||||
|
};
|
||||||
|
const toUser=()=>{
|
||||||
|
router.push({
|
||||||
|
path:"/user"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const selectClick=()=>{
|
||||||
|
// console.log("点击")
|
||||||
|
console.log("123",useSotr)
|
||||||
|
topStore.loadData(type.value);
|
||||||
|
ElMessage({ message: "已切换标签", type: "success" });
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.selectInput {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 2px #000000 solid;
|
||||||
|
border-radius: 30px;
|
||||||
|
input {
|
||||||
|
background-color: #00000000; /* 透明背景 */
|
||||||
|
border: none; /* 无边框 */
|
||||||
|
outline: none; /* 去除聚焦时的轮廓 */
|
||||||
|
padding: 0; /* 无内边距 */
|
||||||
|
margin: 0; /* 无外边距 */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.nav {
|
||||||
|
width: 100%;
|
||||||
|
height: 70px;
|
||||||
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
// justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
.logo-box {
|
||||||
|
margin-top: 11px;
|
||||||
|
margin-left: 52px;
|
||||||
|
margin-bottom: 11px;
|
||||||
|
}
|
||||||
|
.input-with-select {
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
left: 210px;
|
||||||
|
border-radius: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 5px;
|
||||||
|
background: #fefefe;
|
||||||
|
// .search-input{
|
||||||
|
// width: 400px;
|
||||||
|
// height: 38px;
|
||||||
|
// border-radius: 30px;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.user-avatar {
|
||||||
|
position: absolute;
|
||||||
|
right: 2%;
|
||||||
|
}
|
||||||
|
.loogText {
|
||||||
|
font-size: 30px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
// 如果您正在使用CDN引入,请删除下面一行。
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.mount('#app')
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import TokenService from '@/utils/token'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: () => import('@/views/home/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'LOGIN',
|
||||||
|
component: () => import('@/views/login/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/user',
|
||||||
|
name: 'USER',
|
||||||
|
component: () => import('@/views/user/index.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
// 定义全局前置守卫
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
// 假设 token 存储在 store 中
|
||||||
|
const hasToken =TokenService.hasToken();
|
||||||
|
|
||||||
|
if (to.path === '/login') {
|
||||||
|
// 如果目标是登录页面,则允许访问
|
||||||
|
next()
|
||||||
|
} else if (hasToken) {
|
||||||
|
// 如果用户有 token,则允许访问其他页面
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
// 如果用户没有 token,则重定向到登录页面
|
||||||
|
next('/login')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default router
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const doubleCount = computed(() => count.value * 2)
|
||||||
|
function increment() {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count, doubleCount, increment }
|
||||||
|
})
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { getTagList } from "@/api/tag/tag";
|
||||||
|
|
||||||
|
export const useTagStore = defineStore("useTagStore", {
|
||||||
|
state: () => ({
|
||||||
|
count: 0,
|
||||||
|
tagList: [],
|
||||||
|
Val: "测试"
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async getData(name = null) {
|
||||||
|
const res = await getTagList(name);
|
||||||
|
console.log(res);
|
||||||
|
this.tagList = res.data; // 假设 res.data 是你要更新的标签列表
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { getQuestionByTagName ,validationAnswer} from '@/api/question/question'
|
||||||
|
import {collect, cancelCollect ,getCollectList} from '@/api/collect/collect'
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import "element-plus/theme-chalk/el-message.css";
|
||||||
|
// import { fa } from 'element-plus/es/locale';
|
||||||
|
export const useTopicStore = defineStore('useTopicStore', () => {
|
||||||
|
//当前标签
|
||||||
|
const tagName = ref('');
|
||||||
|
//当前索引
|
||||||
|
const index = ref(1);
|
||||||
|
//题目列表
|
||||||
|
const Topics=ref([]);
|
||||||
|
//当前题目
|
||||||
|
const Topic=computed(()=>{
|
||||||
|
return Topics.value[index.value-1];
|
||||||
|
});
|
||||||
|
//收藏列表
|
||||||
|
const collectTopics=ref([]);
|
||||||
|
//收藏列表id
|
||||||
|
const collectTopicIds=computed(()=>{
|
||||||
|
return collectTopics.value.map(item=>item.id);
|
||||||
|
})
|
||||||
|
const count = computed(()=>Topics.value.length);
|
||||||
|
const init=()=>{
|
||||||
|
if(Topics.value.length==0){
|
||||||
|
CollectList();
|
||||||
|
getData();
|
||||||
|
console.log("刷新数据");
|
||||||
|
}else{
|
||||||
|
console.log("已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
//设置标签
|
||||||
|
const setTagName=(name)=>{
|
||||||
|
tagName.value=name;
|
||||||
|
}
|
||||||
|
//根据传入标签情况判断是刷新还是加载
|
||||||
|
const loadData=async(name)=>{
|
||||||
|
if(name===tagName.value){
|
||||||
|
await getData();
|
||||||
|
}else{
|
||||||
|
setTagName(name)
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//刷新
|
||||||
|
const refresh=async()=>{
|
||||||
|
clear();
|
||||||
|
let res=await getQuestionByTagName(tagName.value);
|
||||||
|
if(res.code==200){
|
||||||
|
res.data=res.data.map(e=>{
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
isCollect:collectTopicIds.value.includes(e.id),
|
||||||
|
|
||||||
|
}})
|
||||||
|
Topics.value=res.data;
|
||||||
|
index.value=Topics.value.length>0?1:0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//加载数据
|
||||||
|
const getData=async()=>{
|
||||||
|
let res=await getQuestionByTagName(tagName.value);
|
||||||
|
if(res.code==200){
|
||||||
|
res.data=res.data.map(e=>{
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
isCollect:collectTopicIds.value.includes(e.id),
|
||||||
|
|
||||||
|
}})
|
||||||
|
Topics.value=[...Topics.value,...res.data];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//清空数据
|
||||||
|
const clear=()=>{
|
||||||
|
index.value=0;
|
||||||
|
Topics.value=[];
|
||||||
|
collectTopics.value=[];
|
||||||
|
|
||||||
|
}
|
||||||
|
//提交答案
|
||||||
|
const submitAnswer=async(answer)=>{
|
||||||
|
//先判断是否已经答过
|
||||||
|
if(Topic.value.isAnswer){
|
||||||
|
ElMessage({ message: "已经做过了", type: "warning" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let res=await validationAnswer({questionId:Topic.value.id,answer});
|
||||||
|
if(res.code==200){
|
||||||
|
setAnswer({
|
||||||
|
isAnswer:true,
|
||||||
|
analysis:res.data.analysis,
|
||||||
|
correctAnswer:res.data.correctAnswer?res.data.correctAnswer:answer,
|
||||||
|
correct:res.data.correct,
|
||||||
|
userAnswer:answer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
//设置答题情况
|
||||||
|
const setAnswer=({isAnswer,analysis,userAnswer,correctAnswer,correct})=>{
|
||||||
|
Topics.value[index.value-1]={
|
||||||
|
...Topic.value,
|
||||||
|
isAnswer,
|
||||||
|
analysis,
|
||||||
|
correctAnswer,
|
||||||
|
correct,
|
||||||
|
userAnswer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//收藏题目
|
||||||
|
const Collect=async()=>{
|
||||||
|
console.log("执行")
|
||||||
|
let res;
|
||||||
|
if(isCollect()){
|
||||||
|
res=await cancelCollect(Topic.value.id);
|
||||||
|
if(res.code==200){
|
||||||
|
ElMessage({ message: "取消收藏", type: "info" });
|
||||||
|
Topics.value[index.value-1].isCollect=false;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
res= await collect(Topic.value.id);
|
||||||
|
if(res.code==200){
|
||||||
|
ElMessage({ message: "收藏成功", type: "info" });
|
||||||
|
Topics.value[index.value-1].isCollect=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await CollectList();
|
||||||
|
}
|
||||||
|
//上一题
|
||||||
|
const prev=()=>{
|
||||||
|
if(index.value>=1){
|
||||||
|
index.value--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//下一题
|
||||||
|
const next=()=>{
|
||||||
|
index.value++;
|
||||||
|
if(index.value>count.value){
|
||||||
|
index.value=count.value;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(index.value==count.value){
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
//获取收藏题目列表
|
||||||
|
const CollectList=async()=>{
|
||||||
|
let res=await getCollectList();
|
||||||
|
if(res.code==200){
|
||||||
|
collectTopics.value=res.data.map(item=>item.questionVo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//判断是否收藏
|
||||||
|
const isCollect=()=>{
|
||||||
|
if(count.value==0){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log("123",Topic.value,index.value)
|
||||||
|
return collectTopicIds.value.includes(Topic.value.id);
|
||||||
|
}
|
||||||
|
onMounted(()=>{
|
||||||
|
init();
|
||||||
|
})
|
||||||
|
return { index, Topics, count,collectTopics ,collectTopicIds,Topic,
|
||||||
|
setAnswer,loadData,setTagName,refresh,submitAnswer,Collect,isCollect,clear,getData,
|
||||||
|
prev,next,CollectList,init}
|
||||||
|
})
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { getUserInfo } from '../api/user/user'
|
||||||
|
export const useUserStore = defineStore('useUserStore', () => {
|
||||||
|
//用户头像
|
||||||
|
const avatar = ref('')
|
||||||
|
const username = ref('')
|
||||||
|
//手机号
|
||||||
|
const phone = ref('')
|
||||||
|
//备注
|
||||||
|
const remark = ref('')
|
||||||
|
//获取用户信息
|
||||||
|
const UserInfo = async() => {
|
||||||
|
let res= await getUserInfo();
|
||||||
|
console.log("用户信息",res)
|
||||||
|
if(res.code==200){
|
||||||
|
avatar.value=res.data.avatar;
|
||||||
|
username.value=res.data.username;
|
||||||
|
phone.value=res.data.phone;
|
||||||
|
remark.value=res.data.remark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(()=>{
|
||||||
|
UserInfo();
|
||||||
|
})
|
||||||
|
return { avatar, username,phone,remark,UserInfo}
|
||||||
|
})
|
|
@ -0,0 +1,12 @@
|
||||||
|
const storage ={
|
||||||
|
setStorage:function(key,value){
|
||||||
|
window.localStorage.setItem(key,JSON.stringify(value))
|
||||||
|
},
|
||||||
|
getStorage:function(key){
|
||||||
|
return JSON.parse(window.localStorage.getItem(key))
|
||||||
|
},
|
||||||
|
removeStorage:function(key){
|
||||||
|
window.localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default storage
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { customRef } from "vue";
|
||||||
|
/**
|
||||||
|
* 防抖Ref
|
||||||
|
* @param {} value
|
||||||
|
* @param {*} delay
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const debounceRef = (value, delay = 1000) => {
|
||||||
|
let timeout;
|
||||||
|
return customRef((track, trigger) => ({
|
||||||
|
get() {
|
||||||
|
track();
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
value = val;
|
||||||
|
trigger();
|
||||||
|
}, delay);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
export function debounce(func, delay, immediate) {
|
||||||
|
let timer;
|
||||||
|
return function () {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
if (immediate) {
|
||||||
|
// 复杂的防抖函数
|
||||||
|
// 判断定时器是否为空,如果为空,则会直接执行回调函数
|
||||||
|
let firstRun = !timer;
|
||||||
|
// 不管定时器是否为空,都会重新开启一个新的定时器,不断输入,不断开启新的定时器,当不在输入的delay后,再次输入就会立即执行回调函数
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
timer = null;
|
||||||
|
}, delay);
|
||||||
|
if (firstRun) {
|
||||||
|
func.apply(this, arguments);
|
||||||
|
}
|
||||||
|
// 简单的防抖函数
|
||||||
|
} else {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
func.apply(this, arguments);
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import 'element-plus/theme-chalk/el-message.css'
|
||||||
|
import TokenService from '@/utils/token'
|
||||||
|
import router from '@/router'
|
||||||
|
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
|
||||||
|
const httpInstance=axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_APP_BASE_API,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
// axios请求拦截器
|
||||||
|
httpInstance.interceptors.request.use(config => {
|
||||||
|
// 是否需要设置 token
|
||||||
|
const isToken = (config.headers || {}).isToken === false
|
||||||
|
if (TokenService.hasToken() && !isToken) {
|
||||||
|
config.headers['Authorization'] = TokenService.getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}, e => Promise.reject(e))
|
||||||
|
|
||||||
|
// axios响应式拦截器
|
||||||
|
httpInstance.interceptors.response.use(res => {
|
||||||
|
if (res.data.code === 401){
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
if(res.data.code==0){
|
||||||
|
ElMessage.error(res.data.msg)
|
||||||
|
}
|
||||||
|
return res.data
|
||||||
|
}, e => {
|
||||||
|
return Promise.reject(e)
|
||||||
|
})
|
||||||
|
// let abb=1;
|
||||||
|
export default httpInstance;
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
const TokenService = {
|
||||||
|
// 保存 token 到 localStorage
|
||||||
|
saveToken: function(token) {
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从 localStorage 获取 token
|
||||||
|
getToken: function() {
|
||||||
|
return localStorage.getItem('token');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移除 localStorage 中的 token
|
||||||
|
removeToken: function() {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查是否存在 token
|
||||||
|
hasToken: function() {
|
||||||
|
return this.getToken() !== null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出 TokenService 对象
|
||||||
|
export default TokenService;
|
|
@ -0,0 +1,272 @@
|
||||||
|
<template>
|
||||||
|
<div class="homeBox">
|
||||||
|
<div>
|
||||||
|
<xuaua-header></xuaua-header>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="mainBox">
|
||||||
|
<div class="topicInfo" ref="topicInfoRef" @click="huadong">
|
||||||
|
<div
|
||||||
|
ref="topicInfoItemRef"
|
||||||
|
style="
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
transition: 0.7s;
|
||||||
|
"
|
||||||
|
:style="'top:' + top"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="itme in topStore.Topics"
|
||||||
|
:key="itme.id"
|
||||||
|
class="topicInfoItem"
|
||||||
|
>
|
||||||
|
<TopicBox
|
||||||
|
:id="itme.id"
|
||||||
|
:videoUrl="itme.videoUrl"
|
||||||
|
:introduce="itme.introduce"
|
||||||
|
:tags="itme.tags"
|
||||||
|
:topicTitle="itme.topicTitle"
|
||||||
|
:type="itme.type"
|
||||||
|
:topicList="itme.xuauaQuestionAnswerList"
|
||||||
|
:native="itme"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="topicAnswer">
|
||||||
|
<div class="answerNav">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<el-tabs
|
||||||
|
v-model="activeName"
|
||||||
|
class="demo-tabs"
|
||||||
|
@tab-change="handleClick"
|
||||||
|
>
|
||||||
|
<el-tab-pane label="视频" name="视频"></el-tab-pane>
|
||||||
|
<el-tab-pane label="解析" name="解析"></el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="StarIcon" @click="addCollect">
|
||||||
|
<el-icon
|
||||||
|
v-if="topStore.Topic?.isCollect == true"
|
||||||
|
|
||||||
|
:size="30"
|
||||||
|
color="#FEE082"
|
||||||
|
>
|
||||||
|
<!-- <Star /> -->
|
||||||
|
<StarFilled />
|
||||||
|
</el-icon>
|
||||||
|
<el-icon v-else :size="30" color="#2B2B2B">
|
||||||
|
<Star />
|
||||||
|
<!-- <StarFilled /> -->
|
||||||
|
</el-icon>
|
||||||
|
{{ topStore.Topic?.isCollect == true ? "已" : "未" }}收藏
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="answerInfo">
|
||||||
|
<VideoBox
|
||||||
|
v-if="activeName == '视频'"
|
||||||
|
:src="topStore.Topic?.videoUrl"
|
||||||
|
></VideoBox>
|
||||||
|
<div class="jiexi" v-else>{{ topStore.Topic.analysis }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from "vue";
|
||||||
|
import xuauaHeader from "@/components/Xuaua_header.vue";
|
||||||
|
import TopicBox from "@/components/TopicBox.vue";
|
||||||
|
import VideoBox from "@/components/VideoBox.vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import "element-plus/theme-chalk/el-message.css";
|
||||||
|
import { useTopicStore } from "@/stores/topic.js";
|
||||||
|
import { debounce } from '@/utils/debounceRef'
|
||||||
|
const topicInfoItemRef = ref(null);
|
||||||
|
const topStore = useTopicStore();
|
||||||
|
const activeName = ref("视频");
|
||||||
|
let mouseDown = false;
|
||||||
|
let lastY = null;
|
||||||
|
let currentY = null;
|
||||||
|
const top = computed(() => {
|
||||||
|
const height = topicInfoItemRef?.value?.clientHeight || 0;
|
||||||
|
return `-${(topStore.index - 1 < 0 ? 0 : topStore.index - 1) * height}px`;
|
||||||
|
});
|
||||||
|
const isCollect = computed(() => {
|
||||||
|
return topStore.isCollect();
|
||||||
|
});
|
||||||
|
const addCollect = () => {
|
||||||
|
debounce(()=>{
|
||||||
|
// alert(1)
|
||||||
|
topStore.Collect();
|
||||||
|
},500,true)();
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", function (event) {
|
||||||
|
mouseDown = true;
|
||||||
|
lastY = event.clientY;
|
||||||
|
currentY = event.clientY;
|
||||||
|
document.addEventListener("mousemove", onMouseMove);
|
||||||
|
});
|
||||||
|
// 鼠标移动时触发的事件
|
||||||
|
function onMouseMove(event) {
|
||||||
|
if (mouseDown) {
|
||||||
|
let deltaY = event.clientY - currentY;
|
||||||
|
currentY = event.clientY;
|
||||||
|
setScrollY(deltaY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 鼠标松开时触发的事件
|
||||||
|
document.addEventListener("mouseup", function (event) {
|
||||||
|
mouseDown = false;
|
||||||
|
let deltaY = event.clientY - lastY;
|
||||||
|
console.log("变化", deltaY);
|
||||||
|
flipPage(deltaY);
|
||||||
|
document.removeEventListener("mousemove", onMouseMove);
|
||||||
|
});
|
||||||
|
const topicInfoRef = ref(null);
|
||||||
|
const setScrollY = (y) => {
|
||||||
|
if (topicInfoRef.value) {
|
||||||
|
topicInfoRef.value.scrollTop = topicInfoRef.value.scrollTop - y;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const flipPage = (y) => {
|
||||||
|
if (topicInfoItemRef.value) {
|
||||||
|
topicInfoRef.value.scrollTop = 0;
|
||||||
|
if (y < -80) {
|
||||||
|
let is = topStore.next();
|
||||||
|
if (!is) {
|
||||||
|
ElMessage({ message: "没有更多了", type: "warning" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (y > 80) {
|
||||||
|
let is = topStore.prev();
|
||||||
|
if (!is) {
|
||||||
|
ElMessage({ message: "到顶了", type: "warning" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleClick = (e) => {
|
||||||
|
if (e == "解析" && !topStore.Topics[topStore.index-1].isAnswer) {
|
||||||
|
ElMessage({ message: "未作答不能查看解析", type: "warning" });
|
||||||
|
setTimeout(() => {
|
||||||
|
activeName.value = "视频";
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
topStore.init();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.homeBox {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 100%;
|
||||||
|
//占满的剩余空间
|
||||||
|
flex: 1;
|
||||||
|
background-color: #ededed;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
.mainBox {
|
||||||
|
width: 97%;
|
||||||
|
height: 97%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px;
|
||||||
|
.topicInfo {
|
||||||
|
// animation-duration: 2s;
|
||||||
|
// animation-delay: 250ms;
|
||||||
|
width: 30%;
|
||||||
|
height: 100%;
|
||||||
|
// background-color: #f0f9cc;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background-image: url("@/assets/image/tibg.jpg");
|
||||||
|
background-size: cover; // 背景图片铺满元
|
||||||
|
// background-position: center; // 背景图片居中显示
|
||||||
|
background-repeat: no-repeat; // 背景图片不重复
|
||||||
|
background-position: left left;
|
||||||
|
.topicInfoItem {
|
||||||
|
// top: -10px;
|
||||||
|
// animation-duration: 2s;
|
||||||
|
// animation-delay: 250ms;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: content-box;
|
||||||
|
// border: 1px solid red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.topicAnswer {
|
||||||
|
width: 69%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
.SelectAnswer {
|
||||||
|
width: 97%;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-bottom: 1px #43cf7c solid;
|
||||||
|
height: 7%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
.answerInfo {
|
||||||
|
width: 100%;
|
||||||
|
height: 92%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px;
|
||||||
|
// background-color: #f0f9cc;
|
||||||
|
background-image: url("@/assets/image/tibg.jpg");
|
||||||
|
background-size: cover; // 背景图片铺满元
|
||||||
|
background-position: center; // 背景图片居中显示
|
||||||
|
background-repeat: no-repeat; // 背景图片不重复
|
||||||
|
}
|
||||||
|
// background-color: #F0F9CC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.selectInput {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 2px #000000 solid;
|
||||||
|
border-radius: 30px;
|
||||||
|
input {
|
||||||
|
background-color: #00000000; /* 透明背景 */
|
||||||
|
border: none; /* 无边框 */
|
||||||
|
outline: none; /* 去除聚焦时的轮廓 */
|
||||||
|
padding: 0; /* 无内边距 */
|
||||||
|
margin: 0; /* 无外边距 */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.jiexi {
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.answerNav {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.StarIcon {
|
||||||
|
// float: right;
|
||||||
|
margin-right: 3vw;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,246 @@
|
||||||
|
<template>
|
||||||
|
<div class="loginBox">
|
||||||
|
<div class="loginFrom">
|
||||||
|
<!-- <div class="inputBox">
|
||||||
|
账号<input type="text" v-model="user.username" />
|
||||||
|
</div>
|
||||||
|
<div class="inputBox">
|
||||||
|
密码<input type="password" v-model="user.password" />
|
||||||
|
</div>
|
||||||
|
<div class="checkBox">
|
||||||
|
<el-checkbox v-model="remember"></el-checkbox>记住密码
|
||||||
|
</div> -->
|
||||||
|
<el-form >
|
||||||
|
<el-form-item label="账号" prop="username" >
|
||||||
|
<el-input style="width: 200px;height: 30px;border-radius: 15px;" class="inputBox" type="medium" v-model="user.username" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password" >
|
||||||
|
<el-input style="width: 200px;height: 30px;border-radius: 15px;" v-model="user.password" type="password" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-checkbox v-model="remember">记住密码</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="btnBox">
|
||||||
|
<el-button size="large" type="success" round @click="toLogin"
|
||||||
|
>登录</el-button
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
size="large"
|
||||||
|
type="danger"
|
||||||
|
round
|
||||||
|
@click="dialogFormVisible = true"
|
||||||
|
>注册</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-dialog v-model="dialogFormVisible" title="注册新用户" width="500">
|
||||||
|
<el-form
|
||||||
|
ref="ruleFormRef"
|
||||||
|
style="max-width: 600px"
|
||||||
|
:model="ruleForm"
|
||||||
|
status-icon
|
||||||
|
:rules="rules"
|
||||||
|
label-width="auto"
|
||||||
|
class="demo-ruleForm"
|
||||||
|
>
|
||||||
|
<el-form-item label="账号" prop="username">
|
||||||
|
<el-input v-model="ruleForm.username" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确定密码" prop="password2">
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.password2"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="dialogFormVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm(ruleFormRef)"
|
||||||
|
>注册</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loginBox {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background-image: url("@/assets/image/loginbg.jpg");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
// padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginFrom {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 10px;
|
||||||
|
// box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .inputBox {
|
||||||
|
|
||||||
|
// // margin-bottom: 20px;
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
// justify-content: space-between;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
el-input{
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkBox {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnBox {
|
||||||
|
width: 80%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import "element-plus/theme-chalk/el-message.css";
|
||||||
|
import { login, register } from "@/api/user/user";
|
||||||
|
import { useTopicStore } from "@/stores/topic.js";
|
||||||
|
const topStore = useTopicStore();
|
||||||
|
import storage from "@/utils/Sotrage";
|
||||||
|
import TokenService from "@/utils/token";
|
||||||
|
import router from "@/router";
|
||||||
|
import { ref, reactive, onMounted } from "vue";
|
||||||
|
|
||||||
|
const user = reactive({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const remember = ref(false);
|
||||||
|
|
||||||
|
const toLogin = async () => {
|
||||||
|
if (user.username === "" || user.password === "") {
|
||||||
|
ElMessage({ message: "请输入完整信息", type: "warning" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await login(user);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage({ message: "登录成功", type: "success" });
|
||||||
|
if (remember.value) {
|
||||||
|
storage.setStorage("username", user.username);
|
||||||
|
storage.setStorage("password", user.password);
|
||||||
|
} else {
|
||||||
|
storage.removeStorage("username");
|
||||||
|
storage.removeStorage("password");
|
||||||
|
}
|
||||||
|
TokenService.saveToken(res.token);
|
||||||
|
router.push("/");
|
||||||
|
}else{
|
||||||
|
ElMessage({ message: res.msg, type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogFormVisible = ref(false);
|
||||||
|
const ruleFormRef = ref(null);
|
||||||
|
|
||||||
|
const ruleForm = reactive({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
password2: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const usernamevalidate = (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error("请输入账号"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validatePass = (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error("请输入密码"));
|
||||||
|
} else {
|
||||||
|
if (ruleForm.password !== "") {
|
||||||
|
if (!ruleFormRef.value) return;
|
||||||
|
ruleFormRef.value.validateField("password2");
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validatePass2 = (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error("请输入确认密码"));
|
||||||
|
} else if (value !== ruleForm.password) {
|
||||||
|
callback(new Error("密码不一致"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rules = reactive({
|
||||||
|
username: [{ validator: usernamevalidate, trigger: "blur" }],
|
||||||
|
password: [{ validator: validatePass, trigger: "blur" }],
|
||||||
|
password2: [{ validator: validatePass2, trigger: "blur" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitForm = (formEl) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
let res = await register({
|
||||||
|
username: ruleForm.username,
|
||||||
|
password: ruleForm.password,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage({ message: "注册成功", type: "success" });
|
||||||
|
dialogFormVisible.value = false;
|
||||||
|
} else {
|
||||||
|
ElMessage({ message: res.msg, type: "error" });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("error submit!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
user.username = storage.getStorage("username") || "";
|
||||||
|
user.password = storage.getStorage("password") || "";
|
||||||
|
topStore.clear();
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,423 @@
|
||||||
|
<template>
|
||||||
|
<div class="userPage">
|
||||||
|
<div style="height: 3vh;margin: 0 auto;width: 97%;">
|
||||||
|
<el-page-header @back="router.push('/')">
|
||||||
|
<template #content>
|
||||||
|
<span class="text-large font-600 mr-3"> 个人信息 </span>
|
||||||
|
</template>
|
||||||
|
<template #title >
|
||||||
|
返回首页
|
||||||
|
</template>
|
||||||
|
</el-page-header>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="userBox">
|
||||||
|
<div class="userInfo">
|
||||||
|
<div class="userT" @click="drawer = true">
|
||||||
|
<el-avatar :size="150" :src="useSotr.avatar" />
|
||||||
|
</div>
|
||||||
|
<div class="userName">{{ useSotr.username }}</div>
|
||||||
|
<!-- <div class="userName" @click="router.push('/')">返回首页</div> -->
|
||||||
|
</div>
|
||||||
|
<div class="collectionTopic">
|
||||||
|
<div class="title">收藏</div>
|
||||||
|
<div class="collectionList">
|
||||||
|
<div
|
||||||
|
class="collectionTme"
|
||||||
|
v-for="emit in topStore.collectTopics"
|
||||||
|
:key="emit.id"
|
||||||
|
:Topic="emit"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
[{{ emit.type == "select" ? "选择题" : "填空题" }}]{{
|
||||||
|
emit.introduce
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<el-tag v-for="itme in emit.tags" :key="itme.name" type="primary">{{
|
||||||
|
itme.name
|
||||||
|
}}</el-tag
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
{{ emit.topicTitle }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="collectionTme"></div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-drawer
|
||||||
|
v-model="drawer"
|
||||||
|
title="个人信息"
|
||||||
|
direction="ltr"
|
||||||
|
:before-close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="info">
|
||||||
|
<div class="infoItme">
|
||||||
|
<image-upload
|
||||||
|
ref="imgUpload"
|
||||||
|
:image="userForm.avatar"
|
||||||
|
style="width: 200px; height: 200px; display: inline-block"
|
||||||
|
@upload="upload"
|
||||||
|
:disabled="!isUpdata"
|
||||||
|
></image-upload>
|
||||||
|
</div>
|
||||||
|
<div class="infoItme">
|
||||||
|
<div class="text">用户名</div>
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.username"
|
||||||
|
style="width: 240px"
|
||||||
|
placeholder="Please input"
|
||||||
|
:disabled="!isUpdata"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="infoItme">
|
||||||
|
<div class="text">手机号</div>
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.phone"
|
||||||
|
style="width: 240px"
|
||||||
|
placeholder="未登记手机号"
|
||||||
|
:disabled="!isUpdata"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button v-if="!isUpdata" type="success" @click="isUpdata = true"
|
||||||
|
>更改信息</el-button
|
||||||
|
>
|
||||||
|
<el-button v-else type="success" @click="updateClick">保存</el-button>
|
||||||
|
<el-button type="danger" @click="utLogin">退出登录</el-button>
|
||||||
|
<el-button type="danger" @click="dialogFormVisible = true"
|
||||||
|
>更改密码</el-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
<el-dialog v-model="dialogFormVisible" title="修改密码" width="500">
|
||||||
|
<el-form
|
||||||
|
ref="ruleFormRef"
|
||||||
|
style="max-width: 600px"
|
||||||
|
:model="ruleForm"
|
||||||
|
status-icon
|
||||||
|
:rules="rules"
|
||||||
|
label-width="auto"
|
||||||
|
class="demo-ruleForm"
|
||||||
|
>
|
||||||
|
<el-form-item label="旧密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.password"
|
||||||
|
type="password"
|
||||||
|
utocomplete="off"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.newPassword"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确定密码" prop="newPassword2">
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.newPassword2"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="dialogFormVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm(ruleFormRef)"
|
||||||
|
>确定</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.userPage {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.userBox {
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
width: 100vw;
|
||||||
|
flex: 1;
|
||||||
|
// height: 100vh;
|
||||||
|
background-image: url("@/assets/image/userbg.jpg");
|
||||||
|
background-size: cover; // 背景图片铺满元素
|
||||||
|
background-position: center; // 背景图片居中显示
|
||||||
|
background-repeat: no-repeat; // 背景图片不重复
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.userInfo {
|
||||||
|
height: 90%;
|
||||||
|
width: 20%;
|
||||||
|
// background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
// justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.userName {
|
||||||
|
margin-top: 30px;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userT {
|
||||||
|
margin-top: 90px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collectionTopic {
|
||||||
|
margin-left: 30px;
|
||||||
|
width: 60%;
|
||||||
|
height: 90%;
|
||||||
|
// background-color: #ff0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
// justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 90%;
|
||||||
|
height: 5%;
|
||||||
|
background-color: #ededec;
|
||||||
|
background-image: url("@/assets/image/btbg.png");
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: repeat;
|
||||||
|
border-radius: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collectionList {
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
|
||||||
|
/* IE 10+ */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
/* Chrome Safari */
|
||||||
|
}
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
width: 90%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
// justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.collectionTme {
|
||||||
|
/* 宽度是固定好的 */
|
||||||
|
width: 30%;
|
||||||
|
height: 47%;
|
||||||
|
// background-color: #d2fce3;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-top: 1%;
|
||||||
|
padding-left: 5%;
|
||||||
|
font-family: "黑体";
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 17px;
|
||||||
|
background-image: url("@/assets/image/zhibg.png");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-right: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collectionList:after {
|
||||||
|
content: "";
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoItme {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 80%;
|
||||||
|
margin: 20px auto;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
width: 30%;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script setup>
|
||||||
|
import router from "@/router";
|
||||||
|
import ImageUpload from "@/components/ImageUpload.vue";
|
||||||
|
import { ElMessage, ElStep } from "element-plus";
|
||||||
|
import { ElMessageBox } from "element-plus";
|
||||||
|
import "element-plus/theme-chalk/el-message.css";
|
||||||
|
import { login } from "@/api/user/user";
|
||||||
|
import { updateUserInfo } from "@/api/user/user";
|
||||||
|
import { onMounted, reactive, ref } from "vue";
|
||||||
|
import { useTopicStore } from "@/stores/topic.js";
|
||||||
|
import { useUserStore } from "@/stores/user.js";
|
||||||
|
import TokenService from "@/utils/token";
|
||||||
|
const upload = (url) => {
|
||||||
|
userForm.avatar = url;
|
||||||
|
};
|
||||||
|
const utLogin = () => {
|
||||||
|
TokenService.removeToken();
|
||||||
|
router.push("/login");
|
||||||
|
};
|
||||||
|
const dialogFormVisible = ref(false);
|
||||||
|
const useSotr = useUserStore();
|
||||||
|
const topStore = useTopicStore();
|
||||||
|
const ruleFormRef = ref(null);
|
||||||
|
const drawer = ref(false);
|
||||||
|
const isUpdata = ref(false);
|
||||||
|
const userForm = reactive({
|
||||||
|
username: null,
|
||||||
|
phone: null,
|
||||||
|
remark: null,
|
||||||
|
avatar: null,
|
||||||
|
});
|
||||||
|
const updateClick = async () => {
|
||||||
|
if (
|
||||||
|
userForm.username == null ||
|
||||||
|
userForm.phone == null ||
|
||||||
|
userForm.avatar == null
|
||||||
|
) {
|
||||||
|
ElMessage({ message: "请完善信息", type: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let res = await updateUserInfo(userForm);
|
||||||
|
if (res.code == 200) {
|
||||||
|
ElMessage({ message: "更新成功", type: "success" });
|
||||||
|
isUpdata.value = false;
|
||||||
|
await useSotr.UserInfo();
|
||||||
|
userForm.username = useSotr.username;
|
||||||
|
userForm.phone = useSotr.phone;
|
||||||
|
userForm.remark = useSotr.remark;
|
||||||
|
userForm.avatar = useSotr.avatar;
|
||||||
|
isUpdata.value = false;
|
||||||
|
}
|
||||||
|
console.log(res);
|
||||||
|
};
|
||||||
|
const handleClose = (done) => {
|
||||||
|
if (isUpdata.value) {
|
||||||
|
ElMessageBox.confirm("更改未保存,确定退出吗?", {
|
||||||
|
confirmButtonText: "确定退出",
|
||||||
|
cancelButtonText: "继续修改",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
userForm.username = useSotr.username;
|
||||||
|
userForm.phone = useSotr.phone;
|
||||||
|
userForm.remark = useSotr.remark;
|
||||||
|
userForm.avatar = useSotr.avatar;
|
||||||
|
isUpdata.value = false;
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// catch error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ruleForm = reactive({
|
||||||
|
password: "",
|
||||||
|
newPassword: "",
|
||||||
|
newPassword2: "",
|
||||||
|
});
|
||||||
|
const usernamevalidate = async (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error("请输旧密码"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let res = await login({ username: useSotr.username, password: value });
|
||||||
|
if (res.code == 200) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callback(new Error("密码错误"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validatePass = (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error("请输新入密码"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (ruleForm.password !== "") {
|
||||||
|
if (!ruleFormRef.value) return;
|
||||||
|
ruleFormRef.value.validateField("》》");
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validatePass2 = (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error("请输确认新密码"));
|
||||||
|
}
|
||||||
|
else if (value !== ruleForm.newPassword) {
|
||||||
|
callback(new Error("密码不一致"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const submitForm = () => {
|
||||||
|
ruleFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
let res = await updateUserInfo({
|
||||||
|
password: ruleForm.newPassword,
|
||||||
|
});
|
||||||
|
if (res.code == 200) {
|
||||||
|
ElMessage({ message: "更新成功,请重新登录账号", type: "success" });
|
||||||
|
dialogFormVisible.value = false;
|
||||||
|
ruleForm.password = "";
|
||||||
|
ruleForm.newPassword = "";
|
||||||
|
ruleForm.newPassword2 = "";
|
||||||
|
TokenService.removeToken();
|
||||||
|
router.push("/login");
|
||||||
|
// await useSotr.UserInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const rules = reactive({
|
||||||
|
password: [{ validator: usernamevalidate, trigger: "blur" }],
|
||||||
|
newPassword: [{ validator: validatePass, trigger: "blur" }],
|
||||||
|
newPassword2: [{ validator: validatePass2, trigger: "blur" }],
|
||||||
|
});
|
||||||
|
onMounted(async () => {
|
||||||
|
await useSotr.UserInfo();
|
||||||
|
userForm.username = useSotr.username;
|
||||||
|
userForm.phone = useSotr.phone;
|
||||||
|
userForm.remark = useSotr.remark;
|
||||||
|
userForm.avatar = useSotr.avatar;
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
AutoImport({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|