feat(ui): adopt Tailwind v4 design system and archive change

- 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
This commit is contained in:
Chengdong Zhang
2026-09-22 18:22:50 +08:00
parent 4d2ee3b0bf
commit 0d0e10b97b
101 changed files with 2833 additions and 7207 deletions
+25 -90
View File
@@ -135,13 +135,13 @@ async function save(): Promise<void> {
</script>
<template>
<section class="mpanel addresses-panel">
<h1 class="mpanel-title">
{{ t("user.addresses") }}
<button class="mbtn red" type="button" @click="openAdd">{{ t("user.addAddress") }}</button>
</h1>
<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')" />
<table v-else class="mtable">
<VTable v-else>
<thead>
<tr>
<th>{{ t("user.recipient") }}</th>
@@ -165,99 +165,34 @@ async function save(): Promise<void> {
<td>{{ row.line1 }}</td>
<td>{{ row.postal_code }}</td>
<td>
<span v-if="row.is_default" class="default-tag">{{ t("user.defaultAddress") }}</span>
<button v-else class="mbtn" type="button" @click="setDefault(row.id)">{{ t("user.setDefault") }}</button>
<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>
<button class="mbtn" type="button" @click="openEdit(row)">{{ t("common.edit") }}</button>
<button class="mbtn" type="button" @click="remove(row)">{{ t("common.delete") }}</button>
<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>
</table>
</VTable>
<UiModal :open="open" :title="modalTitle" @close="open = false">
<div class="form-grid">
<label>
<span>{{ t("user.recipient") }}</span>
<input v-model.trim="form.recipient" class="minput" type="text" />
</label>
<label>
<span>{{ t("user.phone") }}</span>
<input v-model.trim="form.phone" class="minput" type="text" />
</label>
<label>
<span>{{ t("user.country") }}</span>
<input v-model.trim="form.country" class="minput" type="text" />
</label>
<label>
<span>{{ t("user.region") }}</span>
<input v-model.trim="form.region" class="minput" type="text" />
</label>
<label>
<span>{{ t("user.city") }}</span>
<input v-model.trim="form.city" class="minput" type="text" />
</label>
<label class="full-line">
<span>{{ t("user.line1") }}</span>
<input v-model.trim="form.line1" class="minput" type="text" />
</label>
<label>
<span>{{ t("user.postalCode") }}</span>
<input v-model.trim="form.postal_code" class="minput" type="text" />
</label>
<label class="checkbox">
<input v-model="form.is_default" type="checkbox" />
<span>{{ t("user.defaultAddress") }}</span>
</label>
<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="error">{{ t(errorKey) }}</p>
<p v-if="errorKey" class="mt-2 text-sm text-danger">{{ t(errorKey) }}</p>
<template #footer>
<button class="mbtn" type="button" @click="open = false">{{ t("common.cancel") }}</button>
<button class="mbtn red" type="button" :disabled="saving" @click="save">{{ t("user.saveAddress") }}</button>
<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>
</section>
</VCard>
</template>
<style scoped>
.addresses-panel {
min-height: 560px;
}
.default-tag {
color: var(--mall-red);
font-size: 12px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.form-grid label > span {
display: block;
margin-bottom: 6px;
color: var(--mall-muted);
font-size: 12px;
}
.full-line {
grid-column: 1 / -1;
}
.checkbox {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox > span {
margin: 0;
}
.error {
margin-top: 8px;
color: var(--mall-red);
font-size: 12px;
}
@media (max-width: 760px) {
.form-grid {
grid-template-columns: 1fr;
}
}
</style>