✨ comment reply framework
This commit is contained in:
28
components.d.ts
vendored
28
components.d.ts
vendored
@@ -7,6 +7,9 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||
ABadgeRibbon: typeof import('ant-design-vue/es')['BadgeRibbon']
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ACard: typeof import('ant-design-vue/es')['Card']
|
||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||
@@ -16,35 +19,60 @@ declare module 'vue' {
|
||||
AFlex: typeof import('ant-design-vue/es')['Flex']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AImage: typeof import('ant-design-vue/es')['Image']
|
||||
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
||||
AQrcode: typeof import('ant-design-vue/es')['QRCode']
|
||||
ASegmented: typeof import('ant-design-vue/es')['Segmented']
|
||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||
BoxDog: typeof import('./src/components/BoxDog/BoxDog.vue')['default']
|
||||
Card3D: typeof import('./src/components/Card3D/Card3D.vue')['default']
|
||||
ChromeOutlined: typeof import('@ant-design/icons-vue')['ChromeOutlined']
|
||||
ClockCircleOutlined: typeof import('@ant-design/icons-vue')['ClockCircleOutlined']
|
||||
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
|
||||
Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default']
|
||||
CommentReply: typeof import('./src/components/CommentReply/CommentReply.vue')['default']
|
||||
CopyOutlined: typeof import('@ant-design/icons-vue')['CopyOutlined']
|
||||
DeleteOutlined: typeof import('@ant-design/icons-vue')['DeleteOutlined']
|
||||
DislikeOutlined: typeof import('@ant-design/icons-vue')['DislikeOutlined']
|
||||
DownOutlined: typeof import('@ant-design/icons-vue')['DownOutlined']
|
||||
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
|
||||
EffectsCard: typeof import('./src/components/EffectsCard/EffectsCard.vue')['default']
|
||||
EllipsisOutlined: typeof import('@ant-design/icons-vue')['EllipsisOutlined']
|
||||
ForgetPage: typeof import('./src/views/Forget/ForgetPage.vue')['default']
|
||||
GithubOutlined: typeof import('@ant-design/icons-vue')['GithubOutlined']
|
||||
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
|
||||
LikeOutlined: typeof import('@ant-design/icons-vue')['LikeOutlined']
|
||||
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
|
||||
LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default']
|
||||
LoginPage: typeof import('./src/views/Login/LoginPage.vue')['default']
|
||||
MainPage: typeof import('./src/views/Main/MainPage.vue')['default']
|
||||
NotFound: typeof import('./src/views/404/NotFound.vue')['default']
|
||||
PictureOutlined: typeof import('@ant-design/icons-vue')['PictureOutlined']
|
||||
PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined']
|
||||
QqOutlined: typeof import('@ant-design/icons-vue')['QqOutlined']
|
||||
QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default']
|
||||
QRLoginFooter: typeof import('./src/views/QRLogin/QRLoginFooter.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SafetyOutlined: typeof import('@ant-design/icons-vue')['SafetyOutlined']
|
||||
SmileOutlined: typeof import('@ant-design/icons-vue')['SmileOutlined']
|
||||
TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined']
|
||||
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
|
||||
WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined']
|
||||
WechatOutlined: typeof import('@ant-design/icons-vue')['WechatOutlined']
|
||||
WindowsOutlined: typeof import('@ant-design/icons-vue')['WindowsOutlined']
|
||||
}
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@
|
||||
"animejs": "^3.2.2",
|
||||
"ant-design-vue": "^4.2.3",
|
||||
"axios": "^1.7.7",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
"eslint": "9.9.1",
|
||||
"go-captcha-vue": "^2",
|
||||
|
19
src/api/comment/index.ts
Normal file
19
src/api/comment/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {service} from "@/utils/alova/service.ts";
|
||||
|
||||
/**
|
||||
* 上传图片转base64
|
||||
*/
|
||||
export const uploadToBase64 = (images: any[]) => {
|
||||
return service.Post('/api/auth/comment/upload_to_base64',
|
||||
{
|
||||
images,
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
meta: {
|
||||
ignoreToken: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
@@ -106,9 +106,10 @@ export const resetPasswordApi = (param: ResetPassword) => {
|
||||
* @param user_id
|
||||
*/
|
||||
export const getUserPermissions = (user_id: string) => {
|
||||
return service.Post('/api/auth/permission/get_user_permissions', {
|
||||
user_id: user_id,
|
||||
}, {
|
||||
return service.Get('/api/auth/permission/get_user_permissions', {
|
||||
params: {
|
||||
user_id: user_id
|
||||
},
|
||||
meta: {
|
||||
ignoreToken: false,
|
||||
}
|
||||
|
@@ -65,159 +65,7 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
<style src="./index.scss" scoped lang="scss">
|
||||
|
||||
h1 {
|
||||
color: #3e3e42;
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -1px;
|
||||
margin-bottom: 30px;
|
||||
transform: translateZ(35px);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #eb285d;
|
||||
font-size: 16px;
|
||||
margin-bottom: 6px;
|
||||
transform: translateZ(25px);
|
||||
}
|
||||
|
||||
.cards {
|
||||
background: #fff;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0px 10px 20px 20px rgba(0, 0, 0, 0.17);
|
||||
display: inline-block;
|
||||
padding: 30px 35px;
|
||||
perspective: 1800px;
|
||||
text-align: left;
|
||||
transform-origin: 50% 50%;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateX(11deg) rotateY(16.5deg);
|
||||
min-width: 595px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 15px;
|
||||
box-shadow: 5px 5px 20px -5px rgba(0, 0, 0, 0.6);
|
||||
display: inline-block;
|
||||
height: 250px;
|
||||
overflow: hidden;
|
||||
perspective: 1200px;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
transform: translatez(35px);
|
||||
transition: transform 200ms ease-out;
|
||||
width: 175px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card:not(:last-child) {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.card__img {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card__bg {
|
||||
bottom: -50px;
|
||||
left: -50px;
|
||||
position: absolute;
|
||||
right: -50px;
|
||||
top: -50px;
|
||||
transform-origin: 50% 50%;
|
||||
transform: translateZ(-50px);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.card__one .card__img {
|
||||
top: 14px;
|
||||
right: -10px;
|
||||
height: 110%;
|
||||
}
|
||||
|
||||
.card__one .card__bg {
|
||||
background: url("@/assets/images/3dr_monobg.jpg") center/cover no-repeat;
|
||||
}
|
||||
|
||||
.card__two .card__img {
|
||||
top: 25px;
|
||||
}
|
||||
|
||||
.card__two .card__bg {
|
||||
background: url("@/assets/images/3dr_spirited.jpg") center/cover no-repeat;
|
||||
}
|
||||
|
||||
.card__three .card__img {
|
||||
top: 5px;
|
||||
left: -4px;
|
||||
height: 110%;
|
||||
}
|
||||
|
||||
.card__three .card__bg {
|
||||
background: url("@/assets/images/3dr_howlbg.jpg") center/cover no-repeat;
|
||||
}
|
||||
|
||||
.card__text {
|
||||
align-items: center;
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.55) 100%);
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 70px;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.card__title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
padding: 0 10px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
background: gold;
|
||||
border-top-left-radius: 6px;
|
||||
bottom: 0;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
padding: 8px 10px;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
}
|
||||
|
||||
.twitter__link {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 12px;
|
||||
z-index: -1;
|
||||
background: #00aced;
|
||||
border-radius: 20px;
|
||||
height: 30px;
|
||||
text-decoration: none;
|
||||
padding-right: 10px;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
width: 74px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.twitter__link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.twitter__icon {
|
||||
height: 30px;
|
||||
}
|
||||
</style>
|
||||
|
153
src/components/Card3D/index.scss
Normal file
153
src/components/Card3D/index.scss
Normal file
@@ -0,0 +1,153 @@
|
||||
h1 {
|
||||
color: #3e3e42;
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -1px;
|
||||
margin-bottom: 30px;
|
||||
transform: translateZ(35px);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #eb285d;
|
||||
font-size: 16px;
|
||||
margin-bottom: 6px;
|
||||
transform: translateZ(25px);
|
||||
}
|
||||
|
||||
.cards {
|
||||
background: #fff;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0px 10px 20px 20px rgba(0, 0, 0, 0.17);
|
||||
display: inline-block;
|
||||
padding: 30px 35px;
|
||||
perspective: 1800px;
|
||||
text-align: left;
|
||||
transform-origin: 50% 50%;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateX(11deg) rotateY(16.5deg);
|
||||
min-width: 595px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 15px;
|
||||
box-shadow: 5px 5px 20px -5px rgba(0, 0, 0, 0.6);
|
||||
display: inline-block;
|
||||
height: 250px;
|
||||
overflow: hidden;
|
||||
perspective: 1200px;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
transform: translatez(35px);
|
||||
transition: transform 200ms ease-out;
|
||||
width: 175px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card:not(:last-child) {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.card__img {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card__bg {
|
||||
bottom: -50px;
|
||||
left: -50px;
|
||||
position: absolute;
|
||||
right: -50px;
|
||||
top: -50px;
|
||||
transform-origin: 50% 50%;
|
||||
transform: translateZ(-50px);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.card__one .card__img {
|
||||
top: 14px;
|
||||
right: -10px;
|
||||
height: 110%;
|
||||
}
|
||||
|
||||
.card__one .card__bg {
|
||||
background: url("@/assets/images/3dr_monobg.jpg") center/cover no-repeat;
|
||||
}
|
||||
|
||||
.card__two .card__img {
|
||||
top: 25px;
|
||||
}
|
||||
|
||||
.card__two .card__bg {
|
||||
background: url("@/assets/images/3dr_spirited.jpg") center/cover no-repeat;
|
||||
}
|
||||
|
||||
.card__three .card__img {
|
||||
top: 5px;
|
||||
left: -4px;
|
||||
height: 110%;
|
||||
}
|
||||
|
||||
.card__three .card__bg {
|
||||
background: url("@/assets/images/3dr_howlbg.jpg") center/cover no-repeat;
|
||||
}
|
||||
|
||||
.card__text {
|
||||
align-items: center;
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.55) 100%);
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 70px;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.card__title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
padding: 0 10px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
background: gold;
|
||||
border-top-left-radius: 6px;
|
||||
bottom: 0;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
padding: 8px 10px;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
}
|
||||
|
||||
.twitter__link {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 12px;
|
||||
z-index: -1;
|
||||
background: #00aced;
|
||||
border-radius: 20px;
|
||||
height: 30px;
|
||||
text-decoration: none;
|
||||
padding-right: 10px;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
width: 74px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.twitter__link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.twitter__icon {
|
||||
height: 30px;
|
||||
}
|
@@ -66,161 +66,6 @@ onMounted(() => {
|
||||
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
<style src="./index.scss" scoped lang="scss">
|
||||
|
||||
button {
|
||||
background: #55B4FB;
|
||||
border-radius: 30px;
|
||||
border: 2px solid #55B4FB;
|
||||
height: 44px;
|
||||
width: 165px;
|
||||
color: white;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
|
||||
transition: box-shadow 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
|
||||
background: #46adfb;
|
||||
}
|
||||
|
||||
button i {
|
||||
background: #75C2FB;
|
||||
padding: 10px;
|
||||
border-radius: 50px;
|
||||
position: absolute;
|
||||
border: 1px solid white;
|
||||
right: 2px;
|
||||
top: 50%;
|
||||
transform: translate(0%, -50%);
|
||||
}
|
||||
|
||||
button .button-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(0%, -50%);
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #F4F5F8;
|
||||
padding: 0 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
z-index: 10;
|
||||
width: 80%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -65%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-content h1 {
|
||||
color: white;
|
||||
font-size: 2.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
font-family: Lato, sans-serif;
|
||||
font-weight: 500;
|
||||
text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-content p {
|
||||
color: white;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
margin-top: 0px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: lighter;
|
||||
max-width: 650px;
|
||||
font-family: "Source Sans Pro", sans-serif;
|
||||
font-weight: 300;
|
||||
text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.flex-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
min-width: 320px;
|
||||
width: 100%;
|
||||
background: #7A90F6;
|
||||
/* fallback for old browsers */
|
||||
/* Chrome 10-25, Safari 5.1-6 */
|
||||
background: linear-gradient(to right, #7A90F6, #7B6FF6);
|
||||
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
||||
position: relative;
|
||||
height: 400px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.background-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.background-container:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
width: 2000px;
|
||||
height: 100%;
|
||||
//background: url("https://www.digitalocean.com/assets/media/products/header-04dcc3be.svg");
|
||||
//background-position: bottom;
|
||||
//background-repeat: no-repeat;
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.clouds {
|
||||
width: 225px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#cloudOne {
|
||||
top: -100px;
|
||||
left: -90px;
|
||||
}
|
||||
|
||||
#cloudTwo {
|
||||
right: -100px;
|
||||
bottom: -90px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#cloudThree {
|
||||
left: 150px;
|
||||
bottom: -150px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#cloudFour {
|
||||
right: 80px;
|
||||
top: -100px;
|
||||
z-index: -1;
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
|
156
src/components/Clouds/index.scss
Normal file
156
src/components/Clouds/index.scss
Normal file
@@ -0,0 +1,156 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #55B4FB;
|
||||
border-radius: 30px;
|
||||
border: 2px solid #55B4FB;
|
||||
height: 44px;
|
||||
width: 165px;
|
||||
color: white;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
|
||||
transition: box-shadow 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
|
||||
background: #46adfb;
|
||||
}
|
||||
|
||||
button i {
|
||||
background: #75C2FB;
|
||||
padding: 10px;
|
||||
border-radius: 50px;
|
||||
position: absolute;
|
||||
border: 1px solid white;
|
||||
right: 2px;
|
||||
top: 50%;
|
||||
transform: translate(0%, -50%);
|
||||
}
|
||||
|
||||
button .button-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(0%, -50%);
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #F4F5F8;
|
||||
padding: 0 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
z-index: 10;
|
||||
width: 80%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -65%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-content h1 {
|
||||
color: white;
|
||||
font-size: 2.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
font-family: Lato, sans-serif;
|
||||
font-weight: 500;
|
||||
text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-content p {
|
||||
color: white;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
margin-top: 0px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: lighter;
|
||||
max-width: 650px;
|
||||
font-family: "Source Sans Pro", sans-serif;
|
||||
font-weight: 300;
|
||||
text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.flex-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
min-width: 320px;
|
||||
width: 100%;
|
||||
background: #7A90F6;
|
||||
/* fallback for old browsers */
|
||||
/* Chrome 10-25, Safari 5.1-6 */
|
||||
background: linear-gradient(to right, #7A90F6, #7B6FF6);
|
||||
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
||||
position: relative;
|
||||
height: 400px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.background-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.background-container:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
width: 2000px;
|
||||
height: 100%;
|
||||
//background: url("https://www.digitalocean.com/assets/media/products/header-04dcc3be.svg");
|
||||
//background-position: bottom;
|
||||
//background-repeat: no-repeat;
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.clouds {
|
||||
width: 225px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#cloudOne {
|
||||
top: -100px;
|
||||
left: -90px;
|
||||
}
|
||||
|
||||
#cloudTwo {
|
||||
right: -100px;
|
||||
bottom: -90px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#cloudThree {
|
||||
left: 150px;
|
||||
bottom: -150px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#cloudFour {
|
||||
right: 80px;
|
||||
top: -100px;
|
||||
z-index: -1;
|
||||
width: 100px;
|
||||
}
|
450
src/components/CommentReply/CommentReply.vue
Normal file
450
src/components/CommentReply/CommentReply.vue
Normal file
@@ -0,0 +1,450 @@
|
||||
<template>
|
||||
<div class="comment-main">
|
||||
<AFlex :vertical="false" justify="flex-start" class="comment-header">
|
||||
<span class="comment-header-title">{{ t('comment.comment') }}</span>
|
||||
</AFlex>
|
||||
<!-- 评论输入框 -->
|
||||
<div class="comment">
|
||||
<AFlex :vertical="false">
|
||||
<AFlex :vertical="true">
|
||||
<AAvatar :size="50" shape="circle" src="https://api.multiavatar.com/Starcrasher.svg"/>
|
||||
</AFlex>
|
||||
<AFlex :vertical="true" class="comment-content">
|
||||
<ATextarea :rows="4" class="comment-text" @focus="onFocusHandler"
|
||||
v-model:value="commentContent"
|
||||
:placeholder="commentTextAreaPlaceholder" allow-clear :showCount="false"/>
|
||||
<AFlex :vertical="false" align="center" justify="space-between" class="comment-actions"
|
||||
v-if="showCommentActions">
|
||||
<AFlex :vertical="false" align="center">
|
||||
<AFlex :vertical="false" align="center" class="comment-action-item">
|
||||
<APopover trigger="click" placement="bottom">
|
||||
<template #content>
|
||||
<div style="width: 170px;height: 200px;overflow: auto;">
|
||||
<template v-for="(item) in EMOJI" :key="item">
|
||||
<AButton @click="insertEmoji(item)" type="text" size="large">{{ item }}</AButton>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<AButton type="text" size="small" :icon="h(SmileOutlined)" class="comment-action-icon">
|
||||
{{ t('comment.emoji') }}
|
||||
</AButton>
|
||||
</APopover>
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="center" class="comment-action-item">
|
||||
<AUpload
|
||||
:accept="'image/jpg, image/png, image/jpeg, image/gif, image/svg+xml, image/webp'"
|
||||
name="images"
|
||||
:max-count="5"
|
||||
:multiple="true"
|
||||
method="post"
|
||||
:directory="false"
|
||||
:show-upload-list="false"
|
||||
:custom-request="customUploadRequest"
|
||||
:before-upload="beforeUpload"
|
||||
:disabled="imageList.length >= 5"
|
||||
>
|
||||
<ABadge :count="imageList.length">
|
||||
<AButton type="text" size="small" :icon="h(PictureOutlined)"
|
||||
class="comment-action-icon">
|
||||
{{ t('comment.picture') }}
|
||||
</AButton>
|
||||
</ABadge>
|
||||
</AUpload>
|
||||
<template v-if="imageList.length > 0">
|
||||
<ABadge style="margin-left: 10px;" v-for="(item, index) in imageList" :key="index">
|
||||
<template #count>
|
||||
<CloseCircleOutlined @click="removeBase64Image(index)" style="color: #f5222d"/>
|
||||
</template>
|
||||
<AAvatar shape="square" size="small">
|
||||
<template #icon>
|
||||
<AImage v-if="item" :src="item"/>
|
||||
</template>
|
||||
</AAvatar>
|
||||
</ABadge>
|
||||
</template>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="center">
|
||||
<AButton
|
||||
@click="console.log(commentContent.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' '))"
|
||||
type="primary" size="middle" class="comment-action-btn">{{ t('comment.sendComment') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</div>
|
||||
<ADivider/>
|
||||
<!-- 回复列表 -->
|
||||
<div class="reply">
|
||||
<div class="reply-header">
|
||||
<!-- 评论列表头部 -->
|
||||
<AFlex :vertical="true">
|
||||
<AFlex :vertical="false">
|
||||
<span class="reply-header-title">{{ t('comment.allComments') }}</span>
|
||||
<span class="reply-header-count">123</span>
|
||||
</AFlex>
|
||||
<ASegmented v-model:value="segmentedValue" :options="data" class="reply-header-sort"/>
|
||||
</AFlex>
|
||||
</div>
|
||||
<div class="reply-list">
|
||||
<div class="reply-item">
|
||||
<AFlex :vertical="false">
|
||||
<!-- 评论头像 -->
|
||||
<AFlex :vertical="true" class="reply-avatar">
|
||||
<AAvatar :size="50" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/>
|
||||
</AFlex>
|
||||
<!-- 评论内容 -->
|
||||
<AFlex :vertical="true" class="reply-content">
|
||||
<AFlex :vertical="true">
|
||||
<AFlex :vertical="false" align="flex-start">
|
||||
<span class="reply-name">张立国</span>
|
||||
<a-tag color="cyan" class="reply-tag" size="small">Lv.5</a-tag>
|
||||
<a-tag color="red" class="reply-tag" size="small">UP</a-tag>
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="flex-end" justify="space-between">
|
||||
<AFlex :vertical="false" align="center" justify="space-between">
|
||||
<span class="reply-ip"> 成都市 </span>
|
||||
<span class="reply-ip" style="margin-left: 10px;">IP: 192.168.1.100 </span>
|
||||
</AFlex>
|
||||
<span class="reply-time">{{ new Date().toLocaleString() }}</span>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="center">
|
||||
|
||||
<ACard class="reply-card" :body-style="{padding: '10px'}">
|
||||
<span class="reply-text">
|
||||
床前明月光,疑是地上霜。<br>
|
||||
举头望明月,低头思故乡。
|
||||
</span>
|
||||
<AFlex :vertical="false" justify="space-between" align="center">
|
||||
<!--评论操作按钮 -->
|
||||
<AFlex :vertical="false" align="center" justify="space-between" class="reply-action-item">
|
||||
<AFlex :vertical="false" align="center">
|
||||
<AButton type="text" size="small" :icon="h(LikeOutlined)" class="reply-action-btn">
|
||||
10
|
||||
</AButton>
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="center">
|
||||
<AButton type="text" size="small" :icon="h(DislikeOutlined)" class="reply-action-btn">
|
||||
1
|
||||
</AButton>
|
||||
</AFlex>
|
||||
<AButton type="text" size="small" :icon="h(MessageOutlined)" class="reply-action-btn">
|
||||
11
|
||||
</AButton>
|
||||
<AButton
|
||||
@click="replyInputVisible === true? (replyInputVisible = false) : (replyInputVisible = true) "
|
||||
type="text" size="small" :icon="h(CommentOutlined)"
|
||||
class="reply-action-btn">
|
||||
{{ t('comment.reply') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
<!-- 评论操作系统信息-->
|
||||
<AFlex :vertical="false" align="center" justify="flex-end" class="reply-action-item-right">
|
||||
<AButton type="text" disabled size="small" :icon="h(WindowsOutlined)" class="reply-action-info">
|
||||
windows 10
|
||||
</AButton>
|
||||
<AButton type="text" disabled size="small" :icon="h(ChromeOutlined)" class="reply-action-info">
|
||||
chrome
|
||||
</AButton>
|
||||
<!-- 评论操作按钮 -->
|
||||
<ADropdown trigger="click">
|
||||
<AButton type="text" size="small" :icon="h(EllipsisOutlined)" class="reply-action-btn"
|
||||
@click.prevent>
|
||||
</AButton>
|
||||
<template #overlay>
|
||||
<AMenu>
|
||||
<AMenuItem key="report">
|
||||
<WarningOutlined/>
|
||||
{{ t('comment.report') }}
|
||||
</AMenuItem>
|
||||
<AMenuItem key="copy">
|
||||
<CopyOutlined/>
|
||||
{{ t('comment.copy') }}
|
||||
</AMenuItem>
|
||||
<AMenuItem key="delete">
|
||||
<DeleteOutlined/>
|
||||
{{ t('comment.delete') }}
|
||||
</AMenuItem>
|
||||
</AMenu>
|
||||
</template>
|
||||
</ADropdown>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</ACard>
|
||||
|
||||
</AFlex>
|
||||
<!-- 回复输入框 -->
|
||||
<AFlex :vertical="true" class="reply-input-main" v-show="replyInputVisible">
|
||||
<AFlex :vertical="false" align="center" class="reply-input-header">
|
||||
<span class="reply-input-title">{{ t('comment.reply') + ':' }}</span>
|
||||
<span class="reply-input-author">张立国</span>
|
||||
<AButton @click="replyInputVisible = false" type="dashed" size="small" :icon="h(CloseOutlined )"
|
||||
class="reply-input-cancel">
|
||||
{{ t('comment.cancelReply') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
<!-- 回复头像-->
|
||||
<AFlex :vertical="false" class="reply-input-content">
|
||||
<AFlex :vertical="true" class="reply-input-avatar">
|
||||
<AAvatar :size="40" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/>
|
||||
</AFlex>
|
||||
<!-- 评论输入框 -->
|
||||
<AFlex :vertical="true" class="reply-input-content-text">
|
||||
<ATextarea :rows="3" class="comment-text-reply"
|
||||
:placeholder="commentTextAreaPlaceholder" allow-clear :showCount="false"/>
|
||||
<AFlex :vertical="false" align="center" justify="space-between" class="comment-actions-reply"
|
||||
>
|
||||
<AFlex :vertical="false" align="center">
|
||||
<AFlex :vertical="false" align="center" class="comment-action-item-reply">
|
||||
<AButton type="text" size="small" :icon="h(SmileOutlined)" class="comment-action-icon-reply">
|
||||
{{ t('comment.emoji') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="center" class="comment-action-item-reply">
|
||||
<AButton type="text" size="small" :icon="h(PictureOutlined)"
|
||||
class="comment-action-icon-reply">
|
||||
{{ t('comment.picture') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="center">
|
||||
<AButton type="primary" size="middle" class="comment-action-btn-reply">
|
||||
{{ t('comment.sendComment') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
|
||||
<!-- 子回复列表 -->
|
||||
<AFlex :vertical="false" class="reply-item-child">
|
||||
<AFlex :vertical="true" class="reply-item-child-avatar">
|
||||
<AAvatar :size="40" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/>
|
||||
</AFlex>
|
||||
<AFlex :vertical="true" class="reply-item-child-content">
|
||||
<AFlex :vertical="true">
|
||||
<AFlex :vertical="false" align="center">
|
||||
<span class="reply-name-child">沈建明</span> <span class="reply-at">@张立国</span>
|
||||
<a-tag color="cyan" class="reply-tag-child" size="small">Lv.5</a-tag>
|
||||
<!-- <a-tag color="red" class="reply-tag" size="small">UP</a-tag>-->
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="flex-end" justify="space-between">
|
||||
<AFlex :vertical="false" align="center" justify="space-between">
|
||||
<span class="reply-ip-child"> 成都市 </span>
|
||||
<span class="reply-ip-child" style="margin-left: 10px;">IP: 192.168.1.100 </span>
|
||||
</AFlex>
|
||||
<span class="reply-time-child">{{ new Date().toLocaleString() }}</span>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
<AFlex :vertical="true" align="center">
|
||||
<ACard class="reply-card-child" :body-style="{padding: '10px'}">
|
||||
<span class="reply-text-child">
|
||||
床前明月光,疑是地上霜。<br>
|
||||
举头望明月,低头思故乡。
|
||||
</span>
|
||||
<AFlex :vertical="false" justify="space-between" align="center">
|
||||
<!--评论操作按钮 -->
|
||||
<AFlex :vertical="false" align="center" justify="space-between" class="reply-action-item-child">
|
||||
<AFlex :vertical="false" align="center">
|
||||
<AButton type="text" size="small" :icon="h(LikeOutlined)" class="reply-action-btn-child">
|
||||
10
|
||||
</AButton>
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="center">
|
||||
<AButton type="text" size="small" :icon="h(DislikeOutlined)" class="reply-action-btn-child">
|
||||
1
|
||||
</AButton>
|
||||
</AFlex>
|
||||
<AButton type="text" size="small" :icon="h(MessageOutlined)" class="reply-action-btn-child">
|
||||
11
|
||||
</AButton>
|
||||
<AButton
|
||||
@click="replyInputVisible === true? (replyInputVisible = false) : (replyInputVisible = true) "
|
||||
type="text" size="small" :icon="h(CommentOutlined)"
|
||||
class="reply-action-btn-child">
|
||||
{{ t('comment.reply') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
<!-- 评论操作系统信息-->
|
||||
<AFlex :vertical="false" align="center" justify="flex-end"
|
||||
class="reply-action-item-right-child">
|
||||
<AButton type="text" disabled size="small" :icon="h(WindowsOutlined)"
|
||||
class="reply-action-info-child">
|
||||
windows 10
|
||||
</AButton>
|
||||
<AButton type="text" disabled size="small" :icon="h(ChromeOutlined)"
|
||||
class="reply-action-info-child">
|
||||
chrome
|
||||
</AButton>
|
||||
<!-- 评论操作按钮 -->
|
||||
<ADropdown trigger="click">
|
||||
<AButton type="text" size="small" :icon="h(EllipsisOutlined)" class="reply-action-btn-child"
|
||||
@click.prevent>
|
||||
</AButton>
|
||||
<template #overlay>
|
||||
<AMenu>
|
||||
<AMenuItem key="report">
|
||||
{{ t('comment.report') }}
|
||||
</AMenuItem>
|
||||
<AMenuItem key="copy">
|
||||
{{ t('comment.copy') }}
|
||||
</AMenuItem>
|
||||
<AMenuItem key="delete">
|
||||
{{ t('comment.delete') }}
|
||||
</AMenuItem>
|
||||
</AMenu>
|
||||
</template>
|
||||
</ADropdown>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</ACard>
|
||||
|
||||
<!-- 子评论回复输入框 -->
|
||||
<AFlex :vertical="true" class="reply-input-main-child" v-show="replyInputVisible">
|
||||
<AFlex :vertical="false" align="center" class="reply-input-header-child">
|
||||
<span class="reply-input-title-child">{{ t('comment.reply') + ':' }}</span>
|
||||
<span class="reply-input-author-child">沈建明</span>
|
||||
<AButton @click="replyInputVisible = false" type="dashed" size="small" :icon="h(CloseOutlined )"
|
||||
class="reply-input-cancel-child">
|
||||
{{ t('comment.cancelReply') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
<!-- 回复头像-->
|
||||
<AFlex :vertical="false" class="reply-input-content-child">
|
||||
<AFlex :vertical="true" class="reply-input-avatar-child">
|
||||
<AAvatar :size="40" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/>
|
||||
</AFlex>
|
||||
<!-- 评论输入框 -->
|
||||
<AFlex :vertical="true" class="reply-input-content-text-child">
|
||||
<ATextarea :rows="3" class="comment-text-reply-child"
|
||||
:placeholder="commentTextAreaPlaceholder" allow-clear :showCount="false"/>
|
||||
<AFlex :vertical="false" align="center" justify="space-between"
|
||||
class="comment-actions-reply-child"
|
||||
>
|
||||
<AFlex :vertical="false" align="center">
|
||||
<AFlex :vertical="false" align="center" class="comment-action-item-reply-child">
|
||||
<AButton type="text" size="small" :icon="h(SmileOutlined)"
|
||||
class="comment-action-icon-reply-child">
|
||||
{{ t('comment.emoji') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="center" class="comment-action-item-reply-child">
|
||||
<AButton type="text" size="small" :icon="h(PictureOutlined)"
|
||||
class="comment-action-icon-reply-child">
|
||||
{{ t('comment.picture') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
<AFlex :vertical="false" align="center">
|
||||
<AButton type="primary" size="middle" class="comment-action-btn-reply-child">
|
||||
{{ t('comment.sendComment') }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
|
||||
</AFlex>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {h, reactive, ref} from "vue";
|
||||
import {
|
||||
ChromeOutlined,
|
||||
CloseOutlined,
|
||||
CommentOutlined,
|
||||
DislikeOutlined,
|
||||
EllipsisOutlined,
|
||||
LikeOutlined,
|
||||
MessageOutlined,
|
||||
PictureOutlined,
|
||||
SmileOutlined,
|
||||
WindowsOutlined
|
||||
} from "@ant-design/icons-vue";
|
||||
import EMOJI from "@/constant/emoji.ts";
|
||||
import imageCompression from "browser-image-compression";
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
const {t} = useI18n();
|
||||
const showCommentActions = ref<boolean>(false);
|
||||
const commentTextAreaPlaceholder = ref<string>(t('comment.placeholder'));
|
||||
const data = reactive([t('comment.latest'), t('comment.hot')]);
|
||||
const segmentedValue = ref<string>(data[0]);
|
||||
const replyInputVisible = ref<boolean>(false);
|
||||
const commentContent = ref<string>("");
|
||||
const fileList = ref<any[]>([]);
|
||||
const imageList = ref<any[]>([]);
|
||||
|
||||
/**
|
||||
* 聚焦事件
|
||||
*/
|
||||
async function onFocusHandler() {
|
||||
showCommentActions.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入表情
|
||||
* @param emoji
|
||||
*/
|
||||
async function insertEmoji(emoji: string) {
|
||||
commentContent.value += emoji;
|
||||
}
|
||||
|
||||
// 压缩图片配置
|
||||
const options = {
|
||||
maxSizeMB: 0.4,
|
||||
maxWidthOrHeight: 750,
|
||||
maxIteration: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传文件前置
|
||||
* @param file
|
||||
*/
|
||||
async function beforeUpload(file: any) {
|
||||
if (!window.FileReader) return false; // 判断是否支持FileReader
|
||||
const compressedFile = await imageCompression(file, options);
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(compressedFile); // 文件转换
|
||||
reader.onloadend = async function () {
|
||||
if (fileList.value.length >= 5) {
|
||||
message.error("最多只能上传5张图片");
|
||||
return false;
|
||||
}
|
||||
fileList.value.push(reader.result);
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义上传图片请求
|
||||
*/
|
||||
async function customUploadRequest() {
|
||||
imageList.value = fileList.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除图片
|
||||
* @param index
|
||||
*/
|
||||
async function removeBase64Image(index: number) {
|
||||
fileList.value.splice(index, 1);
|
||||
imageList.value.splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
<style src="./index.scss" lang="scss" scoped>
|
||||
|
||||
</style>
|
373
src/components/CommentReply/index.scss
Normal file
373
src/components/CommentReply/index.scss
Normal file
@@ -0,0 +1,373 @@
|
||||
.comment-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #ccc;
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
padding: 50px;
|
||||
|
||||
.comment-header-title {
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
margin-left: 20px;
|
||||
|
||||
.comment-text {
|
||||
width: 600px;
|
||||
}
|
||||
.comment-actions {
|
||||
margin-top: 10px;
|
||||
|
||||
.comment-action-item {
|
||||
cursor: pointer;
|
||||
color: #767779;
|
||||
|
||||
|
||||
.comment-action-icon {
|
||||
font-size: 14px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.comment-action-icon:hover {
|
||||
color: #08a327;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reply-header {
|
||||
.reply-header-title {
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.reply-header-count {
|
||||
font-size: 12px;
|
||||
color: #767779;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.reply-header-sort {
|
||||
margin-top: 10px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-list {
|
||||
margin-top: 30px;
|
||||
|
||||
.reply-content {
|
||||
margin-left: 20px;
|
||||
|
||||
.reply-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-ip {
|
||||
font-size: 12px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.reply-time {
|
||||
font-size: 12px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.reply-card {
|
||||
width: 600px;
|
||||
//margin-top: 5px;
|
||||
|
||||
.reply-action-item {
|
||||
margin-top: 10px;
|
||||
width: 200px;
|
||||
|
||||
.reply-action-btn {
|
||||
font-size: 13px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.reply-action-icon {
|
||||
font-size: 14px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.reply-action-icon:hover {
|
||||
font-size: 14px;
|
||||
color: #08a327;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.reply-action-btn:hover {
|
||||
font-size: 13px;
|
||||
color: #08a327;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.reply-action-icon-number {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: #767779;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-action-item-right {
|
||||
margin-top: 10px;
|
||||
|
||||
.reply-action-btn {
|
||||
font-size: 15px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-action-btn:hover {
|
||||
font-size: 15px;
|
||||
color: #08a327;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-action-info {
|
||||
font-size: 12px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reply-input-main {
|
||||
margin-top: 5px;
|
||||
|
||||
.reply-input-title {
|
||||
font-size: 13px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.reply-input-author {
|
||||
font-size: 13px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.reply-input-cancel {
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-input-content {
|
||||
margin-top: 10px;
|
||||
|
||||
.reply-input-content-text {
|
||||
margin-left: 10px;
|
||||
|
||||
.comment-text-reply {
|
||||
width: 550px;
|
||||
}
|
||||
|
||||
.comment-actions-reply {
|
||||
margin-top: 5px;
|
||||
|
||||
.comment-action-item-reply {
|
||||
cursor: pointer;
|
||||
color: #767779;
|
||||
|
||||
.comment-action-icon-reply {
|
||||
font-size: 14px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.comment-action-icon-reply:hover {
|
||||
color: #08a327;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.reply-item-child {
|
||||
margin-top: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
|
||||
.reply-item-child-content {
|
||||
margin-left: 10px;
|
||||
|
||||
.reply-at {
|
||||
font-size: 14px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-at:hover {
|
||||
color: #25a9e3;
|
||||
}
|
||||
|
||||
.reply-name-child {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-tag-child {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-ip-child {
|
||||
font-size: 12px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.reply-time-child {
|
||||
font-size: 12px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.reply-card-child {
|
||||
width: 530px;
|
||||
|
||||
.reply-action-item-child {
|
||||
margin-top: 10px;
|
||||
width: 200px;
|
||||
|
||||
.reply-action-btn-child {
|
||||
font-size: 13px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-action-icon-child {
|
||||
font-size: 14px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.reply-action-icon-child:hover {
|
||||
font-size: 14px;
|
||||
color: #08a327;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.reply-action-btn-child:hover {
|
||||
font-size: 13px;
|
||||
color: #08a327;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.reply-action-icon-number-child {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.reply-action-item-right-child {
|
||||
margin-top: 10px;
|
||||
|
||||
.reply-action-btn-child {
|
||||
font-size: 15px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-action-btn-child:hover {
|
||||
font-size: 15px;
|
||||
color: #08a327;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-action-info-child {
|
||||
font-size: 12px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reply-input-main-child {
|
||||
margin-top: 5px;
|
||||
|
||||
.reply-input-title-child {
|
||||
font-size: 13px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.reply-input-author-child {
|
||||
font-size: 13px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.reply-input-cancel-child {
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
color: #767779;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reply-input-content-child {
|
||||
margin-top: 10px;
|
||||
|
||||
.reply-input-content-text-child {
|
||||
margin-left: 10px;
|
||||
|
||||
.comment-text-reply-child {
|
||||
width: 480px;
|
||||
}
|
||||
|
||||
.comment-actions-reply-child {
|
||||
margin-top: 5px;
|
||||
|
||||
.comment-action-item-reply-child {
|
||||
cursor: pointer;
|
||||
color: #767779;
|
||||
|
||||
.comment-action-icon-reply-child {
|
||||
font-size: 14px;
|
||||
color: #767779;
|
||||
}
|
||||
|
||||
.comment-action-icon-reply-child:hover {
|
||||
color: #08a327;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
123
src/constant/emoji.ts
Normal file
123
src/constant/emoji.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
const EMOJI = {
|
||||
1: "😀",
|
||||
2: "😁",
|
||||
3: "😂",
|
||||
4: "🤣",
|
||||
5: "😃",
|
||||
6: "😄",
|
||||
7: "😅",
|
||||
8: "😆",
|
||||
9: "😉",
|
||||
10: "😊",
|
||||
11: "😋",
|
||||
12: "😎",
|
||||
13: "😍",
|
||||
14: "😘",
|
||||
15: "🥰",
|
||||
16: "😗",
|
||||
17: "😙",
|
||||
18: "🥲",
|
||||
19: "😚",
|
||||
21: "🙂",
|
||||
22: "🤗",
|
||||
23: "🤩",
|
||||
24: "🤔",
|
||||
25: "🫡",
|
||||
26: "🤨",
|
||||
27: "😐",
|
||||
28: "😑",
|
||||
29: "😶",
|
||||
30: "🫥",
|
||||
31: "😶🌫️",
|
||||
32: "🙄",
|
||||
33: "😏",
|
||||
34: "😣",
|
||||
35: "😥",
|
||||
36: "😮",
|
||||
37: "🤐",
|
||||
38: "😯",
|
||||
39: "😪",
|
||||
40: "😫",
|
||||
41: "🥱",
|
||||
42: "😴",
|
||||
43: "😌",
|
||||
44: "😛",
|
||||
45: "😜",
|
||||
46: "😝",
|
||||
47: "🤤",
|
||||
48: "😒",
|
||||
49: "😓",
|
||||
50: "😔",
|
||||
51: "😕",
|
||||
52: "🫤",
|
||||
53: "🙃",
|
||||
54: "🫠",
|
||||
55: "🤑",
|
||||
56: "😲",
|
||||
57: "☹️",
|
||||
58: "🙁",
|
||||
59: "😖",
|
||||
60: "😞",
|
||||
61: "😧",
|
||||
62: "😦",
|
||||
63: "😭",
|
||||
64: "😢",
|
||||
65: "😤",
|
||||
66: "😟",
|
||||
67: "😨",
|
||||
68: "😩",
|
||||
69: "🤯",
|
||||
70: "😬",
|
||||
71: "😮💨",
|
||||
72: "😰",
|
||||
73: "😵",
|
||||
74: "🤪",
|
||||
75: "😳",
|
||||
76: "🥶",
|
||||
77: "🥵",
|
||||
78: "😱",
|
||||
79: "😵💫",
|
||||
80: "🥴",
|
||||
81: "😠",
|
||||
82: "😡",
|
||||
83: "🤬",
|
||||
84: "😷",
|
||||
85: "😇",
|
||||
86: "🤧",
|
||||
87: "🤮",
|
||||
88: "🤢",
|
||||
89: "🤕",
|
||||
90: "🤒",
|
||||
91: "🥳",
|
||||
92: "🥸",
|
||||
93: "🥺",
|
||||
94: "🥹",
|
||||
95: "🤠",
|
||||
96: "🤡",
|
||||
97: "🤭",
|
||||
98: "🤫",
|
||||
99: "🙂↕️",
|
||||
100: "🙂️",
|
||||
101: "🤥",
|
||||
102: "🫢",
|
||||
103: "🫣",
|
||||
104: "🧐",
|
||||
105: "🤓",
|
||||
106: "😈",
|
||||
107: "👿",
|
||||
108: "👽",
|
||||
109: "👻",
|
||||
110: "☠️",
|
||||
111: "💀",
|
||||
112: "👺",
|
||||
113: "👹",
|
||||
114: "👾",
|
||||
115: "🤖",
|
||||
116: "💩",
|
||||
117: "😺",
|
||||
118: "😸",
|
||||
119: "😹",
|
||||
120: "🫨",
|
||||
};
|
||||
|
||||
export default EMOJI;
|
30
src/directives/v-click-outside.ts
Normal file
30
src/directives/v-click-outside.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {ObjectDirective} from 'vue';
|
||||
|
||||
// 定义指令对象
|
||||
const clickOutside: ObjectDirective = {
|
||||
mounted(el, binding) {
|
||||
// 获取指令绑定的回调函数
|
||||
const handler = binding.value;
|
||||
|
||||
// 绑定click事件监听器
|
||||
const listener = (event: Event) => {
|
||||
// 判断点击位置是否在元素之外
|
||||
if (!el.contains(event.target as HTMLElement)) {
|
||||
// 若在元素之外,触发回调函数
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
// 将事件监听器添加到文档中
|
||||
document.addEventListener('click', listener);
|
||||
|
||||
// 指令销毁时,移除事件监听器
|
||||
el.__vueClickOutside__ = listener;
|
||||
},
|
||||
unmounted(el) {
|
||||
// 移除事件监听器
|
||||
document.removeEventListener('click', el.__vueClickOutside__);
|
||||
},
|
||||
};
|
||||
|
||||
export default clickOutside;
|
@@ -79,5 +79,20 @@ export default {
|
||||
pleaseLogin: "please login first",
|
||||
loginExpiredDesc: "this account has been logged in elsewhere, please login again",
|
||||
|
||||
},
|
||||
comment:{
|
||||
comment: 'Comment',
|
||||
sendComment: 'Send Comment',
|
||||
emoji: 'Emoji',
|
||||
picture: 'Picture',
|
||||
placeholder: 'Enter a comment (enter line break, ctrl + enter send)',
|
||||
allComments: 'All Comments',
|
||||
latest: 'Latest',
|
||||
hot: 'Hot',
|
||||
reply: 'Reply',
|
||||
report: 'Report',
|
||||
delete: 'Delete',
|
||||
copy: 'Copy',
|
||||
cancelReply: 'Cancel Reply',
|
||||
}
|
||||
};
|
||||
|
@@ -78,5 +78,21 @@ export default {
|
||||
loginExpired: "登录已过期!",
|
||||
pleaseLogin: "请先登录!",
|
||||
loginExpiredDesc: "该账号在其他地方登录,请刷新后重新登录!"
|
||||
},
|
||||
comment: {
|
||||
comment: '评论',
|
||||
sendComment: '发表评论',
|
||||
emoji: '表情',
|
||||
picture: '图片',
|
||||
placeholder: '输入评论(Enter换行,Ctrl+Enter发送)',
|
||||
allComments: '全部评论',
|
||||
latest: '最新',
|
||||
hot: '最热',
|
||||
reply: '回复',
|
||||
report: '举报',
|
||||
delete: '删除',
|
||||
copy: '复制',
|
||||
cancelReply: '取消',
|
||||
}
|
||||
|
||||
};
|
||||
|
@@ -8,11 +8,13 @@ import "go-captcha-vue/dist/style.css";
|
||||
import GoCaptcha from "go-captcha-vue";
|
||||
import {createPinia, Pinia} from "pinia";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import clickOutside from '@/directives/v-click-outside.ts';
|
||||
|
||||
const pinia: Pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
const app = createApp(App);
|
||||
app.directive('click-outside', clickOutside);
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
|
1
src/types/comment.d.ts
vendored
Normal file
1
src/types/comment.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
@@ -6,7 +6,7 @@ import {localforageStorageAdapter} from "@/utils/alova/adapter/localforageStorag
|
||||
import {createServerTokenAuthentication} from "alova/client";
|
||||
import {AxiosError, AxiosResponse} from "axios";
|
||||
import {handleCode} from "@/utils/errorCode/errorCodeHandler.ts";
|
||||
import {message} from "ant-design-vue";
|
||||
import {message, Modal} from "ant-design-vue";
|
||||
import i18n from "@/locales";
|
||||
import {axiosRequestAdapter} from "@alova/adapter-axios";
|
||||
import {refreshToken} from "@/api/user";
|
||||
@@ -68,14 +68,16 @@ export const service = createAlova({
|
||||
const {code} = response.data;
|
||||
if (code === 403) {
|
||||
localStorage.removeItem('user');
|
||||
message.open({
|
||||
type: 'error',
|
||||
content: i18n.global.t('error.loginExpired'),
|
||||
duration: 2,
|
||||
Modal.warning({
|
||||
title: i18n.global.t('error.loginExpired'),
|
||||
content: i18n.global.t('error.authTokenExpired'),
|
||||
onOk() {
|
||||
window.location.href = '/login';
|
||||
},
|
||||
onCancel() {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
return Promise.reject(response.data);
|
||||
}
|
||||
return response.data;
|
||||
|
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<div class="login-main">
|
||||
<div class="login-left">
|
||||
<BoxDog/>
|
||||
</div>
|
||||
<div class="login-right">
|
||||
<a-spin :spinning="loginLoading" tip="Login..." size="large">
|
||||
<div class="login-main">
|
||||
<div class="login-left">
|
||||
<BoxDog/>
|
||||
</div>
|
||||
<div class="login-right">
|
||||
|
||||
<span class="login-right-title">
|
||||
<img src="@/assets/images/logo-schisandra.png" @click="()=>{
|
||||
@@ -11,174 +12,178 @@
|
||||
}" style="width: 30px; height: 30px;cursor: pointer;" alt="">
|
||||
{{ t("login.title") }}
|
||||
</span>
|
||||
<ACard class="login-card" bordered :hoverable="false">
|
||||
<ATabs :centered="false" size="large">
|
||||
<!-- 短信登录 -->
|
||||
<ATabPane key="phoneLogin">
|
||||
<template #tab>
|
||||
<ACard class="login-card" bordered :hoverable="false">
|
||||
<ATabs :centered="false" size="large">
|
||||
<!-- 短信登录 -->
|
||||
<ATabPane key="phoneLogin">
|
||||
<template #tab>
|
||||
<span class="login-tab-span">
|
||||
{{ t("login.phoneLogin") }}
|
||||
</span>
|
||||
</template>
|
||||
<AForm ref="phoneLoginFormRef" :rules="rules" :model="phoneLoginForm">
|
||||
<AFormItem
|
||||
class="login-form-item"
|
||||
name="phone">
|
||||
<span class="login-card-span">{{ t("login.phone") }}</span>
|
||||
<AInput v-model:value="phoneLoginForm.phone" class="login-form-input" size="large"
|
||||
:placeholder=phoneValidate allow-clear
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<TabletOutlined/>
|
||||
</template>
|
||||
</AInput>
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
class="login-form-item"
|
||||
name="captcha">
|
||||
<AFlex :vertical="true">
|
||||
<span class="login-card-span">{{ t("login.phoneCaptcha") }}</span>
|
||||
<AFlex :vertical="false" align="center" justify="center">
|
||||
<AInput v-model:value="phoneLoginForm.captcha" size="large" :placeholder=captchaValidate
|
||||
allow-clear>
|
||||
<template #prefix>
|
||||
<SafetyOutlined/>
|
||||
</template>
|
||||
</AInput>
|
||||
<AButton v-if="!state.showCountDown" @click="sendCaptchaThrottle" style="margin-left: 10px"
|
||||
size="large">{{
|
||||
t("login.sendCaptcha")
|
||||
}}
|
||||
</AButton>
|
||||
<AButton v-if="state.showCountDown" disabled style="margin-left: 10px" size="large">{{
|
||||
state.countDownTime
|
||||
}}s{{ t("login.reSendCaptcha") }}
|
||||
</AButton>
|
||||
</template>
|
||||
<AForm ref="phoneLoginFormRef" :rules="rules" :model="phoneLoginForm">
|
||||
<AFormItem
|
||||
class="login-form-item"
|
||||
name="phone">
|
||||
<span class="login-card-span">{{ t("login.phone") }}</span>
|
||||
<AInput v-model:value="phoneLoginForm.phone" class="login-form-input" size="large"
|
||||
:placeholder=phoneValidate allow-clear
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<TabletOutlined/>
|
||||
</template>
|
||||
</AInput>
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
class="login-form-item"
|
||||
name="captcha">
|
||||
<AFlex :vertical="true">
|
||||
<span class="login-card-span">{{ t("login.phoneCaptcha") }}</span>
|
||||
<AFlex :vertical="false" align="center" justify="center">
|
||||
<AInput v-model:value="phoneLoginForm.captcha" size="large" :placeholder=captchaValidate
|
||||
allow-clear>
|
||||
<template #prefix>
|
||||
<SafetyOutlined/>
|
||||
</template>
|
||||
</AInput>
|
||||
<AButton v-if="!state.showCountDown" @click="sendCaptchaThrottle" style="margin-left: 10px"
|
||||
size="large">{{
|
||||
t("login.sendCaptcha")
|
||||
}}
|
||||
</AButton>
|
||||
<AButton v-if="state.showCountDown" disabled style="margin-left: 10px" size="large">
|
||||
{{
|
||||
state.countDownTime
|
||||
}}s{{ t("login.reSendCaptcha") }}
|
||||
</AButton>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
|
||||
</AFormItem>
|
||||
<AFormItem id="phone_login_auto" name="auto_login">
|
||||
<AFlex :vertical="false" justify="space-between">
|
||||
<ACheckbox id="phone_login_auto_checkbox" v-model:checked="phoneLoginForm.auto_login">
|
||||
{{ t("login.autoLogin") }}
|
||||
</ACheckbox>
|
||||
</AFlex>
|
||||
</AFormItem>
|
||||
<AFormItem id="phone_login_auto" name="auto_login">
|
||||
<AFlex :vertical="false" justify="space-between">
|
||||
<ACheckbox id="phone_login_auto_checkbox" v-model:checked="phoneLoginForm.auto_login">
|
||||
{{ t("login.autoLogin") }}
|
||||
</ACheckbox>
|
||||
</AFlex>
|
||||
|
||||
</AFormItem>
|
||||
<AFormItem>
|
||||
<AButton @click="phoneLoginSubmitDebounce" type="primary" size="large" class="login-form-button">
|
||||
{{ t("login.loginAndRegister") }}
|
||||
</AButton>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
<LoginFooter/>
|
||||
</ATabPane>
|
||||
<!-- 账号登录 -->
|
||||
<ATabPane key="accountLogin">
|
||||
<template #tab>
|
||||
</AFormItem>
|
||||
<AFormItem>
|
||||
<AButton @click="phoneLoginSubmitDebounce" type="primary" size="large" class="login-form-button">
|
||||
{{ t("login.loginAndRegister") }}
|
||||
</AButton>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
<LoginFooter/>
|
||||
</ATabPane>
|
||||
<!-- 账号登录 -->
|
||||
<ATabPane key="accountLogin">
|
||||
<template #tab>
|
||||
<span class="login-tab-span">
|
||||
{{ t("login.accountLogin") }}
|
||||
</span>
|
||||
</template>
|
||||
<AForm ref="accountLoginFormRef" :rules="rules" :model="accountLoginForm" autocomplete="off">
|
||||
<AFormItem
|
||||
class="login-form-item"
|
||||
name="account">
|
||||
<span class="login-card-span">{{ t("login.account") }}</span>
|
||||
<AInput v-model:value="accountLoginForm.account" class="login-form-input" size="large"
|
||||
:placeholder=accountValidate allow-clear autocomplete="off">
|
||||
<template #prefix>
|
||||
<user-outlined/>
|
||||
</template>
|
||||
</AInput>
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
class="login-form-item"
|
||||
name="password">
|
||||
<AFlex :vertical="true">
|
||||
<span class="login-card-span">{{ t("login.password") }}</span>
|
||||
<AInputPassword v-model:value="accountLoginForm.password" class="login-form-input" size="large"
|
||||
:placeholder=passwordValidate allow-clear autocomplete="off">
|
||||
</template>
|
||||
<AForm ref="accountLoginFormRef" :rules="rules" :model="accountLoginForm" autocomplete="off">
|
||||
<AFormItem
|
||||
class="login-form-item"
|
||||
name="account">
|
||||
<span class="login-card-span">{{ t("login.account") }}</span>
|
||||
<AInput v-model:value="accountLoginForm.account" class="login-form-input" size="large"
|
||||
:placeholder=accountValidate allow-clear autocomplete="off">
|
||||
<template #prefix>
|
||||
<SafetyOutlined/>
|
||||
<user-outlined/>
|
||||
</template>
|
||||
</AInputPassword>
|
||||
</AFlex>
|
||||
</AFormItem>
|
||||
<AFormItem id="account_login_auto" name="auto_login">
|
||||
<AFlex :vertical="false" justify="space-between">
|
||||
<ACheckbox id="account_login_auto_checkbox" v-model:checked="accountLoginForm.auto_login">
|
||||
{{ t("login.autoLogin") }}
|
||||
</ACheckbox>
|
||||
<a @click="()=>{
|
||||
</AInput>
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
class="login-form-item"
|
||||
name="password">
|
||||
<AFlex :vertical="true">
|
||||
<span class="login-card-span">{{ t("login.password") }}</span>
|
||||
<AInputPassword v-model:value="accountLoginForm.password" class="login-form-input" size="large"
|
||||
:placeholder=passwordValidate allow-clear autocomplete="off">
|
||||
<template #prefix>
|
||||
<SafetyOutlined/>
|
||||
</template>
|
||||
</AInputPassword>
|
||||
</AFlex>
|
||||
</AFormItem>
|
||||
<AFormItem id="account_login_auto" name="auto_login">
|
||||
<AFlex :vertical="false" justify="space-between">
|
||||
<ACheckbox id="account_login_auto_checkbox" v-model:checked="accountLoginForm.auto_login">
|
||||
{{ t("login.autoLogin") }}
|
||||
</ACheckbox>
|
||||
<a @click="()=>{
|
||||
router.push('/resetpass')
|
||||
}">{{ t("login.forgotPassword") }}</a>
|
||||
</AFlex>
|
||||
</AFlex>
|
||||
|
||||
</AFormItem>
|
||||
<AFormItem>
|
||||
<AButton @click="accountLoginSubmitDebounce" type="primary" size="large" class="login-form-button">
|
||||
{{
|
||||
t("login.login")
|
||||
}}
|
||||
</AButton>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
<LoginFooter/>
|
||||
</ATabPane>
|
||||
</ATabs>
|
||||
<ATooltip placement="left">
|
||||
<template #title>
|
||||
<span>{{ t("login.qrLogin") }}</span>
|
||||
</template>
|
||||
<div @click="()=>{
|
||||
</AFormItem>
|
||||
<AFormItem>
|
||||
<AButton @click="accountLoginSubmitDebounce" type="primary" size="large" class="login-form-button">
|
||||
{{
|
||||
t("login.login")
|
||||
}}
|
||||
</AButton>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
<LoginFooter/>
|
||||
</ATabPane>
|
||||
</ATabs>
|
||||
<ATooltip placement="left">
|
||||
<template #title>
|
||||
<span>{{ t("login.qrLogin") }}</span>
|
||||
</template>
|
||||
<div @click="()=>{
|
||||
router.push('/qrlogin');
|
||||
}" class="login-right-qrcode"/>
|
||||
</ATooltip>
|
||||
</ACard>
|
||||
</div>
|
||||
<div v-if="showPhoneRotateCaptcha" class="mask">
|
||||
<!-- 滑动验证码 -->
|
||||
<gocaptcha-rotate
|
||||
class="gocaptcha-rotate"
|
||||
v-if="showPhoneRotateCaptcha"
|
||||
:data="captchaData"
|
||||
:config="{
|
||||
</ATooltip>
|
||||
</ACard>
|
||||
</div>
|
||||
<AModal v-model:open="showPhoneRotateCaptcha" :footer="null" :closable="false" width="375" :centered="true"
|
||||
:maskClosable="false" bodyStyle="padding: 0">
|
||||
<!-- 滑动验证码 -->
|
||||
<gocaptcha-rotate
|
||||
class="gocaptcha-rotate"
|
||||
v-if="showPhoneRotateCaptcha"
|
||||
:data="captchaData"
|
||||
:config="{
|
||||
title: t('login.rotateCaptchaTitle'),
|
||||
}"
|
||||
:events="phoneLoginRotateEvent"
|
||||
/>
|
||||
</AModal>
|
||||
<AModal v-model:open="showAccountRotateCaptcha" :footer="null" :closable="false" width="375" :centered="true"
|
||||
:maskClosable="false" bodyStyle="padding: 0">
|
||||
<!-- 滑动验证码 -->
|
||||
<gocaptcha-rotate
|
||||
class="gocaptcha-rotate"
|
||||
v-if="showAccountRotateCaptcha"
|
||||
:data="captchaData"
|
||||
:config="{
|
||||
title: t('login.rotateCaptchaTitle'),
|
||||
}"
|
||||
:events="phoneLoginRotateEvent"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showAccountRotateCaptcha" class="mask">
|
||||
<!-- 滑动验证码 -->
|
||||
<gocaptcha-rotate
|
||||
class="gocaptcha-rotate"
|
||||
v-if="showAccountRotateCaptcha"
|
||||
:data="captchaData"
|
||||
:config="{
|
||||
title: t('login.rotateCaptchaTitle'),
|
||||
}"
|
||||
:events="accountLoginRotateEvent"
|
||||
/>
|
||||
</div>
|
||||
:events="accountLoginRotateEvent"
|
||||
/>
|
||||
</AModal>
|
||||
|
||||
<div class="area">
|
||||
<ul class="circles">
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
<div class="area">
|
||||
<ul class="circles">
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {Rule} from "ant-design-vue/lib/form";
|
||||
@@ -202,6 +207,7 @@ const showPhoneRotateCaptcha = ref<boolean>(false);
|
||||
const showAccountRotateCaptcha = ref<boolean>(false);
|
||||
const captchaData = reactive({angle: 0, image: "", thumb: "", key: ""});
|
||||
const captchaErrorCount = ref<number>(0);
|
||||
const loginLoading = ref<boolean>(false);
|
||||
const phoneLoginRotateEvent = {
|
||||
confirm: (angle: number) => {
|
||||
checkPhoneLoginCaptchaDebounce(angle);
|
||||
@@ -360,6 +366,7 @@ const phoneLoginSubmitDebounce = useDebounceFn(phoneLoginSubmit, 1000);
|
||||
* 手机登录提交
|
||||
*/
|
||||
async function phoneLoginSubmit() {
|
||||
loginLoading.value = true;
|
||||
phoneLoginFormRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
@@ -372,10 +379,12 @@ async function phoneLoginSubmit() {
|
||||
userStore.user.refreshToken = refresh_token;
|
||||
userStore.user.expiresAt = expires_at;
|
||||
message.success(t('login.loginSuccess'));
|
||||
loginLoading.value = false;
|
||||
setTimeout(() => {
|
||||
router.push('/main');
|
||||
}, 1000);
|
||||
} else {
|
||||
loginLoading.value = false;
|
||||
message.error(res.message);
|
||||
}
|
||||
})
|
||||
@@ -459,6 +468,7 @@ async function checkAccountLoginCaptcha(angle: number) {
|
||||
const result: any = await checkRotatedCaptcha(angle, captchaData.key);
|
||||
if (result.code === 0 && result.success) {
|
||||
showAccountRotateCaptcha.value = false;
|
||||
loginLoading.value = true;
|
||||
const res: any = await accountLoginApi(accountLoginForm);
|
||||
if (res.code === 0 && res.success) {
|
||||
const userStore = useStore().user;
|
||||
@@ -468,10 +478,12 @@ async function checkAccountLoginCaptcha(angle: number) {
|
||||
userStore.user.refreshToken = refresh_token;
|
||||
userStore.user.expiresAt = expires_at;
|
||||
message.success(t('login.loginSuccess'));
|
||||
loginLoading.value = false;
|
||||
setTimeout(() => {
|
||||
router.push('/main');
|
||||
}, 1000);
|
||||
} else {
|
||||
loginLoading.value = false;
|
||||
message.error(t('login.loginError'));
|
||||
}
|
||||
} else if (result.code === 1011) {
|
||||
|
@@ -87,26 +87,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.gocaptcha-rotate {
|
||||
width: 330px;
|
||||
height: 350px;
|
||||
position: absolute;
|
||||
border-radius: 15px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.mask { /* 遮罩层的写法 */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
|
||||
// 背景图动画
|
||||
.area {
|
||||
background: #b9f187;
|
||||
background: -webkit-linear-gradient(to left, #b9f187, #90d952, #70c13a, #52a82e) !important;
|
||||
|
@@ -3,12 +3,14 @@
|
||||
<h1>Welcome to Main Page</h1>
|
||||
<AButton @click="handleClick">获取登录用户角色</AButton>
|
||||
{{ data }}
|
||||
<CommentReply/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {useRequest} from "alova/client";
|
||||
import {getUserPermissions} from "@/api/user";
|
||||
import useStore from "@/store";
|
||||
import CommentReply from "@/components/CommentReply/CommentReply.vue";
|
||||
|
||||
const {data, send} = useRequest(getUserPermissions, {
|
||||
immediate: false
|
||||
|
12
yarn.lock
12
yarn.lock
@@ -1076,6 +1076,13 @@ brorand@^1.0.1, brorand@^1.1.0:
|
||||
resolved "https://registry.npmmirror.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||
integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==
|
||||
|
||||
browser-image-compression@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmmirror.com/browser-image-compression/-/browser-image-compression-2.0.2.tgz#4d5ef8882e9e471d6d923715ceb9034499d14eaa"
|
||||
integrity sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==
|
||||
dependencies:
|
||||
uzip "0.20201231.0"
|
||||
|
||||
browser-resolve@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmmirror.com/browser-resolve/-/browser-resolve-2.0.0.tgz#99b7304cb392f8d73dba741bb2d7da28c6d7842b"
|
||||
@@ -3370,6 +3377,11 @@ util@^0.12.4, util@^0.12.5:
|
||||
is-typed-array "^1.1.3"
|
||||
which-typed-array "^1.1.2"
|
||||
|
||||
uzip@0.20201231.0:
|
||||
version "0.20201231.0"
|
||||
resolved "https://registry.npmmirror.com/uzip/-/uzip-0.20201231.0.tgz#9e64b065b9a8ebf26eb7583fe8e77e1d9a15ed14"
|
||||
integrity sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==
|
||||
|
||||
vite-plugin-compression@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.npmmirror.com/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz#a75b0d8f48357ebb377b65016da9f20885ef39b6"
|
||||
|
Reference in New Issue
Block a user