feat(mall): classic B2B2C PC storefront with fixed mock API layer
- Mock adapter implementing @vmall/shared ApiClient (localStorage-persisted cart/orders/invoices), runtime switch via mockApi flag (default on) - Fixed bilingual mock catalog: 3-level categories, 24 products w/ SKUs, stores, brands, banners, floors, seckill/collective/integral, comments, coupons, addresses, seeded orders/shipments/invoices - B2B2C mall shell: top bar, logo/search/cart header, dark nav + category mega-menu, value-prop footer, back-to-top; 1200px grid, #ca151e theme - UI primitives replacing element-plus: carousel, pagination, breadcrumb, qty stepper, rating stars, modal, tabs, step bar, product card - Pages: home floors, search (filters/sort/paging), goods detail (SKU picker, store rail, review tabs), cart -> checkout -> pay -> success, auth pages, user center (dashboard/orders/addresses/favorites/coupons/ invoices), stores, seckill, collective, integral - i18n split into per-domain locale modules (en+zh) - OpenSpec change mall-pc-storefront-replica archived; all specs green
This commit is contained in:
+150
-19
@@ -1,24 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n();
|
||||
const { $api } = useNuxtApp();
|
||||
const session = useSessionStore();
|
||||
const router = useRouter();
|
||||
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
const error = ref("");
|
||||
const errorKey = ref("");
|
||||
const submitting = ref(false);
|
||||
|
||||
function validate(): boolean {
|
||||
if (!email.value || !password.value) {
|
||||
errorKey.value = "auth.validationRequired";
|
||||
return false;
|
||||
}
|
||||
if (!/^\S+@\S+\.\S+$/.test(email.value)) {
|
||||
errorKey.value = "auth.validationEmail";
|
||||
return false;
|
||||
}
|
||||
if (password.value.length < 6) {
|
||||
errorKey.value = "auth.validationPassword";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
errorKey.value = "";
|
||||
if (!validate()) return;
|
||||
submitting.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const auth = await $api.login(email.value.trim(), password.value);
|
||||
if (auth.user.role !== "customer") {
|
||||
error.value = $t("auth.wrongRole");
|
||||
return;
|
||||
}
|
||||
const auth = await $api.login(email.value, password.value);
|
||||
session.setAuth(auth);
|
||||
await navigateTo("/");
|
||||
} catch (value) {
|
||||
error.value = value instanceof Error ? value.message : $t("auth.badCredentials");
|
||||
await router.push("/");
|
||||
} catch {
|
||||
errorKey.value = "auth.requestFailed";
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
@@ -26,14 +42,129 @@ async function submit(): Promise<void> {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="form-narrow card">
|
||||
<h1 class="page-title">{{ $t("auth.welcome") }}</h1>
|
||||
<p v-if="error" class="error-text" role="alert">{{ error }}</p>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="field"><label for="login-email">{{ $t("common.email") }}</label><input id="login-email" v-model="email" type="email" autocomplete="email" required></div>
|
||||
<div class="field"><label for="login-password">{{ $t("common.password") }}</label><input id="login-password" v-model="password" type="password" autocomplete="current-password" required></div>
|
||||
<button class="btn primary" type="submit" :disabled="submitting">{{ submitting ? $t("common.loading") : $t("common.login") }}</button>
|
||||
</form>
|
||||
<p class="muted mt">{{ $t("auth.noAccount") }} <NuxtLink to="/register">{{ $t("common.register") }}</NuxtLink></p>
|
||||
<section class="auth-page">
|
||||
<div class="auth-inner">
|
||||
<div class="auth-panel">
|
||||
<div class="auth-tabs">
|
||||
<h1>{{ t("auth.loginTab") }}</h1>
|
||||
<NuxtLink to="/register">{{ t("auth.registerTab") }}</NuxtLink>
|
||||
</div>
|
||||
<form @submit.prevent="submit">
|
||||
<label class="field">
|
||||
<span>{{ t("common.email") }}</span>
|
||||
<input v-model.trim="email" class="minput" type="email" autocomplete="email" :placeholder="t('auth.emailPlaceholder')" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>{{ t("common.password") }}</span>
|
||||
<input v-model="password" class="minput" type="password" autocomplete="current-password" :placeholder="t('auth.passwordPlaceholder')" />
|
||||
</label>
|
||||
<p v-if="errorKey" class="error">{{ t(errorKey) }}</p>
|
||||
<button class="mbtn red block submit" type="submit" :disabled="submitting">
|
||||
{{ t("auth.loginAction") }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="auth-links">
|
||||
<span>{{ t("auth.noAccount") }}</span>
|
||||
<NuxtLink to="/register">{{ t("auth.registerNow") }}</NuxtLink>
|
||||
<NuxtLink to="/forgot-password">{{ t("auth.forgotPassword") }}</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
min-height: 620px;
|
||||
background: #f6f6f6 url("/mock/login-bg.svg") center top / cover no-repeat;
|
||||
padding: 50px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.auth-inner {
|
||||
width: 1200px;
|
||||
min-height: 450px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.auth-panel {
|
||||
width: 400px;
|
||||
min-height: 450px;
|
||||
box-sizing: border-box;
|
||||
padding: 40px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 22px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.auth-tabs {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 18px;
|
||||
margin-bottom: 28px;
|
||||
border-bottom: 1px solid var(--mall-line);
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: var(--mall-ink);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.auth-tabs a {
|
||||
color: var(--mall-faint);
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.auth-tabs a:hover,
|
||||
.auth-links a:hover {
|
||||
color: var(--mall-red);
|
||||
}
|
||||
.field {
|
||||
display: block;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.field span {
|
||||
display: block;
|
||||
margin-bottom: 7px;
|
||||
color: var(--mall-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.submit {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.error {
|
||||
margin: -4px 0 12px;
|
||||
color: var(--mall-red);
|
||||
font-size: 12px;
|
||||
}
|
||||
.auth-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
color: var(--mall-faint);
|
||||
font-size: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.auth-links a {
|
||||
color: var(--mall-red);
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 1240px) {
|
||||
.auth-inner {
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.auth-page {
|
||||
padding: 20px;
|
||||
}
|
||||
.auth-inner {
|
||||
width: 100%;
|
||||
}
|
||||
.auth-panel {
|
||||
width: 100%;
|
||||
padding: 30px 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user