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:
+63
-80
@@ -109,85 +109,68 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="page-head">
|
||||
<h1 class="page-title">{{ $t("nav.users") }}</h1>
|
||||
<span v-if="!loading" class="muted">{{ $t("admin.pageOf", { page, total: totalPages }) }}</span>
|
||||
<VPage :title="$t('nav.users')">
|
||||
<template #actions>
|
||||
<span v-if="!loading" class="text-sm text-muted">{{ $t("admin.pageOf", { page, total: totalPages }) }}</span>
|
||||
</template>
|
||||
<p v-if="loading" class="text-sm text-muted">{{ $t("common.loading") }}</p>
|
||||
<p v-else-if="errorMessage" class="my-2 text-sm text-danger" role="alert">{{ errorMessage }}</p>
|
||||
<VCard v-else-if="users.length === 0" class="text-muted">{{ $t("common.empty") }}</VCard>
|
||||
<div v-else class="overflow-x-auto"><div class="min-w-[920px]"><VTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t("common.email") }}</th>
|
||||
<th>{{ $t("common.displayName") }}</th>
|
||||
<th>{{ $t("admin.role") }}</th>
|
||||
<th>{{ $t("admin.assignShop") }}</th>
|
||||
<th>{{ $t("admin.created") }}</th>
|
||||
<th>{{ $t("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.display_name }}</td>
|
||||
<td><VBadge tone="blue">{{ $t(`admin.roles.${user.role}`) }}</VBadge></td>
|
||||
<td>
|
||||
<div class="grid min-w-[190px] gap-1.5">
|
||||
<select
|
||||
v-model="draftFor(user).role"
|
||||
class="rounded-md border border-border bg-surface px-3 py-2 text-sm text-text focus:border-primary focus:outline-2 focus:outline-primary/30"
|
||||
:aria-label="$t('admin.role')"
|
||||
@change="normalizeDraft(draftFor(user))"
|
||||
>
|
||||
<option value="platform_admin">{{ $t("admin.roles.platform_admin") }}</option>
|
||||
<option value="shop_owner">{{ $t("admin.roles.shop_owner") }}</option>
|
||||
<option value="shop_staff">{{ $t("admin.roles.shop_staff") }}</option>
|
||||
<option value="customer">{{ $t("admin.roles.customer") }}</option>
|
||||
</select>
|
||||
<select
|
||||
v-if="roleNeedsShop(draftFor(user).role)"
|
||||
v-model="draftFor(user).shopId"
|
||||
class="rounded-md border border-border bg-surface px-3 py-2 text-sm text-text focus:border-primary focus:outline-2 focus:outline-primary/30"
|
||||
:aria-label="$t('admin.assignShop')"
|
||||
>
|
||||
<option :value="null">{{ $t("admin.noAssignedShop") }}</option>
|
||||
<option v-for="shop in shops" :key="shop.id" :value="shop.id">{{ tText(shop.name, locale) }}</option>
|
||||
</select>
|
||||
<span v-else class="text-sm text-muted">{{ shopName(user.shop_id) }}</span>
|
||||
</div>
|
||||
<p v-if="rowErrors[user.id]" class="my-1 text-xs text-danger" role="alert">{{ rowErrors[user.id] }}</p>
|
||||
</td>
|
||||
<td>{{ formatDate(user.created_at) }}</td>
|
||||
<td>
|
||||
<VBtn size="sm" variant="primary" :disabled="savingId === user.id" @click="saveRole(user)">
|
||||
{{ savingId === user.id ? $t("common.loading") : $t("common.save") }}
|
||||
</VBtn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable></div></div>
|
||||
<div v-if="!loading && !errorMessage && users.length > 0" class="mt-4 flex items-center justify-center gap-3">
|
||||
<VBtn size="sm" :disabled="page <= 1" @click="changePage(page - 1)">{{ $t("common.prev") }}</VBtn>
|
||||
<span class="text-sm text-muted">{{ page }} / {{ totalPages }}</span>
|
||||
<VBtn size="sm" :disabled="page >= totalPages" @click="changePage(page + 1)">{{ $t("common.next") }}</VBtn>
|
||||
</div>
|
||||
<p v-if="loading" class="muted">{{ $t("common.loading") }}</p>
|
||||
<p v-else-if="errorMessage" class="error-text" role="alert">{{ errorMessage }}</p>
|
||||
<div v-else-if="users.length === 0" class="card muted">{{ $t("common.empty") }}</div>
|
||||
<div v-else class="table-wrap card">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t("common.email") }}</th>
|
||||
<th>{{ $t("common.displayName") }}</th>
|
||||
<th>{{ $t("admin.role") }}</th>
|
||||
<th>{{ $t("admin.assignShop") }}</th>
|
||||
<th>{{ $t("admin.created") }}</th>
|
||||
<th>{{ $t("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.display_name }}</td>
|
||||
<td>
|
||||
<span class="badge blue">{{ $t(`admin.roles.${user.role}`) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="role-editor">
|
||||
<select
|
||||
v-model="draftFor(user).role"
|
||||
:aria-label="$t('admin.role')"
|
||||
@change="normalizeDraft(draftFor(user))"
|
||||
>
|
||||
<option value="platform_admin">{{ $t("admin.roles.platform_admin") }}</option>
|
||||
<option value="shop_owner">{{ $t("admin.roles.shop_owner") }}</option>
|
||||
<option value="shop_staff">{{ $t("admin.roles.shop_staff") }}</option>
|
||||
<option value="customer">{{ $t("admin.roles.customer") }}</option>
|
||||
</select>
|
||||
<select
|
||||
v-if="roleNeedsShop(draftFor(user).role)"
|
||||
v-model="draftFor(user).shopId"
|
||||
:aria-label="$t('admin.assignShop')"
|
||||
>
|
||||
<option :value="null">{{ $t("admin.noAssignedShop") }}</option>
|
||||
<option v-for="shop in shops" :key="shop.id" :value="shop.id">
|
||||
{{ tText(shop.name, locale) }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-else class="muted">{{ shopName(user.shop_id) }}</span>
|
||||
</div>
|
||||
<p v-if="rowErrors[user.id]" class="error-text row-error" role="alert">{{ rowErrors[user.id] }}</p>
|
||||
</td>
|
||||
<td>{{ formatDate(user.created_at) }}</td>
|
||||
<td>
|
||||
<button class="btn sm primary" type="button" :disabled="savingId === user.id" @click="saveRole(user)">
|
||||
{{ savingId === user.id ? $t("common.loading") : $t("common.save") }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-if="!loading && !errorMessage && users.length > 0" class="pagination">
|
||||
<button class="btn sm" type="button" :disabled="page <= 1" @click="changePage(page - 1)">
|
||||
{{ $t("common.prev") }}
|
||||
</button>
|
||||
<span class="muted">{{ page }} / {{ totalPages }}</span>
|
||||
<button class="btn sm" type="button" :disabled="page >= totalPages" @click="changePage(page + 1)">
|
||||
{{ $t("common.next") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</VPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-wrap { overflow-x: auto; padding: 0; }
|
||||
.table { min-width: 920px; }
|
||||
.role-editor { display: grid; gap: 6px; min-width: 190px; }
|
||||
.row-error { font-size: 12px; margin: 4px 0 0; }
|
||||
.pagination { align-items: center; display: flex; gap: 12px; justify-content: center; margin-top: 16px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user