From 3d6aa6026c5070903aa5813aa64756014e905060 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Tue, 28 Jul 2026 16:42:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20tag=E6=A0=B7=E5=BC=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StatusTag/StatusTag.module.less | 123 ++++++++++++++++++ src/components/StatusTag/StatusTag.tsx | 37 ++++++ src/components/StatusTag/index.ts | 2 + src/components/index.ts | 4 +- src/pages/events/banner/index.tsx | 2 +- .../events/banner/model/useBannerModel.ts | 39 +++--- .../events/list/model/useEventListModel.ts | 20 +-- .../orders/components/OrderDetailModal.tsx | 13 +- .../events/orders/model/useOrderModel.ts | 31 ++--- src/pages/events/users/model/useUserModel.ts | 15 ++- src/pages/finance/reports/index.tsx | 2 +- .../finance/wallet/model/useWalletModel.ts | 22 ++-- src/pages/system/roles/model/useRoleModel.ts | 15 ++- src/pages/system/users/model/useUserModel.ts | 30 ++--- 14 files changed, 264 insertions(+), 91 deletions(-) create mode 100644 src/components/StatusTag/StatusTag.module.less create mode 100644 src/components/StatusTag/StatusTag.tsx create mode 100644 src/components/StatusTag/index.ts diff --git a/src/components/StatusTag/StatusTag.module.less b/src/components/StatusTag/StatusTag.module.less new file mode 100644 index 0000000..57156b2 --- /dev/null +++ b/src/components/StatusTag/StatusTag.module.less @@ -0,0 +1,123 @@ +.statusTag { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 1px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + line-height: 20px; + white-space: nowrap; + border: 1px solid transparent; + letter-spacing: 0.2px; + + &.dimmed { + opacity: 0.55; + } + + .dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + } +} + +.primary { + color: #1677ff; + background: rgba(22, 119, 255, 0.08); + border-color: rgba(22, 119, 255, 0.18); + + .dot { + background: #1677ff; + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.15); + } +} + +.success { + color: #389e0d; + background: rgba(82, 196, 26, 0.1); + border-color: rgba(82, 196, 26, 0.22); + + .dot { + background: #52c41a; + box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.15); + } +} + +.warning { + color: #d48806; + background: rgba(250, 173, 20, 0.12); + border-color: rgba(250, 173, 20, 0.28); + + .dot { + background: #faad14; + box-shadow: 0 0 0 2px rgba(250, 173, 20, 0.18); + } +} + +.danger { + color: #cf1322; + background: rgba(255, 77, 79, 0.08); + border-color: rgba(255, 77, 79, 0.2); + + .dot { + background: #ff4d4f; + box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.15); + } +} + +.orange { + color: #d46b08; + background: rgba(250, 140, 22, 0.1); + border-color: rgba(250, 140, 22, 0.22); + + .dot { + background: #fa8c16; + box-shadow: 0 0 0 2px rgba(250, 140, 22, 0.15); + } +} + +.cyan { + color: #08979c; + background: rgba(19, 194, 194, 0.1); + border-color: rgba(19, 194, 194, 0.22); + + .dot { + background: #13c2c2; + box-shadow: 0 0 0 2px rgba(19, 194, 194, 0.15); + } +} + +.purple { + color: #531dab; + background: rgba(114, 46, 209, 0.08); + border-color: rgba(114, 46, 209, 0.2); + + .dot { + background: #722ed1; + box-shadow: 0 0 0 2px rgba(114, 46, 209, 0.15); + } +} + +.neutral { + color: rgba(0, 0, 0, 0.55); + background: rgba(0, 0, 0, 0.04); + border-color: rgba(0, 0, 0, 0.08); + + .dot { + background: rgba(0, 0, 0, 0.25); + box-shadow: none; + } +} + +.default { + color: rgba(0, 0, 0, 0.65); + background: #fafafa; + border-color: #f0f0f0; + + .dot { + background: rgba(0, 0, 0, 0.2); + box-shadow: none; + } +} diff --git a/src/components/StatusTag/StatusTag.tsx b/src/components/StatusTag/StatusTag.tsx new file mode 100644 index 0000000..59d47bd --- /dev/null +++ b/src/components/StatusTag/StatusTag.tsx @@ -0,0 +1,37 @@ +import { defineComponent, type PropType } from 'vue'; +import styles from './StatusTag.module.less'; + +export type StatusTagTone = + | 'primary' + | 'success' + | 'warning' + | 'danger' + | 'orange' + | 'cyan' + | 'purple' + | 'neutral' + | 'default'; + +const StatusTag = defineComponent({ + name: 'StatusTag', + props: { + label: { type: String, required: true }, + tone: { type: String as PropType, default: 'default' }, + showDot: { type: Boolean, default: true }, + dimmed: { type: Boolean, default: false }, + }, + setup(props) { + return () => ( + + {props.showDot ? : null} + {props.label} + + ); + }, +}); + +export default StatusTag; diff --git a/src/components/StatusTag/index.ts b/src/components/StatusTag/index.ts new file mode 100644 index 0000000..50b9a0f --- /dev/null +++ b/src/components/StatusTag/index.ts @@ -0,0 +1,2 @@ +export { default as StatusTag } from './StatusTag'; +export type { StatusTagTone } from './StatusTag'; diff --git a/src/components/index.ts b/src/components/index.ts index cfe65a6..fc4b600 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,4 +4,6 @@ */ export { default as HelloWorld } from './HelloWorld'; export { default as ChangePasswordModal } from './ChangePasswordModal'; -export { default as PageTabs } from './pageTabs/PageTabs'; +export { default as PageTabs } from './PageTabs/PageTabs'; +export { StatusTag } from './StatusTag'; +export type { StatusTagTone } from './StatusTag'; diff --git a/src/pages/events/banner/index.tsx b/src/pages/events/banner/index.tsx index 39fbbec..71865f0 100644 --- a/src/pages/events/banner/index.tsx +++ b/src/pages/events/banner/index.tsx @@ -65,7 +65,7 @@ function renderBodyCell({ ); } - // 跳转类型列:彩色 Tag + // 跳转类型列:StatusTag if (column.dataIndex === 'jumpType') { return renderJumpType(text); } diff --git a/src/pages/events/banner/model/useBannerModel.ts b/src/pages/events/banner/model/useBannerModel.ts index 1cbab9f..9ee00f5 100644 --- a/src/pages/events/banner/model/useBannerModel.ts +++ b/src/pages/events/banner/model/useBannerModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; -import { message, Tag } from 'ant-design-vue'; +import { message } from 'ant-design-vue'; +import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; // ============================================================ @@ -21,18 +22,18 @@ export const JUMP_TYPE_OPTIONS = [ { value: 'none', label: '无链接' }, ] as const; -/** 跳转类型映射(用于表格 Tag 渲染) */ -const JUMP_TYPE_MAP: Record = { - event_detail: { label: '赛事详情页', color: 'blue' }, - miniprogram_page: { label: '小程序页面', color: 'cyan' }, - custom_link: { label: '自定义链接', color: 'purple' }, - none: { label: '无链接', color: 'default' }, +/** 跳转类型映射(用于表格 StatusTag 渲染) */ +const JUMP_TYPE_MAP: Record = { + event_detail: { label: '赛事详情页', tone: 'primary' }, + miniprogram_page: { label: '小程序页面', tone: 'cyan' }, + custom_link: { label: '自定义链接', tone: 'purple' }, + none: { label: '无链接', tone: 'default' }, }; /** Banner 状态映射 */ -const STATUS_MAP: Record = { - enabled: { label: '启用中', color: 'green' }, - disabled: { label: '已禁用', color: 'default' }, +const STATUS_MAP: Record = { + enabled: { label: '启用中', tone: 'success' }, + disabled: { label: '已禁用', tone: 'default', dimmed: true }, }; /** 关联赛事选项(mock) */ @@ -153,7 +154,6 @@ export function useBannerModel() { dataIndex: 'jumpType', key: 'jumpType', width: 120, - align: 'center' as const, }, { title: '关联赛事', dataIndex: 'relatedEvent', key: 'relatedEvent', width: 200 }, { title: '所属区域', key: 'region', width: 140 }, @@ -282,18 +282,17 @@ export function useBannerModel() { // ===== 供页面 bodyCell 使用的渲染工具 ===== const renderJumpType = (type: string) => { - const info = JUMP_TYPE_MAP[type] || { label: type || '-', color: 'default' }; - return h(Tag, { color: info.color }, () => info.label); + const info = JUMP_TYPE_MAP[type] || { label: type || '-', tone: 'default' as const }; + return h(StatusTag, { label: info.label, tone: info.tone }); }; const renderStatus = (status: string) => { - const info = STATUS_MAP[status] || { label: status || '-', color: 'default' }; - const tagProps: any = { color: info.color }; - // 已禁用状态添加半透明样式 - if (status === 'disabled') { - tagProps.style = { opacity: 0.5 }; - } - return h(Tag, tagProps, () => info.label); + const info = STATUS_MAP[status] || { label: status || '-', tone: 'default' as const }; + return h(StatusTag, { + label: info.label, + tone: info.tone, + dimmed: info.dimmed, + }); }; return { diff --git a/src/pages/events/list/model/useEventListModel.ts b/src/pages/events/list/model/useEventListModel.ts index 1b32698..c58784c 100644 --- a/src/pages/events/list/model/useEventListModel.ts +++ b/src/pages/events/list/model/useEventListModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; -import { message, Tag } from 'ant-design-vue'; +import { message } from 'ant-design-vue'; +import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; // ============================================================ @@ -67,12 +68,12 @@ const MOCK_DATA = Array.from({ length: 12 }, (_, i) => ({ })); /** 状态标签映射 */ -const STATUS_MAP: Record = { - pending: { label: '待审核', color: 'gold' }, - enrolling: { label: '报名中', color: 'blue' }, - deleted: { label: '已删除', color: 'red' }, - removed: { label: '已下架', color: 'orange' }, - rejected: { label: '审核不通过', color: 'volcano' }, +const STATUS_MAP: Record = { + pending: { label: '待审核', tone: 'warning' }, + enrolling: { label: '报名中', tone: 'primary' }, + deleted: { label: '已删除', tone: 'neutral' }, + removed: { label: '已下架', tone: 'orange' }, + rejected: { label: '审核不通过', tone: 'danger' }, }; // ============================================================ @@ -169,9 +170,10 @@ export function useEventListModel() { dataIndex: 'status', key: 'status', width: 90, + align: 'center' as const, customRender: ({ text }: { text: string }) => { - const info = STATUS_MAP[text] || { label: text, color: 'default' }; - return h(Tag, { color: info.color }, () => info.label); + const info = STATUS_MAP[text] || { label: text, tone: 'default' as const }; + return h(StatusTag, { label: info.label, tone: info.tone }); }, }, { title: '报名人数', dataIndex: 'enrolledCount', key: 'enrolledCount', width: 90 }, diff --git a/src/pages/events/orders/components/OrderDetailModal.tsx b/src/pages/events/orders/components/OrderDetailModal.tsx index 426c0fe..0e03f80 100644 --- a/src/pages/events/orders/components/OrderDetailModal.tsx +++ b/src/pages/events/orders/components/OrderDetailModal.tsx @@ -1,5 +1,6 @@ import { defineComponent, ref, computed } from 'vue'; -import { Modal, Table, Button, Tag, Pagination, Image, Space } from 'ant-design-vue'; +import { Modal, Table, Button, Pagination, Image, Space } from 'ant-design-vue'; +import { StatusTag, type StatusTagTone } from '@/components'; import styles from './OrderDetailModal.module.less'; interface OrderDetailModalProps { @@ -69,9 +70,9 @@ const PLAYER_COLUMNS = [ }, ]; -const REFUND_STATUS_MAP: Record = { - success: { label: '退款成功', color: 'green' }, - failed: { label: '退款失败', color: 'red' }, +const REFUND_STATUS_MAP: Record = { + success: { label: '退款成功', tone: 'success' }, + failed: { label: '退款失败', tone: 'danger' }, }; export default defineComponent({ @@ -203,14 +204,14 @@ export default defineComponent({ {pagedRefunds.value.map((r, idx) => { const statusInfo = REFUND_STATUS_MAP[r.status] || { label: '-', - color: 'default', + tone: 'default' as const, }; const realIdx = (refundPage.value - 1) * refundPageSize.value + idx + 1; return (
第{realIdx}次退款 - {statusInfo.label} +
diff --git a/src/pages/events/orders/model/useOrderModel.ts b/src/pages/events/orders/model/useOrderModel.ts index 8145fa7..8385bac 100644 --- a/src/pages/events/orders/model/useOrderModel.ts +++ b/src/pages/events/orders/model/useOrderModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; -import { message, Tag } from 'ant-design-vue'; +import { message } from 'ant-design-vue'; +import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; // ============================================================ @@ -22,18 +23,18 @@ export const REFUND_STATUS_OPTIONS = [ { value: 'refund_failed', label: '退款失败' }, ] as const; -/** 订单状态 Tag 映射 */ -const ORDER_STATUS_MAP: Record = { - paid: { label: '已支付', color: 'green' }, - partial_refund: { label: '部分退款', color: 'orange' }, - full_refund: { label: '全部退款', color: 'blue' }, - closed: { label: '已关闭', color: 'default' }, +/** 订单状态 StatusTag 映射 */ +const ORDER_STATUS_MAP: Record = { + paid: { label: '已支付', tone: 'success' }, + partial_refund: { label: '部分退款', tone: 'orange' }, + full_refund: { label: '全部退款', tone: 'primary' }, + closed: { label: '已关闭', tone: 'default' }, }; -/** 退款状态 Tag 映射 */ -const REFUND_STATUS_MAP: Record = { - refund_success: { label: '退款成功', color: 'green' }, - refund_failed: { label: '退款失败', color: 'red' }, +/** 退款状态 StatusTag 映射 */ +const REFUND_STATUS_MAP: Record = { + refund_success: { label: '退款成功', tone: 'success' }, + refund_failed: { label: '退款失败', tone: 'danger' }, }; // ============================================================ @@ -254,8 +255,8 @@ export function useOrderModel() { width: 100, align: 'center' as const, customRender: ({ text }: { text: string }) => { - const info = ORDER_STATUS_MAP[text] || { label: text || '-', color: 'default' }; - return h(Tag, { color: info.color }, () => info.label); + const info = ORDER_STATUS_MAP[text] || { label: text || '-', tone: 'default' as const }; + return h(StatusTag, { label: info.label, tone: info.tone }); }, }, { @@ -266,8 +267,8 @@ export function useOrderModel() { align: 'center' as const, customRender: ({ text }: { text: string }) => { if (!text) return h('span', '-'); - const info = REFUND_STATUS_MAP[text] || { label: text, color: 'default' }; - return h(Tag, { color: info.color }, () => info.label); + const info = REFUND_STATUS_MAP[text] || { label: text, tone: 'default' as const }; + return h(StatusTag, { label: info.label, tone: info.tone }); }, }, ]; diff --git a/src/pages/events/users/model/useUserModel.ts b/src/pages/events/users/model/useUserModel.ts index c2c6e3e..2b43238 100644 --- a/src/pages/events/users/model/useUserModel.ts +++ b/src/pages/events/users/model/useUserModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; -import { message, Tag } from 'ant-design-vue'; +import { message } from 'ant-design-vue'; +import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; // ============================================================ @@ -13,10 +14,10 @@ export const USER_STATUS_OPTIONS = [ { value: 'disabled', label: '禁用' }, ] as const; -/** 用户状态 Tag 映射 */ -const USER_STATUS_MAP: Record = { - active: { label: '正常', color: 'green' }, - disabled: { label: '禁用', color: 'red' }, +/** 用户状态 StatusTag 映射 */ +const USER_STATUS_MAP: Record = { + active: { label: '正常', tone: 'success' }, + disabled: { label: '禁用', tone: 'danger' }, }; // ============================================================ @@ -94,8 +95,8 @@ export function useUserModel() { width: 90, align: 'center' as const, customRender: ({ text }: { text: string }) => { - const info = USER_STATUS_MAP[text] || { label: text, color: 'default' }; - return h(Tag, { color: info.color }, () => info.label); + const info = USER_STATUS_MAP[text] || { label: text, tone: 'default' as const }; + return h(StatusTag, { label: info.label, tone: info.tone }); }, }, { title: '登录时间', dataIndex: 'loginTime', key: 'loginTime', width: 170 }, diff --git a/src/pages/finance/reports/index.tsx b/src/pages/finance/reports/index.tsx index db858a9..fb91322 100644 --- a/src/pages/finance/reports/index.tsx +++ b/src/pages/finance/reports/index.tsx @@ -170,7 +170,7 @@ export default defineComponent({ value={range.value} onUpdate:value={(val: string) => handleRangeChange(val)} options={[...RANGE_OPTIONS]} - style={{ width: 120 }} + style={{ width: '120px' }} />
diff --git a/src/pages/finance/wallet/model/useWalletModel.ts b/src/pages/finance/wallet/model/useWalletModel.ts index 1da1173..e334496 100644 --- a/src/pages/finance/wallet/model/useWalletModel.ts +++ b/src/pages/finance/wallet/model/useWalletModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; -import { message, Tag } from 'ant-design-vue'; +import { message } from 'ant-design-vue'; +import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; // ============================================================ @@ -22,11 +23,11 @@ export const TRANSACTION_STATUS_OPTIONS = [ { value: '已解冻', label: '已解冻' }, ] as const; -/** 明细状态映射(用于表格 Tag 渲染) */ -const TRANSACTION_STATUS_MAP: Record = { - 正常: { label: '正常', color: 'blue' }, - 冻结中: { label: '冻结中', color: 'orange' }, - 已解冻: { label: '已解冻', color: 'green' }, +/** 明细状态映射(用于表格 StatusTag 渲染) */ +const TRANSACTION_STATUS_MAP: Record = { + 正常: { label: '正常', tone: 'primary' }, + 冻结中: { label: '冻结中', tone: 'orange' }, + 已解冻: { label: '已解冻', tone: 'success' }, }; // ============================================================ @@ -319,10 +320,13 @@ export function useWalletModel() { } }; - /** 渲染交易状态 Tag */ + /** 渲染交易状态 StatusTag */ const renderTransactionStatus = (status: string) => { - const info = TRANSACTION_STATUS_MAP[status] || { label: status || '-', color: 'default' }; - return h(Tag, { color: info.color }, () => info.label); + const info = TRANSACTION_STATUS_MAP[status] || { + label: status || '-', + tone: 'default' as const, + }; + return h(StatusTag, { label: info.label, tone: info.tone }); }; return { diff --git a/src/pages/system/roles/model/useRoleModel.ts b/src/pages/system/roles/model/useRoleModel.ts index d5ff605..76693bd 100644 --- a/src/pages/system/roles/model/useRoleModel.ts +++ b/src/pages/system/roles/model/useRoleModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; -import { message, Tag } from 'ant-design-vue'; +import { message } from 'ant-design-vue'; +import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; // ============================================================ @@ -132,9 +133,9 @@ const MOCK_USERS: Record = { // ============================================================ // 状态文本/颜色映射 // ============================================================ -const STATUS_MAP: Record = { - active: { label: '正常', color: 'green' }, - disabled: { label: '禁用', color: 'red' }, +const STATUS_MAP: Record = { + active: { label: '正常', tone: 'success' }, + disabled: { label: '禁用', tone: 'danger' }, }; // ============================================================ @@ -288,10 +289,10 @@ export function useRoleModel() { /** 根据 roleId 获取该角色下的用户列表 */ const getRoleUsers = (roleId: string) => MOCK_USERS[roleId] || []; - /** 状态 Tag 渲染 */ + /** 状态 StatusTag 渲染 */ const renderStatus = (status: string) => { - const info = STATUS_MAP[status] || { label: status || '-', color: 'default' }; - return h(Tag, { color: info.color }, () => info.label); + const info = STATUS_MAP[status] || { label: status || '-', tone: 'default' as const }; + return h(StatusTag, { label: info.label, tone: info.tone }); }; return { diff --git a/src/pages/system/users/model/useUserModel.ts b/src/pages/system/users/model/useUserModel.ts index 4dde2cb..e885fe1 100644 --- a/src/pages/system/users/model/useUserModel.ts +++ b/src/pages/system/users/model/useUserModel.ts @@ -1,5 +1,6 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; -import { message, Tag } from 'ant-design-vue'; +import { message } from 'ant-design-vue'; +import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; // ============================================================ @@ -19,17 +20,17 @@ export const USER_STATUS_OPTIONS = [ { value: 'disabled', label: '禁用' }, ] as const; -/** 角色文本/颜色映射(表格渲染) */ -const ROLE_MAP: Record = { - admin: { label: '管理员', color: 'blue' }, - operator: { label: '运营人员', color: 'cyan' }, - auditor: { label: '审核人员', color: 'purple' }, +/** 角色文本/色调映射(表格渲染) */ +const ROLE_MAP: Record = { + admin: { label: '管理员', tone: 'primary' }, + operator: { label: '运营人员', tone: 'cyan' }, + auditor: { label: '审核人员', tone: 'purple' }, }; -/** 状态文本/颜色映射 */ -const STATUS_MAP: Record = { - active: { label: '正常', color: 'green' }, - disabled: { label: '禁用', color: 'red' }, +/** 状态文本/色调映射 */ +const STATUS_MAP: Record = { + active: { label: '正常', tone: 'success' }, + disabled: { label: '禁用', tone: 'danger' }, }; // ============================================================ @@ -103,10 +104,9 @@ export function useUserModel() { dataIndex: 'role', key: 'role', width: 120, - align: 'center' as const, customRender: ({ text }: { text: string }) => { - const info = ROLE_MAP[text] || { label: text || '-', color: 'default' }; - return h(Tag, { color: info.color }, () => info.label); + const info = ROLE_MAP[text] || { label: text || '-', tone: 'default' as const }; + return h(StatusTag, { label: info.label, tone: info.tone }); }, }, { @@ -116,8 +116,8 @@ export function useUserModel() { width: 100, align: 'center' as const, customRender: ({ text }: { text: string }) => { - const info = STATUS_MAP[text] || { label: text || '-', color: 'default' }; - return h(Tag, { color: info.color }, () => info.label); + const info = STATUS_MAP[text] || { label: text || '-', tone: 'default' as const }; + return h(StatusTag, { label: info.label, tone: info.tone }); }, }, { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },