feat: tag组件

This commit is contained in:
ggyy
2026-04-25 19:13:58 +08:00
parent e5dd817d74
commit f29fbd53ee
10 changed files with 147 additions and 39 deletions

40
components/StatusTag.vue Normal file
View File

@@ -0,0 +1,40 @@
<template>
<span :class="classes">
<slot />
</span>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = withDefaults(defineProps<{
type?: "primary" | "success" | "danger" | "warning" | "default";
size?: "sm" | "md" | "lg";
variant?: "solid" | "outline" | "pill";
}>(), {
type: "default",
size: "sm",
variant: "solid",
});
const colorMap = {
primary: { solid: "bg-blue-500 text-white", outline: "border border-blue-500 text-blue-500" },
success: { solid: "bg-green-500 text-white", outline: "border border-green-500 text-green-500" },
danger: { solid: "bg-red-500 text-white", outline: "border border-red-500 text-red-500" },
warning: { solid: "bg-orange-400 text-white", outline: "border border-orange-400 text-orange-400" },
default: { solid: "bg-gray-200 text-gray-600", outline: "border border-gray-400 text-gray-500" },
};
const sizeMap = {
sm: "text-xs px-2 py-0.5",
md: "text-sm px-2.5 py-0.5",
lg: "text-base px-3 py-1",
};
const classes = computed(() => {
const color = colorMap[props.type][props.variant === "outline" ? "outline" : "solid"];
const size = sizeMap[props.size];
const radius = props.variant === "pill" ? "rounded-full" : "rounded";
return `inline-flex items-center font-medium ${color} ${size} ${radius}`;
});
</script>

View File

@@ -1,5 +1,50 @@
# 公共组件文档
## StatusTag
状态标签组件,用于展示订单状态、支付状态、发货状态等。
### Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `type` | `"primary" \| "success" \| "danger" \| "warning" \| "default"` | `"default"` | 颜色类型 |
| `size` | `"sm" \| "md" \| "lg"` | `"sm"` | 大小 |
| `variant` | `"solid" \| "outline" \| "pill"` | `"solid"` | 样式风格 |
### 颜色对应
| type | 颜色 | 适用场景 |
|------|------|----------|
| `primary` | 蓝色 | 主要操作、信息 |
| `success` | 绿色 | 已完成、已支付、已发货 |
| `danger` | 红色 | 失败、错误 |
| `warning` | 橙色 | 待处理、未支付、未发货 |
| `default` | 灰色 | 已关闭、中性状态 |
### 基本用法
```components/StatusTag.vue#L1-3
<StatusTag type="success">已支付</StatusTag>
<StatusTag type="warning">待处理</StatusTag>
<StatusTag type="danger">发货失败</StatusTag>
```
### 配合 order-status 工具函数
`lib/utils/order-status.ts` 提供了对应的 type 辅助函数:
- `getOrderStatusType(status)` — 订单状态 → type
- `getPaymentStatusType(status)` — 支付状态 → type
- `getDeliveryStatusType(status)` — 发货状态 → type
```components/StatusTag.vue#L1-5
<StatusTag :type="getOrderStatusType(order.status)">
{{ getOrderStatusLabel(order.status) }}
</StatusTag>
```
## SecretInput
带显示/隐藏切换的密钥输入框用于密码、API Secret 等敏感字段。

View File

