198 lines
7.6 KiB
Vue
198 lines
7.6 KiB
Vue
<!--
|
|||
|
|
Ported from legacy henza_pwa/src/views/user/LoginRegister/components/Login.vue.
|
||
|
|
|
||
|
|
Per your decision: OTP-based flow ported structurally, but the SMS-send step
|
||
|
|
is stubbed — no SMS provider exists. What's real vs. stubbed:
|
||
|
|
- Step 1 (mobile number) is checked against the REAL, already-existing
|
||
|
|
GET /api/customers endpoint (client-side match on `mobile`) — no fake
|
||
|
|
endpoint invented. If it matches a real seeded customer, that customer's
|
||
|
|
exact legacy-notify message path is preserved: a non-match shows
|
||
|
|
legacy's own "اطلاعات کاربری در سیستم وجود ندارد" text (legacy's actual
|
||
|
|
404 handling), not a new message.
|
||
|
|
- Step 2 (OTP code) is NOT verified — any 4-digit code is accepted, since
|
||
|
|
there is no real code to check against. On "verify," the session store
|
||
|
|
logs in as the matched real customer (session.login), so downstream
|
||
|
|
features (balance, ledger, favorites-as-logged-in) work against real
|
||
|
|
seeded data. This is the "session wiring" — apps/api itself is untouched.
|
||
|
|
- `token.fireBaseToken` (Firebase push token) doesn't exist in this port
|
||
|
|
(Firebase was already deferred) — omitted from the stub verify call.
|
||
|
|
- `useOtpToken()` (WebOTP browser autofill) was already dead in legacy
|
||
|
|
itself (its call was commented out in App.vue, and even active, nothing
|
||
|
|
ever read its output into the OTP input) — not ported.
|
||
|
|
-->
|
||
|
|
<template>
|
||
|
|
<div>
|
||
|
|
<NotifyVant :timeOut="2000" v-model="notify.show" :text="notify.text" :color="'background-color: #DC2626'" />
|
||
|
|
<div class="fixed w-full">
|
||
|
|
<img class="w-full absolute left-0 right-0" :src="pattern1" alt="" />
|
||
|
|
<img class="absolute w-full left-0 right-0" :src="pattern2" alt="" />
|
||
|
|
</div>
|
||
|
|
<div class="fixed top-0 z-10 w-full">
|
||
|
|
<AppHeader class="bg-white shadowHeader">
|
||
|
|
<div class="flex justify-between items-center">
|
||
|
|
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer" size="25" @click="router.push({ name: 'home' })" />
|
||
|
|
<h3 class="t bold mb-0">ورود</h3>
|
||
|
|
<div></div>
|
||
|
|
</div>
|
||
|
|
</AppHeader>
|
||
|
|
|
||
|
|
<div class="flex justify-center mb-8 mt-5">
|
||
|
|
<div class="h-[150px] w-[150px] bg-[#F8F7F6] rounded-full flex justify-center items-center bg-opacity-50">
|
||
|
|
<div class="bg-[#F9F5F0] h-[120px] w-[120px] flex justify-center items-center rounded-full">
|
||
|
|
<div>
|
||
|
|
<div class="rounded-full w-[90px] h-[90px] flex justify-center bg-[#F1E8DE]">
|
||
|
|
<img :src="editImg" class="p-6" alt="henza" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="w-full text-center font-bold text-black text-lg">ورود به اپلیکیشن هنزا گلد</div>
|
||
|
|
<div class="px-8 custom-form">
|
||
|
|
<OtpInput
|
||
|
|
class="w-full mb-[10px] text-black mt-5"
|
||
|
|
v-show="body.step == 2 && timer > 0"
|
||
|
|
input-classes="otp-input"
|
||
|
|
inputType="tel"
|
||
|
|
:num-inputs="4"
|
||
|
|
:should-auto-focus="true"
|
||
|
|
:defaultFocus="body.defaultFocus"
|
||
|
|
@on-complete="sendToken"
|
||
|
|
/>
|
||
|
|
<div v-show="body.step == 2 && timer === 0" class="text-center mb-[10px] mt-5">برای دریافت رمز عبور مجدد تلاش کنید</div>
|
||
|
|
<div v-show="body.step == 2 && timer !== 0" class="text-center text-xl mb-[10px] mt-5">
|
||
|
|
امکان دریافت مجدد رمز در {{ ' ' + counterZeroShow(timer) + ' ' }} ثانیه دیگر
|
||
|
|
</div>
|
||
|
|
<Form :model="formModel" layout="vertical" @finish="sendPhone" class="w-full">
|
||
|
|
<FormItem name="username" label="تلفن همراه" v-show="body.step == 1" :rules="[required('تلفن همراه'), mobileRule('تلفن همراه')]">
|
||
|
|
<Input type="number" v-model:value="formModel.username" />
|
||
|
|
</FormItem>
|
||
|
|
<Button class="btn-px-double" type="primary" html-type="submit" size="large" block :loading="body.loading" :disabled="timer > 0 && body.step === 2">
|
||
|
|
{{ btnText }}
|
||
|
|
</Button>
|
||
|
|
</Form>
|
||
|
|
<div class="flex justify-center mt-5">
|
||
|
|
<div>اگر ثبت نام نکرده اید از این قسمت</div>
|
||
|
|
<div class="pr-1 text-primary font-bold" @click="emit('register', true)">ثبت نام کنید.</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { computed, nextTick, reactive, ref } from 'vue'
|
||
|
|
import { useRouter } from 'vue-router'
|
||
|
|
import { Button, Form, FormItem, Input } from 'ant-design-vue'
|
||
|
|
import pattern1 from '../../assets/img/pattern2.svg'
|
||
|
|
import pattern2 from '../../assets/img/pattern3.svg'
|
||
|
|
import editImg from '../../assets/img/henza.svg'
|
||
|
|
import Icon from '../../components/icon/Icon.vue'
|
||
|
|
import AppHeader from '../../components/AppHeader.vue'
|
||
|
|
import NotifyVant from '../../components/NotifyVant.vue'
|
||
|
|
import OtpInput from '../../components/input/OtpInput.vue'
|
||
|
|
import { required, mobile as mobileRule } from '../../utils/validator'
|
||
|
|
import { useSessionStore } from '../../stores/session'
|
||
|
|
import { api, type CustomerSummary } from '../../api/client'
|
||
|
|
|
||
|
|
const emit = defineEmits<{ (e: 'register', value: boolean): void }>()
|
||
|
|
const router = useRouter()
|
||
|
|
const session = useSessionStore()
|
||
|
|
|
||
|
|
const formModel = reactive({ username: undefined as string | undefined })
|
||
|
|
const notify = reactive({ show: false, text: 'اطلاعات کاربری در سیستم وجود ندارد.' })
|
||
|
|
const body = reactive({
|
||
|
|
step: 1,
|
||
|
|
loading: false,
|
||
|
|
defaultFocus: false,
|
||
|
|
})
|
||
|
|
|
||
|
|
let matchedCustomer: CustomerSummary | null = null
|
||
|
|
|
||
|
|
const timer = ref(0)
|
||
|
|
const countDownTimer = () => {
|
||
|
|
if (timer.value > 0) {
|
||
|
|
setTimeout(() => {
|
||
|
|
if (timer.value > 0) {
|
||
|
|
timer.value -= 1
|
||
|
|
countDownTimer()
|
||
|
|
}
|
||
|
|
}, 1000)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const sendPhone = async () => {
|
||
|
|
try {
|
||
|
|
body.loading = true
|
||
|
|
const customers = await api.listCustomers()
|
||
|
|
matchedCustomer = customers.find((c) => c.mobile === formModel.username) ?? null
|
||
|
|
if (!matchedCustomer) {
|
||
|
|
notify.text = 'اطلاعات کاربری در سیستم وجود ندارد.'
|
||
|
|
notify.show = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
body.step = 2
|
||
|
|
timer.value = 120
|
||
|
|
countDownTimer()
|
||
|
|
await nextTick(() => {
|
||
|
|
body.defaultFocus = true
|
||
|
|
})
|
||
|
|
} catch (e) {
|
||
|
|
console.log(e)
|
||
|
|
} finally {
|
||
|
|
body.loading = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
|
const sendToken = async (_otpCode: string) => {
|
||
|
|
// Stub: the OTP code itself isn't verified (no SMS was actually sent to
|
||
|
|
// check it against). `matchedCustomer` was already confirmed real in
|
||
|
|
// sendPhone(), so any 4-digit code here logs in as that customer.
|
||
|
|
try {
|
||
|
|
body.loading = true
|
||
|
|
if (!matchedCustomer) return
|
||
|
|
session.login(matchedCustomer.id, matchedCustomer.mobile)
|
||
|
|
await router.push({ name: 'home' })
|
||
|
|
notify.text = 'با موفقیت وارد شدید'
|
||
|
|
notify.show = true
|
||
|
|
} finally {
|
||
|
|
body.loading = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const counterZeroShow = (value: number) => (value < 10 ? '0' + value : value)
|
||
|
|
|
||
|
|
const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور' : 'دریافت مجدد رمز عبور'))
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style lang="less">
|
||
|
|
.shadowHeader {
|
||
|
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
.custom-form {
|
||
|
|
.otp-input {
|
||
|
|
width: 50px;
|
||
|
|
height: 50px;
|
||
|
|
padding: 5px;
|
||
|
|
margin: 0;
|
||
|
|
font-size: 20px;
|
||
|
|
background: transparent;
|
||
|
|
border: 0 solid @black;
|
||
|
|
border-bottom-width: 2px;
|
||
|
|
text-align: center;
|
||
|
|
|
||
|
|
&.error {
|
||
|
|
border: 1px solid @danger !important;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.otp-input::-webkit-inner-spin-button,
|
||
|
|
.otp-input::-webkit-outer-spin-button {
|
||
|
|
-webkit-appearance: none;
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|