refactor: 移除佣金比例字段,将developer统一为admin

This commit is contained in:
2026-05-01 08:08:11 +08:00
parent c5d1dc4b04
commit 4ed00f29e2
235 changed files with 9082 additions and 8952 deletions
-2
View File
@@ -28,7 +28,6 @@ type User struct {
ApiToken string `gorm:"size:64" json:"api_token"`
ParentAgentID *uint `gorm:"index" json:"parent_agent_id"`
Commission float64 `gorm:"default:0" json:"commission"`
CanCreateAgent bool `gorm:"default:false" json:"can_create_agent"`
Balance float64 `gorm:"default:0" json:"balance"`
@@ -557,7 +556,6 @@ type AgentApplication struct {
AgentID uint `json:"agent_id"`
ApplicationID uint `json:"application_id"`
DeveloperID uint `json:"developer_id"`
Commission float64 `gorm:"default:0.1" json:"commission"`
Discount float64 `gorm:"default:1.0" json:"discount"`
Status string `gorm:"size:20;default:active" json:"status"`
IsReceived bool `gorm:"default:false" json:"is_received"`
-3
View File
@@ -80,7 +80,6 @@ func handleGetApps(c *gin.Context) {
"description": aa.Application.Description,
"status": aa.Application.Status,
"authorized_at": aa.CreatedAt,
"commission": aa.Commission,
})
}
}
@@ -224,7 +223,6 @@ func handleGetFinance(c *gin.Context) {
response.Success(c, gin.H{
"balance": user.Balance,
"commission": user.Commission,
"records": records,
})
}
@@ -244,7 +242,6 @@ func handleGetProfile(c *gin.Context) {
"email": user.Email,
"avatar": user.Avatar,
"balance": user.Balance,
"commission": user.Commission,
"can_create_agent": user.CanCreateAgent,
})
}
@@ -137,7 +137,6 @@ func handleGetAgentApps(c *gin.Context) {
AgentEmail string `json:"agent_email"`
ApplicationID uint `json:"application_id"`
AppName string `json:"app_name"`
Commission float64 `json:"commission"`
Discount float64 `json:"discount"`
Status string `json:"status"`
CardTypes []CardTypeResponse `json:"card_types"`
@@ -181,7 +180,6 @@ func handleGetAgentApps(c *gin.Context) {
AgentEmail: agentEmail,
ApplicationID: aa.ApplicationID,
AppName: appName,
Commission: aa.Commission,
Discount: aa.Discount,
Status: aa.Status,
CardTypes: cardTypes,
@@ -496,7 +494,6 @@ func handleApproveRequest(c *gin.Context) {
AgentID: req.AgentID,
ApplicationID: req.ApplicationID,
DeveloperID: userID,
Commission: 0.1,
Discount: 1.0,
Status: "active",
IsReceived: true,
@@ -606,7 +603,6 @@ func handleGetAgentAppDetail(c *gin.Context) {
AgentEmail string `json:"agent_email"`
ApplicationID uint `json:"application_id"`
AppName string `json:"app_name"`
Commission float64 `json:"commission"`
Discount float64 `json:"discount"`
Status string `json:"status"`
CardTypes []CardTypeResponse `json:"card_types"`
@@ -629,7 +625,6 @@ func handleGetAgentAppDetail(c *gin.Context) {
AgentName: agent.Username,
ApplicationID: agentApp.ApplicationID,
AppName: application.Name,
Commission: agentApp.Commission,
Discount: agentApp.Discount,
Status: agentApp.Status,
CardTypes: cardTypes,
@@ -648,7 +643,6 @@ func handleUpdateAgentApp(c *gin.Context) {
agentAppID := c.Param("id")
var reqBody struct {
Commission *float64 `json:"commission"`
Discount *float64 `json:"discount"`
Status *string `json:"status"`
}
@@ -664,9 +658,6 @@ func handleUpdateAgentApp(c *gin.Context) {
}
updates := make(map[string]interface{})
if reqBody.Commission != nil {
updates["commission"] = *reqBody.Commission
}
if reqBody.Discount != nil {
updates["discount"] = *reqBody.Discount
}
@@ -40,7 +40,6 @@ type AgentTreeNode struct {
CreatedAt string `json:"created_at"`
LastLoginAt string `json:"last_login_at"`
Balance float64 `json:"balance"`
Commission float64 `json:"commission"`
CanCreateAgent bool `json:"can_create_agent"`
CardsCount int `json:"cards_count"`
ChildAgentsCount int `json:"child_agents_count"`
@@ -69,7 +68,6 @@ func handleGetAgents(c *gin.Context) {
CreatedAt string `json:"created_at"`
LastLoginAt string `json:"last_login_at"`
Balance float64 `json:"balance"`
Commission float64 `json:"commission"`
CanCreateAgent bool `json:"can_create_agent"`
CardsCount int `json:"cards_count"`
ChildAgentsCount int `json:"child_agents_count"`
@@ -110,7 +108,6 @@ func handleGetAgents(c *gin.Context) {
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
LastLoginAt: lastLoginAt,
Balance: user.Balance,
Commission: user.Commission,
CanCreateAgent: user.CanCreateAgent,
CardsCount: int(cardsCount),
ChildAgentsCount: int(childAgentsCount),
@@ -185,7 +182,6 @@ func handleGetAgentsTree(c *gin.Context) {
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
LastLoginAt: lastLoginAt,
Balance: user.Balance,
Commission: user.Commission,
CanCreateAgent: user.CanCreateAgent,
CardsCount: int(cardsCount),
ChildAgentsCount: int(childAgentsCount),
@@ -217,7 +213,6 @@ func handleCreateAgent(c *gin.Context) {
Email string `json:"email"`
Password string `json:"password" binding:"required"`
ParentAgentID *uint `json:"parent_agent_id"`
Commission float64 `json:"commission"`
CanCreateAgent bool `json:"can_create_agent"`
Balance float64 `json:"balance"`
}
@@ -259,7 +254,6 @@ func handleCreateAgent(c *gin.Context) {
Role: "agent",
Status: "active",
ParentAgentID: req.ParentAgentID,
Commission: req.Commission,
CanCreateAgent: req.CanCreateAgent,
Balance: req.Balance,
}
@@ -365,7 +359,6 @@ func handleGetAgentDetail(c *gin.Context) {
"role": user.Role,
"parent_agent_id": user.ParentAgentID,
"parent_agent_name": parentAgentName,
"commission": user.Commission,
"can_create_agent": user.CanCreateAgent,
"balance": user.Balance,
"created_at": user.CreatedAt.Format("2006-01-02 15:04:05"),
@@ -387,7 +380,6 @@ func handleUpdateAgent(c *gin.Context) {
var req struct {
ParentAgentID *uint `json:"parent_agent_id"`
Commission *float64 `json:"commission"`
Email *string `json:"email"`
Password *string `json:"password"`
CanCreateAgent *bool `json:"can_create_agent"`
@@ -414,9 +406,6 @@ func handleUpdateAgent(c *gin.Context) {
if req.Email != nil {
user.Email = req.Email
}
if req.Commission != nil {
user.Commission = *req.Commission
}
if req.Password != nil && *req.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
if err != nil {
+1 -1
View File
@@ -153,7 +153,7 @@ func main() {
},
"如何实现代理授权?": {
TitleEn: "How to Implement Agent Authorization?",
ContentEn: "# How to Implement Agent Authorization?\n\n## Overview\nAgent authorization allows you to delegate application management to agents.\n\n## Setup Steps\n\n### 1. Create Agent Application\n1. Go to Console > Agent Applications\n2. Click \"Create Agent Application\"\n3. Select the application to authorize\n4. Set commission rate\n\n### 2. Generate Agent Link\nAfter creation, you'll get an agent link:\n```\nhttps://platform.com/agent/AGENT_CODE\n```\n\n### 3. Agent Dashboard\nAgents can access their dashboard to:\n- View sales statistics\n- Generate card keys\n- Manage users",
ContentEn: "# How to Implement Agent Authorization?\n\n## Overview\nAgent authorization allows you to delegate application management to agents.\n\n## Setup Steps\n\n### 1. Create Agent Application\n1. Go to Console > Agent Applications\n2. Click \"Create Agent Application\"\n3. Select the application to authorize\n4. Set discount rate\n\n### 2. Generate Agent Link\nAfter creation, you'll get an agent link:\n```\nhttps://platform.com/agent/AGENT_CODE\n```\n\n### 3. Agent Dashboard\nAgents can access their dashboard to:\n- View sales statistics\n- Generate card keys\n- Manage users",
},
}
+1 -1
View File
@@ -141,7 +141,7 @@ func main() {
},
"faq-agent": {
TitleEn: "How to Implement Agent Authorization?",
ContentEn: "# How to Implement Agent Authorization?\n\n## What is Agent Authorization?\n\nAgent authorization allows developers to authorize their applications to other developers, who can then generate card keys and sell them.\n\n## Authorization Process\n\n1. **Apply for Authorization**\n - Authorized party submits application to authorizer\n - Enter application ID\n - Wait for authorizer approval\n\n2. **Approve Authorization**\n - Authorizer views application\n - Set card key type permissions after approval\n - Set agent commission and discount\n\n3. **Generate Card Keys**\n - Authorized party selects authorized application\n - Select card key types with permission\n - Generate and sell card keys\n\n4. **Settle Commission**\n - Sales revenue is settled proportionally\n - Authorized party receives commission income\n\n## Permission Management\n\n- Authorizer can modify card key type permissions at any time\n- Can pause or terminate authorization\n- Can view agent's sales data",
ContentEn: "# How to Implement Agent Authorization?\n\n## What is Agent Authorization?\n\nAgent authorization allows developers to authorize their applications to other developers, who can then generate card keys and sell them.\n\n## Authorization Process\n\n1. **Apply for Authorization**\n - Authorized party submits application to authorizer\n - Enter application ID\n - Wait for authorizer approval\n\n2. **Approve Authorization**\n - Authorizer views application\n - Set card key type permissions after approval\n - Set agent discount\n\n3. **Generate Card Keys**\n - Authorized party selects authorized application\n - Select card key types with permission\n - Generate and sell card keys\n\n4. **Settle Revenue**\n - Sales revenue is settled proportionally\n - Authorized party receives income\n\n## Permission Management\n\n- Authorizer can modify card key type permissions at any time\n- Can pause or terminate authorization\n- Can view agent's sales data",
SummaryEn: "Implementation process and permission management for agent authorization",
},
}
@@ -35,7 +35,7 @@ const displayValue = computed(() => {
return ''
})
const buttonTitle = computed(() => props.title || t('developer.cards.timeRange'))
const buttonTitle = computed(() => props.title || t('admin.cards.timeRange'))
function setQuickRange(range: 'today' | 'week' | 'month') {
const now = new Date()
@@ -83,19 +83,19 @@ function clearFilter() {
<UiPopoverContent class="w-auto p-4" align="start">
<div class="space-y-3">
<div class="text-sm font-medium">
{{ t('developer.cards.selectTimeRange') }}
{{ t('admin.cards.selectTimeRange') }}
</div>
<div class="flex items-center gap-2">
<DateTimePicker
:model-value="startModelValue"
:placeholder="t('developer.cards.startTime')"
:placeholder="t('admin.cards.startTime')"
class="w-[180px]"
@update:model-value="emit('update:startModelValue', $event)"
/>
<span class="text-muted-foreground text-sm">~</span>
<DateTimePicker
:model-value="endModelValue"
:placeholder="t('developer.cards.endTime')"
:placeholder="t('admin.cards.endTime')"
class="w-[180px]"
@update:model-value="emit('update:endModelValue', $event)"
/>
@@ -103,17 +103,17 @@ function clearFilter() {
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('today')">
{{ t('developer.cards.today') }}
{{ t('admin.cards.today') }}
</UiButton>
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('week')">
{{ t('developer.cards.last7Days') }}
{{ t('admin.cards.last7Days') }}
</UiButton>
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('month')">
{{ t('developer.cards.thisMonth') }}
{{ t('admin.cards.thisMonth') }}
</UiButton>
</div>
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="clearFilter">
{{ t('developer.cards.reset') }}
{{ t('admin.cards.reset') }}
</UiButton>
</div>
</div>
@@ -70,7 +70,7 @@ const filterFunction = (list: DataTableFacetedFilter['options'], term: string) =
v-for="option in options"
:key="option.value"
:value="option"
@select="(_e) => {
@select="(_e: any) => {
const isSelected = selectedValues.has(option.value)
if (isSelected) {
selectedValues.delete(option.value)
@@ -31,7 +31,7 @@ onMounted(() => {
const teams = [
{
name: '开发者后台',
name: '管理后台',
logo: Code,
plan: 'Developer',
},
@@ -43,42 +43,42 @@ const navMain = [
items: [
{
title: '控制台',
url: '/developer',
url: '/admin',
icon: Gauge,
},
{
title: '应用管理',
url: '/developer/applications',
url: '/admin/applications',
icon: Boxes,
},
{
title: '卡类管理',
url: '/developer/card-types',
url: '/admin/card-types',
icon: Hash,
},
{
title: '卡密管理',
url: '/developer/cards',
url: '/admin/cards',
icon: Key,
},
{
title: '公告管理',
url: '/developer/announcements',
url: '/admin/announcements',
icon: Megaphone,
},
{
title: '版本管理',
url: '/developer/versions',
url: '/admin/versions',
icon: GitBranch,
},
{
title: '用户管理',
url: '/developer/users',
url: '/admin/users',
icon: Users,
},
{
title: '代理管理',
url: '/developer/agents',
url: '/admin/agents',
icon: Network,
},
],
@@ -88,17 +88,17 @@ const navMain = [
items: [
{
title: '财务管理',
url: '/developer/finance',
url: '/admin/finance',
icon: DollarSign,
},
{
title: '日志记录',
url: '/developer/logs',
url: '/admin/logs',
icon: ScrollText,
},
{
title: '工单系统',
url: '/developer/tickets',
url: '/admin/tickets',
icon: MessageSquare,
},
],
@@ -108,27 +108,27 @@ const navMain = [
items: [
{
title: '云端常量',
url: '/developer/cloud-constants',
url: '/admin/cloud-constants',
icon: FileLock,
},
{
title: '云端变量',
url: '/developer/cloud-variables',
url: '/admin/cloud-variables',
icon: Variable,
},
{
title: '云端函数',
url: '/developer/cloud-function',
url: '/admin/cloud-function',
icon: Code,
},
{
title: '风控管理',
url: '/developer/risk-control',
url: '/admin/risk-control',
icon: Shield,
},
{
title: '扩展配置',
url: '/developer/extension',
url: '/admin/extension',
icon: Plug,
},
],
@@ -139,11 +139,11 @@ function handleLogout() {
<UiDropdownMenuSeparator />
<UiDropdownMenuGroup>
<UiDropdownMenuItem @click="router.push('/developer/profile')">
<UiDropdownMenuItem @click="router.push('/admin/profile')">
<UserRoundCog class="mr-2 h-4 w-4" />
个人中心
</UiDropdownMenuItem>
<UiDropdownMenuItem @click="router.push('/developer/tickets')">
<UiDropdownMenuItem @click="router.push('/admin/tickets')">
<Icon icon="lucide:ticket" class="mr-2 h-4 w-4" />
工单系统
</UiDropdownMenuItem>
@@ -66,34 +66,34 @@ const developerMenus = computed(() => ({
main: {
title: t('nav.mainFeatures'),
items: [
{ title: t('nav.console'), url: '/developer', icon: 'lucide:gauge' },
{ title: t('nav.applications'), url: '/developer/applications', icon: 'lucide:boxes' },
{ title: t('nav.cardTypes'), url: '/developer/card-types', icon: 'lucide:hash' },
{ title: t('nav.cards'), url: '/developer/cards', icon: 'lucide:key' },
{ title: t('nav.announcements'), url: '/developer/announcements', icon: 'lucide:megaphone' },
{ title: t('nav.versions'), url: '/developer/versions', icon: 'lucide:git-branch' },
{ title: t('nav.users'), url: '/developer/users', icon: 'lucide:users' },
{ title: t('nav.devices'), url: '/developer/devices', icon: 'lucide:monitor' },
{ title: t('nav.sessions'), url: '/developer/sessions', icon: 'lucide:wifi' },
{ title: t('nav.agentApps'), url: '/developer/agent-apps', icon: 'lucide:share-2' },
{ title: t('nav.console'), url: '/admin', icon: 'lucide:gauge' },
{ title: t('nav.applications'), url: '/admin/applications', icon: 'lucide:boxes' },
{ title: t('nav.cardTypes'), url: '/admin/card-types', icon: 'lucide:hash' },
{ title: t('nav.cards'), url: '/admin/cards', icon: 'lucide:key' },
{ title: t('nav.announcements'), url: '/admin/announcements', icon: 'lucide:megaphone' },
{ title: t('nav.versions'), url: '/admin/versions', icon: 'lucide:git-branch' },
{ title: t('nav.users'), url: '/admin/users', icon: 'lucide:users' },
{ title: t('nav.devices'), url: '/admin/devices', icon: 'lucide:monitor' },
{ title: t('nav.sessions'), url: '/admin/sessions', icon: 'lucide:wifi' },
{ title: t('nav.agentApps'), url: '/admin/agent-apps', icon: 'lucide:share-2' },
],
},
business: {
title: t('nav.businessManagement'),
items: [
{ title: t('nav.finance'), url: '/developer/finance', icon: 'lucide:dollar-sign' },
{ title: t('nav.logs'), url: '/developer/logs', icon: 'lucide:scroll-text' },
{ title: t('nav.tickets'), url: '/developer/tickets', icon: 'lucide:message-square' },
{ title: t('nav.finance'), url: '/admin/finance', icon: 'lucide:dollar-sign' },
{ title: t('nav.logs'), url: '/admin/logs', icon: 'lucide:scroll-text' },
{ title: t('nav.tickets'), url: '/admin/tickets', icon: 'lucide:message-square' },
],
},
advanced: {
title: t('nav.advancedFeatures'),
items: [
{ title: t('nav.cloudConstants'), url: '/developer/cloud-constants', icon: 'lucide:file-lock' },
{ title: t('nav.cloudVariables'), url: '/developer/cloud-variables', icon: 'lucide:variable' },
{ title: t('nav.cloudFunction'), url: '/developer/cloud-function', icon: 'lucide:code' },
{ title: t('nav.riskControl'), url: '/developer/risk-control', icon: 'lucide:shield' },
{ title: t('nav.extension'), url: '/developer/extension', icon: 'lucide:plug' },
{ title: t('nav.cloudConstants'), url: '/admin/cloud-constants', icon: 'lucide:file-lock' },
{ title: t('nav.cloudVariables'), url: '/admin/cloud-variables', icon: 'lucide:variable' },
{ title: t('nav.cloudFunction'), url: '/admin/cloud-function', icon: 'lucide:code' },
{ title: t('nav.riskControl'), url: '/admin/risk-control', icon: 'lucide:shield' },
{ title: t('nav.extension'), url: '/admin/extension', icon: 'lucide:plug' },
],
},
}))
@@ -334,7 +334,7 @@ watch(route, () => {
<Icon icon="lucide:user" class="mr-2 h-4 w-4" />
<span>{{ t('nav.profile') }}</span>
</UiDropdownMenuItem>
<UiDropdownMenuItem v-if="currentUser?.role === 'admin'" @click="router.push('/developer')">
<UiDropdownMenuItem v-if="currentUser?.role === 'admin'" @click="router.push('/admin')">
<Icon icon="lucide:layout-dashboard" class="mr-2 h-4 w-4" />
<span>{{ t('nav.developerDashboard') }}</span>
</UiDropdownMenuItem>
+4 -4
View File
@@ -2,7 +2,7 @@
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import DeveloperSidebar from '@/components/developer-sidebar/index.vue'
import AdminSidebar from '@/components/developer-sidebar/index.vue'
import LanguageChange from '@/components/language-change.vue'
import ThemePopover from '@/components/custom-theme/theme-popover.vue'
import ToggleTheme from '@/components/toggle-theme.vue'
@@ -12,7 +12,7 @@ const router = useRouter()
const breadcrumbs = computed(() => {
const path = router.currentRoute.value.path
const parts = path.split('/').filter(Boolean)
const crumbs = [{ title: '控制台', path: '/developer' }]
const crumbs = [{ title: '控制台', path: '/admin' }]
const titleMap: Record<string, string> = {
applications: '应用管理',
@@ -44,7 +44,7 @@ const breadcrumbs = computed(() => {
invite: '邀请授权',
}
let currentPath = '/developer'
let currentPath = '/admin'
for (let i = 1; i < parts.length; i++) {
const part = parts[i]
currentPath += `/${part}`
@@ -69,7 +69,7 @@ const breadcrumbs = computed(() => {
<template>
<UiSidebarProvider>
<DeveloperSidebar />
<AdminSidebar />
<UiSidebarInset>
<header class="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<UiSidebarTrigger class="-ml-1" />
@@ -76,13 +76,13 @@ async function fetchData() {
}
else {
toast.error('获取授权信息失败')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
}
catch (error) {
console.error('获取数据失败:', error)
toast.error('获取数据失败')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
finally {
loading.value = false
@@ -118,7 +118,7 @@ async function handleSubmit() {
const data = await response.json()
if (data.code === 200) {
toast.success('保存成功')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
else {
toast.error(data.message || '保存失败')
@@ -134,7 +134,7 @@ async function handleSubmit() {
}
function goBack() {
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
onMounted(() => {
@@ -4,7 +4,7 @@ import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { AgentApp } from '@/pages/developer/agent-apps/data/schema'
import type { AgentApp } from '@/pages/admin/agent-apps/data/schema'
import { BasicPage } from '@/components/global-layout'
@@ -17,7 +17,6 @@ const saving = ref(false)
const agentApp = ref<AgentApp | null>(null)
const form = ref({
commission: 0.1,
discount: 1.0,
status: 'active',
})
@@ -33,20 +32,19 @@ async function fetchAgentApp() {
if (data.code === 200 && data.agent_app) {
agentApp.value = data.agent_app
form.value = {
commission: data.agent_app.commission,
discount: data.agent_app.discount,
status: data.agent_app.status,
}
}
else {
toast.error('获取授权信息失败')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
}
catch (error) {
console.error('获取授权信息失败:', error)
toast.error('获取授权信息失败')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
finally {
loading.value = false
@@ -54,10 +52,6 @@ async function fetchAgentApp() {
}
async function handleSubmit() {
if (form.value.commission < 0 || form.value.commission > 1) {
toast.error('佣金比例必须在0-1之间')
return
}
if (form.value.discount < 0 || form.value.discount > 1) {
toast.error('折扣必须在0-1之间')
return
@@ -78,7 +72,7 @@ async function handleSubmit() {
const data = await response.json()
if (data.code === 200) {
toast.success('保存成功')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
else {
toast.error(data.message || '保存失败')
@@ -94,7 +88,7 @@ async function handleSubmit() {
}
function goBack() {
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
onMounted(() => {
@@ -146,29 +140,6 @@ onMounted(() => {
</UiSelect>
</div>
<div class="space-y-2">
<UiLabel for="commission">
佣金比例
</UiLabel>
<div class="flex items-center gap-4">
<UiInput
id="commission"
v-model.number="form.commission"
type="number"
step="0.01"
min="0"
max="1"
class="flex-1"
/>
<span class="text-sm text-muted-foreground w-20">
{{ (form.commission * 100).toFixed(1) }}%
</span>
</div>
<p class="text-xs text-muted-foreground">
代理商获得的佣金比例范围0-1
</p>
</div>
<div class="space-y-2">
<UiLabel for="discount">
折扣
@@ -200,7 +171,7 @@ onMounted(() => {
<UiButton
variant="outline"
size="sm"
@click="router.push(`/developer/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" />
充值余额
@@ -30,13 +30,13 @@ async function fetchAgentApp() {
}
else {
toast.error('获取授权信息失败')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
}
catch (error) {
console.error('获取授权信息失败:', error)
toast.error('获取授权信息失败')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
finally {
loading.value = false
@@ -66,7 +66,7 @@ async function handleSubmit() {
const data = await response.json()
if (data.code === 200) {
toast.success('充值成功')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
else {
toast.error(data.message || '充值失败')
@@ -82,7 +82,7 @@ async function handleSubmit() {
}
function goBack() {
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
const quickAmounts = [100, 500, 1000, 5000]
@@ -3,7 +3,7 @@ import type { ColumnDef } from '@tanstack/vue-table'
import { h } from 'vue'
import { useRouter } from 'vue-router'
import type { CombinedItem } from '@/pages/developer/agent-apps/data/schema'
import type { CombinedItem } from '@/pages/admin/agent-apps/data/schema'
import Badge from '@/components/ui/badge/Badge.vue'
import Button from '@/components/ui/button/Button.vue'
@@ -68,11 +68,11 @@ export function getColumns(actions: {
},
{
accessorKey: 'agent_name',
header: '代理商/开发者',
header: '代理商/管理员',
cell: ({ row }) => {
const agentName = row.getValue('agent_name') as string
const itemType = row.original.itemType
const label = itemType === 'my_request' ? '开发者' : '代理商'
const label = itemType === 'my_request' ? '管理员' : '代理商'
return h('div', {}, [
h('div', { class: 'font-medium' }, agentName || '-'),
h('div', { class: 'text-xs text-muted-foreground' }, label),
@@ -98,17 +98,6 @@ export function getColumns(actions: {
return h(Badge, { variant }, () => text)
},
},
{
accessorKey: 'commission',
header: '佣金比例',
cell: ({ row }) => {
const hasAuthorization = row.original.hasAuthorization
if (!hasAuthorization)
return '-'
const commission = row.getValue('commission') as number
return `${(commission * 100).toFixed(1)}%`
},
},
{
accessorKey: 'discount',
header: '折扣',
@@ -191,15 +180,15 @@ export function getColumns(actions: {
if (item.hasAuthorization) {
items.push(
h(DropdownMenuItem, { onClick: () => router.push(`/developer/agent-apps/${item.authorizationId}/edit`) }, () => [
h(DropdownMenuItem, { onClick: () => router.push(`/admin/agent-apps/${item.authorizationId}/edit`) }, () => [
h('span', { class: 'i-lucide-edit mr-2 h-4 w-4' }),
'编辑',
]),
h(DropdownMenuItem, { onClick: () => router.push(`/developer/agent-apps/${item.authorizationId}/card-types`) }, () => [
h(DropdownMenuItem, { onClick: () => router.push(`/admin/agent-apps/${item.authorizationId}/card-types`) }, () => [
h('span', { class: 'i-lucide-key mr-2 h-4 w-4' }),
'卡密权限',
]),
h(DropdownMenuItem, { onClick: () => router.push(`/developer/agent-apps/${item.authorizationId}/recharge`) }, () => [
h(DropdownMenuItem, { onClick: () => router.push(`/admin/agent-apps/${item.authorizationId}/recharge`) }, () => [
h('span', { class: 'i-lucide-wallet mr-2 h-4 w-4' }),
'充值余额',
]),
@@ -4,7 +4,7 @@ import type { Table } from '@tanstack/vue-table'
import { X } from 'lucide-vue-next'
import { computed } from 'vue'
import type { CombinedItem } from '@/pages/developer/agent-apps/data/schema'
import type { CombinedItem } from '@/pages/admin/agent-apps/data/schema'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -21,7 +21,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template>
<div class="flex items-center flex-1 space-x-2">
<Input
placeholder="搜索代理商/开发者..."
placeholder="搜索代理商/管理员..."
:model-value="(table.getColumn('agent_name')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[200px]"
@input="table.getColumn('agent_name')?.setFilterValue($event.target.value)"
@@ -36,12 +36,12 @@ const table = generateVueTable<CombinedItem>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.agentApps.select',
itemType: 'developer.agentApps.columns.type',
agent_name: 'developer.agentApps.columns.agentName',
app_name: 'developer.agentApps.columns.appName',
status: 'developer.agentApps.columns.status',
created_at: 'developer.agentApps.columns.createdAt',
select: 'admin.agentApps.select',
itemType: 'admin.agentApps.columns.type',
agent_name: 'admin.agentApps.columns.agentName',
app_name: 'admin.agentApps.columns.appName',
status: 'admin.agentApps.columns.status',
created_at: 'admin.agentApps.columns.createdAt',
}
defineExpose({
@@ -9,7 +9,6 @@ export const agentAppSchema = z.object({
application_id: z.number(),
app_name: z.string(),
status: agentAppStatusSchema,
commission: z.number(),
discount: z.number(),
balance: z.number(),
card_types: z.array(z.object({
@@ -44,7 +43,6 @@ export interface CombinedItem {
application_id: number
app_name: string
status: string
commission: number
discount: number
balance: number
card_types?: Array<{ card_type_id: number, can_generate: boolean }>
+34 -36
View File
@@ -5,12 +5,12 @@ import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { AgentApp, AgentRequest, CombinedItem } from '@/pages/developer/agent-apps/data/schema'
import type { AgentApp, AgentRequest, CombinedItem } from '@/pages/admin/agent-apps/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import BulkActions from '@/components/data-table/bulk-actions.vue'
import { BasicPage } from '@/components/global-layout'
import DataTable from '@/pages/developer/agent-apps/components/data-table.vue'
import DataTable from '@/pages/admin/agent-apps/components/data-table.vue'
import api from '@/services/api'
const { t } = useI18n()
@@ -53,7 +53,6 @@ const allItems = computed<CombinedItem[]>(() => {
application_id: item.application_id,
app_name: item.app_name,
status: item.status,
commission: 0,
discount: 1,
balance: 0,
card_types: [],
@@ -72,7 +71,6 @@ const allItems = computed<CombinedItem[]>(() => {
application_id: item.application_id,
app_name: item.app_name,
status: item.status,
commission: 0,
discount: 1,
balance: 0,
card_types: [],
@@ -110,22 +108,22 @@ async function fetchData() {
}
function goToRequest() {
router.push('/developer/agent-apps/request')
router.push('/admin/agent-apps/request')
}
function goToInvite() {
router.push('/developer/agent-apps/invite')
router.push('/admin/agent-apps/invite')
}
async function approveRequest(item: CombinedItem) {
try {
await api.post(`/dev/agent-apps/requests/${item.id}/approve`)
toast.success(t('developer.agentApps.approveSuccess'))
toast.success(t('admin.agentApps.approveSuccess'))
fetchData()
}
catch (error: any) {
console.error('批准申请失败:', error)
toast.error(error.message || t('developer.agentApps.approveFailed'))
toast.error(error.message || t('admin.agentApps.approveFailed'))
}
}
@@ -135,13 +133,13 @@ async function rejectRequest() {
try {
await api.post(`/dev/agent-apps/requests/${rejectingItem.value.id}/reject`, { reason: '' })
toast.success(t('developer.agentApps.rejectSuccess'))
toast.success(t('admin.agentApps.rejectSuccess'))
showRejectDialog.value = false
fetchData()
}
catch (error: any) {
console.error('拒绝申请失败:', error)
toast.error(error.message || t('developer.agentApps.rejectFailed'))
toast.error(error.message || t('admin.agentApps.rejectFailed'))
}
}
@@ -178,12 +176,12 @@ function confirmRemove(item: CombinedItem) {
async function batchApprove(ids: (string | number)[]) {
try {
await api.post('/dev/agent-apps/requests/batch-approve', { ids })
toast.success(t('developer.agentApps.batchApproveSuccess'))
toast.success(t('admin.agentApps.batchApproveSuccess'))
fetchData()
}
catch (error: any) {
console.error('批量批准失败:', error)
toast.error(error.message || t('developer.agentApps.batchApproveFailed'))
toast.error(error.message || t('admin.agentApps.batchApproveFailed'))
}
}
@@ -193,13 +191,13 @@ async function batchReject() {
try {
await api.post('/dev/agent-apps/requests/batch-reject', { ids: batchRejectIds.value, reason: '' })
toast.success(t('developer.agentApps.batchRejectSuccess'))
toast.success(t('admin.agentApps.batchRejectSuccess'))
showBatchRejectDialog.value = false
fetchData()
}
catch (error: any) {
console.error('批量拒绝失败:', error)
toast.error(error.message || t('developer.agentApps.batchRejectFailed'))
toast.error(error.message || t('admin.agentApps.batchRejectFailed'))
}
}
@@ -214,13 +212,13 @@ async function batchRemove() {
try {
await api.delete('/dev/agent-apps/batch', { ids: batchRemoveIds.value })
toast.success(t('developer.agentApps.batchRemoveSuccess'))
toast.success(t('admin.agentApps.batchRemoveSuccess'))
showBatchRemoveDialog.value = false
fetchData()
}
catch (error: any) {
console.error('批量移除失败:', error)
toast.error(error.message || t('developer.agentApps.batchRemoveFailed'))
toast.error(error.message || t('admin.agentApps.batchRemoveFailed'))
}
}
@@ -236,18 +234,18 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.agentApps.title')"
:description="t('developer.agentApps.description')"
:title="t('admin.agentApps.title')"
:description="t('admin.agentApps.description')"
sticky
>
<template #actions>
<UiButton variant="outline" @click="goToRequest">
<Icon icon="lucide:send" class="mr-2 h-4 w-4" />
{{ t('developer.agentApps.requestAuth') }}
{{ t('admin.agentApps.requestAuth') }}
</UiButton>
<UiButton @click="goToInvite">
<Icon icon="lucide:user-plus" class="mr-2 h-4 w-4" />
{{ t('developer.agentApps.inviteAuth') }}
{{ t('admin.agentApps.inviteAuth') }}
</UiButton>
</template>
@@ -256,7 +254,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.agentApps.totalAuth') }}
{{ t('admin.agentApps.totalAuth') }}
</UiCardTitle>
<Icon icon="lucide:share-2" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -270,7 +268,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.agentApps.activeAuth') }}
{{ t('admin.agentApps.activeAuth') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -284,7 +282,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.agentApps.pendingRequests') }}
{{ t('admin.agentApps.pendingRequests') }}
</UiCardTitle>
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -298,7 +296,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.agentApps.agentBalance') }}
{{ t('admin.agentApps.agentBalance') }}
</UiCardTitle>
<Icon icon="lucide:wallet" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -328,7 +326,7 @@ onMounted(() => {
<BulkActions
v-if="tableRef?.table"
:table="tableRef.table"
:entity-name="t('developer.agentApps.auth')"
:entity-name="t('admin.agentApps.auth')"
>
<UiButton
variant="outline"
@@ -336,7 +334,7 @@ onMounted(() => {
@click="batchApprove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
>
<Icon icon="lucide:check" class="mr-2 h-4 w-4" />
{{ t('developer.agentApps.batchApprove') }}
{{ t('admin.agentApps.batchApprove') }}
</UiButton>
<UiButton
variant="outline"
@@ -344,7 +342,7 @@ onMounted(() => {
@click="confirmBatchReject(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
>
<Icon icon="lucide:x" class="mr-2 h-4 w-4" />
{{ t('developer.agentApps.batchReject') }}
{{ t('admin.agentApps.batchReject') }}
</UiButton>
<UiButton
variant="destructive"
@@ -352,38 +350,38 @@ onMounted(() => {
@click="confirmBatchRemove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
>
<Icon icon="lucide:trash-2" class="mr-2 h-4 w-4" />
{{ t('developer.agentApps.batchRemove') }}
{{ t('admin.agentApps.batchRemove') }}
</UiButton>
</BulkActions>
<ConfirmDialog
v-model:open="showRejectDialog"
:title="t('developer.agentApps.reject')"
:description="t('developer.agentApps.rejectConfirm')"
:title="t('admin.agentApps.reject')"
:description="t('admin.agentApps.rejectConfirm')"
confirm-text="确定"
@confirm="rejectRequest"
/>
<ConfirmDialog
v-model:open="showRemoveDialog"
:title="t('developer.agentApps.remove')"
:description="removingItem?.itemType === 'my_request' ? t('developer.agentApps.withdrawConfirm') : t('developer.agentApps.removeConfirm')"
:title="t('admin.agentApps.remove')"
:description="removingItem?.itemType === 'my_request' ? t('admin.agentApps.withdrawConfirm') : t('admin.agentApps.removeConfirm')"
confirm-text="确定"
@confirm="removeItem"
/>
<ConfirmDialog
v-model:open="showBatchRejectDialog"
:title="t('developer.agentApps.batchReject')"
:description="t('developer.agentApps.batchRejectConfirm', { count: batchRejectIds.length })"
:title="t('admin.agentApps.batchReject')"
:description="t('admin.agentApps.batchRejectConfirm', { count: batchRejectIds.length })"
confirm-text="确定"
@confirm="batchReject"
/>
<ConfirmDialog
v-model:open="showBatchRemoveDialog"
:title="t('developer.agentApps.batchRemove')"
:description="t('developer.agentApps.batchRemoveConfirm', { count: batchRemoveIds.length })"
:title="t('admin.agentApps.batchRemove')"
:description="t('admin.agentApps.batchRemoveConfirm', { count: batchRemoveIds.length })"
confirm-text="确定"
@confirm="batchRemove"
/>
@@ -63,7 +63,7 @@ async function handleSubmit() {
const data = await response.json()
if (data.code === 200) {
toast.success('邀请已发送')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
else {
toast.error(data.message || '发送邀请失败')
@@ -79,7 +79,7 @@ async function handleSubmit() {
}
function goBack() {
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
onMounted(() => {
@@ -36,7 +36,7 @@ async function fetchApplications() {
async function handleSubmit() {
if (!form.value.developer_id) {
toast.error('请输入开发者ID')
toast.error('请输入管理员ID')
return
}
if (!form.value.application_id) {
@@ -63,7 +63,7 @@ async function handleSubmit() {
const data = await response.json()
if (data.code === 200) {
toast.success('申请已提交')
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
else {
toast.error(data.message || '提交申请失败')
@@ -79,7 +79,7 @@ async function handleSubmit() {
}
function goBack() {
router.push('/developer/agent-apps')
router.push('/admin/agent-apps')
}
onMounted(() => {
@@ -90,7 +90,7 @@ onMounted(() => {
<template>
<BasicPage
title="申请授权"
description="向开发者申请应用授权"
description="向管理员申请应用授权"
>
<template #actions>
<UiButton variant="outline" @click="goBack">
@@ -104,22 +104,22 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle>申请信息</UiCardTitle>
<UiCardDescription>
填写以下信息向开发者申请应用授权
填写以下信息向管理员申请应用授权
</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="developer_id">
开发者ID
管理员ID
</UiLabel>
<UiInput
id="developer_id"
v-model="form.developer_id"
type="number"
placeholder="请输入开发者ID"
placeholder="请输入管理员ID"
/>
<p class="text-xs text-muted-foreground">
请向开发者索取其ID
请向管理员索取其ID
</p>
</div>
+304 -318
View File
@@ -2,369 +2,355 @@
import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { Agent } from './data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import DataTable from './components/data-table.vue'
import api from '@/services/api'
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
interface Agent {
id: number
username: string
email: string
avatar: string
status: string
parent_agent_id: number | null
parent_agent_name: string
commission: number
can_create_agent: boolean
balance: number
}
const router = useRouter()
const loading = ref(true)
const saving = ref(false)
const agents = ref<Agent[]>([])
const tableRef = ref<InstanceType<typeof DataTable> | null>(null)
const searchFilter = ref('')
const statusFilter = ref<string>('')
const viewMode = ref<'tree' | 'list'>('tree')
const agentId = computed(() => route.params.id as string)
const deleteDialogOpen = ref(false)
const deleteTarget = ref<Agent | null>(null)
const batchDeleteDialogOpen = ref(false)
const batchDeleteIds = ref<number[]>([])
const form = ref({
username: '',
email: '',
password: '',
parent_agent_id: '',
commission: 0,
can_create_agent: false,
balance: 0,
const filteredAgents = computed(() => {
let result = agents.value
if (statusFilter.value) {
const filterByStatus = (items: Agent[]): Agent[] => {
return items.reduce((acc: Agent[], item) => {
const matches = item.status === statusFilter.value
const filteredChildren = item.children ? filterByStatus(item.children) : []
if (matches || filteredChildren.length > 0) {
acc.push({
...item,
children: filteredChildren.length > 0 ? filteredChildren : item.children,
})
}
return acc
}, [])
}
result = filterByStatus(result)
}
if (searchFilter.value) {
const search = searchFilter.value.toLowerCase()
const filterTree = (items: Agent[]): Agent[] => {
return items.reduce((acc: Agent[], item) => {
const matchesSearch =
item.username?.toLowerCase().includes(search) ||
item.email?.toLowerCase().includes(search) ||
item.parent_agent_name?.toLowerCase().includes(search)
const filteredChildren = item.children ? filterTree(item.children) : []
if (matchesSearch || filteredChildren.length > 0) {
acc.push({
...item,
children: filteredChildren.length > 0 ? filteredChildren : item.children,
})
}
return acc
}, [])
}
result = filterTree(result)
}
return result
})
async function fetchAgent() {
const totalAgents = computed(() => {
const countNodes = (items: Agent[]): number => {
return items.reduce((acc, item) => {
return acc + 1 + (item.children ? countNodes(item.children) : 0)
}, 0)
}
return countNodes(agents.value)
})
const activeCount = computed(() => {
const countByStatus = (items: Agent[], status: string): number => {
return items.reduce((acc, item) => {
const self = item.status === status ? 1 : 0
const children = item.children ? countByStatus(item.children, status) : 0
return acc + self + children
}, 0)
}
return countByStatus(agents.value, 'active')
})
const inactiveCount = computed(() => {
const countByStatus = (items: Agent[], status: string): number => {
return items.reduce((acc, item) => {
const self = item.status === status ? 1 : 0
const children = item.children ? countByStatus(item.children, status) : 0
return acc + self + children
}, 0)
}
return countByStatus(agents.value, 'inactive')
})
const bannedCount = computed(() => {
const countByStatus = (items: Agent[], status: string): number => {
return items.reduce((acc, item) => {
const self = item.status === status ? 1 : 0
const children = item.children ? countByStatus(item.children, status) : 0
return acc + self + children
}, 0)
}
return countByStatus(agents.value, 'banned')
})
const statusOptions = computed(() => [
{ label: t('admin.agents.status.active'), value: 'active' },
{ label: t('admin.agents.status.inactive'), value: 'inactive' },
{ label: t('admin.agents.status.banned'), value: 'banned' },
])
async function fetchAgents() {
loading.value = true
try {
const data = await api.get<Agent>(`/dev/agents/${agentId.value}`)
if (data) {
form.value.username = data.username || ''
form.value.email = data.email || ''
form.value.password = ''
form.value.parent_agent_id = data.parent_agent_id ? String(data.parent_agent_id) : 'none'
form.value.commission = data.commission || 0
form.value.can_create_agent = data.can_create_agent === true
form.value.balance = data.balance || 0
const endpoint = viewMode.value === 'tree' ? '/dev/agents/tree' : '/dev/agents'
const data = await api.get<{ tree?: Agent[], agents?: Agent[], total: number }>(endpoint)
if (viewMode.value === 'tree') {
agents.value = data?.tree || []
} else {
agents.value = data?.agents || []
}
}
catch (error) {
console.error('获取代理信息失败:', error)
toast.error(t('developer.agents.edit.fetchFailed'))
router.push('/developer/agents')
}
finally {
} catch (error) {
console.error('获取代理列表失败:', error)
agents.value = []
} finally {
loading.value = false
}
}
async function fetchAgents() {
function goToCreate() {
router.push('/admin/agents/create')
}
function toggleViewMode() {
viewMode.value = viewMode.value === 'tree' ? 'list' : 'tree'
fetchAgents()
}
function handleEdit(agent: Agent) {
router.push(`/admin/agents/${agent.id}/edit`)
}
async function handleToggleStatus(agent: Agent) {
try {
const data = await api.get<{ agents?: Agent[], total: number }>('/dev/agents')
agents.value = (data?.agents || []).filter(a => String(a.id) !== agentId.value)
}
catch (error) {
console.error('获取代理列表失败:', error)
const newStatus = agent.status === 'banned' ? 'active' : 'banned'
await api.put(`/dev/agents/${agent.id}/status`, { status: newStatus })
toast.success(t('admin.agents.statusUpdateSuccess'))
fetchAgents()
} catch (error: any) {
console.error('切换代理状态失败:', error)
toast.error(error.message || t('admin.agents.statusUpdateFailed'))
}
}
const selectedParentAgent = computed(() => {
if (form.value.parent_agent_id && form.value.parent_agent_id !== 'none') {
return agents.value.find(a => String(a.id) === form.value.parent_agent_id)
}
return null
})
function confirmDelete(agent: Agent) {
deleteTarget.value = agent
deleteDialogOpen.value = true
}
async function handleSave() {
if (!form.value.username) {
toast.error(t('developer.agents.create.usernameRequired'))
return
}
if (form.value.password && form.value.password.length < 6) {
toast.error(t('developer.agents.create.passwordMinLength'))
return
}
saving.value = true
async function handleDelete() {
if (!deleteTarget.value) return
try {
const payload: any = {
commission: form.value.commission,
can_create_agent: form.value.can_create_agent,
balance: form.value.balance,
}
if (form.value.email) {
payload.email = form.value.email
}
if (form.value.password) {
payload.password = form.value.password
}
if (form.value.parent_agent_id && form.value.parent_agent_id !== 'none') {
payload.parent_agent_id = parseInt(form.value.parent_agent_id)
} else {
payload.parent_agent_id = null
}
await api.delete(`/dev/agents/${deleteTarget.value.id}`)
toast.success(t('admin.agents.deleteSuccess'))
fetchAgents()
} catch (error: any) {
console.error('删除代理失败:', error)
toast.error(error.message || t('admin.agents.deleteFailed'))
} finally {
deleteTarget.value = null
}
}
await api.put(`/dev/agents/${agentId.value}`, payload)
toast.success(t('developer.agents.edit.saveSuccess'))
router.push('/developer/agents')
function confirmBatchDelete(ids: number[]) {
batchDeleteIds.value = ids
batchDeleteDialogOpen.value = true
}
async function handleBatchDelete() {
try {
await api.delete('/dev/agents/batch', { agent_ids: batchDeleteIds.value } as any)
toast.success(t('admin.agents.batchDeleteSuccess'))
fetchAgents()
} catch (error: any) {
console.error('批量删除失败:', error)
toast.error(error.message || t('admin.agents.batchDeleteFailed'))
} finally {
batchDeleteIds.value = []
}
catch (error: any) {
console.error('保存代理失败:', error)
toast.error(error.message || t('developer.agents.edit.saveFailed'))
}
finally {
saving.value = false
}
async function batchToggleStatus(ids: number[], status: string) {
try {
await api.post('/dev/agents/batch/status', { agent_ids: ids, status })
toast.success(t('admin.agents.batchUpdateSuccess'))
fetchAgents()
} catch (error: any) {
console.error('批量更新状态失败:', error)
toast.error(error.message || t('admin.agents.batchUpdateFailed'))
}
}
onMounted(() => {
fetchAgent()
fetchAgents()
})
</script>
<template>
<BasicPage
:title="t('developer.agents.edit.title')"
:description="t('developer.agents.edit.description')"
:breadcrumbs="[
{ title: t('developer.agents.title'), href: '/developer/agents' },
{ title: t('developer.agents.edit.title') },
]"
:title="t('admin.agents.title')"
:description="t('admin.agents.description')"
sticky
>
<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" />
</div>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('admin.agents.addAgent') }}
</UiButton>
</template>
<div v-else class="space-y-6">
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:user" class="size-5" />
{{ t('developer.agents.create.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.agents.create.basicInfoDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="username">
{{ t('developer.agents.form.username') }}
</UiLabel>
<UiInput
id="username"
v-model="form.username"
disabled
class="bg-muted"
/>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.edit.usernameHint') }}
</p>
</div>
<div class="space-y-6">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agents.totalAgents') }}
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ totalAgents }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="email">
{{ t('developer.agents.form.email') }}
</UiLabel>
<UiInput
id="email"
v-model="form.email"
type="email"
:placeholder="t('developer.agents.form.emailPlaceholder')"
/>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agents.status.active') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ activeCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="password">
{{ t('developer.agents.form.password') }}
</UiLabel>
<UiInput
id="password"
v-model="form.password"
type="password"
:placeholder="t('developer.agents.edit.passwordPlaceholder')"
/>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.edit.passwordHint') }}
</p>
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agents.status.inactive') }}
</UiCardTitle>
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ inactiveCount }}
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.agents.create.agentConfig') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.agents.create.agentConfigDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="parent_agent">
{{ t('developer.agents.form.parentAgent') }}
</UiLabel>
<UiSelect v-model="form.parent_agent_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.agents.form.selectParentAgent')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="none">
{{ t('developer.agents.form.noParent') }}
</UiSelectItem>
<UiSelectItem
v-for="agent in agents"
:key="agent.id"
:value="String(agent.id)"
>
{{ agent.username }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.create.parentAgentHint') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="commission">
{{ t('developer.agents.form.commission') }}
</UiLabel>
<div class="flex items-center gap-4">
<UiNumberField
v-model="form.commission"
:min="0"
:max="1"
:step="0.01"
class="flex-1 max-w-[200px]"
>
<UiNumberFieldContent>
<UiNumberFieldDecrement />
<UiNumberFieldInput />
<UiNumberFieldIncrement />
</UiNumberFieldContent>
</UiNumberField>
<span class="text-sm text-muted-foreground">%</span>
</div>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.form.commissionPlaceholder') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="balance">
{{ t('developer.agents.form.balance') }}
</UiLabel>
<div class="flex items-center gap-4">
<UiNumberField
v-model="form.balance"
:min="0"
:step="0.01"
class="flex-1 max-w-[200px]"
>
<UiNumberFieldContent>
<UiNumberFieldDecrement />
<UiNumberFieldInput />
<UiNumberFieldIncrement />
</UiNumberFieldContent>
</UiNumberField>
<span class="text-sm text-muted-foreground">¥</span>
</div>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.form.balancePlaceholder') }}
</p>
</div>
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel>{{ t('developer.agents.form.canCreateAgent') }}</UiLabel>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.create.canCreateAgentHint') }}
</p>
</div>
<!-- 注意UiSwitch 必须使用 v-model不能使用 v-model:checked -->
<UiSwitch v-model="form.can_create_agent" />
</div>
</UiCardContent>
</UiCard>
</div>
<div class="space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:receipt" class="size-5" />
{{ t('developer.agents.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.username') }}</span>
<span>{{ form.username || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.canCreateAgent') }}</span>
<span>{{ form.can_create_agent ? t('common.yes') : t('common.no') }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.email') }}</span>
<span>{{ form.email || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.parentAgent') }}</span>
<span>{{ selectedParentAgent?.username || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.commission') }}</span>
<span>{{ (form.commission * 100).toFixed(0) }}%</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.balance') }}</span>
<span>¥{{ form.balance.toFixed(2) }}</span>
</div>
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardContent class="pt-6">
<div class="flex flex-col gap-3">
<UiButton
class="w-full"
size="lg"
:disabled="saving"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
{{ t('developer.agents.edit.save') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.agents.create.cancel') }}
</UiButton>
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.agents.status.banned') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ bannedCount }}
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardContent class="p-6">
<DataTable
ref="tableRef"
:loading="loading"
:data="filteredAgents"
:search-filter="searchFilter"
:view-mode="viewMode"
:on-toggle-status="handleToggleStatus"
:on-edit="handleEdit"
:on-delete="confirmDelete"
@refresh="fetchAgents"
@toggle-view="toggleViewMode"
@update:search-filter="searchFilter = $event"
@batch-unban="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'banned').map((r: any) => r.original.id) || [], 'active')"
@batch-ban="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status !== 'banned').map((r: any) => r.original.id) || [], 'banned')"
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
>
<template #filters>
<SingleFilter
v-model="statusFilter"
:title="t('admin.agents.columns.status')"
:options="statusOptions"
/>
</template>
</DataTable>
</UiCardContent>
</UiCard>
</div>
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('common.delete')"
:cancel-button-text="t('common.cancel')"
@confirm="handleDelete"
>
<template #title>
{{ t('admin.agents.deleteAgent') }}
</template>
<template #description>
{{ t('admin.agents.deleteAgentConfirm', { username: deleteTarget?.username }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
:confirm-button-text="t('common.delete')"
:cancel-button-text="t('common.cancel')"
@confirm="handleBatchDelete"
>
<template #title>
{{ t('admin.agents.batchDeleteAgents') }}
</template>
<template #description>
{{ t('admin.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
</template>
@@ -31,7 +31,7 @@ export function getColumns(actions: {
return [
{
accessorKey: 'username',
header: () => t('developer.agents.columns.username'),
header: () => t('admin.agents.columns.username'),
cell: ({ row }) => {
const agent = row.original
const indentStyle = getIndentStyle(row)
@@ -49,13 +49,13 @@ export function getColumns(actions: {
},
{
accessorKey: 'status',
header: () => t('developer.agents.columns.status'),
header: () => t('admin.agents.columns.status'),
cell: ({ row }) => {
const status = row.getValue('status') as string
const statusMap: Record<string, { text: string, variant: any }> = {
active: { text: t('developer.agents.status.active'), variant: 'default' },
inactive: { text: t('developer.agents.status.inactive'), variant: 'secondary' },
banned: { text: t('developer.agents.status.banned'), variant: 'destructive' },
active: { text: t('admin.agents.status.active'), variant: 'default' },
inactive: { text: t('admin.agents.status.inactive'), variant: 'secondary' },
banned: { text: t('admin.agents.status.banned'), variant: 'destructive' },
}
const { text, variant } = statusMap[status] || { text: status, variant: 'default' }
return h(Badge, { variant }, () => text)
@@ -63,7 +63,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'can_create_agent',
header: () => t('developer.agents.columns.canCreateAgent'),
header: () => t('admin.agents.columns.canCreateAgent'),
cell: ({ row }) => {
const canCreate = row.getValue('can_create_agent') as boolean
return h('div', { class: 'flex items-center justify-center' }, [
@@ -75,7 +75,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'cards_count',
header: () => t('developer.agents.columns.cardsCount'),
header: () => t('admin.agents.columns.cardsCount'),
cell: ({ row }) => {
const count = row.getValue('cards_count') as number
return h(Badge, { variant: 'outline' }, () => String(count))
@@ -83,7 +83,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'child_agents_count',
header: () => t('developer.agents.columns.childAgentsCount'),
header: () => t('admin.agents.columns.childAgentsCount'),
cell: ({ row }) => {
const count = row.getValue('child_agents_count') as number
return h(Badge, { variant: 'secondary' }, () => String(count))
@@ -91,23 +91,15 @@ export function getColumns(actions: {
},
{
accessorKey: 'balance',
header: () => t('developer.agents.columns.balance'),
header: () => t('admin.agents.columns.balance'),
cell: ({ row }) => {
const balance = row.getValue('balance') as number
return `¥${balance.toFixed(2)}`
},
},
{
accessorKey: 'commission',
header: () => t('developer.agents.columns.commission'),
cell: ({ row }) => {
const commission = row.getValue('commission') as number
return `${(commission * 100).toFixed(0)}%`
},
},
{
accessorKey: 'created_at',
header: () => t('developer.agents.columns.createdAt'),
header: () => t('admin.agents.columns.createdAt'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at') as string
if (!createdAt) return '-'
@@ -128,7 +120,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'last_login_at',
header: () => t('developer.agents.columns.lastLoginAt'),
header: () => t('admin.agents.columns.lastLoginAt'),
cell: ({ row }) => {
const lastLoginAt = row.getValue('last_login_at') as string
if (!lastLoginAt) return '-'
@@ -174,7 +166,7 @@ export function getColumns(actions: {
]),
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(agent) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }),
isBanned ? t('developer.agents.enable') : t('developer.agents.disable'),
isBanned ? t('admin.agents.enable') : t('admin.agents.disable'),
]),
h(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(agent) }, () => [
@@ -51,16 +51,15 @@ const table = generateVueTable<Agent>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.agents.select',
username: 'developer.agents.columns.username',
status: 'developer.agents.columns.status',
can_create_agent: 'developer.agents.columns.canCreateAgent',
cards_count: 'developer.agents.columns.cardsCount',
child_agents_count: 'developer.agents.columns.childAgentsCount',
balance: 'developer.agents.columns.balance',
commission: 'developer.agents.columns.commission',
created_at: 'developer.agents.columns.createdAt',
last_login_at: 'developer.agents.columns.lastLoginAt',
select: 'admin.agents.select',
username: 'admin.agents.columns.username',
status: 'admin.agents.columns.status',
can_create_agent: 'admin.agents.columns.canCreateAgent',
cards_count: 'admin.agents.columns.cardsCount',
child_agents_count: 'admin.agents.columns.childAgentsCount',
balance: 'admin.agents.columns.balance',
created_at: 'admin.agents.columns.createdAt',
last_login_at: 'admin.agents.columns.lastLoginAt',
}
defineExpose({
@@ -77,13 +76,13 @@ defineExpose({
</div>
<BulkActions :table="table" entity-name="agents">
<UiButton variant="outline" size="sm" @click="emit('batchUnban')">
{{ t('developer.agents.batchUnbanBtn') }}
{{ t('admin.agents.batchUnbanBtn') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchBan')">
{{ t('developer.agents.batchBanBtn') }}
{{ t('admin.agents.batchBanBtn') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.agents.batchDeleteBtn') }}
{{ t('admin.agents.batchDeleteBtn') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
@@ -93,7 +92,7 @@ defineExpose({
<UiInput
:model-value="searchFilter"
type="search"
:placeholder="t('developer.agents.searchPlaceholder')"
:placeholder="t('admin.agents.searchPlaceholder')"
class="h-9 w-[250px] pl-8"
@update:model-value="emit('update:searchFilter', String($event))"
/>
@@ -103,7 +102,7 @@ defineExpose({
<div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" @click="emit('toggleView')">
<Icon :icon="viewMode === 'tree' ? 'lucide:list' : 'lucide:git-branch'" class="mr-2 h-4 w-4" />
{{ viewMode === 'tree' ? t('developer.agents.listView') : t('developer.agents.treeView') }}
{{ viewMode === 'tree' ? t('admin.agents.listView') : t('admin.agents.treeView') }}
</UiButton>
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
</div>
+37 -68
View File
@@ -26,7 +26,6 @@ const form = ref({
email: '',
password: '',
parent_agent_id: '',
commission: 0,
can_create_agent: false,
balance: 0,
})
@@ -50,15 +49,15 @@ const selectedParentAgent = computed(() => {
async function handleCreate() {
if (!form.value.username) {
toast.error(t('developer.agents.create.usernameRequired'))
toast.error(t('admin.agents.create.usernameRequired'))
return
}
if (!form.value.password) {
toast.error(t('developer.agents.create.passwordRequired'))
toast.error(t('admin.agents.create.passwordRequired'))
return
}
if (form.value.password.length < 6) {
toast.error(t('developer.agents.create.passwordMinLength'))
toast.error(t('admin.agents.create.passwordMinLength'))
return
}
@@ -67,7 +66,6 @@ async function handleCreate() {
const payload: any = {
username: form.value.username,
password: form.value.password,
commission: form.value.commission,
can_create_agent: form.value.can_create_agent,
balance: form.value.balance,
}
@@ -81,12 +79,12 @@ async function handleCreate() {
}
await api.post('/dev/agents', payload)
toast.success(t('developer.agents.create.createSuccess'))
router.push('/developer/agents')
toast.success(t('admin.agents.create.createSuccess'))
router.push('/admin/agents')
}
catch (error: any) {
console.error('创建代理失败:', error)
toast.error(error.message || t('developer.agents.create.createFailed'))
toast.error(error.message || t('admin.agents.create.createFailed'))
}
finally {
saving.value = false
@@ -100,11 +98,11 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.agents.create.title')"
:description="t('developer.agents.create.description')"
:title="t('admin.agents.create.title')"
:description="t('admin.agents.create.description')"
:breadcrumbs="[
{ title: t('developer.agents.title'), href: '/developer/agents' },
{ title: t('developer.agents.create.title') },
{ title: t('admin.agents.title'), href: '/admin/agents' },
{ title: t('admin.agents.create.title') },
]"
sticky
>
@@ -115,46 +113,46 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:user-plus" class="size-5" />
{{ t('developer.agents.create.basicInfo') }}
{{ t('admin.agents.create.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.agents.create.basicInfoDesc') }}</UiCardDescription>
<UiCardDescription>{{ t('admin.agents.create.basicInfoDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="username">
{{ t('developer.agents.form.username') }}
{{ t('admin.agents.form.username') }}
</UiLabel>
<UiInput
id="username"
v-model="form.username"
:placeholder="t('developer.agents.form.usernamePlaceholder')"
:placeholder="t('admin.agents.form.usernamePlaceholder')"
/>
</div>
<div class="space-y-2">
<UiLabel for="email">
{{ t('developer.agents.form.email') }}
{{ t('admin.agents.form.email') }}
</UiLabel>
<UiInput
id="email"
v-model="form.email"
type="email"
:placeholder="t('developer.agents.form.emailPlaceholder')"
:placeholder="t('admin.agents.form.emailPlaceholder')"
/>
</div>
<div class="space-y-2">
<UiLabel for="password">
{{ t('developer.agents.form.password') }}
{{ t('admin.agents.form.password') }}
</UiLabel>
<UiInput
id="password"
v-model="form.password"
type="password"
:placeholder="t('developer.agents.form.passwordPlaceholder')"
:placeholder="t('admin.agents.form.passwordPlaceholder')"
/>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.create.passwordHint') }}
{{ t('admin.agents.create.passwordHint') }}
</p>
</div>
</UiCardContent>
@@ -164,22 +162,22 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.agents.create.agentConfig') }}
{{ t('admin.agents.create.agentConfig') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.agents.create.agentConfigDesc') }}</UiCardDescription>
<UiCardDescription>{{ t('admin.agents.create.agentConfigDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="parent_agent">
{{ t('developer.agents.form.parentAgent') }}
{{ t('admin.agents.form.parentAgent') }}
</UiLabel>
<UiSelect v-model="form.parent_agent_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.agents.form.selectParentAgent')" />
<UiSelectValue :placeholder="t('admin.agents.form.selectParentAgent')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="none">
{{ t('developer.agents.form.noParent') }}
{{ t('admin.agents.form.noParent') }}
</UiSelectItem>
<UiSelectItem
v-for="agent in agents"
@@ -191,38 +189,13 @@ onMounted(() => {
</UiSelectContent>
</UiSelect>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.create.parentAgentHint') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="commission">
{{ t('developer.agents.form.commission') }}
</UiLabel>
<div class="flex items-center gap-4">
<UiNumberField
v-model="form.commission"
:min="0"
:max="1"
:step="0.01"
class="flex-1 max-w-[200px]"
>
<UiNumberFieldContent>
<UiNumberFieldDecrement />
<UiNumberFieldInput />
<UiNumberFieldIncrement />
</UiNumberFieldContent>
</UiNumberField>
<span class="text-sm text-muted-foreground">%</span>
</div>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.form.commissionPlaceholder') }}
{{ t('admin.agents.create.parentAgentHint') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="balance">
{{ t('developer.agents.form.balance') }}
{{ t('admin.agents.form.balance') }}
</UiLabel>
<div class="flex items-center gap-4">
<UiNumberField
@@ -240,15 +213,15 @@ onMounted(() => {
<span class="text-sm text-muted-foreground">¥</span>
</div>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.form.balancePlaceholder') }}
{{ t('admin.agents.form.balancePlaceholder') }}
</p>
</div>
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel>{{ t('developer.agents.form.canCreateAgent') }}</UiLabel>
<UiLabel>{{ t('admin.agents.form.canCreateAgent') }}</UiLabel>
<p class="text-xs text-muted-foreground">
{{ t('developer.agents.create.canCreateAgentHint') }}
{{ t('admin.agents.create.canCreateAgentHint') }}
</p>
</div>
<!-- 注意UiSwitch 必须使用 v-model不能使用 v-model:checked -->
@@ -263,33 +236,29 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:receipt" class="size-5" />
{{ t('developer.agents.create.preview') }}
{{ t('admin.agents.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.username') }}</span>
<span class="text-muted-foreground">{{ t('admin.agents.form.username') }}</span>
<span>{{ form.username || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.email') }}</span>
<span class="text-muted-foreground">{{ t('admin.agents.form.email') }}</span>
<span>{{ form.email || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.canCreateAgent') }}</span>
<span class="text-muted-foreground">{{ t('admin.agents.form.canCreateAgent') }}</span>
<span>{{ form.can_create_agent ? t('common.yes') : t('common.no') }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.parentAgent') }}</span>
<span class="text-muted-foreground">{{ t('admin.agents.form.parentAgent') }}</span>
<span>{{ selectedParentAgent?.username || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.commission') }}</span>
<span>{{ (form.commission * 100).toFixed(0) }}%</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.balance') }}</span>
<span class="text-muted-foreground">{{ t('admin.agents.form.balance') }}</span>
<span>¥{{ form.balance.toFixed(2) }}</span>
</div>
</div>
@@ -307,14 +276,14 @@ onMounted(() => {
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:user-plus" class="mr-2 h-4 w-4" />
{{ t('developer.agents.form.create') }}
{{ t('admin.agents.form.create') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.agents.create.cancel') }}
{{ t('admin.agents.create.cancel') }}
</UiButton>
</div>
</UiCardContent>
@@ -10,7 +10,6 @@ export interface Agent {
created_at: string
last_login_at: string
balance: number
commission: number
can_create_agent: boolean
cards_count: number
child_agents_count: number
+25 -25
View File
@@ -119,9 +119,9 @@ const bannedCount = computed(() => {
})
const statusOptions = computed(() => [
{ label: t('developer.agents.status.active'), value: 'active' },
{ label: t('developer.agents.status.inactive'), value: 'inactive' },
{ label: t('developer.agents.status.banned'), value: 'banned' },
{ label: t('admin.agents.status.active'), value: 'active' },
{ label: t('admin.agents.status.inactive'), value: 'inactive' },
{ label: t('admin.agents.status.banned'), value: 'banned' },
])
async function fetchAgents() {
@@ -143,7 +143,7 @@ async function fetchAgents() {
}
function goToCreate() {
router.push('/developer/agents/create')
router.push('/admin/agents/create')
}
function toggleViewMode() {
@@ -152,18 +152,18 @@ function toggleViewMode() {
}
function handleEdit(agent: Agent) {
router.push(`/developer/agents/${agent.id}/edit`)
router.push(`/admin/agents/${agent.id}/edit`)
}
async function handleToggleStatus(agent: Agent) {
try {
const newStatus = agent.status === 'banned' ? 'active' : 'banned'
await api.put(`/dev/agents/${agent.id}/status`, { status: newStatus })
toast.success(t('developer.agents.statusUpdateSuccess'))
toast.success(t('admin.agents.statusUpdateSuccess'))
fetchAgents()
} catch (error: any) {
console.error('切换代理状态失败:', error)
toast.error(error.message || t('developer.agents.statusUpdateFailed'))
toast.error(error.message || t('admin.agents.statusUpdateFailed'))
}
}
@@ -177,11 +177,11 @@ async function handleDelete() {
try {
await api.delete(`/dev/agents/${deleteTarget.value.id}`)
toast.success(t('developer.agents.deleteSuccess'))
toast.success(t('admin.agents.deleteSuccess'))
fetchAgents()
} catch (error: any) {
console.error('删除代理失败:', error)
toast.error(error.message || t('developer.agents.deleteFailed'))
toast.error(error.message || t('admin.agents.deleteFailed'))
} finally {
deleteTarget.value = null
}
@@ -195,11 +195,11 @@ function confirmBatchDelete(ids: number[]) {
async function handleBatchDelete() {
try {
await api.delete('/dev/agents/batch', { agent_ids: batchDeleteIds.value } as any)
toast.success(t('developer.agents.batchDeleteSuccess'))
toast.success(t('admin.agents.batchDeleteSuccess'))
fetchAgents()
} catch (error: any) {
console.error('批量删除失败:', error)
toast.error(error.message || t('developer.agents.batchDeleteFailed'))
toast.error(error.message || t('admin.agents.batchDeleteFailed'))
} finally {
batchDeleteIds.value = []
}
@@ -208,11 +208,11 @@ async function handleBatchDelete() {
async function batchToggleStatus(ids: number[], status: string) {
try {
await api.post('/dev/agents/batch/status', { agent_ids: ids, status })
toast.success(t('developer.agents.batchUpdateSuccess'))
toast.success(t('admin.agents.batchUpdateSuccess'))
fetchAgents()
} catch (error: any) {
console.error('批量更新状态失败:', error)
toast.error(error.message || t('developer.agents.batchUpdateFailed'))
toast.error(error.message || t('admin.agents.batchUpdateFailed'))
}
}
@@ -223,14 +223,14 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.agents.title')"
:description="t('developer.agents.description')"
:title="t('admin.agents.title')"
:description="t('admin.agents.description')"
sticky
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.agents.addAgent') }}
{{ t('admin.agents.addAgent') }}
</UiButton>
</template>
@@ -239,7 +239,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.agents.totalAgents') }}
{{ t('admin.agents.totalAgents') }}
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -253,7 +253,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.agents.status.active') }}
{{ t('admin.agents.status.active') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -267,7 +267,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.agents.status.inactive') }}
{{ t('admin.agents.status.inactive') }}
</UiCardTitle>
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -281,7 +281,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.agents.status.banned') }}
{{ t('admin.agents.status.banned') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -314,7 +314,7 @@ onMounted(() => {
<template #filters>
<SingleFilter
v-model="statusFilter"
:title="t('developer.agents.columns.status')"
:title="t('admin.agents.columns.status')"
:options="statusOptions"
/>
</template>
@@ -331,10 +331,10 @@ onMounted(() => {
@confirm="handleDelete"
>
<template #title>
{{ t('developer.agents.deleteAgent') }}
{{ t('admin.agents.deleteAgent') }}
</template>
<template #description>
{{ t('developer.agents.deleteAgentConfirm', { username: deleteTarget?.username }) }}
{{ t('admin.agents.deleteAgentConfirm', { username: deleteTarget?.username }) }}
</template>
</ConfirmDialog>
@@ -346,10 +346,10 @@ onMounted(() => {
@confirm="handleBatchDelete"
>
<template #title>
{{ t('developer.agents.batchDeleteAgents') }}
{{ t('admin.agents.batchDeleteAgents') }}
</template>
<template #description>
{{ t('developer.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
{{ t('admin.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
+283 -244
View File
@@ -1,61 +1,92 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { Announcement } from '@/pages/admin/announcements/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import api from '@/services/api'
import DataTable from '@/pages/admin/announcements/components/data-table.vue'
import api, { BASE_URL } from '@/services/api'
const { t } = useI18n()
interface Application {
id: number
name: string
}
interface Announcement {
id: number
application_id: number
application_name: string
title: string
content: string
type: 'info' | 'warning' | 'error' | 'success'
status: 'active' | 'inactive'
is_top: boolean
created_at: string
updated_at: string
}
const route = useRoute()
const router = useRouter()
const loading = ref(true)
const saving = ref(false)
const announcements = ref<Announcement[]>([])
const applications = ref<Application[]>([])
const announcement = ref<Announcement | null>(null)
const tableRef = ref()
const formData = ref({
title: '',
content: '',
type: 'info' as 'info' | 'warning' | 'error' | 'success',
status: 'active' as 'active' | 'inactive',
is_status_active: true,
is_top: false,
const appFilter = ref<string>('')
const typeFilter = ref<string>('')
const statusFilter = ref<string>('')
const searchFilter = ref<string>('')
const deleteDialogOpen = ref(false)
const deleteTarget = ref<Announcement | null>(null)
const batchDeleteDialogOpen = ref(false)
const batchDeleteIds = ref<number[]>([])
const applicationOptions = computed(() => {
return applications.value.map(app => ({
label: app.name,
value: String(app.id),
}))
})
const typeOptions = [
{ value: 'info', label: '信息', color: 'bg-blue-500', desc: '普通通知信息' },
{ value: 'warning', label: '警告', color: 'bg-yellow-500', desc: '需要注意的内容' },
{ value: 'error', label: '错误', color: 'bg-red-500', desc: '错误或故障通知' },
{ value: 'success', label: '成功', color: 'bg-green-500', desc: '成功状态通知' },
]
const typeOptions = computed(() => [
{ label: t('admin.announcements.types.info'), value: 'info' },
{ label: t('admin.announcements.types.warning'), value: 'warning' },
{ label: t('admin.announcements.types.error'), value: 'error' },
{ label: t('admin.announcements.types.success'), value: 'success' },
])
const selectedApplication = computed(() => {
return applications.value.find(app => app.id === announcement.value?.application_id)
const statusOptions = computed(() => [
{ label: t('admin.announcements.statuses.active'), value: 'active' },
{ label: t('admin.announcements.statuses.inactive'), value: 'inactive' },
])
const filteredAnnouncements = computed(() => {
let result = announcements.value
if (appFilter.value) {
result = result.filter(a => String(a.application_id) === appFilter.value)
}
if (typeFilter.value) {
result = result.filter(a => a.type === typeFilter.value)
}
if (statusFilter.value) {
result = result.filter(a => a.status === statusFilter.value)
}
if (searchFilter.value) {
const search = searchFilter.value.toLowerCase()
result = result.filter(a =>
a.title?.toLowerCase().includes(search)
|| a.content?.toLowerCase().includes(search)
|| a.application_name?.toLowerCase().includes(search),
)
}
return result
})
const selectedType = computed(() => {
return typeOptions.find(t => t.value === formData.value.type)
})
const activeCount = computed(() => filteredAnnouncements.value.filter(a => a.status === 'active').length)
const inactiveCount = computed(() => filteredAnnouncements.value.filter(a => a.status === 'inactive').length)
const topCount = computed(() => filteredAnnouncements.value.filter(a => a.is_top).length)
async function fetchApplications() {
try {
@@ -67,249 +98,257 @@ async function fetchApplications() {
}
}
async function fetchAnnouncement() {
async function fetchAnnouncements() {
loading.value = true
try {
const data = await api.get<Announcement>(`/dev/announcements/${route.params.id}`)
announcement.value = data
if (data) {
formData.value.title = data.title
formData.value.content = data.content
formData.value.type = data.type
formData.value.status = data.status
formData.value.is_status_active = data.status === 'active'
formData.value.is_top = data.is_top || false
}
const data = await api.get<{ announcements: Announcement[], total: number }>('/dev/announcements')
announcements.value = data?.announcements || []
}
catch (error) {
console.error('获取公告详情失败:', error)
toast.error('公告不存在或已被删除')
router.push('/developer/announcements')
console.error('加载公告失败:', error)
announcements.value = []
}
finally {
loading.value = false
}
}
async function handleSubmit() {
if (!formData.value.title) {
toast.error('请输入公告标题')
return
}
if (!formData.value.content) {
toast.error('请输入公告内容')
return
}
function goToCreate() {
router.push('/admin/announcements/create')
}
saving.value = true
function goToEdit(announcement: Announcement) {
router.push(`/admin/announcements/${announcement.id}`)
}
async function toggleTop(announcement: Announcement) {
try {
await api.put(`/dev/applications/${announcement.value?.application_id}/announcements/${route.params.id}`, {
title: formData.value.title,
content: formData.value.content,
type: formData.value.type,
status: formData.value.is_status_active ? 'active' : 'inactive',
is_top: formData.value.is_top,
})
toast.success('公告更新成功')
router.push('/developer/announcements')
await api.put(`/dev/applications/${announcement.application_id}/announcements/${announcement.id}/top`)
toast.success(announcement.is_top ? t('admin.announcements.unpinned') : t('admin.announcements.pinned'))
fetchAnnouncements()
}
catch (error: any) {
console.error('更新公告失败:', error)
toast.error(error.message || '更新失败')
console.error('切换置顶失败:', error)
toast.error(error.message || t('common.error'))
}
}
function confirmDelete(announcement: Announcement) {
deleteTarget.value = announcement
deleteDialogOpen.value = true
}
async function handleDelete() {
if (!deleteTarget.value)
return
try {
await api.delete(`/dev/applications/${deleteTarget.value.application_id}/announcements/${deleteTarget.value.id}`)
toast.success(t('admin.announcements.deleteSuccess'))
fetchAnnouncements()
}
catch (error: any) {
console.error('删除公告失败:', error)
toast.error(error.message || t('common.error'))
}
finally {
saving.value = false
deleteTarget.value = null
}
}
function handleExport() {
const token = localStorage.getItem('token')
const params = new URLSearchParams()
if (appFilter.value)
params.append('application_id', appFilter.value)
if (typeFilter.value)
params.append('type', typeFilter.value)
if (statusFilter.value)
params.append('status', statusFilter.value)
const queryString = params.toString()
const url = `${BASE_URL}/dev/announcements/export?${queryString}&token=${token}`
window.open(url, '_blank')
}
function handleBatchExport(ids: number[]) {
if (!ids.length)
return
const token = localStorage.getItem('token')
const params = new URLSearchParams()
params.append('ids', ids.join(','))
const url = `${BASE_URL}/dev/announcements/export?${params.toString()}&token=${token}`
window.open(url, '_blank')
}
function confirmBatchDelete(ids: number[]) {
batchDeleteIds.value = ids
batchDeleteDialogOpen.value = true
}
async function handleBatchDelete() {
try {
await api.delete('/dev/announcements/batch', { ids: batchDeleteIds.value } as any)
toast.success(t('admin.announcements.batchDeleteSuccess'))
fetchAnnouncements()
}
catch (error: any) {
console.error('批量删除失败:', error)
toast.error(error.message || t('common.error'))
}
finally {
batchDeleteIds.value = []
}
}
onMounted(() => {
fetchApplications()
fetchAnnouncement()
fetchAnnouncements()
const appParam = route.query.app as string
if (appParam) {
appFilter.value = appParam
}
})
</script>
<template>
<BasicPage
title="编辑公告"
description="编辑现有公告内容"
:breadcrumbs="[
{ title: '公告管理', href: '/developer/announcements' },
{ title: '编辑公告' },
]"
:title="t('admin.announcements.title')"
:description="t('admin.announcements.description')"
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
</div>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('admin.announcements.create') }}
</UiButton>
</template>
<div v-else class="space-y-6">
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:megaphone" class="size-5" />
编辑公告
</UiCardTitle>
<UiCardDescription>修改公告的基本信息和内容</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel>所属应用</UiLabel>
<div class="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
<span>{{ announcement?.application_name || '-' }}</span>
</div>
</div>
<div class="space-y-6">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.announcements.totalAnnouncements') }}
</UiCardTitle>
<Icon icon="lucide:megaphone" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ filteredAnnouncements.length }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="title">
公告标题 <span class="text-destructive">*</span>
</UiLabel>
<UiInput
id="title"
v-model="formData.title"
placeholder="输入公告标题"
:disabled="saving"
/>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.announcements.activeCount') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ activeCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="content">
公告内容 <span class="text-destructive">*</span>
</UiLabel>
<textarea
id="content"
v-model="formData.content"
class="flex min-h-[180px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="输入公告详细内容..."
:disabled="saving"
/>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.announcements.inactiveCount') }}
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ inactiveCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel>公告类型</UiLabel>
<UiSelect v-model="formData.type" :disabled="saving">
<UiSelectTrigger>
<UiSelectValue placeholder="选择类型" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="type in typeOptions" :key="type.value" :value="type.value">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" :class="type.color" />
{{ type.label }}
</div>
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<div class="flex items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<UiLabel class="text-base">
发布状态
</UiLabel>
<p class="text-sm text-muted-foreground">
启用后公告将对用户可见
</p>
</div>
<UiSwitch
v-model="formData.is_status_active"
:disabled="saving"
/>
</div>
<div class="flex items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<UiLabel class="text-base">
置顶公告
</UiLabel>
<p class="text-sm text-muted-foreground">
置顶的公告将在应用内优先显示
</p>
</div>
<UiSwitch
v-model="formData.is_top"
:disabled="saving"
/>
</div>
</UiCardContent>
</UiCard>
</div>
<div class="space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
预览
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">所属应用</span>
<span>{{ selectedApplication?.name || announcement?.application_name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">公告标题</span>
<span class="truncate max-w-[120px]">{{ formData.title || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">公告类型</span>
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full" :class="selectedType?.color" />
<span>{{ selectedType?.label || '-' }}</span>
</div>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">发布状态</span>
<span>{{ formData.is_status_active ? '启用' : '禁用' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">是否置顶</span>
<span>{{ formData.is_top ? '是' : '否' }}</span>
</div>
</div>
<div class="border-t pt-4 mt-4">
<p class="text-sm text-muted-foreground mb-2">
内容预览
</p>
<div class="rounded-lg bg-muted/50 p-3 min-h-[80px]">
<p class="text-sm whitespace-pre-wrap">
{{ formData.content || '暂无内容' }}
</p>
</div>
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardContent class="pt-6">
<div class="flex flex-col gap-3">
<UiButton
class="w-full"
size="lg"
:disabled="saving || !formData.title || !formData.content"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
保存更改
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
取消
</UiButton>
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.announcements.topCount') }}
</UiCardTitle>
<Icon icon="lucide:pin" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ topCount }}
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardContent class="p-6">
<DataTable
ref="tableRef"
:loading
:data="filteredAnnouncements"
:on-toggle-top="toggleTop"
:on-edit="goToEdit"
:on-delete="confirmDelete"
:search-filter="searchFilter"
@refresh="fetchAnnouncements"
@update:search-filter="searchFilter = $event"
@export="handleExport"
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
@batch-export="handleBatchExport(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
>
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('admin.announcements.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="typeFilter"
:title="t('admin.announcements.type')"
:options="typeOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('admin.announcements.status')"
:options="statusOptions"
/>
</template>
</DataTable>
</UiCardContent>
</UiCard>
</div>
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('common.delete')"
:cancel-button-text="t('common.cancel')"
@confirm="handleDelete"
>
<template #title>
{{ t('admin.announcements.deleteAnnouncement') }}
</template>
<template #description>
{{ t('admin.announcements.deleteConfirm', { title: deleteTarget?.title }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
:confirm-button-text="t('common.delete')"
:cancel-button-text="t('common.cancel')"
@confirm="handleBatchDelete"
>
<template #title>
{{ t('admin.announcements.batchDelete') }}
</template>
<template #description>
{{ t('admin.announcements.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
</template>
@@ -18,7 +18,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
return [
{
accessorKey: 'title',
header: () => t('developer.announcements.columns.title'),
header: () => t('admin.announcements.columns.title'),
cell: ({ row }) => {
return h('div', { class: 'space-y-1' }, [
h('div', { class: 'font-medium' }, row.original.title),
@@ -28,7 +28,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
},
{
accessorKey: 'application_name',
header: () => t('developer.announcements.columns.application'),
header: () => t('admin.announcements.columns.application'),
cell: ({ row }) => {
const appName = row.original.application_name
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -36,13 +36,13 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
},
{
accessorKey: 'type',
header: () => t('developer.announcements.columns.type'),
header: () => t('admin.announcements.columns.type'),
cell: ({ row }) => {
const typeLabels: Record<string, string> = {
info: t('developer.announcements.types.info'),
warning: t('developer.announcements.types.warning'),
error: t('developer.announcements.types.error'),
success: t('developer.announcements.types.success'),
info: t('admin.announcements.types.info'),
warning: t('admin.announcements.types.warning'),
error: t('admin.announcements.types.error'),
success: t('admin.announcements.types.success'),
}
const typeClasses: Record<string, string> = {
info: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
@@ -55,11 +55,11 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
},
{
accessorKey: 'status',
header: () => t('developer.announcements.columns.status'),
header: () => t('admin.announcements.columns.status'),
cell: ({ row }) => {
const statusLabels: Record<string, string> = {
active: t('developer.announcements.statuses.active'),
inactive: t('developer.announcements.statuses.inactive'),
active: t('admin.announcements.statuses.active'),
inactive: t('admin.announcements.statuses.inactive'),
}
const statusClasses: Record<string, string> = {
active: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
@@ -70,7 +70,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
},
{
accessorKey: 'is_top',
header: () => t('developer.announcements.columns.isTop'),
header: () => t('admin.announcements.columns.isTop'),
cell: ({ row }) => {
return h(Button, {
variant: 'ghost',
@@ -86,14 +86,14 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
},
{
accessorKey: 'created_at',
header: () => t('developer.announcements.columns.createdAt'),
header: () => t('admin.announcements.columns.createdAt'),
cell: ({ row }) => {
return h('span', { class: 'text-sm text-muted-foreground' }, new Date(row.original.created_at).toLocaleString('zh-CN'))
},
},
{
id: 'actions',
header: () => t('developer.announcements.columns.actions'),
header: () => t('admin.announcements.columns.actions'),
cell: ({ row }) => {
return h('div', { class: 'flex items-center justify-end gap-1' }, [
h(Button, {
@@ -33,7 +33,7 @@ function resetFilters() {
<template>
<div class="flex items-center flex-1 space-x-2">
<Input
:placeholder="t('developer.announcements.searchAll')"
:placeholder="t('admin.announcements.searchAll')"
:model-value="searchFilter"
class="h-8 w-[200px] lg:w-[300px]"
@update:model-value="emit('update:searchFilter', $event as string)"
@@ -45,7 +45,7 @@ function resetFilters() {
class="h-8 px-2 lg:px-3"
@click="resetFilters"
>
{{ t('developer.announcements.reset') }}
{{ t('admin.announcements.reset') }}
<X class="size-4 ml-2" />
</Button>
</div>
@@ -48,13 +48,13 @@ const table = generateVueTable<Announcement>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.announcements.select',
title: 'developer.announcements.columns.title',
application_name: 'developer.announcements.columns.application',
type: 'developer.announcements.columns.type',
status: 'developer.announcements.columns.status',
is_top: 'developer.announcements.columns.isTop',
created_at: 'developer.announcements.columns.createdAt',
select: 'admin.announcements.select',
title: 'admin.announcements.columns.title',
application_name: 'admin.announcements.columns.application',
type: 'admin.announcements.columns.type',
status: 'admin.announcements.columns.status',
is_top: 'admin.announcements.columns.isTop',
created_at: 'admin.announcements.columns.createdAt',
}
defineExpose({
@@ -68,10 +68,10 @@ defineExpose({
<div class="space-y-3">
<BulkActions :table="table" entity-name="announcements">
<UiButton variant="outline" size="sm" @click="emit('batchExport')">
{{ t('developer.announcements.batchExportBtn') }}
{{ t('admin.announcements.batchExportBtn') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.announcements.batchDeleteBtn') }}
{{ t('admin.announcements.batchDeleteBtn') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
@@ -85,7 +85,7 @@ defineExpose({
</div>
<div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" @click="emit('export')">
{{ t('developer.announcements.exportBtn') }}
{{ t('admin.announcements.exportBtn') }}
</UiButton>
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
</div>
@@ -85,7 +85,7 @@ async function handleSubmit() {
is_top: formData.value.is_top,
})
toast.success('公告创建成功')
router.push('/developer/announcements')
router.push('/admin/announcements')
}
catch (error: any) {
console.error('创建公告失败:', error)
@@ -106,7 +106,7 @@ onMounted(() => {
title="创建公告"
description="创建新的系统公告"
:breadcrumbs="[
{ title: '公告管理', href: '/developer/announcements' },
{ title: '公告管理', href: '/admin/announcements' },
{ title: '创建公告' },
]"
sticky
@@ -46,15 +46,15 @@ const applicationOptions = computed(() => {
})
const typeOptions = computed(() => [
{ label: t('developer.announcements.types.info'), value: 'info' },
{ label: t('developer.announcements.types.warning'), value: 'warning' },
{ label: t('developer.announcements.types.error'), value: 'error' },
{ label: t('developer.announcements.types.success'), value: 'success' },
{ label: t('admin.announcements.types.info'), value: 'info' },
{ label: t('admin.announcements.types.warning'), value: 'warning' },
{ label: t('admin.announcements.types.error'), value: 'error' },
{ label: t('admin.announcements.types.success'), value: 'success' },
])
const statusOptions = computed(() => [
{ label: t('developer.announcements.statuses.active'), value: 'active' },
{ label: t('developer.announcements.statuses.inactive'), value: 'inactive' },
{ label: t('admin.announcements.statuses.active'), value: 'active' },
{ label: t('admin.announcements.statuses.inactive'), value: 'inactive' },
])
const filteredAnnouncements = computed(() => {
@@ -114,17 +114,17 @@ async function fetchAnnouncements() {
}
function goToCreate() {
router.push('/developer/announcements/create')
router.push('/admin/announcements/create')
}
function goToEdit(announcement: Announcement) {
router.push(`/developer/announcements/${announcement.id}`)
router.push(`/admin/announcements/${announcement.id}`)
}
async function toggleTop(announcement: Announcement) {
try {
await api.put(`/dev/applications/${announcement.application_id}/announcements/${announcement.id}/top`)
toast.success(announcement.is_top ? t('developer.announcements.unpinned') : t('developer.announcements.pinned'))
toast.success(announcement.is_top ? t('admin.announcements.unpinned') : t('admin.announcements.pinned'))
fetchAnnouncements()
}
catch (error: any) {
@@ -144,7 +144,7 @@ async function handleDelete() {
try {
await api.delete(`/dev/applications/${deleteTarget.value.application_id}/announcements/${deleteTarget.value.id}`)
toast.success(t('developer.announcements.deleteSuccess'))
toast.success(t('admin.announcements.deleteSuccess'))
fetchAnnouncements()
}
catch (error: any) {
@@ -189,7 +189,7 @@ function confirmBatchDelete(ids: number[]) {
async function handleBatchDelete() {
try {
await api.delete('/dev/announcements/batch', { ids: batchDeleteIds.value } as any)
toast.success(t('developer.announcements.batchDeleteSuccess'))
toast.success(t('admin.announcements.batchDeleteSuccess'))
fetchAnnouncements()
}
catch (error: any) {
@@ -213,14 +213,14 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.announcements.title')"
:description="t('developer.announcements.description')"
:title="t('admin.announcements.title')"
:description="t('admin.announcements.description')"
sticky
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.announcements.create') }}
{{ t('admin.announcements.create') }}
</UiButton>
</template>
@@ -229,7 +229,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.announcements.totalAnnouncements') }}
{{ t('admin.announcements.totalAnnouncements') }}
</UiCardTitle>
<Icon icon="lucide:megaphone" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -243,7 +243,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.announcements.activeCount') }}
{{ t('admin.announcements.activeCount') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -257,7 +257,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.announcements.inactiveCount') }}
{{ t('admin.announcements.inactiveCount') }}
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -271,7 +271,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.announcements.topCount') }}
{{ t('admin.announcements.topCount') }}
</UiCardTitle>
<Icon icon="lucide:pin" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -302,17 +302,17 @@ onMounted(() => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.announcements.application')"
:title="t('admin.announcements.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="typeFilter"
:title="t('developer.announcements.type')"
:title="t('admin.announcements.type')"
:options="typeOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('developer.announcements.status')"
:title="t('admin.announcements.status')"
:options="statusOptions"
/>
</template>
@@ -329,10 +329,10 @@ onMounted(() => {
@confirm="handleDelete"
>
<template #title>
{{ t('developer.announcements.deleteAnnouncement') }}
{{ t('admin.announcements.deleteAnnouncement') }}
</template>
<template #description>
{{ t('developer.announcements.deleteConfirm', { title: deleteTarget?.title }) }}
{{ t('admin.announcements.deleteConfirm', { title: deleteTarget?.title }) }}
</template>
</ConfirmDialog>
@@ -344,10 +344,10 @@ onMounted(() => {
@confirm="handleBatchDelete"
>
<template #title>
{{ t('developer.announcements.batchDelete') }}
{{ t('admin.announcements.batchDelete') }}
</template>
<template #description>
{{ t('developer.announcements.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
{{ t('admin.announcements.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
+17 -17
View File
@@ -120,14 +120,14 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.applications.title')"
:description="t('developer.applications.description')"
:title="t('admin.applications.title')"
:description="t('admin.applications.description')"
sticky
>
<template #actions>
<UiButton size="sm" @click="router.push('/developer/applications/create')">
<UiButton size="sm" @click="router.push('/admin/applications/create')">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.applications.createApp') }}
{{ t('admin.applications.createApp') }}
</UiButton>
</template>
@@ -135,7 +135,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.applications.totalApps') }}
{{ t('admin.applications.totalApps') }}
</UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -149,7 +149,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.applications.activeApps') }}
{{ t('admin.applications.activeApps') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -163,7 +163,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.applications.totalUsers') }}
{{ t('admin.applications.totalUsers') }}
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -177,7 +177,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.applications.totalVerifyCount') }}
{{ t('admin.applications.totalVerifyCount') }}
</UiCardTitle>
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -193,23 +193,23 @@ onMounted(() => {
<div class="flex flex-col gap-4 sm:flex-row">
<UiInput
v-model="searchQuery"
:placeholder="t('developer.applications.searchPlaceholder')"
:placeholder="t('admin.applications.searchPlaceholder')"
class="h-9 w-40 lg:w-[250px]"
/>
<UiSelect v-model="statusFilter">
<UiSelectTrigger class="w-32">
<UiSelectValue :placeholder="t('developer.applications.statusFilter')" />
<UiSelectValue :placeholder="t('admin.applications.statusFilter')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="all">
{{ t('developer.applications.allStatus') }}
{{ t('admin.applications.allStatus') }}
</UiSelectItem>
<UiSelectItem value="active">
{{ t('developer.applications.active') }}
{{ t('admin.applications.active') }}
</UiSelectItem>
<UiSelectItem value="inactive">
{{ t('developer.applications.inactive') }}
{{ t('admin.applications.inactive') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
@@ -227,14 +227,14 @@ onMounted(() => {
<Icon icon="lucide:layout-grid" class="size-8 text-muted-foreground" />
</div>
<h3 class="text-lg font-medium">
{{ t('developer.applications.noApps') }}
{{ t('admin.applications.noApps') }}
</h3>
<p class="text-muted-foreground mt-1 mb-4">
{{ searchQuery || statusFilter !== 'all' ? t('developer.applications.noMatch') : t('developer.applications.createFirst') }}
{{ searchQuery || statusFilter !== 'all' ? t('admin.applications.noMatch') : t('admin.applications.createFirst') }}
</p>
<UiButton v-if="!searchQuery && statusFilter === 'all'" @click="router.push('/developer/applications/create')">
<UiButton v-if="!searchQuery && statusFilter === 'all'" @click="router.push('/admin/applications/create')">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.applications.createApp') }}
{{ t('admin.applications.createApp') }}
</UiButton>
</div>
@@ -259,7 +259,7 @@ onMounted(() => {
title="邮箱设置"
description="配置应用的邮箱验证相关设置"
:breadcrumbs="[
{ title: '应用管理', href: '/developer/applications' },
{ title: '应用管理', href: '/admin/applications' },
{ title: '邮箱设置' },
]"
sticky
@@ -140,7 +140,7 @@ onMounted(() => {
title="安全设置"
description="配置应用的安全相关设置,包括加密方式和登录策略"
:breadcrumbs="[
{ title: '应用管理', href: '/developer/applications' },
{ title: '应用管理', href: '/admin/applications' },
{ title: '安全设置' },
]"
sticky
@@ -251,7 +251,7 @@ function toggleWeekday(index: number) {
title="基本设置"
description="修改应用的基本信息,包括名称、描述和图标等"
:breadcrumbs="[
{ title: '应用管理', href: '/developer/applications' },
{ title: '应用管理', href: '/admin/applications' },
{ title: '基本设置' },
]"
sticky
@@ -193,7 +193,7 @@ export function getColumns(actions: {
h(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(app) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }),
t('developer.applications.delete'),
t('admin.applications.delete'),
]),
],
),
@@ -47,14 +47,14 @@ const table = generateVueTable<App>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.applications.select',
name: 'developer.applications.columns.name',
app_key: 'developer.applications.columns.appKey',
billing_type: 'developer.applications.columns.billingType',
login_policy: 'developer.applications.columns.loginPolicy',
enable_trial: 'developer.applications.columns.trial',
status: 'developer.applications.columns.status',
created_at: 'developer.applications.columns.createdAt',
select: 'admin.applications.select',
name: 'admin.applications.columns.name',
app_key: 'admin.applications.columns.appKey',
billing_type: 'admin.applications.columns.billingType',
login_policy: 'admin.applications.columns.loginPolicy',
enable_trial: 'admin.applications.columns.trial',
status: 'admin.applications.columns.status',
created_at: 'admin.applications.columns.createdAt',
}
defineExpose({
@@ -69,7 +69,7 @@ defineExpose({
<div class="flex flex-wrap items-center gap-2">
<UiInput
:model-value="searchFilter"
:placeholder="t('developer.applications.searchPlaceholder')"
:placeholder="t('admin.applications.searchPlaceholder')"
class="h-9 w-40 lg:w-[250px]"
@update:model-value="emit('update:searchFilter', String($event))"
/>
@@ -74,7 +74,7 @@ function toggleWeekday(index: number) {
async function handleCreate() {
if (!formData.value.name.trim()) {
toast.error(t('developer.applications.appNameRequired'))
toast.error(t('admin.applications.appNameRequired'))
return
}
@@ -120,16 +120,16 @@ async function handleCreate() {
const data = await response.json()
if (data.code === 200) {
toast.success(t('developer.applications.createSuccess'))
router.push('/developer/applications')
toast.success(t('admin.applications.createSuccess'))
router.push('/admin/applications')
}
else {
toast.error(data.message || t('developer.applications.createFailed'))
toast.error(data.message || t('admin.applications.createFailed'))
}
}
catch (error) {
console.error('创建应用失败:', error)
toast.error(t('developer.applications.createFailed'))
toast.error(t('admin.applications.createFailed'))
}
finally {
loading.value = false
@@ -139,31 +139,31 @@ async function handleCreate() {
<template>
<BasicPage
:title="t('developer.applications.createApp')"
:description="t('developer.applications.createAppDesc')"
:title="t('admin.applications.createApp')"
:description="t('admin.applications.createAppDesc')"
:breadcrumbs="[
{ title: t('developer.applications.title'), href: '/developer/applications' },
{ title: t('developer.applications.createApp') },
{ title: t('admin.applications.title'), href: '/admin/applications' },
{ title: t('admin.applications.createApp') },
]"
sticky
>
<div class="space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.applications.basicInfo') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.basicInfoDesc') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.applications.basicInfo') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.applications.basicInfoDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<UiLabel for="name">
{{ t('developer.applications.appName') }} *
{{ t('admin.applications.appName') }} *
</UiLabel>
<UiInput id="name" v-model="formData.name" :placeholder="t('developer.applications.appNamePlaceholder')" />
<UiInput id="name" v-model="formData.name" :placeholder="t('admin.applications.appNamePlaceholder')" />
</div>
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.appIcon') }}</UiLabel>
<UiLabel>{{ t('admin.applications.appIcon') }}</UiLabel>
<div class="flex items-center gap-4">
<div v-if="formData.iconPreview" class="relative group">
<img
@@ -185,7 +185,7 @@ async function handleCreate() {
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" />
<span class="text-sm text-muted-foreground">{{ formData.iconPreview ? t('developer.applications.change') : t('developer.applications.upload') }}</span>
<span class="text-sm text-muted-foreground">{{ formData.iconPreview ? t('admin.applications.change') : t('admin.applications.upload') }}</span>
<input
id="appIcon"
type="file"
@@ -200,17 +200,17 @@ async function handleCreate() {
<div class="space-y-2">
<UiLabel for="description">
{{ t('developer.applications.appDescription') }}
{{ t('admin.applications.appDescription') }}
</UiLabel>
<UiTextarea id="description" v-model="formData.description" :placeholder="t('developer.applications.appDescriptionPlaceholder')" rows="3" />
<UiTextarea id="description" v-model="formData.description" :placeholder="t('admin.applications.appDescriptionPlaceholder')" rows="3" />
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.applications.operationMode') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.operationModeDesc') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.applications.operationMode') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.applications.operationModeDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent>
<UiRadioGroup v-model="formData.billing_type" class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -222,10 +222,10 @@ async function handleCreate() {
<UiRadioGroupItem value="free" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.applications.freeMode') }}
{{ t('admin.applications.freeMode') }}
</p>
<p class="text-sm text-muted-foreground">
{{ t('developer.applications.freeModeDesc') }}
{{ t('admin.applications.freeModeDesc') }}
</p>
</div>
</div>
@@ -238,10 +238,10 @@ async function handleCreate() {
<UiRadioGroupItem value="subscription" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.applications.subscriptionMode') }}
{{ t('admin.applications.subscriptionMode') }}
</p>
<p class="text-sm text-muted-foreground">
{{ t('developer.applications.subscriptionModeDesc') }}
{{ t('admin.applications.subscriptionModeDesc') }}
</p>
</div>
</div>
@@ -254,10 +254,10 @@ async function handleCreate() {
<UiRadioGroupItem value="time" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.applications.timeMode') }}
{{ t('admin.applications.timeMode') }}
</p>
<p class="text-sm text-muted-foreground">
{{ t('developer.applications.timeModeDesc') }}
{{ t('admin.applications.timeModeDesc') }}
</p>
</div>
</div>
@@ -270,10 +270,10 @@ async function handleCreate() {
<UiRadioGroupItem value="count" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.applications.countMode') }}
{{ t('admin.applications.countMode') }}
</p>
<p class="text-sm text-muted-foreground">
{{ t('developer.applications.countModeDesc') }}
{{ t('admin.applications.countModeDesc') }}
</p>
</div>
</div>
@@ -282,30 +282,30 @@ async function handleCreate() {
<div v-if="formData.billing_type !== 'free'" class="mt-4 pt-4 border-t space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.deductionCycle') }}</UiLabel>
<UiLabel>{{ t('admin.applications.deductionCycle') }}</UiLabel>
<UiSelect v-model="formData.deduction_cycle">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.applications.selectDeductionCycle')" />
<UiSelectValue :placeholder="t('admin.applications.selectDeductionCycle')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="per_use">
{{ t('developer.applications.perUse') }}
{{ t('admin.applications.perUse') }}
</UiSelectItem>
<UiSelectItem value="per_minute">
{{ t('developer.applications.perMinute') }}
{{ t('admin.applications.perMinute') }}
</UiSelectItem>
<UiSelectItem value="per_hour">
{{ t('developer.applications.perHour') }}
{{ t('admin.applications.perHour') }}
</UiSelectItem>
<UiSelectItem value="per_day">
{{ t('developer.applications.perDay') }}
{{ t('admin.applications.perDay') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.deductionAmount') }}</UiLabel>
<UiLabel>{{ t('admin.applications.deductionAmount') }}</UiLabel>
<UiNumberField v-model="formData.deduction_amount" :min="0" :step="0.01">
<UiNumberFieldContent>
<UiNumberFieldDecrement />
@@ -321,8 +321,8 @@ async function handleCreate() {
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.applications.loginPolicy') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.loginPolicyDesc') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.applications.loginPolicy') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.applications.loginPolicyDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent>
<UiRadioGroup v-model="formData.login_policy" class="grid grid-cols-1 md:grid-cols-3 gap-4">
@@ -334,10 +334,10 @@ async function handleCreate() {
<UiRadioGroupItem value="loose" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.applications.looseMode') }}
{{ t('admin.applications.looseMode') }}
</p>
<p class="text-sm text-muted-foreground">
{{ t('developer.applications.looseModeDesc') }}
{{ t('admin.applications.looseModeDesc') }}
</p>
</div>
</div>
@@ -350,10 +350,10 @@ async function handleCreate() {
<UiRadioGroupItem value="strict" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.applications.strictMode') }}
{{ t('admin.applications.strictMode') }}
</p>
<p class="text-sm text-muted-foreground">
{{ t('developer.applications.strictModeDesc') }}
{{ t('admin.applications.strictModeDesc') }}
</p>
</div>
</div>
@@ -366,10 +366,10 @@ async function handleCreate() {
<UiRadioGroupItem value="hybrid" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.applications.hybridMode') }}
{{ t('admin.applications.hybridMode') }}
</p>
<p class="text-sm text-muted-foreground">
{{ t('developer.applications.hybridModeDesc') }}
{{ t('admin.applications.hybridModeDesc') }}
</p>
</div>
</div>
@@ -379,14 +379,14 @@ async function handleCreate() {
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.applications.trialSettings') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.trialSettings') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.applications.trialSettings') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.applications.trialSettings') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium">
{{ t('developer.applications.enableTrial') }}
{{ t('admin.applications.enableTrial') }}
</p>
<p class="text-sm text-muted-foreground">
新用户注册后自动获得试用权益
@@ -397,7 +397,7 @@ async function handleCreate() {
<div v-if="formData.enable_trial" class="space-y-4 pt-4 border-t">
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.trialBalance') }}</UiLabel>
<UiLabel>{{ t('admin.applications.trialBalance') }}</UiLabel>
<UiNumberField
v-model="formData.trial_balance"
:min="0"
@@ -420,14 +420,14 @@ async function handleCreate() {
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.applications.freePeriodSettings') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.freePeriodSettings') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.applications.freePeriodSettings') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.applications.freePeriodSettings') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium">
{{ t('developer.applications.enableFreePeriod') }}
{{ t('admin.applications.enableFreePeriod') }}
</p>
<p class="text-sm text-muted-foreground">
在设定的时间段内所有用户可免费使用
@@ -438,7 +438,7 @@ async function handleCreate() {
<div v-if="formData.enable_free_period" class="space-y-4 pt-4 border-t">
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.periodType') }}</UiLabel>
<UiLabel>{{ t('admin.applications.periodType') }}</UiLabel>
<UiRadioGroup v-model="formData.free_period_type" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
@@ -448,7 +448,7 @@ async function handleCreate() {
<UiRadioGroupItem value="range" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.applications.dateRange') }}
{{ t('admin.applications.dateRange') }}
</p>
<p class="text-sm text-muted-foreground">
指定开始和结束日期时间
@@ -464,7 +464,7 @@ async function handleCreate() {
<UiRadioGroupItem value="weekly" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.applications.weekly') }}
{{ t('admin.applications.weekly') }}
</p>
<p class="text-sm text-muted-foreground">
每周固定日期和时间段
@@ -476,18 +476,18 @@ async function handleCreate() {
<div v-if="formData.free_period_type === 'range'" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.startTime') }}</UiLabel>
<UiDatePickerDateTimePicker v-model="formData.free_period_start" :placeholder="t('developer.applications.startTime')" />
<UiLabel>{{ t('admin.applications.startTime') }}</UiLabel>
<UiDatePickerDateTimePicker v-model="formData.free_period_start" :placeholder="t('admin.applications.startTime')" />
</div>
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.endTime') }}</UiLabel>
<UiDatePickerDateTimePicker v-model="formData.free_period_end" :placeholder="t('developer.applications.endTime')" />
<UiLabel>{{ t('admin.applications.endTime') }}</UiLabel>
<UiDatePickerDateTimePicker v-model="formData.free_period_end" :placeholder="t('admin.applications.endTime')" />
</div>
</div>
<div v-if="formData.free_period_type === 'weekly'" class="space-y-4">
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.weekdays') }}</UiLabel>
<UiLabel>{{ t('admin.applications.weekdays') }}</UiLabel>
<div class="flex flex-wrap gap-2">
<template v-for="(day, index) in ['周一', '周二', '周三', '周四', '周五', '周六', '周日']" :key="index">
<UiButton
@@ -507,15 +507,15 @@ async function handleCreate() {
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.startTime') }}</UiLabel>
<UiDatePickerTimePicker v-model="formData.free_period_start_time" :placeholder="t('developer.applications.startTime')" />
<UiLabel>{{ t('admin.applications.startTime') }}</UiLabel>
<UiDatePickerTimePicker v-model="formData.free_period_start_time" :placeholder="t('admin.applications.startTime')" />
<p class="text-xs text-muted-foreground">
每天免费开始时间
</p>
</div>
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.endTime') }}</UiLabel>
<UiDatePickerTimePicker v-model="formData.free_period_end_time" :placeholder="t('developer.applications.endTime')" />
<UiLabel>{{ t('admin.applications.endTime') }}</UiLabel>
<UiDatePickerTimePicker v-model="formData.free_period_end_time" :placeholder="t('admin.applications.endTime')" />
<p class="text-xs text-muted-foreground">
每天免费结束时间
</p>
@@ -528,29 +528,29 @@ async function handleCreate() {
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.applications.securitySettings') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.securitySettingsDesc') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.applications.securitySettings') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.applications.securitySettingsDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.encryptionType') }}</UiLabel>
<UiLabel>{{ t('admin.applications.encryptionType') }}</UiLabel>
<UiSelect v-model="formData.encrypt_type">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.applications.encryptionType')" />
<UiSelectValue :placeholder="t('admin.applications.encryptionType')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="none">
{{ t('developer.applications.noEncryption') }}
{{ t('admin.applications.noEncryption') }}
</UiSelectItem>
<UiSelectItem value="aes">
{{ t('developer.applications.aesEncryption') }}
{{ t('admin.applications.aesEncryption') }}
</UiSelectItem>
<UiSelectItem value="rsa">
{{ t('developer.applications.rsaEncryption') }}
{{ t('admin.applications.rsaEncryption') }}
</UiSelectItem>
<UiSelectItem value="rc4">
{{ t('developer.applications.rc4Encryption') }}
{{ t('admin.applications.rc4Encryption') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
@@ -563,7 +563,7 @@ async function handleCreate() {
</div>
<div class="space-y-2">
<UiLabel>{{ t('developer.applications.secretKey') }}</UiLabel>
<UiLabel>{{ t('admin.applications.secretKey') }}</UiLabel>
<div class="relative">
<UiInput
v-model="formData.secret_key"
@@ -591,14 +591,14 @@ async function handleCreate() {
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.applications.deviceBind') }}</UiCardTitle>
<UiCardTitle>{{ t('admin.applications.deviceBind') }}</UiCardTitle>
<UiCardDescription>控制用户登录时是否需要绑定设备</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium">
{{ t('developer.applications.deviceBind') }}
{{ t('admin.applications.deviceBind') }}
</p>
<p class="text-sm text-muted-foreground">
启用后用户登录时需要绑定设备可限制同时登录的设备数量
@@ -611,7 +611,7 @@ async function handleCreate() {
</div>
<div v-if="formData.bind_type === 'device'" class="space-y-2 pt-4 border-t">
<UiLabel>{{ t('developer.applications.maxDevices') }}</UiLabel>
<UiLabel>{{ t('admin.applications.maxDevices') }}</UiLabel>
<UiNumberField v-model="formData.max_devices" :min="1" :max="10">
<UiNumberFieldContent>
<UiNumberFieldDecrement />
@@ -627,7 +627,7 @@ async function handleCreate() {
<div class="flex items-center justify-between pt-4 border-t">
<div>
<p class="font-medium">
{{ t('developer.applications.allowMultiOpen') }}
{{ t('admin.applications.allowMultiOpen') }}
</p>
<p class="text-sm text-muted-foreground">
允许同一账号在同一设备上同时运行多个实例
@@ -639,12 +639,12 @@ async function handleCreate() {
</UiCard>
<div class="flex justify-end gap-3">
<UiButton variant="outline" @click="router.push('/developer/applications')">
<UiButton variant="outline" @click="router.push('/admin/applications')">
取消
</UiButton>
<UiButton :disabled="loading" @click="handleCreate">
<Icon v-if="loading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
{{ t('developer.applications.createApp') }}
{{ t('admin.applications.createApp') }}
</UiButton>
</div>
</div>
+343 -258
View File
@@ -2,57 +2,89 @@
import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { CardType } from '@/pages/developer/card-types/data/schema'
import type { CardType } from '@/pages/admin/card-types/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import api from '@/services/api'
import DataTable from '@/pages/admin/card-types/components/data-table.vue'
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const router = useRouter()
const API_BASE = 'http://localhost:8080/api/v1'
interface Application {
id: string
name: string
}
const loading = ref(true)
const saving = ref(false)
const cardTypes = ref<CardType[]>([])
const applications = ref<Application[]>([])
const cardType = ref<CardType | null>(null)
const tableRef = ref()
const form = ref({
name: '',
description: '',
application_id: '',
price: 0,
value: 0,
validity_type: 'limited' as 'permanent' | 'limited',
const appFilter = ref<string>('')
const statusFilter = ref<string>('')
const deleteDialogOpen = ref(false)
const deleteTarget = ref<CardType | null>(null)
const batchDeleteDialogOpen = ref(false)
const batchDeleteIds = ref<(string | number)[]>([])
const applicationOptions = computed(() => {
return applications.value.map(app => ({
label: app.name,
value: String(app.id),
}))
})
async function fetchCardType() {
const statusOptions = computed(() => [
{ label: t('admin.cardTypes.statusOptions.active'), value: 'active' },
{ label: t('admin.cardTypes.statusOptions.inactive'), value: 'inactive' },
])
const filteredCardTypes = computed(() => {
let result = cardTypes.value
if (appFilter.value) {
result = result.filter(type => String(type.application_id) === appFilter.value)
}
if (statusFilter.value) {
result = result.filter(type => type.status === statusFilter.value)
}
return result
})
const activeTypesCount = computed(() => filteredCardTypes.value.filter(type => type.status === 'active').length)
const totalGeneratedCount = computed(() => filteredCardTypes.value.reduce((sum, type) => sum + (type.generatedCount || 0), 0))
const totalValue = computed(() => filteredCardTypes.value.reduce((sum, type) => sum + (type.price * (type.generatedCount || 0)), 0))
async function fetchCardTypes() {
loading.value = true
try {
const data = await api.get<{ card_type: CardType }>(`/dev/card-types/${route.params.id}`)
cardType.value = data?.card_type || data
if (cardType.value) {
form.value = {
name: cardType.value.name,
description: cardType.value.description,
application_id: cardType.value.application_id ? String(cardType.value.application_id) : '',
price: cardType.value.price,
value: cardType.value.value === -1 ? 0 : cardType.value.value,
validity_type: cardType.value.value === -1 ? 'permanent' : 'limited',
}
const token = localStorage.getItem('token')
const response = await fetch(`${API_BASE}/dev/card-types`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
const data = await response.json()
if (data.code === 200) {
cardTypes.value = Array.isArray(data.data?.card_types) ? data.data.card_types : []
}
else {
cardTypes.value = []
}
}
catch (error: any) {
console.error('获取卡类信息失败:', error)
toast.error(error.message || '获取卡类信息失败')
router.push('/developer/card-types')
catch (error) {
console.error('获取卡密类型失败:', error)
cardTypes.value = []
}
finally {
loading.value = false
@@ -61,264 +93,317 @@ async function fetchCardType() {
async function fetchApplications() {
try {
const data = await api.get<{ applications: Application[] }>('/dev/applications')
applications.value = Array.isArray(data?.applications) ? data.applications : []
const token = localStorage.getItem('token')
const response = await fetch(`${API_BASE}/dev/applications`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
const data = await response.json()
if (data.code === 200) {
applications.value = Array.isArray(data.data?.applications) ? data.data.applications : []
}
else {
applications.value = []
}
}
catch (error) {
console.error('获取应用列表失败:', error)
applications.value = []
}
}
const selectedApplication = computed(() => {
if (form.value.application_id) {
return applications.value.find(app => String(app.id) === form.value.application_id)
}
return null
})
function goToCreate() {
router.push('/admin/card-types/create')
}
const displayValue = computed(() => {
if (form.value.validity_type === 'permanent') {
return t('developer.cardTypes.units.permanent')
}
return form.value.value.toString()
})
function goToEdit(type: CardType) {
router.push(`/admin/card-types/${type.id}`)
}
async function handleSave() {
if (!form.value.name) {
toast.error(t('developer.cardTypes.create.nameRequired'))
return
}
if (!form.value.application_id) {
toast.error(t('developer.cardTypes.create.applicationRequired'))
return
}
if (form.value.validity_type === 'limited' && form.value.value <= 0) {
toast.error(t('developer.cardTypes.create.valueRequired'))
return
}
const value = form.value.validity_type === 'permanent' ? -1 : form.value.value
saving.value = true
async function toggleStatus(type: CardType) {
try {
await api.put(`/dev/card-types/${route.params.id}`, {
application_id: Number(form.value.application_id),
name: form.value.name,
description: form.value.description,
value,
price: form.value.price,
const token = localStorage.getItem('token')
const newStatus = type.status === 'active' ? 'inactive' : 'active'
const response = await fetch(`${API_BASE}/dev/card-types/${type.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: newStatus }),
})
toast.success(t('developer.cardTypes.create.saveSuccess'))
router.push('/developer/card-types')
const data = await response.json()
if (data.code === 200) {
toast.success(t('admin.cardTypes.statusUpdateSuccess'))
fetchCardTypes()
}
else {
toast.error(data.message || t('admin.cardTypes.statusUpdateFailed'))
}
}
catch (error: any) {
console.error('保存卡密类型失败:', error)
toast.error(error.message || t('developer.cardTypes.create.saveFailed'))
catch (error) {
console.error('切换卡密类型状态失败:', error)
toast.error(t('admin.cardTypes.statusUpdateFailed'))
}
}
function confirmDeleteType(type: CardType) {
deleteTarget.value = type
deleteDialogOpen.value = true
}
async function handleDeleteType() {
if (!deleteTarget.value)
return
try {
const token = localStorage.getItem('token')
const response = await fetch(`${API_BASE}/dev/card-types/${deleteTarget.value.id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const data = await response.json()
if (data.code === 200) {
toast.success(t('admin.cardTypes.deleteSuccess'))
fetchCardTypes()
}
else {
toast.error(data.message || t('admin.cardTypes.deleteFailed'))
}
}
catch (error) {
console.error('删除卡密类型失败:', error)
toast.error(t('admin.cardTypes.deleteFailed'))
}
finally {
saving.value = false
deleteTarget.value = null
}
}
function confirmBatchDelete(ids: (string | number)[]) {
batchDeleteIds.value = ids
batchDeleteDialogOpen.value = true
}
async function handleBatchDelete() {
try {
const token = localStorage.getItem('token')
let successCount = 0
let failCount = 0
for (const id of batchDeleteIds.value) {
const response = await fetch(`${API_BASE}/dev/card-types/${id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const data = await response.json()
if (data.code === 200) {
successCount++
}
else {
failCount++
}
}
if (successCount > 0) {
toast.success(t('admin.cardTypes.deleteSuccess'))
fetchCardTypes()
}
if (failCount > 0) {
toast.error(t('admin.cardTypes.deleteFailed'))
}
}
catch (error) {
console.error('批量删除失败:', error)
toast.error(t('admin.cardTypes.deleteFailed'))
}
finally {
batchDeleteIds.value = []
}
}
async function batchToggleStatus(ids: (string | number)[], status: string) {
try {
const token = localStorage.getItem('token')
let successCount = 0
let failCount = 0
for (const id of ids) {
const response = await fetch(`${API_BASE}/dev/card-types/${id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ status }),
})
const data = await response.json()
if (data.code === 200) {
successCount++
}
else {
failCount++
}
}
if (successCount > 0) {
toast.success(t('admin.cardTypes.batchUpdateSuccess'))
fetchCardTypes()
}
if (failCount > 0) {
toast.error(t('admin.cardTypes.batchUpdateFailed'))
}
}
catch (error) {
console.error('批量更新状态失败:', error)
toast.error(t('admin.cardTypes.batchUpdateFailed'))
}
}
onMounted(() => {
fetchCardType()
fetchCardTypes()
fetchApplications()
})
</script>
<template>
<BasicPage
:title="t('developer.cardTypes.create.editTitle')"
:description="t('developer.cardTypes.create.editDescription')"
:breadcrumbs="[
{ title: t('developer.cardTypes.title'), href: '/developer/card-types' },
{ title: t('developer.cardTypes.create.editTitle') },
]"
:title="t('admin.cardTypes.title')"
:description="t('admin.cardTypes.description')"
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
</div>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('admin.cardTypes.createCardType') }}
</UiButton>
</template>
<div v-else class="space-y-6">
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
{{ t('developer.cardTypes.create.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.cardTypes.create.basicInfoDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="name">
{{ t('developer.cardTypes.create.name') }} *
</UiLabel>
<UiInput id="name" v-model="form.name" :placeholder="t('developer.cardTypes.create.namePlaceholder')" />
</div>
<div class="space-y-6">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cardTypes.totalTypes') }}
</UiCardTitle>
<Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ filteredCardTypes.length }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="description">
{{ t('developer.cardTypes.create.description') }}
</UiLabel>
<UiTextarea id="description" v-model="form.description" :placeholder="t('developer.cardTypes.create.descriptionPlaceholder')" rows="3" />
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cardTypes.activeTypes') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ activeTypesCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="application">
{{ t('developer.cardTypes.create.application') }} *
</UiLabel>
<UiSelect v-model="form.application_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cardTypes.create.selectApplication')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
{{ app.name }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cardTypes.generatedCards') }}
</UiCardTitle>
<Icon icon="lucide:hash" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ totalGeneratedCount }}
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cardTypes.create.billingConfig') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.cardTypes.create.billingConfigDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="price">
{{ t('developer.cardTypes.create.price') }}
</UiLabel>
<div class="flex items-center gap-2">
<UiNumberField v-model="form.price" :min="0" :step="0.01" class="max-w-[200px]">
<UiNumberFieldContent>
<UiNumberFieldDecrement />
<UiNumberFieldInput />
<UiNumberFieldIncrement />
</UiNumberFieldContent>
</UiNumberField>
<span class="text-sm text-muted-foreground">{{ t('developer.cardTypes.create.priceUnit') }}</span>
</div>
</div>
<div class="space-y-2">
<UiLabel>{{ t('developer.cardTypes.create.validityType') }}</UiLabel>
<UiRadioGroup v-model="form.validity_type" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
:class="form.validity_type === 'limited' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
@click="form.validity_type = 'limited'"
>
<UiRadioGroupItem value="limited" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.cardTypes.create.limited') }}
</p>
<p class="text-sm text-muted-foreground">
充值指定金额余额
</p>
</div>
</div>
<div
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
:class="form.validity_type === 'permanent' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
@click="form.validity_type = 'permanent'"
>
<UiRadioGroupItem value="permanent" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('developer.cardTypes.create.permanent') }}
</p>
<p class="text-sm text-muted-foreground">
永久会员余额无限
</p>
</div>
</div>
</UiRadioGroup>
</div>
<div v-if="form.validity_type === 'limited'" class="space-y-2">
<UiLabel for="value">
充值金额
</UiLabel>
<UiNumberField v-model="form.value" :min="0.01" :step="0.01" class="max-w-[200px]">
<UiNumberFieldContent>
<UiNumberFieldDecrement />
<UiNumberFieldInput />
<UiNumberFieldIncrement />
</UiNumberFieldContent>
</UiNumberField>
</div>
</UiCardContent>
</UiCard>
</div>
<div class="space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cardTypes.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewName') }}</span>
<span>{{ form.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewApplication') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewPrice') }}</span>
<span>¥{{ form.price.toFixed(2) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewValue') }}</span>
<span>{{ displayValue }}</span>
</div>
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardContent class="pt-6">
<div class="flex flex-col gap-3">
<UiButton
class="w-full"
size="lg"
:disabled="saving || !form.name || !form.application_id"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
{{ t('developer.cardTypes.create.save') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.cardTypes.create.cancel') }}
</UiButton>
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cardTypes.totalValue') }}
</UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
¥{{ totalValue }}
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardContent class="p-6">
<DataTable
ref="tableRef"
:loading
:data="filteredCardTypes"
:on-edit="goToEdit"
:on-toggle-status="toggleStatus"
:on-delete="confirmDeleteType"
@refresh="fetchCardTypes"
@batch-enable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'inactive').map((r: any) => r.original.id) || [], 'active')"
@batch-disable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'active').map((r: any) => r.original.id) || [], 'inactive')"
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
>
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('admin.cardTypes.columns.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('admin.cardTypes.columns.status')"
:options="statusOptions"
/>
</template>
</DataTable>
</UiCardContent>
</UiCard>
</div>
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('admin.cardTypes.delete')"
:cancel-button-text="t('admin.cards.create.cancelBtn')"
@confirm="handleDeleteType"
>
<template #title>
{{ t('admin.cardTypes.delete') }}
</template>
<template #description>
{{ t('admin.cardTypes.deleteConfirm', { name: deleteTarget?.name }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
:confirm-button-text="t('admin.cardTypes.delete')"
:cancel-button-text="t('admin.cards.create.cancelBtn')"
@confirm="handleBatchDelete"
>
<template #title>
{{ t('admin.cardTypes.batchDelete') }}
</template>
<template #description>
{{ t('admin.cardTypes.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
</template>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Ban, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next'
import { h } from 'vue'
import type { CardType } from '@/pages/developer/card-types/data/schema'
import type { CardType } from '@/pages/admin/card-types/data/schema'
import Badge from '@/components/ui/badge/Badge.vue'
import Button from '@/components/ui/button/Button.vue'
@@ -24,14 +24,14 @@ export function getColumns(actions: {
return [
{
accessorKey: 'name',
header: () => t('developer.cardTypes.columns.name'),
header: () => t('admin.cardTypes.columns.name'),
cell: ({ row }) => {
return row.getValue('name') as string
},
},
{
accessorKey: 'description',
header: () => t('developer.cardTypes.columns.description'),
header: () => t('admin.cardTypes.columns.description'),
cell: ({ row }) => {
const description = row.getValue('description') as string
return description || '-'
@@ -39,7 +39,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'application.name',
header: () => t('developer.cardTypes.columns.application'),
header: () => t('admin.cardTypes.columns.application'),
cell: ({ row }) => {
const appName = row.original.application?.name
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -56,7 +56,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'price',
header: () => t('developer.cardTypes.columns.price'),
header: () => t('admin.cardTypes.columns.price'),
cell: ({ row }) => {
const price = row.getValue('price') as number
return `¥${price}`
@@ -64,14 +64,14 @@ export function getColumns(actions: {
},
{
accessorKey: 'value',
header: () => t('developer.cardTypes.columns.value'),
header: () => t('admin.cardTypes.columns.value'),
cell: ({ row }) => {
const value = row.getValue('value') as number
const rechargeType = row.original.recharge_type
const valueUnit = row.original.value_unit
if (value === -1) {
return h(Badge, { variant: 'default' }, () => t('developer.cardTypes.units.permanent'))
return h(Badge, { variant: 'default' }, () => t('admin.cardTypes.units.permanent'))
}
if (rechargeType === 'subscription' && valueUnit) {
@@ -90,24 +90,24 @@ export function getColumns(actions: {
},
{
accessorKey: 'generatedCount',
header: () => t('developer.cardTypes.columns.generatedCount'),
header: () => t('admin.cardTypes.columns.generatedCount'),
cell: ({ row }) => {
const count = row.original.generatedCount ?? 0
return `${count} ${t('developer.cardTypes.units.cards')}`
return `${count} ${t('admin.cardTypes.units.cards')}`
},
},
{
accessorKey: 'status',
header: () => t('developer.cardTypes.columns.status'),
header: () => t('admin.cardTypes.columns.status'),
cell: ({ row }) => {
const status = row.getValue('status') as string
const isActive = status === 'active'
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => t(`developer.cardTypes.statusOptions.${status}`))
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => t(`dmin.cardTypes.statusOptions.${status}`))
},
},
{
accessorKey: 'created_at',
header: () => t('developer.cardTypes.columns.createdAt'),
header: () => t('admin.cardTypes.columns.createdAt'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at')
if (!createdAt)
@@ -152,16 +152,16 @@ export function getColumns(actions: {
() => [
h(DropdownMenuItem, { onClick: () => actions.onEdit(cardType) }, () => [
h(Pencil, { class: 'mr-2 h-4 w-4' }),
t('developer.cardTypes.edit'),
t('admin.cardTypes.edit'),
]),
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(cardType) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }),
isActive ? t('developer.cardTypes.disable') : t('developer.cardTypes.enable'),
isActive ? t('admin.cardTypes.disable') : t('admin.cardTypes.enable'),
]),
h(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(cardType) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }),
t('developer.cardTypes.delete'),
t('admin.cardTypes.delete'),
]),
],
),
@@ -5,7 +5,7 @@ import { X } from 'lucide-vue-next'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { CardType } from '@/pages/developer/card-types/data/schema'
import type { CardType } from '@/pages/admin/card-types/data/schema'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template>
<div class="flex items-center flex-1 space-x-2">
<Input
:placeholder="t('developer.cardTypes.searchName')"
:placeholder="t('admin.cardTypes.searchName')"
:model-value="(table.getColumn('name')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('name')?.setFilterValue($event.target.value)"
@@ -36,7 +36,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
class="h-8 px-2 lg:px-3"
@click="table.resetColumnFilters()"
>
{{ t('developer.cardTypes.reset') }}
{{ t('admin.cardTypes.reset') }}
<X class="size-4 ml-2" />
</Button>
</div>
@@ -46,16 +46,16 @@ const table = generateVueTable<CardType>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.cardTypes.select',
name: 'developer.cardTypes.columns.name',
description: 'developer.cardTypes.columns.description',
application_name: 'developer.cardTypes.columns.application',
billing_type: 'developer.cardTypes.columns.billingType',
price: 'developer.cardTypes.columns.price',
value: 'developer.cardTypes.columns.value',
generatedCount: 'developer.cardTypes.columns.generatedCount',
status: 'developer.cardTypes.columns.status',
created_at: 'developer.cardTypes.columns.createdAt',
select: 'admin.cardTypes.select',
name: 'admin.cardTypes.columns.name',
description: 'admin.cardTypes.columns.description',
application_name: 'admin.cardTypes.columns.application',
billing_type: 'admin.cardTypes.columns.billingType',
price: 'admin.cardTypes.columns.price',
value: 'admin.cardTypes.columns.value',
generatedCount: 'admin.cardTypes.columns.generatedCount',
status: 'admin.cardTypes.columns.status',
created_at: 'admin.cardTypes.columns.createdAt',
}
defineExpose({
@@ -72,13 +72,13 @@ defineExpose({
</div>
<BulkActions :table="table" entity-name="cardTypes">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
{{ t('developer.cardTypes.batchEnableBtn') }}
{{ t('admin.cardTypes.batchEnableBtn') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
{{ t('developer.cardTypes.batchDisableBtn') }}
{{ t('admin.cardTypes.batchDisableBtn') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.cardTypes.batchDeleteBtn') }}
{{ t('admin.cardTypes.batchDeleteBtn') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
+33 -33
View File
@@ -61,15 +61,15 @@ watch(selectedApplication, (app) => {
const displayValue = computed(() => {
if (form.value.is_permanent) {
return t('developer.cardTypes.units.permanent')
return t('admin.cardTypes.units.permanent')
}
if (form.value.recharge_type === 'subscription') {
const unitMap: Record<string, string> = {
minute: t('developer.cardTypes.units.minute'),
hour: t('developer.cardTypes.units.hour'),
day: t('developer.cardTypes.units.day'),
month: t('developer.cardTypes.units.month'),
year: t('developer.cardTypes.units.year'),
minute: t('admin.cardTypes.units.minute'),
hour: t('admin.cardTypes.units.hour'),
day: t('admin.cardTypes.units.day'),
month: t('admin.cardTypes.units.month'),
year: t('admin.cardTypes.units.year'),
}
return `${form.value.value} ${unitMap[form.value.value_unit]}`
}
@@ -78,17 +78,17 @@ const displayValue = computed(() => {
async function handleCreate() {
if (!form.value.name) {
toast.error(t('developer.cardTypes.create.nameRequired'))
toast.error(t('admin.cardTypes.create.nameRequired'))
return
}
if (!form.value.application_id) {
toast.error(t('developer.cardTypes.create.applicationRequired'))
toast.error(t('admin.cardTypes.create.applicationRequired'))
return
}
if (!form.value.is_permanent && form.value.value <= 0) {
toast.error(t('developer.cardTypes.create.valueRequired'))
toast.error(t('admin.cardTypes.create.valueRequired'))
return
}
@@ -103,12 +103,12 @@ async function handleCreate() {
value_unit: form.value.recharge_type === 'subscription' ? form.value.value_unit : '',
price: form.value.price,
})
toast.success(t('developer.cardTypes.create.createSuccess'))
router.push('/developer/card-types')
toast.success(t('admin.cardTypes.create.createSuccess'))
router.push('/admin/card-types')
}
catch (error: any) {
console.error('创建卡密类型失败:', error)
toast.error(error.message || t('developer.cardTypes.create.createFailed'))
toast.error(error.message || t('admin.cardTypes.create.createFailed'))
}
finally {
saving.value = false
@@ -122,11 +122,11 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.cardTypes.create.title')"
:description="t('developer.cardTypes.create.description')"
:title="t('admin.cardTypes.create.title')"
:description="t('admin.cardTypes.create.description')"
:breadcrumbs="[
{ title: t('developer.cardTypes.title'), href: '/developer/card-types' },
{ title: t('developer.cardTypes.create.title') },
{ title: t('admin.cardTypes.title'), href: '/admin/card-types' },
{ title: t('admin.cardTypes.create.title') },
]"
sticky
>
@@ -137,32 +137,32 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
{{ t('developer.cardTypes.create.basicInfo') }}
{{ t('admin.cardTypes.create.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.cardTypes.create.basicInfoDesc') }}</UiCardDescription>
<UiCardDescription>{{ t('admin.cardTypes.create.basicInfoDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="name">
{{ t('developer.cardTypes.create.name') }} *
{{ t('admin.cardTypes.create.name') }} *
</UiLabel>
<UiInput id="name" v-model="form.name" :placeholder="t('developer.cardTypes.create.namePlaceholder')" />
<UiInput id="name" v-model="form.name" :placeholder="t('admin.cardTypes.create.namePlaceholder')" />
</div>
<div class="space-y-2">
<UiLabel for="description">
{{ t('developer.cardTypes.create.description') }}
{{ t('admin.cardTypes.create.description') }}
</UiLabel>
<UiTextarea id="description" v-model="form.description" :placeholder="t('developer.cardTypes.create.descriptionPlaceholder')" rows="3" />
<UiTextarea id="description" v-model="form.description" :placeholder="t('admin.cardTypes.create.descriptionPlaceholder')" rows="3" />
</div>
<div class="space-y-2">
<UiLabel for="application">
{{ t('developer.cardTypes.create.application') }} *
{{ t('admin.cardTypes.create.application') }} *
</UiLabel>
<UiSelect v-model="form.application_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cardTypes.create.selectApplication')" />
<UiSelectValue :placeholder="t('admin.cardTypes.create.selectApplication')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
@@ -222,7 +222,7 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="price">
{{ t('developer.cardTypes.create.price') }}
{{ t('admin.cardTypes.create.price') }}
</UiLabel>
<div class="flex items-center gap-2">
<UiNumberField v-model="form.price" :min="0" :step="0.01" class="max-w-[200px]">
@@ -232,7 +232,7 @@ onMounted(() => {
<UiNumberFieldIncrement />
</UiNumberFieldContent>
</UiNumberField>
<span class="text-sm text-muted-foreground">{{ t('developer.cardTypes.create.priceUnit') }}</span>
<span class="text-sm text-muted-foreground">{{ t('admin.cardTypes.create.priceUnit') }}</span>
</div>
</div>
@@ -313,17 +313,17 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cardTypes.create.preview') }}
{{ t('admin.cardTypes.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewName') }}</span>
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.previewName') }}</span>
<span>{{ form.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewApplication') }}</span>
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.previewApplication') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
@@ -331,11 +331,11 @@ onMounted(() => {
<span>{{ form.recharge_type === 'balance' ? '余额充值' : '订阅充值' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewPrice') }}</span>
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.previewPrice') }}</span>
<span>¥{{ form.price.toFixed(2) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewValue') }}</span>
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.previewValue') }}</span>
<span>{{ displayValue }}</span>
</div>
</div>
@@ -353,14 +353,14 @@ onMounted(() => {
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cardTypes.create.createNow') }}
{{ t('admin.cardTypes.create.createNow') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.cardTypes.create.cancel') }}
{{ t('admin.cardTypes.create.cancel') }}
</UiButton>
</div>
</UiCardContent>
@@ -4,8 +4,8 @@ import type { CardTypeStatus } from './schema'
export function getCardTypeStatuses(t: Composer['t']): { label: string, value: CardTypeStatus }[] {
return [
{ label: t('developer.cardTypes.statusOptions.active'), value: 'active' },
{ label: t('developer.cardTypes.statusOptions.inactive'), value: 'inactive' },
{ label: t('admin.cardTypes.statusOptions.active'), value: 'active' },
{ label: t('admin.cardTypes.statusOptions.inactive'), value: 'inactive' },
]
}
+35 -35
View File
@@ -5,12 +5,12 @@ import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { CardType } from '@/pages/developer/card-types/data/schema'
import type { CardType } from '@/pages/admin/card-types/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import DataTable from '@/pages/developer/card-types/components/data-table.vue'
import DataTable from '@/pages/admin/card-types/components/data-table.vue'
const { t } = useI18n()
@@ -43,8 +43,8 @@ const applicationOptions = computed(() => {
})
const statusOptions = computed(() => [
{ label: t('developer.cardTypes.statusOptions.active'), value: 'active' },
{ label: t('developer.cardTypes.statusOptions.inactive'), value: 'inactive' },
{ label: t('admin.cardTypes.statusOptions.active'), value: 'active' },
{ label: t('admin.cardTypes.statusOptions.inactive'), value: 'inactive' },
])
const filteredCardTypes = computed(() => {
@@ -114,11 +114,11 @@ async function fetchApplications() {
}
function goToCreate() {
router.push('/developer/card-types/create')
router.push('/admin/card-types/create')
}
function goToEdit(type: CardType) {
router.push(`/developer/card-types/${type.id}`)
router.push(`/admin/card-types/${type.id}`)
}
async function toggleStatus(type: CardType) {
@@ -136,16 +136,16 @@ async function toggleStatus(type: CardType) {
const data = await response.json()
if (data.code === 200) {
toast.success(t('developer.cardTypes.statusUpdateSuccess'))
toast.success(t('admin.cardTypes.statusUpdateSuccess'))
fetchCardTypes()
}
else {
toast.error(data.message || t('developer.cardTypes.statusUpdateFailed'))
toast.error(data.message || t('admin.cardTypes.statusUpdateFailed'))
}
}
catch (error) {
console.error('切换卡密类型状态失败:', error)
toast.error(t('developer.cardTypes.statusUpdateFailed'))
toast.error(t('admin.cardTypes.statusUpdateFailed'))
}
}
@@ -169,16 +169,16 @@ async function handleDeleteType() {
const data = await response.json()
if (data.code === 200) {
toast.success(t('developer.cardTypes.deleteSuccess'))
toast.success(t('admin.cardTypes.deleteSuccess'))
fetchCardTypes()
}
else {
toast.error(data.message || t('developer.cardTypes.deleteFailed'))
toast.error(data.message || t('admin.cardTypes.deleteFailed'))
}
}
catch (error) {
console.error('删除卡密类型失败:', error)
toast.error(t('developer.cardTypes.deleteFailed'))
toast.error(t('admin.cardTypes.deleteFailed'))
}
finally {
deleteTarget.value = null
@@ -213,16 +213,16 @@ async function handleBatchDelete() {
}
if (successCount > 0) {
toast.success(t('developer.cardTypes.deleteSuccess'))
toast.success(t('admin.cardTypes.deleteSuccess'))
fetchCardTypes()
}
if (failCount > 0) {
toast.error(t('developer.cardTypes.deleteFailed'))
toast.error(t('admin.cardTypes.deleteFailed'))
}
}
catch (error) {
console.error('批量删除失败:', error)
toast.error(t('developer.cardTypes.deleteFailed'))
toast.error(t('admin.cardTypes.deleteFailed'))
}
finally {
batchDeleteIds.value = []
@@ -254,16 +254,16 @@ async function batchToggleStatus(ids: (string | number)[], status: string) {
}
if (successCount > 0) {
toast.success(t('developer.cardTypes.batchUpdateSuccess'))
toast.success(t('admin.cardTypes.batchUpdateSuccess'))
fetchCardTypes()
}
if (failCount > 0) {
toast.error(t('developer.cardTypes.batchUpdateFailed'))
toast.error(t('admin.cardTypes.batchUpdateFailed'))
}
}
catch (error) {
console.error('批量更新状态失败:', error)
toast.error(t('developer.cardTypes.batchUpdateFailed'))
toast.error(t('admin.cardTypes.batchUpdateFailed'))
}
}
@@ -275,14 +275,14 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.cardTypes.title')"
:description="t('developer.cardTypes.description')"
:title="t('admin.cardTypes.title')"
:description="t('admin.cardTypes.description')"
sticky
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cardTypes.createCardType') }}
{{ t('admin.cardTypes.createCardType') }}
</UiButton>
</template>
@@ -291,7 +291,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cardTypes.totalTypes') }}
{{ t('admin.cardTypes.totalTypes') }}
</UiCardTitle>
<Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -305,7 +305,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cardTypes.activeTypes') }}
{{ t('admin.cardTypes.activeTypes') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -319,7 +319,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cardTypes.generatedCards') }}
{{ t('admin.cardTypes.generatedCards') }}
</UiCardTitle>
<Icon icon="lucide:hash" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -333,7 +333,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cardTypes.totalValue') }}
{{ t('admin.cardTypes.totalValue') }}
</UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -362,12 +362,12 @@ onMounted(() => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.cardTypes.columns.application')"
:title="t('admin.cardTypes.columns.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('developer.cardTypes.columns.status')"
:title="t('admin.cardTypes.columns.status')"
:options="statusOptions"
/>
</template>
@@ -379,30 +379,30 @@ onMounted(() => {
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('developer.cardTypes.delete')"
:cancel-button-text="t('developer.cards.create.cancelBtn')"
:confirm-button-text="t('admin.cardTypes.delete')"
:cancel-button-text="t('admin.cards.create.cancelBtn')"
@confirm="handleDeleteType"
>
<template #title>
{{ t('developer.cardTypes.delete') }}
{{ t('admin.cardTypes.delete') }}
</template>
<template #description>
{{ t('developer.cardTypes.deleteConfirm', { name: deleteTarget?.name }) }}
{{ t('admin.cardTypes.deleteConfirm', { name: deleteTarget?.name }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
:confirm-button-text="t('developer.cardTypes.delete')"
:cancel-button-text="t('developer.cards.create.cancelBtn')"
:confirm-button-text="t('admin.cardTypes.delete')"
:cancel-button-text="t('admin.cards.create.cancelBtn')"
@confirm="handleBatchDelete"
>
<template #title>
{{ t('developer.cardTypes.batchDelete') }}
{{ t('admin.cardTypes.batchDelete') }}
</template>
<template #description>
{{ t('developer.cardTypes.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
{{ t('admin.cardTypes.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Ban, MoreHorizontal, Trash2 } from 'lucide-vue-next'
import { h } from 'vue'
import type { Card } from '@/pages/developer/cards/data/schema'
import type { Card } from '@/pages/admin/cards/data/schema'
import { Copy } from '@/components/sva-ui/copy'
import Badge from '@/components/ui/badge/Badge.vue'
@@ -24,7 +24,7 @@ export function getColumns(actions: {
return [
{
accessorKey: 'card_key',
header: () => t('developer.cards.columns.cardKey'),
header: () => t('admin.cards.columns.cardKey'),
cell: ({ row }) => {
const cardKey = row.getValue('card_key') as string
if (!cardKey)
@@ -37,7 +37,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'application.name',
header: () => t('developer.cards.columns.application'),
header: () => t('admin.cards.columns.application'),
cell: ({ row }) => {
const appName = row.original.application?.name
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -45,7 +45,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'card_type.name',
header: () => t('developer.cards.columns.type'),
header: () => t('admin.cards.columns.type'),
cell: ({ row }) => {
const typeName = row.original.card_type?.name
return typeName ? h(Badge, { variant: 'outline' }, () => typeName) : '-'
@@ -53,7 +53,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'card_type.price',
header: () => t('developer.cards.columns.faceValue'),
header: () => t('admin.cards.columns.faceValue'),
cell: ({ row }) => {
const price = row.original.card_type?.price
return price != null ? `¥${price}` : '-'
@@ -61,35 +61,35 @@ export function getColumns(actions: {
},
{
accessorKey: 'card_type.value',
header: () => t('developer.cards.columns.duration'),
header: () => t('admin.cards.columns.duration'),
cell: ({ row }) => {
const value = row.original.card_type?.value
const billingType = row.original.card_type?.billing_type
if (value == null)
return '-'
if (value === -1)
return t('developer.cards.columns.permanent')
return t('admin.cards.columns.permanent')
if (billingType === 'subscription')
return `${value}${t('developer.cards.columns.hours')}`
return `${value}${t('admin.cards.columns.hours')}`
if (billingType === 'timer')
return `${value}${t('developer.cards.columns.minutes')}`
return `${value}${t('admin.cards.columns.minutes')}`
if (billingType === 'points')
return `${value}${t('developer.cards.columns.points')}`
return `${value}${t('admin.cards.columns.points')}`
return String(value)
},
},
{
accessorKey: 'status',
header: () => t('developer.cards.columns.status'),
header: () => t('admin.cards.columns.status'),
cell: ({ row }) => {
const status = row.getValue('status') as string
if (!status || status === '') {
return h(Badge, { variant: 'default' }, () => t('developer.cards.unused'))
return h(Badge, { variant: 'default' }, () => t('admin.cards.unused'))
}
const statusMap: Record<string, { text: string, variant: any }> = {
unused: { text: t('developer.cards.unused'), variant: 'default' },
used: { text: t('developer.cards.used'), variant: 'secondary' },
banned: { text: t('developer.cards.banned'), variant: 'destructive' },
unused: { text: t('admin.cards.unused'), variant: 'default' },
used: { text: t('admin.cards.used'), variant: 'secondary' },
banned: { text: t('admin.cards.banned'), variant: 'destructive' },
}
const { text, variant } = statusMap[status] || { text: status, variant: 'default' }
return h(Badge, { variant }, () => text)
@@ -97,7 +97,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'app_user.username',
header: () => t('developer.cards.columns.usedUser'),
header: () => t('admin.cards.columns.usedUser'),
cell: ({ row }) => {
const username = row.original.app_user?.username
return username || '-'
@@ -105,7 +105,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'used_at',
header: () => t('developer.cards.columns.usedTime'),
header: () => t('admin.cards.columns.usedTime'),
cell: ({ row }) => {
const usedAt = row.getValue('used_at')
if (!usedAt)
@@ -129,7 +129,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'creator.username',
header: () => t('developer.cards.columns.creator'),
header: () => t('admin.cards.columns.creator'),
cell: ({ row }) => {
const username = row.original.creator?.username
return username || '-'
@@ -137,7 +137,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'created_at',
header: () => t('developer.cards.columns.createdAt'),
header: () => t('admin.cards.columns.createdAt'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at')
if (!createdAt)
@@ -182,12 +182,12 @@ export function getColumns(actions: {
() => [
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(card) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }),
isBanned ? t('developer.cards.enable') : t('developer.cards.disable'),
isBanned ? t('admin.cards.enable') : t('admin.cards.disable'),
]),
h(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(card) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }),
t('developer.cards.delete'),
t('admin.cards.delete'),
]),
],
),
@@ -5,7 +5,7 @@ import { X } from 'lucide-vue-next'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { Card } from '@/pages/developer/cards/data/schema'
import type { Card } from '@/pages/admin/cards/data/schema'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -33,7 +33,7 @@ function resetFilters() {
<template>
<div class="flex items-center flex-1 space-x-2">
<Input
:placeholder="t('developer.cards.searchAll')"
:placeholder="t('admin.cards.searchAll')"
:model-value="searchFilter"
class="h-8 w-[200px] lg:w-[300px]"
@update:model-value="emit('update:searchFilter', $event as string)"
@@ -45,7 +45,7 @@ function resetFilters() {
class="h-8 px-2 lg:px-3"
@click="resetFilters"
>
{{ t('developer.cards.reset') }}
{{ t('admin.cards.reset') }}
<X class="size-4 ml-2" />
</Button>
</div>
@@ -48,17 +48,17 @@ const table = generateVueTable<Card>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.cards.select',
card_key: 'developer.cards.columns.cardKey',
application_name: 'developer.cards.columns.application',
card_type_name: 'developer.cards.columns.type',
card_type_price: 'developer.cards.columns.faceValue',
card_type_value: 'developer.cards.columns.duration',
status: 'developer.cards.columns.status',
app_user_username: 'developer.cards.columns.usedUser',
used_at: 'developer.cards.columns.usedTime',
creator_username: 'developer.cards.columns.creator',
created_at: 'developer.cards.columns.createdAt',
select: 'admin.cards.select',
card_key: 'admin.cards.columns.cardKey',
application_name: 'admin.cards.columns.application',
card_type_name: 'admin.cards.columns.type',
card_type_price: 'admin.cards.columns.faceValue',
card_type_value: 'admin.cards.columns.duration',
status: 'admin.cards.columns.status',
app_user_username: 'admin.cards.columns.usedUser',
used_at: 'admin.cards.columns.usedTime',
creator_username: 'admin.cards.columns.creator',
created_at: 'admin.cards.columns.createdAt',
}
defineExpose({
@@ -75,16 +75,16 @@ defineExpose({
</div>
<BulkActions :table="table" entity-name="cards">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
{{ t('developer.cards.batchEnableBtn') }}
{{ t('admin.cards.batchEnableBtn') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
{{ t('developer.cards.batchDisableBtn') }}
{{ t('admin.cards.batchDisableBtn') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchExport')">
{{ t('developer.cards.batchExportBtn') }}
{{ t('admin.cards.batchExportBtn') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.cards.batchDeleteBtn') }}
{{ t('admin.cards.batchDeleteBtn') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
@@ -98,7 +98,7 @@ defineExpose({
</div>
<div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" @click="emit('export')">
{{ t('developer.cards.exportBtn') }}
{{ t('admin.cards.exportBtn') }}
</UiButton>
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
</div>
+28 -28
View File
@@ -84,15 +84,15 @@ function onApplicationChange() {
async function handleGenerate() {
if (!form.value.application_id) {
toast.error(t('developer.cards.create.selectAppFirst'))
toast.error(t('admin.cards.create.selectAppFirst'))
return
}
if (!form.value.card_type_id) {
toast.error(t('developer.cards.create.selectCardTypeFirst'))
toast.error(t('admin.cards.create.selectCardTypeFirst'))
return
}
if (form.value.count < 1 || form.value.count > 1000) {
toast.error(t('developer.cards.create.countRange'))
toast.error(t('admin.cards.create.countRange'))
return
}
@@ -103,12 +103,12 @@ async function handleGenerate() {
card_type_id: Number(form.value.card_type_id),
count: form.value.count,
})
toast.success(t('developer.cards.create.generateSuccess', { count: form.value.count }))
router.push('/developer/cards')
toast.success(t('admin.cards.create.generateSuccess', { count: form.value.count }))
router.push('/admin/cards')
}
catch (error: any) {
console.error('生成卡密失败:', error)
toast.error(error.message || t('developer.cards.create.generateFailed'))
toast.error(error.message || t('admin.cards.create.generateFailed'))
}
finally {
saving.value = false
@@ -123,11 +123,11 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.cards.create.title')"
:description="t('developer.cards.create.description')"
:title="t('admin.cards.create.title')"
:description="t('admin.cards.create.description')"
:breadcrumbs="[
{ title: t('developer.cards.title'), href: '/developer/cards' },
{ title: t('developer.cards.create.title') },
{ title: t('admin.cards.title'), href: '/admin/cards' },
{ title: t('admin.cards.create.title') },
]"
sticky
>
@@ -138,18 +138,18 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cards.create.config') }}
{{ t('admin.cards.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.cards.create.configDesc') }}</UiCardDescription>
<UiCardDescription>{{ t('admin.cards.create.configDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="application">
{{ t('developer.cards.create.application') }}
{{ t('admin.cards.create.application') }}
</UiLabel>
<UiSelect v-model="form.application_id" @update:model-value="onApplicationChange">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cards.create.selectApplication')" />
<UiSelectValue :placeholder="t('admin.cards.create.selectApplication')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
@@ -161,11 +161,11 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="cardType">
{{ t('developer.cards.create.cardType') }}
{{ t('admin.cards.create.cardType') }}
</UiLabel>
<UiSelect v-model="form.card_type_id" :disabled="!form.application_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cards.create.selectCardType')" />
<UiSelectValue :placeholder="t('admin.cards.create.selectCardType')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="type in filteredCardTypes" :key="type.id" :value="String(type.id)">
@@ -174,13 +174,13 @@ onMounted(() => {
</UiSelectContent>
</UiSelect>
<p v-if="form.application_id && filteredCardTypes.length === 0" class="text-xs text-muted-foreground">
{{ t('developer.cards.create.noCardType') }}
{{ t('admin.cards.create.noCardType') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="count">
{{ t('developer.cards.create.count') }}
{{ t('admin.cards.create.count') }}
</UiLabel>
<div class="flex items-center gap-4">
<UiNumberField v-model="form.count" :min="1" :max="1000" class="flex-1 max-w-[200px]">
@@ -206,7 +206,7 @@ onMounted(() => {
</div>
</div>
<p class="text-xs text-muted-foreground">
{{ t('developer.cards.create.maxCount') }}
{{ t('admin.cards.create.maxCount') }}
</p>
</div>
</UiCardContent>
@@ -218,30 +218,30 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:receipt" class="size-5" />
{{ t('developer.cards.create.preview') }}
{{ t('admin.cards.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cards.create.app') }}</span>
<span class="text-muted-foreground">{{ t('admin.cards.create.app') }}</span>
<span>{{ applications.find(a => String(a.id) === form.application_id)?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cards.create.type') }}</span>
<span class="text-muted-foreground">{{ t('admin.cards.create.type') }}</span>
<span>{{ selectedCardType?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cards.create.unitPrice') }}</span>
<span class="text-muted-foreground">{{ t('admin.cards.create.unitPrice') }}</span>
<span>¥{{ selectedCardType?.price?.toFixed(2) || '0.00' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cards.create.quantity') }}</span>
<span>{{ form.count }} {{ t('developer.cards.create.units') }}</span>
<span class="text-muted-foreground">{{ t('admin.cards.create.quantity') }}</span>
<span>{{ form.count }} {{ t('admin.cards.create.units') }}</span>
</div>
<div class="border-t pt-3 mt-3">
<div class="flex justify-between items-center">
<span class="text-muted-foreground">{{ t('developer.cards.create.totalAmount') }}</span>
<span class="text-muted-foreground">{{ t('admin.cards.create.totalAmount') }}</span>
<span class="text-xl font-bold text-primary">¥{{ totalCost.toFixed(2) }}</span>
</div>
</div>
@@ -260,14 +260,14 @@ onMounted(() => {
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:sparkles" class="mr-2 h-4 w-4" />
{{ t('developer.cards.create.generateNow') }}
{{ t('admin.cards.create.generateNow') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.cards.create.cancel') }}
{{ t('admin.cards.create.cancel') }}
</UiButton>
</div>
</UiCardContent>
+3 -3
View File
@@ -8,21 +8,21 @@ import type { FacetedFilterOption } from '@/components/data-table/types'
export function getCardStatuses(t: Composer['t']): (FacetedFilterOption & { style: string, variant?: string })[] {
return [
{
label: t('developer.cards.unused'),
label: t('admin.cards.unused'),
value: 'unused',
icon: h(CheckCircle2),
style: 'bg-green-500/10 text-green-500',
variant: 'default',
},
{
label: t('developer.cards.used'),
label: t('admin.cards.used'),
value: 'used',
icon: h(Users),
style: 'bg-blue-500/10 text-blue-500',
variant: 'secondary',
},
{
label: t('developer.cards.banned'),
label: t('admin.cards.banned'),
value: 'banned',
icon: h(Ban),
style: 'bg-gray-500/10 text-gray-500',
+32 -32
View File
@@ -5,13 +5,13 @@ import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { Card } from '@/pages/developer/cards/data/schema'
import type { Card } from '@/pages/admin/cards/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import DataTable from '@/pages/developer/cards/components/data-table.vue'
import DataTable from '@/pages/admin/cards/components/data-table.vue'
import api, { BASE_URL } from '@/services/api'
const { t } = useI18n()
@@ -121,9 +121,9 @@ const cardTypeOptions = computed(() => {
})
const statusOptions = computed(() => [
{ label: t('developer.cards.unused'), value: 'unused' },
{ label: t('developer.cards.used'), value: 'used' },
{ label: t('developer.cards.banned'), value: 'banned' },
{ label: t('admin.cards.unused'), value: 'unused' },
{ label: t('admin.cards.used'), value: 'used' },
{ label: t('admin.cards.banned'), value: 'banned' },
])
async function fetchApplications() {
@@ -162,19 +162,19 @@ async function fetchCards() {
}
function goToCreate() {
router.push('/developer/cards/create')
router.push('/admin/cards/create')
}
async function toggleStatus(card: Card) {
try {
const newStatus = card.status === 'banned' ? 'unused' : 'banned'
await api.put(`/dev/cards/${card.id}/status`, { status: newStatus })
toast.success(t('developer.cards.statusUpdateSuccess'))
toast.success(t('admin.cards.statusUpdateSuccess'))
fetchCards()
}
catch (error: any) {
console.error('切换卡密状态失败:', error)
toast.error(error.message || t('developer.cards.statusUpdateFailed'))
toast.error(error.message || t('admin.cards.statusUpdateFailed'))
}
}
@@ -189,12 +189,12 @@ async function handleDeleteCard() {
try {
await api.delete(`/dev/cards/${deleteTarget.value.id}`)
toast.success(t('developer.cards.deleteSuccess'))
toast.success(t('admin.cards.deleteSuccess'))
fetchCards()
}
catch (error: any) {
console.error('删除卡密失败:', error)
toast.error(error.message || t('developer.cards.deleteFailed'))
toast.error(error.message || t('admin.cards.deleteFailed'))
}
finally {
deleteTarget.value = null
@@ -209,12 +209,12 @@ function confirmBatchDelete(ids: (string | number)[]) {
async function handleBatchDelete() {
try {
await api.delete('/dev/cards/batch', { ids: batchDeleteIds.value } as any)
toast.success(t('developer.cards.deleteSuccess'))
toast.success(t('admin.cards.deleteSuccess'))
fetchCards()
}
catch (error: any) {
console.error('批量删除失败:', error)
toast.error(error.message || t('developer.cards.deleteFailed'))
toast.error(error.message || t('admin.cards.deleteFailed'))
}
finally {
batchDeleteIds.value = []
@@ -224,12 +224,12 @@ async function handleBatchDelete() {
async function batchToggleStatus(ids: (string | number)[], status: string) {
try {
await api.put('/dev/cards/batch/status', { ids, status })
toast.success(t('developer.cards.batchUpdateSuccess'))
toast.success(t('admin.cards.batchUpdateSuccess'))
fetchCards()
}
catch (error: any) {
console.error('批量更新状态失败:', error)
toast.error(error.message || t('developer.cards.batchUpdateFailed'))
toast.error(error.message || t('admin.cards.batchUpdateFailed'))
}
}
@@ -277,14 +277,14 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.cards.title')"
:description="t('developer.cards.description')"
:title="t('admin.cards.title')"
:description="t('admin.cards.description')"
sticky
>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cards.generateCard') }}
{{ t('admin.cards.generateCard') }}
</UiButton>
</template>
@@ -293,7 +293,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cards.totalCards') }}
{{ t('admin.cards.totalCards') }}
</UiCardTitle>
<Icon icon="lucide:key" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -307,7 +307,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cards.unused') }}
{{ t('admin.cards.unused') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -321,7 +321,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cards.used') }}
{{ t('admin.cards.used') }}
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -335,7 +335,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cards.banned') }}
{{ t('admin.cards.banned') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -367,17 +367,17 @@ onMounted(() => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.cards.application')"
:title="t('admin.cards.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="cardTypeFilter"
:title="t('developer.cards.cardType')"
:title="t('admin.cards.cardType')"
:options="cardTypeOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('developer.cards.status')"
:title="t('admin.cards.status')"
:options="statusOptions"
/>
<DateRangeFilter
@@ -393,30 +393,30 @@ onMounted(() => {
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('developer.cards.delete')"
:cancel-button-text="t('developer.cards.create.cancelBtn')"
:confirm-button-text="t('admin.cards.delete')"
:cancel-button-text="t('admin.cards.create.cancelBtn')"
@confirm="handleDeleteCard"
>
<template #title>
{{ t('developer.cards.deleteCard') }}
{{ t('admin.cards.deleteCard') }}
</template>
<template #description>
{{ t('developer.cards.deleteCardConfirm', { cardKey: deleteTarget?.card_key }) }}
{{ t('admin.cards.deleteCardConfirm', { cardKey: deleteTarget?.card_key }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
:confirm-button-text="t('developer.cards.delete')"
:cancel-button-text="t('developer.cards.create.cancelBtn')"
:confirm-button-text="t('admin.cards.delete')"
:cancel-button-text="t('admin.cards.create.cancelBtn')"
@confirm="handleBatchDelete"
>
<template #title>
{{ t('developer.cards.batchDeleteCards') }}
{{ t('admin.cards.batchDeleteCards') }}
</template>
<template #description>
{{ t('developer.cards.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
{{ t('admin.cards.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
+268 -204
View File
@@ -2,272 +2,336 @@
import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import api from '@/services/api'
import type { CloudConstant } from './data/schema'
import DataTable from './components/data-table.vue'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
interface Application {
id: number
name: string
app_key: string
}
const loading = ref(true)
const saving = ref(false)
const constants = ref<CloudConstant[]>([])
const applications = ref<Application[]>([])
const form = ref({
id: '',
app_id: '',
key: '',
value: '',
var_type: 'string',
description: '',
status: 'active',
const appFilter = ref<string>('')
const startDate = ref<string>('')
const endDate = ref<string>('')
const deleteDialogOpen = ref(false)
const deleteTarget = ref<CloudConstant | null>(null)
const batchDialogOpen = ref(false)
const batchAction = ref<'enable' | 'disable' | 'delete' | null>(null)
const selectedRows = ref<CloudConstant[]>([])
const filteredConstants = computed(() => {
let result = [...constants.value]
if (appFilter.value) {
result = result.filter(c => String(c.app_id) === appFilter.value)
}
if (startDate.value) {
const fromDateTime = startDate.value.includes('T')
? startDate.value.replace('T', ' ')
: `${startDate.value} 00:00`
result = result.filter(c => c.created_at >= fromDateTime)
}
if (endDate.value) {
const toDateTime = endDate.value.includes('T')
? endDate.value.replace('T', ' ')
: `${endDate.value} 23:59`
result = result.filter(c => c.created_at <= toDateTime)
}
return result
})
const linkedCount = computed(() => constants.value.filter(c => c.app_id).length)
const unlinkedCount = computed(() => constants.value.filter(c => !c.app_id).length)
const applicationOptions = computed(() => {
return applications.value.map(app => ({
label: app.name,
value: String(app.id),
}))
})
async function fetchApplications() {
try {
const data = await api.get<{ applications: Application[] }>('/dev/applications')
applications.value = Array.isArray(data?.applications) ? data.applications : []
applications.value = data?.applications || []
}
catch (error) {
console.error('获取应用列表失败:', error)
}
}
async function fetchConstant() {
async function fetchConstants() {
loading.value = true
try {
const constId = route.params.id
const data = await api.get<{ id: number, app_id: number, key: string, value: string, var_type: string, description: string, status: string }>(`/dev/cloud-constants/${constId}`)
const constant = data
if (constant) {
form.value = {
id: String(constant.id),
app_id: String(constant.app_id || ''),
key: constant.key,
value: constant.value,
var_type: constant.var_type || 'string',
description: constant.description || '',
status: constant.status || 'active',
}
}
else {
toast.error(t('developer.cloudConstants.create.failed'))
router.push('/developer/cloud-constants')
}
const data = await api.get<{ constants: CloudConstant[] }>('/dev/cloud-constants')
const consts = data?.constants || []
constants.value = consts.map((c: any) => ({
...c,
application_name: applications.value.find(app => app.id === c.app_id)?.name || '',
}))
}
catch (error) {
console.error('获取云端常量失败:', error)
toast.error(t('developer.cloudConstants.create.failed'))
router.push('/developer/cloud-constants')
constants.value = []
}
finally {
loading.value = false
}
}
const selectedApplication = computed(() => {
if (form.value.app_id) {
return applications.value.find(app => String(app.id) === form.value.app_id)
}
return null
})
function openCreatePage() {
router.push('/admin/cloud-constants/create')
}
async function handleSave() {
if (!form.value.key) {
toast.error(t('developer.cloudConstants.create.keyRequired'))
return
}
if (!form.value.value) {
toast.error(t('developer.cloudConstants.create.valueRequired'))
return
}
function openEditPage(constant: CloudConstant) {
router.push(`/admin/cloud-constants/${constant.id}`)
}
function confirmDelete(constant: CloudConstant) {
deleteTarget.value = constant
deleteDialogOpen.value = true
}
async function handleDelete() {
if (!deleteTarget.value)
return
saving.value = true
try {
await api.put(`/dev/cloud-constants/${form.value.id}`, {
key: form.value.key,
value: form.value.value,
var_type: form.value.var_type,
description: form.value.description,
status: form.value.status,
})
toast.success(t('developer.cloudConstants.create.success'))
router.push('/developer/cloud-constants')
await api.delete(`/dev/cloud-constants/${deleteTarget.value.id}`)
toast.success(t('admin.cloudConstants.deleteSuccess'))
fetchConstants()
}
catch (error: any) {
console.error('更新云端常量失败:', error)
toast.error(error.message || t('developer.cloudConstants.create.failed'))
console.error('删除云端常量失败:', error)
toast.error(error.message || t('admin.cloudConstants.deleteFailed'))
}
finally {
saving.value = false
deleteTarget.value = null
}
}
async function handleToggleStatus(constant: CloudConstant) {
const newStatus = constant.status === 'active' ? 'inactive' : 'active'
try {
await api.put(`/dev/cloud-constants/${constant.id}`, {
key: constant.key,
value: constant.value,
var_type: constant.var_type,
description: constant.description,
status: newStatus,
})
toast.success(newStatus === 'active' ? t('admin.cloudConstants.enableSuccess') : t('admin.cloudConstants.disableSuccess'))
fetchConstants()
}
catch (error: any) {
console.error('更新状态失败:', error)
toast.error(error.message || t('admin.cloudConstants.updateFailed'))
}
}
function handleDownload(constant: CloudConstant) {
if (constant.var_type !== 'binary')
return
const token = localStorage.getItem('token')
const url = `/api/v1/dev/cloud-constants/${constant.id}/download?token=${token}`
window.open(url, '_blank')
}
function openBatchDialog(action: 'enable' | 'disable' | 'delete', rows: CloudConstant[]) {
batchAction.value = action
selectedRows.value = rows
batchDialogOpen.value = true
}
async function handleBatchAction() {
if (!batchAction.value || selectedRows.value.length === 0)
return
const ids = selectedRows.value.map(c => c.id)
try {
if (batchAction.value === 'delete') {
await Promise.all(ids.map(id => api.delete(`/dev/cloud-constants/${id}`)))
toast.success(t('admin.cloudConstants.batchDeleteSuccess'))
}
else {
const status = batchAction.value === 'enable' ? 'active' : 'inactive'
await Promise.all(selectedRows.value.map(c => api.put(`/dev/cloud-constants/${c.id}`, {
key: c.key,
value: c.value,
var_type: c.var_type,
description: c.description,
status,
})))
toast.success(batchAction.value === 'enable' ? t('admin.cloudConstants.batchEnableSuccess') : t('admin.cloudConstants.batchDisableSuccess'))
}
fetchConstants()
}
catch (error: any) {
console.error('批量操作失败:', error)
toast.error(error.message || t('admin.cloudConstants.batchFailed'))
}
finally {
batchAction.value = null
selectedRows.value = []
}
}
onMounted(async () => {
await fetchApplications()
fetchConstant()
fetchConstants()
})
</script>
<template>
<BasicPage
:title="t('developer.cloudConstants.edit')"
:description="t('developer.cloudConstants.editDescription')"
:breadcrumbs="[
{ title: t('developer.cloudConstants.title'), href: '/developer/cloud-constants' },
{ title: t('developer.cloudConstants.edit') },
]"
:title="t('admin.cloudConstants.title')"
:description="t('admin.cloudConstants.description')"
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
</div>
<template #actions>
<UiButton size="sm" @click="openCreatePage">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('admin.cloudConstants.addConstant') }}
</UiButton>
</template>
<div v-else class="space-y-6">
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cloudConstants.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudConstants.create.configDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel>{{ t('developer.cloudConstants.create.application') }}</UiLabel>
<div class="flex items-center gap-2 p-2.5 rounded-md border bg-muted/50">
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
</div>
<div class="space-y-6">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudConstants.totalConstants') }}
</UiCardTitle>
<Icon icon="lucide:database" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ filteredConstants.length }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="varType">
{{ t('developer.cloudConstants.create.type') }}
</UiLabel>
<UiSelect id="varType" v-model="form.var_type">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudConstants.create.selectType')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="integer">
{{ t('developer.cloudConstants.types.integer') }}
</UiSelectItem>
<UiSelectItem value="decimal">
{{ t('developer.cloudConstants.types.decimal') }}
</UiSelectItem>
<UiSelectItem value="string">
{{ t('developer.cloudConstants.types.string') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudConstants.linkedApps') }}
</UiCardTitle>
<Icon icon="lucide:link" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ linkedCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="key">
{{ t('developer.cloudConstants.create.key') }}
</UiLabel>
<UiInput id="key" v-model="form.key" :placeholder="t('developer.cloudConstants.create.keyPlaceholder')" />
<p class="text-xs text-muted-foreground">
{{ t('developer.cloudConstants.create.keyHint') }}
</p>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudConstants.unlinked') }}
</UiCardTitle>
<Icon icon="lucide:unlink" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ unlinkedCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="value">
{{ t('developer.cloudConstants.create.value') }}
</UiLabel>
<UiTextarea id="value" v-model="form.value" :placeholder="t('developer.cloudConstants.create.valuePlaceholder')" rows="3" />
</div>
<div class="space-y-2">
<UiLabel for="description">
{{ t('developer.cloudConstants.create.description') }}
</UiLabel>
<UiInput id="description" v-model="form.description" :placeholder="t('developer.cloudConstants.create.descriptionPlaceholder')" />
</div>
</UiCardContent>
</UiCard>
</div>
<div class="space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cloudConstants.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.application') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.type') }}</span>
<span>{{ t(`developer.cloudConstants.types.${form.var_type}`) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.key') }}</span>
<span>{{ form.key || '-' }}</span>
</div>
<div class="flex justify-between text-sm items-center">
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.status') }}</span>
<UiSwitch
:checked="form.status === 'active'"
@update:checked="form.status = $event ? 'active' : 'inactive'"
/>
</div>
<div class="border-t pt-3 mt-3">
<div class="text-sm">
<div class="text-muted-foreground mb-2">
{{ t('developer.cloudConstants.create.value') }}
</div>
<div class="bg-muted p-2 rounded max-h-[100px] overflow-y-auto text-xs">
{{ form.value || '-' }}
</div>
</div>
</div>
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardContent class="pt-6">
<div class="flex flex-col gap-3">
<UiButton
class="w-full"
size="lg"
:disabled="saving || !form.key || !form.value"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
{{ t('developer.cloudConstants.create.submit') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.cloudConstants.create.cancel') }}
</UiButton>
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudConstants.appCount') }}
</UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ applications.length }}
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardContent class="p-6">
<DataTable
:loading
:data="filteredConstants"
:on-edit="openEditPage"
:on-delete="confirmDelete"
:on-toggle-status="handleToggleStatus"
:on-download="handleDownload"
@refresh="fetchConstants"
@batch-action="openBatchDialog"
>
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('admin.cloudConstants.application')"
:options="applicationOptions"
/>
<DateRangeFilter
v-model:start-model-value="startDate"
v-model:end-model-value="endDate"
/>
</template>
</DataTable>
</UiCardContent>
</UiCard>
</div>
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('admin.cloudConstants.delete')"
:cancel-button-text="t('common.reset')"
@confirm="handleDelete"
>
<template #title>
{{ t('admin.cloudConstants.deleteConstant') }}
</template>
<template #description>
{{ t('admin.cloudConstants.deleteConfirm', { key: deleteTarget?.key }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDialogOpen"
:destructive="batchAction === 'delete'"
:confirm-button-text="batchAction === 'delete' ? t('admin.cloudConstants.delete') : t('common.confirm')"
:cancel-button-text="t('common.cancel')"
@confirm="handleBatchAction"
>
<template #title>
{{ batchAction === 'delete' ? t('admin.cloudConstants.batchDelete') : batchAction === 'enable' ? t('admin.cloudConstants.batchEnable') : t('admin.cloudConstants.batchDisable') }}
</template>
<template #description>
{{ batchAction === 'delete' ? t('admin.cloudConstants.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('admin.cloudConstants.batchEnableConfirm', { count: selectedRows.length }) : t('admin.cloudConstants.batchDisableConfirm', { count: selectedRows.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
</template>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Download, File, MoreHorizontal, Pencil, Power, PowerOff, Trash2 } from 'lucide-vue-next'
import { h } from 'vue'
import type { CloudConstant } from '@/pages/developer/cloud-constants/data/schema'
import type { CloudConstant } from '@/pages/admin/cloud-constants/data/schema'
import { Copy } from '@/components/sva-ui/copy'
import Badge from '@/components/ui/badge/Badge.vue'
@@ -25,7 +25,7 @@ export function getColumns(actions: {
return [
{
accessorKey: 'key',
header: () => t('developer.cloudConstants.columns.key'),
header: () => t('admin.cloudConstants.columns.key'),
cell: ({ row }) => {
const key = row.getValue('key') as string
if (!key)
@@ -38,7 +38,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'value',
header: () => t('developer.cloudConstants.columns.value'),
header: () => t('admin.cloudConstants.columns.value'),
cell: ({ row }) => {
const value = row.getValue('value') as string
const varType = row.original.var_type
@@ -56,21 +56,21 @@ export function getColumns(actions: {
},
{
accessorKey: 'var_type',
header: () => t('developer.cloudConstants.columns.type'),
header: () => t('admin.cloudConstants.columns.type'),
cell: ({ row }) => {
const type = row.getValue('var_type') as string
const typeMap: Record<string, string> = {
integer: t('developer.cloudConstants.types.integer'),
decimal: t('developer.cloudConstants.types.decimal'),
string: t('developer.cloudConstants.types.string'),
binary: t('developer.cloudConstants.types.binary'),
integer: t('admin.cloudConstants.types.integer'),
decimal: t('admin.cloudConstants.types.decimal'),
string: t('admin.cloudConstants.types.string'),
binary: t('admin.cloudConstants.types.binary'),
}
return h(Badge, { variant: 'secondary' }, () => typeMap[type || 'string'] || type || 'string')
},
},
{
accessorKey: 'description',
header: () => t('developer.cloudConstants.columns.description'),
header: () => t('admin.cloudConstants.columns.description'),
cell: ({ row }) => {
const description = row.getValue('description') as string
if (!description)
@@ -80,12 +80,12 @@ export function getColumns(actions: {
},
{
accessorKey: 'application_name',
header: () => t('developer.cloudConstants.columns.application'),
header: () => t('admin.cloudConstants.columns.application'),
cell: ({ row }) => {
const appName = row.getValue('application_name') as string
const appId = row.original.app_id
if (!appId)
return h(Badge, { variant: 'outline' }, () => t('developer.cloudConstants.notLinked'))
return h(Badge, { variant: 'outline' }, () => t('admin.cloudConstants.notLinked'))
if (!appName)
return '-'
return h(Badge, { variant: 'outline' }, () => appName)
@@ -93,16 +93,16 @@ export function getColumns(actions: {
},
{
accessorKey: 'status',
header: () => t('developer.cloudConstants.columns.status'),
header: () => t('admin.cloudConstants.columns.status'),
cell: ({ row }) => {
const status = row.getValue('status') as string
const isActive = status === 'active'
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => isActive ? t('developer.cloudConstants.statusActive') : t('developer.cloudConstants.statusInactive'))
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => isActive ? t('admin.cloudConstants.statusActive') : t('admin.cloudConstants.statusInactive'))
},
},
{
accessorKey: 'created_at',
header: () => t('developer.cloudConstants.columns.createdAt'),
header: () => t('admin.cloudConstants.columns.createdAt'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at')
if (!createdAt)
@@ -147,7 +147,7 @@ export function getColumns(actions: {
const items = [
h(DropdownMenuItem, { onClick: () => actions.onEdit(constant) }, () => [
h(Pencil, { class: 'mr-2 h-4 w-4' }),
t('developer.cloudConstants.edit'),
t('admin.cloudConstants.edit'),
]),
]
@@ -163,11 +163,11 @@ export function getColumns(actions: {
items.push(
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(constant) }, () => [
constant.status === 'active' ? h(PowerOff, { class: 'mr-2 h-4 w-4' }) : h(Power, { class: 'mr-2 h-4 w-4' }),
constant.status === 'active' ? t('developer.cloudConstants.disable') : t('developer.cloudConstants.enable'),
constant.status === 'active' ? t('admin.cloudConstants.disable') : t('admin.cloudConstants.enable'),
]),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(constant) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }),
t('developer.cloudConstants.delete'),
t('admin.cloudConstants.delete'),
]),
)
@@ -5,7 +5,7 @@ import { X } from 'lucide-vue-next'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { CloudConstant } from '@/pages/developer/cloud-constants/data/schema'
import type { CloudConstant } from '@/pages/admin/cloud-constants/data/schema'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template>
<div class="flex items-center flex-1 space-x-2">
<Input
:placeholder="t('developer.cloudConstants.searchPlaceholder')"
:placeholder="t('admin.cloudConstants.searchPlaceholder')"
:model-value="(table.getColumn('key')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('key')?.setFilterValue($event.target.value)"
@@ -46,14 +46,14 @@ const table = generateVueTable<CloudConstant>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.cloudConstants.select',
key: 'developer.cloudConstants.columns.key',
value: 'developer.cloudConstants.columns.value',
var_type: 'developer.cloudConstants.columns.type',
description: 'developer.cloudConstants.columns.description',
application_name: 'developer.cloudConstants.columns.application',
status: 'developer.cloudConstants.columns.status',
created_at: 'developer.cloudConstants.columns.createdAt',
select: 'admin.cloudConstants.select',
key: 'admin.cloudConstants.columns.key',
value: 'admin.cloudConstants.columns.value',
var_type: 'admin.cloudConstants.columns.type',
description: 'admin.cloudConstants.columns.description',
application_name: 'admin.cloudConstants.columns.application',
status: 'admin.cloudConstants.columns.status',
created_at: 'admin.cloudConstants.columns.createdAt',
}
defineExpose({
@@ -76,13 +76,13 @@ function handleBatchAction(action: 'enable' | 'disable' | 'delete') {
</div>
<BulkActions :table="table" entity-name="constants">
<UiButton variant="outline" size="sm" @click="handleBatchAction('enable')">
{{ t('developer.cloudConstants.batchEnableBtn') }}
{{ t('admin.cloudConstants.batchEnableBtn') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="handleBatchAction('disable')">
{{ t('developer.cloudConstants.batchDisableBtn') }}
{{ t('admin.cloudConstants.batchDisableBtn') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="handleBatchAction('delete')">
{{ t('developer.cloudConstants.batchDeleteBtn') }}
{{ t('admin.cloudConstants.batchDeleteBtn') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
@@ -67,11 +67,11 @@ function clearFile() {
async function handleSave() {
if (!form.value.key) {
toast.error(t('developer.cloudConstants.create.keyRequired'))
toast.error(t('admin.cloudConstants.create.keyRequired'))
return
}
if (!form.value.app_id) {
toast.error(t('developer.cloudConstants.create.appRequired'))
toast.error(t('admin.cloudConstants.create.appRequired'))
return
}
@@ -85,7 +85,7 @@ async function handleSave() {
}
if (!form.value.value) {
toast.error(t('developer.cloudConstants.create.valueRequired'))
toast.error(t('admin.cloudConstants.create.valueRequired'))
return
}
@@ -99,12 +99,12 @@ async function handleSave() {
description: form.value.description,
status: form.value.status,
})
toast.success(t('developer.cloudConstants.create.success'))
router.push('/developer/cloud-constants')
toast.success(t('admin.cloudConstants.create.success'))
router.push('/admin/cloud-constants')
}
catch (error: any) {
console.error('创建云端常量失败:', error)
toast.error(error.message || t('developer.cloudConstants.create.failed'))
toast.error(error.message || t('admin.cloudConstants.create.failed'))
}
finally {
saving.value = false
@@ -125,8 +125,8 @@ async function uploadFile() {
formData.append('status', form.value.status)
await api.postFormData('/dev/cloud-constants/upload', formData)
toast.success(t('developer.cloudConstants.create.success'))
router.push('/developer/cloud-constants')
toast.success(t('admin.cloudConstants.create.success'))
router.push('/admin/cloud-constants')
}
catch (error: any) {
console.error('上传文件失败:', error)
@@ -153,11 +153,11 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.cloudConstants.addConstant')"
:description="t('developer.cloudConstants.createDescription')"
:title="t('admin.cloudConstants.addConstant')"
:description="t('admin.cloudConstants.createDescription')"
:breadcrumbs="[
{ title: t('developer.cloudConstants.title'), href: '/developer/cloud-constants' },
{ title: t('developer.cloudConstants.addConstant') },
{ title: t('admin.cloudConstants.title'), href: '/admin/cloud-constants' },
{ title: t('admin.cloudConstants.addConstant') },
]"
sticky
>
@@ -168,18 +168,18 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cloudConstants.create.config') }}
{{ t('admin.cloudConstants.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudConstants.create.configDesc') }}</UiCardDescription>
<UiCardDescription>{{ t('admin.cloudConstants.create.configDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="application">
{{ t('developer.cloudConstants.create.application') }}
{{ t('admin.cloudConstants.create.application') }}
</UiLabel>
<UiSelect v-model="form.app_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudConstants.create.selectApp')" />
<UiSelectValue :placeholder="t('admin.cloudConstants.create.selectApp')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
@@ -191,21 +191,21 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="varType">
{{ t('developer.cloudConstants.create.type') }}
{{ t('admin.cloudConstants.create.type') }}
</UiLabel>
<UiSelect id="varType" v-model="form.var_type">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudConstants.create.selectType')" />
<UiSelectValue :placeholder="t('admin.cloudConstants.create.selectType')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="integer">
{{ t('developer.cloudConstants.types.integer') }}
{{ t('admin.cloudConstants.types.integer') }}
</UiSelectItem>
<UiSelectItem value="decimal">
{{ t('developer.cloudConstants.types.decimal') }}
{{ t('admin.cloudConstants.types.decimal') }}
</UiSelectItem>
<UiSelectItem value="string">
{{ t('developer.cloudConstants.types.string') }}
{{ t('admin.cloudConstants.types.string') }}
</UiSelectItem>
<UiSelectItem value="binary">
二进制
@@ -216,11 +216,11 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="key">
{{ t('developer.cloudConstants.create.key') }}
{{ t('admin.cloudConstants.create.key') }}
</UiLabel>
<UiInput id="key" v-model="form.key" :placeholder="t('developer.cloudConstants.create.keyPlaceholder')" />
<UiInput id="key" v-model="form.key" :placeholder="t('admin.cloudConstants.create.keyPlaceholder')" />
<p class="text-xs text-muted-foreground">
{{ t('developer.cloudConstants.create.keyHint') }}
{{ t('admin.cloudConstants.create.keyHint') }}
</p>
</div>
@@ -263,16 +263,16 @@ onMounted(() => {
<div v-else class="space-y-2">
<UiLabel for="value">
{{ t('developer.cloudConstants.create.value') }}
{{ t('admin.cloudConstants.create.value') }}
</UiLabel>
<UiTextarea id="value" v-model="form.value" :placeholder="t('developer.cloudConstants.create.valuePlaceholder')" rows="3" />
<UiTextarea id="value" v-model="form.value" :placeholder="t('admin.cloudConstants.create.valuePlaceholder')" rows="3" />
</div>
<div class="space-y-2">
<UiLabel for="description">
{{ t('developer.cloudConstants.create.description') }}
{{ t('admin.cloudConstants.create.description') }}
</UiLabel>
<UiInput id="description" v-model="form.description" :placeholder="t('developer.cloudConstants.create.descriptionPlaceholder')" />
<UiInput id="description" v-model="form.description" :placeholder="t('admin.cloudConstants.create.descriptionPlaceholder')" />
</div>
</UiCardContent>
</UiCard>
@@ -283,25 +283,25 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cloudConstants.create.preview') }}
{{ t('admin.cloudConstants.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.application') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudConstants.create.application') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.type') }}</span>
<span>{{ t(`developer.cloudConstants.types.${form.var_type}`) }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudConstants.create.type') }}</span>
<span>{{ t(`dmin.cloudConstants.types.${form.var_type}`) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.key') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudConstants.create.key') }}</span>
<span>{{ form.key || '-' }}</span>
</div>
<div class="flex justify-between text-sm items-center">
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.status') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudConstants.create.status') }}</span>
<UiSwitch
:checked="form.status === 'active'"
@update:checked="form.status = $event ? 'active' : 'inactive'"
@@ -310,7 +310,7 @@ onMounted(() => {
<div class="border-t pt-3 mt-3">
<div class="text-sm">
<div class="text-muted-foreground mb-2">
{{ t('developer.cloudConstants.create.value') }}
{{ t('admin.cloudConstants.create.value') }}
</div>
<div v-if="form.var_type === 'binary'" class="bg-muted p-2 rounded text-xs">
{{ selectedFile ? `${selectedFile.name} (${formatFileSize(selectedFile.size)})` : '未选择文件' }}
@@ -335,14 +335,14 @@ onMounted(() => {
>
<Icon v-if="saving || uploading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
{{ uploading ? '上传中...' : t('developer.cloudConstants.create.submit') }}
{{ uploading ? '上传中...' : t('admin.cloudConstants.create.submit') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.cloudConstants.create.cancel') }}
{{ t('admin.cloudConstants.create.cancel') }}
</UiButton>
</div>
</UiCardContent>
@@ -102,11 +102,11 @@ async function fetchConstants() {
}
function openCreatePage() {
router.push('/developer/cloud-constants/create')
router.push('/admin/cloud-constants/create')
}
function openEditPage(constant: CloudConstant) {
router.push(`/developer/cloud-constants/${constant.id}`)
router.push(`/admin/cloud-constants/${constant.id}`)
}
function confirmDelete(constant: CloudConstant) {
@@ -120,12 +120,12 @@ async function handleDelete() {
try {
await api.delete(`/dev/cloud-constants/${deleteTarget.value.id}`)
toast.success(t('developer.cloudConstants.deleteSuccess'))
toast.success(t('admin.cloudConstants.deleteSuccess'))
fetchConstants()
}
catch (error: any) {
console.error('删除云端常量失败:', error)
toast.error(error.message || t('developer.cloudConstants.deleteFailed'))
toast.error(error.message || t('admin.cloudConstants.deleteFailed'))
}
finally {
deleteTarget.value = null
@@ -142,12 +142,12 @@ async function handleToggleStatus(constant: CloudConstant) {
description: constant.description,
status: newStatus,
})
toast.success(newStatus === 'active' ? t('developer.cloudConstants.enableSuccess') : t('developer.cloudConstants.disableSuccess'))
toast.success(newStatus === 'active' ? t('admin.cloudConstants.enableSuccess') : t('admin.cloudConstants.disableSuccess'))
fetchConstants()
}
catch (error: any) {
console.error('更新状态失败:', error)
toast.error(error.message || t('developer.cloudConstants.updateFailed'))
toast.error(error.message || t('admin.cloudConstants.updateFailed'))
}
}
@@ -173,7 +173,7 @@ async function handleBatchAction() {
try {
if (batchAction.value === 'delete') {
await Promise.all(ids.map(id => api.delete(`/dev/cloud-constants/${id}`)))
toast.success(t('developer.cloudConstants.batchDeleteSuccess'))
toast.success(t('admin.cloudConstants.batchDeleteSuccess'))
}
else {
const status = batchAction.value === 'enable' ? 'active' : 'inactive'
@@ -184,13 +184,13 @@ async function handleBatchAction() {
description: c.description,
status,
})))
toast.success(batchAction.value === 'enable' ? t('developer.cloudConstants.batchEnableSuccess') : t('developer.cloudConstants.batchDisableSuccess'))
toast.success(batchAction.value === 'enable' ? t('admin.cloudConstants.batchEnableSuccess') : t('admin.cloudConstants.batchDisableSuccess'))
}
fetchConstants()
}
catch (error: any) {
console.error('批量操作失败:', error)
toast.error(error.message || t('developer.cloudConstants.batchFailed'))
toast.error(error.message || t('admin.cloudConstants.batchFailed'))
}
finally {
batchAction.value = null
@@ -206,14 +206,14 @@ onMounted(async () => {
<template>
<BasicPage
:title="t('developer.cloudConstants.title')"
:description="t('developer.cloudConstants.description')"
:title="t('admin.cloudConstants.title')"
:description="t('admin.cloudConstants.description')"
sticky
>
<template #actions>
<UiButton size="sm" @click="openCreatePage">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cloudConstants.addConstant') }}
{{ t('admin.cloudConstants.addConstant') }}
</UiButton>
</template>
@@ -222,7 +222,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudConstants.totalConstants') }}
{{ t('admin.cloudConstants.totalConstants') }}
</UiCardTitle>
<Icon icon="lucide:database" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -236,7 +236,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudConstants.linkedApps') }}
{{ t('admin.cloudConstants.linkedApps') }}
</UiCardTitle>
<Icon icon="lucide:link" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -250,7 +250,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudConstants.unlinked') }}
{{ t('admin.cloudConstants.unlinked') }}
</UiCardTitle>
<Icon icon="lucide:unlink" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -264,7 +264,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudConstants.appCount') }}
{{ t('admin.cloudConstants.appCount') }}
</UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -291,7 +291,7 @@ onMounted(async () => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.cloudConstants.application')"
:title="t('admin.cloudConstants.application')"
:options="applicationOptions"
/>
<DateRangeFilter
@@ -307,30 +307,30 @@ onMounted(async () => {
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('developer.cloudConstants.delete')"
:confirm-button-text="t('admin.cloudConstants.delete')"
:cancel-button-text="t('common.reset')"
@confirm="handleDelete"
>
<template #title>
{{ t('developer.cloudConstants.deleteConstant') }}
{{ t('admin.cloudConstants.deleteConstant') }}
</template>
<template #description>
{{ t('developer.cloudConstants.deleteConfirm', { key: deleteTarget?.key }) }}
{{ t('admin.cloudConstants.deleteConfirm', { key: deleteTarget?.key }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDialogOpen"
:destructive="batchAction === 'delete'"
:confirm-button-text="batchAction === 'delete' ? t('developer.cloudConstants.delete') : t('common.confirm')"
:confirm-button-text="batchAction === 'delete' ? t('admin.cloudConstants.delete') : t('common.confirm')"
:cancel-button-text="t('common.cancel')"
@confirm="handleBatchAction"
>
<template #title>
{{ batchAction === 'delete' ? t('developer.cloudConstants.batchDelete') : batchAction === 'enable' ? t('developer.cloudConstants.batchEnable') : t('developer.cloudConstants.batchDisable') }}
{{ batchAction === 'delete' ? t('admin.cloudConstants.batchDelete') : batchAction === 'enable' ? t('admin.cloudConstants.batchEnable') : t('admin.cloudConstants.batchDisable') }}
</template>
<template #description>
{{ batchAction === 'delete' ? t('developer.cloudConstants.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('developer.cloudConstants.batchEnableConfirm', { count: selectedRows.length }) : t('developer.cloudConstants.batchDisableConfirm', { count: selectedRows.length }) }}
{{ batchAction === 'delete' ? t('admin.cloudConstants.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('admin.cloudConstants.batchEnableConfirm', { count: selectedRows.length }) : t('admin.cloudConstants.batchDisableConfirm', { count: selectedRows.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
+291 -340
View File
@@ -2,409 +2,360 @@
import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import api from '@/services/api'
import type { CloudVariable } from './data/schema'
import DataTable from './components/data-table.vue'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
interface Application {
id: number
name: string
app_key: string
}
const loading = ref(true)
const saving = ref(false)
const variables = ref<CloudVariable[]>([])
const applications = ref<Application[]>([])
const form = ref({
id: '',
app_id: '',
key: '',
default_value: '',
var_type: 'string',
max_records: 0,
scope: 'app',
write_permission: 'admin',
description: '',
status: 'active',
const appFilter = ref<string>('')
const scopeFilter = ref<string>('')
const startDate = ref<string>('')
const endDate = ref<string>('')
const deleteDialogOpen = ref(false)
const deleteTarget = ref<CloudVariable | null>(null)
const batchDialogOpen = ref(false)
const batchAction = ref<'enable' | 'disable' | 'delete' | null>(null)
const selectedRows = ref<CloudVariable[]>([])
const filteredVariables = computed(() => {
let result = [...variables.value]
if (appFilter.value) {
result = result.filter(v => String(v.app_id) === appFilter.value)
}
if (scopeFilter.value) {
result = result.filter(v => v.scope === scopeFilter.value)
}
if (startDate.value) {
const fromDateTime = startDate.value.includes('T')
? startDate.value.replace('T', ' ')
: `${startDate.value} 00:00`
result = result.filter(v => v.created_at >= fromDateTime)
}
if (endDate.value) {
const toDateTime = endDate.value.includes('T')
? endDate.value.replace('T', ' ')
: `${endDate.value} 23:59`
result = result.filter(v => v.created_at <= toDateTime)
}
return result
})
const appScopeCount = computed(() => variables.value.filter(v => v.scope === 'app').length)
const userScopeCount = computed(() => variables.value.filter(v => v.scope === 'user').length)
const applicationOptions = computed(() => {
return applications.value.map(app => ({
label: app.name,
value: String(app.id),
}))
})
const scopeOptions = computed(() => [
{ label: t('admin.cloudVariables.appScope'), value: 'app' },
{ label: t('admin.cloudVariables.userScope'), value: 'user' },
])
async function fetchApplications() {
try {
const data = await api.get<{ applications: Application[] }>('/dev/applications')
applications.value = Array.isArray(data?.applications) ? data.applications : []
applications.value = data?.applications || []
}
catch (error) {
console.error('获取应用列表失败:', error)
}
}
async function fetchVariable() {
async function fetchVariables() {
loading.value = true
try {
const varId = route.params.id
const data = await api.get<{ id: number, app_id: number, key: string, default_value: string, var_type: string, max_records: number, scope: string, write_permission: string, description: string, status: string }>(`/dev/cloud-variables/${varId}`)
const variable = data
if (variable) {
form.value = {
id: String(variable.id),
app_id: String(variable.app_id || ''),
key: variable.key,
default_value: variable.default_value || '',
var_type: variable.var_type || 'string',
max_records: variable.max_records || 0,
scope: variable.scope || 'app',
write_permission: variable.write_permission || 'admin',
description: variable.description || '',
status: variable.status || 'active',
}
}
else {
toast.error(t('developer.cloudVariables.create.failed'))
router.push('/developer/cloud-variables')
}
const data = await api.get<{ variables: CloudVariable[] }>('/dev/cloud-variables')
const vars = data?.variables || []
variables.value = vars.map((v: any) => ({
...v,
application_name: applications.value.find(app => app.id === v.app_id)?.name || '',
}))
}
catch (error) {
console.error('获取云端变量失败:', error)
toast.error(t('developer.cloudVariables.create.failed'))
router.push('/developer/cloud-variables')
variables.value = []
}
finally {
loading.value = false
}
}
const selectedApplication = computed(() => {
if (form.value.app_id) {
return applications.value.find(app => String(app.id) === form.value.app_id)
}
return null
})
function openCreatePage() {
router.push('/admin/cloud-variables/create')
}
async function handleSave() {
if (!form.value.key) {
toast.error(t('developer.cloudVariables.create.keyRequired'))
function openEditPage(variable: CloudVariable) {
router.push(`/admin/cloud-variables/${variable.id}`)
}
function confirmDelete(variable: CloudVariable) {
deleteTarget.value = variable
deleteDialogOpen.value = true
}
async function handleDelete() {
if (!deleteTarget.value)
return
}
saving.value = true
try {
await api.put(`/dev/cloud-variables/${form.value.id}`, {
key: form.value.key,
default_value: form.value.default_value,
var_type: form.value.var_type,
max_records: form.value.max_records,
write_permission: form.value.write_permission,
description: form.value.description,
status: form.value.status,
})
toast.success(t('developer.cloudVariables.create.success'))
router.push('/developer/cloud-variables')
await api.delete(`/dev/cloud-variables/${deleteTarget.value.id}`)
toast.success(t('admin.cloudVariables.deleteSuccess'))
fetchVariables()
}
catch (error: any) {
console.error('更新云端变量失败:', error)
toast.error(error.message || t('developer.cloudVariables.create.failed'))
console.error('删除云端变量失败:', error)
toast.error(error.message || t('admin.cloudVariables.deleteFailed'))
}
finally {
saving.value = false
deleteTarget.value = null
}
}
async function handleToggleStatus(variable: CloudVariable) {
const newStatus = variable.status === 'active' ? 'inactive' : 'active'
try {
await api.put(`/dev/cloud-variables/${variable.id}`, {
key: variable.key,
default_value: variable.default_value,
var_type: variable.var_type,
scope: variable.scope,
write_permission: variable.write_permission,
description: variable.description,
status: newStatus,
})
toast.success(newStatus === 'active' ? t('admin.cloudVariables.enableSuccess') : t('admin.cloudVariables.disableSuccess'))
fetchVariables()
}
catch (error: any) {
console.error('更新状态失败:', error)
toast.error(error.message || t('admin.cloudVariables.updateFailed'))
}
}
function handleDownload(variable: CloudVariable) {
if (variable.var_type !== 'binary')
return
const token = localStorage.getItem('token')
const url = `/api/v1/dev/cloud-variables/${variable.id}/download?token=${token}`
window.open(url, '_blank')
}
function handleViewRecords(variable: CloudVariable) {
router.push(`/admin/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
}
function openBatchDialog(action: 'enable' | 'disable' | 'delete', rows: CloudVariable[]) {
batchAction.value = action
selectedRows.value = rows
batchDialogOpen.value = true
}
async function handleBatchAction() {
if (!batchAction.value || selectedRows.value.length === 0)
return
const ids = selectedRows.value.map(v => v.id)
try {
if (batchAction.value === 'delete') {
await Promise.all(ids.map(id => api.delete(`/dev/cloud-variables/${id}`)))
toast.success(t('admin.cloudVariables.batchDeleteSuccess'))
}
else {
const status = batchAction.value === 'enable' ? 'active' : 'inactive'
await Promise.all(selectedRows.value.map(v => api.put(`/dev/cloud-variables/${v.id}`, {
key: v.key,
default_value: v.default_value,
var_type: v.var_type,
scope: v.scope,
write_permission: v.write_permission,
description: v.description,
status,
})))
toast.success(batchAction.value === 'enable' ? t('admin.cloudVariables.batchEnableSuccess') : t('admin.cloudVariables.batchDisableSuccess'))
}
fetchVariables()
}
catch (error: any) {
console.error('批量操作失败:', error)
toast.error(error.message || t('admin.cloudVariables.batchFailed'))
}
finally {
batchAction.value = null
selectedRows.value = []
}
}
onMounted(async () => {
await fetchApplications()
fetchVariable()
fetchVariables()
})
</script>
<template>
<BasicPage
:title="t('developer.cloudVariables.edit')"
:description="t('developer.cloudVariables.editDescription')"
:breadcrumbs="[
{ title: t('developer.cloudVariables.title'), href: '/developer/cloud-variables' },
{ title: t('developer.cloudVariables.edit') },
]"
:title="t('admin.cloudVariables.title')"
:description="t('admin.cloudVariables.description')"
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
</div>
<template #actions>
<UiButton size="sm" @click="openCreatePage">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('admin.cloudVariables.addVariable') }}
</UiButton>
</template>
<div v-else class="space-y-6">
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cloudVariables.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudVariables.create.configDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel>{{ t('developer.cloudVariables.create.application') }}</UiLabel>
<div class="flex items-center gap-2 p-2.5 rounded-md border bg-muted/50">
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
</div>
<div class="space-y-6">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.totalVariables') }}
</UiCardTitle>
<Icon icon="lucide:variable" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ filteredVariables.length }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="varType">
{{ t('developer.cloudVariables.create.type') }}
</UiLabel>
<UiSelect id="varType" v-model="form.var_type">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudVariables.create.selectType')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="integer">
{{ t('developer.cloudVariables.types.integer') }}
</UiSelectItem>
<UiSelectItem value="decimal">
{{ t('developer.cloudVariables.types.decimal') }}
</UiSelectItem>
<UiSelectItem value="string">
{{ t('developer.cloudVariables.types.string') }}
</UiSelectItem>
<UiSelectItem value="stream">
记录
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.appScopeVariables') }}
</UiCardTitle>
<Icon icon="lucide:globe" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ appScopeCount }}
</div>
</UiCardContent>
</UiCard>
<div v-if="form.var_type === 'stream'" class="space-y-2">
<UiLabel for="maxRecords">
最大记录数
</UiLabel>
<UiInput
id="maxRecords"
v-model.number="form.max_records"
type="number"
min="0"
placeholder="0 = 不限制"
/>
<p class="text-xs text-muted-foreground">
超过限制时自动删除最旧的记录0表示不限制
</p>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.userScopeVariables') }}
</UiCardTitle>
<Icon icon="lucide:user" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ userScopeCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="key">
{{ t('developer.cloudVariables.create.key') }}
</UiLabel>
<UiInput id="key" v-model="form.key" :placeholder="t('developer.cloudVariables.create.keyPlaceholder')" />
<p class="text-xs text-muted-foreground">
{{ t('developer.cloudVariables.create.keyHint') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="defaultValue">
{{ t('developer.cloudVariables.create.defaultValue') }}
</UiLabel>
<UiTextarea id="defaultValue" v-model="form.default_value" :placeholder="t('developer.cloudVariables.create.defaultValuePlaceholder')" rows="3" />
</div>
<div class="space-y-2">
<UiLabel for="description">
{{ t('developer.cloudVariables.create.description') }}
</UiLabel>
<UiInput id="description" v-model="form.description" :placeholder="t('developer.cloudVariables.create.descriptionPlaceholder')" />
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.cloudVariables.create.scopeTitle') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudVariables.create.scopeDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent>
<div
class="flex items-start gap-3 p-4 rounded-lg border"
:class="form.scope === 'app' ? 'border-blue-500 bg-blue-50 dark:bg-blue-950/30' : 'border-purple-500 bg-purple-50 dark:bg-purple-950/30'"
>
<Icon
:icon="form.scope === 'app' ? 'lucide:globe' : 'lucide:user'"
class="size-5 mt-0.5"
:class="form.scope === 'app' ? 'text-blue-500' : 'text-purple-500'"
/>
<div>
<p class="font-medium">
{{ form.scope === 'app' ? t('developer.cloudVariables.create.appScope') : t('developer.cloudVariables.create.userScope') }}
</p>
<p class="text-sm text-muted-foreground mt-1">
{{ form.scope === 'app' ? t('developer.cloudVariables.create.appScopeDesc') : t('developer.cloudVariables.create.userScopeDesc') }}
</p>
</div>
</div>
<p class="text-xs text-muted-foreground mt-2">
{{ t('developer.cloudVariables.create.scopeImmutable') }}
</p>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.cloudVariables.create.permissionTitle') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudVariables.create.permissionDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent>
<UiRadioGroup v-model="form.write_permission" class="grid grid-cols-1 gap-4">
<div
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
:class="form.write_permission === 'admin' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
@click="form.write_permission = 'admin'"
>
<UiRadioGroupItem value="admin" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:lock" class="size-4 text-primary" />
<p class="font-medium">
{{ t('developer.cloudVariables.create.developerOnly') }}
</p>
</div>
<p class="text-sm text-muted-foreground mt-1">
{{ form.scope === 'app' ? t('developer.cloudVariables.create.developerOnlyAppDesc') : t('developer.cloudVariables.create.developerOnlyDesc') }}
</p>
</div>
</div>
<div
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
:class="form.write_permission === 'user' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
@click="form.write_permission = 'user'"
>
<UiRadioGroupItem value="user" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:unlock" class="size-4 text-primary" />
<p class="font-medium">
{{ t('developer.cloudVariables.create.userWritable') }}
</p>
</div>
<p class="text-sm text-muted-foreground mt-1">
{{ form.scope === 'app' ? t('developer.cloudVariables.create.userWritableAppDesc') : t('developer.cloudVariables.create.userWritableDesc') }}
</p>
</div>
</div>
<div
v-if="form.var_type === 'stream'"
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
:class="form.write_permission === 'app_user' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
@click="form.write_permission = 'app_user'"
>
<UiRadioGroupItem value="app_user" class="mt-0.5" />
<div>
<div class="flex items-center gap-2">
<Icon icon="lucide:users" class="size-4 text-primary" />
<p class="font-medium">
应用用户可写
</p>
</div>
<p class="text-sm text-muted-foreground mt-1">
允许应用端用户通过API写入记录适合订单日志等场景
</p>
</div>
</div>
</UiRadioGroup>
</UiCardContent>
</UiCard>
</div>
<div class="space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cloudVariables.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.application') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.type') }}</span>
<span>{{ t(`developer.cloudVariables.types.${form.var_type}`) }}</span>
</div>
<div v-if="form.var_type === 'stream'" class="flex justify-between text-sm">
<span class="text-muted-foreground">最大记录数</span>
<span>{{ form.max_records || '不限制' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.key') }}</span>
<span>{{ form.key || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.scope') }}</span>
<UiBadge variant="outline">
{{ form.scope === 'app' ? t('developer.cloudVariables.appScope') : t('developer.cloudVariables.userScope') }}
</UiBadge>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.writePermission') }}</span>
<UiBadge variant="secondary">
{{ form.write_permission === 'admin' ? t('developer.cloudVariables.create.developerOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('developer.cloudVariables.create.userWritable') }}
</UiBadge>
</div>
<div class="flex justify-between text-sm items-center">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.status') }}</span>
<UiSwitch
:checked="form.status === 'active'"
@update:checked="form.status = $event ? 'active' : 'inactive'"
/>
</div>
<div class="border-t pt-3 mt-3">
<div class="text-sm">
<div class="text-muted-foreground mb-2">
{{ t('developer.cloudVariables.create.defaultValue') }}
</div>
<div class="bg-muted p-2 rounded max-h-[100px] overflow-y-auto text-xs">
{{ form.default_value || '-' }}
</div>
</div>
</div>
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardContent class="pt-6">
<div class="flex flex-col gap-3">
<UiButton
class="w-full"
size="lg"
:disabled="saving || !form.key"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
{{ t('developer.cloudVariables.create.submit') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.cloudVariables.create.cancel') }}
</UiButton>
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.appCount') }}
</UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ applications.length }}
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardContent class="p-6">
<DataTable
:loading
:data="filteredVariables"
:on-edit="openEditPage"
:on-delete="confirmDelete"
:on-toggle-status="handleToggleStatus"
:on-download="handleDownload"
:on-view-records="handleViewRecords"
@refresh="fetchVariables"
@batch-action="openBatchDialog"
>
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('admin.cloudVariables.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="scopeFilter"
:title="t('admin.cloudVariables.scope')"
:options="scopeOptions"
/>
<DateRangeFilter
v-model:start-model-value="startDate"
v-model:end-model-value="endDate"
/>
</template>
</DataTable>
</UiCardContent>
</UiCard>
</div>
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('admin.cloudVariables.delete')"
:cancel-button-text="t('common.reset')"
@confirm="handleDelete"
>
<template #title>
{{ t('admin.cloudVariables.deleteVariable') }}
</template>
<template #description>
{{ t('admin.cloudVariables.deleteConfirm', { key: deleteTarget?.key }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDialogOpen"
:destructive="batchAction === 'delete'"
:confirm-button-text="batchAction === 'delete' ? t('admin.cloudVariables.delete') : t('common.confirm')"
:cancel-button-text="t('common.cancel')"
@confirm="handleBatchAction"
>
<template #title>
{{ batchAction === 'delete' ? t('admin.cloudVariables.batchDelete') : batchAction === 'enable' ? t('admin.cloudVariables.batchEnable') : t('admin.cloudVariables.batchDisable') }}
</template>
<template #description>
{{ batchAction === 'delete' ? t('admin.cloudVariables.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('admin.cloudVariables.batchEnableConfirm', { count: selectedRows.length }) : t('admin.cloudVariables.batchDisableConfirm', { count: selectedRows.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
</template>
@@ -144,7 +144,7 @@ onMounted(async () => {
title="变量记录"
description="查看云端变量的记录数据"
:breadcrumbs="[
{ title: '云端变量', href: '/developer/cloud-variables' },
{ title: '云端变量', href: '/admin/cloud-variables' },
{ title: '变量记录' },
]"
>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Download, File, MoreHorizontal, Pencil, Power, PowerOff, Trash2 } from 'lucide-vue-next'
import { h } from 'vue'
import type { CloudVariable } from '@/pages/developer/cloud-variables/data/schema'
import type { CloudVariable } from '@/pages/admin/cloud-variables/data/schema'
import { Copy } from '@/components/sva-ui/copy'
import Badge from '@/components/ui/badge/Badge.vue'
@@ -26,7 +26,7 @@ export function getColumns(actions: {
return [
{
accessorKey: 'key',
header: () => t('developer.cloudVariables.columns.key'),
header: () => t('admin.cloudVariables.columns.key'),
cell: ({ row }) => {
const key = row.getValue('key') as string
if (!key)
@@ -39,7 +39,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'default_value',
header: () => t('developer.cloudVariables.columns.defaultValue'),
header: () => t('admin.cloudVariables.columns.defaultValue'),
cell: ({ row }) => {
const value = row.getValue('default_value') as string
const varType = row.original.var_type
@@ -57,36 +57,36 @@ export function getColumns(actions: {
},
{
accessorKey: 'scope',
header: () => t('developer.cloudVariables.columns.scope'),
header: () => t('admin.cloudVariables.columns.scope'),
cell: ({ row }) => {
const scope = row.getValue('scope') as string
if (scope === 'app') {
return h(Badge, { variant: 'default' }, () => t('developer.cloudVariables.appScope'))
return h(Badge, { variant: 'default' }, () => t('admin.cloudVariables.appScope'))
}
return h(Badge, { variant: 'secondary' }, () => t('developer.cloudVariables.userScope'))
return h(Badge, { variant: 'secondary' }, () => t('admin.cloudVariables.userScope'))
},
},
{
accessorKey: 'write_permission',
header: () => t('developer.cloudVariables.create.writePermission'),
header: () => t('admin.cloudVariables.create.writePermission'),
cell: ({ row }) => {
const permission = row.getValue('write_permission') as string
if (permission === 'user') {
return h(Badge, { variant: 'outline', class: 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300' }, () => t('developer.cloudVariables.create.userWritable'))
return h(Badge, { variant: 'outline', class: 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300' }, () => t('admin.cloudVariables.create.userWritable'))
}
return h(Badge, { variant: 'outline' }, () => t('developer.cloudVariables.create.developerOnly'))
return h(Badge, { variant: 'outline' }, () => t('admin.cloudVariables.create.developerOnly'))
},
},
{
accessorKey: 'var_type',
header: () => t('developer.cloudVariables.columns.type'),
header: () => t('admin.cloudVariables.columns.type'),
cell: ({ row }) => {
const type = row.getValue('var_type') as string
const typeMap: Record<string, string> = {
integer: t('developer.cloudVariables.types.integer'),
decimal: t('developer.cloudVariables.types.decimal'),
string: t('developer.cloudVariables.types.string'),
binary: t('developer.cloudVariables.types.binary'),
integer: t('admin.cloudVariables.types.integer'),
decimal: t('admin.cloudVariables.types.decimal'),
string: t('admin.cloudVariables.types.string'),
binary: t('admin.cloudVariables.types.binary'),
stream: '记录',
}
return h(Badge, { variant: 'outline' }, () => typeMap[type || 'string'] || type || 'string')
@@ -94,7 +94,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'description',
header: () => t('developer.cloudVariables.columns.description'),
header: () => t('admin.cloudVariables.columns.description'),
cell: ({ row }) => {
const description = row.getValue('description') as string
if (!description)
@@ -104,13 +104,13 @@ export function getColumns(actions: {
},
{
accessorKey: 'application_name',
header: () => t('developer.cloudVariables.columns.application'),
header: () => t('admin.cloudVariables.columns.application'),
cell: ({ row }) => {
const appName = row.getValue('application_name') as string
if (!appName) {
const appId = row.original.app_id
if (!appId)
return h(Badge, { variant: 'outline' }, () => t('developer.cloudVariables.notLinked'))
return h(Badge, { variant: 'outline' }, () => t('admin.cloudVariables.notLinked'))
return '-'
}
return h(Badge, { variant: 'outline' }, () => appName)
@@ -118,16 +118,16 @@ export function getColumns(actions: {
},
{
accessorKey: 'status',
header: () => t('developer.cloudVariables.columns.status'),
header: () => t('admin.cloudVariables.columns.status'),
cell: ({ row }) => {
const status = row.getValue('status') as string
const isActive = status === 'active'
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => isActive ? t('developer.cloudVariables.statusActive') : t('developer.cloudVariables.statusInactive'))
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => isActive ? t('admin.cloudVariables.statusActive') : t('admin.cloudVariables.statusInactive'))
},
},
{
accessorKey: 'created_at',
header: () => t('developer.cloudVariables.columns.createdAt'),
header: () => t('admin.cloudVariables.columns.createdAt'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at')
if (!createdAt)
@@ -172,7 +172,7 @@ export function getColumns(actions: {
const items = [
h(DropdownMenuItem, { onClick: () => actions.onEdit(variable) }, () => [
h(Pencil, { class: 'mr-2 h-4 w-4' }),
t('developer.cloudVariables.edit'),
t('admin.cloudVariables.edit'),
]),
]
@@ -188,11 +188,11 @@ export function getColumns(actions: {
items.push(
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(variable) }, () => [
variable.status === 'active' ? h(PowerOff, { class: 'mr-2 h-4 w-4' }) : h(Power, { class: 'mr-2 h-4 w-4' }),
variable.status === 'active' ? t('developer.cloudVariables.disable') : t('developer.cloudVariables.enable'),
variable.status === 'active' ? t('admin.cloudVariables.disable') : t('admin.cloudVariables.enable'),
]),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(variable) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }),
t('developer.cloudVariables.delete'),
t('admin.cloudVariables.delete'),
]),
)
@@ -20,7 +20,7 @@ const router = useRouter()
const variable = props.row.original
function viewRecords() {
router.push(`/developer/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
router.push(`/admin/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
}
</script>
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template>
<div class="flex items-center flex-1 space-x-2">
<Input
:placeholder="t('developer.cloudVariables.searchPlaceholder')"
:placeholder="t('admin.cloudVariables.searchPlaceholder')"
:model-value="(table.getColumn('key')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('key')?.setFilterValue($event.target.value)"
@@ -48,16 +48,16 @@ const table = generateVueTable<CloudVariable>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.cloudVariables.select',
key: 'developer.cloudVariables.columns.key',
default_value: 'developer.cloudVariables.columns.defaultValue',
scope: 'developer.cloudVariables.columns.scope',
write_permission: 'developer.cloudVariables.create.writePermission',
var_type: 'developer.cloudVariables.columns.type',
description: 'developer.cloudVariables.columns.description',
application_name: 'developer.cloudVariables.columns.application',
status: 'developer.cloudVariables.columns.status',
created_at: 'developer.cloudVariables.columns.createdAt',
select: 'admin.cloudVariables.select',
key: 'admin.cloudVariables.columns.key',
default_value: 'admin.cloudVariables.columns.defaultValue',
scope: 'admin.cloudVariables.columns.scope',
write_permission: 'admin.cloudVariables.create.writePermission',
var_type: 'admin.cloudVariables.columns.type',
description: 'admin.cloudVariables.columns.description',
application_name: 'admin.cloudVariables.columns.application',
status: 'admin.cloudVariables.columns.status',
created_at: 'admin.cloudVariables.columns.createdAt',
}
defineExpose({
@@ -80,13 +80,13 @@ function handleBatchAction(action: 'enable' | 'disable' | 'delete') {
</div>
<BulkActions :table="table" entity-name="variables">
<UiButton variant="outline" size="sm" @click="handleBatchAction('enable')">
{{ t('developer.cloudVariables.batchEnableBtn') }}
{{ t('admin.cloudVariables.batchEnableBtn') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="handleBatchAction('disable')">
{{ t('developer.cloudVariables.batchDisableBtn') }}
{{ t('admin.cloudVariables.batchDisableBtn') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="handleBatchAction('delete')">
{{ t('developer.cloudVariables.batchDeleteBtn') }}
{{ t('admin.cloudVariables.batchDeleteBtn') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
@@ -70,11 +70,11 @@ function clearFile() {
async function handleSave() {
if (!form.value.key) {
toast.error(t('developer.cloudVariables.create.keyRequired'))
toast.error(t('admin.cloudVariables.create.keyRequired'))
return
}
if (!form.value.app_id) {
toast.error(t('developer.cloudVariables.create.appRequired'))
toast.error(t('admin.cloudVariables.create.appRequired'))
return
}
@@ -100,12 +100,12 @@ async function handleSave() {
description: form.value.description,
status: form.value.status,
})
toast.success(t('developer.cloudVariables.create.success'))
router.push('/developer/cloud-variables')
toast.success(t('admin.cloudVariables.create.success'))
router.push('/admin/cloud-variables')
}
catch (error: any) {
console.error('创建云端变量失败:', error)
toast.error(error.message || t('developer.cloudVariables.create.failed'))
toast.error(error.message || t('admin.cloudVariables.create.failed'))
}
finally {
saving.value = false
@@ -128,8 +128,8 @@ async function uploadFile() {
formData.append('write_permission', form.value.write_permission)
await api.postFormData('/dev/cloud-variables/upload', formData)
toast.success(t('developer.cloudVariables.create.success'))
router.push('/developer/cloud-variables')
toast.success(t('admin.cloudVariables.create.success'))
router.push('/admin/cloud-variables')
}
catch (error: any) {
console.error('上传文件失败:', error)
@@ -156,11 +156,11 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.cloudVariables.addVariable')"
:description="t('developer.cloudVariables.createDescription')"
:title="t('admin.cloudVariables.addVariable')"
:description="t('admin.cloudVariables.createDescription')"
:breadcrumbs="[
{ title: t('developer.cloudVariables.title'), href: '/developer/cloud-variables' },
{ title: t('developer.cloudVariables.addVariable') },
{ title: t('admin.cloudVariables.title'), href: '/admin/cloud-variables' },
{ title: t('admin.cloudVariables.addVariable') },
]"
sticky
>
@@ -171,18 +171,18 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cloudVariables.create.config') }}
{{ t('admin.cloudVariables.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudVariables.create.configDesc') }}</UiCardDescription>
<UiCardDescription>{{ t('admin.cloudVariables.create.configDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="application">
{{ t('developer.cloudVariables.create.application') }}
{{ t('admin.cloudVariables.create.application') }}
</UiLabel>
<UiSelect v-model="form.app_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudVariables.create.selectApp')" />
<UiSelectValue :placeholder="t('admin.cloudVariables.create.selectApp')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
@@ -194,21 +194,21 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="varType">
{{ t('developer.cloudVariables.create.type') }}
{{ t('admin.cloudVariables.create.type') }}
</UiLabel>
<UiSelect id="varType" v-model="form.var_type">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudVariables.create.selectType')" />
<UiSelectValue :placeholder="t('admin.cloudVariables.create.selectType')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="integer">
{{ t('developer.cloudVariables.types.integer') }}
{{ t('admin.cloudVariables.types.integer') }}
</UiSelectItem>
<UiSelectItem value="decimal">
{{ t('developer.cloudVariables.types.decimal') }}
{{ t('admin.cloudVariables.types.decimal') }}
</UiSelectItem>
<UiSelectItem value="string">
{{ t('developer.cloudVariables.types.string') }}
{{ t('admin.cloudVariables.types.string') }}
</UiSelectItem>
<UiSelectItem value="binary">
二进制
@@ -238,11 +238,11 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="key">
{{ t('developer.cloudVariables.create.key') }}
{{ t('admin.cloudVariables.create.key') }}
</UiLabel>
<UiInput id="key" v-model="form.key" :placeholder="t('developer.cloudVariables.create.keyPlaceholder')" />
<UiInput id="key" v-model="form.key" :placeholder="t('admin.cloudVariables.create.keyPlaceholder')" />
<p class="text-xs text-muted-foreground">
{{ t('developer.cloudVariables.create.keyHint') }}
{{ t('admin.cloudVariables.create.keyHint') }}
</p>
</div>
@@ -285,24 +285,24 @@ onMounted(() => {
<div v-else class="space-y-2">
<UiLabel for="defaultValue">
{{ t('developer.cloudVariables.create.defaultValue') }}
{{ t('admin.cloudVariables.create.defaultValue') }}
</UiLabel>
<UiTextarea id="defaultValue" v-model="form.default_value" :placeholder="t('developer.cloudVariables.create.defaultValuePlaceholder')" rows="3" />
<UiTextarea id="defaultValue" v-model="form.default_value" :placeholder="t('admin.cloudVariables.create.defaultValuePlaceholder')" rows="3" />
</div>
<div class="space-y-2">
<UiLabel for="description">
{{ t('developer.cloudVariables.create.description') }}
{{ t('admin.cloudVariables.create.description') }}
</UiLabel>
<UiInput id="description" v-model="form.description" :placeholder="t('developer.cloudVariables.create.descriptionPlaceholder')" />
<UiInput id="description" v-model="form.description" :placeholder="t('admin.cloudVariables.create.descriptionPlaceholder')" />
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.cloudVariables.create.scopeTitle') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudVariables.create.scopeDesc') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.cloudVariables.create.scopeTitle') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.cloudVariables.create.scopeDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent>
<UiRadioGroup v-model="form.scope" class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -316,11 +316,11 @@ onMounted(() => {
<div class="flex items-center gap-2">
<Icon icon="lucide:globe" class="size-4 text-primary" />
<p class="font-medium">
{{ t('developer.cloudVariables.create.appScope') }}
{{ t('admin.cloudVariables.create.appScope') }}
</p>
</div>
<p class="text-sm text-muted-foreground mt-1">
{{ t('developer.cloudVariables.create.appScopeDesc') }}
{{ t('admin.cloudVariables.create.appScopeDesc') }}
</p>
</div>
</div>
@@ -335,11 +335,11 @@ onMounted(() => {
<div class="flex items-center gap-2">
<Icon icon="lucide:user" class="size-4 text-primary" />
<p class="font-medium">
{{ t('developer.cloudVariables.create.userScope') }}
{{ t('admin.cloudVariables.create.userScope') }}
</p>
</div>
<p class="text-sm text-muted-foreground mt-1">
{{ t('developer.cloudVariables.create.userScopeDesc') }}
{{ t('admin.cloudVariables.create.userScopeDesc') }}
</p>
</div>
</div>
@@ -349,8 +349,8 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.cloudVariables.create.permissionTitle') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudVariables.create.permissionDesc') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.cloudVariables.create.permissionTitle') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.cloudVariables.create.permissionDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent>
<UiRadioGroup v-model="form.write_permission" class="grid grid-cols-1 gap-4">
@@ -364,11 +364,11 @@ onMounted(() => {
<div class="flex items-center gap-2">
<Icon icon="lucide:lock" class="size-4 text-primary" />
<p class="font-medium">
{{ t('developer.cloudVariables.create.developerOnly') }}
{{ t('admin.cloudVariables.create.developerOnly') }}
</p>
</div>
<p class="text-sm text-muted-foreground mt-1">
{{ form.scope === 'app' ? t('developer.cloudVariables.create.developerOnlyAppDesc') : t('developer.cloudVariables.create.developerOnlyDesc') }}
{{ form.scope === 'app' ? t('admin.cloudVariables.create.developerOnlyAppDesc') : t('admin.cloudVariables.create.developerOnlyDesc') }}
</p>
</div>
</div>
@@ -383,11 +383,11 @@ onMounted(() => {
<div class="flex items-center gap-2">
<Icon icon="lucide:unlock" class="size-4 text-primary" />
<p class="font-medium">
{{ t('developer.cloudVariables.create.userWritable') }}
{{ t('admin.cloudVariables.create.userWritable') }}
</p>
</div>
<p class="text-sm text-muted-foreground mt-1">
{{ form.scope === 'app' ? t('developer.cloudVariables.create.userWritableAppDesc') : t('developer.cloudVariables.create.userWritableDesc') }}
{{ form.scope === 'app' ? t('admin.cloudVariables.create.userWritableAppDesc') : t('admin.cloudVariables.create.userWritableDesc') }}
</p>
</div>
</div>
@@ -421,41 +421,41 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cloudVariables.create.preview') }}
{{ t('admin.cloudVariables.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.application') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.application') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.type') }}</span>
<span>{{ t(`developer.cloudVariables.types.${form.var_type}`) }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.type') }}</span>
<span>{{ t(`dmin.cloudVariables.types.${form.var_type}`) }}</span>
</div>
<div v-if="form.var_type === 'stream'" class="flex justify-between text-sm">
<span class="text-muted-foreground">最大记录数</span>
<span>{{ form.max_records || '不限制' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.key') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.key') }}</span>
<span>{{ form.key || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.scope') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.scope') }}</span>
<UiBadge variant="outline">
{{ form.scope === 'app' ? t('developer.cloudVariables.appScope') : t('developer.cloudVariables.userScope') }}
{{ form.scope === 'app' ? t('admin.cloudVariables.appScope') : t('admin.cloudVariables.userScope') }}
</UiBadge>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.writePermission') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.writePermission') }}</span>
<UiBadge variant="secondary">
{{ form.write_permission === 'admin' ? t('developer.cloudVariables.create.developerOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('developer.cloudVariables.create.userWritable') }}
{{ form.write_permission === 'admin' ? t('admin.cloudVariables.create.developerOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('admin.cloudVariables.create.userWritable') }}
</UiBadge>
</div>
<div class="flex justify-between text-sm items-center">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.status') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.status') }}</span>
<UiSwitch
:checked="form.status === 'active'"
@update:checked="form.status = $event ? 'active' : 'inactive'"
@@ -464,7 +464,7 @@ onMounted(() => {
<div class="border-t pt-3 mt-3">
<div class="text-sm">
<div class="text-muted-foreground mb-2">
{{ t('developer.cloudVariables.create.defaultValue') }}
{{ t('admin.cloudVariables.create.defaultValue') }}
</div>
<div class="bg-muted p-2 rounded max-h-[100px] overflow-y-auto text-xs">
{{ form.default_value || '-' }}
@@ -486,14 +486,14 @@ onMounted(() => {
>
<Icon v-if="saving || uploading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
{{ uploading ? '上传中...' : t('developer.cloudVariables.create.submit') }}
{{ uploading ? '上传中...' : t('admin.cloudVariables.create.submit') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.cloudVariables.create.cancel') }}
{{ t('admin.cloudVariables.create.cancel') }}
</UiButton>
</div>
</UiCardContent>
@@ -78,8 +78,8 @@ const applicationOptions = computed(() => {
})
const scopeOptions = computed(() => [
{ label: t('developer.cloudVariables.appScope'), value: 'app' },
{ label: t('developer.cloudVariables.userScope'), value: 'user' },
{ label: t('admin.cloudVariables.appScope'), value: 'app' },
{ label: t('admin.cloudVariables.userScope'), value: 'user' },
])
async function fetchApplications() {
@@ -112,11 +112,11 @@ async function fetchVariables() {
}
function openCreatePage() {
router.push('/developer/cloud-variables/create')
router.push('/admin/cloud-variables/create')
}
function openEditPage(variable: CloudVariable) {
router.push(`/developer/cloud-variables/${variable.id}`)
router.push(`/admin/cloud-variables/${variable.id}`)
}
function confirmDelete(variable: CloudVariable) {
@@ -130,12 +130,12 @@ async function handleDelete() {
try {
await api.delete(`/dev/cloud-variables/${deleteTarget.value.id}`)
toast.success(t('developer.cloudVariables.deleteSuccess'))
toast.success(t('admin.cloudVariables.deleteSuccess'))
fetchVariables()
}
catch (error: any) {
console.error('删除云端变量失败:', error)
toast.error(error.message || t('developer.cloudVariables.deleteFailed'))
toast.error(error.message || t('admin.cloudVariables.deleteFailed'))
}
finally {
deleteTarget.value = null
@@ -154,12 +154,12 @@ async function handleToggleStatus(variable: CloudVariable) {
description: variable.description,
status: newStatus,
})
toast.success(newStatus === 'active' ? t('developer.cloudVariables.enableSuccess') : t('developer.cloudVariables.disableSuccess'))
toast.success(newStatus === 'active' ? t('admin.cloudVariables.enableSuccess') : t('admin.cloudVariables.disableSuccess'))
fetchVariables()
}
catch (error: any) {
console.error('更新状态失败:', error)
toast.error(error.message || t('developer.cloudVariables.updateFailed'))
toast.error(error.message || t('admin.cloudVariables.updateFailed'))
}
}
@@ -172,7 +172,7 @@ function handleDownload(variable: CloudVariable) {
}
function handleViewRecords(variable: CloudVariable) {
router.push(`/developer/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
router.push(`/admin/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
}
function openBatchDialog(action: 'enable' | 'disable' | 'delete', rows: CloudVariable[]) {
@@ -189,7 +189,7 @@ async function handleBatchAction() {
try {
if (batchAction.value === 'delete') {
await Promise.all(ids.map(id => api.delete(`/dev/cloud-variables/${id}`)))
toast.success(t('developer.cloudVariables.batchDeleteSuccess'))
toast.success(t('admin.cloudVariables.batchDeleteSuccess'))
}
else {
const status = batchAction.value === 'enable' ? 'active' : 'inactive'
@@ -202,13 +202,13 @@ async function handleBatchAction() {
description: v.description,
status,
})))
toast.success(batchAction.value === 'enable' ? t('developer.cloudVariables.batchEnableSuccess') : t('developer.cloudVariables.batchDisableSuccess'))
toast.success(batchAction.value === 'enable' ? t('admin.cloudVariables.batchEnableSuccess') : t('admin.cloudVariables.batchDisableSuccess'))
}
fetchVariables()
}
catch (error: any) {
console.error('批量操作失败:', error)
toast.error(error.message || t('developer.cloudVariables.batchFailed'))
toast.error(error.message || t('admin.cloudVariables.batchFailed'))
}
finally {
batchAction.value = null
@@ -224,14 +224,14 @@ onMounted(async () => {
<template>
<BasicPage
:title="t('developer.cloudVariables.title')"
:description="t('developer.cloudVariables.description')"
:title="t('admin.cloudVariables.title')"
:description="t('admin.cloudVariables.description')"
sticky
>
<template #actions>
<UiButton size="sm" @click="openCreatePage">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cloudVariables.addVariable') }}
{{ t('admin.cloudVariables.addVariable') }}
</UiButton>
</template>
@@ -240,7 +240,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudVariables.totalVariables') }}
{{ t('admin.cloudVariables.totalVariables') }}
</UiCardTitle>
<Icon icon="lucide:variable" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -254,7 +254,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudVariables.appScopeVariables') }}
{{ t('admin.cloudVariables.appScopeVariables') }}
</UiCardTitle>
<Icon icon="lucide:globe" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -268,7 +268,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudVariables.userScopeVariables') }}
{{ t('admin.cloudVariables.userScopeVariables') }}
</UiCardTitle>
<Icon icon="lucide:user" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -282,7 +282,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudVariables.appCount') }}
{{ t('admin.cloudVariables.appCount') }}
</UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -310,12 +310,12 @@ onMounted(async () => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.cloudVariables.application')"
:title="t('admin.cloudVariables.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="scopeFilter"
:title="t('developer.cloudVariables.scope')"
:title="t('admin.cloudVariables.scope')"
:options="scopeOptions"
/>
<DateRangeFilter
@@ -331,30 +331,30 @@ onMounted(async () => {
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('developer.cloudVariables.delete')"
:confirm-button-text="t('admin.cloudVariables.delete')"
:cancel-button-text="t('common.reset')"
@confirm="handleDelete"
>
<template #title>
{{ t('developer.cloudVariables.deleteVariable') }}
{{ t('admin.cloudVariables.deleteVariable') }}
</template>
<template #description>
{{ t('developer.cloudVariables.deleteConfirm', { key: deleteTarget?.key }) }}
{{ t('admin.cloudVariables.deleteConfirm', { key: deleteTarget?.key }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDialogOpen"
:destructive="batchAction === 'delete'"
:confirm-button-text="batchAction === 'delete' ? t('developer.cloudVariables.delete') : t('common.confirm')"
:confirm-button-text="batchAction === 'delete' ? t('admin.cloudVariables.delete') : t('common.confirm')"
:cancel-button-text="t('common.cancel')"
@confirm="handleBatchAction"
>
<template #title>
{{ batchAction === 'delete' ? t('developer.cloudVariables.batchDelete') : batchAction === 'enable' ? t('developer.cloudVariables.batchEnable') : t('developer.cloudVariables.batchDisable') }}
{{ batchAction === 'delete' ? t('admin.cloudVariables.batchDelete') : batchAction === 'enable' ? t('admin.cloudVariables.batchEnable') : t('admin.cloudVariables.batchDisable') }}
</template>
<template #description>
{{ batchAction === 'delete' ? t('developer.cloudVariables.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('developer.cloudVariables.batchEnableConfirm', { count: selectedRows.length }) : t('developer.cloudVariables.batchDisableConfirm', { count: selectedRows.length }) }}
{{ batchAction === 'delete' ? t('admin.cloudVariables.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('admin.cloudVariables.batchEnableConfirm', { count: selectedRows.length }) : t('admin.cloudVariables.batchDisableConfirm', { count: selectedRows.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
@@ -4,7 +4,7 @@ import { Ban, Eye, LogOut, MoreHorizontal, Trash2 } from 'lucide-vue-next'
import { h } from 'vue'
import { useRouter } from 'vue-router'
import type { Device } from '@/pages/developer/devices/data/schema'
import type { Device } from '@/pages/admin/devices/data/schema'
import Badge from '@/components/ui/badge/Badge.vue'
import Button from '@/components/ui/button/Button.vue'
@@ -146,7 +146,7 @@ export function getColumns(actions: {
{ align: 'end' },
() => [
hasOnlineSessions
? h(DropdownMenuItem, { onClick: () => router.push(`/developer/sessions?device_fingerprint=${device.device_id}`) }, () => [
? h(DropdownMenuItem, { onClick: () => router.push(`/admin/sessions?device_fingerprint=${device.device_id}`) }, () => [
h(Eye, { class: 'mr-2 h-4 w-4' }),
'查看实例',
])
@@ -30,7 +30,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
<template>
<div class="flex flex-1 items-center space-x-2">
<Input
:placeholder="t('developer.devices.searchPlaceholder')"
:placeholder="t('admin.devices.searchPlaceholder')"
:model-value="(table.getColumn('device_id')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]"
@update:model-value="table.getColumn('device_id')?.setFilterValue($event)"
@@ -41,7 +41,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
class="h-8 px-2 lg:px-3"
@click="table.resetColumnFilters()"
>
{{ t('developer.devices.reset') }}
{{ t('admin.devices.reset') }}
<Icon icon="lucide:x" class="ml-2 h-4 w-4" />
</Button>
</div>
@@ -59,7 +59,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
<template v-if="selectedCount > 0">
<span class="text-sm text-muted-foreground">
{{ t('developer.devices.selected', { count: selectedCount }) }}
{{ t('admin.devices.selected', { count: selectedCount }) }}
</span>
<Button
variant="outline"
@@ -68,7 +68,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
@click="emit('batchUnban')"
>
<Icon icon="lucide:check-circle" class="mr-2 h-4 w-4" />
{{ t('developer.devices.batchUnban') }}
{{ t('admin.devices.batchUnban') }}
</Button>
<Button
variant="outline"
@@ -77,7 +77,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
@click="emit('batchBan')"
>
<Icon icon="lucide:ban" class="mr-2 h-4 w-4" />
{{ t('developer.devices.batchBan') }}
{{ t('admin.devices.batchBan') }}
</Button>
<Button
variant="destructive"
@@ -86,7 +86,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
@click="emit('batchUnbind')"
>
<Icon icon="lucide:trash-2" class="mr-2 h-4 w-4" />
{{ t('developer.devices.batchUnbind') }}
{{ t('admin.devices.batchUnbind') }}
</Button>
</template>
</div>
@@ -46,16 +46,16 @@ const table = generateVueTable<Device>({
}, columns.value)
const columnLabels: Record<string, string> = {
'select': 'developer.devices.select',
'device_id': 'developer.devices.columns.deviceId',
'device_name': 'developer.devices.columns.deviceName',
'device_type': 'developer.devices.columns.deviceType',
'user.username': 'developer.devices.columns.user',
'application.name': 'developer.devices.columns.application',
'online_status': 'developer.devices.columns.status',
'online_sessions': 'developer.devices.columns.onlineSessions',
'status': 'developer.devices.columns.deviceStatus',
'created_at': 'developer.devices.columns.createdAt',
'select': 'admin.devices.select',
'device_id': 'admin.devices.columns.deviceId',
'device_name': 'admin.devices.columns.deviceName',
'device_type': 'admin.devices.columns.deviceType',
'user.username': 'admin.devices.columns.user',
'application.name': 'admin.devices.columns.application',
'online_status': 'admin.devices.columns.status',
'online_sessions': 'admin.devices.columns.onlineSessions',
'status': 'admin.devices.columns.deviceStatus',
'created_at': 'admin.devices.columns.createdAt',
}
defineExpose({
@@ -69,13 +69,13 @@ defineExpose({
<div class="space-y-3">
<BulkActions :table="table" entity-name="devices">
<UiButton variant="outline" size="sm" @click="emit('batchUnban')">
{{ t('developer.devices.batchUnban') }}
{{ t('admin.devices.batchUnban') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchBan')">
{{ t('developer.devices.batchBan') }}
{{ t('admin.devices.batchBan') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchUnbind')">
{{ t('developer.devices.batchUnbind') }}
{{ t('admin.devices.batchUnbind') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
+27 -27
View File
@@ -5,12 +5,12 @@ import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { toast } from 'vue-sonner'
import type { Device } from '@/pages/developer/devices/data/schema'
import type { Device } from '@/pages/admin/devices/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import DataTable from '@/pages/developer/devices/components/data-table.vue'
import DataTable from '@/pages/admin/devices/components/data-table.vue'
import api from '@/services/api'
const { t } = useI18n()
@@ -80,8 +80,8 @@ const userOptions = computed(() => {
})
const statusOptions = computed(() => [
{ label: t('developer.devices.normal'), value: 'active' },
{ label: t('developer.devices.banned'), value: 'banned' },
{ label: t('admin.devices.normal'), value: 'active' },
{ label: t('admin.devices.banned'), value: 'banned' },
])
async function fetchApplications() {
@@ -138,12 +138,12 @@ async function toggleDeviceStatus(device: Device) {
const newStatus = device.status === 'banned' ? 'active' : 'banned'
try {
await api.put(`/dev/devices/${device.id}/status`, { status: newStatus })
toast.success(t('developer.devices.statusUpdateSuccess'))
toast.success(t('admin.devices.statusUpdateSuccess'))
fetchDevices()
}
catch (error: any) {
console.error('切换设备状态失败:', error)
toast.error(error.message || t('developer.devices.statusUpdateFailed'))
toast.error(error.message || t('admin.devices.statusUpdateFailed'))
}
}
@@ -158,12 +158,12 @@ async function handleDeleteDevice() {
try {
await api.delete(`/dev/devices/${deleteTarget.value.id}`)
toast.success(t('developer.devices.unbindSuccess'))
toast.success(t('admin.devices.unbindSuccess'))
fetchDevices()
}
catch (error: any) {
console.error('解绑设备失败:', error)
toast.error(error.message || t('developer.devices.unbindFailed'))
toast.error(error.message || t('admin.devices.unbindFailed'))
}
finally {
deleteTarget.value = null
@@ -178,12 +178,12 @@ function confirmBatchDelete(ids: (string | number)[]) {
async function handleBatchDelete() {
try {
await api.delete('/dev/devices/batch', { device_ids: batchDeleteIds.value.map(id => Number(id)) } as any)
toast.success(t('developer.devices.batchUnbindSuccess'))
toast.success(t('admin.devices.batchUnbindSuccess'))
fetchDevices()
}
catch (error: any) {
console.error('批量解绑失败:', error)
toast.error(error.message || t('developer.devices.batchUnbindFailed'))
toast.error(error.message || t('admin.devices.batchUnbindFailed'))
}
finally {
batchDeleteIds.value = []
@@ -193,12 +193,12 @@ async function handleBatchDelete() {
async function batchToggleStatus(ids: (string | number)[], status: string) {
try {
await api.post('/dev/devices/batch/status', { device_ids: ids.map(id => Number(id)), status })
toast.success(t('developer.devices.batchUpdateSuccess'))
toast.success(t('admin.devices.batchUpdateSuccess'))
fetchDevices()
}
catch (error: any) {
console.error('批量更新状态失败:', error)
toast.error(error.message || t('developer.devices.batchUpdateFailed'))
toast.error(error.message || t('admin.devices.batchUpdateFailed'))
}
}
@@ -239,8 +239,8 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.devices.title')"
:description="t('developer.devices.description')"
:title="t('admin.devices.title')"
:description="t('admin.devices.description')"
sticky
>
<div class="space-y-6">
@@ -248,7 +248,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.devices.totalDevices') }}
{{ t('admin.devices.totalDevices') }}
</UiCardTitle>
<Icon icon="lucide:monitor" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -262,7 +262,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.devices.onlineSessions') }}
{{ t('admin.devices.onlineSessions') }}
</UiCardTitle>
<Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -276,7 +276,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.devices.normalDevices') }}
{{ t('admin.devices.normalDevices') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -290,7 +290,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.devices.banned') }}
{{ t('admin.devices.banned') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -319,17 +319,17 @@ onMounted(() => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.devices.application')"
:title="t('admin.devices.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="userIdFilter"
:title="t('developer.devices.user')"
:title="t('admin.devices.user')"
:options="userOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('developer.devices.status')"
:title="t('admin.devices.status')"
:options="statusOptions"
/>
</template>
@@ -341,30 +341,30 @@ onMounted(() => {
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('developer.devices.unbind')"
:confirm-button-text="t('admin.devices.unbind')"
:cancel-button-text="t('common.cancel')"
@confirm="handleDeleteDevice"
>
<template #title>
{{ t('developer.devices.unbindDevice') }}
{{ t('admin.devices.unbindDevice') }}
</template>
<template #description>
{{ t('developer.devices.unbindConfirm', { deviceId: deleteTarget?.device_id }) }}
{{ t('admin.devices.unbindConfirm', { deviceId: deleteTarget?.device_id }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
:confirm-button-text="t('developer.devices.unbind')"
:confirm-button-text="t('admin.devices.unbind')"
:cancel-button-text="t('common.cancel')"
@confirm="handleBatchDelete"
>
<template #title>
{{ t('developer.devices.batchUnbind') }}
{{ t('admin.devices.batchUnbind') }}
</template>
<template #description>
{{ t('developer.devices.batchUnbindConfirm', { count: batchDeleteIds.length }) }}
{{ t('admin.devices.batchUnbindConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
+282 -231
View File
@@ -2,49 +2,106 @@
import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { DynamicCode } from '@/pages/developer/dynamic-code/data/schema'
import type { DynamicCode } from '@/pages/admin/dynamic-code/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import DataTable from '@/pages/admin/dynamic-code/components/data-table.vue'
import api from '@/services/api'
const { t } = useI18n()
interface Application {
id: number
name: string
}
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const saving = ref(false)
const loading = ref(true)
const codes = ref<DynamicCode[]>([])
const tableRef = ref()
const applications = ref<Application[]>([])
const codeId = ref<number>(0)
const form = ref({
name: '',
application_id: '',
description: '',
code: '',
enabled: true,
const appFilter = ref<string>('')
const statusFilter = ref<string>('')
const createdStartDate = ref<string>('')
const createdEndDate = ref<string>('')
const updatedStartDate = ref<string>('')
const updatedEndDate = ref<string>('')
const deleteDialogOpen = ref(false)
const deleteTarget = ref<DynamicCode | null>(null)
const batchDeleteDialogOpen = ref(false)
const batchDeleteIds = ref<(string | number)[]>([])
const filteredCodes = computed(() => {
let result = codes.value
if (appFilter.value) {
result = result.filter(code => String(code.application_id) === appFilter.value)
}
if (statusFilter.value) {
if (statusFilter.value === 'enabled') {
result = result.filter(code => code.enabled === true)
}
else if (statusFilter.value === 'disabled') {
result = result.filter(code => code.enabled === false)
}
}
if (createdStartDate.value) {
const fromDateTime = createdStartDate.value.includes('T')
? createdStartDate.value.replace('T', ' ')
: `${createdStartDate.value} 00:00`
result = result.filter(code => code.created_at >= fromDateTime)
}
if (createdEndDate.value) {
const toDateTime = createdEndDate.value.includes('T')
? createdEndDate.value.replace('T', ' ')
: `${createdEndDate.value} 23:59`
result = result.filter(code => code.created_at <= toDateTime)
}
if (updatedStartDate.value) {
const fromDateTime = updatedStartDate.value.includes('T')
? updatedStartDate.value.replace('T', ' ')
: `${updatedStartDate.value} 00:00`
result = result.filter(code => code.updated_at >= fromDateTime)
}
if (updatedEndDate.value) {
const toDateTime = updatedEndDate.value.includes('T')
? updatedEndDate.value.replace('T', ' ')
: `${updatedEndDate.value} 23:59`
result = result.filter(code => code.updated_at <= toDateTime)
}
return result
})
const filteredApplications = computed(() => {
const enabledCount = computed(() => filteredCodes.value.filter(code => code.enabled === true).length)
const disabledCount = computed(() => filteredCodes.value.filter(code => code.enabled === false).length)
const linkedAppsCount = computed(() => new Set(filteredCodes.value.map(code => code.application_id)).size)
const applicationOptions = computed(() => {
return applications.value.map(app => ({
label: app.name,
value: String(app.id),
}))
})
const selectedApplication = computed(() => {
if (form.value.application_id) {
return applications.value.find(app => String(app.id) === form.value.application_id)
}
return null
})
const statusOptions = computed(() => [
{ label: t('admin.cloudFunction.enabled'), value: 'enabled' },
{ label: t('admin.cloudFunction.disabled'), value: 'disabled' },
])
async function fetchApplications() {
try {
@@ -56,250 +113,244 @@ async function fetchApplications() {
}
}
async function fetchCode() {
async function fetchCodes() {
loading.value = true
try {
const data = await api.get<DynamicCode>(`/dev/dynamic-codes/${codeId.value}`)
if (data) {
form.value.name = data.name
form.value.application_id = String(data.application_id)
form.value.description = data.description || ''
form.value.code = data.code
form.value.enabled = data.enabled
}
const data = await api.get<DynamicCode[]>('/dev/dynamic-codes')
codes.value = Array.isArray(data) ? data : []
}
catch (error) {
console.error('获取云端函数失败:', error)
toast.error(t('developer.cloudFunction.fetchFailed'))
router.push('/developer/cloud-function')
console.error('获取云端函数列表失败:', error)
codes.value = []
}
finally {
loading.value = false
}
}
async function handleSubmit() {
if (!form.value.name.trim()) {
toast.error(t('developer.cloudFunction.create.nameRequired'))
return
}
if (!form.value.application_id) {
toast.error(t('developer.cloudFunction.create.selectAppRequired'))
return
}
if (!form.value.code.trim()) {
toast.error(t('developer.cloudFunction.create.codeRequired'))
return
}
function goToCreate() {
router.push('/admin/cloud-function/create')
}
saving.value = true
async function toggleStatus(code: DynamicCode) {
try {
await api.put(`/dev/dynamic-codes/${codeId.value}`, {
name: form.value.name,
application_id: Number.parseInt(form.value.application_id),
description: form.value.description,
code: form.value.code,
enabled: form.value.enabled,
})
toast.success(t('developer.cloudFunction.updateSuccess'))
router.push('/developer/cloud-function')
const newStatus = !code.enabled
await api.put(`/dev/dynamic-codes/${code.id}/status`, { enabled: newStatus })
toast.success(t('admin.cloudFunction.statusUpdateSuccess'))
fetchCodes()
}
catch (error: any) {
console.error('更新云端函数失败:', error)
toast.error(error.message || t('developer.cloudFunction.updateFailed'))
}
finally {
saving.value = false
console.error('切换云端函数状态失败:', error)
toast.error(error.message || t('admin.cloudFunction.statusUpdateFailed'))
}
}
function handleCancel() {
router.push('/developer/cloud-function')
function editCode(code: DynamicCode) {
router.push(`/admin/cloud-function/${code.id}`)
}
function confirmDeleteCode(code: DynamicCode) {
deleteTarget.value = code
deleteDialogOpen.value = true
}
async function handleDeleteCode() {
if (!deleteTarget.value)
return
try {
await api.delete(`/dev/dynamic-codes/${deleteTarget.value.id}`)
toast.success(t('admin.cloudFunction.deleteSuccess'))
fetchCodes()
}
catch (error: any) {
console.error('删除云端函数失败:', error)
toast.error(error.message || t('admin.cloudFunction.deleteFailed'))
}
finally {
deleteTarget.value = null
}
}
function confirmBatchDelete(ids: (string | number)[]) {
batchDeleteIds.value = ids
batchDeleteDialogOpen.value = true
}
async function handleBatchDelete() {
try {
await api.delete('/dev/dynamic-codes/batch', { ids: batchDeleteIds.value } as any)
toast.success(t('admin.cloudFunction.deleteSuccess'))
fetchCodes()
}
catch (error: any) {
console.error('批量删除失败:', error)
toast.error(error.message || t('admin.cloudFunction.deleteFailed'))
}
finally {
batchDeleteIds.value = []
}
}
async function batchToggleStatus(ids: (string | number)[], enabled: boolean) {
try {
await api.put('/dev/dynamic-codes/batch/status', { ids, enabled })
toast.success(t('admin.cloudFunction.batchUpdateSuccess'))
fetchCodes()
}
catch (error: any) {
console.error('批量更新状态失败:', error)
toast.error(error.message || t('admin.cloudFunction.batchUpdateFailed'))
}
}
onMounted(() => {
const id = route.params.id
if (id && typeof id === 'string') {
codeId.value = Number.parseInt(id)
fetchApplications()
fetchCode()
}
else {
toast.error('无效的云端函数ID')
router.push('/developer/cloud-function')
}
fetchApplications()
fetchCodes()
})
</script>
<template>
<BasicPage
:title="t('developer.cloudFunction.edit.title')"
:description="t('developer.cloudFunction.edit.description')"
:breadcrumbs="[
{ title: t('developer.cloudFunction.title'), href: '/developer/cloud-function' },
{ title: t('developer.cloudFunction.edit.title') },
]"
:title="t('admin.cloudFunction.title')"
:description="t('admin.cloudFunction.description')"
sticky
>
<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" />
</div>
<template #actions>
<UiButton size="sm" @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('admin.cloudFunction.createBtn') }}
</UiButton>
</template>
<div v-else class="space-y-6">
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cloudFunction.edit.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudFunction.edit.configDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="application">
{{ t('developer.cloudFunction.create.application') }}
</UiLabel>
<UiSelect v-model="form.application_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudFunction.create.selectAppPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
v-for="app in filteredApplications"
:key="app.value"
:value="app.value"
>
{{ app.label }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<div class="space-y-6">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudFunction.totalCodes') }}
</UiCardTitle>
<Icon icon="lucide:code" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ filteredCodes.length }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="name">
{{ t('developer.cloudFunction.create.name') }}
</UiLabel>
<UiInput
id="name"
v-model="form.name"
:placeholder="t('developer.cloudFunction.create.namePlaceholder')"
/>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudFunction.enabled') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ enabledCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="description">
{{ t('developer.cloudFunction.create.description') }}
</UiLabel>
<UiTextarea
id="description"
v-model="form.description"
:placeholder="t('developer.cloudFunction.create.descriptionPlaceholder')"
rows="3"
/>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudFunction.disabled') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ disabledCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="code">
{{ t('developer.cloudFunction.create.code') }}
</UiLabel>
<UiTextarea
id="code"
v-model="form.code"
:placeholder="t('developer.cloudFunction.create.codePlaceholder')"
rows="12"
class="font-mono text-sm"
/>
<p class="text-xs text-muted-foreground">
{{ t('developer.cloudFunction.create.codeHelp') }}
</p>
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.cloudFunction.create.status') }}</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<div class="flex items-center justify-between">
<div>
<p class="font-medium">
{{ t('developer.cloudFunction.create.enabled') }}
</p>
</div>
<UiSwitch v-model="form.enabled" />
</div>
</UiCardContent>
</UiCard>
</div>
<div class="space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cloudFunction.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.name') }}</span>
<span>{{ form.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.application') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.description') }}</span>
<span class="text-right max-w-[150px] truncate">{{ form.description || '-' }}</span>
</div>
<div class="flex justify-between text-sm items-center">
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.status') }}</span>
<UiSwitch
v-model="form.enabled"
/>
</div>
<div class="border-t pt-3 mt-3">
<div class="text-sm">
<div class="text-muted-foreground mb-2">
{{ t('developer.cloudFunction.create.code') }}
</div>
<div class="font-mono text-xs bg-muted p-2 rounded max-h-[150px] overflow-auto" style="white-space: pre-wrap;">{{ form.code || '-' }}</div>
</div>
</div>
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardContent class="pt-6">
<div class="flex flex-col gap-3">
<UiButton
class="w-full"
size="lg"
:disabled="saving || !form.name || !form.application_id || !form.code"
@click="handleSubmit"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
{{ t('developer.cloudFunction.edit.submitBtn') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="handleCancel"
>
{{ t('developer.cloudFunction.edit.cancelBtn') }}
</UiButton>
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudFunction.linkedApps') }}
</UiCardTitle>
<Icon icon="lucide:link" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ linkedAppsCount }}
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardContent class="p-6">
<DataTable
ref="tableRef"
:loading
:data="filteredCodes"
:on-toggle-status="toggleStatus"
:on-edit="editCode"
:on-delete="confirmDeleteCode"
@refresh="fetchCodes"
@batch-enable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [], true)"
@batch-disable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [], false)"
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
>
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('admin.cloudFunction.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('admin.cloudFunction.status')"
:options="statusOptions"
/>
<DateRangeFilter
v-model:start-model-value="createdStartDate"
v-model:end-model-value="createdEndDate"
:title="t('admin.cloudFunction.columns.createdAt')"
/>
<DateRangeFilter
v-model:start-model-value="updatedStartDate"
v-model:end-model-value="updatedEndDate"
:title="t('admin.cloudFunction.columns.updatedAt')"
/>
</template>
</DataTable>
</UiCardContent>
</UiCard>
</div>
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('admin.cloudFunction.delete')"
:cancel-button-text="t('admin.cloudFunction.create.cancelBtn')"
@confirm="handleDeleteCode"
>
<template #title>
{{ t('admin.cloudFunction.deleteCode') }}
</template>
<template #description>
{{ t('admin.cloudFunction.deleteCodeConfirm', { codeName: deleteTarget?.name }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
:confirm-button-text="t('admin.cloudFunction.delete')"
:cancel-button-text="t('admin.cloudFunction.create.cancelBtn')"
@confirm="handleBatchDelete"
>
<template #title>
{{ t('admin.cloudFunction.batchDelete') }}
</template>
<template #description>
{{ t('admin.cloudFunction.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
</template>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Ban, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next'
import { h } from 'vue'
import type { DynamicCode } from '@/pages/developer/dynamic-code/data/schema'
import type { DynamicCode } from '@/pages/admin/dynamic-code/data/schema'
import Badge from '@/components/ui/badge/Badge.vue'
import Button from '@/components/ui/button/Button.vue'
@@ -24,7 +24,7 @@ export function getColumns(actions: {
return [
{
accessorKey: 'name',
header: () => t('developer.cloudFunction.columns.name'),
header: () => t('admin.cloudFunction.columns.name'),
cell: ({ row }) => {
const name = row.getValue('name') as string
return name || '-'
@@ -32,7 +32,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'application_name',
header: () => t('developer.cloudFunction.columns.application'),
header: () => t('admin.cloudFunction.columns.application'),
cell: ({ row }) => {
const appName = row.getValue('application_name') as string
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -40,7 +40,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'description',
header: () => t('developer.cloudFunction.columns.description'),
header: () => t('admin.cloudFunction.columns.description'),
cell: ({ row }) => {
const description = row.getValue('description') as string
return description || '-'
@@ -48,15 +48,15 @@ export function getColumns(actions: {
},
{
accessorKey: 'enabled',
header: () => t('developer.cloudFunction.columns.status'),
header: () => t('admin.cloudFunction.columns.status'),
cell: ({ row }) => {
const enabled = row.getValue('enabled') as boolean
return h(Badge, { variant: enabled ? 'default' : 'destructive' }, () => enabled ? t('developer.cloudFunction.enabled') : t('developer.cloudFunction.disabled'))
return h(Badge, { variant: enabled ? 'default' : 'destructive' }, () => enabled ? t('admin.cloudFunction.enabled') : t('admin.cloudFunction.disabled'))
},
},
{
accessorKey: 'creator.username',
header: () => t('developer.cloudFunction.columns.creator'),
header: () => t('admin.cloudFunction.columns.creator'),
cell: ({ row }) => {
const username = row.original.creator?.username
return username || '-'
@@ -64,7 +64,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'created_at',
header: () => t('developer.cloudFunction.columns.createdAt'),
header: () => t('admin.cloudFunction.columns.createdAt'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at')
if (!createdAt)
@@ -88,7 +88,7 @@ export function getColumns(actions: {
},
{
accessorKey: 'updated_at',
header: () => t('developer.cloudFunction.columns.updatedAt'),
header: () => t('admin.cloudFunction.columns.updatedAt'),
cell: ({ row }) => {
const updatedAt = row.getValue('updated_at')
if (!updatedAt)
@@ -133,16 +133,16 @@ export function getColumns(actions: {
() => [
h(DropdownMenuItem, { onClick: () => actions.onEdit(code) }, () => [
h(Pencil, { class: 'mr-2 h-4 w-4' }),
t('developer.cloudFunction.edit.title'),
t('admin.cloudFunction.edit.title'),
]),
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(code) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }),
isDisabled ? t('developer.cloudFunction.enable') : t('developer.cloudFunction.disable'),
isDisabled ? t('admin.cloudFunction.enable') : t('admin.cloudFunction.disable'),
]),
h(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(code) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }),
t('developer.cloudFunction.delete'),
t('admin.cloudFunction.delete'),
]),
],
),
@@ -5,7 +5,7 @@ import { X } from 'lucide-vue-next'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { DynamicCode } from '@/pages/developer/dynamic-code/data/schema'
import type { DynamicCode } from '@/pages/admin/dynamic-code/data/schema'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template>
<div class="flex items-center flex-1 space-x-2">
<Input
:placeholder="t('developer.cloudFunction.searchCode')"
:placeholder="t('admin.cloudFunction.searchCode')"
:model-value="(table.getColumn('name')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('name')?.setFilterValue($event.target.value)"
@@ -36,7 +36,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
class="h-8 px-2 lg:px-3"
@click="table.resetColumnFilters()"
>
{{ t('developer.cloudFunction.reset') }}
{{ t('admin.cloudFunction.reset') }}
<X class="size-4 ml-2" />
</Button>
</div>
@@ -45,14 +45,14 @@ const table = generateVueTable<DynamicCode>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.cloudFunction.select',
name: 'developer.cloudFunction.columns.name',
application_name: 'developer.cloudFunction.columns.application',
description: 'developer.cloudFunction.columns.description',
enabled: 'developer.cloudFunction.columns.status',
creator_username: 'developer.cloudFunction.columns.creator',
created_at: 'developer.cloudFunction.columns.createdAt',
updated_at: 'developer.cloudFunction.columns.updatedAt',
select: 'admin.cloudFunction.select',
name: 'admin.cloudFunction.columns.name',
application_name: 'admin.cloudFunction.columns.application',
description: 'admin.cloudFunction.columns.description',
enabled: 'admin.cloudFunction.columns.status',
creator_username: 'admin.cloudFunction.columns.creator',
created_at: 'admin.cloudFunction.columns.createdAt',
updated_at: 'admin.cloudFunction.columns.updatedAt',
}
defineExpose({
@@ -69,13 +69,13 @@ defineExpose({
</div>
<BulkActions :table="table" entity-name="functions">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
{{ t('developer.cloudFunction.batchEnableBtn') }}
{{ t('admin.cloudFunction.batchEnableBtn') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
{{ t('developer.cloudFunction.batchDisableBtn') }}
{{ t('admin.cloudFunction.batchDisableBtn') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.cloudFunction.batchDeleteBtn') }}
{{ t('admin.cloudFunction.batchDeleteBtn') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
@@ -53,15 +53,15 @@ async function fetchApplications() {
async function handleSubmit() {
if (!form.value.name.trim()) {
toast.error(t('developer.cloudFunction.create.nameRequired'))
toast.error(t('admin.cloudFunction.create.nameRequired'))
return
}
if (!form.value.application_id) {
toast.error(t('developer.cloudFunction.create.selectAppRequired'))
toast.error(t('admin.cloudFunction.create.selectAppRequired'))
return
}
if (!form.value.code.trim()) {
toast.error(t('developer.cloudFunction.create.codeRequired'))
toast.error(t('admin.cloudFunction.create.codeRequired'))
return
}
@@ -74,12 +74,12 @@ async function handleSubmit() {
code: form.value.code,
enabled: form.value.enabled,
})
toast.success(t('developer.cloudFunction.create.submitSuccess'))
router.push('/developer/cloud-function')
toast.success(t('admin.cloudFunction.create.submitSuccess'))
router.push('/admin/cloud-function')
}
catch (error: any) {
console.error('创建云端函数失败:', error)
toast.error(error.message || t('developer.cloudFunction.create.submitFailed'))
toast.error(error.message || t('admin.cloudFunction.create.submitFailed'))
}
finally {
saving.value = false
@@ -87,7 +87,7 @@ async function handleSubmit() {
}
function handleCancel() {
router.push('/developer/cloud-function')
router.push('/admin/cloud-function')
}
onMounted(() => {
@@ -97,11 +97,11 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.cloudFunction.create.title')"
:description="t('developer.cloudFunction.create.description')"
:title="t('admin.cloudFunction.create.title')"
:description="t('admin.cloudFunction.create.description')"
:breadcrumbs="[
{ title: t('developer.cloudFunction.title'), href: '/developer/cloud-function' },
{ title: t('developer.cloudFunction.create.title') },
{ title: t('admin.cloudFunction.title'), href: '/admin/cloud-function' },
{ title: t('admin.cloudFunction.create.title') },
]"
sticky
>
@@ -112,18 +112,18 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cloudFunction.create.config') }}
{{ t('admin.cloudFunction.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudFunction.create.configDesc') }}</UiCardDescription>
<UiCardDescription>{{ t('admin.cloudFunction.create.configDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="application">
{{ t('developer.cloudFunction.create.application') }}
{{ t('admin.cloudFunction.create.application') }}
</UiLabel>
<UiSelect v-model="form.application_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudFunction.create.selectAppPlaceholder')" />
<UiSelectValue :placeholder="t('admin.cloudFunction.create.selectAppPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
@@ -139,47 +139,47 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="name">
{{ t('developer.cloudFunction.create.name') }}
{{ t('admin.cloudFunction.create.name') }}
</UiLabel>
<UiInput
id="name"
v-model="form.name"
:placeholder="t('developer.cloudFunction.create.namePlaceholder')"
:placeholder="t('admin.cloudFunction.create.namePlaceholder')"
/>
</div>
<div class="space-y-2">
<UiLabel for="description">
{{ t('developer.cloudFunction.create.description') }}
{{ t('admin.cloudFunction.create.description') }}
</UiLabel>
<UiTextarea
id="description"
v-model="form.description"
:placeholder="t('developer.cloudFunction.create.descriptionPlaceholder')"
:placeholder="t('admin.cloudFunction.create.descriptionPlaceholder')"
rows="3"
/>
</div>
<div class="space-y-2">
<UiLabel for="code">
{{ t('developer.cloudFunction.create.code') }}
{{ t('admin.cloudFunction.create.code') }}
</UiLabel>
<UiTextarea
id="code"
v-model="form.code"
:placeholder="t('developer.cloudFunction.create.codePlaceholder')"
:placeholder="t('admin.cloudFunction.create.codePlaceholder')"
rows="12"
class="font-mono text-sm"
/>
<p class="text-xs text-muted-foreground">
{{ t('developer.cloudFunction.create.codeHelp') }}
{{ t('admin.cloudFunction.create.codeHelp') }}
</p>
</div>
<div class="flex items-center justify-between">
<div>
<p class="font-medium">
{{ t('developer.cloudFunction.create.enabled') }}
{{ t('admin.cloudFunction.create.enabled') }}
</p>
</div>
<UiSwitch v-model="form.enabled" />
@@ -193,25 +193,25 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cloudFunction.create.preview') }}
{{ t('admin.cloudFunction.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.name') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudFunction.create.name') }}</span>
<span>{{ form.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.application') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudFunction.create.application') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.description') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudFunction.create.description') }}</span>
<span class="text-right max-w-[150px] truncate">{{ form.description || '-' }}</span>
</div>
<div class="flex justify-between text-sm items-center">
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.status') }}</span>
<span class="text-muted-foreground">{{ t('admin.cloudFunction.create.status') }}</span>
<UiSwitch
v-model="form.enabled"
/>
@@ -219,7 +219,7 @@ onMounted(() => {
<div class="border-t pt-3 mt-3">
<div class="text-sm">
<div class="text-muted-foreground mb-2">
{{ t('developer.cloudFunction.create.code') }}
{{ t('admin.cloudFunction.create.code') }}
</div>
<div class="font-mono text-xs bg-muted p-2 rounded max-h-[150px] overflow-auto" style="white-space: pre-wrap;">{{ form.code || '-' }}</div>
</div>
@@ -239,14 +239,14 @@ onMounted(() => {
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
{{ t('developer.cloudFunction.create.submitBtn') }}
{{ t('admin.cloudFunction.create.submitBtn') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="handleCancel"
>
{{ t('developer.cloudFunction.create.cancelBtn') }}
{{ t('admin.cloudFunction.create.cancelBtn') }}
</UiButton>
</div>
</UiCardContent>
+33 -33
View File
@@ -5,13 +5,13 @@ import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { DynamicCode } from '@/pages/developer/dynamic-code/data/schema'
import type { DynamicCode } from '@/pages/admin/dynamic-code/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import DataTable from '@/pages/developer/dynamic-code/components/data-table.vue'
import DataTable from '@/pages/admin/dynamic-code/components/data-table.vue'
import api from '@/services/api'
const { t } = useI18n()
@@ -99,8 +99,8 @@ const applicationOptions = computed(() => {
})
const statusOptions = computed(() => [
{ label: t('developer.cloudFunction.enabled'), value: 'enabled' },
{ label: t('developer.cloudFunction.disabled'), value: 'disabled' },
{ label: t('admin.cloudFunction.enabled'), value: 'enabled' },
{ label: t('admin.cloudFunction.disabled'), value: 'disabled' },
])
async function fetchApplications() {
@@ -129,24 +129,24 @@ async function fetchCodes() {
}
function goToCreate() {
router.push('/developer/cloud-function/create')
router.push('/admin/cloud-function/create')
}
async function toggleStatus(code: DynamicCode) {
try {
const newStatus = !code.enabled
await api.put(`/dev/dynamic-codes/${code.id}/status`, { enabled: newStatus })
toast.success(t('developer.cloudFunction.statusUpdateSuccess'))
toast.success(t('admin.cloudFunction.statusUpdateSuccess'))
fetchCodes()
}
catch (error: any) {
console.error('切换云端函数状态失败:', error)
toast.error(error.message || t('developer.cloudFunction.statusUpdateFailed'))
toast.error(error.message || t('admin.cloudFunction.statusUpdateFailed'))
}
}
function editCode(code: DynamicCode) {
router.push(`/developer/cloud-function/${code.id}`)
router.push(`/admin/cloud-function/${code.id}`)
}
function confirmDeleteCode(code: DynamicCode) {
@@ -160,12 +160,12 @@ async function handleDeleteCode() {
try {
await api.delete(`/dev/dynamic-codes/${deleteTarget.value.id}`)
toast.success(t('developer.cloudFunction.deleteSuccess'))
toast.success(t('admin.cloudFunction.deleteSuccess'))
fetchCodes()
}
catch (error: any) {
console.error('删除云端函数失败:', error)
toast.error(error.message || t('developer.cloudFunction.deleteFailed'))
toast.error(error.message || t('admin.cloudFunction.deleteFailed'))
}
finally {
deleteTarget.value = null
@@ -180,12 +180,12 @@ function confirmBatchDelete(ids: (string | number)[]) {
async function handleBatchDelete() {
try {
await api.delete('/dev/dynamic-codes/batch', { ids: batchDeleteIds.value } as any)
toast.success(t('developer.cloudFunction.deleteSuccess'))
toast.success(t('admin.cloudFunction.deleteSuccess'))
fetchCodes()
}
catch (error: any) {
console.error('批量删除失败:', error)
toast.error(error.message || t('developer.cloudFunction.deleteFailed'))
toast.error(error.message || t('admin.cloudFunction.deleteFailed'))
}
finally {
batchDeleteIds.value = []
@@ -195,12 +195,12 @@ async function handleBatchDelete() {
async function batchToggleStatus(ids: (string | number)[], enabled: boolean) {
try {
await api.put('/dev/dynamic-codes/batch/status', { ids, enabled })
toast.success(t('developer.cloudFunction.batchUpdateSuccess'))
toast.success(t('admin.cloudFunction.batchUpdateSuccess'))
fetchCodes()
}
catch (error: any) {
console.error('批量更新状态失败:', error)
toast.error(error.message || t('developer.cloudFunction.batchUpdateFailed'))
toast.error(error.message || t('admin.cloudFunction.batchUpdateFailed'))
}
}
@@ -212,14 +212,14 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.cloudFunction.title')"
:description="t('developer.cloudFunction.description')"
:title="t('admin.cloudFunction.title')"
:description="t('admin.cloudFunction.description')"
sticky
>
<template #actions>
<UiButton size="sm" @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cloudFunction.createBtn') }}
{{ t('admin.cloudFunction.createBtn') }}
</UiButton>
</template>
@@ -228,7 +228,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudFunction.totalCodes') }}
{{ t('admin.cloudFunction.totalCodes') }}
</UiCardTitle>
<Icon icon="lucide:code" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -242,7 +242,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudFunction.enabled') }}
{{ t('admin.cloudFunction.enabled') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -256,7 +256,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudFunction.disabled') }}
{{ t('admin.cloudFunction.disabled') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -270,7 +270,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudFunction.linkedApps') }}
{{ t('admin.cloudFunction.linkedApps') }}
</UiCardTitle>
<Icon icon="lucide:link" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -299,23 +299,23 @@ onMounted(() => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.cloudFunction.application')"
:title="t('admin.cloudFunction.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('developer.cloudFunction.status')"
:title="t('admin.cloudFunction.status')"
:options="statusOptions"
/>
<DateRangeFilter
v-model:start-model-value="createdStartDate"
v-model:end-model-value="createdEndDate"
:title="t('developer.cloudFunction.columns.createdAt')"
:title="t('admin.cloudFunction.columns.createdAt')"
/>
<DateRangeFilter
v-model:start-model-value="updatedStartDate"
v-model:end-model-value="updatedEndDate"
:title="t('developer.cloudFunction.columns.updatedAt')"
:title="t('admin.cloudFunction.columns.updatedAt')"
/>
</template>
</DataTable>
@@ -326,30 +326,30 @@ onMounted(() => {
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('developer.cloudFunction.delete')"
:cancel-button-text="t('developer.cloudFunction.create.cancelBtn')"
:confirm-button-text="t('admin.cloudFunction.delete')"
:cancel-button-text="t('admin.cloudFunction.create.cancelBtn')"
@confirm="handleDeleteCode"
>
<template #title>
{{ t('developer.cloudFunction.deleteCode') }}
{{ t('admin.cloudFunction.deleteCode') }}
</template>
<template #description>
{{ t('developer.cloudFunction.deleteCodeConfirm', { codeName: deleteTarget?.name }) }}
{{ t('admin.cloudFunction.deleteCodeConfirm', { codeName: deleteTarget?.name }) }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
:confirm-button-text="t('developer.cloudFunction.delete')"
:cancel-button-text="t('developer.cloudFunction.create.cancelBtn')"
:confirm-button-text="t('admin.cloudFunction.delete')"
:cancel-button-text="t('admin.cloudFunction.create.cancelBtn')"
@confirm="handleBatchDelete"
>
<template #title>
{{ t('developer.cloudFunction.batchDelete') }}
{{ t('admin.cloudFunction.batchDelete') }}
</template>
<template #description>
{{ t('developer.cloudFunction.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
{{ t('admin.cloudFunction.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
</BasicPage>
@@ -28,34 +28,34 @@ const form = ref({
const permissionGroups = computed(() => [
{
name: t('developer.extension.apiKeys.permissions.groups.user'),
name: t('admin.extension.apiKeys.permissions.groups.user'),
permissions: [
{ value: 'user.read', label: t('developer.extension.apiKeys.permissions.userRead'), description: t('developer.extension.apiKeys.permissions.userReadDesc') },
{ value: 'user.list', label: t('developer.extension.apiKeys.permissions.userList'), description: t('developer.extension.apiKeys.permissions.userListDesc') },
{ value: 'user.recharge', label: t('developer.extension.apiKeys.permissions.userRecharge'), description: t('developer.extension.apiKeys.permissions.userRechargeDesc') },
{ value: 'user.deduct', label: t('developer.extension.apiKeys.permissions.userDeduct'), description: t('developer.extension.apiKeys.permissions.userDeductDesc') },
{ value: 'user.variables', label: t('developer.extension.apiKeys.permissions.userVariables'), description: t('developer.extension.apiKeys.permissions.userVariablesDesc') },
{ value: 'user.read', label: t('admin.extension.apiKeys.permissions.userRead'), description: t('admin.extension.apiKeys.permissions.userReadDesc') },
{ value: 'user.list', label: t('admin.extension.apiKeys.permissions.userList'), description: t('admin.extension.apiKeys.permissions.userListDesc') },
{ value: 'user.recharge', label: t('admin.extension.apiKeys.permissions.userRecharge'), description: t('admin.extension.apiKeys.permissions.userRechargeDesc') },
{ value: 'user.deduct', label: t('admin.extension.apiKeys.permissions.userDeduct'), description: t('admin.extension.apiKeys.permissions.userDeductDesc') },
{ value: 'user.variables', label: t('admin.extension.apiKeys.permissions.userVariables'), description: t('admin.extension.apiKeys.permissions.userVariablesDesc') },
],
},
{
name: t('developer.extension.apiKeys.permissions.groups.card'),
name: t('admin.extension.apiKeys.permissions.groups.card'),
permissions: [
{ value: 'card.read', label: t('developer.extension.apiKeys.permissions.cardRead'), description: t('developer.extension.apiKeys.permissions.cardReadDesc') },
{ value: 'card.generate', label: t('developer.extension.apiKeys.permissions.cardGenerate'), description: t('developer.extension.apiKeys.permissions.cardGenerateDesc') },
{ value: 'card.read', label: t('admin.extension.apiKeys.permissions.cardRead'), description: t('admin.extension.apiKeys.permissions.cardReadDesc') },
{ value: 'card.generate', label: t('admin.extension.apiKeys.permissions.cardGenerate'), description: t('admin.extension.apiKeys.permissions.cardGenerateDesc') },
],
},
{
name: t('developer.extension.apiKeys.permissions.groups.notification'),
name: t('admin.extension.apiKeys.permissions.groups.notification'),
permissions: [
{ value: 'notification.send', label: t('developer.extension.apiKeys.permissions.notificationSend'), description: t('developer.extension.apiKeys.permissions.notificationSendDesc') },
{ value: 'notification.send', label: t('admin.extension.apiKeys.permissions.notificationSend'), description: t('admin.extension.apiKeys.permissions.notificationSendDesc') },
],
},
{
name: t('developer.extension.apiKeys.permissions.groups.app'),
name: t('admin.extension.apiKeys.permissions.groups.app'),
permissions: [
{ value: 'app.info', label: t('developer.extension.apiKeys.permissions.appInfo'), description: t('developer.extension.apiKeys.permissions.appInfoDesc') },
{ value: 'app.stats', label: t('developer.extension.apiKeys.permissions.appStats'), description: t('developer.extension.apiKeys.permissions.appStatsDesc') },
{ value: 'app.variables', label: t('developer.extension.apiKeys.permissions.appVariables'), description: t('developer.extension.apiKeys.permissions.appVariablesDesc') },
{ value: 'app.info', label: t('admin.extension.apiKeys.permissions.appInfo'), description: t('admin.extension.apiKeys.permissions.appInfoDesc') },
{ value: 'app.stats', label: t('admin.extension.apiKeys.permissions.appStats'), description: t('admin.extension.apiKeys.permissions.appStatsDesc') },
{ value: 'app.variables', label: t('admin.extension.apiKeys.permissions.appVariables'), description: t('admin.extension.apiKeys.permissions.appVariablesDesc') },
],
},
])
@@ -113,15 +113,15 @@ function deselectAllInGroup(permissions: { value: string }[]) {
async function handleSubmit() {
if (!form.value.name) {
toast.error(t('developer.extension.apiKeys.create.nameRequired'))
toast.error(t('admin.extension.apiKeys.create.nameRequired'))
return
}
if (!form.value.application_id) {
toast.error(t('developer.extension.apiKeys.create.selectAppRequired'))
toast.error(t('admin.extension.apiKeys.create.selectAppRequired'))
return
}
if (form.value.permissions.length === 0) {
toast.error(t('developer.extension.apiKeys.create.permissionsRequired'))
toast.error(t('admin.extension.apiKeys.create.permissionsRequired'))
return
}
@@ -133,12 +133,12 @@ async function handleSubmit() {
permissions: form.value.permissions,
enabled: form.value.enabled,
})
toast.success(t('developer.extension.apiKeys.create.submitSuccess'))
router.push('/developer/extension')
toast.success(t('admin.extension.apiKeys.create.submitSuccess'))
router.push('/admin/extension')
}
catch (error: any) {
console.error('创建API密钥失败:', error)
toast.error(error.message || t('developer.extension.apiKeys.create.submitFailed'))
toast.error(error.message || t('admin.extension.apiKeys.create.submitFailed'))
}
finally {
saving.value = false
@@ -152,11 +152,11 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.extension.apiKeys.create.title')"
:description="t('developer.extension.apiKeys.create.description')"
:title="t('admin.extension.apiKeys.create.title')"
:description="t('admin.extension.apiKeys.create.description')"
:breadcrumbs="[
{ title: t('developer.extension.title'), href: '/developer/extension' },
{ title: t('developer.extension.apiKeys.create.title') },
{ title: t('admin.extension.title'), href: '/admin/extension' },
{ title: t('admin.extension.apiKeys.create.title') },
]"
sticky
>
@@ -167,29 +167,29 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.extension.apiKeys.create.title') }}
{{ t('admin.extension.apiKeys.create.title') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.extension.apiKeys.create.description') }}</UiCardDescription>
<UiCardDescription>{{ t('admin.extension.apiKeys.create.description') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="name">
{{ t('developer.extension.apiKeys.create.name') }} *
{{ t('admin.extension.apiKeys.create.name') }} *
</UiLabel>
<UiInput
id="name"
v-model="form.name"
:placeholder="t('developer.extension.apiKeys.create.namePlaceholder')"
:placeholder="t('admin.extension.apiKeys.create.namePlaceholder')"
/>
</div>
<div class="space-y-2">
<UiLabel for="application">
{{ t('developer.extension.apiKeys.create.application') }} *
{{ t('admin.extension.apiKeys.create.application') }} *
</UiLabel>
<UiSelect v-model="form.application_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.extension.apiKeys.create.selectAppPlaceholder')" />
<UiSelectValue :placeholder="t('admin.extension.apiKeys.create.selectAppPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in filteredApplications" :key="app.value" :value="app.value">
@@ -201,13 +201,13 @@ onMounted(() => {
<div class="space-y-4">
<div class="flex items-center justify-between">
<UiLabel>{{ t('developer.extension.apiKeys.create.permissions') }} *</UiLabel>
<UiLabel>{{ t('admin.extension.apiKeys.create.permissions') }} *</UiLabel>
<div class="flex gap-2">
<UiButton variant="outline" size="sm" @click="form.permissions = permissionGroups.flatMap(g => g.permissions.map(p => p.value))">
{{ t('developer.extension.apiKeys.create.selectAll') }}
{{ t('admin.extension.apiKeys.create.selectAll') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="form.permissions = []">
{{ t('developer.extension.apiKeys.create.deselectAll') }}
{{ t('admin.extension.apiKeys.create.deselectAll') }}
</UiButton>
</div>
</div>
@@ -219,10 +219,10 @@ onMounted(() => {
</h4>
<div class="flex gap-1">
<UiButton variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="selectAllInGroup(group.permissions)">
{{ t('developer.extension.apiKeys.create.selectAll') }}
{{ t('admin.extension.apiKeys.create.selectAll') }}
</UiButton>
<UiButton variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="deselectAllInGroup(group.permissions)">
{{ t('developer.extension.apiKeys.create.deselectAll') }}
{{ t('admin.extension.apiKeys.create.deselectAll') }}
</UiButton>
</div>
</div>
@@ -255,7 +255,7 @@ onMounted(() => {
<div class="flex items-center space-x-2">
<UiSwitch id="enabled" v-model:checked="form.enabled" />
<UiLabel for="enabled">
{{ t('developer.extension.apiKeys.create.enabled') }}
{{ t('admin.extension.apiKeys.create.enabled') }}
</UiLabel>
</div>
</UiCardContent>
@@ -267,22 +267,22 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
{{ t('developer.extension.apiKeys.create.preview') || '预览' }}
{{ t('admin.extension.apiKeys.create.preview') || '预览' }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.extension.apiKeys.create.name') }}</span>
<span class="text-muted-foreground">{{ t('admin.extension.apiKeys.create.name') }}</span>
<span>{{ form.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.extension.apiKeys.create.application') }}</span>
<span class="text-muted-foreground">{{ t('admin.extension.apiKeys.create.application') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.extension.apiKeys.create.permissions') }}</span>
<span>{{ form.permissions.length }} {{ t('developer.extension.apiKeys.create.selected') || '已选择' }}</span>
<span class="text-muted-foreground">{{ t('admin.extension.apiKeys.create.permissions') }}</span>
<span>{{ form.permissions.length }} {{ t('admin.extension.apiKeys.create.selected') || '已选择' }}</span>
</div>
</div>
</UiCardContent>
@@ -299,14 +299,14 @@ onMounted(() => {
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
{{ t('developer.extension.apiKeys.create.submitBtn') }}
{{ t('admin.extension.apiKeys.create.submitBtn') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.extension.apiKeys.create.cancelBtn') }}
{{ t('admin.extension.apiKeys.create.cancelBtn') }}
</UiButton>
</div>
</UiCardContent>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Ban, MoreHorizontal, RefreshCw, Trash2 } from 'lucide-vue-next'
import { h } from 'vue'
import type { APIKey } from '@/pages/developer/extension/data/schema'
import type { APIKey } from '@/pages/admin/extension/data/schema'
import { Copy } from '@/components/sva-ui/copy'
import Badge from '@/components/ui/badge/Badge.vue'
@@ -25,7 +25,7 @@ export function getAPIKeyColumns(actions: {
return [
{
accessorKey: 'name',
header: () => t('developer.extension.apiKeys.columns.name'),
header: () => t('admin.extension.apiKeys.columns.name'),
cell: ({ row }) => {
const name = row.getValue('name') as string
return name || '-'
@@ -33,7 +33,7 @@ export function getAPIKeyColumns(actions: {
},
{
accessorKey: 'application_name',
header: () => t('developer.extension.apiKeys.columns.application'),
header: () => t('admin.extension.apiKeys.columns.application'),
cell: ({ row }) => {
const appName = row.getValue('application_name') as string
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -41,7 +41,7 @@ export function getAPIKeyColumns(actions: {
},
{
accessorKey: 'access_key',
header: () => t('developer.extension.apiKeys.columns.accessKey'),
header: () => t('admin.extension.apiKeys.columns.accessKey'),
cell: ({ row }) => {
const accessKey = row.getValue('access_key') as string
return accessKey
@@ -54,7 +54,7 @@ export function getAPIKeyColumns(actions: {
},
{
accessorKey: 'secret_key',
header: () => t('developer.extension.apiKeys.columns.secretKey'),
header: () => t('admin.extension.apiKeys.columns.secretKey'),
cell: ({ row }) => {
const secretKey = row.getValue('secret_key') as string
return secretKey ? h('code', { class: 'text-xs bg-muted px-2 py-1 rounded font-mono' }, '••••••••••••••••') : '-'
@@ -62,7 +62,7 @@ export function getAPIKeyColumns(actions: {
},
{
accessorKey: 'permissions',
header: () => t('developer.extension.apiKeys.columns.permissions'),
header: () => t('admin.extension.apiKeys.columns.permissions'),
cell: ({ row }) => {
const permissions = row.getValue('permissions') as string
try {
@@ -78,16 +78,16 @@ export function getAPIKeyColumns(actions: {
},
{
accessorKey: 'status',
header: () => t('developer.extension.apiKeys.columns.status'),
header: () => t('admin.extension.apiKeys.columns.status'),
cell: ({ row }) => {
const status = row.getValue('status') as string
return h(Badge, { variant: status === 'active' ? 'default' : 'secondary' }, () =>
status === 'active' ? t('developer.extension.enabled') : t('developer.extension.disabled'))
status === 'active' ? t('admin.extension.enabled') : t('admin.extension.disabled'))
},
},
{
accessorKey: 'created_at',
header: () => t('developer.extension.apiKeys.columns.createdAt'),
header: () => t('admin.extension.apiKeys.columns.createdAt'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at')
if (!createdAt)
@@ -132,16 +132,16 @@ export function getAPIKeyColumns(actions: {
() => [
h(DropdownMenuItem, { onClick: () => actions.onRegenerate(apiKey) }, () => [
h(RefreshCw, { class: 'mr-2 h-4 w-4' }),
t('developer.extension.apiKeys.regenerate'),
t('admin.extension.apiKeys.regenerate'),
]),
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(apiKey) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }),
isDisabled ? t('developer.extension.apiKeys.enable') : t('developer.extension.apiKeys.disable'),
isDisabled ? t('admin.extension.apiKeys.enable') : t('admin.extension.apiKeys.disable'),
]),
h(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(apiKey) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }),
t('developer.extension.apiKeys.delete'),
t('admin.extension.apiKeys.delete'),
]),
],
),
@@ -33,7 +33,7 @@ function resetFilters() {
<template>
<div class="flex items-center flex-1 space-x-2">
<Input
:placeholder="t('developer.extension.apiKeys.searchAPIKey')"
:placeholder="t('admin.extension.apiKeys.searchAPIKey')"
:model-value="searchFilter"
class="h-8 w-[150px] lg:w-[250px]"
@update:model-value="emit('update:searchFilter', $event as string)"
@@ -45,7 +45,7 @@ function resetFilters() {
class="h-8 px-2 lg:px-3"
@click="resetFilters"
>
{{ t('developer.extension.apiKeys.reset') }}
{{ t('admin.extension.apiKeys.reset') }}
<X class="size-4 ml-2" />
</Button>
</div>
@@ -48,14 +48,14 @@ const table = generateVueTable<APIKey>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.extension.apiKeys.select',
name: 'developer.extension.apiKeys.columns.name',
application_name: 'developer.extension.apiKeys.columns.application',
access_key: 'developer.extension.apiKeys.columns.accessKey',
secret_key: 'developer.extension.apiKeys.columns.secretKey',
permissions: 'developer.extension.apiKeys.columns.permissions',
status: 'developer.extension.apiKeys.columns.status',
created_at: 'developer.extension.apiKeys.columns.createdAt',
select: 'admin.extension.apiKeys.select',
name: 'admin.extension.apiKeys.columns.name',
application_name: 'admin.extension.apiKeys.columns.application',
access_key: 'admin.extension.apiKeys.columns.accessKey',
secret_key: 'admin.extension.apiKeys.columns.secretKey',
permissions: 'admin.extension.apiKeys.columns.permissions',
status: 'admin.extension.apiKeys.columns.status',
created_at: 'admin.extension.apiKeys.columns.createdAt',
}
defineExpose({
@@ -69,13 +69,13 @@ defineExpose({
<div class="space-y-3">
<BulkActions :table="table" entity-name="apiKeys">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
{{ t('developer.extension.apiKeys.enable') }}
{{ t('admin.extension.apiKeys.enable') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
{{ t('developer.extension.apiKeys.disable') }}
{{ t('admin.extension.apiKeys.disable') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.extension.apiKeys.batchDelete') }}
{{ t('admin.extension.apiKeys.batchDelete') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
@@ -25,7 +25,7 @@ export function getWebhookColumns(actions: {
return [
{
accessorKey: 'name',
header: () => t('developer.extension.webhooks.columns.name'),
header: () => t('admin.extension.webhooks.columns.name'),
cell: ({ row }) => {
const name = row.getValue('name') as string
return name || '-'
@@ -33,7 +33,7 @@ export function getWebhookColumns(actions: {
},
{
accessorKey: 'url',
header: () => t('developer.extension.webhooks.columns.url'),
header: () => t('admin.extension.webhooks.columns.url'),
cell: ({ row }) => {
const url = row.getValue('url') as string
return url ? h('code', { class: 'text-xs bg-muted px-2 py-1 rounded font-mono max-w-[200px] truncate inline-block' }, url) : '-'
@@ -41,7 +41,7 @@ export function getWebhookColumns(actions: {
},
{
accessorKey: 'application_name',
header: () => t('developer.extension.webhooks.columns.application'),
header: () => t('admin.extension.webhooks.columns.application'),
cell: ({ row }) => {
const appName = row.getValue('application_name') as string
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -49,7 +49,7 @@ export function getWebhookColumns(actions: {
},
{
accessorKey: 'events',
header: () => t('developer.extension.webhooks.columns.events'),
header: () => t('admin.extension.webhooks.columns.events'),
cell: ({ row }) => {
const events = row.getValue('events') as string
try {
@@ -65,16 +65,16 @@ export function getWebhookColumns(actions: {
},
{
accessorKey: 'status',
header: () => t('developer.extension.webhooks.columns.status'),
header: () => t('admin.extension.webhooks.columns.status'),
cell: ({ row }) => {
const status = row.getValue('status') as string
return h(Badge, { variant: status === 'active' ? 'default' : 'secondary' }, () =>
status === 'active' ? t('developer.extension.enabled') : t('developer.extension.disabled'))
status === 'active' ? t('admin.extension.enabled') : t('admin.extension.disabled'))
},
},
{
accessorKey: 'retry_count',
header: () => t('developer.extension.webhooks.columns.retryCount'),
header: () => t('admin.extension.webhooks.columns.retryCount'),
cell: ({ row }) => {
const retryCount = row.getValue('retry_count') as number
return retryCount ?? '-'
@@ -82,7 +82,7 @@ export function getWebhookColumns(actions: {
},
{
accessorKey: 'timeout',
header: () => t('developer.extension.webhooks.columns.timeout'),
header: () => t('admin.extension.webhooks.columns.timeout'),
cell: ({ row }) => {
const timeout = row.getValue('timeout') as number
return timeout ? `${timeout}s` : '-'
@@ -90,7 +90,7 @@ export function getWebhookColumns(actions: {
},
{
accessorKey: 'created_at',
header: () => t('developer.extension.webhooks.columns.createdAt'),
header: () => t('admin.extension.webhooks.columns.createdAt'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at')
if (!createdAt)
@@ -135,22 +135,22 @@ export function getWebhookColumns(actions: {
() => [
h(DropdownMenuItem, { onClick: () => actions.onTest(webhook) }, () => [
h(Send, { class: 'mr-2 h-4 w-4' }),
t('developer.extension.webhooks.test'),
t('admin.extension.webhooks.test'),
]),
actions.onViewLogs
? h(DropdownMenuItem, { onClick: () => actions.onViewLogs?.(webhook) }, () => [
h(Eye, { class: 'mr-2 h-4 w-4' }),
t('developer.extension.webhooks.viewLogs'),
t('admin.extension.webhooks.viewLogs'),
])
: null,
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(webhook) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }),
isDisabled ? t('developer.extension.webhooks.enable') : t('developer.extension.webhooks.disable'),
isDisabled ? t('admin.extension.webhooks.enable') : t('admin.extension.webhooks.disable'),
]),
h(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(webhook) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }),
t('developer.extension.webhooks.delete'),
t('admin.extension.webhooks.delete'),
]),
],
),
@@ -33,7 +33,7 @@ function resetFilters() {
<template>
<div class="flex items-center flex-1 space-x-2">
<Input
:placeholder="t('developer.extension.webhooks.searchWebhook')"
:placeholder="t('admin.extension.webhooks.searchWebhook')"
:model-value="searchFilter"
class="h-8 w-[150px] lg:w-[250px]"
@update:model-value="emit('update:searchFilter', $event as string)"
@@ -45,7 +45,7 @@ function resetFilters() {
class="h-8 px-2 lg:px-3"
@click="resetFilters"
>
{{ t('developer.extension.webhooks.reset') }}
{{ t('admin.extension.webhooks.reset') }}
<X class="size-4 ml-2" />
</Button>
</div>
@@ -48,15 +48,15 @@ const table = generateVueTable<Webhook>({
}, columns.value)
const columnLabels: Record<string, string> = {
select: 'developer.extension.webhooks.select',
name: 'developer.extension.webhooks.columns.name',
url: 'developer.extension.webhooks.columns.url',
application_name: 'developer.extension.webhooks.columns.application',
events: 'developer.extension.webhooks.columns.events',
status: 'developer.extension.webhooks.columns.status',
retry_count: 'developer.extension.webhooks.columns.retryCount',
timeout: 'developer.extension.webhooks.columns.timeout',
created_at: 'developer.extension.webhooks.columns.createdAt',
select: 'admin.extension.webhooks.select',
name: 'admin.extension.webhooks.columns.name',
url: 'admin.extension.webhooks.columns.url',
application_name: 'admin.extension.webhooks.columns.application',
events: 'admin.extension.webhooks.columns.events',
status: 'admin.extension.webhooks.columns.status',
retry_count: 'admin.extension.webhooks.columns.retryCount',
timeout: 'admin.extension.webhooks.columns.timeout',
created_at: 'admin.extension.webhooks.columns.createdAt',
}
defineExpose({
@@ -70,13 +70,13 @@ defineExpose({
<div class="space-y-3">
<BulkActions :table="table" entity-name="webhooks">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
{{ t('developer.extension.webhooks.enable') }}
{{ t('admin.extension.webhooks.enable') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
{{ t('developer.extension.webhooks.disable') }}
{{ t('admin.extension.webhooks.disable') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.extension.webhooks.batchDelete') }}
{{ t('admin.extension.webhooks.batchDelete') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
+53 -53
View File
@@ -5,14 +5,14 @@ import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { APIKey, Webhook } from '@/pages/developer/extension/data/schema'
import type { APIKey, Webhook } from '@/pages/admin/extension/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import APIKeyDataTable from '@/pages/developer/extension/components/apikey-data-table.vue'
import WebhookDataTable from '@/pages/developer/extension/components/webhook-data-table.vue'
import APIKeyDataTable from '@/pages/admin/extension/components/apikey-data-table.vue'
import WebhookDataTable from '@/pages/admin/extension/components/webhook-data-table.vue'
import api from '@/services/api'
const { t } = useI18n()
@@ -134,8 +134,8 @@ const applicationOptions = computed(() => {
})
const statusOptions = computed(() => [
{ label: t('developer.extension.enabled'), value: 'active' },
{ label: t('developer.extension.disabled'), value: 'inactive' },
{ label: t('admin.extension.enabled'), value: 'active' },
{ label: t('admin.extension.disabled'), value: 'inactive' },
])
async function fetchApplications() {
@@ -179,23 +179,23 @@ async function fetchAPIKeys() {
}
function goToCreateWebhook() {
router.push('/developer/extension/webhooks/create')
router.push('/admin/extension/webhooks/create')
}
function goToCreateAPIKey() {
router.push('/developer/extension/api-keys/create')
router.push('/admin/extension/api-keys/create')
}
async function toggleWebhookStatus(webhook: Webhook) {
try {
const newStatus = webhook.status === 'active' ? 'inactive' : 'active'
await api.put(`/dev/extension/webhooks/${webhook.id}/status`, { status: newStatus })
toast.success(t('developer.extension.webhooks.statusUpdateSuccess'))
toast.success(t('admin.extension.webhooks.statusUpdateSuccess'))
fetchWebhooks()
}
catch (error: any) {
console.error('切换Webhook状态失败:', error)
toast.error(error.message || t('developer.extension.webhooks.statusUpdateFailed'))
toast.error(error.message || t('admin.extension.webhooks.statusUpdateFailed'))
}
}
@@ -203,23 +203,23 @@ async function toggleAPIKeyStatus(apiKey: APIKey) {
try {
const newStatus = apiKey.status === 'active' ? 'inactive' : 'active'
await api.put(`/dev/extension/api-keys/${apiKey.id}/status`, { status: newStatus })
toast.success(t('developer.extension.apiKeys.statusUpdateSuccess'))
toast.success(t('admin.extension.apiKeys.statusUpdateSuccess'))
fetchAPIKeys()
}
catch (error: any) {
console.error('切换API密钥状态失败:', error)
toast.error(error.message || t('developer.extension.apiKeys.statusUpdateFailed'))
toast.error(error.message || t('admin.extension.apiKeys.statusUpdateFailed'))
}
}
async function testWebhook(webhook: Webhook) {
try {
await api.post(`/dev/extension/webhooks/${webhook.id}/test`, {})
toast.success(t('developer.extension.webhooks.testSuccess'))
toast.success(t('admin.extension.webhooks.testSuccess'))
}
catch (error: any) {
console.error('测试Webhook失败:', error)
toast.error(error.message || t('developer.extension.webhooks.testFailed'))
toast.error(error.message || t('admin.extension.webhooks.testFailed'))
}
}
@@ -234,12 +234,12 @@ async function handleRegenerateAPIKey() {
try {
await api.post(`/dev/extension/api-keys/${regenerateTarget.value.id}/regenerate`, {})
toast.success(t('developer.extension.apiKeys.regenerateSuccess'))
toast.success(t('admin.extension.apiKeys.regenerateSuccess'))
fetchAPIKeys()
}
catch (error: any) {
console.error('重新生成API密钥失败:', error)
toast.error(error.message || t('developer.extension.apiKeys.regenerateFailed'))
toast.error(error.message || t('admin.extension.apiKeys.regenerateFailed'))
}
finally {
regenerateTarget.value = null
@@ -265,18 +265,18 @@ async function handleDelete() {
try {
if (deleteType.value === 'webhook') {
await api.delete(`/dev/extension/webhooks/${deleteTarget.value.id}`)
toast.success(t('developer.extension.webhooks.deleteSuccess'))
toast.success(t('admin.extension.webhooks.deleteSuccess'))
fetchWebhooks()
}
else {
await api.delete(`/dev/extension/api-keys/${deleteTarget.value.id}`)
toast.success(t('developer.extension.apiKeys.deleteSuccess'))
toast.success(t('admin.extension.apiKeys.deleteSuccess'))
fetchAPIKeys()
}
}
catch (error: any) {
console.error('删除失败:', error)
toast.error(error.message || t('developer.extension.deleteFailed'))
toast.error(error.message || t('admin.extension.deleteFailed'))
}
finally {
deleteTarget.value = null
@@ -293,18 +293,18 @@ async function handleBatchDelete() {
try {
if (deleteType.value === 'webhook') {
await api.delete('/dev/extension/webhooks/batch', { ids: batchDeleteIds.value } as any)
toast.success(t('developer.extension.webhooks.deleteSuccess'))
toast.success(t('admin.extension.webhooks.deleteSuccess'))
fetchWebhooks()
}
else {
await api.delete('/dev/extension/api-keys/batch', { ids: batchDeleteIds.value } as any)
toast.success(t('developer.extension.apiKeys.deleteSuccess'))
toast.success(t('admin.extension.apiKeys.deleteSuccess'))
fetchAPIKeys()
}
}
catch (error: any) {
console.error('批量删除失败:', error)
toast.error(error.message || t('developer.extension.deleteFailed'))
toast.error(error.message || t('admin.extension.deleteFailed'))
}
finally {
batchDeleteIds.value = []
@@ -314,24 +314,24 @@ async function handleBatchDelete() {
async function batchToggleWebhookStatus(ids: (string | number)[], status: string) {
try {
await api.put('/dev/extension/webhooks/batch/status', { ids, status })
toast.success(t('developer.extension.webhooks.batchUpdateSuccess'))
toast.success(t('admin.extension.webhooks.batchUpdateSuccess'))
fetchWebhooks()
}
catch (error: any) {
console.error('批量更新Webhook状态失败:', error)
toast.error(error.message || t('developer.extension.webhooks.batchUpdateFailed'))
toast.error(error.message || t('admin.extension.webhooks.batchUpdateFailed'))
}
}
async function batchToggleAPIKeyStatus(ids: (string | number)[], status: string) {
try {
await api.put('/dev/extension/api-keys/batch/status', { ids, status })
toast.success(t('developer.extension.apiKeys.batchUpdateSuccess'))
toast.success(t('admin.extension.apiKeys.batchUpdateSuccess'))
fetchAPIKeys()
}
catch (error: any) {
console.error('批量更新API密钥状态失败:', error)
toast.error(error.message || t('developer.extension.apiKeys.batchUpdateFailed'))
toast.error(error.message || t('admin.extension.apiKeys.batchUpdateFailed'))
}
}
@@ -344,18 +344,18 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.extension.title')"
:description="t('developer.extension.description')"
:title="t('admin.extension.title')"
:description="t('admin.extension.description')"
sticky
>
<template #actions>
<UiButton v-if="currentTab === 'webhooks'" @click="goToCreateWebhook">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.extension.webhooks.createBtn') }}
{{ t('admin.extension.webhooks.createBtn') }}
</UiButton>
<UiButton v-else @click="goToCreateAPIKey">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.extension.apiKeys.createBtn') }}
{{ t('admin.extension.apiKeys.createBtn') }}
</UiButton>
</template>
@@ -364,7 +364,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.extension.totalWebhooks') }}
{{ t('admin.extension.totalWebhooks') }}
</UiCardTitle>
<Icon icon="lucide:webhook" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -378,7 +378,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.extension.totalAPIKeys') }}
{{ t('admin.extension.totalAPIKeys') }}
</UiCardTitle>
<Icon icon="lucide:key" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -392,7 +392,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.extension.enabled') }}
{{ t('admin.extension.enabled') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -406,7 +406,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.extension.disabled') }}
{{ t('admin.extension.disabled') }}
</UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -422,11 +422,11 @@ onMounted(() => {
<UiTabsList class="grid w-full grid-cols-2">
<UiTabsTrigger value="webhooks">
<Icon icon="lucide:webhook" class="h-4 w-4 mr-2" />
{{ t('developer.extension.webhooks.title') }}
{{ t('admin.extension.webhooks.title') }}
</UiTabsTrigger>
<UiTabsTrigger value="apikeys">
<Icon icon="lucide:key" class="h-4 w-4 mr-2" />
{{ t('developer.extension.apiKeys.title') }}
{{ t('admin.extension.apiKeys.title') }}
</UiTabsTrigger>
</UiTabsList>
@@ -450,12 +450,12 @@ onMounted(() => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.extension.application')"
:title="t('admin.extension.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('developer.extension.status')"
:title="t('admin.extension.status')"
:options="statusOptions"
/>
<DateRangeFilter
@@ -488,12 +488,12 @@ onMounted(() => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.extension.application')"
:title="t('admin.extension.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('developer.extension.status')"
:title="t('admin.extension.status')"
:options="statusOptions"
/>
<DateRangeFilter
@@ -511,17 +511,17 @@ onMounted(() => {
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('developer.extension.delete')"
:cancel-button-text="t('developer.extension.cancel')"
:confirm-button-text="t('admin.extension.delete')"
:cancel-button-text="t('admin.extension.cancel')"
@confirm="handleDelete"
>
<template #title>
{{ deleteType === 'webhook' ? t('developer.extension.webhooks.deleteWebhook') : t('developer.extension.apiKeys.deleteAPIKey') }}
{{ deleteType === 'webhook' ? t('admin.extension.webhooks.deleteWebhook') : t('admin.extension.apiKeys.deleteAPIKey') }}
</template>
<template #description>
{{ deleteType === 'webhook'
? t('developer.extension.webhooks.deleteConfirm', { webhookName: (deleteTarget as Webhook)?.name })
: t('developer.extension.apiKeys.deleteConfirm', { apiKeyName: (deleteTarget as APIKey)?.name })
? t('admin.extension.webhooks.deleteConfirm', { webhookName: (deleteTarget as Webhook)?.name })
: t('admin.extension.apiKeys.deleteConfirm', { apiKeyName: (deleteTarget as APIKey)?.name })
}}
</template>
</ConfirmDialog>
@@ -529,31 +529,31 @@ onMounted(() => {
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
:confirm-button-text="t('developer.extension.delete')"
:cancel-button-text="t('developer.extension.cancel')"
:confirm-button-text="t('admin.extension.delete')"
:cancel-button-text="t('admin.extension.cancel')"
@confirm="handleBatchDelete"
>
<template #title>
{{ deleteType === 'webhook' ? t('developer.extension.webhooks.batchDelete') : t('developer.extension.apiKeys.batchDelete') }}
{{ deleteType === 'webhook' ? t('admin.extension.webhooks.batchDelete') : t('admin.extension.apiKeys.batchDelete') }}
</template>
<template #description>
{{ deleteType === 'webhook'
? t('developer.extension.webhooks.batchDeleteConfirm', { count: batchDeleteIds.length })
: t('developer.extension.apiKeys.batchDeleteConfirm', { count: batchDeleteIds.length })
? t('admin.extension.webhooks.batchDeleteConfirm', { count: batchDeleteIds.length })
: t('admin.extension.apiKeys.batchDeleteConfirm', { count: batchDeleteIds.length })
}}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="regenerateDialogOpen"
:confirm-button-text="t('developer.extension.apiKeys.regenerate')"
:cancel-button-text="t('developer.extension.cancel')"
:confirm-button-text="t('admin.extension.apiKeys.regenerate')"
:cancel-button-text="t('admin.extension.cancel')"
@confirm="handleRegenerateAPIKey"
>
<template #title>
{{ t('developer.extension.apiKeys.regenerate') }}
{{ t('admin.extension.apiKeys.regenerate') }}
</template>
<template #description>
{{ t('developer.extension.apiKeys.regenerateConfirm') }}
{{ t('admin.extension.apiKeys.regenerateConfirm') }}
</template>
</ConfirmDialog>
</BasicPage>
@@ -31,13 +31,13 @@ const form = ref({
})
const availableEvents = computed(() => [
{ value: 'user.registered', label: t('developer.extension.webhooks.events.userRegistered') },
{ value: 'user.login', label: t('developer.extension.webhooks.events.userLogin') },
{ value: 'user.recharged', label: t('developer.extension.webhooks.events.userRecharged') },
{ value: 'user.expired', label: t('developer.extension.webhooks.events.userExpired') },
{ value: 'card.used', label: t('developer.extension.webhooks.events.cardUsed') },
{ value: 'card.expired', label: t('developer.extension.webhooks.events.cardExpired') },
{ value: 'abnormal.detected', label: t('developer.extension.webhooks.events.abnormalDetected') },
{ value: 'user.registered', label: t('admin.extension.webhooks.events.userRegistered') },
{ value: 'user.login', label: t('admin.extension.webhooks.events.userLogin') },
{ value: 'user.recharged', label: t('admin.extension.webhooks.events.userRecharged') },
{ value: 'user.expired', label: t('admin.extension.webhooks.events.userExpired') },
{ value: 'card.used', label: t('admin.extension.webhooks.events.cardUsed') },
{ value: 'card.expired', label: t('admin.extension.webhooks.events.cardExpired') },
{ value: 'abnormal.detected', label: t('admin.extension.webhooks.events.abnormalDetected') },
])
const filteredApplications = computed(() => {
@@ -76,19 +76,19 @@ function toggleEvent(event: string) {
async function handleSubmit() {
if (!form.value.name) {
toast.error(t('developer.extension.webhooks.create.nameRequired'))
toast.error(t('admin.extension.webhooks.create.nameRequired'))
return
}
if (!form.value.application_id) {
toast.error(t('developer.extension.webhooks.create.selectAppRequired'))
toast.error(t('admin.extension.webhooks.create.selectAppRequired'))
return
}
if (!form.value.url) {
toast.error(t('developer.extension.webhooks.create.urlRequired'))
toast.error(t('admin.extension.webhooks.create.urlRequired'))
return
}
if (form.value.events.length === 0) {
toast.error(t('developer.extension.webhooks.create.eventsRequired'))
toast.error(t('admin.extension.webhooks.create.eventsRequired'))
return
}
@@ -104,12 +104,12 @@ async function handleSubmit() {
timeout: form.value.timeout,
enabled: form.value.enabled,
})
toast.success(t('developer.extension.webhooks.create.submitSuccess'))
router.push('/developer/extension')
toast.success(t('admin.extension.webhooks.create.submitSuccess'))
router.push('/admin/extension')
}
catch (error: any) {
console.error('创建Webhook失败:', error)
toast.error(error.message || t('developer.extension.webhooks.create.submitFailed'))
toast.error(error.message || t('admin.extension.webhooks.create.submitFailed'))
}
finally {
saving.value = false
@@ -123,11 +123,11 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.extension.webhooks.create.title')"
:description="t('developer.extension.webhooks.create.description')"
:title="t('admin.extension.webhooks.create.title')"
:description="t('admin.extension.webhooks.create.description')"
:breadcrumbs="[
{ title: t('developer.extension.title'), href: '/developer/extension' },
{ title: t('developer.extension.webhooks.create.title') },
{ title: t('admin.extension.title'), href: '/admin/extension' },
{ title: t('admin.extension.webhooks.create.title') },
]"
sticky
>
@@ -138,29 +138,29 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.extension.webhooks.create.title') }}
{{ t('admin.extension.webhooks.create.title') }}
</UiCardTitle>
<UiCardDescription>{{ t('developer.extension.webhooks.create.description') }}</UiCardDescription>
<UiCardDescription>{{ t('admin.extension.webhooks.create.description') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="name">
{{ t('developer.extension.webhooks.create.name') }} *
{{ t('admin.extension.webhooks.create.name') }} *
</UiLabel>
<UiInput
id="name"
v-model="form.name"
:placeholder="t('developer.extension.webhooks.create.namePlaceholder')"
:placeholder="t('admin.extension.webhooks.create.namePlaceholder')"
/>
</div>
<div class="space-y-2">
<UiLabel for="application">
{{ t('developer.extension.webhooks.create.application') }} *
{{ t('admin.extension.webhooks.create.application') }} *
</UiLabel>
<UiSelect v-model="form.application_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.extension.webhooks.create.selectAppPlaceholder')" />
<UiSelectValue :placeholder="t('admin.extension.webhooks.create.selectAppPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in filteredApplications" :key="app.value" :value="app.value">
@@ -172,28 +172,28 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="url">
{{ t('developer.extension.webhooks.create.url') }} *
{{ t('admin.extension.webhooks.create.url') }} *
</UiLabel>
<UiInput
id="url"
v-model="form.url"
:placeholder="t('developer.extension.webhooks.create.urlPlaceholder')"
:placeholder="t('admin.extension.webhooks.create.urlPlaceholder')"
/>
</div>
<div class="space-y-2">
<UiLabel for="secret_key">
{{ t('developer.extension.webhooks.create.secretKey') }}
{{ t('admin.extension.webhooks.create.secretKey') }}
</UiLabel>
<UiInput
id="secret_key"
v-model="form.secret_key"
:placeholder="t('developer.extension.webhooks.create.secretKeyPlaceholder')"
:placeholder="t('admin.extension.webhooks.create.secretKeyPlaceholder')"
/>
</div>
<div class="space-y-2">
<UiLabel>{{ t('developer.extension.webhooks.create.events') }} *</UiLabel>
<UiLabel>{{ t('admin.extension.webhooks.create.events') }} *</UiLabel>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<div
v-for="event in availableEvents"
@@ -214,7 +214,7 @@ onMounted(() => {
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<UiLabel for="retry_count">
{{ t('developer.extension.webhooks.create.retryCount') }}
{{ t('admin.extension.webhooks.create.retryCount') }}
</UiLabel>
<UiInput
id="retry_count"
@@ -222,13 +222,13 @@ onMounted(() => {
type="number"
min="0"
max="10"
:placeholder="t('developer.extension.webhooks.create.retryCountPlaceholder')"
:placeholder="t('admin.extension.webhooks.create.retryCountPlaceholder')"
/>
</div>
<div class="space-y-2">
<UiLabel for="timeout">
{{ t('developer.extension.webhooks.create.timeout') }}
{{ t('admin.extension.webhooks.create.timeout') }}
</UiLabel>
<UiInput
id="timeout"
@@ -236,7 +236,7 @@ onMounted(() => {
type="number"
min="1"
max="60"
:placeholder="t('developer.extension.webhooks.create.timeoutPlaceholder')"
:placeholder="t('admin.extension.webhooks.create.timeoutPlaceholder')"
/>
</div>
</div>
@@ -244,7 +244,7 @@ onMounted(() => {
<div class="flex items-center space-x-2">
<UiSwitch id="enabled" v-model:checked="form.enabled" />
<UiLabel for="enabled">
{{ t('developer.extension.webhooks.create.enabled') }}
{{ t('admin.extension.webhooks.create.enabled') }}
</UiLabel>
</div>
</UiCardContent>
@@ -256,29 +256,29 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
{{ t('developer.extension.webhooks.create.preview') || '预览' }}
{{ t('admin.extension.webhooks.create.preview') || '预览' }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.name') }}</span>
<span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.name') }}</span>
<span>{{ form.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.application') }}</span>
<span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.application') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.events') }}</span>
<span>{{ form.events.length }} {{ t('developer.extension.webhooks.create.selected') || '已选择' }}</span>
<span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.events') }}</span>
<span>{{ form.events.length }} {{ t('admin.extension.webhooks.create.selected') || '已选择' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.retryCount') }}</span>
<span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.retryCount') }}</span>
<span>{{ form.retry_count }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.timeout') }}</span>
<span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.timeout') }}</span>
<span>{{ form.timeout }}s</span>
</div>
</div>
@@ -296,14 +296,14 @@ onMounted(() => {
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
{{ t('developer.extension.webhooks.create.submitBtn') }}
{{ t('admin.extension.webhooks.create.submitBtn') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('developer.extension.webhooks.create.cancelBtn') }}
{{ t('admin.extension.webhooks.create.cancelBtn') }}
</UiButton>
</div>
</UiCardContent>
+48 -48
View File
@@ -174,11 +174,11 @@ async function handleDelete() {
try {
await api.delete(`/dev/finance/records/${deleteTarget.value.id}?type=${deleteTarget.value.type}`)
toast.success(t('developer.finance.deleteSuccess'))
toast.success(t('admin.finance.deleteSuccess'))
fetchRecords()
}
catch (error: any) {
toast.error(error.message || t('developer.finance.deleteFailed'))
toast.error(error.message || t('admin.finance.deleteFailed'))
}
finally {
deleteTarget.value = null
@@ -203,15 +203,15 @@ const columns: ColumnDef<FinanceRecord>[] = [
},
{
accessorKey: 'type',
header: () => t('developer.finance.columns.type'),
header: () => t('admin.finance.columns.type'),
cell: ({ row }) => {
const type = row.getValue('type') as string
return h(Badge, { variant: type === 'recharge' ? 'default' : 'secondary' }, () => type === 'recharge' ? t('developer.finance.types.recharge') : t('developer.finance.types.consumption'))
return h(Badge, { variant: type === 'recharge' ? 'default' : 'secondary' }, () => type === 'recharge' ? t('admin.finance.types.recharge') : t('admin.finance.types.consumption'))
},
},
{
accessorKey: 'amount',
header: () => t('developer.finance.columns.amount'),
header: () => t('admin.finance.columns.amount'),
cell: ({ row }) => {
const type = row.getValue('type') as string
const amount = row.getValue('amount') as number
@@ -221,7 +221,7 @@ const columns: ColumnDef<FinanceRecord>[] = [
},
{
accessorKey: 'user',
header: () => t('developer.finance.columns.user'),
header: () => t('admin.finance.columns.user'),
cell: ({ row }) => {
const user = row.original.user
return user?.username || '-'
@@ -229,7 +229,7 @@ const columns: ColumnDef<FinanceRecord>[] = [
},
{
accessorKey: 'detail',
header: () => t('developer.finance.columns.detail'),
header: () => t('admin.finance.columns.detail'),
cell: ({ row }) => {
const detail = row.getValue('detail') as string
return h('span', { class: 'max-w-[200px] truncate text-muted-foreground block' }, detail || '-')
@@ -237,13 +237,13 @@ const columns: ColumnDef<FinanceRecord>[] = [
},
{
accessorKey: 'status',
header: () => t('developer.finance.columns.status'),
header: () => t('admin.finance.columns.status'),
cell: ({ row }) => {
const status = row.getValue('status') as string
const map: Record<string, { label: string, variant: 'default' | 'secondary' | 'destructive' }> = {
success: { label: t('developer.finance.success'), variant: 'default' },
pending: { label: t('developer.finance.pending'), variant: 'secondary' },
failed: { label: t('developer.finance.failed'), variant: 'destructive' },
success: { label: t('admin.finance.success'), variant: 'default' },
pending: { label: t('admin.finance.pending'), variant: 'secondary' },
failed: { label: t('admin.finance.failed'), variant: 'destructive' },
}
const { label, variant } = map[status] || { label: status, variant: 'secondary' }
return h(Badge, { variant }, () => label)
@@ -251,14 +251,14 @@ const columns: ColumnDef<FinanceRecord>[] = [
},
{
accessorKey: 'created_at',
header: () => t('developer.finance.columns.time'),
header: () => t('admin.finance.columns.time'),
cell: ({ row }) => {
return h('span', { class: 'text-muted-foreground whitespace-nowrap' }, formatTime(row.getValue('created_at')))
},
},
{
id: 'actions',
header: () => t('developer.finance.columns.actions'),
header: () => t('admin.finance.columns.actions'),
cell: ({ row }) => {
return h(Button, {
variant: 'ghost',
@@ -293,7 +293,7 @@ const table = useVueTable({
async function confirmBatchDelete() {
const selectedRows = table.getSelectedRowModel().rows
if (selectedRows.length === 0) {
toast.error(t('developer.finance.delete'))
toast.error(t('admin.finance.delete'))
return
}
batchDeleteDialogOpen.value = true
@@ -304,11 +304,11 @@ async function handleBatchDelete() {
const ids = selectedRows.map(row => row.original.id)
try {
await api.post('/dev/finance/records/batch-delete', { ids })
toast.success(t('developer.finance.deleteSuccess'))
toast.success(t('admin.finance.deleteSuccess'))
fetchRecords()
}
catch (error: any) {
toast.error(error.message || t('developer.finance.deleteFailed'))
toast.error(error.message || t('admin.finance.deleteFailed'))
}
}
@@ -344,8 +344,8 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.finance.title')"
:description="t('developer.finance.description')"
:title="t('admin.finance.title')"
:description="t('admin.finance.description')"
sticky
>
<div class="space-y-6">
@@ -353,7 +353,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.finance.rechargeCount') }}
{{ t('admin.finance.rechargeCount') }}
</UiCardTitle>
<Icon icon="lucide:arrow-down-left" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -367,7 +367,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.finance.rechargeAmount') }}
{{ t('admin.finance.rechargeAmount') }}
</UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -381,7 +381,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.finance.consumptionCount') }}
{{ t('admin.finance.consumptionCount') }}
</UiCardTitle>
<Icon icon="lucide:arrow-up-right" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -395,7 +395,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.finance.consumptionAmount') }}
{{ t('admin.finance.consumptionAmount') }}
</UiCardTitle>
<Icon icon="lucide:credit-card" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -409,7 +409,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.finance.profit') }}
{{ t('admin.finance.profit') }}
</UiCardTitle>
<Icon icon="lucide:trending-up" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -423,9 +423,9 @@ onMounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.finance.transactionRecords') }}</UiCardTitle>
<UiCardTitle>{{ t('admin.finance.transactionRecords') }}</UiCardTitle>
<UiCardDescription>
{{ t('developer.finance.transactionRecordsDesc') }}
{{ t('admin.finance.transactionRecordsDesc') }}
</UiCardDescription>
</UiCardHeader>
<UiCardContent>
@@ -438,18 +438,18 @@ onMounted(() => {
@click="confirmBatchDelete"
>
<Icon icon="lucide:trash-2" class="mr-2 size-4" />
{{ t('developer.finance.batchDeleteBtn') }}
{{ t('admin.finance.batchDeleteBtn') }}
</UiButton>
</div>
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-wrap items-center gap-2">
<UiSelect v-model="appFilter" class="w-[140px]">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.finance.allApplications')" />
<UiSelectValue :placeholder="t('admin.finance.allApplications')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="all">
{{ t('developer.finance.allApplications') }}
{{ t('admin.finance.allApplications') }}
</UiSelectItem>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
{{ app.name }}
@@ -459,43 +459,43 @@ onMounted(() => {
<UiSelect v-model="statusFilter" class="w-[120px]">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.finance.allStatus')" />
<UiSelectValue :placeholder="t('admin.finance.allStatus')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="all">
{{ t('developer.finance.allStatus') }}
{{ t('admin.finance.allStatus') }}
</UiSelectItem>
<UiSelectItem value="success">
{{ t('developer.finance.success') }}
{{ t('admin.finance.success') }}
</UiSelectItem>
<UiSelectItem value="pending">
{{ t('developer.finance.pending') }}
{{ t('admin.finance.pending') }}
</UiSelectItem>
<UiSelectItem value="failed">
{{ t('developer.finance.failed') }}
{{ t('admin.finance.failed') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<DateTimePicker
v-model="dateRange.from"
:placeholder="t('developer.finance.startTime')"
:placeholder="t('admin.finance.startTime')"
class="w-[180px]"
/>
<span class="text-muted-foreground">{{ t('developer.finance.to') }}</span>
<span class="text-muted-foreground">{{ t('admin.finance.to') }}</span>
<DateTimePicker
v-model="dateRange.to"
:placeholder="t('developer.finance.endTime')"
:placeholder="t('admin.finance.endTime')"
class="w-[180px]"
/>
<UiButton variant="ghost" size="sm" @click="setQuickRange('today')">
{{ t('developer.finance.today') }}
{{ t('admin.finance.today') }}
</UiButton>
<UiButton variant="ghost" size="sm" @click="setQuickRange('week')">
{{ t('developer.finance.last7Days') }}
{{ t('admin.finance.last7Days') }}
</UiButton>
<UiButton variant="ghost" size="sm" @click="setQuickRange('month')">
{{ t('developer.finance.thisMonth') }}
{{ t('admin.finance.thisMonth') }}
</UiButton>
</div>
</div>
@@ -534,7 +534,7 @@ onMounted(() => {
>
<div class="flex flex-col items-center justify-center text-muted-foreground">
<Icon icon="lucide:inbox" class="size-12 mb-2 opacity-50" />
<span>{{ t('developer.finance.noRecords') }}</span>
<span>{{ t('admin.finance.noRecords') }}</span>
</div>
</TableCell>
</TableRow>
@@ -551,30 +551,30 @@ onMounted(() => {
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
:confirm-button-text="t('developer.finance.delete')"
:cancel-button-text="t('developer.finance.cancel')"
:confirm-button-text="t('admin.finance.delete')"
:cancel-button-text="t('admin.finance.cancel')"
@confirm="handleDelete"
>
<template #title>
{{ t('developer.finance.delete') }}
{{ t('admin.finance.delete') }}
</template>
<template #description>
{{ t('developer.finance.deleteConfirm') }}
{{ t('admin.finance.deleteConfirm') }}
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
:confirm-button-text="t('developer.finance.delete')"
:cancel-button-text="t('developer.finance.cancel')"
:confirm-button-text="t('admin.finance.delete')"
:cancel-button-text="t('admin.finance.cancel')"
@confirm="handleBatchDelete"
>
<template #title>
{{ t('developer.finance.batchDeleteBtn') }}
{{ t('admin.finance.batchDeleteBtn') }}
</template>
<template #description>
{{ t('developer.finance.batchDeleteConfirm', { count: table.getSelectedRowModel().rows.length }) }}
{{ t('admin.finance.batchDeleteConfirm', { count: table.getSelectedRowModel().rows.length }) }}
</template>
</ConfirmDialog>
</template>
+48 -48
View File
@@ -107,10 +107,10 @@ function getTicketStatusColor(status: string) {
function getTicketStatusText(status: string) {
const texts: Record<string, string> = {
open: t('developer.ticketStatusOpen'),
processing: t('developer.ticketStatusProcessing'),
resolved: t('developer.ticketStatusResolved'),
closed: t('developer.ticketStatusClosed'),
open: t('admin.ticketStatusOpen'),
processing: t('admin.ticketStatusProcessing'),
resolved: t('admin.ticketStatusResolved'),
closed: t('admin.ticketStatusClosed'),
}
return texts[status] || status
}
@@ -127,20 +127,20 @@ function getTicketPriorityColor(priority: string) {
function getTicketPriorityText(priority: string) {
const texts: Record<string, string> = {
low: t('developer.priorityLow'),
normal: t('developer.priorityNormal'),
high: t('developer.priorityHigh'),
urgent: t('developer.priorityUrgent'),
low: t('admin.priorityLow'),
normal: t('admin.priorityNormal'),
high: t('admin.priorityHigh'),
urgent: t('admin.priorityUrgent'),
}
return texts[priority] || priority
}
function goToTicket(ticketId: number) {
router.push(`/developer/tickets/${ticketId}`)
router.push(`/admin/tickets/${ticketId}`)
}
function goToTickets() {
router.push('/developer/tickets')
router.push('/admin/tickets')
}
async function fetchDashboard() {
@@ -196,21 +196,21 @@ async function fetchDashboard() {
if (data.data.recentActivities && Array.isArray(data.data.recentActivities)) {
recentActivities.value = data.data.recentActivities.map((log: any) => {
let icon = 'lucide:activity'
let title = log.action || t('developer.operation')
let description = log.details || log.resource || t('developer.noDescription')
let title = log.action || t('admin.operation')
let description = log.details || log.resource || t('admin.noDescription')
if (log.log_type === 'operation') {
icon = 'lucide:plus-circle'
title = t('developer.operationRecord')
title = t('admin.operationRecord')
}
else if (log.log_type === 'verification') {
icon = 'lucide:check-circle'
title = t('developer.cardVerification')
description = `${t('developer.verification')}${log.status === 'success' ? t('developer.success') : t('developer.failed')}`
title = t('admin.cardVerification')
description = `${t('admin.verification')}${log.status === 'success' ? t('admin.success') : t('admin.failed')}`
}
else if (log.log_type === 'exception') {
icon = 'lucide:alert-circle'
title = t('developer.exceptionRecord')
title = t('admin.exceptionRecord')
}
return {
@@ -238,7 +238,7 @@ async function fetchDashboard() {
}
}
catch (error) {
console.error(t('developer.fetchDashboardError'), error)
console.error(t('admin.fetchDashboardError'), error)
}
finally {
loading.value = false
@@ -276,7 +276,7 @@ async function initMapChart() {
})
}
catch (error) {
console.error(t('developer.initMapError'), error)
console.error(t('admin.initMapError'), error)
}
}
@@ -298,12 +298,12 @@ function updateMapOption() {
if (params.data && params.data.count > 0) {
return `<div style="padding: 8px;">
<div style="font-weight: bold; margin-bottom: 4px; color: #f8fafc;">${params.name}</div>
<div style="color: #cbd5e1;">${t('developer.userCount')}: ${params.data.count}</div>
<div style="color: #cbd5e1;">${t('admin.userCount')}: ${params.data.count}</div>
</div>`
}
return `<div style="padding: 8px;">
<div style="font-weight: bold; margin-bottom: 4px; color: #f8fafc;">${params.name}</div>
<div style="color: #cbd5e1;">${t('developer.noUserData')}</div>
<div style="color: #cbd5e1;">${t('admin.noUserData')}</div>
</div>`
},
},
@@ -342,7 +342,7 @@ function updateMapOption() {
},
series: [
{
name: t('developer.userDistribution'),
name: t('admin.userDistribution'),
type: 'map',
geoIndex: 0,
data,
@@ -383,7 +383,7 @@ function initActivityChart() {
labels,
datasets: [
{
label: t('developer.verificationCount'),
label: t('admin.verificationCount'),
data,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
@@ -413,7 +413,7 @@ function initActivityChart() {
padding: 12,
displayColors: false,
callbacks: {
label: (context: any) => `${context.parsed.y} ${t('developer.online')}`,
label: (context: any) => `${context.parsed.y} ${t('admin.online')}`,
},
},
},
@@ -451,7 +451,7 @@ function initActivityChart() {
})
}
catch (error) {
console.error(t('developer.initActivityChartError'), error)
console.error(t('admin.initActivityChartError'), error)
}
}
@@ -478,14 +478,14 @@ onUnmounted(() => {
<template>
<BasicPage
:title="t('developer.dashboard')"
:description="`${t('developer.welcomeBack')}, ${currentUser?.username || t('developer.developer')}`"
:title="t('admin.dashboard')"
:description="`${t('admin.welcomeBack')}, ${currentUser?.username || t('admin.developer')}`"
>
<div v-if="loading" class="flex items-center justify-center py-12">
<div class="text-center">
<Icon icon="lucide:loader-2" class="size-8 animate-spin mx-auto text-muted-foreground" />
<p class="mt-4 text-muted-foreground">
{{ t('developer.loading') }}...
{{ t('admin.loading') }}...
</p>
</div>
</div>
@@ -495,7 +495,7 @@ onUnmounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.totalApplications') }}
{{ t('admin.totalApplications') }}
</UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -504,7 +504,7 @@ onUnmounted(() => {
{{ formatNumber(stats.totalApplications) }}
</div>
<p class="text-xs text-muted-foreground">
+12.5% {{ t('developer.lastMonth') }}
+12.5% {{ t('admin.lastMonth') }}
</p>
</UiCardContent>
</UiCard>
@@ -512,7 +512,7 @@ onUnmounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.totalCards') }}
{{ t('admin.totalCards') }}
</UiCardTitle>
<Icon icon="lucide:key" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -521,7 +521,7 @@ onUnmounted(() => {
{{ formatNumber(stats.totalCards) }}
</div>
<p class="text-xs text-muted-foreground">
+8.2% {{ t('developer.lastMonth') }}
+8.2% {{ t('admin.lastMonth') }}
</p>
</UiCardContent>
</UiCard>
@@ -529,7 +529,7 @@ onUnmounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.totalUsers') }}
{{ t('admin.totalUsers') }}
</UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -538,7 +538,7 @@ onUnmounted(() => {
{{ formatNumber(totalUsers) }}
</div>
<p class="text-xs text-muted-foreground">
+15.3% {{ t('developer.lastMonth') }}
+15.3% {{ t('admin.lastMonth') }}
</p>
</UiCardContent>
</UiCard>
@@ -546,7 +546,7 @@ onUnmounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.monthlyRevenue') }}
{{ t('admin.monthlyRevenue') }}
</UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -555,7 +555,7 @@ onUnmounted(() => {
¥{{ formatNumber(stats.monthlyRevenue) }}
</div>
<p class="text-xs text-muted-foreground">
+23.1% {{ t('developer.lastMonth') }}
+23.1% {{ t('admin.lastMonth') }}
</p>
</UiCardContent>
</UiCard>
@@ -566,17 +566,17 @@ onUnmounted(() => {
<UiCardHeader>
<div class="flex items-center justify-between">
<div>
<UiCardTitle>{{ t('developer.userDistribution') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.userDistributionDesc') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.userDistribution') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.userDistributionDesc') }}</UiCardDescription>
</div>
<div class="flex items-center gap-4 text-sm">
<div class="flex items-center gap-2">
<div class="size-3 rounded-full bg-blue-500" />
<span class="text-muted-foreground">{{ t('developer.domesticUsers') }}: {{ formatNumber(domesticUsers) }}</span>
<span class="text-muted-foreground">{{ t('admin.domesticUsers') }}: {{ formatNumber(domesticUsers) }}</span>
</div>
<div class="flex items-center gap-2">
<div class="size-3 rounded-full bg-green-500" />
<span class="text-muted-foreground">{{ t('developer.overseasUsers') }}: {{ formatNumber(overseasUsers) }}</span>
<span class="text-muted-foreground">{{ t('admin.overseasUsers') }}: {{ formatNumber(overseasUsers) }}</span>
</div>
</div>
</div>
@@ -590,8 +590,8 @@ onUnmounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.onlineTrend') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.onlineTrendDesc') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.onlineTrend') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.onlineTrendDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="pl-2">
<div class="relative h-[350px]">
@@ -605,11 +605,11 @@ onUnmounted(() => {
<UiCardHeader>
<div class="flex items-center justify-between">
<div>
<UiCardTitle>{{ t('developer.recentTickets') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.recentTicketsDesc') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.recentTickets') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.recentTicketsDesc') }}</UiCardDescription>
</div>
<UiButton variant="ghost" size="sm" @click="goToTickets">
{{ t('developer.viewAll') }}
{{ t('admin.viewAll') }}
<Icon icon="lucide:arrow-right" class="ml-1 size-4" />
</UiButton>
</div>
@@ -646,7 +646,7 @@ onUnmounted(() => {
<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" />
<p class="text-sm">
{{ t('developer.noTickets') }}
{{ t('admin.noTickets') }}
</p>
</div>
</UiCardContent>
@@ -654,8 +654,8 @@ onUnmounted(() => {
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ t('developer.recentActivities') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.recentActivitiesDesc') }}</UiCardDescription>
<UiCardTitle>{{ t('admin.recentActivities') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.recentActivitiesDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent>
<div v-if="recentActivities.length > 0" class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
@@ -685,7 +685,7 @@ onUnmounted(() => {
<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" />
<p class="text-sm">
{{ t('developer.noActivities') }}
{{ t('admin.noActivities') }}
</p>
</div>
</UiCardContent>
+14 -14
View File
@@ -72,14 +72,14 @@ const applicationOptions = computed(() => {
})
const typeOptions = computed(() => [
{ label: t('developer.logs.types.operation'), value: 'operation' },
{ label: t('developer.logs.types.verification'), value: 'verification' },
{ label: t('developer.logs.types.exception'), value: 'exception' },
{ label: t('admin.logs.types.operation'), value: 'operation' },
{ label: t('admin.logs.types.verification'), value: 'verification' },
{ label: t('admin.logs.types.exception'), value: 'exception' },
])
const statusOptions = computed(() => [
{ label: t('developer.success'), value: 'success' },
{ label: t('developer.failed'), value: 'failed' },
{ label: t('admin.success'), value: 'success' },
{ label: t('admin.failed'), value: 'failed' },
])
async function fetchApplications() {
@@ -115,8 +115,8 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.logs.title')"
:description="t('developer.logs.description')"
:title="t('admin.logs.title')"
:description="t('admin.logs.description')"
sticky
>
<div class="space-y-6">
@@ -124,7 +124,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.totalLogs') }}
{{ t('admin.logs.totalLogs') }}
</UiCardTitle>
<Icon icon="lucide:file-text" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -138,7 +138,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.successCount') }}
{{ t('admin.logs.successCount') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -152,7 +152,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.failedCount') }}
{{ t('admin.logs.failedCount') }}
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -166,7 +166,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.operationCount') }}
{{ t('admin.logs.operationCount') }}
</UiCardTitle>
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -188,17 +188,17 @@ onMounted(() => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.logs.columns.application')"
:title="t('admin.logs.columns.application')"
:options="applicationOptions"
/>
<SingleFilter
v-model="typeFilter"
:title="t('developer.logs.columns.type')"
:title="t('admin.logs.columns.type')"
:options="typeOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('developer.logs.columns.status')"
:title="t('admin.logs.columns.status')"
:options="statusOptions"
/>
<DateRangeFilter
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import Badge from '@/components/ui/badge/Badge.vue'
import type { Log } from '@/pages/developer/logs/data/schema'
import type { Log } from '@/pages/admin/logs/data/schema'
function translateDetails(t: Composer['t'], details: string): string {
if (!details) return '-'
@@ -16,222 +16,222 @@ function translateDetails(t: Composer['t'], details: string): string {
}> = [
{
pattern: /^用户注册: (.+)$/,
template: 'developer.logs.details.register',
template: 'admin.logs.details.register',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^注册失败: 用户已存在 - (.+)$/,
template: 'developer.logs.details.register_failed_exists',
template: 'admin.logs.details.register_failed_exists',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^用户登录: (.+)$/,
template: 'developer.logs.details.login',
template: 'admin.logs.details.login',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^登录失败: 用户不存在 - (.+)$/,
template: 'developer.logs.details.login_failed_not_found',
template: 'admin.logs.details.login_failed_not_found',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^登录失败: 密码错误 - (.+)$/,
template: 'developer.logs.details.login_failed_password',
template: 'admin.logs.details.login_failed_password',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^登录失败: 余额不足 - (.+)$/,
template: 'developer.logs.details.login_failed_balance',
template: 'admin.logs.details.login_failed_balance',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^登录失败: 设备绑定数量已达上限 - (.+)$/,
template: 'developer.logs.details.login_failed_device_limit',
template: 'admin.logs.details.login_failed_device_limit',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^登录失败: 多开数量已达上限 - (.+)$/,
template: 'developer.logs.details.login_failed_multi_limit',
template: 'admin.logs.details.login_failed_multi_limit',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^用户充值: (.+), 卡密: (.+), 金额: ([\d.]+)$/,
template: 'developer.logs.details.recharge',
template: 'admin.logs.details.recharge',
extract: (m) => ({ username: m[1], card: m[2], amount: m[3] }),
},
{
pattern: /^充值失败: 用户不存在 - (.+)$/,
template: 'developer.logs.details.recharge_failed_user_not_found',
template: 'admin.logs.details.recharge_failed_user_not_found',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^充值失败: 用户已是永久会员 - (.+)$/,
template: 'developer.logs.details.recharge_failed_permanent',
template: 'admin.logs.details.recharge_failed_permanent',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^充值失败: 卡密不存在 - (.+)$/,
template: 'developer.logs.details.recharge_failed_card_not_found',
template: 'admin.logs.details.recharge_failed_card_not_found',
extract: (m) => ({ card: m[1] }),
},
{
pattern: /^充值失败: 卡密已使用 - (.+)$/,
template: 'developer.logs.details.recharge_failed_card_used',
template: 'admin.logs.details.recharge_failed_card_used',
extract: (m) => ({ card: m[1] }),
},
{
pattern: /^用户解绑设备: (.+)$/,
template: 'developer.logs.details.unbind_device',
template: 'admin.logs.details.unbind_device',
extract: (m) => ({ device: m[1] }),
},
{
pattern: /^扩展API充值: 用户(.+), 类型:(.+), 数量:(\d+)$/,
template: 'developer.logs.details.extension_recharge',
template: 'admin.logs.details.extension_recharge',
extract: (m) => ({ username: m[1], type: m[2], amount: m[3] }),
},
{
pattern: /^扩展API充值失败: 用户不存在 - (.+)$/,
template: 'developer.logs.details.extension_recharge_failed_user_not_found',
template: 'admin.logs.details.extension_recharge_failed_user_not_found',
extract: (m) => ({ userId: m[1] }),
},
{
pattern: /^扩展API充值失败: 用户已是永久会员 - (.+)$/,
template: 'developer.logs.details.extension_recharge_failed_permanent',
template: 'admin.logs.details.extension_recharge_failed_permanent',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^扩展API充值失败: 类型无效 - (.+)$/,
template: 'developer.logs.details.extension_recharge_failed_type',
template: 'admin.logs.details.extension_recharge_failed_type',
extract: (m) => ({ type: m[1] }),
},
{
pattern: /^扩展API充值失败: 保存失败 - (.+)$/,
template: 'developer.logs.details.extension_recharge_failed_save',
template: 'admin.logs.details.extension_recharge_failed_save',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^扩展API扣费: 用户(.+), 类型:(.+), 数量:(\d+)$/,
template: 'developer.logs.details.extension_deduct',
template: 'admin.logs.details.extension_deduct',
extract: (m) => ({ username: m[1], type: m[2], amount: m[3] }),
},
{
pattern: /^扩展API扣费失败: 用户不存在 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_user_not_found',
template: 'admin.logs.details.extension_deduct_failed_user_not_found',
extract: (m) => ({ userId: m[1] }),
},
{
pattern: /^扩展API扣费失败: 用户已是永久会员 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_permanent',
template: 'admin.logs.details.extension_deduct_failed_permanent',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^扩展API扣费失败: 用户订阅已过期 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_expired',
template: 'admin.logs.details.extension_deduct_failed_expired',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^扩展API扣费失败: 余额不足 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_balance',
template: 'admin.logs.details.extension_deduct_failed_balance',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^扩展API扣费失败: 类型无效 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_type',
template: 'admin.logs.details.extension_deduct_failed_type',
extract: (m) => ({ type: m[1] }),
},
{
pattern: /^扩展API扣费失败: 保存失败 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_save',
template: 'admin.logs.details.extension_deduct_failed_save',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^创建应用: (.+)$/,
template: 'developer.logs.details.create_application',
template: 'admin.logs.details.create_application',
extract: (m) => ({ name: m[1] }),
},
{
pattern: /^更新应用: (.+)$/,
template: 'developer.logs.details.update_application',
template: 'admin.logs.details.update_application',
extract: (m) => ({ name: m[1] }),
},
{
pattern: /^删除应用: (.+)$/,
template: 'developer.logs.details.delete_application',
template: 'admin.logs.details.delete_application',
extract: (m) => ({ name: m[1] }),
},
{
pattern: /^创建用户: (.+) \(应用: (.+)\)$/,
template: 'developer.logs.details.create_user',
template: 'admin.logs.details.create_user',
extract: (m) => ({ username: m[1], app: m[2] }),
},
{
pattern: /^更新用户: (.+)$/,
template: 'developer.logs.details.update_user',
template: 'admin.logs.details.update_user',
extract: (m) => ({ username: m[1] }),
},
{
pattern: /^删除用户: (.+) \(应用: (.+)\)$/,
template: 'developer.logs.details.delete_user',
template: 'admin.logs.details.delete_user',
extract: (m) => ({ username: m[1], app: m[2] }),
},
{
pattern: /^解绑设备: (.+) \(应用: (.+)\)$/,
template: 'developer.logs.details.unbind_device_app',
template: 'admin.logs.details.unbind_device_app',
extract: (m) => ({ device: m[1], app: m[2] }),
},
{
pattern: /^批量解绑设备: (\d+)个$/,
template: 'developer.logs.details.batch_unbind',
template: 'admin.logs.details.batch_unbind',
extract: (m) => ({ count: m[1] }),
},
{
pattern: /^创建工单: (.+)$/,
template: 'developer.logs.details.create_ticket',
template: 'admin.logs.details.create_ticket',
extract: (m) => ({ title: m[1] }),
},
{
pattern: /^删除工单: (.+)$/,
template: 'developer.logs.details.delete_ticket',
template: 'admin.logs.details.delete_ticket',
extract: (m) => ({ title: m[1] }),
},
{
pattern: /^创建Webhook: (.+)$/,
template: 'developer.logs.details.create_webhook',
template: 'admin.logs.details.create_webhook',
extract: (m) => ({ name: m[1] }),
},
{
pattern: /^删除Webhook: (.+)$/,
template: 'developer.logs.details.delete_webhook',
template: 'admin.logs.details.delete_webhook',
extract: (m) => ({ name: m[1] }),
},
{
pattern: /^创建云端常量: (.+)$/,
template: 'developer.logs.details.create_cloud_constant',
template: 'admin.logs.details.create_cloud_constant',
extract: (m) => ({ key: m[1] }),
},
{
pattern: /^更新云端常量: (.+)$/,
template: 'developer.logs.details.update_cloud_constant',
template: 'admin.logs.details.update_cloud_constant',
extract: (m) => ({ key: m[1] }),
},
{
pattern: /^删除云端常量: (.+)$/,
template: 'developer.logs.details.delete_cloud_constant',
template: 'admin.logs.details.delete_cloud_constant',
extract: (m) => ({ key: m[1] }),
},
{
pattern: /^创建卡密类型: (.+)$/,
template: 'developer.logs.details.create_card_type',
template: 'admin.logs.details.create_card_type',
extract: (m) => ({ name: m[1] }),
},
{
pattern: /^更新卡密类型: (.+)$/,
template: 'developer.logs.details.update_card_type',
template: 'admin.logs.details.update_card_type',
extract: (m) => ({ name: m[1] }),
},
{
pattern: /^删除卡密类型: (.+)$/,
template: 'developer.logs.details.delete_card_type',
template: 'admin.logs.details.delete_card_type',
extract: (m) => ({ name: m[1] }),
},
]
@@ -251,13 +251,13 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
return [
{
accessorKey: 'log_type',
header: () => t('developer.logs.columns.type'),
header: () => t('admin.logs.columns.type'),
cell: ({ row }) => {
const type = row.getValue('log_type') as string
const typeMap: Record<string, { text: string, variant: any }> = {
operation: { text: t('developer.logs.types.operation'), variant: 'default' },
verification: { text: t('developer.logs.types.verification'), variant: 'secondary' },
exception: { text: t('developer.logs.types.exception'), variant: 'destructive' },
operation: { text: t('admin.logs.types.operation'), variant: 'default' },
verification: { text: t('admin.logs.types.verification'), variant: 'secondary' },
exception: { text: t('admin.logs.types.exception'), variant: 'destructive' },
}
const { text, variant } = typeMap[type] || { text: type, variant: 'default' }
return h(Badge, { variant }, () => text)
@@ -265,18 +265,18 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
},
{
accessorKey: 'action',
header: () => t('developer.logs.columns.action'),
header: () => t('admin.logs.columns.action'),
cell: ({ row }) => {
const action = row.getValue('action') as string
if (!action) return '-'
const actionKey = `developer.logs.actions.${action}`
const actionKey = `dmin.logs.actions.${action}`
const translated = t(actionKey)
return translated === actionKey ? action : translated
},
},
{
accessorKey: 'details',
header: () => t('developer.logs.columns.detail'),
header: () => t('admin.logs.columns.detail'),
cell: ({ row }) => {
const details = row.getValue('details') as string
if (!details) return '-'
@@ -286,12 +286,12 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
},
{
accessorKey: 'status',
header: () => t('developer.logs.columns.status'),
header: () => t('admin.logs.columns.status'),
cell: ({ row }) => {
const status = row.getValue('status') as string
const statusMap: Record<string, { text: string, variant: any }> = {
success: { text: t('developer.success'), variant: 'default' },
failed: { text: t('developer.failed'), variant: 'destructive' },
success: { text: t('admin.success'), variant: 'default' },
failed: { text: t('admin.failed'), variant: 'destructive' },
}
const { text, variant } = statusMap[status] || { text: status, variant: 'default' }
return h(Badge, { variant }, () => text)
@@ -299,7 +299,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
},
{
accessorKey: 'ip_address',
header: () => t('developer.logs.columns.ip'),
header: () => t('admin.logs.columns.ip'),
cell: ({ row }) => {
const ip = row.getValue('ip_address') as string
return h('code', { class: 'text-xs font-mono' }, ip || '-')
@@ -307,7 +307,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
},
{
accessorKey: 'application.name',
header: () => t('developer.logs.columns.application'),
header: () => t('admin.logs.columns.application'),
cell: ({ row }) => {
const appName = row.original.application?.name
return appName ? h(Badge, { variant: 'outline' }, () => appName) : '-'
@@ -315,7 +315,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
},
{
accessorKey: 'user.username',
header: () => t('developer.logs.columns.user'),
header: () => t('admin.logs.columns.user'),
cell: ({ row }) => {
const username = row.original.user?.username || row.original.app_user?.username
return username || '-'
@@ -323,7 +323,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
},
{
accessorKey: 'created_at',
header: () => t('developer.logs.columns.time'),
header: () => t('admin.logs.columns.time'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at')
if (!createdAt) return '-'
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template>
<div class="flex items-center flex-1 space-x-2">
<Input
:placeholder="t('developer.logs.searchPlaceholder')"
:placeholder="t('admin.logs.searchPlaceholder')"
:model-value="(table.getColumn('action')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('action')?.setFilterValue($event.target.value)"
@@ -44,15 +44,15 @@ const table = generateVueTable<Log>({
}, columns.value)
const columnLabels: Record<string, string> = {
'select': 'developer.logs.select',
'type': 'developer.logs.columns.type',
'action': 'developer.logs.columns.action',
'details': 'developer.logs.columns.detail',
'status': 'developer.logs.columns.status',
'ip': 'developer.logs.columns.ip',
'application_name': 'developer.logs.columns.application',
'user_username': 'developer.logs.columns.user',
'created_at': 'developer.logs.columns.time',
'select': 'admin.logs.select',
'type': 'admin.logs.columns.type',
'action': 'admin.logs.columns.action',
'details': 'admin.logs.columns.detail',
'status': 'admin.logs.columns.status',
'ip': 'admin.logs.columns.ip',
'application_name': 'admin.logs.columns.application',
'user_username': 'admin.logs.columns.user',
'created_at': 'admin.logs.columns.time',
}
const serverPagination = computed(() => {
+17 -17
View File
@@ -49,16 +49,16 @@ const applicationOptions = computed(() => {
})
const typeOptions = computed(() => [
{ label: t('developer.logs.types.all'), value: 'all' },
{ label: t('developer.logs.types.operation'), value: 'operation' },
{ label: t('developer.logs.types.verification'), value: 'verification' },
{ label: t('developer.logs.types.exception'), value: 'exception' },
{ label: t('admin.logs.types.all'), value: 'all' },
{ label: t('admin.logs.types.operation'), value: 'operation' },
{ label: t('admin.logs.types.verification'), value: 'verification' },
{ label: t('admin.logs.types.exception'), value: 'exception' },
])
const statusOptions = computed(() => [
{ label: t('developer.logs.statuses.all'), value: 'all' },
{ label: t('developer.success'), value: 'success' },
{ label: t('developer.failed'), value: 'failed' },
{ label: t('admin.logs.statuses.all'), value: 'all' },
{ label: t('admin.success'), value: 'success' },
{ label: t('admin.failed'), value: 'failed' },
])
async function fetchApplications() {
@@ -125,8 +125,8 @@ onMounted(() => {
<template>
<BasicPage
:title="t('developer.logs.title')"
:description="t('developer.logs.description')"
:title="t('admin.logs.title')"
:description="t('admin.logs.description')"
sticky
>
<div class="space-y-6">
@@ -134,7 +134,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.totalLogs') }}
{{ t('admin.logs.totalLogs') }}
</UiCardTitle>
<Icon icon="lucide:file-text" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -148,7 +148,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.successCount') }}
{{ t('admin.logs.successCount') }}
</UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -162,7 +162,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.failedCount') }}
{{ t('admin.logs.failedCount') }}
</UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -176,7 +176,7 @@ onMounted(() => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.operationCount') }}
{{ t('admin.logs.operationCount') }}
</UiCardTitle>
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -205,17 +205,17 @@ onMounted(() => {
<template #filters>
<SingleFilter
v-model="appFilter"
:title="t('developer.logs.columns.application')"
:options="[{ label: t('developer.logs.allApplications'), value: 'all' }, ...applicationOptions]"
:title="t('admin.logs.columns.application')"
:options="[{ label: t('admin.logs.allApplications'), value: 'all' }, ...applicationOptions]"
/>
<SingleFilter
v-model="typeFilter"
:title="t('developer.logs.columns.type')"
:title="t('admin.logs.columns.type')"
:options="typeOptions"
/>
<SingleFilter
v-model="statusFilter"
:title="t('developer.logs.columns.status')"
:title="t('admin.logs.columns.status')"
:options="statusOptions"
/>
<DateRangeFilter
+240 -271
View File
@@ -1,320 +1,289 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import CountrySelect from '@/components/country-select.vue'
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import DataTable from '@/pages/admin/risk-control/components/data-table.vue'
import api from '@/services/api'
const router = useRouter()
const route = useRoute()
const loading = ref(true)
const saving = ref(false)
const rules = ref<RiskRule[]>([])
const tableRef = ref()
const form = ref({
type: 'ip' as 'ip' | 'device' | 'user' | 'region',
value: '',
reason: '',
expires_at: '',
const typeFilter = ref<string>('')
const statusFilter = ref<string>('')
const deleteDialogOpen = ref(false)
const deleteTarget = ref<RiskRule | null>(null)
const batchDeleteDialogOpen = ref(false)
const batchDeleteIds = ref<(string | number)[]>([])
const typeOptions = computed(() => [
{ label: 'IP封禁', value: 'ip' },
{ label: '设备封禁', value: 'device' },
{ label: '用户封禁', value: 'user' },
{ label: '地区封禁', value: 'region' },
])
const statusOptions = computed(() => [
{ label: '生效中', value: 'active' },
{ label: '已失效', value: 'inactive' },
])
const filteredRules = computed(() => {
let result = rules.value
if (typeFilter.value) {
result = result.filter(rule => rule.type === typeFilter.value)
}
if (statusFilter.value) {
result = result.filter(rule => rule.status === statusFilter.value)
}
return result
})
const typeOptions: { value: 'ip' | 'device' | 'user' | 'region', label: string, icon: string, description: string, placeholder: string }[] = [
{ value: 'ip', label: 'IP封禁', icon: 'lucide:globe', description: '封禁特定IP地址或IP段', placeholder: '输入IP地址,如 192.168.1.1 或 192.168.1.0/24' },
{ value: 'device', label: '设备封禁', icon: 'lucide:smartphone', description: '封禁特定设备指纹', placeholder: '输入设备指纹' },
{ value: 'user', label: '用户封禁', icon: 'lucide:user-x', description: '封禁特定用户账号', placeholder: '输入用户ID或用户名' },
{ value: 'region', label: '地区封禁', icon: 'lucide:map-pin', description: '封禁特定地区访问', placeholder: '' },
]
function getTypeInfo(type: string) {
return typeOptions.find(t => t.value === type) || typeOptions[0]
}
const countries: Record<string, { name: string, flag: string }> = {
CN: { name: '中国', flag: '🇨🇳' },
US: { name: '美国', flag: '🇺🇸' },
JP: { name: '日本', flag: '🇯🇵' },
KR: { name: '韩国', flag: '🇰🇷' },
GB: { name: '英国', flag: '🇬🇧' },
DE: { name: '德国', flag: '🇩🇪' },
FR: { name: '法国', flag: '🇫🇷' },
IT: { name: '意大利', flag: '🇮🇹' },
ES: { name: '西班牙', flag: '🇪🇸' },
RU: { name: '俄罗斯', flag: '🇷🇺' },
CA: { name: '加拿大', flag: '🇨🇦' },
AU: { name: '澳大利亚', flag: '🇦🇺' },
BR: { name: '巴西', flag: '🇧🇷' },
IN: { name: '印度', flag: '🇮🇳' },
MX: { name: '墨西哥', flag: '🇲🇽' },
NL: { name: '荷兰', flag: '🇳🇱' },
SG: { name: '新加坡', flag: '🇸🇬' },
HK: { name: '中国香港', flag: '🇭🇰' },
TW: { name: '中国台湾', flag: '🇹🇼' },
TH: { name: '泰国', flag: '🇹🇭' },
VN: { name: '越南', flag: '🇻🇳' },
MY: { name: '马来西亚', flag: '🇲🇾' },
ID: { name: '印度尼西亚', flag: '🇮🇩' },
PH: { name: '菲律宾', flag: '🇵🇭' },
AE: { name: '阿联酋', flag: '🇦🇪' },
SA: { name: '沙特阿拉伯', flag: '🇸🇦' },
TR: { name: '土耳其', flag: '🇹🇷' },
PL: { name: '波兰', flag: '🇵🇱' },
SE: { name: '瑞典', flag: '🇸🇪' },
CH: { name: '瑞士', flag: '🇨🇭' },
AT: { name: '奥地利', flag: '🇦🇹' },
BE: { name: '比利时', flag: '🇧🇪' },
DK: { name: '丹麦', flag: '🇩🇰' },
FI: { name: '芬兰', flag: '🇫🇮' },
NO: { name: '挪威', flag: '🇳🇴' },
IE: { name: '爱尔兰', flag: '🇮🇪' },
PT: { name: '葡萄牙', flag: '🇵🇹' },
CZ: { name: '捷克', flag: '🇨🇿' },
RO: { name: '罗马尼亚', flag: '🇷🇴' },
HU: { name: '匈牙利', flag: '🇭🇺' },
IL: { name: '以色列', flag: '🇮🇱' },
ZA: { name: '南非', flag: '🇿🇦' },
EG: { name: '埃及', flag: '🇪🇬' },
NG: { name: '尼日利亚', flag: '🇳🇬' },
AR: { name: '阿根廷', flag: '🇦🇷' },
CL: { name: '智利', flag: '🇨🇱' },
CO: { name: '哥伦比亚', flag: '🇨🇴' },
PE: { name: '秘鲁', flag: '🇵🇪' },
NZ: { name: '新西兰', flag: '🇳🇿' },
}
function getCountryDisplay(code: string) {
const country = countries[code]
return country ? `${country.flag} ${country.name} (${code})` : code
}
async function fetchRule() {
const id = route.params.id
if (!id) {
toast.error('规则ID不存在')
router.push('/developer/risk-control')
return
}
const activeCount = computed(() => filteredRules.value.filter(rule => rule.status === 'active').length)
const inactiveCount = computed(() => filteredRules.value.filter(rule => rule.status === 'inactive').length)
const ipCount = computed(() => filteredRules.value.filter(rule => rule.type === 'ip').length)
const deviceCount = computed(() => filteredRules.value.filter(rule => rule.type === 'device').length)
async function fetchRules() {
loading.value = true
try {
const data = await api.get<any>(`/dev/risk-control/rules`)
const rules = Array.isArray(data) ? data : []
const rule = rules.find((r: any) => String(r.id) === String(id))
if (!rule) {
toast.error('规则不存在')
router.push('/developer/risk-control')
return
}
form.value = {
type: rule.type,
value: rule.value,
reason: rule.reason || '',
expires_at: rule.expires_at ? rule.expires_at.replace(' ', 'T').slice(0, 16) : '',
}
const data = await api.get<RiskRule[]>('/dev/risk-control/rules')
rules.value = Array.isArray(data) ? data : []
}
catch (error) {
console.error('获取规则详情失败:', error)
toast.error('获取规则详情失败')
router.push('/developer/risk-control')
console.error('获取风控规则失败:', error)
rules.value = []
}
finally {
loading.value = false
}
}
async function handleSave() {
if (!form.value.value.trim()) {
toast.error('请输入封禁值')
return
}
function goToCreate() {
router.push('/admin/risk-control/create')
}
const id = route.params.id
saving.value = true
function openEditPage(rule: RiskRule) {
router.push(`/admin/risk-control/${rule.id}`)
}
async function toggleStatus(rule: RiskRule) {
try {
await api.put(`/dev/risk-control/rules/${id}`, {
type: form.value.type,
value: form.value.value,
reason: form.value.reason,
expires_at: form.value.expires_at || null,
})
toast.success('更新成功')
router.push('/developer/risk-control')
const newStatus = rule.status === 'active' ? 'inactive' : 'active'
await api.put(`/dev/risk-control/rules/${rule.id}/status`, { status: newStatus })
toast.success('状态更新成功')
fetchRules()
}
catch (error: any) {
console.error('更新风控规则失败:', error)
toast.error(error.message || '更新失败')
console.error('切换状态失败:', error)
toast.error(error.message || '状态更新失败')
}
}
function confirmDelete(rule: RiskRule) {
deleteTarget.value = rule
deleteDialogOpen.value = true
}
async function handleDelete() {
if (!deleteTarget.value)
return
try {
await api.delete(`/dev/risk-control/rules/${deleteTarget.value.id}`)
toast.success('删除成功')
fetchRules()
}
catch (error: any) {
console.error('删除风控规则失败:', error)
toast.error(error.message || '删除失败')
}
finally {
saving.value = false
deleteTarget.value = null
}
}
function confirmBatchDelete(ids: (string | number)[]) {
batchDeleteIds.value = ids
batchDeleteDialogOpen.value = true
}
async function handleBatchDelete() {
try {
await api.delete('/dev/risk-control/rules/batch', { ids: batchDeleteIds.value } as any)
toast.success('批量删除成功')
fetchRules()
}
catch (error: any) {
console.error('批量删除失败:', error)
toast.error(error.message || '批量删除失败')
}
finally {
batchDeleteIds.value = []
}
}
async function batchToggleStatus(ids: (string | number)[], status: string) {
try {
await api.put('/dev/risk-control/rules/batch/status', { ids, status })
toast.success('批量更新状态成功')
fetchRules()
}
catch (error: any) {
console.error('批量更新状态失败:', error)
toast.error(error.message || '批量更新状态失败')
}
}
onMounted(() => {
fetchRule()
fetchRules()
})
</script>
<template>
<BasicPage
title="编辑规则"
description="编辑现有风控规则"
:breadcrumbs="[
{ title: '风控管理', href: '/developer/risk-control' },
{ title: '编辑规则' },
]"
title="风控管理"
description="管理封禁的IP、设备、用户等,保护应用安全"
sticky
>
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
</div>
<template #actions>
<UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
添加规则
</UiButton>
</template>
<div v-else class="space-y-6">
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:shield-edit" class="size-5" />
规则配置
</UiCardTitle>
<UiCardDescription>修改风控规则的类型和封禁目标</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-3">
<UiLabel>规则类型</UiLabel>
<div class="grid gap-3 sm:grid-cols-2">
<button
v-for="option in typeOptions"
:key="option.value"
type="button"
class="flex items-start gap-3 p-4 rounded-lg border text-left transition-all duration-200"
:class="form.type === option.value ? 'border-primary bg-primary/5 ring-1 ring-primary' : 'border-border hover:border-primary/50 hover:bg-muted/50'"
@click="form.type = option.value"
>
<Icon
:icon="option.icon"
class="size-5 mt-0.5 transition-colors duration-200"
:class="form.type === option.value ? 'text-primary' : 'text-muted-foreground'"
/>
<div>
<div
class="font-medium transition-colors duration-200"
:class="form.type === option.value ? 'text-primary' : ''"
>
{{ option.label }}
</div>
<div class="text-sm text-muted-foreground">
{{ option.description }}
</div>
</div>
</button>
</div>
</div>
<div class="space-y-6">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
生效中
</UiCardTitle>
<Icon icon="lucide:shield-check" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ activeCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="value">
封禁值
</UiLabel>
<CountrySelect
v-if="form.type === 'region'"
v-model="form.value"
/>
<UiInput
v-else
id="value"
:key="form.type"
v-model="form.value"
:placeholder="getTypeInfo(form.type).placeholder"
/>
<p class="text-xs text-muted-foreground">
{{ form.type === 'region' ? '选择要封禁的国家或地区' : '根据规则类型输入对应的封禁目标' }}
</p>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
已失效
</UiCardTitle>
<Icon icon="lucide:shield-off" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ inactiveCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="reason">
封禁原因
</UiLabel>
<UiInput id="reason" v-model="form.reason" placeholder="输入封禁原因(可选)" />
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
IP封禁
</UiCardTitle>
<Icon icon="lucide:globe" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ ipCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel for="expires">
过期时间
</UiLabel>
<UiInput id="expires" v-model="form.expires_at" type="datetime-local" />
<p class="text-xs text-muted-foreground">
留空表示永久封禁
</p>
</div>
</UiCardContent>
</UiCard>
</div>
<div class="space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" />
规则预览
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">规则类型</span>
<span class="flex items-center gap-1">
<Icon :icon="getTypeInfo(form.type).icon" class="size-4" />
{{ getTypeInfo(form.type).label }}
</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">封禁值</span>
<span class="font-mono">
{{ form.type === 'region' && form.value ? getCountryDisplay(form.value) : (form.value || '-') }}
</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">封禁原因</span>
<span>{{ form.reason || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">过期时间</span>
<span>{{ form.expires_at ? new Date(form.expires_at).toLocaleString('zh-CN') : '永久' }}</span>
</div>
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardContent class="pt-6">
<div class="flex flex-col gap-3">
<UiButton
class="w-full"
size="lg"
:disabled="saving || !form.value.trim()"
@click="handleSave"
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
保存修改
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
取消
</UiButton>
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
设备封禁
</UiCardTitle>
<Icon icon="lucide:smartphone" class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ deviceCount }}
</div>
</UiCardContent>
</UiCard>
</div>
<UiCard>
<UiCardContent class="p-6">
<DataTable
ref="tableRef"
:loading
:data="filteredRules"
:on-toggle-status="toggleStatus"
:on-edit="openEditPage"
:on-delete="confirmDelete"
@refresh="fetchRules"
@batch-disable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'active').map((r: any) => r.original.id) || [], 'inactive')"
@batch-enable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'inactive').map((r: any) => r.original.id) || [], 'active')"
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
>
<template #filters>
<SingleFilter
v-model="typeFilter"
title="规则类型"
:options="typeOptions"
/>
<SingleFilter
v-model="statusFilter"
title="状态"
:options="statusOptions"
/>
</template>
</DataTable>
</UiCardContent>
</UiCard>
</div>
<ConfirmDialog
v-model:open="deleteDialogOpen"
destructive
confirm-button-text="删除"
cancel-button-text="取消"
@confirm="handleDelete"
>
<template #title>
删除风控规则
</template>
<template #description>
确定要删除该风控规则吗此操作不可撤销
</template>
</ConfirmDialog>
<ConfirmDialog
v-model:open="batchDeleteDialogOpen"
destructive
confirm-button-text="删除"
cancel-button-text="取消"
@confirm="handleBatchDelete"
>
<template #title>
批量删除风控规则
</template>
<template #description>
确定要删除选中的 {{ batchDeleteIds.length }} 条规则吗此操作不可撤销
</template>
</ConfirmDialog>
</BasicPage>
</template>
@@ -3,7 +3,7 @@ import type { ColumnDef } from '@tanstack/vue-table'
import { MoreHorizontal, Pencil, ShieldCheck, ShieldOff, Trash2 } from 'lucide-vue-next'
import { h } from 'vue'
import type { RiskRule } from '@/pages/developer/risk-control/data/schema'
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
import { Copy } from '@/components/sva-ui/copy'
import Badge from '@/components/ui/badge/Badge.vue'
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Table } from '@tanstack/vue-table'
import type { RiskRule } from '@/pages/developer/risk-control/data/schema'
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
import { Input } from '@/components/ui/input'
@@ -2,15 +2,15 @@
import type { ColumnDef } from '@tanstack/vue-table'
import type { DataTableProps } from '@/components/data-table/types'
import type { RiskRule } from '@/pages/developer/risk-control/data/schema'
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
import BulkActions from '@/components/data-table/bulk-actions.vue'
import DataTable from '@/components/data-table/data-table.vue'
import { SelectColumn } from '@/components/data-table/table-columns'
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
import DataTableViewOptions from '@/components/data-table/view-options.vue'
import { getColumns } from '@/pages/developer/risk-control/components/columns'
import DataTableToolbar from '@/pages/developer/risk-control/components/data-table-toolbar.vue'
import { getColumns } from '@/pages/admin/risk-control/components/columns'
import DataTableToolbar from '@/pages/admin/risk-control/components/data-table-toolbar.vue'
const props = defineProps<Omit<DataTableProps<RiskRule>, 'columns'> & {
onToggleStatus: (row: RiskRule) => void
@@ -165,7 +165,7 @@ async function handleSave() {
await api.post('/dev/risk-control/rules', payload)
toast.success('添加成功')
router.push('/developer/risk-control')
router.push('/admin/risk-control')
}
catch (error: any) {
console.error('添加风控规则失败:', error)
@@ -182,7 +182,7 @@ async function handleSave() {
title="添加规则"
description="创建新的风控规则"
:breadcrumbs="[
{ title: '风控管理', href: '/developer/risk-control' },
{ title: '风控管理', href: '/admin/risk-control' },
{ title: '添加规则' },
]"
sticky
@@ -4,12 +4,12 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import type { RiskRule } from '@/pages/developer/risk-control/data/schema'
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout'
import DataTable from '@/pages/developer/risk-control/components/data-table.vue'
import DataTable from '@/pages/admin/risk-control/components/data-table.vue'
import api from '@/services/api'
const router = useRouter()
@@ -73,11 +73,11 @@ async function fetchRules() {
}
function goToCreate() {
router.push('/developer/risk-control/create')
router.push('/admin/risk-control/create')
}
function openEditPage(rule: RiskRule) {
router.push(`/developer/risk-control/${rule.id}`)
router.push(`/admin/risk-control/${rule.id}`)
}
async function toggleStatus(rule: RiskRule) {

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