اپ: نام برند در همهی صفحهها و پیامها یکسان شده و از یک تنظیم مرکزی خوانده میشود. اپ: کارت حسابداری در صفحهی مشتری بازطراحی شد؛ ماندهها و وضعیت لینک واضحتر نمایش داده میشوند. پنل: کارت حسابداری در پروندهی مشتری بازطراحی شد؛ ماندهها، آخرین ارسال و فرم لینک دستی واضحتر شدهاند. پنل: ستون «کد» در جدولها بهطور پیشفرض پنهان است و از انتخاب ستونها قابل نمایش است. پنل: تغییر نمایش ستونها در جدولها بلافاصله اعمال میشود. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
241 lines
7.6 KiB
Vue
241 lines
7.6 KiB
Vue
<!--
|
|
Ported from legacy henza_pwa/src/views/user/LoginRegister/components/Register.vue,
|
|
restyled into the redesigned design language (see components/home/* and
|
|
HomeView).
|
|
|
|
DROPPED as confirmed-dead code (verified unreachable, not merely "possibly
|
|
dead"): legacy rendered a whole cascading country->province->city picker
|
|
(WorkActionSheet + selectCityArrays + getCities() against POST /cities/list)
|
|
alongside a plain "شهر" text <Input>. The only element that ever opened the
|
|
picker (`#citybtn`) was itself commented out in legacy's own template, so the
|
|
picker was unreachable UI even in the original app; the visible form uses the
|
|
plain text input. That whole complex, and the parallel unused
|
|
`userFormModel`/`defaultUserFormModel` objects, are not ported.
|
|
|
|
Submits to POST customer/auth/register (legacy POST /major/register). The new
|
|
customer is created inactive and lands in the admin's «مشتریان در انتظار»
|
|
list; they can sign in once an admin approves them.
|
|
-->
|
|
<template>
|
|
<div class="auth">
|
|
<PageBar title="ثبت نام" back-to="home" />
|
|
|
|
<Spinner :spinning="profileData.loading" tip="در حال ثبت اطلاعات">
|
|
<container class="auth__body">
|
|
<section class="auth__intro">
|
|
<div class="auth__mark" aria-hidden="true">
|
|
<Icon icon="vuesax-linear:user-add" size="28" />
|
|
</div>
|
|
<h2 class="auth__title">ساخت حساب کاربری</h2>
|
|
<p class="auth__sub">اطلاعات فروشگاه خود را وارد کنید</p>
|
|
</section>
|
|
|
|
<section class="auth__card">
|
|
<Form :model="formModel" layout="vertical" @finish="register" class="auth__form">
|
|
<FormItem name="fname" label="نام" :rules="required('نام')">
|
|
<Input v-model:value="formModel.fname" />
|
|
</FormItem>
|
|
<FormItem name="lname" label="نام خانوادگی" :rules="required('نام خانوادگی')">
|
|
<Input v-model:value="formModel.lname" />
|
|
</FormItem>
|
|
<FormItem
|
|
name="mobile"
|
|
label="شماره موبایل"
|
|
:rules="[mobileRule('شماره موبایل'), required('شماره موبایل')]"
|
|
>
|
|
<Input v-model:value="formModel.mobile" type="number" />
|
|
</FormItem>
|
|
<FormItem name="store_name" label="نام فروشگاه" :rules="required('نام فروشگاه')">
|
|
<Input v-model:value="formModel.store_name" />
|
|
</FormItem>
|
|
<FormItem name="phone" label="تلفن فروشگاه" :rules="[required('تلفن فروشگاه')]">
|
|
<Input v-model:value="formModel.phone" type="number" />
|
|
</FormItem>
|
|
<FormItem name="address" label="آدرس پستی" :rules="required('آدرس')">
|
|
<Textarea v-model:value="formModel.address" :rows="3" />
|
|
</FormItem>
|
|
<FormItem name="city" label="شهر" :rules="required('شهر')">
|
|
<Input v-model:value="formModel.city" />
|
|
</FormItem>
|
|
|
|
<button type="submit" class="auth__cta">ثبت اطلاعات</button>
|
|
</Form>
|
|
</section>
|
|
|
|
<p class="auth__switch">
|
|
اگر حساب کاربری دارید
|
|
<button type="button" @click="emit('register', false)">وارد شوید</button>
|
|
</p>
|
|
</container>
|
|
</Spinner>
|
|
|
|
<DialogVant :timeOut="-1" v-model:show="registeredInfo" :closeOnClickOverlay="false">
|
|
<template v-slot:img>
|
|
<img :src="registerDelayImage" alt="" class="opacity-80" />
|
|
</template>
|
|
<template v-slot:title>
|
|
<div class="p-5">
|
|
ثبت نام شما با موفقیت انجام شد. منتظر تایید اطلاعات ثبت نامی شما توسط کارشناسان بمانید.
|
|
</div>
|
|
</template>
|
|
<template v-slot:button>
|
|
<div class="text-center" @click="router.push({ name: 'home' })">متوجه شدم</div>
|
|
</template>
|
|
</DialogVant>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { reactive, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { Form, FormItem, Input, Textarea } from 'ant-design-vue'
|
|
import registerDelayImage from '../../assets/img/registerDelay.svg'
|
|
import Icon from '../../components/icon/Icon.vue'
|
|
import PageBar from '../../components/ui/PageBar.vue'
|
|
import Spinner from '../../components/Spinner.vue'
|
|
import DialogVant from '../../components/DialogVant.vue'
|
|
import { required, mobile as mobileRule } from '../../utils/validator'
|
|
import { api } from '../../api/client'
|
|
import { toast } from '../../composables/useToast'
|
|
|
|
const emit = defineEmits<{ (e: 'register', value: boolean): void }>()
|
|
const router = useRouter()
|
|
|
|
const profileData = reactive({ loading: false })
|
|
const registeredInfo = ref(false)
|
|
|
|
const formModel = reactive({
|
|
fname: undefined as string | undefined,
|
|
lname: undefined as string | undefined,
|
|
mobile: undefined as string | undefined,
|
|
store_name: undefined as string | undefined,
|
|
phone: undefined as string | undefined,
|
|
address: undefined as string | undefined,
|
|
city: undefined as string | undefined,
|
|
})
|
|
|
|
const register = async () => {
|
|
try {
|
|
profileData.loading = true
|
|
await api.register({
|
|
fname: String(formModel.fname ?? ''),
|
|
lname: String(formModel.lname ?? ''),
|
|
mobile: String(formModel.mobile ?? ''),
|
|
store_name: String(formModel.store_name ?? ''),
|
|
phone: String(formModel.phone ?? ''),
|
|
address: String(formModel.address ?? ''),
|
|
city: String(formModel.city ?? ''),
|
|
})
|
|
registeredInfo.value = true
|
|
} catch (e: unknown) {
|
|
toast.error(serverMessage(e) ?? 'خطا در ثبت اطلاعات. لطفاً دوباره تلاش کنید.')
|
|
} finally {
|
|
profileData.loading = false
|
|
}
|
|
}
|
|
|
|
/** The API's Persian `message` (duplicate number, bad mobile, throttle), if any. */
|
|
function serverMessage(e: unknown): string | null {
|
|
const match = e instanceof Error ? e.message.match(/\{.*\}/) : null
|
|
if (!match) return null
|
|
try {
|
|
const parsed = JSON.parse(match[0]) as { message?: string | string[] }
|
|
return Array.isArray(parsed.message) ? parsed.message[0] ?? null : parsed.message ?? null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.auth {
|
|
background: @page;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.auth__body {
|
|
padding-top: 86px;
|
|
padding-bottom: 90px;
|
|
}
|
|
|
|
.auth__intro {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
padding-bottom: 24px;
|
|
}
|
|
|
|
.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 20px;
|
|
border: 1px solid @hairline;
|
|
border-radius: 20px;
|
|
background: @canvasSoft;
|
|
}
|
|
|
|
.auth__form :deep(.ant-form-item-label > label) {
|
|
font-size: 12px;
|
|
color: @inkMuted;
|
|
}
|
|
|
|
.auth__cta {
|
|
width: 100%;
|
|
margin-top: 4px;
|
|
padding: 13px 18px;
|
|
border: 0;
|
|
cursor: pointer;
|
|
border-radius: 14px;
|
|
font-family: inherit;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
background: @primary;
|
|
transition: transform 0.15s ease;
|
|
}
|
|
|
|
.auth__cta:active {
|
|
transform: scale(0.99);
|
|
}
|
|
|
|
.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>
|