user-app: extend the redesign across the account screens
Phase 2, completing the app-wide rollout of the home/product-detail design language (LESS colour tokens, BEM class names, hairline-bordered cards, SectionHead, dashed empty states) onto the account side: dashboard, orders, transactions, profile, login and register. Reuse over re-implementation: the dashboard's live rate block is now components/home/RateStrip.vue rather than a second hand-built strip, and every screen uses components/ui/PageBar.vue for its chrome. OrderCard and OrderItemCard are restyled to match. OrderItemCard drops its always-empty note row: our OrderItem schema has no note field (legacy's per-item note was a real column there; ours is not), so it could only ever print a dash. OrdersView swaps ant Tabs for the same scrolling chip rail the rest of the redesign uses, and its full-screen Modal with hand-written CSS overrides for a bottom Drawer, matching how every other detail surface opens. Deleted AppHeader.vue, orphaned once every screen moved to PageBar. Its shutter animation was driven by transitionFlag, which nothing reads any more — the router afterEach comment claiming otherwise is corrected rather than left asserting something untrue, and the now-inert half is flagged in place. ScrollFetchSearch loses its isFav prop: it gated two empty-state branches that rendered the identical image, and its only caller (FavoritesView) now renders its own. Behaviour is preserved throughout. Orders still reads GET /customers/:id/orders and filters client-side; transactions still reads GET /customers/:id/ledger-entries with legacy's CREDIT/DEBIT colour meaning intact; profile stays read-only over real fields; login keeps the real customer lookup plus the documented OTP stub; register stays stubbed with copy that does not claim an account was created. Verified in a real browser against the running API and native database: dashboard 4 tiles with a real balance, orders 1 real order card, profile 4 real spec rows, transactions the correct empty state for a customer with no ledger rows, and login/register confirmed with the session cleared (the guard bounces a signed-in user away from /login, which is why they needed a separate pass). No console errors, no horizontal overflow. vue-tsc and vite build both clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cc62975dd0
commit
0e5f9ccd7b
@@ -1,94 +1,93 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/views/user/LoginRegister/components/Login.vue.
|
||||
Ported from legacy henza_pwa/src/views/user/LoginRegister/components/Login.vue,
|
||||
restyled into the redesigned design language (see components/home/* and
|
||||
HomeView).
|
||||
|
||||
Per your decision: OTP-based flow ported structurally, but the SMS-send step
|
||||
is stubbed — no SMS provider exists. What's real vs. stubbed:
|
||||
Per the earlier decision: OTP-based flow ported structurally, but the
|
||||
SMS-send step is stubbed — no SMS provider exists. What's real vs stubbed,
|
||||
all carried over unchanged from the previous version:
|
||||
- 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.
|
||||
endpoint invented. A non-match shows legacy's own
|
||||
"اطلاعات کاربری در سیستم وجود ندارد" text, 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.
|
||||
there is no real code to check against. On "verify", the session store
|
||||
logs in as the matched real customer, so downstream features (balance,
|
||||
ledger, favourites-as-logged-in) work against real seeded data.
|
||||
- `token.fireBaseToken` doesn't exist in this port (Firebase deferred).
|
||||
- `useOtpToken()` (WebOTP autofill) was already dead in legacy itself.
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div class="auth">
|
||||
<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>
|
||||
<PageBar title="ورود" back-to="home" />
|
||||
|
||||
<container class="auth__body">
|
||||
<section class="auth__intro">
|
||||
<div class="auth__mark" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:security-user" size="28" />
|
||||
</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('تلفن همراه')]">
|
||||
<h2 class="auth__title">ورود به هنزاگلد</h2>
|
||||
<p class="auth__sub">
|
||||
{{ body.step === 1 ? 'شماره موبایل خود را وارد کنید' : 'رمز یکبارمصرف را وارد کنید' }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="auth__card">
|
||||
<!-- Step 2: OTP -->
|
||||
<template v-if="body.step === 2">
|
||||
<OtpInput
|
||||
class="auth__otp"
|
||||
input-classes="otp-input"
|
||||
inputType="tel"
|
||||
:num-inputs="4"
|
||||
:should-auto-focus="true"
|
||||
:defaultFocus="body.defaultFocus"
|
||||
@on-complete="sendToken"
|
||||
/>
|
||||
|
||||
<p v-if="timer > 0" class="auth__timer">
|
||||
امکان دریافت مجدد رمز در {{ counterZeroShow(timer) }} ثانیه دیگر
|
||||
</p>
|
||||
<p v-else class="auth__timer">برای دریافت رمز عبور مجدد تلاش کنید</p>
|
||||
</template>
|
||||
|
||||
<Form :model="formModel" layout="vertical" @finish="sendPhone" class="auth__form">
|
||||
<FormItem
|
||||
v-show="body.step === 1"
|
||||
name="username"
|
||||
label="تلفن همراه"
|
||||
: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">
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="auth__cta"
|
||||
:class="{ 'is-loading': body.loading }"
|
||||
:disabled="body.loading || (timer > 0 && body.step === 2)"
|
||||
>
|
||||
{{ btnText }}
|
||||
</Button>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<p class="auth__switch">
|
||||
اگر ثبت نام نکردهاید
|
||||
<button type="button" @click="emit('register', true)">ثبت نام کنید</button>
|
||||
</p>
|
||||
</container>
|
||||
</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 { Form, FormItem, Input } from 'ant-design-vue'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import AppHeader from '../../components/AppHeader.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import NotifyVant from '../../components/NotifyVant.vue'
|
||||
import OtpInput from '../../components/input/OtpInput.vue'
|
||||
import { required, mobile as mobileRule } from '../../utils/validator'
|
||||
@@ -166,32 +165,147 @@ 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);
|
||||
<style scoped lang="less">
|
||||
.auth {
|
||||
background: @canvas;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.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;
|
||||
.auth__body {
|
||||
padding-top: 26px;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border: 1px solid @danger !important;
|
||||
}
|
||||
}
|
||||
.auth__intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.otp-input::-webkit-inner-spin-button,
|
||||
.otp-input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
.auth__mark {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
}
|
||||
|
||||
.auth__title {
|
||||
margin: 14px 0 0;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.auth__sub {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12.5px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.auth__card {
|
||||
padding: 18px 16px;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 20px;
|
||||
background: @canvasSoft;
|
||||
}
|
||||
|
||||
.auth__otp {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
direction: ltr;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.auth__timer {
|
||||
margin: 12px 0 16px;
|
||||
text-align: center;
|
||||
font-size: 11.5px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-form-item-label > label) {
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.auth__cta {
|
||||
width: 100%;
|
||||
padding: 13px 18px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.auth__cta:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.auth__cta:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.auth__switch {
|
||||
margin: 20px 0 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.auth__switch button {
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: @wine;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
/* OtpInput renders its inputs with this class from a prop, so the rule cannot
|
||||
be scoped to this component. */
|
||||
.otp-input {
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
background: @canvas;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 13px;
|
||||
text-align: center;
|
||||
transition: border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.otp-input:focus {
|
||||
outline: none;
|
||||
border-color: @ink;
|
||||
}
|
||||
|
||||
.otp-input.error {
|
||||
border-color: @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