refactor(frontend): 完成所有组件的内联SVG统一替换为Icon组件
- 扩展 Icon.vue 组件,新增 60+ 图标路径 - 导航类: arrowRight, arrowLeft, arrowUp, arrowDown, chevronUp, externalLink - 状态类: checkCircle, xCircle, exclamationCircle, exclamationTriangle, infoCircle - 用户类: user, userCircle, userPlus, users - 文档类: document, clipboard, copy, inbox - 操作类: download, upload, filter, sort - 安全类: key, lock, shield - UI类: menu, calendar, home, terminal, gift, creditCard, mail - 数据类: chartBar, trendingUp, database, cube - 其他: bolt, sparkles, cloud, server, sun, moon, book 等 - 重构 56 个 Vue 组件,用 Icon 组件替换内联 SVG - 净减少约 2200 行代码 - 提升代码可维护性和一致性 - 统一图标样式和尺寸管理
This commit is contained in:
@@ -21,15 +21,7 @@
|
||||
class="-mr-2 rounded-xl p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:text-dark-500 dark:hover:bg-dark-700 dark:hover:text-dark-300"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<Icon name="x" size="md" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -50,6 +42,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onMounted, onUnmounted, ref, nextTick } from 'vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
// 生成唯一ID以避免多个对话框时ID冲突
|
||||
let dialogIdCounter = 0
|
||||
|
||||
@@ -66,19 +66,11 @@
|
||||
>
|
||||
<slot name="empty">
|
||||
<div class="flex flex-col items-center">
|
||||
<svg
|
||||
<Icon
|
||||
name="inbox"
|
||||
size="xl"
|
||||
class="mb-4 h-12 w-12 text-gray-400 dark:text-dark-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
||||
/>
|
||||
</svg>
|
||||
/>
|
||||
<p class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ t('empty.noData') }}
|
||||
</p>
|
||||
@@ -117,6 +109,7 @@
|
||||
import { computed, ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { Column } from './types'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -6,33 +6,17 @@
|
||||
:class="['date-picker-trigger', isOpen && 'date-picker-trigger-open']"
|
||||
>
|
||||
<span class="date-picker-icon">
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="calendar" size="sm" />
|
||||
</span>
|
||||
<span class="date-picker-value">
|
||||
{{ displayValue }}
|
||||
</span>
|
||||
<span class="date-picker-chevron">
|
||||
<svg
|
||||
:class="['h-4 w-4 transition-transform duration-200', isOpen && 'rotate-180']"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
<Icon
|
||||
name="chevronDown"
|
||||
size="sm"
|
||||
:class="['transition-transform duration-200', isOpen && 'rotate-180']"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -65,19 +49,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="date-picker-separator">
|
||||
<svg
|
||||
class="h-4 w-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25L21 12m0 0l-3.75 3.75M21 12H3"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="arrowRight" size="sm" class="text-gray-400" />
|
||||
</div>
|
||||
<div class="date-picker-field">
|
||||
<label class="date-picker-label">{{ t('dates.endDate') }}</label>
|
||||
@@ -106,6 +78,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
interface DatePreset {
|
||||
labelKey: string
|
||||
|
||||
@@ -43,16 +43,7 @@
|
||||
@click="!actionTo && $emit('action')"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<svg
|
||||
v-if="actionIcon"
|
||||
class="mr-2 h-5 w-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<Icon v-if="actionIcon" name="plus" size="md" class="mr-2" />
|
||||
{{ actionText }}
|
||||
</component>
|
||||
</slot>
|
||||
@@ -64,6 +55,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { Component } from 'vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -7,16 +7,12 @@
|
||||
>
|
||||
<span class="text-base">{{ currentLocale?.flag }}</span>
|
||||
<span class="hidden sm:inline">{{ currentLocale?.code.toUpperCase() }}</span>
|
||||
<svg
|
||||
class="h-3.5 w-3.5 text-gray-400 transition-transform duration-200"
|
||||
<Icon
|
||||
name="chevronDown"
|
||||
size="xs"
|
||||
class="text-gray-400 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': isOpen }"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
|
||||
<transition name="dropdown">
|
||||
@@ -36,16 +32,7 @@
|
||||
>
|
||||
<span class="text-base">{{ locale.flag }}</span>
|
||||
<span>{{ locale.name }}</span>
|
||||
<svg
|
||||
v-if="locale.code === currentLocaleCode"
|
||||
class="ml-auto h-4 w-4 text-primary-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
<Icon v-if="locale.code === currentLocaleCode" name="check" size="sm" class="ml-auto text-primary-500" />
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -55,6 +42,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { setLocale, availableLocales } from '@/i18n'
|
||||
|
||||
const { locale } = useI18n()
|
||||
|
||||
@@ -63,13 +63,7 @@
|
||||
class="relative inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-dark-600 dark:bg-dark-700 dark:text-gray-400 dark:hover:bg-dark-600"
|
||||
:aria-label="t('pagination.previous')"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="chevronLeft" size="md" />
|
||||
</button>
|
||||
|
||||
<!-- Page numbers -->
|
||||
@@ -100,13 +94,7 @@
|
||||
class="relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-dark-600 dark:bg-dark-700 dark:text-gray-400 dark:hover:bg-dark-600"
|
||||
:aria-label="t('pagination.next')"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="chevronRight" size="md" />
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -116,6 +104,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import Select from './Select.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -23,35 +23,9 @@
|
||||
/>
|
||||
</svg>
|
||||
<!-- Setup Token icon -->
|
||||
<svg
|
||||
v-else-if="type === 'setup-token'"
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon v-else-if="type === 'setup-token'" name="shield" size="xs" />
|
||||
<!-- API Key icon -->
|
||||
<svg
|
||||
v-else
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon v-else name="key" size="xs" />
|
||||
<span>{{ typeLabel }}</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -61,6 +35,7 @@
|
||||
import { computed } from 'vue'
|
||||
import type { AccountPlatform, AccountType } from '@/types'
|
||||
import PlatformIcon from './PlatformIcon.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
interface Props {
|
||||
platform: AccountPlatform
|
||||
|
||||
@@ -14,15 +14,11 @@
|
||||
{{ selectedLabel }}
|
||||
</span>
|
||||
<span class="select-icon">
|
||||
<svg
|
||||
:class="['h-5 w-5 transition-transform duration-200', isOpen && 'rotate-180']"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
<Icon
|
||||
name="chevronDown"
|
||||
size="md"
|
||||
:class="['transition-transform duration-200', isOpen && 'rotate-180']"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -31,19 +27,7 @@
|
||||
<!-- Search and Batch Test Header -->
|
||||
<div class="select-header">
|
||||
<div class="select-search">
|
||||
<svg
|
||||
class="h-4 w-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="search" size="sm" class="text-gray-400" />
|
||||
<input
|
||||
ref="searchInputRef"
|
||||
v-model="searchQuery"
|
||||
@@ -76,20 +60,7 @@
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 010 1.972l-11.54 6.347a1.125 1.125 0 01-1.667-.986V5.653z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon v-else name="play" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -101,16 +72,7 @@
|
||||
:class="['select-option', modelValue === null && 'select-option-selected']"
|
||||
>
|
||||
<span class="select-option-label">{{ t('admin.accounts.noProxy') }}</span>
|
||||
<svg
|
||||
v-if="modelValue === null"
|
||||
class="h-4 w-4 text-primary-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
<Icon v-if="modelValue === null" name="check" size="sm" class="text-primary-500" />
|
||||
</div>
|
||||
|
||||
<!-- Proxy options -->
|
||||
@@ -184,32 +146,15 @@
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 010 1.972l-11.54 6.347a1.125 1.125 0 01-1.667-.986V5.653z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon v-else name="play" size="xs" />
|
||||
</button>
|
||||
|
||||
<svg
|
||||
<Icon
|
||||
v-if="modelValue === proxy.id"
|
||||
class="h-4 w-4 flex-shrink-0 text-primary-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
name="check"
|
||||
size="sm"
|
||||
class="flex-shrink-0 text-primary-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
@@ -226,6 +171,7 @@
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import type { Proxy } from '@/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
<template>
|
||||
<div class="relative w-full">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="search" size="md" class="text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
:value="modelValue"
|
||||
@@ -27,6 +15,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string
|
||||
|
||||
@@ -23,15 +23,11 @@
|
||||
</slot>
|
||||
</span>
|
||||
<span class="select-icon">
|
||||
<svg
|
||||
:class="['h-5 w-5 transition-transform duration-200', isOpen && 'rotate-180']"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
<Icon
|
||||
name="chevronDown"
|
||||
size="md"
|
||||
:class="['transition-transform duration-200', isOpen && 'rotate-180']"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -51,19 +47,7 @@
|
||||
>
|
||||
<!-- Search input -->
|
||||
<div v-if="searchable" class="select-search">
|
||||
<svg
|
||||
class="h-4 w-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="search" size="sm" class="text-gray-400" />
|
||||
<input
|
||||
ref="searchInputRef"
|
||||
v-model="searchQuery"
|
||||
@@ -93,16 +77,13 @@
|
||||
>
|
||||
<slot name="option" :option="option" :selected="isSelected(option)">
|
||||
<span class="select-option-label">{{ getOptionLabel(option) }}</span>
|
||||
<svg
|
||||
<Icon
|
||||
v-if="isSelected(option)"
|
||||
class="h-4 w-4 text-primary-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
name="check"
|
||||
size="sm"
|
||||
class="text-primary-500"
|
||||
:stroke-width="2"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
@@ -120,6 +101,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -478,4 +460,4 @@ onUnmounted(() => {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -8,18 +8,12 @@
|
||||
<div class="mt-1 flex items-baseline gap-2">
|
||||
<p class="stat-value">{{ formattedValue }}</p>
|
||||
<span v-if="change !== undefined" :class="['stat-trend', trendClass]">
|
||||
<svg
|
||||
<Icon
|
||||
v-if="changeType !== 'neutral'"
|
||||
:class="['h-3 w-3', changeType === 'down' && 'rotate-180']"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
name="arrowUp"
|
||||
size="xs"
|
||||
:class="changeType === 'down' && 'rotate-180'"
|
||||
/>
|
||||
{{ formattedChange }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -30,6 +24,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
type ChangeType = 'up' | 'down' | 'neutral'
|
||||
type IconVariant = 'primary' | 'success' | 'warning' | 'danger'
|
||||
|
||||
@@ -6,19 +6,7 @@
|
||||
class="flex cursor-pointer items-center gap-2 rounded-xl bg-purple-50 px-3 py-1.5 transition-colors hover:bg-purple-100 dark:bg-purple-900/20 dark:hover:bg-purple-900/30"
|
||||
:title="t('subscriptionProgress.viewDetails')"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-purple-600 dark:text-purple-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="creditCard" size="sm" class="text-purple-600 dark:text-purple-400" />
|
||||
<div class="flex items-center gap-1.5">
|
||||
<!-- Combined progress indicator -->
|
||||
<div class="flex items-center gap-0.5">
|
||||
@@ -192,6 +180,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { useSubscriptionStore } from '@/stores'
|
||||
import type { UserSubscription } from '@/types'
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@
|
||||
<div class="flex items-start gap-3">
|
||||
<!-- Icon -->
|
||||
<div class="mt-0.5 flex-shrink-0">
|
||||
<component
|
||||
:is="getIcon(toast.type)"
|
||||
:class="['h-5 w-5', getIconColor(toast.type)]"
|
||||
<Icon
|
||||
:name="getToastIconName(toast.type)"
|
||||
size="md"
|
||||
:class="getIconColor(toast.type)"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
@@ -57,13 +58,7 @@
|
||||
class="-m-1 flex-shrink-0 rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:text-gray-500 dark:hover:bg-dark-700 dark:hover:text-gray-300"
|
||||
aria-label="Close notification"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="x" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,77 +77,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, h } from 'vue'
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const toasts = computed(() => appStore.toasts)
|
||||
|
||||
const getIcon = (type: string) => {
|
||||
const icons = {
|
||||
success: () =>
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
fill: 'currentColor',
|
||||
viewBox: '0 0 20 20'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
'fill-rule': 'evenodd',
|
||||
d: 'M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z',
|
||||
'clip-rule': 'evenodd'
|
||||
})
|
||||
]
|
||||
),
|
||||
error: () =>
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
fill: 'currentColor',
|
||||
viewBox: '0 0 20 20'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
'fill-rule': 'evenodd',
|
||||
d: 'M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z',
|
||||
'clip-rule': 'evenodd'
|
||||
})
|
||||
]
|
||||
),
|
||||
warning: () =>
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
fill: 'currentColor',
|
||||
viewBox: '0 0 20 20'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
'fill-rule': 'evenodd',
|
||||
d: 'M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z',
|
||||
'clip-rule': 'evenodd'
|
||||
})
|
||||
]
|
||||
),
|
||||
info: () =>
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
fill: 'currentColor',
|
||||
viewBox: '0 0 20 20'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
'fill-rule': 'evenodd',
|
||||
d: 'M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z',
|
||||
'clip-rule': 'evenodd'
|
||||
})
|
||||
]
|
||||
)
|
||||
const getToastIconName = (type: string): 'checkCircle' | 'xCircle' | 'exclamationTriangle' | 'infoCircle' => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return 'checkCircle'
|
||||
case 'error':
|
||||
return 'xCircle'
|
||||
case 'warning':
|
||||
return 'exclamationTriangle'
|
||||
case 'info':
|
||||
default:
|
||||
return 'infoCircle'
|
||||
}
|
||||
return icons[type as keyof typeof icons] || icons.info
|
||||
}
|
||||
|
||||
const getIconColor = (type: string): string => {
|
||||
|
||||
@@ -46,20 +46,12 @@
|
||||
:disabled="loading"
|
||||
:title="t('version.refresh')"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
<Icon
|
||||
name="refresh"
|
||||
size="sm"
|
||||
:stroke-width="2"
|
||||
:class="{ 'animate-spin': loading }"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -129,19 +121,12 @@
|
||||
<div
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/50"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-red-600 dark:text-red-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<Icon
|
||||
name="x"
|
||||
size="sm"
|
||||
:stroke-width="2"
|
||||
class="text-red-600 dark:text-red-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
@@ -253,19 +238,12 @@
|
||||
<div
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/50"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-amber-600 dark:text-amber-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
<Icon
|
||||
name="download"
|
||||
size="sm"
|
||||
:stroke-width="2"
|
||||
class="text-amber-600 dark:text-amber-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-amber-700 dark:text-amber-300">
|
||||
@@ -314,23 +292,16 @@
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800/50 dark:bg-amber-900/20"
|
||||
>
|
||||
<div
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/50"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-amber-600 dark:text-amber-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/50"
|
||||
>
|
||||
<Icon
|
||||
name="download"
|
||||
size="sm"
|
||||
:stroke-width="2"
|
||||
class="text-amber-600 dark:text-amber-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-amber-700 dark:text-amber-300">
|
||||
{{ t('version.updateAvailable') }}
|
||||
@@ -362,20 +333,7 @@
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
<Icon v-else name="download" size="sm" :stroke-width="2" />
|
||||
{{ updating ? t('version.updating') : t('version.updateNow') }}
|
||||
</button>
|
||||
|
||||
@@ -388,19 +346,7 @@
|
||||
class="flex items-center justify-center gap-1 text-xs text-gray-500 transition-colors hover:text-gray-700 dark:text-dark-400 dark:hover:text-dark-200"
|
||||
>
|
||||
{{ t('version.viewChangelog') }}
|
||||
<svg
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="externalLink" size="xs" :stroke-width="2" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -439,6 +385,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore, useAppStore } from '@/stores'
|
||||
import { performUpdate, restartService } from '@/api/admin/system'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user