- Add tailwindcss v4 + @tailwindcss/vite to mall, shop-admin, admin - Add @vmall/shared/theme.css tokens with html[data-accent] presets - Add @vmall/ui kit (VBtn/VBadge/VField/VInput/VCard/VPanel/VTable/VPage, VAccentSwatch, useAccent) as a Nuxt module - Convert all three apps to kit + utilities; delete ui.css/mall.css and every <style scoped>; consoles get accent presets, mall locked to red - Fix VCard boolean prop default (padding) and PDP/store stale useAsyncData keys on param navigation - Archive adopt-tailwind-design-system; new frontend-ui capability spec
199 lines
5.9 KiB
Vue
199 lines
5.9 KiB
Vue
<script setup lang="ts">
|
|
import type { AddressBookEntry, AddressInput } from "@vmall/shared";
|
|
|
|
definePageMeta({ middleware: "auth" });
|
|
|
|
const { t } = useI18n();
|
|
const { $api } = useNuxtApp();
|
|
|
|
const rows = ref<AddressBookEntry[]>([]);
|
|
const loading = ref(true);
|
|
const open = ref(false);
|
|
const saving = ref(false);
|
|
const editingId = ref<string | null>(null);
|
|
|
|
interface AddressForm {
|
|
recipient: string;
|
|
phone: string;
|
|
country: string;
|
|
region: string;
|
|
city: string;
|
|
line1: string;
|
|
postal_code: string;
|
|
is_default: boolean;
|
|
}
|
|
|
|
const form = reactive<AddressForm>({
|
|
recipient: "",
|
|
phone: "",
|
|
country: "US",
|
|
region: "",
|
|
city: "",
|
|
line1: "",
|
|
postal_code: "",
|
|
is_default: false,
|
|
});
|
|
|
|
const errorKey = ref("");
|
|
|
|
const modalTitle = computed(() => (editingId.value ? t("user.editAddress") : t("user.addAddress")));
|
|
|
|
async function load(): Promise<void> {
|
|
loading.value = true;
|
|
errorKey.value = "";
|
|
try {
|
|
rows.value = await $api.listMyAddresses();
|
|
} catch {
|
|
errorKey.value = "user.loadFailed";
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(() => void load());
|
|
|
|
function resetForm(): void {
|
|
form.recipient = "";
|
|
form.phone = "";
|
|
form.country = "US";
|
|
form.region = "";
|
|
form.city = "";
|
|
form.line1 = "";
|
|
form.postal_code = "";
|
|
form.is_default = false;
|
|
errorKey.value = "";
|
|
}
|
|
|
|
function openAdd(): void {
|
|
editingId.value = null;
|
|
resetForm();
|
|
open.value = true;
|
|
}
|
|
|
|
function openEdit(row: AddressBookEntry): void {
|
|
editingId.value = row.id;
|
|
form.recipient = row.recipient;
|
|
form.phone = row.phone;
|
|
form.country = row.country;
|
|
form.region = row.region;
|
|
form.city = row.city;
|
|
form.line1 = row.line1;
|
|
form.postal_code = row.postal_code;
|
|
form.is_default = row.is_default;
|
|
errorKey.value = "";
|
|
open.value = true;
|
|
}
|
|
|
|
async function setDefault(id: string): Promise<void> {
|
|
try {
|
|
await $api.setDefaultAddress(id);
|
|
await load();
|
|
} catch {
|
|
errorKey.value = "user.saveFailed";
|
|
}
|
|
}
|
|
|
|
async function remove(row: AddressBookEntry): Promise<void> {
|
|
try {
|
|
rows.value = await $api.deleteAddress(row.id);
|
|
} catch {
|
|
errorKey.value = "user.saveFailed";
|
|
}
|
|
}
|
|
|
|
async function save(): Promise<void> {
|
|
if (!form.recipient || !form.phone || !form.country || !form.region || !form.city || !form.line1 || !form.postal_code) {
|
|
errorKey.value = "user.validationRequired";
|
|
return;
|
|
}
|
|
const input: AddressInput = {
|
|
recipient: form.recipient,
|
|
phone: form.phone,
|
|
country: form.country,
|
|
region: form.region,
|
|
city: form.city,
|
|
line1: form.line1,
|
|
postal_code: form.postal_code,
|
|
is_default: form.is_default,
|
|
};
|
|
saving.value = true;
|
|
errorKey.value = "";
|
|
try {
|
|
if (editingId.value) {
|
|
await $api.updateAddress(editingId.value, input);
|
|
} else {
|
|
await $api.createAddress(input);
|
|
}
|
|
open.value = false;
|
|
await load();
|
|
} catch {
|
|
errorKey.value = "user.saveFailed";
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<VCard class="min-h-[560px]">
|
|
<div class="mb-4 flex items-center justify-between gap-3">
|
|
<h1 class="m-0 text-xl font-bold text-text">{{ t("user.addresses") }}</h1>
|
|
<VBtn variant="primary" size="sm" type="button" @click="openAdd">{{ t("user.addAddress") }}</VBtn>
|
|
</div>
|
|
<UiEmptyState v-if="!loading && rows.length === 0" :text="t('user.noAddresses')" />
|
|
<VTable v-else>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ t("user.recipient") }}</th>
|
|
<th>{{ t("user.phone") }}</th>
|
|
<th>{{ t("user.country") }}</th>
|
|
<th>{{ t("user.region") }}</th>
|
|
<th>{{ t("user.city") }}</th>
|
|
<th>{{ t("user.line1") }}</th>
|
|
<th>{{ t("user.postalCode") }}</th>
|
|
<th>{{ t("common.status") }}</th>
|
|
<th>{{ t("common.actions") }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in rows" :key="row.id">
|
|
<td>{{ row.recipient }}</td>
|
|
<td>{{ row.phone }}</td>
|
|
<td>{{ row.country }}</td>
|
|
<td>{{ row.region }}</td>
|
|
<td>{{ row.city }}</td>
|
|
<td>{{ row.line1 }}</td>
|
|
<td>{{ row.postal_code }}</td>
|
|
<td>
|
|
<span v-if="row.is_default" class="text-xs text-primary">{{ t("user.defaultAddress") }}</span>
|
|
<VBtn v-else size="sm" type="button" @click="setDefault(row.id)">{{ t("user.setDefault") }}</VBtn>
|
|
</td>
|
|
<td class="space-x-2 whitespace-nowrap">
|
|
<VBtn size="sm" type="button" @click="openEdit(row)">{{ t("common.edit") }}</VBtn>
|
|
<VBtn size="sm" variant="danger" type="button" @click="remove(row)">{{ t("common.delete") }}</VBtn>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</VTable>
|
|
|
|
<UiModal :open="open" :title="modalTitle" @close="open = false">
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
<VField :label="t('user.recipient')"><VInput v-model="form.recipient" /></VField>
|
|
<VField :label="t('user.phone')"><VInput v-model="form.phone" /></VField>
|
|
<VField :label="t('user.country')"><VInput v-model="form.country" /></VField>
|
|
<VField :label="t('user.region')"><VInput v-model="form.region" /></VField>
|
|
<VField :label="t('user.city')"><VInput v-model="form.city" /></VField>
|
|
<VField class="sm:col-span-2" :label="t('user.line1')"><VInput v-model="form.line1" /></VField>
|
|
<VField :label="t('user.postalCode')"><VInput v-model="form.postal_code" /></VField>
|
|
<label class="flex items-center gap-2 self-end pb-3 text-sm text-text"><input v-model="form.is_default" class="h-4 w-4 accent-primary" type="checkbox" /> <span>{{ t("user.defaultAddress") }}</span></label>
|
|
</div>
|
|
<p v-if="errorKey" class="mt-2 text-sm text-danger">{{ t(errorKey) }}</p>
|
|
<template #footer>
|
|
<VBtn type="button" @click="open = false">{{ t("common.cancel") }}</VBtn>
|
|
<VBtn variant="primary" type="button" :disabled="saving" @click="save">{{ t("user.saveAddress") }}</VBtn>
|
|
</template>
|
|
</UiModal>
|
|
</VCard>
|
|
</template>
|
|
|