feat: initial commit
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
<!--
|
||||
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>
|
||||
Reference in New Issue
Block a user