refactor: 将 @iconify/vue 图标替换为 lucide-vue-next 本地组件

- 所有 lucide: 前缀的图标字符串替换为 lucide-vue-next 组件直接引用
- 动态图标绑定改为 <component :is> 渲染
- h(Icon, { icon: 'lucide:xxx' }) 改为 h(Xxx, { class: ... })
- app-card.vue 的 icon_url 改为 <img> 标签(实际是图片路径)
- 修复 Webhook 标识符冲突(重命名为 WebhookIcon)
- payment-channels 保留 @iconify/vue 用于品牌图标(支付宝、微信、Stripe、PayPal、USDT)
This commit is contained in:
2026-05-03 04:25:34 +08:00
parent 1f7dfd17a7
commit c653ed071d
101 changed files with 910 additions and 769 deletions
+19
View File
@@ -47,6 +47,7 @@
"vue-input-otp": "^0.3.2",
"vue-router": "^5.0.3",
"vue-sonner": "^2.0.9",
"vue3-flag-icons": "^0.1.4",
"zod": "^4.3.6"
},
"devDependencies": {
@@ -7449,6 +7450,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/flag-icons": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.5.0.tgz",
"integrity": "sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==",
"license": "MIT"
},
"node_modules/flat-cache": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
@@ -12517,6 +12524,18 @@
"typescript": ">=5.0.0"
}
},
"node_modules/vue3-flag-icons": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/vue3-flag-icons/-/vue3-flag-icons-0.1.4.tgz",
"integrity": "sha512-bJrrZuVHcs5XCW1CiXG3gpZnkzczk3gXlLgVmKt4XTeKRb3U/qt57hNtTP2ybXEXaigE26WxhO9rVXF9gsL7Dg==",
"license": "MIT",
"dependencies": {
"flag-icons": "^7.2.3"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+1
View File
@@ -68,6 +68,7 @@
"vue-input-otp": "^0.3.2",
"vue-router": "^5.0.3",
"vue-sonner": "^2.0.9",
"vue3-flag-icons": "^0.1.4",
"zod": "^4.3.6"
},
"devDependencies": {
+123
View File
@@ -0,0 +1,123 @@
import { readFileSync, writeFileSync, readdirSync } from 'node:fs'
import { join } from 'node:path'
const SRC_DIR = 'D:\\Code\\verify\\frontend\\src'
function getAllFiles(dir, exts) {
const results = []
const entries = readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
results.push(...getAllFiles(fullPath, exts))
} else if (exts.some(ext => entry.name.endsWith(ext))) {
results.push(fullPath)
}
}
return results
}
function lucideToPascal(kebab) {
return kebab
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('')
}
function processFile(filePath) {
let content = readFileSync(filePath, 'utf-8')
const originalContent = content
if (!content.includes('@iconify/vue') && !content.includes('icon="lucide:') && !content.includes("icon='lucide:") && !content.includes('h(Icon')) {
return null
}
const usedIcons = new Set()
// Step 1: Replace <Icon icon="lucide:xxx" ... /> (single line)
content = content.replace(/<Icon\s+icon="lucide:([a-z0-9-]+)"([^>]*)\/>/g, (match, iconName, restAttrs) => {
const pascalName = lucideToPascal(iconName)
usedIcons.add(pascalName)
const trimmedAttrs = restAttrs.trim()
if (trimmedAttrs) {
return `<${pascalName} ${trimmedAttrs} />`
}
return `<${pascalName} />`
})
// Step 2: Replace multiline <Icon\n icon="lucide:xxx"\n .../>
content = content.replace(/<Icon\s+([^>]*?)icon="lucide:([a-z0-9-]+)"([^>]*?)\/>/gs, (match, before, iconName, after) => {
const pascalName = lucideToPascal(iconName)
usedIcons.add(pascalName)
const newAttrs = (before + after).replace(/\s+/g, ' ').trim()
if (newAttrs) {
return `<${pascalName} ${newAttrs} />`
}
return `<${pascalName} />`
})
// Step 3: Replace h(Icon, { icon: 'lucide:xxx', ... }) in columns.ts files
content = content.replace(/h\(\s*Icon\s*,\s*\{\s*icon:\s*['"]lucide:([a-z0-9-]+)['"]\s*,?\s*([^}]*)\}\s*\)/g, (match, iconName, restProps) => {
const pascalName = lucideToPascal(iconName)
usedIcons.add(pascalName)
const trimmedProps = restProps.trim().replace(/,\s*$/, '')
if (trimmedProps) {
return `h(${pascalName}, { ${trimmedProps} })`
}
return `h(${pascalName})`
})
// Step 4: Replace import { Icon } from '@iconify/vue'
content = content.replace(/import\s*\{\s*Icon\s*\}\s*from\s*['"]@iconify\/vue['"]\s*\n?/g, '')
// Step 5: Add lucide-vue-next import if we used any icons
if (usedIcons.size > 0 && !content.includes("from 'lucide-vue-next'") && !content.includes('from "lucide-vue-next"')) {
const sortedIcons = [...usedIcons].sort()
const importLine = `import { ${sortedIcons.join(', ')} } from 'lucide-vue-next'\n`
const firstImportMatch = content.match(/^import\s/m)
if (firstImportMatch) {
const insertPos = content.indexOf(firstImportMatch[0])
content = content.slice(0, insertPos) + importLine + content.slice(insertPos)
} else {
content = content.replace(/(<script[^>]*>)/, `$1\n${importLine}`)
}
} else if (usedIcons.size > 0) {
const existingImportMatch = content.match(/import\s*\{([^}]*)\}\s*from\s*['"]lucide-vue-next['"]/s)
if (existingImportMatch) {
const existingIcons = existingImportMatch[1]
.split(',')
.map(s => s.trim())
.filter(Boolean)
const newIcons = [...usedIcons].filter(icon => !existingIcons.includes(icon))
if (newIcons.length > 0) {
const allIcons = [...existingIcons, ...newIcons].sort()
const newImport = `import { ${allIcons.join(', ')} } from 'lucide-vue-next'`
content = content.replace(existingImportMatch[0], newImport)
}
}
}
if (content !== originalContent) {
writeFileSync(filePath, content, 'utf-8')
return { file: filePath, icons: [...usedIcons] }
}
return null
}
const files = getAllFiles(SRC_DIR, ['.vue', '.ts'])
const results = []
for (const file of files) {
const result = processFile(file)
if (result) {
results.push(result)
}
}
console.log(`\nProcessed ${results.length} files:`)
for (const r of results) {
console.log(` ${r.file}: ${r.icons.join(', ')}`)
}
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ChevronsUpDown, Database, LogOut, UserRoundCog, Zap } from 'lucide-vue-next'
import { Crown, ChevronsUpDown, Database, LogOut, Ticket, UserRoundCog, Zap } from 'lucide-vue-next'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
@@ -49,7 +48,7 @@ function handleLogout() {
<UiCardContent class="p-3 space-y-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<Icon icon="lucide:crown" class="size-4 text-primary" />
<Crown class="size-4 text-primary" />
<span class="text-xs font-medium">{{ subscription.plan }}</span>
</div>
<UiBadge
@@ -144,7 +143,7 @@ function handleLogout() {
个人中心
</UiDropdownMenuItem>
<UiDropdownMenuItem @click="router.push('/admin/tickets')">
<Icon icon="lucide:ticket" class="mr-2 h-4 w-4" />
<Ticket class="mr-2 h-4 w-4" />
工单系统
</UiDropdownMenuItem>
</UiDropdownMenuGroup>
@@ -1,5 +1,4 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import {
ChevronRight,
} from 'lucide-vue-next'
@@ -48,8 +47,7 @@ function isActive(menu: NavItem): boolean {
<UiTooltipTrigger as-child>
<UiSidebarMenuButton as-child :is-active="isActive(menu)">
<router-link :to="menu.url">
<component :is="menu.icon" v-if="typeof menu.icon === 'function'" class="size-4" />
<Icon v-else-if="menu.icon" :icon="menu.icon" />
<component :is="menu.icon" v-if="menu.icon" class="size-4" />
<span>{{ menu.title }}</span>
</router-link>
</UiSidebarMenuButton>
@@ -60,8 +58,7 @@ function isActive(menu: NavItem): boolean {
</UiTooltip>
<UiSidebarMenuButton v-else as-child :is-active="isActive(menu)">
<router-link :to="menu.url">
<component :is="menu.icon" v-if="typeof menu.icon === 'function'" class="size-4" />
<Icon v-else-if="menu.icon" :icon="menu.icon" />
<component :is="menu.icon" v-if="menu.icon" class="size-4" />
<span>{{ menu.title }}</span>
</router-link>
</UiSidebarMenuButton>
@@ -77,8 +74,7 @@ function isActive(menu: NavItem): boolean {
<UiSidebarMenuItem>
<UiCollapsibleTrigger as-child>
<UiSidebarMenuButton>
<component :is="menu.icon" v-if="typeof menu.icon === 'function'" class="size-4" />
<Icon v-else-if="menu.icon" :icon="menu.icon" />
<component :is="menu.icon" v-if="menu.icon" class="size-4" />
<span>{{ menu.title }}</span>
<ChevronRight
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
@@ -91,8 +87,7 @@ function isActive(menu: NavItem): boolean {
<UiSidebarMenuSubItem v-for="subItem in menu.items" :key="subItem.title">
<UiSidebarMenuSubButton as-child :is-active="isActive(subItem as NavItem)">
<router-link :to="subItem?.url || '/'">
<component :is="subItem.icon" v-if="typeof subItem.icon === 'function'" class="size-4" />
<Icon v-else-if="subItem.icon" :icon="subItem.icon" />
<component :is="subItem.icon" v-if="subItem.icon" class="size-4" />
<span>{{ subItem.title }}</span>
</router-link>
</UiSidebarMenuSubButton>
@@ -104,8 +99,7 @@ function isActive(menu: NavItem): boolean {
<UiTooltip v-else>
<UiTooltipTrigger as-child>
<UiSidebarMenuButton>
<component :is="menu.icon" v-if="typeof menu.icon === 'function'" class="size-4" />
<Icon v-else-if="menu.icon" :icon="menu.icon" />
<component :is="menu.icon" v-if="menu.icon" class="size-4" />
<span class="sr-only">{{ menu.title }}</span>
</UiSidebarMenuButton>
</UiTooltipTrigger>
@@ -122,8 +116,7 @@ function isActive(menu: NavItem): boolean {
<UiDropdownMenuSeparator />
<UiDropdownMenuItem v-for="subItem in menu.items" :key="subItem.title" as-child>
<router-link :to="subItem?.url || '/'">
<component :is="subItem.icon" v-if="typeof subItem.icon === 'function'" class="size-4" />
<Icon v-else-if="subItem.icon" :icon="subItem.icon" />
<component :is="subItem.icon" v-if="subItem.icon" class="size-4" />
<span>{{ subItem.title }}</span>
</router-link>
</UiDropdownMenuItem>
@@ -1,5 +1,4 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { ref } from 'vue'
import type { Team } from './types'
@@ -18,8 +17,7 @@ const activeTeam = ref<Team>(teams[0])
<div
class="flex items-center justify-center rounded-lg aspect-square size-8 bg-sidebar-primary text-sidebar-primary-foreground"
>
<component :is="activeTeam.logo" v-if="typeof activeTeam.logo === 'function'" class="size-4" />
<Icon v-else :icon="activeTeam.logo" class="size-4" />
<component :is="activeTeam.logo" v-if="activeTeam.logo" class="size-4" />
</div>
<div class="grid flex-1 text-sm leading-tight">
<span class="font-semibold truncate">{{ activeTeam.name }}</span>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { Settings } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import {
@@ -19,7 +19,7 @@ import ToggleColorMode from './toggle-color-mode.vue'
<Popover>
<PopoverTrigger>
<Button variant="ghost" size="icon" class="h-9 w-9">
<Icon icon="lucide:settings" class="h-5 w-5" />
<Settings class="h-5 w-5" />
<span class="sr-only">设置</span>
</Button>
</PopoverTrigger>
+6 -6
View File
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { AcceptableValue } from 'reka-ui'
import { Icon } from '@iconify/vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import FlagIcon from 'vue3-flag-icons'
import type { Language } from '@/plugins/i18n'
@@ -26,8 +26,8 @@ function handleLocaleChange(val: AcceptableValue) {
appLocale.value = val as Language
}
const currentFlag = computed(() => {
return locale.value === 'zh' ? 'flag:cn-4x3' : 'flag:us-4x3'
const currentFlagCode = computed(() => {
return locale.value === 'zh' ? 'cn' : 'us'
})
</script>
@@ -35,7 +35,7 @@ const currentFlag = computed(() => {
<UiDropdownMenu>
<UiDropdownMenuTrigger as-child>
<UiButton variant="ghost" size="icon" class="h-9 w-9">
<Icon :icon="currentFlag" class="h-5 w-5" />
<FlagIcon :code="currentFlagCode" :size="20" />
<span class="sr-only">切换语言</span>
</UiButton>
</UiDropdownMenuTrigger>
@@ -47,11 +47,11 @@ const currentFlag = computed(() => {
@update:model-value="handleLocaleChange"
>
<UiDropdownMenuRadioItem value="en">
<Icon icon="flag:us-4x3" />
<FlagIcon code="us" :size="18" />
<span class="ml-2">English</span>
</UiDropdownMenuRadioItem>
<UiDropdownMenuRadioItem value="zh">
<Icon icon="flag:cn-4x3" />
<FlagIcon code="cn" :size="18" />
<span class="ml-2">中文</span>
</UiDropdownMenuRadioItem>
</UiDropdownMenuRadioGroup>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Mail, Send } from 'lucide-vue-next'
import { useColorMode } from '@vueuse/core'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -65,11 +65,11 @@ const links = computed(() => [
</h3>
<ul class="space-y-2 text-sm text-muted-foreground">
<li class="flex items-center">
<Icon icon="lucide:mail" class="h-4 w-4 mr-2" />
<Mail class="h-4 w-4 mr-2" />
admin@weishouquan.com
</li>
<li class="flex items-center">
<Icon icon="lucide:send" class="h-4 w-4 mr-2" />
<Send class="h-4 w-4 mr-2" />
weishouquan
</li>
</ul>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { BookOpen, Boxes, ChevronDown, Code, CreditCard, DollarSign, FileLock, Gauge, GitBranch, Hash, Home, Key, LayoutDashboard, LogOut, Megaphone, Menu, MessageSquare, Monitor, Moon, Plug, ScrollText, Share2, Shield, Sun, User, Users, Variable, Wifi } from 'lucide-vue-next'
import { useColorMode } from '@vueuse/core'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -66,42 +66,42 @@ const adminMenus = computed(() => ({
main: {
title: t('nav.mainFeatures'),
items: [
{ title: t('nav.console'), url: '/admin', icon: 'lucide:gauge' },
{ title: t('nav.applications'), url: '/admin/applications', icon: 'lucide:boxes' },
{ title: t('nav.cardTypes'), url: '/admin/card-types', icon: 'lucide:hash' },
{ title: t('nav.cards'), url: '/admin/cards', icon: 'lucide:key' },
{ title: t('nav.announcements'), url: '/admin/announcements', icon: 'lucide:megaphone' },
{ title: t('nav.versions'), url: '/admin/versions', icon: 'lucide:git-branch' },
{ title: t('nav.users'), url: '/admin/users', icon: 'lucide:users' },
{ title: t('nav.devices'), url: '/admin/devices', icon: 'lucide:monitor' },
{ title: t('nav.sessions'), url: '/admin/sessions', icon: 'lucide:wifi' },
{ title: t('nav.agentApps'), url: '/admin/agent-apps', icon: 'lucide:share-2' },
{ title: t('nav.console'), url: '/admin', icon: Gauge },
{ title: t('nav.applications'), url: '/admin/applications', icon: Boxes },
{ title: t('nav.cardTypes'), url: '/admin/card-types', icon: Hash },
{ title: t('nav.cards'), url: '/admin/cards', icon: Key },
{ title: t('nav.announcements'), url: '/admin/announcements', icon: Megaphone },
{ title: t('nav.versions'), url: '/admin/versions', icon: GitBranch },
{ title: t('nav.users'), url: '/admin/users', icon: Users },
{ title: t('nav.devices'), url: '/admin/devices', icon: Monitor },
{ title: t('nav.sessions'), url: '/admin/sessions', icon: Wifi },
{ title: t('nav.agentApps'), url: '/admin/agent-apps', icon: Share2 },
],
},
business: {
title: t('nav.businessManagement'),
items: [
{ title: t('nav.finance'), url: '/admin/finance', icon: 'lucide:dollar-sign' },
{ title: t('nav.logs'), url: '/admin/logs', icon: 'lucide:scroll-text' },
{ title: t('nav.tickets'), url: '/admin/tickets', icon: 'lucide:message-square' },
{ title: t('nav.finance'), url: '/admin/finance', icon: DollarSign },
{ title: t('nav.logs'), url: '/admin/logs', icon: ScrollText },
{ title: t('nav.tickets'), url: '/admin/tickets', icon: MessageSquare },
],
},
advanced: {
title: t('nav.advancedFeatures'),
items: [
{ title: t('nav.cloudConstants'), url: '/admin/cloud-constants', icon: 'lucide:file-lock' },
{ title: t('nav.cloudVariables'), url: '/admin/cloud-variables', icon: 'lucide:variable' },
{ title: t('nav.cloudFunction'), url: '/admin/cloud-function', icon: 'lucide:code' },
{ title: t('nav.riskControl'), url: '/admin/risk-control', icon: 'lucide:shield' },
{ title: t('nav.extension'), url: '/admin/extension', icon: 'lucide:plug' },
{ title: t('nav.cloudConstants'), url: '/admin/cloud-constants', icon: FileLock },
{ title: t('nav.cloudVariables'), url: '/admin/cloud-variables', icon: Variable },
{ title: t('nav.cloudFunction'), url: '/admin/cloud-function', icon: Code },
{ title: t('nav.riskControl'), url: '/admin/risk-control', icon: Shield },
{ title: t('nav.extension'), url: '/admin/extension', icon: Plug },
],
},
}))
const publicMenus = computed(() => [
{ title: t('nav.home'), url: '/', icon: 'lucide:home' },
{ title: t('nav.docs'), url: '/docs', icon: 'lucide:book-open' },
{ title: t('nav.pricing'), url: '/pricing', icon: 'lucide:credit-card' },
{ title: t('nav.home'), url: '/', icon: Home },
{ title: t('nav.docs'), url: '/docs', icon: BookOpen },
{ title: t('nav.pricing'), url: '/pricing', icon: CreditCard },
])
function isMenuActive(items: { url: string }[]) {
@@ -207,7 +207,7 @@ watch(route, () => {
:class="isMenuActive(adminMenus.main.items) ? 'text-foreground bg-accent' : 'text-foreground/60'"
>
{{ adminMenus.main.title }}
<Icon icon="lucide:chevron-down" class="h-4 w-4" />
<ChevronDown class="h-4 w-4" />
</button>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent align="start" class="w-40">
@@ -217,7 +217,7 @@ watch(route, () => {
:class="route.path === item.url ? 'bg-accent' : ''"
@click="router.push(item.url)"
>
<Icon :icon="item.icon" class="mr-2 h-4 w-4" />
<component :is="item.icon" class="mr-2 h-4 w-4" />
{{ item.title }}
</UiDropdownMenuItem>
</UiDropdownMenuContent>
@@ -230,7 +230,7 @@ watch(route, () => {
:class="isMenuActive(adminMenus.business.items) ? 'text-foreground bg-accent' : 'text-foreground/60'"
>
{{ adminMenus.business.title }}
<Icon icon="lucide:chevron-down" class="h-4 w-4" />
<ChevronDown class="h-4 w-4" />
</button>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent align="start" class="w-40">
@@ -240,7 +240,7 @@ watch(route, () => {
:class="route.path === item.url ? 'bg-accent' : ''"
@click="router.push(item.url)"
>
<Icon :icon="item.icon" class="mr-2 h-4 w-4" />
<component :is="item.icon" class="mr-2 h-4 w-4" />
{{ item.title }}
</UiDropdownMenuItem>
</UiDropdownMenuContent>
@@ -253,7 +253,7 @@ watch(route, () => {
:class="isMenuActive(adminMenus.advanced.items) ? 'text-foreground bg-accent' : 'text-foreground/60'"
>
{{ adminMenus.advanced.title }}
<Icon icon="lucide:chevron-down" class="h-4 w-4" />
<ChevronDown class="h-4 w-4" />
</button>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent align="start" class="w-40">
@@ -263,7 +263,7 @@ watch(route, () => {
:class="route.path === item.url ? 'bg-accent' : ''"
@click="router.push(item.url)"
>
<Icon :icon="item.icon" class="mr-2 h-4 w-4" />
<component :is="item.icon" class="mr-2 h-4 w-4" />
{{ item.title }}
</UiDropdownMenuItem>
</UiDropdownMenuContent>
@@ -276,7 +276,7 @@ watch(route, () => {
class="md:hidden mr-2 flex items-center justify-center size-9 rounded-md hover:bg-accent"
@click="mobileMenuOpen = true"
>
<Icon icon="lucide:menu" class="size-5" />
<Menu class="size-5" />
</button>
<router-link to="/" class="mr-6 flex items-center space-x-2 md:hidden">
@@ -295,12 +295,10 @@ watch(route, () => {
class="h-9 w-9"
@click="mode = mode === 'dark' ? 'light' : 'dark'"
>
<Icon
icon="lucide:sun"
<Sun
class="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
/>
<Icon
icon="lucide:moon"
<Moon
class="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
/>
<span class="sr-only">切换主题</span>
@@ -331,20 +329,20 @@ watch(route, () => {
</UiDropdownMenuLabel>
<UiDropdownMenuSeparator />
<UiDropdownMenuItem @click="router.push('/profile')">
<Icon icon="lucide:user" class="mr-2 h-4 w-4" />
<User class="mr-2 h-4 w-4" />
<span>{{ t('nav.profile') }}</span>
</UiDropdownMenuItem>
<UiDropdownMenuItem v-if="currentUser?.role === 'admin'" @click="router.push('/admin')">
<Icon icon="lucide:layout-dashboard" class="mr-2 h-4 w-4" />
<LayoutDashboard class="mr-2 h-4 w-4" />
<span>{{ t('nav.adminDashboard') }}</span>
</UiDropdownMenuItem>
<UiDropdownMenuItem v-if="currentUser?.role === 'agent'" @click="router.push('/agent')">
<Icon icon="lucide:layout-dashboard" class="mr-2 h-4 w-4" />
<LayoutDashboard class="mr-2 h-4 w-4" />
<span>{{ t('nav.agentDashboard') }}</span>
</UiDropdownMenuItem>
<UiDropdownMenuSeparator />
<UiDropdownMenuItem @click="handleLogout">
<Icon icon="lucide:log-out" class="mr-2 h-4 w-4" />
<LogOut class="mr-2 h-4 w-4" />
<span>{{ t('nav.logout') }}</span>
</UiDropdownMenuItem>
</UiDropdownMenuContent>
@@ -389,7 +387,7 @@ watch(route, () => {
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
@click="navigateTo(item.url)"
>
<Icon :icon="item.icon" class="size-4" />
<component :is="item.icon" class="size-4" />
{{ item.title }}
</button>
</div>
@@ -406,7 +404,7 @@ watch(route, () => {
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
@click="navigateTo(item.url)"
>
<Icon :icon="item.icon" class="size-4" />
<component :is="item.icon" class="size-4" />
{{ item.title }}
</button>
</div>
@@ -422,7 +420,7 @@ watch(route, () => {
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
@click="navigateTo(item.url)"
>
<Icon :icon="item.icon" class="size-4" />
<component :is="item.icon" class="size-4" />
{{ item.title }}
</button>
</div>
@@ -438,7 +436,7 @@ watch(route, () => {
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
@click="navigateTo(item.url)"
>
<Icon :icon="item.icon" class="size-4" />
<component :is="item.icon" class="size-4" />
{{ item.title }}
</button>
</div>
@@ -474,7 +472,7 @@ watch(route, () => {
</div>
</div>
<UiButton variant="outline" class="w-full mt-2" @click="handleLogout">
<Icon icon="lucide:log-out" class="mr-2 h-4 w-4" />
<LogOut class="mr-2 h-4 w-4" />
{{ t('nav.logout') }}
</UiButton>
</div>
+3 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Moon, Sun } from 'lucide-vue-next'
import { useColorMode } from '@vueuse/core'
const mode = useColorMode()
@@ -12,12 +12,10 @@ const mode = useColorMode()
class="h-9 w-9"
@click="mode = mode === 'dark' ? 'light' : 'dark'"
>
<Icon
icon="lucide:sun"
<Sun
class="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
/>
<Icon
icon="lucide:moon"
<Moon
class="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
/>
<span class="sr-only">切换主题</span>
@@ -3,7 +3,7 @@ import type { DateValue } from '@internationalized/date'
import type { HTMLAttributes } from 'vue'
import { parseDate } from '@internationalized/date'
import { cn } from '@/lib/utils'
import { Icon } from '@iconify/vue'
import { Calendar } from 'lucide-vue-next'
import { computed } from 'vue'
import UiButton from '@/components/ui/button/Button.vue'
import UiPopover from '@/components/ui/popover/Popover.vue'
@@ -73,7 +73,7 @@ function handleSelect(date: DateValue | undefined) {
)"
:disabled="disabled"
>
<Icon icon="lucide:calendar" class="mr-2 h-4 w-4" />
<Calendar class="mr-2 h-4 w-4" />
{{ formattedDate || placeholder || '选择日期' }}
</UiButton>
</UiPopoverTrigger>
@@ -3,7 +3,7 @@ import type { DateValue } from '@internationalized/date'
import type { HTMLAttributes } from 'vue'
import { parseDate } from '@internationalized/date'
import { cn } from '@/lib/utils'
import { Icon } from '@iconify/vue'
import { CalendarClock } from 'lucide-vue-next'
import { computed, ref, watch } from 'vue'
import UiButton from '@/components/ui/button/Button.vue'
import UiPopover from '@/components/ui/popover/Popover.vue'
@@ -95,7 +95,7 @@ watch(() => props.modelValue, initFromModelValue, { immediate: true })
)"
:disabled="disabled"
>
<Icon icon="lucide:calendar-clock" class="mr-2 h-4 w-4 shrink-0" />
<CalendarClock class="mr-2 h-4 w-4 shrink-0" />
<span class="truncate text-sm">{{ displayText || placeholder || '选择日期时间' }}</span>
</UiButton>
</UiPopoverTrigger>
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Icon } from '@iconify/vue'
import { Clock } from 'lucide-vue-next'
import { computed } from 'vue'
import UiButton from '@/components/ui/button/Button.vue'
import UiPopover from '@/components/ui/popover/Popover.vue'
@@ -53,7 +53,7 @@ function handleTimeChange(time: string | number) {
)"
:disabled="disabled"
>
<Icon icon="lucide:clock" class="mr-2 h-4 w-4" />
<Clock class="mr-2 h-4 w-4" />
{{ formattedTime || placeholder || '选择时间' }}
</UiButton>
</UiPopoverTrigger>
+3 -4
View File
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Boxes, CreditCard, Gauge, Key, Users } from 'lucide-vue-next'
import { Boxes, CreditCard, Gauge, Key, LogOut, User, Users } from 'lucide-vue-next'
import { computed, onMounted, reactive } from 'vue'
import { useRouter } from 'vue-router'
@@ -180,14 +179,14 @@ function handleLogout() {
<UiDropdownMenuSeparator />
<UiDropdownMenuGroup>
<UiDropdownMenuItem @click="router.push('/agent/profile')">
<Icon icon="lucide:user" class="mr-2 h-4 w-4" />
<User class="mr-2 h-4 w-4" />
个人中心
</UiDropdownMenuItem>
</UiDropdownMenuGroup>
<UiDropdownMenuSeparator />
<UiDropdownMenuItem class="text-red-500" @click="handleLogout">
<Icon icon="lucide:log-out" class="mr-2 h-4 w-4" />
<LogOut class="mr-2 h-4 w-4" />
退出登录
</UiDropdownMenuItem>
</UiDropdownMenuContent>
+2 -1
View File
@@ -7,7 +7,8 @@ import '@/assets/index.css'
import '@/assets/scrollbar.css'
import '@/assets/themes.css'
import '@/assets/chart-theme.css'
import 'vue-sonner/style.css' // vue sonner style
import 'vue-sonner/style.css'
import 'vue3-flag-icons/styles'
import '@/utils/env'
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ArrowLeft, CheckCheck, Loader2, X } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -149,13 +149,13 @@ onMounted(() => {
>
<template #actions>
<UiButton variant="outline" @click="goBack">
<Icon icon="lucide:arrow-left" class="mr-2 h-4 w-4" />
<ArrowLeft class="mr-2 h-4 w-4" />
返回
</UiButton>
</template>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="h-8 w-8 animate-spin text-muted-foreground" />
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -169,11 +169,11 @@ onMounted(() => {
<UiCardContent>
<div class="flex gap-2 mb-4">
<UiButton variant="outline" size="sm" @click="selectAll">
<Icon icon="lucide:check-check" class="mr-2 h-4 w-4" />
<CheckCheck class="mr-2 h-4 w-4" />
全选
</UiButton>
<UiButton variant="outline" size="sm" @click="clearAll">
<Icon icon="lucide:x" class="mr-2 h-4 w-4" />
<X class="mr-2 h-4 w-4" />
清空
</UiButton>
</div>
@@ -226,7 +226,7 @@ onMounted(() => {
取消
</UiButton>
<UiButton :disabled="saving" @click="handleSubmit">
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
保存
</UiButton>
</UiCardFooter>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ArrowLeft, Loader2, Wallet } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -103,13 +103,13 @@ onMounted(() => {
>
<template #actions>
<UiButton variant="outline" @click="goBack">
<Icon icon="lucide:arrow-left" class="mr-2 h-4 w-4" />
<ArrowLeft class="mr-2 h-4 w-4" />
返回
</UiButton>
</template>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="h-8 w-8 animate-spin text-muted-foreground" />
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
</div>
<div v-else-if="agentApp" class="max-w-2xl">
@@ -173,7 +173,7 @@ onMounted(() => {
size="sm"
@click="router.push(`/admin/agent-apps/${route.params.id}/recharge`)"
>
<Icon icon="lucide:wallet" class="mr-2 h-4 w-4" />
<Wallet class="mr-2 h-4 w-4" />
充值余额
</UiButton>
</div>
@@ -183,7 +183,7 @@ onMounted(() => {
取消
</UiButton>
<UiButton :disabled="saving" @click="handleSubmit">
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
保存
</UiButton>
</UiCardFooter>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ArrowLeft, Loader2 } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -103,13 +103,13 @@ onMounted(() => {
>
<template #actions>
<UiButton variant="outline" @click="goBack">
<Icon icon="lucide:arrow-left" class="mr-2 h-4 w-4" />
<ArrowLeft class="mr-2 h-4 w-4" />
返回
</UiButton>
</template>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="h-8 w-8 animate-spin text-muted-foreground" />
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="max-w-2xl">
@@ -175,7 +175,7 @@ onMounted(() => {
取消
</UiButton>
<UiButton :disabled="saving || rechargeAmount <= 0" @click="handleSubmit">
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
确认充值
</UiButton>
</UiCardFooter>
+10 -10
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, CheckCircle, Clock, Send, Share2, Trash2, UserPlus, Wallet, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -240,11 +240,11 @@ onMounted(() => {
>
<template #actions>
<UiButton variant="outline" @click="goToRequest">
<Icon icon="lucide:send" class="mr-2 h-4 w-4" />
<Send class="mr-2 h-4 w-4" />
{{ t('admin.agentApps.requestAuth') }}
</UiButton>
<UiButton @click="goToInvite">
<Icon icon="lucide:user-plus" class="mr-2 h-4 w-4" />
<UserPlus class="mr-2 h-4 w-4" />
{{ t('admin.agentApps.inviteAuth') }}
</UiButton>
</template>
@@ -256,7 +256,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agentApps.totalAuth') }}
</UiCardTitle>
<Icon icon="lucide:share-2" class="size-4 text-muted-foreground" />
<Share2 class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -270,7 +270,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agentApps.activeAuth') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -284,7 +284,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agentApps.pendingRequests') }}
</UiCardTitle>
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
<Clock class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -298,7 +298,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agentApps.agentBalance') }}
</UiCardTitle>
<Icon icon="lucide:wallet" class="size-4 text-muted-foreground" />
<Wallet class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -333,7 +333,7 @@ onMounted(() => {
size="sm"
@click="batchApprove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
>
<Icon icon="lucide:check" class="mr-2 h-4 w-4" />
<Check class="mr-2 h-4 w-4" />
{{ t('admin.agentApps.batchApprove') }}
</UiButton>
<UiButton
@@ -341,7 +341,7 @@ onMounted(() => {
size="sm"
@click="confirmBatchReject(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
>
<Icon icon="lucide:x" class="mr-2 h-4 w-4" />
<X class="mr-2 h-4 w-4" />
{{ t('admin.agentApps.batchReject') }}
</UiButton>
<UiButton
@@ -349,7 +349,7 @@ onMounted(() => {
size="sm"
@click="confirmBatchRemove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
>
<Icon icon="lucide:trash-2" class="mr-2 h-4 w-4" />
<Trash2 class="mr-2 h-4 w-4" />
{{ t('admin.agentApps.batchRemove') }}
</UiButton>
</BulkActions>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ArrowLeft, Loader2 } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -94,7 +94,7 @@ onMounted(() => {
>
<template #actions>
<UiButton variant="outline" @click="goBack">
<Icon icon="lucide:arrow-left" class="mr-2 h-4 w-4" />
<ArrowLeft class="mr-2 h-4 w-4" />
返回
</UiButton>
</template>
@@ -160,7 +160,7 @@ onMounted(() => {
取消
</UiButton>
<UiButton :disabled="saving" @click="handleSubmit">
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
发送邀请
</UiButton>
</UiCardFooter>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ArrowLeft, Loader2 } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -94,7 +94,7 @@ onMounted(() => {
>
<template #actions>
<UiButton variant="outline" @click="goBack">
<Icon icon="lucide:arrow-left" class="mr-2 h-4 w-4" />
<ArrowLeft class="mr-2 h-4 w-4" />
返回
</UiButton>
</template>
@@ -160,7 +160,7 @@ onMounted(() => {
取消
</UiButton>
<UiButton :disabled="saving" @click="handleSubmit">
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
提交申请
</UiButton>
</UiCardFooter>
+7 -7
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Loader2, Receipt, Settings2, UserPlus } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
@@ -128,7 +128,7 @@ onMounted(() => {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -137,7 +137,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:user-plus" class="size-5" />
<UserPlus class="size-5" />
{{ t('admin.agents.create.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.agents.create.basicInfoDesc') }}</UiCardDescription>
@@ -186,7 +186,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.agents.create.agentConfig') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.agents.create.agentConfigDesc') }}</UiCardDescription>
@@ -253,7 +253,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:receipt" class="size-5" />
<Receipt class="size-5" />
{{ t('admin.agents.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -292,8 +292,8 @@ onMounted(() => {
:disabled="saving || !form.username"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ t('admin.agents.create.saveBtn') || '保存修改' }}
</UiButton>
<UiButton
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { GitBranch, List, Search } from 'lucide-vue-next'
import type { ColumnDef } from '@tanstack/vue-table'
import { computed } from 'vue'
@@ -13,8 +14,6 @@ import { SelectColumn } from '@/components/data-table/table-columns'
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
import DataTableViewOptions from '@/components/data-table/view-options.vue'
import { getColumns } from './columns'
import { Icon } from '@iconify/vue'
const props = defineProps<Omit<DataTableProps<Agent>, 'columns'> & {
searchFilter?: string
viewMode?: 'tree' | 'list'
@@ -88,7 +87,7 @@ defineExpose({
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Icon icon="lucide:search" class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<UiInput
:model-value="searchFilter"
type="search"
@@ -101,7 +100,7 @@ defineExpose({
</div>
<div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" @click="emit('toggleView')">
<Icon :icon="viewMode === 'tree' ? 'lucide:list' : 'lucide:git-branch'" class="mr-2 h-4 w-4" />
<component :is="viewMode === 'tree' ? List : GitBranch" class="mr-2 h-4 w-4" />
{{ viewMode === 'tree' ? t('admin.agents.listView') : t('admin.agents.treeView') }}
</UiButton>
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
+6 -6
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Loader2, Receipt, Settings2, UserPlus } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -112,7 +112,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:user-plus" class="size-5" />
<UserPlus class="size-5" />
{{ t('admin.agents.create.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.agents.create.basicInfoDesc') }}</UiCardDescription>
@@ -161,7 +161,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.agents.create.agentConfig') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.agents.create.agentConfigDesc') }}</UiCardDescription>
@@ -235,7 +235,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:receipt" class="size-5" />
<Receipt class="size-5" />
{{ t('admin.agents.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -274,8 +274,8 @@ onMounted(() => {
:disabled="saving || !form.username || !form.password"
@click="handleCreate"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:user-plus" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<UserPlus v-else class="mr-2 h-4 w-4" />
{{ t('admin.agents.form.create') }}
</UiButton>
<UiButton
+6 -6
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Ban, CheckCircle, Clock, Plus, Users } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -229,7 +229,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.agents.addAgent') }}
</UiButton>
</template>
@@ -241,7 +241,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agents.totalAgents') }}
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
<Users class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -255,7 +255,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agents.status.active') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -269,7 +269,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agents.status.inactive') }}
</UiCardTitle>
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
<Clock class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -283,7 +283,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agents.status.banned') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
<Ban class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Eye, Loader2, Megaphone } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -129,7 +129,7 @@ onMounted(() => {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -138,7 +138,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:megaphone" class="size-5" />
<Megaphone class="size-5" />
公告配置
</UiCardTitle>
<UiCardDescription>修改公告的基本信息和内容</UiCardDescription>
@@ -239,7 +239,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -292,8 +292,8 @@ onMounted(() => {
:disabled="saving || !formData.application_id || !formData.title || !formData.content"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
保存修改
</UiButton>
<UiButton
@@ -1,6 +1,6 @@
import { Edit, Pin, PinOff, Trash2 } from 'lucide-vue-next'
import type { ColumnDef } from '@tanstack/vue-table'
import { Icon } from '@iconify/vue'
import { h } from 'vue'
import type { Announcement } from '@/pages/admin/announcements/data/schema'
@@ -77,8 +77,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
size: 'sm',
onClick: () => options.onToggleTop(row.original),
}, () => [
h(Icon, {
icon: row.original.is_top ? 'lucide:pin' : 'lucide:pin-off',
h(row.original.is_top ? Pin : PinOff, {
class: `h-4 w-4 ${row.original.is_top ? 'text-primary' : 'text-muted-foreground'}`,
}),
])
@@ -101,14 +100,14 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
size: 'sm',
onClick: () => options.onEdit(row.original),
}, () => [
h(Icon, { icon: 'lucide:edit', class: 'h-4 w-4' }),
h(Edit, { class: 'h-4 w-4' }),
]),
h(Button, {
variant: 'ghost',
size: 'sm',
onClick: () => options.onDelete(row.original),
}, () => [
h(Icon, { icon: 'lucide:trash-2', class: 'h-4 w-4 text-destructive' }),
h(Trash2, { class: 'h-4 w-4 text-destructive' }),
]),
])
},
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Eye, Loader2, Megaphone, Send } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -117,7 +117,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:megaphone" class="size-5" />
<Megaphone class="size-5" />
公告配置
</UiCardTitle>
<UiCardDescription>填写公告的基本信息和内容</UiCardDescription>
@@ -218,7 +218,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -271,8 +271,8 @@ onMounted(() => {
:disabled="saving || !formData.application_id || !formData.title || !formData.content"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:send" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Send v-else class="mr-2 h-4 w-4" />
发布公告
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { CheckCircle, Megaphone, Pin, Plus, XCircle } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
@@ -219,7 +219,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.announcements.create') }}
</UiButton>
</template>
@@ -231,7 +231,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.announcements.totalAnnouncements') }}
</UiCardTitle>
<Icon icon="lucide:megaphone" class="size-4 text-muted-foreground" />
<Megaphone class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -245,7 +245,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.announcements.activeCount') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -259,7 +259,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.announcements.inactiveCount') }}
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
<XCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -273,7 +273,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.announcements.topCount') }}
</UiCardTitle>
<Icon icon="lucide:pin" class="size-4 text-muted-foreground" />
<Pin class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
+6 -6
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Activity, CheckCircle, LayoutGrid, Plus, Users } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -144,7 +144,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.applications.createApp') }}
</UiButton>
</template>
@@ -156,7 +156,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.applications.totalApps') }}
</UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
<LayoutGrid class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -170,7 +170,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.applications.activeApps') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -184,7 +184,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.applications.totalUsers') }}
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
<Users class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -198,7 +198,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.applications.totalVerifyCount') }}
</UiCardTitle>
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
<Activity class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Loader2, RefreshCw } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -146,7 +146,7 @@ onMounted(() => {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -215,7 +215,7 @@ onMounted(() => {
加密密钥
</UiLabel>
<UiButton variant="outline" size="sm" @click="generateEncryptKey">
<Icon icon="lucide:refresh-cw" class="mr-2 h-4 w-4" />
<RefreshCw class="mr-2 h-4 w-4" />
自动生成
</UiButton>
</div>
@@ -548,7 +548,7 @@ onMounted(() => {
<div class="flex justify-end">
<UiButton :disabled="saving" @click="handleSave">
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
保存设置
</UiButton>
</div>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Info, Loader2, Upload, X } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -321,7 +321,7 @@ function toggleWeekday(index: number) {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -406,14 +406,14 @@ function toggleWeekday(index: number) {
class="absolute -top-1.5 -right-1.5 h-5 w-5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
@click="clearIcon"
>
<Icon icon="lucide:x" class="h-3 w-3" />
<X class="h-3 w-3" />
</UiButton>
</div>
<label
for="appIcon"
class="flex items-center justify-center h-16 px-4 border-2 border-dashed rounded-lg cursor-pointer hover:bg-accent/50 transition-colors"
>
<Icon icon="lucide:upload" class="w-4 h-4 mr-2 text-muted-foreground" />
<Upload class="w-4 h-4 mr-2 text-muted-foreground" />
<span class="text-sm text-muted-foreground">{{ form.iconPreview ? '更换图标' : '上传图标' }}</span>
<input
id="appIcon"
@@ -630,7 +630,7 @@ function toggleWeekday(index: number) {
<div v-else class="p-4 rounded-lg bg-muted/50 border">
<div class="flex items-start gap-3">
<Icon icon="lucide:info" class="size-5 text-blue-500 mt-0.5" />
<Info class="size-5 text-blue-500 mt-0.5" />
<div class="space-y-2">
<p class="font-medium text-sm">
手动扣费说明
@@ -1083,7 +1083,7 @@ function toggleWeekday(index: number) {
<div class="flex justify-end">
<UiButton :disabled="saving" @click="handleSave">
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
保存设置
</UiButton>
</div>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Box } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import type { App } from '../data/schema'
@@ -25,8 +25,8 @@ function formatDate(date: string | Date) {
<div class="flex items-start justify-between">
<div class="flex items-center gap-3">
<div class="size-10 rounded-lg bg-primary/10 flex items-center justify-center">
<Icon v-if="props.app.icon_url" :icon="props.app.icon_url" class="size-6 text-primary" />
<Icon v-else icon="lucide:box" class="size-6 text-primary" />
<img v-if="props.app.icon_url" :src="props.app.icon_url.startsWith('http') ? props.app.icon_url : `http://localhost:8080${props.app.icon_url}`" class="size-6 object-cover rounded" alt="" />
<Box v-else class="size-6 text-primary" />
</div>
<div>
<UiCardTitle class="text-base">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Box, Check, CheckCircle, Coins, CreditCard, Eye, Gift, Info, Loader2, Plus, Repeat, Upload, X } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -22,9 +22,9 @@ const formData = ref({
})
const billingOptions = [
{ value: 'free', label: '免费模式', desc: '用户无需付费', icon: 'lucide:gift' },
{ value: 'subscription', label: '订阅模式', desc: '按周期付费订阅', icon: 'lucide:repeat' },
{ value: 'balance', label: '余额模式', desc: '按使用量计费', icon: 'lucide:coins' },
{ value: 'free', label: '免费模式', desc: '用户无需付费', iconComponent: Gift },
{ value: 'subscription', label: '订阅模式', desc: '按周期付费订阅', iconComponent: Repeat },
{ value: 'balance', label: '余额模式', desc: '按使用量计费', iconComponent: Coins },
]
const selectedBilling = computed(() => {
@@ -133,7 +133,7 @@ function goToAppList() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:box" class="size-5" />
<Box class="size-5" />
基本信息
</UiCardTitle>
<UiCardDescription>设置应用的基本信息</UiCardDescription>
@@ -166,14 +166,14 @@ function goToAppList() {
class="absolute -top-1.5 -right-1.5 h-5 w-5 rounded-full bg-destructive text-destructive-foreground flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
@click="clearIcon"
>
<Icon icon="lucide:x" class="h-3 w-3" />
<X class="h-3 w-3" />
</button>
</div>
<label
for="appIcon"
class="flex items-center justify-center h-12 px-4 border-2 border-dashed rounded-lg cursor-pointer hover:bg-accent/50 transition-colors"
>
<Icon icon="lucide:upload" class="w-4 h-4 mr-2 text-muted-foreground" />
<Upload class="w-4 h-4 mr-2 text-muted-foreground" />
<span class="text-sm text-muted-foreground">
{{ formData.iconPreview ? '更换' : '上传' }}
</span>
@@ -205,7 +205,7 @@ function goToAppList() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:credit-card" class="size-5" />
<CreditCard class="size-5" />
运营模式
</UiCardTitle>
<UiCardDescription>选择应用的计费方式</UiCardDescription>
@@ -223,18 +223,14 @@ function goToAppList() {
class="flex items-center justify-center w-10 h-10 rounded-lg shrink-0"
:class="formData.billing_type === option.value ? 'bg-primary text-primary-foreground' : 'bg-muted'"
>
<Icon :icon="option.icon" class="size-5" />
<component :is="option.iconComponent" class="size-5" />
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<p class="font-medium">
{{ option.label }}
</p>
<Icon
v-if="formData.billing_type === option.value"
icon="lucide:check"
class="size-4 text-primary"
/>
<Check v-if="formData.billing_type === option.value" class="size-4 text-primary" />
</div>
<p class="text-sm text-muted-foreground">
{{ option.desc }}
@@ -250,7 +246,7 @@ function goToAppList() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -265,7 +261,7 @@ function goToAppList() {
alt="图标"
class="w-full h-full object-cover"
>
<Icon v-else icon="lucide:box" class="size-6" />
<Box v-else class="size-6" />
</div>
<div class="flex-1 min-w-0">
<p class="font-medium truncate">
@@ -303,8 +299,8 @@ function goToAppList() {
:disabled="loading || !formData.name"
@click="handleCreate"
>
<Icon v-if="loading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:plus" class="mr-2 h-4 w-4" />
<Loader2 v-if="loading" class="mr-2 h-4 w-4 animate-spin" />
<Plus v-else class="mr-2 h-4 w-4" />
创建应用
</UiButton>
<UiButton
@@ -321,22 +317,22 @@ function goToAppList() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2 text-sm">
<Icon icon="lucide:info" class="size-4" />
<Info class="size-4" />
提示
</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<ul class="text-sm text-muted-foreground space-y-2">
<li class="flex items-start gap-2">
<Icon icon="lucide:check" class="size-4 text-green-500 mt-0.5 shrink-0" />
<Check class="size-4 text-green-500 mt-0.5 shrink-0" />
创建后可在设置页面配置更多选项
</li>
<li class="flex items-start gap-2">
<Icon icon="lucide:check" class="size-4 text-green-500 mt-0.5 shrink-0" />
<Check class="size-4 text-green-500 mt-0.5 shrink-0" />
支持配置验证方式安全策略等
</li>
<li class="flex items-start gap-2">
<Icon icon="lucide:check" class="size-4 text-green-500 mt-0.5 shrink-0" />
<Check class="size-4 text-green-500 mt-0.5 shrink-0" />
可随时修改应用信息和设置
</li>
</ul>
@@ -350,7 +346,7 @@ function goToAppList() {
<UiDialogContent class="sm:max-w-md">
<UiDialogHeader>
<UiDialogTitle class="flex items-center gap-2">
<Icon icon="lucide:check-circle" class="h-5 w-5 text-green-500" />
<CheckCircle class="h-5 w-5 text-green-500" />
应用创建成功
</UiDialogTitle>
<UiDialogDescription>
@@ -364,15 +360,15 @@ function goToAppList() {
</p>
<ul class="text-sm text-muted-foreground space-y-2">
<li class="flex items-center gap-2">
<Icon icon="lucide:check" class="h-4 w-4 text-green-500" />
<Check class="h-4 w-4 text-green-500" />
配置验证方式邮箱/短信验证
</li>
<li class="flex items-center gap-2">
<Icon icon="lucide:check" class="h-4 w-4 text-green-500" />
<Check class="h-4 w-4 text-green-500" />
设置安全策略加密绑定
</li>
<li class="flex items-center gap-2">
<Icon icon="lucide:check" class="h-4 w-4 text-green-500" />
<Check class="h-4 w-4 text-green-500" />
上传应用版本文件
</li>
</ul>
+7 -7
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, CreditCard, Eye, Info, Loader2 } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
@@ -160,7 +160,7 @@ onMounted(() => {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -169,7 +169,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
<Info class="size-5" />
{{ t('admin.cardTypes.create.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cardTypes.create.basicInfoDesc') }}</UiCardDescription>
@@ -210,7 +210,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:credit-card" class="size-5" />
<CreditCard class="size-5" />
{{ t('admin.cardTypes.create.rechargeConfig') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cardTypes.create.rechargeConfigDesc') }}</UiCardDescription>
@@ -345,7 +345,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
{{ t('admin.cardTypes.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -384,8 +384,8 @@ onMounted(() => {
:disabled="saving || !form.name || !form.application_id"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ t('admin.cardTypes.create.saveBtn') }}
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { CreditCard, Eye, Info, Loader2, Plus } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -136,7 +136,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
<Info class="size-5" />
{{ t('admin.cardTypes.create.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cardTypes.create.basicInfoDesc') }}</UiCardDescription>
@@ -177,7 +177,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:credit-card" class="size-5" />
<CreditCard class="size-5" />
{{ t('admin.cardTypes.create.rechargeConfig') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cardTypes.create.rechargeConfigDesc') }}</UiCardDescription>
@@ -312,7 +312,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
{{ t('admin.cardTypes.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -351,8 +351,8 @@ onMounted(() => {
:disabled="saving || !form.name || !form.application_id"
@click="handleCreate"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:plus" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Plus v-else class="mr-2 h-4 w-4" />
{{ t('admin.cardTypes.create.createNow') }}
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { CheckCircle, DollarSign, Hash, Layers, Plus } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -281,7 +281,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.cardTypes.createCardType') }}
</UiButton>
</template>
@@ -293,7 +293,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cardTypes.totalTypes') }}
</UiCardTitle>
<Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
<Layers class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -307,7 +307,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cardTypes.activeTypes') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -321,7 +321,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cardTypes.generatedCards') }}
</UiCardTitle>
<Icon icon="lucide:hash" class="size-4 text-muted-foreground" />
<Hash class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -335,7 +335,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cardTypes.totalValue') }}
</UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
<DollarSign class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
+5 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Loader2, Receipt, Settings2, Sparkles } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -137,7 +137,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.cards.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cards.create.configDesc') }}</UiCardDescription>
@@ -217,7 +217,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:receipt" class="size-5" />
<Receipt class="size-5" />
{{ t('admin.cards.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -258,8 +258,8 @@ onMounted(() => {
:disabled="saving || !form.application_id || !form.card_type_id"
@click="handleGenerate"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:sparkles" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Sparkles v-else class="mr-2 h-4 w-4" />
{{ t('admin.cards.create.generateNow') }}
</UiButton>
<UiButton
+6 -6
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Ban, CheckCircle, Key, Plus, Users } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -283,7 +283,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.cards.generateCard') }}
</UiButton>
</template>
@@ -295,7 +295,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cards.totalCards') }}
</UiCardTitle>
<Icon icon="lucide:key" class="size-4 text-muted-foreground" />
<Key class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -309,7 +309,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cards.unused') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -323,7 +323,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cards.used') }}
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
<Users class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -337,7 +337,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cards.banned') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
<Ban class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Eye, File, Loader2, Settings2, Upload, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
@@ -186,7 +186,7 @@ onMounted(() => {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -195,7 +195,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.cloudConstants.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cloudConstants.create.configDesc') }}</UiCardDescription>
@@ -258,7 +258,7 @@ onMounted(() => {
class="hidden"
@change="handleFileSelect"
>
<Icon icon="lucide:upload" class="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<Upload class="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<p class="text-sm text-muted-foreground mb-2">
拖拽文件到此处或点击选择
</p>
@@ -269,7 +269,7 @@ onMounted(() => {
<div v-else class="border rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Icon icon="lucide:file" class="h-8 w-8 text-muted-foreground" />
<File class="h-8 w-8 text-muted-foreground" />
<div>
<p class="font-medium">
{{ selectedFile.name }}
@@ -280,7 +280,7 @@ onMounted(() => {
</div>
</div>
<UiButton variant="ghost" size="icon" @click="clearFile">
<Icon icon="lucide:x" class="h-4 w-4" />
<X class="h-4 w-4" />
</UiButton>
</div>
</div>
@@ -307,7 +307,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
{{ t('admin.cloudConstants.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -358,8 +358,8 @@ onMounted(() => {
:disabled="(saving || uploading) || !form.app_id || !form.key || (form.var_type !== 'binary' && !form.value) || (form.var_type === 'binary' && !selectedFile)"
@click="handleSave"
>
<Icon v-if="saving || uploading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving || uploading" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ uploading ? '上传中...' : t('admin.cloudConstants.create.submit') }}
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Eye, File, Loader2, Settings2, Upload, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -167,7 +167,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.cloudConstants.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cloudConstants.create.configDesc') }}</UiCardDescription>
@@ -233,7 +233,7 @@ onMounted(() => {
class="hidden"
@change="handleFileSelect"
>
<Icon icon="lucide:upload" class="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<Upload class="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<p class="text-sm text-muted-foreground mb-2">
拖拽文件到此处或点击选择
</p>
@@ -244,7 +244,7 @@ onMounted(() => {
<div v-else class="border rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Icon icon="lucide:file" class="h-8 w-8 text-muted-foreground" />
<File class="h-8 w-8 text-muted-foreground" />
<div>
<p class="font-medium">
{{ selectedFile.name }}
@@ -255,7 +255,7 @@ onMounted(() => {
</div>
</div>
<UiButton variant="ghost" size="icon" @click="clearFile">
<Icon icon="lucide:x" class="h-4 w-4" />
<X class="h-4 w-4" />
</UiButton>
</div>
</div>
@@ -282,7 +282,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
{{ t('admin.cloudConstants.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -333,8 +333,8 @@ onMounted(() => {
:disabled="(saving || uploading) || !form.app_id || !form.key || (form.var_type !== 'binary' && !form.value) || (form.var_type === 'binary' && !selectedFile)"
@click="handleSave"
>
<Icon v-if="saving || uploading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving || uploading" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ uploading ? '上传中...' : t('admin.cloudConstants.create.submit') }}
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Database, LayoutGrid, Link, Plus, Unlink } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -212,7 +212,7 @@ onMounted(async () => {
>
<template #actions>
<UiButton size="sm" @click="openCreatePage">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.cloudConstants.addConstant') }}
</UiButton>
</template>
@@ -224,7 +224,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudConstants.totalConstants') }}
</UiCardTitle>
<Icon icon="lucide:database" class="size-4 text-muted-foreground" />
<Database class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -238,7 +238,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudConstants.linkedApps') }}
</UiCardTitle>
<Icon icon="lucide:link" class="size-4 text-muted-foreground" />
<Link class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -252,7 +252,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudConstants.unlinked') }}
</UiCardTitle>
<Icon icon="lucide:unlink" class="size-4 text-muted-foreground" />
<Unlink class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -266,7 +266,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudConstants.appCount') }}
</UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
<LayoutGrid class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Eye, File, Globe, Loader2, Lock, Settings2, Unlock, Upload, User, Users, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
@@ -192,7 +192,7 @@ onMounted(() => {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -201,7 +201,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.cloudVariables.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cloudVariables.create.configDesc') }}</UiCardDescription>
@@ -280,7 +280,7 @@ onMounted(() => {
class="hidden"
@change="handleFileSelect"
>
<Icon icon="lucide:upload" class="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<Upload class="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<p class="text-sm text-muted-foreground mb-2">
拖拽文件到此处或点击选择
</p>
@@ -291,7 +291,7 @@ onMounted(() => {
<div v-else class="border rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Icon icon="lucide:file" class="h-8 w-8 text-muted-foreground" />
<File class="h-8 w-8 text-muted-foreground" />
<div>
<p class="font-medium">
{{ selectedFile.name }}
@@ -302,7 +302,7 @@ onMounted(() => {
</div>
</div>
<UiButton variant="ghost" size="icon" @click="clearFile">
<Icon icon="lucide:x" class="h-4 w-4" />
<X class="h-4 w-4" />
</UiButton>
</div>
</div>
@@ -339,7 +339,7 @@ onMounted(() => {
<UiRadioGroupItem value="app" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:globe" class="size-4 text-primary" />
<Globe class="size-4 text-primary" />
<p class="font-medium">
{{ t('admin.cloudVariables.create.appScope') }}
</p>
@@ -358,7 +358,7 @@ onMounted(() => {
<UiRadioGroupItem value="user" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:user" class="size-4 text-primary" />
<User class="size-4 text-primary" />
<p class="font-medium">
{{ t('admin.cloudVariables.create.userScope') }}
</p>
@@ -387,7 +387,7 @@ onMounted(() => {
<UiRadioGroupItem value="admin" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:lock" class="size-4 text-primary" />
<Lock class="size-4 text-primary" />
<p class="font-medium">
{{ t('admin.cloudVariables.create.adminOnly') }}
</p>
@@ -403,7 +403,7 @@ onMounted(() => {
<UiRadioGroupItem value="user" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:unlock" class="size-4 text-primary" />
<Unlock class="size-4 text-primary" />
<p class="font-medium">
{{ t('admin.cloudVariables.create.userWritable') }}
</p>
@@ -420,7 +420,7 @@ onMounted(() => {
<UiRadioGroupItem value="app_user" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:users" class="size-4 text-primary" />
<Users class="size-4 text-primary" />
<p class="font-medium">
应用用户可写
</p>
@@ -436,7 +436,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
{{ t('admin.cloudVariables.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -500,8 +500,8 @@ onMounted(() => {
:disabled="(saving || uploading) || !form.app_id || !form.key || (form.var_type === 'binary' && !selectedFile)"
@click="handleSave"
>
<Icon v-if="saving || uploading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving || uploading" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ uploading ? '上传中...' : t('admin.cloudVariables.create.submit') }}
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Search, Trash2 } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -168,7 +168,7 @@ onMounted(async () => {
/>
</div>
<UiButton @click="fetchRecords">
<Icon icon="lucide:search" class="h-4 w-4 mr-2" />
<Search class="h-4 w-4 mr-2" />
查询
</UiButton>
</div>
@@ -178,7 +178,7 @@ onMounted(async () => {
variant="destructive"
@click="handleDeleteSelected"
>
<Icon icon="lucide:trash-2" class="h-4 w-4 mr-2" />
<Trash2 class="h-4 w-4 mr-2" />
删除选中 ({{ selectedIds.length }})
</UiButton>
</div>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Eye, File, Globe, Loader2, Lock, Settings2, Unlock, Upload, User, Users, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -170,7 +170,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.cloudVariables.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cloudVariables.create.configDesc') }}</UiCardDescription>
@@ -255,7 +255,7 @@ onMounted(() => {
class="hidden"
@change="handleFileSelect"
>
<Icon icon="lucide:upload" class="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<Upload class="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<p class="text-sm text-muted-foreground mb-2">
拖拽文件到此处或点击选择
</p>
@@ -266,7 +266,7 @@ onMounted(() => {
<div v-else class="border rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Icon icon="lucide:file" class="h-8 w-8 text-muted-foreground" />
<File class="h-8 w-8 text-muted-foreground" />
<div>
<p class="font-medium">
{{ selectedFile.name }}
@@ -277,7 +277,7 @@ onMounted(() => {
</div>
</div>
<UiButton variant="ghost" size="icon" @click="clearFile">
<Icon icon="lucide:x" class="h-4 w-4" />
<X class="h-4 w-4" />
</UiButton>
</div>
</div>
@@ -314,7 +314,7 @@ onMounted(() => {
<UiRadioGroupItem value="app" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:globe" class="size-4 text-primary" />
<Globe class="size-4 text-primary" />
<p class="font-medium">
{{ t('admin.cloudVariables.create.appScope') }}
</p>
@@ -333,7 +333,7 @@ onMounted(() => {
<UiRadioGroupItem value="user" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:user" class="size-4 text-primary" />
<User class="size-4 text-primary" />
<p class="font-medium">
{{ t('admin.cloudVariables.create.userScope') }}
</p>
@@ -362,7 +362,7 @@ onMounted(() => {
<UiRadioGroupItem value="admin" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:lock" class="size-4 text-primary" />
<Lock class="size-4 text-primary" />
<p class="font-medium">
{{ t('admin.cloudVariables.create.adminOnly') }}
</p>
@@ -381,7 +381,7 @@ onMounted(() => {
<UiRadioGroupItem value="user" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:unlock" class="size-4 text-primary" />
<Unlock class="size-4 text-primary" />
<p class="font-medium">
{{ t('admin.cloudVariables.create.userWritable') }}
</p>
@@ -401,7 +401,7 @@ onMounted(() => {
<UiRadioGroupItem value="app_user" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:users" class="size-4 text-primary" />
<Users class="size-4 text-primary" />
<p class="font-medium">
应用用户可写
</p>
@@ -420,7 +420,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
{{ t('admin.cloudVariables.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -484,8 +484,8 @@ onMounted(() => {
:disabled="(saving || uploading) || !form.app_id || !form.key || (form.var_type === 'binary' && !selectedFile)"
@click="handleSave"
>
<Icon v-if="saving || uploading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving || uploading" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ uploading ? '上传中...' : t('admin.cloudVariables.create.submit') }}
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Globe, LayoutGrid, Plus, User, Variable } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -230,7 +230,7 @@ onMounted(async () => {
>
<template #actions>
<UiButton size="sm" @click="openCreatePage">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.cloudVariables.addVariable') }}
</UiButton>
</template>
@@ -242,7 +242,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.totalVariables') }}
</UiCardTitle>
<Icon icon="lucide:variable" class="size-4 text-muted-foreground" />
<Variable class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -256,7 +256,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.appScopeVariables') }}
</UiCardTitle>
<Icon icon="lucide:globe" class="size-4 text-muted-foreground" />
<Globe class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -270,7 +270,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.userScopeVariables') }}
</UiCardTitle>
<Icon icon="lucide:user" class="size-4 text-muted-foreground" />
<User class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -284,7 +284,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.appCount') }}
</UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
<LayoutGrid class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { Ban, CheckCircle, RefreshCw, Trash2, X } from 'lucide-vue-next'
import type { Table } from '@tanstack/vue-table'
import { Icon } from '@iconify/vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -42,7 +42,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
@click="table.resetColumnFilters()"
>
{{ t('admin.devices.reset') }}
<Icon icon="lucide:x" class="ml-2 h-4 w-4" />
<X class="ml-2 h-4 w-4" />
</Button>
</div>
@@ -53,7 +53,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
class="h-8"
@click="emit('refresh')"
>
<Icon icon="lucide:refresh-cw" class="mr-2 h-4 w-4" />
<RefreshCw class="mr-2 h-4 w-4" />
刷新
</Button>
@@ -67,7 +67,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
class="h-8"
@click="emit('batchUnban')"
>
<Icon icon="lucide:check-circle" class="mr-2 h-4 w-4" />
<CheckCircle class="mr-2 h-4 w-4" />
{{ t('admin.devices.batchUnban') }}
</Button>
<Button
@@ -76,7 +76,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
class="h-8"
@click="emit('batchBan')"
>
<Icon icon="lucide:ban" class="mr-2 h-4 w-4" />
<Ban class="mr-2 h-4 w-4" />
{{ t('admin.devices.batchBan') }}
</Button>
<Button
@@ -85,7 +85,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
class="h-8"
@click="emit('batchUnbind')"
>
<Icon icon="lucide:trash-2" class="mr-2 h-4 w-4" />
<Trash2 class="mr-2 h-4 w-4" />
{{ t('admin.devices.batchUnbind') }}
</Button>
</template>
+5 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Ban, CheckCircle, Layers, Monitor } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
@@ -250,7 +250,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.devices.totalDevices') }}
</UiCardTitle>
<Icon icon="lucide:monitor" class="size-4 text-muted-foreground" />
<Monitor class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -264,7 +264,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.devices.onlineSessions') }}
</UiCardTitle>
<Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
<Layers class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -278,7 +278,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.devices.normalDevices') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -292,7 +292,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.devices.banned') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
<Ban class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
+5 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Code, Eye, Loader2 } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
const API_BASE = 'http://localhost:8080/api/v1'
@@ -113,14 +113,14 @@ onMounted(() => {
<UiCard>
<UiCardContent class="p-4 sm:p-6">
<div v-if="loading" class="text-center py-12">
<Icon icon="lucide:loader-2" class="h-8 w-8 animate-spin mx-auto text-muted-foreground" />
<Loader2 class="h-8 w-8 animate-spin mx-auto text-muted-foreground" />
<p class="mt-2 text-muted-foreground">
加载中...
</p>
</div>
<div v-else-if="codes.length === 0" class="text-center py-8 text-muted-foreground">
<Icon icon="lucide:code" class="mx-auto h-12 w-12 text-muted-foreground mb-2" />
<Code class="mx-auto h-12 w-12 text-muted-foreground mb-2" />
<p>暂无云端函数</p>
</div>
@@ -134,7 +134,7 @@ onMounted(() => {
<div class="flex items-start justify-between">
<div class="flex items-center space-x-3">
<div class="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
<Icon icon="lucide:code" class="h-5 w-5 text-primary" />
<Code class="h-5 w-5 text-primary" />
</div>
<div>
<p class="font-medium">
@@ -153,7 +153,7 @@ onMounted(() => {
{{ code.enabled ? '禁用' : '启用' }}
</UiButton>
<UiButton variant="outline" size="sm" @click="openCodeDialog(code)">
<Icon icon="lucide:eye" class="h-4 w-4 mr-1" />
<Eye class="h-4 w-4 mr-1" />
查看代码
</UiButton>
</div>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Eye, Loader2, Settings2 } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
@@ -128,7 +128,7 @@ onMounted(() => {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -137,7 +137,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.cloudFunction.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cloudFunction.create.configDesc') }}</UiCardDescription>
@@ -218,7 +218,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
{{ t('admin.cloudFunction.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -261,8 +261,8 @@ onMounted(() => {
:disabled="saving || !form.name || !form.application_id || !form.code"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ t('admin.cloudFunction.create.saveBtn') || '保存修改' }}
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Eye, Loader2, Settings2 } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -111,7 +111,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.cloudFunction.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cloudFunction.create.configDesc') }}</UiCardDescription>
@@ -192,7 +192,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
{{ t('admin.cloudFunction.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -237,8 +237,8 @@ onMounted(() => {
:disabled="saving || !form.name || !form.application_id || !form.code"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ t('admin.cloudFunction.create.submitBtn') }}
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Ban, CheckCircle, Code, Link, Plus } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -218,7 +218,7 @@ onMounted(() => {
>
<template #actions>
<UiButton size="sm" @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.cloudFunction.createBtn') }}
</UiButton>
</template>
@@ -230,7 +230,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudFunction.totalCodes') }}
</UiCardTitle>
<Icon icon="lucide:code" class="size-4 text-muted-foreground" />
<Code class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -244,7 +244,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudFunction.enabled') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -258,7 +258,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudFunction.disabled') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
<Ban class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -272,7 +272,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudFunction.linkedApps') }}
</UiCardTitle>
<Icon icon="lucide:link" class="size-4 text-muted-foreground" />
<Link class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Eye, Loader2, Mail, Save } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -115,7 +115,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:mail" class="size-5" />
<Mail class="size-5" />
基本配置
</UiCardTitle>
<UiCardDescription>修改SMTP服务器的基本信息</UiCardDescription>
@@ -253,7 +253,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -300,8 +300,8 @@ onMounted(() => {
:disabled="saving || !formData.name || !formData.smtp_host"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Save v-else class="mr-2 h-4 w-4" />
保存修改
</UiButton>
<UiButton
@@ -1,6 +1,6 @@
import { Edit, Mail, Send, Trash2 } from 'lucide-vue-next'
import type { ColumnDef } from '@tanstack/vue-table'
import { Icon } from '@iconify/vue'
import { h } from 'vue'
import type { EmailConfig } from '@/pages/admin/email-settings/data/schema'
@@ -22,7 +22,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
header: () => '配置名称',
cell: ({ row }) => {
return h('div', { class: 'flex items-center gap-2' }, [
h(Icon, { icon: 'lucide:mail', class: 'h-4 w-4 text-muted-foreground' }),
h(Mail, { class: 'h-4 w-4 text-muted-foreground' }),
h('span', { class: 'font-medium' }, row.original.name),
])
},
@@ -87,21 +87,21 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
title: '测试发送',
onClick: () => options.onTest(row.original),
}, () => [
h(Icon, { icon: 'lucide:send', class: 'h-4 w-4 text-blue-500' }),
h(Send, { class: 'h-4 w-4 text-blue-500' }),
]),
h(Button, {
variant: 'ghost',
size: 'sm',
onClick: () => options.onEdit(row.original),
}, () => [
h(Icon, { icon: 'lucide:edit', class: 'h-4 w-4' }),
h(Edit, { class: 'h-4 w-4' }),
]),
h(Button, {
variant: 'ghost',
size: 'sm',
onClick: () => options.onDelete(row.original),
}, () => [
h(Icon, { icon: 'lucide:trash-2', class: 'h-4 w-4 text-destructive' }),
h(Trash2, { class: 'h-4 w-4 text-destructive' }),
]),
])
},
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Eye, Loader2, Mail, Plus } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -84,7 +84,7 @@ async function handleSubmit() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:mail" class="size-5" />
<Mail class="size-5" />
基本配置
</UiCardTitle>
<UiCardDescription>填写SMTP服务器的基本信息</UiCardDescription>
@@ -222,7 +222,7 @@ async function handleSubmit() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -269,8 +269,8 @@ async function handleSubmit() {
:disabled="saving || !formData.name || !formData.smtp_host"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:plus" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Plus v-else class="mr-2 h-4 w-4" />
添加配置
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { CheckCircle, Loader2, Mail, Percent, Plus, Send, XCircle } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -159,7 +159,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
添加配置
</UiButton>
</template>
@@ -171,7 +171,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
总配置数
</UiCardTitle>
<Icon icon="lucide:mail" class="size-4 text-muted-foreground" />
<Mail class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -185,7 +185,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
已启用
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -199,7 +199,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
已禁用
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
<XCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -213,7 +213,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
启用率
</UiCardTitle>
<Icon icon="lucide:percent" class="size-4 text-muted-foreground" />
<Percent class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -297,8 +297,8 @@ onMounted(() => {
取消
</UiButton>
<UiButton :disabled="!testEmail || testSending" @click="handleTest">
<Icon v-if="testSending" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:send" class="mr-2 h-4 w-4" />
<Loader2 v-if="testSending" class="mr-2 h-4 w-4 animate-spin" />
<Send v-else class="mr-2 h-4 w-4" />
发送
</UiButton>
</UiDialogFooter>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Info, Loader2, Settings2 } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -166,7 +166,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.extension.apiKeys.create.title') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.extension.apiKeys.create.description') }}</UiCardDescription>
@@ -266,7 +266,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
<Info class="size-5" />
{{ t('admin.extension.apiKeys.create.preview') || '预览' }}
</UiCardTitle>
</UiCardHeader>
@@ -297,8 +297,8 @@ onMounted(() => {
:disabled="saving || !form.name || !form.application_id || form.permissions.length === 0"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ t('admin.extension.apiKeys.create.submitBtn') }}
</UiButton>
<UiButton
+9 -9
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Ban, CheckCircle, Key, Plus, Webhook as WebhookIcon } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -350,11 +350,11 @@ onMounted(() => {
>
<template #actions>
<UiButton v-if="currentTab === 'webhooks'" @click="goToCreateWebhook">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.extension.webhooks.createBtn') }}
</UiButton>
<UiButton v-else @click="goToCreateAPIKey">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.extension.apiKeys.createBtn') }}
</UiButton>
</template>
@@ -366,7 +366,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.extension.totalWebhooks') }}
</UiCardTitle>
<Icon icon="lucide:webhook" class="size-4 text-muted-foreground" />
<WebhookIcon class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -380,7 +380,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.extension.totalAPIKeys') }}
</UiCardTitle>
<Icon icon="lucide:key" class="size-4 text-muted-foreground" />
<Key class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -394,7 +394,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.extension.enabled') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -408,7 +408,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.extension.disabled') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
<Ban class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -421,11 +421,11 @@ onMounted(() => {
<UiTabs v-model="currentTab" default-value="webhooks" class="w-full">
<UiTabsList class="grid w-full grid-cols-2">
<UiTabsTrigger value="webhooks">
<Icon icon="lucide:webhook" class="h-4 w-4 mr-2" />
<WebhookIcon class="h-4 w-4 mr-2" />
{{ t('admin.extension.webhooks.title') }}
</UiTabsTrigger>
<UiTabsTrigger value="apikeys">
<Icon icon="lucide:key" class="h-4 w-4 mr-2" />
<Key class="h-4 w-4 mr-2" />
{{ t('admin.extension.apiKeys.title') }}
</UiTabsTrigger>
</UiTabsList>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Info, Loader2, Settings2 } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -137,7 +137,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
<Settings2 class="size-5" />
{{ t('admin.extension.webhooks.create.title') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.extension.webhooks.create.description') }}</UiCardDescription>
@@ -255,7 +255,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
<Info class="size-5" />
{{ t('admin.extension.webhooks.create.preview') || '预览' }}
</UiCardTitle>
</UiCardHeader>
@@ -294,8 +294,8 @@ onMounted(() => {
:disabled="saving || !form.name || !form.application_id || !form.url || form.events.length === 0"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ t('admin.extension.webhooks.create.submitBtn') }}
</UiButton>
<UiButton
+10 -10
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { ArrowDownLeft, ArrowUpRight, CreditCard, DollarSign, Inbox, Loader2, Trash2, TrendingUp } from 'lucide-vue-next'
import type {
ColumnDef,
ColumnFiltersState,
@@ -6,7 +7,6 @@ import type {
VisibilityState,
} from '@tanstack/vue-table'
import { Icon } from '@iconify/vue'
import {
FlexRender,
getCoreRowModel,
@@ -265,7 +265,7 @@ const columns: ColumnDef<FinanceRecord>[] = [
size: 'sm',
class: 'h-8 w-8 p-0',
onClick: () => confirmDelete(row.original),
}, () => h(Icon, { icon: 'lucide:trash-2', class: 'size-4 text-muted-foreground hover:text-destructive' }))
}, () => h(Trash2, { class: 'size-4 text-muted-foreground hover:text-destructive' }))
},
},
]
@@ -355,7 +355,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.finance.rechargeCount') }}
</UiCardTitle>
<Icon icon="lucide:arrow-down-left" class="size-4 text-muted-foreground" />
<ArrowDownLeft class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -369,7 +369,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.finance.rechargeAmount') }}
</UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
<DollarSign class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -383,7 +383,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.finance.consumptionCount') }}
</UiCardTitle>
<Icon icon="lucide:arrow-up-right" class="size-4 text-muted-foreground" />
<ArrowUpRight class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -397,7 +397,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.finance.consumptionAmount') }}
</UiCardTitle>
<Icon icon="lucide:credit-card" class="size-4 text-muted-foreground" />
<CreditCard class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -411,7 +411,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.finance.profit') }}
</UiCardTitle>
<Icon icon="lucide:trending-up" class="size-4 text-muted-foreground" />
<TrendingUp class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -437,7 +437,7 @@ onMounted(() => {
size="sm"
@click="confirmBatchDelete"
>
<Icon icon="lucide:trash-2" class="mr-2 size-4" />
<Trash2 class="mr-2 size-4" />
{{ t('admin.finance.batchDeleteBtn') }}
</UiButton>
</div>
@@ -502,7 +502,7 @@ onMounted(() => {
</div>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="border rounded-md">
@@ -533,7 +533,7 @@ onMounted(() => {
class="h-24 text-center"
>
<div class="flex flex-col items-center justify-center text-muted-foreground">
<Icon icon="lucide:inbox" class="size-12 mb-2 opacity-50" />
<Inbox class="size-12 mb-2 opacity-50" />
<span>{{ t('admin.finance.noRecords') }}</span>
</div>
</TableCell>
+18 -16
View File
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Activity, AlertCircle, ArrowRight, CheckCircle, DollarSign, Key, LayoutGrid, Loader2, PlusCircle, Ticket, Users } from 'lucide-vue-next'
import type { LucideProps } from 'lucide-vue-next'
import * as echarts from 'echarts'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import type { FunctionalComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -22,7 +24,7 @@ interface Activity {
id: number
title: string
description: string
icon: string
icon: FunctionalComponent<LucideProps>
createdAt: Date
}
@@ -195,21 +197,21 @@ async function fetchDashboard() {
if (data.data.recentActivities && Array.isArray(data.data.recentActivities)) {
recentActivities.value = data.data.recentActivities.map((log: any) => {
let icon = 'lucide:activity'
let icon = Activity
let title = log.action || t('admin.operation')
let description = log.details || log.resource || t('admin.noDescription')
if (log.log_type === 'operation') {
icon = 'lucide:plus-circle'
icon = PlusCircle
title = t('admin.operationRecord')
}
else if (log.log_type === 'verification') {
icon = 'lucide:check-circle'
icon = CheckCircle
title = t('admin.cardVerification')
description = `${t('admin.verification')}${log.status === 'success' ? t('admin.success') : t('admin.failed')}`
}
else if (log.log_type === 'exception') {
icon = 'lucide:alert-circle'
icon = AlertCircle
title = t('admin.exceptionRecord')
}
@@ -483,7 +485,7 @@ onUnmounted(() => {
>
<div v-if="loading" class="flex items-center justify-center py-12">
<div class="text-center">
<Icon icon="lucide:loader-2" class="size-8 animate-spin mx-auto text-muted-foreground" />
<Loader2 class="size-8 animate-spin mx-auto text-muted-foreground" />
<p class="mt-4 text-muted-foreground">
{{ t('admin.loading') }}...
</p>
@@ -497,7 +499,7 @@ onUnmounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.totalApplications') }}
</UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
<LayoutGrid class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -514,7 +516,7 @@ onUnmounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.totalCards') }}
</UiCardTitle>
<Icon icon="lucide:key" class="size-4 text-muted-foreground" />
<Key class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -531,7 +533,7 @@ onUnmounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.totalUsers') }}
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
<Users class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -548,7 +550,7 @@ onUnmounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.monthlyRevenue') }}
</UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
<DollarSign class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -610,7 +612,7 @@ onUnmounted(() => {
</div>
<UiButton variant="ghost" size="sm" @click="goToTickets">
{{ t('admin.viewAll') }}
<Icon icon="lucide:arrow-right" class="ml-1 size-4" />
<ArrowRight class="ml-1 size-4" />
</UiButton>
</div>
</UiCardHeader>
@@ -624,7 +626,7 @@ onUnmounted(() => {
>
<div class="flex-shrink-0">
<div class="size-8 rounded-full bg-primary/10 flex items-center justify-center">
<Icon icon="lucide:ticket" class="size-4 text-primary" />
<Ticket class="size-4 text-primary" />
</div>
</div>
<div class="flex-1 min-w-0">
@@ -644,7 +646,7 @@ onUnmounted(() => {
</div>
</div>
<div v-else class="text-center py-8 text-muted-foreground">
<Icon icon="lucide:ticket" class="mx-auto size-10 text-muted-foreground mb-2" />
<Ticket class="mx-auto size-10 text-muted-foreground mb-2" />
<p class="text-sm">
{{ t('admin.noTickets') }}
</p>
@@ -666,7 +668,7 @@ onUnmounted(() => {
>
<div class="flex-shrink-0">
<div class="size-7 rounded-full bg-primary/10 flex items-center justify-center">
<Icon :icon="activity.icon" class="size-3.5 text-primary" />
<component :is="activity.icon" class="size-3.5 text-primary" />
</div>
</div>
<div class="flex-1 min-w-0">
@@ -683,7 +685,7 @@ onUnmounted(() => {
</div>
</div>
<div v-else class="text-center py-8 text-muted-foreground">
<Icon icon="lucide:activity" class="mx-auto size-10 text-muted-foreground mb-2" />
<Activity class="mx-auto size-10 text-muted-foreground mb-2" />
<p class="text-sm">
{{ t('admin.noActivities') }}
</p>
+5 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Activity, CheckCircle, FileText, XCircle } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -126,7 +126,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.logs.totalLogs') }}
</UiCardTitle>
<Icon icon="lucide:file-text" class="size-4 text-muted-foreground" />
<FileText class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -140,7 +140,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.logs.successCount') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -154,7 +154,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.logs.failedCount') }}
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
<XCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -168,7 +168,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.logs.operationCount') }}
</UiCardTitle>
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
<Activity class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
+5 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Activity, CheckCircle, FileText, XCircle } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -136,7 +136,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.logs.totalLogs') }}
</UiCardTitle>
<Icon icon="lucide:file-text" class="size-4 text-muted-foreground" />
<FileText class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -150,7 +150,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.logs.successCount') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -164,7 +164,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.logs.failedCount') }}
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
<XCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -178,7 +178,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.logs.operationCount') }}
</UiCardTitle>
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
<Activity class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -1,5 +1,7 @@
<script setup lang="ts">
// @iconify/vue StripePayPalUSDT lucide
import { Icon } from '@iconify/vue'
import { CreditCard, Eye, Loader2, Save, Wallet } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -24,13 +26,13 @@ const formData = ref({
})
const typeOptions = [
{ value: 'alipay', label: '支付宝', icon: 'ri:alipay-fill', color: 'text-blue-500' },
{ value: 'wechat', label: '微信支付', icon: 'ri:wechat-pay-fill', color: 'text-green-500' },
{ value: 'stripe', label: 'Stripe', icon: 'logos:stripe', color: '' },
{ value: 'paypal', label: 'PayPal', icon: 'logos:paypal', color: '' },
{ value: 'bepusdt', label: 'BEPUSDT', icon: 'cryptocurrency:usdt', color: 'text-green-500' },
{ value: 'epay', label: '易支付', icon: 'lucide:wallet', color: 'text-orange-500' },
{ value: 'other', label: '其他', icon: 'lucide:credit-card', color: 'text-gray-500' },
{ value: 'alipay', label: '支付宝', icon: 'ri:alipay-fill', color: 'text-blue-500', isBrand: true },
{ value: 'wechat', label: '微信支付', icon: 'ri:wechat-pay-fill', color: 'text-green-500', isBrand: true },
{ value: 'stripe', label: 'Stripe', icon: 'logos:stripe', color: '', isBrand: true },
{ value: 'paypal', label: 'PayPal', icon: 'logos:paypal', color: '', isBrand: true },
{ value: 'bepusdt', label: 'BEPUSDT', icon: 'cryptocurrency:usdt', color: 'text-green-500', isBrand: true },
{ value: 'epay', label: '易支付', iconComponent: Wallet, color: 'text-orange-500' },
{ value: 'other', label: '其他', iconComponent: CreditCard, color: 'text-gray-500' },
]
const selectedType = computed(() => {
@@ -109,7 +111,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:credit-card" class="size-5" />
<CreditCard class="size-5" />
渠道配置
</UiCardTitle>
<UiCardDescription>修改支付渠道的基本信息</UiCardDescription>
@@ -137,7 +139,8 @@ onMounted(() => {
<UiSelectContent>
<UiSelectItem v-for="type in typeOptions" :key="type.value" :value="type.value">
<div class="flex items-center gap-2">
<Icon :icon="type.icon" :class="['h-4 w-4', type.color]" />
<Icon v-if="type.isBrand" :icon="type.icon" :class="['h-4 w-4', type.color]" />
<component v-else :is="type.iconComponent" :class="['h-4 w-4', type.color]" />
{{ type.label }}
</div>
</UiSelectItem>
@@ -211,7 +214,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -224,7 +227,8 @@ onMounted(() => {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">渠道类型</span>
<div class="flex items-center gap-1.5">
<Icon :icon="selectedType?.icon || 'lucide:credit-card'" :class="['h-4 w-4', selectedType?.color]" />
<Icon v-if="selectedType?.isBrand" :icon="selectedType?.icon" :class="['h-4 w-4', selectedType?.color]" />
<component v-else :is="selectedType?.iconComponent || CreditCard" :class="['h-4 w-4', selectedType?.color]" />
<span>{{ selectedType?.label || '-' }}</span>
</div>
</div>
@@ -249,8 +253,8 @@ onMounted(() => {
:disabled="saving || !formData.name"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Save v-else class="mr-2 h-4 w-4" />
保存修改
</UiButton>
<UiButton
@@ -1,6 +1,8 @@
// @iconify/vue 保留用于品牌图标(支付宝、微信、Stripe、PayPal、USDT),这些图标在 lucide 中不存在
import { Icon } from '@iconify/vue'
import { CreditCard, Edit, Trash2, Wallet } from 'lucide-vue-next'
import type { ColumnDef } from '@tanstack/vue-table'
import { Icon } from '@iconify/vue'
import { h } from 'vue'
import type { PaymentChannel } from '@/pages/admin/payment-channels/data/schema'
@@ -25,16 +27,18 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
other: '其他',
}
const typeIcons: Record<string, string> = {
const typeIcons: Record<string, any> = {
alipay: 'ri:alipay-fill',
wechat: 'ri:wechat-pay-fill',
stripe: 'logos:stripe',
paypal: 'logos:paypal',
bepusdt: 'cryptocurrency:usdt',
epay: 'lucide:wallet',
other: 'lucide:credit-card',
epay: Wallet,
other: CreditCard,
}
const brandIcons = new Set(['ri:alipay-fill', 'ri:wechat-pay-fill', 'logos:stripe', 'logos:paypal', 'cryptocurrency:usdt'])
return [
{
accessorKey: 'name',
@@ -43,7 +47,12 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
return h('div', { class: 'flex items-center gap-2' }, [
row.original.icon
? h('img', { src: row.original.icon, class: 'h-5 w-5 rounded', alt: '' })
: h(Icon, { icon: typeIcons[row.original.type] || 'lucide:credit-card', class: 'h-5 w-5' }),
: (() => {
const icon = typeIcons[row.original.type] || CreditCard
return typeof icon === 'string' && brandIcons.has(icon)
? h(Icon, { icon, class: 'h-5 w-5' })
: h(icon, { class: 'h-5 w-5' })
})(),
h('span', { class: 'font-medium' }, row.original.name),
])
},
@@ -104,14 +113,14 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
size: 'sm',
onClick: () => options.onEdit(row.original),
}, () => [
h(Icon, { icon: 'lucide:edit', class: 'h-4 w-4' }),
h(Edit, { class: 'h-4 w-4' }),
]),
h(Button, {
variant: 'ghost',
size: 'sm',
onClick: () => options.onDelete(row.original),
}, () => [
h(Icon, { icon: 'lucide:trash-2', class: 'h-4 w-4 text-destructive' }),
h(Trash2, { class: 'h-4 w-4 text-destructive' }),
]),
])
},
@@ -1,5 +1,7 @@
<script setup lang="ts">
// @iconify/vue StripePayPalUSDT lucide
import { Icon } from '@iconify/vue'
import { CreditCard, Eye, Loader2, Plus, Wallet } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -22,13 +24,13 @@ const formData = ref({
})
const typeOptions = [
{ value: 'alipay', label: '支付宝', icon: 'ri:alipay-fill', color: 'text-blue-500' },
{ value: 'wechat', label: '微信支付', icon: 'ri:wechat-pay-fill', color: 'text-green-500' },
{ value: 'stripe', label: 'Stripe', icon: 'logos:stripe', color: '' },
{ value: 'paypal', label: 'PayPal', icon: 'logos:paypal', color: '' },
{ value: 'bepusdt', label: 'BEPUSDT', icon: 'cryptocurrency:usdt', color: 'text-green-500' },
{ value: 'epay', label: '易支付', icon: 'lucide:wallet', color: 'text-orange-500' },
{ value: 'other', label: '其他', icon: 'lucide:credit-card', color: 'text-gray-500' },
{ value: 'alipay', label: '支付宝', icon: 'ri:alipay-fill', color: 'text-blue-500', isBrand: true },
{ value: 'wechat', label: '微信支付', icon: 'ri:wechat-pay-fill', color: 'text-green-500', isBrand: true },
{ value: 'stripe', label: 'Stripe', icon: 'logos:stripe', color: '', isBrand: true },
{ value: 'paypal', label: 'PayPal', icon: 'logos:paypal', color: '', isBrand: true },
{ value: 'bepusdt', label: 'BEPUSDT', icon: 'cryptocurrency:usdt', color: 'text-green-500', isBrand: true },
{ value: 'epay', label: '易支付', iconComponent: Wallet, color: 'text-orange-500' },
{ value: 'other', label: '其他', iconComponent: CreditCard, color: 'text-gray-500' },
]
const selectedType = computed(() => {
@@ -77,7 +79,7 @@ async function handleSubmit() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:credit-card" class="size-5" />
<CreditCard class="size-5" />
渠道配置
</UiCardTitle>
<UiCardDescription>填写支付渠道的基本信息</UiCardDescription>
@@ -105,7 +107,8 @@ async function handleSubmit() {
<UiSelectContent>
<UiSelectItem v-for="type in typeOptions" :key="type.value" :value="type.value">
<div class="flex items-center gap-2">
<Icon :icon="type.icon" :class="['h-4 w-4', type.color]" />
<Icon v-if="type.isBrand" :icon="type.icon" :class="['h-4 w-4', type.color]" />
<component v-else :is="type.iconComponent" :class="['h-4 w-4', type.color]" />
{{ type.label }}
</div>
</UiSelectItem>
@@ -179,7 +182,7 @@ async function handleSubmit() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -192,7 +195,8 @@ async function handleSubmit() {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">渠道类型</span>
<div class="flex items-center gap-1.5">
<Icon :icon="selectedType?.icon || 'lucide:credit-card'" :class="['h-4 w-4', selectedType?.color]" />
<Icon v-if="selectedType?.isBrand" :icon="selectedType?.icon" :class="['h-4 w-4', selectedType?.color]" />
<component v-else :is="selectedType?.iconComponent || CreditCard" :class="['h-4 w-4', selectedType?.color]" />
<span>{{ selectedType?.label || '-' }}</span>
</div>
</div>
@@ -217,8 +221,8 @@ async function handleSubmit() {
:disabled="saving || !formData.name"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:plus" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Plus v-else class="mr-2 h-4 w-4" />
添加渠道
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { CheckCircle, CreditCard, Percent, Plus, XCircle } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -129,7 +129,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
添加渠道
</UiButton>
</template>
@@ -141,7 +141,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
总渠道数
</UiCardTitle>
<Icon icon="lucide:credit-card" class="size-4 text-muted-foreground" />
<CreditCard class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -155,7 +155,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
已启用
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -169,7 +169,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
已禁用
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
<XCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -183,7 +183,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
启用率
</UiCardTitle>
<Icon icon="lucide:percent" class="size-4 text-muted-foreground" />
<Percent class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
+19 -25
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { AppWindow, Check, Globe, Globe2, Info, Loader2, MapPin, ShieldPlus, Smartphone, UserX } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -32,11 +32,11 @@ const form = ref({
is_global: true,
})
const typeOptions: { value: 'ip' | 'device' | 'user' | 'region', label: string, icon: string, description: string, placeholder: string, scopeType: 'global' | 'app' | 'both' }[] = [
{ value: 'ip', label: 'IP封禁', icon: 'lucide:globe', description: '封禁特定IP地址或IP段', placeholder: '输入IP地址,如 192.168.1.1 或 192.168.1.0/24', scopeType: 'both' },
{ value: 'device', label: '设备封禁', icon: 'lucide:smartphone', description: '封禁特定设备指纹', placeholder: '输入设备指纹', scopeType: 'both' },
{ value: 'user', label: '用户封禁', icon: 'lucide:user-x', description: '封禁特定用户账号(需指定应用)', placeholder: '输入用户ID或用户名', scopeType: 'app' },
{ value: 'region', label: '地区封禁', icon: 'lucide:map-pin', description: '封禁特定地区访问', placeholder: '', scopeType: 'both' },
const typeOptions: { value: 'ip' | 'device' | 'user' | 'region', label: string, icon: any, description: string, placeholder: string, scopeType: 'global' | 'app' | 'both' }[] = [
{ value: 'ip', label: 'IP封禁', icon: Globe, description: '封禁特定IP地址或IP段', placeholder: '输入IP地址,如 192.168.1.1 或 192.168.1.0/24', scopeType: 'both' },
{ value: 'device', label: '设备封禁', icon: Smartphone, description: '封禁特定设备指纹', placeholder: '输入设备指纹', scopeType: 'both' },
{ value: 'user', label: '用户封禁', icon: UserX, description: '封禁特定用户账号(需指定应用)', placeholder: '输入用户ID或用户名', scopeType: 'app' },
{ value: 'region', label: '地区封禁', icon: MapPin, description: '封禁特定地区访问', placeholder: '', scopeType: 'both' },
]
const currentTypeInfo = computed(() => {
@@ -212,7 +212,7 @@ onMounted(() => {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -221,7 +221,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:shield-plus" class="size-5" />
<ShieldPlus class="size-5" />
规则配置
</UiCardTitle>
<UiCardDescription>配置风控规则的类型和封禁目标</UiCardDescription>
@@ -238,8 +238,8 @@ onMounted(() => {
:class="form.type === option.value ? 'border-primary bg-primary/5 ring-1 ring-primary' : 'border-border hover:border-primary/50 hover:bg-muted/50'"
@click="form.type = option.value"
>
<Icon
:icon="option.icon"
<component
:is="option.icon"
class="size-5 mt-0.5 transition-colors duration-200"
:class="form.type === option.value ? 'text-primary' : 'text-muted-foreground'"
/>
@@ -267,11 +267,8 @@ onMounted(() => {
:class="form.is_global ? 'border-primary bg-primary/5 ring-1 ring-primary' : 'border-border hover:border-primary/50 hover:bg-muted/50'"
@click="form.is_global = true"
>
<Icon
icon="lucide:globe-2"
class="size-5 mt-0.5 transition-colors duration-200"
:class="form.is_global ? 'text-primary' : 'text-muted-foreground'"
/>
<Globe2 class="size-5 mt-0.5 transition-colors duration-200"
:class="form.is_global ? 'text-primary' : 'text-muted-foreground'" />
<div>
<div
class="font-medium transition-colors duration-200"
@@ -290,11 +287,8 @@ onMounted(() => {
:class="!form.is_global ? 'border-primary bg-primary/5 ring-1 ring-primary' : 'border-border hover:border-primary/50 hover:bg-muted/50'"
@click="form.is_global = false"
>
<Icon
icon="lucide:app-window"
class="size-5 mt-0.5 transition-colors duration-200"
:class="!form.is_global ? 'text-primary' : 'text-muted-foreground'"
/>
<AppWindow class="size-5 mt-0.5 transition-colors duration-200"
:class="!form.is_global ? 'text-primary' : 'text-muted-foreground'" />
<div>
<div
class="font-medium transition-colors duration-200"
@@ -373,7 +367,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
<Info class="size-5" />
规则预览
</UiCardTitle>
</UiCardHeader>
@@ -382,7 +376,7 @@ onMounted(() => {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">生效范围</span>
<span class="flex items-center gap-1">
<Icon :icon="form.is_global ? 'lucide:globe-2' : 'lucide:app-window'" class="size-4" />
<component :is="form.is_global ? Globe2 : AppWindow" class="size-4" />
{{ form.is_global ? '全局封禁' : '应用级封禁' }}
</span>
</div>
@@ -393,7 +387,7 @@ onMounted(() => {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">规则类型</span>
<span class="flex items-center gap-1">
<Icon :icon="currentTypeInfo.icon" class="size-4" />
<component :is="currentTypeInfo.icon" class="size-4" />
{{ currentTypeInfo.label }}
</span>
</div>
@@ -424,8 +418,8 @@ onMounted(() => {
:disabled="saving || !form.value.trim()"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
保存修改
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { AppWindow, Globe, Globe2, Info, Loader2, MapPin, Plus, ShieldPlus, Smartphone, UserX } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -29,11 +29,11 @@ const form = ref({
is_global: true,
})
const typeOptions: { value: 'ip' | 'device' | 'user' | 'region', label: string, icon: string, description: string, placeholder: string, scopeType: 'global' | 'app' | 'both' }[] = [
{ value: 'ip', label: 'IP封禁', icon: 'lucide:globe', description: '封禁特定IP地址或IP段', placeholder: '输入IP地址,如 192.168.1.1 或 192.168.1.0/24', scopeType: 'both' },
{ value: 'device', label: '设备封禁', icon: 'lucide:smartphone', description: '封禁特定设备指纹', placeholder: '输入设备指纹', scopeType: 'both' },
{ value: 'user', label: '用户封禁', icon: 'lucide:user-x', description: '封禁特定用户账号(需指定应用)', placeholder: '输入用户ID或用户名', scopeType: 'app' },
{ value: 'region', label: '地区封禁', icon: 'lucide:map-pin', description: '封禁特定地区访问', placeholder: '', scopeType: 'both' },
const typeOptions: { value: 'ip' | 'device' | 'user' | 'region', label: string, icon: any, description: string, placeholder: string, scopeType: 'global' | 'app' | 'both' }[] = [
{ value: 'ip', label: 'IP封禁', icon: Globe, description: '封禁特定IP地址或IP段', placeholder: '输入IP地址,如 192.168.1.1 或 192.168.1.0/24', scopeType: 'both' },
{ value: 'device', label: '设备封禁', icon: Smartphone, description: '封禁特定设备指纹', placeholder: '输入设备指纹', scopeType: 'both' },
{ value: 'user', label: '用户封禁', icon: UserX, description: '封禁特定用户账号(需指定应用)', placeholder: '输入用户ID或用户名', scopeType: 'app' },
{ value: 'region', label: '地区封禁', icon: MapPin, description: '封禁特定地区访问', placeholder: '', scopeType: 'both' },
]
const currentTypeInfo = computed(() => {
@@ -193,7 +193,7 @@ async function handleSave() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:shield-plus" class="size-5" />
<ShieldPlus class="size-5" />
规则配置
</UiCardTitle>
<UiCardDescription>配置风控规则的类型和封禁目标</UiCardDescription>
@@ -210,8 +210,8 @@ async function handleSave() {
:class="form.type === option.value ? 'border-primary bg-primary/5 ring-1 ring-primary' : 'border-border hover:border-primary/50 hover:bg-muted/50'"
@click="form.type = option.value"
>
<Icon
:icon="option.icon"
<component
:is="option.icon"
class="size-5 mt-0.5 transition-colors duration-200"
:class="form.type === option.value ? 'text-primary' : 'text-muted-foreground'"
/>
@@ -239,11 +239,8 @@ async function handleSave() {
:class="form.is_global ? 'border-primary bg-primary/5 ring-1 ring-primary' : 'border-border hover:border-primary/50 hover:bg-muted/50'"
@click="form.is_global = true"
>
<Icon
icon="lucide:globe-2"
class="size-5 mt-0.5 transition-colors duration-200"
:class="form.is_global ? 'text-primary' : 'text-muted-foreground'"
/>
<Globe2 class="size-5 mt-0.5 transition-colors duration-200"
:class="form.is_global ? 'text-primary' : 'text-muted-foreground'" />
<div>
<div
class="font-medium transition-colors duration-200"
@@ -262,11 +259,8 @@ async function handleSave() {
:class="!form.is_global ? 'border-primary bg-primary/5 ring-1 ring-primary' : 'border-border hover:border-primary/50 hover:bg-muted/50'"
@click="form.is_global = false"
>
<Icon
icon="lucide:app-window"
class="size-5 mt-0.5 transition-colors duration-200"
:class="!form.is_global ? 'text-primary' : 'text-muted-foreground'"
/>
<AppWindow class="size-5 mt-0.5 transition-colors duration-200"
:class="!form.is_global ? 'text-primary' : 'text-muted-foreground'" />
<div>
<div
class="font-medium transition-colors duration-200"
@@ -345,7 +339,7 @@ async function handleSave() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
<Info class="size-5" />
规则预览
</UiCardTitle>
</UiCardHeader>
@@ -354,7 +348,7 @@ async function handleSave() {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">生效范围</span>
<span class="flex items-center gap-1">
<Icon :icon="form.is_global ? 'lucide:globe-2' : 'lucide:app-window'" class="size-4" />
<component :is="form.is_global ? Globe2 : AppWindow" class="size-4" />
{{ form.is_global ? '全局封禁' : '应用级封禁' }}
</span>
</div>
@@ -365,7 +359,7 @@ async function handleSave() {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">规则类型</span>
<span class="flex items-center gap-1">
<Icon :icon="currentTypeInfo.icon" class="size-4" />
<component :is="currentTypeInfo.icon" class="size-4" />
{{ currentTypeInfo.label }}
</span>
</div>
@@ -396,8 +390,8 @@ async function handleSave() {
:disabled="saving || !form.value.trim()"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:plus" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Plus v-else class="mr-2 h-4 w-4" />
添加规则
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Globe, Plus, ShieldCheck, ShieldOff, Smartphone } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -161,7 +161,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
添加规则
</UiButton>
</template>
@@ -173,7 +173,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
生效中
</UiCardTitle>
<Icon icon="lucide:shield-check" class="size-4 text-muted-foreground" />
<ShieldCheck class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -187,7 +187,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
已失效
</UiCardTitle>
<Icon icon="lucide:shield-off" class="size-4 text-muted-foreground" />
<ShieldOff class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -201,7 +201,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
IP封禁
</UiCardTitle>
<Icon icon="lucide:globe" class="size-4 text-muted-foreground" />
<Globe class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -215,7 +215,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
设备封禁
</UiCardTitle>
<Icon icon="lucide:smartphone" class="size-4 text-muted-foreground" />
<Smartphone class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
+5 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Boxes, Monitor, Users, Wifi } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
@@ -147,7 +147,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
在线实例数
</UiCardTitle>
<Icon icon="lucide:wifi" class="size-4 text-muted-foreground" />
<Wifi class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -161,7 +161,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
应用数量
</UiCardTitle>
<Icon icon="lucide:boxes" class="size-4 text-muted-foreground" />
<Boxes class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -175,7 +175,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
在线设备数
</UiCardTitle>
<Icon icon="lucide:monitor" class="size-4 text-muted-foreground" />
<Monitor class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -189,7 +189,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
在线用户数
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
<Users class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Cloud, Eye, Loader2, Save, Settings, Smartphone } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -22,9 +22,9 @@ const formData = ref({
})
const typeOptions = [
{ value: 'aliyun', label: '阿里云', desc: '阿里云短信服务', icon: 'lucide:cloud' },
{ value: 'tencent', label: '腾讯云', desc: '腾讯云短信服务', icon: 'lucide:cloud' },
{ value: 'other', label: '其他', desc: '其他短信服务商', icon: 'lucide:settings' },
{ value: 'aliyun', label: '阿里云', desc: '阿里云短信服务', icon: Cloud },
{ value: 'tencent', label: '腾讯云', desc: '腾讯云短信服务', icon: Cloud },
{ value: 'other', label: '其他', desc: '其他短信服务商', icon: Settings },
]
const selectedType = computed(() => {
@@ -127,7 +127,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:smartphone" class="size-5" />
<Smartphone class="size-5" />
基本配置
</UiCardTitle>
<UiCardDescription>修改短信服务的基本信息</UiCardDescription>
@@ -157,7 +157,7 @@ onMounted(() => {
<UiSelectContent>
<UiSelectItem v-for="type in typeOptions" :key="type.value" :value="type.value">
<div class="flex items-center gap-2">
<Icon :icon="type.icon" class="size-4" />
<component :is="type.icon" class="size-4" />
<span>{{ type.label }}</span>
</div>
</UiSelectItem>
@@ -213,7 +213,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -248,8 +248,8 @@ onMounted(() => {
:disabled="saving || !formData.name"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Save v-else class="mr-2 h-4 w-4" />
保存修改
</UiButton>
<UiButton
@@ -1,6 +1,6 @@
import { Cloud, Edit, Send, Settings, Smartphone, Trash2 } from 'lucide-vue-next'
import type { ColumnDef } from '@tanstack/vue-table'
import { Icon } from '@iconify/vue'
import { h } from 'vue'
import type { SmsConfig } from '@/pages/admin/sms-settings/data/schema'
@@ -22,7 +22,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
header: () => '配置名称',
cell: ({ row }) => {
return h('div', { class: 'flex items-center gap-2' }, [
h(Icon, { icon: 'lucide:smartphone', class: 'h-4 w-4 text-muted-foreground' }),
h(Smartphone, { class: 'h-4 w-4 text-muted-foreground' }),
h('span', { class: 'font-medium' }, row.original.name),
])
},
@@ -36,13 +36,13 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
tencent: '腾讯云',
other: '其他',
}
const typeIcons: Record<string, string> = {
aliyun: 'lucide:cloud',
tencent: 'lucide:cloud',
other: 'lucide:settings',
const typeIcons: Record<string, any> = {
aliyun: Cloud,
tencent: Cloud,
other: Settings,
}
return h('div', { class: 'flex items-center gap-2' }, [
h(Icon, { icon: typeIcons[row.original.type] || 'lucide:settings', class: 'h-4 w-4 text-muted-foreground' }),
h(typeIcons[row.original.type] || Settings, { class: 'h-4 w-4 text-muted-foreground' }),
h('span', { class: 'text-sm' }, typeLabels[row.original.type] || row.original.type),
])
},
@@ -90,21 +90,21 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
title: '测试发送',
onClick: () => options.onTest(row.original),
}, () => [
h(Icon, { icon: 'lucide:send', class: 'h-4 w-4 text-blue-500' }),
h(Send, { class: 'h-4 w-4 text-blue-500' }),
]),
h(Button, {
variant: 'ghost',
size: 'sm',
onClick: () => options.onEdit(row.original),
}, () => [
h(Icon, { icon: 'lucide:edit', class: 'h-4 w-4' }),
h(Edit, { class: 'h-4 w-4' }),
]),
h(Button, {
variant: 'ghost',
size: 'sm',
onClick: () => options.onDelete(row.original),
}, () => [
h(Icon, { icon: 'lucide:trash-2', class: 'h-4 w-4 text-destructive' }),
h(Trash2, { class: 'h-4 w-4 text-destructive' }),
]),
])
},
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Cloud, Eye, Loader2, Plus, Settings, Smartphone } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -20,9 +20,9 @@ const formData = ref({
})
const typeOptions = [
{ value: 'aliyun', label: '阿里云', desc: '阿里云短信服务', icon: 'lucide:cloud' },
{ value: 'tencent', label: '腾讯云', desc: '腾讯云短信服务', icon: 'lucide:cloud' },
{ value: 'other', label: '其他', desc: '其他短信服务商', icon: 'lucide:settings' },
{ value: 'aliyun', label: '阿里云', desc: '阿里云短信服务', icon: Cloud },
{ value: 'tencent', label: '腾讯云', desc: '腾讯云短信服务', icon: Cloud },
{ value: 'other', label: '其他', desc: '其他短信服务商', icon: Settings },
]
const selectedType = computed(() => {
@@ -97,7 +97,7 @@ async function handleSubmit() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:smartphone" class="size-5" />
<Smartphone class="size-5" />
基本配置
</UiCardTitle>
<UiCardDescription>填写短信服务的基本信息</UiCardDescription>
@@ -127,7 +127,7 @@ async function handleSubmit() {
<UiSelectContent>
<UiSelectItem v-for="type in typeOptions" :key="type.value" :value="type.value">
<div class="flex items-center gap-2">
<Icon :icon="type.icon" class="size-4" />
<component :is="type.icon" class="size-4" />
<span>{{ type.label }}</span>
</div>
</UiSelectItem>
@@ -183,7 +183,7 @@ async function handleSubmit() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -218,8 +218,8 @@ async function handleSubmit() {
:disabled="saving || !formData.name"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:plus" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Plus v-else class="mr-2 h-4 w-4" />
添加配置
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { CheckCircle, Loader2, Percent, Plus, Send, Smartphone, XCircle } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -159,7 +159,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
添加配置
</UiButton>
</template>
@@ -171,7 +171,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
总配置数
</UiCardTitle>
<Icon icon="lucide:smartphone" class="size-4 text-muted-foreground" />
<Smartphone class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -185,7 +185,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
已启用
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -199,7 +199,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
已禁用
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
<XCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -213,7 +213,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
启用率
</UiCardTitle>
<Icon icon="lucide:percent" class="size-4 text-muted-foreground" />
<Percent class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -297,8 +297,8 @@ onMounted(() => {
取消
</UiButton>
<UiButton :disabled="!testPhone || testSending" @click="handleTest">
<Icon v-if="testSending" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:send" class="mr-2 h-4 w-4" />
<Loader2 v-if="testSending" class="mr-2 h-4 w-4 animate-spin" />
<Send v-else class="mr-2 h-4 w-4" />
发送
</UiButton>
</UiDialogFooter>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Cloud, Database, Eye, Folder, Globe, HardDrive, Loader2, Lock, Plug, Save } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -36,11 +36,11 @@ const isActive = computed({
})
const storageTypes = [
{ value: 'local', label: '本地存储', icon: 'lucide:hard-drive', description: '存储在服务器本地磁盘' },
{ value: 's3', label: 'S3存储', icon: 'lucide:cloud', description: '兼容S3协议的对象存储' },
{ value: 'webdav', label: 'WebDAV', icon: 'lucide:globe', description: 'WebDAV协议存储' },
{ value: 'ftp', label: 'FTP', icon: 'lucide:folder', description: 'FTP协议存储' },
{ value: 'sftp', label: 'SFTP', icon: 'lucide:lock', description: 'SFTP协议存储' },
{ value: 'local', label: '本地存储', icon: HardDrive, description: '存储在服务器本地磁盘' },
{ value: 's3', label: 'S3存储', icon: Cloud, description: '兼容S3协议的对象存储' },
{ value: 'webdav', label: 'WebDAV', icon: Globe, description: 'WebDAV协议存储' },
{ value: 'ftp', label: 'FTP', icon: Folder, description: 'FTP协议存储' },
{ value: 'sftp', label: 'SFTP', icon: Lock, description: 'SFTP协议存储' },
]
const selectedType = computed(() => {
@@ -160,7 +160,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:database" class="size-5" />
<Database class="size-5" />
存储配置
</UiCardTitle>
<UiCardDescription>填写存储的基本信息</UiCardDescription>
@@ -188,7 +188,7 @@ onMounted(() => {
<UiSelectContent>
<UiSelectItem v-for="type in storageTypes" :key="type.value" :value="type.value">
<div class="flex items-center gap-2">
<Icon :icon="type.icon" class="h-4 w-4" />
<component :is="type.icon" class="h-4 w-4" />
{{ type.label }}
</div>
</UiSelectItem>
@@ -319,7 +319,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -332,7 +332,7 @@ onMounted(() => {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">存储类型</span>
<div class="flex items-center gap-1.5">
<Icon :icon="selectedType?.icon || 'lucide:storage'" class="h-4 w-4" />
<component :is="selectedType?.icon || HardDrive" class="h-4 w-4" />
<span>{{ selectedType?.label || '-' }}</span>
</div>
</div>
@@ -361,8 +361,8 @@ onMounted(() => {
:disabled="saving || !formData.name"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Save v-else class="mr-2 h-4 w-4" />
保存更改
</UiButton>
<UiButton
@@ -372,8 +372,8 @@ onMounted(() => {
:disabled="testing"
@click="handleTest"
>
<Icon v-if="testing" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:plug" class="mr-2 h-4 w-4" />
<Loader2 v-if="testing" class="mr-2 h-4 w-4 animate-spin" />
<Plug v-else class="mr-2 h-4 w-4" />
测试连接
</UiButton>
<UiButton
@@ -1,6 +1,6 @@
import { Cloud, Edit, Folder, Globe, HardDrive, Lock, Star, Trash2 } from 'lucide-vue-next'
import type { ColumnDef } from '@tanstack/vue-table'
import { Icon } from '@iconify/vue'
import { h } from 'vue'
import type { StorageConfig } from '@/pages/admin/storage-configs/data/schema'
@@ -24,12 +24,12 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
sftp: 'SFTP',
}
const typeIcons: Record<string, string> = {
local: 'lucide:hard-drive',
s3: 'lucide:cloud',
webdav: 'lucide:globe',
ftp: 'lucide:folder',
sftp: 'lucide:lock',
const typeIcons: Record<string, any> = {
local: HardDrive,
s3: Cloud,
webdav: Globe,
ftp: Folder,
sftp: Lock,
}
return [
@@ -38,7 +38,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
header: () => '名称',
cell: ({ row }) => {
return h('div', { class: 'flex items-center gap-2' }, [
h(Icon, { icon: typeIcons[row.original.type] || 'lucide:storage', class: 'h-4 w-4 text-muted-foreground' }),
h(typeIcons[row.original.type] || HardDrive, { class: 'h-4 w-4 text-muted-foreground' }),
h('span', { class: 'font-medium' }, row.original.name),
row.original.is_default
? h(Badge, { variant: 'secondary', class: 'text-xs' }, () => '默认')
@@ -98,7 +98,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
onClick: () => options.onSetDefault(row.original),
title: '设为默认',
}, () => [
h(Icon, { icon: 'lucide:star', class: 'h-4 w-4' }),
h(Star, { class: 'h-4 w-4' }),
])
: null,
h(Button, {
@@ -106,7 +106,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
size: 'sm',
onClick: () => options.onEdit(row.original),
}, () => [
h(Icon, { icon: 'lucide:edit', class: 'h-4 w-4' }),
h(Edit, { class: 'h-4 w-4' }),
]),
!isLocal
? h(Button, {
@@ -114,7 +114,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
size: 'sm',
onClick: () => options.onDelete(row.original),
}, () => [
h(Icon, { icon: 'lucide:trash-2', class: 'h-4 w-4 text-destructive' }),
h(Trash2, { class: 'h-4 w-4 text-destructive' }),
])
: null,
])
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Cloud, Database, Eye, Folder, Globe, HardDrive, Loader2, Lock, Plug, Plus } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -27,11 +27,11 @@ const formData = ref({
})
const storageTypes = [
{ value: 'local', label: '本地存储', icon: 'lucide:hard-drive', description: '存储在服务器本地磁盘' },
{ value: 's3', label: 'S3存储', icon: 'lucide:cloud', description: '兼容S3协议的对象存储' },
{ value: 'webdav', label: 'WebDAV', icon: 'lucide:globe', description: 'WebDAV协议存储' },
{ value: 'ftp', label: 'FTP', icon: 'lucide:folder', description: 'FTP协议存储' },
{ value: 'sftp', label: 'SFTP', icon: 'lucide:lock', description: 'SFTP协议存储' },
{ value: 'local', label: '本地存储', icon: HardDrive, description: '存储在服务器本地磁盘' },
{ value: 's3', label: 'S3存储', icon: Cloud, description: '兼容S3协议的对象存储' },
{ value: 'webdav', label: 'WebDAV', icon: Globe, description: 'WebDAV协议存储' },
{ value: 'ftp', label: 'FTP', icon: Folder, description: 'FTP协议存储' },
{ value: 'sftp', label: 'SFTP', icon: Lock, description: 'SFTP协议存储' },
]
const selectedType = computed(() => {
@@ -113,7 +113,7 @@ async function handleSubmit() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:database" class="size-5" />
<Database class="size-5" />
存储配置
</UiCardTitle>
<UiCardDescription>填写存储的基本信息</UiCardDescription>
@@ -141,7 +141,7 @@ async function handleSubmit() {
<UiSelectContent>
<UiSelectItem v-for="type in storageTypes" :key="type.value" :value="type.value">
<div class="flex items-center gap-2">
<Icon :icon="type.icon" class="h-4 w-4" />
<component :is="type.icon" class="h-4 w-4" />
{{ type.label }}
</div>
</UiSelectItem>
@@ -273,7 +273,7 @@ async function handleSubmit() {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -286,7 +286,7 @@ async function handleSubmit() {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">存储类型</span>
<div class="flex items-center gap-1.5">
<Icon :icon="selectedType?.icon || 'lucide:storage'" class="h-4 w-4" />
<component :is="selectedType?.icon || HardDrive" class="h-4 w-4" />
<span>{{ selectedType?.label || '-' }}</span>
</div>
</div>
@@ -315,8 +315,8 @@ async function handleSubmit() {
:disabled="saving || !formData.name"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:plus" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Plus v-else class="mr-2 h-4 w-4" />
添加存储
</UiButton>
<UiButton
@@ -326,8 +326,8 @@ async function handleSubmit() {
:disabled="testing"
@click="handleTest"
>
<Icon v-if="testing" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:plug" class="mr-2 h-4 w-4" />
<Loader2 v-if="testing" class="mr-2 h-4 w-4 animate-spin" />
<Plug v-else class="mr-2 h-4 w-4" />
测试连接
</UiButton>
<UiButton
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { CheckCircle, Database, Percent, Plus, XCircle } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -141,7 +141,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
添加存储
</UiButton>
</template>
@@ -153,7 +153,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
存储配置总数
</UiCardTitle>
<Icon icon="lucide:database" class="size-4 text-muted-foreground" />
<Database class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -167,7 +167,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
已启用
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -181,7 +181,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
已禁用
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
<XCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -195,7 +195,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
启用率
</UiCardTitle>
<Icon icon="lucide:percent" class="size-4 text-muted-foreground" />
<Percent class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Bell, Database, Loader2, Settings, Shield, ToggleLeft, Upload } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { toast } from 'vue-sonner'
@@ -195,11 +195,11 @@ async function handleFaviconUpload(event: Event) {
}
const tabs = [
{ id: 'basic', label: '基本设置', icon: 'lucide:settings' },
{ id: 'security', label: '安全设置', icon: 'lucide:shield' },
{ id: 'backup', label: '备份设置', icon: 'lucide:database' },
{ id: 'feature', label: '功能设置', icon: 'lucide:toggle-left' },
{ id: 'notification', label: '通知设置', icon: 'lucide:bell' },
{ id: 'basic', label: '基本设置', icon: Settings },
{ id: 'security', label: '安全设置', icon: Shield },
{ id: 'backup', label: '备份设置', icon: Database },
{ id: 'feature', label: '功能设置', icon: ToggleLeft },
{ id: 'notification', label: '通知设置', icon: Bell },
]
onMounted(() => {
@@ -224,7 +224,7 @@ onMounted(() => {
: 'text-muted-foreground hover:text-foreground'"
@click="activeTab = tab.id"
>
<Icon :icon="tab.icon" class="h-4 w-4" />
<component :is="tab.icon" class="h-4 w-4" />
{{ tab.label }}
</button>
</div>
@@ -246,8 +246,8 @@ onMounted(() => {
<div class="flex-1">
<div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" :disabled="uploadingLogo" @click="triggerLogoUpload">
<Icon v-if="uploadingLogo" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:upload" class="mr-2 h-4 w-4" />
<Loader2 v-if="uploadingLogo" class="mr-2 h-4 w-4 animate-spin" />
<Upload v-else class="mr-2 h-4 w-4" />
上传Logo
</UiButton>
<span class="text-xs text-muted-foreground">支持 JPGPNGSVG 格式</span>
@@ -285,8 +285,8 @@ onMounted(() => {
<div class="flex-1">
<div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" :disabled="uploadingFavicon" @click="triggerFaviconUpload">
<Icon v-if="uploadingFavicon" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:upload" class="mr-2 h-4 w-4" />
<Loader2 v-if="uploadingFavicon" class="mr-2 h-4 w-4 animate-spin" />
<Upload v-else class="mr-2 h-4 w-4" />
上传图标
</UiButton>
<span class="text-xs text-muted-foreground">推荐 32x32 64x64 像素的 ICO/PNG</span>
@@ -605,7 +605,7 @@ onMounted(() => {
<div class="flex justify-end">
<UiButton :disabled="saving" @click="saveSettings">
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
保存设置
</UiButton>
</div>
+6 -6
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { AlertCircle, CheckCircle, Clock, MessageSquare, Plus } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -193,7 +193,7 @@ onMounted(() => {
>
<template #actions>
<UiButton size="sm" @click="openCreatePage">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.tickets.createTicket') }}
</UiButton>
</template>
@@ -205,7 +205,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.tickets.openCount') }}
</UiCardTitle>
<Icon icon="lucide:alert-circle" class="size-4 text-muted-foreground" />
<AlertCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -219,7 +219,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.tickets.pendingCount') }}
</UiCardTitle>
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
<Clock class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -233,7 +233,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.tickets.resolvedCount') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -247,7 +247,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.tickets.totalTickets') }}
</UiCardTitle>
<Icon icon="lucide:message-square" class="size-4 text-muted-foreground" />
<MessageSquare class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
+17 -17
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { AlertCircle, ArrowDown, ArrowUp, CheckCircle, FileText, Info, Loader2, Minus, Send, Server, Smartphone, Upload, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -145,7 +145,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:file-text" class="size-5" />
<FileText class="size-5" />
{{ t('admin.tickets.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.tickets.create.configDesc') }}</UiCardDescription>
@@ -162,13 +162,13 @@ onMounted(() => {
<UiSelectContent>
<UiSelectItem value="system">
<div class="flex items-center gap-2">
<Icon icon="lucide:server" class="size-4 text-muted-foreground" />
<Server class="size-4 text-muted-foreground" />
{{ t('admin.tickets.create.ticketTypeSystem') }}
</div>
</UiSelectItem>
<UiSelectItem value="application">
<div class="flex items-center gap-2">
<Icon icon="lucide:smartphone" class="size-4 text-muted-foreground" />
<Smartphone class="size-4 text-muted-foreground" />
{{ t('admin.tickets.create.ticketTypeApplication') }}
</div>
</UiSelectItem>
@@ -217,25 +217,25 @@ onMounted(() => {
<UiSelectContent>
<UiSelectItem value="low">
<div class="flex items-center gap-2">
<Icon icon="lucide:arrow-down" class="size-4 text-muted-foreground" />
<ArrowDown class="size-4 text-muted-foreground" />
{{ t('admin.tickets.priority.low') }}
</div>
</UiSelectItem>
<UiSelectItem value="normal">
<div class="flex items-center gap-2">
<Icon icon="lucide:minus" class="size-4 text-muted-foreground" />
<Minus class="size-4 text-muted-foreground" />
{{ t('admin.tickets.priority.normal') }}
</div>
</UiSelectItem>
<UiSelectItem value="high">
<div class="flex items-center gap-2">
<Icon icon="lucide:arrow-up" class="size-4 text-muted-foreground" />
<ArrowUp class="size-4 text-muted-foreground" />
{{ t('admin.tickets.priority.high') }}
</div>
</UiSelectItem>
<UiSelectItem value="urgent">
<div class="flex items-center gap-2">
<Icon icon="lucide:alert-circle" class="size-4 text-muted-foreground" />
<AlertCircle class="size-4 text-muted-foreground" />
{{ t('admin.tickets.priority.urgent') }}
</div>
</UiSelectItem>
@@ -275,7 +275,7 @@ onMounted(() => {
class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity"
@click="removeImage(index)"
>
<Icon icon="lucide:x" class="h-3 w-3" />
<X class="h-3 w-3" />
</UiButton>
</div>
<label
@@ -289,7 +289,7 @@ onMounted(() => {
class="hidden"
@change="handleImageUpload"
>
<Icon icon="lucide:upload" class="w-6 h-6 text-muted-foreground" />
<Upload class="w-6 h-6 text-muted-foreground" />
</label>
</div>
<p class="text-xs text-muted-foreground">
@@ -304,26 +304,26 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
<Info class="size-5" />
{{ t('admin.tickets.create.tips') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3 text-sm">
<div class="flex gap-3">
<Icon icon="lucide:check-circle" class="size-5 text-primary flex-shrink-0" />
<CheckCircle class="size-5 text-primary flex-shrink-0" />
<p>{{ t('admin.tickets.create.tip1') }}</p>
</div>
<div class="flex gap-3">
<Icon icon="lucide:check-circle" class="size-5 text-primary flex-shrink-0" />
<CheckCircle class="size-5 text-primary flex-shrink-0" />
<p>{{ t('admin.tickets.create.tip2') }}</p>
</div>
<div class="flex gap-3">
<Icon icon="lucide:check-circle" class="size-5 text-primary flex-shrink-0" />
<CheckCircle class="size-5 text-primary flex-shrink-0" />
<p>{{ t('admin.tickets.create.tip3') }}</p>
</div>
<div class="flex gap-3">
<Icon icon="lucide:check-circle" class="size-5 text-primary flex-shrink-0" />
<CheckCircle class="size-5 text-primary flex-shrink-0" />
<p>{{ t('admin.tickets.create.tip4') }}</p>
</div>
</div>
@@ -339,8 +339,8 @@ onMounted(() => {
:disabled="saving || !form.title.trim() || !form.content.trim() || (form.type === 'application' && !form.application_id)"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:send" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Send v-else class="mr-2 h-4 w-4" />
{{ t('admin.tickets.create.submit') }}
</UiButton>
<UiButton
+17 -17
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { AlertCircle, ArrowDown, ArrowUp, CheckCircle, FileText, Info, Loader2, Minus, Send, Server, Smartphone, Upload, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -145,7 +145,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:file-text" class="size-5" />
<FileText class="size-5" />
{{ t('admin.tickets.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.tickets.create.configDesc') }}</UiCardDescription>
@@ -162,13 +162,13 @@ onMounted(() => {
<UiSelectContent>
<UiSelectItem value="system">
<div class="flex items-center gap-2">
<Icon icon="lucide:server" class="size-4 text-muted-foreground" />
<Server class="size-4 text-muted-foreground" />
{{ t('admin.tickets.create.ticketTypeSystem') }}
</div>
</UiSelectItem>
<UiSelectItem value="application">
<div class="flex items-center gap-2">
<Icon icon="lucide:smartphone" class="size-4 text-muted-foreground" />
<Smartphone class="size-4 text-muted-foreground" />
{{ t('admin.tickets.create.ticketTypeApplication') }}
</div>
</UiSelectItem>
@@ -217,25 +217,25 @@ onMounted(() => {
<UiSelectContent>
<UiSelectItem value="low">
<div class="flex items-center gap-2">
<Icon icon="lucide:arrow-down" class="size-4 text-muted-foreground" />
<ArrowDown class="size-4 text-muted-foreground" />
{{ t('admin.tickets.priority.low') }}
</div>
</UiSelectItem>
<UiSelectItem value="normal">
<div class="flex items-center gap-2">
<Icon icon="lucide:minus" class="size-4 text-muted-foreground" />
<Minus class="size-4 text-muted-foreground" />
{{ t('admin.tickets.priority.normal') }}
</div>
</UiSelectItem>
<UiSelectItem value="high">
<div class="flex items-center gap-2">
<Icon icon="lucide:arrow-up" class="size-4 text-muted-foreground" />
<ArrowUp class="size-4 text-muted-foreground" />
{{ t('admin.tickets.priority.high') }}
</div>
</UiSelectItem>
<UiSelectItem value="urgent">
<div class="flex items-center gap-2">
<Icon icon="lucide:alert-circle" class="size-4 text-muted-foreground" />
<AlertCircle class="size-4 text-muted-foreground" />
{{ t('admin.tickets.priority.urgent') }}
</div>
</UiSelectItem>
@@ -275,7 +275,7 @@ onMounted(() => {
class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity"
@click="removeImage(index)"
>
<Icon icon="lucide:x" class="h-3 w-3" />
<X class="h-3 w-3" />
</UiButton>
</div>
<label
@@ -289,7 +289,7 @@ onMounted(() => {
class="hidden"
@change="handleImageUpload"
>
<Icon icon="lucide:upload" class="w-6 h-6 text-muted-foreground" />
<Upload class="w-6 h-6 text-muted-foreground" />
</label>
</div>
<p class="text-xs text-muted-foreground">
@@ -304,26 +304,26 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
<Info class="size-5" />
{{ t('admin.tickets.create.tips') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3 text-sm">
<div class="flex gap-3">
<Icon icon="lucide:check-circle" class="size-5 text-primary flex-shrink-0" />
<CheckCircle class="size-5 text-primary flex-shrink-0" />
<p>{{ t('admin.tickets.create.tip1') }}</p>
</div>
<div class="flex gap-3">
<Icon icon="lucide:check-circle" class="size-5 text-primary flex-shrink-0" />
<CheckCircle class="size-5 text-primary flex-shrink-0" />
<p>{{ t('admin.tickets.create.tip2') }}</p>
</div>
<div class="flex gap-3">
<Icon icon="lucide:check-circle" class="size-5 text-primary flex-shrink-0" />
<CheckCircle class="size-5 text-primary flex-shrink-0" />
<p>{{ t('admin.tickets.create.tip3') }}</p>
</div>
<div class="flex gap-3">
<Icon icon="lucide:check-circle" class="size-5 text-primary flex-shrink-0" />
<CheckCircle class="size-5 text-primary flex-shrink-0" />
<p>{{ t('admin.tickets.create.tip4') }}</p>
</div>
</div>
@@ -339,8 +339,8 @@ onMounted(() => {
:disabled="saving || !form.title.trim() || !form.content.trim() || (form.type === 'application' && !form.application_id)"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:send" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Send v-else class="mr-2 h-4 w-4" />
{{ t('admin.tickets.create.submit') }}
</UiButton>
<UiButton
+6 -6
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Eye, Loader2, UserPlus } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
@@ -122,7 +122,7 @@ onMounted(() => {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -131,7 +131,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:user-plus" class="size-5" />
<UserPlus class="size-5" />
{{ t('admin.users.edit.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.users.edit.basicInfoDesc') }}</UiCardDescription>
@@ -184,7 +184,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
{{ t('admin.users.edit.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -215,8 +215,8 @@ onMounted(() => {
:disabled="saving || !form.username || !form.email || !form.application_id"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ t('admin.users.edit.saveBtn') }}
</UiButton>
<UiButton
+5 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Eye, Loader2, UserPlus } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -106,7 +106,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:user-plus" class="size-5" />
<UserPlus class="size-5" />
{{ t('admin.users.create.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.users.create.basicInfoDesc') }}</UiCardDescription>
@@ -156,7 +156,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
{{ t('admin.users.create.preview') }}
</UiCardTitle>
</UiCardHeader>
@@ -187,8 +187,8 @@ onMounted(() => {
:disabled="saving || !isFormValid"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:user-plus" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<UserPlus v-else class="mr-2 h-4 w-4" />
{{ t('admin.users.create.submit') }}
</UiButton>
<UiButton
+6 -6
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Ban, CheckCircle, Clock, Plus, Users } from 'lucide-vue-next'
import { computed, onActivated, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -241,7 +241,7 @@ onActivated(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.users.addUser') }}
</UiButton>
</template>
@@ -253,7 +253,7 @@ onActivated(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.users.totalUsers') }}
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
<Users class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -267,7 +267,7 @@ onActivated(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.users.online') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
<CheckCircle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -281,7 +281,7 @@ onActivated(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.users.offline') }}
</UiCardTitle>
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
<Clock class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -295,7 +295,7 @@ onActivated(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.users.banned') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
<Ban class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
+13 -13
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, CheckCircle, Eye, FileArchive, FilePlus, GitBranch, Loader2, Upload, UploadCloud, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -215,7 +215,7 @@ onMounted(() => {
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else class="space-y-6">
@@ -224,7 +224,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:git-branch" class="size-5" />
<GitBranch class="size-5" />
版本配置
</UiCardTitle>
<UiCardDescription>修改版本的基本信息和上传更新文件</UiCardDescription>
@@ -295,19 +295,19 @@ onMounted(() => {
@change="handleFileSelect"
>
<div v-if="!formData.file" class="text-center">
<Icon icon="lucide:upload-cloud" class="size-10 mx-auto text-muted-foreground mb-2" />
<UploadCloud class="size-10 mx-auto text-muted-foreground mb-2" />
<p class="text-sm text-muted-foreground mb-2">
拖拽ZIP文件到此处或点击上传
</p>
<UiButton variant="outline" size="sm" :disabled="saving || uploading" @click="fileInputRef?.click()">
<Icon icon="lucide:file-plus" class="h-4 w-4 mr-2" />
<FilePlus class="h-4 w-4 mr-2" />
选择文件
</UiButton>
</div>
<div v-else class="space-y-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Icon icon="lucide:file-archive" class="size-8 text-primary" />
<FileArchive class="size-8 text-primary" />
<div>
<p class="text-sm font-medium">
{{ formData.file.name }}
@@ -324,18 +324,18 @@ onMounted(() => {
:disabled="uploading"
@click="uploadZipFile"
>
<Icon v-if="uploading" icon="lucide:loader-2" class="h-4 w-4 mr-2 animate-spin" />
<Icon v-else icon="lucide:upload" class="h-4 w-4 mr-2" />
<Loader2 v-if="uploading" class="h-4 w-4 mr-2 animate-spin" />
<Upload v-else class="h-4 w-4 mr-2" />
上传解析
</UiButton>
<UiButton variant="ghost" size="icon" :disabled="saving" @click="removeFile">
<Icon icon="lucide:x" class="size-4" />
<X class="size-4" />
</UiButton>
</div>
</div>
<div v-if="uploadedData" class="rounded-lg bg-muted/50 p-3">
<div class="flex items-center gap-2 text-sm text-green-600 mb-2">
<Icon icon="lucide:check-circle" class="size-4" />
<CheckCircle class="size-4" />
<span>文件已上传并解析</span>
</div>
<div class="text-xs text-muted-foreground space-y-1">
@@ -463,7 +463,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -517,8 +517,8 @@ onMounted(() => {
:disabled="saving || !formData.application_id || !formData.version"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
保存修改
</UiButton>
<UiButton
@@ -1,6 +1,6 @@
import { Download, Edit, Trash2 } from 'lucide-vue-next'
import type { ColumnDef } from '@tanstack/vue-table'
import { Icon } from '@iconify/vue'
import { h } from 'vue'
import type { Version } from '@/pages/admin/versions/data/schema'
@@ -103,7 +103,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
size: 'sm',
onClick: () => options.onDownload(row.original),
}, () => [
h(Icon, { icon: 'lucide:download', class: 'h-4 w-4' }),
h(Download, { class: 'h-4 w-4' }),
]),
)
}
@@ -113,14 +113,14 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
size: 'sm',
onClick: () => options.onEdit(row.original),
}, () => [
h(Icon, { icon: 'lucide:edit', class: 'h-4 w-4' }),
h(Edit, { class: 'h-4 w-4' }),
]),
h(Button, {
variant: 'ghost',
size: 'sm',
onClick: () => options.onDelete(row.original),
}, () => [
h(Icon, { icon: 'lucide:trash-2', class: 'h-4 w-4 text-destructive' }),
h(Trash2, { class: 'h-4 w-4 text-destructive' }),
]),
)
return h('div', { class: 'flex items-center justify-end gap-1' }, buttons)
+12 -12
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { CheckCircle, Eye, FileArchive, FilePlus, GitBranch, Loader2, Rocket, Upload, UploadCloud, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -201,7 +201,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:git-branch" class="size-5" />
<GitBranch class="size-5" />
版本配置
</UiCardTitle>
<UiCardDescription>填写版本的基本信息和上传更新文件</UiCardDescription>
@@ -272,19 +272,19 @@ onMounted(() => {
@change="handleFileSelect"
>
<div v-if="!formData.file" class="text-center">
<Icon icon="lucide:upload-cloud" class="size-10 mx-auto text-muted-foreground mb-2" />
<UploadCloud class="size-10 mx-auto text-muted-foreground mb-2" />
<p class="text-sm text-muted-foreground mb-2">
拖拽ZIP文件到此处或点击上传
</p>
<UiButton variant="outline" size="sm" :disabled="saving || uploading" @click="fileInputRef?.click()">
<Icon icon="lucide:file-plus" class="h-4 w-4 mr-2" />
<FilePlus class="h-4 w-4 mr-2" />
选择文件
</UiButton>
</div>
<div v-else class="space-y-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Icon icon="lucide:file-archive" class="size-8 text-primary" />
<FileArchive class="size-8 text-primary" />
<div>
<p class="text-sm font-medium">
{{ formData.file.name }}
@@ -301,18 +301,18 @@ onMounted(() => {
:disabled="uploading"
@click="uploadZipFile"
>
<Icon v-if="uploading" icon="lucide:loader-2" class="h-4 w-4 mr-2 animate-spin" />
<Icon v-else icon="lucide:upload" class="h-4 w-4 mr-2" />
<Loader2 v-if="uploading" class="h-4 w-4 mr-2 animate-spin" />
<Upload v-else class="h-4 w-4 mr-2" />
上传解析
</UiButton>
<UiButton variant="ghost" size="icon" :disabled="saving" @click="removeFile">
<Icon icon="lucide:x" class="size-4" />
<X class="size-4" />
</UiButton>
</div>
</div>
<div v-if="uploadedData" class="rounded-lg bg-muted/50 p-3">
<div class="flex items-center gap-2 text-sm text-green-600 mb-2">
<Icon icon="lucide:check-circle" class="size-4" />
<CheckCircle class="size-4" />
<span>文件已上传并解析</span>
</div>
<div class="text-xs text-muted-foreground space-y-1">
@@ -443,7 +443,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
<Eye class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
@@ -509,8 +509,8 @@ onMounted(() => {
:disabled="saving || !formData.application_id || !formData.version || !uploadedData"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:rocket" class="mr-2 h-4 w-4" />
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Rocket v-else class="mr-2 h-4 w-4" />
发布版本
</UiButton>
<UiButton
+6 -6
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { AlertTriangle, GitBranch, HardDrive, Package, Plus } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
@@ -224,7 +224,7 @@ onMounted(() => {
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
{{ t('admin.versions.create') }}
</UiButton>
</template>
@@ -236,7 +236,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.versions.totalVersions') }}
</UiCardTitle>
<Icon icon="lucide:git-branch" class="size-4 text-muted-foreground" />
<GitBranch class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -250,7 +250,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.versions.forcedCount') }}
</UiCardTitle>
<Icon icon="lucide:alert-triangle" class="size-4 text-muted-foreground" />
<AlertTriangle class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -264,7 +264,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.versions.totalSize') }}
</UiCardTitle>
<Icon icon="lucide:hard-drive" class="size-4 text-muted-foreground" />
<HardDrive class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -278,7 +278,7 @@ onMounted(() => {
<UiCardTitle class="text-sm font-medium">
{{ t('admin.versions.appCount') }}
</UiCardTitle>
<Icon icon="lucide:package" class="size-4 text-muted-foreground" />
<Package class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
+4 -4
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { AlertCircle, Box, Loader2 } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
@@ -30,13 +30,13 @@ onMounted(async () => {
<template>
<div class="space-y-6">
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<template v-else-if="app">
<div class="flex items-center gap-4">
<div class="size-16 rounded-xl bg-primary/10 flex items-center justify-center">
<Icon icon="lucide:box" class="size-8 text-primary" />
<Box class="size-8 text-primary" />
</div>
<div>
<h1 class="text-2xl font-bold">
@@ -92,7 +92,7 @@ onMounted(async () => {
</template>
<div v-else class="text-center py-12 text-muted-foreground">
<Icon icon="lucide:alert-circle" class="size-16 mx-auto mb-4 opacity-30" />
<AlertCircle class="size-16 mx-auto mb-4 opacity-30" />
<p>应用不存在或无权访问</p>
</div>
</div>
+4 -4
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Box, Boxes, Loader2 } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import api from '@/services/api'
@@ -50,11 +50,11 @@ function formatDate(dateStr: string) {
<UiCard>
<UiCardContent class="p-0">
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else-if="apps.length === 0" class="text-center py-12 text-muted-foreground">
<Icon icon="lucide:boxes" class="size-16 mx-auto mb-4 opacity-30" />
<Boxes class="size-16 mx-auto mb-4 opacity-30" />
<p>暂无授权应用</p>
</div>
@@ -66,7 +66,7 @@ function formatDate(dateStr: string) {
>
<div class="flex items-center gap-4">
<div class="size-12 rounded-xl bg-primary/10 flex items-center justify-center">
<Icon icon="lucide:box" class="size-6 text-primary" />
<Box class="size-6 text-primary" />
</div>
<div>
<h3 class="font-medium">
+2 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Loader2 } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -165,7 +165,7 @@ async function handleGenerate() {
:disabled="loading || !form.app_id || !form.card_type_id"
@click="handleGenerate"
>
<Icon v-if="loading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="loading" class="mr-2 h-4 w-4 animate-spin" />
{{ loading ? '生成中...' : '生成卡密' }}
</UiButton>
</div>
+4 -4
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Key, Loader2, Plus } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import api from '@/services/api'
@@ -60,7 +60,7 @@ function getStatusBadge(status: string) {
</div>
<UiButton as-child>
<router-link to="/agent/cards/create">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
<Plus class="mr-2 h-4 w-4" />
生成卡密
</router-link>
</UiButton>
@@ -69,11 +69,11 @@ function getStatusBadge(status: string) {
<UiCard>
<UiCardContent class="p-0">
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else-if="cards.length === 0" class="text-center py-12 text-muted-foreground">
<Icon icon="lucide:key" class="size-16 mx-auto mb-4 opacity-30" />
<Key class="size-16 mx-auto mb-4 opacity-30" />
<p>暂无卡密记录</p>
<UiButton class="mt-4" as-child>
<router-link to="/agent/cards/create">
+5 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ArrowDownLeft, ArrowUpRight, Loader2, Receipt, RotateCcw } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import api from '@/services/api'
@@ -86,11 +86,11 @@ function getTypeClass(type: string) {
</UiCardHeader>
<UiCardContent class="p-0">
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else-if="transactions.length === 0" class="text-center py-12 text-muted-foreground">
<Icon icon="lucide:receipt" class="size-16 mx-auto mb-4 opacity-30" />
<Receipt class="size-16 mx-auto mb-4 opacity-30" />
<p>暂无交易记录</p>
</div>
@@ -105,8 +105,8 @@ function getTypeClass(type: string) {
class="size-10 rounded-xl flex items-center justify-center"
:class="tx.type === 'recharge' ? 'bg-green-500/10' : tx.type === 'refund' ? 'bg-blue-500/10' : 'bg-red-500/10'"
>
<Icon
:icon="tx.type === 'recharge' ? 'lucide:arrow-down-left' : tx.type === 'refund' ? 'lucide:rotate-ccw' : 'lucide:arrow-up-right'"
<component
:is="tx.type === 'recharge' ? ArrowDownLeft : tx.type === 'refund' ? RotateCcw : ArrowUpRight"
:class="getTypeClass(tx.type)"
class="size-5"
/>
+10 -10
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Boxes, CreditCard, DollarSign, Key, Plus, Users } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import api from '@/services/api'
@@ -63,7 +63,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
授权应用
</UiCardTitle>
<Icon icon="lucide:boxes" class="size-4 text-muted-foreground" />
<Boxes class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -80,7 +80,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
卡密总数
</UiCardTitle>
<Icon icon="lucide:key" class="size-4 text-muted-foreground" />
<Key class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -97,7 +97,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
用户总数
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
<Users class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -114,7 +114,7 @@ onMounted(async () => {
<UiCardTitle class="text-sm font-medium">
累计收入
</UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
<DollarSign class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
@@ -135,25 +135,25 @@ onMounted(async () => {
<UiCardContent class="grid gap-4 sm:grid-cols-2">
<router-link to="/agent/cards/create">
<UiButton variant="outline" class="w-full h-20 flex-col gap-2">
<Icon icon="lucide:plus" class="size-5" />
<Plus class="size-5" />
<span>生成卡密</span>
</UiButton>
</router-link>
<router-link to="/agent/apps">
<UiButton variant="outline" class="w-full h-20 flex-col gap-2">
<Icon icon="lucide:boxes" class="size-5" />
<Boxes class="size-5" />
<span>应用管理</span>
</UiButton>
</router-link>
<router-link to="/agent/users">
<UiButton variant="outline" class="w-full h-20 flex-col gap-2">
<Icon icon="lucide:users" class="size-5" />
<Users class="size-5" />
<span>用户管理</span>
</UiButton>
</router-link>
<router-link to="/agent/finance">
<UiButton variant="outline" class="w-full h-20 flex-col gap-2">
<Icon icon="lucide:credit-card" class="size-5" />
<CreditCard class="size-5" />
<span>财务管理</span>
</UiButton>
</router-link>
@@ -181,7 +181,7 @@ onMounted(async () => {
</div>
</div>
<div v-else class="text-center py-8 text-muted-foreground">
<Icon icon="lucide:key" class="size-12 mx-auto mb-2 opacity-30" />
<Key class="size-12 mx-auto mb-2 opacity-30" />
<p>暂无卡密记录</p>
</div>
</UiCardContent>
+3 -3
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Loader2, Users } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import api from '@/services/api'
@@ -50,11 +50,11 @@ function formatDate(dateStr: string | null) {
<UiCard>
<UiCardContent class="p-0">
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
<Loader2 class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else-if="users.length === 0" class="text-center py-12 text-muted-foreground">
<Icon icon="lucide:users" class="size-16 mx-auto mb-4 opacity-30" />
<Users class="size-16 mx-auto mb-4 opacity-30" />
<p>暂无用户记录</p>
</div>
+8 -8
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ChartLine, Loader2, Shield, ShieldCheck, Users, Zap } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -66,7 +66,7 @@ async function handleLogin() {
<div class="relative z-10 flex flex-col justify-center px-12">
<div class="flex items-center gap-3 mb-8">
<div class="size-12 rounded-xl bg-primary flex items-center justify-center">
<Icon icon="lucide:shield-check" class="size-7 text-primary-foreground" />
<ShieldCheck class="size-7 text-primary-foreground" />
</div>
<span class="text-2xl font-bold">管理平台</span>
</div>
@@ -78,19 +78,19 @@ async function handleLogin() {
</p>
<div class="grid grid-cols-2 gap-4">
<div class="flex items-center gap-3 p-4 rounded-lg bg-background/50 backdrop-blur">
<Icon icon="lucide:zap" class="size-5 text-primary" />
<Zap class="size-5 text-primary" />
<span class="text-sm">快速集成</span>
</div>
<div class="flex items-center gap-3 p-4 rounded-lg bg-background/50 backdrop-blur">
<Icon icon="lucide:shield" class="size-5 text-primary" />
<Shield class="size-5 text-primary" />
<span class="text-sm">安全可靠</span>
</div>
<div class="flex items-center gap-3 p-4 rounded-lg bg-background/50 backdrop-blur">
<Icon icon="lucide:chart-line" class="size-5 text-primary" />
<ChartLine class="size-5 text-primary" />
<span class="text-sm">数据分析</span>
</div>
<div class="flex items-center gap-3 p-4 rounded-lg bg-background/50 backdrop-blur">
<Icon icon="lucide:users" class="size-5 text-primary" />
<Users class="size-5 text-primary" />
<span class="text-sm">用户管理</span>
</div>
</div>
@@ -102,7 +102,7 @@ async function handleLogin() {
<div class="text-center mb-8 lg:hidden">
<div class="flex items-center justify-center gap-3 mb-4">
<div class="size-10 rounded-xl bg-primary flex items-center justify-center">
<Icon icon="lucide:shield-check" class="size-6 text-primary-foreground" />
<ShieldCheck class="size-6 text-primary-foreground" />
</div>
<span class="text-xl font-bold">管理平台</span>
</div>
@@ -166,7 +166,7 @@ async function handleLogin() {
class="w-full"
:disabled="loading || !isValid"
>
<Icon v-if="loading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="loading" class="mr-2 h-4 w-4 animate-spin" />
{{ loading ? '登录中...' : '登录' }}
</UiButton>
</form>
+7 -7
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Check, Loader2, ShieldCheck } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -82,7 +82,7 @@ async function handleRegister() {
<div class="relative z-10 flex flex-col justify-center px-12">
<div class="flex items-center gap-3 mb-8">
<div class="size-12 rounded-xl bg-primary flex items-center justify-center">
<Icon icon="lucide:shield-check" class="size-7 text-primary-foreground" />
<ShieldCheck class="size-7 text-primary-foreground" />
</div>
<span class="text-2xl font-bold">管理平台</span>
</div>
@@ -95,19 +95,19 @@ async function handleRegister() {
<div class="space-y-4">
<div class="flex items-center gap-3">
<div class="size-8 rounded-full bg-primary/20 flex items-center justify-center">
<Icon icon="lucide:check" class="size-4 text-primary" />
<Check class="size-4 text-primary" />
</div>
<span>免费创建应用快速集成 SDK</span>
</div>
<div class="flex items-center gap-3">
<div class="size-8 rounded-full bg-primary/20 flex items-center justify-center">
<Icon icon="lucide:check" class="size-4 text-primary" />
<Check class="size-4 text-primary" />
</div>
<span>完善的用户管理和数据分析</span>
</div>
<div class="flex items-center gap-3">
<div class="size-8 rounded-full bg-primary/20 flex items-center justify-center">
<Icon icon="lucide:check" class="size-4 text-primary" />
<Check class="size-4 text-primary" />
</div>
<span>灵活的卡密授权方案</span>
</div>
@@ -120,7 +120,7 @@ async function handleRegister() {
<div class="text-center mb-8 lg:hidden">
<div class="flex items-center justify-center gap-3 mb-4">
<div class="size-10 rounded-xl bg-primary flex items-center justify-center">
<Icon icon="lucide:shield-check" class="size-6 text-primary-foreground" />
<ShieldCheck class="size-6 text-primary-foreground" />
</div>
<span class="text-xl font-bold">管理平台</span>
</div>
@@ -198,7 +198,7 @@ async function handleRegister() {
class="w-full"
:disabled="loading || !isValid"
>
<Icon v-if="loading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Loader2 v-if="loading" class="mr-2 h-4 w-4 animate-spin" />
{{ loading ? '注册中...' : '注册' }}
</UiButton>
</form>

Some files were not shown because too many files have changed in this diff Show More