Files
tehran_taj/apps/user-app/src/views/user/LoginView.vue
T

198 lines
7.6 KiB
Vue
Raw Normal View History

2026-09-09 06:46:27 +03:30
<!--
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">
امکان دریافت مجدد رمز در &nbsp;{{ ' ' + 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>