chore:构架
0
Yi.Bbs.Vue3/.env
Normal file
4
Yi.Bbs.Vue3/.env.development
Normal file
@@ -0,0 +1,4 @@
|
||||
# 接口前缀
|
||||
VITE_APP_BASEAPI="/api-dev"
|
||||
VITE_APP_URL="http://localhost:19001/api/app"
|
||||
VITE_APP_ENV_NAME = "dev"
|
||||
4
Yi.Bbs.Vue3/.env.production
Normal file
@@ -0,0 +1,4 @@
|
||||
# 接口前缀
|
||||
VITE_APP_BASEAPI="/prod-api"
|
||||
|
||||
VITE_APP_ENV_NAME = "production"
|
||||
28
Yi.Bbs.Vue3/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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?
|
||||
3
Yi.Bbs.Vue3/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
29
Yi.Bbs.Vue3/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# yi-bbs
|
||||
|
||||
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) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## 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
|
||||
```
|
||||
14
Yi.Bbs.Vue3/index.html
Normal file
@@ -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" />
|
||||
<title>意社区</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
6842
Yi.Bbs.Vue3/package-lock.json
generated
Normal file
35
Yi.Bbs.Vue3/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "yi-bbs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"axios": "^1.3.4",
|
||||
"echarts": "^5.4.2",
|
||||
"element-plus": "^2.2.32",
|
||||
"highlight": "^0.2.4",
|
||||
"i": "^0.3.7",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^4.2.12",
|
||||
"mavon-editor": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.32",
|
||||
"vue": "^3.2.47",
|
||||
"vue-cropper": "1.0.3",
|
||||
"vue-router": "^4.1.6",
|
||||
"yarm": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"sass": "1.52.1",
|
||||
"unplugin-auto-import": "^0.15.0",
|
||||
"unplugin-vue-components": "^0.24.0",
|
||||
"vite": "^4.1.3"
|
||||
}
|
||||
}
|
||||
BIN
Yi.Bbs.Vue3/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
28
Yi.Bbs.Vue3/src/App.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="locale">
|
||||
<RouterView />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
<script setup>
|
||||
import useConfigStore from "@/stores/config";
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import {onMounted } from "vue";
|
||||
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
const locale= zhCn;
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
||||
//加载全局信息
|
||||
onMounted(async()=>{
|
||||
|
||||
await configStore.getConfig();
|
||||
})
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
17
Yi.Bbs.Vue3/src/apis/accessApi.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import request from "@/config/axios/service";
|
||||
|
||||
// 触发访问
|
||||
export function access() {
|
||||
return request({
|
||||
url: "/access-log",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
// 获取本周数据
|
||||
export function getWeek() {
|
||||
return request({
|
||||
url: "/access-log/week",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
78
Yi.Bbs.Vue3/src/apis/accountApi.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import request from "@/config/axios/service";
|
||||
|
||||
// 登录方法
|
||||
export function login(username, password, code, uuid) {
|
||||
const data = {
|
||||
username,
|
||||
password,
|
||||
code,
|
||||
uuid,
|
||||
};
|
||||
return request({
|
||||
url: "/account/login",
|
||||
headers: {
|
||||
isToken: false,
|
||||
},
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 注册方法
|
||||
export function register(userName, password, phone, code, uuid) {
|
||||
const data = {
|
||||
userName,
|
||||
password,
|
||||
phone,
|
||||
code,
|
||||
uuid,
|
||||
};
|
||||
return request({
|
||||
url: "/account/register",
|
||||
headers: {
|
||||
isToken: false,
|
||||
},
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取用户详细信息
|
||||
export function getInfo() {
|
||||
return request({
|
||||
url: "/account",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 退出方法
|
||||
export function logout() {
|
||||
return request({
|
||||
url: "/account/logout",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
export function getCodeImg() {
|
||||
return request({
|
||||
url: "/account/captcha-image",
|
||||
headers: {
|
||||
isToken: false,
|
||||
},
|
||||
method: "get",
|
||||
timeout: 20000,
|
||||
});
|
||||
}
|
||||
// 获取短信验证码
|
||||
export function getCodePhone(phone) {
|
||||
return request({
|
||||
url: "/account/captcha-phone",
|
||||
headers: {
|
||||
isToken: false,
|
||||
},
|
||||
method: "post",
|
||||
timeout: 20000,
|
||||
data: { phone },
|
||||
});
|
||||
}
|
||||
10
Yi.Bbs.Vue3/src/apis/agreeApi.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import request from "@/config/axios/service";
|
||||
export function operate(discussId) {
|
||||
if (discussId == undefined) {
|
||||
return;
|
||||
}
|
||||
return request({
|
||||
url: `/agree/operate/${discussId}`,
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
40
Yi.Bbs.Vue3/src/apis/articleApi.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import request from "@/config/axios/service";
|
||||
export function getList(data) {
|
||||
return request({
|
||||
url: "/article",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
export function get(id) {
|
||||
return request({
|
||||
url: `/article/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
export function add(data) {
|
||||
return request({
|
||||
url: `/article`,
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function update(id, data) {
|
||||
return request({
|
||||
url: `/article/${id}`,
|
||||
method: "put",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function del(ids) {
|
||||
return request({
|
||||
url: `/article/${ids}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
export function all(discussId) {
|
||||
return request({
|
||||
url: `/article/all/discuss-id/${discussId}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
65
Yi.Bbs.Vue3/src/apis/auth.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import request from "@/config/axios/service";
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param {*} data 账号密码
|
||||
*/
|
||||
export function userLogin(data) {
|
||||
return request({
|
||||
url: `/account/login`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @param {*} data 账号密码
|
||||
*/
|
||||
export function userRegister(data) {
|
||||
return request({
|
||||
url: `/account/register`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详细信息
|
||||
*/
|
||||
export function getUserDetailInfo() {
|
||||
return request({
|
||||
url: `/account`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户退出
|
||||
*/
|
||||
export function userLogout() {
|
||||
return request({
|
||||
url: `/account/logout`,
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
export function getCodeImg() {
|
||||
return request({
|
||||
url: `/account/captcha-image`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取短信验证码
|
||||
*/
|
||||
export function getCodePhone(data) {
|
||||
return request({
|
||||
url: `/account/captcha-phone`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
9
Yi.Bbs.Vue3/src/apis/bannerApi.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from "@/config/axios/service";
|
||||
|
||||
export function getList(data) {
|
||||
return request({
|
||||
url: "/banner",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
23
Yi.Bbs.Vue3/src/apis/commentApi.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import request from "@/config/axios/service";
|
||||
|
||||
export function getListByDiscussId(discussId, data) {
|
||||
return request({
|
||||
url: `/comment/discuss-id/${discussId}`,
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
export function add(data) {
|
||||
return request({
|
||||
url: `/comment`,
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function del(ids) {
|
||||
return request({
|
||||
url: `/comment/${ids}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
9
Yi.Bbs.Vue3/src/apis/configApi.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from "@/config/axios/service";
|
||||
|
||||
//获取配置
|
||||
export function getAll() {
|
||||
return request({
|
||||
url: "/config",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
48
Yi.Bbs.Vue3/src/apis/discussApi.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import request from "@/config/axios/service";
|
||||
|
||||
export function getList(data) {
|
||||
return request({
|
||||
url: "/discuss",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
export function getTopList(data) {
|
||||
if (data == undefined) {
|
||||
data = { isTop: true };
|
||||
} else {
|
||||
data["isTop"] = true;
|
||||
}
|
||||
|
||||
return request({
|
||||
url: "/discuss",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
export function get(id) {
|
||||
return request({
|
||||
url: `/discuss/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
export function add(data) {
|
||||
return request({
|
||||
url: `/discuss`,
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function update(id, data) {
|
||||
return request({
|
||||
url: `/discuss/${id}`,
|
||||
method: "put",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function del(ids) {
|
||||
return request({
|
||||
url: `/discuss/${ids}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
10
Yi.Bbs.Vue3/src/apis/fileApi.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import request from "@/config/axios/service";
|
||||
|
||||
export function upload(data) {
|
||||
return request({
|
||||
url: "/file",
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
}
|
||||
9
Yi.Bbs.Vue3/src/apis/plateApi.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from "@/config/axios/service";
|
||||
|
||||
export function getList(data) {
|
||||
return request({
|
||||
url: "/plate",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
137
Yi.Bbs.Vue3/src/apis/userApi.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import request from "@/config/axios/service";
|
||||
|
||||
// 查询用户列表
|
||||
export function listUser(query) {
|
||||
return request({
|
||||
url: "/user",
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
// 查询用户详细
|
||||
export function getUser(userId) {
|
||||
return request({
|
||||
url: "/user/" + parseStrEmpty(userId),
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 新增用户
|
||||
export function addUser(data) {
|
||||
return request({
|
||||
url: "/user",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改用户
|
||||
export function updateUser(id, data) {
|
||||
return request({
|
||||
url: `/user/${id}`,
|
||||
method: "put",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export function delUser(userId) {
|
||||
return request({
|
||||
url: `/user/${userId}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
// 用户密码重置
|
||||
export function resetUserPwd(id, password) {
|
||||
const data = {
|
||||
password,
|
||||
};
|
||||
|
||||
return request({
|
||||
url: `/account/rest-password/${id}`,
|
||||
method: "put",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 用户状态修改
|
||||
export function changeUserStatus(userId, isDel) {
|
||||
return request({
|
||||
url: `/user/${userId}/${isDel}`,
|
||||
method: "put",
|
||||
});
|
||||
}
|
||||
|
||||
// 查询用户个人信息
|
||||
export function getUserProfile() {
|
||||
return request({
|
||||
url: "/account",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 修改用户个人信息
|
||||
export function updateUserProfile(data) {
|
||||
return request({
|
||||
url: `/user/profile`,
|
||||
method: "put",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
// 只修改用户头像
|
||||
export function updateUserIcon(data) {
|
||||
return request({
|
||||
url: `/account/icon`,
|
||||
method: "put",
|
||||
data: { icon: data },
|
||||
});
|
||||
}
|
||||
|
||||
// 用户密码重置
|
||||
export function updateUserPwd(oldPassword, newPassword) {
|
||||
const data = {
|
||||
oldPassword,
|
||||
newPassword,
|
||||
};
|
||||
return request({
|
||||
url: "/account/password",
|
||||
method: "put",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 用户头像上传
|
||||
export function uploadAvatar(data) {
|
||||
return request({
|
||||
url: "/system/user/profile/avatar",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 查询授权角色
|
||||
export function getAuthRole(userId) {
|
||||
return request({
|
||||
url: "/system/user/authRole/" + userId,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 保存授权角色
|
||||
export function updateAuthRole(data) {
|
||||
return request({
|
||||
url: "/system/user/authRole",
|
||||
method: "put",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
// // 查询部门下拉树结构
|
||||
// export function deptTreeSelect() {
|
||||
// return request({
|
||||
// url: '/system/user/deptTree',
|
||||
// method: 'get'
|
||||
// })
|
||||
// }
|
||||
BIN
Yi.Bbs.Vue3/src/assets/401_images/401.gif
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
Yi.Bbs.Vue3/src/assets/404_images/404.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
Yi.Bbs.Vue3/src/assets/404_images/404_cloud.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
0
Yi.Bbs.Vue3/src/assets/base.css
Normal file
710
Yi.Bbs.Vue3/src/assets/github-markdown.css
Normal file
@@ -0,0 +1,710 @@
|
||||
@font-face {
|
||||
font-family: octicons-link;
|
||||
src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff');
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
line-height: 1.5;
|
||||
color: #24292e;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.markdown-body .pl-c {
|
||||
color: #6a737d;
|
||||
}
|
||||
|
||||
.markdown-body .pl-c1,
|
||||
.markdown-body .pl-s .pl-v {
|
||||
color: #005cc5;
|
||||
}
|
||||
|
||||
.markdown-body .pl-e,
|
||||
.markdown-body .pl-en {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.markdown-body .pl-smi,
|
||||
.markdown-body .pl-s .pl-s1 {
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.markdown-body .pl-ent {
|
||||
color: #22863a;
|
||||
}
|
||||
|
||||
.markdown-body .pl-k {
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
.markdown-body .pl-s,
|
||||
.markdown-body .pl-pds,
|
||||
.markdown-body .pl-s .pl-pse .pl-s1,
|
||||
.markdown-body .pl-sr,
|
||||
.markdown-body .pl-sr .pl-cce,
|
||||
.markdown-body .pl-sr .pl-sre,
|
||||
.markdown-body .pl-sr .pl-sra {
|
||||
color: #032f62;
|
||||
}
|
||||
|
||||
.markdown-body .pl-v,
|
||||
.markdown-body .pl-smw {
|
||||
color: #e36209;
|
||||
}
|
||||
|
||||
.markdown-body .pl-bu {
|
||||
color: #b31d28;
|
||||
}
|
||||
|
||||
.markdown-body .pl-ii {
|
||||
color: #fafbfc;
|
||||
background-color: #b31d28;
|
||||
}
|
||||
|
||||
.markdown-body .pl-c2 {
|
||||
color: #fafbfc;
|
||||
background-color: #d73a49;
|
||||
}
|
||||
|
||||
.markdown-body .pl-c2::before {
|
||||
content: "^M";
|
||||
}
|
||||
|
||||
.markdown-body .pl-sr .pl-cce {
|
||||
font-weight: bold;
|
||||
color: #22863a;
|
||||
}
|
||||
|
||||
.markdown-body .pl-ml {
|
||||
color: #735c0f;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mh,
|
||||
.markdown-body .pl-mh .pl-en,
|
||||
.markdown-body .pl-ms {
|
||||
font-weight: bold;
|
||||
color: #005cc5;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mi {
|
||||
font-style: italic;
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mb {
|
||||
font-weight: bold;
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.markdown-body .pl-md {
|
||||
color: #b31d28;
|
||||
background-color: #ffeef0;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mi1 {
|
||||
color: #22863a;
|
||||
background-color: #f0fff4;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mc {
|
||||
color: #e36209;
|
||||
background-color: #ffebda;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mi2 {
|
||||
color: #f6f8fa;
|
||||
background-color: #005cc5;
|
||||
}
|
||||
|
||||
.markdown-body .pl-mdr {
|
||||
font-weight: bold;
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.markdown-body .pl-ba {
|
||||
color: #586069;
|
||||
}
|
||||
|
||||
.markdown-body .pl-sg {
|
||||
color: #959da5;
|
||||
}
|
||||
|
||||
.markdown-body .pl-corl {
|
||||
text-decoration: underline;
|
||||
color: #032f62;
|
||||
}
|
||||
|
||||
.markdown-body .octicon {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
background-color: transparent;
|
||||
-webkit-text-decoration-skip: objects;
|
||||
}
|
||||
|
||||
.markdown-body a:active,
|
||||
.markdown-body a:hover {
|
||||
outline-width: 0;
|
||||
}
|
||||
|
||||
.markdown-body strong {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.markdown-body strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
.markdown-body img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
.markdown-body svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-body code,
|
||||
.markdown-body kbd,
|
||||
.markdown-body pre {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.markdown-body input {
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown-body input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.markdown-body [type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.markdown-body input {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
color: #0366d6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-body strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
height: 0;
|
||||
margin: 15px 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-body hr::before {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.markdown-body hr::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.markdown-body td,
|
||||
.markdown-body th {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body h5 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body h6 {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
padding-left: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body ol ol,
|
||||
.markdown-body ul ol {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
|
||||
.markdown-body ul ul ol,
|
||||
.markdown-body ul ol ol,
|
||||
.markdown-body ol ul ol,
|
||||
.markdown-body ol ol ol {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
.markdown-body dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown-body .octicon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.markdown-body .pl-0 {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.markdown-body .pl-1 {
|
||||
padding-left: 4px !important;
|
||||
}
|
||||
|
||||
.markdown-body .pl-2 {
|
||||
padding-left: 8px !important;
|
||||
}
|
||||
|
||||
.markdown-body .pl-3 {
|
||||
padding-left: 16px !important;
|
||||
}
|
||||
|
||||
.markdown-body .pl-4 {
|
||||
padding-left: 24px !important;
|
||||
}
|
||||
|
||||
.markdown-body .pl-5 {
|
||||
padding-left: 32px !important;
|
||||
}
|
||||
|
||||
.markdown-body .pl-6 {
|
||||
padding-left: 40px !important;
|
||||
}
|
||||
|
||||
.markdown-body::before {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.markdown-body::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.markdown-body>*:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.markdown-body>*:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.markdown-body a:not([href]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body .anchor {
|
||||
float: left;
|
||||
padding-right: 4px;
|
||||
margin-left: -20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.markdown-body .anchor:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.markdown-body p,
|
||||
.markdown-body blockquote,
|
||||
.markdown-body ul,
|
||||
.markdown-body ol,
|
||||
.markdown-body dl,
|
||||
.markdown-body table,
|
||||
.markdown-body pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: #e1e4e8;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
padding: 0 1em;
|
||||
color: #6a737d;
|
||||
border-left: 0.25em solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-body blockquote>:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-body blockquote>:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body kbd {
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
font-size: 11px;
|
||||
line-height: 10px;
|
||||
color: #444d56;
|
||||
vertical-align: middle;
|
||||
background-color: #fafbfc;
|
||||
border: solid 1px #c6cbd1;
|
||||
border-bottom-color: #959da5;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 #959da5;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.markdown-body h1 .octicon-link,
|
||||
.markdown-body h2 .octicon-link,
|
||||
.markdown-body h3 .octicon-link,
|
||||
.markdown-body h4 .octicon-link,
|
||||
.markdown-body h5 .octicon-link,
|
||||
.markdown-body h6 .octicon-link {
|
||||
color: #1b1f23;
|
||||
vertical-align: middle;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.markdown-body h1:hover .anchor,
|
||||
.markdown-body h2:hover .anchor,
|
||||
.markdown-body h3:hover .anchor,
|
||||
.markdown-body h4:hover .anchor,
|
||||
.markdown-body h5:hover .anchor,
|
||||
.markdown-body h6:hover .anchor {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body h1:hover .anchor .octicon-link,
|
||||
.markdown-body h2:hover .anchor .octicon-link,
|
||||
.markdown-body h3:hover .anchor .octicon-link,
|
||||
.markdown-body h4:hover .anchor .octicon-link,
|
||||
.markdown-body h5:hover .anchor .octicon-link,
|
||||
.markdown-body h6:hover .anchor .octicon-link {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.markdown-body h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.markdown-body h5 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.markdown-body h6 {
|
||||
font-size: 0.85em;
|
||||
color: #6a737d;
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.markdown-body ul ul,
|
||||
.markdown-body ul ol,
|
||||
.markdown-body ol ol,
|
||||
.markdown-body ol ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body li>p {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.markdown-body li+li {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.markdown-body dl {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body dl dt {
|
||||
padding: 0;
|
||||
margin-top: 16px;
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body dl dd {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.markdown-body table th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body table th,
|
||||
.markdown-body table td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-body table tr {
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #c6cbd1;
|
||||
}
|
||||
|
||||
.markdown-body table tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
box-sizing: content-box;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.markdown-body img[align=right] {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.markdown-body img[align=left] {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
padding: 0;
|
||||
padding-top: 0.2em;
|
||||
padding-bottom: 0.2em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(27,31,35,0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown-body code::before,
|
||||
.markdown-body code::after {
|
||||
letter-spacing: -0.2em;
|
||||
content: "\00a0";
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
.markdown-body pre>code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.markdown-body .highlight {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body .highlight pre {
|
||||
margin-bottom: 0;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.markdown-body .highlight pre,
|
||||
.markdown-body pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
/* background-color: #f6f8fa; */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
display: inline;
|
||||
max-width: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
line-height: inherit;
|
||||
word-wrap: normal;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.markdown-body pre code::before,
|
||||
.markdown-body pre code::after {
|
||||
content: normal;
|
||||
}
|
||||
|
||||
.markdown-body .full-commit .btn-outline:not(:disabled):hover {
|
||||
color: #005cc5;
|
||||
border-color: #005cc5;
|
||||
}
|
||||
|
||||
.markdown-body kbd {
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
line-height: 10px;
|
||||
color: #444d56;
|
||||
vertical-align: middle;
|
||||
background-color: #fafbfc;
|
||||
border: solid 1px #d1d5da;
|
||||
border-bottom-color: #c6cbd1;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 #c6cbd1;
|
||||
}
|
||||
|
||||
.markdown-body :checked+.radio-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-color: #0366d6;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item+.task-list-item {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item input {
|
||||
margin: 0 0.2em 0.25em -1.6em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
border-bottom-color: #eee;
|
||||
}
|
||||
|
||||
BIN
Yi.Bbs.Vue3/src/assets/login_images/QQ.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
Yi.Bbs.Vue3/src/assets/login_images/WeChat.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
Yi.Bbs.Vue3/src/assets/login_images/login_bg_phone.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
Yi.Bbs.Vue3/src/assets/login_images/login_two.jpg
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
Yi.Bbs.Vue3/src/assets/logo.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
48
Yi.Bbs.Vue3/src/assets/main.css
Normal file
@@ -0,0 +1,48 @@
|
||||
/* @import './base.css'; */
|
||||
/* #app {
|
||||
margin: 0 auto;
|
||||
font-weight: normal;
|
||||
} */
|
||||
|
||||
body
|
||||
{
|
||||
margin: 0 auto;
|
||||
font-weight: normal;
|
||||
}
|
||||
a{
|
||||
text-decoration: none;
|
||||
}
|
||||
/*
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
} */
|
||||
190
Yi.Bbs.Vue3/src/assets/styles/index.scss
Normal file
@@ -0,0 +1,190 @@
|
||||
// @import './variables.module.scss';
|
||||
// @import './mixin.scss';
|
||||
// @import './transition.scss';
|
||||
// @import './element-ui.scss';
|
||||
// @import './sidebar.scss';
|
||||
// @import './btn.scss';
|
||||
// @import './ruoyi.scss';
|
||||
|
||||
// body {
|
||||
// height: 100%;
|
||||
// margin: 0;
|
||||
// -moz-osx-font-smoothing: grayscale;
|
||||
// -webkit-font-smoothing: antialiased;
|
||||
// text-rendering: optimizeLegibility;
|
||||
// font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
// }
|
||||
|
||||
// label {
|
||||
// font-weight: 700;
|
||||
// }
|
||||
|
||||
// html {
|
||||
// height: 100%;
|
||||
// box-sizing: border-box;
|
||||
// }
|
||||
|
||||
// #app {
|
||||
// height: 100%;
|
||||
// }
|
||||
|
||||
// *,
|
||||
// *:before,
|
||||
// *:after {
|
||||
// box-sizing: inherit;
|
||||
// }
|
||||
|
||||
// .no-padding {
|
||||
// padding: 0px !important;
|
||||
// }
|
||||
|
||||
// .padding-content {
|
||||
// padding: 4px 0;
|
||||
// }
|
||||
|
||||
// a:focus,
|
||||
// a:active {
|
||||
// outline: none;
|
||||
// }
|
||||
|
||||
// a,
|
||||
// a:focus,
|
||||
// a:hover {
|
||||
// cursor: pointer;
|
||||
// color: inherit;
|
||||
// text-decoration: none;
|
||||
// }
|
||||
|
||||
// div:focus {
|
||||
// outline: none;
|
||||
// }
|
||||
|
||||
// .fr {
|
||||
// float: right;
|
||||
// }
|
||||
|
||||
// .fl {
|
||||
// float: left;
|
||||
// }
|
||||
|
||||
// .pr-5 {
|
||||
// padding-right: 5px;
|
||||
// }
|
||||
|
||||
// .pl-5 {
|
||||
// padding-left: 5px;
|
||||
// }
|
||||
|
||||
// .block {
|
||||
// display: block;
|
||||
// }
|
||||
|
||||
// .pointer {
|
||||
// cursor: pointer;
|
||||
// }
|
||||
|
||||
// .inlineBlock {
|
||||
// display: block;
|
||||
// }
|
||||
|
||||
// .clearfix {
|
||||
// &:after {
|
||||
// visibility: hidden;
|
||||
// display: block;
|
||||
// font-size: 0;
|
||||
// content: " ";
|
||||
// clear: both;
|
||||
// height: 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
// aside {
|
||||
// background: #eef1f6;
|
||||
// padding: 8px 24px;
|
||||
// margin-bottom: 20px;
|
||||
// border-radius: 2px;
|
||||
// display: block;
|
||||
// line-height: 32px;
|
||||
// font-size: 16px;
|
||||
// font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
// color: #2c3e50;
|
||||
// -webkit-font-smoothing: antialiased;
|
||||
// -moz-osx-font-smoothing: grayscale;
|
||||
|
||||
// a {
|
||||
// color: #337ab7;
|
||||
// cursor: pointer;
|
||||
|
||||
// &:hover {
|
||||
// color: rgb(32, 160, 255);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// .components-container {
|
||||
// margin: 30px 50px;
|
||||
// position: relative;
|
||||
// }
|
||||
|
||||
// .pagination-container {
|
||||
// margin-top: 30px;
|
||||
// }
|
||||
|
||||
.text-center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
// .sub-navbar {
|
||||
// height: 50px;
|
||||
// line-height: 50px;
|
||||
// position: relative;
|
||||
// width: 100%;
|
||||
// text-align: right;
|
||||
// padding-right: 20px;
|
||||
// transition: 600ms ease position;
|
||||
// background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
|
||||
|
||||
// .subtitle {
|
||||
// font-size: 20px;
|
||||
// color: #fff;
|
||||
// }
|
||||
|
||||
// &.draft {
|
||||
// background: #d0d0d0;
|
||||
// }
|
||||
|
||||
// &.deleted {
|
||||
// background: #d0d0d0;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .link-type,
|
||||
// .link-type:focus {
|
||||
// color: #337ab7;
|
||||
// cursor: pointer;
|
||||
|
||||
// &:hover {
|
||||
// color: rgb(32, 160, 255);
|
||||
// }
|
||||
// }
|
||||
|
||||
// .filter-container {
|
||||
// padding-bottom: 10px;
|
||||
|
||||
// .filter-item {
|
||||
// display: inline-block;
|
||||
// vertical-align: middle;
|
||||
// margin-bottom: 10px;
|
||||
// }
|
||||
// }
|
||||
|
||||
// //refine vue-multiselect plugin
|
||||
// .multiselect {
|
||||
// line-height: 16px;
|
||||
// }
|
||||
|
||||
// .multiselect--active {
|
||||
// z-index: 1000 !important;
|
||||
// }
|
||||
206
Yi.Bbs.Vue3/src/assets/styles/login.scss
Normal file
@@ -0,0 +1,206 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/*公共CSS*/
|
||||
.box {
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgb(29, 67, 89);
|
||||
.content {
|
||||
box-shadow: 0px 1px 6px #3B4859;
|
||||
.login-wrapper {
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
.login-form {
|
||||
.form-item {
|
||||
margin: 20px 0;
|
||||
span {
|
||||
display: block;
|
||||
margin: 5px 20px;
|
||||
font-weight: 100;
|
||||
}
|
||||
.input-item {
|
||||
width: 100%;
|
||||
border-radius: 40px;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 20px;
|
||||
font-weight: 200;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
border-radius: 40px;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
font-weight: 100;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
span:nth-child(1) {
|
||||
flex: 1;
|
||||
}
|
||||
span:nth-child(3) {
|
||||
flex: 1;
|
||||
}
|
||||
.line {
|
||||
display: inline-block;
|
||||
max-width: 30%;
|
||||
width: 30%;
|
||||
}
|
||||
.divider-text {
|
||||
vertical-align: middle;
|
||||
margin: 0px 20px;
|
||||
line-height: 0px;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
.other-login-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.other-login-item {
|
||||
border: 1px solid rgb(214, 222, 228);
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/*一般大于手机的尺寸CSS*/
|
||||
@media (min-width: 767px) {
|
||||
.box {
|
||||
background-color: #F0F2F5;
|
||||
.content {
|
||||
width: 85vw;
|
||||
height: 90vh;
|
||||
background: url("@/assets/login_images/login_two.jpg") no-repeat;
|
||||
background-size: 90% 100%;
|
||||
position: absolute;
|
||||
right: 15%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 20px;
|
||||
background-color: #fff;
|
||||
.login-wrapper {
|
||||
width: 25vw;
|
||||
position: absolute;
|
||||
right: 15%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 45px;
|
||||
color: rgb(81, 100, 115);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.login-form {
|
||||
margin: 10px 0;
|
||||
.form-item {
|
||||
span {
|
||||
color: rgb(81, 100, 115);
|
||||
}
|
||||
.input-item {
|
||||
height: 60px;
|
||||
border: 1px solid rgb(214, 222, 228);
|
||||
}
|
||||
}
|
||||
.login-btn {
|
||||
height: 50px;
|
||||
background-color: rgb(59, 72, 89);
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
.line {
|
||||
border-bottom: 1px solid rgb(214, 222, 228);
|
||||
}
|
||||
}
|
||||
.other-login-item {
|
||||
border-radius: 20px;
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/*手机端CSS*/
|
||||
@media (max-width: 768px) {
|
||||
.box {
|
||||
.content {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: url("@/assets/login_images/login_bg_phone.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
.login-wrapper {
|
||||
width: 70%;
|
||||
height: 60%;
|
||||
padding-top: 15%;
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
color: #fff;
|
||||
}
|
||||
.login-form {
|
||||
.form-item {
|
||||
margin: 10px 0;
|
||||
span {
|
||||
color: rgb(113, 129, 141);
|
||||
}
|
||||
.input-item {
|
||||
height: 30px;
|
||||
border: 1px solid rgb(113, 129, 141);
|
||||
background-color: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.login-btn {
|
||||
height: 40px;
|
||||
background-color: rgb(235, 95, 93);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
.line {
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
.divider-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.other-login-item {
|
||||
border-radius: 15px;
|
||||
img {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
271
Yi.Bbs.Vue3/src/assets/styles/ruoyi.scss
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* 通用css样式布局处理
|
||||
* Copyright (c) 2019 ruoyi
|
||||
*/
|
||||
|
||||
/** 基础通用 **/
|
||||
.pt5 {
|
||||
padding-top: 5px;
|
||||
}
|
||||
.pr5 {
|
||||
padding-right: 5px;
|
||||
}
|
||||
.pb5 {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.mt5 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.mr5 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.mb5 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.mb8 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.ml5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.mt10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.mr10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.mb10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.mt20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.mr20 {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.mb20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.ml20 {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.el-dialog:not(.is-fullscreen) {
|
||||
margin-top: 6vh !important;
|
||||
}
|
||||
|
||||
.el-dialog.scrollbar .el-dialog__body {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 70vh;
|
||||
padding: 10px 20px 0;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
.el-table__header-wrapper, .el-table__fixed-header-wrapper {
|
||||
th {
|
||||
word-break: break-word;
|
||||
background-color: #f8f8f9 !important;
|
||||
color: #515a6e;
|
||||
height: 40px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
.el-table__body-wrapper {
|
||||
.el-button [class*="el-icon-"] + span {
|
||||
margin-left: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 表单布局 **/
|
||||
.form-header {
|
||||
font-size:15px;
|
||||
color:#6379bb;
|
||||
border-bottom:1px solid #ddd;
|
||||
margin:8px 10px 25px 10px;
|
||||
padding-bottom:5px
|
||||
}
|
||||
|
||||
/** 表格布局 **/
|
||||
.pagination-container {
|
||||
// position: relative;
|
||||
height: 25px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 15px;
|
||||
padding: 10px 20px !important;
|
||||
}
|
||||
|
||||
/* tree border */
|
||||
.tree-border {
|
||||
margin-top: 5px;
|
||||
border: 1px solid #e5e6e7;
|
||||
background: #FFFFFF none;
|
||||
border-radius:4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-container .el-pagination {
|
||||
right: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@media ( max-width : 768px) {
|
||||
.pagination-container .el-pagination > .el-pagination__jump {
|
||||
display: none !important;
|
||||
}
|
||||
.pagination-container .el-pagination > .el-pagination__sizes {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table .fixed-width .el-button--small {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
/** 表格更多操作下拉样式 */
|
||||
.el-table .el-dropdown-link {
|
||||
cursor: pointer;
|
||||
color: #409EFF;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.el-table .el-dropdown, .el-icon-arrow-down {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.el-tree-node__content > .el-checkbox {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.list-group-striped > .list-group-item {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-radius: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
padding-left: 0px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border-bottom: 1px solid #e7eaec;
|
||||
border-top: 1px solid #e7eaec;
|
||||
margin-bottom: -1px;
|
||||
padding: 11px 0px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
// .el-card__header {
|
||||
// padding: 14px 15px 7px !important;
|
||||
// min-height: 40px;
|
||||
// }
|
||||
|
||||
// .el-card__body {
|
||||
// padding: 15px 20px 20px 20px !important;
|
||||
// }
|
||||
|
||||
.card-box {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* button color */
|
||||
.el-button--cyan.is-active,
|
||||
.el-button--cyan:active {
|
||||
background: #20B2AA;
|
||||
border-color: #20B2AA;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.el-button--cyan:focus,
|
||||
.el-button--cyan:hover {
|
||||
background: #48D1CC;
|
||||
border-color: #48D1CC;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.el-button--cyan {
|
||||
background-color: #20B2AA;
|
||||
border-color: #20B2AA;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* text color */
|
||||
.text-navy {
|
||||
color: #1ab394;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #1c84c6;
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: #23c6c8;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #f8ac59;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #ed5565;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
/* image */
|
||||
.img-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.img-lg {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.avatar-upload-preview {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(50%, -50%);
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px #ccc;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 拖拽列样式 */
|
||||
.sortable-ghost{
|
||||
opacity: .8;
|
||||
color: #fff!important;
|
||||
background: #42b983!important;
|
||||
}
|
||||
|
||||
/* 表格右侧工具栏样式 */
|
||||
.top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
55
Yi.Bbs.Vue3/src/components/AgreeInfo.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<el-button text @click="agree">
|
||||
<el-icon v-if="data.isAgree" color="#409EFF">
|
||||
<CircleCheckFilled />
|
||||
</el-icon>
|
||||
<el-icon v-else color="#1E1E1E">
|
||||
<Pointer />
|
||||
</el-icon> 点赞:{{ data.agreeNum ?? 0 }}</el-button>
|
||||
</template>
|
||||
<script setup>
|
||||
import {onMounted,reactive,watch} from 'vue'
|
||||
import { operate } from '@/apis/agreeApi'
|
||||
|
||||
|
||||
//'isAgree','agreeNum','id'
|
||||
const props = defineProps([ 'data'])
|
||||
|
||||
watch(()=>props,(n)=>{
|
||||
data.id=n.data.id;
|
||||
data.isAgree=n.data.isAgree;
|
||||
data.agreeNum=n.data.agreeNum;
|
||||
},{deep:true})
|
||||
|
||||
|
||||
const data=reactive({
|
||||
id:'',
|
||||
isAgree:false,
|
||||
agreeNum:0
|
||||
})
|
||||
// onMounted(()=>{
|
||||
|
||||
// })
|
||||
//点赞操作
|
||||
const agree = async () => {
|
||||
const response = await operate(data.id)
|
||||
const res = response.data;
|
||||
//提示框,颜色区分
|
||||
if (res.isAgree) {
|
||||
data.isAgree = true;
|
||||
data.agreeNum += 1;
|
||||
ElMessage({
|
||||
message: res.message,
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
else {
|
||||
data.isAgree = false;
|
||||
data.agreeNum-= 1;
|
||||
ElMessage({
|
||||
message: res.message,
|
||||
type: 'warning',
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
43
Yi.Bbs.Vue3/src/components/ArticleContentInfo.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="markdown-body" v-html="outputHtml"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { marked } from 'marked';
|
||||
|
||||
import hljs from "highlight.js";
|
||||
//可以设置加载样式切换主题
|
||||
import 'highlight.js/styles/atom-one-dark.css'
|
||||
import '@/assets/github-markdown.css'
|
||||
import { ref,watch } from 'vue';
|
||||
|
||||
|
||||
|
||||
const outputHtml=ref("")
|
||||
const props = defineProps(['code'])
|
||||
watch(props,(n,o)=>{
|
||||
marked.setOptions({
|
||||
renderer: new marked.Renderer(),
|
||||
highlight: function(code) {
|
||||
return hljs.highlightAuto(code).value;
|
||||
},
|
||||
pedantic: false,
|
||||
gfm: true,//允许 Git Hub标准的markdown
|
||||
tables: true,//支持表格
|
||||
|
||||
breaks: true,
|
||||
sanitize: false,
|
||||
smartLists: true,
|
||||
smartypants: false,
|
||||
xhtml: false,
|
||||
smartLists: true,
|
||||
}
|
||||
);
|
||||
//需要注意代码块样式
|
||||
outputHtml.value = marked(n.code).replace(/<pre>/g, "<pre class='hljs'>")
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
131
Yi.Bbs.Vue3/src/components/AvatarInfo.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template >
|
||||
<div class="avatar">
|
||||
<div class="avatar-left">
|
||||
<el-avatar :size="props.size" :src="iconUrl" />
|
||||
|
||||
<div v-if="props.isSelf">
|
||||
<div class="nick"> {{ userInfo.nick }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!props.isSelf">
|
||||
|
||||
<div class="nick" :class="{ mt_1: props.time != 'undefined' }"> {{ userInfo.nick }}</div>
|
||||
<div class="remarks" v-if="props.time"> {{ props.time }}</div>
|
||||
<div class="remarks">
|
||||
<slot name="bottom" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="info" v-if="!props.isSelf">
|
||||
<el-tag class="ml-2" type="warning">V8</el-tag>
|
||||
<el-tag class="ml-2" type="danger">会员</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<el-button v-if="props.showWatching" type="primary" size="default" icon="Plus">关注</el-button>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import useUserStore from '@/stores/user'
|
||||
import { reactive, watch, onMounted, computed, ref } from 'vue';
|
||||
//userInfo
|
||||
//{icon,name,role,id},根据判断userInfo是否等于未定义,来觉得是当前登录用户信息,还是其他人信息
|
||||
const props = defineProps(['size', 'showWatching', 'time', 'userInfo', 'isSelf'])
|
||||
const userStore = useUserStore();
|
||||
const userInfo = reactive({
|
||||
icon: "",
|
||||
nick: "",
|
||||
role: [],
|
||||
id: ""
|
||||
});
|
||||
const iconUrl=ref('/src/assets/logo.ico');
|
||||
const iconUrlHandler = () => {
|
||||
if (userInfo.icon == null || userInfo.icon == undefined || userInfo.icon == '') {
|
||||
|
||||
return '/src/assets/logo.ico';
|
||||
}
|
||||
return `${import.meta.env.VITE_APP_BASEAPI}/file/${userInfo.icon}`;
|
||||
}
|
||||
|
||||
watch(userStore, (n) => {
|
||||
if (props.userInfo == undefined) {
|
||||
userInfo.nick = n.name;
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
watch(() => props, (n) => {
|
||||
Init();
|
||||
}, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
Init();
|
||||
})
|
||||
|
||||
const Init = () => {
|
||||
//使用传入值
|
||||
if (props.userInfo != undefined) {
|
||||
userInfo.icon = props.userInfo.icon;
|
||||
userInfo.nick = props.userInfo.nick;
|
||||
userInfo.role = props.userInfo.role;
|
||||
userInfo.id = props.userInfo.id;
|
||||
iconUrl.value=iconUrlHandler(userInfo.icon)
|
||||
}
|
||||
|
||||
//使用当前登录用户
|
||||
else {
|
||||
|
||||
userInfo.icon = userStore.icon;
|
||||
userInfo.nick = userStore.name;
|
||||
userInfo.role = userStore.role;
|
||||
userInfo.id = userStore.id;
|
||||
iconUrl.value=userInfo.icon;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.mt_1 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.nick {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 0.6rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.info .el-tag {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: white;
|
||||
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.avatar-left {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-avatar {
|
||||
margin-right: 1.2rem;
|
||||
}
|
||||
|
||||
.remarks {
|
||||
padding-top: 0.5rem;
|
||||
color: #8C8C8C;
|
||||
}
|
||||
</style>
|
||||
38
Yi.Bbs.Vue3/src/components/BottomInfo.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="botton-div">
|
||||
<a><el-icon><UserFilled /></el-icon>站长:{{configStore.author}}</a>
|
||||
<a><el-icon><Search /></el-icon>{{configStore.bottom}}</a>
|
||||
<a><el-icon><View /></el-icon>关于本站</a>
|
||||
<a><el-icon><Message /></el-icon>建议反馈</a>
|
||||
<p></p>
|
||||
<a ><el-icon><Position /></el-icon>{{configStore.icp}}</a>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import useConfigStore from "@/stores/config";
|
||||
const configStore= useConfigStore();
|
||||
</script>
|
||||
<style scoped>
|
||||
.el-icon
|
||||
{margin: 0 0.2rem;}
|
||||
a{
|
||||
margin-right: 2rem;
|
||||
line-height: 1.8rem;
|
||||
}
|
||||
a:hover {
|
||||
color: #40a9ff;
|
||||
cursor:pointer;
|
||||
}
|
||||
.botton-div
|
||||
{
|
||||
background: transparent;
|
||||
color: rgba(0,0,0,.45);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
width: auto;
|
||||
justify-content: center;
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
</style>
|
||||
204
Yi.Bbs.Vue3/src/components/CommentInfo.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick">
|
||||
<el-tab-pane label="评论" name="comment"></el-tab-pane>
|
||||
<el-tab-pane label="相关内容" name="interrelated"></el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="total">
|
||||
<div style="align-self: center;"> 共{{total}}个评论</div>
|
||||
<div> <el-radio-group v-model="selectRadio">
|
||||
<el-radio-button label="new" name="new">最新</el-radio-button>
|
||||
<el-radio-button label="host" name="host">最热</el-radio-button>
|
||||
</el-radio-group></div>
|
||||
</div>
|
||||
|
||||
|
||||
<el-divider />
|
||||
<div v-hasPer="['bbs:comment:add']">
|
||||
<el-input v-model="topContent" placeholder="发表一个友善的评论吧~" :rows="5" type="textarea"></el-input>
|
||||
<el-button @click="addTopComment" type="primary" class="btn-top-comment" >发表评论</el-button>
|
||||
<el-button class="btn-top-comment">其他</el-button>
|
||||
|
||||
<el-divider />
|
||||
</div>
|
||||
|
||||
<!-- 开始评论主体 -->
|
||||
|
||||
<div v-for="item in commentList" :key="item.id" class="comment1">
|
||||
<AvatarInfo :userInfo="item.createUser" />
|
||||
<div class="content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
<span class="time"> {{ item.creationTime }} </span>
|
||||
<span class="pointer"><el-icon>
|
||||
<Pointer />
|
||||
</el-icon> 0</span>
|
||||
<el-button type="primary" @click="replay(item.createUser.nick, item.id, item.id)" size="large" text v-hasPer="['bbs:comment:add']">回复</el-button>
|
||||
<el-button type="danger" @click="delComment(item.id)" size="large" text v-hasPer="['bbs:comment:remove']">删除</el-button>
|
||||
<div v-show="replayId == item.id" class="input-reply">
|
||||
<el-input v-model="form.content" :placeholder="placeholder" :rows="3" type="textarea"></el-input>
|
||||
<div class="btn-reply">
|
||||
<el-button @click="addComment" type="primary" v-hasPer="['bbs:comment:add']">回复</el-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 开始子评论主体 -->
|
||||
<div v-for="children in item.children" :key="children.id" class="comment2">
|
||||
|
||||
<div style="display: flex ;">
|
||||
<AvatarInfo :userInfo="children.createUser" />
|
||||
<span style="align-self: center;color:#606266;"> 回复@{{ children.commentedUser.nick }}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{ children.content }}
|
||||
</div>
|
||||
<span class="time">{{ children.creationTime }} </span>
|
||||
<span class="pointer"> <el-icon>
|
||||
<Pointer />
|
||||
</el-icon>0</span>
|
||||
<el-button type="primary" @click="replay(children.createUser.nick, children.id, item.id)" size="large" text v-hasPer="['bbs:comment:add']">回复</el-button>
|
||||
<el-button type="danger" @click="delComment(children.id)" size="large" text v-hasPer="['bbs:comment:remove']">删除</el-button>
|
||||
<div v-show="replayId == children.id" class="input-reply">
|
||||
<el-input v-model="form.content" :placeholder="placeholder" :rows="3" type="textarea"></el-input>
|
||||
<div class="btn-reply">
|
||||
<el-button @click="addComment" type="primary" v-hasPer="['bbs:comment:add']">回复</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
</div>
|
||||
<el-empty v-show="commentList.length<=0" description="评论空空如也,快来抢占沙发~" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getListByDiscussId, add ,del} from "@/apis/commentApi.js";
|
||||
import AvatarInfo from './AvatarInfo.vue';
|
||||
//数据定义
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const commentList = ref([]);
|
||||
const query = reactive({});
|
||||
const topContent=ref('');
|
||||
//当前回复id
|
||||
const replayId = ref('');
|
||||
//回复文本框
|
||||
const placeholder = ref('')
|
||||
|
||||
//选择类型:评论
|
||||
const activeName = ref('comment');
|
||||
//选择 最新
|
||||
const selectRadio = ref('new');
|
||||
//评论总数
|
||||
const total=ref(0);
|
||||
const form = reactive({
|
||||
content: "",
|
||||
discussId: route.params.discussId,
|
||||
query,
|
||||
parentId: 0,
|
||||
rootId: 0,
|
||||
});
|
||||
onMounted(async () => {
|
||||
await loadComment();
|
||||
});
|
||||
const loadComment = async () => {
|
||||
topContent.value='';
|
||||
form.content = '';
|
||||
const response = await getListByDiscussId(route.params.discussId, query);
|
||||
commentList.value = response.data.items;
|
||||
total.value=response.data.total
|
||||
};
|
||||
const addTopComment = async () => {
|
||||
form.parentId = 0;
|
||||
form.rootId = 0;
|
||||
form.content=topContent.value;
|
||||
await addComment();
|
||||
}
|
||||
const addComment = async () => {
|
||||
if(form.content.length<=0)
|
||||
{
|
||||
ElMessage.error('输入评论不能为空!')
|
||||
return
|
||||
}
|
||||
await add(form);
|
||||
await loadComment();
|
||||
ElMessage({
|
||||
message: '评论发表成功!',
|
||||
type: 'success',
|
||||
})
|
||||
};
|
||||
const delComment=async(ids)=>{
|
||||
ElMessageBox.confirm(`确定是否删除编号[${ids}]的评论吗?`, "警告", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
|
||||
await del(ids);
|
||||
await loadComment();
|
||||
ElMessage({
|
||||
message: '评论已删除!',
|
||||
type: 'success',
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
const replay = async (parentUserName, parentId, rootId) => {
|
||||
replayId.value = parentId;
|
||||
form.parentId = parentId;
|
||||
form.rootId = rootId;
|
||||
placeholder.value = `回复@${parentUserName}`;
|
||||
}
|
||||
|
||||
//切换 评论、相关内容
|
||||
const handleClick = () => { }
|
||||
</script>
|
||||
<style scoped>
|
||||
.input-reply {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-reply {
|
||||
margin: 1rem 0;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.comment1 .pointer {
|
||||
margin: 0 0 0 1rem;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: #8C8C8C;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.total {
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.comment2 {
|
||||
margin-left: 3rem;
|
||||
}
|
||||
|
||||
.el-divider {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.btn-top-comment {
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
197
Yi.Bbs.Vue3/src/components/DisscussCard.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<el-badge :value="props.badge ?? ''" class="box-card">
|
||||
<el-card shadow="never" :style="{ 'border-color': discuss.color }">
|
||||
<el-row>
|
||||
|
||||
<!-- 头部 -->
|
||||
<el-col :span=24 class="card-header">
|
||||
<AvatarInfo :userInfo="discuss.user" :time="discuss.creationTime" />
|
||||
</el-col>
|
||||
|
||||
|
||||
<!-- 身体 -->
|
||||
|
||||
<el-col :span=18 >
|
||||
<el-row>
|
||||
|
||||
<el-col v-if="discuss.isBan" :span=24 class=" item item-title "> <el-link size="100" :underline="false"
|
||||
style="color:#F56C6C;" >{{ discuss.title }}</el-link></el-col>
|
||||
|
||||
<el-col v-else :span=24 class=" item item-title "> <el-link size="100" :underline="false"
|
||||
@click="enterDiscuss(discuss.id)">{{ discuss.title }}</el-link></el-col>
|
||||
|
||||
|
||||
|
||||
<el-col :span=24 class=" item item-description">{{ discuss.introduction }}</el-col>
|
||||
<el-col :span=24 class=" item item-tag"><el-tag v-for="i in 4" :key="i">教程</el-tag></el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
|
||||
<el-col :span=6 style=" display: flex;justify-content: center;">
|
||||
<el-image :preview-src-list="[getUrl(discuss.cover)]" v-if="discuss.cover" :src="getUrl(discuss.cover)" style="width: 100px;height: 100px;" />
|
||||
</el-col>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 底部 -->
|
||||
<el-col :span=24 class=" item item-bottom " style=" margin-bottom: 0;">
|
||||
<el-space :size="10" :spacer="spacer">
|
||||
<div class="item-description">
|
||||
{{ discuss.creationTime }}
|
||||
</div>
|
||||
<AgreeInfo :data="discuss"/>
|
||||
<!--
|
||||
<el-button text @click="agree">
|
||||
<el-icon v-if="discuss.isAgree" color="#409EFF">
|
||||
<CircleCheckFilled />
|
||||
</el-icon>
|
||||
<el-icon v-else color="#1E1E1E">
|
||||
<Pointer />
|
||||
</el-icon> 点赞:{{ discuss.agreeNum ?? 0 }}</el-button>
|
||||
<el-button icon="Star" text>
|
||||
收藏</el-button> -->
|
||||
|
||||
<el-button icon="View" text>
|
||||
浏览数:{{ discuss.seeNum ?? 0 }}</el-button>
|
||||
|
||||
|
||||
</el-space>
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
|
||||
</el-card>
|
||||
</el-badge>
|
||||
</template>
|
||||
<script setup>
|
||||
import { h, ref, toRef, onMounted ,reactive} from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AvatarInfo from './AvatarInfo.vue';
|
||||
import AgreeInfo from './AgreeInfo.vue'
|
||||
import { operate } from '@/apis/agreeApi'
|
||||
|
||||
const props = defineProps(['discuss','badge'])
|
||||
const discuss=reactive({
|
||||
id:'',
|
||||
title:"",
|
||||
introduction:"",
|
||||
creationTime:"",
|
||||
user:{},
|
||||
color:"",
|
||||
seeNum:0,
|
||||
agreeNum:0,
|
||||
isAgree:false,
|
||||
cover:"",
|
||||
isBan:false,
|
||||
isAgree:false,
|
||||
agreeNum:0
|
||||
})
|
||||
const router = useRouter()
|
||||
const spacer = h(ElDivider, { direction: 'vertical' })
|
||||
const enterDiscuss = (id) => {
|
||||
router.push(`/article/${id}`)
|
||||
}
|
||||
const getUrl= (str)=>{
|
||||
return `${import.meta.env.VITE_APP_BASEAPI}/file/${str}`
|
||||
}
|
||||
|
||||
//点赞操作
|
||||
const agree = async () => {
|
||||
const response = await operate(discuss.id)
|
||||
const res = response.data;
|
||||
//提示框,颜色区分
|
||||
if (res.isAgree) {
|
||||
discuss.isAgree = true;
|
||||
discuss.agreeNum += 1;
|
||||
ElMessage({
|
||||
message: res.message,
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
else {
|
||||
discuss.isAgree = false;
|
||||
discuss.agreeNum-= 1;
|
||||
ElMessage({
|
||||
message: res.message,
|
||||
type: 'warning',
|
||||
})
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
|
||||
// id:'',
|
||||
// title:"",
|
||||
// introduction:"",
|
||||
// creationTime:"",
|
||||
// user:{},
|
||||
// color:"",
|
||||
// seeNum:0,
|
||||
// agreeNum:0,
|
||||
// isAgree:""
|
||||
discuss.id=props.discuss.id;
|
||||
discuss.title=props.discuss.title;
|
||||
discuss.introduction=props.discuss.introduction;
|
||||
discuss.creationTime=props.discuss.creationTime;
|
||||
discuss.user=props.discuss.user;
|
||||
discuss.color=props.discuss.color;
|
||||
discuss.seeNum=props.discuss.seeNum;
|
||||
discuss.isAgree=props.discuss.isAgree;
|
||||
discuss.agreeNum=props.discuss.agreeNum;
|
||||
discuss.isBan=props.discuss.isBan;
|
||||
discuss.cover=props.discuss.cover;
|
||||
discuss.value = props.isAgree;
|
||||
discuss.value = props.agreeNum;
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.el-card {
|
||||
border: 2px solid white
|
||||
}
|
||||
|
||||
.item-bottom .el-icon {
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
margin-bottom: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.box-card {
|
||||
width: 100%;
|
||||
min-height: 15rem;
|
||||
/* right: calc(1px + var(--el-badge-size)/ 2) !important; */
|
||||
/* top: 0 !important; */
|
||||
|
||||
}
|
||||
|
||||
.item-title {
|
||||
/* font-size: var(--el-font-size-large); */
|
||||
}
|
||||
|
||||
.item-description {
|
||||
font-size: var(--el-font-size-small);
|
||||
color: #8C8C8C;
|
||||
}
|
||||
|
||||
.item .el-tag {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 1.2rem;
|
||||
}
|
||||
|
||||
.el-link {
|
||||
font-size: initial;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
55
Yi.Bbs.Vue3/src/components/InfoCard.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps(['items','header','text','hideDivider'])
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="box-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{props.header}}</span>
|
||||
<el-link :underline="false" type="primary">{{props.text}}</el-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<slot name="content" />
|
||||
|
||||
|
||||
<div v-for="(item,i) in props.items " >
|
||||
<div class="text item">
|
||||
<slot name="item" v-bind="item"/>
|
||||
</div>
|
||||
<el-divider v-if="i!=props.items.length-1&&hideDivider==undefined" />
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
.el-divider
|
||||
{
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.box-card {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
151
Yi.Bbs.Vue3/src/components/MavonEdit.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<mavon-editor ref='md' v-model="text" :subfield="true" :codeStyle="props.codeStyle" :ishljs="true"
|
||||
:style="{ minHeight: props.height, maxHeight: '100%' }" class="edit" @imgAdd="imgAdd">
|
||||
|
||||
<!-- 引用视频链接的自定义按钮 -->
|
||||
<template v-slot:left-toolbar-after>
|
||||
<!--点击按钮触发的事件是打开表单对话框-->
|
||||
|
||||
|
||||
<el-button @click="fileDialogVisible=true" aria-hidden="true" class="op-icon fa" title="上传文件">
|
||||
<el-icon ><FolderChecked /></el-icon>
|
||||
</el-button>
|
||||
|
||||
|
||||
<el-dropdown :hide-on-click='false'>
|
||||
<el-button aria-hidden="true" class="op-icon fa" title="表情包">
|
||||
😊
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu >
|
||||
<el-dropdown-item>
|
||||
<table border="1">
|
||||
<tr>
|
||||
<td @click="text+='😊'">😊</td>
|
||||
<td>😊</td>
|
||||
<td>😊</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>😊</td>
|
||||
<td>😊</td>
|
||||
<td>😊</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>😊</td>
|
||||
<td>😊</td>
|
||||
<td>😊</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</el-dropdown-item>
|
||||
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
|
||||
<el-dialog
|
||||
:modal=false
|
||||
:draggable=true
|
||||
|
||||
v-model="fileDialogVisible"
|
||||
title="文件上传"
|
||||
width="30%"
|
||||
:before-close="fileHandleClose"
|
||||
>
|
||||
<span>选择你的文件:</span>
|
||||
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
drag
|
||||
:action="fileUploadUrl"
|
||||
multiple
|
||||
:on-success="onSuccess"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
可将文件拖拽到这里 <em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
文件需小于100MB以内
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<p v-for="(item,i) in fileUrlList" :key="i">{{` ${i+1}: ${getUrl(item)}` }} <el-button></el-button></p>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="fileDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dialogVisible = false">
|
||||
确认
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
</mavon-editor>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { mavonEditor } from 'mavon-editor'
|
||||
import 'mavon-editor/dist/css/index.css'
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { upload } from '@/apis/fileApi'
|
||||
|
||||
const md = ref(null);
|
||||
const props = defineProps(['height', 'modelValue', "codeStyle"])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const fileDialogVisible=ref(false)
|
||||
|
||||
//已经上传好的文件列表url
|
||||
const fileUrlList=ref([])
|
||||
|
||||
const fileUploadUrl=`${import.meta.env.VITE_APP_BASEAPI}/file`
|
||||
// //v-model传值出去
|
||||
const text = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const getUrl= (str)=>{
|
||||
return `${import.meta.env.VITE_APP_BASEAPI}/file/${str}`
|
||||
}
|
||||
|
||||
//关闭文件上传弹窗
|
||||
const fileHandleClose=()=>{
|
||||
fileDialogVisible.value=false;
|
||||
}
|
||||
//文件上传成功后
|
||||
const onSuccess=(response)=>{
|
||||
fileUrlList.value.push(response.data[0].id)
|
||||
|
||||
}
|
||||
//图片上传
|
||||
const imgAdd = async (pos, $file) => {
|
||||
// 第一步.将图片上传到服务器.
|
||||
var formdata = new FormData();
|
||||
formdata.append('file', $file);
|
||||
const response = await upload(formdata)
|
||||
const url = `${import.meta.env.VITE_APP_BASEAPI}/file/${response.data[0].id}/true`;
|
||||
console.log(url)
|
||||
md.value.$img2Url(pos, url);
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.edit {
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
49
Yi.Bbs.Vue3/src/components/PlateCard.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<el-row>
|
||||
<el-col
|
||||
>
|
||||
<el-card :body-style="{ padding: '0px' }" shadow="never">
|
||||
<img
|
||||
src=""
|
||||
class="image"
|
||||
/>
|
||||
<div style="padding: 14px">
|
||||
<span>{{props.name}}</span>
|
||||
<div class="bottom">
|
||||
<time class="remarks">{{ props.introduction }}</time>
|
||||
<RouterLink :to="`/discuss/${props.id}`"> <el-button text class="button" type="primary">进入<el-icon><CaretRight /></el-icon></el-button> </RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const props = defineProps(['name','introduction','id'])
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.remarks {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
margin-top: 13px;
|
||||
line-height: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
28
Yi.Bbs.Vue3/src/components/ScrollbarInfo.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<el-scrollbar>
|
||||
<div class="scrollbar-flex-content">
|
||||
<p v-for="item in 50" :key="item" class="scrollbar-item">
|
||||
推荐
|
||||
</p>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
.scrollbar-flex-content {
|
||||
display: flex;
|
||||
}
|
||||
.scrollbar-item {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
height: 2.6rem;
|
||||
margin: 0 0.2rem ;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
background-color:#FAFAFA;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
67
Yi.Bbs.Vue3/src/components/TreeArticleInfo.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<el-tree
|
||||
:data="props.data==''?[]:props.data"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
:expand-on-click-node="false"
|
||||
node-key="id"
|
||||
:default-expand-all='true'
|
||||
:highlight-current="true"
|
||||
:current-node-key="currentNodeKey"
|
||||
>
|
||||
|
||||
<template #default="{ node, data }">
|
||||
<span class="custom-tree-node">
|
||||
<span>{{data.name}}</span>
|
||||
<span>
|
||||
|
||||
<a style="color: #409EFF; margin-left: 8px" @click="$emit('create',node, data)" v-hasPer="['bbs:article:add']"
|
||||
|
||||
> + </a>
|
||||
<a style="color: #409EFF; margin-left: 8px" @click="$emit('update',node, data)" v-hasPer="['bbs:article:edit']"
|
||||
|
||||
> 编辑 </a>
|
||||
<a style="color: #f56c6c; margin-left: 8px" @click="$emit('remove',node, data)" v-hasPer="['bbs:article:remove']"
|
||||
|
||||
> 删除 </a>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
|
||||
const props = defineProps(['data',"currentNodeKey"])
|
||||
const emits= defineEmits(["handleNodeClick"])
|
||||
|
||||
const currentNodeKey=props.currentNodeKey;
|
||||
//数据定义
|
||||
//子文章数据
|
||||
// const articleData =ref([]);
|
||||
//树形子文章选项
|
||||
const defaultProps = {
|
||||
children: "children",
|
||||
label: "name",
|
||||
};
|
||||
// //子文章初始化
|
||||
// const loadArticleData=async()=>
|
||||
// {
|
||||
// articleData.value= await articleall(route.params.discussId);
|
||||
// }
|
||||
//点击事件
|
||||
const handleNodeClick = (data) => {
|
||||
emits('handleNodeClick',data)
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.custom-tree-node {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
</style>
|
||||
129
Yi.Bbs.Vue3/src/components/UserSelectInfo.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<el-select
|
||||
style="width: 600px;"
|
||||
v-model="value"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="请输入用户账号(可多选)"
|
||||
remote-show-suffix
|
||||
:remote-method="remoteMethod"
|
||||
:loading="loading"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref,computed } from 'vue'
|
||||
import {listUser} from '@/apis/userApi'
|
||||
const props = defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
|
||||
//这个为可选择的列表,{value,label},value为用户id,label为账号名称(不可重复)
|
||||
const options = ref([])
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted( async()=>{
|
||||
|
||||
const response= await listUser({ids:value.value.join()});
|
||||
const res=response.data.items;
|
||||
//下拉列表
|
||||
options.value = res
|
||||
.map((item) => {
|
||||
return { value: `${item.id}`, label: `用户:${item.userName}` }
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const loadUser=async(query)=>{
|
||||
const response= await listUser({userName:query});
|
||||
const res=response.data.items;
|
||||
//下拉列表
|
||||
options.value = res
|
||||
.map((item) => {
|
||||
return { value: `${item.id}`, label: `用户:${item.userName}` }
|
||||
})
|
||||
}
|
||||
|
||||
const remoteMethod =async (query) => {
|
||||
if (query) {
|
||||
loading.value = true
|
||||
await loadUser(query);
|
||||
loading.value = false
|
||||
} else {
|
||||
options.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const states = [
|
||||
'Alabama',
|
||||
'Alaska',
|
||||
'Arizona',
|
||||
'Arkansas',
|
||||
'California',
|
||||
'Colorado',
|
||||
'Connecticut',
|
||||
'Delaware',
|
||||
'Florida',
|
||||
'Georgia',
|
||||
'Hawaii',
|
||||
'Idaho',
|
||||
'Illinois',
|
||||
'Indiana',
|
||||
'Iowa',
|
||||
'Kansas',
|
||||
'Kentucky',
|
||||
'Louisiana',
|
||||
'Maine',
|
||||
'Maryland',
|
||||
'Massachusetts',
|
||||
'Michigan',
|
||||
'Minnesota',
|
||||
'Mississippi',
|
||||
'Missouri',
|
||||
'Montana',
|
||||
'Nebraska',
|
||||
'Nevada',
|
||||
'New Hampshire',
|
||||
'New Jersey',
|
||||
'New Mexico',
|
||||
'New York',
|
||||
'North Carolina',
|
||||
'North Dakota',
|
||||
'Ohio',
|
||||
'Oklahoma',
|
||||
'Oregon',
|
||||
'Pennsylvania',
|
||||
'Rhode Island',
|
||||
'South Carolina',
|
||||
'South Dakota',
|
||||
'Tennessee',
|
||||
'Texas',
|
||||
'Utah',
|
||||
'Vermont',
|
||||
'Virginia',
|
||||
'Washington',
|
||||
'West Virginia',
|
||||
'Wisconsin',
|
||||
'Wyoming',
|
||||
]
|
||||
</script>
|
||||
13
Yi.Bbs.Vue3/src/config/axios/ErrorCode.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
"000": "操作太频繁,请勿重复请求",
|
||||
401: "当前操作没有权限,请退出重试",
|
||||
403: "当前操作没有权限,请退出重试",
|
||||
404: "资源不存在",
|
||||
417: "未绑定登录账号,请使用密码登录后绑定",
|
||||
423: "演示环境不能操作,如需了解联系",
|
||||
426: "用户名不存在或密码错误",
|
||||
428: "验证码错误,请重新输入",
|
||||
429: "请求过频繁",
|
||||
479: "演示环境,没有权限操作",
|
||||
default: "系统未知错误,请反馈给管理员",
|
||||
};
|
||||
34
Yi.Bbs.Vue3/src/config/axios/config.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const config = {
|
||||
/**
|
||||
* api请求基础路径
|
||||
*/
|
||||
base_url: {
|
||||
// 开发环境接口前缀
|
||||
dev: import.meta.env.VITE_APP_BASEAPI,
|
||||
// 打包生产环境接口前缀
|
||||
pro: window.location.protocol + "//" + window.location.hostname + ":19001",
|
||||
},
|
||||
|
||||
/**
|
||||
* 接口请求前缀
|
||||
*/
|
||||
pre_interface: import.meta.env.VITE_APP_BASEAPI,
|
||||
|
||||
/**
|
||||
* 接口成功返回状态码
|
||||
*/
|
||||
result_code: "0000",
|
||||
|
||||
/**
|
||||
* 接口请求超时时间
|
||||
*/
|
||||
request_timeout: 60000,
|
||||
|
||||
/**
|
||||
* 默认接口请求类型
|
||||
* 可选值:application/x-www-form-urlencoded multipart/form-data
|
||||
*/
|
||||
default_headers: "application/json",
|
||||
};
|
||||
|
||||
export { config };
|
||||
89
Yi.Bbs.Vue3/src/config/axios/service.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import axios from "axios";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { config } from "@/config/axios/config";
|
||||
import { Session } from "@/utils/storage";
|
||||
import useAuths from "@/hooks/useAuths";
|
||||
|
||||
const { getToken } = useAuths();
|
||||
|
||||
const { request_timeout } = config;
|
||||
export const PATH_URL = import.meta.env.VITE_APP_BASEAPI;
|
||||
|
||||
// 配置新建一个 axios 实例
|
||||
const service = axios.create({
|
||||
baseURL: PATH_URL, // api 的 base_url
|
||||
timeout: request_timeout, // 请求超时时间
|
||||
headers: { "Content-Type": "application/json" },
|
||||
hideerror: false, //是否在底层显示错误信息
|
||||
});
|
||||
|
||||
// 添加请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 在发送请求之前做些什么 token
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
config.headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
if (Session.get("tenantId")) {
|
||||
config.headers["TenantId"] = Session.get("tenantId");
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
// 对请求错误做些什么
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 添加响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
return Promise.resolve(response);
|
||||
},
|
||||
(error) => {
|
||||
// 对响应错误做点什么
|
||||
if (error.message.indexOf("timeout") != -1) {
|
||||
ElMessage({
|
||||
type: "danger",
|
||||
message: "网络超时",
|
||||
});
|
||||
} else if (error.message == "Network Error") {
|
||||
ElMessage({
|
||||
type: "danger",
|
||||
message: "网络连接错误",
|
||||
});
|
||||
} else {
|
||||
const res = error.response || {};
|
||||
const status = Number(res.status) || 200;
|
||||
const message = res.data.error.message;
|
||||
if (status === 401) {
|
||||
ElMessage({
|
||||
type: "danger",
|
||||
message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (status !== 200) {
|
||||
if (status >= 500) {
|
||||
ElMessage({
|
||||
type: "danger",
|
||||
message: "网络开小差了,请稍后再试",
|
||||
});
|
||||
return Promise.reject(new Error(message));
|
||||
}
|
||||
// 避开找不到后端接口的提醒
|
||||
if (status !== 404) {
|
||||
ElMessage({
|
||||
type: "danger",
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.reject(new Error(error));
|
||||
}
|
||||
);
|
||||
|
||||
// 导出 axios 实例
|
||||
export default service;
|
||||
9
Yi.Bbs.Vue3/src/directive/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import hasRole from './permission/hasRole'
|
||||
import hasPermi from './permission/hasPermi'
|
||||
// import copyText from './common/copyText'
|
||||
|
||||
export default function directive(app){
|
||||
app.directive('hasRole', hasRole)
|
||||
app.directive('hasPer', hasPermi)
|
||||
// app.directive('copyText', copyText)
|
||||
}
|
||||
29
Yi.Bbs.Vue3/src/directive/permission/hasDiscussPermi.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* v-hasDiscussPermi 操作权限处理
|
||||
*/
|
||||
|
||||
import useUserStore from '@/stores/user'
|
||||
|
||||
//传一个值,一个主题id的创建者id,判断当前主题是否为自己创建,拥有*:*:*,直接跳过
|
||||
export default {
|
||||
mounted(el, binding, vnode) {
|
||||
const { value } = binding
|
||||
const all_permission = "*:*:*";
|
||||
const permissions = useUserStore().permissions
|
||||
const userId = useUserStore().id
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const permissionFlag = value
|
||||
|
||||
const hasPermissions = permissions.some(permission => {
|
||||
return all_permission === permission || permissionFlag==userId
|
||||
})
|
||||
|
||||
if (!hasPermissions) {
|
||||
el.parentNode && el.parentNode.removeChild(el)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`请设置操作主题用户签值`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
Yi.Bbs.Vue3/src/directive/permission/hasPermi.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* v-hasPermi 操作权限处理
|
||||
* Copyright (c) 2019 ruoyi
|
||||
*/
|
||||
|
||||
import useUserStore from '@/stores/user'
|
||||
|
||||
export default {
|
||||
mounted(el, binding, vnode) {
|
||||
const { value } = binding
|
||||
const all_permission = "*:*:*";
|
||||
const permissions = useUserStore().permissions
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const permissionFlag = value
|
||||
|
||||
const hasPermissions = permissions.some(permission => {
|
||||
return all_permission === permission || permissionFlag.includes(permission)
|
||||
})
|
||||
|
||||
if (!hasPermissions) {
|
||||
el.parentNode && el.parentNode.removeChild(el)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`请设置操作权限标签值`)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Yi.Bbs.Vue3/src/directive/permission/hasRole.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* v-hasRole 角色权限处理
|
||||
* Copyright (c) 2019 ruoyi
|
||||
*/
|
||||
|
||||
import useUserStore from '@/stores/user'
|
||||
|
||||
export default {
|
||||
mounted(el, binding, vnode) {
|
||||
const { value } = binding
|
||||
const super_admin = "admin";
|
||||
const roles = useUserStore().roles
|
||||
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const roleFlag = value
|
||||
|
||||
const hasRole = roles.some(role => {
|
||||
return super_admin === role || roleFlag.includes(role)
|
||||
})
|
||||
|
||||
if (!hasRole) {
|
||||
el.parentNode && el.parentNode.removeChild(el)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`请设置角色权限标签值"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
138
Yi.Bbs.Vue3/src/hooks/useAuths.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import useUserStore from "@/stores/user";
|
||||
import router from "@/router";
|
||||
import { Session, Local } from "@/utils/storage";
|
||||
import { userLogin, getUserDetailInfo, userLogout } from "@/apis/auth";
|
||||
|
||||
const TokenKey = "AccessToken";
|
||||
export const AUTH_MENUS = "AUTH_MENUS";
|
||||
export const AUTH_USER = "AUTH_USER";
|
||||
|
||||
export default function useAuths(opt) {
|
||||
const defaultOpt = {
|
||||
loginUrl: "/login", // 登录页跳转url 默认: /login
|
||||
loginReUrl: "", // 登录页登陆成功后带重定向redirect=的跳转url 默认为空
|
||||
homeUrl: "/index", // 主页跳转url 默认: /index
|
||||
otherQuery: {}, // 成功登录后携带的(除redirect外)其他参数
|
||||
};
|
||||
|
||||
let option = {
|
||||
...defaultOpt,
|
||||
...opt,
|
||||
};
|
||||
|
||||
// 获取token
|
||||
const getToken = () => {
|
||||
return Session.get(TokenKey);
|
||||
};
|
||||
|
||||
// 存储token到cookies
|
||||
const setToken = (token) => {
|
||||
if (token == null) {
|
||||
return false;
|
||||
}
|
||||
Session.set(TokenKey, token);
|
||||
return true;
|
||||
};
|
||||
|
||||
// 删除token
|
||||
const removeToken = () => {
|
||||
Session.remove(TokenKey);
|
||||
return true;
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const logoutFun = async () => {
|
||||
let flag = true;
|
||||
try {
|
||||
await userLogout().then((res) => {
|
||||
ElMessage({
|
||||
message: "退出成功",
|
||||
type: "info",
|
||||
duration: 2000,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
flag = await ElMessageBox.confirm(
|
||||
`退出登录失败,是否强制退出?`,
|
||||
"提示",
|
||||
{
|
||||
confirmButtonText: "确 定",
|
||||
cancelButtonText: "取 消",
|
||||
type: "warning",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
//取消
|
||||
return false;
|
||||
});
|
||||
}
|
||||
if (flag) {
|
||||
clearStorage();
|
||||
}
|
||||
};
|
||||
|
||||
// 清空本地存储的信息
|
||||
const clearStorage = () => {
|
||||
Session.clear();
|
||||
Local.clear();
|
||||
removeToken();
|
||||
window.location.reload();
|
||||
Session.set("vuex", null);
|
||||
};
|
||||
|
||||
// 用户名密码登录
|
||||
const loginFun = async (params) => {
|
||||
const res = await userLogin(params);
|
||||
ElMessage({
|
||||
message: `您好${params.userName},登录成功!`,
|
||||
type: "success",
|
||||
});
|
||||
loginSuccess(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
// 获取用户基本信息、角色、菜单权限
|
||||
const getUserInfo = async () => {
|
||||
try {
|
||||
let { data } = await getUserDetailInfo();
|
||||
// useUserStore
|
||||
// store.dispatch("updateUserInfo", result);
|
||||
return data;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
// 登录成功之后的操作
|
||||
const loginSuccess = async (res) => {
|
||||
const { token } = res.data;
|
||||
|
||||
setToken(token);
|
||||
try {
|
||||
// 存储用户信息
|
||||
await getUserInfo(); // 用户信息
|
||||
// 登录成功后 路由跳转
|
||||
router.replace({
|
||||
path: option.loginReUrl ? option.loginReUrl : option.homeUrl,
|
||||
query: option.otherQuery,
|
||||
});
|
||||
} catch (error) {
|
||||
removeToken();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getToken,
|
||||
setToken,
|
||||
removeToken,
|
||||
loginFun,
|
||||
getUserInfo,
|
||||
logoutFun,
|
||||
clearStorage,
|
||||
};
|
||||
}
|
||||
50
Yi.Bbs.Vue3/src/hooks/useEcharts.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as echarts from "echarts";
|
||||
import { onMounted, onUnmounted, watch } from "vue";
|
||||
import { debounce } from "@/utils/index";
|
||||
import _ from "lodash";
|
||||
|
||||
// containerRef echarts实例 props 传入的值 baseOptions 图表初始化渲染
|
||||
export default function useEcharts(containerRef, emits, props, baseOptions) {
|
||||
let chart = null;
|
||||
|
||||
onMounted(() => {
|
||||
chart = echarts.init(containerRef.value);
|
||||
init();
|
||||
chart.on("click", function (param) {
|
||||
emits("chart-click", param);
|
||||
});
|
||||
//自适应不同屏幕时改变图表尺寸
|
||||
window.addEventListener("resize", cancalDebounce);
|
||||
});
|
||||
// 用于将echarts实例暴露出去使用
|
||||
const getChart = () => chart;
|
||||
onUnmounted(() => {
|
||||
chart && chart.dispose();
|
||||
window.removeEventListener("resize", cancalDebounce);
|
||||
});
|
||||
|
||||
function init() {
|
||||
let option = _.cloneDeep(baseOptions);
|
||||
chart.setOption(option);
|
||||
chart.setOption(props.option);
|
||||
}
|
||||
|
||||
function resize() {
|
||||
chart && chart.resize();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.option,
|
||||
(val) => {
|
||||
chart && chart.setOption(val);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const cancalDebounce = debounce(resize, 500);
|
||||
|
||||
return {
|
||||
getChart,
|
||||
resize,
|
||||
};
|
||||
}
|
||||
24
Yi.Bbs.Vue3/src/layout/AppBody.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
<template class="back-color">
|
||||
|
||||
<div class="content-main">
|
||||
<RouterView />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.body-main{
|
||||
min-height: 10rem;
|
||||
min-width: 10rem;
|
||||
background-color:#F0F2F5;
|
||||
|
||||
}
|
||||
.content-main{
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 1150px;
|
||||
}
|
||||
</style>
|
||||
112
Yi.Bbs.Vue3/src/layout/AppHeader.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="el-menu-demo"
|
||||
mode="horizontal"
|
||||
:ellipsis="false"
|
||||
@select="handleSelect"
|
||||
|
||||
>
|
||||
<el-menu-item class="logo" index="" @click="enterIndex" >
|
||||
<img class="img-icon" style="width: 35px; height: 35px" src="@/assets/logo.ico" />{{configStore.name}}</el-menu-item>
|
||||
<el-menu-item index="1" @click="enterIndex">主页</el-menu-item>
|
||||
<el-sub-menu index="2">
|
||||
<template #title>学习</template>
|
||||
<el-menu-item index="2-1">学习 one</el-menu-item>
|
||||
<el-menu-item index="2-2">学习 two</el-menu-item>
|
||||
<el-menu-item index="2-3">学习 three</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="3">
|
||||
<template #title>资源</template>
|
||||
<el-menu-item index="3-1">资源 one</el-menu-item>
|
||||
<el-menu-item index="3-2">资源 two</el-menu-item>
|
||||
<el-menu-item index="3-3">资源 three</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="4">
|
||||
<template #title>问答</template>
|
||||
<el-menu-item index="4-1">问答 one</el-menu-item>
|
||||
<el-menu-item index="4-2">问答 two</el-menu-item>
|
||||
<el-menu-item index="4-3">问答 three</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<div class="flex-grow" />
|
||||
|
||||
<el-menu-item index="5">
|
||||
<div style="width: 350px;">
|
||||
<el-input style="width: 300px;" v-model="searchText" placeholder="全站搜索" clearable prefix-icon="Search" />
|
||||
<el-button type="primary" plain @click="search">搜索</el-button>
|
||||
</div>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="6" @click="enterProfile" >
|
||||
<AvatarInfo :size='30' :isSelf="true" />
|
||||
</el-menu-item>
|
||||
<el-sub-menu index="6">
|
||||
<template #title>个人中心</template>
|
||||
<el-menu-item index="6-1" @click="enterProfile">进入个人中心</el-menu-item>
|
||||
<el-menu-item index="6-2" @click="enterProfile">其他</el-menu-item>
|
||||
<el-menu-item index="6-3" @click="logout">登出</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
</el-menu>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
import AvatarInfo from '@/components/AvatarInfo.vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import useUserStore from '@/stores/user.js'
|
||||
import useConfigStore from "@/stores/config";
|
||||
const configStore= useConfigStore();
|
||||
const router = useRouter()
|
||||
const userStore =useUserStore();
|
||||
const activeIndex = ref('1')
|
||||
const searchText=ref('')
|
||||
const handleSelect = (key, keyPath) => {
|
||||
console.log(key, keyPath)
|
||||
}
|
||||
const logout= async()=>{
|
||||
ElMessageBox.confirm(`确定登出系统吗?`, "警告", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then( async() => {
|
||||
//异步
|
||||
await userStore.logOut();
|
||||
//删除成功后,跳转到主页
|
||||
router.push("/login");
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "登出成功",
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
const enterIndex=()=>{
|
||||
router.push("/index");
|
||||
};
|
||||
const enterProfile=()=>{
|
||||
router.push("/profile");}
|
||||
|
||||
const search=()=>{
|
||||
var routerPer = { path: `/discuss`,query:{q:searchText.value} };
|
||||
searchText.value='';
|
||||
router.push(routerPer)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.logo{
|
||||
min-width: 14rem;
|
||||
font-size: large;
|
||||
font-weight: 600;
|
||||
}
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.img-icon
|
||||
{
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
27
Yi.Bbs.Vue3/src/layout/Index.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container>
|
||||
<el-header>
|
||||
|
||||
<AppHeader />
|
||||
|
||||
</el-header>
|
||||
<el-main>
|
||||
<AppBody />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import AppHeader from "./AppHeader.vue"
|
||||
import AppBody from "./AppBody.vue"
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-main{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 10rem;
|
||||
background-color: #F0F2F5;
|
||||
}
|
||||
</style>
|
||||
8
Yi.Bbs.Vue3/src/layout/LoginLayout.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="box">
|
||||
<div class="content">
|
||||
<RouterView/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style src="@/assets/styles/login.scss" scoped></style>
|
||||
23
Yi.Bbs.Vue3/src/main.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
import './assets/main.css'
|
||||
import '@/assets/styles/index.scss' // global css
|
||||
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import directive from './directive' // directive
|
||||
|
||||
import "./permission"
|
||||
|
||||
const app = createApp(App)
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
directive(app);
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
52
Yi.Bbs.Vue3/src/permission.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import router from "./router";
|
||||
import useAuths from "@/hooks/useAuths";
|
||||
import { ElMessage } from "element-plus";
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
import useUserStore from "@/stores/user";
|
||||
|
||||
NProgress.configure({ showSpinner: false });
|
||||
const { getToken, logoutFun } = useAuths();
|
||||
const whiteList = ["/login", "/auth-redirect", "/bind", "/register"];
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start();
|
||||
const hasToken = getToken();
|
||||
if (hasToken) {
|
||||
if (to.path === "/login") {
|
||||
// 已经登陆跳转到首页
|
||||
next({ path: "/" });
|
||||
NProgress.done();
|
||||
} else {
|
||||
if (useUserStore().roles.length === 0) {
|
||||
// 判断当前用户是否已拉取完user_info信息
|
||||
useUserStore()
|
||||
.getInfo()
|
||||
.then(() => {
|
||||
next({ ...to, replace: true });
|
||||
})
|
||||
.catch((err) => {
|
||||
logoutFun.then(() => {
|
||||
ElMessage.error(err);
|
||||
next({ path: "/" });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 没有token
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
// 在免登录白名单,直接进入
|
||||
next();
|
||||
} else {
|
||||
next(`/login?redirect=${to.path}&unTourist=true`); // 否则全部重定向到登录页
|
||||
NProgress.done();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done();
|
||||
});
|
||||
74
Yi.Bbs.Vue3/src/router/index.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import Layout from "../layout/Index.vue";
|
||||
import NotFound from "../views/error/404.vue";
|
||||
import LoginLayout from "../layout/LoginLayout.vue";
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// 始终滚动到顶部
|
||||
return { top: 0 };
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
name: "test",
|
||||
path: "/test",
|
||||
component: () => import("../views/Test.vue"),
|
||||
},
|
||||
{
|
||||
path: "/loginLayout",
|
||||
name: "loginLayout",
|
||||
component: LoginLayout,
|
||||
redirect: "/login",
|
||||
children: [
|
||||
{
|
||||
name: "login",
|
||||
path: "/login",
|
||||
component: () => import("../views/Login.vue"),
|
||||
},
|
||||
{
|
||||
name: "register",
|
||||
path: "/register",
|
||||
component: () => import("../views/Register.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
name: "layout",
|
||||
component: Layout,
|
||||
redirect: "/index",
|
||||
children: [
|
||||
{
|
||||
name: "index",
|
||||
path: "/index",
|
||||
component: () => import("../views/home/Index.vue"),
|
||||
},
|
||||
{
|
||||
name: "article",
|
||||
path: "/article/:discussId/:articleId?",
|
||||
component: () => import("../views/Article.vue"),
|
||||
},
|
||||
{
|
||||
name: "discuss",
|
||||
path: "/discuss/:plateId?",
|
||||
component: () => import("../views/Discuss.vue"),
|
||||
},
|
||||
{
|
||||
//artType:discuss主题、article文章
|
||||
//operType:create创建、update更新
|
||||
name: "editArt",
|
||||
path: "/editArt",
|
||||
component: () => import("../views/EditArticle.vue"),
|
||||
},
|
||||
{
|
||||
name: "profile",
|
||||
path: "/profile",
|
||||
component: () => import("../views/profile/Index.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: "/:pathMatch(.*)*", name: "NotFound", component: NotFound },
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
||||
22
Yi.Bbs.Vue3/src/stores/config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import {getAll} from '@/apis/configApi'
|
||||
import { defineStore } from 'pinia'
|
||||
const useConfigStore = defineStore('config',
|
||||
{
|
||||
state: () => ({
|
||||
data: []
|
||||
}),
|
||||
getters: {
|
||||
name:(state)=>state.data.filter(s=> s.configKey=='bbs.site.name').map(x=>x.configValue)[0],
|
||||
author:(state)=>state.data.filter(s=> s.configKey=='bbs.site.author').map(x=>x.configValue)[0],
|
||||
icp:(state)=>state.data.filter(s=> s.configKey=='bbs.site.icp').map(x=>x.configValue)[0],
|
||||
bottom:(state)=>state.data.filter(s=>s.configKey=='bbs.site.bottom').map(x=>x.configValue)[0]
|
||||
},
|
||||
actions: {
|
||||
// 登录
|
||||
async getConfig() {
|
||||
const response = await getAll();
|
||||
this.data = response.data.items;
|
||||
},
|
||||
},
|
||||
})
|
||||
export default useConfigStore;
|
||||
109
Yi.Bbs.Vue3/src/stores/user.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import { login, logout, getInfo, register } from "@/apis/accountApi";
|
||||
import { getUserDetailInfo } from "@/apis/auth";
|
||||
import useAuths from "@/hooks/useAuths";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
const { getToken, setToken, removeToken } = useAuths();
|
||||
|
||||
const useUserStore = defineStore("user", {
|
||||
state: () => ({
|
||||
id: "",
|
||||
token: getToken(),
|
||||
name: "游客",
|
||||
userName: "",
|
||||
icon: null,
|
||||
roles: [],
|
||||
permissions: [],
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
// 登录
|
||||
login(userInfo) {
|
||||
const userName = userInfo.userName.trim();
|
||||
const password = userInfo.password;
|
||||
const code = userInfo.code;
|
||||
const uuid = userInfo.uuid;
|
||||
return new Promise((resolve, reject) => {
|
||||
login(userName, password, code, uuid)
|
||||
.then((response) => {
|
||||
const res = response.data;
|
||||
setToken(res.token);
|
||||
this.token = res.token;
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
// 获取用户信息
|
||||
getInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getUserDetailInfo()
|
||||
.then((response) => {
|
||||
const res = response.data;
|
||||
const user = res.user;
|
||||
const avatar =
|
||||
user.icon == "" || user.icon == null
|
||||
? "/src/assets/logo.ico"
|
||||
: import.meta.env.VITE_APP_BASEAPI + "/file/" + user.icon;
|
||||
|
||||
if (res.roleCodes && res.roleCodes.length > 0) {
|
||||
// 验证返回的roles是否是一个非空数组
|
||||
this.roles = res.roleCodes;
|
||||
this.permissions = res.permissionCodes;
|
||||
// this.roles = ["admin"];
|
||||
// this.permissions=["*:*:*"]
|
||||
} else {
|
||||
this.roles = ["ROLE_DEFAULT"];
|
||||
}
|
||||
// this.roles = ["admin"];
|
||||
// this.permissions=["*:*:*"]
|
||||
this.name = user.nick;
|
||||
this.icon = avatar;
|
||||
|
||||
this.userName = user.userName;
|
||||
this.id = user.id;
|
||||
resolve(res);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
// 退出系统
|
||||
logOut() {
|
||||
return new Promise((resolve, reject) => {
|
||||
logout()
|
||||
.then(() => {
|
||||
this.token = "";
|
||||
this.roles = [];
|
||||
this.permissions = [];
|
||||
removeToken();
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
// 注册
|
||||
register(userInfo) {
|
||||
const userName = userInfo.userName.trim();
|
||||
const password = userInfo.password.trim();
|
||||
const phone = userInfo.phone;
|
||||
const uuid = userInfo.uuid;
|
||||
const code = userInfo.code;
|
||||
return new Promise((resolve, reject) => {
|
||||
register(userName, password, phone, code, uuid)
|
||||
.then((response) => {
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
export default useUserStore;
|
||||
13
Yi.Bbs.Vue3/src/utils/auth.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const TokenKey = 'Admin-Token'
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem(TokenKey)
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
return localStorage.setItem(TokenKey, token)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
return localStorage.removeItem (TokenKey)
|
||||
}
|
||||
140
Yi.Bbs.Vue3/src/utils/file.js
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 根据后缀判断文件类型
|
||||
* @param {string} fileName 文件后缀名
|
||||
* @returns
|
||||
*/
|
||||
export function matchType(fileName) {
|
||||
// 后缀获取
|
||||
let suffix = "";
|
||||
// 获取类型结果
|
||||
let result = "";
|
||||
try {
|
||||
let flieArr = fileName.split(".");
|
||||
suffix = flieArr[flieArr.length - 1];
|
||||
} catch (err) {
|
||||
suffix = "";
|
||||
}
|
||||
// fileName无后缀返回 false
|
||||
if (!suffix) {
|
||||
result = false;
|
||||
return result;
|
||||
}
|
||||
// 图片格式
|
||||
let imglist = ["png", "jpg", "jpeg", "bmp", "gif"];
|
||||
// 进行图片匹配
|
||||
result = imglist.some(function (item) {
|
||||
return item == suffix;
|
||||
});
|
||||
if (result) {
|
||||
result = "image";
|
||||
return result;
|
||||
}
|
||||
// 匹配txt
|
||||
let txtlist = ["txt"];
|
||||
result = txtlist.some(function (item) {
|
||||
return item == suffix;
|
||||
});
|
||||
if (result) {
|
||||
result = "txt";
|
||||
return result;
|
||||
}
|
||||
// 匹配 excel
|
||||
let excelist = ["xls", "xlsx"];
|
||||
result = excelist.some(function (item) {
|
||||
return item == suffix;
|
||||
});
|
||||
if (result) {
|
||||
result = "excel";
|
||||
return result;
|
||||
}
|
||||
// 匹配 word
|
||||
let wordlist = ["doc", "docx"];
|
||||
result = wordlist.some(function (item) {
|
||||
return item == suffix;
|
||||
});
|
||||
if (result) {
|
||||
result = "word";
|
||||
return result;
|
||||
}
|
||||
// 匹配 pdf
|
||||
let pdflist = ["pdf"];
|
||||
result = pdflist.some(function (item) {
|
||||
return item == suffix;
|
||||
});
|
||||
if (result) {
|
||||
result = "pdf";
|
||||
return result;
|
||||
}
|
||||
// 匹配 ppt
|
||||
let pptlist = ["ppt"];
|
||||
result = pptlist.some(function (item) {
|
||||
return item == suffix;
|
||||
});
|
||||
if (result) {
|
||||
result = "ppt";
|
||||
return result;
|
||||
}
|
||||
// 匹配 视频
|
||||
let videolist = ["mp4", "m2v", "mkv"];
|
||||
result = videolist.some(function (item) {
|
||||
return item == suffix;
|
||||
});
|
||||
if (result) {
|
||||
result = "video";
|
||||
return result;
|
||||
}
|
||||
// 匹配 音频
|
||||
let radiolist = ["mp3", "wav", "wmv"];
|
||||
result = radiolist.some(function (item) {
|
||||
return item == suffix;
|
||||
});
|
||||
if (result) {
|
||||
result = "radio";
|
||||
return result;
|
||||
}
|
||||
// 其他 文件类型
|
||||
result = "other";
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* url处理
|
||||
* @param {string} path url路径
|
||||
* @returns
|
||||
*/
|
||||
export function convertToUrl(path) {
|
||||
// 替换反斜杠为正斜杠
|
||||
const normalizedPathWithSlashes = path.replace(/\\/g, "/");
|
||||
// 去掉开始的点号和反斜杠
|
||||
const removedDotsAndSlashes = normalizedPathWithSlashes.replace(/^\.\//, "");
|
||||
// 添加斜杠作为根路径
|
||||
const url = `/${removedDotsAndSlashes}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*
|
||||
* @param {*} path 下载地址/下载请求地址。
|
||||
* @param {string} name 下载文件的名字(考虑到兼容性问题,最好加上后缀名
|
||||
*/
|
||||
export const downLoadFile = (path, name) => {
|
||||
const link = document.createElement("a");
|
||||
link.href = path;
|
||||
link.download = name;
|
||||
|
||||
if (isMobileDevice()) {
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
}
|
||||
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
// 判断是否移动设备
|
||||
export function isMobileDevice() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
}
|
||||
81
Yi.Bbs.Vue3/src/utils/hljsData.js
Normal file
@@ -0,0 +1,81 @@
|
||||
export default {
|
||||
'agate': 1,
|
||||
'androidstudio': 1,
|
||||
'arduino-light': 1,
|
||||
'arta': 1,
|
||||
'ascetic': 1,
|
||||
'atelier-cave-dark': 1,
|
||||
'atelier-cave-light': 1,
|
||||
'atelier-dune-dark': 1,
|
||||
'atelier-dune-light': 1,
|
||||
'atelier-estuary-dark': 1,
|
||||
'atelier-estuary-light': 1,
|
||||
'atelier-forest-dark': 1,
|
||||
'atelier-forest-light': 1,
|
||||
'atelier-heath-dark': 1,
|
||||
'atelier-heath-light': 1,
|
||||
'atelier-lakeside-dark': 1,
|
||||
'atelier-lakeside-light': 1,
|
||||
'atelier-plateau-dark': 1,
|
||||
'atelier-plateau-light': 1,
|
||||
'atelier-savanna-dark': 1,
|
||||
'atelier-savanna-light': 1,
|
||||
'atelier-seaside-dark': 1,
|
||||
'atelier-seaside-light': 1,
|
||||
'atelier-sulphurpool-dark': 1,
|
||||
'atelier-sulphurpool-light': 1,
|
||||
'atom-one-dark': 1,
|
||||
'atom-one-light': 1,
|
||||
'brown-paper': 1,
|
||||
'codepen-embed': 1,
|
||||
'color-brewer': 1,
|
||||
'darcula': 1,
|
||||
'dark': 1,
|
||||
'darkula': 1,
|
||||
'default': 1,
|
||||
'docco': 1,
|
||||
'dracula': 1,
|
||||
'far': 1,
|
||||
'foundation': 1,
|
||||
'github-gist': 1,
|
||||
'github': 1,
|
||||
'googlecode': 1,
|
||||
'grayscale': 1,
|
||||
'gruvbox-dark': 1,
|
||||
'gruvbox-light': 1,
|
||||
'hopscotch': 1,
|
||||
'hybrid': 1,
|
||||
'idea': 1,
|
||||
'ir-black': 1,
|
||||
'kimbie.dark': 1,
|
||||
'kimbie.light': 1,
|
||||
'magula': 1,
|
||||
'mono-blue': 1,
|
||||
'monokai-sublime': 1,
|
||||
'monokai': 1,
|
||||
'obsidian': 1,
|
||||
'ocean': 1,
|
||||
'paraiso-dark': 1,
|
||||
'paraiso-light': 1,
|
||||
'pojoaque': 1,
|
||||
'purebasic': 1,
|
||||
'qtcreator_dark': 1,
|
||||
'qtcreator_light': 1,
|
||||
'railscasts': 1,
|
||||
'rainbow': 1,
|
||||
'routeros': 1,
|
||||
'school-book': 1,
|
||||
'solarized-dark': 1,
|
||||
'solarized-light': 1,
|
||||
'sunburst': 1,
|
||||
'tomorrow-night-blue': 1,
|
||||
'tomorrow-night-bright': 1,
|
||||
'tomorrow-night-eighties': 1,
|
||||
'tomorrow-night': 1,
|
||||
'tomorrow': 1,
|
||||
'vs': 1,
|
||||
'vs2015': 1,
|
||||
'xcode': 1,
|
||||
'xt256': 1,
|
||||
'zenburn': 1
|
||||
};
|
||||
12
Yi.Bbs.Vue3/src/utils/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
//设置防抖,保证无论拖动窗口大小,只执行一次获取浏览器宽高的方法
|
||||
export const debounce = (fun, delay) => {
|
||||
let timer;
|
||||
return function () {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
fun();
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
64
Yi.Bbs.Vue3/src/utils/request.js
Normal file
@@ -0,0 +1,64 @@
|
||||
|
||||
import axios from 'axios';
|
||||
import { getToken } from '@/utils/auth'
|
||||
export let isRelogin = { show: false };
|
||||
// import JsonBig from 'json-bigint'
|
||||
const myaxios = axios.create({
|
||||
baseURL:import.meta.env.VITE_APP_BASEAPI,
|
||||
timeout: 50000,
|
||||
// transformResponse: [data => {
|
||||
// try {
|
||||
// const json = JsonBig({
|
||||
// storeAsString: true
|
||||
// })
|
||||
// return json.parse(data)
|
||||
// } catch (err) {
|
||||
// // 如果转换失败,则包装为统一数据格式并返回
|
||||
// return {
|
||||
// data
|
||||
// }
|
||||
// }
|
||||
// }],
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 请求拦截器
|
||||
myaxios.interceptors.request.use(function (config) {
|
||||
if (getToken()) {
|
||||
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||
}
|
||||
|
||||
return config;
|
||||
}, function (error) {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// 响应拦截器
|
||||
myaxios.interceptors.response.use(function (response) {
|
||||
//业务错误
|
||||
if(response.data.statusCode==403)
|
||||
{
|
||||
ElMessage.error(response.data.errors)
|
||||
}
|
||||
return response;
|
||||
}, function (error) {
|
||||
const code = error.response.status;
|
||||
const msg = error.message;
|
||||
//业务异常+应用异常,统一处理
|
||||
switch(code)
|
||||
{
|
||||
case 401:
|
||||
ElMessage.error('登录已过期')
|
||||
break;
|
||||
case 403:
|
||||
ElMessage.error(msg)
|
||||
break;
|
||||
case 500:
|
||||
ElMessage.error(msg)
|
||||
break;
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
export default myaxios
|
||||
53
Yi.Bbs.Vue3/src/utils/storage.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* window.localStorage 浏览器永久缓存
|
||||
* @method set 设置永久缓存
|
||||
* @method get 获取永久缓存
|
||||
* @method remove 移除永久缓存
|
||||
* @method clear 移除全部永久缓存
|
||||
*/
|
||||
export const Local = {
|
||||
// 设置永久缓存
|
||||
set(key, val) {
|
||||
window.localStorage.setItem(key, JSON.stringify(val));
|
||||
},
|
||||
// 获取永久缓存
|
||||
get(key) {
|
||||
let json = window.localStorage.getItem(key);
|
||||
return JSON.parse(json);
|
||||
},
|
||||
// 移除永久缓存
|
||||
remove(key) {
|
||||
window.localStorage.removeItem(key);
|
||||
},
|
||||
// 移除全部永久缓存
|
||||
clear() {
|
||||
window.localStorage.clear();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* window.sessionStorage 浏览器会话临时缓存
|
||||
* @method set 设置临时缓存
|
||||
* @method get 获取临时缓存
|
||||
* @method remove 移除临时缓存
|
||||
* @method clear 移除全部临时缓存
|
||||
*/
|
||||
export const Session = {
|
||||
// 设置临时缓存
|
||||
set(key, val) {
|
||||
window.sessionStorage.setItem(key, JSON.stringify(val));
|
||||
},
|
||||
// 获取临时缓存
|
||||
get(key) {
|
||||
let json = window.sessionStorage.getItem(key);
|
||||
return JSON.parse(json);
|
||||
},
|
||||
// 移除临时缓存
|
||||
remove(key) {
|
||||
window.sessionStorage.removeItem(key);
|
||||
},
|
||||
// 移除全部临时缓存
|
||||
clear() {
|
||||
window.sessionStorage.clear();
|
||||
},
|
||||
};
|
||||
386
Yi.Bbs.Vue3/src/views/Article.vue
Normal file
@@ -0,0 +1,386 @@
|
||||
<template>
|
||||
<div style="width: 90%; min-width: 1200px">
|
||||
<!-- <div style="width: 1200px;"> -->
|
||||
<el-row :gutter="20" class="top-div">
|
||||
<el-col :span="5">
|
||||
<el-row class="art-info-left">
|
||||
<el-col :span="24">
|
||||
<InfoCard header="文章信息" text="展开" hideDivider="true">
|
||||
<template #content>
|
||||
<el-button style="width: 100%; margin-bottom: 0.8rem" @click="loadDiscuss(true)">首页</el-button>
|
||||
<el-button v-hasPer="['bbs:article:add']" @click="addArticle(0)" type="primary"
|
||||
style="width: 100%; margin-bottom: 0.8rem; margin-left: 0">添加子文章</el-button>
|
||||
<!--目录在这里 -->
|
||||
<TreeArticleInfo :data="articleData" @remove="delArticle" @update="updateArticle" @create="addNextArticle"
|
||||
@handleNodeClick="handleNodeClick" :currentNodeKey="currentNodeKey"/>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<InfoCard :items="items" header="作者分享" text="更多">
|
||||
<template #item="temp">
|
||||
<AvatarInfo />
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<InfoCard :items="items" header="内容推荐" text="更多">
|
||||
<template #item="temp">
|
||||
<AvatarInfo />
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="14">
|
||||
<el-row class="left-div">
|
||||
<el-col :span="24">
|
||||
<!-- {{ discuss.user }} -->
|
||||
<AvatarInfo :size="50" :showWatching="true" :time="discuss.creationTime" :userInfo="discuss.user"></AvatarInfo>
|
||||
<!-- :userInfo="{nick:'qwe'} -->
|
||||
<el-divider />
|
||||
<h2>{{ discuss.title }}</h2>
|
||||
<el-image :preview-src-list="[getUrl(discuss.cover)]" v-if="discuss.cover" :src="getUrl(discuss.cover)" style="width: 150px;height: 150px;" />
|
||||
|
||||
|
||||
<ArticleContentInfo :code="discuss.content??''"></ArticleContentInfo>
|
||||
|
||||
<el-divider class="tab-divider" />
|
||||
|
||||
<el-space :size="10" :spacer="spacer">
|
||||
<AgreeInfo :data="discuss"/>
|
||||
<el-button icon="Star" text> 0</el-button>
|
||||
<el-button icon="Share" text> 分享</el-button>
|
||||
<el-button icon="Operation" text> 操作</el-button>
|
||||
</el-space>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="comment">
|
||||
|
||||
<CommentInfo/>
|
||||
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
<BottomInfo/>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="5">
|
||||
<el-row class="right-div">
|
||||
<el-col :span="24">
|
||||
<InfoCard class="art-info-right" header="主题信息" text="更多" hideDivider="true">
|
||||
<template #content>
|
||||
<div>
|
||||
<ul class="art-info-ul">
|
||||
|
||||
<li>
|
||||
<el-button type="primary" size="default"
|
||||
v-hasPer="['bbs:discuss:edit']"
|
||||
@click="updateHander(route.params.discussId)">编辑</el-button>
|
||||
<el-button style="margin-left: 1rem" type="danger"
|
||||
v-hasPer="['bbs:discuss:remove']"
|
||||
@click="delHander(route.params.discussId)">删除</el-button>
|
||||
</li>
|
||||
<li>分类: <span>主题</span></li>
|
||||
标签:
|
||||
<el-tag type="success">主题</el-tag>
|
||||
<el-tag type="info">资源</el-tag>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<InfoCard class="art-info-right" header="目录" hideDivider="true">
|
||||
<template #content>
|
||||
<div>
|
||||
<el-empty :image-size="100" style="padding: 20px 0;" v-if="catalogueData.length==0" description="无目录" />
|
||||
<ul v-else class="art-info-ul">
|
||||
<li v-for="(item, i) in catalogueData" :key="i">
|
||||
<el-button style="width: 100%; justify-content: left" type="primary" text>{{ `${i + 1} : ${item}`
|
||||
}}</el-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<InfoCard :items="items" header="其他" text="更多">
|
||||
<template #item="temp">
|
||||
<AvatarInfo />
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<InfoCard :items="items" header="其他" text="更多">
|
||||
<template #item="temp">
|
||||
<AvatarInfo />
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { h, ref, onMounted } from "vue";
|
||||
import AvatarInfo from "@/components/AvatarInfo.vue";
|
||||
import InfoCard from "@/components/InfoCard.vue";
|
||||
import ArticleContentInfo from "@/components/ArticleContentInfo.vue";
|
||||
import CommentInfo from "@/components/CommentInfo.vue";
|
||||
import BottomInfo from '@/components/BottomInfo.vue'
|
||||
import TreeArticleInfo from "@/components/TreeArticleInfo.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import AgreeInfo from '@/components/AgreeInfo.vue'
|
||||
import { get as discussGet, del as discussDel } from "@/apis/discussApi.js";
|
||||
import { all as articleall, del as articleDel, get as articleGet } from "@/apis/articleApi.js";
|
||||
//数据定义
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const spacer = h(ElDivider, { direction: "vertical" });
|
||||
const items = [{ user: "用户1" }, { user: "用户2" }, { user: "用户3" }];
|
||||
|
||||
|
||||
//子文章数据
|
||||
const articleData = ref([]);
|
||||
//主题内容
|
||||
const discuss = ref({});
|
||||
//封面url
|
||||
const getUrl= (str)=>{
|
||||
return `${import.meta.env.VITE_APP_BASEAPI}/file/${str}`
|
||||
}
|
||||
|
||||
//当前默认选择的子文章
|
||||
const currentNodeKey=route.params.articleId;
|
||||
//目录数据
|
||||
const catalogueData = ref([]);
|
||||
|
||||
//子文章初始化
|
||||
const loadArticleData = async () => {
|
||||
const response= await articleall(route.params.discussId)
|
||||
articleData.value = response.data;
|
||||
}
|
||||
|
||||
//主题初始化
|
||||
const loadDiscuss = async (isRewrite) => {
|
||||
|
||||
if (isRewrite) {
|
||||
//跳转路由
|
||||
router.push(`/article/${route.params.discussId}`);
|
||||
}
|
||||
discuss.value = (await discussGet(route.params.discussId)).data;
|
||||
if (route.params.articleId != "") {
|
||||
const response = await articleGet(route.params.articleId);
|
||||
discuss.value.content = response.data.content;
|
||||
}
|
||||
ContentHander();
|
||||
};
|
||||
//加载文章及目录
|
||||
const ContentHander = () => {
|
||||
//加载目录
|
||||
var reg = /(#{1,6})\s(.*)/g;
|
||||
|
||||
if(discuss.value.content)
|
||||
{
|
||||
var myArray = discuss.value.content.match(reg);
|
||||
if (myArray != null) {
|
||||
catalogueData.value = myArray.map((x) => {
|
||||
return x.replace(/#/g, "").replace(/\s/g, "");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
//添加树型子文章
|
||||
const addArticle = (parentArticleId) => {
|
||||
//跳转路由
|
||||
var routerPer = {
|
||||
path: "/editArt",
|
||||
query: {
|
||||
operType: "create",
|
||||
artType: "article",
|
||||
discussId: route.params.discussId,
|
||||
parentArticleId: parentArticleId,
|
||||
},
|
||||
};
|
||||
router.push(routerPer);
|
||||
};
|
||||
|
||||
//删除主题
|
||||
const delHander = async (ids) => {
|
||||
ElMessageBox.confirm(`确定是否删除编号[${ids}]的主题吗?`, "警告", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
const response = await discussDel(ids);
|
||||
//删除成功后,跳转到主页
|
||||
router.push("/index");
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "删除成功",
|
||||
});
|
||||
});
|
||||
};
|
||||
//更新操作
|
||||
const updateHander = (discussId) => {
|
||||
//跳转路由
|
||||
var routerPer = {
|
||||
path: "/editArt",
|
||||
query: {
|
||||
operType: "update",
|
||||
artType: "discuss",
|
||||
discussId: discussId,
|
||||
},
|
||||
};
|
||||
router.push(routerPer);
|
||||
};
|
||||
|
||||
//跳转添加子菜单
|
||||
const addNextArticle = (node, data) => {
|
||||
//跳转路由
|
||||
var routerPer = {
|
||||
path: "/editArt",
|
||||
query: {
|
||||
operType: "create",
|
||||
artType: "article",
|
||||
discussId: data.discussId,
|
||||
parentArticleId: data.id,
|
||||
},
|
||||
};
|
||||
router.push(routerPer);
|
||||
}
|
||||
|
||||
//跳转更新子菜单
|
||||
const updateArticle = (node, data) => {
|
||||
//跳转路由
|
||||
var routerPer = {
|
||||
path: "/editArt",
|
||||
query: {
|
||||
operType: "update",
|
||||
artType: "article",
|
||||
discussId: data.discussId,
|
||||
parentArticleId: data.parentId,
|
||||
articleId: data.id
|
||||
},
|
||||
};
|
||||
router.push(routerPer);
|
||||
}
|
||||
//单机节点
|
||||
const handleNodeClick = async(data) => {
|
||||
|
||||
//跳转路由
|
||||
|
||||
router.push(`/article/${route.params.discussId}/${data.id}`);
|
||||
|
||||
const response=await articleGet(data.id);
|
||||
discuss.value.content = response.data.content;
|
||||
ContentHander();
|
||||
}
|
||||
//删除子文章
|
||||
const delArticle = (node, data) => {
|
||||
ElMessageBox.confirm(`确定是否删除编号[${data.id}]的子文章吗?`, "警告", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
|
||||
await articleDel(data.id);
|
||||
await loadArticleData();
|
||||
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "删除成功",
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
onMounted(async () => {
|
||||
await loadDiscuss();
|
||||
await loadArticleData();
|
||||
});
|
||||
</script>
|
||||
<style scoped >
|
||||
.comment {
|
||||
min-height: 40rem;
|
||||
}
|
||||
|
||||
.art-info-left .el-col {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.art-info-ul span {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.art-info-ul .el-tag {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.art-info-ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.art-info-right {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.left-div .el-col {
|
||||
background-color: #ffffff;
|
||||
min-height: 12rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.right-div .el-col {
|
||||
background-color: #ffffff;
|
||||
min-height: 10rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.left-top-div .el-col {
|
||||
min-height: 2rem;
|
||||
background-color: #fafafa;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.top-div {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.left-top-div {
|
||||
font-size: small;
|
||||
text-align: center;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0.5em;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-weight: 600;
|
||||
font-size: 30px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.left-div .el-col {
|
||||
padding: 1.4rem 1.4rem 0.5rem 1.4rem;
|
||||
}
|
||||
|
||||
.el-space {
|
||||
display: flex;
|
||||
vertical-align: top;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.tab-divider {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
224
Yi.Bbs.Vue3/src/views/Discuss.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div style="width: 1200px" class="body-div">
|
||||
<div class="header">
|
||||
<el-form :inline="true">
|
||||
<el-form-item label="标题:">
|
||||
<el-input v-model="query.title" placeholder="请输入标题"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标签:">
|
||||
<el-input placeholder="搜索当下分类下的标签" />
|
||||
</el-form-item>
|
||||
<div class="form-right">
|
||||
<el-button>重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="
|
||||
async () => {
|
||||
await loadDiscussList();
|
||||
}
|
||||
"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button
|
||||
@click="enterEditArticle"
|
||||
type="primary"
|
||||
v-hasPer="['bbs:discuss:add']"
|
||||
>分享</el-button
|
||||
>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
展开
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>Action 1</el-dropdown-item>
|
||||
<el-dropdown-item>Action 2</el-dropdown-item>
|
||||
<el-dropdown-item>Action 3</el-dropdown-item>
|
||||
<el-dropdown-item disabled>Action 4</el-dropdown-item>
|
||||
<el-dropdown-item divided>Action 5</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" @tab-change="handleClick">
|
||||
<el-tab-pane label="最新" name="new"> </el-tab-pane>
|
||||
<el-tab-pane label="推荐" name="suggest"> </el-tab-pane>
|
||||
<el-tab-pane label="最热" name="host"> </el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-collapse class="collapse-list" style="background-color: #f0f2f5">
|
||||
<el-collapse-item>
|
||||
<template #title>
|
||||
<div class="collapse-top">
|
||||
已置顶主题<el-icon class="header-icon">
|
||||
<info-filled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<div class="div-item" v-for="i in topDiscussList">
|
||||
<DisscussCard :discuss="i" badge="置顶" />
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-divider v-show="topDiscussList.length > 0" />
|
||||
|
||||
<div class="div-item" v-for="i in discussList">
|
||||
<DisscussCard :discuss="i" />
|
||||
</div>
|
||||
<div>
|
||||
<el-pagination
|
||||
v-model:current-page="query.skipCount"
|
||||
v-model:page-size="query.maxResultCount"
|
||||
:page-sizes="[10, 20, 30, 50]"
|
||||
:background="true"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="
|
||||
async (val) => {
|
||||
await loadDiscussList();
|
||||
}
|
||||
"
|
||||
@current-change="
|
||||
async (val) => {
|
||||
await loadDiscussList();
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="discussList.length == 0" description="空空如也" />
|
||||
<BottomInfo />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DisscussCard from "@/components/DisscussCard.vue";
|
||||
import { getList, getTopList } from "@/apis/discussApi.js";
|
||||
import { onMounted, ref, reactive } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import BottomInfo from "@/components/BottomInfo.vue";
|
||||
//数据定义
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const activeName = ref("new");
|
||||
//主题内容
|
||||
const discussList = ref([]);
|
||||
//置顶主题内容
|
||||
const topDiscussList = ref([]);
|
||||
const total = ref(100);
|
||||
const query = reactive({
|
||||
skipCount: 1,
|
||||
maxResultCount: 10,
|
||||
title: "",
|
||||
plateId: route.params.plateId,
|
||||
type: activeName.value,
|
||||
});
|
||||
|
||||
const handleClick = async (tab, event) => {
|
||||
query.type = activeName.value;
|
||||
await loadDiscussList();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.query.q != undefined) {
|
||||
query.title = route.query.q ?? "";
|
||||
router.push("/discuss");
|
||||
}
|
||||
await loadDiscussList();
|
||||
});
|
||||
|
||||
//加载discuss
|
||||
const loadDiscussList = async () => {
|
||||
const response = await getList(query);
|
||||
discussList.value = response.data.items;
|
||||
total.value = Number(response.data.total);
|
||||
|
||||
//全查,无需参数
|
||||
const topResponse = await getTopList();
|
||||
topDiscussList.value = topResponse.data.items;
|
||||
};
|
||||
|
||||
//进入添加主题页面
|
||||
const enterEditArticle = () => {
|
||||
//跳转路由
|
||||
var routerPer = {
|
||||
path: "/editArt",
|
||||
query: {
|
||||
operType: "create",
|
||||
artType: "discuss",
|
||||
plateId: route.params.plateId,
|
||||
},
|
||||
};
|
||||
router.push(routerPer);
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.el-pagination {
|
||||
margin: 2rem 0rem 2rem 0rem;
|
||||
justify-content: right;
|
||||
}
|
||||
.body-div {
|
||||
min-height: 1000px;
|
||||
}
|
||||
.el-dropdown-link {
|
||||
cursor: pointer;
|
||||
color: var(--el-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header {
|
||||
background-color: #ffffff;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0rem;
|
||||
}
|
||||
.collapse-top {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.header .el-input {
|
||||
}
|
||||
.el-tabs {
|
||||
background-color: #ffffff;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.el-tabs >>> .el-tabs__header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.div-item {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.el-form {
|
||||
--el-form-label-font-size: var(--el-font-size-base);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.el-form-item {
|
||||
padding-top: 0.8rem;
|
||||
}
|
||||
.form-right {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
}
|
||||
.form-right .el-button {
|
||||
margin-right: 0.6rem;
|
||||
}
|
||||
.header .el-input {
|
||||
width: 20rem;
|
||||
}
|
||||
.collapse-list >>> .el-collapse-item__header {
|
||||
border-bottom-color: #f0f2f5 !important;
|
||||
}
|
||||
|
||||
.el-divider {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
</style>
|
||||
294
Yi.Bbs.Vue3/src/views/EditArticle.vue
Normal file
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div style="width: 100%">
|
||||
<div class="body-div">
|
||||
<el-form label-width="120px" :model="editForm" label-position="left" :rules="rules" ref="ruleFormRef">
|
||||
<el-form-item label="类型:">
|
||||
<el-radio-group v-model="radio">
|
||||
<el-radio-button label="discuss">主题</el-radio-button>
|
||||
<el-radio-button label="article">文章</el-radio-button>
|
||||
<el-radio-button label="plate">板块</el-radio-button>
|
||||
<el-radio-button label="orther">其他</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="权限:" v-if="route.query.artType == 'discuss'">
|
||||
<el-radio-group v-model="perRadio">
|
||||
<el-radio-button label="Public">公开</el-radio-button>
|
||||
<el-radio-button label="Oneself">仅自己可见</el-radio-button>
|
||||
<el-radio-button label="User">部分用户可见</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="可见用户:" v-if="route.query.artType == 'discuss' && perRadio=='User'">
|
||||
<UserSelectInfo v-model="editForm.permissionUserIds"/>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
|
||||
<el-form-item v-if="route.query.artType == 'article'" label="子文章名称:" prop="name">
|
||||
<el-input placeholder="请输入" v-model="editForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item v-else label="标题:" prop="title">
|
||||
<el-input placeholder="请输入" v-model="editForm.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述:" prop="introduction">
|
||||
<el-input placeholder="请输入" v-model="editForm.introduction" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容:" prop="content">
|
||||
<MavonEdit height="30rem" v-model="editForm.content" :codeStyle="codeStyle" />
|
||||
</el-form-item>
|
||||
<el-form-item label="封面:" v-if="route.query.artType == 'discuss'">
|
||||
|
||||
<!-- 主题封面选择 -->
|
||||
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:action="fileUploadUrl"
|
||||
:show-file-list="false"
|
||||
:on-success="onSuccess"
|
||||
>
|
||||
<el-image v-if="dialogImageUrl" :src="getUrl(dialogImageUrl)" style="width: 178px;height: 178px;" class="avatar" />
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
|
||||
|
||||
</el-form-item>
|
||||
<el-form-item label="标签:" prop="types">
|
||||
<el-input placeholder="请输入" v-model="editForm.types" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="submit(ruleFormRef)" class="submit-btn" type="primary">提交</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import MavonEdit from "@/components/MavonEdit.vue";
|
||||
import UserSelectInfo from '@/components/UserSelectInfo.vue'
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import {
|
||||
add as discussAdd,
|
||||
update as discussUpdate,
|
||||
get as discussGet,
|
||||
} from "@/apis/discussApi.js";
|
||||
|
||||
import {
|
||||
add as articleAdd,
|
||||
update as articleUpdate,
|
||||
get as articleGet,
|
||||
} from "@/apis/articleApi.js";
|
||||
|
||||
//数据定义
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const perRadio=ref("Public");
|
||||
const radio = ref(route.query.artType);
|
||||
const codeStyle = "atom-one-dark";
|
||||
|
||||
//封面完整显示的url
|
||||
const fileUploadUrl=`${import.meta.env.VITE_APP_BASEAPI}/file`
|
||||
//封面的url
|
||||
const dialogImageUrl = ref('')
|
||||
|
||||
//文件上传成功后
|
||||
const onSuccess=(response)=>{
|
||||
dialogImageUrl.value=response.data[0].id
|
||||
}
|
||||
//封面url
|
||||
const getUrl= (str)=>{
|
||||
return `${import.meta.env.VITE_APP_BASEAPI}/file/${str}`
|
||||
}
|
||||
|
||||
|
||||
//整个页面上的表单
|
||||
const editForm = reactive({
|
||||
title: "",
|
||||
types: "",
|
||||
introduction: "",
|
||||
content: "",
|
||||
name: "",
|
||||
permissionUserIds:[]
|
||||
});
|
||||
|
||||
//组装主题内容: 需要更新主题信息
|
||||
const discuss = {
|
||||
|
||||
};
|
||||
|
||||
//组装文章内容:需要添加的文章信息
|
||||
const article = {
|
||||
};
|
||||
|
||||
//定义效验规则
|
||||
const ruleFormRef = ref(null);
|
||||
const rules = reactive({
|
||||
title: [
|
||||
{ required: true, message: "请输入标题", trigger: "blur" },
|
||||
{ min: 3, max: 40, message: "长度 3 到 20", trigger: "blur" },
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: "请输入子文章名称", trigger: "blur" },
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: "请输入内容", trigger: "blur" },
|
||||
{ min: 10, message: "长度 大于 10", trigger: "blur" },
|
||||
],
|
||||
});
|
||||
//提交按钮,需要区分操作类型
|
||||
const submit = async (formEl) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate(async (valid, fields) => {
|
||||
if (valid) {
|
||||
//dicuss主题处理
|
||||
if (route.query.artType == "discuss") {
|
||||
|
||||
|
||||
discuss.title = editForm.title;
|
||||
discuss.types = editForm.types;
|
||||
discuss.introduction = editForm.introduction;
|
||||
discuss.content = editForm.content;
|
||||
discuss.plateId = discuss.plateId ?? route.query.plateId
|
||||
discuss.cover=dialogImageUrl.value;
|
||||
discuss.permissionType=perRadio.value;
|
||||
|
||||
discuss.permissionUserIds=editForm.permissionUserIds;
|
||||
//主题创建
|
||||
if (route.query.operType == "create") {
|
||||
const response = await discussAdd(discuss);
|
||||
|
||||
ElMessage({
|
||||
message: `[${discuss.title}]主题创建成功!`,
|
||||
type: 'success',
|
||||
})
|
||||
var routerPer = { path: `/article/${response.data.id}` };
|
||||
router.push(routerPer);
|
||||
}
|
||||
//主题更新
|
||||
else if (route.query.operType == "update") {
|
||||
await discussUpdate(route.query.discussId, discuss);
|
||||
|
||||
ElMessage({
|
||||
message: `[${discuss.title}]主题更新成功!`,
|
||||
type: 'success',
|
||||
})
|
||||
var routerPer = { path: `/article/${route.query.discussId}` };
|
||||
router.push(routerPer);
|
||||
}
|
||||
}
|
||||
|
||||
//artcle文章处理
|
||||
else if (route.query.artType == "article") {
|
||||
//组装文章内容:需要添加的文章信息
|
||||
article.content = editForm.content;
|
||||
article.name = editForm.name;
|
||||
article.discussId = route.query.discussId;
|
||||
article.parentId = route.query.parentArticleId
|
||||
//文章创建
|
||||
if (route.query.operType == "create") {
|
||||
const response = await articleAdd(article);
|
||||
ElMessage({
|
||||
message: `[${article.name}]文章创建成功!`,
|
||||
type: 'success',
|
||||
})
|
||||
var routerPer = { path: `/article/${route.query.discussId}/${response.data.id}` };
|
||||
router.push(routerPer);
|
||||
}
|
||||
//文章更新
|
||||
else if (route.query.operType == "update") {
|
||||
await articleUpdate(route.query.articleId, article);
|
||||
ElMessage({
|
||||
message: `[${article.name}]文章更新成功!`,
|
||||
type: 'success',
|
||||
})
|
||||
var routerPer = { path: `/article/${route.query.discussId}/${route.query.articleId}` };
|
||||
router.push(routerPer);
|
||||
}
|
||||
}
|
||||
//添加成功后跳转到该页面
|
||||
// var routerPer = { path: `/discuss/${discuss.plateId}` };
|
||||
// router.push(routerPer);
|
||||
// ruleFormRef.value.resetFields();
|
||||
// discuss.plateId = route.query.plateId;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
//如果是更新操作,需要先查询
|
||||
if (route.query.operType == "update") {
|
||||
|
||||
//更新主题
|
||||
if (route.query.artType == "discuss") {
|
||||
await loadDiscuss();
|
||||
|
||||
//更新文章
|
||||
} else if (route.query.artType == "article") {
|
||||
await loadArticle();
|
||||
}
|
||||
}
|
||||
});
|
||||
//加载主题
|
||||
const loadDiscuss = async () => {
|
||||
const response = await discussGet(route.query.discussId);
|
||||
const res = response.data
|
||||
editForm.content = res.content;
|
||||
editForm.title = res.title;
|
||||
editForm.types = res.types;
|
||||
editForm.introduction = res.introduction;
|
||||
discuss.plateId = res.plateId;
|
||||
dialogImageUrl.value= res.cover;
|
||||
perRadio.value=res.permissionType;
|
||||
editForm.permissionUserIds=res.permissionUserIds;
|
||||
};
|
||||
//加载文章
|
||||
const loadArticle = async () => {
|
||||
const response = await articleGet(route.query.articleId);
|
||||
const res = response.data
|
||||
editForm.content = res.content;
|
||||
editForm.name = res.name;
|
||||
editForm.discussId = res.discussId;
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.submit-btn {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.body-div {
|
||||
min-height: 1000px;
|
||||
background-color: #fff;
|
||||
margin: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.avatar-uploader >>>.el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
}
|
||||
|
||||
.avatar-uploader >>>.el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-icon.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
text-align: center;
|
||||
}
|
||||
.el-upload
|
||||
{
|
||||
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
102
Yi.Bbs.Vue3/src/views/Login.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="login-wrapper">
|
||||
<h1>{{ configStore.name }}-登录</h1>
|
||||
<div class="login-form">
|
||||
<el-form ref="loginFormRef" :model="loginForm" :rules="rules">
|
||||
<div class="username form-item">
|
||||
<el-form-item prop="userName">
|
||||
<span>使用账号</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input-item"
|
||||
v-model="loginForm.userName"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="password form-item">
|
||||
<el-form-item prop="password">
|
||||
<span>密码</span>
|
||||
<input
|
||||
type="password"
|
||||
class="input-item"
|
||||
v-model="loginForm.password"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<RouterLink to="/register"> 没有账号?前往注册</RouterLink>
|
||||
<button class="login-btn" @click="login(loginFormRef)">登 录</button>
|
||||
<button class="login-btn" @click="guestlogin">游客临时登录</button>
|
||||
</div>
|
||||
|
||||
<div class="divider">
|
||||
<span class="line"></span>
|
||||
<span class="divider-text">其他方式登录</span>
|
||||
<span class="line"></span>
|
||||
</div>
|
||||
|
||||
<div class="other-login-wrapper">
|
||||
<div class="other-login-item">
|
||||
<img src="@/assets/login_images/QQ.png" alt="" />
|
||||
</div>
|
||||
<div class="other-login-item">
|
||||
<img src="@/assets/login_images/WeChat.png" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <h2> 登录-欢迎</h2>
|
||||
<el-input v-model="loginForm.userName" placeholder="用户名" />
|
||||
<el-input v-model="loginForm.password" placeholder="密码" show-password />
|
||||
<el-button class="login-btn" type="primary" @click="login">登录</el-button>
|
||||
<br>
|
||||
<el-button class="login-btn" type="primary" @click="guestlogin">游客临时登录</el-button> -->
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import useUserStore from "@/stores/user.js";
|
||||
import useConfigStore from "@/stores/config";
|
||||
import useAuths from "@/hooks/useAuths";
|
||||
const configStore = useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const { loginFun } = useAuths();
|
||||
|
||||
const loginFormRef = ref();
|
||||
const rules = reactive({
|
||||
userName: [{ required: true, message: "请输入账号名", trigger: "blur" }],
|
||||
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
|
||||
});
|
||||
const loginForm = reactive({
|
||||
userName: "cc",
|
||||
password: "123456",
|
||||
uuid: "",
|
||||
code: "",
|
||||
});
|
||||
const guestlogin = async () => {
|
||||
loginForm.userName = "guest";
|
||||
loginForm.password = "123456";
|
||||
await userStore.login(loginForm);
|
||||
const redirect = route.query?.redirect ?? "/index";
|
||||
router.push(redirect);
|
||||
};
|
||||
const login = async (formEl) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
loginFun(loginForm);
|
||||
} catch (error) {
|
||||
ElMessage({
|
||||
message: error.message,
|
||||
type: "error",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style src="@/assets/styles/login.scss" scoped></style>
|
||||
4
Yi.Bbs.Vue3/src/views/NotFound.vue
Normal file
@@ -0,0 +1,4 @@
|
||||
<template>
|
||||
404
|
||||
|
||||
</template>
|
||||
108
Yi.Bbs.Vue3/src/views/Register.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="login-wrapper">
|
||||
<h1>{{configStore.name}}-注册</h1>
|
||||
<div class="login-form">
|
||||
<div class="username form-item">
|
||||
<span>登录账号</span>
|
||||
<input type="text" class="input-item" v-model="registerForm.userName">
|
||||
</div>
|
||||
<div class="username form-item">
|
||||
<span>手机号</span>
|
||||
<input style="width: 70%;" type="text" class="input-item" v-model="registerForm.phone">
|
||||
<button v-if="!isSendCaptcha" style="width: 30%;background-color: #C14949;" class="login-btn" @click="captcha" >验证码</button>
|
||||
<button v-else style="width: 30%;background-color:#F0F2F5;" class="login-btn" >已发送</button>
|
||||
</div>
|
||||
<div class="username form-item" v-show="isSendCaptcha">
|
||||
<span>手机短信验证码</span>
|
||||
<input type="text" class="input-item" v-model="registerForm.code">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="password form-item">
|
||||
<span>密码</span>
|
||||
<input type="password" class="input-item" v-model="registerForm.password">
|
||||
</div>
|
||||
<div class="password form-item">
|
||||
<span>确认密码</span>
|
||||
<input type="password" class="input-item" v-model="passwordConfirm">
|
||||
</div>
|
||||
<RouterLink to="/login" > 已有账号,前往登录</RouterLink>
|
||||
<button class="login-btn" @click="register">注册</button>
|
||||
</div>
|
||||
<div class="divider">
|
||||
<span class="line"></span>
|
||||
<span class="divider-text">其他方式注册</span>
|
||||
<span class="line"></span>
|
||||
</div>
|
||||
<div class="other-login-wrapper">
|
||||
<div class="other-login-item">
|
||||
<img src="@/assets/login_images/QQ.png" alt="">
|
||||
</div>
|
||||
<div class="other-login-item">
|
||||
<img src="@/assets/login_images/WeChat.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive ,ref} from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import {getCodePhone} from '@/apis/accountApi'
|
||||
import useUserStore from '@/stores/user.js'
|
||||
import useConfigStore from "@/stores/config";
|
||||
const configStore= useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const passwordConfirm=ref('');
|
||||
const registerForm = reactive({
|
||||
userName: "",
|
||||
password: "",
|
||||
uuid: "",
|
||||
code: "",
|
||||
phone:""
|
||||
})
|
||||
|
||||
const isSendCaptcha=ref(false)
|
||||
|
||||
//验证码
|
||||
const captcha=async()=>{
|
||||
isSendCaptcha.value=true;
|
||||
const response= await getCodePhone(registerForm.phone);
|
||||
ElMessage({
|
||||
message: `已向${registerForm.phone}发送验证码,请注意查收`,
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
const register = async () => {
|
||||
|
||||
if(registerForm.password!=passwordConfirm.value)
|
||||
{
|
||||
ElMessage.error('两次密码输入不一致')
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const response = await userStore.register(registerForm).catch((e) => {
|
||||
registerForm.password="";
|
||||
passwordConfirm.value="";
|
||||
});
|
||||
|
||||
//成功
|
||||
if (response!=undefined) {
|
||||
ElMessage({
|
||||
message: `恭喜!${registerForm.userName},注册成功!请登录!`,
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
const redirect = route.query?.redirect ?? '/login'
|
||||
router.push(redirect)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
<style src="@/assets/styles/login.scss" scoped></style>
|
||||
1
Yi.Bbs.Vue3/src/views/Test.vue
Normal file
@@ -0,0 +1 @@
|
||||
<template></template>
|
||||
85
Yi.Bbs.Vue3/src/views/error/401.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="errPage-container">
|
||||
<el-button icon="arrow-left" class="pan-back-btn" @click="back">
|
||||
返回
|
||||
</el-button>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<h1 class="text-jumbo text-ginormous">
|
||||
401错误!
|
||||
</h1>
|
||||
<h2>您没有访问权限!</h2>
|
||||
<h6>对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li class="link-type">
|
||||
<router-link to="/">
|
||||
回首页
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import errImage from "@/assets/401_images/401.gif";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
let { proxy } = getCurrentInstance();
|
||||
const router=useRouter();
|
||||
|
||||
const errGif = ref(errImage + "?" + +new Date());
|
||||
|
||||
function back() {
|
||||
if (proxy.$route.query.noGoBack) {
|
||||
router.push({ path: "/" });
|
||||
} else {
|
||||
router.go(-1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.errPage-container {
|
||||
width: 800px;
|
||||
max-width: 100%;
|
||||
margin: 100px auto;
|
||||
.pan-back-btn {
|
||||
background: #008489;
|
||||
color: #fff;
|
||||
border: none !important;
|
||||
}
|
||||
.pan-gif {
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
.pan-img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.text-jumbo {
|
||||
font-size: 60px;
|
||||
font-weight: 700;
|
||||
color: #484848;
|
||||
}
|
||||
.list-unstyled {
|
||||
font-size: 14px;
|
||||
li {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
a {
|
||||
color: #008489;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
228
Yi.Bbs.Vue3/src/views/error/404.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="wscn-http404-container">
|
||||
<div class="wscn-http404">
|
||||
<div class="pic-404">
|
||||
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
|
||||
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||
</div>
|
||||
<div class="bullshit">
|
||||
<div class="bullshit__oops">
|
||||
404错误!
|
||||
</div>
|
||||
<div class="bullshit__headline">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="bullshit__info">
|
||||
对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
|
||||
</div>
|
||||
<router-link to="/index" class="bullshit__return-home">
|
||||
返回首页
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed} from 'vue'
|
||||
let message = computed(() => {
|
||||
return '找不到网页!'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wscn-http404-container{
|
||||
transform: translate(-50%,-50%);
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
}
|
||||
.wscn-http404 {
|
||||
position: relative;
|
||||
width: 1200px;
|
||||
padding: 0 50px;
|
||||
overflow: hidden;
|
||||
.pic-404 {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 600px;
|
||||
overflow: hidden;
|
||||
&__parent {
|
||||
width: 100%;
|
||||
}
|
||||
&__child {
|
||||
position: absolute;
|
||||
&.left {
|
||||
width: 80px;
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
animation-name: cloudLeft;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
&.mid {
|
||||
width: 46px;
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
animation-name: cloudMid;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
&.right {
|
||||
width: 62px;
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
animation-name: cloudRight;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
@keyframes cloudLeft {
|
||||
0% {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 33px;
|
||||
left: 188px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 81px;
|
||||
left: 92px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 97px;
|
||||
left: 60px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudMid {
|
||||
0% {
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 40px;
|
||||
left: 360px;
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
top: 130px;
|
||||
left: 180px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 160px;
|
||||
left: 120px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudRight {
|
||||
0% {
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 120px;
|
||||
left: 460px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 180px;
|
||||
left: 340px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 200px;
|
||||
left: 300px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.bullshit {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 300px;
|
||||
padding: 30px 0;
|
||||
overflow: hidden;
|
||||
&__oops {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
color: #1482f0;
|
||||
opacity: 0;
|
||||
margin-bottom: 20px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__headline {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
margin-bottom: 10px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__info {
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
color: grey;
|
||||
opacity: 0;
|
||||
margin-bottom: 30px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__return-home {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 110px;
|
||||
height: 36px;
|
||||
background: #1482f0;
|
||||
border-radius: 100px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
opacity: 0;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
199
Yi.Bbs.Vue3/src/views/home/Index.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div style="width: 1200px">
|
||||
<el-row :gutter="20" class="top-div">
|
||||
<el-col :span="17">
|
||||
<div class="scrollbar">
|
||||
<ScrollbarInfo />
|
||||
</div>
|
||||
<el-row class="left-div">
|
||||
<el-col
|
||||
:span="8"
|
||||
v-for="i in plateList"
|
||||
class="plate"
|
||||
:style="{
|
||||
'padding-left': i % 3 == 1 ? 0 : 0.2 + 'rem',
|
||||
'padding-right': i % 3 == 0 ? 0 : 0.2 + 'rem',
|
||||
}"
|
||||
>
|
||||
<PlateCard
|
||||
:name="i.name"
|
||||
:introduction="i.introduction"
|
||||
:id="i.id"
|
||||
/>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" v-for="i in discussList">
|
||||
<DisscussCard :discuss="i" />
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-empty
|
||||
v-show="discussList.length <= 0"
|
||||
description="推荐位置,空空如也"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="7">
|
||||
<el-row class="right-div">
|
||||
<el-col :span="24">
|
||||
<el-carousel trigger="click" height="150px">
|
||||
<el-carousel-item v-for="item in bannerList">
|
||||
<div class="carousel-font" :style="{ color: item.color }">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<el-image
|
||||
style="width: 100%; height: 100%"
|
||||
:src="item.logo"
|
||||
fit="cover"
|
||||
/>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24">
|
||||
<InfoCard header="访问统计" class="VisitsLineChart" text="详情">
|
||||
<template #content>
|
||||
<VisitsLineChart :option="statisOptions" class="statisChart" />
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24">
|
||||
<InfoCard header="简介" text="详情">
|
||||
<template #content>
|
||||
<div class="introduce">
|
||||
没有什么能够阻挡,人类对代码<span style="color: #1890ff"
|
||||
>优雅</span
|
||||
>的追求
|
||||
</div>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24">
|
||||
<InfoCard :items="items" header="本月排行" text="更多">
|
||||
<template #item="temp">
|
||||
<AvatarInfo>
|
||||
<template #bottom> 本月积分:290 </template>
|
||||
</AvatarInfo>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24">
|
||||
<InfoCard :items="items" header="推荐好友" text="更多">
|
||||
<template #item="temp">
|
||||
<AvatarInfo />
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" style="background: transparent">
|
||||
<BottomInfo />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, reactive, computed } from "vue";
|
||||
import DisscussCard from "@/components/DisscussCard.vue";
|
||||
import InfoCard from "@/components/InfoCard.vue";
|
||||
import PlateCard from "@/components/PlateCard.vue";
|
||||
import ScrollbarInfo from "@/components/ScrollbarInfo.vue";
|
||||
import AvatarInfo from "@/components/AvatarInfo.vue";
|
||||
import BottomInfo from "@/components/BottomInfo.vue";
|
||||
import VisitsLineChart from "./components/VisitsLineChart.vue";
|
||||
|
||||
import { access } from "@/apis/accessApi.js";
|
||||
import { getList } from "@/apis/plateApi.js";
|
||||
import { getList as bannerGetList } from "@/apis/bannerApi.js";
|
||||
import { getList as discussGetList } from "@/apis/discussApi.js";
|
||||
import { getWeek } from "@/apis/accessApi.js";
|
||||
|
||||
var plateList = ref([]);
|
||||
var discussList = ref([]);
|
||||
var bannerList = ref([]);
|
||||
const weekList = ref([]);
|
||||
|
||||
const items = [{ user: "用户1" }, { user: "用户2" }, { user: "用户3" }];
|
||||
//主题查询参数
|
||||
const query = reactive({
|
||||
skipCount: 1,
|
||||
maxResultCount: 10,
|
||||
isTop: true,
|
||||
});
|
||||
|
||||
//初始化
|
||||
onMounted(async () => {
|
||||
access();
|
||||
const { data: plateData } = await getList();
|
||||
plateList.value = plateData.items;
|
||||
const { data: discussData } = await discussGetList(query);
|
||||
discussList.value = discussData.items;
|
||||
const { data: bannerData } = await bannerGetList();
|
||||
bannerList.value = bannerData.items;
|
||||
const { data: weekData } = await getWeek();
|
||||
weekList.value = weekData;
|
||||
});
|
||||
|
||||
// 访问统计
|
||||
const statisOptions = computed(() => {
|
||||
return {
|
||||
xAxis: {
|
||||
data: weekList.value.map((item) => item.creationTime),
|
||||
},
|
||||
series: {
|
||||
data: weekList.value.map((item) => item.number),
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.introduce {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.plate {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.left-div .el-col {
|
||||
background-color: #ffffff;
|
||||
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.right-div .el-col {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.carousel-font {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
}
|
||||
|
||||
.top-div {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.VisitsLineChart >>> .el-card__body {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.statisChart {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
30
Yi.Bbs.Vue3/src/views/home/components/VisitsLineChart.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="v-chart" ref="statis"></div>
|
||||
</template>
|
||||
|
||||
<script setup name="VisitsLineChart">
|
||||
import { ref, defineEmits, defineProps, defineExpose } from "vue";
|
||||
import useEcharts from "@/hooks/useEcharts";
|
||||
import { statisticsEcharts } from "../hooks/echartsConfig";
|
||||
const props = defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
const emits = defineEmits([
|
||||
"chart-click", // 点击chart
|
||||
]);
|
||||
|
||||
let statis = ref(null);
|
||||
const { resize } = useEcharts(statis, emits, props, statisticsEcharts);
|
||||
defineExpose({
|
||||
resize,
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.v-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
81
Yi.Bbs.Vue3/src/views/home/hooks/echartsConfig.js
Normal file
@@ -0,0 +1,81 @@
|
||||
export const statisticsEcharts = {
|
||||
grid: {
|
||||
top: "10%",
|
||||
left: "4%",
|
||||
right: "4%",
|
||||
bottom: "5%",
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
xAxis: {
|
||||
show: false,
|
||||
type: "category",
|
||||
data: [],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#999",
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
splitNumber: 4,
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: "dashed",
|
||||
color: "#DDD",
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: "#333",
|
||||
},
|
||||
},
|
||||
nameTextStyle: {
|
||||
color: "#999",
|
||||
},
|
||||
splitArea: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
series: {
|
||||
name: "访问量",
|
||||
type: "line",
|
||||
data: [],
|
||||
lineStyle: {
|
||||
normal: {
|
||||
width: 5,
|
||||
color: {
|
||||
type: "linear",
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: "#a0cfff", // 浅蓝色,0% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: "#0047AB", // 深蓝色,100% 处的颜色
|
||||
},
|
||||
],
|
||||
globalCoord: false, // 缺省为 false
|
||||
},
|
||||
shadowColor: "rgba(72,216,191, 0.3)",
|
||||
shadowBlur: 10,
|
||||
shadowOffsetY: 20,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: "#fff",
|
||||
borderWidth: 10,
|
||||
borderColor: "#A9F387",
|
||||
},
|
||||
},
|
||||
smooth: true,
|
||||
},
|
||||
};
|
||||
124
Yi.Bbs.Vue3/src/views/profile/Index.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="app-container" style="width: 1400px;">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" :xs="24">
|
||||
<el-card class="box-card">
|
||||
<template v-slot:header>
|
||||
<div class="clearfix">
|
||||
<span>个人信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<userAvatar :user="state.user" />
|
||||
</div>
|
||||
<ul class="list-group list-group-striped">
|
||||
<li class="list-group-item">
|
||||
<el-icon><User /></el-icon> 用户名称
|
||||
<div class="pull-right">{{ state.user.userName }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<el-icon><Phone /></el-icon> 手机号码
|
||||
<div class="pull-right">{{ state.user.phone }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<el-icon><Message /></el-icon> 用户邮箱
|
||||
<div class="pull-right">{{ state.user.email }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<el-icon><HelpFilled /></el-icon> 所属部门
|
||||
<div class="pull-right" v-if="state.dept">{{ state.dept.deptName }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<el-icon><Avatar /></el-icon> 所属角色
|
||||
<div class="pull-right"><span v-for="role in state.roles" :key="role.id">{{ role.roleName }} /</span></div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<el-icon><Stopwatch /></el-icon> 创建日期
|
||||
<div class="pull-right">{{ state.user.creationTime }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="18" :xs="24">
|
||||
<el-card>
|
||||
<template v-slot:header>
|
||||
<div class="clearfix">
|
||||
<span>基本资料</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="基本资料" name="userinfo">
|
||||
<userInfo :user="state.user" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="修改密码" name="resetPwd">
|
||||
<resetPwd />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Profile">
|
||||
import userAvatar from "./UserAvatar.vue";
|
||||
import userInfo from "./UserInfo.vue";
|
||||
import resetPwd from "./ResetPwd.vue";
|
||||
import { getUserProfile } from "@/apis/userApi.js";
|
||||
import { onMounted ,ref,reactive} from "vue";
|
||||
|
||||
const activeTab = ref("userinfo");
|
||||
const state = reactive({
|
||||
user: {},
|
||||
dept: {},
|
||||
roles: [],
|
||||
roleGroup: {},
|
||||
postGroup: {}
|
||||
});
|
||||
|
||||
function getUser() {
|
||||
getUserProfile().then(response => {
|
||||
const res=response.data;
|
||||
state.user = res.user;
|
||||
state.dept=res.dept;
|
||||
state.roles=res.roles;
|
||||
state.roleGroup = res.roleGroup;
|
||||
state.postGroup = res.postGroup;
|
||||
});
|
||||
};
|
||||
onMounted(()=>{
|
||||
getUser();
|
||||
})
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.pull-right {
|
||||
float: right !important;
|
||||
}
|
||||
.list-group-striped > .list-group-item {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-radius: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
padding-left: 0px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border-bottom: 1px solid #e7eaec;
|
||||
border-top: 1px solid #e7eaec;
|
||||
margin-bottom: -1px;
|
||||
padding: 11px 0px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
66
Yi.Bbs.Vue3/src/views/profile/ResetPwd.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
|
||||
<el-form-item label="旧密码" prop="oldPassword">
|
||||
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submit">保存</el-button>
|
||||
<el-button type="danger" @click="close">关闭</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { updateUserPwd } from "@/apis/userApi";
|
||||
import { ref,reactive } from "vue";
|
||||
|
||||
const pwdRef=ref(null);
|
||||
|
||||
const user = reactive({
|
||||
oldPassword: undefined,
|
||||
newPassword: undefined,
|
||||
confirmPassword: undefined
|
||||
});
|
||||
|
||||
const equalToPassword = (rule, value, callback) => {
|
||||
if (user.newPassword !== value) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const rules = ref({
|
||||
oldPassword: [{ required: true, message: "旧密码不能为空", trigger: "blur" }],
|
||||
newPassword: [{ required: true, message: "新密码不能为空", trigger: "blur" }, { min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" }],
|
||||
confirmPassword: [{ required: true, message: "确认密码不能为空", trigger: "blur" }, { required: true, validator: equalToPassword, trigger: "blur" }]
|
||||
});
|
||||
|
||||
/** 提交按钮 */
|
||||
function submit() {
|
||||
pwdRef.value.validate(async valid => {
|
||||
if (valid) {
|
||||
const response=await updateUserPwd(user.oldPassword, user.newPassword)
|
||||
|
||||
if(response.status==200)
|
||||
{
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "更新密码成功",
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 关闭按钮 */
|
||||
function close() {
|
||||
|
||||
// proxy.$tab.closePage();
|
||||
};
|
||||
</script>
|
||||
187
Yi.Bbs.Vue3/src/views/profile/UserAvatar.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="user-info-head" @click="editCropper()"><img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
|
||||
</div>
|
||||
<el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
|
||||
<el-row>
|
||||
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
|
||||
<vue-cropper ref="cropper" :img="options.img" :info="true" :autoCrop="options.autoCrop"
|
||||
:autoCropWidth="options.autoCropWidth" :autoCropHeight="options.autoCropHeight" :fixedBox="options.fixedBox"
|
||||
@realTime="realTime" v-if="visible" />
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
|
||||
<div class="avatar-upload-preview">
|
||||
<img :src="options.previews.url" :style="options.previews.img" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<br />
|
||||
<el-row>
|
||||
<el-col :lg="2" :md="2">
|
||||
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
|
||||
<el-button>
|
||||
选择
|
||||
<el-icon class="el-icon--right">
|
||||
<Upload />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
|
||||
<el-button icon="Plus" @click="changeScale(1)"></el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
||||
<el-button icon="Minus" @click="changeScale(-1)"></el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
||||
<el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
||||
<el-button icon="RefreshRight" @click="rotateRight()"></el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
|
||||
<el-button type="primary" @click="uploadImg()">上传</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import "vue-cropper/dist/index.css";
|
||||
import { VueCropper } from "vue-cropper";
|
||||
import { upload } from "@/apis/fileApi";
|
||||
import { updateUserIcon } from "@/apis/userApi";
|
||||
import useUserStore from '@/stores/user'
|
||||
import { ref, reactive } from "vue";
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const cropper = ref(null);
|
||||
|
||||
const open = ref(false);
|
||||
const visible = ref(false);
|
||||
const title = ref("修改头像");
|
||||
|
||||
//图片裁剪数据
|
||||
const options = reactive({
|
||||
img: userStore.icon, // 裁剪图片的地址
|
||||
autoCrop: true, // 是否默认生成截图框
|
||||
autoCropWidth: 200, // 默认生成截图框宽度
|
||||
autoCropHeight: 200, // 默认生成截图框高度
|
||||
fixedBox: true, // 固定截图框大小 不允许改变
|
||||
previews: {} //预览数据
|
||||
});
|
||||
|
||||
/** 编辑头像 */
|
||||
function editCropper() {
|
||||
open.value = true;
|
||||
};
|
||||
/** 打开弹出层结束时的回调 */
|
||||
function modalOpened() {
|
||||
visible.value = true;
|
||||
};
|
||||
/** 覆盖默认上传行为 */
|
||||
function requestUpload() {
|
||||
};
|
||||
/** 向左旋转 */
|
||||
function rotateLeft() {
|
||||
cropper.value.rotateLeft();
|
||||
};
|
||||
/** 向右旋转 */
|
||||
function rotateRight() {
|
||||
cropper.value.rotateRight();
|
||||
};
|
||||
/** 图片缩放 */
|
||||
function changeScale(num) {
|
||||
num = num || 1;
|
||||
cropper.value.changeScale(num);
|
||||
};
|
||||
/** 上传预处理 */
|
||||
function beforeUpload(file) {
|
||||
if (file.type.indexOf("image/") == -1) {
|
||||
ElMessage.error("文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。")
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
options.img = reader.result;
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
/** 上传图片 */
|
||||
async function uploadImg() {
|
||||
await cropper.value.getCropBlob(async data => {
|
||||
let formData = new FormData();
|
||||
formData.append("file", data);
|
||||
const response = await upload(formData)
|
||||
open.value = false;
|
||||
options.img = import.meta.env.VITE_APP_BASEAPI + "/file/" + response.data[0].id;
|
||||
userStore.icon = options.img;
|
||||
const iconResponse= await updateUserIcon(response.data[0].id);
|
||||
if(iconResponse.status==200)
|
||||
{
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "头像更新成功",
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
/** 实时预览 */
|
||||
function realTime(data) {
|
||||
options.previews = data;
|
||||
};
|
||||
/** 关闭窗口 */
|
||||
function closeDialog() {
|
||||
options.img = userStore.icon;
|
||||
options.visible = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.user-info-head {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.user-info-head:hover:after {
|
||||
content: "+";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
color: #eee;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
cursor: pointer;
|
||||
line-height: 110px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.img-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.img-lg {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.avatar-upload-preview {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(50%, -50%);
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px #ccc;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
66
Yi.Bbs.Vue3/src/views/profile/UserInfo.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<el-form ref="userRef" :model="user" :rules="rules" label-width="80px">
|
||||
<el-form-item label="账号" prop="userName">
|
||||
<el-input v-model="user.userName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户昵称" prop="nick">
|
||||
<el-input v-model="user.nick" maxlength="30" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码" prop="phone">
|
||||
<el-input v-model="user.phone" maxlength="11" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="user.email" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="user.sex">
|
||||
<el-radio :label="'Male'">男</el-radio>
|
||||
<el-radio :label="'Woman'">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submit">保存</el-button>
|
||||
<el-button type="danger" @click="close">关闭</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { updateUserProfile } from "@/apis/userApi";
|
||||
import useUserStore from "@/stores/user"
|
||||
import { ref } from "vue";
|
||||
const userStore = useUserStore();
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object
|
||||
}
|
||||
});
|
||||
|
||||
const userRef = ref(null);
|
||||
|
||||
const rules = ref({
|
||||
nick: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
|
||||
email: [{ required: true, message: "邮箱地址不能为空", trigger: "blur" }, { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
|
||||
phone: [{ required: true, message: "手机号码不能为空", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
|
||||
});
|
||||
|
||||
/** 提交按钮 */
|
||||
function submit() {
|
||||
userRef.value.validate(async valid => {
|
||||
if (valid) {
|
||||
const response = await updateUserProfile(props.user);
|
||||
if (response.status == 200) {
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "用户信息修改成功",
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 关闭按钮 */
|
||||
function close() {
|
||||
// proxy.$tab.closePage();
|
||||
};
|
||||
</script>
|
||||
45
Yi.Bbs.Vue3/vite.config.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig, loadEnv } 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'
|
||||
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
/** @type {import('vite').UserConfig} */
|
||||
export default defineConfig(({ command, mode })=>{
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
return {
|
||||
// envDir: 'env',
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server:{
|
||||
port:18001,
|
||||
open:true,
|
||||
proxy:{
|
||||
|
||||
[env.VITE_APP_BASEAPI]: {
|
||||
target: env.VITE_APP_URL,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api-dev/, ''),
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||