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"` ApiToken string `gorm:"size:64" json:"api_token"`
ParentAgentID *uint `gorm:"index" json:"parent_agent_id"` ParentAgentID *uint `gorm:"index" json:"parent_agent_id"`
Commission float64 `gorm:"default:0" json:"commission"`
CanCreateAgent bool `gorm:"default:false" json:"can_create_agent"` CanCreateAgent bool `gorm:"default:false" json:"can_create_agent"`
Balance float64 `gorm:"default:0" json:"balance"` Balance float64 `gorm:"default:0" json:"balance"`
@@ -557,7 +556,6 @@ type AgentApplication struct {
AgentID uint `json:"agent_id"` AgentID uint `json:"agent_id"`
ApplicationID uint `json:"application_id"` ApplicationID uint `json:"application_id"`
DeveloperID uint `json:"developer_id"` DeveloperID uint `json:"developer_id"`
Commission float64 `gorm:"default:0.1" json:"commission"`
Discount float64 `gorm:"default:1.0" json:"discount"` Discount float64 `gorm:"default:1.0" json:"discount"`
Status string `gorm:"size:20;default:active" json:"status"` Status string `gorm:"size:20;default:active" json:"status"`
IsReceived bool `gorm:"default:false" json:"is_received"` IsReceived bool `gorm:"default:false" json:"is_received"`
-3
View File
@@ -80,7 +80,6 @@ func handleGetApps(c *gin.Context) {
"description": aa.Application.Description, "description": aa.Application.Description,
"status": aa.Application.Status, "status": aa.Application.Status,
"authorized_at": aa.CreatedAt, "authorized_at": aa.CreatedAt,
"commission": aa.Commission,
}) })
} }
} }
@@ -224,7 +223,6 @@ func handleGetFinance(c *gin.Context) {
response.Success(c, gin.H{ response.Success(c, gin.H{
"balance": user.Balance, "balance": user.Balance,
"commission": user.Commission,
"records": records, "records": records,
}) })
} }
@@ -244,7 +242,6 @@ func handleGetProfile(c *gin.Context) {
"email": user.Email, "email": user.Email,
"avatar": user.Avatar, "avatar": user.Avatar,
"balance": user.Balance, "balance": user.Balance,
"commission": user.Commission,
"can_create_agent": user.CanCreateAgent, "can_create_agent": user.CanCreateAgent,
}) })
} }
@@ -137,7 +137,6 @@ func handleGetAgentApps(c *gin.Context) {
AgentEmail string `json:"agent_email"` AgentEmail string `json:"agent_email"`
ApplicationID uint `json:"application_id"` ApplicationID uint `json:"application_id"`
AppName string `json:"app_name"` AppName string `json:"app_name"`
Commission float64 `json:"commission"`
Discount float64 `json:"discount"` Discount float64 `json:"discount"`
Status string `json:"status"` Status string `json:"status"`
CardTypes []CardTypeResponse `json:"card_types"` CardTypes []CardTypeResponse `json:"card_types"`
@@ -181,7 +180,6 @@ func handleGetAgentApps(c *gin.Context) {
AgentEmail: agentEmail, AgentEmail: agentEmail,
ApplicationID: aa.ApplicationID, ApplicationID: aa.ApplicationID,
AppName: appName, AppName: appName,
Commission: aa.Commission,
Discount: aa.Discount, Discount: aa.Discount,
Status: aa.Status, Status: aa.Status,
CardTypes: cardTypes, CardTypes: cardTypes,
@@ -496,7 +494,6 @@ func handleApproveRequest(c *gin.Context) {
AgentID: req.AgentID, AgentID: req.AgentID,
ApplicationID: req.ApplicationID, ApplicationID: req.ApplicationID,
DeveloperID: userID, DeveloperID: userID,
Commission: 0.1,
Discount: 1.0, Discount: 1.0,
Status: "active", Status: "active",
IsReceived: true, IsReceived: true,
@@ -606,7 +603,6 @@ func handleGetAgentAppDetail(c *gin.Context) {
AgentEmail string `json:"agent_email"` AgentEmail string `json:"agent_email"`
ApplicationID uint `json:"application_id"` ApplicationID uint `json:"application_id"`
AppName string `json:"app_name"` AppName string `json:"app_name"`
Commission float64 `json:"commission"`
Discount float64 `json:"discount"` Discount float64 `json:"discount"`
Status string `json:"status"` Status string `json:"status"`
CardTypes []CardTypeResponse `json:"card_types"` CardTypes []CardTypeResponse `json:"card_types"`
@@ -629,7 +625,6 @@ func handleGetAgentAppDetail(c *gin.Context) {
AgentName: agent.Username, AgentName: agent.Username,
ApplicationID: agentApp.ApplicationID, ApplicationID: agentApp.ApplicationID,
AppName: application.Name, AppName: application.Name,
Commission: agentApp.Commission,
Discount: agentApp.Discount, Discount: agentApp.Discount,
Status: agentApp.Status, Status: agentApp.Status,
CardTypes: cardTypes, CardTypes: cardTypes,
@@ -648,7 +643,6 @@ func handleUpdateAgentApp(c *gin.Context) {
agentAppID := c.Param("id") agentAppID := c.Param("id")
var reqBody struct { var reqBody struct {
Commission *float64 `json:"commission"`
Discount *float64 `json:"discount"` Discount *float64 `json:"discount"`
Status *string `json:"status"` Status *string `json:"status"`
} }
@@ -664,9 +658,6 @@ func handleUpdateAgentApp(c *gin.Context) {
} }
updates := make(map[string]interface{}) updates := make(map[string]interface{})
if reqBody.Commission != nil {
updates["commission"] = *reqBody.Commission
}
if reqBody.Discount != nil { if reqBody.Discount != nil {
updates["discount"] = *reqBody.Discount updates["discount"] = *reqBody.Discount
} }
@@ -40,7 +40,6 @@ type AgentTreeNode struct {
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
LastLoginAt string `json:"last_login_at"` LastLoginAt string `json:"last_login_at"`
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
Commission float64 `json:"commission"`
CanCreateAgent bool `json:"can_create_agent"` CanCreateAgent bool `json:"can_create_agent"`
CardsCount int `json:"cards_count"` CardsCount int `json:"cards_count"`
ChildAgentsCount int `json:"child_agents_count"` ChildAgentsCount int `json:"child_agents_count"`
@@ -69,7 +68,6 @@ func handleGetAgents(c *gin.Context) {
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
LastLoginAt string `json:"last_login_at"` LastLoginAt string `json:"last_login_at"`
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
Commission float64 `json:"commission"`
CanCreateAgent bool `json:"can_create_agent"` CanCreateAgent bool `json:"can_create_agent"`
CardsCount int `json:"cards_count"` CardsCount int `json:"cards_count"`
ChildAgentsCount int `json:"child_agents_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"), CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
LastLoginAt: lastLoginAt, LastLoginAt: lastLoginAt,
Balance: user.Balance, Balance: user.Balance,
Commission: user.Commission,
CanCreateAgent: user.CanCreateAgent, CanCreateAgent: user.CanCreateAgent,
CardsCount: int(cardsCount), CardsCount: int(cardsCount),
ChildAgentsCount: int(childAgentsCount), ChildAgentsCount: int(childAgentsCount),
@@ -185,7 +182,6 @@ func handleGetAgentsTree(c *gin.Context) {
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
LastLoginAt: lastLoginAt, LastLoginAt: lastLoginAt,
Balance: user.Balance, Balance: user.Balance,
Commission: user.Commission,
CanCreateAgent: user.CanCreateAgent, CanCreateAgent: user.CanCreateAgent,
CardsCount: int(cardsCount), CardsCount: int(cardsCount),
ChildAgentsCount: int(childAgentsCount), ChildAgentsCount: int(childAgentsCount),
@@ -217,7 +213,6 @@ func handleCreateAgent(c *gin.Context) {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password" binding:"required"` Password string `json:"password" binding:"required"`
ParentAgentID *uint `json:"parent_agent_id"` ParentAgentID *uint `json:"parent_agent_id"`
Commission float64 `json:"commission"`
CanCreateAgent bool `json:"can_create_agent"` CanCreateAgent bool `json:"can_create_agent"`
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
} }
@@ -259,7 +254,6 @@ func handleCreateAgent(c *gin.Context) {
Role: "agent", Role: "agent",
Status: "active", Status: "active",
ParentAgentID: req.ParentAgentID, ParentAgentID: req.ParentAgentID,
Commission: req.Commission,
CanCreateAgent: req.CanCreateAgent, CanCreateAgent: req.CanCreateAgent,
Balance: req.Balance, Balance: req.Balance,
} }
@@ -365,7 +359,6 @@ func handleGetAgentDetail(c *gin.Context) {
"role": user.Role, "role": user.Role,
"parent_agent_id": user.ParentAgentID, "parent_agent_id": user.ParentAgentID,
"parent_agent_name": parentAgentName, "parent_agent_name": parentAgentName,
"commission": user.Commission,
"can_create_agent": user.CanCreateAgent, "can_create_agent": user.CanCreateAgent,
"balance": user.Balance, "balance": user.Balance,
"created_at": user.CreatedAt.Format("2006-01-02 15:04:05"), "created_at": user.CreatedAt.Format("2006-01-02 15:04:05"),
@@ -387,7 +380,6 @@ func handleUpdateAgent(c *gin.Context) {
var req struct { var req struct {
ParentAgentID *uint `json:"parent_agent_id"` ParentAgentID *uint `json:"parent_agent_id"`
Commission *float64 `json:"commission"`
Email *string `json:"email"` Email *string `json:"email"`
Password *string `json:"password"` Password *string `json:"password"`
CanCreateAgent *bool `json:"can_create_agent"` CanCreateAgent *bool `json:"can_create_agent"`
@@ -414,9 +406,6 @@ func handleUpdateAgent(c *gin.Context) {
if req.Email != nil { if req.Email != nil {
user.Email = req.Email user.Email = req.Email
} }
if req.Commission != nil {
user.Commission = *req.Commission
}
if req.Password != nil && *req.Password != "" { if req.Password != nil && *req.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
+1 -1
View File
@@ -153,7 +153,7 @@ func main() {
}, },
"如何实现代理授权?": { "如何实现代理授权?": {
TitleEn: "How to Implement Agent Authorization?", 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": { "faq-agent": {
TitleEn: "How to Implement Agent Authorization?", 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", SummaryEn: "Implementation process and permission management for agent authorization",
}, },
} }
@@ -35,7 +35,7 @@ const displayValue = computed(() => {
return '' 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') { function setQuickRange(range: 'today' | 'week' | 'month') {
const now = new Date() const now = new Date()
@@ -83,19 +83,19 @@ function clearFilter() {
<UiPopoverContent class="w-auto p-4" align="start"> <UiPopoverContent class="w-auto p-4" align="start">
<div class="space-y-3"> <div class="space-y-3">
<div class="text-sm font-medium"> <div class="text-sm font-medium">
{{ t('developer.cards.selectTimeRange') }} {{ t('admin.cards.selectTimeRange') }}
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<DateTimePicker <DateTimePicker
:model-value="startModelValue" :model-value="startModelValue"
:placeholder="t('developer.cards.startTime')" :placeholder="t('admin.cards.startTime')"
class="w-[180px]" class="w-[180px]"
@update:model-value="emit('update:startModelValue', $event)" @update:model-value="emit('update:startModelValue', $event)"
/> />
<span class="text-muted-foreground text-sm">~</span> <span class="text-muted-foreground text-sm">~</span>
<DateTimePicker <DateTimePicker
:model-value="endModelValue" :model-value="endModelValue"
:placeholder="t('developer.cards.endTime')" :placeholder="t('admin.cards.endTime')"
class="w-[180px]" class="w-[180px]"
@update:model-value="emit('update:endModelValue', $event)" @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 justify-between">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('today')"> <UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('today')">
{{ t('developer.cards.today') }} {{ t('admin.cards.today') }}
</UiButton> </UiButton>
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('week')"> <UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('week')">
{{ t('developer.cards.last7Days') }} {{ t('admin.cards.last7Days') }}
</UiButton> </UiButton>
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('month')"> <UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('month')">
{{ t('developer.cards.thisMonth') }} {{ t('admin.cards.thisMonth') }}
</UiButton> </UiButton>
</div> </div>
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="clearFilter"> <UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="clearFilter">
{{ t('developer.cards.reset') }} {{ t('admin.cards.reset') }}
</UiButton> </UiButton>
</div> </div>
</div> </div>
@@ -70,7 +70,7 @@ const filterFunction = (list: DataTableFacetedFilter['options'], term: string) =
v-for="option in options" v-for="option in options"
:key="option.value" :key="option.value"
:value="option" :value="option"
@select="(_e) => { @select="(_e: any) => {
const isSelected = selectedValues.has(option.value) const isSelected = selectedValues.has(option.value)
if (isSelected) { if (isSelected) {
selectedValues.delete(option.value) selectedValues.delete(option.value)
@@ -31,7 +31,7 @@ onMounted(() => {
const teams = [ const teams = [
{ {
name: '开发者后台', name: '管理后台',
logo: Code, logo: Code,
plan: 'Developer', plan: 'Developer',
}, },
@@ -43,42 +43,42 @@ const navMain = [
items: [ items: [
{ {
title: '控制台', title: '控制台',
url: '/developer', url: '/admin',
icon: Gauge, icon: Gauge,
}, },
{ {
title: '应用管理', title: '应用管理',
url: '/developer/applications', url: '/admin/applications',
icon: Boxes, icon: Boxes,
}, },
{ {
title: '卡类管理', title: '卡类管理',
url: '/developer/card-types', url: '/admin/card-types',
icon: Hash, icon: Hash,
}, },
{ {
title: '卡密管理', title: '卡密管理',
url: '/developer/cards', url: '/admin/cards',
icon: Key, icon: Key,
}, },
{ {
title: '公告管理', title: '公告管理',
url: '/developer/announcements', url: '/admin/announcements',
icon: Megaphone, icon: Megaphone,
}, },
{ {
title: '版本管理', title: '版本管理',
url: '/developer/versions', url: '/admin/versions',
icon: GitBranch, icon: GitBranch,
}, },
{ {
title: '用户管理', title: '用户管理',
url: '/developer/users', url: '/admin/users',
icon: Users, icon: Users,
}, },
{ {
title: '代理管理', title: '代理管理',
url: '/developer/agents', url: '/admin/agents',
icon: Network, icon: Network,
}, },
], ],
@@ -88,17 +88,17 @@ const navMain = [
items: [ items: [
{ {
title: '财务管理', title: '财务管理',
url: '/developer/finance', url: '/admin/finance',
icon: DollarSign, icon: DollarSign,
}, },
{ {
title: '日志记录', title: '日志记录',
url: '/developer/logs', url: '/admin/logs',
icon: ScrollText, icon: ScrollText,
}, },
{ {
title: '工单系统', title: '工单系统',
url: '/developer/tickets', url: '/admin/tickets',
icon: MessageSquare, icon: MessageSquare,
}, },
], ],
@@ -108,27 +108,27 @@ const navMain = [
items: [ items: [
{ {
title: '云端常量', title: '云端常量',
url: '/developer/cloud-constants', url: '/admin/cloud-constants',
icon: FileLock, icon: FileLock,
}, },
{ {
title: '云端变量', title: '云端变量',
url: '/developer/cloud-variables', url: '/admin/cloud-variables',
icon: Variable, icon: Variable,
}, },
{ {
title: '云端函数', title: '云端函数',
url: '/developer/cloud-function', url: '/admin/cloud-function',
icon: Code, icon: Code,
}, },
{ {
title: '风控管理', title: '风控管理',
url: '/developer/risk-control', url: '/admin/risk-control',
icon: Shield, icon: Shield,
}, },
{ {
title: '扩展配置', title: '扩展配置',
url: '/developer/extension', url: '/admin/extension',
icon: Plug, icon: Plug,
}, },
], ],
@@ -139,11 +139,11 @@ function handleLogout() {
<UiDropdownMenuSeparator /> <UiDropdownMenuSeparator />
<UiDropdownMenuGroup> <UiDropdownMenuGroup>
<UiDropdownMenuItem @click="router.push('/developer/profile')"> <UiDropdownMenuItem @click="router.push('/admin/profile')">
<UserRoundCog class="mr-2 h-4 w-4" /> <UserRoundCog class="mr-2 h-4 w-4" />
个人中心 个人中心
</UiDropdownMenuItem> </UiDropdownMenuItem>
<UiDropdownMenuItem @click="router.push('/developer/tickets')"> <UiDropdownMenuItem @click="router.push('/admin/tickets')">
<Icon icon="lucide:ticket" class="mr-2 h-4 w-4" /> <Icon icon="lucide:ticket" class="mr-2 h-4 w-4" />
工单系统 工单系统
</UiDropdownMenuItem> </UiDropdownMenuItem>
@@ -66,34 +66,34 @@ const developerMenus = computed(() => ({
main: { main: {
title: t('nav.mainFeatures'), title: t('nav.mainFeatures'),
items: [ items: [
{ title: t('nav.console'), url: '/developer', icon: 'lucide:gauge' }, { title: t('nav.console'), url: '/admin', icon: 'lucide:gauge' },
{ title: t('nav.applications'), url: '/developer/applications', icon: 'lucide:boxes' }, { title: t('nav.applications'), url: '/admin/applications', icon: 'lucide:boxes' },
{ title: t('nav.cardTypes'), url: '/developer/card-types', icon: 'lucide:hash' }, { title: t('nav.cardTypes'), url: '/admin/card-types', icon: 'lucide:hash' },
{ title: t('nav.cards'), url: '/developer/cards', icon: 'lucide:key' }, { title: t('nav.cards'), url: '/admin/cards', icon: 'lucide:key' },
{ title: t('nav.announcements'), url: '/developer/announcements', icon: 'lucide:megaphone' }, { title: t('nav.announcements'), url: '/admin/announcements', icon: 'lucide:megaphone' },
{ title: t('nav.versions'), url: '/developer/versions', icon: 'lucide:git-branch' }, { title: t('nav.versions'), url: '/admin/versions', icon: 'lucide:git-branch' },
{ title: t('nav.users'), url: '/developer/users', icon: 'lucide:users' }, { title: t('nav.users'), url: '/admin/users', icon: 'lucide:users' },
{ title: t('nav.devices'), url: '/developer/devices', icon: 'lucide:monitor' }, { title: t('nav.devices'), url: '/admin/devices', icon: 'lucide:monitor' },
{ title: t('nav.sessions'), url: '/developer/sessions', icon: 'lucide:wifi' }, { title: t('nav.sessions'), url: '/admin/sessions', icon: 'lucide:wifi' },
{ title: t('nav.agentApps'), url: '/developer/agent-apps', icon: 'lucide:share-2' }, { title: t('nav.agentApps'), url: '/admin/agent-apps', icon: 'lucide:share-2' },
], ],
}, },
business: { business: {
title: t('nav.businessManagement'), title: t('nav.businessManagement'),
items: [ items: [
{ title: t('nav.finance'), url: '/developer/finance', icon: 'lucide:dollar-sign' }, { title: t('nav.finance'), url: '/admin/finance', icon: 'lucide:dollar-sign' },
{ title: t('nav.logs'), url: '/developer/logs', icon: 'lucide:scroll-text' }, { title: t('nav.logs'), url: '/admin/logs', icon: 'lucide:scroll-text' },
{ title: t('nav.tickets'), url: '/developer/tickets', icon: 'lucide:message-square' }, { title: t('nav.tickets'), url: '/admin/tickets', icon: 'lucide:message-square' },
], ],
}, },
advanced: { advanced: {
title: t('nav.advancedFeatures'), title: t('nav.advancedFeatures'),
items: [ items: [
{ title: t('nav.cloudConstants'), url: '/developer/cloud-constants', icon: 'lucide:file-lock' }, { title: t('nav.cloudConstants'), url: '/admin/cloud-constants', icon: 'lucide:file-lock' },
{ title: t('nav.cloudVariables'), url: '/developer/cloud-variables', icon: 'lucide:variable' }, { title: t('nav.cloudVariables'), url: '/admin/cloud-variables', icon: 'lucide:variable' },
{ title: t('nav.cloudFunction'), url: '/developer/cloud-function', icon: 'lucide:code' }, { title: t('nav.cloudFunction'), url: '/admin/cloud-function', icon: 'lucide:code' },
{ title: t('nav.riskControl'), url: '/developer/risk-control', icon: 'lucide:shield' }, { title: t('nav.riskControl'), url: '/admin/risk-control', icon: 'lucide:shield' },
{ title: t('nav.extension'), url: '/developer/extension', icon: 'lucide:plug' }, { 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" /> <Icon icon="lucide:user" class="mr-2 h-4 w-4" />
<span>{{ t('nav.profile') }}</span> <span>{{ t('nav.profile') }}</span>
</UiDropdownMenuItem> </UiDropdownMenuItem>
<UiDropdownMenuItem v-if="currentUser?.role === 'admin'" @click="router.push('/developer')"> <UiDropdownMenuItem v-if="currentUser?.role === 'admin'" @click="router.push('/admin')">
<Icon icon="lucide:layout-dashboard" class="mr-2 h-4 w-4" /> <Icon icon="lucide:layout-dashboard" class="mr-2 h-4 w-4" />
<span>{{ t('nav.developerDashboard') }}</span> <span>{{ t('nav.developerDashboard') }}</span>
</UiDropdownMenuItem> </UiDropdownMenuItem>
+4 -4
View File
@@ -2,7 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useRouter } from 'vue-router' 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 LanguageChange from '@/components/language-change.vue'
import ThemePopover from '@/components/custom-theme/theme-popover.vue' import ThemePopover from '@/components/custom-theme/theme-popover.vue'
import ToggleTheme from '@/components/toggle-theme.vue' import ToggleTheme from '@/components/toggle-theme.vue'
@@ -12,7 +12,7 @@ const router = useRouter()
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
const path = router.currentRoute.value.path const path = router.currentRoute.value.path
const parts = path.split('/').filter(Boolean) const parts = path.split('/').filter(Boolean)
const crumbs = [{ title: '控制台', path: '/developer' }] const crumbs = [{ title: '控制台', path: '/admin' }]
const titleMap: Record<string, string> = { const titleMap: Record<string, string> = {
applications: '应用管理', applications: '应用管理',
@@ -44,7 +44,7 @@ const breadcrumbs = computed(() => {
invite: '邀请授权', invite: '邀请授权',
} }
let currentPath = '/developer' let currentPath = '/admin'
for (let i = 1; i < parts.length; i++) { for (let i = 1; i < parts.length; i++) {
const part = parts[i] const part = parts[i]
currentPath += `/${part}` currentPath += `/${part}`
@@ -69,7 +69,7 @@ const breadcrumbs = computed(() => {
<template> <template>
<UiSidebarProvider> <UiSidebarProvider>
<DeveloperSidebar /> <AdminSidebar />
<UiSidebarInset> <UiSidebarInset>
<header class="flex h-14 shrink-0 items-center gap-2 border-b px-4"> <header class="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<UiSidebarTrigger class="-ml-1" /> <UiSidebarTrigger class="-ml-1" />
@@ -76,13 +76,13 @@ async function fetchData() {
} }
else { else {
toast.error('获取授权信息失败') toast.error('获取授权信息失败')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
} }
catch (error) { catch (error) {
console.error('获取数据失败:', error) console.error('获取数据失败:', error)
toast.error('获取数据失败') toast.error('获取数据失败')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
finally { finally {
loading.value = false loading.value = false
@@ -118,7 +118,7 @@ async function handleSubmit() {
const data = await response.json() const data = await response.json()
if (data.code === 200) { if (data.code === 200) {
toast.success('保存成功') toast.success('保存成功')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
else { else {
toast.error(data.message || '保存失败') toast.error(data.message || '保存失败')
@@ -134,7 +134,7 @@ async function handleSubmit() {
} }
function goBack() { function goBack() {
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
onMounted(() => { onMounted(() => {
@@ -4,7 +4,7 @@ import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
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' import { BasicPage } from '@/components/global-layout'
@@ -17,7 +17,6 @@ const saving = ref(false)
const agentApp = ref<AgentApp | null>(null) const agentApp = ref<AgentApp | null>(null)
const form = ref({ const form = ref({
commission: 0.1,
discount: 1.0, discount: 1.0,
status: 'active', status: 'active',
}) })
@@ -33,20 +32,19 @@ async function fetchAgentApp() {
if (data.code === 200 && data.agent_app) { if (data.code === 200 && data.agent_app) {
agentApp.value = data.agent_app agentApp.value = data.agent_app
form.value = { form.value = {
commission: data.agent_app.commission,
discount: data.agent_app.discount, discount: data.agent_app.discount,
status: data.agent_app.status, status: data.agent_app.status,
} }
} }
else { else {
toast.error('获取授权信息失败') toast.error('获取授权信息失败')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
} }
catch (error) { catch (error) {
console.error('获取授权信息失败:', error) console.error('获取授权信息失败:', error)
toast.error('获取授权信息失败') toast.error('获取授权信息失败')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
finally { finally {
loading.value = false loading.value = false
@@ -54,10 +52,6 @@ async function fetchAgentApp() {
} }
async function handleSubmit() { 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) { if (form.value.discount < 0 || form.value.discount > 1) {
toast.error('折扣必须在0-1之间') toast.error('折扣必须在0-1之间')
return return
@@ -78,7 +72,7 @@ async function handleSubmit() {
const data = await response.json() const data = await response.json()
if (data.code === 200) { if (data.code === 200) {
toast.success('保存成功') toast.success('保存成功')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
else { else {
toast.error(data.message || '保存失败') toast.error(data.message || '保存失败')
@@ -94,7 +88,7 @@ async function handleSubmit() {
} }
function goBack() { function goBack() {
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
onMounted(() => { onMounted(() => {
@@ -146,29 +140,6 @@ onMounted(() => {
</UiSelect> </UiSelect>
</div> </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"> <div class="space-y-2">
<UiLabel for="discount"> <UiLabel for="discount">
折扣 折扣
@@ -200,7 +171,7 @@ onMounted(() => {
<UiButton <UiButton
variant="outline" variant="outline"
size="sm" 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" /> <Icon icon="lucide:wallet" class="mr-2 h-4 w-4" />
充值余额 充值余额
@@ -30,13 +30,13 @@ async function fetchAgentApp() {
} }
else { else {
toast.error('获取授权信息失败') toast.error('获取授权信息失败')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
} }
catch (error) { catch (error) {
console.error('获取授权信息失败:', error) console.error('获取授权信息失败:', error)
toast.error('获取授权信息失败') toast.error('获取授权信息失败')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
finally { finally {
loading.value = false loading.value = false
@@ -66,7 +66,7 @@ async function handleSubmit() {
const data = await response.json() const data = await response.json()
if (data.code === 200) { if (data.code === 200) {
toast.success('充值成功') toast.success('充值成功')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
else { else {
toast.error(data.message || '充值失败') toast.error(data.message || '充值失败')
@@ -82,7 +82,7 @@ async function handleSubmit() {
} }
function goBack() { function goBack() {
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
const quickAmounts = [100, 500, 1000, 5000] const quickAmounts = [100, 500, 1000, 5000]
@@ -3,7 +3,7 @@ import type { ColumnDef } from '@tanstack/vue-table'
import { h } from 'vue' import { h } from 'vue'
import { useRouter } from 'vue-router' 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 Badge from '@/components/ui/badge/Badge.vue'
import Button from '@/components/ui/button/Button.vue' import Button from '@/components/ui/button/Button.vue'
@@ -68,11 +68,11 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'agent_name', accessorKey: 'agent_name',
header: '代理商/开发者', header: '代理商/管理员',
cell: ({ row }) => { cell: ({ row }) => {
const agentName = row.getValue('agent_name') as string const agentName = row.getValue('agent_name') as string
const itemType = row.original.itemType const itemType = row.original.itemType
const label = itemType === 'my_request' ? '开发者' : '代理商' const label = itemType === 'my_request' ? '管理员' : '代理商'
return h('div', {}, [ return h('div', {}, [
h('div', { class: 'font-medium' }, agentName || '-'), h('div', { class: 'font-medium' }, agentName || '-'),
h('div', { class: 'text-xs text-muted-foreground' }, label), h('div', { class: 'text-xs text-muted-foreground' }, label),
@@ -98,17 +98,6 @@ export function getColumns(actions: {
return h(Badge, { variant }, () => text) 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', accessorKey: 'discount',
header: '折扣', header: '折扣',
@@ -191,15 +180,15 @@ export function getColumns(actions: {
if (item.hasAuthorization) { if (item.hasAuthorization) {
items.push( 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('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('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' }), 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 { X } from 'lucide-vue-next'
import { computed } from 'vue' 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 { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -21,7 +21,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template> <template>
<div class="flex items-center flex-1 space-x-2"> <div class="flex items-center flex-1 space-x-2">
<Input <Input
placeholder="搜索代理商/开发者..." placeholder="搜索代理商/管理员..."
:model-value="(table.getColumn('agent_name')?.getFilterValue() as string) ?? ''" :model-value="(table.getColumn('agent_name')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[200px]" class="h-8 w-[150px] lg:w-[200px]"
@input="table.getColumn('agent_name')?.setFilterValue($event.target.value)" @input="table.getColumn('agent_name')?.setFilterValue($event.target.value)"
@@ -36,12 +36,12 @@ const table = generateVueTable<CombinedItem>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.agentApps.select', select: 'admin.agentApps.select',
itemType: 'developer.agentApps.columns.type', itemType: 'admin.agentApps.columns.type',
agent_name: 'developer.agentApps.columns.agentName', agent_name: 'admin.agentApps.columns.agentName',
app_name: 'developer.agentApps.columns.appName', app_name: 'admin.agentApps.columns.appName',
status: 'developer.agentApps.columns.status', status: 'admin.agentApps.columns.status',
created_at: 'developer.agentApps.columns.createdAt', created_at: 'admin.agentApps.columns.createdAt',
} }
defineExpose({ defineExpose({
@@ -9,7 +9,6 @@ export const agentAppSchema = z.object({
application_id: z.number(), application_id: z.number(),
app_name: z.string(), app_name: z.string(),
status: agentAppStatusSchema, status: agentAppStatusSchema,
commission: z.number(),
discount: z.number(), discount: z.number(),
balance: z.number(), balance: z.number(),
card_types: z.array(z.object({ card_types: z.array(z.object({
@@ -44,7 +43,6 @@ export interface CombinedItem {
application_id: number application_id: number
app_name: string app_name: string
status: string status: string
commission: number
discount: number discount: number
balance: number balance: number
card_types?: Array<{ card_type_id: number, can_generate: boolean }> 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 { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 ConfirmDialog from '@/components/confirm-dialog.vue'
import BulkActions from '@/components/data-table/bulk-actions.vue' import BulkActions from '@/components/data-table/bulk-actions.vue'
import { BasicPage } from '@/components/global-layout' 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' import api from '@/services/api'
const { t } = useI18n() const { t } = useI18n()
@@ -53,7 +53,6 @@ const allItems = computed<CombinedItem[]>(() => {
application_id: item.application_id, application_id: item.application_id,
app_name: item.app_name, app_name: item.app_name,
status: item.status, status: item.status,
commission: 0,
discount: 1, discount: 1,
balance: 0, balance: 0,
card_types: [], card_types: [],
@@ -72,7 +71,6 @@ const allItems = computed<CombinedItem[]>(() => {
application_id: item.application_id, application_id: item.application_id,
app_name: item.app_name, app_name: item.app_name,
status: item.status, status: item.status,
commission: 0,
discount: 1, discount: 1,
balance: 0, balance: 0,
card_types: [], card_types: [],
@@ -110,22 +108,22 @@ async function fetchData() {
} }
function goToRequest() { function goToRequest() {
router.push('/developer/agent-apps/request') router.push('/admin/agent-apps/request')
} }
function goToInvite() { function goToInvite() {
router.push('/developer/agent-apps/invite') router.push('/admin/agent-apps/invite')
} }
async function approveRequest(item: CombinedItem) { async function approveRequest(item: CombinedItem) {
try { try {
await api.post(`/dev/agent-apps/requests/${item.id}/approve`) await api.post(`/dev/agent-apps/requests/${item.id}/approve`)
toast.success(t('developer.agentApps.approveSuccess')) toast.success(t('admin.agentApps.approveSuccess'))
fetchData() fetchData()
} }
catch (error: any) { catch (error: any) {
console.error('批准申请失败:', error) 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 { try {
await api.post(`/dev/agent-apps/requests/${rejectingItem.value.id}/reject`, { reason: '' }) 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 showRejectDialog.value = false
fetchData() fetchData()
} }
catch (error: any) { catch (error: any) {
console.error('拒绝申请失败:', error) 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)[]) { async function batchApprove(ids: (string | number)[]) {
try { try {
await api.post('/dev/agent-apps/requests/batch-approve', { ids }) await api.post('/dev/agent-apps/requests/batch-approve', { ids })
toast.success(t('developer.agentApps.batchApproveSuccess')) toast.success(t('admin.agentApps.batchApproveSuccess'))
fetchData() fetchData()
} }
catch (error: any) { catch (error: any) {
console.error('批量批准失败:', error) 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 { try {
await api.post('/dev/agent-apps/requests/batch-reject', { ids: batchRejectIds.value, reason: '' }) 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 showBatchRejectDialog.value = false
fetchData() fetchData()
} }
catch (error: any) { catch (error: any) {
console.error('批量拒绝失败:', error) 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 { try {
await api.delete('/dev/agent-apps/batch', { ids: batchRemoveIds.value }) 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 showBatchRemoveDialog.value = false
fetchData() fetchData()
} }
catch (error: any) { catch (error: any) {
console.error('批量移除失败:', error) 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> <template>
<BasicPage <BasicPage
:title="t('developer.agentApps.title')" :title="t('admin.agentApps.title')"
:description="t('developer.agentApps.description')" :description="t('admin.agentApps.description')"
sticky sticky
> >
<template #actions> <template #actions>
<UiButton variant="outline" @click="goToRequest"> <UiButton variant="outline" @click="goToRequest">
<Icon icon="lucide:send" class="mr-2 h-4 w-4" /> <Icon icon="lucide:send" class="mr-2 h-4 w-4" />
{{ t('developer.agentApps.requestAuth') }} {{ t('admin.agentApps.requestAuth') }}
</UiButton> </UiButton>
<UiButton @click="goToInvite"> <UiButton @click="goToInvite">
<Icon icon="lucide:user-plus" class="mr-2 h-4 w-4" /> <Icon icon="lucide:user-plus" class="mr-2 h-4 w-4" />
{{ t('developer.agentApps.inviteAuth') }} {{ t('admin.agentApps.inviteAuth') }}
</UiButton> </UiButton>
</template> </template>
@@ -256,7 +254,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.agentApps.totalAuth') }} {{ t('admin.agentApps.totalAuth') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:share-2" class="size-4 text-muted-foreground" /> <Icon icon="lucide:share-2" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -270,7 +268,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.agentApps.activeAuth') }} {{ t('admin.agentApps.activeAuth') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -284,7 +282,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.agentApps.pendingRequests') }} {{ t('admin.agentApps.pendingRequests') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" /> <Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -298,7 +296,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.agentApps.agentBalance') }} {{ t('admin.agentApps.agentBalance') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:wallet" class="size-4 text-muted-foreground" /> <Icon icon="lucide:wallet" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -328,7 +326,7 @@ onMounted(() => {
<BulkActions <BulkActions
v-if="tableRef?.table" v-if="tableRef?.table"
:table="tableRef.table" :table="tableRef.table"
:entity-name="t('developer.agentApps.auth')" :entity-name="t('admin.agentApps.auth')"
> >
<UiButton <UiButton
variant="outline" variant="outline"
@@ -336,7 +334,7 @@ onMounted(() => {
@click="batchApprove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))" @click="batchApprove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
> >
<Icon icon="lucide:check" class="mr-2 h-4 w-4" /> <Icon icon="lucide:check" class="mr-2 h-4 w-4" />
{{ t('developer.agentApps.batchApprove') }} {{ t('admin.agentApps.batchApprove') }}
</UiButton> </UiButton>
<UiButton <UiButton
variant="outline" variant="outline"
@@ -344,7 +342,7 @@ onMounted(() => {
@click="confirmBatchReject(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))" @click="confirmBatchReject(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
> >
<Icon icon="lucide:x" class="mr-2 h-4 w-4" /> <Icon icon="lucide:x" class="mr-2 h-4 w-4" />
{{ t('developer.agentApps.batchReject') }} {{ t('admin.agentApps.batchReject') }}
</UiButton> </UiButton>
<UiButton <UiButton
variant="destructive" variant="destructive"
@@ -352,38 +350,38 @@ onMounted(() => {
@click="confirmBatchRemove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))" @click="confirmBatchRemove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
> >
<Icon icon="lucide:trash-2" class="mr-2 h-4 w-4" /> <Icon icon="lucide:trash-2" class="mr-2 h-4 w-4" />
{{ t('developer.agentApps.batchRemove') }} {{ t('admin.agentApps.batchRemove') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<ConfirmDialog <ConfirmDialog
v-model:open="showRejectDialog" v-model:open="showRejectDialog"
:title="t('developer.agentApps.reject')" :title="t('admin.agentApps.reject')"
:description="t('developer.agentApps.rejectConfirm')" :description="t('admin.agentApps.rejectConfirm')"
confirm-text="确定" confirm-text="确定"
@confirm="rejectRequest" @confirm="rejectRequest"
/> />
<ConfirmDialog <ConfirmDialog
v-model:open="showRemoveDialog" v-model:open="showRemoveDialog"
:title="t('developer.agentApps.remove')" :title="t('admin.agentApps.remove')"
:description="removingItem?.itemType === 'my_request' ? t('developer.agentApps.withdrawConfirm') : t('developer.agentApps.removeConfirm')" :description="removingItem?.itemType === 'my_request' ? t('admin.agentApps.withdrawConfirm') : t('admin.agentApps.removeConfirm')"
confirm-text="确定" confirm-text="确定"
@confirm="removeItem" @confirm="removeItem"
/> />
<ConfirmDialog <ConfirmDialog
v-model:open="showBatchRejectDialog" v-model:open="showBatchRejectDialog"
:title="t('developer.agentApps.batchReject')" :title="t('admin.agentApps.batchReject')"
:description="t('developer.agentApps.batchRejectConfirm', { count: batchRejectIds.length })" :description="t('admin.agentApps.batchRejectConfirm', { count: batchRejectIds.length })"
confirm-text="确定" confirm-text="确定"
@confirm="batchReject" @confirm="batchReject"
/> />
<ConfirmDialog <ConfirmDialog
v-model:open="showBatchRemoveDialog" v-model:open="showBatchRemoveDialog"
:title="t('developer.agentApps.batchRemove')" :title="t('admin.agentApps.batchRemove')"
:description="t('developer.agentApps.batchRemoveConfirm', { count: batchRemoveIds.length })" :description="t('admin.agentApps.batchRemoveConfirm', { count: batchRemoveIds.length })"
confirm-text="确定" confirm-text="确定"
@confirm="batchRemove" @confirm="batchRemove"
/> />
@@ -63,7 +63,7 @@ async function handleSubmit() {
const data = await response.json() const data = await response.json()
if (data.code === 200) { if (data.code === 200) {
toast.success('邀请已发送') toast.success('邀请已发送')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
else { else {
toast.error(data.message || '发送邀请失败') toast.error(data.message || '发送邀请失败')
@@ -79,7 +79,7 @@ async function handleSubmit() {
} }
function goBack() { function goBack() {
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
onMounted(() => { onMounted(() => {
@@ -36,7 +36,7 @@ async function fetchApplications() {
async function handleSubmit() { async function handleSubmit() {
if (!form.value.developer_id) { if (!form.value.developer_id) {
toast.error('请输入开发者ID') toast.error('请输入管理员ID')
return return
} }
if (!form.value.application_id) { if (!form.value.application_id) {
@@ -63,7 +63,7 @@ async function handleSubmit() {
const data = await response.json() const data = await response.json()
if (data.code === 200) { if (data.code === 200) {
toast.success('申请已提交') toast.success('申请已提交')
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
else { else {
toast.error(data.message || '提交申请失败') toast.error(data.message || '提交申请失败')
@@ -79,7 +79,7 @@ async function handleSubmit() {
} }
function goBack() { function goBack() {
router.push('/developer/agent-apps') router.push('/admin/agent-apps')
} }
onMounted(() => { onMounted(() => {
@@ -90,7 +90,7 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
title="申请授权" title="申请授权"
description="向开发者申请应用授权" description="向管理员申请应用授权"
> >
<template #actions> <template #actions>
<UiButton variant="outline" @click="goBack"> <UiButton variant="outline" @click="goBack">
@@ -104,22 +104,22 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle>申请信息</UiCardTitle> <UiCardTitle>申请信息</UiCardTitle>
<UiCardDescription> <UiCardDescription>
填写以下信息向开发者申请应用授权 填写以下信息向管理员申请应用授权
</UiCardDescription> </UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="developer_id"> <UiLabel for="developer_id">
开发者ID 管理员ID
</UiLabel> </UiLabel>
<UiInput <UiInput
id="developer_id" id="developer_id"
v-model="form.developer_id" v-model="form.developer_id"
type="number" type="number"
placeholder="请输入开发者ID" placeholder="请输入管理员ID"
/> />
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
请向开发者索取其ID 请向管理员索取其ID
</p> </p>
</div> </div>
+304 -318
View File
@@ -2,369 +2,355 @@
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 { BasicPage } from '@/components/global-layout'
import DataTable from './components/data-table.vue'
import api from '@/services/api' import api from '@/services/api'
const router = useRouter()
const route = useRoute()
const { t } = useI18n() const { t } = useI18n()
const router = useRouter()
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 loading = ref(true) const loading = ref(true)
const saving = ref(false)
const agents = ref<Agent[]>([]) 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({ const filteredAgents = computed(() => {
username: '', let result = agents.value
email: '',
password: '', if (statusFilter.value) {
parent_agent_id: '', const filterByStatus = (items: Agent[]): Agent[] => {
commission: 0, return items.reduce((acc: Agent[], item) => {
can_create_agent: false, const matches = item.status === statusFilter.value
balance: 0, 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 loading.value = true
try { try {
const data = await api.get<Agent>(`/dev/agents/${agentId.value}`) const endpoint = viewMode.value === 'tree' ? '/dev/agents/tree' : '/dev/agents'
if (data) { const data = await api.get<{ tree?: Agent[], agents?: Agent[], total: number }>(endpoint)
form.value.username = data.username || '' if (viewMode.value === 'tree') {
form.value.email = data.email || '' agents.value = data?.tree || []
form.value.password = '' } else {
form.value.parent_agent_id = data.parent_agent_id ? String(data.parent_agent_id) : 'none' agents.value = data?.agents || []
form.value.commission = data.commission || 0
form.value.can_create_agent = data.can_create_agent === true
form.value.balance = data.balance || 0
} }
} } catch (error) {
catch (error) { console.error('获取代理列表失败:', error)
console.error('获取代理信息失败:', error) agents.value = []
toast.error(t('developer.agents.edit.fetchFailed')) } finally {
router.push('/developer/agents')
}
finally {
loading.value = false 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 { try {
const data = await api.get<{ agents?: Agent[], total: number }>('/dev/agents') const newStatus = agent.status === 'banned' ? 'active' : 'banned'
agents.value = (data?.agents || []).filter(a => String(a.id) !== agentId.value) await api.put(`/dev/agents/${agent.id}/status`, { status: newStatus })
} toast.success(t('admin.agents.statusUpdateSuccess'))
catch (error) { fetchAgents()
console.error('获取代理列表失败:', error) } catch (error: any) {
console.error('切换代理状态失败:', error)
toast.error(error.message || t('admin.agents.statusUpdateFailed'))
} }
} }
const selectedParentAgent = computed(() => { function confirmDelete(agent: Agent) {
if (form.value.parent_agent_id && form.value.parent_agent_id !== 'none') { deleteTarget.value = agent
return agents.value.find(a => String(a.id) === form.value.parent_agent_id) deleteDialogOpen.value = true
} }
return null
})
async function handleSave() { async function handleDelete() {
if (!form.value.username) { if (!deleteTarget.value) return
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
try { try {
const payload: any = { await api.delete(`/dev/agents/${deleteTarget.value.id}`)
commission: form.value.commission, toast.success(t('admin.agents.deleteSuccess'))
can_create_agent: form.value.can_create_agent, fetchAgents()
balance: form.value.balance, } catch (error: any) {
} console.error('删除代理失败:', error)
toast.error(error.message || t('admin.agents.deleteFailed'))
if (form.value.email) { } finally {
payload.email = form.value.email deleteTarget.value = null
} }
}
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.put(`/dev/agents/${agentId.value}`, payload) function confirmBatchDelete(ids: number[]) {
toast.success(t('developer.agents.edit.saveSuccess')) batchDeleteIds.value = ids
router.push('/developer/agents') 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')) async function batchToggleStatus(ids: number[], status: string) {
} try {
finally { await api.post('/dev/agents/batch/status', { agent_ids: ids, status })
saving.value = false toast.success(t('admin.agents.batchUpdateSuccess'))
fetchAgents()
} catch (error: any) {
console.error('批量更新状态失败:', error)
toast.error(error.message || t('admin.agents.batchUpdateFailed'))
} }
} }
onMounted(() => { onMounted(() => {
fetchAgent()
fetchAgents() fetchAgents()
}) })
</script> </script>
<template> <template>
<BasicPage <BasicPage
:title="t('developer.agents.edit.title')" :title="t('admin.agents.title')"
:description="t('developer.agents.edit.description')" :description="t('admin.agents.description')"
:breadcrumbs="[
{ title: t('developer.agents.title'), href: '/developer/agents' },
{ title: t('developer.agents.edit.title') },
]"
sticky sticky
> >
<div v-if="loading" class="flex items-center justify-center py-12"> <template #actions>
<Icon icon="lucide:loader-2" class="h-8 w-8 animate-spin text-muted-foreground" /> <UiButton @click="goToCreate">
</div> <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="space-y-6">
<div class="grid gap-6 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="lg:col-span-2 space-y-6"> <UiCard>
<UiCard> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardHeader> <UiCardTitle class="text-sm font-medium">
<UiCardTitle class="flex items-center gap-2"> {{ t('admin.agents.totalAgents') }}
<Icon icon="lucide:user" class="size-5" /> </UiCardTitle>
{{ t('developer.agents.create.basicInfo') }} <Icon icon="lucide:users" class="size-4 text-muted-foreground" />
</UiCardTitle> </UiCardHeader>
<UiCardDescription>{{ t('developer.agents.create.basicInfoDesc') }}</UiCardDescription> <UiCardContent>
</UiCardHeader> <div class="text-2xl font-bold">
<UiCardContent class="space-y-6"> {{ totalAgents }}
<div class="space-y-2"> </div>
<UiLabel for="username"> </UiCardContent>
{{ t('developer.agents.form.username') }} </UiCard>
</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-2"> <UiCard>
<UiLabel for="email"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.agents.form.email') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.agents.status.active') }}
<UiInput </UiCardTitle>
id="email" <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
v-model="form.email" </UiCardHeader>
type="email" <UiCardContent>
:placeholder="t('developer.agents.form.emailPlaceholder')" <div class="text-2xl font-bold">
/> {{ activeCount }}
</div> </div>
</UiCardContent>
</UiCard>
<div class="space-y-2"> <UiCard>
<UiLabel for="password"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.agents.form.password') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.agents.status.inactive') }}
<UiInput </UiCardTitle>
id="password" <Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
v-model="form.password" </UiCardHeader>
type="password" <UiCardContent>
:placeholder="t('developer.agents.edit.passwordPlaceholder')" <div class="text-2xl font-bold">
/> {{ inactiveCount }}
<p class="text-xs text-muted-foreground"> </div>
{{ t('developer.agents.edit.passwordHint') }} </UiCardContent>
</p> </UiCard>
</div>
</UiCardContent>
</UiCard>
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="text-sm font-medium">
<Icon icon="lucide:settings-2" class="size-5" /> {{ t('admin.agents.status.banned') }}
{{ t('developer.agents.create.agentConfig') }} </UiCardTitle>
</UiCardTitle> <Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
<UiCardDescription>{{ t('developer.agents.create.agentConfigDesc') }}</UiCardDescription> </UiCardHeader>
</UiCardHeader> <UiCardContent>
<UiCardContent class="space-y-6"> <div class="text-2xl font-bold">
<div class="space-y-2"> {{ bannedCount }}
<UiLabel for="parent_agent"> </div>
{{ t('developer.agents.form.parentAgent') }} </UiCardContent>
</UiLabel> </UiCard>
<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>
</div> </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> </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> </BasicPage>
</template> </template>
@@ -31,7 +31,7 @@ export function getColumns(actions: {
return [ return [
{ {
accessorKey: 'username', accessorKey: 'username',
header: () => t('developer.agents.columns.username'), header: () => t('admin.agents.columns.username'),
cell: ({ row }) => { cell: ({ row }) => {
const agent = row.original const agent = row.original
const indentStyle = getIndentStyle(row) const indentStyle = getIndentStyle(row)
@@ -49,13 +49,13 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'status', accessorKey: 'status',
header: () => t('developer.agents.columns.status'), header: () => t('admin.agents.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const status = row.getValue('status') as string const status = row.getValue('status') as string
const statusMap: Record<string, { text: string, variant: any }> = { const statusMap: Record<string, { text: string, variant: any }> = {
active: { text: t('developer.agents.status.active'), variant: 'default' }, active: { text: t('admin.agents.status.active'), variant: 'default' },
inactive: { text: t('developer.agents.status.inactive'), variant: 'secondary' }, inactive: { text: t('admin.agents.status.inactive'), variant: 'secondary' },
banned: { text: t('developer.agents.status.banned'), variant: 'destructive' }, banned: { text: t('admin.agents.status.banned'), variant: 'destructive' },
} }
const { text, variant } = statusMap[status] || { text: status, variant: 'default' } const { text, variant } = statusMap[status] || { text: status, variant: 'default' }
return h(Badge, { variant }, () => text) return h(Badge, { variant }, () => text)
@@ -63,7 +63,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'can_create_agent', accessorKey: 'can_create_agent',
header: () => t('developer.agents.columns.canCreateAgent'), header: () => t('admin.agents.columns.canCreateAgent'),
cell: ({ row }) => { cell: ({ row }) => {
const canCreate = row.getValue('can_create_agent') as boolean const canCreate = row.getValue('can_create_agent') as boolean
return h('div', { class: 'flex items-center justify-center' }, [ return h('div', { class: 'flex items-center justify-center' }, [
@@ -75,7 +75,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'cards_count', accessorKey: 'cards_count',
header: () => t('developer.agents.columns.cardsCount'), header: () => t('admin.agents.columns.cardsCount'),
cell: ({ row }) => { cell: ({ row }) => {
const count = row.getValue('cards_count') as number const count = row.getValue('cards_count') as number
return h(Badge, { variant: 'outline' }, () => String(count)) return h(Badge, { variant: 'outline' }, () => String(count))
@@ -83,7 +83,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'child_agents_count', accessorKey: 'child_agents_count',
header: () => t('developer.agents.columns.childAgentsCount'), header: () => t('admin.agents.columns.childAgentsCount'),
cell: ({ row }) => { cell: ({ row }) => {
const count = row.getValue('child_agents_count') as number const count = row.getValue('child_agents_count') as number
return h(Badge, { variant: 'secondary' }, () => String(count)) return h(Badge, { variant: 'secondary' }, () => String(count))
@@ -91,23 +91,15 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'balance', accessorKey: 'balance',
header: () => t('developer.agents.columns.balance'), header: () => t('admin.agents.columns.balance'),
cell: ({ row }) => { cell: ({ row }) => {
const balance = row.getValue('balance') as number const balance = row.getValue('balance') as number
return `¥${balance.toFixed(2)}` 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', accessorKey: 'created_at',
header: () => t('developer.agents.columns.createdAt'), header: () => t('admin.agents.columns.createdAt'),
cell: ({ row }) => { cell: ({ row }) => {
const createdAt = row.getValue('created_at') as string const createdAt = row.getValue('created_at') as string
if (!createdAt) return '-' if (!createdAt) return '-'
@@ -128,7 +120,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'last_login_at', accessorKey: 'last_login_at',
header: () => t('developer.agents.columns.lastLoginAt'), header: () => t('admin.agents.columns.lastLoginAt'),
cell: ({ row }) => { cell: ({ row }) => {
const lastLoginAt = row.getValue('last_login_at') as string const lastLoginAt = row.getValue('last_login_at') as string
if (!lastLoginAt) return '-' if (!lastLoginAt) return '-'
@@ -174,7 +166,7 @@ export function getColumns(actions: {
]), ]),
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(agent) }, () => [ h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(agent) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }), 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(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(agent) }, () => [ h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(agent) }, () => [
@@ -51,16 +51,15 @@ const table = generateVueTable<Agent>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.agents.select', select: 'admin.agents.select',
username: 'developer.agents.columns.username', username: 'admin.agents.columns.username',
status: 'developer.agents.columns.status', status: 'admin.agents.columns.status',
can_create_agent: 'developer.agents.columns.canCreateAgent', can_create_agent: 'admin.agents.columns.canCreateAgent',
cards_count: 'developer.agents.columns.cardsCount', cards_count: 'admin.agents.columns.cardsCount',
child_agents_count: 'developer.agents.columns.childAgentsCount', child_agents_count: 'admin.agents.columns.childAgentsCount',
balance: 'developer.agents.columns.balance', balance: 'admin.agents.columns.balance',
commission: 'developer.agents.columns.commission', created_at: 'admin.agents.columns.createdAt',
created_at: 'developer.agents.columns.createdAt', last_login_at: 'admin.agents.columns.lastLoginAt',
last_login_at: 'developer.agents.columns.lastLoginAt',
} }
defineExpose({ defineExpose({
@@ -77,13 +76,13 @@ defineExpose({
</div> </div>
<BulkActions :table="table" entity-name="agents"> <BulkActions :table="table" entity-name="agents">
<UiButton variant="outline" size="sm" @click="emit('batchUnban')"> <UiButton variant="outline" size="sm" @click="emit('batchUnban')">
{{ t('developer.agents.batchUnbanBtn') }} {{ t('admin.agents.batchUnbanBtn') }}
</UiButton> </UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchBan')"> <UiButton variant="outline" size="sm" @click="emit('batchBan')">
{{ t('developer.agents.batchBanBtn') }} {{ t('admin.agents.batchBanBtn') }}
</UiButton> </UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')"> <UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.agents.batchDeleteBtn') }} {{ t('admin.agents.batchDeleteBtn') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
@@ -93,7 +92,7 @@ defineExpose({
<UiInput <UiInput
:model-value="searchFilter" :model-value="searchFilter"
type="search" type="search"
:placeholder="t('developer.agents.searchPlaceholder')" :placeholder="t('admin.agents.searchPlaceholder')"
class="h-9 w-[250px] pl-8" class="h-9 w-[250px] pl-8"
@update:model-value="emit('update:searchFilter', String($event))" @update:model-value="emit('update:searchFilter', String($event))"
/> />
@@ -103,7 +102,7 @@ defineExpose({
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" @click="emit('toggleView')"> <UiButton variant="outline" size="sm" @click="emit('toggleView')">
<Icon :icon="viewMode === 'tree' ? 'lucide:list' : 'lucide:git-branch'" class="mr-2 h-4 w-4" /> <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> </UiButton>
<DataTableViewOptions :table="table" :column-labels="columnLabels" /> <DataTableViewOptions :table="table" :column-labels="columnLabels" />
</div> </div>
+37 -68
View File
@@ -26,7 +26,6 @@ const form = ref({
email: '', email: '',
password: '', password: '',
parent_agent_id: '', parent_agent_id: '',
commission: 0,
can_create_agent: false, can_create_agent: false,
balance: 0, balance: 0,
}) })
@@ -50,15 +49,15 @@ const selectedParentAgent = computed(() => {
async function handleCreate() { async function handleCreate() {
if (!form.value.username) { if (!form.value.username) {
toast.error(t('developer.agents.create.usernameRequired')) toast.error(t('admin.agents.create.usernameRequired'))
return return
} }
if (!form.value.password) { if (!form.value.password) {
toast.error(t('developer.agents.create.passwordRequired')) toast.error(t('admin.agents.create.passwordRequired'))
return return
} }
if (form.value.password.length < 6) { if (form.value.password.length < 6) {
toast.error(t('developer.agents.create.passwordMinLength')) toast.error(t('admin.agents.create.passwordMinLength'))
return return
} }
@@ -67,7 +66,6 @@ async function handleCreate() {
const payload: any = { const payload: any = {
username: form.value.username, username: form.value.username,
password: form.value.password, password: form.value.password,
commission: form.value.commission,
can_create_agent: form.value.can_create_agent, can_create_agent: form.value.can_create_agent,
balance: form.value.balance, balance: form.value.balance,
} }
@@ -81,12 +79,12 @@ async function handleCreate() {
} }
await api.post('/dev/agents', payload) await api.post('/dev/agents', payload)
toast.success(t('developer.agents.create.createSuccess')) toast.success(t('admin.agents.create.createSuccess'))
router.push('/developer/agents') router.push('/admin/agents')
} }
catch (error: any) { catch (error: any) {
console.error('创建代理失败:', error) console.error('创建代理失败:', error)
toast.error(error.message || t('developer.agents.create.createFailed')) toast.error(error.message || t('admin.agents.create.createFailed'))
} }
finally { finally {
saving.value = false saving.value = false
@@ -100,11 +98,11 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.agents.create.title')" :title="t('admin.agents.create.title')"
:description="t('developer.agents.create.description')" :description="t('admin.agents.create.description')"
:breadcrumbs="[ :breadcrumbs="[
{ title: t('developer.agents.title'), href: '/developer/agents' }, { title: t('admin.agents.title'), href: '/admin/agents' },
{ title: t('developer.agents.create.title') }, { title: t('admin.agents.create.title') },
]" ]"
sticky sticky
> >
@@ -115,46 +113,46 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:user-plus" class="size-5" /> <Icon icon="lucide:user-plus" class="size-5" />
{{ t('developer.agents.create.basicInfo') }} {{ t('admin.agents.create.basicInfo') }}
</UiCardTitle> </UiCardTitle>
<UiCardDescription>{{ t('developer.agents.create.basicInfoDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.agents.create.basicInfoDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="username"> <UiLabel for="username">
{{ t('developer.agents.form.username') }} {{ t('admin.agents.form.username') }}
</UiLabel> </UiLabel>
<UiInput <UiInput
id="username" id="username"
v-model="form.username" v-model="form.username"
:placeholder="t('developer.agents.form.usernamePlaceholder')" :placeholder="t('admin.agents.form.usernamePlaceholder')"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="email"> <UiLabel for="email">
{{ t('developer.agents.form.email') }} {{ t('admin.agents.form.email') }}
</UiLabel> </UiLabel>
<UiInput <UiInput
id="email" id="email"
v-model="form.email" v-model="form.email"
type="email" type="email"
:placeholder="t('developer.agents.form.emailPlaceholder')" :placeholder="t('admin.agents.form.emailPlaceholder')"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="password"> <UiLabel for="password">
{{ t('developer.agents.form.password') }} {{ t('admin.agents.form.password') }}
</UiLabel> </UiLabel>
<UiInput <UiInput
id="password" id="password"
v-model="form.password" v-model="form.password"
type="password" type="password"
:placeholder="t('developer.agents.form.passwordPlaceholder')" :placeholder="t('admin.agents.form.passwordPlaceholder')"
/> />
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
{{ t('developer.agents.create.passwordHint') }} {{ t('admin.agents.create.passwordHint') }}
</p> </p>
</div> </div>
</UiCardContent> </UiCardContent>
@@ -164,22 +162,22 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" /> <Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.agents.create.agentConfig') }} {{ t('admin.agents.create.agentConfig') }}
</UiCardTitle> </UiCardTitle>
<UiCardDescription>{{ t('developer.agents.create.agentConfigDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.agents.create.agentConfigDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="parent_agent"> <UiLabel for="parent_agent">
{{ t('developer.agents.form.parentAgent') }} {{ t('admin.agents.form.parentAgent') }}
</UiLabel> </UiLabel>
<UiSelect v-model="form.parent_agent_id"> <UiSelect v-model="form.parent_agent_id">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.agents.form.selectParentAgent')" /> <UiSelectValue :placeholder="t('admin.agents.form.selectParentAgent')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem value="none"> <UiSelectItem value="none">
{{ t('developer.agents.form.noParent') }} {{ t('admin.agents.form.noParent') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem <UiSelectItem
v-for="agent in agents" v-for="agent in agents"
@@ -191,38 +189,13 @@ onMounted(() => {
</UiSelectContent> </UiSelectContent>
</UiSelect> </UiSelect>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
{{ t('developer.agents.create.parentAgentHint') }} {{ t('admin.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> </p>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="balance"> <UiLabel for="balance">
{{ t('developer.agents.form.balance') }} {{ t('admin.agents.form.balance') }}
</UiLabel> </UiLabel>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<UiNumberField <UiNumberField
@@ -240,15 +213,15 @@ onMounted(() => {
<span class="text-sm text-muted-foreground">¥</span> <span class="text-sm text-muted-foreground">¥</span>
</div> </div>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
{{ t('developer.agents.form.balancePlaceholder') }} {{ t('admin.agents.form.balancePlaceholder') }}
</p> </p>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="space-y-0.5"> <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"> <p class="text-xs text-muted-foreground">
{{ t('developer.agents.create.canCreateAgentHint') }} {{ t('admin.agents.create.canCreateAgentHint') }}
</p> </p>
</div> </div>
<!-- 注意UiSwitch 必须使用 v-model不能使用 v-model:checked --> <!-- 注意UiSwitch 必须使用 v-model不能使用 v-model:checked -->
@@ -263,33 +236,29 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:receipt" class="size-5" /> <Icon icon="lucide:receipt" class="size-5" />
{{ t('developer.agents.create.preview') }} {{ t('admin.agents.create.preview') }}
</UiCardTitle> </UiCardTitle>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between text-sm"> <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> <span>{{ form.username || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ form.email || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ form.can_create_agent ? t('common.yes') : t('common.no') }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ selectedParentAgent?.username || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.agents.form.commission') }}</span> <span class="text-muted-foreground">{{ t('admin.agents.form.balance') }}</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> <span>¥{{ form.balance.toFixed(2) }}</span>
</div> </div>
</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-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" /> <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>
<UiButton <UiButton
variant="outline" variant="outline"
class="w-full" class="w-full"
@click="router.back()" @click="router.back()"
> >
{{ t('developer.agents.create.cancel') }} {{ t('admin.agents.create.cancel') }}
</UiButton> </UiButton>
</div> </div>
</UiCardContent> </UiCardContent>
@@ -10,7 +10,6 @@ export interface Agent {
created_at: string created_at: string
last_login_at: string last_login_at: string
balance: number balance: number
commission: number
can_create_agent: boolean can_create_agent: boolean
cards_count: number cards_count: number
child_agents_count: number child_agents_count: number
+25 -25
View File
@@ -119,9 +119,9 @@ const bannedCount = computed(() => {
}) })
const statusOptions = computed(() => [ const statusOptions = computed(() => [
{ label: t('developer.agents.status.active'), value: 'active' }, { label: t('admin.agents.status.active'), value: 'active' },
{ label: t('developer.agents.status.inactive'), value: 'inactive' }, { label: t('admin.agents.status.inactive'), value: 'inactive' },
{ label: t('developer.agents.status.banned'), value: 'banned' }, { label: t('admin.agents.status.banned'), value: 'banned' },
]) ])
async function fetchAgents() { async function fetchAgents() {
@@ -143,7 +143,7 @@ async function fetchAgents() {
} }
function goToCreate() { function goToCreate() {
router.push('/developer/agents/create') router.push('/admin/agents/create')
} }
function toggleViewMode() { function toggleViewMode() {
@@ -152,18 +152,18 @@ function toggleViewMode() {
} }
function handleEdit(agent: Agent) { function handleEdit(agent: Agent) {
router.push(`/developer/agents/${agent.id}/edit`) router.push(`/admin/agents/${agent.id}/edit`)
} }
async function handleToggleStatus(agent: Agent) { async function handleToggleStatus(agent: Agent) {
try { try {
const newStatus = agent.status === 'banned' ? 'active' : 'banned' const newStatus = agent.status === 'banned' ? 'active' : 'banned'
await api.put(`/dev/agents/${agent.id}/status`, { status: newStatus }) await api.put(`/dev/agents/${agent.id}/status`, { status: newStatus })
toast.success(t('developer.agents.statusUpdateSuccess')) toast.success(t('admin.agents.statusUpdateSuccess'))
fetchAgents() fetchAgents()
} catch (error: any) { } catch (error: any) {
console.error('切换代理状态失败:', error) 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 { try {
await api.delete(`/dev/agents/${deleteTarget.value.id}`) await api.delete(`/dev/agents/${deleteTarget.value.id}`)
toast.success(t('developer.agents.deleteSuccess')) toast.success(t('admin.agents.deleteSuccess'))
fetchAgents() fetchAgents()
} catch (error: any) { } catch (error: any) {
console.error('删除代理失败:', error) console.error('删除代理失败:', error)
toast.error(error.message || t('developer.agents.deleteFailed')) toast.error(error.message || t('admin.agents.deleteFailed'))
} finally { } finally {
deleteTarget.value = null deleteTarget.value = null
} }
@@ -195,11 +195,11 @@ function confirmBatchDelete(ids: number[]) {
async function handleBatchDelete() { async function handleBatchDelete() {
try { try {
await api.delete('/dev/agents/batch', { agent_ids: batchDeleteIds.value } as any) 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() fetchAgents()
} catch (error: any) { } catch (error: any) {
console.error('批量删除失败:', error) console.error('批量删除失败:', error)
toast.error(error.message || t('developer.agents.batchDeleteFailed')) toast.error(error.message || t('admin.agents.batchDeleteFailed'))
} finally { } finally {
batchDeleteIds.value = [] batchDeleteIds.value = []
} }
@@ -208,11 +208,11 @@ async function handleBatchDelete() {
async function batchToggleStatus(ids: number[], status: string) { async function batchToggleStatus(ids: number[], status: string) {
try { try {
await api.post('/dev/agents/batch/status', { agent_ids: ids, status }) await api.post('/dev/agents/batch/status', { agent_ids: ids, status })
toast.success(t('developer.agents.batchUpdateSuccess')) toast.success(t('admin.agents.batchUpdateSuccess'))
fetchAgents() fetchAgents()
} catch (error: any) { } catch (error: any) {
console.error('批量更新状态失败:', error) 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> <template>
<BasicPage <BasicPage
:title="t('developer.agents.title')" :title="t('admin.agents.title')"
:description="t('developer.agents.description')" :description="t('admin.agents.description')"
sticky sticky
> >
<template #actions> <template #actions>
<UiButton @click="goToCreate"> <UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.agents.addAgent') }} {{ t('admin.agents.addAgent') }}
</UiButton> </UiButton>
</template> </template>
@@ -239,7 +239,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.agents.totalAgents') }} {{ t('admin.agents.totalAgents') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" /> <Icon icon="lucide:users" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -253,7 +253,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.agents.status.active') }} {{ t('admin.agents.status.active') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -267,7 +267,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.agents.status.inactive') }} {{ t('admin.agents.status.inactive') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" /> <Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -281,7 +281,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.agents.status.banned') }} {{ t('admin.agents.status.banned') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" /> <Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -314,7 +314,7 @@ onMounted(() => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="statusFilter" v-model="statusFilter"
:title="t('developer.agents.columns.status')" :title="t('admin.agents.columns.status')"
:options="statusOptions" :options="statusOptions"
/> />
</template> </template>
@@ -331,10 +331,10 @@ onMounted(() => {
@confirm="handleDelete" @confirm="handleDelete"
> >
<template #title> <template #title>
{{ t('developer.agents.deleteAgent') }} {{ t('admin.agents.deleteAgent') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.agents.deleteAgentConfirm', { username: deleteTarget?.username }) }} {{ t('admin.agents.deleteAgentConfirm', { username: deleteTarget?.username }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
@@ -346,10 +346,10 @@ onMounted(() => {
@confirm="handleBatchDelete" @confirm="handleBatchDelete"
> >
<template #title> <template #title>
{{ t('developer.agents.batchDeleteAgents') }} {{ t('admin.agents.batchDeleteAgents') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }} {{ t('admin.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
</BasicPage> </BasicPage>
+283 -244
View File
@@ -1,61 +1,92 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 { 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 { interface Application {
id: number id: number
name: string 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 route = useRoute()
const router = useRouter() const router = useRouter()
const loading = ref(true) const loading = ref(true)
const saving = ref(false) const announcements = ref<Announcement[]>([])
const applications = ref<Application[]>([]) const applications = ref<Application[]>([])
const announcement = ref<Announcement | null>(null) const tableRef = ref()
const formData = ref({ const appFilter = ref<string>('')
title: '', const typeFilter = ref<string>('')
content: '', const statusFilter = ref<string>('')
type: 'info' as 'info' | 'warning' | 'error' | 'success', const searchFilter = ref<string>('')
status: 'active' as 'active' | 'inactive',
is_status_active: true, const deleteDialogOpen = ref(false)
is_top: 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 = [ const typeOptions = computed(() => [
{ value: 'info', label: '信息', color: 'bg-blue-500', desc: '普通通知信息' }, { label: t('admin.announcements.types.info'), value: 'info' },
{ value: 'warning', label: '警告', color: 'bg-yellow-500', desc: '需要注意的内容' }, { label: t('admin.announcements.types.warning'), value: 'warning' },
{ value: 'error', label: '错误', color: 'bg-red-500', desc: '错误或故障通知' }, { label: t('admin.announcements.types.error'), value: 'error' },
{ value: 'success', label: '成功', color: 'bg-green-500', desc: '成功状态通知' }, { label: t('admin.announcements.types.success'), value: 'success' },
] ])
const selectedApplication = computed(() => { const statusOptions = computed(() => [
return applications.value.find(app => app.id === announcement.value?.application_id) { 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(() => { const activeCount = computed(() => filteredAnnouncements.value.filter(a => a.status === 'active').length)
return typeOptions.find(t => t.value === formData.value.type) 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() { async function fetchApplications() {
try { try {
@@ -67,249 +98,257 @@ async function fetchApplications() {
} }
} }
async function fetchAnnouncement() { async function fetchAnnouncements() {
loading.value = true loading.value = true
try { try {
const data = await api.get<Announcement>(`/dev/announcements/${route.params.id}`) const data = await api.get<{ announcements: Announcement[], total: number }>('/dev/announcements')
announcement.value = data announcements.value = data?.announcements || []
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
}
} }
catch (error) { catch (error) {
console.error('获取公告详情失败:', error) console.error('加载公告失败:', error)
toast.error('公告不存在或已被删除') announcements.value = []
router.push('/developer/announcements')
} }
finally { finally {
loading.value = false loading.value = false
} }
} }
async function handleSubmit() { function goToCreate() {
if (!formData.value.title) { router.push('/admin/announcements/create')
toast.error('请输入公告标题') }
return
}
if (!formData.value.content) {
toast.error('请输入公告内容')
return
}
saving.value = true function goToEdit(announcement: Announcement) {
router.push(`/admin/announcements/${announcement.id}`)
}
async function toggleTop(announcement: Announcement) {
try { try {
await api.put(`/dev/applications/${announcement.value?.application_id}/announcements/${route.params.id}`, { await api.put(`/dev/applications/${announcement.application_id}/announcements/${announcement.id}/top`)
title: formData.value.title, toast.success(announcement.is_top ? t('admin.announcements.unpinned') : t('admin.announcements.pinned'))
content: formData.value.content, fetchAnnouncements()
type: formData.value.type,
status: formData.value.is_status_active ? 'active' : 'inactive',
is_top: formData.value.is_top,
})
toast.success('公告更新成功')
router.push('/developer/announcements')
} }
catch (error: any) { catch (error: any) {
console.error('更新公告失败:', error) console.error('切换置顶失败:', error)
toast.error(error.message || '更新失败') 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 { 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(() => { onMounted(() => {
fetchApplications() fetchApplications()
fetchAnnouncement() fetchAnnouncements()
const appParam = route.query.app as string
if (appParam) {
appFilter.value = appParam
}
}) })
</script> </script>
<template> <template>
<BasicPage <BasicPage
title="编辑公告" :title="t('admin.announcements.title')"
description="编辑现有公告内容" :description="t('admin.announcements.description')"
:breadcrumbs="[
{ title: '公告管理', href: '/developer/announcements' },
{ title: '编辑公告' },
]"
sticky sticky
> >
<div v-if="loading" class="flex items-center justify-center py-12"> <template #actions>
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" /> <UiButton @click="goToCreate">
</div> <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="space-y-6">
<div class="grid gap-6 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="lg:col-span-2 space-y-6"> <UiCard>
<UiCard> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardHeader> <UiCardTitle class="text-sm font-medium">
<UiCardTitle class="flex items-center gap-2"> {{ t('admin.announcements.totalAnnouncements') }}
<Icon icon="lucide:megaphone" class="size-5" /> </UiCardTitle>
编辑公告 <Icon icon="lucide:megaphone" class="size-4 text-muted-foreground" />
</UiCardTitle> </UiCardHeader>
<UiCardDescription>修改公告的基本信息和内容</UiCardDescription> <UiCardContent>
</UiCardHeader> <div class="text-2xl font-bold">
<UiCardContent class="space-y-6"> {{ filteredAnnouncements.length }}
<div class="space-y-2"> </div>
<UiLabel>所属应用</UiLabel> </UiCardContent>
<div class="flex items-center gap-2 p-3 rounded-lg bg-muted/50"> </UiCard>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
<span>{{ announcement?.application_name || '-' }}</span>
</div>
</div>
<div class="space-y-2"> <UiCard>
<UiLabel for="title"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
公告标题 <span class="text-destructive">*</span> <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.announcements.activeCount') }}
<UiInput </UiCardTitle>
id="title" <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
v-model="formData.title" </UiCardHeader>
placeholder="输入公告标题" <UiCardContent>
:disabled="saving" <div class="text-2xl font-bold">
/> {{ activeCount }}
</div> </div>
</UiCardContent>
</UiCard>
<div class="space-y-2"> <UiCard>
<UiLabel for="content"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
公告内容 <span class="text-destructive">*</span> <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.announcements.inactiveCount') }}
<textarea </UiCardTitle>
id="content" <Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
v-model="formData.content" </UiCardHeader>
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" <UiCardContent>
placeholder="输入公告详细内容..." <div class="text-2xl font-bold">
:disabled="saving" {{ inactiveCount }}
/> </div>
</div> </UiCardContent>
</UiCard>
<div class="space-y-2"> <UiCard>
<UiLabel>公告类型</UiLabel> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiSelect v-model="formData.type" :disabled="saving"> <UiCardTitle class="text-sm font-medium">
<UiSelectTrigger> {{ t('admin.announcements.topCount') }}
<UiSelectValue placeholder="选择类型" /> </UiCardTitle>
</UiSelectTrigger> <Icon icon="lucide:pin" class="size-4 text-muted-foreground" />
<UiSelectContent> </UiCardHeader>
<UiSelectItem v-for="type in typeOptions" :key="type.value" :value="type.value"> <UiCardContent>
<div class="flex items-center gap-2"> <div class="text-2xl font-bold">
<span class="w-2 h-2 rounded-full" :class="type.color" /> {{ topCount }}
{{ type.label }} </div>
</div> </UiCardContent>
</UiSelectItem> </UiCard>
</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>
</div> </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> </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> </BasicPage>
</template> </template>
@@ -18,7 +18,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
return [ return [
{ {
accessorKey: 'title', accessorKey: 'title',
header: () => t('developer.announcements.columns.title'), header: () => t('admin.announcements.columns.title'),
cell: ({ row }) => { cell: ({ row }) => {
return h('div', { class: 'space-y-1' }, [ return h('div', { class: 'space-y-1' }, [
h('div', { class: 'font-medium' }, row.original.title), h('div', { class: 'font-medium' }, row.original.title),
@@ -28,7 +28,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
}, },
{ {
accessorKey: 'application_name', accessorKey: 'application_name',
header: () => t('developer.announcements.columns.application'), header: () => t('admin.announcements.columns.application'),
cell: ({ row }) => { cell: ({ row }) => {
const appName = row.original.application_name const appName = row.original.application_name
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-' return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -36,13 +36,13 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
}, },
{ {
accessorKey: 'type', accessorKey: 'type',
header: () => t('developer.announcements.columns.type'), header: () => t('admin.announcements.columns.type'),
cell: ({ row }) => { cell: ({ row }) => {
const typeLabels: Record<string, string> = { const typeLabels: Record<string, string> = {
info: t('developer.announcements.types.info'), info: t('admin.announcements.types.info'),
warning: t('developer.announcements.types.warning'), warning: t('admin.announcements.types.warning'),
error: t('developer.announcements.types.error'), error: t('admin.announcements.types.error'),
success: t('developer.announcements.types.success'), success: t('admin.announcements.types.success'),
} }
const typeClasses: Record<string, string> = { const typeClasses: Record<string, string> = {
info: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', 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', accessorKey: 'status',
header: () => t('developer.announcements.columns.status'), header: () => t('admin.announcements.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const statusLabels: Record<string, string> = { const statusLabels: Record<string, string> = {
active: t('developer.announcements.statuses.active'), active: t('admin.announcements.statuses.active'),
inactive: t('developer.announcements.statuses.inactive'), inactive: t('admin.announcements.statuses.inactive'),
} }
const statusClasses: Record<string, string> = { const statusClasses: Record<string, string> = {
active: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', 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', accessorKey: 'is_top',
header: () => t('developer.announcements.columns.isTop'), header: () => t('admin.announcements.columns.isTop'),
cell: ({ row }) => { cell: ({ row }) => {
return h(Button, { return h(Button, {
variant: 'ghost', variant: 'ghost',
@@ -86,14 +86,14 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
}, },
{ {
accessorKey: 'created_at', accessorKey: 'created_at',
header: () => t('developer.announcements.columns.createdAt'), header: () => t('admin.announcements.columns.createdAt'),
cell: ({ row }) => { cell: ({ row }) => {
return h('span', { class: 'text-sm text-muted-foreground' }, new Date(row.original.created_at).toLocaleString('zh-CN')) return h('span', { class: 'text-sm text-muted-foreground' }, new Date(row.original.created_at).toLocaleString('zh-CN'))
}, },
}, },
{ {
id: 'actions', id: 'actions',
header: () => t('developer.announcements.columns.actions'), header: () => t('admin.announcements.columns.actions'),
cell: ({ row }) => { cell: ({ row }) => {
return h('div', { class: 'flex items-center justify-end gap-1' }, [ return h('div', { class: 'flex items-center justify-end gap-1' }, [
h(Button, { h(Button, {
@@ -33,7 +33,7 @@ function resetFilters() {
<template> <template>
<div class="flex items-center flex-1 space-x-2"> <div class="flex items-center flex-1 space-x-2">
<Input <Input
:placeholder="t('developer.announcements.searchAll')" :placeholder="t('admin.announcements.searchAll')"
:model-value="searchFilter" :model-value="searchFilter"
class="h-8 w-[200px] lg:w-[300px]" class="h-8 w-[200px] lg:w-[300px]"
@update:model-value="emit('update:searchFilter', $event as string)" @update:model-value="emit('update:searchFilter', $event as string)"
@@ -45,7 +45,7 @@ function resetFilters() {
class="h-8 px-2 lg:px-3" class="h-8 px-2 lg:px-3"
@click="resetFilters" @click="resetFilters"
> >
{{ t('developer.announcements.reset') }} {{ t('admin.announcements.reset') }}
<X class="size-4 ml-2" /> <X class="size-4 ml-2" />
</Button> </Button>
</div> </div>
@@ -48,13 +48,13 @@ const table = generateVueTable<Announcement>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.announcements.select', select: 'admin.announcements.select',
title: 'developer.announcements.columns.title', title: 'admin.announcements.columns.title',
application_name: 'developer.announcements.columns.application', application_name: 'admin.announcements.columns.application',
type: 'developer.announcements.columns.type', type: 'admin.announcements.columns.type',
status: 'developer.announcements.columns.status', status: 'admin.announcements.columns.status',
is_top: 'developer.announcements.columns.isTop', is_top: 'admin.announcements.columns.isTop',
created_at: 'developer.announcements.columns.createdAt', created_at: 'admin.announcements.columns.createdAt',
} }
defineExpose({ defineExpose({
@@ -68,10 +68,10 @@ defineExpose({
<div class="space-y-3"> <div class="space-y-3">
<BulkActions :table="table" entity-name="announcements"> <BulkActions :table="table" entity-name="announcements">
<UiButton variant="outline" size="sm" @click="emit('batchExport')"> <UiButton variant="outline" size="sm" @click="emit('batchExport')">
{{ t('developer.announcements.batchExportBtn') }} {{ t('admin.announcements.batchExportBtn') }}
</UiButton> </UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')"> <UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.announcements.batchDeleteBtn') }} {{ t('admin.announcements.batchDeleteBtn') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
@@ -85,7 +85,7 @@ defineExpose({
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" @click="emit('export')"> <UiButton variant="outline" size="sm" @click="emit('export')">
{{ t('developer.announcements.exportBtn') }} {{ t('admin.announcements.exportBtn') }}
</UiButton> </UiButton>
<DataTableViewOptions :table="table" :column-labels="columnLabels" /> <DataTableViewOptions :table="table" :column-labels="columnLabels" />
</div> </div>
@@ -85,7 +85,7 @@ async function handleSubmit() {
is_top: formData.value.is_top, is_top: formData.value.is_top,
}) })
toast.success('公告创建成功') toast.success('公告创建成功')
router.push('/developer/announcements') router.push('/admin/announcements')
} }
catch (error: any) { catch (error: any) {
console.error('创建公告失败:', error) console.error('创建公告失败:', error)
@@ -106,7 +106,7 @@ onMounted(() => {
title="创建公告" title="创建公告"
description="创建新的系统公告" description="创建新的系统公告"
:breadcrumbs="[ :breadcrumbs="[
{ title: '公告管理', href: '/developer/announcements' }, { title: '公告管理', href: '/admin/announcements' },
{ title: '创建公告' }, { title: '创建公告' },
]" ]"
sticky sticky
@@ -46,15 +46,15 @@ const applicationOptions = computed(() => {
}) })
const typeOptions = computed(() => [ const typeOptions = computed(() => [
{ label: t('developer.announcements.types.info'), value: 'info' }, { label: t('admin.announcements.types.info'), value: 'info' },
{ label: t('developer.announcements.types.warning'), value: 'warning' }, { label: t('admin.announcements.types.warning'), value: 'warning' },
{ label: t('developer.announcements.types.error'), value: 'error' }, { label: t('admin.announcements.types.error'), value: 'error' },
{ label: t('developer.announcements.types.success'), value: 'success' }, { label: t('admin.announcements.types.success'), value: 'success' },
]) ])
const statusOptions = computed(() => [ const statusOptions = computed(() => [
{ label: t('developer.announcements.statuses.active'), value: 'active' }, { label: t('admin.announcements.statuses.active'), value: 'active' },
{ label: t('developer.announcements.statuses.inactive'), value: 'inactive' }, { label: t('admin.announcements.statuses.inactive'), value: 'inactive' },
]) ])
const filteredAnnouncements = computed(() => { const filteredAnnouncements = computed(() => {
@@ -114,17 +114,17 @@ async function fetchAnnouncements() {
} }
function goToCreate() { function goToCreate() {
router.push('/developer/announcements/create') router.push('/admin/announcements/create')
} }
function goToEdit(announcement: Announcement) { function goToEdit(announcement: Announcement) {
router.push(`/developer/announcements/${announcement.id}`) router.push(`/admin/announcements/${announcement.id}`)
} }
async function toggleTop(announcement: Announcement) { async function toggleTop(announcement: Announcement) {
try { try {
await api.put(`/dev/applications/${announcement.application_id}/announcements/${announcement.id}/top`) 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() fetchAnnouncements()
} }
catch (error: any) { catch (error: any) {
@@ -144,7 +144,7 @@ async function handleDelete() {
try { try {
await api.delete(`/dev/applications/${deleteTarget.value.application_id}/announcements/${deleteTarget.value.id}`) 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() fetchAnnouncements()
} }
catch (error: any) { catch (error: any) {
@@ -189,7 +189,7 @@ function confirmBatchDelete(ids: number[]) {
async function handleBatchDelete() { async function handleBatchDelete() {
try { try {
await api.delete('/dev/announcements/batch', { ids: batchDeleteIds.value } as any) await api.delete('/dev/announcements/batch', { ids: batchDeleteIds.value } as any)
toast.success(t('developer.announcements.batchDeleteSuccess')) toast.success(t('admin.announcements.batchDeleteSuccess'))
fetchAnnouncements() fetchAnnouncements()
} }
catch (error: any) { catch (error: any) {
@@ -213,14 +213,14 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.announcements.title')" :title="t('admin.announcements.title')"
:description="t('developer.announcements.description')" :description="t('admin.announcements.description')"
sticky sticky
> >
<template #actions> <template #actions>
<UiButton @click="goToCreate"> <UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.announcements.create') }} {{ t('admin.announcements.create') }}
</UiButton> </UiButton>
</template> </template>
@@ -229,7 +229,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.announcements.totalAnnouncements') }} {{ t('admin.announcements.totalAnnouncements') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:megaphone" class="size-4 text-muted-foreground" /> <Icon icon="lucide:megaphone" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -243,7 +243,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.announcements.activeCount') }} {{ t('admin.announcements.activeCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -257,7 +257,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.announcements.inactiveCount') }} {{ t('admin.announcements.inactiveCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -271,7 +271,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.announcements.topCount') }} {{ t('admin.announcements.topCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:pin" class="size-4 text-muted-foreground" /> <Icon icon="lucide:pin" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -302,17 +302,17 @@ onMounted(() => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.announcements.application')" :title="t('admin.announcements.application')"
:options="applicationOptions" :options="applicationOptions"
/> />
<SingleFilter <SingleFilter
v-model="typeFilter" v-model="typeFilter"
:title="t('developer.announcements.type')" :title="t('admin.announcements.type')"
:options="typeOptions" :options="typeOptions"
/> />
<SingleFilter <SingleFilter
v-model="statusFilter" v-model="statusFilter"
:title="t('developer.announcements.status')" :title="t('admin.announcements.status')"
:options="statusOptions" :options="statusOptions"
/> />
</template> </template>
@@ -329,10 +329,10 @@ onMounted(() => {
@confirm="handleDelete" @confirm="handleDelete"
> >
<template #title> <template #title>
{{ t('developer.announcements.deleteAnnouncement') }} {{ t('admin.announcements.deleteAnnouncement') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.announcements.deleteConfirm', { title: deleteTarget?.title }) }} {{ t('admin.announcements.deleteConfirm', { title: deleteTarget?.title }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
@@ -344,10 +344,10 @@ onMounted(() => {
@confirm="handleBatchDelete" @confirm="handleBatchDelete"
> >
<template #title> <template #title>
{{ t('developer.announcements.batchDelete') }} {{ t('admin.announcements.batchDelete') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.announcements.batchDeleteConfirm', { count: batchDeleteIds.length }) }} {{ t('admin.announcements.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
</BasicPage> </BasicPage>
+17 -17
View File
@@ -120,14 +120,14 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.applications.title')" :title="t('admin.applications.title')"
:description="t('developer.applications.description')" :description="t('admin.applications.description')"
sticky sticky
> >
<template #actions> <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" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.applications.createApp') }} {{ t('admin.applications.createApp') }}
</UiButton> </UiButton>
</template> </template>
@@ -135,7 +135,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.applications.totalApps') }} {{ t('admin.applications.totalApps') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" /> <Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -149,7 +149,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.applications.activeApps') }} {{ t('admin.applications.activeApps') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -163,7 +163,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.applications.totalUsers') }} {{ t('admin.applications.totalUsers') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" /> <Icon icon="lucide:users" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -177,7 +177,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.applications.totalVerifyCount') }} {{ t('admin.applications.totalVerifyCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" /> <Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -193,23 +193,23 @@ onMounted(() => {
<div class="flex flex-col gap-4 sm:flex-row"> <div class="flex flex-col gap-4 sm:flex-row">
<UiInput <UiInput
v-model="searchQuery" v-model="searchQuery"
:placeholder="t('developer.applications.searchPlaceholder')" :placeholder="t('admin.applications.searchPlaceholder')"
class="h-9 w-40 lg:w-[250px]" class="h-9 w-40 lg:w-[250px]"
/> />
<UiSelect v-model="statusFilter"> <UiSelect v-model="statusFilter">
<UiSelectTrigger class="w-32"> <UiSelectTrigger class="w-32">
<UiSelectValue :placeholder="t('developer.applications.statusFilter')" /> <UiSelectValue :placeholder="t('admin.applications.statusFilter')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem value="all"> <UiSelectItem value="all">
{{ t('developer.applications.allStatus') }} {{ t('admin.applications.allStatus') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="active"> <UiSelectItem value="active">
{{ t('developer.applications.active') }} {{ t('admin.applications.active') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="inactive"> <UiSelectItem value="inactive">
{{ t('developer.applications.inactive') }} {{ t('admin.applications.inactive') }}
</UiSelectItem> </UiSelectItem>
</UiSelectContent> </UiSelectContent>
</UiSelect> </UiSelect>
@@ -227,14 +227,14 @@ onMounted(() => {
<Icon icon="lucide:layout-grid" class="size-8 text-muted-foreground" /> <Icon icon="lucide:layout-grid" class="size-8 text-muted-foreground" />
</div> </div>
<h3 class="text-lg font-medium"> <h3 class="text-lg font-medium">
{{ t('developer.applications.noApps') }} {{ t('admin.applications.noApps') }}
</h3> </h3>
<p class="text-muted-foreground mt-1 mb-4"> <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> </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" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.applications.createApp') }} {{ t('admin.applications.createApp') }}
</UiButton> </UiButton>
</div> </div>
@@ -259,7 +259,7 @@ onMounted(() => {
title="邮箱设置" title="邮箱设置"
description="配置应用的邮箱验证相关设置" description="配置应用的邮箱验证相关设置"
:breadcrumbs="[ :breadcrumbs="[
{ title: '应用管理', href: '/developer/applications' }, { title: '应用管理', href: '/admin/applications' },
{ title: '邮箱设置' }, { title: '邮箱设置' },
]" ]"
sticky sticky
@@ -140,7 +140,7 @@ onMounted(() => {
title="安全设置" title="安全设置"
description="配置应用的安全相关设置,包括加密方式和登录策略" description="配置应用的安全相关设置,包括加密方式和登录策略"
:breadcrumbs="[ :breadcrumbs="[
{ title: '应用管理', href: '/developer/applications' }, { title: '应用管理', href: '/admin/applications' },
{ title: '安全设置' }, { title: '安全设置' },
]" ]"
sticky sticky
@@ -251,7 +251,7 @@ function toggleWeekday(index: number) {
title="基本设置" title="基本设置"
description="修改应用的基本信息,包括名称、描述和图标等" description="修改应用的基本信息,包括名称、描述和图标等"
:breadcrumbs="[ :breadcrumbs="[
{ title: '应用管理', href: '/developer/applications' }, { title: '应用管理', href: '/admin/applications' },
{ title: '基本设置' }, { title: '基本设置' },
]" ]"
sticky sticky
@@ -193,7 +193,7 @@ export function getColumns(actions: {
h(DropdownMenuSeparator), h(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(app) }, () => [ h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(app) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }), 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) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.applications.select', select: 'admin.applications.select',
name: 'developer.applications.columns.name', name: 'admin.applications.columns.name',
app_key: 'developer.applications.columns.appKey', app_key: 'admin.applications.columns.appKey',
billing_type: 'developer.applications.columns.billingType', billing_type: 'admin.applications.columns.billingType',
login_policy: 'developer.applications.columns.loginPolicy', login_policy: 'admin.applications.columns.loginPolicy',
enable_trial: 'developer.applications.columns.trial', enable_trial: 'admin.applications.columns.trial',
status: 'developer.applications.columns.status', status: 'admin.applications.columns.status',
created_at: 'developer.applications.columns.createdAt', created_at: 'admin.applications.columns.createdAt',
} }
defineExpose({ defineExpose({
@@ -69,7 +69,7 @@ defineExpose({
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UiInput <UiInput
:model-value="searchFilter" :model-value="searchFilter"
:placeholder="t('developer.applications.searchPlaceholder')" :placeholder="t('admin.applications.searchPlaceholder')"
class="h-9 w-40 lg:w-[250px]" class="h-9 w-40 lg:w-[250px]"
@update:model-value="emit('update:searchFilter', String($event))" @update:model-value="emit('update:searchFilter', String($event))"
/> />
@@ -74,7 +74,7 @@ function toggleWeekday(index: number) {
async function handleCreate() { async function handleCreate() {
if (!formData.value.name.trim()) { if (!formData.value.name.trim()) {
toast.error(t('developer.applications.appNameRequired')) toast.error(t('admin.applications.appNameRequired'))
return return
} }
@@ -120,16 +120,16 @@ async function handleCreate() {
const data = await response.json() const data = await response.json()
if (data.code === 200) { if (data.code === 200) {
toast.success(t('developer.applications.createSuccess')) toast.success(t('admin.applications.createSuccess'))
router.push('/developer/applications') router.push('/admin/applications')
} }
else { else {
toast.error(data.message || t('developer.applications.createFailed')) toast.error(data.message || t('admin.applications.createFailed'))
} }
} }
catch (error) { catch (error) {
console.error('创建应用失败:', error) console.error('创建应用失败:', error)
toast.error(t('developer.applications.createFailed')) toast.error(t('admin.applications.createFailed'))
} }
finally { finally {
loading.value = false loading.value = false
@@ -139,31 +139,31 @@ async function handleCreate() {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.applications.createApp')" :title="t('admin.applications.createApp')"
:description="t('developer.applications.createAppDesc')" :description="t('admin.applications.createAppDesc')"
:breadcrumbs="[ :breadcrumbs="[
{ title: t('developer.applications.title'), href: '/developer/applications' }, { title: t('admin.applications.title'), href: '/admin/applications' },
{ title: t('developer.applications.createApp') }, { title: t('admin.applications.createApp') },
]" ]"
sticky sticky
> >
<div class="space-y-6"> <div class="space-y-6">
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.applications.basicInfo') }}</UiCardTitle> <UiCardTitle>{{ t('admin.applications.basicInfo') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.basicInfoDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.applications.basicInfoDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="name"> <UiLabel for="name">
{{ t('developer.applications.appName') }} * {{ t('admin.applications.appName') }} *
</UiLabel> </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>
<div class="space-y-2"> <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 class="flex items-center gap-4">
<div v-if="formData.iconPreview" class="relative group"> <div v-if="formData.iconPreview" class="relative group">
<img <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" 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" /> <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 <input
id="appIcon" id="appIcon"
type="file" type="file"
@@ -200,17 +200,17 @@ async function handleCreate() {
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="description"> <UiLabel for="description">
{{ t('developer.applications.appDescription') }} {{ t('admin.applications.appDescription') }}
</UiLabel> </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> </div>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.applications.operationMode') }}</UiCardTitle> <UiCardTitle>{{ t('admin.applications.operationMode') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.operationModeDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.applications.operationModeDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent>
<UiRadioGroup v-model="formData.billing_type" class="grid grid-cols-1 md:grid-cols-2 gap-4"> <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" /> <UiRadioGroupItem value="free" class="mt-0.5" />
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.freeMode') }} {{ t('admin.applications.freeMode') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{{ t('developer.applications.freeModeDesc') }} {{ t('admin.applications.freeModeDesc') }}
</p> </p>
</div> </div>
</div> </div>
@@ -238,10 +238,10 @@ async function handleCreate() {
<UiRadioGroupItem value="subscription" class="mt-0.5" /> <UiRadioGroupItem value="subscription" class="mt-0.5" />
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.subscriptionMode') }} {{ t('admin.applications.subscriptionMode') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{{ t('developer.applications.subscriptionModeDesc') }} {{ t('admin.applications.subscriptionModeDesc') }}
</p> </p>
</div> </div>
</div> </div>
@@ -254,10 +254,10 @@ async function handleCreate() {
<UiRadioGroupItem value="time" class="mt-0.5" /> <UiRadioGroupItem value="time" class="mt-0.5" />
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.timeMode') }} {{ t('admin.applications.timeMode') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{{ t('developer.applications.timeModeDesc') }} {{ t('admin.applications.timeModeDesc') }}
</p> </p>
</div> </div>
</div> </div>
@@ -270,10 +270,10 @@ async function handleCreate() {
<UiRadioGroupItem value="count" class="mt-0.5" /> <UiRadioGroupItem value="count" class="mt-0.5" />
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.countMode') }} {{ t('admin.applications.countMode') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{{ t('developer.applications.countModeDesc') }} {{ t('admin.applications.countModeDesc') }}
</p> </p>
</div> </div>
</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 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="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>{{ t('developer.applications.deductionCycle') }}</UiLabel> <UiLabel>{{ t('admin.applications.deductionCycle') }}</UiLabel>
<UiSelect v-model="formData.deduction_cycle"> <UiSelect v-model="formData.deduction_cycle">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.applications.selectDeductionCycle')" /> <UiSelectValue :placeholder="t('admin.applications.selectDeductionCycle')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem value="per_use"> <UiSelectItem value="per_use">
{{ t('developer.applications.perUse') }} {{ t('admin.applications.perUse') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="per_minute"> <UiSelectItem value="per_minute">
{{ t('developer.applications.perMinute') }} {{ t('admin.applications.perMinute') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="per_hour"> <UiSelectItem value="per_hour">
{{ t('developer.applications.perHour') }} {{ t('admin.applications.perHour') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="per_day"> <UiSelectItem value="per_day">
{{ t('developer.applications.perDay') }} {{ t('admin.applications.perDay') }}
</UiSelectItem> </UiSelectItem>
</UiSelectContent> </UiSelectContent>
</UiSelect> </UiSelect>
</div> </div>
<div class="space-y-2"> <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"> <UiNumberField v-model="formData.deduction_amount" :min="0" :step="0.01">
<UiNumberFieldContent> <UiNumberFieldContent>
<UiNumberFieldDecrement /> <UiNumberFieldDecrement />
@@ -321,8 +321,8 @@ async function handleCreate() {
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.applications.loginPolicy') }}</UiCardTitle> <UiCardTitle>{{ t('admin.applications.loginPolicy') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.loginPolicyDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.applications.loginPolicyDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent>
<UiRadioGroup v-model="formData.login_policy" class="grid grid-cols-1 md:grid-cols-3 gap-4"> <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" /> <UiRadioGroupItem value="loose" class="mt-0.5" />
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.looseMode') }} {{ t('admin.applications.looseMode') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{{ t('developer.applications.looseModeDesc') }} {{ t('admin.applications.looseModeDesc') }}
</p> </p>
</div> </div>
</div> </div>
@@ -350,10 +350,10 @@ async function handleCreate() {
<UiRadioGroupItem value="strict" class="mt-0.5" /> <UiRadioGroupItem value="strict" class="mt-0.5" />
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.strictMode') }} {{ t('admin.applications.strictMode') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{{ t('developer.applications.strictModeDesc') }} {{ t('admin.applications.strictModeDesc') }}
</p> </p>
</div> </div>
</div> </div>
@@ -366,10 +366,10 @@ async function handleCreate() {
<UiRadioGroupItem value="hybrid" class="mt-0.5" /> <UiRadioGroupItem value="hybrid" class="mt-0.5" />
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.hybridMode') }} {{ t('admin.applications.hybridMode') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{{ t('developer.applications.hybridModeDesc') }} {{ t('admin.applications.hybridModeDesc') }}
</p> </p>
</div> </div>
</div> </div>
@@ -379,14 +379,14 @@ async function handleCreate() {
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.applications.trialSettings') }}</UiCardTitle> <UiCardTitle>{{ t('admin.applications.trialSettings') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.trialSettings') }}</UiCardDescription> <UiCardDescription>{{ t('admin.applications.trialSettings') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.enableTrial') }} {{ t('admin.applications.enableTrial') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <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 v-if="formData.enable_trial" class="space-y-4 pt-4 border-t">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>{{ t('developer.applications.trialBalance') }}</UiLabel> <UiLabel>{{ t('admin.applications.trialBalance') }}</UiLabel>
<UiNumberField <UiNumberField
v-model="formData.trial_balance" v-model="formData.trial_balance"
:min="0" :min="0"
@@ -420,14 +420,14 @@ async function handleCreate() {
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.applications.freePeriodSettings') }}</UiCardTitle> <UiCardTitle>{{ t('admin.applications.freePeriodSettings') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.freePeriodSettings') }}</UiCardDescription> <UiCardDescription>{{ t('admin.applications.freePeriodSettings') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.enableFreePeriod') }} {{ t('admin.applications.enableFreePeriod') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <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 v-if="formData.enable_free_period" class="space-y-4 pt-4 border-t">
<div class="space-y-2"> <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"> <UiRadioGroup v-model="formData.free_period_type" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div <div
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors" 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" /> <UiRadioGroupItem value="range" class="mt-0.5" />
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.dateRange') }} {{ t('admin.applications.dateRange') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
指定开始和结束日期时间 指定开始和结束日期时间
@@ -464,7 +464,7 @@ async function handleCreate() {
<UiRadioGroupItem value="weekly" class="mt-0.5" /> <UiRadioGroupItem value="weekly" class="mt-0.5" />
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.weekly') }} {{ t('admin.applications.weekly') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <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 v-if="formData.free_period_type === 'range'" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>{{ t('developer.applications.startTime') }}</UiLabel> <UiLabel>{{ t('admin.applications.startTime') }}</UiLabel>
<UiDatePickerDateTimePicker v-model="formData.free_period_start" :placeholder="t('developer.applications.startTime')" /> <UiDatePickerDateTimePicker v-model="formData.free_period_start" :placeholder="t('admin.applications.startTime')" />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>{{ t('developer.applications.endTime') }}</UiLabel> <UiLabel>{{ t('admin.applications.endTime') }}</UiLabel>
<UiDatePickerDateTimePicker v-model="formData.free_period_end" :placeholder="t('developer.applications.endTime')" /> <UiDatePickerDateTimePicker v-model="formData.free_period_end" :placeholder="t('admin.applications.endTime')" />
</div> </div>
</div> </div>
<div v-if="formData.free_period_type === 'weekly'" class="space-y-4"> <div v-if="formData.free_period_type === 'weekly'" class="space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>{{ t('developer.applications.weekdays') }}</UiLabel> <UiLabel>{{ t('admin.applications.weekdays') }}</UiLabel>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<template v-for="(day, index) in ['周一', '周二', '周三', '周四', '周五', '周六', '周日']" :key="index"> <template v-for="(day, index) in ['周一', '周二', '周三', '周四', '周五', '周六', '周日']" :key="index">
<UiButton <UiButton
@@ -507,15 +507,15 @@ async function handleCreate() {
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>{{ t('developer.applications.startTime') }}</UiLabel> <UiLabel>{{ t('admin.applications.startTime') }}</UiLabel>
<UiDatePickerTimePicker v-model="formData.free_period_start_time" :placeholder="t('developer.applications.startTime')" /> <UiDatePickerTimePicker v-model="formData.free_period_start_time" :placeholder="t('admin.applications.startTime')" />
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
每天免费开始时间 每天免费开始时间
</p> </p>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>{{ t('developer.applications.endTime') }}</UiLabel> <UiLabel>{{ t('admin.applications.endTime') }}</UiLabel>
<UiDatePickerTimePicker v-model="formData.free_period_end_time" :placeholder="t('developer.applications.endTime')" /> <UiDatePickerTimePicker v-model="formData.free_period_end_time" :placeholder="t('admin.applications.endTime')" />
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
每天免费结束时间 每天免费结束时间
</p> </p>
@@ -528,29 +528,29 @@ async function handleCreate() {
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.applications.securitySettings') }}</UiCardTitle> <UiCardTitle>{{ t('admin.applications.securitySettings') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.applications.securitySettingsDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.applications.securitySettingsDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>{{ t('developer.applications.encryptionType') }}</UiLabel> <UiLabel>{{ t('admin.applications.encryptionType') }}</UiLabel>
<UiSelect v-model="formData.encrypt_type"> <UiSelect v-model="formData.encrypt_type">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.applications.encryptionType')" /> <UiSelectValue :placeholder="t('admin.applications.encryptionType')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem value="none"> <UiSelectItem value="none">
{{ t('developer.applications.noEncryption') }} {{ t('admin.applications.noEncryption') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="aes"> <UiSelectItem value="aes">
{{ t('developer.applications.aesEncryption') }} {{ t('admin.applications.aesEncryption') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="rsa"> <UiSelectItem value="rsa">
{{ t('developer.applications.rsaEncryption') }} {{ t('admin.applications.rsaEncryption') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="rc4"> <UiSelectItem value="rc4">
{{ t('developer.applications.rc4Encryption') }} {{ t('admin.applications.rc4Encryption') }}
</UiSelectItem> </UiSelectItem>
</UiSelectContent> </UiSelectContent>
</UiSelect> </UiSelect>
@@ -563,7 +563,7 @@ async function handleCreate() {
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>{{ t('developer.applications.secretKey') }}</UiLabel> <UiLabel>{{ t('admin.applications.secretKey') }}</UiLabel>
<div class="relative"> <div class="relative">
<UiInput <UiInput
v-model="formData.secret_key" v-model="formData.secret_key"
@@ -591,14 +591,14 @@ async function handleCreate() {
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.applications.deviceBind') }}</UiCardTitle> <UiCardTitle>{{ t('admin.applications.deviceBind') }}</UiCardTitle>
<UiCardDescription>控制用户登录时是否需要绑定设备</UiCardDescription> <UiCardDescription>控制用户登录时是否需要绑定设备</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.deviceBind') }} {{ t('admin.applications.deviceBind') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
启用后用户登录时需要绑定设备可限制同时登录的设备数量 启用后用户登录时需要绑定设备可限制同时登录的设备数量
@@ -611,7 +611,7 @@ async function handleCreate() {
</div> </div>
<div v-if="formData.bind_type === 'device'" class="space-y-2 pt-4 border-t"> <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"> <UiNumberField v-model="formData.max_devices" :min="1" :max="10">
<UiNumberFieldContent> <UiNumberFieldContent>
<UiNumberFieldDecrement /> <UiNumberFieldDecrement />
@@ -627,7 +627,7 @@ async function handleCreate() {
<div class="flex items-center justify-between pt-4 border-t"> <div class="flex items-center justify-between pt-4 border-t">
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.applications.allowMultiOpen') }} {{ t('admin.applications.allowMultiOpen') }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
允许同一账号在同一设备上同时运行多个实例 允许同一账号在同一设备上同时运行多个实例
@@ -639,12 +639,12 @@ async function handleCreate() {
</UiCard> </UiCard>
<div class="flex justify-end gap-3"> <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>
<UiButton :disabled="loading" @click="handleCreate"> <UiButton :disabled="loading" @click="handleCreate">
<Icon v-if="loading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" /> <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> </UiButton>
</div> </div>
</div> </div>
+343 -258
View File
@@ -2,57 +2,89 @@
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 { 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 { t } = useI18n()
const router = useRouter()
const API_BASE = 'http://localhost:8080/api/v1'
interface Application { interface Application {
id: string id: string
name: string name: string
} }
const loading = ref(true) const loading = ref(true)
const saving = ref(false) const cardTypes = ref<CardType[]>([])
const applications = ref<Application[]>([]) const applications = ref<Application[]>([])
const cardType = ref<CardType | null>(null) const tableRef = ref()
const form = ref({ const appFilter = ref<string>('')
name: '', const statusFilter = ref<string>('')
description: '',
application_id: '', const deleteDialogOpen = ref(false)
price: 0, const deleteTarget = ref<CardType | null>(null)
value: 0, const batchDeleteDialogOpen = ref(false)
validity_type: 'limited' as 'permanent' | 'limited', 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 loading.value = true
try { try {
const data = await api.get<{ card_type: CardType }>(`/dev/card-types/${route.params.id}`) const token = localStorage.getItem('token')
cardType.value = data?.card_type || data const response = await fetch(`${API_BASE}/dev/card-types`, {
if (cardType.value) { headers: {
form.value = { Authorization: `Bearer ${token}`,
name: cardType.value.name, },
description: cardType.value.description, })
application_id: cardType.value.application_id ? String(cardType.value.application_id) : '', const data = await response.json()
price: cardType.value.price, if (data.code === 200) {
value: cardType.value.value === -1 ? 0 : cardType.value.value, cardTypes.value = Array.isArray(data.data?.card_types) ? data.data.card_types : []
validity_type: cardType.value.value === -1 ? 'permanent' : 'limited', }
} else {
cardTypes.value = []
} }
} }
catch (error: any) { catch (error) {
console.error('获取卡类信息失败:', error) console.error('获取卡密类型失败:', error)
toast.error(error.message || '获取卡类信息失败') cardTypes.value = []
router.push('/developer/card-types')
} }
finally { finally {
loading.value = false loading.value = false
@@ -61,264 +93,317 @@ async function fetchCardType() {
async function fetchApplications() { async function fetchApplications() {
try { try {
const data = await api.get<{ applications: Application[] }>('/dev/applications') const token = localStorage.getItem('token')
applications.value = Array.isArray(data?.applications) ? data.applications : [] 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) { catch (error) {
console.error('获取应用列表失败:', error) console.error('获取应用列表失败:', error)
applications.value = []
} }
} }
const selectedApplication = computed(() => { function goToCreate() {
if (form.value.application_id) { router.push('/admin/card-types/create')
return applications.value.find(app => String(app.id) === form.value.application_id) }
}
return null
})
const displayValue = computed(() => { function goToEdit(type: CardType) {
if (form.value.validity_type === 'permanent') { router.push(`/admin/card-types/${type.id}`)
return t('developer.cardTypes.units.permanent') }
}
return form.value.value.toString()
})
async function handleSave() { async function toggleStatus(type: CardType) {
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
try { try {
await api.put(`/dev/card-types/${route.params.id}`, { const token = localStorage.getItem('token')
application_id: Number(form.value.application_id), const newStatus = type.status === 'active' ? 'inactive' : 'active'
name: form.value.name, const response = await fetch(`${API_BASE}/dev/card-types/${type.id}`, {
description: form.value.description, method: 'PUT',
value, headers: {
price: form.value.price, '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) { catch (error) {
console.error('保存卡密类型失败:', error) console.error('切换卡密类型状态失败:', error)
toast.error(error.message || t('developer.cardTypes.create.saveFailed')) 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 { 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(() => { onMounted(() => {
fetchCardType() fetchCardTypes()
fetchApplications() fetchApplications()
}) })
</script> </script>
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cardTypes.create.editTitle')" :title="t('admin.cardTypes.title')"
:description="t('developer.cardTypes.create.editDescription')" :description="t('admin.cardTypes.description')"
:breadcrumbs="[
{ title: t('developer.cardTypes.title'), href: '/developer/card-types' },
{ title: t('developer.cardTypes.create.editTitle') },
]"
sticky sticky
> >
<div v-if="loading" class="flex items-center justify-center py-12"> <template #actions>
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" /> <UiButton @click="goToCreate">
</div> <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="space-y-6">
<div class="grid gap-6 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="lg:col-span-2 space-y-6"> <UiCard>
<UiCard> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardHeader> <UiCardTitle class="text-sm font-medium">
<UiCardTitle class="flex items-center gap-2"> {{ t('admin.cardTypes.totalTypes') }}
<Icon icon="lucide:info" class="size-5" /> </UiCardTitle>
{{ t('developer.cardTypes.create.basicInfo') }} <Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
</UiCardTitle> </UiCardHeader>
<UiCardDescription>{{ t('developer.cardTypes.create.basicInfoDesc') }}</UiCardDescription> <UiCardContent>
</UiCardHeader> <div class="text-2xl font-bold">
<UiCardContent class="space-y-6"> {{ filteredCardTypes.length }}
<div class="space-y-2"> </div>
<UiLabel for="name"> </UiCardContent>
{{ t('developer.cardTypes.create.name') }} * </UiCard>
</UiLabel>
<UiInput id="name" v-model="form.name" :placeholder="t('developer.cardTypes.create.namePlaceholder')" />
</div>
<div class="space-y-2"> <UiCard>
<UiLabel for="description"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.cardTypes.create.description') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cardTypes.activeTypes') }}
<UiTextarea id="description" v-model="form.description" :placeholder="t('developer.cardTypes.create.descriptionPlaceholder')" rows="3" /> </UiCardTitle>
</div> <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"> <UiCard>
<UiLabel for="application"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.cardTypes.create.application') }} * <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cardTypes.generatedCards') }}
<UiSelect v-model="form.application_id"> </UiCardTitle>
<UiSelectTrigger> <Icon icon="lucide:hash" class="size-4 text-muted-foreground" />
<UiSelectValue :placeholder="t('developer.cardTypes.create.selectApplication')" /> </UiCardHeader>
</UiSelectTrigger> <UiCardContent>
<UiSelectContent> <div class="text-2xl font-bold">
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)"> {{ totalGeneratedCount }}
{{ app.name }} </div>
</UiSelectItem> </UiCardContent>
</UiSelectContent> </UiCard>
</UiSelect>
</div>
</UiCardContent>
</UiCard>
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="text-sm font-medium">
<Icon icon="lucide:settings-2" class="size-5" /> {{ t('admin.cardTypes.totalValue') }}
{{ t('developer.cardTypes.create.billingConfig') }} </UiCardTitle>
</UiCardTitle> <Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
<UiCardDescription>{{ t('developer.cardTypes.create.billingConfigDesc') }}</UiCardDescription> </UiCardHeader>
</UiCardHeader> <UiCardContent>
<UiCardContent class="space-y-6"> <div class="text-2xl font-bold">
<div class="space-y-2"> ¥{{ totalValue }}
<UiLabel for="price"> </div>
{{ t('developer.cardTypes.create.price') }} </UiCardContent>
</UiLabel> </UiCard>
<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>
</div> </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> </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> </BasicPage>
</template> </template>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Ban, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next' import { Ban, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next'
import { h } from 'vue' 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 Badge from '@/components/ui/badge/Badge.vue'
import Button from '@/components/ui/button/Button.vue' import Button from '@/components/ui/button/Button.vue'
@@ -24,14 +24,14 @@ export function getColumns(actions: {
return [ return [
{ {
accessorKey: 'name', accessorKey: 'name',
header: () => t('developer.cardTypes.columns.name'), header: () => t('admin.cardTypes.columns.name'),
cell: ({ row }) => { cell: ({ row }) => {
return row.getValue('name') as string return row.getValue('name') as string
}, },
}, },
{ {
accessorKey: 'description', accessorKey: 'description',
header: () => t('developer.cardTypes.columns.description'), header: () => t('admin.cardTypes.columns.description'),
cell: ({ row }) => { cell: ({ row }) => {
const description = row.getValue('description') as string const description = row.getValue('description') as string
return description || '-' return description || '-'
@@ -39,7 +39,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'application.name', accessorKey: 'application.name',
header: () => t('developer.cardTypes.columns.application'), header: () => t('admin.cardTypes.columns.application'),
cell: ({ row }) => { cell: ({ row }) => {
const appName = row.original.application?.name const appName = row.original.application?.name
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-' return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -56,7 +56,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'price', accessorKey: 'price',
header: () => t('developer.cardTypes.columns.price'), header: () => t('admin.cardTypes.columns.price'),
cell: ({ row }) => { cell: ({ row }) => {
const price = row.getValue('price') as number const price = row.getValue('price') as number
return `¥${price}` return `¥${price}`
@@ -64,14 +64,14 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'value', accessorKey: 'value',
header: () => t('developer.cardTypes.columns.value'), header: () => t('admin.cardTypes.columns.value'),
cell: ({ row }) => { cell: ({ row }) => {
const value = row.getValue('value') as number const value = row.getValue('value') as number
const rechargeType = row.original.recharge_type const rechargeType = row.original.recharge_type
const valueUnit = row.original.value_unit const valueUnit = row.original.value_unit
if (value === -1) { 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) { if (rechargeType === 'subscription' && valueUnit) {
@@ -90,24 +90,24 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'generatedCount', accessorKey: 'generatedCount',
header: () => t('developer.cardTypes.columns.generatedCount'), header: () => t('admin.cardTypes.columns.generatedCount'),
cell: ({ row }) => { cell: ({ row }) => {
const count = row.original.generatedCount ?? 0 const count = row.original.generatedCount ?? 0
return `${count} ${t('developer.cardTypes.units.cards')}` return `${count} ${t('admin.cardTypes.units.cards')}`
}, },
}, },
{ {
accessorKey: 'status', accessorKey: 'status',
header: () => t('developer.cardTypes.columns.status'), header: () => t('admin.cardTypes.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const status = row.getValue('status') as string const status = row.getValue('status') as string
const isActive = status === 'active' 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', accessorKey: 'created_at',
header: () => t('developer.cardTypes.columns.createdAt'), header: () => t('admin.cardTypes.columns.createdAt'),
cell: ({ row }) => { cell: ({ row }) => {
const createdAt = row.getValue('created_at') const createdAt = row.getValue('created_at')
if (!createdAt) if (!createdAt)
@@ -152,16 +152,16 @@ export function getColumns(actions: {
() => [ () => [
h(DropdownMenuItem, { onClick: () => actions.onEdit(cardType) }, () => [ h(DropdownMenuItem, { onClick: () => actions.onEdit(cardType) }, () => [
h(Pencil, { class: 'mr-2 h-4 w-4' }), h(Pencil, { class: 'mr-2 h-4 w-4' }),
t('developer.cardTypes.edit'), t('admin.cardTypes.edit'),
]), ]),
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(cardType) }, () => [ h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(cardType) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }), 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(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(cardType) }, () => [ h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(cardType) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }), 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 { computed } from 'vue'
import { useI18n } from 'vue-i18n' 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 { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template> <template>
<div class="flex items-center flex-1 space-x-2"> <div class="flex items-center flex-1 space-x-2">
<Input <Input
:placeholder="t('developer.cardTypes.searchName')" :placeholder="t('admin.cardTypes.searchName')"
:model-value="(table.getColumn('name')?.getFilterValue() as string) ?? ''" :model-value="(table.getColumn('name')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]" class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('name')?.setFilterValue($event.target.value)" @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" class="h-8 px-2 lg:px-3"
@click="table.resetColumnFilters()" @click="table.resetColumnFilters()"
> >
{{ t('developer.cardTypes.reset') }} {{ t('admin.cardTypes.reset') }}
<X class="size-4 ml-2" /> <X class="size-4 ml-2" />
</Button> </Button>
</div> </div>
@@ -46,16 +46,16 @@ const table = generateVueTable<CardType>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.cardTypes.select', select: 'admin.cardTypes.select',
name: 'developer.cardTypes.columns.name', name: 'admin.cardTypes.columns.name',
description: 'developer.cardTypes.columns.description', description: 'admin.cardTypes.columns.description',
application_name: 'developer.cardTypes.columns.application', application_name: 'admin.cardTypes.columns.application',
billing_type: 'developer.cardTypes.columns.billingType', billing_type: 'admin.cardTypes.columns.billingType',
price: 'developer.cardTypes.columns.price', price: 'admin.cardTypes.columns.price',
value: 'developer.cardTypes.columns.value', value: 'admin.cardTypes.columns.value',
generatedCount: 'developer.cardTypes.columns.generatedCount', generatedCount: 'admin.cardTypes.columns.generatedCount',
status: 'developer.cardTypes.columns.status', status: 'admin.cardTypes.columns.status',
created_at: 'developer.cardTypes.columns.createdAt', created_at: 'admin.cardTypes.columns.createdAt',
} }
defineExpose({ defineExpose({
@@ -72,13 +72,13 @@ defineExpose({
</div> </div>
<BulkActions :table="table" entity-name="cardTypes"> <BulkActions :table="table" entity-name="cardTypes">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')"> <UiButton variant="outline" size="sm" @click="emit('batchEnable')">
{{ t('developer.cardTypes.batchEnableBtn') }} {{ t('admin.cardTypes.batchEnableBtn') }}
</UiButton> </UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')"> <UiButton variant="outline" size="sm" @click="emit('batchDisable')">
{{ t('developer.cardTypes.batchDisableBtn') }} {{ t('admin.cardTypes.batchDisableBtn') }}
</UiButton> </UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')"> <UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.cardTypes.batchDeleteBtn') }} {{ t('admin.cardTypes.batchDeleteBtn') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2"> <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(() => { const displayValue = computed(() => {
if (form.value.is_permanent) { if (form.value.is_permanent) {
return t('developer.cardTypes.units.permanent') return t('admin.cardTypes.units.permanent')
} }
if (form.value.recharge_type === 'subscription') { if (form.value.recharge_type === 'subscription') {
const unitMap: Record<string, string> = { const unitMap: Record<string, string> = {
minute: t('developer.cardTypes.units.minute'), minute: t('admin.cardTypes.units.minute'),
hour: t('developer.cardTypes.units.hour'), hour: t('admin.cardTypes.units.hour'),
day: t('developer.cardTypes.units.day'), day: t('admin.cardTypes.units.day'),
month: t('developer.cardTypes.units.month'), month: t('admin.cardTypes.units.month'),
year: t('developer.cardTypes.units.year'), year: t('admin.cardTypes.units.year'),
} }
return `${form.value.value} ${unitMap[form.value.value_unit]}` return `${form.value.value} ${unitMap[form.value.value_unit]}`
} }
@@ -78,17 +78,17 @@ const displayValue = computed(() => {
async function handleCreate() { async function handleCreate() {
if (!form.value.name) { if (!form.value.name) {
toast.error(t('developer.cardTypes.create.nameRequired')) toast.error(t('admin.cardTypes.create.nameRequired'))
return return
} }
if (!form.value.application_id) { if (!form.value.application_id) {
toast.error(t('developer.cardTypes.create.applicationRequired')) toast.error(t('admin.cardTypes.create.applicationRequired'))
return return
} }
if (!form.value.is_permanent && form.value.value <= 0) { if (!form.value.is_permanent && form.value.value <= 0) {
toast.error(t('developer.cardTypes.create.valueRequired')) toast.error(t('admin.cardTypes.create.valueRequired'))
return return
} }
@@ -103,12 +103,12 @@ async function handleCreate() {
value_unit: form.value.recharge_type === 'subscription' ? form.value.value_unit : '', value_unit: form.value.recharge_type === 'subscription' ? form.value.value_unit : '',
price: form.value.price, price: form.value.price,
}) })
toast.success(t('developer.cardTypes.create.createSuccess')) toast.success(t('admin.cardTypes.create.createSuccess'))
router.push('/developer/card-types') router.push('/admin/card-types')
} }
catch (error: any) { catch (error: any) {
console.error('创建卡密类型失败:', error) console.error('创建卡密类型失败:', error)
toast.error(error.message || t('developer.cardTypes.create.createFailed')) toast.error(error.message || t('admin.cardTypes.create.createFailed'))
} }
finally { finally {
saving.value = false saving.value = false
@@ -122,11 +122,11 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cardTypes.create.title')" :title="t('admin.cardTypes.create.title')"
:description="t('developer.cardTypes.create.description')" :description="t('admin.cardTypes.create.description')"
:breadcrumbs="[ :breadcrumbs="[
{ title: t('developer.cardTypes.title'), href: '/developer/card-types' }, { title: t('admin.cardTypes.title'), href: '/admin/card-types' },
{ title: t('developer.cardTypes.create.title') }, { title: t('admin.cardTypes.create.title') },
]" ]"
sticky sticky
> >
@@ -137,32 +137,32 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" /> <Icon icon="lucide:info" class="size-5" />
{{ t('developer.cardTypes.create.basicInfo') }} {{ t('admin.cardTypes.create.basicInfo') }}
</UiCardTitle> </UiCardTitle>
<UiCardDescription>{{ t('developer.cardTypes.create.basicInfoDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.cardTypes.create.basicInfoDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="name"> <UiLabel for="name">
{{ t('developer.cardTypes.create.name') }} * {{ t('admin.cardTypes.create.name') }} *
</UiLabel> </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>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="description"> <UiLabel for="description">
{{ t('developer.cardTypes.create.description') }} {{ t('admin.cardTypes.create.description') }}
</UiLabel> </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>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="application"> <UiLabel for="application">
{{ t('developer.cardTypes.create.application') }} * {{ t('admin.cardTypes.create.application') }} *
</UiLabel> </UiLabel>
<UiSelect v-model="form.application_id"> <UiSelect v-model="form.application_id">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cardTypes.create.selectApplication')" /> <UiSelectValue :placeholder="t('admin.cardTypes.create.selectApplication')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)"> <UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
@@ -222,7 +222,7 @@ onMounted(() => {
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="price"> <UiLabel for="price">
{{ t('developer.cardTypes.create.price') }} {{ t('admin.cardTypes.create.price') }}
</UiLabel> </UiLabel>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UiNumberField v-model="form.price" :min="0" :step="0.01" class="max-w-[200px]"> <UiNumberField v-model="form.price" :min="0" :step="0.01" class="max-w-[200px]">
@@ -232,7 +232,7 @@ onMounted(() => {
<UiNumberFieldIncrement /> <UiNumberFieldIncrement />
</UiNumberFieldContent> </UiNumberFieldContent>
</UiNumberField> </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>
</div> </div>
@@ -313,17 +313,17 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" /> <Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cardTypes.create.preview') }} {{ t('admin.cardTypes.create.preview') }}
</UiCardTitle> </UiCardTitle>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between text-sm"> <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> <span>{{ form.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ selectedApplication?.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
@@ -331,11 +331,11 @@ onMounted(() => {
<span>{{ form.recharge_type === 'balance' ? '余额充值' : '订阅充值' }}</span> <span>{{ form.recharge_type === 'balance' ? '余额充值' : '订阅充值' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>¥{{ form.price.toFixed(2) }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ displayValue }}</span>
</div> </div>
</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-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" /> <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>
<UiButton <UiButton
variant="outline" variant="outline"
class="w-full" class="w-full"
@click="router.back()" @click="router.back()"
> >
{{ t('developer.cardTypes.create.cancel') }} {{ t('admin.cardTypes.create.cancel') }}
</UiButton> </UiButton>
</div> </div>
</UiCardContent> </UiCardContent>
@@ -4,8 +4,8 @@ import type { CardTypeStatus } from './schema'
export function getCardTypeStatuses(t: Composer['t']): { label: string, value: CardTypeStatus }[] { export function getCardTypeStatuses(t: Composer['t']): { label: string, value: CardTypeStatus }[] {
return [ return [
{ label: t('developer.cardTypes.statusOptions.active'), value: 'active' }, { label: t('admin.cardTypes.statusOptions.active'), value: 'active' },
{ label: t('developer.cardTypes.statusOptions.inactive'), value: 'inactive' }, { 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 { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 ConfirmDialog from '@/components/confirm-dialog.vue'
import SingleFilter from '@/components/data-table/single-filter.vue' import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout' 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() const { t } = useI18n()
@@ -43,8 +43,8 @@ const applicationOptions = computed(() => {
}) })
const statusOptions = computed(() => [ const statusOptions = computed(() => [
{ label: t('developer.cardTypes.statusOptions.active'), value: 'active' }, { label: t('admin.cardTypes.statusOptions.active'), value: 'active' },
{ label: t('developer.cardTypes.statusOptions.inactive'), value: 'inactive' }, { label: t('admin.cardTypes.statusOptions.inactive'), value: 'inactive' },
]) ])
const filteredCardTypes = computed(() => { const filteredCardTypes = computed(() => {
@@ -114,11 +114,11 @@ async function fetchApplications() {
} }
function goToCreate() { function goToCreate() {
router.push('/developer/card-types/create') router.push('/admin/card-types/create')
} }
function goToEdit(type: CardType) { function goToEdit(type: CardType) {
router.push(`/developer/card-types/${type.id}`) router.push(`/admin/card-types/${type.id}`)
} }
async function toggleStatus(type: CardType) { async function toggleStatus(type: CardType) {
@@ -136,16 +136,16 @@ async function toggleStatus(type: CardType) {
const data = await response.json() const data = await response.json()
if (data.code === 200) { if (data.code === 200) {
toast.success(t('developer.cardTypes.statusUpdateSuccess')) toast.success(t('admin.cardTypes.statusUpdateSuccess'))
fetchCardTypes() fetchCardTypes()
} }
else { else {
toast.error(data.message || t('developer.cardTypes.statusUpdateFailed')) toast.error(data.message || t('admin.cardTypes.statusUpdateFailed'))
} }
} }
catch (error) { catch (error) {
console.error('切换卡密类型状态失败:', 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() const data = await response.json()
if (data.code === 200) { if (data.code === 200) {
toast.success(t('developer.cardTypes.deleteSuccess')) toast.success(t('admin.cardTypes.deleteSuccess'))
fetchCardTypes() fetchCardTypes()
} }
else { else {
toast.error(data.message || t('developer.cardTypes.deleteFailed')) toast.error(data.message || t('admin.cardTypes.deleteFailed'))
} }
} }
catch (error) { catch (error) {
console.error('删除卡密类型失败:', error) console.error('删除卡密类型失败:', error)
toast.error(t('developer.cardTypes.deleteFailed')) toast.error(t('admin.cardTypes.deleteFailed'))
} }
finally { finally {
deleteTarget.value = null deleteTarget.value = null
@@ -213,16 +213,16 @@ async function handleBatchDelete() {
} }
if (successCount > 0) { if (successCount > 0) {
toast.success(t('developer.cardTypes.deleteSuccess')) toast.success(t('admin.cardTypes.deleteSuccess'))
fetchCardTypes() fetchCardTypes()
} }
if (failCount > 0) { if (failCount > 0) {
toast.error(t('developer.cardTypes.deleteFailed')) toast.error(t('admin.cardTypes.deleteFailed'))
} }
} }
catch (error) { catch (error) {
console.error('批量删除失败:', error) console.error('批量删除失败:', error)
toast.error(t('developer.cardTypes.deleteFailed')) toast.error(t('admin.cardTypes.deleteFailed'))
} }
finally { finally {
batchDeleteIds.value = [] batchDeleteIds.value = []
@@ -254,16 +254,16 @@ async function batchToggleStatus(ids: (string | number)[], status: string) {
} }
if (successCount > 0) { if (successCount > 0) {
toast.success(t('developer.cardTypes.batchUpdateSuccess')) toast.success(t('admin.cardTypes.batchUpdateSuccess'))
fetchCardTypes() fetchCardTypes()
} }
if (failCount > 0) { if (failCount > 0) {
toast.error(t('developer.cardTypes.batchUpdateFailed')) toast.error(t('admin.cardTypes.batchUpdateFailed'))
} }
} }
catch (error) { catch (error) {
console.error('批量更新状态失败:', error) console.error('批量更新状态失败:', error)
toast.error(t('developer.cardTypes.batchUpdateFailed')) toast.error(t('admin.cardTypes.batchUpdateFailed'))
} }
} }
@@ -275,14 +275,14 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cardTypes.title')" :title="t('admin.cardTypes.title')"
:description="t('developer.cardTypes.description')" :description="t('admin.cardTypes.description')"
sticky sticky
> >
<template #actions> <template #actions>
<UiButton @click="goToCreate"> <UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cardTypes.createCardType') }} {{ t('admin.cardTypes.createCardType') }}
</UiButton> </UiButton>
</template> </template>
@@ -291,7 +291,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cardTypes.totalTypes') }} {{ t('admin.cardTypes.totalTypes') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:layers" class="size-4 text-muted-foreground" /> <Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -305,7 +305,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cardTypes.activeTypes') }} {{ t('admin.cardTypes.activeTypes') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -319,7 +319,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cardTypes.generatedCards') }} {{ t('admin.cardTypes.generatedCards') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:hash" class="size-4 text-muted-foreground" /> <Icon icon="lucide:hash" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -333,7 +333,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cardTypes.totalValue') }} {{ t('admin.cardTypes.totalValue') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" /> <Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -362,12 +362,12 @@ onMounted(() => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.cardTypes.columns.application')" :title="t('admin.cardTypes.columns.application')"
:options="applicationOptions" :options="applicationOptions"
/> />
<SingleFilter <SingleFilter
v-model="statusFilter" v-model="statusFilter"
:title="t('developer.cardTypes.columns.status')" :title="t('admin.cardTypes.columns.status')"
:options="statusOptions" :options="statusOptions"
/> />
</template> </template>
@@ -379,30 +379,30 @@ onMounted(() => {
<ConfirmDialog <ConfirmDialog
v-model:open="deleteDialogOpen" v-model:open="deleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.cardTypes.delete')" :confirm-button-text="t('admin.cardTypes.delete')"
:cancel-button-text="t('developer.cards.create.cancelBtn')" :cancel-button-text="t('admin.cards.create.cancelBtn')"
@confirm="handleDeleteType" @confirm="handleDeleteType"
> >
<template #title> <template #title>
{{ t('developer.cardTypes.delete') }} {{ t('admin.cardTypes.delete') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.cardTypes.deleteConfirm', { name: deleteTarget?.name }) }} {{ t('admin.cardTypes.deleteConfirm', { name: deleteTarget?.name }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
v-model:open="batchDeleteDialogOpen" v-model:open="batchDeleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.cardTypes.delete')" :confirm-button-text="t('admin.cardTypes.delete')"
:cancel-button-text="t('developer.cards.create.cancelBtn')" :cancel-button-text="t('admin.cards.create.cancelBtn')"
@confirm="handleBatchDelete" @confirm="handleBatchDelete"
> >
<template #title> <template #title>
{{ t('developer.cardTypes.batchDelete') }} {{ t('admin.cardTypes.batchDelete') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.cardTypes.batchDeleteConfirm', { count: batchDeleteIds.length }) }} {{ t('admin.cardTypes.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
</BasicPage> </BasicPage>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Ban, MoreHorizontal, Trash2 } from 'lucide-vue-next' import { Ban, MoreHorizontal, Trash2 } from 'lucide-vue-next'
import { h } from 'vue' 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 { Copy } from '@/components/sva-ui/copy'
import Badge from '@/components/ui/badge/Badge.vue' import Badge from '@/components/ui/badge/Badge.vue'
@@ -24,7 +24,7 @@ export function getColumns(actions: {
return [ return [
{ {
accessorKey: 'card_key', accessorKey: 'card_key',
header: () => t('developer.cards.columns.cardKey'), header: () => t('admin.cards.columns.cardKey'),
cell: ({ row }) => { cell: ({ row }) => {
const cardKey = row.getValue('card_key') as string const cardKey = row.getValue('card_key') as string
if (!cardKey) if (!cardKey)
@@ -37,7 +37,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'application.name', accessorKey: 'application.name',
header: () => t('developer.cards.columns.application'), header: () => t('admin.cards.columns.application'),
cell: ({ row }) => { cell: ({ row }) => {
const appName = row.original.application?.name const appName = row.original.application?.name
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-' return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -45,7 +45,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'card_type.name', accessorKey: 'card_type.name',
header: () => t('developer.cards.columns.type'), header: () => t('admin.cards.columns.type'),
cell: ({ row }) => { cell: ({ row }) => {
const typeName = row.original.card_type?.name const typeName = row.original.card_type?.name
return typeName ? h(Badge, { variant: 'outline' }, () => typeName) : '-' return typeName ? h(Badge, { variant: 'outline' }, () => typeName) : '-'
@@ -53,7 +53,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'card_type.price', accessorKey: 'card_type.price',
header: () => t('developer.cards.columns.faceValue'), header: () => t('admin.cards.columns.faceValue'),
cell: ({ row }) => { cell: ({ row }) => {
const price = row.original.card_type?.price const price = row.original.card_type?.price
return price != null ? `¥${price}` : '-' return price != null ? `¥${price}` : '-'
@@ -61,35 +61,35 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'card_type.value', accessorKey: 'card_type.value',
header: () => t('developer.cards.columns.duration'), header: () => t('admin.cards.columns.duration'),
cell: ({ row }) => { cell: ({ row }) => {
const value = row.original.card_type?.value const value = row.original.card_type?.value
const billingType = row.original.card_type?.billing_type const billingType = row.original.card_type?.billing_type
if (value == null) if (value == null)
return '-' return '-'
if (value === -1) if (value === -1)
return t('developer.cards.columns.permanent') return t('admin.cards.columns.permanent')
if (billingType === 'subscription') if (billingType === 'subscription')
return `${value}${t('developer.cards.columns.hours')}` return `${value}${t('admin.cards.columns.hours')}`
if (billingType === 'timer') if (billingType === 'timer')
return `${value}${t('developer.cards.columns.minutes')}` return `${value}${t('admin.cards.columns.minutes')}`
if (billingType === 'points') if (billingType === 'points')
return `${value}${t('developer.cards.columns.points')}` return `${value}${t('admin.cards.columns.points')}`
return String(value) return String(value)
}, },
}, },
{ {
accessorKey: 'status', accessorKey: 'status',
header: () => t('developer.cards.columns.status'), header: () => t('admin.cards.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const status = row.getValue('status') as string const status = row.getValue('status') as string
if (!status || status === '') { 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 }> = { const statusMap: Record<string, { text: string, variant: any }> = {
unused: { text: t('developer.cards.unused'), variant: 'default' }, unused: { text: t('admin.cards.unused'), variant: 'default' },
used: { text: t('developer.cards.used'), variant: 'secondary' }, used: { text: t('admin.cards.used'), variant: 'secondary' },
banned: { text: t('developer.cards.banned'), variant: 'destructive' }, banned: { text: t('admin.cards.banned'), variant: 'destructive' },
} }
const { text, variant } = statusMap[status] || { text: status, variant: 'default' } const { text, variant } = statusMap[status] || { text: status, variant: 'default' }
return h(Badge, { variant }, () => text) return h(Badge, { variant }, () => text)
@@ -97,7 +97,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'app_user.username', accessorKey: 'app_user.username',
header: () => t('developer.cards.columns.usedUser'), header: () => t('admin.cards.columns.usedUser'),
cell: ({ row }) => { cell: ({ row }) => {
const username = row.original.app_user?.username const username = row.original.app_user?.username
return username || '-' return username || '-'
@@ -105,7 +105,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'used_at', accessorKey: 'used_at',
header: () => t('developer.cards.columns.usedTime'), header: () => t('admin.cards.columns.usedTime'),
cell: ({ row }) => { cell: ({ row }) => {
const usedAt = row.getValue('used_at') const usedAt = row.getValue('used_at')
if (!usedAt) if (!usedAt)
@@ -129,7 +129,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'creator.username', accessorKey: 'creator.username',
header: () => t('developer.cards.columns.creator'), header: () => t('admin.cards.columns.creator'),
cell: ({ row }) => { cell: ({ row }) => {
const username = row.original.creator?.username const username = row.original.creator?.username
return username || '-' return username || '-'
@@ -137,7 +137,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'created_at', accessorKey: 'created_at',
header: () => t('developer.cards.columns.createdAt'), header: () => t('admin.cards.columns.createdAt'),
cell: ({ row }) => { cell: ({ row }) => {
const createdAt = row.getValue('created_at') const createdAt = row.getValue('created_at')
if (!createdAt) if (!createdAt)
@@ -182,12 +182,12 @@ export function getColumns(actions: {
() => [ () => [
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(card) }, () => [ h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(card) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }), 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(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(card) }, () => [ h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(card) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }), 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 { computed } from 'vue'
import { useI18n } from 'vue-i18n' 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 { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -33,7 +33,7 @@ function resetFilters() {
<template> <template>
<div class="flex items-center flex-1 space-x-2"> <div class="flex items-center flex-1 space-x-2">
<Input <Input
:placeholder="t('developer.cards.searchAll')" :placeholder="t('admin.cards.searchAll')"
:model-value="searchFilter" :model-value="searchFilter"
class="h-8 w-[200px] lg:w-[300px]" class="h-8 w-[200px] lg:w-[300px]"
@update:model-value="emit('update:searchFilter', $event as string)" @update:model-value="emit('update:searchFilter', $event as string)"
@@ -45,7 +45,7 @@ function resetFilters() {
class="h-8 px-2 lg:px-3" class="h-8 px-2 lg:px-3"
@click="resetFilters" @click="resetFilters"
> >
{{ t('developer.cards.reset') }} {{ t('admin.cards.reset') }}
<X class="size-4 ml-2" /> <X class="size-4 ml-2" />
</Button> </Button>
</div> </div>
@@ -48,17 +48,17 @@ const table = generateVueTable<Card>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.cards.select', select: 'admin.cards.select',
card_key: 'developer.cards.columns.cardKey', card_key: 'admin.cards.columns.cardKey',
application_name: 'developer.cards.columns.application', application_name: 'admin.cards.columns.application',
card_type_name: 'developer.cards.columns.type', card_type_name: 'admin.cards.columns.type',
card_type_price: 'developer.cards.columns.faceValue', card_type_price: 'admin.cards.columns.faceValue',
card_type_value: 'developer.cards.columns.duration', card_type_value: 'admin.cards.columns.duration',
status: 'developer.cards.columns.status', status: 'admin.cards.columns.status',
app_user_username: 'developer.cards.columns.usedUser', app_user_username: 'admin.cards.columns.usedUser',
used_at: 'developer.cards.columns.usedTime', used_at: 'admin.cards.columns.usedTime',
creator_username: 'developer.cards.columns.creator', creator_username: 'admin.cards.columns.creator',
created_at: 'developer.cards.columns.createdAt', created_at: 'admin.cards.columns.createdAt',
} }
defineExpose({ defineExpose({
@@ -75,16 +75,16 @@ defineExpose({
</div> </div>
<BulkActions :table="table" entity-name="cards"> <BulkActions :table="table" entity-name="cards">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')"> <UiButton variant="outline" size="sm" @click="emit('batchEnable')">
{{ t('developer.cards.batchEnableBtn') }} {{ t('admin.cards.batchEnableBtn') }}
</UiButton> </UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')"> <UiButton variant="outline" size="sm" @click="emit('batchDisable')">
{{ t('developer.cards.batchDisableBtn') }} {{ t('admin.cards.batchDisableBtn') }}
</UiButton> </UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchExport')"> <UiButton variant="outline" size="sm" @click="emit('batchExport')">
{{ t('developer.cards.batchExportBtn') }} {{ t('admin.cards.batchExportBtn') }}
</UiButton> </UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')"> <UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.cards.batchDeleteBtn') }} {{ t('admin.cards.batchDeleteBtn') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
@@ -98,7 +98,7 @@ defineExpose({
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" @click="emit('export')"> <UiButton variant="outline" size="sm" @click="emit('export')">
{{ t('developer.cards.exportBtn') }} {{ t('admin.cards.exportBtn') }}
</UiButton> </UiButton>
<DataTableViewOptions :table="table" :column-labels="columnLabels" /> <DataTableViewOptions :table="table" :column-labels="columnLabels" />
</div> </div>
+28 -28
View File
@@ -84,15 +84,15 @@ function onApplicationChange() {
async function handleGenerate() { async function handleGenerate() {
if (!form.value.application_id) { if (!form.value.application_id) {
toast.error(t('developer.cards.create.selectAppFirst')) toast.error(t('admin.cards.create.selectAppFirst'))
return return
} }
if (!form.value.card_type_id) { if (!form.value.card_type_id) {
toast.error(t('developer.cards.create.selectCardTypeFirst')) toast.error(t('admin.cards.create.selectCardTypeFirst'))
return return
} }
if (form.value.count < 1 || form.value.count > 1000) { if (form.value.count < 1 || form.value.count > 1000) {
toast.error(t('developer.cards.create.countRange')) toast.error(t('admin.cards.create.countRange'))
return return
} }
@@ -103,12 +103,12 @@ async function handleGenerate() {
card_type_id: Number(form.value.card_type_id), card_type_id: Number(form.value.card_type_id),
count: form.value.count, count: form.value.count,
}) })
toast.success(t('developer.cards.create.generateSuccess', { count: form.value.count })) toast.success(t('admin.cards.create.generateSuccess', { count: form.value.count }))
router.push('/developer/cards') router.push('/admin/cards')
} }
catch (error: any) { catch (error: any) {
console.error('生成卡密失败:', error) console.error('生成卡密失败:', error)
toast.error(error.message || t('developer.cards.create.generateFailed')) toast.error(error.message || t('admin.cards.create.generateFailed'))
} }
finally { finally {
saving.value = false saving.value = false
@@ -123,11 +123,11 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cards.create.title')" :title="t('admin.cards.create.title')"
:description="t('developer.cards.create.description')" :description="t('admin.cards.create.description')"
:breadcrumbs="[ :breadcrumbs="[
{ title: t('developer.cards.title'), href: '/developer/cards' }, { title: t('admin.cards.title'), href: '/admin/cards' },
{ title: t('developer.cards.create.title') }, { title: t('admin.cards.create.title') },
]" ]"
sticky sticky
> >
@@ -138,18 +138,18 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" /> <Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cards.create.config') }} {{ t('admin.cards.create.config') }}
</UiCardTitle> </UiCardTitle>
<UiCardDescription>{{ t('developer.cards.create.configDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.cards.create.configDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="application"> <UiLabel for="application">
{{ t('developer.cards.create.application') }} {{ t('admin.cards.create.application') }}
</UiLabel> </UiLabel>
<UiSelect v-model="form.application_id" @update:model-value="onApplicationChange"> <UiSelect v-model="form.application_id" @update:model-value="onApplicationChange">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cards.create.selectApplication')" /> <UiSelectValue :placeholder="t('admin.cards.create.selectApplication')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)"> <UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
@@ -161,11 +161,11 @@ onMounted(() => {
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="cardType"> <UiLabel for="cardType">
{{ t('developer.cards.create.cardType') }} {{ t('admin.cards.create.cardType') }}
</UiLabel> </UiLabel>
<UiSelect v-model="form.card_type_id" :disabled="!form.application_id"> <UiSelect v-model="form.card_type_id" :disabled="!form.application_id">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cards.create.selectCardType')" /> <UiSelectValue :placeholder="t('admin.cards.create.selectCardType')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem v-for="type in filteredCardTypes" :key="type.id" :value="String(type.id)"> <UiSelectItem v-for="type in filteredCardTypes" :key="type.id" :value="String(type.id)">
@@ -174,13 +174,13 @@ onMounted(() => {
</UiSelectContent> </UiSelectContent>
</UiSelect> </UiSelect>
<p v-if="form.application_id && filteredCardTypes.length === 0" class="text-xs text-muted-foreground"> <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> </p>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="count"> <UiLabel for="count">
{{ t('developer.cards.create.count') }} {{ t('admin.cards.create.count') }}
</UiLabel> </UiLabel>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<UiNumberField v-model="form.count" :min="1" :max="1000" class="flex-1 max-w-[200px]"> <UiNumberField v-model="form.count" :min="1" :max="1000" class="flex-1 max-w-[200px]">
@@ -206,7 +206,7 @@ onMounted(() => {
</div> </div>
</div> </div>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
{{ t('developer.cards.create.maxCount') }} {{ t('admin.cards.create.maxCount') }}
</p> </p>
</div> </div>
</UiCardContent> </UiCardContent>
@@ -218,30 +218,30 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:receipt" class="size-5" /> <Icon icon="lucide:receipt" class="size-5" />
{{ t('developer.cards.create.preview') }} {{ t('admin.cards.create.preview') }}
</UiCardTitle> </UiCardTitle>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between text-sm"> <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> <span>{{ applications.find(a => String(a.id) === form.application_id)?.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ selectedCardType?.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>¥{{ selectedCardType?.price?.toFixed(2) || '0.00' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cards.create.quantity') }}</span> <span class="text-muted-foreground">{{ t('admin.cards.create.quantity') }}</span>
<span>{{ form.count }} {{ t('developer.cards.create.units') }}</span> <span>{{ form.count }} {{ t('admin.cards.create.units') }}</span>
</div> </div>
<div class="border-t pt-3 mt-3"> <div class="border-t pt-3 mt-3">
<div class="flex justify-between items-center"> <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> <span class="text-xl font-bold text-primary">¥{{ totalCost.toFixed(2) }}</span>
</div> </div>
</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-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" /> <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>
<UiButton <UiButton
variant="outline" variant="outline"
class="w-full" class="w-full"
@click="router.back()" @click="router.back()"
> >
{{ t('developer.cards.create.cancel') }} {{ t('admin.cards.create.cancel') }}
</UiButton> </UiButton>
</div> </div>
</UiCardContent> </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 })[] { export function getCardStatuses(t: Composer['t']): (FacetedFilterOption & { style: string, variant?: string })[] {
return [ return [
{ {
label: t('developer.cards.unused'), label: t('admin.cards.unused'),
value: 'unused', value: 'unused',
icon: h(CheckCircle2), icon: h(CheckCircle2),
style: 'bg-green-500/10 text-green-500', style: 'bg-green-500/10 text-green-500',
variant: 'default', variant: 'default',
}, },
{ {
label: t('developer.cards.used'), label: t('admin.cards.used'),
value: 'used', value: 'used',
icon: h(Users), icon: h(Users),
style: 'bg-blue-500/10 text-blue-500', style: 'bg-blue-500/10 text-blue-500',
variant: 'secondary', variant: 'secondary',
}, },
{ {
label: t('developer.cards.banned'), label: t('admin.cards.banned'),
value: 'banned', value: 'banned',
icon: h(Ban), icon: h(Ban),
style: 'bg-gray-500/10 text-gray-500', 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 { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 ConfirmDialog from '@/components/confirm-dialog.vue'
import DateRangeFilter from '@/components/data-table/date-range-filter.vue' import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
import SingleFilter from '@/components/data-table/single-filter.vue' import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout' 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' import api, { BASE_URL } from '@/services/api'
const { t } = useI18n() const { t } = useI18n()
@@ -121,9 +121,9 @@ const cardTypeOptions = computed(() => {
}) })
const statusOptions = computed(() => [ const statusOptions = computed(() => [
{ label: t('developer.cards.unused'), value: 'unused' }, { label: t('admin.cards.unused'), value: 'unused' },
{ label: t('developer.cards.used'), value: 'used' }, { label: t('admin.cards.used'), value: 'used' },
{ label: t('developer.cards.banned'), value: 'banned' }, { label: t('admin.cards.banned'), value: 'banned' },
]) ])
async function fetchApplications() { async function fetchApplications() {
@@ -162,19 +162,19 @@ async function fetchCards() {
} }
function goToCreate() { function goToCreate() {
router.push('/developer/cards/create') router.push('/admin/cards/create')
} }
async function toggleStatus(card: Card) { async function toggleStatus(card: Card) {
try { try {
const newStatus = card.status === 'banned' ? 'unused' : 'banned' const newStatus = card.status === 'banned' ? 'unused' : 'banned'
await api.put(`/dev/cards/${card.id}/status`, { status: newStatus }) await api.put(`/dev/cards/${card.id}/status`, { status: newStatus })
toast.success(t('developer.cards.statusUpdateSuccess')) toast.success(t('admin.cards.statusUpdateSuccess'))
fetchCards() fetchCards()
} }
catch (error: any) { catch (error: any) {
console.error('切换卡密状态失败:', error) 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 { try {
await api.delete(`/dev/cards/${deleteTarget.value.id}`) await api.delete(`/dev/cards/${deleteTarget.value.id}`)
toast.success(t('developer.cards.deleteSuccess')) toast.success(t('admin.cards.deleteSuccess'))
fetchCards() fetchCards()
} }
catch (error: any) { catch (error: any) {
console.error('删除卡密失败:', error) console.error('删除卡密失败:', error)
toast.error(error.message || t('developer.cards.deleteFailed')) toast.error(error.message || t('admin.cards.deleteFailed'))
} }
finally { finally {
deleteTarget.value = null deleteTarget.value = null
@@ -209,12 +209,12 @@ function confirmBatchDelete(ids: (string | number)[]) {
async function handleBatchDelete() { async function handleBatchDelete() {
try { try {
await api.delete('/dev/cards/batch', { ids: batchDeleteIds.value } as any) await api.delete('/dev/cards/batch', { ids: batchDeleteIds.value } as any)
toast.success(t('developer.cards.deleteSuccess')) toast.success(t('admin.cards.deleteSuccess'))
fetchCards() fetchCards()
} }
catch (error: any) { catch (error: any) {
console.error('批量删除失败:', error) console.error('批量删除失败:', error)
toast.error(error.message || t('developer.cards.deleteFailed')) toast.error(error.message || t('admin.cards.deleteFailed'))
} }
finally { finally {
batchDeleteIds.value = [] batchDeleteIds.value = []
@@ -224,12 +224,12 @@ async function handleBatchDelete() {
async function batchToggleStatus(ids: (string | number)[], status: string) { async function batchToggleStatus(ids: (string | number)[], status: string) {
try { try {
await api.put('/dev/cards/batch/status', { ids, status }) await api.put('/dev/cards/batch/status', { ids, status })
toast.success(t('developer.cards.batchUpdateSuccess')) toast.success(t('admin.cards.batchUpdateSuccess'))
fetchCards() fetchCards()
} }
catch (error: any) { catch (error: any) {
console.error('批量更新状态失败:', error) 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> <template>
<BasicPage <BasicPage
:title="t('developer.cards.title')" :title="t('admin.cards.title')"
:description="t('developer.cards.description')" :description="t('admin.cards.description')"
sticky sticky
> >
<template #actions> <template #actions>
<UiButton @click="goToCreate"> <UiButton @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cards.generateCard') }} {{ t('admin.cards.generateCard') }}
</UiButton> </UiButton>
</template> </template>
@@ -293,7 +293,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cards.totalCards') }} {{ t('admin.cards.totalCards') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:key" class="size-4 text-muted-foreground" /> <Icon icon="lucide:key" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -307,7 +307,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cards.unused') }} {{ t('admin.cards.unused') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -321,7 +321,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cards.used') }} {{ t('admin.cards.used') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" /> <Icon icon="lucide:users" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -335,7 +335,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cards.banned') }} {{ t('admin.cards.banned') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" /> <Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -367,17 +367,17 @@ onMounted(() => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.cards.application')" :title="t('admin.cards.application')"
:options="applicationOptions" :options="applicationOptions"
/> />
<SingleFilter <SingleFilter
v-model="cardTypeFilter" v-model="cardTypeFilter"
:title="t('developer.cards.cardType')" :title="t('admin.cards.cardType')"
:options="cardTypeOptions" :options="cardTypeOptions"
/> />
<SingleFilter <SingleFilter
v-model="statusFilter" v-model="statusFilter"
:title="t('developer.cards.status')" :title="t('admin.cards.status')"
:options="statusOptions" :options="statusOptions"
/> />
<DateRangeFilter <DateRangeFilter
@@ -393,30 +393,30 @@ onMounted(() => {
<ConfirmDialog <ConfirmDialog
v-model:open="deleteDialogOpen" v-model:open="deleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.cards.delete')" :confirm-button-text="t('admin.cards.delete')"
:cancel-button-text="t('developer.cards.create.cancelBtn')" :cancel-button-text="t('admin.cards.create.cancelBtn')"
@confirm="handleDeleteCard" @confirm="handleDeleteCard"
> >
<template #title> <template #title>
{{ t('developer.cards.deleteCard') }} {{ t('admin.cards.deleteCard') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.cards.deleteCardConfirm', { cardKey: deleteTarget?.card_key }) }} {{ t('admin.cards.deleteCardConfirm', { cardKey: deleteTarget?.card_key }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
v-model:open="batchDeleteDialogOpen" v-model:open="batchDeleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.cards.delete')" :confirm-button-text="t('admin.cards.delete')"
:cancel-button-text="t('developer.cards.create.cancelBtn')" :cancel-button-text="t('admin.cards.create.cancelBtn')"
@confirm="handleBatchDelete" @confirm="handleBatchDelete"
> >
<template #title> <template #title>
{{ t('developer.cards.batchDeleteCards') }} {{ t('admin.cards.batchDeleteCards') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.cards.batchDeleteConfirm', { count: batchDeleteIds.length }) }} {{ t('admin.cards.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
</BasicPage> </BasicPage>
+268 -204
View File
@@ -2,272 +2,336 @@
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 { BasicPage } from '@/components/global-layout'
import api from '@/services/api' import api from '@/services/api'
import type { CloudConstant } from './data/schema'
import DataTable from './components/data-table.vue'
const { t } = useI18n() const { t } = useI18n()
const route = useRoute()
const router = useRouter() const router = useRouter()
interface Application { interface Application {
id: number id: number
name: string name: string
app_key: string
} }
const loading = ref(true) const loading = ref(true)
const saving = ref(false) const constants = ref<CloudConstant[]>([])
const applications = ref<Application[]>([]) const applications = ref<Application[]>([])
const form = ref({ const appFilter = ref<string>('')
id: '', const startDate = ref<string>('')
app_id: '', const endDate = ref<string>('')
key: '',
value: '', const deleteDialogOpen = ref(false)
var_type: 'string', const deleteTarget = ref<CloudConstant | null>(null)
description: '', const batchDialogOpen = ref(false)
status: 'active', 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() { async function fetchApplications() {
try { try {
const data = await api.get<{ applications: Application[] }>('/dev/applications') const data = await api.get<{ applications: Application[] }>('/dev/applications')
applications.value = Array.isArray(data?.applications) ? data.applications : [] applications.value = data?.applications || []
} }
catch (error) { catch (error) {
console.error('获取应用列表失败:', error) console.error('获取应用列表失败:', error)
} }
} }
async function fetchConstant() { async function fetchConstants() {
loading.value = true loading.value = true
try { try {
const constId = route.params.id const data = await api.get<{ constants: CloudConstant[] }>('/dev/cloud-constants')
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 consts = data?.constants || []
const constant = data constants.value = consts.map((c: any) => ({
if (constant) { ...c,
form.value = { application_name: applications.value.find(app => app.id === c.app_id)?.name || '',
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')
}
} }
catch (error) { catch (error) {
console.error('获取云端常量失败:', error) console.error('获取云端常量失败:', error)
toast.error(t('developer.cloudConstants.create.failed')) constants.value = []
router.push('/developer/cloud-constants')
} }
finally { finally {
loading.value = false loading.value = false
} }
} }
const selectedApplication = computed(() => { function openCreatePage() {
if (form.value.app_id) { router.push('/admin/cloud-constants/create')
return applications.value.find(app => String(app.id) === form.value.app_id) }
}
return null
})
async function handleSave() { function openEditPage(constant: CloudConstant) {
if (!form.value.key) { router.push(`/admin/cloud-constants/${constant.id}`)
toast.error(t('developer.cloudConstants.create.keyRequired')) }
return
} function confirmDelete(constant: CloudConstant) {
if (!form.value.value) { deleteTarget.value = constant
toast.error(t('developer.cloudConstants.create.valueRequired')) deleteDialogOpen.value = true
return }
}
async function handleDelete() {
if (!deleteTarget.value)
return
saving.value = true
try { try {
await api.put(`/dev/cloud-constants/${form.value.id}`, { await api.delete(`/dev/cloud-constants/${deleteTarget.value.id}`)
key: form.value.key, toast.success(t('admin.cloudConstants.deleteSuccess'))
value: form.value.value, fetchConstants()
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')
} }
catch (error: any) { catch (error: any) {
console.error('更新云端常量失败:', error) console.error('删除云端常量失败:', error)
toast.error(error.message || t('developer.cloudConstants.create.failed')) toast.error(error.message || t('admin.cloudConstants.deleteFailed'))
} }
finally { 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 () => { onMounted(async () => {
await fetchApplications() await fetchApplications()
fetchConstant() fetchConstants()
}) })
</script> </script>
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cloudConstants.edit')" :title="t('admin.cloudConstants.title')"
:description="t('developer.cloudConstants.editDescription')" :description="t('admin.cloudConstants.description')"
:breadcrumbs="[
{ title: t('developer.cloudConstants.title'), href: '/developer/cloud-constants' },
{ title: t('developer.cloudConstants.edit') },
]"
sticky sticky
> >
<div v-if="loading" class="flex items-center justify-center py-12"> <template #actions>
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" /> <UiButton size="sm" @click="openCreatePage">
</div> <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="space-y-6">
<div class="grid gap-6 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="lg:col-span-2 space-y-6"> <UiCard>
<UiCard> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardHeader> <UiCardTitle class="text-sm font-medium">
<UiCardTitle class="flex items-center gap-2"> {{ t('admin.cloudConstants.totalConstants') }}
<Icon icon="lucide:settings-2" class="size-5" /> </UiCardTitle>
{{ t('developer.cloudConstants.create.config') }} <Icon icon="lucide:database" class="size-4 text-muted-foreground" />
</UiCardTitle> </UiCardHeader>
<UiCardDescription>{{ t('developer.cloudConstants.create.configDesc') }}</UiCardDescription> <UiCardContent>
</UiCardHeader> <div class="text-2xl font-bold">
<UiCardContent class="space-y-6"> {{ filteredConstants.length }}
<div class="space-y-2"> </div>
<UiLabel>{{ t('developer.cloudConstants.create.application') }}</UiLabel> </UiCardContent>
<div class="flex items-center gap-2 p-2.5 rounded-md border bg-muted/50"> </UiCard>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
</div>
<div class="space-y-2"> <UiCard>
<UiLabel for="varType"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.cloudConstants.create.type') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cloudConstants.linkedApps') }}
<UiSelect id="varType" v-model="form.var_type"> </UiCardTitle>
<UiSelectTrigger> <Icon icon="lucide:link" class="size-4 text-muted-foreground" />
<UiSelectValue :placeholder="t('developer.cloudConstants.create.selectType')" /> </UiCardHeader>
</UiSelectTrigger> <UiCardContent>
<UiSelectContent> <div class="text-2xl font-bold">
<UiSelectItem value="integer"> {{ linkedCount }}
{{ t('developer.cloudConstants.types.integer') }} </div>
</UiSelectItem> </UiCardContent>
<UiSelectItem value="decimal"> </UiCard>
{{ t('developer.cloudConstants.types.decimal') }}
</UiSelectItem>
<UiSelectItem value="string">
{{ t('developer.cloudConstants.types.string') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<div class="space-y-2"> <UiCard>
<UiLabel for="key"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.cloudConstants.create.key') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cloudConstants.unlinked') }}
<UiInput id="key" v-model="form.key" :placeholder="t('developer.cloudConstants.create.keyPlaceholder')" /> </UiCardTitle>
<p class="text-xs text-muted-foreground"> <Icon icon="lucide:unlink" class="size-4 text-muted-foreground" />
{{ t('developer.cloudConstants.create.keyHint') }} </UiCardHeader>
</p> <UiCardContent>
</div> <div class="text-2xl font-bold">
{{ unlinkedCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2"> <UiCard>
<UiLabel for="value"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.cloudConstants.create.value') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cloudConstants.appCount') }}
<UiTextarea id="value" v-model="form.value" :placeholder="t('developer.cloudConstants.create.valuePlaceholder')" rows="3" /> </UiCardTitle>
</div> <Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader>
<div class="space-y-2"> <UiCardContent>
<UiLabel for="description"> <div class="text-2xl font-bold">
{{ t('developer.cloudConstants.create.description') }} {{ applications.length }}
</UiLabel> </div>
<UiInput id="description" v-model="form.description" :placeholder="t('developer.cloudConstants.create.descriptionPlaceholder')" /> </UiCardContent>
</div> </UiCard>
</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>
</div> </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> </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> </BasicPage>
</template> </template>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Download, File, MoreHorizontal, Pencil, Power, PowerOff, Trash2 } from 'lucide-vue-next' import { Download, File, MoreHorizontal, Pencil, Power, PowerOff, Trash2 } from 'lucide-vue-next'
import { h } from 'vue' 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 { Copy } from '@/components/sva-ui/copy'
import Badge from '@/components/ui/badge/Badge.vue' import Badge from '@/components/ui/badge/Badge.vue'
@@ -25,7 +25,7 @@ export function getColumns(actions: {
return [ return [
{ {
accessorKey: 'key', accessorKey: 'key',
header: () => t('developer.cloudConstants.columns.key'), header: () => t('admin.cloudConstants.columns.key'),
cell: ({ row }) => { cell: ({ row }) => {
const key = row.getValue('key') as string const key = row.getValue('key') as string
if (!key) if (!key)
@@ -38,7 +38,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'value', accessorKey: 'value',
header: () => t('developer.cloudConstants.columns.value'), header: () => t('admin.cloudConstants.columns.value'),
cell: ({ row }) => { cell: ({ row }) => {
const value = row.getValue('value') as string const value = row.getValue('value') as string
const varType = row.original.var_type const varType = row.original.var_type
@@ -56,21 +56,21 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'var_type', accessorKey: 'var_type',
header: () => t('developer.cloudConstants.columns.type'), header: () => t('admin.cloudConstants.columns.type'),
cell: ({ row }) => { cell: ({ row }) => {
const type = row.getValue('var_type') as string const type = row.getValue('var_type') as string
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
integer: t('developer.cloudConstants.types.integer'), integer: t('admin.cloudConstants.types.integer'),
decimal: t('developer.cloudConstants.types.decimal'), decimal: t('admin.cloudConstants.types.decimal'),
string: t('developer.cloudConstants.types.string'), string: t('admin.cloudConstants.types.string'),
binary: t('developer.cloudConstants.types.binary'), binary: t('admin.cloudConstants.types.binary'),
} }
return h(Badge, { variant: 'secondary' }, () => typeMap[type || 'string'] || type || 'string') return h(Badge, { variant: 'secondary' }, () => typeMap[type || 'string'] || type || 'string')
}, },
}, },
{ {
accessorKey: 'description', accessorKey: 'description',
header: () => t('developer.cloudConstants.columns.description'), header: () => t('admin.cloudConstants.columns.description'),
cell: ({ row }) => { cell: ({ row }) => {
const description = row.getValue('description') as string const description = row.getValue('description') as string
if (!description) if (!description)
@@ -80,12 +80,12 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'application_name', accessorKey: 'application_name',
header: () => t('developer.cloudConstants.columns.application'), header: () => t('admin.cloudConstants.columns.application'),
cell: ({ row }) => { cell: ({ row }) => {
const appName = row.getValue('application_name') as string const appName = row.getValue('application_name') as string
const appId = row.original.app_id const appId = row.original.app_id
if (!appId) if (!appId)
return h(Badge, { variant: 'outline' }, () => t('developer.cloudConstants.notLinked')) return h(Badge, { variant: 'outline' }, () => t('admin.cloudConstants.notLinked'))
if (!appName) if (!appName)
return '-' return '-'
return h(Badge, { variant: 'outline' }, () => appName) return h(Badge, { variant: 'outline' }, () => appName)
@@ -93,16 +93,16 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'status', accessorKey: 'status',
header: () => t('developer.cloudConstants.columns.status'), header: () => t('admin.cloudConstants.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const status = row.getValue('status') as string const status = row.getValue('status') as string
const isActive = status === 'active' 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', accessorKey: 'created_at',
header: () => t('developer.cloudConstants.columns.createdAt'), header: () => t('admin.cloudConstants.columns.createdAt'),
cell: ({ row }) => { cell: ({ row }) => {
const createdAt = row.getValue('created_at') const createdAt = row.getValue('created_at')
if (!createdAt) if (!createdAt)
@@ -147,7 +147,7 @@ export function getColumns(actions: {
const items = [ const items = [
h(DropdownMenuItem, { onClick: () => actions.onEdit(constant) }, () => [ h(DropdownMenuItem, { onClick: () => actions.onEdit(constant) }, () => [
h(Pencil, { class: 'mr-2 h-4 w-4' }), 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( items.push(
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(constant) }, () => [ 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' ? 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(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(constant) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }), 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 { computed } from 'vue'
import { useI18n } from 'vue-i18n' 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 { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template> <template>
<div class="flex items-center flex-1 space-x-2"> <div class="flex items-center flex-1 space-x-2">
<Input <Input
:placeholder="t('developer.cloudConstants.searchPlaceholder')" :placeholder="t('admin.cloudConstants.searchPlaceholder')"
:model-value="(table.getColumn('key')?.getFilterValue() as string) ?? ''" :model-value="(table.getColumn('key')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]" class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('key')?.setFilterValue($event.target.value)" @input="table.getColumn('key')?.setFilterValue($event.target.value)"
@@ -46,14 +46,14 @@ const table = generateVueTable<CloudConstant>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.cloudConstants.select', select: 'admin.cloudConstants.select',
key: 'developer.cloudConstants.columns.key', key: 'admin.cloudConstants.columns.key',
value: 'developer.cloudConstants.columns.value', value: 'admin.cloudConstants.columns.value',
var_type: 'developer.cloudConstants.columns.type', var_type: 'admin.cloudConstants.columns.type',
description: 'developer.cloudConstants.columns.description', description: 'admin.cloudConstants.columns.description',
application_name: 'developer.cloudConstants.columns.application', application_name: 'admin.cloudConstants.columns.application',
status: 'developer.cloudConstants.columns.status', status: 'admin.cloudConstants.columns.status',
created_at: 'developer.cloudConstants.columns.createdAt', created_at: 'admin.cloudConstants.columns.createdAt',
} }
defineExpose({ defineExpose({
@@ -76,13 +76,13 @@ function handleBatchAction(action: 'enable' | 'disable' | 'delete') {
</div> </div>
<BulkActions :table="table" entity-name="constants"> <BulkActions :table="table" entity-name="constants">
<UiButton variant="outline" size="sm" @click="handleBatchAction('enable')"> <UiButton variant="outline" size="sm" @click="handleBatchAction('enable')">
{{ t('developer.cloudConstants.batchEnableBtn') }} {{ t('admin.cloudConstants.batchEnableBtn') }}
</UiButton> </UiButton>
<UiButton variant="outline" size="sm" @click="handleBatchAction('disable')"> <UiButton variant="outline" size="sm" @click="handleBatchAction('disable')">
{{ t('developer.cloudConstants.batchDisableBtn') }} {{ t('admin.cloudConstants.batchDisableBtn') }}
</UiButton> </UiButton>
<UiButton variant="destructive" size="sm" @click="handleBatchAction('delete')"> <UiButton variant="destructive" size="sm" @click="handleBatchAction('delete')">
{{ t('developer.cloudConstants.batchDeleteBtn') }} {{ t('admin.cloudConstants.batchDeleteBtn') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
@@ -67,11 +67,11 @@ function clearFile() {
async function handleSave() { async function handleSave() {
if (!form.value.key) { if (!form.value.key) {
toast.error(t('developer.cloudConstants.create.keyRequired')) toast.error(t('admin.cloudConstants.create.keyRequired'))
return return
} }
if (!form.value.app_id) { if (!form.value.app_id) {
toast.error(t('developer.cloudConstants.create.appRequired')) toast.error(t('admin.cloudConstants.create.appRequired'))
return return
} }
@@ -85,7 +85,7 @@ async function handleSave() {
} }
if (!form.value.value) { if (!form.value.value) {
toast.error(t('developer.cloudConstants.create.valueRequired')) toast.error(t('admin.cloudConstants.create.valueRequired'))
return return
} }
@@ -99,12 +99,12 @@ async function handleSave() {
description: form.value.description, description: form.value.description,
status: form.value.status, status: form.value.status,
}) })
toast.success(t('developer.cloudConstants.create.success')) toast.success(t('admin.cloudConstants.create.success'))
router.push('/developer/cloud-constants') router.push('/admin/cloud-constants')
} }
catch (error: any) { catch (error: any) {
console.error('创建云端常量失败:', error) console.error('创建云端常量失败:', error)
toast.error(error.message || t('developer.cloudConstants.create.failed')) toast.error(error.message || t('admin.cloudConstants.create.failed'))
} }
finally { finally {
saving.value = false saving.value = false
@@ -125,8 +125,8 @@ async function uploadFile() {
formData.append('status', form.value.status) formData.append('status', form.value.status)
await api.postFormData('/dev/cloud-constants/upload', formData) await api.postFormData('/dev/cloud-constants/upload', formData)
toast.success(t('developer.cloudConstants.create.success')) toast.success(t('admin.cloudConstants.create.success'))
router.push('/developer/cloud-constants') router.push('/admin/cloud-constants')
} }
catch (error: any) { catch (error: any) {
console.error('上传文件失败:', error) console.error('上传文件失败:', error)
@@ -153,11 +153,11 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cloudConstants.addConstant')" :title="t('admin.cloudConstants.addConstant')"
:description="t('developer.cloudConstants.createDescription')" :description="t('admin.cloudConstants.createDescription')"
:breadcrumbs="[ :breadcrumbs="[
{ title: t('developer.cloudConstants.title'), href: '/developer/cloud-constants' }, { title: t('admin.cloudConstants.title'), href: '/admin/cloud-constants' },
{ title: t('developer.cloudConstants.addConstant') }, { title: t('admin.cloudConstants.addConstant') },
]" ]"
sticky sticky
> >
@@ -168,18 +168,18 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" /> <Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cloudConstants.create.config') }} {{ t('admin.cloudConstants.create.config') }}
</UiCardTitle> </UiCardTitle>
<UiCardDescription>{{ t('developer.cloudConstants.create.configDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.cloudConstants.create.configDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="application"> <UiLabel for="application">
{{ t('developer.cloudConstants.create.application') }} {{ t('admin.cloudConstants.create.application') }}
</UiLabel> </UiLabel>
<UiSelect v-model="form.app_id"> <UiSelect v-model="form.app_id">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudConstants.create.selectApp')" /> <UiSelectValue :placeholder="t('admin.cloudConstants.create.selectApp')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)"> <UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
@@ -191,21 +191,21 @@ onMounted(() => {
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="varType"> <UiLabel for="varType">
{{ t('developer.cloudConstants.create.type') }} {{ t('admin.cloudConstants.create.type') }}
</UiLabel> </UiLabel>
<UiSelect id="varType" v-model="form.var_type"> <UiSelect id="varType" v-model="form.var_type">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudConstants.create.selectType')" /> <UiSelectValue :placeholder="t('admin.cloudConstants.create.selectType')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem value="integer"> <UiSelectItem value="integer">
{{ t('developer.cloudConstants.types.integer') }} {{ t('admin.cloudConstants.types.integer') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="decimal"> <UiSelectItem value="decimal">
{{ t('developer.cloudConstants.types.decimal') }} {{ t('admin.cloudConstants.types.decimal') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="string"> <UiSelectItem value="string">
{{ t('developer.cloudConstants.types.string') }} {{ t('admin.cloudConstants.types.string') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="binary"> <UiSelectItem value="binary">
二进制 二进制
@@ -216,11 +216,11 @@ onMounted(() => {
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="key"> <UiLabel for="key">
{{ t('developer.cloudConstants.create.key') }} {{ t('admin.cloudConstants.create.key') }}
</UiLabel> </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"> <p class="text-xs text-muted-foreground">
{{ t('developer.cloudConstants.create.keyHint') }} {{ t('admin.cloudConstants.create.keyHint') }}
</p> </p>
</div> </div>
@@ -263,16 +263,16 @@ onMounted(() => {
<div v-else class="space-y-2"> <div v-else class="space-y-2">
<UiLabel for="value"> <UiLabel for="value">
{{ t('developer.cloudConstants.create.value') }} {{ t('admin.cloudConstants.create.value') }}
</UiLabel> </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>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="description"> <UiLabel for="description">
{{ t('developer.cloudConstants.create.description') }} {{ t('admin.cloudConstants.create.description') }}
</UiLabel> </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> </div>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -283,25 +283,25 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" /> <Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cloudConstants.create.preview') }} {{ t('admin.cloudConstants.create.preview') }}
</UiCardTitle> </UiCardTitle>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between text-sm"> <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> <span>{{ selectedApplication?.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.type') }}</span> <span class="text-muted-foreground">{{ t('admin.cloudConstants.create.type') }}</span>
<span>{{ t(`developer.cloudConstants.types.${form.var_type}`) }}</span> <span>{{ t(`dmin.cloudConstants.types.${form.var_type}`) }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ form.key || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm items-center"> <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 <UiSwitch
:checked="form.status === 'active'" :checked="form.status === 'active'"
@update:checked="form.status = $event ? 'active' : 'inactive'" @update:checked="form.status = $event ? 'active' : 'inactive'"
@@ -310,7 +310,7 @@ onMounted(() => {
<div class="border-t pt-3 mt-3"> <div class="border-t pt-3 mt-3">
<div class="text-sm"> <div class="text-sm">
<div class="text-muted-foreground mb-2"> <div class="text-muted-foreground mb-2">
{{ t('developer.cloudConstants.create.value') }} {{ t('admin.cloudConstants.create.value') }}
</div> </div>
<div v-if="form.var_type === 'binary'" class="bg-muted p-2 rounded text-xs"> <div v-if="form.var_type === 'binary'" class="bg-muted p-2 rounded text-xs">
{{ selectedFile ? `${selectedFile.name} (${formatFileSize(selectedFile.size)})` : '未选择文件' }} {{ 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-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" /> <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>
<UiButton <UiButton
variant="outline" variant="outline"
class="w-full" class="w-full"
@click="router.back()" @click="router.back()"
> >
{{ t('developer.cloudConstants.create.cancel') }} {{ t('admin.cloudConstants.create.cancel') }}
</UiButton> </UiButton>
</div> </div>
</UiCardContent> </UiCardContent>
@@ -102,11 +102,11 @@ async function fetchConstants() {
} }
function openCreatePage() { function openCreatePage() {
router.push('/developer/cloud-constants/create') router.push('/admin/cloud-constants/create')
} }
function openEditPage(constant: CloudConstant) { function openEditPage(constant: CloudConstant) {
router.push(`/developer/cloud-constants/${constant.id}`) router.push(`/admin/cloud-constants/${constant.id}`)
} }
function confirmDelete(constant: CloudConstant) { function confirmDelete(constant: CloudConstant) {
@@ -120,12 +120,12 @@ async function handleDelete() {
try { try {
await api.delete(`/dev/cloud-constants/${deleteTarget.value.id}`) await api.delete(`/dev/cloud-constants/${deleteTarget.value.id}`)
toast.success(t('developer.cloudConstants.deleteSuccess')) toast.success(t('admin.cloudConstants.deleteSuccess'))
fetchConstants() fetchConstants()
} }
catch (error: any) { catch (error: any) {
console.error('删除云端常量失败:', error) console.error('删除云端常量失败:', error)
toast.error(error.message || t('developer.cloudConstants.deleteFailed')) toast.error(error.message || t('admin.cloudConstants.deleteFailed'))
} }
finally { finally {
deleteTarget.value = null deleteTarget.value = null
@@ -142,12 +142,12 @@ async function handleToggleStatus(constant: CloudConstant) {
description: constant.description, description: constant.description,
status: newStatus, 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() fetchConstants()
} }
catch (error: any) { catch (error: any) {
console.error('更新状态失败:', error) 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 { try {
if (batchAction.value === 'delete') { if (batchAction.value === 'delete') {
await Promise.all(ids.map(id => api.delete(`/dev/cloud-constants/${id}`))) 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 { else {
const status = batchAction.value === 'enable' ? 'active' : 'inactive' const status = batchAction.value === 'enable' ? 'active' : 'inactive'
@@ -184,13 +184,13 @@ async function handleBatchAction() {
description: c.description, description: c.description,
status, 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() fetchConstants()
} }
catch (error: any) { catch (error: any) {
console.error('批量操作失败:', error) console.error('批量操作失败:', error)
toast.error(error.message || t('developer.cloudConstants.batchFailed')) toast.error(error.message || t('admin.cloudConstants.batchFailed'))
} }
finally { finally {
batchAction.value = null batchAction.value = null
@@ -206,14 +206,14 @@ onMounted(async () => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cloudConstants.title')" :title="t('admin.cloudConstants.title')"
:description="t('developer.cloudConstants.description')" :description="t('admin.cloudConstants.description')"
sticky sticky
> >
<template #actions> <template #actions>
<UiButton size="sm" @click="openCreatePage"> <UiButton size="sm" @click="openCreatePage">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cloudConstants.addConstant') }} {{ t('admin.cloudConstants.addConstant') }}
</UiButton> </UiButton>
</template> </template>
@@ -222,7 +222,7 @@ onMounted(async () => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudConstants.totalConstants') }} {{ t('admin.cloudConstants.totalConstants') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:database" class="size-4 text-muted-foreground" /> <Icon icon="lucide:database" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -236,7 +236,7 @@ onMounted(async () => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudConstants.linkedApps') }} {{ t('admin.cloudConstants.linkedApps') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:link" class="size-4 text-muted-foreground" /> <Icon icon="lucide:link" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -250,7 +250,7 @@ onMounted(async () => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudConstants.unlinked') }} {{ t('admin.cloudConstants.unlinked') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:unlink" class="size-4 text-muted-foreground" /> <Icon icon="lucide:unlink" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -264,7 +264,7 @@ onMounted(async () => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudConstants.appCount') }} {{ t('admin.cloudConstants.appCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" /> <Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -291,7 +291,7 @@ onMounted(async () => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.cloudConstants.application')" :title="t('admin.cloudConstants.application')"
:options="applicationOptions" :options="applicationOptions"
/> />
<DateRangeFilter <DateRangeFilter
@@ -307,30 +307,30 @@ onMounted(async () => {
<ConfirmDialog <ConfirmDialog
v-model:open="deleteDialogOpen" v-model:open="deleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.cloudConstants.delete')" :confirm-button-text="t('admin.cloudConstants.delete')"
:cancel-button-text="t('common.reset')" :cancel-button-text="t('common.reset')"
@confirm="handleDelete" @confirm="handleDelete"
> >
<template #title> <template #title>
{{ t('developer.cloudConstants.deleteConstant') }} {{ t('admin.cloudConstants.deleteConstant') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.cloudConstants.deleteConfirm', { key: deleteTarget?.key }) }} {{ t('admin.cloudConstants.deleteConfirm', { key: deleteTarget?.key }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
v-model:open="batchDialogOpen" v-model:open="batchDialogOpen"
:destructive="batchAction === 'delete'" :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')" :cancel-button-text="t('common.cancel')"
@confirm="handleBatchAction" @confirm="handleBatchAction"
> >
<template #title> <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>
<template #description> <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> </template>
</ConfirmDialog> </ConfirmDialog>
</BasicPage> </BasicPage>
+291 -340
View File
@@ -2,409 +2,360 @@
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 { BasicPage } from '@/components/global-layout'
import api from '@/services/api' import api from '@/services/api'
import type { CloudVariable } from './data/schema'
import DataTable from './components/data-table.vue'
const { t } = useI18n() const { t } = useI18n()
const route = useRoute()
const router = useRouter() const router = useRouter()
interface Application { interface Application {
id: number id: number
name: string name: string
app_key: string
} }
const loading = ref(true) const loading = ref(true)
const saving = ref(false) const variables = ref<CloudVariable[]>([])
const applications = ref<Application[]>([]) const applications = ref<Application[]>([])
const form = ref({ const appFilter = ref<string>('')
id: '', const scopeFilter = ref<string>('')
app_id: '', const startDate = ref<string>('')
key: '', const endDate = ref<string>('')
default_value: '',
var_type: 'string', const deleteDialogOpen = ref(false)
max_records: 0, const deleteTarget = ref<CloudVariable | null>(null)
scope: 'app', const batchDialogOpen = ref(false)
write_permission: 'admin', const batchAction = ref<'enable' | 'disable' | 'delete' | null>(null)
description: '', const selectedRows = ref<CloudVariable[]>([])
status: 'active',
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() { async function fetchApplications() {
try { try {
const data = await api.get<{ applications: Application[] }>('/dev/applications') const data = await api.get<{ applications: Application[] }>('/dev/applications')
applications.value = Array.isArray(data?.applications) ? data.applications : [] applications.value = data?.applications || []
} }
catch (error) { catch (error) {
console.error('获取应用列表失败:', error) console.error('获取应用列表失败:', error)
} }
} }
async function fetchVariable() { async function fetchVariables() {
loading.value = true loading.value = true
try { try {
const varId = route.params.id const data = await api.get<{ variables: CloudVariable[] }>('/dev/cloud-variables')
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 vars = data?.variables || []
const variable = data variables.value = vars.map((v: any) => ({
if (variable) { ...v,
form.value = { application_name: applications.value.find(app => app.id === v.app_id)?.name || '',
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')
}
} }
catch (error) { catch (error) {
console.error('获取云端变量失败:', error) console.error('获取云端变量失败:', error)
toast.error(t('developer.cloudVariables.create.failed')) variables.value = []
router.push('/developer/cloud-variables')
} }
finally { finally {
loading.value = false loading.value = false
} }
} }
const selectedApplication = computed(() => { function openCreatePage() {
if (form.value.app_id) { router.push('/admin/cloud-variables/create')
return applications.value.find(app => String(app.id) === form.value.app_id) }
}
return null
})
async function handleSave() { function openEditPage(variable: CloudVariable) {
if (!form.value.key) { router.push(`/admin/cloud-variables/${variable.id}`)
toast.error(t('developer.cloudVariables.create.keyRequired')) }
function confirmDelete(variable: CloudVariable) {
deleteTarget.value = variable
deleteDialogOpen.value = true
}
async function handleDelete() {
if (!deleteTarget.value)
return return
}
saving.value = true
try { try {
await api.put(`/dev/cloud-variables/${form.value.id}`, { await api.delete(`/dev/cloud-variables/${deleteTarget.value.id}`)
key: form.value.key, toast.success(t('admin.cloudVariables.deleteSuccess'))
default_value: form.value.default_value, fetchVariables()
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')
} }
catch (error: any) { catch (error: any) {
console.error('更新云端变量失败:', error) console.error('删除云端变量失败:', error)
toast.error(error.message || t('developer.cloudVariables.create.failed')) toast.error(error.message || t('admin.cloudVariables.deleteFailed'))
} }
finally { 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 () => { onMounted(async () => {
await fetchApplications() await fetchApplications()
fetchVariable() fetchVariables()
}) })
</script> </script>
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cloudVariables.edit')" :title="t('admin.cloudVariables.title')"
:description="t('developer.cloudVariables.editDescription')" :description="t('admin.cloudVariables.description')"
:breadcrumbs="[
{ title: t('developer.cloudVariables.title'), href: '/developer/cloud-variables' },
{ title: t('developer.cloudVariables.edit') },
]"
sticky sticky
> >
<div v-if="loading" class="flex items-center justify-center py-12"> <template #actions>
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" /> <UiButton size="sm" @click="openCreatePage">
</div> <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="space-y-6">
<div class="grid gap-6 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="lg:col-span-2 space-y-6"> <UiCard>
<UiCard> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardHeader> <UiCardTitle class="text-sm font-medium">
<UiCardTitle class="flex items-center gap-2"> {{ t('admin.cloudVariables.totalVariables') }}
<Icon icon="lucide:settings-2" class="size-5" /> </UiCardTitle>
{{ t('developer.cloudVariables.create.config') }} <Icon icon="lucide:variable" class="size-4 text-muted-foreground" />
</UiCardTitle> </UiCardHeader>
<UiCardDescription>{{ t('developer.cloudVariables.create.configDesc') }}</UiCardDescription> <UiCardContent>
</UiCardHeader> <div class="text-2xl font-bold">
<UiCardContent class="space-y-6"> {{ filteredVariables.length }}
<div class="space-y-2"> </div>
<UiLabel>{{ t('developer.cloudVariables.create.application') }}</UiLabel> </UiCardContent>
<div class="flex items-center gap-2 p-2.5 rounded-md border bg-muted/50"> </UiCard>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
</div>
<div class="space-y-2"> <UiCard>
<UiLabel for="varType"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.cloudVariables.create.type') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cloudVariables.appScopeVariables') }}
<UiSelect id="varType" v-model="form.var_type"> </UiCardTitle>
<UiSelectTrigger> <Icon icon="lucide:globe" class="size-4 text-muted-foreground" />
<UiSelectValue :placeholder="t('developer.cloudVariables.create.selectType')" /> </UiCardHeader>
</UiSelectTrigger> <UiCardContent>
<UiSelectContent> <div class="text-2xl font-bold">
<UiSelectItem value="integer"> {{ appScopeCount }}
{{ t('developer.cloudVariables.types.integer') }} </div>
</UiSelectItem> </UiCardContent>
<UiSelectItem value="decimal"> </UiCard>
{{ t('developer.cloudVariables.types.decimal') }}
</UiSelectItem>
<UiSelectItem value="string">
{{ t('developer.cloudVariables.types.string') }}
</UiSelectItem>
<UiSelectItem value="stream">
记录
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<div v-if="form.var_type === 'stream'" class="space-y-2"> <UiCard>
<UiLabel for="maxRecords"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
最大记录数 <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cloudVariables.userScopeVariables') }}
<UiInput </UiCardTitle>
id="maxRecords" <Icon icon="lucide:user" class="size-4 text-muted-foreground" />
v-model.number="form.max_records" </UiCardHeader>
type="number" <UiCardContent>
min="0" <div class="text-2xl font-bold">
placeholder="0 = 不限制" {{ userScopeCount }}
/> </div>
<p class="text-xs text-muted-foreground"> </UiCardContent>
超过限制时自动删除最旧的记录0表示不限制 </UiCard>
</p>
</div>
<div class="space-y-2"> <UiCard>
<UiLabel for="key"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.cloudVariables.create.key') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cloudVariables.appCount') }}
<UiInput id="key" v-model="form.key" :placeholder="t('developer.cloudVariables.create.keyPlaceholder')" /> </UiCardTitle>
<p class="text-xs text-muted-foreground"> <Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
{{ t('developer.cloudVariables.create.keyHint') }} </UiCardHeader>
</p> <UiCardContent>
</div> <div class="text-2xl font-bold">
{{ applications.length }}
<div class="space-y-2"> </div>
<UiLabel for="defaultValue"> </UiCardContent>
{{ t('developer.cloudVariables.create.defaultValue') }} </UiCard>
</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>
</div> </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> </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> </BasicPage>
</template> </template>
@@ -144,7 +144,7 @@ onMounted(async () => {
title="变量记录" title="变量记录"
description="查看云端变量的记录数据" description="查看云端变量的记录数据"
:breadcrumbs="[ :breadcrumbs="[
{ title: '云端变量', href: '/developer/cloud-variables' }, { title: '云端变量', href: '/admin/cloud-variables' },
{ title: '变量记录' }, { title: '变量记录' },
]" ]"
> >
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Download, File, MoreHorizontal, Pencil, Power, PowerOff, Trash2 } from 'lucide-vue-next' import { Download, File, MoreHorizontal, Pencil, Power, PowerOff, Trash2 } from 'lucide-vue-next'
import { h } from 'vue' 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 { Copy } from '@/components/sva-ui/copy'
import Badge from '@/components/ui/badge/Badge.vue' import Badge from '@/components/ui/badge/Badge.vue'
@@ -26,7 +26,7 @@ export function getColumns(actions: {
return [ return [
{ {
accessorKey: 'key', accessorKey: 'key',
header: () => t('developer.cloudVariables.columns.key'), header: () => t('admin.cloudVariables.columns.key'),
cell: ({ row }) => { cell: ({ row }) => {
const key = row.getValue('key') as string const key = row.getValue('key') as string
if (!key) if (!key)
@@ -39,7 +39,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'default_value', accessorKey: 'default_value',
header: () => t('developer.cloudVariables.columns.defaultValue'), header: () => t('admin.cloudVariables.columns.defaultValue'),
cell: ({ row }) => { cell: ({ row }) => {
const value = row.getValue('default_value') as string const value = row.getValue('default_value') as string
const varType = row.original.var_type const varType = row.original.var_type
@@ -57,36 +57,36 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'scope', accessorKey: 'scope',
header: () => t('developer.cloudVariables.columns.scope'), header: () => t('admin.cloudVariables.columns.scope'),
cell: ({ row }) => { cell: ({ row }) => {
const scope = row.getValue('scope') as string const scope = row.getValue('scope') as string
if (scope === 'app') { 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', accessorKey: 'write_permission',
header: () => t('developer.cloudVariables.create.writePermission'), header: () => t('admin.cloudVariables.create.writePermission'),
cell: ({ row }) => { cell: ({ row }) => {
const permission = row.getValue('write_permission') as string const permission = row.getValue('write_permission') as string
if (permission === 'user') { 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', accessorKey: 'var_type',
header: () => t('developer.cloudVariables.columns.type'), header: () => t('admin.cloudVariables.columns.type'),
cell: ({ row }) => { cell: ({ row }) => {
const type = row.getValue('var_type') as string const type = row.getValue('var_type') as string
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
integer: t('developer.cloudVariables.types.integer'), integer: t('admin.cloudVariables.types.integer'),
decimal: t('developer.cloudVariables.types.decimal'), decimal: t('admin.cloudVariables.types.decimal'),
string: t('developer.cloudVariables.types.string'), string: t('admin.cloudVariables.types.string'),
binary: t('developer.cloudVariables.types.binary'), binary: t('admin.cloudVariables.types.binary'),
stream: '记录', stream: '记录',
} }
return h(Badge, { variant: 'outline' }, () => typeMap[type || 'string'] || type || 'string') return h(Badge, { variant: 'outline' }, () => typeMap[type || 'string'] || type || 'string')
@@ -94,7 +94,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'description', accessorKey: 'description',
header: () => t('developer.cloudVariables.columns.description'), header: () => t('admin.cloudVariables.columns.description'),
cell: ({ row }) => { cell: ({ row }) => {
const description = row.getValue('description') as string const description = row.getValue('description') as string
if (!description) if (!description)
@@ -104,13 +104,13 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'application_name', accessorKey: 'application_name',
header: () => t('developer.cloudVariables.columns.application'), header: () => t('admin.cloudVariables.columns.application'),
cell: ({ row }) => { cell: ({ row }) => {
const appName = row.getValue('application_name') as string const appName = row.getValue('application_name') as string
if (!appName) { if (!appName) {
const appId = row.original.app_id const appId = row.original.app_id
if (!appId) if (!appId)
return h(Badge, { variant: 'outline' }, () => t('developer.cloudVariables.notLinked')) return h(Badge, { variant: 'outline' }, () => t('admin.cloudVariables.notLinked'))
return '-' return '-'
} }
return h(Badge, { variant: 'outline' }, () => appName) return h(Badge, { variant: 'outline' }, () => appName)
@@ -118,16 +118,16 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'status', accessorKey: 'status',
header: () => t('developer.cloudVariables.columns.status'), header: () => t('admin.cloudVariables.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const status = row.getValue('status') as string const status = row.getValue('status') as string
const isActive = status === 'active' 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', accessorKey: 'created_at',
header: () => t('developer.cloudVariables.columns.createdAt'), header: () => t('admin.cloudVariables.columns.createdAt'),
cell: ({ row }) => { cell: ({ row }) => {
const createdAt = row.getValue('created_at') const createdAt = row.getValue('created_at')
if (!createdAt) if (!createdAt)
@@ -172,7 +172,7 @@ export function getColumns(actions: {
const items = [ const items = [
h(DropdownMenuItem, { onClick: () => actions.onEdit(variable) }, () => [ h(DropdownMenuItem, { onClick: () => actions.onEdit(variable) }, () => [
h(Pencil, { class: 'mr-2 h-4 w-4' }), 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( items.push(
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(variable) }, () => [ 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' ? 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(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(variable) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }), 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 const variable = props.row.original
function viewRecords() { 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> </script>
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template> <template>
<div class="flex items-center flex-1 space-x-2"> <div class="flex items-center flex-1 space-x-2">
<Input <Input
:placeholder="t('developer.cloudVariables.searchPlaceholder')" :placeholder="t('admin.cloudVariables.searchPlaceholder')"
:model-value="(table.getColumn('key')?.getFilterValue() as string) ?? ''" :model-value="(table.getColumn('key')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]" class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('key')?.setFilterValue($event.target.value)" @input="table.getColumn('key')?.setFilterValue($event.target.value)"
@@ -48,16 +48,16 @@ const table = generateVueTable<CloudVariable>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.cloudVariables.select', select: 'admin.cloudVariables.select',
key: 'developer.cloudVariables.columns.key', key: 'admin.cloudVariables.columns.key',
default_value: 'developer.cloudVariables.columns.defaultValue', default_value: 'admin.cloudVariables.columns.defaultValue',
scope: 'developer.cloudVariables.columns.scope', scope: 'admin.cloudVariables.columns.scope',
write_permission: 'developer.cloudVariables.create.writePermission', write_permission: 'admin.cloudVariables.create.writePermission',
var_type: 'developer.cloudVariables.columns.type', var_type: 'admin.cloudVariables.columns.type',
description: 'developer.cloudVariables.columns.description', description: 'admin.cloudVariables.columns.description',
application_name: 'developer.cloudVariables.columns.application', application_name: 'admin.cloudVariables.columns.application',
status: 'developer.cloudVariables.columns.status', status: 'admin.cloudVariables.columns.status',
created_at: 'developer.cloudVariables.columns.createdAt', created_at: 'admin.cloudVariables.columns.createdAt',
} }
defineExpose({ defineExpose({
@@ -80,13 +80,13 @@ function handleBatchAction(action: 'enable' | 'disable' | 'delete') {
</div> </div>
<BulkActions :table="table" entity-name="variables"> <BulkActions :table="table" entity-name="variables">
<UiButton variant="outline" size="sm" @click="handleBatchAction('enable')"> <UiButton variant="outline" size="sm" @click="handleBatchAction('enable')">
{{ t('developer.cloudVariables.batchEnableBtn') }} {{ t('admin.cloudVariables.batchEnableBtn') }}
</UiButton> </UiButton>
<UiButton variant="outline" size="sm" @click="handleBatchAction('disable')"> <UiButton variant="outline" size="sm" @click="handleBatchAction('disable')">
{{ t('developer.cloudVariables.batchDisableBtn') }} {{ t('admin.cloudVariables.batchDisableBtn') }}
</UiButton> </UiButton>
<UiButton variant="destructive" size="sm" @click="handleBatchAction('delete')"> <UiButton variant="destructive" size="sm" @click="handleBatchAction('delete')">
{{ t('developer.cloudVariables.batchDeleteBtn') }} {{ t('admin.cloudVariables.batchDeleteBtn') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
@@ -70,11 +70,11 @@ function clearFile() {
async function handleSave() { async function handleSave() {
if (!form.value.key) { if (!form.value.key) {
toast.error(t('developer.cloudVariables.create.keyRequired')) toast.error(t('admin.cloudVariables.create.keyRequired'))
return return
} }
if (!form.value.app_id) { if (!form.value.app_id) {
toast.error(t('developer.cloudVariables.create.appRequired')) toast.error(t('admin.cloudVariables.create.appRequired'))
return return
} }
@@ -100,12 +100,12 @@ async function handleSave() {
description: form.value.description, description: form.value.description,
status: form.value.status, status: form.value.status,
}) })
toast.success(t('developer.cloudVariables.create.success')) toast.success(t('admin.cloudVariables.create.success'))
router.push('/developer/cloud-variables') router.push('/admin/cloud-variables')
} }
catch (error: any) { catch (error: any) {
console.error('创建云端变量失败:', error) console.error('创建云端变量失败:', error)
toast.error(error.message || t('developer.cloudVariables.create.failed')) toast.error(error.message || t('admin.cloudVariables.create.failed'))
} }
finally { finally {
saving.value = false saving.value = false
@@ -128,8 +128,8 @@ async function uploadFile() {
formData.append('write_permission', form.value.write_permission) formData.append('write_permission', form.value.write_permission)
await api.postFormData('/dev/cloud-variables/upload', formData) await api.postFormData('/dev/cloud-variables/upload', formData)
toast.success(t('developer.cloudVariables.create.success')) toast.success(t('admin.cloudVariables.create.success'))
router.push('/developer/cloud-variables') router.push('/admin/cloud-variables')
} }
catch (error: any) { catch (error: any) {
console.error('上传文件失败:', error) console.error('上传文件失败:', error)
@@ -156,11 +156,11 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cloudVariables.addVariable')" :title="t('admin.cloudVariables.addVariable')"
:description="t('developer.cloudVariables.createDescription')" :description="t('admin.cloudVariables.createDescription')"
:breadcrumbs="[ :breadcrumbs="[
{ title: t('developer.cloudVariables.title'), href: '/developer/cloud-variables' }, { title: t('admin.cloudVariables.title'), href: '/admin/cloud-variables' },
{ title: t('developer.cloudVariables.addVariable') }, { title: t('admin.cloudVariables.addVariable') },
]" ]"
sticky sticky
> >
@@ -171,18 +171,18 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" /> <Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cloudVariables.create.config') }} {{ t('admin.cloudVariables.create.config') }}
</UiCardTitle> </UiCardTitle>
<UiCardDescription>{{ t('developer.cloudVariables.create.configDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.cloudVariables.create.configDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="application"> <UiLabel for="application">
{{ t('developer.cloudVariables.create.application') }} {{ t('admin.cloudVariables.create.application') }}
</UiLabel> </UiLabel>
<UiSelect v-model="form.app_id"> <UiSelect v-model="form.app_id">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudVariables.create.selectApp')" /> <UiSelectValue :placeholder="t('admin.cloudVariables.create.selectApp')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)"> <UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
@@ -194,21 +194,21 @@ onMounted(() => {
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="varType"> <UiLabel for="varType">
{{ t('developer.cloudVariables.create.type') }} {{ t('admin.cloudVariables.create.type') }}
</UiLabel> </UiLabel>
<UiSelect id="varType" v-model="form.var_type"> <UiSelect id="varType" v-model="form.var_type">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudVariables.create.selectType')" /> <UiSelectValue :placeholder="t('admin.cloudVariables.create.selectType')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem value="integer"> <UiSelectItem value="integer">
{{ t('developer.cloudVariables.types.integer') }} {{ t('admin.cloudVariables.types.integer') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="decimal"> <UiSelectItem value="decimal">
{{ t('developer.cloudVariables.types.decimal') }} {{ t('admin.cloudVariables.types.decimal') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="string"> <UiSelectItem value="string">
{{ t('developer.cloudVariables.types.string') }} {{ t('admin.cloudVariables.types.string') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="binary"> <UiSelectItem value="binary">
二进制 二进制
@@ -238,11 +238,11 @@ onMounted(() => {
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="key"> <UiLabel for="key">
{{ t('developer.cloudVariables.create.key') }} {{ t('admin.cloudVariables.create.key') }}
</UiLabel> </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"> <p class="text-xs text-muted-foreground">
{{ t('developer.cloudVariables.create.keyHint') }} {{ t('admin.cloudVariables.create.keyHint') }}
</p> </p>
</div> </div>
@@ -285,24 +285,24 @@ onMounted(() => {
<div v-else class="space-y-2"> <div v-else class="space-y-2">
<UiLabel for="defaultValue"> <UiLabel for="defaultValue">
{{ t('developer.cloudVariables.create.defaultValue') }} {{ t('admin.cloudVariables.create.defaultValue') }}
</UiLabel> </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>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="description"> <UiLabel for="description">
{{ t('developer.cloudVariables.create.description') }} {{ t('admin.cloudVariables.create.description') }}
</UiLabel> </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> </div>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.cloudVariables.create.scopeTitle') }}</UiCardTitle> <UiCardTitle>{{ t('admin.cloudVariables.create.scopeTitle') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudVariables.create.scopeDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.cloudVariables.create.scopeDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent>
<UiRadioGroup v-model="form.scope" class="grid grid-cols-1 md:grid-cols-2 gap-4"> <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"> <div class="flex items-center gap-2">
<Icon icon="lucide:globe" class="size-4 text-primary" /> <Icon icon="lucide:globe" class="size-4 text-primary" />
<p class="font-medium"> <p class="font-medium">
{{ t('developer.cloudVariables.create.appScope') }} {{ t('admin.cloudVariables.create.appScope') }}
</p> </p>
</div> </div>
<p class="text-sm text-muted-foreground mt-1"> <p class="text-sm text-muted-foreground mt-1">
{{ t('developer.cloudVariables.create.appScopeDesc') }} {{ t('admin.cloudVariables.create.appScopeDesc') }}
</p> </p>
</div> </div>
</div> </div>
@@ -335,11 +335,11 @@ onMounted(() => {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Icon icon="lucide:user" class="size-4 text-primary" /> <Icon icon="lucide:user" class="size-4 text-primary" />
<p class="font-medium"> <p class="font-medium">
{{ t('developer.cloudVariables.create.userScope') }} {{ t('admin.cloudVariables.create.userScope') }}
</p> </p>
</div> </div>
<p class="text-sm text-muted-foreground mt-1"> <p class="text-sm text-muted-foreground mt-1">
{{ t('developer.cloudVariables.create.userScopeDesc') }} {{ t('admin.cloudVariables.create.userScopeDesc') }}
</p> </p>
</div> </div>
</div> </div>
@@ -349,8 +349,8 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.cloudVariables.create.permissionTitle') }}</UiCardTitle> <UiCardTitle>{{ t('admin.cloudVariables.create.permissionTitle') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.cloudVariables.create.permissionDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.cloudVariables.create.permissionDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent>
<UiRadioGroup v-model="form.write_permission" class="grid grid-cols-1 gap-4"> <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"> <div class="flex items-center gap-2">
<Icon icon="lucide:lock" class="size-4 text-primary" /> <Icon icon="lucide:lock" class="size-4 text-primary" />
<p class="font-medium"> <p class="font-medium">
{{ t('developer.cloudVariables.create.developerOnly') }} {{ t('admin.cloudVariables.create.developerOnly') }}
</p> </p>
</div> </div>
<p class="text-sm text-muted-foreground mt-1"> <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> </p>
</div> </div>
</div> </div>
@@ -383,11 +383,11 @@ onMounted(() => {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Icon icon="lucide:unlock" class="size-4 text-primary" /> <Icon icon="lucide:unlock" class="size-4 text-primary" />
<p class="font-medium"> <p class="font-medium">
{{ t('developer.cloudVariables.create.userWritable') }} {{ t('admin.cloudVariables.create.userWritable') }}
</p> </p>
</div> </div>
<p class="text-sm text-muted-foreground mt-1"> <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> </p>
</div> </div>
</div> </div>
@@ -421,41 +421,41 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" /> <Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cloudVariables.create.preview') }} {{ t('admin.cloudVariables.create.preview') }}
</UiCardTitle> </UiCardTitle>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between text-sm"> <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> <span>{{ selectedApplication?.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.type') }}</span> <span class="text-muted-foreground">{{ t('admin.cloudVariables.create.type') }}</span>
<span>{{ t(`developer.cloudVariables.types.${form.var_type}`) }}</span> <span>{{ t(`dmin.cloudVariables.types.${form.var_type}`) }}</span>
</div> </div>
<div v-if="form.var_type === 'stream'" class="flex justify-between text-sm"> <div v-if="form.var_type === 'stream'" class="flex justify-between text-sm">
<span class="text-muted-foreground">最大记录数</span> <span class="text-muted-foreground">最大记录数</span>
<span>{{ form.max_records || '不限制' }}</span> <span>{{ form.max_records || '不限制' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ form.key || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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"> <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> </UiBadge>
</div> </div>
<div class="flex justify-between text-sm"> <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"> <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> </UiBadge>
</div> </div>
<div class="flex justify-between text-sm items-center"> <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 <UiSwitch
:checked="form.status === 'active'" :checked="form.status === 'active'"
@update:checked="form.status = $event ? 'active' : 'inactive'" @update:checked="form.status = $event ? 'active' : 'inactive'"
@@ -464,7 +464,7 @@ onMounted(() => {
<div class="border-t pt-3 mt-3"> <div class="border-t pt-3 mt-3">
<div class="text-sm"> <div class="text-sm">
<div class="text-muted-foreground mb-2"> <div class="text-muted-foreground mb-2">
{{ t('developer.cloudVariables.create.defaultValue') }} {{ t('admin.cloudVariables.create.defaultValue') }}
</div> </div>
<div class="bg-muted p-2 rounded max-h-[100px] overflow-y-auto text-xs"> <div class="bg-muted p-2 rounded max-h-[100px] overflow-y-auto text-xs">
{{ form.default_value || '-' }} {{ 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-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" /> <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>
<UiButton <UiButton
variant="outline" variant="outline"
class="w-full" class="w-full"
@click="router.back()" @click="router.back()"
> >
{{ t('developer.cloudVariables.create.cancel') }} {{ t('admin.cloudVariables.create.cancel') }}
</UiButton> </UiButton>
</div> </div>
</UiCardContent> </UiCardContent>
@@ -78,8 +78,8 @@ const applicationOptions = computed(() => {
}) })
const scopeOptions = computed(() => [ const scopeOptions = computed(() => [
{ label: t('developer.cloudVariables.appScope'), value: 'app' }, { label: t('admin.cloudVariables.appScope'), value: 'app' },
{ label: t('developer.cloudVariables.userScope'), value: 'user' }, { label: t('admin.cloudVariables.userScope'), value: 'user' },
]) ])
async function fetchApplications() { async function fetchApplications() {
@@ -112,11 +112,11 @@ async function fetchVariables() {
} }
function openCreatePage() { function openCreatePage() {
router.push('/developer/cloud-variables/create') router.push('/admin/cloud-variables/create')
} }
function openEditPage(variable: CloudVariable) { function openEditPage(variable: CloudVariable) {
router.push(`/developer/cloud-variables/${variable.id}`) router.push(`/admin/cloud-variables/${variable.id}`)
} }
function confirmDelete(variable: CloudVariable) { function confirmDelete(variable: CloudVariable) {
@@ -130,12 +130,12 @@ async function handleDelete() {
try { try {
await api.delete(`/dev/cloud-variables/${deleteTarget.value.id}`) await api.delete(`/dev/cloud-variables/${deleteTarget.value.id}`)
toast.success(t('developer.cloudVariables.deleteSuccess')) toast.success(t('admin.cloudVariables.deleteSuccess'))
fetchVariables() fetchVariables()
} }
catch (error: any) { catch (error: any) {
console.error('删除云端变量失败:', error) console.error('删除云端变量失败:', error)
toast.error(error.message || t('developer.cloudVariables.deleteFailed')) toast.error(error.message || t('admin.cloudVariables.deleteFailed'))
} }
finally { finally {
deleteTarget.value = null deleteTarget.value = null
@@ -154,12 +154,12 @@ async function handleToggleStatus(variable: CloudVariable) {
description: variable.description, description: variable.description,
status: newStatus, 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() fetchVariables()
} }
catch (error: any) { catch (error: any) {
console.error('更新状态失败:', error) 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) { 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[]) { function openBatchDialog(action: 'enable' | 'disable' | 'delete', rows: CloudVariable[]) {
@@ -189,7 +189,7 @@ async function handleBatchAction() {
try { try {
if (batchAction.value === 'delete') { if (batchAction.value === 'delete') {
await Promise.all(ids.map(id => api.delete(`/dev/cloud-variables/${id}`))) 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 { else {
const status = batchAction.value === 'enable' ? 'active' : 'inactive' const status = batchAction.value === 'enable' ? 'active' : 'inactive'
@@ -202,13 +202,13 @@ async function handleBatchAction() {
description: v.description, description: v.description,
status, 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() fetchVariables()
} }
catch (error: any) { catch (error: any) {
console.error('批量操作失败:', error) console.error('批量操作失败:', error)
toast.error(error.message || t('developer.cloudVariables.batchFailed')) toast.error(error.message || t('admin.cloudVariables.batchFailed'))
} }
finally { finally {
batchAction.value = null batchAction.value = null
@@ -224,14 +224,14 @@ onMounted(async () => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cloudVariables.title')" :title="t('admin.cloudVariables.title')"
:description="t('developer.cloudVariables.description')" :description="t('admin.cloudVariables.description')"
sticky sticky
> >
<template #actions> <template #actions>
<UiButton size="sm" @click="openCreatePage"> <UiButton size="sm" @click="openCreatePage">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cloudVariables.addVariable') }} {{ t('admin.cloudVariables.addVariable') }}
</UiButton> </UiButton>
</template> </template>
@@ -240,7 +240,7 @@ onMounted(async () => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudVariables.totalVariables') }} {{ t('admin.cloudVariables.totalVariables') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:variable" class="size-4 text-muted-foreground" /> <Icon icon="lucide:variable" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -254,7 +254,7 @@ onMounted(async () => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudVariables.appScopeVariables') }} {{ t('admin.cloudVariables.appScopeVariables') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:globe" class="size-4 text-muted-foreground" /> <Icon icon="lucide:globe" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -268,7 +268,7 @@ onMounted(async () => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudVariables.userScopeVariables') }} {{ t('admin.cloudVariables.userScopeVariables') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:user" class="size-4 text-muted-foreground" /> <Icon icon="lucide:user" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -282,7 +282,7 @@ onMounted(async () => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudVariables.appCount') }} {{ t('admin.cloudVariables.appCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" /> <Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -310,12 +310,12 @@ onMounted(async () => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.cloudVariables.application')" :title="t('admin.cloudVariables.application')"
:options="applicationOptions" :options="applicationOptions"
/> />
<SingleFilter <SingleFilter
v-model="scopeFilter" v-model="scopeFilter"
:title="t('developer.cloudVariables.scope')" :title="t('admin.cloudVariables.scope')"
:options="scopeOptions" :options="scopeOptions"
/> />
<DateRangeFilter <DateRangeFilter
@@ -331,30 +331,30 @@ onMounted(async () => {
<ConfirmDialog <ConfirmDialog
v-model:open="deleteDialogOpen" v-model:open="deleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.cloudVariables.delete')" :confirm-button-text="t('admin.cloudVariables.delete')"
:cancel-button-text="t('common.reset')" :cancel-button-text="t('common.reset')"
@confirm="handleDelete" @confirm="handleDelete"
> >
<template #title> <template #title>
{{ t('developer.cloudVariables.deleteVariable') }} {{ t('admin.cloudVariables.deleteVariable') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.cloudVariables.deleteConfirm', { key: deleteTarget?.key }) }} {{ t('admin.cloudVariables.deleteConfirm', { key: deleteTarget?.key }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
v-model:open="batchDialogOpen" v-model:open="batchDialogOpen"
:destructive="batchAction === 'delete'" :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')" :cancel-button-text="t('common.cancel')"
@confirm="handleBatchAction" @confirm="handleBatchAction"
> >
<template #title> <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>
<template #description> <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> </template>
</ConfirmDialog> </ConfirmDialog>
</BasicPage> </BasicPage>
@@ -4,7 +4,7 @@ import { Ban, Eye, LogOut, MoreHorizontal, Trash2 } from 'lucide-vue-next'
import { h } from 'vue' import { h } from 'vue'
import { useRouter } from 'vue-router' 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 Badge from '@/components/ui/badge/Badge.vue'
import Button from '@/components/ui/button/Button.vue' import Button from '@/components/ui/button/Button.vue'
@@ -146,7 +146,7 @@ export function getColumns(actions: {
{ align: 'end' }, { align: 'end' },
() => [ () => [
hasOnlineSessions 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' }), h(Eye, { class: 'mr-2 h-4 w-4' }),
'查看实例', '查看实例',
]) ])
@@ -30,7 +30,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
<template> <template>
<div class="flex flex-1 items-center space-x-2"> <div class="flex flex-1 items-center space-x-2">
<Input <Input
:placeholder="t('developer.devices.searchPlaceholder')" :placeholder="t('admin.devices.searchPlaceholder')"
:model-value="(table.getColumn('device_id')?.getFilterValue() as string) ?? ''" :model-value="(table.getColumn('device_id')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]" class="h-8 w-[150px] lg:w-[250px]"
@update:model-value="table.getColumn('device_id')?.setFilterValue($event)" @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" class="h-8 px-2 lg:px-3"
@click="table.resetColumnFilters()" @click="table.resetColumnFilters()"
> >
{{ t('developer.devices.reset') }} {{ t('admin.devices.reset') }}
<Icon icon="lucide:x" class="ml-2 h-4 w-4" /> <Icon icon="lucide:x" class="ml-2 h-4 w-4" />
</Button> </Button>
</div> </div>
@@ -59,7 +59,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
<template v-if="selectedCount > 0"> <template v-if="selectedCount > 0">
<span class="text-sm text-muted-foreground"> <span class="text-sm text-muted-foreground">
{{ t('developer.devices.selected', { count: selectedCount }) }} {{ t('admin.devices.selected', { count: selectedCount }) }}
</span> </span>
<Button <Button
variant="outline" variant="outline"
@@ -68,7 +68,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
@click="emit('batchUnban')" @click="emit('batchUnban')"
> >
<Icon icon="lucide:check-circle" class="mr-2 h-4 w-4" /> <Icon icon="lucide:check-circle" class="mr-2 h-4 w-4" />
{{ t('developer.devices.batchUnban') }} {{ t('admin.devices.batchUnban') }}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -77,7 +77,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
@click="emit('batchBan')" @click="emit('batchBan')"
> >
<Icon icon="lucide:ban" class="mr-2 h-4 w-4" /> <Icon icon="lucide:ban" class="mr-2 h-4 w-4" />
{{ t('developer.devices.batchBan') }} {{ t('admin.devices.batchBan') }}
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
@@ -86,7 +86,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
@click="emit('batchUnbind')" @click="emit('batchUnbind')"
> >
<Icon icon="lucide:trash-2" class="mr-2 h-4 w-4" /> <Icon icon="lucide:trash-2" class="mr-2 h-4 w-4" />
{{ t('developer.devices.batchUnbind') }} {{ t('admin.devices.batchUnbind') }}
</Button> </Button>
</template> </template>
</div> </div>
@@ -46,16 +46,16 @@ const table = generateVueTable<Device>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
'select': 'developer.devices.select', 'select': 'admin.devices.select',
'device_id': 'developer.devices.columns.deviceId', 'device_id': 'admin.devices.columns.deviceId',
'device_name': 'developer.devices.columns.deviceName', 'device_name': 'admin.devices.columns.deviceName',
'device_type': 'developer.devices.columns.deviceType', 'device_type': 'admin.devices.columns.deviceType',
'user.username': 'developer.devices.columns.user', 'user.username': 'admin.devices.columns.user',
'application.name': 'developer.devices.columns.application', 'application.name': 'admin.devices.columns.application',
'online_status': 'developer.devices.columns.status', 'online_status': 'admin.devices.columns.status',
'online_sessions': 'developer.devices.columns.onlineSessions', 'online_sessions': 'admin.devices.columns.onlineSessions',
'status': 'developer.devices.columns.deviceStatus', 'status': 'admin.devices.columns.deviceStatus',
'created_at': 'developer.devices.columns.createdAt', 'created_at': 'admin.devices.columns.createdAt',
} }
defineExpose({ defineExpose({
@@ -69,13 +69,13 @@ defineExpose({
<div class="space-y-3"> <div class="space-y-3">
<BulkActions :table="table" entity-name="devices"> <BulkActions :table="table" entity-name="devices">
<UiButton variant="outline" size="sm" @click="emit('batchUnban')"> <UiButton variant="outline" size="sm" @click="emit('batchUnban')">
{{ t('developer.devices.batchUnban') }} {{ t('admin.devices.batchUnban') }}
</UiButton> </UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchBan')"> <UiButton variant="outline" size="sm" @click="emit('batchBan')">
{{ t('developer.devices.batchBan') }} {{ t('admin.devices.batchBan') }}
</UiButton> </UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchUnbind')"> <UiButton variant="destructive" size="sm" @click="emit('batchUnbind')">
{{ t('developer.devices.batchUnbind') }} {{ t('admin.devices.batchUnbind') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2"> <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 { useRoute } from 'vue-router'
import { toast } from 'vue-sonner' 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 ConfirmDialog from '@/components/confirm-dialog.vue'
import SingleFilter from '@/components/data-table/single-filter.vue' import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout' 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' import api from '@/services/api'
const { t } = useI18n() const { t } = useI18n()
@@ -80,8 +80,8 @@ const userOptions = computed(() => {
}) })
const statusOptions = computed(() => [ const statusOptions = computed(() => [
{ label: t('developer.devices.normal'), value: 'active' }, { label: t('admin.devices.normal'), value: 'active' },
{ label: t('developer.devices.banned'), value: 'banned' }, { label: t('admin.devices.banned'), value: 'banned' },
]) ])
async function fetchApplications() { async function fetchApplications() {
@@ -138,12 +138,12 @@ async function toggleDeviceStatus(device: Device) {
const newStatus = device.status === 'banned' ? 'active' : 'banned' const newStatus = device.status === 'banned' ? 'active' : 'banned'
try { try {
await api.put(`/dev/devices/${device.id}/status`, { status: newStatus }) await api.put(`/dev/devices/${device.id}/status`, { status: newStatus })
toast.success(t('developer.devices.statusUpdateSuccess')) toast.success(t('admin.devices.statusUpdateSuccess'))
fetchDevices() fetchDevices()
} }
catch (error: any) { catch (error: any) {
console.error('切换设备状态失败:', error) 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 { try {
await api.delete(`/dev/devices/${deleteTarget.value.id}`) await api.delete(`/dev/devices/${deleteTarget.value.id}`)
toast.success(t('developer.devices.unbindSuccess')) toast.success(t('admin.devices.unbindSuccess'))
fetchDevices() fetchDevices()
} }
catch (error: any) { catch (error: any) {
console.error('解绑设备失败:', error) console.error('解绑设备失败:', error)
toast.error(error.message || t('developer.devices.unbindFailed')) toast.error(error.message || t('admin.devices.unbindFailed'))
} }
finally { finally {
deleteTarget.value = null deleteTarget.value = null
@@ -178,12 +178,12 @@ function confirmBatchDelete(ids: (string | number)[]) {
async function handleBatchDelete() { async function handleBatchDelete() {
try { try {
await api.delete('/dev/devices/batch', { device_ids: batchDeleteIds.value.map(id => Number(id)) } as any) 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() fetchDevices()
} }
catch (error: any) { catch (error: any) {
console.error('批量解绑失败:', error) console.error('批量解绑失败:', error)
toast.error(error.message || t('developer.devices.batchUnbindFailed')) toast.error(error.message || t('admin.devices.batchUnbindFailed'))
} }
finally { finally {
batchDeleteIds.value = [] batchDeleteIds.value = []
@@ -193,12 +193,12 @@ async function handleBatchDelete() {
async function batchToggleStatus(ids: (string | number)[], status: string) { async function batchToggleStatus(ids: (string | number)[], status: string) {
try { try {
await api.post('/dev/devices/batch/status', { device_ids: ids.map(id => Number(id)), status }) 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() fetchDevices()
} }
catch (error: any) { catch (error: any) {
console.error('批量更新状态失败:', error) 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> <template>
<BasicPage <BasicPage
:title="t('developer.devices.title')" :title="t('admin.devices.title')"
:description="t('developer.devices.description')" :description="t('admin.devices.description')"
sticky sticky
> >
<div class="space-y-6"> <div class="space-y-6">
@@ -248,7 +248,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.devices.totalDevices') }} {{ t('admin.devices.totalDevices') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:monitor" class="size-4 text-muted-foreground" /> <Icon icon="lucide:monitor" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -262,7 +262,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.devices.onlineSessions') }} {{ t('admin.devices.onlineSessions') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:layers" class="size-4 text-muted-foreground" /> <Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -276,7 +276,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.devices.normalDevices') }} {{ t('admin.devices.normalDevices') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -290,7 +290,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.devices.banned') }} {{ t('admin.devices.banned') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" /> <Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -319,17 +319,17 @@ onMounted(() => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.devices.application')" :title="t('admin.devices.application')"
:options="applicationOptions" :options="applicationOptions"
/> />
<SingleFilter <SingleFilter
v-model="userIdFilter" v-model="userIdFilter"
:title="t('developer.devices.user')" :title="t('admin.devices.user')"
:options="userOptions" :options="userOptions"
/> />
<SingleFilter <SingleFilter
v-model="statusFilter" v-model="statusFilter"
:title="t('developer.devices.status')" :title="t('admin.devices.status')"
:options="statusOptions" :options="statusOptions"
/> />
</template> </template>
@@ -341,30 +341,30 @@ onMounted(() => {
<ConfirmDialog <ConfirmDialog
v-model:open="deleteDialogOpen" v-model:open="deleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.devices.unbind')" :confirm-button-text="t('admin.devices.unbind')"
:cancel-button-text="t('common.cancel')" :cancel-button-text="t('common.cancel')"
@confirm="handleDeleteDevice" @confirm="handleDeleteDevice"
> >
<template #title> <template #title>
{{ t('developer.devices.unbindDevice') }} {{ t('admin.devices.unbindDevice') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.devices.unbindConfirm', { deviceId: deleteTarget?.device_id }) }} {{ t('admin.devices.unbindConfirm', { deviceId: deleteTarget?.device_id }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
v-model:open="batchDeleteDialogOpen" v-model:open="batchDeleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.devices.unbind')" :confirm-button-text="t('admin.devices.unbind')"
:cancel-button-text="t('common.cancel')" :cancel-button-text="t('common.cancel')"
@confirm="handleBatchDelete" @confirm="handleBatchDelete"
> >
<template #title> <template #title>
{{ t('developer.devices.batchUnbind') }} {{ t('admin.devices.batchUnbind') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.devices.batchUnbindConfirm', { count: batchDeleteIds.length }) }} {{ t('admin.devices.batchUnbindConfirm', { count: batchDeleteIds.length }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
+282 -231
View File
@@ -2,49 +2,106 @@
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 { BasicPage } from '@/components/global-layout'
import DataTable from '@/pages/admin/dynamic-code/components/data-table.vue'
import api from '@/services/api' import api from '@/services/api'
const { t } = useI18n()
interface Application { interface Application {
id: number id: number
name: string name: string
} }
const router = useRouter() const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const saving = ref(false)
const loading = ref(true) const loading = ref(true)
const codes = ref<DynamicCode[]>([])
const tableRef = ref()
const applications = ref<Application[]>([]) const applications = ref<Application[]>([])
const codeId = ref<number>(0)
const form = ref({ const appFilter = ref<string>('')
name: '', const statusFilter = ref<string>('')
application_id: '', const createdStartDate = ref<string>('')
description: '', const createdEndDate = ref<string>('')
code: '', const updatedStartDate = ref<string>('')
enabled: true, 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 => ({ return applications.value.map(app => ({
label: app.name, label: app.name,
value: String(app.id), value: String(app.id),
})) }))
}) })
const selectedApplication = computed(() => { const statusOptions = computed(() => [
if (form.value.application_id) { { label: t('admin.cloudFunction.enabled'), value: 'enabled' },
return applications.value.find(app => String(app.id) === form.value.application_id) { label: t('admin.cloudFunction.disabled'), value: 'disabled' },
} ])
return null
})
async function fetchApplications() { async function fetchApplications() {
try { try {
@@ -56,250 +113,244 @@ async function fetchApplications() {
} }
} }
async function fetchCode() { async function fetchCodes() {
loading.value = true loading.value = true
try { try {
const data = await api.get<DynamicCode>(`/dev/dynamic-codes/${codeId.value}`) const data = await api.get<DynamicCode[]>('/dev/dynamic-codes')
if (data) { codes.value = Array.isArray(data) ? 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
}
} }
catch (error) { catch (error) {
console.error('获取云端函数失败:', error) console.error('获取云端函数列表失败:', error)
toast.error(t('developer.cloudFunction.fetchFailed')) codes.value = []
router.push('/developer/cloud-function')
} }
finally { finally {
loading.value = false loading.value = false
} }
} }
async function handleSubmit() { function goToCreate() {
if (!form.value.name.trim()) { router.push('/admin/cloud-function/create')
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
}
saving.value = true async function toggleStatus(code: DynamicCode) {
try { try {
await api.put(`/dev/dynamic-codes/${codeId.value}`, { const newStatus = !code.enabled
name: form.value.name, await api.put(`/dev/dynamic-codes/${code.id}/status`, { enabled: newStatus })
application_id: Number.parseInt(form.value.application_id), toast.success(t('admin.cloudFunction.statusUpdateSuccess'))
description: form.value.description, fetchCodes()
code: form.value.code,
enabled: form.value.enabled,
})
toast.success(t('developer.cloudFunction.updateSuccess'))
router.push('/developer/cloud-function')
} }
catch (error: any) { catch (error: any) {
console.error('更新云端函数失败:', error) console.error('切换云端函数状态失败:', error)
toast.error(error.message || t('developer.cloudFunction.updateFailed')) toast.error(error.message || t('admin.cloudFunction.statusUpdateFailed'))
}
finally {
saving.value = false
} }
} }
function handleCancel() { function editCode(code: DynamicCode) {
router.push('/developer/cloud-function') 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(() => { onMounted(() => {
const id = route.params.id fetchApplications()
if (id && typeof id === 'string') { fetchCodes()
codeId.value = Number.parseInt(id)
fetchApplications()
fetchCode()
}
else {
toast.error('无效的云端函数ID')
router.push('/developer/cloud-function')
}
}) })
</script> </script>
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cloudFunction.edit.title')" :title="t('admin.cloudFunction.title')"
:description="t('developer.cloudFunction.edit.description')" :description="t('admin.cloudFunction.description')"
:breadcrumbs="[
{ title: t('developer.cloudFunction.title'), href: '/developer/cloud-function' },
{ title: t('developer.cloudFunction.edit.title') },
]"
sticky sticky
> >
<div v-if="loading" class="flex items-center justify-center py-12"> <template #actions>
<Icon icon="lucide:loader-2" class="h-8 w-8 animate-spin text-muted-foreground" /> <UiButton size="sm" @click="goToCreate">
</div> <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="space-y-6">
<div class="grid gap-6 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="lg:col-span-2 space-y-6"> <UiCard>
<UiCard> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardHeader> <UiCardTitle class="text-sm font-medium">
<UiCardTitle class="flex items-center gap-2"> {{ t('admin.cloudFunction.totalCodes') }}
<Icon icon="lucide:settings-2" class="size-5" /> </UiCardTitle>
{{ t('developer.cloudFunction.edit.config') }} <Icon icon="lucide:code" class="size-4 text-muted-foreground" />
</UiCardTitle> </UiCardHeader>
<UiCardDescription>{{ t('developer.cloudFunction.edit.configDesc') }}</UiCardDescription> <UiCardContent>
</UiCardHeader> <div class="text-2xl font-bold">
<UiCardContent class="space-y-6"> {{ filteredCodes.length }}
<div class="space-y-2"> </div>
<UiLabel for="application"> </UiCardContent>
{{ t('developer.cloudFunction.create.application') }} </UiCard>
</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-2"> <UiCard>
<UiLabel for="name"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.cloudFunction.create.name') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cloudFunction.enabled') }}
<UiInput </UiCardTitle>
id="name" <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
v-model="form.name" </UiCardHeader>
:placeholder="t('developer.cloudFunction.create.namePlaceholder')" <UiCardContent>
/> <div class="text-2xl font-bold">
</div> {{ enabledCount }}
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2"> <UiCard>
<UiLabel for="description"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.cloudFunction.create.description') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cloudFunction.disabled') }}
<UiTextarea </UiCardTitle>
id="description" <Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
v-model="form.description" </UiCardHeader>
:placeholder="t('developer.cloudFunction.create.descriptionPlaceholder')" <UiCardContent>
rows="3" <div class="text-2xl font-bold">
/> {{ disabledCount }}
</div> </div>
</UiCardContent>
</UiCard>
<div class="space-y-2"> <UiCard>
<UiLabel for="code"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
{{ t('developer.cloudFunction.create.code') }} <UiCardTitle class="text-sm font-medium">
</UiLabel> {{ t('admin.cloudFunction.linkedApps') }}
<UiTextarea </UiCardTitle>
id="code" <Icon icon="lucide:link" class="size-4 text-muted-foreground" />
v-model="form.code" </UiCardHeader>
:placeholder="t('developer.cloudFunction.create.codePlaceholder')" <UiCardContent>
rows="12" <div class="text-2xl font-bold">
class="font-mono text-sm" {{ linkedAppsCount }}
/> </div>
<p class="text-xs text-muted-foreground"> </UiCardContent>
{{ t('developer.cloudFunction.create.codeHelp') }} </UiCard>
</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>
</div> </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> </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> </BasicPage>
</template> </template>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Ban, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next' import { Ban, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next'
import { h } from 'vue' 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 Badge from '@/components/ui/badge/Badge.vue'
import Button from '@/components/ui/button/Button.vue' import Button from '@/components/ui/button/Button.vue'
@@ -24,7 +24,7 @@ export function getColumns(actions: {
return [ return [
{ {
accessorKey: 'name', accessorKey: 'name',
header: () => t('developer.cloudFunction.columns.name'), header: () => t('admin.cloudFunction.columns.name'),
cell: ({ row }) => { cell: ({ row }) => {
const name = row.getValue('name') as string const name = row.getValue('name') as string
return name || '-' return name || '-'
@@ -32,7 +32,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'application_name', accessorKey: 'application_name',
header: () => t('developer.cloudFunction.columns.application'), header: () => t('admin.cloudFunction.columns.application'),
cell: ({ row }) => { cell: ({ row }) => {
const appName = row.getValue('application_name') as string const appName = row.getValue('application_name') as string
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-' return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -40,7 +40,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'description', accessorKey: 'description',
header: () => t('developer.cloudFunction.columns.description'), header: () => t('admin.cloudFunction.columns.description'),
cell: ({ row }) => { cell: ({ row }) => {
const description = row.getValue('description') as string const description = row.getValue('description') as string
return description || '-' return description || '-'
@@ -48,15 +48,15 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'enabled', accessorKey: 'enabled',
header: () => t('developer.cloudFunction.columns.status'), header: () => t('admin.cloudFunction.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const enabled = row.getValue('enabled') as boolean 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', accessorKey: 'creator.username',
header: () => t('developer.cloudFunction.columns.creator'), header: () => t('admin.cloudFunction.columns.creator'),
cell: ({ row }) => { cell: ({ row }) => {
const username = row.original.creator?.username const username = row.original.creator?.username
return username || '-' return username || '-'
@@ -64,7 +64,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'created_at', accessorKey: 'created_at',
header: () => t('developer.cloudFunction.columns.createdAt'), header: () => t('admin.cloudFunction.columns.createdAt'),
cell: ({ row }) => { cell: ({ row }) => {
const createdAt = row.getValue('created_at') const createdAt = row.getValue('created_at')
if (!createdAt) if (!createdAt)
@@ -88,7 +88,7 @@ export function getColumns(actions: {
}, },
{ {
accessorKey: 'updated_at', accessorKey: 'updated_at',
header: () => t('developer.cloudFunction.columns.updatedAt'), header: () => t('admin.cloudFunction.columns.updatedAt'),
cell: ({ row }) => { cell: ({ row }) => {
const updatedAt = row.getValue('updated_at') const updatedAt = row.getValue('updated_at')
if (!updatedAt) if (!updatedAt)
@@ -133,16 +133,16 @@ export function getColumns(actions: {
() => [ () => [
h(DropdownMenuItem, { onClick: () => actions.onEdit(code) }, () => [ h(DropdownMenuItem, { onClick: () => actions.onEdit(code) }, () => [
h(Pencil, { class: 'mr-2 h-4 w-4' }), 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(DropdownMenuItem, { onClick: () => actions.onToggleStatus(code) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }), 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(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(code) }, () => [ h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(code) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }), 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 { computed } from 'vue'
import { useI18n } from 'vue-i18n' 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 { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template> <template>
<div class="flex items-center flex-1 space-x-2"> <div class="flex items-center flex-1 space-x-2">
<Input <Input
:placeholder="t('developer.cloudFunction.searchCode')" :placeholder="t('admin.cloudFunction.searchCode')"
:model-value="(table.getColumn('name')?.getFilterValue() as string) ?? ''" :model-value="(table.getColumn('name')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]" class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('name')?.setFilterValue($event.target.value)" @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" class="h-8 px-2 lg:px-3"
@click="table.resetColumnFilters()" @click="table.resetColumnFilters()"
> >
{{ t('developer.cloudFunction.reset') }} {{ t('admin.cloudFunction.reset') }}
<X class="size-4 ml-2" /> <X class="size-4 ml-2" />
</Button> </Button>
</div> </div>
@@ -45,14 +45,14 @@ const table = generateVueTable<DynamicCode>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.cloudFunction.select', select: 'admin.cloudFunction.select',
name: 'developer.cloudFunction.columns.name', name: 'admin.cloudFunction.columns.name',
application_name: 'developer.cloudFunction.columns.application', application_name: 'admin.cloudFunction.columns.application',
description: 'developer.cloudFunction.columns.description', description: 'admin.cloudFunction.columns.description',
enabled: 'developer.cloudFunction.columns.status', enabled: 'admin.cloudFunction.columns.status',
creator_username: 'developer.cloudFunction.columns.creator', creator_username: 'admin.cloudFunction.columns.creator',
created_at: 'developer.cloudFunction.columns.createdAt', created_at: 'admin.cloudFunction.columns.createdAt',
updated_at: 'developer.cloudFunction.columns.updatedAt', updated_at: 'admin.cloudFunction.columns.updatedAt',
} }
defineExpose({ defineExpose({
@@ -69,13 +69,13 @@ defineExpose({
</div> </div>
<BulkActions :table="table" entity-name="functions"> <BulkActions :table="table" entity-name="functions">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')"> <UiButton variant="outline" size="sm" @click="emit('batchEnable')">
{{ t('developer.cloudFunction.batchEnableBtn') }} {{ t('admin.cloudFunction.batchEnableBtn') }}
</UiButton> </UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')"> <UiButton variant="outline" size="sm" @click="emit('batchDisable')">
{{ t('developer.cloudFunction.batchDisableBtn') }} {{ t('admin.cloudFunction.batchDisableBtn') }}
</UiButton> </UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')"> <UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.cloudFunction.batchDeleteBtn') }} {{ t('admin.cloudFunction.batchDeleteBtn') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
@@ -53,15 +53,15 @@ async function fetchApplications() {
async function handleSubmit() { async function handleSubmit() {
if (!form.value.name.trim()) { if (!form.value.name.trim()) {
toast.error(t('developer.cloudFunction.create.nameRequired')) toast.error(t('admin.cloudFunction.create.nameRequired'))
return return
} }
if (!form.value.application_id) { if (!form.value.application_id) {
toast.error(t('developer.cloudFunction.create.selectAppRequired')) toast.error(t('admin.cloudFunction.create.selectAppRequired'))
return return
} }
if (!form.value.code.trim()) { if (!form.value.code.trim()) {
toast.error(t('developer.cloudFunction.create.codeRequired')) toast.error(t('admin.cloudFunction.create.codeRequired'))
return return
} }
@@ -74,12 +74,12 @@ async function handleSubmit() {
code: form.value.code, code: form.value.code,
enabled: form.value.enabled, enabled: form.value.enabled,
}) })
toast.success(t('developer.cloudFunction.create.submitSuccess')) toast.success(t('admin.cloudFunction.create.submitSuccess'))
router.push('/developer/cloud-function') router.push('/admin/cloud-function')
} }
catch (error: any) { catch (error: any) {
console.error('创建云端函数失败:', error) console.error('创建云端函数失败:', error)
toast.error(error.message || t('developer.cloudFunction.create.submitFailed')) toast.error(error.message || t('admin.cloudFunction.create.submitFailed'))
} }
finally { finally {
saving.value = false saving.value = false
@@ -87,7 +87,7 @@ async function handleSubmit() {
} }
function handleCancel() { function handleCancel() {
router.push('/developer/cloud-function') router.push('/admin/cloud-function')
} }
onMounted(() => { onMounted(() => {
@@ -97,11 +97,11 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.cloudFunction.create.title')" :title="t('admin.cloudFunction.create.title')"
:description="t('developer.cloudFunction.create.description')" :description="t('admin.cloudFunction.create.description')"
:breadcrumbs="[ :breadcrumbs="[
{ title: t('developer.cloudFunction.title'), href: '/developer/cloud-function' }, { title: t('admin.cloudFunction.title'), href: '/admin/cloud-function' },
{ title: t('developer.cloudFunction.create.title') }, { title: t('admin.cloudFunction.create.title') },
]" ]"
sticky sticky
> >
@@ -112,18 +112,18 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" /> <Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.cloudFunction.create.config') }} {{ t('admin.cloudFunction.create.config') }}
</UiCardTitle> </UiCardTitle>
<UiCardDescription>{{ t('developer.cloudFunction.create.configDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.cloudFunction.create.configDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="application"> <UiLabel for="application">
{{ t('developer.cloudFunction.create.application') }} {{ t('admin.cloudFunction.create.application') }}
</UiLabel> </UiLabel>
<UiSelect v-model="form.application_id"> <UiSelect v-model="form.application_id">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.cloudFunction.create.selectAppPlaceholder')" /> <UiSelectValue :placeholder="t('admin.cloudFunction.create.selectAppPlaceholder')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem <UiSelectItem
@@ -139,47 +139,47 @@ onMounted(() => {
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="name"> <UiLabel for="name">
{{ t('developer.cloudFunction.create.name') }} {{ t('admin.cloudFunction.create.name') }}
</UiLabel> </UiLabel>
<UiInput <UiInput
id="name" id="name"
v-model="form.name" v-model="form.name"
:placeholder="t('developer.cloudFunction.create.namePlaceholder')" :placeholder="t('admin.cloudFunction.create.namePlaceholder')"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="description"> <UiLabel for="description">
{{ t('developer.cloudFunction.create.description') }} {{ t('admin.cloudFunction.create.description') }}
</UiLabel> </UiLabel>
<UiTextarea <UiTextarea
id="description" id="description"
v-model="form.description" v-model="form.description"
:placeholder="t('developer.cloudFunction.create.descriptionPlaceholder')" :placeholder="t('admin.cloudFunction.create.descriptionPlaceholder')"
rows="3" rows="3"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="code"> <UiLabel for="code">
{{ t('developer.cloudFunction.create.code') }} {{ t('admin.cloudFunction.create.code') }}
</UiLabel> </UiLabel>
<UiTextarea <UiTextarea
id="code" id="code"
v-model="form.code" v-model="form.code"
:placeholder="t('developer.cloudFunction.create.codePlaceholder')" :placeholder="t('admin.cloudFunction.create.codePlaceholder')"
rows="12" rows="12"
class="font-mono text-sm" class="font-mono text-sm"
/> />
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
{{ t('developer.cloudFunction.create.codeHelp') }} {{ t('admin.cloudFunction.create.codeHelp') }}
</p> </p>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="font-medium"> <p class="font-medium">
{{ t('developer.cloudFunction.create.enabled') }} {{ t('admin.cloudFunction.create.enabled') }}
</p> </p>
</div> </div>
<UiSwitch v-model="form.enabled" /> <UiSwitch v-model="form.enabled" />
@@ -193,25 +193,25 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" /> <Icon icon="lucide:eye" class="size-5" />
{{ t('developer.cloudFunction.create.preview') }} {{ t('admin.cloudFunction.create.preview') }}
</UiCardTitle> </UiCardTitle>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between text-sm"> <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> <span>{{ form.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ selectedApplication?.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span class="text-right max-w-[150px] truncate">{{ form.description || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm items-center"> <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 <UiSwitch
v-model="form.enabled" v-model="form.enabled"
/> />
@@ -219,7 +219,7 @@ onMounted(() => {
<div class="border-t pt-3 mt-3"> <div class="border-t pt-3 mt-3">
<div class="text-sm"> <div class="text-sm">
<div class="text-muted-foreground mb-2"> <div class="text-muted-foreground mb-2">
{{ t('developer.cloudFunction.create.code') }} {{ t('admin.cloudFunction.create.code') }}
</div> </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 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>
@@ -239,14 +239,14 @@ onMounted(() => {
> >
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" /> <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" /> <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>
<UiButton <UiButton
variant="outline" variant="outline"
class="w-full" class="w-full"
@click="handleCancel" @click="handleCancel"
> >
{{ t('developer.cloudFunction.create.cancelBtn') }} {{ t('admin.cloudFunction.create.cancelBtn') }}
</UiButton> </UiButton>
</div> </div>
</UiCardContent> </UiCardContent>
+33 -33
View File
@@ -5,13 +5,13 @@ import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 ConfirmDialog from '@/components/confirm-dialog.vue'
import DateRangeFilter from '@/components/data-table/date-range-filter.vue' import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
import SingleFilter from '@/components/data-table/single-filter.vue' import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout' 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' import api from '@/services/api'
const { t } = useI18n() const { t } = useI18n()
@@ -99,8 +99,8 @@ const applicationOptions = computed(() => {
}) })
const statusOptions = computed(() => [ const statusOptions = computed(() => [
{ label: t('developer.cloudFunction.enabled'), value: 'enabled' }, { label: t('admin.cloudFunction.enabled'), value: 'enabled' },
{ label: t('developer.cloudFunction.disabled'), value: 'disabled' }, { label: t('admin.cloudFunction.disabled'), value: 'disabled' },
]) ])
async function fetchApplications() { async function fetchApplications() {
@@ -129,24 +129,24 @@ async function fetchCodes() {
} }
function goToCreate() { function goToCreate() {
router.push('/developer/cloud-function/create') router.push('/admin/cloud-function/create')
} }
async function toggleStatus(code: DynamicCode) { async function toggleStatus(code: DynamicCode) {
try { try {
const newStatus = !code.enabled const newStatus = !code.enabled
await api.put(`/dev/dynamic-codes/${code.id}/status`, { enabled: newStatus }) await api.put(`/dev/dynamic-codes/${code.id}/status`, { enabled: newStatus })
toast.success(t('developer.cloudFunction.statusUpdateSuccess')) toast.success(t('admin.cloudFunction.statusUpdateSuccess'))
fetchCodes() fetchCodes()
} }
catch (error: any) { catch (error: any) {
console.error('切换云端函数状态失败:', error) console.error('切换云端函数状态失败:', error)
toast.error(error.message || t('developer.cloudFunction.statusUpdateFailed')) toast.error(error.message || t('admin.cloudFunction.statusUpdateFailed'))
} }
} }
function editCode(code: DynamicCode) { function editCode(code: DynamicCode) {
router.push(`/developer/cloud-function/${code.id}`) router.push(`/admin/cloud-function/${code.id}`)
} }
function confirmDeleteCode(code: DynamicCode) { function confirmDeleteCode(code: DynamicCode) {
@@ -160,12 +160,12 @@ async function handleDeleteCode() {
try { try {
await api.delete(`/dev/dynamic-codes/${deleteTarget.value.id}`) await api.delete(`/dev/dynamic-codes/${deleteTarget.value.id}`)
toast.success(t('developer.cloudFunction.deleteSuccess')) toast.success(t('admin.cloudFunction.deleteSuccess'))
fetchCodes() fetchCodes()
} }
catch (error: any) { catch (error: any) {
console.error('删除云端函数失败:', error) console.error('删除云端函数失败:', error)
toast.error(error.message || t('developer.cloudFunction.deleteFailed')) toast.error(error.message || t('admin.cloudFunction.deleteFailed'))
} }
finally { finally {
deleteTarget.value = null deleteTarget.value = null
@@ -180,12 +180,12 @@ function confirmBatchDelete(ids: (string | number)[]) {
async function handleBatchDelete() { async function handleBatchDelete() {
try { try {
await api.delete('/dev/dynamic-codes/batch', { ids: batchDeleteIds.value } as any) 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() fetchCodes()
} }
catch (error: any) { catch (error: any) {
console.error('批量删除失败:', error) console.error('批量删除失败:', error)
toast.error(error.message || t('developer.cloudFunction.deleteFailed')) toast.error(error.message || t('admin.cloudFunction.deleteFailed'))
} }
finally { finally {
batchDeleteIds.value = [] batchDeleteIds.value = []
@@ -195,12 +195,12 @@ async function handleBatchDelete() {
async function batchToggleStatus(ids: (string | number)[], enabled: boolean) { async function batchToggleStatus(ids: (string | number)[], enabled: boolean) {
try { try {
await api.put('/dev/dynamic-codes/batch/status', { ids, enabled }) await api.put('/dev/dynamic-codes/batch/status', { ids, enabled })
toast.success(t('developer.cloudFunction.batchUpdateSuccess')) toast.success(t('admin.cloudFunction.batchUpdateSuccess'))
fetchCodes() fetchCodes()
} }
catch (error: any) { catch (error: any) {
console.error('批量更新状态失败:', error) 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> <template>
<BasicPage <BasicPage
:title="t('developer.cloudFunction.title')" :title="t('admin.cloudFunction.title')"
:description="t('developer.cloudFunction.description')" :description="t('admin.cloudFunction.description')"
sticky sticky
> >
<template #actions> <template #actions>
<UiButton size="sm" @click="goToCreate"> <UiButton size="sm" @click="goToCreate">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.cloudFunction.createBtn') }} {{ t('admin.cloudFunction.createBtn') }}
</UiButton> </UiButton>
</template> </template>
@@ -228,7 +228,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudFunction.totalCodes') }} {{ t('admin.cloudFunction.totalCodes') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:code" class="size-4 text-muted-foreground" /> <Icon icon="lucide:code" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -242,7 +242,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudFunction.enabled') }} {{ t('admin.cloudFunction.enabled') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -256,7 +256,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudFunction.disabled') }} {{ t('admin.cloudFunction.disabled') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" /> <Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -270,7 +270,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.cloudFunction.linkedApps') }} {{ t('admin.cloudFunction.linkedApps') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:link" class="size-4 text-muted-foreground" /> <Icon icon="lucide:link" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -299,23 +299,23 @@ onMounted(() => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.cloudFunction.application')" :title="t('admin.cloudFunction.application')"
:options="applicationOptions" :options="applicationOptions"
/> />
<SingleFilter <SingleFilter
v-model="statusFilter" v-model="statusFilter"
:title="t('developer.cloudFunction.status')" :title="t('admin.cloudFunction.status')"
:options="statusOptions" :options="statusOptions"
/> />
<DateRangeFilter <DateRangeFilter
v-model:start-model-value="createdStartDate" v-model:start-model-value="createdStartDate"
v-model:end-model-value="createdEndDate" v-model:end-model-value="createdEndDate"
:title="t('developer.cloudFunction.columns.createdAt')" :title="t('admin.cloudFunction.columns.createdAt')"
/> />
<DateRangeFilter <DateRangeFilter
v-model:start-model-value="updatedStartDate" v-model:start-model-value="updatedStartDate"
v-model:end-model-value="updatedEndDate" v-model:end-model-value="updatedEndDate"
:title="t('developer.cloudFunction.columns.updatedAt')" :title="t('admin.cloudFunction.columns.updatedAt')"
/> />
</template> </template>
</DataTable> </DataTable>
@@ -326,30 +326,30 @@ onMounted(() => {
<ConfirmDialog <ConfirmDialog
v-model:open="deleteDialogOpen" v-model:open="deleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.cloudFunction.delete')" :confirm-button-text="t('admin.cloudFunction.delete')"
:cancel-button-text="t('developer.cloudFunction.create.cancelBtn')" :cancel-button-text="t('admin.cloudFunction.create.cancelBtn')"
@confirm="handleDeleteCode" @confirm="handleDeleteCode"
> >
<template #title> <template #title>
{{ t('developer.cloudFunction.deleteCode') }} {{ t('admin.cloudFunction.deleteCode') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.cloudFunction.deleteCodeConfirm', { codeName: deleteTarget?.name }) }} {{ t('admin.cloudFunction.deleteCodeConfirm', { codeName: deleteTarget?.name }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
v-model:open="batchDeleteDialogOpen" v-model:open="batchDeleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.cloudFunction.delete')" :confirm-button-text="t('admin.cloudFunction.delete')"
:cancel-button-text="t('developer.cloudFunction.create.cancelBtn')" :cancel-button-text="t('admin.cloudFunction.create.cancelBtn')"
@confirm="handleBatchDelete" @confirm="handleBatchDelete"
> >
<template #title> <template #title>
{{ t('developer.cloudFunction.batchDelete') }} {{ t('admin.cloudFunction.batchDelete') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.cloudFunction.batchDeleteConfirm', { count: batchDeleteIds.length }) }} {{ t('admin.cloudFunction.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
</BasicPage> </BasicPage>
@@ -28,34 +28,34 @@ const form = ref({
const permissionGroups = computed(() => [ const permissionGroups = computed(() => [
{ {
name: t('developer.extension.apiKeys.permissions.groups.user'), name: t('admin.extension.apiKeys.permissions.groups.user'),
permissions: [ permissions: [
{ value: 'user.read', label: t('developer.extension.apiKeys.permissions.userRead'), description: t('developer.extension.apiKeys.permissions.userReadDesc') }, { value: 'user.read', label: t('admin.extension.apiKeys.permissions.userRead'), description: t('admin.extension.apiKeys.permissions.userReadDesc') },
{ value: 'user.list', label: t('developer.extension.apiKeys.permissions.userList'), description: t('developer.extension.apiKeys.permissions.userListDesc') }, { value: 'user.list', label: t('admin.extension.apiKeys.permissions.userList'), description: t('admin.extension.apiKeys.permissions.userListDesc') },
{ value: 'user.recharge', label: t('developer.extension.apiKeys.permissions.userRecharge'), description: t('developer.extension.apiKeys.permissions.userRechargeDesc') }, { value: 'user.recharge', label: t('admin.extension.apiKeys.permissions.userRecharge'), description: t('admin.extension.apiKeys.permissions.userRechargeDesc') },
{ value: 'user.deduct', label: t('developer.extension.apiKeys.permissions.userDeduct'), description: t('developer.extension.apiKeys.permissions.userDeductDesc') }, { value: 'user.deduct', label: t('admin.extension.apiKeys.permissions.userDeduct'), description: t('admin.extension.apiKeys.permissions.userDeductDesc') },
{ value: 'user.variables', label: t('developer.extension.apiKeys.permissions.userVariables'), description: t('developer.extension.apiKeys.permissions.userVariablesDesc') }, { 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: [ permissions: [
{ value: 'card.read', label: t('developer.extension.apiKeys.permissions.cardRead'), description: t('developer.extension.apiKeys.permissions.cardReadDesc') }, { value: 'card.read', label: t('admin.extension.apiKeys.permissions.cardRead'), description: t('admin.extension.apiKeys.permissions.cardReadDesc') },
{ value: 'card.generate', label: t('developer.extension.apiKeys.permissions.cardGenerate'), description: t('developer.extension.apiKeys.permissions.cardGenerateDesc') }, { 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: [ 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: [ permissions: [
{ value: 'app.info', label: t('developer.extension.apiKeys.permissions.appInfo'), description: t('developer.extension.apiKeys.permissions.appInfoDesc') }, { value: 'app.info', label: t('admin.extension.apiKeys.permissions.appInfo'), description: t('admin.extension.apiKeys.permissions.appInfoDesc') },
{ value: 'app.stats', label: t('developer.extension.apiKeys.permissions.appStats'), description: t('developer.extension.apiKeys.permissions.appStatsDesc') }, { value: 'app.stats', label: t('admin.extension.apiKeys.permissions.appStats'), description: t('admin.extension.apiKeys.permissions.appStatsDesc') },
{ value: 'app.variables', label: t('developer.extension.apiKeys.permissions.appVariables'), description: t('developer.extension.apiKeys.permissions.appVariablesDesc') }, { 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() { async function handleSubmit() {
if (!form.value.name) { if (!form.value.name) {
toast.error(t('developer.extension.apiKeys.create.nameRequired')) toast.error(t('admin.extension.apiKeys.create.nameRequired'))
return return
} }
if (!form.value.application_id) { if (!form.value.application_id) {
toast.error(t('developer.extension.apiKeys.create.selectAppRequired')) toast.error(t('admin.extension.apiKeys.create.selectAppRequired'))
return return
} }
if (form.value.permissions.length === 0) { if (form.value.permissions.length === 0) {
toast.error(t('developer.extension.apiKeys.create.permissionsRequired')) toast.error(t('admin.extension.apiKeys.create.permissionsRequired'))
return return
} }
@@ -133,12 +133,12 @@ async function handleSubmit() {
permissions: form.value.permissions, permissions: form.value.permissions,
enabled: form.value.enabled, enabled: form.value.enabled,
}) })
toast.success(t('developer.extension.apiKeys.create.submitSuccess')) toast.success(t('admin.extension.apiKeys.create.submitSuccess'))
router.push('/developer/extension') router.push('/admin/extension')
} }
catch (error: any) { catch (error: any) {
console.error('创建API密钥失败:', error) 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 { finally {
saving.value = false saving.value = false
@@ -152,11 +152,11 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.extension.apiKeys.create.title')" :title="t('admin.extension.apiKeys.create.title')"
:description="t('developer.extension.apiKeys.create.description')" :description="t('admin.extension.apiKeys.create.description')"
:breadcrumbs="[ :breadcrumbs="[
{ title: t('developer.extension.title'), href: '/developer/extension' }, { title: t('admin.extension.title'), href: '/admin/extension' },
{ title: t('developer.extension.apiKeys.create.title') }, { title: t('admin.extension.apiKeys.create.title') },
]" ]"
sticky sticky
> >
@@ -167,29 +167,29 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" /> <Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.extension.apiKeys.create.title') }} {{ t('admin.extension.apiKeys.create.title') }}
</UiCardTitle> </UiCardTitle>
<UiCardDescription>{{ t('developer.extension.apiKeys.create.description') }}</UiCardDescription> <UiCardDescription>{{ t('admin.extension.apiKeys.create.description') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="name"> <UiLabel for="name">
{{ t('developer.extension.apiKeys.create.name') }} * {{ t('admin.extension.apiKeys.create.name') }} *
</UiLabel> </UiLabel>
<UiInput <UiInput
id="name" id="name"
v-model="form.name" v-model="form.name"
:placeholder="t('developer.extension.apiKeys.create.namePlaceholder')" :placeholder="t('admin.extension.apiKeys.create.namePlaceholder')"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="application"> <UiLabel for="application">
{{ t('developer.extension.apiKeys.create.application') }} * {{ t('admin.extension.apiKeys.create.application') }} *
</UiLabel> </UiLabel>
<UiSelect v-model="form.application_id"> <UiSelect v-model="form.application_id">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.extension.apiKeys.create.selectAppPlaceholder')" /> <UiSelectValue :placeholder="t('admin.extension.apiKeys.create.selectAppPlaceholder')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem v-for="app in filteredApplications" :key="app.value" :value="app.value"> <UiSelectItem v-for="app in filteredApplications" :key="app.value" :value="app.value">
@@ -201,13 +201,13 @@ onMounted(() => {
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center justify-between"> <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"> <div class="flex gap-2">
<UiButton variant="outline" size="sm" @click="form.permissions = permissionGroups.flatMap(g => g.permissions.map(p => p.value))"> <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>
<UiButton variant="outline" size="sm" @click="form.permissions = []"> <UiButton variant="outline" size="sm" @click="form.permissions = []">
{{ t('developer.extension.apiKeys.create.deselectAll') }} {{ t('admin.extension.apiKeys.create.deselectAll') }}
</UiButton> </UiButton>
</div> </div>
</div> </div>
@@ -219,10 +219,10 @@ onMounted(() => {
</h4> </h4>
<div class="flex gap-1"> <div class="flex gap-1">
<UiButton variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="selectAllInGroup(group.permissions)"> <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>
<UiButton variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="deselectAllInGroup(group.permissions)"> <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> </UiButton>
</div> </div>
</div> </div>
@@ -255,7 +255,7 @@ onMounted(() => {
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<UiSwitch id="enabled" v-model:checked="form.enabled" /> <UiSwitch id="enabled" v-model:checked="form.enabled" />
<UiLabel for="enabled"> <UiLabel for="enabled">
{{ t('developer.extension.apiKeys.create.enabled') }} {{ t('admin.extension.apiKeys.create.enabled') }}
</UiLabel> </UiLabel>
</div> </div>
</UiCardContent> </UiCardContent>
@@ -267,22 +267,22 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" /> <Icon icon="lucide:info" class="size-5" />
{{ t('developer.extension.apiKeys.create.preview') || '预览' }} {{ t('admin.extension.apiKeys.create.preview') || '预览' }}
</UiCardTitle> </UiCardTitle>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between text-sm"> <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> <span>{{ form.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ selectedApplication?.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.extension.apiKeys.create.permissions') }}</span> <span class="text-muted-foreground">{{ t('admin.extension.apiKeys.create.permissions') }}</span>
<span>{{ form.permissions.length }} {{ t('developer.extension.apiKeys.create.selected') || '已选择' }}</span> <span>{{ form.permissions.length }} {{ t('admin.extension.apiKeys.create.selected') || '已选择' }}</span>
</div> </div>
</div> </div>
</UiCardContent> </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-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" /> <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>
<UiButton <UiButton
variant="outline" variant="outline"
class="w-full" class="w-full"
@click="router.back()" @click="router.back()"
> >
{{ t('developer.extension.apiKeys.create.cancelBtn') }} {{ t('admin.extension.apiKeys.create.cancelBtn') }}
</UiButton> </UiButton>
</div> </div>
</UiCardContent> </UiCardContent>
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import { Ban, MoreHorizontal, RefreshCw, Trash2 } from 'lucide-vue-next' import { Ban, MoreHorizontal, RefreshCw, Trash2 } from 'lucide-vue-next'
import { h } from 'vue' 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 { Copy } from '@/components/sva-ui/copy'
import Badge from '@/components/ui/badge/Badge.vue' import Badge from '@/components/ui/badge/Badge.vue'
@@ -25,7 +25,7 @@ export function getAPIKeyColumns(actions: {
return [ return [
{ {
accessorKey: 'name', accessorKey: 'name',
header: () => t('developer.extension.apiKeys.columns.name'), header: () => t('admin.extension.apiKeys.columns.name'),
cell: ({ row }) => { cell: ({ row }) => {
const name = row.getValue('name') as string const name = row.getValue('name') as string
return name || '-' return name || '-'
@@ -33,7 +33,7 @@ export function getAPIKeyColumns(actions: {
}, },
{ {
accessorKey: 'application_name', accessorKey: 'application_name',
header: () => t('developer.extension.apiKeys.columns.application'), header: () => t('admin.extension.apiKeys.columns.application'),
cell: ({ row }) => { cell: ({ row }) => {
const appName = row.getValue('application_name') as string const appName = row.getValue('application_name') as string
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-' return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -41,7 +41,7 @@ export function getAPIKeyColumns(actions: {
}, },
{ {
accessorKey: 'access_key', accessorKey: 'access_key',
header: () => t('developer.extension.apiKeys.columns.accessKey'), header: () => t('admin.extension.apiKeys.columns.accessKey'),
cell: ({ row }) => { cell: ({ row }) => {
const accessKey = row.getValue('access_key') as string const accessKey = row.getValue('access_key') as string
return accessKey return accessKey
@@ -54,7 +54,7 @@ export function getAPIKeyColumns(actions: {
}, },
{ {
accessorKey: 'secret_key', accessorKey: 'secret_key',
header: () => t('developer.extension.apiKeys.columns.secretKey'), header: () => t('admin.extension.apiKeys.columns.secretKey'),
cell: ({ row }) => { cell: ({ row }) => {
const secretKey = row.getValue('secret_key') as string const secretKey = row.getValue('secret_key') as string
return secretKey ? h('code', { class: 'text-xs bg-muted px-2 py-1 rounded font-mono' }, '••••••••••••••••') : '-' 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', accessorKey: 'permissions',
header: () => t('developer.extension.apiKeys.columns.permissions'), header: () => t('admin.extension.apiKeys.columns.permissions'),
cell: ({ row }) => { cell: ({ row }) => {
const permissions = row.getValue('permissions') as string const permissions = row.getValue('permissions') as string
try { try {
@@ -78,16 +78,16 @@ export function getAPIKeyColumns(actions: {
}, },
{ {
accessorKey: 'status', accessorKey: 'status',
header: () => t('developer.extension.apiKeys.columns.status'), header: () => t('admin.extension.apiKeys.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const status = row.getValue('status') as string const status = row.getValue('status') as string
return h(Badge, { variant: status === 'active' ? 'default' : 'secondary' }, () => 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', accessorKey: 'created_at',
header: () => t('developer.extension.apiKeys.columns.createdAt'), header: () => t('admin.extension.apiKeys.columns.createdAt'),
cell: ({ row }) => { cell: ({ row }) => {
const createdAt = row.getValue('created_at') const createdAt = row.getValue('created_at')
if (!createdAt) if (!createdAt)
@@ -132,16 +132,16 @@ export function getAPIKeyColumns(actions: {
() => [ () => [
h(DropdownMenuItem, { onClick: () => actions.onRegenerate(apiKey) }, () => [ h(DropdownMenuItem, { onClick: () => actions.onRegenerate(apiKey) }, () => [
h(RefreshCw, { class: 'mr-2 h-4 w-4' }), 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(DropdownMenuItem, { onClick: () => actions.onToggleStatus(apiKey) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }), 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(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(apiKey) }, () => [ h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(apiKey) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }), 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> <template>
<div class="flex items-center flex-1 space-x-2"> <div class="flex items-center flex-1 space-x-2">
<Input <Input
:placeholder="t('developer.extension.apiKeys.searchAPIKey')" :placeholder="t('admin.extension.apiKeys.searchAPIKey')"
:model-value="searchFilter" :model-value="searchFilter"
class="h-8 w-[150px] lg:w-[250px]" class="h-8 w-[150px] lg:w-[250px]"
@update:model-value="emit('update:searchFilter', $event as string)" @update:model-value="emit('update:searchFilter', $event as string)"
@@ -45,7 +45,7 @@ function resetFilters() {
class="h-8 px-2 lg:px-3" class="h-8 px-2 lg:px-3"
@click="resetFilters" @click="resetFilters"
> >
{{ t('developer.extension.apiKeys.reset') }} {{ t('admin.extension.apiKeys.reset') }}
<X class="size-4 ml-2" /> <X class="size-4 ml-2" />
</Button> </Button>
</div> </div>
@@ -48,14 +48,14 @@ const table = generateVueTable<APIKey>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.extension.apiKeys.select', select: 'admin.extension.apiKeys.select',
name: 'developer.extension.apiKeys.columns.name', name: 'admin.extension.apiKeys.columns.name',
application_name: 'developer.extension.apiKeys.columns.application', application_name: 'admin.extension.apiKeys.columns.application',
access_key: 'developer.extension.apiKeys.columns.accessKey', access_key: 'admin.extension.apiKeys.columns.accessKey',
secret_key: 'developer.extension.apiKeys.columns.secretKey', secret_key: 'admin.extension.apiKeys.columns.secretKey',
permissions: 'developer.extension.apiKeys.columns.permissions', permissions: 'admin.extension.apiKeys.columns.permissions',
status: 'developer.extension.apiKeys.columns.status', status: 'admin.extension.apiKeys.columns.status',
created_at: 'developer.extension.apiKeys.columns.createdAt', created_at: 'admin.extension.apiKeys.columns.createdAt',
} }
defineExpose({ defineExpose({
@@ -69,13 +69,13 @@ defineExpose({
<div class="space-y-3"> <div class="space-y-3">
<BulkActions :table="table" entity-name="apiKeys"> <BulkActions :table="table" entity-name="apiKeys">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')"> <UiButton variant="outline" size="sm" @click="emit('batchEnable')">
{{ t('developer.extension.apiKeys.enable') }} {{ t('admin.extension.apiKeys.enable') }}
</UiButton> </UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')"> <UiButton variant="outline" size="sm" @click="emit('batchDisable')">
{{ t('developer.extension.apiKeys.disable') }} {{ t('admin.extension.apiKeys.disable') }}
</UiButton> </UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')"> <UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.extension.apiKeys.batchDelete') }} {{ t('admin.extension.apiKeys.batchDelete') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
@@ -25,7 +25,7 @@ export function getWebhookColumns(actions: {
return [ return [
{ {
accessorKey: 'name', accessorKey: 'name',
header: () => t('developer.extension.webhooks.columns.name'), header: () => t('admin.extension.webhooks.columns.name'),
cell: ({ row }) => { cell: ({ row }) => {
const name = row.getValue('name') as string const name = row.getValue('name') as string
return name || '-' return name || '-'
@@ -33,7 +33,7 @@ export function getWebhookColumns(actions: {
}, },
{ {
accessorKey: 'url', accessorKey: 'url',
header: () => t('developer.extension.webhooks.columns.url'), header: () => t('admin.extension.webhooks.columns.url'),
cell: ({ row }) => { cell: ({ row }) => {
const url = row.getValue('url') as string 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) : '-' 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', accessorKey: 'application_name',
header: () => t('developer.extension.webhooks.columns.application'), header: () => t('admin.extension.webhooks.columns.application'),
cell: ({ row }) => { cell: ({ row }) => {
const appName = row.getValue('application_name') as string const appName = row.getValue('application_name') as string
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-' return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
@@ -49,7 +49,7 @@ export function getWebhookColumns(actions: {
}, },
{ {
accessorKey: 'events', accessorKey: 'events',
header: () => t('developer.extension.webhooks.columns.events'), header: () => t('admin.extension.webhooks.columns.events'),
cell: ({ row }) => { cell: ({ row }) => {
const events = row.getValue('events') as string const events = row.getValue('events') as string
try { try {
@@ -65,16 +65,16 @@ export function getWebhookColumns(actions: {
}, },
{ {
accessorKey: 'status', accessorKey: 'status',
header: () => t('developer.extension.webhooks.columns.status'), header: () => t('admin.extension.webhooks.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const status = row.getValue('status') as string const status = row.getValue('status') as string
return h(Badge, { variant: status === 'active' ? 'default' : 'secondary' }, () => 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', accessorKey: 'retry_count',
header: () => t('developer.extension.webhooks.columns.retryCount'), header: () => t('admin.extension.webhooks.columns.retryCount'),
cell: ({ row }) => { cell: ({ row }) => {
const retryCount = row.getValue('retry_count') as number const retryCount = row.getValue('retry_count') as number
return retryCount ?? '-' return retryCount ?? '-'
@@ -82,7 +82,7 @@ export function getWebhookColumns(actions: {
}, },
{ {
accessorKey: 'timeout', accessorKey: 'timeout',
header: () => t('developer.extension.webhooks.columns.timeout'), header: () => t('admin.extension.webhooks.columns.timeout'),
cell: ({ row }) => { cell: ({ row }) => {
const timeout = row.getValue('timeout') as number const timeout = row.getValue('timeout') as number
return timeout ? `${timeout}s` : '-' return timeout ? `${timeout}s` : '-'
@@ -90,7 +90,7 @@ export function getWebhookColumns(actions: {
}, },
{ {
accessorKey: 'created_at', accessorKey: 'created_at',
header: () => t('developer.extension.webhooks.columns.createdAt'), header: () => t('admin.extension.webhooks.columns.createdAt'),
cell: ({ row }) => { cell: ({ row }) => {
const createdAt = row.getValue('created_at') const createdAt = row.getValue('created_at')
if (!createdAt) if (!createdAt)
@@ -135,22 +135,22 @@ export function getWebhookColumns(actions: {
() => [ () => [
h(DropdownMenuItem, { onClick: () => actions.onTest(webhook) }, () => [ h(DropdownMenuItem, { onClick: () => actions.onTest(webhook) }, () => [
h(Send, { class: 'mr-2 h-4 w-4' }), h(Send, { class: 'mr-2 h-4 w-4' }),
t('developer.extension.webhooks.test'), t('admin.extension.webhooks.test'),
]), ]),
actions.onViewLogs actions.onViewLogs
? h(DropdownMenuItem, { onClick: () => actions.onViewLogs?.(webhook) }, () => [ ? h(DropdownMenuItem, { onClick: () => actions.onViewLogs?.(webhook) }, () => [
h(Eye, { class: 'mr-2 h-4 w-4' }), h(Eye, { class: 'mr-2 h-4 w-4' }),
t('developer.extension.webhooks.viewLogs'), t('admin.extension.webhooks.viewLogs'),
]) ])
: null, : null,
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(webhook) }, () => [ h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(webhook) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }), 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(DropdownMenuSeparator),
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(webhook) }, () => [ h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(webhook) }, () => [
h(Trash2, { class: 'mr-2 h-4 w-4' }), 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> <template>
<div class="flex items-center flex-1 space-x-2"> <div class="flex items-center flex-1 space-x-2">
<Input <Input
:placeholder="t('developer.extension.webhooks.searchWebhook')" :placeholder="t('admin.extension.webhooks.searchWebhook')"
:model-value="searchFilter" :model-value="searchFilter"
class="h-8 w-[150px] lg:w-[250px]" class="h-8 w-[150px] lg:w-[250px]"
@update:model-value="emit('update:searchFilter', $event as string)" @update:model-value="emit('update:searchFilter', $event as string)"
@@ -45,7 +45,7 @@ function resetFilters() {
class="h-8 px-2 lg:px-3" class="h-8 px-2 lg:px-3"
@click="resetFilters" @click="resetFilters"
> >
{{ t('developer.extension.webhooks.reset') }} {{ t('admin.extension.webhooks.reset') }}
<X class="size-4 ml-2" /> <X class="size-4 ml-2" />
</Button> </Button>
</div> </div>
@@ -48,15 +48,15 @@ const table = generateVueTable<Webhook>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
select: 'developer.extension.webhooks.select', select: 'admin.extension.webhooks.select',
name: 'developer.extension.webhooks.columns.name', name: 'admin.extension.webhooks.columns.name',
url: 'developer.extension.webhooks.columns.url', url: 'admin.extension.webhooks.columns.url',
application_name: 'developer.extension.webhooks.columns.application', application_name: 'admin.extension.webhooks.columns.application',
events: 'developer.extension.webhooks.columns.events', events: 'admin.extension.webhooks.columns.events',
status: 'developer.extension.webhooks.columns.status', status: 'admin.extension.webhooks.columns.status',
retry_count: 'developer.extension.webhooks.columns.retryCount', retry_count: 'admin.extension.webhooks.columns.retryCount',
timeout: 'developer.extension.webhooks.columns.timeout', timeout: 'admin.extension.webhooks.columns.timeout',
created_at: 'developer.extension.webhooks.columns.createdAt', created_at: 'admin.extension.webhooks.columns.createdAt',
} }
defineExpose({ defineExpose({
@@ -70,13 +70,13 @@ defineExpose({
<div class="space-y-3"> <div class="space-y-3">
<BulkActions :table="table" entity-name="webhooks"> <BulkActions :table="table" entity-name="webhooks">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')"> <UiButton variant="outline" size="sm" @click="emit('batchEnable')">
{{ t('developer.extension.webhooks.enable') }} {{ t('admin.extension.webhooks.enable') }}
</UiButton> </UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')"> <UiButton variant="outline" size="sm" @click="emit('batchDisable')">
{{ t('developer.extension.webhooks.disable') }} {{ t('admin.extension.webhooks.disable') }}
</UiButton> </UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')"> <UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
{{ t('developer.extension.webhooks.batchDelete') }} {{ t('admin.extension.webhooks.batchDelete') }}
</UiButton> </UiButton>
</BulkActions> </BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2"> <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 { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 ConfirmDialog from '@/components/confirm-dialog.vue'
import DateRangeFilter from '@/components/data-table/date-range-filter.vue' import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
import SingleFilter from '@/components/data-table/single-filter.vue' import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout' import { BasicPage } from '@/components/global-layout'
import APIKeyDataTable from '@/pages/developer/extension/components/apikey-data-table.vue' import APIKeyDataTable from '@/pages/admin/extension/components/apikey-data-table.vue'
import WebhookDataTable from '@/pages/developer/extension/components/webhook-data-table.vue' import WebhookDataTable from '@/pages/admin/extension/components/webhook-data-table.vue'
import api from '@/services/api' import api from '@/services/api'
const { t } = useI18n() const { t } = useI18n()
@@ -134,8 +134,8 @@ const applicationOptions = computed(() => {
}) })
const statusOptions = computed(() => [ const statusOptions = computed(() => [
{ label: t('developer.extension.enabled'), value: 'active' }, { label: t('admin.extension.enabled'), value: 'active' },
{ label: t('developer.extension.disabled'), value: 'inactive' }, { label: t('admin.extension.disabled'), value: 'inactive' },
]) ])
async function fetchApplications() { async function fetchApplications() {
@@ -179,23 +179,23 @@ async function fetchAPIKeys() {
} }
function goToCreateWebhook() { function goToCreateWebhook() {
router.push('/developer/extension/webhooks/create') router.push('/admin/extension/webhooks/create')
} }
function goToCreateAPIKey() { function goToCreateAPIKey() {
router.push('/developer/extension/api-keys/create') router.push('/admin/extension/api-keys/create')
} }
async function toggleWebhookStatus(webhook: Webhook) { async function toggleWebhookStatus(webhook: Webhook) {
try { try {
const newStatus = webhook.status === 'active' ? 'inactive' : 'active' const newStatus = webhook.status === 'active' ? 'inactive' : 'active'
await api.put(`/dev/extension/webhooks/${webhook.id}/status`, { status: newStatus }) 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() fetchWebhooks()
} }
catch (error: any) { catch (error: any) {
console.error('切换Webhook状态失败:', error) 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 { try {
const newStatus = apiKey.status === 'active' ? 'inactive' : 'active' const newStatus = apiKey.status === 'active' ? 'inactive' : 'active'
await api.put(`/dev/extension/api-keys/${apiKey.id}/status`, { status: newStatus }) 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() fetchAPIKeys()
} }
catch (error: any) { catch (error: any) {
console.error('切换API密钥状态失败:', error) 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) { async function testWebhook(webhook: Webhook) {
try { try {
await api.post(`/dev/extension/webhooks/${webhook.id}/test`, {}) 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) { catch (error: any) {
console.error('测试Webhook失败:', error) 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 { try {
await api.post(`/dev/extension/api-keys/${regenerateTarget.value.id}/regenerate`, {}) 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() fetchAPIKeys()
} }
catch (error: any) { catch (error: any) {
console.error('重新生成API密钥失败:', error) console.error('重新生成API密钥失败:', error)
toast.error(error.message || t('developer.extension.apiKeys.regenerateFailed')) toast.error(error.message || t('admin.extension.apiKeys.regenerateFailed'))
} }
finally { finally {
regenerateTarget.value = null regenerateTarget.value = null
@@ -265,18 +265,18 @@ async function handleDelete() {
try { try {
if (deleteType.value === 'webhook') { if (deleteType.value === 'webhook') {
await api.delete(`/dev/extension/webhooks/${deleteTarget.value.id}`) await api.delete(`/dev/extension/webhooks/${deleteTarget.value.id}`)
toast.success(t('developer.extension.webhooks.deleteSuccess')) toast.success(t('admin.extension.webhooks.deleteSuccess'))
fetchWebhooks() fetchWebhooks()
} }
else { else {
await api.delete(`/dev/extension/api-keys/${deleteTarget.value.id}`) 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() fetchAPIKeys()
} }
} }
catch (error: any) { catch (error: any) {
console.error('删除失败:', error) console.error('删除失败:', error)
toast.error(error.message || t('developer.extension.deleteFailed')) toast.error(error.message || t('admin.extension.deleteFailed'))
} }
finally { finally {
deleteTarget.value = null deleteTarget.value = null
@@ -293,18 +293,18 @@ async function handleBatchDelete() {
try { try {
if (deleteType.value === 'webhook') { if (deleteType.value === 'webhook') {
await api.delete('/dev/extension/webhooks/batch', { ids: batchDeleteIds.value } as any) 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() fetchWebhooks()
} }
else { else {
await api.delete('/dev/extension/api-keys/batch', { ids: batchDeleteIds.value } as any) 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() fetchAPIKeys()
} }
} }
catch (error: any) { catch (error: any) {
console.error('批量删除失败:', error) console.error('批量删除失败:', error)
toast.error(error.message || t('developer.extension.deleteFailed')) toast.error(error.message || t('admin.extension.deleteFailed'))
} }
finally { finally {
batchDeleteIds.value = [] batchDeleteIds.value = []
@@ -314,24 +314,24 @@ async function handleBatchDelete() {
async function batchToggleWebhookStatus(ids: (string | number)[], status: string) { async function batchToggleWebhookStatus(ids: (string | number)[], status: string) {
try { try {
await api.put('/dev/extension/webhooks/batch/status', { ids, status }) 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() fetchWebhooks()
} }
catch (error: any) { catch (error: any) {
console.error('批量更新Webhook状态失败:', error) 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) { async function batchToggleAPIKeyStatus(ids: (string | number)[], status: string) {
try { try {
await api.put('/dev/extension/api-keys/batch/status', { ids, status }) 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() fetchAPIKeys()
} }
catch (error: any) { catch (error: any) {
console.error('批量更新API密钥状态失败:', error) 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> <template>
<BasicPage <BasicPage
:title="t('developer.extension.title')" :title="t('admin.extension.title')"
:description="t('developer.extension.description')" :description="t('admin.extension.description')"
sticky sticky
> >
<template #actions> <template #actions>
<UiButton v-if="currentTab === 'webhooks'" @click="goToCreateWebhook"> <UiButton v-if="currentTab === 'webhooks'" @click="goToCreateWebhook">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.extension.webhooks.createBtn') }} {{ t('admin.extension.webhooks.createBtn') }}
</UiButton> </UiButton>
<UiButton v-else @click="goToCreateAPIKey"> <UiButton v-else @click="goToCreateAPIKey">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" /> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
{{ t('developer.extension.apiKeys.createBtn') }} {{ t('admin.extension.apiKeys.createBtn') }}
</UiButton> </UiButton>
</template> </template>
@@ -364,7 +364,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.extension.totalWebhooks') }} {{ t('admin.extension.totalWebhooks') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:webhook" class="size-4 text-muted-foreground" /> <Icon icon="lucide:webhook" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -378,7 +378,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.extension.totalAPIKeys') }} {{ t('admin.extension.totalAPIKeys') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:key" class="size-4 text-muted-foreground" /> <Icon icon="lucide:key" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -392,7 +392,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.extension.enabled') }} {{ t('admin.extension.enabled') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -406,7 +406,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.extension.disabled') }} {{ t('admin.extension.disabled') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" /> <Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -422,11 +422,11 @@ onMounted(() => {
<UiTabsList class="grid w-full grid-cols-2"> <UiTabsList class="grid w-full grid-cols-2">
<UiTabsTrigger value="webhooks"> <UiTabsTrigger value="webhooks">
<Icon icon="lucide:webhook" class="h-4 w-4 mr-2" /> <Icon icon="lucide:webhook" class="h-4 w-4 mr-2" />
{{ t('developer.extension.webhooks.title') }} {{ t('admin.extension.webhooks.title') }}
</UiTabsTrigger> </UiTabsTrigger>
<UiTabsTrigger value="apikeys"> <UiTabsTrigger value="apikeys">
<Icon icon="lucide:key" class="h-4 w-4 mr-2" /> <Icon icon="lucide:key" class="h-4 w-4 mr-2" />
{{ t('developer.extension.apiKeys.title') }} {{ t('admin.extension.apiKeys.title') }}
</UiTabsTrigger> </UiTabsTrigger>
</UiTabsList> </UiTabsList>
@@ -450,12 +450,12 @@ onMounted(() => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.extension.application')" :title="t('admin.extension.application')"
:options="applicationOptions" :options="applicationOptions"
/> />
<SingleFilter <SingleFilter
v-model="statusFilter" v-model="statusFilter"
:title="t('developer.extension.status')" :title="t('admin.extension.status')"
:options="statusOptions" :options="statusOptions"
/> />
<DateRangeFilter <DateRangeFilter
@@ -488,12 +488,12 @@ onMounted(() => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.extension.application')" :title="t('admin.extension.application')"
:options="applicationOptions" :options="applicationOptions"
/> />
<SingleFilter <SingleFilter
v-model="statusFilter" v-model="statusFilter"
:title="t('developer.extension.status')" :title="t('admin.extension.status')"
:options="statusOptions" :options="statusOptions"
/> />
<DateRangeFilter <DateRangeFilter
@@ -511,17 +511,17 @@ onMounted(() => {
<ConfirmDialog <ConfirmDialog
v-model:open="deleteDialogOpen" v-model:open="deleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.extension.delete')" :confirm-button-text="t('admin.extension.delete')"
:cancel-button-text="t('developer.extension.cancel')" :cancel-button-text="t('admin.extension.cancel')"
@confirm="handleDelete" @confirm="handleDelete"
> >
<template #title> <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>
<template #description> <template #description>
{{ deleteType === 'webhook' {{ deleteType === 'webhook'
? t('developer.extension.webhooks.deleteConfirm', { webhookName: (deleteTarget as Webhook)?.name }) ? t('admin.extension.webhooks.deleteConfirm', { webhookName: (deleteTarget as Webhook)?.name })
: t('developer.extension.apiKeys.deleteConfirm', { apiKeyName: (deleteTarget as APIKey)?.name }) : t('admin.extension.apiKeys.deleteConfirm', { apiKeyName: (deleteTarget as APIKey)?.name })
}} }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
@@ -529,31 +529,31 @@ onMounted(() => {
<ConfirmDialog <ConfirmDialog
v-model:open="batchDeleteDialogOpen" v-model:open="batchDeleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.extension.delete')" :confirm-button-text="t('admin.extension.delete')"
:cancel-button-text="t('developer.extension.cancel')" :cancel-button-text="t('admin.extension.cancel')"
@confirm="handleBatchDelete" @confirm="handleBatchDelete"
> >
<template #title> <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>
<template #description> <template #description>
{{ deleteType === 'webhook' {{ deleteType === 'webhook'
? t('developer.extension.webhooks.batchDeleteConfirm', { count: batchDeleteIds.length }) ? t('admin.extension.webhooks.batchDeleteConfirm', { count: batchDeleteIds.length })
: t('developer.extension.apiKeys.batchDeleteConfirm', { count: batchDeleteIds.length }) : t('admin.extension.apiKeys.batchDeleteConfirm', { count: batchDeleteIds.length })
}} }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
v-model:open="regenerateDialogOpen" v-model:open="regenerateDialogOpen"
:confirm-button-text="t('developer.extension.apiKeys.regenerate')" :confirm-button-text="t('admin.extension.apiKeys.regenerate')"
:cancel-button-text="t('developer.extension.cancel')" :cancel-button-text="t('admin.extension.cancel')"
@confirm="handleRegenerateAPIKey" @confirm="handleRegenerateAPIKey"
> >
<template #title> <template #title>
{{ t('developer.extension.apiKeys.regenerate') }} {{ t('admin.extension.apiKeys.regenerate') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.extension.apiKeys.regenerateConfirm') }} {{ t('admin.extension.apiKeys.regenerateConfirm') }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
</BasicPage> </BasicPage>
@@ -31,13 +31,13 @@ const form = ref({
}) })
const availableEvents = computed(() => [ const availableEvents = computed(() => [
{ value: 'user.registered', label: t('developer.extension.webhooks.events.userRegistered') }, { value: 'user.registered', label: t('admin.extension.webhooks.events.userRegistered') },
{ value: 'user.login', label: t('developer.extension.webhooks.events.userLogin') }, { value: 'user.login', label: t('admin.extension.webhooks.events.userLogin') },
{ value: 'user.recharged', label: t('developer.extension.webhooks.events.userRecharged') }, { value: 'user.recharged', label: t('admin.extension.webhooks.events.userRecharged') },
{ value: 'user.expired', label: t('developer.extension.webhooks.events.userExpired') }, { value: 'user.expired', label: t('admin.extension.webhooks.events.userExpired') },
{ value: 'card.used', label: t('developer.extension.webhooks.events.cardUsed') }, { value: 'card.used', label: t('admin.extension.webhooks.events.cardUsed') },
{ value: 'card.expired', label: t('developer.extension.webhooks.events.cardExpired') }, { value: 'card.expired', label: t('admin.extension.webhooks.events.cardExpired') },
{ value: 'abnormal.detected', label: t('developer.extension.webhooks.events.abnormalDetected') }, { value: 'abnormal.detected', label: t('admin.extension.webhooks.events.abnormalDetected') },
]) ])
const filteredApplications = computed(() => { const filteredApplications = computed(() => {
@@ -76,19 +76,19 @@ function toggleEvent(event: string) {
async function handleSubmit() { async function handleSubmit() {
if (!form.value.name) { if (!form.value.name) {
toast.error(t('developer.extension.webhooks.create.nameRequired')) toast.error(t('admin.extension.webhooks.create.nameRequired'))
return return
} }
if (!form.value.application_id) { if (!form.value.application_id) {
toast.error(t('developer.extension.webhooks.create.selectAppRequired')) toast.error(t('admin.extension.webhooks.create.selectAppRequired'))
return return
} }
if (!form.value.url) { if (!form.value.url) {
toast.error(t('developer.extension.webhooks.create.urlRequired')) toast.error(t('admin.extension.webhooks.create.urlRequired'))
return return
} }
if (form.value.events.length === 0) { if (form.value.events.length === 0) {
toast.error(t('developer.extension.webhooks.create.eventsRequired')) toast.error(t('admin.extension.webhooks.create.eventsRequired'))
return return
} }
@@ -104,12 +104,12 @@ async function handleSubmit() {
timeout: form.value.timeout, timeout: form.value.timeout,
enabled: form.value.enabled, enabled: form.value.enabled,
}) })
toast.success(t('developer.extension.webhooks.create.submitSuccess')) toast.success(t('admin.extension.webhooks.create.submitSuccess'))
router.push('/developer/extension') router.push('/admin/extension')
} }
catch (error: any) { catch (error: any) {
console.error('创建Webhook失败:', error) 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 { finally {
saving.value = false saving.value = false
@@ -123,11 +123,11 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.extension.webhooks.create.title')" :title="t('admin.extension.webhooks.create.title')"
:description="t('developer.extension.webhooks.create.description')" :description="t('admin.extension.webhooks.create.description')"
:breadcrumbs="[ :breadcrumbs="[
{ title: t('developer.extension.title'), href: '/developer/extension' }, { title: t('admin.extension.title'), href: '/admin/extension' },
{ title: t('developer.extension.webhooks.create.title') }, { title: t('admin.extension.webhooks.create.title') },
]" ]"
sticky sticky
> >
@@ -138,29 +138,29 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:settings-2" class="size-5" /> <Icon icon="lucide:settings-2" class="size-5" />
{{ t('developer.extension.webhooks.create.title') }} {{ t('admin.extension.webhooks.create.title') }}
</UiCardTitle> </UiCardTitle>
<UiCardDescription>{{ t('developer.extension.webhooks.create.description') }}</UiCardDescription> <UiCardDescription>{{ t('admin.extension.webhooks.create.description') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="name"> <UiLabel for="name">
{{ t('developer.extension.webhooks.create.name') }} * {{ t('admin.extension.webhooks.create.name') }} *
</UiLabel> </UiLabel>
<UiInput <UiInput
id="name" id="name"
v-model="form.name" v-model="form.name"
:placeholder="t('developer.extension.webhooks.create.namePlaceholder')" :placeholder="t('admin.extension.webhooks.create.namePlaceholder')"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="application"> <UiLabel for="application">
{{ t('developer.extension.webhooks.create.application') }} * {{ t('admin.extension.webhooks.create.application') }} *
</UiLabel> </UiLabel>
<UiSelect v-model="form.application_id"> <UiSelect v-model="form.application_id">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.extension.webhooks.create.selectAppPlaceholder')" /> <UiSelectValue :placeholder="t('admin.extension.webhooks.create.selectAppPlaceholder')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem v-for="app in filteredApplications" :key="app.value" :value="app.value"> <UiSelectItem v-for="app in filteredApplications" :key="app.value" :value="app.value">
@@ -172,28 +172,28 @@ onMounted(() => {
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="url"> <UiLabel for="url">
{{ t('developer.extension.webhooks.create.url') }} * {{ t('admin.extension.webhooks.create.url') }} *
</UiLabel> </UiLabel>
<UiInput <UiInput
id="url" id="url"
v-model="form.url" v-model="form.url"
:placeholder="t('developer.extension.webhooks.create.urlPlaceholder')" :placeholder="t('admin.extension.webhooks.create.urlPlaceholder')"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="secret_key"> <UiLabel for="secret_key">
{{ t('developer.extension.webhooks.create.secretKey') }} {{ t('admin.extension.webhooks.create.secretKey') }}
</UiLabel> </UiLabel>
<UiInput <UiInput
id="secret_key" id="secret_key"
v-model="form.secret_key" v-model="form.secret_key"
:placeholder="t('developer.extension.webhooks.create.secretKeyPlaceholder')" :placeholder="t('admin.extension.webhooks.create.secretKeyPlaceholder')"
/> />
</div> </div>
<div class="space-y-2"> <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 class="grid grid-cols-1 md:grid-cols-2 gap-2">
<div <div
v-for="event in availableEvents" v-for="event in availableEvents"
@@ -214,7 +214,7 @@ onMounted(() => {
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="retry_count"> <UiLabel for="retry_count">
{{ t('developer.extension.webhooks.create.retryCount') }} {{ t('admin.extension.webhooks.create.retryCount') }}
</UiLabel> </UiLabel>
<UiInput <UiInput
id="retry_count" id="retry_count"
@@ -222,13 +222,13 @@ onMounted(() => {
type="number" type="number"
min="0" min="0"
max="10" max="10"
:placeholder="t('developer.extension.webhooks.create.retryCountPlaceholder')" :placeholder="t('admin.extension.webhooks.create.retryCountPlaceholder')"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="timeout"> <UiLabel for="timeout">
{{ t('developer.extension.webhooks.create.timeout') }} {{ t('admin.extension.webhooks.create.timeout') }}
</UiLabel> </UiLabel>
<UiInput <UiInput
id="timeout" id="timeout"
@@ -236,7 +236,7 @@ onMounted(() => {
type="number" type="number"
min="1" min="1"
max="60" max="60"
:placeholder="t('developer.extension.webhooks.create.timeoutPlaceholder')" :placeholder="t('admin.extension.webhooks.create.timeoutPlaceholder')"
/> />
</div> </div>
</div> </div>
@@ -244,7 +244,7 @@ onMounted(() => {
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<UiSwitch id="enabled" v-model:checked="form.enabled" /> <UiSwitch id="enabled" v-model:checked="form.enabled" />
<UiLabel for="enabled"> <UiLabel for="enabled">
{{ t('developer.extension.webhooks.create.enabled') }} {{ t('admin.extension.webhooks.create.enabled') }}
</UiLabel> </UiLabel>
</div> </div>
</UiCardContent> </UiCardContent>
@@ -256,29 +256,29 @@ onMounted(() => {
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-5" /> <Icon icon="lucide:info" class="size-5" />
{{ t('developer.extension.webhooks.create.preview') || '预览' }} {{ t('admin.extension.webhooks.create.preview') || '预览' }}
</UiCardTitle> </UiCardTitle>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="space-y-4"> <UiCardContent class="space-y-4">
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between text-sm"> <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> <span>{{ form.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ selectedApplication?.name || '-' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.events') }}</span> <span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.events') }}</span>
<span>{{ form.events.length }} {{ t('developer.extension.webhooks.create.selected') || '已选择' }}</span> <span>{{ form.events.length }} {{ t('admin.extension.webhooks.create.selected') || '已选择' }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ form.retry_count }}</span>
</div> </div>
<div class="flex justify-between text-sm"> <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> <span>{{ form.timeout }}s</span>
</div> </div>
</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-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" /> <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>
<UiButton <UiButton
variant="outline" variant="outline"
class="w-full" class="w-full"
@click="router.back()" @click="router.back()"
> >
{{ t('developer.extension.webhooks.create.cancelBtn') }} {{ t('admin.extension.webhooks.create.cancelBtn') }}
</UiButton> </UiButton>
</div> </div>
</UiCardContent> </UiCardContent>
+48 -48
View File
@@ -174,11 +174,11 @@ async function handleDelete() {
try { try {
await api.delete(`/dev/finance/records/${deleteTarget.value.id}?type=${deleteTarget.value.type}`) 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() fetchRecords()
} }
catch (error: any) { catch (error: any) {
toast.error(error.message || t('developer.finance.deleteFailed')) toast.error(error.message || t('admin.finance.deleteFailed'))
} }
finally { finally {
deleteTarget.value = null deleteTarget.value = null
@@ -203,15 +203,15 @@ const columns: ColumnDef<FinanceRecord>[] = [
}, },
{ {
accessorKey: 'type', accessorKey: 'type',
header: () => t('developer.finance.columns.type'), header: () => t('admin.finance.columns.type'),
cell: ({ row }) => { cell: ({ row }) => {
const type = row.getValue('type') as string 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', accessorKey: 'amount',
header: () => t('developer.finance.columns.amount'), header: () => t('admin.finance.columns.amount'),
cell: ({ row }) => { cell: ({ row }) => {
const type = row.getValue('type') as string const type = row.getValue('type') as string
const amount = row.getValue('amount') as number const amount = row.getValue('amount') as number
@@ -221,7 +221,7 @@ const columns: ColumnDef<FinanceRecord>[] = [
}, },
{ {
accessorKey: 'user', accessorKey: 'user',
header: () => t('developer.finance.columns.user'), header: () => t('admin.finance.columns.user'),
cell: ({ row }) => { cell: ({ row }) => {
const user = row.original.user const user = row.original.user
return user?.username || '-' return user?.username || '-'
@@ -229,7 +229,7 @@ const columns: ColumnDef<FinanceRecord>[] = [
}, },
{ {
accessorKey: 'detail', accessorKey: 'detail',
header: () => t('developer.finance.columns.detail'), header: () => t('admin.finance.columns.detail'),
cell: ({ row }) => { cell: ({ row }) => {
const detail = row.getValue('detail') as string const detail = row.getValue('detail') as string
return h('span', { class: 'max-w-[200px] truncate text-muted-foreground block' }, detail || '-') return h('span', { class: 'max-w-[200px] truncate text-muted-foreground block' }, detail || '-')
@@ -237,13 +237,13 @@ const columns: ColumnDef<FinanceRecord>[] = [
}, },
{ {
accessorKey: 'status', accessorKey: 'status',
header: () => t('developer.finance.columns.status'), header: () => t('admin.finance.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const status = row.getValue('status') as string const status = row.getValue('status') as string
const map: Record<string, { label: string, variant: 'default' | 'secondary' | 'destructive' }> = { const map: Record<string, { label: string, variant: 'default' | 'secondary' | 'destructive' }> = {
success: { label: t('developer.finance.success'), variant: 'default' }, success: { label: t('admin.finance.success'), variant: 'default' },
pending: { label: t('developer.finance.pending'), variant: 'secondary' }, pending: { label: t('admin.finance.pending'), variant: 'secondary' },
failed: { label: t('developer.finance.failed'), variant: 'destructive' }, failed: { label: t('admin.finance.failed'), variant: 'destructive' },
} }
const { label, variant } = map[status] || { label: status, variant: 'secondary' } const { label, variant } = map[status] || { label: status, variant: 'secondary' }
return h(Badge, { variant }, () => label) return h(Badge, { variant }, () => label)
@@ -251,14 +251,14 @@ const columns: ColumnDef<FinanceRecord>[] = [
}, },
{ {
accessorKey: 'created_at', accessorKey: 'created_at',
header: () => t('developer.finance.columns.time'), header: () => t('admin.finance.columns.time'),
cell: ({ row }) => { cell: ({ row }) => {
return h('span', { class: 'text-muted-foreground whitespace-nowrap' }, formatTime(row.getValue('created_at'))) return h('span', { class: 'text-muted-foreground whitespace-nowrap' }, formatTime(row.getValue('created_at')))
}, },
}, },
{ {
id: 'actions', id: 'actions',
header: () => t('developer.finance.columns.actions'), header: () => t('admin.finance.columns.actions'),
cell: ({ row }) => { cell: ({ row }) => {
return h(Button, { return h(Button, {
variant: 'ghost', variant: 'ghost',
@@ -293,7 +293,7 @@ const table = useVueTable({
async function confirmBatchDelete() { async function confirmBatchDelete() {
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
if (selectedRows.length === 0) { if (selectedRows.length === 0) {
toast.error(t('developer.finance.delete')) toast.error(t('admin.finance.delete'))
return return
} }
batchDeleteDialogOpen.value = true batchDeleteDialogOpen.value = true
@@ -304,11 +304,11 @@ async function handleBatchDelete() {
const ids = selectedRows.map(row => row.original.id) const ids = selectedRows.map(row => row.original.id)
try { try {
await api.post('/dev/finance/records/batch-delete', { ids }) await api.post('/dev/finance/records/batch-delete', { ids })
toast.success(t('developer.finance.deleteSuccess')) toast.success(t('admin.finance.deleteSuccess'))
fetchRecords() fetchRecords()
} }
catch (error: any) { 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> <template>
<BasicPage <BasicPage
:title="t('developer.finance.title')" :title="t('admin.finance.title')"
:description="t('developer.finance.description')" :description="t('admin.finance.description')"
sticky sticky
> >
<div class="space-y-6"> <div class="space-y-6">
@@ -353,7 +353,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.finance.rechargeCount') }} {{ t('admin.finance.rechargeCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:arrow-down-left" class="size-4 text-muted-foreground" /> <Icon icon="lucide:arrow-down-left" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -367,7 +367,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.finance.rechargeAmount') }} {{ t('admin.finance.rechargeAmount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" /> <Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -381,7 +381,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.finance.consumptionCount') }} {{ t('admin.finance.consumptionCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:arrow-up-right" class="size-4 text-muted-foreground" /> <Icon icon="lucide:arrow-up-right" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -395,7 +395,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.finance.consumptionAmount') }} {{ t('admin.finance.consumptionAmount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:credit-card" class="size-4 text-muted-foreground" /> <Icon icon="lucide:credit-card" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -409,7 +409,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.finance.profit') }} {{ t('admin.finance.profit') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:trending-up" class="size-4 text-muted-foreground" /> <Icon icon="lucide:trending-up" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -423,9 +423,9 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.finance.transactionRecords') }}</UiCardTitle> <UiCardTitle>{{ t('admin.finance.transactionRecords') }}</UiCardTitle>
<UiCardDescription> <UiCardDescription>
{{ t('developer.finance.transactionRecordsDesc') }} {{ t('admin.finance.transactionRecordsDesc') }}
</UiCardDescription> </UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent>
@@ -438,18 +438,18 @@ onMounted(() => {
@click="confirmBatchDelete" @click="confirmBatchDelete"
> >
<Icon icon="lucide:trash-2" class="mr-2 size-4" /> <Icon icon="lucide:trash-2" class="mr-2 size-4" />
{{ t('developer.finance.batchDeleteBtn') }} {{ t('admin.finance.batchDeleteBtn') }}
</UiButton> </UiButton>
</div> </div>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UiSelect v-model="appFilter" class="w-[140px]"> <UiSelect v-model="appFilter" class="w-[140px]">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.finance.allApplications')" /> <UiSelectValue :placeholder="t('admin.finance.allApplications')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem value="all"> <UiSelectItem value="all">
{{ t('developer.finance.allApplications') }} {{ t('admin.finance.allApplications') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)"> <UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
{{ app.name }} {{ app.name }}
@@ -459,43 +459,43 @@ onMounted(() => {
<UiSelect v-model="statusFilter" class="w-[120px]"> <UiSelect v-model="statusFilter" class="w-[120px]">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue :placeholder="t('developer.finance.allStatus')" /> <UiSelectValue :placeholder="t('admin.finance.allStatus')" />
</UiSelectTrigger> </UiSelectTrigger>
<UiSelectContent> <UiSelectContent>
<UiSelectItem value="all"> <UiSelectItem value="all">
{{ t('developer.finance.allStatus') }} {{ t('admin.finance.allStatus') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="success"> <UiSelectItem value="success">
{{ t('developer.finance.success') }} {{ t('admin.finance.success') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="pending"> <UiSelectItem value="pending">
{{ t('developer.finance.pending') }} {{ t('admin.finance.pending') }}
</UiSelectItem> </UiSelectItem>
<UiSelectItem value="failed"> <UiSelectItem value="failed">
{{ t('developer.finance.failed') }} {{ t('admin.finance.failed') }}
</UiSelectItem> </UiSelectItem>
</UiSelectContent> </UiSelectContent>
</UiSelect> </UiSelect>
<DateTimePicker <DateTimePicker
v-model="dateRange.from" v-model="dateRange.from"
:placeholder="t('developer.finance.startTime')" :placeholder="t('admin.finance.startTime')"
class="w-[180px]" class="w-[180px]"
/> />
<span class="text-muted-foreground">{{ t('developer.finance.to') }}</span> <span class="text-muted-foreground">{{ t('admin.finance.to') }}</span>
<DateTimePicker <DateTimePicker
v-model="dateRange.to" v-model="dateRange.to"
:placeholder="t('developer.finance.endTime')" :placeholder="t('admin.finance.endTime')"
class="w-[180px]" class="w-[180px]"
/> />
<UiButton variant="ghost" size="sm" @click="setQuickRange('today')"> <UiButton variant="ghost" size="sm" @click="setQuickRange('today')">
{{ t('developer.finance.today') }} {{ t('admin.finance.today') }}
</UiButton> </UiButton>
<UiButton variant="ghost" size="sm" @click="setQuickRange('week')"> <UiButton variant="ghost" size="sm" @click="setQuickRange('week')">
{{ t('developer.finance.last7Days') }} {{ t('admin.finance.last7Days') }}
</UiButton> </UiButton>
<UiButton variant="ghost" size="sm" @click="setQuickRange('month')"> <UiButton variant="ghost" size="sm" @click="setQuickRange('month')">
{{ t('developer.finance.thisMonth') }} {{ t('admin.finance.thisMonth') }}
</UiButton> </UiButton>
</div> </div>
</div> </div>
@@ -534,7 +534,7 @@ onMounted(() => {
> >
<div class="flex flex-col items-center justify-center text-muted-foreground"> <div class="flex flex-col items-center justify-center text-muted-foreground">
<Icon icon="lucide:inbox" class="size-12 mb-2 opacity-50" /> <Icon icon="lucide:inbox" class="size-12 mb-2 opacity-50" />
<span>{{ t('developer.finance.noRecords') }}</span> <span>{{ t('admin.finance.noRecords') }}</span>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -551,30 +551,30 @@ onMounted(() => {
<ConfirmDialog <ConfirmDialog
v-model:open="deleteDialogOpen" v-model:open="deleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.finance.delete')" :confirm-button-text="t('admin.finance.delete')"
:cancel-button-text="t('developer.finance.cancel')" :cancel-button-text="t('admin.finance.cancel')"
@confirm="handleDelete" @confirm="handleDelete"
> >
<template #title> <template #title>
{{ t('developer.finance.delete') }} {{ t('admin.finance.delete') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.finance.deleteConfirm') }} {{ t('admin.finance.deleteConfirm') }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
v-model:open="batchDeleteDialogOpen" v-model:open="batchDeleteDialogOpen"
destructive destructive
:confirm-button-text="t('developer.finance.delete')" :confirm-button-text="t('admin.finance.delete')"
:cancel-button-text="t('developer.finance.cancel')" :cancel-button-text="t('admin.finance.cancel')"
@confirm="handleBatchDelete" @confirm="handleBatchDelete"
> >
<template #title> <template #title>
{{ t('developer.finance.batchDeleteBtn') }} {{ t('admin.finance.batchDeleteBtn') }}
</template> </template>
<template #description> <template #description>
{{ t('developer.finance.batchDeleteConfirm', { count: table.getSelectedRowModel().rows.length }) }} {{ t('admin.finance.batchDeleteConfirm', { count: table.getSelectedRowModel().rows.length }) }}
</template> </template>
</ConfirmDialog> </ConfirmDialog>
</template> </template>
+48 -48
View File
@@ -107,10 +107,10 @@ function getTicketStatusColor(status: string) {
function getTicketStatusText(status: string) { function getTicketStatusText(status: string) {
const texts: Record<string, string> = { const texts: Record<string, string> = {
open: t('developer.ticketStatusOpen'), open: t('admin.ticketStatusOpen'),
processing: t('developer.ticketStatusProcessing'), processing: t('admin.ticketStatusProcessing'),
resolved: t('developer.ticketStatusResolved'), resolved: t('admin.ticketStatusResolved'),
closed: t('developer.ticketStatusClosed'), closed: t('admin.ticketStatusClosed'),
} }
return texts[status] || status return texts[status] || status
} }
@@ -127,20 +127,20 @@ function getTicketPriorityColor(priority: string) {
function getTicketPriorityText(priority: string) { function getTicketPriorityText(priority: string) {
const texts: Record<string, string> = { const texts: Record<string, string> = {
low: t('developer.priorityLow'), low: t('admin.priorityLow'),
normal: t('developer.priorityNormal'), normal: t('admin.priorityNormal'),
high: t('developer.priorityHigh'), high: t('admin.priorityHigh'),
urgent: t('developer.priorityUrgent'), urgent: t('admin.priorityUrgent'),
} }
return texts[priority] || priority return texts[priority] || priority
} }
function goToTicket(ticketId: number) { function goToTicket(ticketId: number) {
router.push(`/developer/tickets/${ticketId}`) router.push(`/admin/tickets/${ticketId}`)
} }
function goToTickets() { function goToTickets() {
router.push('/developer/tickets') router.push('/admin/tickets')
} }
async function fetchDashboard() { async function fetchDashboard() {
@@ -196,21 +196,21 @@ async function fetchDashboard() {
if (data.data.recentActivities && Array.isArray(data.data.recentActivities)) { if (data.data.recentActivities && Array.isArray(data.data.recentActivities)) {
recentActivities.value = data.data.recentActivities.map((log: any) => { recentActivities.value = data.data.recentActivities.map((log: any) => {
let icon = 'lucide:activity' let icon = 'lucide:activity'
let title = log.action || t('developer.operation') let title = log.action || t('admin.operation')
let description = log.details || log.resource || t('developer.noDescription') let description = log.details || log.resource || t('admin.noDescription')
if (log.log_type === 'operation') { if (log.log_type === 'operation') {
icon = 'lucide:plus-circle' icon = 'lucide:plus-circle'
title = t('developer.operationRecord') title = t('admin.operationRecord')
} }
else if (log.log_type === 'verification') { else if (log.log_type === 'verification') {
icon = 'lucide:check-circle' icon = 'lucide:check-circle'
title = t('developer.cardVerification') title = t('admin.cardVerification')
description = `${t('developer.verification')}${log.status === 'success' ? t('developer.success') : t('developer.failed')}` description = `${t('admin.verification')}${log.status === 'success' ? t('admin.success') : t('admin.failed')}`
} }
else if (log.log_type === 'exception') { else if (log.log_type === 'exception') {
icon = 'lucide:alert-circle' icon = 'lucide:alert-circle'
title = t('developer.exceptionRecord') title = t('admin.exceptionRecord')
} }
return { return {
@@ -238,7 +238,7 @@ async function fetchDashboard() {
} }
} }
catch (error) { catch (error) {
console.error(t('developer.fetchDashboardError'), error) console.error(t('admin.fetchDashboardError'), error)
} }
finally { finally {
loading.value = false loading.value = false
@@ -276,7 +276,7 @@ async function initMapChart() {
}) })
} }
catch (error) { 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) { if (params.data && params.data.count > 0) {
return `<div style="padding: 8px;"> return `<div style="padding: 8px;">
<div style="font-weight: bold; margin-bottom: 4px; color: #f8fafc;">${params.name}</div> <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>` </div>`
} }
return `<div style="padding: 8px;"> return `<div style="padding: 8px;">
<div style="font-weight: bold; margin-bottom: 4px; color: #f8fafc;">${params.name}</div> <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>` </div>`
}, },
}, },
@@ -342,7 +342,7 @@ function updateMapOption() {
}, },
series: [ series: [
{ {
name: t('developer.userDistribution'), name: t('admin.userDistribution'),
type: 'map', type: 'map',
geoIndex: 0, geoIndex: 0,
data, data,
@@ -383,7 +383,7 @@ function initActivityChart() {
labels, labels,
datasets: [ datasets: [
{ {
label: t('developer.verificationCount'), label: t('admin.verificationCount'),
data, data,
borderColor: '#3b82f6', borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)', backgroundColor: 'rgba(59, 130, 246, 0.1)',
@@ -413,7 +413,7 @@ function initActivityChart() {
padding: 12, padding: 12,
displayColors: false, displayColors: false,
callbacks: { 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) { catch (error) {
console.error(t('developer.initActivityChartError'), error) console.error(t('admin.initActivityChartError'), error)
} }
} }
@@ -478,14 +478,14 @@ onUnmounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.dashboard')" :title="t('admin.dashboard')"
:description="`${t('developer.welcomeBack')}, ${currentUser?.username || t('developer.developer')}`" :description="`${t('admin.welcomeBack')}, ${currentUser?.username || t('admin.developer')}`"
> >
<div v-if="loading" class="flex items-center justify-center py-12"> <div v-if="loading" class="flex items-center justify-center py-12">
<div class="text-center"> <div class="text-center">
<Icon icon="lucide:loader-2" class="size-8 animate-spin mx-auto text-muted-foreground" /> <Icon icon="lucide:loader-2" class="size-8 animate-spin mx-auto text-muted-foreground" />
<p class="mt-4 text-muted-foreground"> <p class="mt-4 text-muted-foreground">
{{ t('developer.loading') }}... {{ t('admin.loading') }}...
</p> </p>
</div> </div>
</div> </div>
@@ -495,7 +495,7 @@ onUnmounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.totalApplications') }} {{ t('admin.totalApplications') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" /> <Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -504,7 +504,7 @@ onUnmounted(() => {
{{ formatNumber(stats.totalApplications) }} {{ formatNumber(stats.totalApplications) }}
</div> </div>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
+12.5% {{ t('developer.lastMonth') }} +12.5% {{ t('admin.lastMonth') }}
</p> </p>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -512,7 +512,7 @@ onUnmounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.totalCards') }} {{ t('admin.totalCards') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:key" class="size-4 text-muted-foreground" /> <Icon icon="lucide:key" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -521,7 +521,7 @@ onUnmounted(() => {
{{ formatNumber(stats.totalCards) }} {{ formatNumber(stats.totalCards) }}
</div> </div>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
+8.2% {{ t('developer.lastMonth') }} +8.2% {{ t('admin.lastMonth') }}
</p> </p>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -529,7 +529,7 @@ onUnmounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.totalUsers') }} {{ t('admin.totalUsers') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:users" class="size-4 text-muted-foreground" /> <Icon icon="lucide:users" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -538,7 +538,7 @@ onUnmounted(() => {
{{ formatNumber(totalUsers) }} {{ formatNumber(totalUsers) }}
</div> </div>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
+15.3% {{ t('developer.lastMonth') }} +15.3% {{ t('admin.lastMonth') }}
</p> </p>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -546,7 +546,7 @@ onUnmounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.monthlyRevenue') }} {{ t('admin.monthlyRevenue') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" /> <Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -555,7 +555,7 @@ onUnmounted(() => {
¥{{ formatNumber(stats.monthlyRevenue) }} ¥{{ formatNumber(stats.monthlyRevenue) }}
</div> </div>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
+23.1% {{ t('developer.lastMonth') }} +23.1% {{ t('admin.lastMonth') }}
</p> </p>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -566,17 +566,17 @@ onUnmounted(() => {
<UiCardHeader> <UiCardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<UiCardTitle>{{ t('developer.userDistribution') }}</UiCardTitle> <UiCardTitle>{{ t('admin.userDistribution') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.userDistributionDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.userDistributionDesc') }}</UiCardDescription>
</div> </div>
<div class="flex items-center gap-4 text-sm"> <div class="flex items-center gap-4 text-sm">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="size-3 rounded-full bg-blue-500" /> <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>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="size-3 rounded-full bg-green-500" /> <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> </div>
</div> </div>
@@ -590,8 +590,8 @@ onUnmounted(() => {
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.onlineTrend') }}</UiCardTitle> <UiCardTitle>{{ t('admin.onlineTrend') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.onlineTrendDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.onlineTrendDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent class="pl-2"> <UiCardContent class="pl-2">
<div class="relative h-[350px]"> <div class="relative h-[350px]">
@@ -605,11 +605,11 @@ onUnmounted(() => {
<UiCardHeader> <UiCardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<UiCardTitle>{{ t('developer.recentTickets') }}</UiCardTitle> <UiCardTitle>{{ t('admin.recentTickets') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.recentTicketsDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.recentTicketsDesc') }}</UiCardDescription>
</div> </div>
<UiButton variant="ghost" size="sm" @click="goToTickets"> <UiButton variant="ghost" size="sm" @click="goToTickets">
{{ t('developer.viewAll') }} {{ t('admin.viewAll') }}
<Icon icon="lucide:arrow-right" class="ml-1 size-4" /> <Icon icon="lucide:arrow-right" class="ml-1 size-4" />
</UiButton> </UiButton>
</div> </div>
@@ -646,7 +646,7 @@ onUnmounted(() => {
<div v-else class="text-center py-8 text-muted-foreground"> <div v-else class="text-center py-8 text-muted-foreground">
<Icon icon="lucide:ticket" class="mx-auto size-10 text-muted-foreground mb-2" /> <Icon icon="lucide:ticket" class="mx-auto size-10 text-muted-foreground mb-2" />
<p class="text-sm"> <p class="text-sm">
{{ t('developer.noTickets') }} {{ t('admin.noTickets') }}
</p> </p>
</div> </div>
</UiCardContent> </UiCardContent>
@@ -654,8 +654,8 @@ onUnmounted(() => {
<UiCard> <UiCard>
<UiCardHeader> <UiCardHeader>
<UiCardTitle>{{ t('developer.recentActivities') }}</UiCardTitle> <UiCardTitle>{{ t('admin.recentActivities') }}</UiCardTitle>
<UiCardDescription>{{ t('developer.recentActivitiesDesc') }}</UiCardDescription> <UiCardDescription>{{ t('admin.recentActivitiesDesc') }}</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent>
<div v-if="recentActivities.length > 0" class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> <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"> <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" /> <Icon icon="lucide:activity" class="mx-auto size-10 text-muted-foreground mb-2" />
<p class="text-sm"> <p class="text-sm">
{{ t('developer.noActivities') }} {{ t('admin.noActivities') }}
</p> </p>
</div> </div>
</UiCardContent> </UiCardContent>
+14 -14
View File
@@ -72,14 +72,14 @@ const applicationOptions = computed(() => {
}) })
const typeOptions = computed(() => [ const typeOptions = computed(() => [
{ label: t('developer.logs.types.operation'), value: 'operation' }, { label: t('admin.logs.types.operation'), value: 'operation' },
{ label: t('developer.logs.types.verification'), value: 'verification' }, { label: t('admin.logs.types.verification'), value: 'verification' },
{ label: t('developer.logs.types.exception'), value: 'exception' }, { label: t('admin.logs.types.exception'), value: 'exception' },
]) ])
const statusOptions = computed(() => [ const statusOptions = computed(() => [
{ label: t('developer.success'), value: 'success' }, { label: t('admin.success'), value: 'success' },
{ label: t('developer.failed'), value: 'failed' }, { label: t('admin.failed'), value: 'failed' },
]) ])
async function fetchApplications() { async function fetchApplications() {
@@ -115,8 +115,8 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.logs.title')" :title="t('admin.logs.title')"
:description="t('developer.logs.description')" :description="t('admin.logs.description')"
sticky sticky
> >
<div class="space-y-6"> <div class="space-y-6">
@@ -124,7 +124,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.totalLogs') }} {{ t('admin.logs.totalLogs') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:file-text" class="size-4 text-muted-foreground" /> <Icon icon="lucide:file-text" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -138,7 +138,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.successCount') }} {{ t('admin.logs.successCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -152,7 +152,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.failedCount') }} {{ t('admin.logs.failedCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -166,7 +166,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.operationCount') }} {{ t('admin.logs.operationCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" /> <Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -188,17 +188,17 @@ onMounted(() => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.logs.columns.application')" :title="t('admin.logs.columns.application')"
:options="applicationOptions" :options="applicationOptions"
/> />
<SingleFilter <SingleFilter
v-model="typeFilter" v-model="typeFilter"
:title="t('developer.logs.columns.type')" :title="t('admin.logs.columns.type')"
:options="typeOptions" :options="typeOptions"
/> />
<SingleFilter <SingleFilter
v-model="statusFilter" v-model="statusFilter"
:title="t('developer.logs.columns.status')" :title="t('admin.logs.columns.status')"
:options="statusOptions" :options="statusOptions"
/> />
<DateRangeFilter <DateRangeFilter
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
import Badge from '@/components/ui/badge/Badge.vue' 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 { function translateDetails(t: Composer['t'], details: string): string {
if (!details) return '-' if (!details) return '-'
@@ -16,222 +16,222 @@ function translateDetails(t: Composer['t'], details: string): string {
}> = [ }> = [
{ {
pattern: /^用户注册: (.+)$/, pattern: /^用户注册: (.+)$/,
template: 'developer.logs.details.register', template: 'admin.logs.details.register',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^注册失败: 用户已存在 - (.+)$/, pattern: /^注册失败: 用户已存在 - (.+)$/,
template: 'developer.logs.details.register_failed_exists', template: 'admin.logs.details.register_failed_exists',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^用户登录: (.+)$/, pattern: /^用户登录: (.+)$/,
template: 'developer.logs.details.login', template: 'admin.logs.details.login',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^登录失败: 用户不存在 - (.+)$/, pattern: /^登录失败: 用户不存在 - (.+)$/,
template: 'developer.logs.details.login_failed_not_found', template: 'admin.logs.details.login_failed_not_found',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^登录失败: 密码错误 - (.+)$/, pattern: /^登录失败: 密码错误 - (.+)$/,
template: 'developer.logs.details.login_failed_password', template: 'admin.logs.details.login_failed_password',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^登录失败: 余额不足 - (.+)$/, pattern: /^登录失败: 余额不足 - (.+)$/,
template: 'developer.logs.details.login_failed_balance', template: 'admin.logs.details.login_failed_balance',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^登录失败: 设备绑定数量已达上限 - (.+)$/, pattern: /^登录失败: 设备绑定数量已达上限 - (.+)$/,
template: 'developer.logs.details.login_failed_device_limit', template: 'admin.logs.details.login_failed_device_limit',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^登录失败: 多开数量已达上限 - (.+)$/, pattern: /^登录失败: 多开数量已达上限 - (.+)$/,
template: 'developer.logs.details.login_failed_multi_limit', template: 'admin.logs.details.login_failed_multi_limit',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^用户充值: (.+), 卡密: (.+), 金额: ([\d.]+)$/, pattern: /^用户充值: (.+), 卡密: (.+), 金额: ([\d.]+)$/,
template: 'developer.logs.details.recharge', template: 'admin.logs.details.recharge',
extract: (m) => ({ username: m[1], card: m[2], amount: m[3] }), extract: (m) => ({ username: m[1], card: m[2], amount: m[3] }),
}, },
{ {
pattern: /^充值失败: 用户不存在 - (.+)$/, pattern: /^充值失败: 用户不存在 - (.+)$/,
template: 'developer.logs.details.recharge_failed_user_not_found', template: 'admin.logs.details.recharge_failed_user_not_found',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^充值失败: 用户已是永久会员 - (.+)$/, pattern: /^充值失败: 用户已是永久会员 - (.+)$/,
template: 'developer.logs.details.recharge_failed_permanent', template: 'admin.logs.details.recharge_failed_permanent',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^充值失败: 卡密不存在 - (.+)$/, pattern: /^充值失败: 卡密不存在 - (.+)$/,
template: 'developer.logs.details.recharge_failed_card_not_found', template: 'admin.logs.details.recharge_failed_card_not_found',
extract: (m) => ({ card: m[1] }), extract: (m) => ({ card: m[1] }),
}, },
{ {
pattern: /^充值失败: 卡密已使用 - (.+)$/, pattern: /^充值失败: 卡密已使用 - (.+)$/,
template: 'developer.logs.details.recharge_failed_card_used', template: 'admin.logs.details.recharge_failed_card_used',
extract: (m) => ({ card: m[1] }), extract: (m) => ({ card: m[1] }),
}, },
{ {
pattern: /^用户解绑设备: (.+)$/, pattern: /^用户解绑设备: (.+)$/,
template: 'developer.logs.details.unbind_device', template: 'admin.logs.details.unbind_device',
extract: (m) => ({ device: m[1] }), extract: (m) => ({ device: m[1] }),
}, },
{ {
pattern: /^扩展API充值: 用户(.+), 类型:(.+), 数量:(\d+)$/, 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] }), extract: (m) => ({ username: m[1], type: m[2], amount: m[3] }),
}, },
{ {
pattern: /^扩展API充值失败: 用户不存在 - (.+)$/, 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] }), extract: (m) => ({ userId: m[1] }),
}, },
{ {
pattern: /^扩展API充值失败: 用户已是永久会员 - (.+)$/, pattern: /^扩展API充值失败: 用户已是永久会员 - (.+)$/,
template: 'developer.logs.details.extension_recharge_failed_permanent', template: 'admin.logs.details.extension_recharge_failed_permanent',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^扩展API充值失败: 类型无效 - (.+)$/, pattern: /^扩展API充值失败: 类型无效 - (.+)$/,
template: 'developer.logs.details.extension_recharge_failed_type', template: 'admin.logs.details.extension_recharge_failed_type',
extract: (m) => ({ type: m[1] }), extract: (m) => ({ type: m[1] }),
}, },
{ {
pattern: /^扩展API充值失败: 保存失败 - (.+)$/, pattern: /^扩展API充值失败: 保存失败 - (.+)$/,
template: 'developer.logs.details.extension_recharge_failed_save', template: 'admin.logs.details.extension_recharge_failed_save',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^扩展API扣费: 用户(.+), 类型:(.+), 数量:(\d+)$/, 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] }), extract: (m) => ({ username: m[1], type: m[2], amount: m[3] }),
}, },
{ {
pattern: /^扩展API扣费失败: 用户不存在 - (.+)$/, 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] }), extract: (m) => ({ userId: m[1] }),
}, },
{ {
pattern: /^扩展API扣费失败: 用户已是永久会员 - (.+)$/, pattern: /^扩展API扣费失败: 用户已是永久会员 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_permanent', template: 'admin.logs.details.extension_deduct_failed_permanent',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^扩展API扣费失败: 用户订阅已过期 - (.+)$/, pattern: /^扩展API扣费失败: 用户订阅已过期 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_expired', template: 'admin.logs.details.extension_deduct_failed_expired',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^扩展API扣费失败: 余额不足 - (.+)$/, pattern: /^扩展API扣费失败: 余额不足 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_balance', template: 'admin.logs.details.extension_deduct_failed_balance',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^扩展API扣费失败: 类型无效 - (.+)$/, pattern: /^扩展API扣费失败: 类型无效 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_type', template: 'admin.logs.details.extension_deduct_failed_type',
extract: (m) => ({ type: m[1] }), extract: (m) => ({ type: m[1] }),
}, },
{ {
pattern: /^扩展API扣费失败: 保存失败 - (.+)$/, pattern: /^扩展API扣费失败: 保存失败 - (.+)$/,
template: 'developer.logs.details.extension_deduct_failed_save', template: 'admin.logs.details.extension_deduct_failed_save',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^创建应用: (.+)$/, pattern: /^创建应用: (.+)$/,
template: 'developer.logs.details.create_application', template: 'admin.logs.details.create_application',
extract: (m) => ({ name: m[1] }), extract: (m) => ({ name: m[1] }),
}, },
{ {
pattern: /^更新应用: (.+)$/, pattern: /^更新应用: (.+)$/,
template: 'developer.logs.details.update_application', template: 'admin.logs.details.update_application',
extract: (m) => ({ name: m[1] }), extract: (m) => ({ name: m[1] }),
}, },
{ {
pattern: /^删除应用: (.+)$/, pattern: /^删除应用: (.+)$/,
template: 'developer.logs.details.delete_application', template: 'admin.logs.details.delete_application',
extract: (m) => ({ name: m[1] }), extract: (m) => ({ name: m[1] }),
}, },
{ {
pattern: /^创建用户: (.+) \(应用: (.+)\)$/, pattern: /^创建用户: (.+) \(应用: (.+)\)$/,
template: 'developer.logs.details.create_user', template: 'admin.logs.details.create_user',
extract: (m) => ({ username: m[1], app: m[2] }), extract: (m) => ({ username: m[1], app: m[2] }),
}, },
{ {
pattern: /^更新用户: (.+)$/, pattern: /^更新用户: (.+)$/,
template: 'developer.logs.details.update_user', template: 'admin.logs.details.update_user',
extract: (m) => ({ username: m[1] }), extract: (m) => ({ username: m[1] }),
}, },
{ {
pattern: /^删除用户: (.+) \(应用: (.+)\)$/, pattern: /^删除用户: (.+) \(应用: (.+)\)$/,
template: 'developer.logs.details.delete_user', template: 'admin.logs.details.delete_user',
extract: (m) => ({ username: m[1], app: m[2] }), extract: (m) => ({ username: m[1], app: m[2] }),
}, },
{ {
pattern: /^解绑设备: (.+) \(应用: (.+)\)$/, pattern: /^解绑设备: (.+) \(应用: (.+)\)$/,
template: 'developer.logs.details.unbind_device_app', template: 'admin.logs.details.unbind_device_app',
extract: (m) => ({ device: m[1], app: m[2] }), extract: (m) => ({ device: m[1], app: m[2] }),
}, },
{ {
pattern: /^批量解绑设备: (\d+)个$/, pattern: /^批量解绑设备: (\d+)个$/,
template: 'developer.logs.details.batch_unbind', template: 'admin.logs.details.batch_unbind',
extract: (m) => ({ count: m[1] }), extract: (m) => ({ count: m[1] }),
}, },
{ {
pattern: /^创建工单: (.+)$/, pattern: /^创建工单: (.+)$/,
template: 'developer.logs.details.create_ticket', template: 'admin.logs.details.create_ticket',
extract: (m) => ({ title: m[1] }), extract: (m) => ({ title: m[1] }),
}, },
{ {
pattern: /^删除工单: (.+)$/, pattern: /^删除工单: (.+)$/,
template: 'developer.logs.details.delete_ticket', template: 'admin.logs.details.delete_ticket',
extract: (m) => ({ title: m[1] }), extract: (m) => ({ title: m[1] }),
}, },
{ {
pattern: /^创建Webhook: (.+)$/, pattern: /^创建Webhook: (.+)$/,
template: 'developer.logs.details.create_webhook', template: 'admin.logs.details.create_webhook',
extract: (m) => ({ name: m[1] }), extract: (m) => ({ name: m[1] }),
}, },
{ {
pattern: /^删除Webhook: (.+)$/, pattern: /^删除Webhook: (.+)$/,
template: 'developer.logs.details.delete_webhook', template: 'admin.logs.details.delete_webhook',
extract: (m) => ({ name: m[1] }), extract: (m) => ({ name: m[1] }),
}, },
{ {
pattern: /^创建云端常量: (.+)$/, pattern: /^创建云端常量: (.+)$/,
template: 'developer.logs.details.create_cloud_constant', template: 'admin.logs.details.create_cloud_constant',
extract: (m) => ({ key: m[1] }), extract: (m) => ({ key: m[1] }),
}, },
{ {
pattern: /^更新云端常量: (.+)$/, pattern: /^更新云端常量: (.+)$/,
template: 'developer.logs.details.update_cloud_constant', template: 'admin.logs.details.update_cloud_constant',
extract: (m) => ({ key: m[1] }), extract: (m) => ({ key: m[1] }),
}, },
{ {
pattern: /^删除云端常量: (.+)$/, pattern: /^删除云端常量: (.+)$/,
template: 'developer.logs.details.delete_cloud_constant', template: 'admin.logs.details.delete_cloud_constant',
extract: (m) => ({ key: m[1] }), extract: (m) => ({ key: m[1] }),
}, },
{ {
pattern: /^创建卡密类型: (.+)$/, pattern: /^创建卡密类型: (.+)$/,
template: 'developer.logs.details.create_card_type', template: 'admin.logs.details.create_card_type',
extract: (m) => ({ name: m[1] }), extract: (m) => ({ name: m[1] }),
}, },
{ {
pattern: /^更新卡密类型: (.+)$/, pattern: /^更新卡密类型: (.+)$/,
template: 'developer.logs.details.update_card_type', template: 'admin.logs.details.update_card_type',
extract: (m) => ({ name: m[1] }), extract: (m) => ({ name: m[1] }),
}, },
{ {
pattern: /^删除卡密类型: (.+)$/, pattern: /^删除卡密类型: (.+)$/,
template: 'developer.logs.details.delete_card_type', template: 'admin.logs.details.delete_card_type',
extract: (m) => ({ name: m[1] }), extract: (m) => ({ name: m[1] }),
}, },
] ]
@@ -251,13 +251,13 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
return [ return [
{ {
accessorKey: 'log_type', accessorKey: 'log_type',
header: () => t('developer.logs.columns.type'), header: () => t('admin.logs.columns.type'),
cell: ({ row }) => { cell: ({ row }) => {
const type = row.getValue('log_type') as string const type = row.getValue('log_type') as string
const typeMap: Record<string, { text: string, variant: any }> = { const typeMap: Record<string, { text: string, variant: any }> = {
operation: { text: t('developer.logs.types.operation'), variant: 'default' }, operation: { text: t('admin.logs.types.operation'), variant: 'default' },
verification: { text: t('developer.logs.types.verification'), variant: 'secondary' }, verification: { text: t('admin.logs.types.verification'), variant: 'secondary' },
exception: { text: t('developer.logs.types.exception'), variant: 'destructive' }, exception: { text: t('admin.logs.types.exception'), variant: 'destructive' },
} }
const { text, variant } = typeMap[type] || { text: type, variant: 'default' } const { text, variant } = typeMap[type] || { text: type, variant: 'default' }
return h(Badge, { variant }, () => text) return h(Badge, { variant }, () => text)
@@ -265,18 +265,18 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
}, },
{ {
accessorKey: 'action', accessorKey: 'action',
header: () => t('developer.logs.columns.action'), header: () => t('admin.logs.columns.action'),
cell: ({ row }) => { cell: ({ row }) => {
const action = row.getValue('action') as string const action = row.getValue('action') as string
if (!action) return '-' if (!action) return '-'
const actionKey = `developer.logs.actions.${action}` const actionKey = `dmin.logs.actions.${action}`
const translated = t(actionKey) const translated = t(actionKey)
return translated === actionKey ? action : translated return translated === actionKey ? action : translated
}, },
}, },
{ {
accessorKey: 'details', accessorKey: 'details',
header: () => t('developer.logs.columns.detail'), header: () => t('admin.logs.columns.detail'),
cell: ({ row }) => { cell: ({ row }) => {
const details = row.getValue('details') as string const details = row.getValue('details') as string
if (!details) return '-' if (!details) return '-'
@@ -286,12 +286,12 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
}, },
{ {
accessorKey: 'status', accessorKey: 'status',
header: () => t('developer.logs.columns.status'), header: () => t('admin.logs.columns.status'),
cell: ({ row }) => { cell: ({ row }) => {
const status = row.getValue('status') as string const status = row.getValue('status') as string
const statusMap: Record<string, { text: string, variant: any }> = { const statusMap: Record<string, { text: string, variant: any }> = {
success: { text: t('developer.success'), variant: 'default' }, success: { text: t('admin.success'), variant: 'default' },
failed: { text: t('developer.failed'), variant: 'destructive' }, failed: { text: t('admin.failed'), variant: 'destructive' },
} }
const { text, variant } = statusMap[status] || { text: status, variant: 'default' } const { text, variant } = statusMap[status] || { text: status, variant: 'default' }
return h(Badge, { variant }, () => text) return h(Badge, { variant }, () => text)
@@ -299,7 +299,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
}, },
{ {
accessorKey: 'ip_address', accessorKey: 'ip_address',
header: () => t('developer.logs.columns.ip'), header: () => t('admin.logs.columns.ip'),
cell: ({ row }) => { cell: ({ row }) => {
const ip = row.getValue('ip_address') as string const ip = row.getValue('ip_address') as string
return h('code', { class: 'text-xs font-mono' }, ip || '-') return h('code', { class: 'text-xs font-mono' }, ip || '-')
@@ -307,7 +307,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
}, },
{ {
accessorKey: 'application.name', accessorKey: 'application.name',
header: () => t('developer.logs.columns.application'), header: () => t('admin.logs.columns.application'),
cell: ({ row }) => { cell: ({ row }) => {
const appName = row.original.application?.name const appName = row.original.application?.name
return appName ? h(Badge, { variant: 'outline' }, () => appName) : '-' return appName ? h(Badge, { variant: 'outline' }, () => appName) : '-'
@@ -315,7 +315,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
}, },
{ {
accessorKey: 'user.username', accessorKey: 'user.username',
header: () => t('developer.logs.columns.user'), header: () => t('admin.logs.columns.user'),
cell: ({ row }) => { cell: ({ row }) => {
const username = row.original.user?.username || row.original.app_user?.username const username = row.original.user?.username || row.original.app_user?.username
return username || '-' return username || '-'
@@ -323,7 +323,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
}, },
{ {
accessorKey: 'created_at', accessorKey: 'created_at',
header: () => t('developer.logs.columns.time'), header: () => t('admin.logs.columns.time'),
cell: ({ row }) => { cell: ({ row }) => {
const createdAt = row.getValue('created_at') const createdAt = row.getValue('created_at')
if (!createdAt) return '-' if (!createdAt) return '-'
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
<template> <template>
<div class="flex items-center flex-1 space-x-2"> <div class="flex items-center flex-1 space-x-2">
<Input <Input
:placeholder="t('developer.logs.searchPlaceholder')" :placeholder="t('admin.logs.searchPlaceholder')"
:model-value="(table.getColumn('action')?.getFilterValue() as string) ?? ''" :model-value="(table.getColumn('action')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]" class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('action')?.setFilterValue($event.target.value)" @input="table.getColumn('action')?.setFilterValue($event.target.value)"
@@ -44,15 +44,15 @@ const table = generateVueTable<Log>({
}, columns.value) }, columns.value)
const columnLabels: Record<string, string> = { const columnLabels: Record<string, string> = {
'select': 'developer.logs.select', 'select': 'admin.logs.select',
'type': 'developer.logs.columns.type', 'type': 'admin.logs.columns.type',
'action': 'developer.logs.columns.action', 'action': 'admin.logs.columns.action',
'details': 'developer.logs.columns.detail', 'details': 'admin.logs.columns.detail',
'status': 'developer.logs.columns.status', 'status': 'admin.logs.columns.status',
'ip': 'developer.logs.columns.ip', 'ip': 'admin.logs.columns.ip',
'application_name': 'developer.logs.columns.application', 'application_name': 'admin.logs.columns.application',
'user_username': 'developer.logs.columns.user', 'user_username': 'admin.logs.columns.user',
'created_at': 'developer.logs.columns.time', 'created_at': 'admin.logs.columns.time',
} }
const serverPagination = computed(() => { const serverPagination = computed(() => {
+17 -17
View File
@@ -49,16 +49,16 @@ const applicationOptions = computed(() => {
}) })
const typeOptions = computed(() => [ const typeOptions = computed(() => [
{ label: t('developer.logs.types.all'), value: 'all' }, { label: t('admin.logs.types.all'), value: 'all' },
{ label: t('developer.logs.types.operation'), value: 'operation' }, { label: t('admin.logs.types.operation'), value: 'operation' },
{ label: t('developer.logs.types.verification'), value: 'verification' }, { label: t('admin.logs.types.verification'), value: 'verification' },
{ label: t('developer.logs.types.exception'), value: 'exception' }, { label: t('admin.logs.types.exception'), value: 'exception' },
]) ])
const statusOptions = computed(() => [ const statusOptions = computed(() => [
{ label: t('developer.logs.statuses.all'), value: 'all' }, { label: t('admin.logs.statuses.all'), value: 'all' },
{ label: t('developer.success'), value: 'success' }, { label: t('admin.success'), value: 'success' },
{ label: t('developer.failed'), value: 'failed' }, { label: t('admin.failed'), value: 'failed' },
]) ])
async function fetchApplications() { async function fetchApplications() {
@@ -125,8 +125,8 @@ onMounted(() => {
<template> <template>
<BasicPage <BasicPage
:title="t('developer.logs.title')" :title="t('admin.logs.title')"
:description="t('developer.logs.description')" :description="t('admin.logs.description')"
sticky sticky
> >
<div class="space-y-6"> <div class="space-y-6">
@@ -134,7 +134,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.totalLogs') }} {{ t('admin.logs.totalLogs') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:file-text" class="size-4 text-muted-foreground" /> <Icon icon="lucide:file-text" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -148,7 +148,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.successCount') }} {{ t('admin.logs.successCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -162,7 +162,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.failedCount') }} {{ t('admin.logs.failedCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" /> <Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -176,7 +176,7 @@ onMounted(() => {
<UiCard> <UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium"> <UiCardTitle class="text-sm font-medium">
{{ t('developer.logs.operationCount') }} {{ t('admin.logs.operationCount') }}
</UiCardTitle> </UiCardTitle>
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" /> <Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
</UiCardHeader> </UiCardHeader>
@@ -205,17 +205,17 @@ onMounted(() => {
<template #filters> <template #filters>
<SingleFilter <SingleFilter
v-model="appFilter" v-model="appFilter"
:title="t('developer.logs.columns.application')" :title="t('admin.logs.columns.application')"
:options="[{ label: t('developer.logs.allApplications'), value: 'all' }, ...applicationOptions]" :options="[{ label: t('admin.logs.allApplications'), value: 'all' }, ...applicationOptions]"
/> />
<SingleFilter <SingleFilter
v-model="typeFilter" v-model="typeFilter"
:title="t('developer.logs.columns.type')" :title="t('admin.logs.columns.type')"
:options="typeOptions" :options="typeOptions"
/> />
<SingleFilter <SingleFilter
v-model="statusFilter" v-model="statusFilter"
:title="t('developer.logs.columns.status')" :title="t('admin.logs.columns.status')"
:options="statusOptions" :options="statusOptions"
/> />
<DateRangeFilter <DateRangeFilter
+240 -271
View File
@@ -1,320 +1,289 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' 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 { BasicPage } from '@/components/global-layout'
import DataTable from '@/pages/admin/risk-control/components/data-table.vue'
import api from '@/services/api' import api from '@/services/api'
const router = useRouter() const router = useRouter()
const route = useRoute()
const loading = ref(true) const loading = ref(true)
const saving = ref(false) const rules = ref<RiskRule[]>([])
const tableRef = ref()
const form = ref({ const typeFilter = ref<string>('')
type: 'ip' as 'ip' | 'device' | 'user' | 'region', const statusFilter = ref<string>('')
value: '',
reason: '', const deleteDialogOpen = ref(false)
expires_at: '', 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 }[] = [ const activeCount = computed(() => filteredRules.value.filter(rule => rule.status === 'active').length)
{ value: 'ip', label: 'IP封禁', icon: 'lucide:globe', description: '封禁特定IP地址或IP段', placeholder: '输入IP地址,如 192.168.1.1 或 192.168.1.0/24' }, const inactiveCount = computed(() => filteredRules.value.filter(rule => rule.status === 'inactive').length)
{ value: 'device', label: '设备封禁', icon: 'lucide:smartphone', description: '封禁特定设备指纹', placeholder: '输入设备指纹' }, const ipCount = computed(() => filteredRules.value.filter(rule => rule.type === 'ip').length)
{ value: 'user', label: '用户封禁', icon: 'lucide:user-x', description: '封禁特定用户账号', placeholder: '输入用户ID或用户名' }, const deviceCount = computed(() => filteredRules.value.filter(rule => rule.type === 'device').length)
{ 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
}
async function fetchRules() {
loading.value = true loading.value = true
try { try {
const data = await api.get<any>(`/dev/risk-control/rules`) const data = await api.get<RiskRule[]>('/dev/risk-control/rules')
const rules = Array.isArray(data) ? data : [] rules.value = 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) : '',
}
} }
catch (error) { catch (error) {
console.error('获取规则详情失败:', error) console.error('获取风控规则失败:', error)
toast.error('获取规则详情失败') rules.value = []
router.push('/developer/risk-control')
} }
finally { finally {
loading.value = false loading.value = false
} }
} }
async function handleSave() { function goToCreate() {
if (!form.value.value.trim()) { router.push('/admin/risk-control/create')
toast.error('请输入封禁值') }
return
}
const id = route.params.id function openEditPage(rule: RiskRule) {
saving.value = true router.push(`/admin/risk-control/${rule.id}`)
}
async function toggleStatus(rule: RiskRule) {
try { try {
await api.put(`/dev/risk-control/rules/${id}`, { const newStatus = rule.status === 'active' ? 'inactive' : 'active'
type: form.value.type, await api.put(`/dev/risk-control/rules/${rule.id}/status`, { status: newStatus })
value: form.value.value, toast.success('状态更新成功')
reason: form.value.reason, fetchRules()
expires_at: form.value.expires_at || null,
})
toast.success('更新成功')
router.push('/developer/risk-control')
} }
catch (error: any) { catch (error: any) {
console.error('更新风控规则失败:', error) console.error('切换状态失败:', error)
toast.error(error.message || '更新失败') 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 { 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(() => { onMounted(() => {
fetchRule() fetchRules()
}) })
</script> </script>
<template> <template>
<BasicPage <BasicPage
title="编辑规则" title="风控管理"
description="编辑现有风控规则" description="管理封禁的IP、设备、用户等,保护应用安全"
:breadcrumbs="[
{ title: '风控管理', href: '/developer/risk-control' },
{ title: '编辑规则' },
]"
sticky sticky
> >
<div v-if="loading" class="flex items-center justify-center py-12"> <template #actions>
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" /> <UiButton @click="goToCreate">
</div> <Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
添加规则
</UiButton>
</template>
<div v-else class="space-y-6"> <div class="space-y-6">
<div class="grid gap-6 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="lg:col-span-2 space-y-6"> <UiCard>
<UiCard> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardHeader> <UiCardTitle class="text-sm font-medium">
<UiCardTitle class="flex items-center gap-2"> 生效中
<Icon icon="lucide:shield-edit" class="size-5" /> </UiCardTitle>
规则配置 <Icon icon="lucide:shield-check" class="size-4 text-muted-foreground" />
</UiCardTitle> </UiCardHeader>
<UiCardDescription>修改风控规则的类型和封禁目标</UiCardDescription> <UiCardContent>
</UiCardHeader> <div class="text-2xl font-bold">
<UiCardContent class="space-y-6"> {{ activeCount }}
<div class="space-y-3"> </div>
<UiLabel>规则类型</UiLabel> </UiCardContent>
<div class="grid gap-3 sm:grid-cols-2"> </UiCard>
<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-2"> <UiCard>
<UiLabel for="value"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
封禁值 <UiCardTitle class="text-sm font-medium">
</UiLabel> 已失效
<CountrySelect </UiCardTitle>
v-if="form.type === 'region'" <Icon icon="lucide:shield-off" class="size-4 text-muted-foreground" />
v-model="form.value" </UiCardHeader>
/> <UiCardContent>
<UiInput <div class="text-2xl font-bold">
v-else {{ inactiveCount }}
id="value" </div>
:key="form.type" </UiCardContent>
v-model="form.value" </UiCard>
:placeholder="getTypeInfo(form.type).placeholder"
/>
<p class="text-xs text-muted-foreground">
{{ form.type === 'region' ? '选择要封禁的国家或地区' : '根据规则类型输入对应的封禁目标' }}
</p>
</div>
<div class="space-y-2"> <UiCard>
<UiLabel for="reason"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
封禁原因 <UiCardTitle class="text-sm font-medium">
</UiLabel> IP封禁
<UiInput id="reason" v-model="form.reason" placeholder="输入封禁原因(可选)" /> </UiCardTitle>
</div> <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"> <UiCard>
<UiLabel for="expires"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
过期时间 <UiCardTitle class="text-sm font-medium">
</UiLabel> 设备封禁
<UiInput id="expires" v-model="form.expires_at" type="datetime-local" /> </UiCardTitle>
<p class="text-xs text-muted-foreground"> <Icon icon="lucide:smartphone" class="size-4 text-muted-foreground" />
留空表示永久封禁 </UiCardHeader>
</p> <UiCardContent>
</div> <div class="text-2xl font-bold">
</UiCardContent> {{ deviceCount }}
</UiCard> </div>
</div> </UiCardContent>
</UiCard>
<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>
</div> </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> </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> </BasicPage>
</template> </template>
@@ -3,7 +3,7 @@ import type { ColumnDef } from '@tanstack/vue-table'
import { MoreHorizontal, Pencil, ShieldCheck, ShieldOff, Trash2 } from 'lucide-vue-next' import { MoreHorizontal, Pencil, ShieldCheck, ShieldOff, Trash2 } from 'lucide-vue-next'
import { h } from 'vue' 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 { Copy } from '@/components/sva-ui/copy'
import Badge from '@/components/ui/badge/Badge.vue' import Badge from '@/components/ui/badge/Badge.vue'
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Table } from '@tanstack/vue-table' 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' import { Input } from '@/components/ui/input'
@@ -2,15 +2,15 @@
import type { ColumnDef } from '@tanstack/vue-table' import type { ColumnDef } from '@tanstack/vue-table'
import type { DataTableProps } from '@/components/data-table/types' 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 BulkActions from '@/components/data-table/bulk-actions.vue'
import DataTable from '@/components/data-table/data-table.vue' import DataTable from '@/components/data-table/data-table.vue'
import { SelectColumn } from '@/components/data-table/table-columns' import { SelectColumn } from '@/components/data-table/table-columns'
import { generateVueTable } from '@/components/data-table/use-generate-vue-table' import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
import DataTableViewOptions from '@/components/data-table/view-options.vue' import DataTableViewOptions from '@/components/data-table/view-options.vue'
import { getColumns } from '@/pages/developer/risk-control/components/columns' import { getColumns } from '@/pages/admin/risk-control/components/columns'
import DataTableToolbar from '@/pages/developer/risk-control/components/data-table-toolbar.vue' import DataTableToolbar from '@/pages/admin/risk-control/components/data-table-toolbar.vue'
const props = defineProps<Omit<DataTableProps<RiskRule>, 'columns'> & { const props = defineProps<Omit<DataTableProps<RiskRule>, 'columns'> & {
onToggleStatus: (row: RiskRule) => void onToggleStatus: (row: RiskRule) => void
@@ -165,7 +165,7 @@ async function handleSave() {
await api.post('/dev/risk-control/rules', payload) await api.post('/dev/risk-control/rules', payload)
toast.success('添加成功') toast.success('添加成功')
router.push('/developer/risk-control') router.push('/admin/risk-control')
} }
catch (error: any) { catch (error: any) {
console.error('添加风控规则失败:', error) console.error('添加风控规则失败:', error)
@@ -182,7 +182,7 @@ async function handleSave() {
title="添加规则" title="添加规则"
description="创建新的风控规则" description="创建新的风控规则"
:breadcrumbs="[ :breadcrumbs="[
{ title: '风控管理', href: '/developer/risk-control' }, { title: '风控管理', href: '/admin/risk-control' },
{ title: '添加规则' }, { title: '添加规则' },
]" ]"
sticky sticky
@@ -4,12 +4,12 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
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 ConfirmDialog from '@/components/confirm-dialog.vue'
import SingleFilter from '@/components/data-table/single-filter.vue' import SingleFilter from '@/components/data-table/single-filter.vue'
import { BasicPage } from '@/components/global-layout' 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' import api from '@/services/api'
const router = useRouter() const router = useRouter()
@@ -73,11 +73,11 @@ async function fetchRules() {
} }
function goToCreate() { function goToCreate() {
router.push('/developer/risk-control/create') router.push('/admin/risk-control/create')
} }
function openEditPage(rule: RiskRule) { function openEditPage(rule: RiskRule) {
router.push(`/developer/risk-control/${rule.id}`) router.push(`/admin/risk-control/${rule.id}`)
} }
async function toggleStatus(rule: RiskRule) { async function toggleStatus(rule: RiskRule) {

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