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:
Generated
+19
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">支持 JPG、PNG、SVG 格式</span>
|
<span class="text-xs text-muted-foreground">支持 JPG、PNG、SVG 格式</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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user