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

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