Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/Aida_Purchaser_Front
This commit is contained in:
@@ -107,13 +107,42 @@ body,
|
||||
--mosaic-bg-size: 1rem;
|
||||
--mosaic-bg-color1: #efefef;
|
||||
--mosaic-bg-color2: #fff;
|
||||
background-image: repeating-conic-gradient(var(--mosaic-bg-color1) 0% 25%, var(--mosaic-bg-color2) 0% 50%);
|
||||
background-image: repeating-conic-gradient(
|
||||
var(--mosaic-bg-color1) 0% 25%,
|
||||
var(--mosaic-bg-color2) 0% 50%
|
||||
);
|
||||
background-repeat: repeat;
|
||||
background-position: 50% 50%;
|
||||
background-size: var(--mosaic-bg-size) var(--mosaic-bg-size);
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.flex-center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
.space-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
button[custom],
|
||||
button[custom="white"] {
|
||||
button[custom='white'] {
|
||||
min-width: 19.4rem;
|
||||
height: 5rem;
|
||||
padding: 0 1rem;
|
||||
@@ -126,11 +155,11 @@ button[custom="white"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
button[custom]:active,
|
||||
button[custom="white"]:active {
|
||||
button[custom='white']:active {
|
||||
background: var(--button-click-bgcolor, #e4e4e4);
|
||||
color: var(--button-click-color, #232323);
|
||||
}
|
||||
button[custom="black"] {
|
||||
button[custom='black'] {
|
||||
--button-bgcolor: #232323;
|
||||
--button-color: #fff;
|
||||
--button-click-bgcolor: #333;
|
||||
|
||||
5
src/assets/icons/ThumbEdit.svg
Normal file
5
src/assets/icons/ThumbEdit.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="15" r="14.5" fill="#232323" stroke="white"/>
|
||||
<path d="M8.18359 19.5693V22.3285H11.8137C12.056 22.3285 12.2879 22.2298 12.4559 22.055L22.7247 11.3755C23.061 11.0257 23.0556 10.4711 22.7125 10.128L20.4083 7.82384C20.0557 7.47124 19.4824 7.47665 19.1365 7.83584L8.43278 18.9513C8.27291 19.1173 8.18359 19.3388 8.18359 19.5693Z" stroke="white" stroke-width="0.89098" stroke-linecap="round"/>
|
||||
<path d="M17.9883 8.96387L21.5522 12.5278" stroke="white" stroke-width="0.89098" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 641 B |
3
src/assets/icons/dui.svg
Normal file
3
src/assets/icons/dui.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="14" viewBox="0 0 20 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.7269 0.156104C19.5188 -0.0520347 19.1025 -0.0520347 18.8944 0.156104L7.13454 11.9159C6.97843 12.0721 6.66622 12.0721 6.51012 11.9159L0.994441 6.40027C0.786302 6.19213 0.370025 6.19213 0.161886 6.40027C-0.0462532 6.60841 0.00578167 6.66044 0.00578167 6.81655C0.00578167 6.97265 0.0578162 7.12876 0.161886 7.23283L6.40605 13.477C6.51012 13.5811 6.66622 13.6331 6.82233 13.6331C6.97843 13.6331 7.13454 13.5811 7.23861 13.477L19.7269 0.98866C19.831 0.88459 19.883 0.728486 19.883 0.572382C19.883 0.416278 19.831 0.260174 19.7269 0.156104Z" fill="#232323"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 669 B |
5
src/assets/icons/eye.svg
Normal file
5
src/assets/icons/eye.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="19" height="13" viewBox="0 0 19 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.5 6.49998C3.33333 1.97224 10.9 -4.36659 18.5 6.49998" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.5 6.49998C15.6667 11.0277 8.1 17.3666 0.5 6.49998" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9.5" cy="6.49998" r="2.5" stroke="#232323"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 432 B |
BIN
src/assets/images/edit.png
Normal file
BIN
src/assets/images/edit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 928 B |
@@ -7,9 +7,11 @@ export default {
|
||||
name: 'Name',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
passwordConfirmation: 'Password Confirmation',
|
||||
enterName: 'Enter your name',
|
||||
enterEmail: 'Enter your email',
|
||||
enterPassword: 'Enter your password',
|
||||
enterPasswordAgain: 'Enter your password again',
|
||||
forgotPassword: 'Forget password?',
|
||||
pleaseInputName: 'Please input the name',
|
||||
nameLengthError: 'Name length must be between {min} and {max} characters',
|
||||
@@ -28,7 +30,7 @@ export default {
|
||||
havenAccountToLogin: `Already have an account? <span onclick="onClickLogin()">Log in</span>`,
|
||||
verifyEmail: 'Verify your email address',
|
||||
verifyCodeHasSent: 'A verification code has been sent to<br><span>{email}</span>',
|
||||
verify: 'Verify',
|
||||
verify: 'VERIFY',
|
||||
resendCode: 'Resend Code',
|
||||
resendCodeIn: 'Resend Code in {time}',
|
||||
orContinueWith: 'or continue with',
|
||||
|
||||
@@ -8,9 +8,11 @@ export default {
|
||||
name: '姓名',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
passwordConfirmation: '密码确认',
|
||||
enterName: '请输入姓名',
|
||||
enterEmail: '请输入邮箱',
|
||||
enterPassword: '请输入密码',
|
||||
enterPasswordAgain: '请输入密码确认',
|
||||
forgotPassword: '忘记密码?',
|
||||
pleaseInputName: '请输入姓名',
|
||||
nameLengthError: '姓名长度必须在 {min} 到 {max} 个字符之间',
|
||||
|
||||
@@ -7,42 +7,46 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
* 3. 路由的name默认是文件名,如果文件名与name不一致,通过defineOptions({ name: 'componentName' })来设置
|
||||
*/
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/'),
|
||||
// history: createWebHistory(import.meta.env.VITE_APP_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/home/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/collectionStory',
|
||||
name: 'collectionStory',
|
||||
component: () => import('../views/collectionStory/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/brand',
|
||||
name: 'brand',
|
||||
component: () => import('../views/brand/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/digitalItem',
|
||||
name: 'digitalItem',
|
||||
component: () => import('../views/digitalItem/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)',
|
||||
name: '404',
|
||||
component: () => import('../views/404.vue'),
|
||||
},
|
||||
]
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/home/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/collectionStory',
|
||||
name: 'collectionStory',
|
||||
component: () => import('../views/collectionStory/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/brand',
|
||||
name: 'brand',
|
||||
component: () => import('../views/brand/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/digitalItem',
|
||||
name: 'digitalItem',
|
||||
component: () => import('../views/digitalItem/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('@/views/setting/index.vue'),
|
||||
meta: { cache: true }
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)',
|
||||
name: '404',
|
||||
component: () => import('../views/404.vue')
|
||||
}
|
||||
],
|
||||
history: createWebHistory('/')
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
next()
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
})
|
||||
router.afterEach(() => {})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
--el-input-border-radius: 0;
|
||||
--el-input-text-color: #232323;
|
||||
--el-border-color: #C4C4C4;
|
||||
font-size: 1.4rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.retrieve-password:deep(.el-form) .el-input::placeholder,
|
||||
.register:deep(.el-form) .el-input::placeholder,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="visible-code">
|
||||
<div class="email-verify">
|
||||
<div class="tip" v-html="$t('Login.verifyCodeHasSent', { email: props.email })"></div>
|
||||
<input-code @submit="onVerify" v-model="code" ref="inputCodeRef" />
|
||||
<p class="time" v-if="time > 0">{{ $t('Login.resendCodeIn', { time: timeStr }) }}</p>
|
||||
@@ -7,7 +7,7 @@
|
||||
<span @click="onResend">{{ $t('Login.resendCode') }}</span>
|
||||
</p>
|
||||
<button class="verify" custom="black" @click="onVerify">{{ $t('Login.verify') }}</button>
|
||||
<other-login />
|
||||
<other-login v-if="isShowOtherLogin" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
type: String as () => 'LOGIN' | 'REGISTER' | 'FORGOT_PWD',
|
||||
required: true
|
||||
},
|
||||
password: { type: String, default: '' }
|
||||
password: { type: String, default: '' },
|
||||
isShowOtherLogin: { type: Boolean, default: true }
|
||||
})
|
||||
const code = ref('')
|
||||
const time = ref(60)
|
||||
@@ -96,7 +97,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.visible-code {
|
||||
.email-verify {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -123,17 +124,15 @@
|
||||
--button-font-size: 1.4rem;
|
||||
}
|
||||
> .time {
|
||||
font-family: KaiseiOpti-Regular;
|
||||
user-select: none;
|
||||
margin-top: 2.4rem;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.2rem;
|
||||
color: #666;
|
||||
font-family: Regular;
|
||||
> span {
|
||||
color: #ff7a50;
|
||||
color: #232323;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-family: Medium;
|
||||
}
|
||||
}
|
||||
> .other-login {
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
<template>
|
||||
<div class="index">
|
||||
<div class="header">
|
||||
<span class="tip">{{ $t('AlphaVersion') }}</span>
|
||||
<img src="@/assets/images/logo-1.png" class="logo" />
|
||||
<p class="split"></p>
|
||||
<button class="login" @click="onLogin">{{ $t('Login.login') }}</button>
|
||||
<button class="register" @click="onRegister">{{ $t('Login.register') }}</button>
|
||||
</div>
|
||||
<img src="@/assets/images/login/index-title.png" class="title" draggable="false" />
|
||||
<img src="@/assets/images/login/index-zhuangshi.png" class="zhuangshi" draggable="false" />
|
||||
<div class="tip">{{ $t('Login.indexTip') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
const onLogin = () => {
|
||||
router.push({ name: 'login' })
|
||||
}
|
||||
const onRegister = () => {
|
||||
router.push({ name: 'register' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.index {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-image: url('@/assets/images/login/index-bg.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
> .header {
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
> * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
> .tip {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
font-family: Regular;
|
||||
color: #fff;
|
||||
z-index: 0;
|
||||
}
|
||||
> .logo {
|
||||
width: auto;
|
||||
height: 2.5rem;
|
||||
margin-left: 3.8rem;
|
||||
}
|
||||
> .split {
|
||||
margin: 0 auto;
|
||||
}
|
||||
> button {
|
||||
margin-right: 3rem;
|
||||
width: 20rem;
|
||||
height: 5.2rem;
|
||||
border-radius: 5rem;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 600;
|
||||
font-family: SemiBold;
|
||||
border: 0.2rem solid #fff;
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
> .login {
|
||||
background-color: #fff;
|
||||
color: #713e1f;
|
||||
}
|
||||
> .register {
|
||||
background-color: transparent;
|
||||
color: #ffffff;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
> .zhuangshi,
|
||||
> .title,
|
||||
> .tip {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
> .title {
|
||||
// width: 55%;
|
||||
width: 105.6rem;
|
||||
height: auto;
|
||||
top: 20.5rem;
|
||||
}
|
||||
> .zhuangshi {
|
||||
width: 21.5rem;
|
||||
height: auto;
|
||||
bottom: 13.4rem;
|
||||
}
|
||||
> .tip {
|
||||
font-size: 2.8rem;
|
||||
font-family: Regular;
|
||||
color: #fff;
|
||||
bottom: 8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -32,7 +32,7 @@
|
||||
--el-input-border-radius: 0;
|
||||
--el-input-text-color: #232323;
|
||||
--el-border-color: #C4C4C4;
|
||||
font-size: 1.4rem;
|
||||
font-size: 1rem;
|
||||
|
||||
&::placeholder {
|
||||
color: #9F9F9F;
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="login-dialog-content">
|
||||
<img class="bg" src="@/assets/images/login/bg.jpg" />
|
||||
<img class="logo" src="@/assets/images/logo.png" />
|
||||
<div class="close" @click="show = false"><svg-icon name="close" /></div>
|
||||
<div class="content" v-if="curentTabInfo">
|
||||
@@ -22,8 +21,10 @@
|
||||
<div class="nav" v-show="!curentTabInfo.title">
|
||||
<div
|
||||
class="item"
|
||||
:class="{ active: currentTab === TabNames.sign_up }"
|
||||
@click="currentTab = TabNames.sign_up"
|
||||
:class="{
|
||||
active: [TabNames.register, TabNames.register_success].includes(currentTab)
|
||||
}"
|
||||
@click="currentTab = TabNames.register"
|
||||
>
|
||||
SIGN UP
|
||||
</div>
|
||||
@@ -42,6 +43,7 @@
|
||||
@login="onLogin"
|
||||
@register="onRegister"
|
||||
@submit-email-code="onSubmitEmailCode"
|
||||
@back="onBack"
|
||||
:name="data.name"
|
||||
:email="data.email"
|
||||
:password="data.password"
|
||||
@@ -58,6 +60,8 @@
|
||||
import login from './login.vue'
|
||||
import register from './register.vue'
|
||||
import emailVerify from './email-verify.vue'
|
||||
import registerSuccess from './register-success.vue'
|
||||
import retrievePassword from './retrieve-password.vue'
|
||||
import myEvent from '@/utils/myEvent'
|
||||
const data = ref({
|
||||
name: '',
|
||||
@@ -65,11 +69,11 @@
|
||||
password: '',
|
||||
type: ''
|
||||
})
|
||||
const show = ref(false)
|
||||
const TabNames = {
|
||||
login: 'login',
|
||||
sign_up: 'sign_up',
|
||||
register: 'register',
|
||||
email_verify: 'email_verify',
|
||||
register_success: 'register_success',
|
||||
retrieve_password: 'retrieve_password'
|
||||
}
|
||||
const tabList = markRaw([
|
||||
@@ -78,7 +82,7 @@
|
||||
component: login
|
||||
},
|
||||
{
|
||||
name: TabNames.sign_up,
|
||||
name: TabNames.register,
|
||||
component: register
|
||||
},
|
||||
{
|
||||
@@ -89,15 +93,20 @@
|
||||
{
|
||||
name: TabNames.retrieve_password,
|
||||
title: 'RETRIEVE PASSWORD',
|
||||
component: login
|
||||
component: retrievePassword
|
||||
},
|
||||
{
|
||||
name: TabNames.register_success,
|
||||
component: registerSuccess
|
||||
}
|
||||
])
|
||||
const show = ref(false)
|
||||
const currentTab = ref(TabNames.login)
|
||||
const curentTabInfo = computed(() => tabList.find((v) => v.name === currentTab.value))
|
||||
const lastTab = ref('')
|
||||
watch(currentTab, (v, o) => (lastTab.value = o))
|
||||
const onBack = () => {
|
||||
if (lastTab.value) currentTab.value = lastTab.value
|
||||
currentTab.value = lastTab.value || TabNames.login
|
||||
}
|
||||
const open = (type?: string) => {
|
||||
currentTab.value = TabNames[type] || TabNames.login
|
||||
@@ -118,13 +127,17 @@
|
||||
}
|
||||
const onRegister = (res: any) => {
|
||||
data.value = res
|
||||
data.value.type = TabNames.sign_up
|
||||
data.value.type = TabNames.register
|
||||
currentTab.value = TabNames.email_verify
|
||||
}
|
||||
const onSubmitEmailCode = (code: string) => {
|
||||
// data.value.code = code
|
||||
console.log(code)
|
||||
show.value = false
|
||||
if (data.value.type === TabNames.login) {
|
||||
console.log('登录', code)
|
||||
show.value = false
|
||||
} else {
|
||||
console.log('注册', code)
|
||||
currentTab.value = TabNames.register_success
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -147,14 +160,16 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: url('@/assets/images/login/bg.jpg') no-repeat center center / 100% 100%;
|
||||
> *:not(.content) {
|
||||
position: absolute;
|
||||
}
|
||||
> .bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
> * {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
> .logo {
|
||||
width: auto;
|
||||
height: 4rem;
|
||||
@@ -170,9 +185,10 @@
|
||||
}
|
||||
> .content {
|
||||
width: 34rem;
|
||||
top: 5rem;
|
||||
right: 6rem;
|
||||
height: calc(100% - 10rem);
|
||||
margin: 5rem 6rem auto auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> .header {
|
||||
--padding-bottom: 1.2rem;
|
||||
padding-bottom: var(--padding-bottom);
|
||||
|
||||
@@ -40,15 +40,15 @@
|
||||
.password-tip {
|
||||
background: #404040;
|
||||
color: #fff;
|
||||
font-size: 1.4rem;
|
||||
padding: 2rem;
|
||||
border-radius: 2rem;
|
||||
font-size: 1.1rem;
|
||||
padding: 1.5rem;
|
||||
border-radius: 1.5rem;
|
||||
line-height: normal;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
113
src/views/login/register-success.vue
Normal file
113
src/views/login/register-success.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="register-success">
|
||||
<div class="icon"><svg-icon name="dui" size="20" /></div>
|
||||
<div class="title">Welcome to Stylish Parade!</div>
|
||||
<div class="title">Please switch to the Login tab to log in.</div>
|
||||
<div class="footer">
|
||||
<div class="title">
|
||||
<span class="text">What awaits you in Stylish Parade</span>
|
||||
<span class="icon"><svg-icon name="arrow_right" size="11" /></span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div>
|
||||
<div class="title">Behind the design</div>
|
||||
<div class="tip">
|
||||
Discover how designers bring ideas to life with AiDA — from first sketch to final look.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">Creative digital works</div>
|
||||
<div class="tip">
|
||||
Unlock a growing library of inspiring digital works to refresh your creative mind.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">A fashion community</div>
|
||||
<div class="tip">
|
||||
Join a space where fashion speaks — exchange ideas and connect with creators worldwide.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.register-success {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 7.7rem;
|
||||
> .icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
border: 0.1rem solid #e8e8e8;
|
||||
margin-bottom: 1.8rem;
|
||||
}
|
||||
> .title {
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
text-align: center;
|
||||
color: #232323;
|
||||
}
|
||||
> .footer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
bottom: 7rem;
|
||||
padding: 0 6rem;
|
||||
> .title {
|
||||
font-family: KaiseiOpti-Regular;
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.4rem;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 1.2rem;
|
||||
> .icon {
|
||||
margin-left: 1.2rem;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
> .content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.8rem;
|
||||
> div {
|
||||
padding: 2.4rem 1.5rem 0;
|
||||
height: 14.8rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 3px 4px 8px 0px rgba(0, 0, 0, 0.11);
|
||||
flex: 1;
|
||||
&:first-child {
|
||||
flex: 0.84;
|
||||
}
|
||||
> .title {
|
||||
font-family: KaiseiOpti-Bold;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
margin-bottom: 1.2rem;
|
||||
color: #232323;
|
||||
}
|
||||
> .tip {
|
||||
font-family: KaiseiOpti-Regular;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.7rem;
|
||||
color: #585858;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,120 +1,183 @@
|
||||
<template>
|
||||
<div class="retrieve-password">
|
||||
<div class="left">
|
||||
<img class="bg" src="@/assets/images/login/left-bg.png" />
|
||||
<img class="logo" src="@/assets/images/logo-1.png" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="top">
|
||||
<button class="back" @click="onBack">
|
||||
<svg-icon name="arrow-left" size="37" />
|
||||
<el-form
|
||||
:model="formData"
|
||||
:rules="ruleForm"
|
||||
label-position="top"
|
||||
ref="form1Ref"
|
||||
v-show="index === 0"
|
||||
>
|
||||
<div class="title">Please enter your email address below to verify your identity.</div>
|
||||
<el-form-item :label="$t('Login.email')" prop="email">
|
||||
<el-input v-model="formData.email" :placeholder="$t('Login.enterEmail')" name="email" />
|
||||
</el-form-item>
|
||||
<el-form-item class="submit-item">
|
||||
<button class="submit" type="submit" custom="black" @click.prevent="onSubmit1">
|
||||
SUBMIT
|
||||
</button>
|
||||
</div>
|
||||
<div class="box">
|
||||
<img src="@/assets/images/login/elephant.png" />
|
||||
<template v-if="!isVisible">
|
||||
<div class="title">{{ $t('Login.retrievePassword') }}</div>
|
||||
<el-form :model="formData" :rules="ruleForm" label-position="top" ref="formRef">
|
||||
<el-form-item :label="$t('Login.email')" prop="email">
|
||||
<el-input
|
||||
v-model="formData.email"
|
||||
:placeholder="$t('Login.enterEmail')"
|
||||
name="email"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('Login.password')" prop="password">
|
||||
<password-tip :value="formData.password" v-show="showPasswordTip" />
|
||||
<el-input
|
||||
v-model="formData.password"
|
||||
:placeholder="$t('Login.enterPassword')"
|
||||
type="password"
|
||||
show-password
|
||||
name="password"
|
||||
@blur="showPasswordTip = false"
|
||||
@focus="showPasswordTip = true"
|
||||
/>
|
||||
</el-form-item>
|
||||
<br />
|
||||
<br />
|
||||
<el-form-item>
|
||||
<el-button class="submit" type="primary" @click="onSubmit">{{
|
||||
$t('submit')
|
||||
}}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<!-- <visible-code
|
||||
v-show="isVisible"
|
||||
type="FORGOT_PWD"
|
||||
ref="visibleCodeRef"
|
||||
:email="formData.email"
|
||||
@submit="onVerifyCode"
|
||||
/> -->
|
||||
<other-login />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="verify-box" v-if="index === 1">
|
||||
<email-verify
|
||||
type="FORGOT_PWD"
|
||||
:email="formData.email"
|
||||
@submit-email-code="onVerifyCode"
|
||||
:is-show-other-login="false"
|
||||
/>
|
||||
</div>
|
||||
<el-form
|
||||
:model="formData"
|
||||
:rules="ruleForm"
|
||||
label-position="top"
|
||||
ref="form2Ref"
|
||||
v-show="index === 2"
|
||||
>
|
||||
<div class="title">
|
||||
Enter a new password for <br />
|
||||
<span>{{ formData.email }}</span>
|
||||
</div>
|
||||
<el-form-item :label="$t('Login.password')" prop="password">
|
||||
<password-tip :value="formData.password" v-show="showPasswordTip" />
|
||||
<el-input
|
||||
v-model="formData.password"
|
||||
:placeholder="$t('Login.enterPassword')"
|
||||
type="password"
|
||||
show-password
|
||||
name="password"
|
||||
@blur="showPasswordTip = false"
|
||||
@focus="showPasswordTip = true"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="password-warning">
|
||||
<span class="icon"><svg-icon name="warning" size="12" /></span>
|
||||
<span class="label">You must satisfy ALL password conditions to register.</span>
|
||||
</div>
|
||||
<el-form-item :label="$t('Login.passwordConfirmation')" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="formData.confirmPassword"
|
||||
:placeholder="$t('Login.enterPasswordAgain')"
|
||||
type="password"
|
||||
show-password
|
||||
name="password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="submit-item">
|
||||
<button class="submit" type="submit" custom="black" @click.prevent="onSubmit2">
|
||||
SUBMIT
|
||||
</button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import md5 from 'md5'
|
||||
import { ForgotPassword } from '@/api/user'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { validateEmail, validatePass } from './tools'
|
||||
import OtherLogin from './other-login.vue'
|
||||
import emailVerify from './email-verify.vue'
|
||||
import PasswordTip from './password-tip.vue'
|
||||
import { useUserInfoStore } from '@/stores'
|
||||
const userInfoStore = useUserInfoStore()
|
||||
const router = useRouter()
|
||||
import EmailVerify from './email-verify.vue'
|
||||
const emit = defineEmits(['back'])
|
||||
const validateConfirmPassword = (rule: any, value: string, callback: any) => {
|
||||
if (value !== formData.password) {
|
||||
callback(new Error('Passwords do not match'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const index = ref(0)
|
||||
const ruleForm = reactive({
|
||||
email: [{ validator: validateEmail, trigger: 'change' }],
|
||||
password: [{ validator: validatePass, trigger: 'change' }]
|
||||
password: [{ validator: validatePass, trigger: 'change' }],
|
||||
confirmPassword: [{ validator: validateConfirmPassword, trigger: 'change' }]
|
||||
})
|
||||
const isVisible = ref(false)
|
||||
const showPasswordTip = ref(false)
|
||||
const formData = reactive({
|
||||
email: '',
|
||||
password: ''
|
||||
code: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
const formRef = ref(null)
|
||||
const onBack = () => {
|
||||
if (isVisible.value) {
|
||||
isVisible.value = false
|
||||
} else {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
const form1Ref = ref(null)
|
||||
const visibleCodeRef = ref(null)
|
||||
const onSubmit = () => {
|
||||
formRef.value?.validate?.((valid) => {
|
||||
const form2Ref = ref(null)
|
||||
const onSubmit1 = () => {
|
||||
form1Ref.value?.validate?.((valid) => {
|
||||
if (valid) {
|
||||
// console.log('submit!')
|
||||
visibleCodeRef.value?.onSendCode().then(() => {
|
||||
isVisible.value = true
|
||||
})
|
||||
index.value = 1
|
||||
} else {
|
||||
console.warn('error submit!')
|
||||
}
|
||||
})
|
||||
}
|
||||
const onSubmit2 = () => {
|
||||
form2Ref.value?.validate?.((valid) => {
|
||||
if (valid) {
|
||||
const data = {
|
||||
email: formData.email,
|
||||
code: formData.code,
|
||||
password: md5(formData.password)
|
||||
}
|
||||
console.log(data)
|
||||
emit('back')
|
||||
} else {
|
||||
console.warn('error submit!')
|
||||
}
|
||||
})
|
||||
}
|
||||
const onVerifyCode = (code: string) => {
|
||||
// console.log(code)
|
||||
ForgotPassword({
|
||||
email: formData.email,
|
||||
newPassword: md5(formData.password),
|
||||
verificationCode: code
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) router.push({ name: 'login' })
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(error)
|
||||
})
|
||||
if (!code) return
|
||||
formData.code = code
|
||||
index.value = 2
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import './less/style.less';
|
||||
|
||||
.retrieve-password {
|
||||
flex: 1;
|
||||
&:deep(.el-form) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.el-form-item.submit-item {
|
||||
margin-top: auto;
|
||||
}
|
||||
.el-input {
|
||||
--el-input-height: 4.8rem;
|
||||
}
|
||||
.el-form-item:nth-last-child(2) {
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
> .title {
|
||||
font-family: KaiseiOpti-Regular;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
text-align: center;
|
||||
color: #585858;
|
||||
margin-top: auto;
|
||||
margin-bottom: 3rem;
|
||||
> span {
|
||||
font-family: KaiseiOpti-Medium;
|
||||
color: #252727;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .verify-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
&:deep(.email-verify) {
|
||||
> .tip {
|
||||
margin-top: 8rem;
|
||||
}
|
||||
> .input-code {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
> .verify {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
}
|
||||
const onSettings = () => {
|
||||
hideProfilePopover()
|
||||
console.log('settings')
|
||||
router.push('/settings')
|
||||
}
|
||||
const onLogout = () => {
|
||||
hideProfilePopover()
|
||||
|
||||
103
src/views/setting/components/Radio.vue
Normal file
103
src/views/setting/components/Radio.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="radio-button-group">
|
||||
<button
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
:class="[
|
||||
'radio-button',
|
||||
{
|
||||
'is-active': multiple ? selectedValues.includes(item.value) : modelValue === item.value
|
||||
}
|
||||
]"
|
||||
@click="selectOption(item.value)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Option {
|
||||
name: string | number
|
||||
value: string | number | boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | number | boolean | Array<string | number | boolean> | null
|
||||
options: Option[] // 按钮选项数组
|
||||
multiple?: boolean // 是否支持多选,默认为 false
|
||||
max?: number // 多选时最多可选数量,不传则不限制
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: any): void
|
||||
}>()
|
||||
|
||||
const multiple = props.multiple === true
|
||||
|
||||
const selectedValues = computed(() => {
|
||||
if (!multiple) {
|
||||
return typeof props.modelValue === 'undefined' || props.modelValue === null
|
||||
? []
|
||||
: [props.modelValue]
|
||||
}
|
||||
|
||||
return Array.isArray(props.modelValue) ? props.modelValue : []
|
||||
})
|
||||
|
||||
const selectOption = (value: any) => {
|
||||
if (multiple) {
|
||||
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
||||
const index = current.indexOf(value)
|
||||
if (index >= 0) {
|
||||
current.splice(index, 1)
|
||||
} else {
|
||||
if (typeof props.max === 'number' && props.max > 0 && current.length >= props.max) {
|
||||
current.shift()
|
||||
}
|
||||
current.push(value)
|
||||
}
|
||||
emit('update:modelValue', current)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.modelValue !== value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.radio-button-group {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
border: 1px solid #979797;
|
||||
height: 4rem;
|
||||
min-width: 8rem;
|
||||
padding: 0 2rem;
|
||||
color: #979797;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: #fff;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.radio-button:hover {
|
||||
border-color: #000;
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.radio-button.is-active {
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
border-color: #000;
|
||||
}
|
||||
</style>
|
||||
798
src/views/setting/index.vue
Normal file
798
src/views/setting/index.vue
Normal file
@@ -0,0 +1,798 @@
|
||||
<template>
|
||||
<div class="setting-wrapper mini-scrollbar">
|
||||
<div class="banner flex flex-center flex-col">
|
||||
<div class="title">Settings</div>
|
||||
<div class="slogan">Manage your account settings and preferences</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-content">
|
||||
<div class="setting-item flex">
|
||||
<div class="label-container">
|
||||
<div class="label">Profile</div>
|
||||
<div class="label-desc">
|
||||
Update your display name, avatar, social links and account security.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="context-container">
|
||||
<div class="profile-header flex align-center">
|
||||
<div class="avatar relative">
|
||||
<SvgIcon name="user_0" size="46" class="avatar-icon" />
|
||||
<img src="@/assets/images/edit.png" class="avatar-edit-icon" />
|
||||
</div>
|
||||
<div class="profile-summary flex flex-col">
|
||||
<div class="profile-name">{{ fullName }}</div>
|
||||
<div class="profile-email">{{ displayData.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="read-section">
|
||||
<div class="read-row two-column">
|
||||
<div class="read-item">
|
||||
<div class="read-label">FIRST NAME</div>
|
||||
<div v-show="!isEditing" class="read-box">{{ displayData.firstName }}</div>
|
||||
<div v-show="isEditing" class="form-item-value name">
|
||||
<el-input v-model="draftData.firstName" placeholder="First Name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="read-item">
|
||||
<div class="read-label">LAST NAME</div>
|
||||
<div v-show="!isEditing" class="read-box">{{ displayData.lastName }}</div>
|
||||
<div v-show="isEditing" class="form-item-value name">
|
||||
<el-input v-model="draftData.lastName" placeholder="Last Name" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="read-row">
|
||||
<div class="read-label">USERNAME</div>
|
||||
<div v-show="!isEditing" class="read-box">{{ displayData.username }}</div>
|
||||
<div v-show="isEditing" class="form-item-value">
|
||||
<el-input v-model="draftData.username" placeholder="Username" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="read-tip">Your public username on Stylish Parade.</div>
|
||||
|
||||
<div class="read-row role-row">
|
||||
<div class="read-label">ROLE</div>
|
||||
<div :class="{ 'readonly-radio-group': !isEditing }">
|
||||
<Radio multiple :max="2" v-model="roleModel" :options="roleList" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="read-tip">Select up to 2 labels that suit you.</div>
|
||||
|
||||
<div class="social-links read-social-links">
|
||||
<div class="title">SOCIAL LINKS</div>
|
||||
<div class="links-list flex flex-col">
|
||||
<div
|
||||
class="links-item flex align-center"
|
||||
v-for="(item, index) in displayData.links"
|
||||
:key="`view-${index}`"
|
||||
>
|
||||
<div class="link-index">Link {{ index + 1 }}</div>
|
||||
<div v-show="!isEditing" class="link-href flex-1 readonly">{{ item }}</div>
|
||||
<div v-show="isEditing" class="link-href flex-1">
|
||||
<el-input v-model="draftData.links[index]" />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-show="isEditing"
|
||||
type="button"
|
||||
class="add-link-btn"
|
||||
@click="handleAddLink"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gap" />
|
||||
|
||||
<div class="setting-item flex">
|
||||
<div class="label-container">
|
||||
<div class="label">Security</div>
|
||||
<div class="label-desc">Manage your login email and password.</div>
|
||||
</div>
|
||||
|
||||
<div class="context-container security-container">
|
||||
<div class="inner-divider" />
|
||||
<div class="security-row">
|
||||
<div class="security-inline-row flex align-center">
|
||||
<div class="security-label inline">EMAIL</div>
|
||||
<div class="security-static flex-1">{{ displayData.email }}</div>
|
||||
<button
|
||||
v-show="isEditing"
|
||||
type="button"
|
||||
class="small-btn"
|
||||
@click="resetSecurityEmail"
|
||||
>
|
||||
CANCEL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="isEditing" class="security-row">
|
||||
<div class="security-label">NEW EMAIL ADDRESS</div>
|
||||
<div class="outlined-field verify-field align-center">
|
||||
<el-input v-model="securityDraft.newEmail" placeholder="Enter new email" />
|
||||
<div class="verify-btn">Verify</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inner-divider" />
|
||||
|
||||
<div class="security-row">
|
||||
<div class="security-inline-row flex align-center">
|
||||
<div class="security-label inline">PASSWORD</div>
|
||||
<div class="security-static password-mask flex-1">.........</div>
|
||||
<button
|
||||
v-show="isEditing"
|
||||
type="button"
|
||||
class="small-btn"
|
||||
@click="resetSecurityPassword"
|
||||
>
|
||||
CANCEL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="isEditing" class="security-row">
|
||||
<div class="security-label">NEW PASSWORD</div>
|
||||
<div class="outlined-field">
|
||||
<el-input
|
||||
v-model="securityDraft.newPassword"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="Enter new password"
|
||||
/>
|
||||
</div>
|
||||
<div class="security-tip">You must satisfy ALL password conditions to register.</div>
|
||||
</div>
|
||||
|
||||
<div v-show="isEditing" class="security-row">
|
||||
<div class="security-label">CURRENT PASSWORD</div>
|
||||
<div class="outlined-field">
|
||||
<el-input
|
||||
v-model="securityDraft.currentPassword"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="Confirm with your password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner-divider" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gap" />
|
||||
|
||||
<div class="setting-item flex">
|
||||
<div class="label-container">
|
||||
<div class="label">Language & Region</div>
|
||||
<div class="label-desc">Set your preferred language, region and currency display.</div>
|
||||
</div>
|
||||
|
||||
<div class="context-container region-container">
|
||||
<div class="region-row">
|
||||
<div class="security-label">DISPLAY LANGUAGE</div>
|
||||
<div v-show="!isEditing" class="security-static field-box">
|
||||
{{ displayData.language }}
|
||||
</div>
|
||||
<div v-show="isEditing" class="outlined-field select-field">
|
||||
<el-select v-model="draftData.language" placeholder="Select language">
|
||||
<el-option v-for="item in languageList" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="region-row">
|
||||
<div class="security-label">REGION</div>
|
||||
<div v-show="!isEditing" class="security-static field-box">
|
||||
{{ displayData.region }}
|
||||
</div>
|
||||
<div v-show="isEditing" class="outlined-field select-field">
|
||||
<el-select v-model="draftData.region" placeholder="Select region">
|
||||
<el-option v-for="item in regionList" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gap bottom-gap" />
|
||||
|
||||
<div class="action-container flex">
|
||||
<template v-if="isEditing">
|
||||
<button type="button" class="primary-btn" :disabled="saving" @click="handleSave">
|
||||
{{ saving ? 'SAVING...' : 'SAVE CHANGE' }}
|
||||
</button>
|
||||
<button type="button" class="secondary-btn" :disabled="saving" @click="handleDiscard">
|
||||
DISCARD
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button type="button" class="primary-btn edit-btn" @click="handleEdit">EDIT</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import Radio from './components/Radio.vue'
|
||||
|
||||
interface SettingsData {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
username: string
|
||||
role: string[]
|
||||
links: string[]
|
||||
language: string
|
||||
region: string
|
||||
}
|
||||
|
||||
interface SecurityDraft {
|
||||
newEmail: string
|
||||
newPassword: string
|
||||
currentPassword: string
|
||||
}
|
||||
|
||||
const roleList = [
|
||||
'Fashion Enthusiast',
|
||||
'Content Creator',
|
||||
'Student',
|
||||
'Retail / Buyer',
|
||||
'Fashion Designer',
|
||||
'Brand / Business',
|
||||
'PR & Communications',
|
||||
'Stylist',
|
||||
'Graphic Designer',
|
||||
'3D Artist',
|
||||
'Other'
|
||||
].map((item) => ({ name: item, value: item }))
|
||||
|
||||
const languageList = ['English', 'Chinese', 'Japanese']
|
||||
const regionList = ['Hong Kong SAR', 'Mainland China', 'Singapore', 'United Kingdom']
|
||||
|
||||
const createDefaultData = (): SettingsData => ({
|
||||
firstName: 'Alexandra',
|
||||
lastName: 'Chen',
|
||||
email: 'alex.chen@gmail.com',
|
||||
username: '@alexandra_chen',
|
||||
role: ['Student', 'Graphic Designer'],
|
||||
links: ['https://instagram.com/username', 'https://...'],
|
||||
language: 'English',
|
||||
region: 'Hong Kong SAR'
|
||||
})
|
||||
|
||||
const cloneSettingsData = (data: SettingsData): SettingsData => ({
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
username: data.username,
|
||||
role: [...data.role],
|
||||
links: [...data.links],
|
||||
language: data.language,
|
||||
region: data.region
|
||||
})
|
||||
|
||||
const createEmptySecurityDraft = (): SecurityDraft => ({
|
||||
newEmail: '',
|
||||
newPassword: '',
|
||||
currentPassword: ''
|
||||
})
|
||||
|
||||
const sourceData = ref<SettingsData>(createDefaultData())
|
||||
const draftData = ref<SettingsData>(cloneSettingsData(sourceData.value))
|
||||
const securityDraft = ref<SecurityDraft>(createEmptySecurityDraft())
|
||||
const isEditing = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value))
|
||||
|
||||
const fullName = computed(() => {
|
||||
const data = displayData.value
|
||||
return `${data.firstName} ${data.lastName}`.trim()
|
||||
})
|
||||
|
||||
const roleModel = computed({
|
||||
get: () => displayData.value.role,
|
||||
set: (value: string[]) => {
|
||||
if (isEditing.value) {
|
||||
draftData.value.role = value
|
||||
return
|
||||
}
|
||||
sourceData.value.role = value
|
||||
}
|
||||
})
|
||||
|
||||
const resetDraftState = () => {
|
||||
draftData.value = cloneSettingsData(sourceData.value)
|
||||
securityDraft.value = createEmptySecurityDraft()
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
resetDraftState()
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const handleAddLink = () => {
|
||||
draftData.value.links.push('')
|
||||
}
|
||||
|
||||
const resetSecurityEmail = () => {
|
||||
securityDraft.value.newEmail = ''
|
||||
}
|
||||
|
||||
const resetSecurityPassword = () => {
|
||||
securityDraft.value.newPassword = ''
|
||||
securityDraft.value.currentPassword = ''
|
||||
}
|
||||
|
||||
const handleDiscard = () => {
|
||||
resetDraftState()
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const buildNextData = () => {
|
||||
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
|
||||
return {
|
||||
firstName: draftData.value.firstName.trim(),
|
||||
lastName: draftData.value.lastName.trim(),
|
||||
username: draftData.value.username.trim(),
|
||||
email: nextEmail,
|
||||
role: draftData.value.role,
|
||||
links: draftData.value.links.map((item) => item.trim()).filter(Boolean),
|
||||
language: draftData.value.language,
|
||||
region: draftData.value.region
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const nextData = buildNextData()
|
||||
saving.value = true
|
||||
try {
|
||||
sourceData.value = cloneSettingsData({
|
||||
...nextData,
|
||||
links: nextData.links.length ? nextData.links : ['']
|
||||
})
|
||||
draftData.value = cloneSettingsData(sourceData.value)
|
||||
securityDraft.value = createEmptySecurityDraft()
|
||||
isEditing.value = false
|
||||
ElMessage.success('Settings updated')
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
|
||||
.field-text() {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.field-frame() {
|
||||
width: 100%;
|
||||
min-height: 4rem;
|
||||
border: 0.1rem solid #979797;
|
||||
}
|
||||
|
||||
.control-wrapper() {
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.banner {
|
||||
height: 14.8rem;
|
||||
row-gap: 1.2rem;
|
||||
background: linear-gradient(rgba(255, 255, 255, 0.91), rgba(255, 255, 255, 0.91)),
|
||||
linear-gradient(90deg, #f2eee8 0%, #fbfaf8 40%, #f1ede7 100%);
|
||||
|
||||
.title {
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
font-size: 4rem;
|
||||
line-height: 3.6rem;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
color: #585858;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-content {
|
||||
padding: 4rem 18rem 7rem;
|
||||
.setting-item {
|
||||
column-gap: 21.4rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.label-container {
|
||||
width: 27.6rem;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
|
||||
.label {
|
||||
font-size: 2.8rem;
|
||||
line-height: 3.6rem;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.label-desc {
|
||||
width: 24rem;
|
||||
margin-top: 2rem;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.2rem;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.context-container {
|
||||
width: 58rem;
|
||||
padding-top: 0.2rem;
|
||||
}
|
||||
|
||||
.form-item-label,
|
||||
.read-label,
|
||||
.security-label {
|
||||
margin-bottom: 0.8rem;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.4rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #585858;
|
||||
}
|
||||
|
||||
.form-tip,
|
||||
.read-tip,
|
||||
.security-tip {
|
||||
margin-top: 0.8rem;
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6rem;
|
||||
color: #9f9f9f;
|
||||
}
|
||||
|
||||
.form-item-value,
|
||||
.outlined-field,
|
||||
.link-href {
|
||||
.field-frame();
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select) {
|
||||
width: 100%;
|
||||
min-height: 4rem;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-select__wrapper) {
|
||||
.control-wrapper();
|
||||
min-height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-value {
|
||||
&.noborder {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.name {
|
||||
width: 28.4rem;
|
||||
}
|
||||
|
||||
&.radio {
|
||||
width: 58rem;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
.field-text();
|
||||
}
|
||||
}
|
||||
|
||||
.read-box,
|
||||
.field-box,
|
||||
.security-static {
|
||||
.field-frame();
|
||||
.field-text();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.8rem 2rem;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
column-gap: 2.6rem;
|
||||
margin-bottom: 3.6rem;
|
||||
|
||||
.avatar {
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
border-radius: 50%;
|
||||
border: 0.1rem solid #d8d0c7;
|
||||
}
|
||||
|
||||
.avatar-edit-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 0.1rem solid #fff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-summary {
|
||||
row-gap: 0.6rem;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 2.4rem;
|
||||
line-height: 3.6rem;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.profile-email {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.8rem;
|
||||
line-height: 2.4rem;
|
||||
color: #979797;
|
||||
}
|
||||
|
||||
.read-section {
|
||||
width: 58rem;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
row-gap: 3rem;
|
||||
}
|
||||
|
||||
.col-gap-2 {
|
||||
column-gap: 2rem;
|
||||
}
|
||||
|
||||
.read-row + .read-row,
|
||||
.region-row + .region-row {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.read-row.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: 28.4rem 28.4rem;
|
||||
column-gap: 2rem;
|
||||
}
|
||||
|
||||
.readonly-radio-group {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.role-row {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
margin-top: 5.8rem;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
|
||||
.title {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.4rem;
|
||||
color: #585858;
|
||||
}
|
||||
|
||||
&.read-social-links {
|
||||
margin-top: 4.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.links-list {
|
||||
row-gap: 0.8rem;
|
||||
}
|
||||
|
||||
.links-item {
|
||||
column-gap: 3.4rem;
|
||||
}
|
||||
|
||||
.link-index {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.4rem;
|
||||
line-height: 2rem;
|
||||
color: #979797;
|
||||
}
|
||||
|
||||
.link-href {
|
||||
color: #979797;
|
||||
|
||||
&.readonly {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.8rem 2rem;
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.4rem;
|
||||
line-height: 2rem;
|
||||
color: #9f9f9f;
|
||||
}
|
||||
|
||||
:deep(.el-input) {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.4rem;
|
||||
color: #9f9f9f;
|
||||
}
|
||||
}
|
||||
|
||||
.add-link-btn {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border: 0.1rem solid #f0ebe5;
|
||||
background: #f6f6f6;
|
||||
color: #b4aea6;
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.security-container {
|
||||
.security-row + .security-row {
|
||||
margin-top: 2.8rem;
|
||||
}
|
||||
|
||||
.security-label {
|
||||
margin: 0 0 0.8rem;
|
||||
|
||||
&.inline {
|
||||
width: 10.8rem;
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.security-static {
|
||||
min-height: 2.4rem;
|
||||
padding: 0.1rem 0 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.security-inline-row {
|
||||
gap: 2.8rem;
|
||||
min-height: 3.2rem;
|
||||
}
|
||||
|
||||
.security-tip {
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
.verify-field {
|
||||
display: flex;
|
||||
margin-top: 0.8rem;
|
||||
|
||||
:deep(.el-input) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.verify-btn {
|
||||
border: none;
|
||||
height: 2.8rem;
|
||||
line-height: 2.8rem;
|
||||
border-left: 0.1rem solid #979797;
|
||||
background: #ffffff;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 1.4rem;
|
||||
color: #232323;
|
||||
cursor: pointer;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.password-mask {
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
letter-spacing: 0.08rem;
|
||||
}
|
||||
|
||||
.inner-divider {
|
||||
height: 1px;
|
||||
margin: 2rem 0;
|
||||
background-color: #c4c4c4;
|
||||
}
|
||||
}
|
||||
|
||||
.region-container {
|
||||
.field-box {
|
||||
padding: 0.8rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.small-btn,
|
||||
.secondary-btn,
|
||||
.primary-btn {
|
||||
border: 0.1rem solid #c4c4c4;
|
||||
background: #f6f6f6;
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
color: #232323;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
width: 10rem;
|
||||
height: 3.2rem;
|
||||
align-self: flex-start;
|
||||
font-size: 1.2rem;
|
||||
line-height: 2.6rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.gap {
|
||||
height: 0.05rem;
|
||||
margin-top: 6rem;
|
||||
margin-bottom: 4rem;
|
||||
background-color: #c4c4c4;
|
||||
|
||||
&.bottom-gap {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.action-container {
|
||||
justify-content: center;
|
||||
column-gap: 1.2rem;
|
||||
margin-top: 2.8rem;
|
||||
}
|
||||
|
||||
.primary-btn,
|
||||
.secondary-btn {
|
||||
height: 4.4rem;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.6rem;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
min-width: 23.6rem;
|
||||
padding: 0 2.4rem;
|
||||
border-color: #232323;
|
||||
background: #232323;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
width: 12rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
min-width: 12rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-select-dropdown__item) {
|
||||
padding: 0 2rem !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user