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:
IanShaw027
2026-01-05 20:22:48 +08:00
parent 34aa77e4e1
commit 4251a5a451
56 changed files with 688 additions and 2882 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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>

View File

@@ -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'

View File

@@ -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'

View File

@@ -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 => {

View File

@@ -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()