@@ -52,6 +52,35 @@ export function getPaymentProviderLabel(provider: string) {
}
}
export function getOrderStatusType(status: string): "warning" | "success" | "primary" | "danger" | "default" {
switch (status) {
case "PENDING": return "warning";
case "PAID": return "success";
case "DELIVERED": return "success";
case "CLOSED": return "default";
case "FAILED": return "danger";
default: return "default";
}
}
export function getPaymentStatusType(status: string): "warning" | "success" | "danger" | "default" {
switch (status) {
case "UNPAID": return "warning";
case "PAID": return "success";
case "FAILED": return "danger";
default: return "default";
}
}
export function getDeliveryStatusType(status: string): "warning" | "success" | "danger" | "default" {
switch (status) {
case "NOT_DELIVERED": return "warning";
case "DELIVERED": return "success";
case "FAILED": return "danger";
default: return "default";
}
}
export function getVerifyStatusLabel(status: string) {
switch (status) {
case "PENDING":

View File

@@ -78,7 +78,7 @@
<code>{{ value }}</code>
</template>
<template #status="{ value }">
<span class="badge" :class="getStatusBadgeClass(value)">{{ getStatusLabel(value) }}</span>
<StatusTag :type="getCardStatusType(value)">{{ getStatusLabel(value) }}</StatusTag>
</template>
<template #createdAt="{ value }">
{{ formatDate(value) }}
@@ -106,6 +106,7 @@ import { onImportCards } from "./importCards.telefunc";
import { onQueryCards } from "./queryCards.telefunc";
import { onDeleteCard } from "./deleteCard.telefunc";
import DataTable from "../../../components/DataTable.vue";
import StatusTag from "../../../components/StatusTag.vue";
import type { Data } from "./+data";
const { cards, products, overview } = useData<Data>();
@@ -141,8 +142,8 @@ function getStatusLabel(status: string) {
return ({ UNUSED: "未售出", SOLD: "已售出", LOCKED: "锁定中", INVALID: "已失效" } as Record<string, string>)[status] || status;
}
function getStatusBadgeClass(status: string) {
return ({ UNUSED: "badge-success", SOLD: "badge-ghost", LOCKED: "badge-warning", INVALID: "badge-error" } as Record<string, string>)[status] || "badge-ghost";
function getCardStatusType(status: string): "success" | "default" | "warning" | "danger" {
return ({ UNUSED: "success", SOLD: "default", LOCKED: "warning", INVALID: "danger" } as Record<string, "success" | "default" | "warning" | "danger">)[status] ?? "default";
}
async function fetchPage(page: number) {

View File

@@ -64,9 +64,9 @@
<td>{{ category.slug }}</td>
<td>{{ category.sort }}</td>
<td>
<span class="badge" :class="category.status === 'ACTIVE' ? 'badge-success' : 'badge-ghost'">
<StatusTag :type="category.status === 'ACTIVE' ? 'success' : 'default'">
{{ category.status === 'ACTIVE' ? '启用' : '停用' }}
</span>
</StatusTag>
</td>
<td>
<div class="flex gap-2">
@@ -90,6 +90,7 @@
import { normalizeTelefuncError } from "../../../lib/app-error";
import { reactive, ref } from "vue";
import { useData } from "vike-vue/useData";
import StatusTag from "../../../components/StatusTag.vue";
import { onDeleteCategory } from "./deleteCategory.telefunc";
import { onSaveCategory } from "./saveCategory.telefunc";
import { onToggleCategory } from "./toggleCategory.telefunc";

View File

@@ -119,7 +119,7 @@
<td class="font-mono text-sm">{{ item.id }}</td>
<td>{{ item.name || '-' }}</td>
<td>
<span class="badge badge-outline">{{ getChannelLabel(item.provider) }}</span>
<StatusTag variant="outline">{{ getChannelLabel(item.provider) }}</StatusTag>
</td>
<td>{{ item.fromEmail || '-' }}</td>
<td>
@@ -128,9 +128,9 @@
<span v-else>{{ (item as any).cloudflareBindingName || '-' }}</span>
</td>
<td>
<span class="badge" :class="item.isEnabled ? 'badge-success' : 'badge-ghost'">
<StatusTag :type="item.isEnabled ? 'success' : 'default'">
{{ item.isEnabled ? '已激活' : '未激活' }}
</span>
</StatusTag>
</td>
<td>
<div class="flex items-center gap-2">
@@ -410,9 +410,9 @@
<td class="whitespace-nowrap">{{ configs.find(c => c.provider === log.provider)?.name || '-' }}</td>
<td class="whitespace-nowrap">{{ getSceneLabel(log.scene) }}</td>
<td>
<span class="badge whitespace-nowrap" :class="log.status === 'SUCCESS' ? 'badge-success' : 'badge-error'">
<StatusTag class="whitespace-nowrap" :type="log.status === 'SUCCESS' ? 'success' : 'danger'">
{{ log.status === 'SUCCESS' ? '成功' : '失败' }}
</span>
</StatusTag>
</td>
<td class="whitespace-nowrap">{{ log.toEmail }}</td>
<td class="max-w-xs truncate" :title="log.subject">{{ log.subject }}</td>
@@ -481,6 +481,7 @@
<script setup lang="ts">
import SecretInput from "../../../components/SecretInput.vue";
import StatusTag from "../../../components/StatusTag.vue";
import { normalizeTelefuncError } from "../../../lib/app-error";
import { reactive, ref, computed } from "vue";
import { useData } from "vike-vue/useData";

View File

@@ -38,9 +38,9 @@
<template #paymentProvider="{ value }">{{ getPaymentProviderLabel(value) }}</template>
<template #status="{ row }">
<div class="flex flex-wrap gap-1">
<span class="badge" :class="orderStatusClass(row.status)">{{ getOrderStatusLabel(row.status) }}</span>
<span class="badge" :class="paymentStatusClass(row.paymentStatus)">{{ getPaymentStatusLabel(row.paymentStatus) }}</span>
<span class="badge" :class="deliveryStatusClass(row.deliveryStatus)">{{ getDeliveryStatusLabel(row.deliveryStatus) }}</span>
<StatusTag :type="getOrderStatusType(row.status)">{{ getOrderStatusLabel(row.status) }}</StatusTag>
<StatusTag :type="getPaymentStatusType(row.paymentStatus)">{{ getPaymentStatusLabel(row.paymentStatus) }}</StatusTag>
<StatusTag :type="getDeliveryStatusType(row.deliveryStatus)">{{ getDeliveryStatusLabel(row.deliveryStatus) }}</StatusTag>
</div>
</template>
<template #createdAt="{ value }">{{ new Date(value).toLocaleString() }}</template>
@@ -57,7 +57,8 @@ import { reactive, ref } from "vue";
import { useData } from "vike-vue/useData";
import DataTable from "../../../components/DataTable.vue";
import { formatCents } from "../../../lib/utils/money";
import { getDeliveryStatusLabel, getOrderStatusLabel, getPaymentProviderLabel, getPaymentStatusLabel } from "../../../lib/utils/order-status";
import { getDeliveryStatusLabel, getDeliveryStatusType, getOrderStatusLabel, getOrderStatusType, getPaymentProviderLabel, getPaymentStatusLabel, getPaymentStatusType } from "../../../lib/utils/order-status";
import StatusTag from "../../../components/StatusTag.vue";
import { onQueryOrders } from "./queryOrders.telefunc";
import type { Data } from "./+data";
@@ -98,23 +99,7 @@ async function handleSearch() { await fetchPage(1); }
/**
* 获取订单状态对应的样式类,采用 badge-soft 提升可读性
*/
function orderStatusClass(s: string) {
return "badge-soft " + ({ PENDING: "badge-warning", PAID: "badge-info", DELIVERED: "badge-success", CLOSED: "badge-ghost", FAILED: "badge-error" }[s] ?? "badge-outline");
}
/**
* 获取支付状态对应的样式类
*/
function paymentStatusClass(s: string) {
return "badge-soft " + ({ UNPAID: "badge-warning", PAID: "badge-success", FAILED: "badge-error" }[s] ?? "badge-outline");
}
/**
* 获取发货状态对应的样式类
*/
function deliveryStatusClass(s: string) {
return "badge-soft " + ({ NOT_DELIVERED: "badge-warning", DELIVERED: "badge-success", FAILED: "badge-error" }[s] ?? "badge-outline");
}
async function handleReset() {
filter.orderNo = "";

View File

@@ -9,9 +9,9 @@
<p class="text-sm text-base-content/70">订单号{{ order.orderNo }}</p>
</div>
<div class="flex flex-wrap gap-2">
<span class="badge badge-outline">{{ getOrderStatusLabel(order.status) }}</span>
<span class="badge badge-outline">{{ getPaymentStatusLabel(order.paymentStatus) }}</span>
<span class="badge badge-outline">{{ getDeliveryStatusLabel(order.deliveryStatus) }}</span>
<StatusTag :type="getOrderStatusType(order.status)">{{ getOrderStatusLabel(order.status) }}</StatusTag>
<StatusTag :type="getPaymentStatusType(order.paymentStatus)">{{ getPaymentStatusLabel(order.paymentStatus) }}</StatusTag>
<StatusTag :type="getDeliveryStatusType(order.deliveryStatus)">{{ getDeliveryStatusLabel(order.deliveryStatus) }}</StatusTag>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
@@ -77,11 +77,15 @@ import { useData } from "vike-vue/useData";
import { formatCents } from "../../../../lib/utils/money";
import {
getDeliveryStatusLabel,
getDeliveryStatusType,
getOrderStatusLabel,
getOrderStatusType,
getPaymentProviderLabel,
getPaymentStatusLabel,
getPaymentStatusType,
getVerifyStatusLabel,
} from "../../../../lib/utils/order-status";
import StatusTag from "../../../../components/StatusTag.vue";
import { onCloseOrder } from "./closeOrder.telefunc";
import { onRedeliver } from "./redeliver.telefunc";
import type { Data } from "./+data";

View File

@@ -36,9 +36,9 @@
<td>{{ formatCents(product.price) }}</td>
<td>{{ product.minBuy }} - {{ product.maxBuy }}</td>
<td>
<span class="badge" :class="product.status === 'ACTIVE' ? 'badge-success' : 'badge-ghost'">
<StatusTag :type="product.status === 'ACTIVE' ? 'success' : 'default'">
{{ product.status === 'ACTIVE' ? '上架' : '下架' }}
</span>
</StatusTag>
</td>
<td>
<div class="flex gap-2">
@@ -59,6 +59,7 @@ import { normalizeTelefuncError } from "../../../lib/app-error";
import { ref } from "vue";
import { useData } from "vike-vue/useData";
import { formatCents } from "../../../lib/utils/money";
import StatusTag from "../../../components/StatusTag.vue";
import { onDeleteProduct } from "./deleteProduct.telefunc";
import type { Data } from "./+data";

View File

@@ -9,9 +9,9 @@
<h1 class="text-2xl font-bold">{{ order.orderNo }}</h1>
</div>
<div class="flex gap-2">
<span class="badge badge-outline">{{ getOrderStatusLabel(order.status) }}</span>
<span class="badge badge-outline">{{ getPaymentStatusLabel(order.paymentStatus) }}</span>
<span class="badge badge-outline">{{ getDeliveryStatusLabel(order.deliveryStatus) }}</span>
<StatusTag :type="getOrderStatusType(order.status)">{{ getOrderStatusLabel(order.status) }}</StatusTag>
<StatusTag :type="getPaymentStatusType(order.paymentStatus)">{{ getPaymentStatusLabel(order.paymentStatus) }}</StatusTag>
<StatusTag :type="getDeliveryStatusType(order.deliveryStatus)">{{ getDeliveryStatusLabel(order.deliveryStatus) }}</StatusTag>
</div>
</div>
</div>
@@ -54,7 +54,8 @@ import { normalizeTelefuncError } from "../../../lib/app-error";
import { ref, onMounted } from "vue";
import { useData } from "vike-vue/useData";
import { formatCents } from "../../../lib/utils/money";
import { getDeliveryStatusLabel, getOrderStatusLabel, getPaymentProviderLabel, getPaymentStatusLabel } from "../../../lib/utils/order-status";
import { getDeliveryStatusLabel, getDeliveryStatusType, getOrderStatusLabel, getOrderStatusType, getPaymentProviderLabel, getPaymentStatusLabel, getPaymentStatusType } from "../../../lib/utils/order-status";
import StatusTag from "../../../components/StatusTag.vue";
import { onCreatePayment } from "./createPayment.telefunc";
import { onQueryAlipayPayment } from "./queryAlipayPayment.telefunc";
import type { Data } from "./+data";