wip(freight): migration 0017 + model/column-list groundwork for add-freight-templates

This commit is contained in:
Chengdong Zhang
2026-09-23 17:39:45 +08:00
parent a83e857bdf
commit 5b426486ac
105 changed files with 6193 additions and 1566 deletions
+82 -58
View File
@@ -111,66 +111,90 @@ onMounted(() => {
<template>
<VPage :title="$t('nav.users')">
<template #actions>
<span v-if="!loading" class="text-sm text-muted">{{ $t("admin.pageOf", { page, total: totalPages }) }}</span>
<span v-if="!loading" class="text-muted text-sm">{{
$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>
<p v-if="loading" class="text-muted text-sm">{{ $t("common.loading") }}</p>
<p v-else-if="errorMessage" class="text-danger my-2 text-sm" 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 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="border-border bg-surface text-text focus:border-primary focus:outline-primary/30 rounded-md border px-3 py-2 text-sm focus:outline-2"
: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="border-border bg-surface text-text focus:border-primary focus:outline-primary/30 rounded-md border px-3 py-2 text-sm focus:outline-2"
: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-muted text-sm">{{ shopName(user.shop_id) }}</span>
</div>
<p v-if="rowErrors[user.id]" class="text-danger my-1 text-xs" 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-muted text-sm">{{ page }} / {{ totalPages }}</span>
<VBtn size="sm" :disabled="page >= totalPages" @click="changePage(page + 1)">{{
$t("common.next")
}}</VBtn>
</div>
</VPage>
</template>