refactor: 移除佣金比例字段,将developer统一为admin
This commit is contained in:
@@ -28,7 +28,6 @@ type User struct {
|
||||
ApiToken string `gorm:"size:64" json:"api_token"`
|
||||
|
||||
ParentAgentID *uint `gorm:"index" json:"parent_agent_id"`
|
||||
Commission float64 `gorm:"default:0" json:"commission"`
|
||||
CanCreateAgent bool `gorm:"default:false" json:"can_create_agent"`
|
||||
Balance float64 `gorm:"default:0" json:"balance"`
|
||||
|
||||
@@ -557,7 +556,6 @@ type AgentApplication struct {
|
||||
AgentID uint `json:"agent_id"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
DeveloperID uint `json:"developer_id"`
|
||||
Commission float64 `gorm:"default:0.1" json:"commission"`
|
||||
Discount float64 `gorm:"default:1.0" json:"discount"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
IsReceived bool `gorm:"default:false" json:"is_received"`
|
||||
|
||||
@@ -80,7 +80,6 @@ func handleGetApps(c *gin.Context) {
|
||||
"description": aa.Application.Description,
|
||||
"status": aa.Application.Status,
|
||||
"authorized_at": aa.CreatedAt,
|
||||
"commission": aa.Commission,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -224,7 +223,6 @@ func handleGetFinance(c *gin.Context) {
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"balance": user.Balance,
|
||||
"commission": user.Commission,
|
||||
"records": records,
|
||||
})
|
||||
}
|
||||
@@ -244,7 +242,6 @@ func handleGetProfile(c *gin.Context) {
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"balance": user.Balance,
|
||||
"commission": user.Commission,
|
||||
"can_create_agent": user.CanCreateAgent,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -137,7 +137,6 @@ func handleGetAgentApps(c *gin.Context) {
|
||||
AgentEmail string `json:"agent_email"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
AppName string `json:"app_name"`
|
||||
Commission float64 `json:"commission"`
|
||||
Discount float64 `json:"discount"`
|
||||
Status string `json:"status"`
|
||||
CardTypes []CardTypeResponse `json:"card_types"`
|
||||
@@ -181,7 +180,6 @@ func handleGetAgentApps(c *gin.Context) {
|
||||
AgentEmail: agentEmail,
|
||||
ApplicationID: aa.ApplicationID,
|
||||
AppName: appName,
|
||||
Commission: aa.Commission,
|
||||
Discount: aa.Discount,
|
||||
Status: aa.Status,
|
||||
CardTypes: cardTypes,
|
||||
@@ -496,7 +494,6 @@ func handleApproveRequest(c *gin.Context) {
|
||||
AgentID: req.AgentID,
|
||||
ApplicationID: req.ApplicationID,
|
||||
DeveloperID: userID,
|
||||
Commission: 0.1,
|
||||
Discount: 1.0,
|
||||
Status: "active",
|
||||
IsReceived: true,
|
||||
@@ -606,7 +603,6 @@ func handleGetAgentAppDetail(c *gin.Context) {
|
||||
AgentEmail string `json:"agent_email"`
|
||||
ApplicationID uint `json:"application_id"`
|
||||
AppName string `json:"app_name"`
|
||||
Commission float64 `json:"commission"`
|
||||
Discount float64 `json:"discount"`
|
||||
Status string `json:"status"`
|
||||
CardTypes []CardTypeResponse `json:"card_types"`
|
||||
@@ -629,7 +625,6 @@ func handleGetAgentAppDetail(c *gin.Context) {
|
||||
AgentName: agent.Username,
|
||||
ApplicationID: agentApp.ApplicationID,
|
||||
AppName: application.Name,
|
||||
Commission: agentApp.Commission,
|
||||
Discount: agentApp.Discount,
|
||||
Status: agentApp.Status,
|
||||
CardTypes: cardTypes,
|
||||
@@ -648,7 +643,6 @@ func handleUpdateAgentApp(c *gin.Context) {
|
||||
agentAppID := c.Param("id")
|
||||
|
||||
var reqBody struct {
|
||||
Commission *float64 `json:"commission"`
|
||||
Discount *float64 `json:"discount"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
@@ -664,9 +658,6 @@ func handleUpdateAgentApp(c *gin.Context) {
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if reqBody.Commission != nil {
|
||||
updates["commission"] = *reqBody.Commission
|
||||
}
|
||||
if reqBody.Discount != nil {
|
||||
updates["discount"] = *reqBody.Discount
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ type AgentTreeNode struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastLoginAt string `json:"last_login_at"`
|
||||
Balance float64 `json:"balance"`
|
||||
Commission float64 `json:"commission"`
|
||||
CanCreateAgent bool `json:"can_create_agent"`
|
||||
CardsCount int `json:"cards_count"`
|
||||
ChildAgentsCount int `json:"child_agents_count"`
|
||||
@@ -69,7 +68,6 @@ func handleGetAgents(c *gin.Context) {
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastLoginAt string `json:"last_login_at"`
|
||||
Balance float64 `json:"balance"`
|
||||
Commission float64 `json:"commission"`
|
||||
CanCreateAgent bool `json:"can_create_agent"`
|
||||
CardsCount int `json:"cards_count"`
|
||||
ChildAgentsCount int `json:"child_agents_count"`
|
||||
@@ -110,7 +108,6 @@ func handleGetAgents(c *gin.Context) {
|
||||
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
LastLoginAt: lastLoginAt,
|
||||
Balance: user.Balance,
|
||||
Commission: user.Commission,
|
||||
CanCreateAgent: user.CanCreateAgent,
|
||||
CardsCount: int(cardsCount),
|
||||
ChildAgentsCount: int(childAgentsCount),
|
||||
@@ -185,7 +182,6 @@ func handleGetAgentsTree(c *gin.Context) {
|
||||
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
LastLoginAt: lastLoginAt,
|
||||
Balance: user.Balance,
|
||||
Commission: user.Commission,
|
||||
CanCreateAgent: user.CanCreateAgent,
|
||||
CardsCount: int(cardsCount),
|
||||
ChildAgentsCount: int(childAgentsCount),
|
||||
@@ -217,7 +213,6 @@ func handleCreateAgent(c *gin.Context) {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
ParentAgentID *uint `json:"parent_agent_id"`
|
||||
Commission float64 `json:"commission"`
|
||||
CanCreateAgent bool `json:"can_create_agent"`
|
||||
Balance float64 `json:"balance"`
|
||||
}
|
||||
@@ -259,7 +254,6 @@ func handleCreateAgent(c *gin.Context) {
|
||||
Role: "agent",
|
||||
Status: "active",
|
||||
ParentAgentID: req.ParentAgentID,
|
||||
Commission: req.Commission,
|
||||
CanCreateAgent: req.CanCreateAgent,
|
||||
Balance: req.Balance,
|
||||
}
|
||||
@@ -365,7 +359,6 @@ func handleGetAgentDetail(c *gin.Context) {
|
||||
"role": user.Role,
|
||||
"parent_agent_id": user.ParentAgentID,
|
||||
"parent_agent_name": parentAgentName,
|
||||
"commission": user.Commission,
|
||||
"can_create_agent": user.CanCreateAgent,
|
||||
"balance": user.Balance,
|
||||
"created_at": user.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
@@ -387,7 +380,6 @@ func handleUpdateAgent(c *gin.Context) {
|
||||
|
||||
var req struct {
|
||||
ParentAgentID *uint `json:"parent_agent_id"`
|
||||
Commission *float64 `json:"commission"`
|
||||
Email *string `json:"email"`
|
||||
Password *string `json:"password"`
|
||||
CanCreateAgent *bool `json:"can_create_agent"`
|
||||
@@ -414,9 +406,6 @@ func handleUpdateAgent(c *gin.Context) {
|
||||
if req.Email != nil {
|
||||
user.Email = req.Email
|
||||
}
|
||||
if req.Commission != nil {
|
||||
user.Commission = *req.Commission
|
||||
}
|
||||
if req.Password != nil && *req.Password != "" {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
|
||||
@@ -153,7 +153,7 @@ func main() {
|
||||
},
|
||||
"如何实现代理授权?": {
|
||||
TitleEn: "How to Implement Agent Authorization?",
|
||||
ContentEn: "# How to Implement Agent Authorization?\n\n## Overview\nAgent authorization allows you to delegate application management to agents.\n\n## Setup Steps\n\n### 1. Create Agent Application\n1. Go to Console > Agent Applications\n2. Click \"Create Agent Application\"\n3. Select the application to authorize\n4. Set commission rate\n\n### 2. Generate Agent Link\nAfter creation, you'll get an agent link:\n```\nhttps://platform.com/agent/AGENT_CODE\n```\n\n### 3. Agent Dashboard\nAgents can access their dashboard to:\n- View sales statistics\n- Generate card keys\n- Manage users",
|
||||
ContentEn: "# How to Implement Agent Authorization?\n\n## Overview\nAgent authorization allows you to delegate application management to agents.\n\n## Setup Steps\n\n### 1. Create Agent Application\n1. Go to Console > Agent Applications\n2. Click \"Create Agent Application\"\n3. Select the application to authorize\n4. Set discount rate\n\n### 2. Generate Agent Link\nAfter creation, you'll get an agent link:\n```\nhttps://platform.com/agent/AGENT_CODE\n```\n\n### 3. Agent Dashboard\nAgents can access their dashboard to:\n- View sales statistics\n- Generate card keys\n- Manage users",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ func main() {
|
||||
},
|
||||
"faq-agent": {
|
||||
TitleEn: "How to Implement Agent Authorization?",
|
||||
ContentEn: "# How to Implement Agent Authorization?\n\n## What is Agent Authorization?\n\nAgent authorization allows developers to authorize their applications to other developers, who can then generate card keys and sell them.\n\n## Authorization Process\n\n1. **Apply for Authorization**\n - Authorized party submits application to authorizer\n - Enter application ID\n - Wait for authorizer approval\n\n2. **Approve Authorization**\n - Authorizer views application\n - Set card key type permissions after approval\n - Set agent commission and discount\n\n3. **Generate Card Keys**\n - Authorized party selects authorized application\n - Select card key types with permission\n - Generate and sell card keys\n\n4. **Settle Commission**\n - Sales revenue is settled proportionally\n - Authorized party receives commission income\n\n## Permission Management\n\n- Authorizer can modify card key type permissions at any time\n- Can pause or terminate authorization\n- Can view agent's sales data",
|
||||
ContentEn: "# How to Implement Agent Authorization?\n\n## What is Agent Authorization?\n\nAgent authorization allows developers to authorize their applications to other developers, who can then generate card keys and sell them.\n\n## Authorization Process\n\n1. **Apply for Authorization**\n - Authorized party submits application to authorizer\n - Enter application ID\n - Wait for authorizer approval\n\n2. **Approve Authorization**\n - Authorizer views application\n - Set card key type permissions after approval\n - Set agent discount\n\n3. **Generate Card Keys**\n - Authorized party selects authorized application\n - Select card key types with permission\n - Generate and sell card keys\n\n4. **Settle Revenue**\n - Sales revenue is settled proportionally\n - Authorized party receives income\n\n## Permission Management\n\n- Authorizer can modify card key type permissions at any time\n- Can pause or terminate authorization\n- Can view agent's sales data",
|
||||
SummaryEn: "Implementation process and permission management for agent authorization",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ const displayValue = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
const buttonTitle = computed(() => props.title || t('developer.cards.timeRange'))
|
||||
const buttonTitle = computed(() => props.title || t('admin.cards.timeRange'))
|
||||
|
||||
function setQuickRange(range: 'today' | 'week' | 'month') {
|
||||
const now = new Date()
|
||||
@@ -83,19 +83,19 @@ function clearFilter() {
|
||||
<UiPopoverContent class="w-auto p-4" align="start">
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-medium">
|
||||
{{ t('developer.cards.selectTimeRange') }}
|
||||
{{ t('admin.cards.selectTimeRange') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<DateTimePicker
|
||||
:model-value="startModelValue"
|
||||
:placeholder="t('developer.cards.startTime')"
|
||||
:placeholder="t('admin.cards.startTime')"
|
||||
class="w-[180px]"
|
||||
@update:model-value="emit('update:startModelValue', $event)"
|
||||
/>
|
||||
<span class="text-muted-foreground text-sm">~</span>
|
||||
<DateTimePicker
|
||||
:model-value="endModelValue"
|
||||
:placeholder="t('developer.cards.endTime')"
|
||||
:placeholder="t('admin.cards.endTime')"
|
||||
class="w-[180px]"
|
||||
@update:model-value="emit('update:endModelValue', $event)"
|
||||
/>
|
||||
@@ -103,17 +103,17 @@ function clearFilter() {
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-1">
|
||||
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('today')">
|
||||
{{ t('developer.cards.today') }}
|
||||
{{ t('admin.cards.today') }}
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('week')">
|
||||
{{ t('developer.cards.last7Days') }}
|
||||
{{ t('admin.cards.last7Days') }}
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="setQuickRange('month')">
|
||||
{{ t('developer.cards.thisMonth') }}
|
||||
{{ t('admin.cards.thisMonth') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
<UiButton variant="ghost" size="sm" class="h-7 text-xs" @click="clearFilter">
|
||||
{{ t('developer.cards.reset') }}
|
||||
{{ t('admin.cards.reset') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@ const filterFunction = (list: DataTableFacetedFilter['options'], term: string) =
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
@select="(_e) => {
|
||||
@select="(_e: any) => {
|
||||
const isSelected = selectedValues.has(option.value)
|
||||
if (isSelected) {
|
||||
selectedValues.delete(option.value)
|
||||
|
||||
@@ -31,7 +31,7 @@ onMounted(() => {
|
||||
|
||||
const teams = [
|
||||
{
|
||||
name: '开发者后台',
|
||||
name: '管理后台',
|
||||
logo: Code,
|
||||
plan: 'Developer',
|
||||
},
|
||||
@@ -43,42 +43,42 @@ const navMain = [
|
||||
items: [
|
||||
{
|
||||
title: '控制台',
|
||||
url: '/developer',
|
||||
url: '/admin',
|
||||
icon: Gauge,
|
||||
},
|
||||
{
|
||||
title: '应用管理',
|
||||
url: '/developer/applications',
|
||||
url: '/admin/applications',
|
||||
icon: Boxes,
|
||||
},
|
||||
{
|
||||
title: '卡类管理',
|
||||
url: '/developer/card-types',
|
||||
url: '/admin/card-types',
|
||||
icon: Hash,
|
||||
},
|
||||
{
|
||||
title: '卡密管理',
|
||||
url: '/developer/cards',
|
||||
url: '/admin/cards',
|
||||
icon: Key,
|
||||
},
|
||||
{
|
||||
title: '公告管理',
|
||||
url: '/developer/announcements',
|
||||
url: '/admin/announcements',
|
||||
icon: Megaphone,
|
||||
},
|
||||
{
|
||||
title: '版本管理',
|
||||
url: '/developer/versions',
|
||||
url: '/admin/versions',
|
||||
icon: GitBranch,
|
||||
},
|
||||
{
|
||||
title: '用户管理',
|
||||
url: '/developer/users',
|
||||
url: '/admin/users',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: '代理管理',
|
||||
url: '/developer/agents',
|
||||
url: '/admin/agents',
|
||||
icon: Network,
|
||||
},
|
||||
],
|
||||
@@ -88,17 +88,17 @@ const navMain = [
|
||||
items: [
|
||||
{
|
||||
title: '财务管理',
|
||||
url: '/developer/finance',
|
||||
url: '/admin/finance',
|
||||
icon: DollarSign,
|
||||
},
|
||||
{
|
||||
title: '日志记录',
|
||||
url: '/developer/logs',
|
||||
url: '/admin/logs',
|
||||
icon: ScrollText,
|
||||
},
|
||||
{
|
||||
title: '工单系统',
|
||||
url: '/developer/tickets',
|
||||
url: '/admin/tickets',
|
||||
icon: MessageSquare,
|
||||
},
|
||||
],
|
||||
@@ -108,27 +108,27 @@ const navMain = [
|
||||
items: [
|
||||
{
|
||||
title: '云端常量',
|
||||
url: '/developer/cloud-constants',
|
||||
url: '/admin/cloud-constants',
|
||||
icon: FileLock,
|
||||
},
|
||||
{
|
||||
title: '云端变量',
|
||||
url: '/developer/cloud-variables',
|
||||
url: '/admin/cloud-variables',
|
||||
icon: Variable,
|
||||
},
|
||||
{
|
||||
title: '云端函数',
|
||||
url: '/developer/cloud-function',
|
||||
url: '/admin/cloud-function',
|
||||
icon: Code,
|
||||
},
|
||||
{
|
||||
title: '风控管理',
|
||||
url: '/developer/risk-control',
|
||||
url: '/admin/risk-control',
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
title: '扩展配置',
|
||||
url: '/developer/extension',
|
||||
url: '/admin/extension',
|
||||
icon: Plug,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -139,11 +139,11 @@ function handleLogout() {
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuGroup>
|
||||
<UiDropdownMenuItem @click="router.push('/developer/profile')">
|
||||
<UiDropdownMenuItem @click="router.push('/admin/profile')">
|
||||
<UserRoundCog class="mr-2 h-4 w-4" />
|
||||
个人中心
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="router.push('/developer/tickets')">
|
||||
<UiDropdownMenuItem @click="router.push('/admin/tickets')">
|
||||
<Icon icon="lucide:ticket" class="mr-2 h-4 w-4" />
|
||||
工单系统
|
||||
</UiDropdownMenuItem>
|
||||
|
||||
@@ -66,34 +66,34 @@ const developerMenus = computed(() => ({
|
||||
main: {
|
||||
title: t('nav.mainFeatures'),
|
||||
items: [
|
||||
{ title: t('nav.console'), url: '/developer', icon: 'lucide:gauge' },
|
||||
{ title: t('nav.applications'), url: '/developer/applications', icon: 'lucide:boxes' },
|
||||
{ title: t('nav.cardTypes'), url: '/developer/card-types', icon: 'lucide:hash' },
|
||||
{ title: t('nav.cards'), url: '/developer/cards', icon: 'lucide:key' },
|
||||
{ title: t('nav.announcements'), url: '/developer/announcements', icon: 'lucide:megaphone' },
|
||||
{ title: t('nav.versions'), url: '/developer/versions', icon: 'lucide:git-branch' },
|
||||
{ title: t('nav.users'), url: '/developer/users', icon: 'lucide:users' },
|
||||
{ title: t('nav.devices'), url: '/developer/devices', icon: 'lucide:monitor' },
|
||||
{ title: t('nav.sessions'), url: '/developer/sessions', icon: 'lucide:wifi' },
|
||||
{ title: t('nav.agentApps'), url: '/developer/agent-apps', icon: 'lucide:share-2' },
|
||||
{ title: t('nav.console'), url: '/admin', icon: 'lucide:gauge' },
|
||||
{ title: t('nav.applications'), url: '/admin/applications', icon: 'lucide:boxes' },
|
||||
{ title: t('nav.cardTypes'), url: '/admin/card-types', icon: 'lucide:hash' },
|
||||
{ title: t('nav.cards'), url: '/admin/cards', icon: 'lucide:key' },
|
||||
{ title: t('nav.announcements'), url: '/admin/announcements', icon: 'lucide:megaphone' },
|
||||
{ title: t('nav.versions'), url: '/admin/versions', icon: 'lucide:git-branch' },
|
||||
{ title: t('nav.users'), url: '/admin/users', icon: 'lucide:users' },
|
||||
{ title: t('nav.devices'), url: '/admin/devices', icon: 'lucide:monitor' },
|
||||
{ title: t('nav.sessions'), url: '/admin/sessions', icon: 'lucide:wifi' },
|
||||
{ title: t('nav.agentApps'), url: '/admin/agent-apps', icon: 'lucide:share-2' },
|
||||
],
|
||||
},
|
||||
business: {
|
||||
title: t('nav.businessManagement'),
|
||||
items: [
|
||||
{ title: t('nav.finance'), url: '/developer/finance', icon: 'lucide:dollar-sign' },
|
||||
{ title: t('nav.logs'), url: '/developer/logs', icon: 'lucide:scroll-text' },
|
||||
{ title: t('nav.tickets'), url: '/developer/tickets', icon: 'lucide:message-square' },
|
||||
{ title: t('nav.finance'), url: '/admin/finance', icon: 'lucide:dollar-sign' },
|
||||
{ title: t('nav.logs'), url: '/admin/logs', icon: 'lucide:scroll-text' },
|
||||
{ title: t('nav.tickets'), url: '/admin/tickets', icon: 'lucide:message-square' },
|
||||
],
|
||||
},
|
||||
advanced: {
|
||||
title: t('nav.advancedFeatures'),
|
||||
items: [
|
||||
{ title: t('nav.cloudConstants'), url: '/developer/cloud-constants', icon: 'lucide:file-lock' },
|
||||
{ title: t('nav.cloudVariables'), url: '/developer/cloud-variables', icon: 'lucide:variable' },
|
||||
{ title: t('nav.cloudFunction'), url: '/developer/cloud-function', icon: 'lucide:code' },
|
||||
{ title: t('nav.riskControl'), url: '/developer/risk-control', icon: 'lucide:shield' },
|
||||
{ title: t('nav.extension'), url: '/developer/extension', icon: 'lucide:plug' },
|
||||
{ title: t('nav.cloudConstants'), url: '/admin/cloud-constants', icon: 'lucide:file-lock' },
|
||||
{ title: t('nav.cloudVariables'), url: '/admin/cloud-variables', icon: 'lucide:variable' },
|
||||
{ title: t('nav.cloudFunction'), url: '/admin/cloud-function', icon: 'lucide:code' },
|
||||
{ title: t('nav.riskControl'), url: '/admin/risk-control', icon: 'lucide:shield' },
|
||||
{ title: t('nav.extension'), url: '/admin/extension', icon: 'lucide:plug' },
|
||||
],
|
||||
},
|
||||
}))
|
||||
@@ -334,7 +334,7 @@ watch(route, () => {
|
||||
<Icon icon="lucide:user" class="mr-2 h-4 w-4" />
|
||||
<span>{{ t('nav.profile') }}</span>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem v-if="currentUser?.role === 'admin'" @click="router.push('/developer')">
|
||||
<UiDropdownMenuItem v-if="currentUser?.role === 'admin'" @click="router.push('/admin')">
|
||||
<Icon icon="lucide:layout-dashboard" class="mr-2 h-4 w-4" />
|
||||
<span>{{ t('nav.developerDashboard') }}</span>
|
||||
</UiDropdownMenuItem>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import DeveloperSidebar from '@/components/developer-sidebar/index.vue'
|
||||
import AdminSidebar from '@/components/developer-sidebar/index.vue'
|
||||
import LanguageChange from '@/components/language-change.vue'
|
||||
import ThemePopover from '@/components/custom-theme/theme-popover.vue'
|
||||
import ToggleTheme from '@/components/toggle-theme.vue'
|
||||
@@ -12,7 +12,7 @@ const router = useRouter()
|
||||
const breadcrumbs = computed(() => {
|
||||
const path = router.currentRoute.value.path
|
||||
const parts = path.split('/').filter(Boolean)
|
||||
const crumbs = [{ title: '控制台', path: '/developer' }]
|
||||
const crumbs = [{ title: '控制台', path: '/admin' }]
|
||||
|
||||
const titleMap: Record<string, string> = {
|
||||
applications: '应用管理',
|
||||
@@ -44,7 +44,7 @@ const breadcrumbs = computed(() => {
|
||||
invite: '邀请授权',
|
||||
}
|
||||
|
||||
let currentPath = '/developer'
|
||||
let currentPath = '/admin'
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
currentPath += `/${part}`
|
||||
@@ -69,7 +69,7 @@ const breadcrumbs = computed(() => {
|
||||
|
||||
<template>
|
||||
<UiSidebarProvider>
|
||||
<DeveloperSidebar />
|
||||
<AdminSidebar />
|
||||
<UiSidebarInset>
|
||||
<header class="flex h-14 shrink-0 items-center gap-2 border-b px-4">
|
||||
<UiSidebarTrigger class="-ml-1" />
|
||||
|
||||
@@ -76,13 +76,13 @@ async function fetchData() {
|
||||
}
|
||||
else {
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
toast.error('获取数据失败')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -118,7 +118,7 @@ async function handleSubmit() {
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
toast.success('保存成功')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
else {
|
||||
toast.error(data.message || '保存失败')
|
||||
@@ -134,7 +134,7 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { AgentApp } from '@/pages/developer/agent-apps/data/schema'
|
||||
import type { AgentApp } from '@/pages/admin/agent-apps/data/schema'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
|
||||
@@ -17,7 +17,6 @@ const saving = ref(false)
|
||||
const agentApp = ref<AgentApp | null>(null)
|
||||
|
||||
const form = ref({
|
||||
commission: 0.1,
|
||||
discount: 1.0,
|
||||
status: 'active',
|
||||
})
|
||||
@@ -33,20 +32,19 @@ async function fetchAgentApp() {
|
||||
if (data.code === 200 && data.agent_app) {
|
||||
agentApp.value = data.agent_app
|
||||
form.value = {
|
||||
commission: data.agent_app.commission,
|
||||
discount: data.agent_app.discount,
|
||||
status: data.agent_app.status,
|
||||
}
|
||||
}
|
||||
else {
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取授权信息失败:', error)
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -54,10 +52,6 @@ async function fetchAgentApp() {
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (form.value.commission < 0 || form.value.commission > 1) {
|
||||
toast.error('佣金比例必须在0-1之间')
|
||||
return
|
||||
}
|
||||
if (form.value.discount < 0 || form.value.discount > 1) {
|
||||
toast.error('折扣必须在0-1之间')
|
||||
return
|
||||
@@ -78,7 +72,7 @@ async function handleSubmit() {
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
toast.success('保存成功')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
else {
|
||||
toast.error(data.message || '保存失败')
|
||||
@@ -94,7 +88,7 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -146,29 +140,6 @@ onMounted(() => {
|
||||
</UiSelect>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="commission">
|
||||
佣金比例
|
||||
</UiLabel>
|
||||
<div class="flex items-center gap-4">
|
||||
<UiInput
|
||||
id="commission"
|
||||
v-model.number="form.commission"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="1"
|
||||
class="flex-1"
|
||||
/>
|
||||
<span class="text-sm text-muted-foreground w-20">
|
||||
{{ (form.commission * 100).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
代理商获得的佣金比例,范围0-1
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="discount">
|
||||
折扣
|
||||
@@ -200,7 +171,7 @@ onMounted(() => {
|
||||
<UiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="router.push(`/developer/agent-apps/${route.params.id}/recharge`)"
|
||||
@click="router.push(`/admin/agent-apps/${route.params.id}/recharge`)"
|
||||
>
|
||||
<Icon icon="lucide:wallet" class="mr-2 h-4 w-4" />
|
||||
充值余额
|
||||
|
||||
@@ -30,13 +30,13 @@ async function fetchAgentApp() {
|
||||
}
|
||||
else {
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取授权信息失败:', error)
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -66,7 +66,7 @@ async function handleSubmit() {
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
toast.success('充值成功')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
else {
|
||||
toast.error(data.message || '充值失败')
|
||||
@@ -82,7 +82,7 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
|
||||
const quickAmounts = [100, 500, 1000, 5000]
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type { CombinedItem } from '@/pages/developer/agent-apps/data/schema'
|
||||
import type { CombinedItem } from '@/pages/admin/agent-apps/data/schema'
|
||||
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -68,11 +68,11 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'agent_name',
|
||||
header: '代理商/开发者',
|
||||
header: '代理商/管理员',
|
||||
cell: ({ row }) => {
|
||||
const agentName = row.getValue('agent_name') as string
|
||||
const itemType = row.original.itemType
|
||||
const label = itemType === 'my_request' ? '开发者' : '代理商'
|
||||
const label = itemType === 'my_request' ? '管理员' : '代理商'
|
||||
return h('div', {}, [
|
||||
h('div', { class: 'font-medium' }, agentName || '-'),
|
||||
h('div', { class: 'text-xs text-muted-foreground' }, label),
|
||||
@@ -98,17 +98,6 @@ export function getColumns(actions: {
|
||||
return h(Badge, { variant }, () => text)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'commission',
|
||||
header: '佣金比例',
|
||||
cell: ({ row }) => {
|
||||
const hasAuthorization = row.original.hasAuthorization
|
||||
if (!hasAuthorization)
|
||||
return '-'
|
||||
const commission = row.getValue('commission') as number
|
||||
return `${(commission * 100).toFixed(1)}%`
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'discount',
|
||||
header: '折扣',
|
||||
@@ -191,15 +180,15 @@ export function getColumns(actions: {
|
||||
|
||||
if (item.hasAuthorization) {
|
||||
items.push(
|
||||
h(DropdownMenuItem, { onClick: () => router.push(`/developer/agent-apps/${item.authorizationId}/edit`) }, () => [
|
||||
h(DropdownMenuItem, { onClick: () => router.push(`/admin/agent-apps/${item.authorizationId}/edit`) }, () => [
|
||||
h('span', { class: 'i-lucide-edit mr-2 h-4 w-4' }),
|
||||
'编辑',
|
||||
]),
|
||||
h(DropdownMenuItem, { onClick: () => router.push(`/developer/agent-apps/${item.authorizationId}/card-types`) }, () => [
|
||||
h(DropdownMenuItem, { onClick: () => router.push(`/admin/agent-apps/${item.authorizationId}/card-types`) }, () => [
|
||||
h('span', { class: 'i-lucide-key mr-2 h-4 w-4' }),
|
||||
'卡密权限',
|
||||
]),
|
||||
h(DropdownMenuItem, { onClick: () => router.push(`/developer/agent-apps/${item.authorizationId}/recharge`) }, () => [
|
||||
h(DropdownMenuItem, { onClick: () => router.push(`/admin/agent-apps/${item.authorizationId}/recharge`) }, () => [
|
||||
h('span', { class: 'i-lucide-wallet mr-2 h-4 w-4' }),
|
||||
'充值余额',
|
||||
]),
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Table } from '@tanstack/vue-table'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { CombinedItem } from '@/pages/developer/agent-apps/data/schema'
|
||||
import type { CombinedItem } from '@/pages/admin/agent-apps/data/schema'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -21,7 +21,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
|
||||
<template>
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
placeholder="搜索代理商/开发者..."
|
||||
placeholder="搜索代理商/管理员..."
|
||||
:model-value="(table.getColumn('agent_name')?.getFilterValue() as string) ?? ''"
|
||||
class="h-8 w-[150px] lg:w-[200px]"
|
||||
@input="table.getColumn('agent_name')?.setFilterValue($event.target.value)"
|
||||
|
||||
@@ -36,12 +36,12 @@ const table = generateVueTable<CombinedItem>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.agentApps.select',
|
||||
itemType: 'developer.agentApps.columns.type',
|
||||
agent_name: 'developer.agentApps.columns.agentName',
|
||||
app_name: 'developer.agentApps.columns.appName',
|
||||
status: 'developer.agentApps.columns.status',
|
||||
created_at: 'developer.agentApps.columns.createdAt',
|
||||
select: 'admin.agentApps.select',
|
||||
itemType: 'admin.agentApps.columns.type',
|
||||
agent_name: 'admin.agentApps.columns.agentName',
|
||||
app_name: 'admin.agentApps.columns.appName',
|
||||
status: 'admin.agentApps.columns.status',
|
||||
created_at: 'admin.agentApps.columns.createdAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -9,7 +9,6 @@ export const agentAppSchema = z.object({
|
||||
application_id: z.number(),
|
||||
app_name: z.string(),
|
||||
status: agentAppStatusSchema,
|
||||
commission: z.number(),
|
||||
discount: z.number(),
|
||||
balance: z.number(),
|
||||
card_types: z.array(z.object({
|
||||
@@ -44,7 +43,6 @@ export interface CombinedItem {
|
||||
application_id: number
|
||||
app_name: string
|
||||
status: string
|
||||
commission: number
|
||||
discount: number
|
||||
balance: number
|
||||
card_types?: Array<{ card_type_id: number, can_generate: boolean }>
|
||||
|
||||
@@ -5,12 +5,12 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { AgentApp, AgentRequest, CombinedItem } from '@/pages/developer/agent-apps/data/schema'
|
||||
import type { AgentApp, AgentRequest, CombinedItem } from '@/pages/admin/agent-apps/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import BulkActions from '@/components/data-table/bulk-actions.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/developer/agent-apps/components/data-table.vue'
|
||||
import DataTable from '@/pages/admin/agent-apps/components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -53,7 +53,6 @@ const allItems = computed<CombinedItem[]>(() => {
|
||||
application_id: item.application_id,
|
||||
app_name: item.app_name,
|
||||
status: item.status,
|
||||
commission: 0,
|
||||
discount: 1,
|
||||
balance: 0,
|
||||
card_types: [],
|
||||
@@ -72,7 +71,6 @@ const allItems = computed<CombinedItem[]>(() => {
|
||||
application_id: item.application_id,
|
||||
app_name: item.app_name,
|
||||
status: item.status,
|
||||
commission: 0,
|
||||
discount: 1,
|
||||
balance: 0,
|
||||
card_types: [],
|
||||
@@ -110,22 +108,22 @@ async function fetchData() {
|
||||
}
|
||||
|
||||
function goToRequest() {
|
||||
router.push('/developer/agent-apps/request')
|
||||
router.push('/admin/agent-apps/request')
|
||||
}
|
||||
|
||||
function goToInvite() {
|
||||
router.push('/developer/agent-apps/invite')
|
||||
router.push('/admin/agent-apps/invite')
|
||||
}
|
||||
|
||||
async function approveRequest(item: CombinedItem) {
|
||||
try {
|
||||
await api.post(`/dev/agent-apps/requests/${item.id}/approve`)
|
||||
toast.success(t('developer.agentApps.approveSuccess'))
|
||||
toast.success(t('admin.agentApps.approveSuccess'))
|
||||
fetchData()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批准申请失败:', error)
|
||||
toast.error(error.message || t('developer.agentApps.approveFailed'))
|
||||
toast.error(error.message || t('admin.agentApps.approveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,13 +133,13 @@ async function rejectRequest() {
|
||||
|
||||
try {
|
||||
await api.post(`/dev/agent-apps/requests/${rejectingItem.value.id}/reject`, { reason: '' })
|
||||
toast.success(t('developer.agentApps.rejectSuccess'))
|
||||
toast.success(t('admin.agentApps.rejectSuccess'))
|
||||
showRejectDialog.value = false
|
||||
fetchData()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('拒绝申请失败:', error)
|
||||
toast.error(error.message || t('developer.agentApps.rejectFailed'))
|
||||
toast.error(error.message || t('admin.agentApps.rejectFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,12 +176,12 @@ function confirmRemove(item: CombinedItem) {
|
||||
async function batchApprove(ids: (string | number)[]) {
|
||||
try {
|
||||
await api.post('/dev/agent-apps/requests/batch-approve', { ids })
|
||||
toast.success(t('developer.agentApps.batchApproveSuccess'))
|
||||
toast.success(t('admin.agentApps.batchApproveSuccess'))
|
||||
fetchData()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量批准失败:', error)
|
||||
toast.error(error.message || t('developer.agentApps.batchApproveFailed'))
|
||||
toast.error(error.message || t('admin.agentApps.batchApproveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,13 +191,13 @@ async function batchReject() {
|
||||
|
||||
try {
|
||||
await api.post('/dev/agent-apps/requests/batch-reject', { ids: batchRejectIds.value, reason: '' })
|
||||
toast.success(t('developer.agentApps.batchRejectSuccess'))
|
||||
toast.success(t('admin.agentApps.batchRejectSuccess'))
|
||||
showBatchRejectDialog.value = false
|
||||
fetchData()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量拒绝失败:', error)
|
||||
toast.error(error.message || t('developer.agentApps.batchRejectFailed'))
|
||||
toast.error(error.message || t('admin.agentApps.batchRejectFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,13 +212,13 @@ async function batchRemove() {
|
||||
|
||||
try {
|
||||
await api.delete('/dev/agent-apps/batch', { ids: batchRemoveIds.value })
|
||||
toast.success(t('developer.agentApps.batchRemoveSuccess'))
|
||||
toast.success(t('admin.agentApps.batchRemoveSuccess'))
|
||||
showBatchRemoveDialog.value = false
|
||||
fetchData()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量移除失败:', error)
|
||||
toast.error(error.message || t('developer.agentApps.batchRemoveFailed'))
|
||||
toast.error(error.message || t('admin.agentApps.batchRemoveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,18 +234,18 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.agentApps.title')"
|
||||
:description="t('developer.agentApps.description')"
|
||||
:title="t('admin.agentApps.title')"
|
||||
:description="t('admin.agentApps.description')"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="outline" @click="goToRequest">
|
||||
<Icon icon="lucide:send" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.agentApps.requestAuth') }}
|
||||
{{ t('admin.agentApps.requestAuth') }}
|
||||
</UiButton>
|
||||
<UiButton @click="goToInvite">
|
||||
<Icon icon="lucide:user-plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.agentApps.inviteAuth') }}
|
||||
{{ t('admin.agentApps.inviteAuth') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -256,7 +254,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.agentApps.totalAuth') }}
|
||||
{{ t('admin.agentApps.totalAuth') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:share-2" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -270,7 +268,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.agentApps.activeAuth') }}
|
||||
{{ t('admin.agentApps.activeAuth') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -284,7 +282,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.agentApps.pendingRequests') }}
|
||||
{{ t('admin.agentApps.pendingRequests') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -298,7 +296,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.agentApps.agentBalance') }}
|
||||
{{ t('admin.agentApps.agentBalance') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:wallet" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -328,7 +326,7 @@ onMounted(() => {
|
||||
<BulkActions
|
||||
v-if="tableRef?.table"
|
||||
:table="tableRef.table"
|
||||
:entity-name="t('developer.agentApps.auth')"
|
||||
:entity-name="t('admin.agentApps.auth')"
|
||||
>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
@@ -336,7 +334,7 @@ onMounted(() => {
|
||||
@click="batchApprove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
|
||||
>
|
||||
<Icon icon="lucide:check" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.agentApps.batchApprove') }}
|
||||
{{ t('admin.agentApps.batchApprove') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
@@ -344,7 +342,7 @@ onMounted(() => {
|
||||
@click="confirmBatchReject(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
|
||||
>
|
||||
<Icon icon="lucide:x" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.agentApps.batchReject') }}
|
||||
{{ t('admin.agentApps.batchReject') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="destructive"
|
||||
@@ -352,38 +350,38 @@ onMounted(() => {
|
||||
@click="confirmBatchRemove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
|
||||
>
|
||||
<Icon icon="lucide:trash-2" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.agentApps.batchRemove') }}
|
||||
{{ t('admin.agentApps.batchRemove') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="showRejectDialog"
|
||||
:title="t('developer.agentApps.reject')"
|
||||
:description="t('developer.agentApps.rejectConfirm')"
|
||||
:title="t('admin.agentApps.reject')"
|
||||
:description="t('admin.agentApps.rejectConfirm')"
|
||||
confirm-text="确定"
|
||||
@confirm="rejectRequest"
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="showRemoveDialog"
|
||||
:title="t('developer.agentApps.remove')"
|
||||
:description="removingItem?.itemType === 'my_request' ? t('developer.agentApps.withdrawConfirm') : t('developer.agentApps.removeConfirm')"
|
||||
:title="t('admin.agentApps.remove')"
|
||||
:description="removingItem?.itemType === 'my_request' ? t('admin.agentApps.withdrawConfirm') : t('admin.agentApps.removeConfirm')"
|
||||
confirm-text="确定"
|
||||
@confirm="removeItem"
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="showBatchRejectDialog"
|
||||
:title="t('developer.agentApps.batchReject')"
|
||||
:description="t('developer.agentApps.batchRejectConfirm', { count: batchRejectIds.length })"
|
||||
:title="t('admin.agentApps.batchReject')"
|
||||
:description="t('admin.agentApps.batchRejectConfirm', { count: batchRejectIds.length })"
|
||||
confirm-text="确定"
|
||||
@confirm="batchReject"
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="showBatchRemoveDialog"
|
||||
:title="t('developer.agentApps.batchRemove')"
|
||||
:description="t('developer.agentApps.batchRemoveConfirm', { count: batchRemoveIds.length })"
|
||||
:title="t('admin.agentApps.batchRemove')"
|
||||
:description="t('admin.agentApps.batchRemoveConfirm', { count: batchRemoveIds.length })"
|
||||
confirm-text="确定"
|
||||
@confirm="batchRemove"
|
||||
/>
|
||||
|
||||
@@ -63,7 +63,7 @@ async function handleSubmit() {
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
toast.success('邀请已发送')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
else {
|
||||
toast.error(data.message || '发送邀请失败')
|
||||
@@ -79,7 +79,7 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -36,7 +36,7 @@ async function fetchApplications() {
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.value.developer_id) {
|
||||
toast.error('请输入开发者ID')
|
||||
toast.error('请输入管理员ID')
|
||||
return
|
||||
}
|
||||
if (!form.value.application_id) {
|
||||
@@ -63,7 +63,7 @@ async function handleSubmit() {
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
toast.success('申请已提交')
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
else {
|
||||
toast.error(data.message || '提交申请失败')
|
||||
@@ -79,7 +79,7 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/developer/agent-apps')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -90,7 +90,7 @@ onMounted(() => {
|
||||
<template>
|
||||
<BasicPage
|
||||
title="申请授权"
|
||||
description="向开发者申请应用授权"
|
||||
description="向管理员申请应用授权"
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="outline" @click="goBack">
|
||||
@@ -104,22 +104,22 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>申请信息</UiCardTitle>
|
||||
<UiCardDescription>
|
||||
填写以下信息向开发者申请应用授权
|
||||
填写以下信息向管理员申请应用授权
|
||||
</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="developer_id">
|
||||
开发者ID
|
||||
管理员ID
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="developer_id"
|
||||
v-model="form.developer_id"
|
||||
type="number"
|
||||
placeholder="请输入开发者ID"
|
||||
placeholder="请输入管理员ID"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
请向开发者索取其ID
|
||||
请向管理员索取其ID
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,369 +2,355 @@
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { Agent } from './data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from './components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Agent {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
avatar: string
|
||||
status: string
|
||||
parent_agent_id: number | null
|
||||
parent_agent_name: string
|
||||
commission: number
|
||||
can_create_agent: boolean
|
||||
balance: number
|
||||
}
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const agents = ref<Agent[]>([])
|
||||
const tableRef = ref<InstanceType<typeof DataTable> | null>(null)
|
||||
const searchFilter = ref('')
|
||||
const statusFilter = ref<string>('')
|
||||
const viewMode = ref<'tree' | 'list'>('tree')
|
||||
|
||||
const agentId = computed(() => route.params.id as string)
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<Agent | null>(null)
|
||||
const batchDeleteDialogOpen = ref(false)
|
||||
const batchDeleteIds = ref<number[]>([])
|
||||
|
||||
const form = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
parent_agent_id: '',
|
||||
commission: 0,
|
||||
can_create_agent: false,
|
||||
balance: 0,
|
||||
const filteredAgents = computed(() => {
|
||||
let result = agents.value
|
||||
|
||||
if (statusFilter.value) {
|
||||
const filterByStatus = (items: Agent[]): Agent[] => {
|
||||
return items.reduce((acc: Agent[], item) => {
|
||||
const matches = item.status === statusFilter.value
|
||||
const filteredChildren = item.children ? filterByStatus(item.children) : []
|
||||
|
||||
if (matches || filteredChildren.length > 0) {
|
||||
acc.push({
|
||||
...item,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : item.children,
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
result = filterByStatus(result)
|
||||
}
|
||||
|
||||
if (searchFilter.value) {
|
||||
const search = searchFilter.value.toLowerCase()
|
||||
const filterTree = (items: Agent[]): Agent[] => {
|
||||
return items.reduce((acc: Agent[], item) => {
|
||||
const matchesSearch =
|
||||
item.username?.toLowerCase().includes(search) ||
|
||||
item.email?.toLowerCase().includes(search) ||
|
||||
item.parent_agent_name?.toLowerCase().includes(search)
|
||||
|
||||
const filteredChildren = item.children ? filterTree(item.children) : []
|
||||
|
||||
if (matchesSearch || filteredChildren.length > 0) {
|
||||
acc.push({
|
||||
...item,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : item.children,
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
result = filterTree(result)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
async function fetchAgent() {
|
||||
const totalAgents = computed(() => {
|
||||
const countNodes = (items: Agent[]): number => {
|
||||
return items.reduce((acc, item) => {
|
||||
return acc + 1 + (item.children ? countNodes(item.children) : 0)
|
||||
}, 0)
|
||||
}
|
||||
return countNodes(agents.value)
|
||||
})
|
||||
|
||||
const activeCount = computed(() => {
|
||||
const countByStatus = (items: Agent[], status: string): number => {
|
||||
return items.reduce((acc, item) => {
|
||||
const self = item.status === status ? 1 : 0
|
||||
const children = item.children ? countByStatus(item.children, status) : 0
|
||||
return acc + self + children
|
||||
}, 0)
|
||||
}
|
||||
return countByStatus(agents.value, 'active')
|
||||
})
|
||||
|
||||
const inactiveCount = computed(() => {
|
||||
const countByStatus = (items: Agent[], status: string): number => {
|
||||
return items.reduce((acc, item) => {
|
||||
const self = item.status === status ? 1 : 0
|
||||
const children = item.children ? countByStatus(item.children, status) : 0
|
||||
return acc + self + children
|
||||
}, 0)
|
||||
}
|
||||
return countByStatus(agents.value, 'inactive')
|
||||
})
|
||||
|
||||
const bannedCount = computed(() => {
|
||||
const countByStatus = (items: Agent[], status: string): number => {
|
||||
return items.reduce((acc, item) => {
|
||||
const self = item.status === status ? 1 : 0
|
||||
const children = item.children ? countByStatus(item.children, status) : 0
|
||||
return acc + self + children
|
||||
}, 0)
|
||||
}
|
||||
return countByStatus(agents.value, 'banned')
|
||||
})
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('admin.agents.status.active'), value: 'active' },
|
||||
{ label: t('admin.agents.status.inactive'), value: 'inactive' },
|
||||
{ label: t('admin.agents.status.banned'), value: 'banned' },
|
||||
])
|
||||
|
||||
async function fetchAgents() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<Agent>(`/dev/agents/${agentId.value}`)
|
||||
if (data) {
|
||||
form.value.username = data.username || ''
|
||||
form.value.email = data.email || ''
|
||||
form.value.password = ''
|
||||
form.value.parent_agent_id = data.parent_agent_id ? String(data.parent_agent_id) : 'none'
|
||||
form.value.commission = data.commission || 0
|
||||
form.value.can_create_agent = data.can_create_agent === true
|
||||
form.value.balance = data.balance || 0
|
||||
const endpoint = viewMode.value === 'tree' ? '/dev/agents/tree' : '/dev/agents'
|
||||
const data = await api.get<{ tree?: Agent[], agents?: Agent[], total: number }>(endpoint)
|
||||
if (viewMode.value === 'tree') {
|
||||
agents.value = data?.tree || []
|
||||
} else {
|
||||
agents.value = data?.agents || []
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取代理信息失败:', error)
|
||||
toast.error(t('developer.agents.edit.fetchFailed'))
|
||||
router.push('/developer/agents')
|
||||
}
|
||||
finally {
|
||||
} catch (error) {
|
||||
console.error('获取代理列表失败:', error)
|
||||
agents.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAgents() {
|
||||
function goToCreate() {
|
||||
router.push('/admin/agents/create')
|
||||
}
|
||||
|
||||
function toggleViewMode() {
|
||||
viewMode.value = viewMode.value === 'tree' ? 'list' : 'tree'
|
||||
fetchAgents()
|
||||
}
|
||||
|
||||
function handleEdit(agent: Agent) {
|
||||
router.push(`/admin/agents/${agent.id}/edit`)
|
||||
}
|
||||
|
||||
async function handleToggleStatus(agent: Agent) {
|
||||
try {
|
||||
const data = await api.get<{ agents?: Agent[], total: number }>('/dev/agents')
|
||||
agents.value = (data?.agents || []).filter(a => String(a.id) !== agentId.value)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取代理列表失败:', error)
|
||||
const newStatus = agent.status === 'banned' ? 'active' : 'banned'
|
||||
await api.put(`/dev/agents/${agent.id}/status`, { status: newStatus })
|
||||
toast.success(t('admin.agents.statusUpdateSuccess'))
|
||||
fetchAgents()
|
||||
} catch (error: any) {
|
||||
console.error('切换代理状态失败:', error)
|
||||
toast.error(error.message || t('admin.agents.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const selectedParentAgent = computed(() => {
|
||||
if (form.value.parent_agent_id && form.value.parent_agent_id !== 'none') {
|
||||
return agents.value.find(a => String(a.id) === form.value.parent_agent_id)
|
||||
}
|
||||
return null
|
||||
})
|
||||
function confirmDelete(agent: Agent) {
|
||||
deleteTarget.value = agent
|
||||
deleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.username) {
|
||||
toast.error(t('developer.agents.create.usernameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
if (form.value.password && form.value.password.length < 6) {
|
||||
toast.error(t('developer.agents.create.passwordMinLength'))
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
async function handleDelete() {
|
||||
if (!deleteTarget.value) return
|
||||
|
||||
try {
|
||||
const payload: any = {
|
||||
commission: form.value.commission,
|
||||
can_create_agent: form.value.can_create_agent,
|
||||
balance: form.value.balance,
|
||||
}
|
||||
|
||||
if (form.value.email) {
|
||||
payload.email = form.value.email
|
||||
}
|
||||
|
||||
if (form.value.password) {
|
||||
payload.password = form.value.password
|
||||
}
|
||||
|
||||
if (form.value.parent_agent_id && form.value.parent_agent_id !== 'none') {
|
||||
payload.parent_agent_id = parseInt(form.value.parent_agent_id)
|
||||
} else {
|
||||
payload.parent_agent_id = null
|
||||
}
|
||||
await api.delete(`/dev/agents/${deleteTarget.value.id}`)
|
||||
toast.success(t('admin.agents.deleteSuccess'))
|
||||
fetchAgents()
|
||||
} catch (error: any) {
|
||||
console.error('删除代理失败:', error)
|
||||
toast.error(error.message || t('admin.agents.deleteFailed'))
|
||||
} finally {
|
||||
deleteTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
await api.put(`/dev/agents/${agentId.value}`, payload)
|
||||
toast.success(t('developer.agents.edit.saveSuccess'))
|
||||
router.push('/developer/agents')
|
||||
function confirmBatchDelete(ids: number[]) {
|
||||
batchDeleteIds.value = ids
|
||||
batchDeleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
await api.delete('/dev/agents/batch', { agent_ids: batchDeleteIds.value } as any)
|
||||
toast.success(t('admin.agents.batchDeleteSuccess'))
|
||||
fetchAgents()
|
||||
} catch (error: any) {
|
||||
console.error('批量删除失败:', error)
|
||||
toast.error(error.message || t('admin.agents.batchDeleteFailed'))
|
||||
} finally {
|
||||
batchDeleteIds.value = []
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('保存代理失败:', error)
|
||||
toast.error(error.message || t('developer.agents.edit.saveFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
async function batchToggleStatus(ids: number[], status: string) {
|
||||
try {
|
||||
await api.post('/dev/agents/batch/status', { agent_ids: ids, status })
|
||||
toast.success(t('admin.agents.batchUpdateSuccess'))
|
||||
fetchAgents()
|
||||
} catch (error: any) {
|
||||
console.error('批量更新状态失败:', error)
|
||||
toast.error(error.message || t('admin.agents.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchAgent()
|
||||
fetchAgents()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.agents.edit.title')"
|
||||
:description="t('developer.agents.edit.description')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.agents.title'), href: '/developer/agents' },
|
||||
{ title: t('developer.agents.edit.title') },
|
||||
]"
|
||||
:title="t('admin.agents.title')"
|
||||
:description="t('admin.agents.description')"
|
||||
sticky
|
||||
>
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Icon icon="lucide:loader-2" class="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<template #actions>
|
||||
<UiButton @click="goToCreate">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('admin.agents.addAgent') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:user" class="size-5" />
|
||||
{{ t('developer.agents.create.basicInfo') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.agents.create.basicInfoDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="username">
|
||||
{{ t('developer.agents.form.username') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
disabled
|
||||
class="bg-muted"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.edit.usernameHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.agents.totalAgents') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ totalAgents }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="email">
|
||||
{{ t('developer.agents.form.email') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
:placeholder="t('developer.agents.form.emailPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.agents.status.active') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ activeCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="password">
|
||||
{{ t('developer.agents.form.password') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
:placeholder="t('developer.agents.edit.passwordPlaceholder')"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.edit.passwordHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.agents.status.inactive') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ inactiveCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.agents.create.agentConfig') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.agents.create.agentConfigDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="parent_agent">
|
||||
{{ t('developer.agents.form.parentAgent') }}
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.parent_agent_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.agents.form.selectParentAgent')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="none">
|
||||
{{ t('developer.agents.form.noParent') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem
|
||||
v-for="agent in agents"
|
||||
:key="agent.id"
|
||||
:value="String(agent.id)"
|
||||
>
|
||||
{{ agent.username }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.create.parentAgentHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="commission">
|
||||
{{ t('developer.agents.form.commission') }}
|
||||
</UiLabel>
|
||||
<div class="flex items-center gap-4">
|
||||
<UiNumberField
|
||||
v-model="form.commission"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
class="flex-1 max-w-[200px]"
|
||||
>
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
<UiNumberFieldInput />
|
||||
<UiNumberFieldIncrement />
|
||||
</UiNumberFieldContent>
|
||||
</UiNumberField>
|
||||
<span class="text-sm text-muted-foreground">%</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.form.commissionPlaceholder') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="balance">
|
||||
{{ t('developer.agents.form.balance') }}
|
||||
</UiLabel>
|
||||
<div class="flex items-center gap-4">
|
||||
<UiNumberField
|
||||
v-model="form.balance"
|
||||
:min="0"
|
||||
:step="0.01"
|
||||
class="flex-1 max-w-[200px]"
|
||||
>
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
<UiNumberFieldInput />
|
||||
<UiNumberFieldIncrement />
|
||||
</UiNumberFieldContent>
|
||||
</UiNumberField>
|
||||
<span class="text-sm text-muted-foreground">¥</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.form.balancePlaceholder') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-0.5">
|
||||
<UiLabel>{{ t('developer.agents.form.canCreateAgent') }}</UiLabel>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.create.canCreateAgentHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 注意:UiSwitch 必须使用 v-model,不能使用 v-model:checked -->
|
||||
<UiSwitch v-model="form.can_create_agent" />
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:receipt" class="size-5" />
|
||||
{{ t('developer.agents.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.username') }}</span>
|
||||
<span>{{ form.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.canCreateAgent') }}</span>
|
||||
<span>{{ form.can_create_agent ? t('common.yes') : t('common.no') }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.email') }}</span>
|
||||
<span>{{ form.email || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.parentAgent') }}</span>
|
||||
<span>{{ selectedParentAgent?.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.commission') }}</span>
|
||||
<span>{{ (form.commission * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.balance') }}</span>
|
||||
<span>¥{{ form.balance.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.agents.edit.save') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.agents.create.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.agents.status.banned') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ bannedCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:data="filteredAgents"
|
||||
:search-filter="searchFilter"
|
||||
:view-mode="viewMode"
|
||||
:on-toggle-status="handleToggleStatus"
|
||||
:on-edit="handleEdit"
|
||||
:on-delete="confirmDelete"
|
||||
@refresh="fetchAgents"
|
||||
@toggle-view="toggleViewMode"
|
||||
@update:search-filter="searchFilter = $event"
|
||||
@batch-unban="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'banned').map((r: any) => r.original.id) || [], 'active')"
|
||||
@batch-ban="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status !== 'banned').map((r: any) => r.original.id) || [], 'banned')"
|
||||
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||
>
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('admin.agents.columns.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('common.delete')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.agents.deleteAgent') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.agents.deleteAgentConfirm', { username: deleteTarget?.username }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('common.delete')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.agents.batchDeleteAgents') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -31,7 +31,7 @@ export function getColumns(actions: {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'username',
|
||||
header: () => t('developer.agents.columns.username'),
|
||||
header: () => t('admin.agents.columns.username'),
|
||||
cell: ({ row }) => {
|
||||
const agent = row.original
|
||||
const indentStyle = getIndentStyle(row)
|
||||
@@ -49,13 +49,13 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => t('developer.agents.columns.status'),
|
||||
header: () => t('admin.agents.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
const statusMap: Record<string, { text: string, variant: any }> = {
|
||||
active: { text: t('developer.agents.status.active'), variant: 'default' },
|
||||
inactive: { text: t('developer.agents.status.inactive'), variant: 'secondary' },
|
||||
banned: { text: t('developer.agents.status.banned'), variant: 'destructive' },
|
||||
active: { text: t('admin.agents.status.active'), variant: 'default' },
|
||||
inactive: { text: t('admin.agents.status.inactive'), variant: 'secondary' },
|
||||
banned: { text: t('admin.agents.status.banned'), variant: 'destructive' },
|
||||
}
|
||||
const { text, variant } = statusMap[status] || { text: status, variant: 'default' }
|
||||
return h(Badge, { variant }, () => text)
|
||||
@@ -63,7 +63,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'can_create_agent',
|
||||
header: () => t('developer.agents.columns.canCreateAgent'),
|
||||
header: () => t('admin.agents.columns.canCreateAgent'),
|
||||
cell: ({ row }) => {
|
||||
const canCreate = row.getValue('can_create_agent') as boolean
|
||||
return h('div', { class: 'flex items-center justify-center' }, [
|
||||
@@ -75,7 +75,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'cards_count',
|
||||
header: () => t('developer.agents.columns.cardsCount'),
|
||||
header: () => t('admin.agents.columns.cardsCount'),
|
||||
cell: ({ row }) => {
|
||||
const count = row.getValue('cards_count') as number
|
||||
return h(Badge, { variant: 'outline' }, () => String(count))
|
||||
@@ -83,7 +83,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'child_agents_count',
|
||||
header: () => t('developer.agents.columns.childAgentsCount'),
|
||||
header: () => t('admin.agents.columns.childAgentsCount'),
|
||||
cell: ({ row }) => {
|
||||
const count = row.getValue('child_agents_count') as number
|
||||
return h(Badge, { variant: 'secondary' }, () => String(count))
|
||||
@@ -91,23 +91,15 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'balance',
|
||||
header: () => t('developer.agents.columns.balance'),
|
||||
header: () => t('admin.agents.columns.balance'),
|
||||
cell: ({ row }) => {
|
||||
const balance = row.getValue('balance') as number
|
||||
return `¥${balance.toFixed(2)}`
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'commission',
|
||||
header: () => t('developer.agents.columns.commission'),
|
||||
cell: ({ row }) => {
|
||||
const commission = row.getValue('commission') as number
|
||||
return `${(commission * 100).toFixed(0)}%`
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.agents.columns.createdAt'),
|
||||
header: () => t('admin.agents.columns.createdAt'),
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue('created_at') as string
|
||||
if (!createdAt) return '-'
|
||||
@@ -128,7 +120,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'last_login_at',
|
||||
header: () => t('developer.agents.columns.lastLoginAt'),
|
||||
header: () => t('admin.agents.columns.lastLoginAt'),
|
||||
cell: ({ row }) => {
|
||||
const lastLoginAt = row.getValue('last_login_at') as string
|
||||
if (!lastLoginAt) return '-'
|
||||
@@ -174,7 +166,7 @@ export function getColumns(actions: {
|
||||
]),
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(agent) }, () => [
|
||||
h(Ban, { class: 'mr-2 h-4 w-4' }),
|
||||
isBanned ? t('developer.agents.enable') : t('developer.agents.disable'),
|
||||
isBanned ? t('admin.agents.enable') : t('admin.agents.disable'),
|
||||
]),
|
||||
h(DropdownMenuSeparator),
|
||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(agent) }, () => [
|
||||
|
||||
@@ -51,16 +51,15 @@ const table = generateVueTable<Agent>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.agents.select',
|
||||
username: 'developer.agents.columns.username',
|
||||
status: 'developer.agents.columns.status',
|
||||
can_create_agent: 'developer.agents.columns.canCreateAgent',
|
||||
cards_count: 'developer.agents.columns.cardsCount',
|
||||
child_agents_count: 'developer.agents.columns.childAgentsCount',
|
||||
balance: 'developer.agents.columns.balance',
|
||||
commission: 'developer.agents.columns.commission',
|
||||
created_at: 'developer.agents.columns.createdAt',
|
||||
last_login_at: 'developer.agents.columns.lastLoginAt',
|
||||
select: 'admin.agents.select',
|
||||
username: 'admin.agents.columns.username',
|
||||
status: 'admin.agents.columns.status',
|
||||
can_create_agent: 'admin.agents.columns.canCreateAgent',
|
||||
cards_count: 'admin.agents.columns.cardsCount',
|
||||
child_agents_count: 'admin.agents.columns.childAgentsCount',
|
||||
balance: 'admin.agents.columns.balance',
|
||||
created_at: 'admin.agents.columns.createdAt',
|
||||
last_login_at: 'admin.agents.columns.lastLoginAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -77,13 +76,13 @@ defineExpose({
|
||||
</div>
|
||||
<BulkActions :table="table" entity-name="agents">
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchUnban')">
|
||||
{{ t('developer.agents.batchUnbanBtn') }}
|
||||
{{ t('admin.agents.batchUnbanBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchBan')">
|
||||
{{ t('developer.agents.batchBanBtn') }}
|
||||
{{ t('admin.agents.batchBanBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||
{{ t('developer.agents.batchDeleteBtn') }}
|
||||
{{ t('admin.agents.batchDeleteBtn') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
@@ -93,7 +92,7 @@ defineExpose({
|
||||
<UiInput
|
||||
:model-value="searchFilter"
|
||||
type="search"
|
||||
:placeholder="t('developer.agents.searchPlaceholder')"
|
||||
:placeholder="t('admin.agents.searchPlaceholder')"
|
||||
class="h-9 w-[250px] pl-8"
|
||||
@update:model-value="emit('update:searchFilter', String($event))"
|
||||
/>
|
||||
@@ -103,7 +102,7 @@ defineExpose({
|
||||
<div class="flex items-center gap-2">
|
||||
<UiButton variant="outline" size="sm" @click="emit('toggleView')">
|
||||
<Icon :icon="viewMode === 'tree' ? 'lucide:list' : 'lucide:git-branch'" class="mr-2 h-4 w-4" />
|
||||
{{ viewMode === 'tree' ? t('developer.agents.listView') : t('developer.agents.treeView') }}
|
||||
{{ viewMode === 'tree' ? t('admin.agents.listView') : t('admin.agents.treeView') }}
|
||||
</UiButton>
|
||||
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,6 @@ const form = ref({
|
||||
email: '',
|
||||
password: '',
|
||||
parent_agent_id: '',
|
||||
commission: 0,
|
||||
can_create_agent: false,
|
||||
balance: 0,
|
||||
})
|
||||
@@ -50,15 +49,15 @@ const selectedParentAgent = computed(() => {
|
||||
|
||||
async function handleCreate() {
|
||||
if (!form.value.username) {
|
||||
toast.error(t('developer.agents.create.usernameRequired'))
|
||||
toast.error(t('admin.agents.create.usernameRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.password) {
|
||||
toast.error(t('developer.agents.create.passwordRequired'))
|
||||
toast.error(t('admin.agents.create.passwordRequired'))
|
||||
return
|
||||
}
|
||||
if (form.value.password.length < 6) {
|
||||
toast.error(t('developer.agents.create.passwordMinLength'))
|
||||
toast.error(t('admin.agents.create.passwordMinLength'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -67,7 +66,6 @@ async function handleCreate() {
|
||||
const payload: any = {
|
||||
username: form.value.username,
|
||||
password: form.value.password,
|
||||
commission: form.value.commission,
|
||||
can_create_agent: form.value.can_create_agent,
|
||||
balance: form.value.balance,
|
||||
}
|
||||
@@ -81,12 +79,12 @@ async function handleCreate() {
|
||||
}
|
||||
|
||||
await api.post('/dev/agents', payload)
|
||||
toast.success(t('developer.agents.create.createSuccess'))
|
||||
router.push('/developer/agents')
|
||||
toast.success(t('admin.agents.create.createSuccess'))
|
||||
router.push('/admin/agents')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('创建代理失败:', error)
|
||||
toast.error(error.message || t('developer.agents.create.createFailed'))
|
||||
toast.error(error.message || t('admin.agents.create.createFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
@@ -100,11 +98,11 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.agents.create.title')"
|
||||
:description="t('developer.agents.create.description')"
|
||||
:title="t('admin.agents.create.title')"
|
||||
:description="t('admin.agents.create.description')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.agents.title'), href: '/developer/agents' },
|
||||
{ title: t('developer.agents.create.title') },
|
||||
{ title: t('admin.agents.title'), href: '/admin/agents' },
|
||||
{ title: t('admin.agents.create.title') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
@@ -115,46 +113,46 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:user-plus" class="size-5" />
|
||||
{{ t('developer.agents.create.basicInfo') }}
|
||||
{{ t('admin.agents.create.basicInfo') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.agents.create.basicInfoDesc') }}</UiCardDescription>
|
||||
<UiCardDescription>{{ t('admin.agents.create.basicInfoDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="username">
|
||||
{{ t('developer.agents.form.username') }}
|
||||
{{ t('admin.agents.form.username') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
:placeholder="t('developer.agents.form.usernamePlaceholder')"
|
||||
:placeholder="t('admin.agents.form.usernamePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="email">
|
||||
{{ t('developer.agents.form.email') }}
|
||||
{{ t('admin.agents.form.email') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
:placeholder="t('developer.agents.form.emailPlaceholder')"
|
||||
:placeholder="t('admin.agents.form.emailPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="password">
|
||||
{{ t('developer.agents.form.password') }}
|
||||
{{ t('admin.agents.form.password') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
:placeholder="t('developer.agents.form.passwordPlaceholder')"
|
||||
:placeholder="t('admin.agents.form.passwordPlaceholder')"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.create.passwordHint') }}
|
||||
{{ t('admin.agents.create.passwordHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
@@ -164,22 +162,22 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.agents.create.agentConfig') }}
|
||||
{{ t('admin.agents.create.agentConfig') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.agents.create.agentConfigDesc') }}</UiCardDescription>
|
||||
<UiCardDescription>{{ t('admin.agents.create.agentConfigDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="parent_agent">
|
||||
{{ t('developer.agents.form.parentAgent') }}
|
||||
{{ t('admin.agents.form.parentAgent') }}
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.parent_agent_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.agents.form.selectParentAgent')" />
|
||||
<UiSelectValue :placeholder="t('admin.agents.form.selectParentAgent')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="none">
|
||||
{{ t('developer.agents.form.noParent') }}
|
||||
{{ t('admin.agents.form.noParent') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem
|
||||
v-for="agent in agents"
|
||||
@@ -191,38 +189,13 @@ onMounted(() => {
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.create.parentAgentHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="commission">
|
||||
{{ t('developer.agents.form.commission') }}
|
||||
</UiLabel>
|
||||
<div class="flex items-center gap-4">
|
||||
<UiNumberField
|
||||
v-model="form.commission"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
class="flex-1 max-w-[200px]"
|
||||
>
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
<UiNumberFieldInput />
|
||||
<UiNumberFieldIncrement />
|
||||
</UiNumberFieldContent>
|
||||
</UiNumberField>
|
||||
<span class="text-sm text-muted-foreground">%</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.form.commissionPlaceholder') }}
|
||||
{{ t('admin.agents.create.parentAgentHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="balance">
|
||||
{{ t('developer.agents.form.balance') }}
|
||||
{{ t('admin.agents.form.balance') }}
|
||||
</UiLabel>
|
||||
<div class="flex items-center gap-4">
|
||||
<UiNumberField
|
||||
@@ -240,15 +213,15 @@ onMounted(() => {
|
||||
<span class="text-sm text-muted-foreground">¥</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.form.balancePlaceholder') }}
|
||||
{{ t('admin.agents.form.balancePlaceholder') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-0.5">
|
||||
<UiLabel>{{ t('developer.agents.form.canCreateAgent') }}</UiLabel>
|
||||
<UiLabel>{{ t('admin.agents.form.canCreateAgent') }}</UiLabel>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.agents.create.canCreateAgentHint') }}
|
||||
{{ t('admin.agents.create.canCreateAgentHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 注意:UiSwitch 必须使用 v-model,不能使用 v-model:checked -->
|
||||
@@ -263,33 +236,29 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:receipt" class="size-5" />
|
||||
{{ t('developer.agents.create.preview') }}
|
||||
{{ t('admin.agents.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.username') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.agents.form.username') }}</span>
|
||||
<span>{{ form.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.email') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.agents.form.email') }}</span>
|
||||
<span>{{ form.email || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.canCreateAgent') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.agents.form.canCreateAgent') }}</span>
|
||||
<span>{{ form.can_create_agent ? t('common.yes') : t('common.no') }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.parentAgent') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.agents.form.parentAgent') }}</span>
|
||||
<span>{{ selectedParentAgent?.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.commission') }}</span>
|
||||
<span>{{ (form.commission * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.agents.form.balance') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.agents.form.balance') }}</span>
|
||||
<span>¥{{ form.balance.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -307,14 +276,14 @@ onMounted(() => {
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:user-plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.agents.form.create') }}
|
||||
{{ t('admin.agents.form.create') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.agents.create.cancel') }}
|
||||
{{ t('admin.agents.create.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface Agent {
|
||||
created_at: string
|
||||
last_login_at: string
|
||||
balance: number
|
||||
commission: number
|
||||
can_create_agent: boolean
|
||||
cards_count: number
|
||||
child_agents_count: number
|
||||
|
||||
@@ -119,9 +119,9 @@ const bannedCount = computed(() => {
|
||||
})
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('developer.agents.status.active'), value: 'active' },
|
||||
{ label: t('developer.agents.status.inactive'), value: 'inactive' },
|
||||
{ label: t('developer.agents.status.banned'), value: 'banned' },
|
||||
{ label: t('admin.agents.status.active'), value: 'active' },
|
||||
{ label: t('admin.agents.status.inactive'), value: 'inactive' },
|
||||
{ label: t('admin.agents.status.banned'), value: 'banned' },
|
||||
])
|
||||
|
||||
async function fetchAgents() {
|
||||
@@ -143,7 +143,7 @@ async function fetchAgents() {
|
||||
}
|
||||
|
||||
function goToCreate() {
|
||||
router.push('/developer/agents/create')
|
||||
router.push('/admin/agents/create')
|
||||
}
|
||||
|
||||
function toggleViewMode() {
|
||||
@@ -152,18 +152,18 @@ function toggleViewMode() {
|
||||
}
|
||||
|
||||
function handleEdit(agent: Agent) {
|
||||
router.push(`/developer/agents/${agent.id}/edit`)
|
||||
router.push(`/admin/agents/${agent.id}/edit`)
|
||||
}
|
||||
|
||||
async function handleToggleStatus(agent: Agent) {
|
||||
try {
|
||||
const newStatus = agent.status === 'banned' ? 'active' : 'banned'
|
||||
await api.put(`/dev/agents/${agent.id}/status`, { status: newStatus })
|
||||
toast.success(t('developer.agents.statusUpdateSuccess'))
|
||||
toast.success(t('admin.agents.statusUpdateSuccess'))
|
||||
fetchAgents()
|
||||
} catch (error: any) {
|
||||
console.error('切换代理状态失败:', error)
|
||||
toast.error(error.message || t('developer.agents.statusUpdateFailed'))
|
||||
toast.error(error.message || t('admin.agents.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,11 +177,11 @@ async function handleDelete() {
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/agents/${deleteTarget.value.id}`)
|
||||
toast.success(t('developer.agents.deleteSuccess'))
|
||||
toast.success(t('admin.agents.deleteSuccess'))
|
||||
fetchAgents()
|
||||
} catch (error: any) {
|
||||
console.error('删除代理失败:', error)
|
||||
toast.error(error.message || t('developer.agents.deleteFailed'))
|
||||
toast.error(error.message || t('admin.agents.deleteFailed'))
|
||||
} finally {
|
||||
deleteTarget.value = null
|
||||
}
|
||||
@@ -195,11 +195,11 @@ function confirmBatchDelete(ids: number[]) {
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
await api.delete('/dev/agents/batch', { agent_ids: batchDeleteIds.value } as any)
|
||||
toast.success(t('developer.agents.batchDeleteSuccess'))
|
||||
toast.success(t('admin.agents.batchDeleteSuccess'))
|
||||
fetchAgents()
|
||||
} catch (error: any) {
|
||||
console.error('批量删除失败:', error)
|
||||
toast.error(error.message || t('developer.agents.batchDeleteFailed'))
|
||||
toast.error(error.message || t('admin.agents.batchDeleteFailed'))
|
||||
} finally {
|
||||
batchDeleteIds.value = []
|
||||
}
|
||||
@@ -208,11 +208,11 @@ async function handleBatchDelete() {
|
||||
async function batchToggleStatus(ids: number[], status: string) {
|
||||
try {
|
||||
await api.post('/dev/agents/batch/status', { agent_ids: ids, status })
|
||||
toast.success(t('developer.agents.batchUpdateSuccess'))
|
||||
toast.success(t('admin.agents.batchUpdateSuccess'))
|
||||
fetchAgents()
|
||||
} catch (error: any) {
|
||||
console.error('批量更新状态失败:', error)
|
||||
toast.error(error.message || t('developer.agents.batchUpdateFailed'))
|
||||
toast.error(error.message || t('admin.agents.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,14 +223,14 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.agents.title')"
|
||||
:description="t('developer.agents.description')"
|
||||
:title="t('admin.agents.title')"
|
||||
:description="t('admin.agents.description')"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton @click="goToCreate">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.agents.addAgent') }}
|
||||
{{ t('admin.agents.addAgent') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -239,7 +239,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.agents.totalAgents') }}
|
||||
{{ t('admin.agents.totalAgents') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -253,7 +253,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.agents.status.active') }}
|
||||
{{ t('admin.agents.status.active') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -267,7 +267,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.agents.status.inactive') }}
|
||||
{{ t('admin.agents.status.inactive') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:clock" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -281,7 +281,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.agents.status.banned') }}
|
||||
{{ t('admin.agents.status.banned') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -314,7 +314,7 @@ onMounted(() => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('developer.agents.columns.status')"
|
||||
:title="t('admin.agents.columns.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</template>
|
||||
@@ -331,10 +331,10 @@ onMounted(() => {
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.agents.deleteAgent') }}
|
||||
{{ t('admin.agents.deleteAgent') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.agents.deleteAgentConfirm', { username: deleteTarget?.username }) }}
|
||||
{{ t('admin.agents.deleteAgentConfirm', { username: deleteTarget?.username }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
@@ -346,10 +346,10 @@ onMounted(() => {
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.agents.batchDeleteAgents') }}
|
||||
{{ t('admin.agents.batchDeleteAgents') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
{{ t('admin.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
|
||||
@@ -1,61 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { Announcement } from '@/pages/admin/announcements/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
import DataTable from '@/pages/admin/announcements/components/data-table.vue'
|
||||
import api, { BASE_URL } from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Application {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Announcement {
|
||||
id: number
|
||||
application_id: number
|
||||
application_name: string
|
||||
title: string
|
||||
content: string
|
||||
type: 'info' | 'warning' | 'error' | 'success'
|
||||
status: 'active' | 'inactive'
|
||||
is_top: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const announcements = ref<Announcement[]>([])
|
||||
const applications = ref<Application[]>([])
|
||||
const announcement = ref<Announcement | null>(null)
|
||||
const tableRef = ref()
|
||||
|
||||
const formData = ref({
|
||||
title: '',
|
||||
content: '',
|
||||
type: 'info' as 'info' | 'warning' | 'error' | 'success',
|
||||
status: 'active' as 'active' | 'inactive',
|
||||
is_status_active: true,
|
||||
is_top: false,
|
||||
const appFilter = ref<string>('')
|
||||
const typeFilter = ref<string>('')
|
||||
const statusFilter = ref<string>('')
|
||||
const searchFilter = ref<string>('')
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<Announcement | null>(null)
|
||||
const batchDeleteDialogOpen = ref(false)
|
||||
const batchDeleteIds = ref<number[]>([])
|
||||
|
||||
const applicationOptions = computed(() => {
|
||||
return applications.value.map(app => ({
|
||||
label: app.name,
|
||||
value: String(app.id),
|
||||
}))
|
||||
})
|
||||
|
||||
const typeOptions = [
|
||||
{ value: 'info', label: '信息', color: 'bg-blue-500', desc: '普通通知信息' },
|
||||
{ value: 'warning', label: '警告', color: 'bg-yellow-500', desc: '需要注意的内容' },
|
||||
{ value: 'error', label: '错误', color: 'bg-red-500', desc: '错误或故障通知' },
|
||||
{ value: 'success', label: '成功', color: 'bg-green-500', desc: '成功状态通知' },
|
||||
]
|
||||
const typeOptions = computed(() => [
|
||||
{ label: t('admin.announcements.types.info'), value: 'info' },
|
||||
{ label: t('admin.announcements.types.warning'), value: 'warning' },
|
||||
{ label: t('admin.announcements.types.error'), value: 'error' },
|
||||
{ label: t('admin.announcements.types.success'), value: 'success' },
|
||||
])
|
||||
|
||||
const selectedApplication = computed(() => {
|
||||
return applications.value.find(app => app.id === announcement.value?.application_id)
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('admin.announcements.statuses.active'), value: 'active' },
|
||||
{ label: t('admin.announcements.statuses.inactive'), value: 'inactive' },
|
||||
])
|
||||
|
||||
const filteredAnnouncements = computed(() => {
|
||||
let result = announcements.value
|
||||
|
||||
if (appFilter.value) {
|
||||
result = result.filter(a => String(a.application_id) === appFilter.value)
|
||||
}
|
||||
|
||||
if (typeFilter.value) {
|
||||
result = result.filter(a => a.type === typeFilter.value)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
result = result.filter(a => a.status === statusFilter.value)
|
||||
}
|
||||
|
||||
if (searchFilter.value) {
|
||||
const search = searchFilter.value.toLowerCase()
|
||||
result = result.filter(a =>
|
||||
a.title?.toLowerCase().includes(search)
|
||||
|| a.content?.toLowerCase().includes(search)
|
||||
|| a.application_name?.toLowerCase().includes(search),
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const selectedType = computed(() => {
|
||||
return typeOptions.find(t => t.value === formData.value.type)
|
||||
})
|
||||
const activeCount = computed(() => filteredAnnouncements.value.filter(a => a.status === 'active').length)
|
||||
const inactiveCount = computed(() => filteredAnnouncements.value.filter(a => a.status === 'inactive').length)
|
||||
const topCount = computed(() => filteredAnnouncements.value.filter(a => a.is_top).length)
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
@@ -67,249 +98,257 @@ async function fetchApplications() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAnnouncement() {
|
||||
async function fetchAnnouncements() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<Announcement>(`/dev/announcements/${route.params.id}`)
|
||||
announcement.value = data
|
||||
if (data) {
|
||||
formData.value.title = data.title
|
||||
formData.value.content = data.content
|
||||
formData.value.type = data.type
|
||||
formData.value.status = data.status
|
||||
formData.value.is_status_active = data.status === 'active'
|
||||
formData.value.is_top = data.is_top || false
|
||||
}
|
||||
const data = await api.get<{ announcements: Announcement[], total: number }>('/dev/announcements')
|
||||
announcements.value = data?.announcements || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取公告详情失败:', error)
|
||||
toast.error('公告不存在或已被删除')
|
||||
router.push('/developer/announcements')
|
||||
console.error('加载公告失败:', error)
|
||||
announcements.value = []
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formData.value.title) {
|
||||
toast.error('请输入公告标题')
|
||||
return
|
||||
}
|
||||
if (!formData.value.content) {
|
||||
toast.error('请输入公告内容')
|
||||
return
|
||||
}
|
||||
function goToCreate() {
|
||||
router.push('/admin/announcements/create')
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
function goToEdit(announcement: Announcement) {
|
||||
router.push(`/admin/announcements/${announcement.id}`)
|
||||
}
|
||||
|
||||
async function toggleTop(announcement: Announcement) {
|
||||
try {
|
||||
await api.put(`/dev/applications/${announcement.value?.application_id}/announcements/${route.params.id}`, {
|
||||
title: formData.value.title,
|
||||
content: formData.value.content,
|
||||
type: formData.value.type,
|
||||
status: formData.value.is_status_active ? 'active' : 'inactive',
|
||||
is_top: formData.value.is_top,
|
||||
})
|
||||
toast.success('公告更新成功')
|
||||
router.push('/developer/announcements')
|
||||
await api.put(`/dev/applications/${announcement.application_id}/announcements/${announcement.id}/top`)
|
||||
toast.success(announcement.is_top ? t('admin.announcements.unpinned') : t('admin.announcements.pinned'))
|
||||
fetchAnnouncements()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('更新公告失败:', error)
|
||||
toast.error(error.message || '更新失败')
|
||||
console.error('切换置顶失败:', error)
|
||||
toast.error(error.message || t('common.error'))
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(announcement: Announcement) {
|
||||
deleteTarget.value = announcement
|
||||
deleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!deleteTarget.value)
|
||||
return
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/applications/${deleteTarget.value.application_id}/announcements/${deleteTarget.value.id}`)
|
||||
toast.success(t('admin.announcements.deleteSuccess'))
|
||||
fetchAnnouncements()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('删除公告失败:', error)
|
||||
toast.error(error.message || t('common.error'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
deleteTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
const token = localStorage.getItem('token')
|
||||
const params = new URLSearchParams()
|
||||
if (appFilter.value)
|
||||
params.append('application_id', appFilter.value)
|
||||
if (typeFilter.value)
|
||||
params.append('type', typeFilter.value)
|
||||
if (statusFilter.value)
|
||||
params.append('status', statusFilter.value)
|
||||
|
||||
const queryString = params.toString()
|
||||
const url = `${BASE_URL}/dev/announcements/export?${queryString}&token=${token}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function handleBatchExport(ids: number[]) {
|
||||
if (!ids.length)
|
||||
return
|
||||
const token = localStorage.getItem('token')
|
||||
const params = new URLSearchParams()
|
||||
params.append('ids', ids.join(','))
|
||||
const url = `${BASE_URL}/dev/announcements/export?${params.toString()}&token=${token}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function confirmBatchDelete(ids: number[]) {
|
||||
batchDeleteIds.value = ids
|
||||
batchDeleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
await api.delete('/dev/announcements/batch', { ids: batchDeleteIds.value } as any)
|
||||
toast.success(t('admin.announcements.batchDeleteSuccess'))
|
||||
fetchAnnouncements()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量删除失败:', error)
|
||||
toast.error(error.message || t('common.error'))
|
||||
}
|
||||
finally {
|
||||
batchDeleteIds.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchApplications()
|
||||
fetchAnnouncement()
|
||||
fetchAnnouncements()
|
||||
const appParam = route.query.app as string
|
||||
if (appParam) {
|
||||
appFilter.value = appParam
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
title="编辑公告"
|
||||
description="编辑现有公告内容"
|
||||
:breadcrumbs="[
|
||||
{ title: '公告管理', href: '/developer/announcements' },
|
||||
{ title: '编辑公告' },
|
||||
]"
|
||||
:title="t('admin.announcements.title')"
|
||||
:description="t('admin.announcements.description')"
|
||||
sticky
|
||||
>
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<template #actions>
|
||||
<UiButton @click="goToCreate">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('admin.announcements.create') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:megaphone" class="size-5" />
|
||||
编辑公告
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>修改公告的基本信息和内容</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>所属应用</UiLabel>
|
||||
<div class="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
|
||||
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
||||
<span>{{ announcement?.application_name || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.announcements.totalAnnouncements') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:megaphone" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ filteredAnnouncements.length }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="title">
|
||||
公告标题 <span class="text-destructive">*</span>
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="title"
|
||||
v-model="formData.title"
|
||||
placeholder="输入公告标题"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.announcements.activeCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ activeCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="content">
|
||||
公告内容 <span class="text-destructive">*</span>
|
||||
</UiLabel>
|
||||
<textarea
|
||||
id="content"
|
||||
v-model="formData.content"
|
||||
class="flex min-h-[180px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder="输入公告详细内容..."
|
||||
:disabled="saving"
|
||||
/>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.announcements.inactiveCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ inactiveCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel>公告类型</UiLabel>
|
||||
<UiSelect v-model="formData.type" :disabled="saving">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue placeholder="选择类型" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="type in typeOptions" :key="type.value" :value="type.value">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full" :class="type.color" />
|
||||
{{ type.label }}
|
||||
</div>
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-lg border p-4">
|
||||
<div class="space-y-0.5">
|
||||
<UiLabel class="text-base">
|
||||
发布状态
|
||||
</UiLabel>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
启用后公告将对用户可见
|
||||
</p>
|
||||
</div>
|
||||
<UiSwitch
|
||||
v-model="formData.is_status_active"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-lg border p-4">
|
||||
<div class="space-y-0.5">
|
||||
<UiLabel class="text-base">
|
||||
置顶公告
|
||||
</UiLabel>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
置顶的公告将在应用内优先显示
|
||||
</p>
|
||||
</div>
|
||||
<UiSwitch
|
||||
v-model="formData.is_top"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:eye" class="size-5" />
|
||||
预览
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">所属应用</span>
|
||||
<span>{{ selectedApplication?.name || announcement?.application_name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">公告标题</span>
|
||||
<span class="truncate max-w-[120px]">{{ formData.title || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">公告类型</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="w-2 h-2 rounded-full" :class="selectedType?.color" />
|
||||
<span>{{ selectedType?.label || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">发布状态</span>
|
||||
<span>{{ formData.is_status_active ? '启用' : '禁用' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">是否置顶</span>
|
||||
<span>{{ formData.is_top ? '是' : '否' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<p class="text-sm text-muted-foreground mb-2">
|
||||
内容预览
|
||||
</p>
|
||||
<div class="rounded-lg bg-muted/50 p-3 min-h-[80px]">
|
||||
<p class="text-sm whitespace-pre-wrap">
|
||||
{{ formData.content || '暂无内容' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving || !formData.title || !formData.content"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
|
||||
保存更改
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
取消
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.announcements.topCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:pin" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ topCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
ref="tableRef"
|
||||
:loading
|
||||
:data="filteredAnnouncements"
|
||||
:on-toggle-top="toggleTop"
|
||||
:on-edit="goToEdit"
|
||||
:on-delete="confirmDelete"
|
||||
:search-filter="searchFilter"
|
||||
@refresh="fetchAnnouncements"
|
||||
@update:search-filter="searchFilter = $event"
|
||||
@export="handleExport"
|
||||
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||
@batch-export="handleBatchExport(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||
>
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('admin.announcements.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="typeFilter"
|
||||
:title="t('admin.announcements.type')"
|
||||
:options="typeOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('admin.announcements.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('common.delete')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.announcements.deleteAnnouncement') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.announcements.deleteConfirm', { title: deleteTarget?.title }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('common.delete')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.announcements.batchDelete') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.announcements.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
|
||||
return [
|
||||
{
|
||||
accessorKey: 'title',
|
||||
header: () => t('developer.announcements.columns.title'),
|
||||
header: () => t('admin.announcements.columns.title'),
|
||||
cell: ({ row }) => {
|
||||
return h('div', { class: 'space-y-1' }, [
|
||||
h('div', { class: 'font-medium' }, row.original.title),
|
||||
@@ -28,7 +28,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
|
||||
},
|
||||
{
|
||||
accessorKey: 'application_name',
|
||||
header: () => t('developer.announcements.columns.application'),
|
||||
header: () => t('admin.announcements.columns.application'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.original.application_name
|
||||
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
|
||||
@@ -36,13 +36,13 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: () => t('developer.announcements.columns.type'),
|
||||
header: () => t('admin.announcements.columns.type'),
|
||||
cell: ({ row }) => {
|
||||
const typeLabels: Record<string, string> = {
|
||||
info: t('developer.announcements.types.info'),
|
||||
warning: t('developer.announcements.types.warning'),
|
||||
error: t('developer.announcements.types.error'),
|
||||
success: t('developer.announcements.types.success'),
|
||||
info: t('admin.announcements.types.info'),
|
||||
warning: t('admin.announcements.types.warning'),
|
||||
error: t('admin.announcements.types.error'),
|
||||
success: t('admin.announcements.types.success'),
|
||||
}
|
||||
const typeClasses: Record<string, string> = {
|
||||
info: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
@@ -55,11 +55,11 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => t('developer.announcements.columns.status'),
|
||||
header: () => t('admin.announcements.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const statusLabels: Record<string, string> = {
|
||||
active: t('developer.announcements.statuses.active'),
|
||||
inactive: t('developer.announcements.statuses.inactive'),
|
||||
active: t('admin.announcements.statuses.active'),
|
||||
inactive: t('admin.announcements.statuses.inactive'),
|
||||
}
|
||||
const statusClasses: Record<string, string> = {
|
||||
active: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||
@@ -70,7 +70,7 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
|
||||
},
|
||||
{
|
||||
accessorKey: 'is_top',
|
||||
header: () => t('developer.announcements.columns.isTop'),
|
||||
header: () => t('admin.announcements.columns.isTop'),
|
||||
cell: ({ row }) => {
|
||||
return h(Button, {
|
||||
variant: 'ghost',
|
||||
@@ -86,14 +86,14 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.announcements.columns.createdAt'),
|
||||
header: () => t('admin.announcements.columns.createdAt'),
|
||||
cell: ({ row }) => {
|
||||
return h('span', { class: 'text-sm text-muted-foreground' }, new Date(row.original.created_at).toLocaleString('zh-CN'))
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => t('developer.announcements.columns.actions'),
|
||||
header: () => t('admin.announcements.columns.actions'),
|
||||
cell: ({ row }) => {
|
||||
return h('div', { class: 'flex items-center justify-end gap-1' }, [
|
||||
h(Button, {
|
||||
|
||||
@@ -33,7 +33,7 @@ function resetFilters() {
|
||||
<template>
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
:placeholder="t('developer.announcements.searchAll')"
|
||||
:placeholder="t('admin.announcements.searchAll')"
|
||||
:model-value="searchFilter"
|
||||
class="h-8 w-[200px] lg:w-[300px]"
|
||||
@update:model-value="emit('update:searchFilter', $event as string)"
|
||||
@@ -45,7 +45,7 @@ function resetFilters() {
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="resetFilters"
|
||||
>
|
||||
{{ t('developer.announcements.reset') }}
|
||||
{{ t('admin.announcements.reset') }}
|
||||
<X class="size-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -48,13 +48,13 @@ const table = generateVueTable<Announcement>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.announcements.select',
|
||||
title: 'developer.announcements.columns.title',
|
||||
application_name: 'developer.announcements.columns.application',
|
||||
type: 'developer.announcements.columns.type',
|
||||
status: 'developer.announcements.columns.status',
|
||||
is_top: 'developer.announcements.columns.isTop',
|
||||
created_at: 'developer.announcements.columns.createdAt',
|
||||
select: 'admin.announcements.select',
|
||||
title: 'admin.announcements.columns.title',
|
||||
application_name: 'admin.announcements.columns.application',
|
||||
type: 'admin.announcements.columns.type',
|
||||
status: 'admin.announcements.columns.status',
|
||||
is_top: 'admin.announcements.columns.isTop',
|
||||
created_at: 'admin.announcements.columns.createdAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -68,10 +68,10 @@ defineExpose({
|
||||
<div class="space-y-3">
|
||||
<BulkActions :table="table" entity-name="announcements">
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchExport')">
|
||||
{{ t('developer.announcements.batchExportBtn') }}
|
||||
{{ t('admin.announcements.batchExportBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||
{{ t('developer.announcements.batchDeleteBtn') }}
|
||||
{{ t('admin.announcements.batchDeleteBtn') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
@@ -85,7 +85,7 @@ defineExpose({
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UiButton variant="outline" size="sm" @click="emit('export')">
|
||||
{{ t('developer.announcements.exportBtn') }}
|
||||
{{ t('admin.announcements.exportBtn') }}
|
||||
</UiButton>
|
||||
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@ async function handleSubmit() {
|
||||
is_top: formData.value.is_top,
|
||||
})
|
||||
toast.success('公告创建成功')
|
||||
router.push('/developer/announcements')
|
||||
router.push('/admin/announcements')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('创建公告失败:', error)
|
||||
@@ -106,7 +106,7 @@ onMounted(() => {
|
||||
title="创建公告"
|
||||
description="创建新的系统公告"
|
||||
:breadcrumbs="[
|
||||
{ title: '公告管理', href: '/developer/announcements' },
|
||||
{ title: '公告管理', href: '/admin/announcements' },
|
||||
{ title: '创建公告' },
|
||||
]"
|
||||
sticky
|
||||
|
||||
@@ -46,15 +46,15 @@ const applicationOptions = computed(() => {
|
||||
})
|
||||
|
||||
const typeOptions = computed(() => [
|
||||
{ label: t('developer.announcements.types.info'), value: 'info' },
|
||||
{ label: t('developer.announcements.types.warning'), value: 'warning' },
|
||||
{ label: t('developer.announcements.types.error'), value: 'error' },
|
||||
{ label: t('developer.announcements.types.success'), value: 'success' },
|
||||
{ label: t('admin.announcements.types.info'), value: 'info' },
|
||||
{ label: t('admin.announcements.types.warning'), value: 'warning' },
|
||||
{ label: t('admin.announcements.types.error'), value: 'error' },
|
||||
{ label: t('admin.announcements.types.success'), value: 'success' },
|
||||
])
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('developer.announcements.statuses.active'), value: 'active' },
|
||||
{ label: t('developer.announcements.statuses.inactive'), value: 'inactive' },
|
||||
{ label: t('admin.announcements.statuses.active'), value: 'active' },
|
||||
{ label: t('admin.announcements.statuses.inactive'), value: 'inactive' },
|
||||
])
|
||||
|
||||
const filteredAnnouncements = computed(() => {
|
||||
@@ -114,17 +114,17 @@ async function fetchAnnouncements() {
|
||||
}
|
||||
|
||||
function goToCreate() {
|
||||
router.push('/developer/announcements/create')
|
||||
router.push('/admin/announcements/create')
|
||||
}
|
||||
|
||||
function goToEdit(announcement: Announcement) {
|
||||
router.push(`/developer/announcements/${announcement.id}`)
|
||||
router.push(`/admin/announcements/${announcement.id}`)
|
||||
}
|
||||
|
||||
async function toggleTop(announcement: Announcement) {
|
||||
try {
|
||||
await api.put(`/dev/applications/${announcement.application_id}/announcements/${announcement.id}/top`)
|
||||
toast.success(announcement.is_top ? t('developer.announcements.unpinned') : t('developer.announcements.pinned'))
|
||||
toast.success(announcement.is_top ? t('admin.announcements.unpinned') : t('admin.announcements.pinned'))
|
||||
fetchAnnouncements()
|
||||
}
|
||||
catch (error: any) {
|
||||
@@ -144,7 +144,7 @@ async function handleDelete() {
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/applications/${deleteTarget.value.application_id}/announcements/${deleteTarget.value.id}`)
|
||||
toast.success(t('developer.announcements.deleteSuccess'))
|
||||
toast.success(t('admin.announcements.deleteSuccess'))
|
||||
fetchAnnouncements()
|
||||
}
|
||||
catch (error: any) {
|
||||
@@ -189,7 +189,7 @@ function confirmBatchDelete(ids: number[]) {
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
await api.delete('/dev/announcements/batch', { ids: batchDeleteIds.value } as any)
|
||||
toast.success(t('developer.announcements.batchDeleteSuccess'))
|
||||
toast.success(t('admin.announcements.batchDeleteSuccess'))
|
||||
fetchAnnouncements()
|
||||
}
|
||||
catch (error: any) {
|
||||
@@ -213,14 +213,14 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.announcements.title')"
|
||||
:description="t('developer.announcements.description')"
|
||||
:title="t('admin.announcements.title')"
|
||||
:description="t('admin.announcements.description')"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton @click="goToCreate">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.announcements.create') }}
|
||||
{{ t('admin.announcements.create') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -229,7 +229,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.announcements.totalAnnouncements') }}
|
||||
{{ t('admin.announcements.totalAnnouncements') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:megaphone" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -243,7 +243,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.announcements.activeCount') }}
|
||||
{{ t('admin.announcements.activeCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -257,7 +257,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.announcements.inactiveCount') }}
|
||||
{{ t('admin.announcements.inactiveCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -271,7 +271,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.announcements.topCount') }}
|
||||
{{ t('admin.announcements.topCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:pin" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -302,17 +302,17 @@ onMounted(() => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.announcements.application')"
|
||||
:title="t('admin.announcements.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="typeFilter"
|
||||
:title="t('developer.announcements.type')"
|
||||
:title="t('admin.announcements.type')"
|
||||
:options="typeOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('developer.announcements.status')"
|
||||
:title="t('admin.announcements.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</template>
|
||||
@@ -329,10 +329,10 @@ onMounted(() => {
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.announcements.deleteAnnouncement') }}
|
||||
{{ t('admin.announcements.deleteAnnouncement') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.announcements.deleteConfirm', { title: deleteTarget?.title }) }}
|
||||
{{ t('admin.announcements.deleteConfirm', { title: deleteTarget?.title }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
@@ -344,10 +344,10 @@ onMounted(() => {
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.announcements.batchDelete') }}
|
||||
{{ t('admin.announcements.batchDelete') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.announcements.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
{{ t('admin.announcements.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
|
||||
@@ -120,14 +120,14 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.applications.title')"
|
||||
:description="t('developer.applications.description')"
|
||||
:title="t('admin.applications.title')"
|
||||
:description="t('admin.applications.description')"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton size="sm" @click="router.push('/developer/applications/create')">
|
||||
<UiButton size="sm" @click="router.push('/admin/applications/create')">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.applications.createApp') }}
|
||||
{{ t('admin.applications.createApp') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -135,7 +135,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.applications.totalApps') }}
|
||||
{{ t('admin.applications.totalApps') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -149,7 +149,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.applications.activeApps') }}
|
||||
{{ t('admin.applications.activeApps') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -163,7 +163,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.applications.totalUsers') }}
|
||||
{{ t('admin.applications.totalUsers') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -177,7 +177,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.applications.totalVerifyCount') }}
|
||||
{{ t('admin.applications.totalVerifyCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -193,23 +193,23 @@ onMounted(() => {
|
||||
<div class="flex flex-col gap-4 sm:flex-row">
|
||||
<UiInput
|
||||
v-model="searchQuery"
|
||||
:placeholder="t('developer.applications.searchPlaceholder')"
|
||||
:placeholder="t('admin.applications.searchPlaceholder')"
|
||||
class="h-9 w-40 lg:w-[250px]"
|
||||
/>
|
||||
|
||||
<UiSelect v-model="statusFilter">
|
||||
<UiSelectTrigger class="w-32">
|
||||
<UiSelectValue :placeholder="t('developer.applications.statusFilter')" />
|
||||
<UiSelectValue :placeholder="t('admin.applications.statusFilter')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="all">
|
||||
{{ t('developer.applications.allStatus') }}
|
||||
{{ t('admin.applications.allStatus') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="active">
|
||||
{{ t('developer.applications.active') }}
|
||||
{{ t('admin.applications.active') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="inactive">
|
||||
{{ t('developer.applications.inactive') }}
|
||||
{{ t('admin.applications.inactive') }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
@@ -227,14 +227,14 @@ onMounted(() => {
|
||||
<Icon icon="lucide:layout-grid" class="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ t('developer.applications.noApps') }}
|
||||
{{ t('admin.applications.noApps') }}
|
||||
</h3>
|
||||
<p class="text-muted-foreground mt-1 mb-4">
|
||||
{{ searchQuery || statusFilter !== 'all' ? t('developer.applications.noMatch') : t('developer.applications.createFirst') }}
|
||||
{{ searchQuery || statusFilter !== 'all' ? t('admin.applications.noMatch') : t('admin.applications.createFirst') }}
|
||||
</p>
|
||||
<UiButton v-if="!searchQuery && statusFilter === 'all'" @click="router.push('/developer/applications/create')">
|
||||
<UiButton v-if="!searchQuery && statusFilter === 'all'" @click="router.push('/admin/applications/create')">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.applications.createApp') }}
|
||||
{{ t('admin.applications.createApp') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ onMounted(() => {
|
||||
title="邮箱设置"
|
||||
description="配置应用的邮箱验证相关设置"
|
||||
:breadcrumbs="[
|
||||
{ title: '应用管理', href: '/developer/applications' },
|
||||
{ title: '应用管理', href: '/admin/applications' },
|
||||
{ title: '邮箱设置' },
|
||||
]"
|
||||
sticky
|
||||
|
||||
@@ -140,7 +140,7 @@ onMounted(() => {
|
||||
title="安全设置"
|
||||
description="配置应用的安全相关设置,包括加密方式和登录策略"
|
||||
:breadcrumbs="[
|
||||
{ title: '应用管理', href: '/developer/applications' },
|
||||
{ title: '应用管理', href: '/admin/applications' },
|
||||
{ title: '安全设置' },
|
||||
]"
|
||||
sticky
|
||||
|
||||
@@ -251,7 +251,7 @@ function toggleWeekday(index: number) {
|
||||
title="基本设置"
|
||||
description="修改应用的基本信息,包括名称、描述和图标等"
|
||||
:breadcrumbs="[
|
||||
{ title: '应用管理', href: '/developer/applications' },
|
||||
{ title: '应用管理', href: '/admin/applications' },
|
||||
{ title: '基本设置' },
|
||||
]"
|
||||
sticky
|
||||
|
||||
@@ -193,7 +193,7 @@ export function getColumns(actions: {
|
||||
h(DropdownMenuSeparator),
|
||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(app) }, () => [
|
||||
h(Trash2, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.applications.delete'),
|
||||
t('admin.applications.delete'),
|
||||
]),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -47,14 +47,14 @@ const table = generateVueTable<App>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.applications.select',
|
||||
name: 'developer.applications.columns.name',
|
||||
app_key: 'developer.applications.columns.appKey',
|
||||
billing_type: 'developer.applications.columns.billingType',
|
||||
login_policy: 'developer.applications.columns.loginPolicy',
|
||||
enable_trial: 'developer.applications.columns.trial',
|
||||
status: 'developer.applications.columns.status',
|
||||
created_at: 'developer.applications.columns.createdAt',
|
||||
select: 'admin.applications.select',
|
||||
name: 'admin.applications.columns.name',
|
||||
app_key: 'admin.applications.columns.appKey',
|
||||
billing_type: 'admin.applications.columns.billingType',
|
||||
login_policy: 'admin.applications.columns.loginPolicy',
|
||||
enable_trial: 'admin.applications.columns.trial',
|
||||
status: 'admin.applications.columns.status',
|
||||
created_at: 'admin.applications.columns.createdAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -69,7 +69,7 @@ defineExpose({
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UiInput
|
||||
:model-value="searchFilter"
|
||||
:placeholder="t('developer.applications.searchPlaceholder')"
|
||||
:placeholder="t('admin.applications.searchPlaceholder')"
|
||||
class="h-9 w-40 lg:w-[250px]"
|
||||
@update:model-value="emit('update:searchFilter', String($event))"
|
||||
/>
|
||||
|
||||
@@ -74,7 +74,7 @@ function toggleWeekday(index: number) {
|
||||
|
||||
async function handleCreate() {
|
||||
if (!formData.value.name.trim()) {
|
||||
toast.error(t('developer.applications.appNameRequired'))
|
||||
toast.error(t('admin.applications.appNameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,16 +120,16 @@ async function handleCreate() {
|
||||
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
toast.success(t('developer.applications.createSuccess'))
|
||||
router.push('/developer/applications')
|
||||
toast.success(t('admin.applications.createSuccess'))
|
||||
router.push('/admin/applications')
|
||||
}
|
||||
else {
|
||||
toast.error(data.message || t('developer.applications.createFailed'))
|
||||
toast.error(data.message || t('admin.applications.createFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('创建应用失败:', error)
|
||||
toast.error(t('developer.applications.createFailed'))
|
||||
toast.error(t('admin.applications.createFailed'))
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -139,31 +139,31 @@ async function handleCreate() {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.applications.createApp')"
|
||||
:description="t('developer.applications.createAppDesc')"
|
||||
:title="t('admin.applications.createApp')"
|
||||
:description="t('admin.applications.createAppDesc')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.applications.title'), href: '/developer/applications' },
|
||||
{ title: t('developer.applications.createApp') },
|
||||
{ title: t('admin.applications.title'), href: '/admin/applications' },
|
||||
{ title: t('admin.applications.createApp') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.applications.basicInfo') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.applications.basicInfoDesc') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.applications.basicInfo') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.applications.basicInfoDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="name">
|
||||
{{ t('developer.applications.appName') }} *
|
||||
{{ t('admin.applications.appName') }} *
|
||||
</UiLabel>
|
||||
<UiInput id="name" v-model="formData.name" :placeholder="t('developer.applications.appNamePlaceholder')" />
|
||||
<UiInput id="name" v-model="formData.name" :placeholder="t('admin.applications.appNamePlaceholder')" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.appIcon') }}</UiLabel>
|
||||
<UiLabel>{{ t('admin.applications.appIcon') }}</UiLabel>
|
||||
<div class="flex items-center gap-4">
|
||||
<div v-if="formData.iconPreview" class="relative group">
|
||||
<img
|
||||
@@ -185,7 +185,7 @@ async function handleCreate() {
|
||||
class="flex items-center justify-center h-12 px-4 border-2 border-dashed rounded-lg cursor-pointer hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<Icon icon="lucide:upload" class="w-4 h-4 mr-2 text-muted-foreground" />
|
||||
<span class="text-sm text-muted-foreground">{{ formData.iconPreview ? t('developer.applications.change') : t('developer.applications.upload') }}</span>
|
||||
<span class="text-sm text-muted-foreground">{{ formData.iconPreview ? t('admin.applications.change') : t('admin.applications.upload') }}</span>
|
||||
<input
|
||||
id="appIcon"
|
||||
type="file"
|
||||
@@ -200,17 +200,17 @@ async function handleCreate() {
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="description">
|
||||
{{ t('developer.applications.appDescription') }}
|
||||
{{ t('admin.applications.appDescription') }}
|
||||
</UiLabel>
|
||||
<UiTextarea id="description" v-model="formData.description" :placeholder="t('developer.applications.appDescriptionPlaceholder')" rows="3" />
|
||||
<UiTextarea id="description" v-model="formData.description" :placeholder="t('admin.applications.appDescriptionPlaceholder')" rows="3" />
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.applications.operationMode') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.applications.operationModeDesc') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.applications.operationMode') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.applications.operationModeDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<UiRadioGroup v-model="formData.billing_type" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -222,10 +222,10 @@ async function handleCreate() {
|
||||
<UiRadioGroupItem value="free" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.freeMode') }}
|
||||
{{ t('admin.applications.freeMode') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('developer.applications.freeModeDesc') }}
|
||||
{{ t('admin.applications.freeModeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,10 +238,10 @@ async function handleCreate() {
|
||||
<UiRadioGroupItem value="subscription" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.subscriptionMode') }}
|
||||
{{ t('admin.applications.subscriptionMode') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('developer.applications.subscriptionModeDesc') }}
|
||||
{{ t('admin.applications.subscriptionModeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -254,10 +254,10 @@ async function handleCreate() {
|
||||
<UiRadioGroupItem value="time" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.timeMode') }}
|
||||
{{ t('admin.applications.timeMode') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('developer.applications.timeModeDesc') }}
|
||||
{{ t('admin.applications.timeModeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -270,10 +270,10 @@ async function handleCreate() {
|
||||
<UiRadioGroupItem value="count" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.countMode') }}
|
||||
{{ t('admin.applications.countMode') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('developer.applications.countModeDesc') }}
|
||||
{{ t('admin.applications.countModeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,30 +282,30 @@ async function handleCreate() {
|
||||
<div v-if="formData.billing_type !== 'free'" class="mt-4 pt-4 border-t space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.deductionCycle') }}</UiLabel>
|
||||
<UiLabel>{{ t('admin.applications.deductionCycle') }}</UiLabel>
|
||||
<UiSelect v-model="formData.deduction_cycle">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.applications.selectDeductionCycle')" />
|
||||
<UiSelectValue :placeholder="t('admin.applications.selectDeductionCycle')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="per_use">
|
||||
{{ t('developer.applications.perUse') }}
|
||||
{{ t('admin.applications.perUse') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="per_minute">
|
||||
{{ t('developer.applications.perMinute') }}
|
||||
{{ t('admin.applications.perMinute') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="per_hour">
|
||||
{{ t('developer.applications.perHour') }}
|
||||
{{ t('admin.applications.perHour') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="per_day">
|
||||
{{ t('developer.applications.perDay') }}
|
||||
{{ t('admin.applications.perDay') }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.deductionAmount') }}</UiLabel>
|
||||
<UiLabel>{{ t('admin.applications.deductionAmount') }}</UiLabel>
|
||||
<UiNumberField v-model="formData.deduction_amount" :min="0" :step="0.01">
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
@@ -321,8 +321,8 @@ async function handleCreate() {
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.applications.loginPolicy') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.applications.loginPolicyDesc') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.applications.loginPolicy') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.applications.loginPolicyDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<UiRadioGroup v-model="formData.login_policy" class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
@@ -334,10 +334,10 @@ async function handleCreate() {
|
||||
<UiRadioGroupItem value="loose" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.looseMode') }}
|
||||
{{ t('admin.applications.looseMode') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('developer.applications.looseModeDesc') }}
|
||||
{{ t('admin.applications.looseModeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -350,10 +350,10 @@ async function handleCreate() {
|
||||
<UiRadioGroupItem value="strict" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.strictMode') }}
|
||||
{{ t('admin.applications.strictMode') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('developer.applications.strictModeDesc') }}
|
||||
{{ t('admin.applications.strictModeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -366,10 +366,10 @@ async function handleCreate() {
|
||||
<UiRadioGroupItem value="hybrid" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.hybridMode') }}
|
||||
{{ t('admin.applications.hybridMode') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('developer.applications.hybridModeDesc') }}
|
||||
{{ t('admin.applications.hybridModeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -379,14 +379,14 @@ async function handleCreate() {
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.applications.trialSettings') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.applications.trialSettings') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.applications.trialSettings') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.applications.trialSettings') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.enableTrial') }}
|
||||
{{ t('admin.applications.enableTrial') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
新用户注册后自动获得试用权益
|
||||
@@ -397,7 +397,7 @@ async function handleCreate() {
|
||||
|
||||
<div v-if="formData.enable_trial" class="space-y-4 pt-4 border-t">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.trialBalance') }}</UiLabel>
|
||||
<UiLabel>{{ t('admin.applications.trialBalance') }}</UiLabel>
|
||||
<UiNumberField
|
||||
v-model="formData.trial_balance"
|
||||
:min="0"
|
||||
@@ -420,14 +420,14 @@ async function handleCreate() {
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.applications.freePeriodSettings') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.applications.freePeriodSettings') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.applications.freePeriodSettings') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.applications.freePeriodSettings') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.enableFreePeriod') }}
|
||||
{{ t('admin.applications.enableFreePeriod') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
在设定的时间段内,所有用户可免费使用
|
||||
@@ -438,7 +438,7 @@ async function handleCreate() {
|
||||
|
||||
<div v-if="formData.enable_free_period" class="space-y-4 pt-4 border-t">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.periodType') }}</UiLabel>
|
||||
<UiLabel>{{ t('admin.applications.periodType') }}</UiLabel>
|
||||
<UiRadioGroup v-model="formData.free_period_type" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||||
@@ -448,7 +448,7 @@ async function handleCreate() {
|
||||
<UiRadioGroupItem value="range" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.dateRange') }}
|
||||
{{ t('admin.applications.dateRange') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
指定开始和结束日期时间
|
||||
@@ -464,7 +464,7 @@ async function handleCreate() {
|
||||
<UiRadioGroupItem value="weekly" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.weekly') }}
|
||||
{{ t('admin.applications.weekly') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
每周固定日期和时间段
|
||||
@@ -476,18 +476,18 @@ async function handleCreate() {
|
||||
|
||||
<div v-if="formData.free_period_type === 'range'" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.startTime') }}</UiLabel>
|
||||
<UiDatePickerDateTimePicker v-model="formData.free_period_start" :placeholder="t('developer.applications.startTime')" />
|
||||
<UiLabel>{{ t('admin.applications.startTime') }}</UiLabel>
|
||||
<UiDatePickerDateTimePicker v-model="formData.free_period_start" :placeholder="t('admin.applications.startTime')" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.endTime') }}</UiLabel>
|
||||
<UiDatePickerDateTimePicker v-model="formData.free_period_end" :placeholder="t('developer.applications.endTime')" />
|
||||
<UiLabel>{{ t('admin.applications.endTime') }}</UiLabel>
|
||||
<UiDatePickerDateTimePicker v-model="formData.free_period_end" :placeholder="t('admin.applications.endTime')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.free_period_type === 'weekly'" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.weekdays') }}</UiLabel>
|
||||
<UiLabel>{{ t('admin.applications.weekdays') }}</UiLabel>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template v-for="(day, index) in ['周一', '周二', '周三', '周四', '周五', '周六', '周日']" :key="index">
|
||||
<UiButton
|
||||
@@ -507,15 +507,15 @@ async function handleCreate() {
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.startTime') }}</UiLabel>
|
||||
<UiDatePickerTimePicker v-model="formData.free_period_start_time" :placeholder="t('developer.applications.startTime')" />
|
||||
<UiLabel>{{ t('admin.applications.startTime') }}</UiLabel>
|
||||
<UiDatePickerTimePicker v-model="formData.free_period_start_time" :placeholder="t('admin.applications.startTime')" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
每天免费开始时间
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.endTime') }}</UiLabel>
|
||||
<UiDatePickerTimePicker v-model="formData.free_period_end_time" :placeholder="t('developer.applications.endTime')" />
|
||||
<UiLabel>{{ t('admin.applications.endTime') }}</UiLabel>
|
||||
<UiDatePickerTimePicker v-model="formData.free_period_end_time" :placeholder="t('admin.applications.endTime')" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
每天免费结束时间
|
||||
</p>
|
||||
@@ -528,29 +528,29 @@ async function handleCreate() {
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.applications.securitySettings') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.applications.securitySettingsDesc') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.applications.securitySettings') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.applications.securitySettingsDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.encryptionType') }}</UiLabel>
|
||||
<UiLabel>{{ t('admin.applications.encryptionType') }}</UiLabel>
|
||||
<UiSelect v-model="formData.encrypt_type">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.applications.encryptionType')" />
|
||||
<UiSelectValue :placeholder="t('admin.applications.encryptionType')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="none">
|
||||
{{ t('developer.applications.noEncryption') }}
|
||||
{{ t('admin.applications.noEncryption') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="aes">
|
||||
{{ t('developer.applications.aesEncryption') }}
|
||||
{{ t('admin.applications.aesEncryption') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="rsa">
|
||||
{{ t('developer.applications.rsaEncryption') }}
|
||||
{{ t('admin.applications.rsaEncryption') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="rc4">
|
||||
{{ t('developer.applications.rc4Encryption') }}
|
||||
{{ t('admin.applications.rc4Encryption') }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
@@ -563,7 +563,7 @@ async function handleCreate() {
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.applications.secretKey') }}</UiLabel>
|
||||
<UiLabel>{{ t('admin.applications.secretKey') }}</UiLabel>
|
||||
<div class="relative">
|
||||
<UiInput
|
||||
v-model="formData.secret_key"
|
||||
@@ -591,14 +591,14 @@ async function handleCreate() {
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.applications.deviceBind') }}</UiCardTitle>
|
||||
<UiCardTitle>{{ t('admin.applications.deviceBind') }}</UiCardTitle>
|
||||
<UiCardDescription>控制用户登录时是否需要绑定设备</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.deviceBind') }}
|
||||
{{ t('admin.applications.deviceBind') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
启用后,用户登录时需要绑定设备,可限制同时登录的设备数量
|
||||
@@ -611,7 +611,7 @@ async function handleCreate() {
|
||||
</div>
|
||||
|
||||
<div v-if="formData.bind_type === 'device'" class="space-y-2 pt-4 border-t">
|
||||
<UiLabel>{{ t('developer.applications.maxDevices') }}</UiLabel>
|
||||
<UiLabel>{{ t('admin.applications.maxDevices') }}</UiLabel>
|
||||
<UiNumberField v-model="formData.max_devices" :min="1" :max="10">
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
@@ -627,7 +627,7 @@ async function handleCreate() {
|
||||
<div class="flex items-center justify-between pt-4 border-t">
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.applications.allowMultiOpen') }}
|
||||
{{ t('admin.applications.allowMultiOpen') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
允许同一账号在同一设备上同时运行多个实例
|
||||
@@ -639,12 +639,12 @@ async function handleCreate() {
|
||||
</UiCard>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<UiButton variant="outline" @click="router.push('/developer/applications')">
|
||||
<UiButton variant="outline" @click="router.push('/admin/applications')">
|
||||
取消
|
||||
</UiButton>
|
||||
<UiButton :disabled="loading" @click="handleCreate">
|
||||
<Icon v-if="loading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
{{ t('developer.applications.createApp') }}
|
||||
{{ t('admin.applications.createApp') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,57 +2,89 @@
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { CardType } from '@/pages/developer/card-types/data/schema'
|
||||
import type { CardType } from '@/pages/admin/card-types/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
import DataTable from '@/pages/admin/card-types/components/data-table.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const API_BASE = 'http://localhost:8080/api/v1'
|
||||
|
||||
interface Application {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const cardTypes = ref<CardType[]>([])
|
||||
const applications = ref<Application[]>([])
|
||||
const cardType = ref<CardType | null>(null)
|
||||
const tableRef = ref()
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
application_id: '',
|
||||
price: 0,
|
||||
value: 0,
|
||||
validity_type: 'limited' as 'permanent' | 'limited',
|
||||
const appFilter = ref<string>('')
|
||||
const statusFilter = ref<string>('')
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<CardType | null>(null)
|
||||
const batchDeleteDialogOpen = ref(false)
|
||||
const batchDeleteIds = ref<(string | number)[]>([])
|
||||
|
||||
const applicationOptions = computed(() => {
|
||||
return applications.value.map(app => ({
|
||||
label: app.name,
|
||||
value: String(app.id),
|
||||
}))
|
||||
})
|
||||
|
||||
async function fetchCardType() {
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('admin.cardTypes.statusOptions.active'), value: 'active' },
|
||||
{ label: t('admin.cardTypes.statusOptions.inactive'), value: 'inactive' },
|
||||
])
|
||||
|
||||
const filteredCardTypes = computed(() => {
|
||||
let result = cardTypes.value
|
||||
|
||||
if (appFilter.value) {
|
||||
result = result.filter(type => String(type.application_id) === appFilter.value)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
result = result.filter(type => type.status === statusFilter.value)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const activeTypesCount = computed(() => filteredCardTypes.value.filter(type => type.status === 'active').length)
|
||||
const totalGeneratedCount = computed(() => filteredCardTypes.value.reduce((sum, type) => sum + (type.generatedCount || 0), 0))
|
||||
const totalValue = computed(() => filteredCardTypes.value.reduce((sum, type) => sum + (type.price * (type.generatedCount || 0)), 0))
|
||||
|
||||
async function fetchCardTypes() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<{ card_type: CardType }>(`/dev/card-types/${route.params.id}`)
|
||||
cardType.value = data?.card_type || data
|
||||
if (cardType.value) {
|
||||
form.value = {
|
||||
name: cardType.value.name,
|
||||
description: cardType.value.description,
|
||||
application_id: cardType.value.application_id ? String(cardType.value.application_id) : '',
|
||||
price: cardType.value.price,
|
||||
value: cardType.value.value === -1 ? 0 : cardType.value.value,
|
||||
validity_type: cardType.value.value === -1 ? 'permanent' : 'limited',
|
||||
}
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch(`${API_BASE}/dev/card-types`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
cardTypes.value = Array.isArray(data.data?.card_types) ? data.data.card_types : []
|
||||
}
|
||||
else {
|
||||
cardTypes.value = []
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('获取卡类信息失败:', error)
|
||||
toast.error(error.message || '获取卡类信息失败')
|
||||
router.push('/developer/card-types')
|
||||
catch (error) {
|
||||
console.error('获取卡密类型失败:', error)
|
||||
cardTypes.value = []
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -61,264 +93,317 @@ async function fetchCardType() {
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
const data = await api.get<{ applications: Application[] }>('/dev/applications')
|
||||
applications.value = Array.isArray(data?.applications) ? data.applications : []
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch(`${API_BASE}/dev/applications`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
applications.value = Array.isArray(data.data?.applications) ? data.data.applications : []
|
||||
}
|
||||
else {
|
||||
applications.value = []
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取应用列表失败:', error)
|
||||
applications.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const selectedApplication = computed(() => {
|
||||
if (form.value.application_id) {
|
||||
return applications.value.find(app => String(app.id) === form.value.application_id)
|
||||
}
|
||||
return null
|
||||
})
|
||||
function goToCreate() {
|
||||
router.push('/admin/card-types/create')
|
||||
}
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (form.value.validity_type === 'permanent') {
|
||||
return t('developer.cardTypes.units.permanent')
|
||||
}
|
||||
return form.value.value.toString()
|
||||
})
|
||||
function goToEdit(type: CardType) {
|
||||
router.push(`/admin/card-types/${type.id}`)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.name) {
|
||||
toast.error(t('developer.cardTypes.create.nameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.application_id) {
|
||||
toast.error(t('developer.cardTypes.create.applicationRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
if (form.value.validity_type === 'limited' && form.value.value <= 0) {
|
||||
toast.error(t('developer.cardTypes.create.valueRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
const value = form.value.validity_type === 'permanent' ? -1 : form.value.value
|
||||
|
||||
saving.value = true
|
||||
async function toggleStatus(type: CardType) {
|
||||
try {
|
||||
await api.put(`/dev/card-types/${route.params.id}`, {
|
||||
application_id: Number(form.value.application_id),
|
||||
name: form.value.name,
|
||||
description: form.value.description,
|
||||
value,
|
||||
price: form.value.price,
|
||||
const token = localStorage.getItem('token')
|
||||
const newStatus = type.status === 'active' ? 'inactive' : 'active'
|
||||
const response = await fetch(`${API_BASE}/dev/card-types/${type.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
})
|
||||
toast.success(t('developer.cardTypes.create.saveSuccess'))
|
||||
router.push('/developer/card-types')
|
||||
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
toast.success(t('admin.cardTypes.statusUpdateSuccess'))
|
||||
fetchCardTypes()
|
||||
}
|
||||
else {
|
||||
toast.error(data.message || t('admin.cardTypes.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('保存卡密类型失败:', error)
|
||||
toast.error(error.message || t('developer.cardTypes.create.saveFailed'))
|
||||
catch (error) {
|
||||
console.error('切换卡密类型状态失败:', error)
|
||||
toast.error(t('admin.cardTypes.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteType(type: CardType) {
|
||||
deleteTarget.value = type
|
||||
deleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDeleteType() {
|
||||
if (!deleteTarget.value)
|
||||
return
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch(`${API_BASE}/dev/card-types/${deleteTarget.value.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
toast.success(t('admin.cardTypes.deleteSuccess'))
|
||||
fetchCardTypes()
|
||||
}
|
||||
else {
|
||||
toast.error(data.message || t('admin.cardTypes.deleteFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('删除卡密类型失败:', error)
|
||||
toast.error(t('admin.cardTypes.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
deleteTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function confirmBatchDelete(ids: (string | number)[]) {
|
||||
batchDeleteIds.value = ids
|
||||
batchDeleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
for (const id of batchDeleteIds.value) {
|
||||
const response = await fetch(`${API_BASE}/dev/card-types/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
successCount++
|
||||
}
|
||||
else {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
toast.success(t('admin.cardTypes.deleteSuccess'))
|
||||
fetchCardTypes()
|
||||
}
|
||||
if (failCount > 0) {
|
||||
toast.error(t('admin.cardTypes.deleteFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
toast.error(t('admin.cardTypes.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
batchDeleteIds.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function batchToggleStatus(ids: (string | number)[], status: string) {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
for (const id of ids) {
|
||||
const response = await fetch(`${API_BASE}/dev/card-types/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ status }),
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
successCount++
|
||||
}
|
||||
else {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
toast.success(t('admin.cardTypes.batchUpdateSuccess'))
|
||||
fetchCardTypes()
|
||||
}
|
||||
if (failCount > 0) {
|
||||
toast.error(t('admin.cardTypes.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('批量更新状态失败:', error)
|
||||
toast.error(t('admin.cardTypes.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCardType()
|
||||
fetchCardTypes()
|
||||
fetchApplications()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cardTypes.create.editTitle')"
|
||||
:description="t('developer.cardTypes.create.editDescription')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.cardTypes.title'), href: '/developer/card-types' },
|
||||
{ title: t('developer.cardTypes.create.editTitle') },
|
||||
]"
|
||||
:title="t('admin.cardTypes.title')"
|
||||
:description="t('admin.cardTypes.description')"
|
||||
sticky
|
||||
>
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<template #actions>
|
||||
<UiButton @click="goToCreate">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('admin.cardTypes.createCardType') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:info" class="size-5" />
|
||||
{{ t('developer.cardTypes.create.basicInfo') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cardTypes.create.basicInfoDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="name">
|
||||
{{ t('developer.cardTypes.create.name') }} *
|
||||
</UiLabel>
|
||||
<UiInput id="name" v-model="form.name" :placeholder="t('developer.cardTypes.create.namePlaceholder')" />
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cardTypes.totalTypes') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ filteredCardTypes.length }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="description">
|
||||
{{ t('developer.cardTypes.create.description') }}
|
||||
</UiLabel>
|
||||
<UiTextarea id="description" v-model="form.description" :placeholder="t('developer.cardTypes.create.descriptionPlaceholder')" rows="3" />
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cardTypes.activeTypes') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ activeTypesCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="application">
|
||||
{{ t('developer.cardTypes.create.application') }} *
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.application_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cardTypes.create.selectApplication')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
|
||||
{{ app.name }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cardTypes.generatedCards') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:hash" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ totalGeneratedCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.cardTypes.create.billingConfig') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cardTypes.create.billingConfigDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="price">
|
||||
{{ t('developer.cardTypes.create.price') }}
|
||||
</UiLabel>
|
||||
<div class="flex items-center gap-2">
|
||||
<UiNumberField v-model="form.price" :min="0" :step="0.01" class="max-w-[200px]">
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
<UiNumberFieldInput />
|
||||
<UiNumberFieldIncrement />
|
||||
</UiNumberFieldContent>
|
||||
</UiNumberField>
|
||||
<span class="text-sm text-muted-foreground">{{ t('developer.cardTypes.create.priceUnit') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.cardTypes.create.validityType') }}</UiLabel>
|
||||
<UiRadioGroup v-model="form.validity_type" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||||
:class="form.validity_type === 'limited' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
|
||||
@click="form.validity_type = 'limited'"
|
||||
>
|
||||
<UiRadioGroupItem value="limited" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.cardTypes.create.limited') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
充值指定金额余额
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||||
:class="form.validity_type === 'permanent' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
|
||||
@click="form.validity_type = 'permanent'"
|
||||
>
|
||||
<UiRadioGroupItem value="permanent" class="mt-0.5" />
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.cardTypes.create.permanent') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
永久会员,余额无限
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</UiRadioGroup>
|
||||
</div>
|
||||
|
||||
<div v-if="form.validity_type === 'limited'" class="space-y-2">
|
||||
<UiLabel for="value">
|
||||
充值金额
|
||||
</UiLabel>
|
||||
<UiNumberField v-model="form.value" :min="0.01" :step="0.01" class="max-w-[200px]">
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
<UiNumberFieldInput />
|
||||
<UiNumberFieldIncrement />
|
||||
</UiNumberFieldContent>
|
||||
</UiNumberField>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:eye" class="size-5" />
|
||||
{{ t('developer.cardTypes.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewName') }}</span>
|
||||
<span>{{ form.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewApplication') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewPrice') }}</span>
|
||||
<span>¥{{ form.price.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewValue') }}</span>
|
||||
<span>{{ displayValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving || !form.name || !form.application_id"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cardTypes.create.save') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.cardTypes.create.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cardTypes.totalValue') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
¥{{ totalValue }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
ref="tableRef"
|
||||
:loading
|
||||
:data="filteredCardTypes"
|
||||
:on-edit="goToEdit"
|
||||
:on-toggle-status="toggleStatus"
|
||||
:on-delete="confirmDeleteType"
|
||||
@refresh="fetchCardTypes"
|
||||
@batch-enable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'inactive').map((r: any) => r.original.id) || [], 'active')"
|
||||
@batch-disable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'active').map((r: any) => r.original.id) || [], 'inactive')"
|
||||
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||
>
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('admin.cardTypes.columns.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('admin.cardTypes.columns.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('admin.cardTypes.delete')"
|
||||
:cancel-button-text="t('admin.cards.create.cancelBtn')"
|
||||
@confirm="handleDeleteType"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.cardTypes.delete') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.cardTypes.deleteConfirm', { name: deleteTarget?.name }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('admin.cardTypes.delete')"
|
||||
:cancel-button-text="t('admin.cards.create.cancelBtn')"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.cardTypes.batchDelete') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.cardTypes.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
|
||||
import { Ban, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { CardType } from '@/pages/developer/card-types/data/schema'
|
||||
import type { CardType } from '@/pages/admin/card-types/data/schema'
|
||||
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -24,14 +24,14 @@ export function getColumns(actions: {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: () => t('developer.cardTypes.columns.name'),
|
||||
header: () => t('admin.cardTypes.columns.name'),
|
||||
cell: ({ row }) => {
|
||||
return row.getValue('name') as string
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: () => t('developer.cardTypes.columns.description'),
|
||||
header: () => t('admin.cardTypes.columns.description'),
|
||||
cell: ({ row }) => {
|
||||
const description = row.getValue('description') as string
|
||||
return description || '-'
|
||||
@@ -39,7 +39,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'application.name',
|
||||
header: () => t('developer.cardTypes.columns.application'),
|
||||
header: () => t('admin.cardTypes.columns.application'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.original.application?.name
|
||||
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
|
||||
@@ -56,7 +56,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'price',
|
||||
header: () => t('developer.cardTypes.columns.price'),
|
||||
header: () => t('admin.cardTypes.columns.price'),
|
||||
cell: ({ row }) => {
|
||||
const price = row.getValue('price') as number
|
||||
return `¥${price}`
|
||||
@@ -64,14 +64,14 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'value',
|
||||
header: () => t('developer.cardTypes.columns.value'),
|
||||
header: () => t('admin.cardTypes.columns.value'),
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue('value') as number
|
||||
const rechargeType = row.original.recharge_type
|
||||
const valueUnit = row.original.value_unit
|
||||
|
||||
if (value === -1) {
|
||||
return h(Badge, { variant: 'default' }, () => t('developer.cardTypes.units.permanent'))
|
||||
return h(Badge, { variant: 'default' }, () => t('admin.cardTypes.units.permanent'))
|
||||
}
|
||||
|
||||
if (rechargeType === 'subscription' && valueUnit) {
|
||||
@@ -90,24 +90,24 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'generatedCount',
|
||||
header: () => t('developer.cardTypes.columns.generatedCount'),
|
||||
header: () => t('admin.cardTypes.columns.generatedCount'),
|
||||
cell: ({ row }) => {
|
||||
const count = row.original.generatedCount ?? 0
|
||||
return `${count} ${t('developer.cardTypes.units.cards')}`
|
||||
return `${count} ${t('admin.cardTypes.units.cards')}`
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => t('developer.cardTypes.columns.status'),
|
||||
header: () => t('admin.cardTypes.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
const isActive = status === 'active'
|
||||
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => t(`developer.cardTypes.statusOptions.${status}`))
|
||||
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => t(`dmin.cardTypes.statusOptions.${status}`))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.cardTypes.columns.createdAt'),
|
||||
header: () => t('admin.cardTypes.columns.createdAt'),
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue('created_at')
|
||||
if (!createdAt)
|
||||
@@ -152,16 +152,16 @@ export function getColumns(actions: {
|
||||
() => [
|
||||
h(DropdownMenuItem, { onClick: () => actions.onEdit(cardType) }, () => [
|
||||
h(Pencil, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.cardTypes.edit'),
|
||||
t('admin.cardTypes.edit'),
|
||||
]),
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(cardType) }, () => [
|
||||
h(Ban, { class: 'mr-2 h-4 w-4' }),
|
||||
isActive ? t('developer.cardTypes.disable') : t('developer.cardTypes.enable'),
|
||||
isActive ? t('admin.cardTypes.disable') : t('admin.cardTypes.enable'),
|
||||
]),
|
||||
h(DropdownMenuSeparator),
|
||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(cardType) }, () => [
|
||||
h(Trash2, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.cardTypes.delete'),
|
||||
t('admin.cardTypes.delete'),
|
||||
]),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { X } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { CardType } from '@/pages/developer/card-types/data/schema'
|
||||
import type { CardType } from '@/pages/admin/card-types/data/schema'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
|
||||
<template>
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
:placeholder="t('developer.cardTypes.searchName')"
|
||||
:placeholder="t('admin.cardTypes.searchName')"
|
||||
:model-value="(table.getColumn('name')?.getFilterValue() as string) ?? ''"
|
||||
class="h-8 w-[150px] lg:w-[250px]"
|
||||
@input="table.getColumn('name')?.setFilterValue($event.target.value)"
|
||||
@@ -36,7 +36,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="table.resetColumnFilters()"
|
||||
>
|
||||
{{ t('developer.cardTypes.reset') }}
|
||||
{{ t('admin.cardTypes.reset') }}
|
||||
<X class="size-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -46,16 +46,16 @@ const table = generateVueTable<CardType>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.cardTypes.select',
|
||||
name: 'developer.cardTypes.columns.name',
|
||||
description: 'developer.cardTypes.columns.description',
|
||||
application_name: 'developer.cardTypes.columns.application',
|
||||
billing_type: 'developer.cardTypes.columns.billingType',
|
||||
price: 'developer.cardTypes.columns.price',
|
||||
value: 'developer.cardTypes.columns.value',
|
||||
generatedCount: 'developer.cardTypes.columns.generatedCount',
|
||||
status: 'developer.cardTypes.columns.status',
|
||||
created_at: 'developer.cardTypes.columns.createdAt',
|
||||
select: 'admin.cardTypes.select',
|
||||
name: 'admin.cardTypes.columns.name',
|
||||
description: 'admin.cardTypes.columns.description',
|
||||
application_name: 'admin.cardTypes.columns.application',
|
||||
billing_type: 'admin.cardTypes.columns.billingType',
|
||||
price: 'admin.cardTypes.columns.price',
|
||||
value: 'admin.cardTypes.columns.value',
|
||||
generatedCount: 'admin.cardTypes.columns.generatedCount',
|
||||
status: 'admin.cardTypes.columns.status',
|
||||
created_at: 'admin.cardTypes.columns.createdAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -72,13 +72,13 @@ defineExpose({
|
||||
</div>
|
||||
<BulkActions :table="table" entity-name="cardTypes">
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
|
||||
{{ t('developer.cardTypes.batchEnableBtn') }}
|
||||
{{ t('admin.cardTypes.batchEnableBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
|
||||
{{ t('developer.cardTypes.batchDisableBtn') }}
|
||||
{{ t('admin.cardTypes.batchDisableBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||
{{ t('developer.cardTypes.batchDeleteBtn') }}
|
||||
{{ t('admin.cardTypes.batchDeleteBtn') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
|
||||
@@ -61,15 +61,15 @@ watch(selectedApplication, (app) => {
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (form.value.is_permanent) {
|
||||
return t('developer.cardTypes.units.permanent')
|
||||
return t('admin.cardTypes.units.permanent')
|
||||
}
|
||||
if (form.value.recharge_type === 'subscription') {
|
||||
const unitMap: Record<string, string> = {
|
||||
minute: t('developer.cardTypes.units.minute'),
|
||||
hour: t('developer.cardTypes.units.hour'),
|
||||
day: t('developer.cardTypes.units.day'),
|
||||
month: t('developer.cardTypes.units.month'),
|
||||
year: t('developer.cardTypes.units.year'),
|
||||
minute: t('admin.cardTypes.units.minute'),
|
||||
hour: t('admin.cardTypes.units.hour'),
|
||||
day: t('admin.cardTypes.units.day'),
|
||||
month: t('admin.cardTypes.units.month'),
|
||||
year: t('admin.cardTypes.units.year'),
|
||||
}
|
||||
return `${form.value.value} ${unitMap[form.value.value_unit]}`
|
||||
}
|
||||
@@ -78,17 +78,17 @@ const displayValue = computed(() => {
|
||||
|
||||
async function handleCreate() {
|
||||
if (!form.value.name) {
|
||||
toast.error(t('developer.cardTypes.create.nameRequired'))
|
||||
toast.error(t('admin.cardTypes.create.nameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.application_id) {
|
||||
toast.error(t('developer.cardTypes.create.applicationRequired'))
|
||||
toast.error(t('admin.cardTypes.create.applicationRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.is_permanent && form.value.value <= 0) {
|
||||
toast.error(t('developer.cardTypes.create.valueRequired'))
|
||||
toast.error(t('admin.cardTypes.create.valueRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,12 +103,12 @@ async function handleCreate() {
|
||||
value_unit: form.value.recharge_type === 'subscription' ? form.value.value_unit : '',
|
||||
price: form.value.price,
|
||||
})
|
||||
toast.success(t('developer.cardTypes.create.createSuccess'))
|
||||
router.push('/developer/card-types')
|
||||
toast.success(t('admin.cardTypes.create.createSuccess'))
|
||||
router.push('/admin/card-types')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('创建卡密类型失败:', error)
|
||||
toast.error(error.message || t('developer.cardTypes.create.createFailed'))
|
||||
toast.error(error.message || t('admin.cardTypes.create.createFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
@@ -122,11 +122,11 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cardTypes.create.title')"
|
||||
:description="t('developer.cardTypes.create.description')"
|
||||
:title="t('admin.cardTypes.create.title')"
|
||||
:description="t('admin.cardTypes.create.description')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.cardTypes.title'), href: '/developer/card-types' },
|
||||
{ title: t('developer.cardTypes.create.title') },
|
||||
{ title: t('admin.cardTypes.title'), href: '/admin/card-types' },
|
||||
{ title: t('admin.cardTypes.create.title') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
@@ -137,32 +137,32 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:info" class="size-5" />
|
||||
{{ t('developer.cardTypes.create.basicInfo') }}
|
||||
{{ t('admin.cardTypes.create.basicInfo') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cardTypes.create.basicInfoDesc') }}</UiCardDescription>
|
||||
<UiCardDescription>{{ t('admin.cardTypes.create.basicInfoDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="name">
|
||||
{{ t('developer.cardTypes.create.name') }} *
|
||||
{{ t('admin.cardTypes.create.name') }} *
|
||||
</UiLabel>
|
||||
<UiInput id="name" v-model="form.name" :placeholder="t('developer.cardTypes.create.namePlaceholder')" />
|
||||
<UiInput id="name" v-model="form.name" :placeholder="t('admin.cardTypes.create.namePlaceholder')" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="description">
|
||||
{{ t('developer.cardTypes.create.description') }}
|
||||
{{ t('admin.cardTypes.create.description') }}
|
||||
</UiLabel>
|
||||
<UiTextarea id="description" v-model="form.description" :placeholder="t('developer.cardTypes.create.descriptionPlaceholder')" rows="3" />
|
||||
<UiTextarea id="description" v-model="form.description" :placeholder="t('admin.cardTypes.create.descriptionPlaceholder')" rows="3" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="application">
|
||||
{{ t('developer.cardTypes.create.application') }} *
|
||||
{{ t('admin.cardTypes.create.application') }} *
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.application_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cardTypes.create.selectApplication')" />
|
||||
<UiSelectValue :placeholder="t('admin.cardTypes.create.selectApplication')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
|
||||
@@ -222,7 +222,7 @@ onMounted(() => {
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="price">
|
||||
{{ t('developer.cardTypes.create.price') }}
|
||||
{{ t('admin.cardTypes.create.price') }}
|
||||
</UiLabel>
|
||||
<div class="flex items-center gap-2">
|
||||
<UiNumberField v-model="form.price" :min="0" :step="0.01" class="max-w-[200px]">
|
||||
@@ -232,7 +232,7 @@ onMounted(() => {
|
||||
<UiNumberFieldIncrement />
|
||||
</UiNumberFieldContent>
|
||||
</UiNumberField>
|
||||
<span class="text-sm text-muted-foreground">{{ t('developer.cardTypes.create.priceUnit') }}</span>
|
||||
<span class="text-sm text-muted-foreground">{{ t('admin.cardTypes.create.priceUnit') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -313,17 +313,17 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:eye" class="size-5" />
|
||||
{{ t('developer.cardTypes.create.preview') }}
|
||||
{{ t('admin.cardTypes.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewName') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.previewName') }}</span>
|
||||
<span>{{ form.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewApplication') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.previewApplication') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
@@ -331,11 +331,11 @@ onMounted(() => {
|
||||
<span>{{ form.recharge_type === 'balance' ? '余额充值' : '订阅充值' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewPrice') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.previewPrice') }}</span>
|
||||
<span>¥{{ form.price.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cardTypes.create.previewValue') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.previewValue') }}</span>
|
||||
<span>{{ displayValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -353,14 +353,14 @@ onMounted(() => {
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cardTypes.create.createNow') }}
|
||||
{{ t('admin.cardTypes.create.createNow') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.cardTypes.create.cancel') }}
|
||||
{{ t('admin.cardTypes.create.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { CardTypeStatus } from './schema'
|
||||
|
||||
export function getCardTypeStatuses(t: Composer['t']): { label: string, value: CardTypeStatus }[] {
|
||||
return [
|
||||
{ label: t('developer.cardTypes.statusOptions.active'), value: 'active' },
|
||||
{ label: t('developer.cardTypes.statusOptions.inactive'), value: 'inactive' },
|
||||
{ label: t('admin.cardTypes.statusOptions.active'), value: 'active' },
|
||||
{ label: t('admin.cardTypes.statusOptions.inactive'), value: 'inactive' },
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { CardType } from '@/pages/developer/card-types/data/schema'
|
||||
import type { CardType } from '@/pages/admin/card-types/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/developer/card-types/components/data-table.vue'
|
||||
import DataTable from '@/pages/admin/card-types/components/data-table.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -43,8 +43,8 @@ const applicationOptions = computed(() => {
|
||||
})
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('developer.cardTypes.statusOptions.active'), value: 'active' },
|
||||
{ label: t('developer.cardTypes.statusOptions.inactive'), value: 'inactive' },
|
||||
{ label: t('admin.cardTypes.statusOptions.active'), value: 'active' },
|
||||
{ label: t('admin.cardTypes.statusOptions.inactive'), value: 'inactive' },
|
||||
])
|
||||
|
||||
const filteredCardTypes = computed(() => {
|
||||
@@ -114,11 +114,11 @@ async function fetchApplications() {
|
||||
}
|
||||
|
||||
function goToCreate() {
|
||||
router.push('/developer/card-types/create')
|
||||
router.push('/admin/card-types/create')
|
||||
}
|
||||
|
||||
function goToEdit(type: CardType) {
|
||||
router.push(`/developer/card-types/${type.id}`)
|
||||
router.push(`/admin/card-types/${type.id}`)
|
||||
}
|
||||
|
||||
async function toggleStatus(type: CardType) {
|
||||
@@ -136,16 +136,16 @@ async function toggleStatus(type: CardType) {
|
||||
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
toast.success(t('developer.cardTypes.statusUpdateSuccess'))
|
||||
toast.success(t('admin.cardTypes.statusUpdateSuccess'))
|
||||
fetchCardTypes()
|
||||
}
|
||||
else {
|
||||
toast.error(data.message || t('developer.cardTypes.statusUpdateFailed'))
|
||||
toast.error(data.message || t('admin.cardTypes.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('切换卡密类型状态失败:', error)
|
||||
toast.error(t('developer.cardTypes.statusUpdateFailed'))
|
||||
toast.error(t('admin.cardTypes.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,16 +169,16 @@ async function handleDeleteType() {
|
||||
|
||||
const data = await response.json()
|
||||
if (data.code === 200) {
|
||||
toast.success(t('developer.cardTypes.deleteSuccess'))
|
||||
toast.success(t('admin.cardTypes.deleteSuccess'))
|
||||
fetchCardTypes()
|
||||
}
|
||||
else {
|
||||
toast.error(data.message || t('developer.cardTypes.deleteFailed'))
|
||||
toast.error(data.message || t('admin.cardTypes.deleteFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('删除卡密类型失败:', error)
|
||||
toast.error(t('developer.cardTypes.deleteFailed'))
|
||||
toast.error(t('admin.cardTypes.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
deleteTarget.value = null
|
||||
@@ -213,16 +213,16 @@ async function handleBatchDelete() {
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
toast.success(t('developer.cardTypes.deleteSuccess'))
|
||||
toast.success(t('admin.cardTypes.deleteSuccess'))
|
||||
fetchCardTypes()
|
||||
}
|
||||
if (failCount > 0) {
|
||||
toast.error(t('developer.cardTypes.deleteFailed'))
|
||||
toast.error(t('admin.cardTypes.deleteFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
toast.error(t('developer.cardTypes.deleteFailed'))
|
||||
toast.error(t('admin.cardTypes.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
batchDeleteIds.value = []
|
||||
@@ -254,16 +254,16 @@ async function batchToggleStatus(ids: (string | number)[], status: string) {
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
toast.success(t('developer.cardTypes.batchUpdateSuccess'))
|
||||
toast.success(t('admin.cardTypes.batchUpdateSuccess'))
|
||||
fetchCardTypes()
|
||||
}
|
||||
if (failCount > 0) {
|
||||
toast.error(t('developer.cardTypes.batchUpdateFailed'))
|
||||
toast.error(t('admin.cardTypes.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('批量更新状态失败:', error)
|
||||
toast.error(t('developer.cardTypes.batchUpdateFailed'))
|
||||
toast.error(t('admin.cardTypes.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,14 +275,14 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cardTypes.title')"
|
||||
:description="t('developer.cardTypes.description')"
|
||||
:title="t('admin.cardTypes.title')"
|
||||
:description="t('admin.cardTypes.description')"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton @click="goToCreate">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cardTypes.createCardType') }}
|
||||
{{ t('admin.cardTypes.createCardType') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -291,7 +291,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cardTypes.totalTypes') }}
|
||||
{{ t('admin.cardTypes.totalTypes') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -305,7 +305,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cardTypes.activeTypes') }}
|
||||
{{ t('admin.cardTypes.activeTypes') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -319,7 +319,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cardTypes.generatedCards') }}
|
||||
{{ t('admin.cardTypes.generatedCards') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:hash" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -333,7 +333,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cardTypes.totalValue') }}
|
||||
{{ t('admin.cardTypes.totalValue') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -362,12 +362,12 @@ onMounted(() => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.cardTypes.columns.application')"
|
||||
:title="t('admin.cardTypes.columns.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('developer.cardTypes.columns.status')"
|
||||
:title="t('admin.cardTypes.columns.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</template>
|
||||
@@ -379,30 +379,30 @@ onMounted(() => {
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.cardTypes.delete')"
|
||||
:cancel-button-text="t('developer.cards.create.cancelBtn')"
|
||||
:confirm-button-text="t('admin.cardTypes.delete')"
|
||||
:cancel-button-text="t('admin.cards.create.cancelBtn')"
|
||||
@confirm="handleDeleteType"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.cardTypes.delete') }}
|
||||
{{ t('admin.cardTypes.delete') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.cardTypes.deleteConfirm', { name: deleteTarget?.name }) }}
|
||||
{{ t('admin.cardTypes.deleteConfirm', { name: deleteTarget?.name }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.cardTypes.delete')"
|
||||
:cancel-button-text="t('developer.cards.create.cancelBtn')"
|
||||
:confirm-button-text="t('admin.cardTypes.delete')"
|
||||
:cancel-button-text="t('admin.cards.create.cancelBtn')"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.cardTypes.batchDelete') }}
|
||||
{{ t('admin.cardTypes.batchDelete') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.cardTypes.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
{{ t('admin.cardTypes.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
|
||||
import { Ban, MoreHorizontal, Trash2 } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { Card } from '@/pages/developer/cards/data/schema'
|
||||
import type { Card } from '@/pages/admin/cards/data/schema'
|
||||
|
||||
import { Copy } from '@/components/sva-ui/copy'
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
@@ -24,7 +24,7 @@ export function getColumns(actions: {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'card_key',
|
||||
header: () => t('developer.cards.columns.cardKey'),
|
||||
header: () => t('admin.cards.columns.cardKey'),
|
||||
cell: ({ row }) => {
|
||||
const cardKey = row.getValue('card_key') as string
|
||||
if (!cardKey)
|
||||
@@ -37,7 +37,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'application.name',
|
||||
header: () => t('developer.cards.columns.application'),
|
||||
header: () => t('admin.cards.columns.application'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.original.application?.name
|
||||
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
|
||||
@@ -45,7 +45,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'card_type.name',
|
||||
header: () => t('developer.cards.columns.type'),
|
||||
header: () => t('admin.cards.columns.type'),
|
||||
cell: ({ row }) => {
|
||||
const typeName = row.original.card_type?.name
|
||||
return typeName ? h(Badge, { variant: 'outline' }, () => typeName) : '-'
|
||||
@@ -53,7 +53,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'card_type.price',
|
||||
header: () => t('developer.cards.columns.faceValue'),
|
||||
header: () => t('admin.cards.columns.faceValue'),
|
||||
cell: ({ row }) => {
|
||||
const price = row.original.card_type?.price
|
||||
return price != null ? `¥${price}` : '-'
|
||||
@@ -61,35 +61,35 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'card_type.value',
|
||||
header: () => t('developer.cards.columns.duration'),
|
||||
header: () => t('admin.cards.columns.duration'),
|
||||
cell: ({ row }) => {
|
||||
const value = row.original.card_type?.value
|
||||
const billingType = row.original.card_type?.billing_type
|
||||
if (value == null)
|
||||
return '-'
|
||||
if (value === -1)
|
||||
return t('developer.cards.columns.permanent')
|
||||
return t('admin.cards.columns.permanent')
|
||||
if (billingType === 'subscription')
|
||||
return `${value}${t('developer.cards.columns.hours')}`
|
||||
return `${value}${t('admin.cards.columns.hours')}`
|
||||
if (billingType === 'timer')
|
||||
return `${value}${t('developer.cards.columns.minutes')}`
|
||||
return `${value}${t('admin.cards.columns.minutes')}`
|
||||
if (billingType === 'points')
|
||||
return `${value}${t('developer.cards.columns.points')}`
|
||||
return `${value}${t('admin.cards.columns.points')}`
|
||||
return String(value)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => t('developer.cards.columns.status'),
|
||||
header: () => t('admin.cards.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
if (!status || status === '') {
|
||||
return h(Badge, { variant: 'default' }, () => t('developer.cards.unused'))
|
||||
return h(Badge, { variant: 'default' }, () => t('admin.cards.unused'))
|
||||
}
|
||||
const statusMap: Record<string, { text: string, variant: any }> = {
|
||||
unused: { text: t('developer.cards.unused'), variant: 'default' },
|
||||
used: { text: t('developer.cards.used'), variant: 'secondary' },
|
||||
banned: { text: t('developer.cards.banned'), variant: 'destructive' },
|
||||
unused: { text: t('admin.cards.unused'), variant: 'default' },
|
||||
used: { text: t('admin.cards.used'), variant: 'secondary' },
|
||||
banned: { text: t('admin.cards.banned'), variant: 'destructive' },
|
||||
}
|
||||
const { text, variant } = statusMap[status] || { text: status, variant: 'default' }
|
||||
return h(Badge, { variant }, () => text)
|
||||
@@ -97,7 +97,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'app_user.username',
|
||||
header: () => t('developer.cards.columns.usedUser'),
|
||||
header: () => t('admin.cards.columns.usedUser'),
|
||||
cell: ({ row }) => {
|
||||
const username = row.original.app_user?.username
|
||||
return username || '-'
|
||||
@@ -105,7 +105,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'used_at',
|
||||
header: () => t('developer.cards.columns.usedTime'),
|
||||
header: () => t('admin.cards.columns.usedTime'),
|
||||
cell: ({ row }) => {
|
||||
const usedAt = row.getValue('used_at')
|
||||
if (!usedAt)
|
||||
@@ -129,7 +129,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'creator.username',
|
||||
header: () => t('developer.cards.columns.creator'),
|
||||
header: () => t('admin.cards.columns.creator'),
|
||||
cell: ({ row }) => {
|
||||
const username = row.original.creator?.username
|
||||
return username || '-'
|
||||
@@ -137,7 +137,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.cards.columns.createdAt'),
|
||||
header: () => t('admin.cards.columns.createdAt'),
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue('created_at')
|
||||
if (!createdAt)
|
||||
@@ -182,12 +182,12 @@ export function getColumns(actions: {
|
||||
() => [
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(card) }, () => [
|
||||
h(Ban, { class: 'mr-2 h-4 w-4' }),
|
||||
isBanned ? t('developer.cards.enable') : t('developer.cards.disable'),
|
||||
isBanned ? t('admin.cards.enable') : t('admin.cards.disable'),
|
||||
]),
|
||||
h(DropdownMenuSeparator),
|
||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(card) }, () => [
|
||||
h(Trash2, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.cards.delete'),
|
||||
t('admin.cards.delete'),
|
||||
]),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { X } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { Card } from '@/pages/developer/cards/data/schema'
|
||||
import type { Card } from '@/pages/admin/cards/data/schema'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -33,7 +33,7 @@ function resetFilters() {
|
||||
<template>
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
:placeholder="t('developer.cards.searchAll')"
|
||||
:placeholder="t('admin.cards.searchAll')"
|
||||
:model-value="searchFilter"
|
||||
class="h-8 w-[200px] lg:w-[300px]"
|
||||
@update:model-value="emit('update:searchFilter', $event as string)"
|
||||
@@ -45,7 +45,7 @@ function resetFilters() {
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="resetFilters"
|
||||
>
|
||||
{{ t('developer.cards.reset') }}
|
||||
{{ t('admin.cards.reset') }}
|
||||
<X class="size-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -48,17 +48,17 @@ const table = generateVueTable<Card>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.cards.select',
|
||||
card_key: 'developer.cards.columns.cardKey',
|
||||
application_name: 'developer.cards.columns.application',
|
||||
card_type_name: 'developer.cards.columns.type',
|
||||
card_type_price: 'developer.cards.columns.faceValue',
|
||||
card_type_value: 'developer.cards.columns.duration',
|
||||
status: 'developer.cards.columns.status',
|
||||
app_user_username: 'developer.cards.columns.usedUser',
|
||||
used_at: 'developer.cards.columns.usedTime',
|
||||
creator_username: 'developer.cards.columns.creator',
|
||||
created_at: 'developer.cards.columns.createdAt',
|
||||
select: 'admin.cards.select',
|
||||
card_key: 'admin.cards.columns.cardKey',
|
||||
application_name: 'admin.cards.columns.application',
|
||||
card_type_name: 'admin.cards.columns.type',
|
||||
card_type_price: 'admin.cards.columns.faceValue',
|
||||
card_type_value: 'admin.cards.columns.duration',
|
||||
status: 'admin.cards.columns.status',
|
||||
app_user_username: 'admin.cards.columns.usedUser',
|
||||
used_at: 'admin.cards.columns.usedTime',
|
||||
creator_username: 'admin.cards.columns.creator',
|
||||
created_at: 'admin.cards.columns.createdAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -75,16 +75,16 @@ defineExpose({
|
||||
</div>
|
||||
<BulkActions :table="table" entity-name="cards">
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
|
||||
{{ t('developer.cards.batchEnableBtn') }}
|
||||
{{ t('admin.cards.batchEnableBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
|
||||
{{ t('developer.cards.batchDisableBtn') }}
|
||||
{{ t('admin.cards.batchDisableBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchExport')">
|
||||
{{ t('developer.cards.batchExportBtn') }}
|
||||
{{ t('admin.cards.batchExportBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||
{{ t('developer.cards.batchDeleteBtn') }}
|
||||
{{ t('admin.cards.batchDeleteBtn') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
@@ -98,7 +98,7 @@ defineExpose({
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UiButton variant="outline" size="sm" @click="emit('export')">
|
||||
{{ t('developer.cards.exportBtn') }}
|
||||
{{ t('admin.cards.exportBtn') }}
|
||||
</UiButton>
|
||||
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||
</div>
|
||||
|
||||
@@ -84,15 +84,15 @@ function onApplicationChange() {
|
||||
|
||||
async function handleGenerate() {
|
||||
if (!form.value.application_id) {
|
||||
toast.error(t('developer.cards.create.selectAppFirst'))
|
||||
toast.error(t('admin.cards.create.selectAppFirst'))
|
||||
return
|
||||
}
|
||||
if (!form.value.card_type_id) {
|
||||
toast.error(t('developer.cards.create.selectCardTypeFirst'))
|
||||
toast.error(t('admin.cards.create.selectCardTypeFirst'))
|
||||
return
|
||||
}
|
||||
if (form.value.count < 1 || form.value.count > 1000) {
|
||||
toast.error(t('developer.cards.create.countRange'))
|
||||
toast.error(t('admin.cards.create.countRange'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,12 +103,12 @@ async function handleGenerate() {
|
||||
card_type_id: Number(form.value.card_type_id),
|
||||
count: form.value.count,
|
||||
})
|
||||
toast.success(t('developer.cards.create.generateSuccess', { count: form.value.count }))
|
||||
router.push('/developer/cards')
|
||||
toast.success(t('admin.cards.create.generateSuccess', { count: form.value.count }))
|
||||
router.push('/admin/cards')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('生成卡密失败:', error)
|
||||
toast.error(error.message || t('developer.cards.create.generateFailed'))
|
||||
toast.error(error.message || t('admin.cards.create.generateFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
@@ -123,11 +123,11 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cards.create.title')"
|
||||
:description="t('developer.cards.create.description')"
|
||||
:title="t('admin.cards.create.title')"
|
||||
:description="t('admin.cards.create.description')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.cards.title'), href: '/developer/cards' },
|
||||
{ title: t('developer.cards.create.title') },
|
||||
{ title: t('admin.cards.title'), href: '/admin/cards' },
|
||||
{ title: t('admin.cards.create.title') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
@@ -138,18 +138,18 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.cards.create.config') }}
|
||||
{{ t('admin.cards.create.config') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cards.create.configDesc') }}</UiCardDescription>
|
||||
<UiCardDescription>{{ t('admin.cards.create.configDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="application">
|
||||
{{ t('developer.cards.create.application') }}
|
||||
{{ t('admin.cards.create.application') }}
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.application_id" @update:model-value="onApplicationChange">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cards.create.selectApplication')" />
|
||||
<UiSelectValue :placeholder="t('admin.cards.create.selectApplication')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
|
||||
@@ -161,11 +161,11 @@ onMounted(() => {
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="cardType">
|
||||
{{ t('developer.cards.create.cardType') }}
|
||||
{{ t('admin.cards.create.cardType') }}
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.card_type_id" :disabled="!form.application_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cards.create.selectCardType')" />
|
||||
<UiSelectValue :placeholder="t('admin.cards.create.selectCardType')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="type in filteredCardTypes" :key="type.id" :value="String(type.id)">
|
||||
@@ -174,13 +174,13 @@ onMounted(() => {
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
<p v-if="form.application_id && filteredCardTypes.length === 0" class="text-xs text-muted-foreground">
|
||||
{{ t('developer.cards.create.noCardType') }}
|
||||
{{ t('admin.cards.create.noCardType') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="count">
|
||||
{{ t('developer.cards.create.count') }}
|
||||
{{ t('admin.cards.create.count') }}
|
||||
</UiLabel>
|
||||
<div class="flex items-center gap-4">
|
||||
<UiNumberField v-model="form.count" :min="1" :max="1000" class="flex-1 max-w-[200px]">
|
||||
@@ -206,7 +206,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.cards.create.maxCount') }}
|
||||
{{ t('admin.cards.create.maxCount') }}
|
||||
</p>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
@@ -218,30 +218,30 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:receipt" class="size-5" />
|
||||
{{ t('developer.cards.create.preview') }}
|
||||
{{ t('admin.cards.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cards.create.app') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cards.create.app') }}</span>
|
||||
<span>{{ applications.find(a => String(a.id) === form.application_id)?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cards.create.type') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cards.create.type') }}</span>
|
||||
<span>{{ selectedCardType?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cards.create.unitPrice') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cards.create.unitPrice') }}</span>
|
||||
<span>¥{{ selectedCardType?.price?.toFixed(2) || '0.00' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cards.create.quantity') }}</span>
|
||||
<span>{{ form.count }} {{ t('developer.cards.create.units') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cards.create.quantity') }}</span>
|
||||
<span>{{ form.count }} {{ t('admin.cards.create.units') }}</span>
|
||||
</div>
|
||||
<div class="border-t pt-3 mt-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-muted-foreground">{{ t('developer.cards.create.totalAmount') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cards.create.totalAmount') }}</span>
|
||||
<span class="text-xl font-bold text-primary">¥{{ totalCost.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -260,14 +260,14 @@ onMounted(() => {
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:sparkles" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cards.create.generateNow') }}
|
||||
{{ t('admin.cards.create.generateNow') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.cards.create.cancel') }}
|
||||
{{ t('admin.cards.create.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
|
||||
@@ -8,21 +8,21 @@ import type { FacetedFilterOption } from '@/components/data-table/types'
|
||||
export function getCardStatuses(t: Composer['t']): (FacetedFilterOption & { style: string, variant?: string })[] {
|
||||
return [
|
||||
{
|
||||
label: t('developer.cards.unused'),
|
||||
label: t('admin.cards.unused'),
|
||||
value: 'unused',
|
||||
icon: h(CheckCircle2),
|
||||
style: 'bg-green-500/10 text-green-500',
|
||||
variant: 'default',
|
||||
},
|
||||
{
|
||||
label: t('developer.cards.used'),
|
||||
label: t('admin.cards.used'),
|
||||
value: 'used',
|
||||
icon: h(Users),
|
||||
style: 'bg-blue-500/10 text-blue-500',
|
||||
variant: 'secondary',
|
||||
},
|
||||
{
|
||||
label: t('developer.cards.banned'),
|
||||
label: t('admin.cards.banned'),
|
||||
value: 'banned',
|
||||
icon: h(Ban),
|
||||
style: 'bg-gray-500/10 text-gray-500',
|
||||
|
||||
@@ -5,13 +5,13 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { Card } from '@/pages/developer/cards/data/schema'
|
||||
import type { Card } from '@/pages/admin/cards/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/developer/cards/components/data-table.vue'
|
||||
import DataTable from '@/pages/admin/cards/components/data-table.vue'
|
||||
import api, { BASE_URL } from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -121,9 +121,9 @@ const cardTypeOptions = computed(() => {
|
||||
})
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('developer.cards.unused'), value: 'unused' },
|
||||
{ label: t('developer.cards.used'), value: 'used' },
|
||||
{ label: t('developer.cards.banned'), value: 'banned' },
|
||||
{ label: t('admin.cards.unused'), value: 'unused' },
|
||||
{ label: t('admin.cards.used'), value: 'used' },
|
||||
{ label: t('admin.cards.banned'), value: 'banned' },
|
||||
])
|
||||
|
||||
async function fetchApplications() {
|
||||
@@ -162,19 +162,19 @@ async function fetchCards() {
|
||||
}
|
||||
|
||||
function goToCreate() {
|
||||
router.push('/developer/cards/create')
|
||||
router.push('/admin/cards/create')
|
||||
}
|
||||
|
||||
async function toggleStatus(card: Card) {
|
||||
try {
|
||||
const newStatus = card.status === 'banned' ? 'unused' : 'banned'
|
||||
await api.put(`/dev/cards/${card.id}/status`, { status: newStatus })
|
||||
toast.success(t('developer.cards.statusUpdateSuccess'))
|
||||
toast.success(t('admin.cards.statusUpdateSuccess'))
|
||||
fetchCards()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('切换卡密状态失败:', error)
|
||||
toast.error(error.message || t('developer.cards.statusUpdateFailed'))
|
||||
toast.error(error.message || t('admin.cards.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,12 +189,12 @@ async function handleDeleteCard() {
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/cards/${deleteTarget.value.id}`)
|
||||
toast.success(t('developer.cards.deleteSuccess'))
|
||||
toast.success(t('admin.cards.deleteSuccess'))
|
||||
fetchCards()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('删除卡密失败:', error)
|
||||
toast.error(error.message || t('developer.cards.deleteFailed'))
|
||||
toast.error(error.message || t('admin.cards.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
deleteTarget.value = null
|
||||
@@ -209,12 +209,12 @@ function confirmBatchDelete(ids: (string | number)[]) {
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
await api.delete('/dev/cards/batch', { ids: batchDeleteIds.value } as any)
|
||||
toast.success(t('developer.cards.deleteSuccess'))
|
||||
toast.success(t('admin.cards.deleteSuccess'))
|
||||
fetchCards()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量删除失败:', error)
|
||||
toast.error(error.message || t('developer.cards.deleteFailed'))
|
||||
toast.error(error.message || t('admin.cards.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
batchDeleteIds.value = []
|
||||
@@ -224,12 +224,12 @@ async function handleBatchDelete() {
|
||||
async function batchToggleStatus(ids: (string | number)[], status: string) {
|
||||
try {
|
||||
await api.put('/dev/cards/batch/status', { ids, status })
|
||||
toast.success(t('developer.cards.batchUpdateSuccess'))
|
||||
toast.success(t('admin.cards.batchUpdateSuccess'))
|
||||
fetchCards()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量更新状态失败:', error)
|
||||
toast.error(error.message || t('developer.cards.batchUpdateFailed'))
|
||||
toast.error(error.message || t('admin.cards.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,14 +277,14 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cards.title')"
|
||||
:description="t('developer.cards.description')"
|
||||
:title="t('admin.cards.title')"
|
||||
:description="t('admin.cards.description')"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton @click="goToCreate">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cards.generateCard') }}
|
||||
{{ t('admin.cards.generateCard') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -293,7 +293,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cards.totalCards') }}
|
||||
{{ t('admin.cards.totalCards') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:key" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -307,7 +307,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cards.unused') }}
|
||||
{{ t('admin.cards.unused') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -321,7 +321,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cards.used') }}
|
||||
{{ t('admin.cards.used') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -335,7 +335,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cards.banned') }}
|
||||
{{ t('admin.cards.banned') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -367,17 +367,17 @@ onMounted(() => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.cards.application')"
|
||||
:title="t('admin.cards.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="cardTypeFilter"
|
||||
:title="t('developer.cards.cardType')"
|
||||
:title="t('admin.cards.cardType')"
|
||||
:options="cardTypeOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('developer.cards.status')"
|
||||
:title="t('admin.cards.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
@@ -393,30 +393,30 @@ onMounted(() => {
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.cards.delete')"
|
||||
:cancel-button-text="t('developer.cards.create.cancelBtn')"
|
||||
:confirm-button-text="t('admin.cards.delete')"
|
||||
:cancel-button-text="t('admin.cards.create.cancelBtn')"
|
||||
@confirm="handleDeleteCard"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.cards.deleteCard') }}
|
||||
{{ t('admin.cards.deleteCard') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.cards.deleteCardConfirm', { cardKey: deleteTarget?.card_key }) }}
|
||||
{{ t('admin.cards.deleteCardConfirm', { cardKey: deleteTarget?.card_key }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.cards.delete')"
|
||||
:cancel-button-text="t('developer.cards.create.cancelBtn')"
|
||||
:confirm-button-text="t('admin.cards.delete')"
|
||||
:cancel-button-text="t('admin.cards.create.cancelBtn')"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.cards.batchDeleteCards') }}
|
||||
{{ t('admin.cards.batchDeleteCards') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.cards.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
{{ t('admin.cards.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
|
||||
@@ -2,272 +2,336 @@
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
|
||||
import type { CloudConstant } from './data/schema'
|
||||
|
||||
import DataTable from './components/data-table.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
interface Application {
|
||||
id: number
|
||||
name: string
|
||||
app_key: string
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const constants = ref<CloudConstant[]>([])
|
||||
const applications = ref<Application[]>([])
|
||||
|
||||
const form = ref({
|
||||
id: '',
|
||||
app_id: '',
|
||||
key: '',
|
||||
value: '',
|
||||
var_type: 'string',
|
||||
description: '',
|
||||
status: 'active',
|
||||
const appFilter = ref<string>('')
|
||||
const startDate = ref<string>('')
|
||||
const endDate = ref<string>('')
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<CloudConstant | null>(null)
|
||||
const batchDialogOpen = ref(false)
|
||||
const batchAction = ref<'enable' | 'disable' | 'delete' | null>(null)
|
||||
const selectedRows = ref<CloudConstant[]>([])
|
||||
|
||||
const filteredConstants = computed(() => {
|
||||
let result = [...constants.value]
|
||||
|
||||
if (appFilter.value) {
|
||||
result = result.filter(c => String(c.app_id) === appFilter.value)
|
||||
}
|
||||
|
||||
if (startDate.value) {
|
||||
const fromDateTime = startDate.value.includes('T')
|
||||
? startDate.value.replace('T', ' ')
|
||||
: `${startDate.value} 00:00`
|
||||
result = result.filter(c => c.created_at >= fromDateTime)
|
||||
}
|
||||
|
||||
if (endDate.value) {
|
||||
const toDateTime = endDate.value.includes('T')
|
||||
? endDate.value.replace('T', ' ')
|
||||
: `${endDate.value} 23:59`
|
||||
result = result.filter(c => c.created_at <= toDateTime)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const linkedCount = computed(() => constants.value.filter(c => c.app_id).length)
|
||||
const unlinkedCount = computed(() => constants.value.filter(c => !c.app_id).length)
|
||||
|
||||
const applicationOptions = computed(() => {
|
||||
return applications.value.map(app => ({
|
||||
label: app.name,
|
||||
value: String(app.id),
|
||||
}))
|
||||
})
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
const data = await api.get<{ applications: Application[] }>('/dev/applications')
|
||||
applications.value = Array.isArray(data?.applications) ? data.applications : []
|
||||
applications.value = data?.applications || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取应用列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchConstant() {
|
||||
async function fetchConstants() {
|
||||
loading.value = true
|
||||
try {
|
||||
const constId = route.params.id
|
||||
const data = await api.get<{ id: number, app_id: number, key: string, value: string, var_type: string, description: string, status: string }>(`/dev/cloud-constants/${constId}`)
|
||||
const constant = data
|
||||
if (constant) {
|
||||
form.value = {
|
||||
id: String(constant.id),
|
||||
app_id: String(constant.app_id || ''),
|
||||
key: constant.key,
|
||||
value: constant.value,
|
||||
var_type: constant.var_type || 'string',
|
||||
description: constant.description || '',
|
||||
status: constant.status || 'active',
|
||||
}
|
||||
}
|
||||
else {
|
||||
toast.error(t('developer.cloudConstants.create.failed'))
|
||||
router.push('/developer/cloud-constants')
|
||||
}
|
||||
const data = await api.get<{ constants: CloudConstant[] }>('/dev/cloud-constants')
|
||||
const consts = data?.constants || []
|
||||
constants.value = consts.map((c: any) => ({
|
||||
...c,
|
||||
application_name: applications.value.find(app => app.id === c.app_id)?.name || '',
|
||||
}))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取云端常量失败:', error)
|
||||
toast.error(t('developer.cloudConstants.create.failed'))
|
||||
router.push('/developer/cloud-constants')
|
||||
constants.value = []
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const selectedApplication = computed(() => {
|
||||
if (form.value.app_id) {
|
||||
return applications.value.find(app => String(app.id) === form.value.app_id)
|
||||
}
|
||||
return null
|
||||
})
|
||||
function openCreatePage() {
|
||||
router.push('/admin/cloud-constants/create')
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.key) {
|
||||
toast.error(t('developer.cloudConstants.create.keyRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.value) {
|
||||
toast.error(t('developer.cloudConstants.create.valueRequired'))
|
||||
return
|
||||
}
|
||||
function openEditPage(constant: CloudConstant) {
|
||||
router.push(`/admin/cloud-constants/${constant.id}`)
|
||||
}
|
||||
|
||||
function confirmDelete(constant: CloudConstant) {
|
||||
deleteTarget.value = constant
|
||||
deleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!deleteTarget.value)
|
||||
return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await api.put(`/dev/cloud-constants/${form.value.id}`, {
|
||||
key: form.value.key,
|
||||
value: form.value.value,
|
||||
var_type: form.value.var_type,
|
||||
description: form.value.description,
|
||||
status: form.value.status,
|
||||
})
|
||||
toast.success(t('developer.cloudConstants.create.success'))
|
||||
router.push('/developer/cloud-constants')
|
||||
await api.delete(`/dev/cloud-constants/${deleteTarget.value.id}`)
|
||||
toast.success(t('admin.cloudConstants.deleteSuccess'))
|
||||
fetchConstants()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('更新云端常量失败:', error)
|
||||
toast.error(error.message || t('developer.cloudConstants.create.failed'))
|
||||
console.error('删除云端常量失败:', error)
|
||||
toast.error(error.message || t('admin.cloudConstants.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
deleteTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleStatus(constant: CloudConstant) {
|
||||
const newStatus = constant.status === 'active' ? 'inactive' : 'active'
|
||||
try {
|
||||
await api.put(`/dev/cloud-constants/${constant.id}`, {
|
||||
key: constant.key,
|
||||
value: constant.value,
|
||||
var_type: constant.var_type,
|
||||
description: constant.description,
|
||||
status: newStatus,
|
||||
})
|
||||
toast.success(newStatus === 'active' ? t('admin.cloudConstants.enableSuccess') : t('admin.cloudConstants.disableSuccess'))
|
||||
fetchConstants()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('更新状态失败:', error)
|
||||
toast.error(error.message || t('admin.cloudConstants.updateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownload(constant: CloudConstant) {
|
||||
if (constant.var_type !== 'binary')
|
||||
return
|
||||
const token = localStorage.getItem('token')
|
||||
const url = `/api/v1/dev/cloud-constants/${constant.id}/download?token=${token}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function openBatchDialog(action: 'enable' | 'disable' | 'delete', rows: CloudConstant[]) {
|
||||
batchAction.value = action
|
||||
selectedRows.value = rows
|
||||
batchDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleBatchAction() {
|
||||
if (!batchAction.value || selectedRows.value.length === 0)
|
||||
return
|
||||
|
||||
const ids = selectedRows.value.map(c => c.id)
|
||||
try {
|
||||
if (batchAction.value === 'delete') {
|
||||
await Promise.all(ids.map(id => api.delete(`/dev/cloud-constants/${id}`)))
|
||||
toast.success(t('admin.cloudConstants.batchDeleteSuccess'))
|
||||
}
|
||||
else {
|
||||
const status = batchAction.value === 'enable' ? 'active' : 'inactive'
|
||||
await Promise.all(selectedRows.value.map(c => api.put(`/dev/cloud-constants/${c.id}`, {
|
||||
key: c.key,
|
||||
value: c.value,
|
||||
var_type: c.var_type,
|
||||
description: c.description,
|
||||
status,
|
||||
})))
|
||||
toast.success(batchAction.value === 'enable' ? t('admin.cloudConstants.batchEnableSuccess') : t('admin.cloudConstants.batchDisableSuccess'))
|
||||
}
|
||||
fetchConstants()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量操作失败:', error)
|
||||
toast.error(error.message || t('admin.cloudConstants.batchFailed'))
|
||||
}
|
||||
finally {
|
||||
batchAction.value = null
|
||||
selectedRows.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchApplications()
|
||||
fetchConstant()
|
||||
fetchConstants()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cloudConstants.edit')"
|
||||
:description="t('developer.cloudConstants.editDescription')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.cloudConstants.title'), href: '/developer/cloud-constants' },
|
||||
{ title: t('developer.cloudConstants.edit') },
|
||||
]"
|
||||
:title="t('admin.cloudConstants.title')"
|
||||
:description="t('admin.cloudConstants.description')"
|
||||
sticky
|
||||
>
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<template #actions>
|
||||
<UiButton size="sm" @click="openCreatePage">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('admin.cloudConstants.addConstant') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.cloudConstants.create.config') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cloudConstants.create.configDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.cloudConstants.create.application') }}</UiLabel>
|
||||
<div class="flex items-center gap-2 p-2.5 rounded-md border bg-muted/50">
|
||||
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudConstants.totalConstants') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:database" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ filteredConstants.length }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="varType">
|
||||
{{ t('developer.cloudConstants.create.type') }}
|
||||
</UiLabel>
|
||||
<UiSelect id="varType" v-model="form.var_type">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cloudConstants.create.selectType')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="integer">
|
||||
{{ t('developer.cloudConstants.types.integer') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="decimal">
|
||||
{{ t('developer.cloudConstants.types.decimal') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="string">
|
||||
{{ t('developer.cloudConstants.types.string') }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudConstants.linkedApps') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:link" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ linkedCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="key">
|
||||
{{ t('developer.cloudConstants.create.key') }}
|
||||
</UiLabel>
|
||||
<UiInput id="key" v-model="form.key" :placeholder="t('developer.cloudConstants.create.keyPlaceholder')" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.cloudConstants.create.keyHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudConstants.unlinked') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:unlink" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ unlinkedCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="value">
|
||||
{{ t('developer.cloudConstants.create.value') }}
|
||||
</UiLabel>
|
||||
<UiTextarea id="value" v-model="form.value" :placeholder="t('developer.cloudConstants.create.valuePlaceholder')" rows="3" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="description">
|
||||
{{ t('developer.cloudConstants.create.description') }}
|
||||
</UiLabel>
|
||||
<UiInput id="description" v-model="form.description" :placeholder="t('developer.cloudConstants.create.descriptionPlaceholder')" />
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:eye" class="size-5" />
|
||||
{{ t('developer.cloudConstants.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.application') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.type') }}</span>
|
||||
<span>{{ t(`developer.cloudConstants.types.${form.var_type}`) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.key') }}</span>
|
||||
<span>{{ form.key || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm items-center">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.status') }}</span>
|
||||
<UiSwitch
|
||||
:checked="form.status === 'active'"
|
||||
@update:checked="form.status = $event ? 'active' : 'inactive'"
|
||||
/>
|
||||
</div>
|
||||
<div class="border-t pt-3 mt-3">
|
||||
<div class="text-sm">
|
||||
<div class="text-muted-foreground mb-2">
|
||||
{{ t('developer.cloudConstants.create.value') }}
|
||||
</div>
|
||||
<div class="bg-muted p-2 rounded max-h-[100px] overflow-y-auto text-xs">
|
||||
{{ form.value || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving || !form.key || !form.value"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cloudConstants.create.submit') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.cloudConstants.create.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudConstants.appCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ applications.length }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
:loading
|
||||
:data="filteredConstants"
|
||||
:on-edit="openEditPage"
|
||||
:on-delete="confirmDelete"
|
||||
:on-toggle-status="handleToggleStatus"
|
||||
:on-download="handleDownload"
|
||||
@refresh="fetchConstants"
|
||||
@batch-action="openBatchDialog"
|
||||
>
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('admin.cloudConstants.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
v-model:start-model-value="startDate"
|
||||
v-model:end-model-value="endDate"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('admin.cloudConstants.delete')"
|
||||
:cancel-button-text="t('common.reset')"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.cloudConstants.deleteConstant') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.cloudConstants.deleteConfirm', { key: deleteTarget?.key }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDialogOpen"
|
||||
:destructive="batchAction === 'delete'"
|
||||
:confirm-button-text="batchAction === 'delete' ? t('admin.cloudConstants.delete') : t('common.confirm')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleBatchAction"
|
||||
>
|
||||
<template #title>
|
||||
{{ batchAction === 'delete' ? t('admin.cloudConstants.batchDelete') : batchAction === 'enable' ? t('admin.cloudConstants.batchEnable') : t('admin.cloudConstants.batchDisable') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ batchAction === 'delete' ? t('admin.cloudConstants.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('admin.cloudConstants.batchEnableConfirm', { count: selectedRows.length }) : t('admin.cloudConstants.batchDisableConfirm', { count: selectedRows.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
|
||||
import { Download, File, MoreHorizontal, Pencil, Power, PowerOff, Trash2 } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { CloudConstant } from '@/pages/developer/cloud-constants/data/schema'
|
||||
import type { CloudConstant } from '@/pages/admin/cloud-constants/data/schema'
|
||||
|
||||
import { Copy } from '@/components/sva-ui/copy'
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
@@ -25,7 +25,7 @@ export function getColumns(actions: {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'key',
|
||||
header: () => t('developer.cloudConstants.columns.key'),
|
||||
header: () => t('admin.cloudConstants.columns.key'),
|
||||
cell: ({ row }) => {
|
||||
const key = row.getValue('key') as string
|
||||
if (!key)
|
||||
@@ -38,7 +38,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'value',
|
||||
header: () => t('developer.cloudConstants.columns.value'),
|
||||
header: () => t('admin.cloudConstants.columns.value'),
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue('value') as string
|
||||
const varType = row.original.var_type
|
||||
@@ -56,21 +56,21 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'var_type',
|
||||
header: () => t('developer.cloudConstants.columns.type'),
|
||||
header: () => t('admin.cloudConstants.columns.type'),
|
||||
cell: ({ row }) => {
|
||||
const type = row.getValue('var_type') as string
|
||||
const typeMap: Record<string, string> = {
|
||||
integer: t('developer.cloudConstants.types.integer'),
|
||||
decimal: t('developer.cloudConstants.types.decimal'),
|
||||
string: t('developer.cloudConstants.types.string'),
|
||||
binary: t('developer.cloudConstants.types.binary'),
|
||||
integer: t('admin.cloudConstants.types.integer'),
|
||||
decimal: t('admin.cloudConstants.types.decimal'),
|
||||
string: t('admin.cloudConstants.types.string'),
|
||||
binary: t('admin.cloudConstants.types.binary'),
|
||||
}
|
||||
return h(Badge, { variant: 'secondary' }, () => typeMap[type || 'string'] || type || 'string')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: () => t('developer.cloudConstants.columns.description'),
|
||||
header: () => t('admin.cloudConstants.columns.description'),
|
||||
cell: ({ row }) => {
|
||||
const description = row.getValue('description') as string
|
||||
if (!description)
|
||||
@@ -80,12 +80,12 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'application_name',
|
||||
header: () => t('developer.cloudConstants.columns.application'),
|
||||
header: () => t('admin.cloudConstants.columns.application'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.getValue('application_name') as string
|
||||
const appId = row.original.app_id
|
||||
if (!appId)
|
||||
return h(Badge, { variant: 'outline' }, () => t('developer.cloudConstants.notLinked'))
|
||||
return h(Badge, { variant: 'outline' }, () => t('admin.cloudConstants.notLinked'))
|
||||
if (!appName)
|
||||
return '-'
|
||||
return h(Badge, { variant: 'outline' }, () => appName)
|
||||
@@ -93,16 +93,16 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => t('developer.cloudConstants.columns.status'),
|
||||
header: () => t('admin.cloudConstants.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
const isActive = status === 'active'
|
||||
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => isActive ? t('developer.cloudConstants.statusActive') : t('developer.cloudConstants.statusInactive'))
|
||||
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => isActive ? t('admin.cloudConstants.statusActive') : t('admin.cloudConstants.statusInactive'))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.cloudConstants.columns.createdAt'),
|
||||
header: () => t('admin.cloudConstants.columns.createdAt'),
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue('created_at')
|
||||
if (!createdAt)
|
||||
@@ -147,7 +147,7 @@ export function getColumns(actions: {
|
||||
const items = [
|
||||
h(DropdownMenuItem, { onClick: () => actions.onEdit(constant) }, () => [
|
||||
h(Pencil, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.cloudConstants.edit'),
|
||||
t('admin.cloudConstants.edit'),
|
||||
]),
|
||||
]
|
||||
|
||||
@@ -163,11 +163,11 @@ export function getColumns(actions: {
|
||||
items.push(
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(constant) }, () => [
|
||||
constant.status === 'active' ? h(PowerOff, { class: 'mr-2 h-4 w-4' }) : h(Power, { class: 'mr-2 h-4 w-4' }),
|
||||
constant.status === 'active' ? t('developer.cloudConstants.disable') : t('developer.cloudConstants.enable'),
|
||||
constant.status === 'active' ? t('admin.cloudConstants.disable') : t('admin.cloudConstants.enable'),
|
||||
]),
|
||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(constant) }, () => [
|
||||
h(Trash2, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.cloudConstants.delete'),
|
||||
t('admin.cloudConstants.delete'),
|
||||
]),
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { X } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { CloudConstant } from '@/pages/developer/cloud-constants/data/schema'
|
||||
import type { CloudConstant } from '@/pages/admin/cloud-constants/data/schema'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
|
||||
<template>
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
:placeholder="t('developer.cloudConstants.searchPlaceholder')"
|
||||
:placeholder="t('admin.cloudConstants.searchPlaceholder')"
|
||||
:model-value="(table.getColumn('key')?.getFilterValue() as string) ?? ''"
|
||||
class="h-8 w-[150px] lg:w-[250px]"
|
||||
@input="table.getColumn('key')?.setFilterValue($event.target.value)"
|
||||
|
||||
@@ -46,14 +46,14 @@ const table = generateVueTable<CloudConstant>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.cloudConstants.select',
|
||||
key: 'developer.cloudConstants.columns.key',
|
||||
value: 'developer.cloudConstants.columns.value',
|
||||
var_type: 'developer.cloudConstants.columns.type',
|
||||
description: 'developer.cloudConstants.columns.description',
|
||||
application_name: 'developer.cloudConstants.columns.application',
|
||||
status: 'developer.cloudConstants.columns.status',
|
||||
created_at: 'developer.cloudConstants.columns.createdAt',
|
||||
select: 'admin.cloudConstants.select',
|
||||
key: 'admin.cloudConstants.columns.key',
|
||||
value: 'admin.cloudConstants.columns.value',
|
||||
var_type: 'admin.cloudConstants.columns.type',
|
||||
description: 'admin.cloudConstants.columns.description',
|
||||
application_name: 'admin.cloudConstants.columns.application',
|
||||
status: 'admin.cloudConstants.columns.status',
|
||||
created_at: 'admin.cloudConstants.columns.createdAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -76,13 +76,13 @@ function handleBatchAction(action: 'enable' | 'disable' | 'delete') {
|
||||
</div>
|
||||
<BulkActions :table="table" entity-name="constants">
|
||||
<UiButton variant="outline" size="sm" @click="handleBatchAction('enable')">
|
||||
{{ t('developer.cloudConstants.batchEnableBtn') }}
|
||||
{{ t('admin.cloudConstants.batchEnableBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="handleBatchAction('disable')">
|
||||
{{ t('developer.cloudConstants.batchDisableBtn') }}
|
||||
{{ t('admin.cloudConstants.batchDisableBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="destructive" size="sm" @click="handleBatchAction('delete')">
|
||||
{{ t('developer.cloudConstants.batchDeleteBtn') }}
|
||||
{{ t('admin.cloudConstants.batchDeleteBtn') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
|
||||
@@ -67,11 +67,11 @@ function clearFile() {
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.key) {
|
||||
toast.error(t('developer.cloudConstants.create.keyRequired'))
|
||||
toast.error(t('admin.cloudConstants.create.keyRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.app_id) {
|
||||
toast.error(t('developer.cloudConstants.create.appRequired'))
|
||||
toast.error(t('admin.cloudConstants.create.appRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ async function handleSave() {
|
||||
}
|
||||
|
||||
if (!form.value.value) {
|
||||
toast.error(t('developer.cloudConstants.create.valueRequired'))
|
||||
toast.error(t('admin.cloudConstants.create.valueRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -99,12 +99,12 @@ async function handleSave() {
|
||||
description: form.value.description,
|
||||
status: form.value.status,
|
||||
})
|
||||
toast.success(t('developer.cloudConstants.create.success'))
|
||||
router.push('/developer/cloud-constants')
|
||||
toast.success(t('admin.cloudConstants.create.success'))
|
||||
router.push('/admin/cloud-constants')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('创建云端常量失败:', error)
|
||||
toast.error(error.message || t('developer.cloudConstants.create.failed'))
|
||||
toast.error(error.message || t('admin.cloudConstants.create.failed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
@@ -125,8 +125,8 @@ async function uploadFile() {
|
||||
formData.append('status', form.value.status)
|
||||
|
||||
await api.postFormData('/dev/cloud-constants/upload', formData)
|
||||
toast.success(t('developer.cloudConstants.create.success'))
|
||||
router.push('/developer/cloud-constants')
|
||||
toast.success(t('admin.cloudConstants.create.success'))
|
||||
router.push('/admin/cloud-constants')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('上传文件失败:', error)
|
||||
@@ -153,11 +153,11 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cloudConstants.addConstant')"
|
||||
:description="t('developer.cloudConstants.createDescription')"
|
||||
:title="t('admin.cloudConstants.addConstant')"
|
||||
:description="t('admin.cloudConstants.createDescription')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.cloudConstants.title'), href: '/developer/cloud-constants' },
|
||||
{ title: t('developer.cloudConstants.addConstant') },
|
||||
{ title: t('admin.cloudConstants.title'), href: '/admin/cloud-constants' },
|
||||
{ title: t('admin.cloudConstants.addConstant') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
@@ -168,18 +168,18 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.cloudConstants.create.config') }}
|
||||
{{ t('admin.cloudConstants.create.config') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cloudConstants.create.configDesc') }}</UiCardDescription>
|
||||
<UiCardDescription>{{ t('admin.cloudConstants.create.configDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="application">
|
||||
{{ t('developer.cloudConstants.create.application') }}
|
||||
{{ t('admin.cloudConstants.create.application') }}
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.app_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cloudConstants.create.selectApp')" />
|
||||
<UiSelectValue :placeholder="t('admin.cloudConstants.create.selectApp')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
|
||||
@@ -191,21 +191,21 @@ onMounted(() => {
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="varType">
|
||||
{{ t('developer.cloudConstants.create.type') }}
|
||||
{{ t('admin.cloudConstants.create.type') }}
|
||||
</UiLabel>
|
||||
<UiSelect id="varType" v-model="form.var_type">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cloudConstants.create.selectType')" />
|
||||
<UiSelectValue :placeholder="t('admin.cloudConstants.create.selectType')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="integer">
|
||||
{{ t('developer.cloudConstants.types.integer') }}
|
||||
{{ t('admin.cloudConstants.types.integer') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="decimal">
|
||||
{{ t('developer.cloudConstants.types.decimal') }}
|
||||
{{ t('admin.cloudConstants.types.decimal') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="string">
|
||||
{{ t('developer.cloudConstants.types.string') }}
|
||||
{{ t('admin.cloudConstants.types.string') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="binary">
|
||||
二进制
|
||||
@@ -216,11 +216,11 @@ onMounted(() => {
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="key">
|
||||
{{ t('developer.cloudConstants.create.key') }}
|
||||
{{ t('admin.cloudConstants.create.key') }}
|
||||
</UiLabel>
|
||||
<UiInput id="key" v-model="form.key" :placeholder="t('developer.cloudConstants.create.keyPlaceholder')" />
|
||||
<UiInput id="key" v-model="form.key" :placeholder="t('admin.cloudConstants.create.keyPlaceholder')" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.cloudConstants.create.keyHint') }}
|
||||
{{ t('admin.cloudConstants.create.keyHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -263,16 +263,16 @@ onMounted(() => {
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<UiLabel for="value">
|
||||
{{ t('developer.cloudConstants.create.value') }}
|
||||
{{ t('admin.cloudConstants.create.value') }}
|
||||
</UiLabel>
|
||||
<UiTextarea id="value" v-model="form.value" :placeholder="t('developer.cloudConstants.create.valuePlaceholder')" rows="3" />
|
||||
<UiTextarea id="value" v-model="form.value" :placeholder="t('admin.cloudConstants.create.valuePlaceholder')" rows="3" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="description">
|
||||
{{ t('developer.cloudConstants.create.description') }}
|
||||
{{ t('admin.cloudConstants.create.description') }}
|
||||
</UiLabel>
|
||||
<UiInput id="description" v-model="form.description" :placeholder="t('developer.cloudConstants.create.descriptionPlaceholder')" />
|
||||
<UiInput id="description" v-model="form.description" :placeholder="t('admin.cloudConstants.create.descriptionPlaceholder')" />
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
@@ -283,25 +283,25 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:eye" class="size-5" />
|
||||
{{ t('developer.cloudConstants.create.preview') }}
|
||||
{{ t('admin.cloudConstants.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.application') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudConstants.create.application') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.type') }}</span>
|
||||
<span>{{ t(`developer.cloudConstants.types.${form.var_type}`) }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudConstants.create.type') }}</span>
|
||||
<span>{{ t(`dmin.cloudConstants.types.${form.var_type}`) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.key') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudConstants.create.key') }}</span>
|
||||
<span>{{ form.key || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm items-center">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudConstants.create.status') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudConstants.create.status') }}</span>
|
||||
<UiSwitch
|
||||
:checked="form.status === 'active'"
|
||||
@update:checked="form.status = $event ? 'active' : 'inactive'"
|
||||
@@ -310,7 +310,7 @@ onMounted(() => {
|
||||
<div class="border-t pt-3 mt-3">
|
||||
<div class="text-sm">
|
||||
<div class="text-muted-foreground mb-2">
|
||||
{{ t('developer.cloudConstants.create.value') }}
|
||||
{{ t('admin.cloudConstants.create.value') }}
|
||||
</div>
|
||||
<div v-if="form.var_type === 'binary'" class="bg-muted p-2 rounded text-xs">
|
||||
{{ selectedFile ? `${selectedFile.name} (${formatFileSize(selectedFile.size)})` : '未选择文件' }}
|
||||
@@ -335,14 +335,14 @@ onMounted(() => {
|
||||
>
|
||||
<Icon v-if="saving || uploading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
|
||||
{{ uploading ? '上传中...' : t('developer.cloudConstants.create.submit') }}
|
||||
{{ uploading ? '上传中...' : t('admin.cloudConstants.create.submit') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.cloudConstants.create.cancel') }}
|
||||
{{ t('admin.cloudConstants.create.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
|
||||
@@ -102,11 +102,11 @@ async function fetchConstants() {
|
||||
}
|
||||
|
||||
function openCreatePage() {
|
||||
router.push('/developer/cloud-constants/create')
|
||||
router.push('/admin/cloud-constants/create')
|
||||
}
|
||||
|
||||
function openEditPage(constant: CloudConstant) {
|
||||
router.push(`/developer/cloud-constants/${constant.id}`)
|
||||
router.push(`/admin/cloud-constants/${constant.id}`)
|
||||
}
|
||||
|
||||
function confirmDelete(constant: CloudConstant) {
|
||||
@@ -120,12 +120,12 @@ async function handleDelete() {
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/cloud-constants/${deleteTarget.value.id}`)
|
||||
toast.success(t('developer.cloudConstants.deleteSuccess'))
|
||||
toast.success(t('admin.cloudConstants.deleteSuccess'))
|
||||
fetchConstants()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('删除云端常量失败:', error)
|
||||
toast.error(error.message || t('developer.cloudConstants.deleteFailed'))
|
||||
toast.error(error.message || t('admin.cloudConstants.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
deleteTarget.value = null
|
||||
@@ -142,12 +142,12 @@ async function handleToggleStatus(constant: CloudConstant) {
|
||||
description: constant.description,
|
||||
status: newStatus,
|
||||
})
|
||||
toast.success(newStatus === 'active' ? t('developer.cloudConstants.enableSuccess') : t('developer.cloudConstants.disableSuccess'))
|
||||
toast.success(newStatus === 'active' ? t('admin.cloudConstants.enableSuccess') : t('admin.cloudConstants.disableSuccess'))
|
||||
fetchConstants()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('更新状态失败:', error)
|
||||
toast.error(error.message || t('developer.cloudConstants.updateFailed'))
|
||||
toast.error(error.message || t('admin.cloudConstants.updateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ async function handleBatchAction() {
|
||||
try {
|
||||
if (batchAction.value === 'delete') {
|
||||
await Promise.all(ids.map(id => api.delete(`/dev/cloud-constants/${id}`)))
|
||||
toast.success(t('developer.cloudConstants.batchDeleteSuccess'))
|
||||
toast.success(t('admin.cloudConstants.batchDeleteSuccess'))
|
||||
}
|
||||
else {
|
||||
const status = batchAction.value === 'enable' ? 'active' : 'inactive'
|
||||
@@ -184,13 +184,13 @@ async function handleBatchAction() {
|
||||
description: c.description,
|
||||
status,
|
||||
})))
|
||||
toast.success(batchAction.value === 'enable' ? t('developer.cloudConstants.batchEnableSuccess') : t('developer.cloudConstants.batchDisableSuccess'))
|
||||
toast.success(batchAction.value === 'enable' ? t('admin.cloudConstants.batchEnableSuccess') : t('admin.cloudConstants.batchDisableSuccess'))
|
||||
}
|
||||
fetchConstants()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量操作失败:', error)
|
||||
toast.error(error.message || t('developer.cloudConstants.batchFailed'))
|
||||
toast.error(error.message || t('admin.cloudConstants.batchFailed'))
|
||||
}
|
||||
finally {
|
||||
batchAction.value = null
|
||||
@@ -206,14 +206,14 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cloudConstants.title')"
|
||||
:description="t('developer.cloudConstants.description')"
|
||||
:title="t('admin.cloudConstants.title')"
|
||||
:description="t('admin.cloudConstants.description')"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton size="sm" @click="openCreatePage">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cloudConstants.addConstant') }}
|
||||
{{ t('admin.cloudConstants.addConstant') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -222,7 +222,7 @@ onMounted(async () => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudConstants.totalConstants') }}
|
||||
{{ t('admin.cloudConstants.totalConstants') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:database" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -236,7 +236,7 @@ onMounted(async () => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudConstants.linkedApps') }}
|
||||
{{ t('admin.cloudConstants.linkedApps') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:link" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -250,7 +250,7 @@ onMounted(async () => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudConstants.unlinked') }}
|
||||
{{ t('admin.cloudConstants.unlinked') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:unlink" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -264,7 +264,7 @@ onMounted(async () => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudConstants.appCount') }}
|
||||
{{ t('admin.cloudConstants.appCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -291,7 +291,7 @@ onMounted(async () => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.cloudConstants.application')"
|
||||
:title="t('admin.cloudConstants.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
@@ -307,30 +307,30 @@ onMounted(async () => {
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.cloudConstants.delete')"
|
||||
:confirm-button-text="t('admin.cloudConstants.delete')"
|
||||
:cancel-button-text="t('common.reset')"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.cloudConstants.deleteConstant') }}
|
||||
{{ t('admin.cloudConstants.deleteConstant') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.cloudConstants.deleteConfirm', { key: deleteTarget?.key }) }}
|
||||
{{ t('admin.cloudConstants.deleteConfirm', { key: deleteTarget?.key }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDialogOpen"
|
||||
:destructive="batchAction === 'delete'"
|
||||
:confirm-button-text="batchAction === 'delete' ? t('developer.cloudConstants.delete') : t('common.confirm')"
|
||||
:confirm-button-text="batchAction === 'delete' ? t('admin.cloudConstants.delete') : t('common.confirm')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleBatchAction"
|
||||
>
|
||||
<template #title>
|
||||
{{ batchAction === 'delete' ? t('developer.cloudConstants.batchDelete') : batchAction === 'enable' ? t('developer.cloudConstants.batchEnable') : t('developer.cloudConstants.batchDisable') }}
|
||||
{{ batchAction === 'delete' ? t('admin.cloudConstants.batchDelete') : batchAction === 'enable' ? t('admin.cloudConstants.batchEnable') : t('admin.cloudConstants.batchDisable') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ batchAction === 'delete' ? t('developer.cloudConstants.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('developer.cloudConstants.batchEnableConfirm', { count: selectedRows.length }) : t('developer.cloudConstants.batchDisableConfirm', { count: selectedRows.length }) }}
|
||||
{{ batchAction === 'delete' ? t('admin.cloudConstants.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('admin.cloudConstants.batchEnableConfirm', { count: selectedRows.length }) : t('admin.cloudConstants.batchDisableConfirm', { count: selectedRows.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
|
||||
@@ -2,409 +2,360 @@
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
|
||||
import type { CloudVariable } from './data/schema'
|
||||
|
||||
import DataTable from './components/data-table.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
interface Application {
|
||||
id: number
|
||||
name: string
|
||||
app_key: string
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const variables = ref<CloudVariable[]>([])
|
||||
const applications = ref<Application[]>([])
|
||||
|
||||
const form = ref({
|
||||
id: '',
|
||||
app_id: '',
|
||||
key: '',
|
||||
default_value: '',
|
||||
var_type: 'string',
|
||||
max_records: 0,
|
||||
scope: 'app',
|
||||
write_permission: 'admin',
|
||||
description: '',
|
||||
status: 'active',
|
||||
const appFilter = ref<string>('')
|
||||
const scopeFilter = ref<string>('')
|
||||
const startDate = ref<string>('')
|
||||
const endDate = ref<string>('')
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<CloudVariable | null>(null)
|
||||
const batchDialogOpen = ref(false)
|
||||
const batchAction = ref<'enable' | 'disable' | 'delete' | null>(null)
|
||||
const selectedRows = ref<CloudVariable[]>([])
|
||||
|
||||
const filteredVariables = computed(() => {
|
||||
let result = [...variables.value]
|
||||
|
||||
if (appFilter.value) {
|
||||
result = result.filter(v => String(v.app_id) === appFilter.value)
|
||||
}
|
||||
|
||||
if (scopeFilter.value) {
|
||||
result = result.filter(v => v.scope === scopeFilter.value)
|
||||
}
|
||||
|
||||
if (startDate.value) {
|
||||
const fromDateTime = startDate.value.includes('T')
|
||||
? startDate.value.replace('T', ' ')
|
||||
: `${startDate.value} 00:00`
|
||||
result = result.filter(v => v.created_at >= fromDateTime)
|
||||
}
|
||||
|
||||
if (endDate.value) {
|
||||
const toDateTime = endDate.value.includes('T')
|
||||
? endDate.value.replace('T', ' ')
|
||||
: `${endDate.value} 23:59`
|
||||
result = result.filter(v => v.created_at <= toDateTime)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const appScopeCount = computed(() => variables.value.filter(v => v.scope === 'app').length)
|
||||
const userScopeCount = computed(() => variables.value.filter(v => v.scope === 'user').length)
|
||||
|
||||
const applicationOptions = computed(() => {
|
||||
return applications.value.map(app => ({
|
||||
label: app.name,
|
||||
value: String(app.id),
|
||||
}))
|
||||
})
|
||||
|
||||
const scopeOptions = computed(() => [
|
||||
{ label: t('admin.cloudVariables.appScope'), value: 'app' },
|
||||
{ label: t('admin.cloudVariables.userScope'), value: 'user' },
|
||||
])
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
const data = await api.get<{ applications: Application[] }>('/dev/applications')
|
||||
applications.value = Array.isArray(data?.applications) ? data.applications : []
|
||||
applications.value = data?.applications || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取应用列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchVariable() {
|
||||
async function fetchVariables() {
|
||||
loading.value = true
|
||||
try {
|
||||
const varId = route.params.id
|
||||
const data = await api.get<{ id: number, app_id: number, key: string, default_value: string, var_type: string, max_records: number, scope: string, write_permission: string, description: string, status: string }>(`/dev/cloud-variables/${varId}`)
|
||||
const variable = data
|
||||
if (variable) {
|
||||
form.value = {
|
||||
id: String(variable.id),
|
||||
app_id: String(variable.app_id || ''),
|
||||
key: variable.key,
|
||||
default_value: variable.default_value || '',
|
||||
var_type: variable.var_type || 'string',
|
||||
max_records: variable.max_records || 0,
|
||||
scope: variable.scope || 'app',
|
||||
write_permission: variable.write_permission || 'admin',
|
||||
description: variable.description || '',
|
||||
status: variable.status || 'active',
|
||||
}
|
||||
}
|
||||
else {
|
||||
toast.error(t('developer.cloudVariables.create.failed'))
|
||||
router.push('/developer/cloud-variables')
|
||||
}
|
||||
const data = await api.get<{ variables: CloudVariable[] }>('/dev/cloud-variables')
|
||||
const vars = data?.variables || []
|
||||
variables.value = vars.map((v: any) => ({
|
||||
...v,
|
||||
application_name: applications.value.find(app => app.id === v.app_id)?.name || '',
|
||||
}))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取云端变量失败:', error)
|
||||
toast.error(t('developer.cloudVariables.create.failed'))
|
||||
router.push('/developer/cloud-variables')
|
||||
variables.value = []
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const selectedApplication = computed(() => {
|
||||
if (form.value.app_id) {
|
||||
return applications.value.find(app => String(app.id) === form.value.app_id)
|
||||
}
|
||||
return null
|
||||
})
|
||||
function openCreatePage() {
|
||||
router.push('/admin/cloud-variables/create')
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.key) {
|
||||
toast.error(t('developer.cloudVariables.create.keyRequired'))
|
||||
function openEditPage(variable: CloudVariable) {
|
||||
router.push(`/admin/cloud-variables/${variable.id}`)
|
||||
}
|
||||
|
||||
function confirmDelete(variable: CloudVariable) {
|
||||
deleteTarget.value = variable
|
||||
deleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!deleteTarget.value)
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await api.put(`/dev/cloud-variables/${form.value.id}`, {
|
||||
key: form.value.key,
|
||||
default_value: form.value.default_value,
|
||||
var_type: form.value.var_type,
|
||||
max_records: form.value.max_records,
|
||||
write_permission: form.value.write_permission,
|
||||
description: form.value.description,
|
||||
status: form.value.status,
|
||||
})
|
||||
toast.success(t('developer.cloudVariables.create.success'))
|
||||
router.push('/developer/cloud-variables')
|
||||
await api.delete(`/dev/cloud-variables/${deleteTarget.value.id}`)
|
||||
toast.success(t('admin.cloudVariables.deleteSuccess'))
|
||||
fetchVariables()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('更新云端变量失败:', error)
|
||||
toast.error(error.message || t('developer.cloudVariables.create.failed'))
|
||||
console.error('删除云端变量失败:', error)
|
||||
toast.error(error.message || t('admin.cloudVariables.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
deleteTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleStatus(variable: CloudVariable) {
|
||||
const newStatus = variable.status === 'active' ? 'inactive' : 'active'
|
||||
try {
|
||||
await api.put(`/dev/cloud-variables/${variable.id}`, {
|
||||
key: variable.key,
|
||||
default_value: variable.default_value,
|
||||
var_type: variable.var_type,
|
||||
scope: variable.scope,
|
||||
write_permission: variable.write_permission,
|
||||
description: variable.description,
|
||||
status: newStatus,
|
||||
})
|
||||
toast.success(newStatus === 'active' ? t('admin.cloudVariables.enableSuccess') : t('admin.cloudVariables.disableSuccess'))
|
||||
fetchVariables()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('更新状态失败:', error)
|
||||
toast.error(error.message || t('admin.cloudVariables.updateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownload(variable: CloudVariable) {
|
||||
if (variable.var_type !== 'binary')
|
||||
return
|
||||
const token = localStorage.getItem('token')
|
||||
const url = `/api/v1/dev/cloud-variables/${variable.id}/download?token=${token}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function handleViewRecords(variable: CloudVariable) {
|
||||
router.push(`/admin/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
|
||||
}
|
||||
|
||||
function openBatchDialog(action: 'enable' | 'disable' | 'delete', rows: CloudVariable[]) {
|
||||
batchAction.value = action
|
||||
selectedRows.value = rows
|
||||
batchDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleBatchAction() {
|
||||
if (!batchAction.value || selectedRows.value.length === 0)
|
||||
return
|
||||
|
||||
const ids = selectedRows.value.map(v => v.id)
|
||||
try {
|
||||
if (batchAction.value === 'delete') {
|
||||
await Promise.all(ids.map(id => api.delete(`/dev/cloud-variables/${id}`)))
|
||||
toast.success(t('admin.cloudVariables.batchDeleteSuccess'))
|
||||
}
|
||||
else {
|
||||
const status = batchAction.value === 'enable' ? 'active' : 'inactive'
|
||||
await Promise.all(selectedRows.value.map(v => api.put(`/dev/cloud-variables/${v.id}`, {
|
||||
key: v.key,
|
||||
default_value: v.default_value,
|
||||
var_type: v.var_type,
|
||||
scope: v.scope,
|
||||
write_permission: v.write_permission,
|
||||
description: v.description,
|
||||
status,
|
||||
})))
|
||||
toast.success(batchAction.value === 'enable' ? t('admin.cloudVariables.batchEnableSuccess') : t('admin.cloudVariables.batchDisableSuccess'))
|
||||
}
|
||||
fetchVariables()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量操作失败:', error)
|
||||
toast.error(error.message || t('admin.cloudVariables.batchFailed'))
|
||||
}
|
||||
finally {
|
||||
batchAction.value = null
|
||||
selectedRows.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchApplications()
|
||||
fetchVariable()
|
||||
fetchVariables()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cloudVariables.edit')"
|
||||
:description="t('developer.cloudVariables.editDescription')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.cloudVariables.title'), href: '/developer/cloud-variables' },
|
||||
{ title: t('developer.cloudVariables.edit') },
|
||||
]"
|
||||
:title="t('admin.cloudVariables.title')"
|
||||
:description="t('admin.cloudVariables.description')"
|
||||
sticky
|
||||
>
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<template #actions>
|
||||
<UiButton size="sm" @click="openCreatePage">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('admin.cloudVariables.addVariable') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.cloudVariables.create.config') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cloudVariables.create.configDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.cloudVariables.create.application') }}</UiLabel>
|
||||
<div class="flex items-center gap-2 p-2.5 rounded-md border bg-muted/50">
|
||||
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudVariables.totalVariables') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:variable" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ filteredVariables.length }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="varType">
|
||||
{{ t('developer.cloudVariables.create.type') }}
|
||||
</UiLabel>
|
||||
<UiSelect id="varType" v-model="form.var_type">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cloudVariables.create.selectType')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="integer">
|
||||
{{ t('developer.cloudVariables.types.integer') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="decimal">
|
||||
{{ t('developer.cloudVariables.types.decimal') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="string">
|
||||
{{ t('developer.cloudVariables.types.string') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="stream">
|
||||
记录
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudVariables.appScopeVariables') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:globe" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ appScopeCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div v-if="form.var_type === 'stream'" class="space-y-2">
|
||||
<UiLabel for="maxRecords">
|
||||
最大记录数
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="maxRecords"
|
||||
v-model.number="form.max_records"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0 = 不限制"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
超过限制时自动删除最旧的记录,0表示不限制
|
||||
</p>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudVariables.userScopeVariables') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:user" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ userScopeCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="key">
|
||||
{{ t('developer.cloudVariables.create.key') }}
|
||||
</UiLabel>
|
||||
<UiInput id="key" v-model="form.key" :placeholder="t('developer.cloudVariables.create.keyPlaceholder')" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.cloudVariables.create.keyHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="defaultValue">
|
||||
{{ t('developer.cloudVariables.create.defaultValue') }}
|
||||
</UiLabel>
|
||||
<UiTextarea id="defaultValue" v-model="form.default_value" :placeholder="t('developer.cloudVariables.create.defaultValuePlaceholder')" rows="3" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="description">
|
||||
{{ t('developer.cloudVariables.create.description') }}
|
||||
</UiLabel>
|
||||
<UiInput id="description" v-model="form.description" :placeholder="t('developer.cloudVariables.create.descriptionPlaceholder')" />
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.cloudVariables.create.scopeTitle') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cloudVariables.create.scopeDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div
|
||||
class="flex items-start gap-3 p-4 rounded-lg border"
|
||||
:class="form.scope === 'app' ? 'border-blue-500 bg-blue-50 dark:bg-blue-950/30' : 'border-purple-500 bg-purple-50 dark:bg-purple-950/30'"
|
||||
>
|
||||
<Icon
|
||||
:icon="form.scope === 'app' ? 'lucide:globe' : 'lucide:user'"
|
||||
class="size-5 mt-0.5"
|
||||
:class="form.scope === 'app' ? 'text-blue-500' : 'text-purple-500'"
|
||||
/>
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ form.scope === 'app' ? t('developer.cloudVariables.create.appScope') : t('developer.cloudVariables.create.userScope') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{{ form.scope === 'app' ? t('developer.cloudVariables.create.appScopeDesc') : t('developer.cloudVariables.create.userScopeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-2">
|
||||
{{ t('developer.cloudVariables.create.scopeImmutable') }}
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.cloudVariables.create.permissionTitle') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cloudVariables.create.permissionDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<UiRadioGroup v-model="form.write_permission" class="grid grid-cols-1 gap-4">
|
||||
<div
|
||||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||||
:class="form.write_permission === 'admin' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
|
||||
@click="form.write_permission = 'admin'"
|
||||
>
|
||||
<UiRadioGroupItem value="admin" class="mt-0.5" />
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="lucide:lock" class="size-4 text-primary" />
|
||||
<p class="font-medium">
|
||||
{{ t('developer.cloudVariables.create.developerOnly') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{{ form.scope === 'app' ? t('developer.cloudVariables.create.developerOnlyAppDesc') : t('developer.cloudVariables.create.developerOnlyDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||||
:class="form.write_permission === 'user' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
|
||||
@click="form.write_permission = 'user'"
|
||||
>
|
||||
<UiRadioGroupItem value="user" class="mt-0.5" />
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="lucide:unlock" class="size-4 text-primary" />
|
||||
<p class="font-medium">
|
||||
{{ t('developer.cloudVariables.create.userWritable') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{{ form.scope === 'app' ? t('developer.cloudVariables.create.userWritableAppDesc') : t('developer.cloudVariables.create.userWritableDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.var_type === 'stream'"
|
||||
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
|
||||
:class="form.write_permission === 'app_user' ? 'border-primary bg-primary/5' : 'hover:bg-accent'"
|
||||
@click="form.write_permission = 'app_user'"
|
||||
>
|
||||
<UiRadioGroupItem value="app_user" class="mt-0.5" />
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="lucide:users" class="size-4 text-primary" />
|
||||
<p class="font-medium">
|
||||
应用用户可写
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
允许应用端用户通过API写入记录,适合订单、日志等场景
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</UiRadioGroup>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:eye" class="size-5" />
|
||||
{{ t('developer.cloudVariables.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.application') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.type') }}</span>
|
||||
<span>{{ t(`developer.cloudVariables.types.${form.var_type}`) }}</span>
|
||||
</div>
|
||||
<div v-if="form.var_type === 'stream'" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">最大记录数</span>
|
||||
<span>{{ form.max_records || '不限制' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.key') }}</span>
|
||||
<span>{{ form.key || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.scope') }}</span>
|
||||
<UiBadge variant="outline">
|
||||
{{ form.scope === 'app' ? t('developer.cloudVariables.appScope') : t('developer.cloudVariables.userScope') }}
|
||||
</UiBadge>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.writePermission') }}</span>
|
||||
<UiBadge variant="secondary">
|
||||
{{ form.write_permission === 'admin' ? t('developer.cloudVariables.create.developerOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('developer.cloudVariables.create.userWritable') }}
|
||||
</UiBadge>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm items-center">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.status') }}</span>
|
||||
<UiSwitch
|
||||
:checked="form.status === 'active'"
|
||||
@update:checked="form.status = $event ? 'active' : 'inactive'"
|
||||
/>
|
||||
</div>
|
||||
<div class="border-t pt-3 mt-3">
|
||||
<div class="text-sm">
|
||||
<div class="text-muted-foreground mb-2">
|
||||
{{ t('developer.cloudVariables.create.defaultValue') }}
|
||||
</div>
|
||||
<div class="bg-muted p-2 rounded max-h-[100px] overflow-y-auto text-xs">
|
||||
{{ form.default_value || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving || !form.key"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cloudVariables.create.submit') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.cloudVariables.create.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudVariables.appCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ applications.length }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
:loading
|
||||
:data="filteredVariables"
|
||||
:on-edit="openEditPage"
|
||||
:on-delete="confirmDelete"
|
||||
:on-toggle-status="handleToggleStatus"
|
||||
:on-download="handleDownload"
|
||||
:on-view-records="handleViewRecords"
|
||||
@refresh="fetchVariables"
|
||||
@batch-action="openBatchDialog"
|
||||
>
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('admin.cloudVariables.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="scopeFilter"
|
||||
:title="t('admin.cloudVariables.scope')"
|
||||
:options="scopeOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
v-model:start-model-value="startDate"
|
||||
v-model:end-model-value="endDate"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('admin.cloudVariables.delete')"
|
||||
:cancel-button-text="t('common.reset')"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.cloudVariables.deleteVariable') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.cloudVariables.deleteConfirm', { key: deleteTarget?.key }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDialogOpen"
|
||||
:destructive="batchAction === 'delete'"
|
||||
:confirm-button-text="batchAction === 'delete' ? t('admin.cloudVariables.delete') : t('common.confirm')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleBatchAction"
|
||||
>
|
||||
<template #title>
|
||||
{{ batchAction === 'delete' ? t('admin.cloudVariables.batchDelete') : batchAction === 'enable' ? t('admin.cloudVariables.batchEnable') : t('admin.cloudVariables.batchDisable') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ batchAction === 'delete' ? t('admin.cloudVariables.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('admin.cloudVariables.batchEnableConfirm', { count: selectedRows.length }) : t('admin.cloudVariables.batchDisableConfirm', { count: selectedRows.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -144,7 +144,7 @@ onMounted(async () => {
|
||||
title="变量记录"
|
||||
description="查看云端变量的记录数据"
|
||||
:breadcrumbs="[
|
||||
{ title: '云端变量', href: '/developer/cloud-variables' },
|
||||
{ title: '云端变量', href: '/admin/cloud-variables' },
|
||||
{ title: '变量记录' },
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
|
||||
import { Download, File, MoreHorizontal, Pencil, Power, PowerOff, Trash2 } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { CloudVariable } from '@/pages/developer/cloud-variables/data/schema'
|
||||
import type { CloudVariable } from '@/pages/admin/cloud-variables/data/schema'
|
||||
|
||||
import { Copy } from '@/components/sva-ui/copy'
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
@@ -26,7 +26,7 @@ export function getColumns(actions: {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'key',
|
||||
header: () => t('developer.cloudVariables.columns.key'),
|
||||
header: () => t('admin.cloudVariables.columns.key'),
|
||||
cell: ({ row }) => {
|
||||
const key = row.getValue('key') as string
|
||||
if (!key)
|
||||
@@ -39,7 +39,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'default_value',
|
||||
header: () => t('developer.cloudVariables.columns.defaultValue'),
|
||||
header: () => t('admin.cloudVariables.columns.defaultValue'),
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue('default_value') as string
|
||||
const varType = row.original.var_type
|
||||
@@ -57,36 +57,36 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'scope',
|
||||
header: () => t('developer.cloudVariables.columns.scope'),
|
||||
header: () => t('admin.cloudVariables.columns.scope'),
|
||||
cell: ({ row }) => {
|
||||
const scope = row.getValue('scope') as string
|
||||
if (scope === 'app') {
|
||||
return h(Badge, { variant: 'default' }, () => t('developer.cloudVariables.appScope'))
|
||||
return h(Badge, { variant: 'default' }, () => t('admin.cloudVariables.appScope'))
|
||||
}
|
||||
return h(Badge, { variant: 'secondary' }, () => t('developer.cloudVariables.userScope'))
|
||||
return h(Badge, { variant: 'secondary' }, () => t('admin.cloudVariables.userScope'))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'write_permission',
|
||||
header: () => t('developer.cloudVariables.create.writePermission'),
|
||||
header: () => t('admin.cloudVariables.create.writePermission'),
|
||||
cell: ({ row }) => {
|
||||
const permission = row.getValue('write_permission') as string
|
||||
if (permission === 'user') {
|
||||
return h(Badge, { variant: 'outline', class: 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300' }, () => t('developer.cloudVariables.create.userWritable'))
|
||||
return h(Badge, { variant: 'outline', class: 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300' }, () => t('admin.cloudVariables.create.userWritable'))
|
||||
}
|
||||
return h(Badge, { variant: 'outline' }, () => t('developer.cloudVariables.create.developerOnly'))
|
||||
return h(Badge, { variant: 'outline' }, () => t('admin.cloudVariables.create.developerOnly'))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'var_type',
|
||||
header: () => t('developer.cloudVariables.columns.type'),
|
||||
header: () => t('admin.cloudVariables.columns.type'),
|
||||
cell: ({ row }) => {
|
||||
const type = row.getValue('var_type') as string
|
||||
const typeMap: Record<string, string> = {
|
||||
integer: t('developer.cloudVariables.types.integer'),
|
||||
decimal: t('developer.cloudVariables.types.decimal'),
|
||||
string: t('developer.cloudVariables.types.string'),
|
||||
binary: t('developer.cloudVariables.types.binary'),
|
||||
integer: t('admin.cloudVariables.types.integer'),
|
||||
decimal: t('admin.cloudVariables.types.decimal'),
|
||||
string: t('admin.cloudVariables.types.string'),
|
||||
binary: t('admin.cloudVariables.types.binary'),
|
||||
stream: '记录',
|
||||
}
|
||||
return h(Badge, { variant: 'outline' }, () => typeMap[type || 'string'] || type || 'string')
|
||||
@@ -94,7 +94,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: () => t('developer.cloudVariables.columns.description'),
|
||||
header: () => t('admin.cloudVariables.columns.description'),
|
||||
cell: ({ row }) => {
|
||||
const description = row.getValue('description') as string
|
||||
if (!description)
|
||||
@@ -104,13 +104,13 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'application_name',
|
||||
header: () => t('developer.cloudVariables.columns.application'),
|
||||
header: () => t('admin.cloudVariables.columns.application'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.getValue('application_name') as string
|
||||
if (!appName) {
|
||||
const appId = row.original.app_id
|
||||
if (!appId)
|
||||
return h(Badge, { variant: 'outline' }, () => t('developer.cloudVariables.notLinked'))
|
||||
return h(Badge, { variant: 'outline' }, () => t('admin.cloudVariables.notLinked'))
|
||||
return '-'
|
||||
}
|
||||
return h(Badge, { variant: 'outline' }, () => appName)
|
||||
@@ -118,16 +118,16 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => t('developer.cloudVariables.columns.status'),
|
||||
header: () => t('admin.cloudVariables.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
const isActive = status === 'active'
|
||||
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => isActive ? t('developer.cloudVariables.statusActive') : t('developer.cloudVariables.statusInactive'))
|
||||
return h(Badge, { variant: isActive ? 'default' : 'destructive' }, () => isActive ? t('admin.cloudVariables.statusActive') : t('admin.cloudVariables.statusInactive'))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.cloudVariables.columns.createdAt'),
|
||||
header: () => t('admin.cloudVariables.columns.createdAt'),
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue('created_at')
|
||||
if (!createdAt)
|
||||
@@ -172,7 +172,7 @@ export function getColumns(actions: {
|
||||
const items = [
|
||||
h(DropdownMenuItem, { onClick: () => actions.onEdit(variable) }, () => [
|
||||
h(Pencil, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.cloudVariables.edit'),
|
||||
t('admin.cloudVariables.edit'),
|
||||
]),
|
||||
]
|
||||
|
||||
@@ -188,11 +188,11 @@ export function getColumns(actions: {
|
||||
items.push(
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(variable) }, () => [
|
||||
variable.status === 'active' ? h(PowerOff, { class: 'mr-2 h-4 w-4' }) : h(Power, { class: 'mr-2 h-4 w-4' }),
|
||||
variable.status === 'active' ? t('developer.cloudVariables.disable') : t('developer.cloudVariables.enable'),
|
||||
variable.status === 'active' ? t('admin.cloudVariables.disable') : t('admin.cloudVariables.enable'),
|
||||
]),
|
||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(variable) }, () => [
|
||||
h(Trash2, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.cloudVariables.delete'),
|
||||
t('admin.cloudVariables.delete'),
|
||||
]),
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const router = useRouter()
|
||||
const variable = props.row.original
|
||||
|
||||
function viewRecords() {
|
||||
router.push(`/developer/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
|
||||
router.push(`/admin/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
|
||||
<template>
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
:placeholder="t('developer.cloudVariables.searchPlaceholder')"
|
||||
:placeholder="t('admin.cloudVariables.searchPlaceholder')"
|
||||
:model-value="(table.getColumn('key')?.getFilterValue() as string) ?? ''"
|
||||
class="h-8 w-[150px] lg:w-[250px]"
|
||||
@input="table.getColumn('key')?.setFilterValue($event.target.value)"
|
||||
|
||||
@@ -48,16 +48,16 @@ const table = generateVueTable<CloudVariable>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.cloudVariables.select',
|
||||
key: 'developer.cloudVariables.columns.key',
|
||||
default_value: 'developer.cloudVariables.columns.defaultValue',
|
||||
scope: 'developer.cloudVariables.columns.scope',
|
||||
write_permission: 'developer.cloudVariables.create.writePermission',
|
||||
var_type: 'developer.cloudVariables.columns.type',
|
||||
description: 'developer.cloudVariables.columns.description',
|
||||
application_name: 'developer.cloudVariables.columns.application',
|
||||
status: 'developer.cloudVariables.columns.status',
|
||||
created_at: 'developer.cloudVariables.columns.createdAt',
|
||||
select: 'admin.cloudVariables.select',
|
||||
key: 'admin.cloudVariables.columns.key',
|
||||
default_value: 'admin.cloudVariables.columns.defaultValue',
|
||||
scope: 'admin.cloudVariables.columns.scope',
|
||||
write_permission: 'admin.cloudVariables.create.writePermission',
|
||||
var_type: 'admin.cloudVariables.columns.type',
|
||||
description: 'admin.cloudVariables.columns.description',
|
||||
application_name: 'admin.cloudVariables.columns.application',
|
||||
status: 'admin.cloudVariables.columns.status',
|
||||
created_at: 'admin.cloudVariables.columns.createdAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -80,13 +80,13 @@ function handleBatchAction(action: 'enable' | 'disable' | 'delete') {
|
||||
</div>
|
||||
<BulkActions :table="table" entity-name="variables">
|
||||
<UiButton variant="outline" size="sm" @click="handleBatchAction('enable')">
|
||||
{{ t('developer.cloudVariables.batchEnableBtn') }}
|
||||
{{ t('admin.cloudVariables.batchEnableBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="handleBatchAction('disable')">
|
||||
{{ t('developer.cloudVariables.batchDisableBtn') }}
|
||||
{{ t('admin.cloudVariables.batchDisableBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="destructive" size="sm" @click="handleBatchAction('delete')">
|
||||
{{ t('developer.cloudVariables.batchDeleteBtn') }}
|
||||
{{ t('admin.cloudVariables.batchDeleteBtn') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
|
||||
@@ -70,11 +70,11 @@ function clearFile() {
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.key) {
|
||||
toast.error(t('developer.cloudVariables.create.keyRequired'))
|
||||
toast.error(t('admin.cloudVariables.create.keyRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.app_id) {
|
||||
toast.error(t('developer.cloudVariables.create.appRequired'))
|
||||
toast.error(t('admin.cloudVariables.create.appRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,12 +100,12 @@ async function handleSave() {
|
||||
description: form.value.description,
|
||||
status: form.value.status,
|
||||
})
|
||||
toast.success(t('developer.cloudVariables.create.success'))
|
||||
router.push('/developer/cloud-variables')
|
||||
toast.success(t('admin.cloudVariables.create.success'))
|
||||
router.push('/admin/cloud-variables')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('创建云端变量失败:', error)
|
||||
toast.error(error.message || t('developer.cloudVariables.create.failed'))
|
||||
toast.error(error.message || t('admin.cloudVariables.create.failed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
@@ -128,8 +128,8 @@ async function uploadFile() {
|
||||
formData.append('write_permission', form.value.write_permission)
|
||||
|
||||
await api.postFormData('/dev/cloud-variables/upload', formData)
|
||||
toast.success(t('developer.cloudVariables.create.success'))
|
||||
router.push('/developer/cloud-variables')
|
||||
toast.success(t('admin.cloudVariables.create.success'))
|
||||
router.push('/admin/cloud-variables')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('上传文件失败:', error)
|
||||
@@ -156,11 +156,11 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cloudVariables.addVariable')"
|
||||
:description="t('developer.cloudVariables.createDescription')"
|
||||
:title="t('admin.cloudVariables.addVariable')"
|
||||
:description="t('admin.cloudVariables.createDescription')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.cloudVariables.title'), href: '/developer/cloud-variables' },
|
||||
{ title: t('developer.cloudVariables.addVariable') },
|
||||
{ title: t('admin.cloudVariables.title'), href: '/admin/cloud-variables' },
|
||||
{ title: t('admin.cloudVariables.addVariable') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
@@ -171,18 +171,18 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.cloudVariables.create.config') }}
|
||||
{{ t('admin.cloudVariables.create.config') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cloudVariables.create.configDesc') }}</UiCardDescription>
|
||||
<UiCardDescription>{{ t('admin.cloudVariables.create.configDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="application">
|
||||
{{ t('developer.cloudVariables.create.application') }}
|
||||
{{ t('admin.cloudVariables.create.application') }}
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.app_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cloudVariables.create.selectApp')" />
|
||||
<UiSelectValue :placeholder="t('admin.cloudVariables.create.selectApp')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
|
||||
@@ -194,21 +194,21 @@ onMounted(() => {
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="varType">
|
||||
{{ t('developer.cloudVariables.create.type') }}
|
||||
{{ t('admin.cloudVariables.create.type') }}
|
||||
</UiLabel>
|
||||
<UiSelect id="varType" v-model="form.var_type">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cloudVariables.create.selectType')" />
|
||||
<UiSelectValue :placeholder="t('admin.cloudVariables.create.selectType')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="integer">
|
||||
{{ t('developer.cloudVariables.types.integer') }}
|
||||
{{ t('admin.cloudVariables.types.integer') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="decimal">
|
||||
{{ t('developer.cloudVariables.types.decimal') }}
|
||||
{{ t('admin.cloudVariables.types.decimal') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="string">
|
||||
{{ t('developer.cloudVariables.types.string') }}
|
||||
{{ t('admin.cloudVariables.types.string') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="binary">
|
||||
二进制
|
||||
@@ -238,11 +238,11 @@ onMounted(() => {
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="key">
|
||||
{{ t('developer.cloudVariables.create.key') }}
|
||||
{{ t('admin.cloudVariables.create.key') }}
|
||||
</UiLabel>
|
||||
<UiInput id="key" v-model="form.key" :placeholder="t('developer.cloudVariables.create.keyPlaceholder')" />
|
||||
<UiInput id="key" v-model="form.key" :placeholder="t('admin.cloudVariables.create.keyPlaceholder')" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.cloudVariables.create.keyHint') }}
|
||||
{{ t('admin.cloudVariables.create.keyHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -285,24 +285,24 @@ onMounted(() => {
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<UiLabel for="defaultValue">
|
||||
{{ t('developer.cloudVariables.create.defaultValue') }}
|
||||
{{ t('admin.cloudVariables.create.defaultValue') }}
|
||||
</UiLabel>
|
||||
<UiTextarea id="defaultValue" v-model="form.default_value" :placeholder="t('developer.cloudVariables.create.defaultValuePlaceholder')" rows="3" />
|
||||
<UiTextarea id="defaultValue" v-model="form.default_value" :placeholder="t('admin.cloudVariables.create.defaultValuePlaceholder')" rows="3" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="description">
|
||||
{{ t('developer.cloudVariables.create.description') }}
|
||||
{{ t('admin.cloudVariables.create.description') }}
|
||||
</UiLabel>
|
||||
<UiInput id="description" v-model="form.description" :placeholder="t('developer.cloudVariables.create.descriptionPlaceholder')" />
|
||||
<UiInput id="description" v-model="form.description" :placeholder="t('admin.cloudVariables.create.descriptionPlaceholder')" />
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.cloudVariables.create.scopeTitle') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cloudVariables.create.scopeDesc') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.cloudVariables.create.scopeTitle') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.cloudVariables.create.scopeDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<UiRadioGroup v-model="form.scope" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -316,11 +316,11 @@ onMounted(() => {
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="lucide:globe" class="size-4 text-primary" />
|
||||
<p class="font-medium">
|
||||
{{ t('developer.cloudVariables.create.appScope') }}
|
||||
{{ t('admin.cloudVariables.create.appScope') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{{ t('developer.cloudVariables.create.appScopeDesc') }}
|
||||
{{ t('admin.cloudVariables.create.appScopeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -335,11 +335,11 @@ onMounted(() => {
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="lucide:user" class="size-4 text-primary" />
|
||||
<p class="font-medium">
|
||||
{{ t('developer.cloudVariables.create.userScope') }}
|
||||
{{ t('admin.cloudVariables.create.userScope') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{{ t('developer.cloudVariables.create.userScopeDesc') }}
|
||||
{{ t('admin.cloudVariables.create.userScopeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,8 +349,8 @@ onMounted(() => {
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.cloudVariables.create.permissionTitle') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cloudVariables.create.permissionDesc') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.cloudVariables.create.permissionTitle') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.cloudVariables.create.permissionDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<UiRadioGroup v-model="form.write_permission" class="grid grid-cols-1 gap-4">
|
||||
@@ -364,11 +364,11 @@ onMounted(() => {
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="lucide:lock" class="size-4 text-primary" />
|
||||
<p class="font-medium">
|
||||
{{ t('developer.cloudVariables.create.developerOnly') }}
|
||||
{{ t('admin.cloudVariables.create.developerOnly') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{{ form.scope === 'app' ? t('developer.cloudVariables.create.developerOnlyAppDesc') : t('developer.cloudVariables.create.developerOnlyDesc') }}
|
||||
{{ form.scope === 'app' ? t('admin.cloudVariables.create.developerOnlyAppDesc') : t('admin.cloudVariables.create.developerOnlyDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -383,11 +383,11 @@ onMounted(() => {
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="lucide:unlock" class="size-4 text-primary" />
|
||||
<p class="font-medium">
|
||||
{{ t('developer.cloudVariables.create.userWritable') }}
|
||||
{{ t('admin.cloudVariables.create.userWritable') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{{ form.scope === 'app' ? t('developer.cloudVariables.create.userWritableAppDesc') : t('developer.cloudVariables.create.userWritableDesc') }}
|
||||
{{ form.scope === 'app' ? t('admin.cloudVariables.create.userWritableAppDesc') : t('admin.cloudVariables.create.userWritableDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -421,41 +421,41 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:eye" class="size-5" />
|
||||
{{ t('developer.cloudVariables.create.preview') }}
|
||||
{{ t('admin.cloudVariables.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.application') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.application') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.type') }}</span>
|
||||
<span>{{ t(`developer.cloudVariables.types.${form.var_type}`) }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.type') }}</span>
|
||||
<span>{{ t(`dmin.cloudVariables.types.${form.var_type}`) }}</span>
|
||||
</div>
|
||||
<div v-if="form.var_type === 'stream'" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">最大记录数</span>
|
||||
<span>{{ form.max_records || '不限制' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.key') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.key') }}</span>
|
||||
<span>{{ form.key || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.scope') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.scope') }}</span>
|
||||
<UiBadge variant="outline">
|
||||
{{ form.scope === 'app' ? t('developer.cloudVariables.appScope') : t('developer.cloudVariables.userScope') }}
|
||||
{{ form.scope === 'app' ? t('admin.cloudVariables.appScope') : t('admin.cloudVariables.userScope') }}
|
||||
</UiBadge>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.writePermission') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.writePermission') }}</span>
|
||||
<UiBadge variant="secondary">
|
||||
{{ form.write_permission === 'admin' ? t('developer.cloudVariables.create.developerOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('developer.cloudVariables.create.userWritable') }}
|
||||
{{ form.write_permission === 'admin' ? t('admin.cloudVariables.create.developerOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('admin.cloudVariables.create.userWritable') }}
|
||||
</UiBadge>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm items-center">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudVariables.create.status') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.status') }}</span>
|
||||
<UiSwitch
|
||||
:checked="form.status === 'active'"
|
||||
@update:checked="form.status = $event ? 'active' : 'inactive'"
|
||||
@@ -464,7 +464,7 @@ onMounted(() => {
|
||||
<div class="border-t pt-3 mt-3">
|
||||
<div class="text-sm">
|
||||
<div class="text-muted-foreground mb-2">
|
||||
{{ t('developer.cloudVariables.create.defaultValue') }}
|
||||
{{ t('admin.cloudVariables.create.defaultValue') }}
|
||||
</div>
|
||||
<div class="bg-muted p-2 rounded max-h-[100px] overflow-y-auto text-xs">
|
||||
{{ form.default_value || '-' }}
|
||||
@@ -486,14 +486,14 @@ onMounted(() => {
|
||||
>
|
||||
<Icon v-if="saving || uploading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
|
||||
{{ uploading ? '上传中...' : t('developer.cloudVariables.create.submit') }}
|
||||
{{ uploading ? '上传中...' : t('admin.cloudVariables.create.submit') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.cloudVariables.create.cancel') }}
|
||||
{{ t('admin.cloudVariables.create.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
|
||||
@@ -78,8 +78,8 @@ const applicationOptions = computed(() => {
|
||||
})
|
||||
|
||||
const scopeOptions = computed(() => [
|
||||
{ label: t('developer.cloudVariables.appScope'), value: 'app' },
|
||||
{ label: t('developer.cloudVariables.userScope'), value: 'user' },
|
||||
{ label: t('admin.cloudVariables.appScope'), value: 'app' },
|
||||
{ label: t('admin.cloudVariables.userScope'), value: 'user' },
|
||||
])
|
||||
|
||||
async function fetchApplications() {
|
||||
@@ -112,11 +112,11 @@ async function fetchVariables() {
|
||||
}
|
||||
|
||||
function openCreatePage() {
|
||||
router.push('/developer/cloud-variables/create')
|
||||
router.push('/admin/cloud-variables/create')
|
||||
}
|
||||
|
||||
function openEditPage(variable: CloudVariable) {
|
||||
router.push(`/developer/cloud-variables/${variable.id}`)
|
||||
router.push(`/admin/cloud-variables/${variable.id}`)
|
||||
}
|
||||
|
||||
function confirmDelete(variable: CloudVariable) {
|
||||
@@ -130,12 +130,12 @@ async function handleDelete() {
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/cloud-variables/${deleteTarget.value.id}`)
|
||||
toast.success(t('developer.cloudVariables.deleteSuccess'))
|
||||
toast.success(t('admin.cloudVariables.deleteSuccess'))
|
||||
fetchVariables()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('删除云端变量失败:', error)
|
||||
toast.error(error.message || t('developer.cloudVariables.deleteFailed'))
|
||||
toast.error(error.message || t('admin.cloudVariables.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
deleteTarget.value = null
|
||||
@@ -154,12 +154,12 @@ async function handleToggleStatus(variable: CloudVariable) {
|
||||
description: variable.description,
|
||||
status: newStatus,
|
||||
})
|
||||
toast.success(newStatus === 'active' ? t('developer.cloudVariables.enableSuccess') : t('developer.cloudVariables.disableSuccess'))
|
||||
toast.success(newStatus === 'active' ? t('admin.cloudVariables.enableSuccess') : t('admin.cloudVariables.disableSuccess'))
|
||||
fetchVariables()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('更新状态失败:', error)
|
||||
toast.error(error.message || t('developer.cloudVariables.updateFailed'))
|
||||
toast.error(error.message || t('admin.cloudVariables.updateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ function handleDownload(variable: CloudVariable) {
|
||||
}
|
||||
|
||||
function handleViewRecords(variable: CloudVariable) {
|
||||
router.push(`/developer/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
|
||||
router.push(`/admin/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
|
||||
}
|
||||
|
||||
function openBatchDialog(action: 'enable' | 'disable' | 'delete', rows: CloudVariable[]) {
|
||||
@@ -189,7 +189,7 @@ async function handleBatchAction() {
|
||||
try {
|
||||
if (batchAction.value === 'delete') {
|
||||
await Promise.all(ids.map(id => api.delete(`/dev/cloud-variables/${id}`)))
|
||||
toast.success(t('developer.cloudVariables.batchDeleteSuccess'))
|
||||
toast.success(t('admin.cloudVariables.batchDeleteSuccess'))
|
||||
}
|
||||
else {
|
||||
const status = batchAction.value === 'enable' ? 'active' : 'inactive'
|
||||
@@ -202,13 +202,13 @@ async function handleBatchAction() {
|
||||
description: v.description,
|
||||
status,
|
||||
})))
|
||||
toast.success(batchAction.value === 'enable' ? t('developer.cloudVariables.batchEnableSuccess') : t('developer.cloudVariables.batchDisableSuccess'))
|
||||
toast.success(batchAction.value === 'enable' ? t('admin.cloudVariables.batchEnableSuccess') : t('admin.cloudVariables.batchDisableSuccess'))
|
||||
}
|
||||
fetchVariables()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量操作失败:', error)
|
||||
toast.error(error.message || t('developer.cloudVariables.batchFailed'))
|
||||
toast.error(error.message || t('admin.cloudVariables.batchFailed'))
|
||||
}
|
||||
finally {
|
||||
batchAction.value = null
|
||||
@@ -224,14 +224,14 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cloudVariables.title')"
|
||||
:description="t('developer.cloudVariables.description')"
|
||||
:title="t('admin.cloudVariables.title')"
|
||||
:description="t('admin.cloudVariables.description')"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton size="sm" @click="openCreatePage">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cloudVariables.addVariable') }}
|
||||
{{ t('admin.cloudVariables.addVariable') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -240,7 +240,7 @@ onMounted(async () => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudVariables.totalVariables') }}
|
||||
{{ t('admin.cloudVariables.totalVariables') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:variable" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -254,7 +254,7 @@ onMounted(async () => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudVariables.appScopeVariables') }}
|
||||
{{ t('admin.cloudVariables.appScopeVariables') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:globe" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -268,7 +268,7 @@ onMounted(async () => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudVariables.userScopeVariables') }}
|
||||
{{ t('admin.cloudVariables.userScopeVariables') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:user" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -282,7 +282,7 @@ onMounted(async () => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudVariables.appCount') }}
|
||||
{{ t('admin.cloudVariables.appCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -310,12 +310,12 @@ onMounted(async () => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.cloudVariables.application')"
|
||||
:title="t('admin.cloudVariables.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="scopeFilter"
|
||||
:title="t('developer.cloudVariables.scope')"
|
||||
:title="t('admin.cloudVariables.scope')"
|
||||
:options="scopeOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
@@ -331,30 +331,30 @@ onMounted(async () => {
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.cloudVariables.delete')"
|
||||
:confirm-button-text="t('admin.cloudVariables.delete')"
|
||||
:cancel-button-text="t('common.reset')"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.cloudVariables.deleteVariable') }}
|
||||
{{ t('admin.cloudVariables.deleteVariable') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.cloudVariables.deleteConfirm', { key: deleteTarget?.key }) }}
|
||||
{{ t('admin.cloudVariables.deleteConfirm', { key: deleteTarget?.key }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDialogOpen"
|
||||
:destructive="batchAction === 'delete'"
|
||||
:confirm-button-text="batchAction === 'delete' ? t('developer.cloudVariables.delete') : t('common.confirm')"
|
||||
:confirm-button-text="batchAction === 'delete' ? t('admin.cloudVariables.delete') : t('common.confirm')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleBatchAction"
|
||||
>
|
||||
<template #title>
|
||||
{{ batchAction === 'delete' ? t('developer.cloudVariables.batchDelete') : batchAction === 'enable' ? t('developer.cloudVariables.batchEnable') : t('developer.cloudVariables.batchDisable') }}
|
||||
{{ batchAction === 'delete' ? t('admin.cloudVariables.batchDelete') : batchAction === 'enable' ? t('admin.cloudVariables.batchEnable') : t('admin.cloudVariables.batchDisable') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ batchAction === 'delete' ? t('developer.cloudVariables.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('developer.cloudVariables.batchEnableConfirm', { count: selectedRows.length }) : t('developer.cloudVariables.batchDisableConfirm', { count: selectedRows.length }) }}
|
||||
{{ batchAction === 'delete' ? t('admin.cloudVariables.batchDeleteConfirm', { count: selectedRows.length }) : batchAction === 'enable' ? t('admin.cloudVariables.batchEnableConfirm', { count: selectedRows.length }) : t('admin.cloudVariables.batchDisableConfirm', { count: selectedRows.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Ban, Eye, LogOut, MoreHorizontal, Trash2 } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type { Device } from '@/pages/developer/devices/data/schema'
|
||||
import type { Device } from '@/pages/admin/devices/data/schema'
|
||||
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -146,7 +146,7 @@ export function getColumns(actions: {
|
||||
{ align: 'end' },
|
||||
() => [
|
||||
hasOnlineSessions
|
||||
? h(DropdownMenuItem, { onClick: () => router.push(`/developer/sessions?device_fingerprint=${device.device_id}`) }, () => [
|
||||
? h(DropdownMenuItem, { onClick: () => router.push(`/admin/sessions?device_fingerprint=${device.device_id}`) }, () => [
|
||||
h(Eye, { class: 'mr-2 h-4 w-4' }),
|
||||
'查看实例',
|
||||
])
|
||||
|
||||
@@ -30,7 +30,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
|
||||
<template>
|
||||
<div class="flex flex-1 items-center space-x-2">
|
||||
<Input
|
||||
:placeholder="t('developer.devices.searchPlaceholder')"
|
||||
:placeholder="t('admin.devices.searchPlaceholder')"
|
||||
:model-value="(table.getColumn('device_id')?.getFilterValue() as string) ?? ''"
|
||||
class="h-8 w-[150px] lg:w-[250px]"
|
||||
@update:model-value="table.getColumn('device_id')?.setFilterValue($event)"
|
||||
@@ -41,7 +41,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="table.resetColumnFilters()"
|
||||
>
|
||||
{{ t('developer.devices.reset') }}
|
||||
{{ t('admin.devices.reset') }}
|
||||
<Icon icon="lucide:x" class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -59,7 +59,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
|
||||
|
||||
<template v-if="selectedCount > 0">
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ t('developer.devices.selected', { count: selectedCount }) }}
|
||||
{{ t('admin.devices.selected', { count: selectedCount }) }}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -68,7 +68,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
|
||||
@click="emit('batchUnban')"
|
||||
>
|
||||
<Icon icon="lucide:check-circle" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.devices.batchUnban') }}
|
||||
{{ t('admin.devices.batchUnban') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -77,7 +77,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
|
||||
@click="emit('batchBan')"
|
||||
>
|
||||
<Icon icon="lucide:ban" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.devices.batchBan') }}
|
||||
{{ t('admin.devices.batchBan') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -86,7 +86,7 @@ const selectedCount = computed(() => props.table.getSelectedRowModel().rows.leng
|
||||
@click="emit('batchUnbind')"
|
||||
>
|
||||
<Icon icon="lucide:trash-2" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.devices.batchUnbind') }}
|
||||
{{ t('admin.devices.batchUnbind') }}
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -46,16 +46,16 @@ const table = generateVueTable<Device>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
'select': 'developer.devices.select',
|
||||
'device_id': 'developer.devices.columns.deviceId',
|
||||
'device_name': 'developer.devices.columns.deviceName',
|
||||
'device_type': 'developer.devices.columns.deviceType',
|
||||
'user.username': 'developer.devices.columns.user',
|
||||
'application.name': 'developer.devices.columns.application',
|
||||
'online_status': 'developer.devices.columns.status',
|
||||
'online_sessions': 'developer.devices.columns.onlineSessions',
|
||||
'status': 'developer.devices.columns.deviceStatus',
|
||||
'created_at': 'developer.devices.columns.createdAt',
|
||||
'select': 'admin.devices.select',
|
||||
'device_id': 'admin.devices.columns.deviceId',
|
||||
'device_name': 'admin.devices.columns.deviceName',
|
||||
'device_type': 'admin.devices.columns.deviceType',
|
||||
'user.username': 'admin.devices.columns.user',
|
||||
'application.name': 'admin.devices.columns.application',
|
||||
'online_status': 'admin.devices.columns.status',
|
||||
'online_sessions': 'admin.devices.columns.onlineSessions',
|
||||
'status': 'admin.devices.columns.deviceStatus',
|
||||
'created_at': 'admin.devices.columns.createdAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -69,13 +69,13 @@ defineExpose({
|
||||
<div class="space-y-3">
|
||||
<BulkActions :table="table" entity-name="devices">
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchUnban')">
|
||||
{{ t('developer.devices.batchUnban') }}
|
||||
{{ t('admin.devices.batchUnban') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchBan')">
|
||||
{{ t('developer.devices.batchBan') }}
|
||||
{{ t('admin.devices.batchBan') }}
|
||||
</UiButton>
|
||||
<UiButton variant="destructive" size="sm" @click="emit('batchUnbind')">
|
||||
{{ t('developer.devices.batchUnbind') }}
|
||||
{{ t('admin.devices.batchUnbind') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
|
||||
@@ -5,12 +5,12 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { Device } from '@/pages/developer/devices/data/schema'
|
||||
import type { Device } from '@/pages/admin/devices/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/developer/devices/components/data-table.vue'
|
||||
import DataTable from '@/pages/admin/devices/components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -80,8 +80,8 @@ const userOptions = computed(() => {
|
||||
})
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('developer.devices.normal'), value: 'active' },
|
||||
{ label: t('developer.devices.banned'), value: 'banned' },
|
||||
{ label: t('admin.devices.normal'), value: 'active' },
|
||||
{ label: t('admin.devices.banned'), value: 'banned' },
|
||||
])
|
||||
|
||||
async function fetchApplications() {
|
||||
@@ -138,12 +138,12 @@ async function toggleDeviceStatus(device: Device) {
|
||||
const newStatus = device.status === 'banned' ? 'active' : 'banned'
|
||||
try {
|
||||
await api.put(`/dev/devices/${device.id}/status`, { status: newStatus })
|
||||
toast.success(t('developer.devices.statusUpdateSuccess'))
|
||||
toast.success(t('admin.devices.statusUpdateSuccess'))
|
||||
fetchDevices()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('切换设备状态失败:', error)
|
||||
toast.error(error.message || t('developer.devices.statusUpdateFailed'))
|
||||
toast.error(error.message || t('admin.devices.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,12 +158,12 @@ async function handleDeleteDevice() {
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/devices/${deleteTarget.value.id}`)
|
||||
toast.success(t('developer.devices.unbindSuccess'))
|
||||
toast.success(t('admin.devices.unbindSuccess'))
|
||||
fetchDevices()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('解绑设备失败:', error)
|
||||
toast.error(error.message || t('developer.devices.unbindFailed'))
|
||||
toast.error(error.message || t('admin.devices.unbindFailed'))
|
||||
}
|
||||
finally {
|
||||
deleteTarget.value = null
|
||||
@@ -178,12 +178,12 @@ function confirmBatchDelete(ids: (string | number)[]) {
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
await api.delete('/dev/devices/batch', { device_ids: batchDeleteIds.value.map(id => Number(id)) } as any)
|
||||
toast.success(t('developer.devices.batchUnbindSuccess'))
|
||||
toast.success(t('admin.devices.batchUnbindSuccess'))
|
||||
fetchDevices()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量解绑失败:', error)
|
||||
toast.error(error.message || t('developer.devices.batchUnbindFailed'))
|
||||
toast.error(error.message || t('admin.devices.batchUnbindFailed'))
|
||||
}
|
||||
finally {
|
||||
batchDeleteIds.value = []
|
||||
@@ -193,12 +193,12 @@ async function handleBatchDelete() {
|
||||
async function batchToggleStatus(ids: (string | number)[], status: string) {
|
||||
try {
|
||||
await api.post('/dev/devices/batch/status', { device_ids: ids.map(id => Number(id)), status })
|
||||
toast.success(t('developer.devices.batchUpdateSuccess'))
|
||||
toast.success(t('admin.devices.batchUpdateSuccess'))
|
||||
fetchDevices()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量更新状态失败:', error)
|
||||
toast.error(error.message || t('developer.devices.batchUpdateFailed'))
|
||||
toast.error(error.message || t('admin.devices.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,8 +239,8 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.devices.title')"
|
||||
:description="t('developer.devices.description')"
|
||||
:title="t('admin.devices.title')"
|
||||
:description="t('admin.devices.description')"
|
||||
sticky
|
||||
>
|
||||
<div class="space-y-6">
|
||||
@@ -248,7 +248,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.devices.totalDevices') }}
|
||||
{{ t('admin.devices.totalDevices') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:monitor" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -262,7 +262,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.devices.onlineSessions') }}
|
||||
{{ t('admin.devices.onlineSessions') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:layers" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -276,7 +276,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.devices.normalDevices') }}
|
||||
{{ t('admin.devices.normalDevices') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -290,7 +290,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.devices.banned') }}
|
||||
{{ t('admin.devices.banned') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -319,17 +319,17 @@ onMounted(() => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.devices.application')"
|
||||
:title="t('admin.devices.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="userIdFilter"
|
||||
:title="t('developer.devices.user')"
|
||||
:title="t('admin.devices.user')"
|
||||
:options="userOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('developer.devices.status')"
|
||||
:title="t('admin.devices.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</template>
|
||||
@@ -341,30 +341,30 @@ onMounted(() => {
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.devices.unbind')"
|
||||
:confirm-button-text="t('admin.devices.unbind')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleDeleteDevice"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.devices.unbindDevice') }}
|
||||
{{ t('admin.devices.unbindDevice') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.devices.unbindConfirm', { deviceId: deleteTarget?.device_id }) }}
|
||||
{{ t('admin.devices.unbindConfirm', { deviceId: deleteTarget?.device_id }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.devices.unbind')"
|
||||
:confirm-button-text="t('admin.devices.unbind')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.devices.batchUnbind') }}
|
||||
{{ t('admin.devices.batchUnbind') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.devices.batchUnbindConfirm', { count: batchDeleteIds.length }) }}
|
||||
{{ t('admin.devices.batchUnbindConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
|
||||
@@ -2,49 +2,106 @@
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { DynamicCode } from '@/pages/developer/dynamic-code/data/schema'
|
||||
import type { DynamicCode } from '@/pages/admin/dynamic-code/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/admin/dynamic-code/components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Application {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const saving = ref(false)
|
||||
const loading = ref(true)
|
||||
const codes = ref<DynamicCode[]>([])
|
||||
const tableRef = ref()
|
||||
const applications = ref<Application[]>([])
|
||||
const codeId = ref<number>(0)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
application_id: '',
|
||||
description: '',
|
||||
code: '',
|
||||
enabled: true,
|
||||
const appFilter = ref<string>('')
|
||||
const statusFilter = ref<string>('')
|
||||
const createdStartDate = ref<string>('')
|
||||
const createdEndDate = ref<string>('')
|
||||
const updatedStartDate = ref<string>('')
|
||||
const updatedEndDate = ref<string>('')
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<DynamicCode | null>(null)
|
||||
const batchDeleteDialogOpen = ref(false)
|
||||
const batchDeleteIds = ref<(string | number)[]>([])
|
||||
|
||||
const filteredCodes = computed(() => {
|
||||
let result = codes.value
|
||||
|
||||
if (appFilter.value) {
|
||||
result = result.filter(code => String(code.application_id) === appFilter.value)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
if (statusFilter.value === 'enabled') {
|
||||
result = result.filter(code => code.enabled === true)
|
||||
}
|
||||
else if (statusFilter.value === 'disabled') {
|
||||
result = result.filter(code => code.enabled === false)
|
||||
}
|
||||
}
|
||||
|
||||
if (createdStartDate.value) {
|
||||
const fromDateTime = createdStartDate.value.includes('T')
|
||||
? createdStartDate.value.replace('T', ' ')
|
||||
: `${createdStartDate.value} 00:00`
|
||||
result = result.filter(code => code.created_at >= fromDateTime)
|
||||
}
|
||||
|
||||
if (createdEndDate.value) {
|
||||
const toDateTime = createdEndDate.value.includes('T')
|
||||
? createdEndDate.value.replace('T', ' ')
|
||||
: `${createdEndDate.value} 23:59`
|
||||
result = result.filter(code => code.created_at <= toDateTime)
|
||||
}
|
||||
|
||||
if (updatedStartDate.value) {
|
||||
const fromDateTime = updatedStartDate.value.includes('T')
|
||||
? updatedStartDate.value.replace('T', ' ')
|
||||
: `${updatedStartDate.value} 00:00`
|
||||
result = result.filter(code => code.updated_at >= fromDateTime)
|
||||
}
|
||||
|
||||
if (updatedEndDate.value) {
|
||||
const toDateTime = updatedEndDate.value.includes('T')
|
||||
? updatedEndDate.value.replace('T', ' ')
|
||||
: `${updatedEndDate.value} 23:59`
|
||||
result = result.filter(code => code.updated_at <= toDateTime)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const filteredApplications = computed(() => {
|
||||
const enabledCount = computed(() => filteredCodes.value.filter(code => code.enabled === true).length)
|
||||
const disabledCount = computed(() => filteredCodes.value.filter(code => code.enabled === false).length)
|
||||
const linkedAppsCount = computed(() => new Set(filteredCodes.value.map(code => code.application_id)).size)
|
||||
|
||||
const applicationOptions = computed(() => {
|
||||
return applications.value.map(app => ({
|
||||
label: app.name,
|
||||
value: String(app.id),
|
||||
}))
|
||||
})
|
||||
|
||||
const selectedApplication = computed(() => {
|
||||
if (form.value.application_id) {
|
||||
return applications.value.find(app => String(app.id) === form.value.application_id)
|
||||
}
|
||||
return null
|
||||
})
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('admin.cloudFunction.enabled'), value: 'enabled' },
|
||||
{ label: t('admin.cloudFunction.disabled'), value: 'disabled' },
|
||||
])
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
@@ -56,250 +113,244 @@ async function fetchApplications() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCode() {
|
||||
async function fetchCodes() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<DynamicCode>(`/dev/dynamic-codes/${codeId.value}`)
|
||||
if (data) {
|
||||
form.value.name = data.name
|
||||
form.value.application_id = String(data.application_id)
|
||||
form.value.description = data.description || ''
|
||||
form.value.code = data.code
|
||||
form.value.enabled = data.enabled
|
||||
}
|
||||
const data = await api.get<DynamicCode[]>('/dev/dynamic-codes')
|
||||
codes.value = Array.isArray(data) ? data : []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取云端函数失败:', error)
|
||||
toast.error(t('developer.cloudFunction.fetchFailed'))
|
||||
router.push('/developer/cloud-function')
|
||||
console.error('获取云端函数列表失败:', error)
|
||||
codes.value = []
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.value.name.trim()) {
|
||||
toast.error(t('developer.cloudFunction.create.nameRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.application_id) {
|
||||
toast.error(t('developer.cloudFunction.create.selectAppRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.code.trim()) {
|
||||
toast.error(t('developer.cloudFunction.create.codeRequired'))
|
||||
return
|
||||
}
|
||||
function goToCreate() {
|
||||
router.push('/admin/cloud-function/create')
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
async function toggleStatus(code: DynamicCode) {
|
||||
try {
|
||||
await api.put(`/dev/dynamic-codes/${codeId.value}`, {
|
||||
name: form.value.name,
|
||||
application_id: Number.parseInt(form.value.application_id),
|
||||
description: form.value.description,
|
||||
code: form.value.code,
|
||||
enabled: form.value.enabled,
|
||||
})
|
||||
toast.success(t('developer.cloudFunction.updateSuccess'))
|
||||
router.push('/developer/cloud-function')
|
||||
const newStatus = !code.enabled
|
||||
await api.put(`/dev/dynamic-codes/${code.id}/status`, { enabled: newStatus })
|
||||
toast.success(t('admin.cloudFunction.statusUpdateSuccess'))
|
||||
fetchCodes()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('更新云端函数失败:', error)
|
||||
toast.error(error.message || t('developer.cloudFunction.updateFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
console.error('切换云端函数状态失败:', error)
|
||||
toast.error(error.message || t('admin.cloudFunction.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
router.push('/developer/cloud-function')
|
||||
function editCode(code: DynamicCode) {
|
||||
router.push(`/admin/cloud-function/${code.id}`)
|
||||
}
|
||||
|
||||
function confirmDeleteCode(code: DynamicCode) {
|
||||
deleteTarget.value = code
|
||||
deleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDeleteCode() {
|
||||
if (!deleteTarget.value)
|
||||
return
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/dynamic-codes/${deleteTarget.value.id}`)
|
||||
toast.success(t('admin.cloudFunction.deleteSuccess'))
|
||||
fetchCodes()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('删除云端函数失败:', error)
|
||||
toast.error(error.message || t('admin.cloudFunction.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
deleteTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function confirmBatchDelete(ids: (string | number)[]) {
|
||||
batchDeleteIds.value = ids
|
||||
batchDeleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
await api.delete('/dev/dynamic-codes/batch', { ids: batchDeleteIds.value } as any)
|
||||
toast.success(t('admin.cloudFunction.deleteSuccess'))
|
||||
fetchCodes()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量删除失败:', error)
|
||||
toast.error(error.message || t('admin.cloudFunction.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
batchDeleteIds.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function batchToggleStatus(ids: (string | number)[], enabled: boolean) {
|
||||
try {
|
||||
await api.put('/dev/dynamic-codes/batch/status', { ids, enabled })
|
||||
toast.success(t('admin.cloudFunction.batchUpdateSuccess'))
|
||||
fetchCodes()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量更新状态失败:', error)
|
||||
toast.error(error.message || t('admin.cloudFunction.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const id = route.params.id
|
||||
if (id && typeof id === 'string') {
|
||||
codeId.value = Number.parseInt(id)
|
||||
fetchApplications()
|
||||
fetchCode()
|
||||
}
|
||||
else {
|
||||
toast.error('无效的云端函数ID')
|
||||
router.push('/developer/cloud-function')
|
||||
}
|
||||
fetchApplications()
|
||||
fetchCodes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cloudFunction.edit.title')"
|
||||
:description="t('developer.cloudFunction.edit.description')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.cloudFunction.title'), href: '/developer/cloud-function' },
|
||||
{ title: t('developer.cloudFunction.edit.title') },
|
||||
]"
|
||||
:title="t('admin.cloudFunction.title')"
|
||||
:description="t('admin.cloudFunction.description')"
|
||||
sticky
|
||||
>
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Icon icon="lucide:loader-2" class="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<template #actions>
|
||||
<UiButton size="sm" @click="goToCreate">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('admin.cloudFunction.createBtn') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.cloudFunction.edit.config') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cloudFunction.edit.configDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="application">
|
||||
{{ t('developer.cloudFunction.create.application') }}
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.application_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cloudFunction.create.selectAppPlaceholder')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem
|
||||
v-for="app in filteredApplications"
|
||||
:key="app.value"
|
||||
:value="app.value"
|
||||
>
|
||||
{{ app.label }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudFunction.totalCodes') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:code" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ filteredCodes.length }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="name">
|
||||
{{ t('developer.cloudFunction.create.name') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
:placeholder="t('developer.cloudFunction.create.namePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudFunction.enabled') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ enabledCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="description">
|
||||
{{ t('developer.cloudFunction.create.description') }}
|
||||
</UiLabel>
|
||||
<UiTextarea
|
||||
id="description"
|
||||
v-model="form.description"
|
||||
:placeholder="t('developer.cloudFunction.create.descriptionPlaceholder')"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudFunction.disabled') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ disabledCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="code">
|
||||
{{ t('developer.cloudFunction.create.code') }}
|
||||
</UiLabel>
|
||||
<UiTextarea
|
||||
id="code"
|
||||
v-model="form.code"
|
||||
:placeholder="t('developer.cloudFunction.create.codePlaceholder')"
|
||||
rows="12"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.cloudFunction.create.codeHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.cloudFunction.create.status') }}</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.cloudFunction.create.enabled') }}
|
||||
</p>
|
||||
</div>
|
||||
<UiSwitch v-model="form.enabled" />
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:eye" class="size-5" />
|
||||
{{ t('developer.cloudFunction.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.name') }}</span>
|
||||
<span>{{ form.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.application') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.description') }}</span>
|
||||
<span class="text-right max-w-[150px] truncate">{{ form.description || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm items-center">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.status') }}</span>
|
||||
<UiSwitch
|
||||
v-model="form.enabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="border-t pt-3 mt-3">
|
||||
<div class="text-sm">
|
||||
<div class="text-muted-foreground mb-2">
|
||||
{{ t('developer.cloudFunction.create.code') }}
|
||||
</div>
|
||||
<div class="font-mono text-xs bg-muted p-2 rounded max-h-[150px] overflow-auto" style="white-space: pre-wrap;">{{ form.code || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving || !form.name || !form.application_id || !form.code"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cloudFunction.edit.submitBtn') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ t('developer.cloudFunction.edit.cancelBtn') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.cloudFunction.linkedApps') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:link" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ linkedAppsCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
ref="tableRef"
|
||||
:loading
|
||||
:data="filteredCodes"
|
||||
:on-toggle-status="toggleStatus"
|
||||
:on-edit="editCode"
|
||||
:on-delete="confirmDeleteCode"
|
||||
@refresh="fetchCodes"
|
||||
@batch-enable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [], true)"
|
||||
@batch-disable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [], false)"
|
||||
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||
>
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('admin.cloudFunction.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('admin.cloudFunction.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
v-model:start-model-value="createdStartDate"
|
||||
v-model:end-model-value="createdEndDate"
|
||||
:title="t('admin.cloudFunction.columns.createdAt')"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
v-model:start-model-value="updatedStartDate"
|
||||
v-model:end-model-value="updatedEndDate"
|
||||
:title="t('admin.cloudFunction.columns.updatedAt')"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('admin.cloudFunction.delete')"
|
||||
:cancel-button-text="t('admin.cloudFunction.create.cancelBtn')"
|
||||
@confirm="handleDeleteCode"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.cloudFunction.deleteCode') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.cloudFunction.deleteCodeConfirm', { codeName: deleteTarget?.name }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('admin.cloudFunction.delete')"
|
||||
:cancel-button-text="t('admin.cloudFunction.create.cancelBtn')"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.cloudFunction.batchDelete') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.cloudFunction.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
|
||||
import { Ban, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { DynamicCode } from '@/pages/developer/dynamic-code/data/schema'
|
||||
import type { DynamicCode } from '@/pages/admin/dynamic-code/data/schema'
|
||||
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -24,7 +24,7 @@ export function getColumns(actions: {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: () => t('developer.cloudFunction.columns.name'),
|
||||
header: () => t('admin.cloudFunction.columns.name'),
|
||||
cell: ({ row }) => {
|
||||
const name = row.getValue('name') as string
|
||||
return name || '-'
|
||||
@@ -32,7 +32,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'application_name',
|
||||
header: () => t('developer.cloudFunction.columns.application'),
|
||||
header: () => t('admin.cloudFunction.columns.application'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.getValue('application_name') as string
|
||||
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
|
||||
@@ -40,7 +40,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: () => t('developer.cloudFunction.columns.description'),
|
||||
header: () => t('admin.cloudFunction.columns.description'),
|
||||
cell: ({ row }) => {
|
||||
const description = row.getValue('description') as string
|
||||
return description || '-'
|
||||
@@ -48,15 +48,15 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'enabled',
|
||||
header: () => t('developer.cloudFunction.columns.status'),
|
||||
header: () => t('admin.cloudFunction.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const enabled = row.getValue('enabled') as boolean
|
||||
return h(Badge, { variant: enabled ? 'default' : 'destructive' }, () => enabled ? t('developer.cloudFunction.enabled') : t('developer.cloudFunction.disabled'))
|
||||
return h(Badge, { variant: enabled ? 'default' : 'destructive' }, () => enabled ? t('admin.cloudFunction.enabled') : t('admin.cloudFunction.disabled'))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'creator.username',
|
||||
header: () => t('developer.cloudFunction.columns.creator'),
|
||||
header: () => t('admin.cloudFunction.columns.creator'),
|
||||
cell: ({ row }) => {
|
||||
const username = row.original.creator?.username
|
||||
return username || '-'
|
||||
@@ -64,7 +64,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.cloudFunction.columns.createdAt'),
|
||||
header: () => t('admin.cloudFunction.columns.createdAt'),
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue('created_at')
|
||||
if (!createdAt)
|
||||
@@ -88,7 +88,7 @@ export function getColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'updated_at',
|
||||
header: () => t('developer.cloudFunction.columns.updatedAt'),
|
||||
header: () => t('admin.cloudFunction.columns.updatedAt'),
|
||||
cell: ({ row }) => {
|
||||
const updatedAt = row.getValue('updated_at')
|
||||
if (!updatedAt)
|
||||
@@ -133,16 +133,16 @@ export function getColumns(actions: {
|
||||
() => [
|
||||
h(DropdownMenuItem, { onClick: () => actions.onEdit(code) }, () => [
|
||||
h(Pencil, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.cloudFunction.edit.title'),
|
||||
t('admin.cloudFunction.edit.title'),
|
||||
]),
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(code) }, () => [
|
||||
h(Ban, { class: 'mr-2 h-4 w-4' }),
|
||||
isDisabled ? t('developer.cloudFunction.enable') : t('developer.cloudFunction.disable'),
|
||||
isDisabled ? t('admin.cloudFunction.enable') : t('admin.cloudFunction.disable'),
|
||||
]),
|
||||
h(DropdownMenuSeparator),
|
||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(code) }, () => [
|
||||
h(Trash2, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.cloudFunction.delete'),
|
||||
t('admin.cloudFunction.delete'),
|
||||
]),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { X } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { DynamicCode } from '@/pages/developer/dynamic-code/data/schema'
|
||||
import type { DynamicCode } from '@/pages/admin/dynamic-code/data/schema'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
|
||||
<template>
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
:placeholder="t('developer.cloudFunction.searchCode')"
|
||||
:placeholder="t('admin.cloudFunction.searchCode')"
|
||||
:model-value="(table.getColumn('name')?.getFilterValue() as string) ?? ''"
|
||||
class="h-8 w-[150px] lg:w-[250px]"
|
||||
@input="table.getColumn('name')?.setFilterValue($event.target.value)"
|
||||
@@ -36,7 +36,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="table.resetColumnFilters()"
|
||||
>
|
||||
{{ t('developer.cloudFunction.reset') }}
|
||||
{{ t('admin.cloudFunction.reset') }}
|
||||
<X class="size-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -45,14 +45,14 @@ const table = generateVueTable<DynamicCode>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.cloudFunction.select',
|
||||
name: 'developer.cloudFunction.columns.name',
|
||||
application_name: 'developer.cloudFunction.columns.application',
|
||||
description: 'developer.cloudFunction.columns.description',
|
||||
enabled: 'developer.cloudFunction.columns.status',
|
||||
creator_username: 'developer.cloudFunction.columns.creator',
|
||||
created_at: 'developer.cloudFunction.columns.createdAt',
|
||||
updated_at: 'developer.cloudFunction.columns.updatedAt',
|
||||
select: 'admin.cloudFunction.select',
|
||||
name: 'admin.cloudFunction.columns.name',
|
||||
application_name: 'admin.cloudFunction.columns.application',
|
||||
description: 'admin.cloudFunction.columns.description',
|
||||
enabled: 'admin.cloudFunction.columns.status',
|
||||
creator_username: 'admin.cloudFunction.columns.creator',
|
||||
created_at: 'admin.cloudFunction.columns.createdAt',
|
||||
updated_at: 'admin.cloudFunction.columns.updatedAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -69,13 +69,13 @@ defineExpose({
|
||||
</div>
|
||||
<BulkActions :table="table" entity-name="functions">
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
|
||||
{{ t('developer.cloudFunction.batchEnableBtn') }}
|
||||
{{ t('admin.cloudFunction.batchEnableBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
|
||||
{{ t('developer.cloudFunction.batchDisableBtn') }}
|
||||
{{ t('admin.cloudFunction.batchDisableBtn') }}
|
||||
</UiButton>
|
||||
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||
{{ t('developer.cloudFunction.batchDeleteBtn') }}
|
||||
{{ t('admin.cloudFunction.batchDeleteBtn') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
|
||||
@@ -53,15 +53,15 @@ async function fetchApplications() {
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.value.name.trim()) {
|
||||
toast.error(t('developer.cloudFunction.create.nameRequired'))
|
||||
toast.error(t('admin.cloudFunction.create.nameRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.application_id) {
|
||||
toast.error(t('developer.cloudFunction.create.selectAppRequired'))
|
||||
toast.error(t('admin.cloudFunction.create.selectAppRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.code.trim()) {
|
||||
toast.error(t('developer.cloudFunction.create.codeRequired'))
|
||||
toast.error(t('admin.cloudFunction.create.codeRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,12 +74,12 @@ async function handleSubmit() {
|
||||
code: form.value.code,
|
||||
enabled: form.value.enabled,
|
||||
})
|
||||
toast.success(t('developer.cloudFunction.create.submitSuccess'))
|
||||
router.push('/developer/cloud-function')
|
||||
toast.success(t('admin.cloudFunction.create.submitSuccess'))
|
||||
router.push('/admin/cloud-function')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('创建云端函数失败:', error)
|
||||
toast.error(error.message || t('developer.cloudFunction.create.submitFailed'))
|
||||
toast.error(error.message || t('admin.cloudFunction.create.submitFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
@@ -87,7 +87,7 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
router.push('/developer/cloud-function')
|
||||
router.push('/admin/cloud-function')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -97,11 +97,11 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cloudFunction.create.title')"
|
||||
:description="t('developer.cloudFunction.create.description')"
|
||||
:title="t('admin.cloudFunction.create.title')"
|
||||
:description="t('admin.cloudFunction.create.description')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.cloudFunction.title'), href: '/developer/cloud-function' },
|
||||
{ title: t('developer.cloudFunction.create.title') },
|
||||
{ title: t('admin.cloudFunction.title'), href: '/admin/cloud-function' },
|
||||
{ title: t('admin.cloudFunction.create.title') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
@@ -112,18 +112,18 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.cloudFunction.create.config') }}
|
||||
{{ t('admin.cloudFunction.create.config') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.cloudFunction.create.configDesc') }}</UiCardDescription>
|
||||
<UiCardDescription>{{ t('admin.cloudFunction.create.configDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="application">
|
||||
{{ t('developer.cloudFunction.create.application') }}
|
||||
{{ t('admin.cloudFunction.create.application') }}
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.application_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.cloudFunction.create.selectAppPlaceholder')" />
|
||||
<UiSelectValue :placeholder="t('admin.cloudFunction.create.selectAppPlaceholder')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem
|
||||
@@ -139,47 +139,47 @@ onMounted(() => {
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="name">
|
||||
{{ t('developer.cloudFunction.create.name') }}
|
||||
{{ t('admin.cloudFunction.create.name') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
:placeholder="t('developer.cloudFunction.create.namePlaceholder')"
|
||||
:placeholder="t('admin.cloudFunction.create.namePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="description">
|
||||
{{ t('developer.cloudFunction.create.description') }}
|
||||
{{ t('admin.cloudFunction.create.description') }}
|
||||
</UiLabel>
|
||||
<UiTextarea
|
||||
id="description"
|
||||
v-model="form.description"
|
||||
:placeholder="t('developer.cloudFunction.create.descriptionPlaceholder')"
|
||||
:placeholder="t('admin.cloudFunction.create.descriptionPlaceholder')"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="code">
|
||||
{{ t('developer.cloudFunction.create.code') }}
|
||||
{{ t('admin.cloudFunction.create.code') }}
|
||||
</UiLabel>
|
||||
<UiTextarea
|
||||
id="code"
|
||||
v-model="form.code"
|
||||
:placeholder="t('developer.cloudFunction.create.codePlaceholder')"
|
||||
:placeholder="t('admin.cloudFunction.create.codePlaceholder')"
|
||||
rows="12"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('developer.cloudFunction.create.codeHelp') }}
|
||||
{{ t('admin.cloudFunction.create.codeHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
{{ t('developer.cloudFunction.create.enabled') }}
|
||||
{{ t('admin.cloudFunction.create.enabled') }}
|
||||
</p>
|
||||
</div>
|
||||
<UiSwitch v-model="form.enabled" />
|
||||
@@ -193,25 +193,25 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:eye" class="size-5" />
|
||||
{{ t('developer.cloudFunction.create.preview') }}
|
||||
{{ t('admin.cloudFunction.create.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.name') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudFunction.create.name') }}</span>
|
||||
<span>{{ form.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.application') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudFunction.create.application') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.description') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudFunction.create.description') }}</span>
|
||||
<span class="text-right max-w-[150px] truncate">{{ form.description || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm items-center">
|
||||
<span class="text-muted-foreground">{{ t('developer.cloudFunction.create.status') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.cloudFunction.create.status') }}</span>
|
||||
<UiSwitch
|
||||
v-model="form.enabled"
|
||||
/>
|
||||
@@ -219,7 +219,7 @@ onMounted(() => {
|
||||
<div class="border-t pt-3 mt-3">
|
||||
<div class="text-sm">
|
||||
<div class="text-muted-foreground mb-2">
|
||||
{{ t('developer.cloudFunction.create.code') }}
|
||||
{{ t('admin.cloudFunction.create.code') }}
|
||||
</div>
|
||||
<div class="font-mono text-xs bg-muted p-2 rounded max-h-[150px] overflow-auto" style="white-space: pre-wrap;">{{ form.code || '-' }}</div>
|
||||
</div>
|
||||
@@ -239,14 +239,14 @@ onMounted(() => {
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cloudFunction.create.submitBtn') }}
|
||||
{{ t('admin.cloudFunction.create.submitBtn') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ t('developer.cloudFunction.create.cancelBtn') }}
|
||||
{{ t('admin.cloudFunction.create.cancelBtn') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
|
||||
@@ -5,13 +5,13 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { DynamicCode } from '@/pages/developer/dynamic-code/data/schema'
|
||||
import type { DynamicCode } from '@/pages/admin/dynamic-code/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/developer/dynamic-code/components/data-table.vue'
|
||||
import DataTable from '@/pages/admin/dynamic-code/components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -99,8 +99,8 @@ const applicationOptions = computed(() => {
|
||||
})
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('developer.cloudFunction.enabled'), value: 'enabled' },
|
||||
{ label: t('developer.cloudFunction.disabled'), value: 'disabled' },
|
||||
{ label: t('admin.cloudFunction.enabled'), value: 'enabled' },
|
||||
{ label: t('admin.cloudFunction.disabled'), value: 'disabled' },
|
||||
])
|
||||
|
||||
async function fetchApplications() {
|
||||
@@ -129,24 +129,24 @@ async function fetchCodes() {
|
||||
}
|
||||
|
||||
function goToCreate() {
|
||||
router.push('/developer/cloud-function/create')
|
||||
router.push('/admin/cloud-function/create')
|
||||
}
|
||||
|
||||
async function toggleStatus(code: DynamicCode) {
|
||||
try {
|
||||
const newStatus = !code.enabled
|
||||
await api.put(`/dev/dynamic-codes/${code.id}/status`, { enabled: newStatus })
|
||||
toast.success(t('developer.cloudFunction.statusUpdateSuccess'))
|
||||
toast.success(t('admin.cloudFunction.statusUpdateSuccess'))
|
||||
fetchCodes()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('切换云端函数状态失败:', error)
|
||||
toast.error(error.message || t('developer.cloudFunction.statusUpdateFailed'))
|
||||
toast.error(error.message || t('admin.cloudFunction.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
function editCode(code: DynamicCode) {
|
||||
router.push(`/developer/cloud-function/${code.id}`)
|
||||
router.push(`/admin/cloud-function/${code.id}`)
|
||||
}
|
||||
|
||||
function confirmDeleteCode(code: DynamicCode) {
|
||||
@@ -160,12 +160,12 @@ async function handleDeleteCode() {
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/dynamic-codes/${deleteTarget.value.id}`)
|
||||
toast.success(t('developer.cloudFunction.deleteSuccess'))
|
||||
toast.success(t('admin.cloudFunction.deleteSuccess'))
|
||||
fetchCodes()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('删除云端函数失败:', error)
|
||||
toast.error(error.message || t('developer.cloudFunction.deleteFailed'))
|
||||
toast.error(error.message || t('admin.cloudFunction.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
deleteTarget.value = null
|
||||
@@ -180,12 +180,12 @@ function confirmBatchDelete(ids: (string | number)[]) {
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
await api.delete('/dev/dynamic-codes/batch', { ids: batchDeleteIds.value } as any)
|
||||
toast.success(t('developer.cloudFunction.deleteSuccess'))
|
||||
toast.success(t('admin.cloudFunction.deleteSuccess'))
|
||||
fetchCodes()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量删除失败:', error)
|
||||
toast.error(error.message || t('developer.cloudFunction.deleteFailed'))
|
||||
toast.error(error.message || t('admin.cloudFunction.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
batchDeleteIds.value = []
|
||||
@@ -195,12 +195,12 @@ async function handleBatchDelete() {
|
||||
async function batchToggleStatus(ids: (string | number)[], enabled: boolean) {
|
||||
try {
|
||||
await api.put('/dev/dynamic-codes/batch/status', { ids, enabled })
|
||||
toast.success(t('developer.cloudFunction.batchUpdateSuccess'))
|
||||
toast.success(t('admin.cloudFunction.batchUpdateSuccess'))
|
||||
fetchCodes()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量更新状态失败:', error)
|
||||
toast.error(error.message || t('developer.cloudFunction.batchUpdateFailed'))
|
||||
toast.error(error.message || t('admin.cloudFunction.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,14 +212,14 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.cloudFunction.title')"
|
||||
:description="t('developer.cloudFunction.description')"
|
||||
:title="t('admin.cloudFunction.title')"
|
||||
:description="t('admin.cloudFunction.description')"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton size="sm" @click="goToCreate">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.cloudFunction.createBtn') }}
|
||||
{{ t('admin.cloudFunction.createBtn') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -228,7 +228,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudFunction.totalCodes') }}
|
||||
{{ t('admin.cloudFunction.totalCodes') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:code" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -242,7 +242,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudFunction.enabled') }}
|
||||
{{ t('admin.cloudFunction.enabled') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -256,7 +256,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudFunction.disabled') }}
|
||||
{{ t('admin.cloudFunction.disabled') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -270,7 +270,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.cloudFunction.linkedApps') }}
|
||||
{{ t('admin.cloudFunction.linkedApps') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:link" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -299,23 +299,23 @@ onMounted(() => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.cloudFunction.application')"
|
||||
:title="t('admin.cloudFunction.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('developer.cloudFunction.status')"
|
||||
:title="t('admin.cloudFunction.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
v-model:start-model-value="createdStartDate"
|
||||
v-model:end-model-value="createdEndDate"
|
||||
:title="t('developer.cloudFunction.columns.createdAt')"
|
||||
:title="t('admin.cloudFunction.columns.createdAt')"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
v-model:start-model-value="updatedStartDate"
|
||||
v-model:end-model-value="updatedEndDate"
|
||||
:title="t('developer.cloudFunction.columns.updatedAt')"
|
||||
:title="t('admin.cloudFunction.columns.updatedAt')"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
@@ -326,30 +326,30 @@ onMounted(() => {
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.cloudFunction.delete')"
|
||||
:cancel-button-text="t('developer.cloudFunction.create.cancelBtn')"
|
||||
:confirm-button-text="t('admin.cloudFunction.delete')"
|
||||
:cancel-button-text="t('admin.cloudFunction.create.cancelBtn')"
|
||||
@confirm="handleDeleteCode"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.cloudFunction.deleteCode') }}
|
||||
{{ t('admin.cloudFunction.deleteCode') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.cloudFunction.deleteCodeConfirm', { codeName: deleteTarget?.name }) }}
|
||||
{{ t('admin.cloudFunction.deleteCodeConfirm', { codeName: deleteTarget?.name }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.cloudFunction.delete')"
|
||||
:cancel-button-text="t('developer.cloudFunction.create.cancelBtn')"
|
||||
:confirm-button-text="t('admin.cloudFunction.delete')"
|
||||
:cancel-button-text="t('admin.cloudFunction.create.cancelBtn')"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.cloudFunction.batchDelete') }}
|
||||
{{ t('admin.cloudFunction.batchDelete') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.cloudFunction.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
{{ t('admin.cloudFunction.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
|
||||
@@ -28,34 +28,34 @@ const form = ref({
|
||||
|
||||
const permissionGroups = computed(() => [
|
||||
{
|
||||
name: t('developer.extension.apiKeys.permissions.groups.user'),
|
||||
name: t('admin.extension.apiKeys.permissions.groups.user'),
|
||||
permissions: [
|
||||
{ value: 'user.read', label: t('developer.extension.apiKeys.permissions.userRead'), description: t('developer.extension.apiKeys.permissions.userReadDesc') },
|
||||
{ value: 'user.list', label: t('developer.extension.apiKeys.permissions.userList'), description: t('developer.extension.apiKeys.permissions.userListDesc') },
|
||||
{ value: 'user.recharge', label: t('developer.extension.apiKeys.permissions.userRecharge'), description: t('developer.extension.apiKeys.permissions.userRechargeDesc') },
|
||||
{ value: 'user.deduct', label: t('developer.extension.apiKeys.permissions.userDeduct'), description: t('developer.extension.apiKeys.permissions.userDeductDesc') },
|
||||
{ value: 'user.variables', label: t('developer.extension.apiKeys.permissions.userVariables'), description: t('developer.extension.apiKeys.permissions.userVariablesDesc') },
|
||||
{ value: 'user.read', label: t('admin.extension.apiKeys.permissions.userRead'), description: t('admin.extension.apiKeys.permissions.userReadDesc') },
|
||||
{ value: 'user.list', label: t('admin.extension.apiKeys.permissions.userList'), description: t('admin.extension.apiKeys.permissions.userListDesc') },
|
||||
{ value: 'user.recharge', label: t('admin.extension.apiKeys.permissions.userRecharge'), description: t('admin.extension.apiKeys.permissions.userRechargeDesc') },
|
||||
{ value: 'user.deduct', label: t('admin.extension.apiKeys.permissions.userDeduct'), description: t('admin.extension.apiKeys.permissions.userDeductDesc') },
|
||||
{ value: 'user.variables', label: t('admin.extension.apiKeys.permissions.userVariables'), description: t('admin.extension.apiKeys.permissions.userVariablesDesc') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('developer.extension.apiKeys.permissions.groups.card'),
|
||||
name: t('admin.extension.apiKeys.permissions.groups.card'),
|
||||
permissions: [
|
||||
{ value: 'card.read', label: t('developer.extension.apiKeys.permissions.cardRead'), description: t('developer.extension.apiKeys.permissions.cardReadDesc') },
|
||||
{ value: 'card.generate', label: t('developer.extension.apiKeys.permissions.cardGenerate'), description: t('developer.extension.apiKeys.permissions.cardGenerateDesc') },
|
||||
{ value: 'card.read', label: t('admin.extension.apiKeys.permissions.cardRead'), description: t('admin.extension.apiKeys.permissions.cardReadDesc') },
|
||||
{ value: 'card.generate', label: t('admin.extension.apiKeys.permissions.cardGenerate'), description: t('admin.extension.apiKeys.permissions.cardGenerateDesc') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('developer.extension.apiKeys.permissions.groups.notification'),
|
||||
name: t('admin.extension.apiKeys.permissions.groups.notification'),
|
||||
permissions: [
|
||||
{ value: 'notification.send', label: t('developer.extension.apiKeys.permissions.notificationSend'), description: t('developer.extension.apiKeys.permissions.notificationSendDesc') },
|
||||
{ value: 'notification.send', label: t('admin.extension.apiKeys.permissions.notificationSend'), description: t('admin.extension.apiKeys.permissions.notificationSendDesc') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('developer.extension.apiKeys.permissions.groups.app'),
|
||||
name: t('admin.extension.apiKeys.permissions.groups.app'),
|
||||
permissions: [
|
||||
{ value: 'app.info', label: t('developer.extension.apiKeys.permissions.appInfo'), description: t('developer.extension.apiKeys.permissions.appInfoDesc') },
|
||||
{ value: 'app.stats', label: t('developer.extension.apiKeys.permissions.appStats'), description: t('developer.extension.apiKeys.permissions.appStatsDesc') },
|
||||
{ value: 'app.variables', label: t('developer.extension.apiKeys.permissions.appVariables'), description: t('developer.extension.apiKeys.permissions.appVariablesDesc') },
|
||||
{ value: 'app.info', label: t('admin.extension.apiKeys.permissions.appInfo'), description: t('admin.extension.apiKeys.permissions.appInfoDesc') },
|
||||
{ value: 'app.stats', label: t('admin.extension.apiKeys.permissions.appStats'), description: t('admin.extension.apiKeys.permissions.appStatsDesc') },
|
||||
{ value: 'app.variables', label: t('admin.extension.apiKeys.permissions.appVariables'), description: t('admin.extension.apiKeys.permissions.appVariablesDesc') },
|
||||
],
|
||||
},
|
||||
])
|
||||
@@ -113,15 +113,15 @@ function deselectAllInGroup(permissions: { value: string }[]) {
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.value.name) {
|
||||
toast.error(t('developer.extension.apiKeys.create.nameRequired'))
|
||||
toast.error(t('admin.extension.apiKeys.create.nameRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.application_id) {
|
||||
toast.error(t('developer.extension.apiKeys.create.selectAppRequired'))
|
||||
toast.error(t('admin.extension.apiKeys.create.selectAppRequired'))
|
||||
return
|
||||
}
|
||||
if (form.value.permissions.length === 0) {
|
||||
toast.error(t('developer.extension.apiKeys.create.permissionsRequired'))
|
||||
toast.error(t('admin.extension.apiKeys.create.permissionsRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -133,12 +133,12 @@ async function handleSubmit() {
|
||||
permissions: form.value.permissions,
|
||||
enabled: form.value.enabled,
|
||||
})
|
||||
toast.success(t('developer.extension.apiKeys.create.submitSuccess'))
|
||||
router.push('/developer/extension')
|
||||
toast.success(t('admin.extension.apiKeys.create.submitSuccess'))
|
||||
router.push('/admin/extension')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('创建API密钥失败:', error)
|
||||
toast.error(error.message || t('developer.extension.apiKeys.create.submitFailed'))
|
||||
toast.error(error.message || t('admin.extension.apiKeys.create.submitFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
@@ -152,11 +152,11 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.extension.apiKeys.create.title')"
|
||||
:description="t('developer.extension.apiKeys.create.description')"
|
||||
:title="t('admin.extension.apiKeys.create.title')"
|
||||
:description="t('admin.extension.apiKeys.create.description')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.extension.title'), href: '/developer/extension' },
|
||||
{ title: t('developer.extension.apiKeys.create.title') },
|
||||
{ title: t('admin.extension.title'), href: '/admin/extension' },
|
||||
{ title: t('admin.extension.apiKeys.create.title') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
@@ -167,29 +167,29 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.extension.apiKeys.create.title') }}
|
||||
{{ t('admin.extension.apiKeys.create.title') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.extension.apiKeys.create.description') }}</UiCardDescription>
|
||||
<UiCardDescription>{{ t('admin.extension.apiKeys.create.description') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="name">
|
||||
{{ t('developer.extension.apiKeys.create.name') }} *
|
||||
{{ t('admin.extension.apiKeys.create.name') }} *
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
:placeholder="t('developer.extension.apiKeys.create.namePlaceholder')"
|
||||
:placeholder="t('admin.extension.apiKeys.create.namePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="application">
|
||||
{{ t('developer.extension.apiKeys.create.application') }} *
|
||||
{{ t('admin.extension.apiKeys.create.application') }} *
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.application_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.extension.apiKeys.create.selectAppPlaceholder')" />
|
||||
<UiSelectValue :placeholder="t('admin.extension.apiKeys.create.selectAppPlaceholder')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="app in filteredApplications" :key="app.value" :value="app.value">
|
||||
@@ -201,13 +201,13 @@ onMounted(() => {
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<UiLabel>{{ t('developer.extension.apiKeys.create.permissions') }} *</UiLabel>
|
||||
<UiLabel>{{ t('admin.extension.apiKeys.create.permissions') }} *</UiLabel>
|
||||
<div class="flex gap-2">
|
||||
<UiButton variant="outline" size="sm" @click="form.permissions = permissionGroups.flatMap(g => g.permissions.map(p => p.value))">
|
||||
{{ t('developer.extension.apiKeys.create.selectAll') }}
|
||||
{{ t('admin.extension.apiKeys.create.selectAll') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="form.permissions = []">
|
||||
{{ t('developer.extension.apiKeys.create.deselectAll') }}
|
||||
{{ t('admin.extension.apiKeys.create.deselectAll') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -219,10 +219,10 @@ onMounted(() => {
|
||||
</h4>
|
||||
<div class="flex gap-1">
|
||||
<UiButton variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="selectAllInGroup(group.permissions)">
|
||||
{{ t('developer.extension.apiKeys.create.selectAll') }}
|
||||
{{ t('admin.extension.apiKeys.create.selectAll') }}
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="deselectAllInGroup(group.permissions)">
|
||||
{{ t('developer.extension.apiKeys.create.deselectAll') }}
|
||||
{{ t('admin.extension.apiKeys.create.deselectAll') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,7 +255,7 @@ onMounted(() => {
|
||||
<div class="flex items-center space-x-2">
|
||||
<UiSwitch id="enabled" v-model:checked="form.enabled" />
|
||||
<UiLabel for="enabled">
|
||||
{{ t('developer.extension.apiKeys.create.enabled') }}
|
||||
{{ t('admin.extension.apiKeys.create.enabled') }}
|
||||
</UiLabel>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
@@ -267,22 +267,22 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:info" class="size-5" />
|
||||
{{ t('developer.extension.apiKeys.create.preview') || '预览' }}
|
||||
{{ t('admin.extension.apiKeys.create.preview') || '预览' }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.extension.apiKeys.create.name') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.extension.apiKeys.create.name') }}</span>
|
||||
<span>{{ form.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.extension.apiKeys.create.application') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.extension.apiKeys.create.application') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.extension.apiKeys.create.permissions') }}</span>
|
||||
<span>{{ form.permissions.length }} {{ t('developer.extension.apiKeys.create.selected') || '已选择' }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.extension.apiKeys.create.permissions') }}</span>
|
||||
<span>{{ form.permissions.length }} {{ t('admin.extension.apiKeys.create.selected') || '已选择' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
@@ -299,14 +299,14 @@ onMounted(() => {
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.extension.apiKeys.create.submitBtn') }}
|
||||
{{ t('admin.extension.apiKeys.create.submitBtn') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.extension.apiKeys.create.cancelBtn') }}
|
||||
{{ t('admin.extension.apiKeys.create.cancelBtn') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
|
||||
import { Ban, MoreHorizontal, RefreshCw, Trash2 } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { APIKey } from '@/pages/developer/extension/data/schema'
|
||||
import type { APIKey } from '@/pages/admin/extension/data/schema'
|
||||
|
||||
import { Copy } from '@/components/sva-ui/copy'
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
@@ -25,7 +25,7 @@ export function getAPIKeyColumns(actions: {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: () => t('developer.extension.apiKeys.columns.name'),
|
||||
header: () => t('admin.extension.apiKeys.columns.name'),
|
||||
cell: ({ row }) => {
|
||||
const name = row.getValue('name') as string
|
||||
return name || '-'
|
||||
@@ -33,7 +33,7 @@ export function getAPIKeyColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'application_name',
|
||||
header: () => t('developer.extension.apiKeys.columns.application'),
|
||||
header: () => t('admin.extension.apiKeys.columns.application'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.getValue('application_name') as string
|
||||
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
|
||||
@@ -41,7 +41,7 @@ export function getAPIKeyColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'access_key',
|
||||
header: () => t('developer.extension.apiKeys.columns.accessKey'),
|
||||
header: () => t('admin.extension.apiKeys.columns.accessKey'),
|
||||
cell: ({ row }) => {
|
||||
const accessKey = row.getValue('access_key') as string
|
||||
return accessKey
|
||||
@@ -54,7 +54,7 @@ export function getAPIKeyColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'secret_key',
|
||||
header: () => t('developer.extension.apiKeys.columns.secretKey'),
|
||||
header: () => t('admin.extension.apiKeys.columns.secretKey'),
|
||||
cell: ({ row }) => {
|
||||
const secretKey = row.getValue('secret_key') as string
|
||||
return secretKey ? h('code', { class: 'text-xs bg-muted px-2 py-1 rounded font-mono' }, '••••••••••••••••') : '-'
|
||||
@@ -62,7 +62,7 @@ export function getAPIKeyColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'permissions',
|
||||
header: () => t('developer.extension.apiKeys.columns.permissions'),
|
||||
header: () => t('admin.extension.apiKeys.columns.permissions'),
|
||||
cell: ({ row }) => {
|
||||
const permissions = row.getValue('permissions') as string
|
||||
try {
|
||||
@@ -78,16 +78,16 @@ export function getAPIKeyColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => t('developer.extension.apiKeys.columns.status'),
|
||||
header: () => t('admin.extension.apiKeys.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
return h(Badge, { variant: status === 'active' ? 'default' : 'secondary' }, () =>
|
||||
status === 'active' ? t('developer.extension.enabled') : t('developer.extension.disabled'))
|
||||
status === 'active' ? t('admin.extension.enabled') : t('admin.extension.disabled'))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.extension.apiKeys.columns.createdAt'),
|
||||
header: () => t('admin.extension.apiKeys.columns.createdAt'),
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue('created_at')
|
||||
if (!createdAt)
|
||||
@@ -132,16 +132,16 @@ export function getAPIKeyColumns(actions: {
|
||||
() => [
|
||||
h(DropdownMenuItem, { onClick: () => actions.onRegenerate(apiKey) }, () => [
|
||||
h(RefreshCw, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.extension.apiKeys.regenerate'),
|
||||
t('admin.extension.apiKeys.regenerate'),
|
||||
]),
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(apiKey) }, () => [
|
||||
h(Ban, { class: 'mr-2 h-4 w-4' }),
|
||||
isDisabled ? t('developer.extension.apiKeys.enable') : t('developer.extension.apiKeys.disable'),
|
||||
isDisabled ? t('admin.extension.apiKeys.enable') : t('admin.extension.apiKeys.disable'),
|
||||
]),
|
||||
h(DropdownMenuSeparator),
|
||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(apiKey) }, () => [
|
||||
h(Trash2, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.extension.apiKeys.delete'),
|
||||
t('admin.extension.apiKeys.delete'),
|
||||
]),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -33,7 +33,7 @@ function resetFilters() {
|
||||
<template>
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
:placeholder="t('developer.extension.apiKeys.searchAPIKey')"
|
||||
:placeholder="t('admin.extension.apiKeys.searchAPIKey')"
|
||||
:model-value="searchFilter"
|
||||
class="h-8 w-[150px] lg:w-[250px]"
|
||||
@update:model-value="emit('update:searchFilter', $event as string)"
|
||||
@@ -45,7 +45,7 @@ function resetFilters() {
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="resetFilters"
|
||||
>
|
||||
{{ t('developer.extension.apiKeys.reset') }}
|
||||
{{ t('admin.extension.apiKeys.reset') }}
|
||||
<X class="size-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -48,14 +48,14 @@ const table = generateVueTable<APIKey>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.extension.apiKeys.select',
|
||||
name: 'developer.extension.apiKeys.columns.name',
|
||||
application_name: 'developer.extension.apiKeys.columns.application',
|
||||
access_key: 'developer.extension.apiKeys.columns.accessKey',
|
||||
secret_key: 'developer.extension.apiKeys.columns.secretKey',
|
||||
permissions: 'developer.extension.apiKeys.columns.permissions',
|
||||
status: 'developer.extension.apiKeys.columns.status',
|
||||
created_at: 'developer.extension.apiKeys.columns.createdAt',
|
||||
select: 'admin.extension.apiKeys.select',
|
||||
name: 'admin.extension.apiKeys.columns.name',
|
||||
application_name: 'admin.extension.apiKeys.columns.application',
|
||||
access_key: 'admin.extension.apiKeys.columns.accessKey',
|
||||
secret_key: 'admin.extension.apiKeys.columns.secretKey',
|
||||
permissions: 'admin.extension.apiKeys.columns.permissions',
|
||||
status: 'admin.extension.apiKeys.columns.status',
|
||||
created_at: 'admin.extension.apiKeys.columns.createdAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -69,13 +69,13 @@ defineExpose({
|
||||
<div class="space-y-3">
|
||||
<BulkActions :table="table" entity-name="apiKeys">
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
|
||||
{{ t('developer.extension.apiKeys.enable') }}
|
||||
{{ t('admin.extension.apiKeys.enable') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
|
||||
{{ t('developer.extension.apiKeys.disable') }}
|
||||
{{ t('admin.extension.apiKeys.disable') }}
|
||||
</UiButton>
|
||||
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||
{{ t('developer.extension.apiKeys.batchDelete') }}
|
||||
{{ t('admin.extension.apiKeys.batchDelete') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
|
||||
@@ -25,7 +25,7 @@ export function getWebhookColumns(actions: {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: () => t('developer.extension.webhooks.columns.name'),
|
||||
header: () => t('admin.extension.webhooks.columns.name'),
|
||||
cell: ({ row }) => {
|
||||
const name = row.getValue('name') as string
|
||||
return name || '-'
|
||||
@@ -33,7 +33,7 @@ export function getWebhookColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'url',
|
||||
header: () => t('developer.extension.webhooks.columns.url'),
|
||||
header: () => t('admin.extension.webhooks.columns.url'),
|
||||
cell: ({ row }) => {
|
||||
const url = row.getValue('url') as string
|
||||
return url ? h('code', { class: 'text-xs bg-muted px-2 py-1 rounded font-mono max-w-[200px] truncate inline-block' }, url) : '-'
|
||||
@@ -41,7 +41,7 @@ export function getWebhookColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'application_name',
|
||||
header: () => t('developer.extension.webhooks.columns.application'),
|
||||
header: () => t('admin.extension.webhooks.columns.application'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.getValue('application_name') as string
|
||||
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
|
||||
@@ -49,7 +49,7 @@ export function getWebhookColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'events',
|
||||
header: () => t('developer.extension.webhooks.columns.events'),
|
||||
header: () => t('admin.extension.webhooks.columns.events'),
|
||||
cell: ({ row }) => {
|
||||
const events = row.getValue('events') as string
|
||||
try {
|
||||
@@ -65,16 +65,16 @@ export function getWebhookColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => t('developer.extension.webhooks.columns.status'),
|
||||
header: () => t('admin.extension.webhooks.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
return h(Badge, { variant: status === 'active' ? 'default' : 'secondary' }, () =>
|
||||
status === 'active' ? t('developer.extension.enabled') : t('developer.extension.disabled'))
|
||||
status === 'active' ? t('admin.extension.enabled') : t('admin.extension.disabled'))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'retry_count',
|
||||
header: () => t('developer.extension.webhooks.columns.retryCount'),
|
||||
header: () => t('admin.extension.webhooks.columns.retryCount'),
|
||||
cell: ({ row }) => {
|
||||
const retryCount = row.getValue('retry_count') as number
|
||||
return retryCount ?? '-'
|
||||
@@ -82,7 +82,7 @@ export function getWebhookColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'timeout',
|
||||
header: () => t('developer.extension.webhooks.columns.timeout'),
|
||||
header: () => t('admin.extension.webhooks.columns.timeout'),
|
||||
cell: ({ row }) => {
|
||||
const timeout = row.getValue('timeout') as number
|
||||
return timeout ? `${timeout}s` : '-'
|
||||
@@ -90,7 +90,7 @@ export function getWebhookColumns(actions: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.extension.webhooks.columns.createdAt'),
|
||||
header: () => t('admin.extension.webhooks.columns.createdAt'),
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue('created_at')
|
||||
if (!createdAt)
|
||||
@@ -135,22 +135,22 @@ export function getWebhookColumns(actions: {
|
||||
() => [
|
||||
h(DropdownMenuItem, { onClick: () => actions.onTest(webhook) }, () => [
|
||||
h(Send, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.extension.webhooks.test'),
|
||||
t('admin.extension.webhooks.test'),
|
||||
]),
|
||||
actions.onViewLogs
|
||||
? h(DropdownMenuItem, { onClick: () => actions.onViewLogs?.(webhook) }, () => [
|
||||
h(Eye, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.extension.webhooks.viewLogs'),
|
||||
t('admin.extension.webhooks.viewLogs'),
|
||||
])
|
||||
: null,
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(webhook) }, () => [
|
||||
h(Ban, { class: 'mr-2 h-4 w-4' }),
|
||||
isDisabled ? t('developer.extension.webhooks.enable') : t('developer.extension.webhooks.disable'),
|
||||
isDisabled ? t('admin.extension.webhooks.enable') : t('admin.extension.webhooks.disable'),
|
||||
]),
|
||||
h(DropdownMenuSeparator),
|
||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(webhook) }, () => [
|
||||
h(Trash2, { class: 'mr-2 h-4 w-4' }),
|
||||
t('developer.extension.webhooks.delete'),
|
||||
t('admin.extension.webhooks.delete'),
|
||||
]),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -33,7 +33,7 @@ function resetFilters() {
|
||||
<template>
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
:placeholder="t('developer.extension.webhooks.searchWebhook')"
|
||||
:placeholder="t('admin.extension.webhooks.searchWebhook')"
|
||||
:model-value="searchFilter"
|
||||
class="h-8 w-[150px] lg:w-[250px]"
|
||||
@update:model-value="emit('update:searchFilter', $event as string)"
|
||||
@@ -45,7 +45,7 @@ function resetFilters() {
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="resetFilters"
|
||||
>
|
||||
{{ t('developer.extension.webhooks.reset') }}
|
||||
{{ t('admin.extension.webhooks.reset') }}
|
||||
<X class="size-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -48,15 +48,15 @@ const table = generateVueTable<Webhook>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
select: 'developer.extension.webhooks.select',
|
||||
name: 'developer.extension.webhooks.columns.name',
|
||||
url: 'developer.extension.webhooks.columns.url',
|
||||
application_name: 'developer.extension.webhooks.columns.application',
|
||||
events: 'developer.extension.webhooks.columns.events',
|
||||
status: 'developer.extension.webhooks.columns.status',
|
||||
retry_count: 'developer.extension.webhooks.columns.retryCount',
|
||||
timeout: 'developer.extension.webhooks.columns.timeout',
|
||||
created_at: 'developer.extension.webhooks.columns.createdAt',
|
||||
select: 'admin.extension.webhooks.select',
|
||||
name: 'admin.extension.webhooks.columns.name',
|
||||
url: 'admin.extension.webhooks.columns.url',
|
||||
application_name: 'admin.extension.webhooks.columns.application',
|
||||
events: 'admin.extension.webhooks.columns.events',
|
||||
status: 'admin.extension.webhooks.columns.status',
|
||||
retry_count: 'admin.extension.webhooks.columns.retryCount',
|
||||
timeout: 'admin.extension.webhooks.columns.timeout',
|
||||
created_at: 'admin.extension.webhooks.columns.createdAt',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -70,13 +70,13 @@ defineExpose({
|
||||
<div class="space-y-3">
|
||||
<BulkActions :table="table" entity-name="webhooks">
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
|
||||
{{ t('developer.extension.webhooks.enable') }}
|
||||
{{ t('admin.extension.webhooks.enable') }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
|
||||
{{ t('developer.extension.webhooks.disable') }}
|
||||
{{ t('admin.extension.webhooks.disable') }}
|
||||
</UiButton>
|
||||
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||
{{ t('developer.extension.webhooks.batchDelete') }}
|
||||
{{ t('admin.extension.webhooks.batchDelete') }}
|
||||
</UiButton>
|
||||
</BulkActions>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
|
||||
@@ -5,14 +5,14 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { APIKey, Webhook } from '@/pages/developer/extension/data/schema'
|
||||
import type { APIKey, Webhook } from '@/pages/admin/extension/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import APIKeyDataTable from '@/pages/developer/extension/components/apikey-data-table.vue'
|
||||
import WebhookDataTable from '@/pages/developer/extension/components/webhook-data-table.vue'
|
||||
import APIKeyDataTable from '@/pages/admin/extension/components/apikey-data-table.vue'
|
||||
import WebhookDataTable from '@/pages/admin/extension/components/webhook-data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -134,8 +134,8 @@ const applicationOptions = computed(() => {
|
||||
})
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('developer.extension.enabled'), value: 'active' },
|
||||
{ label: t('developer.extension.disabled'), value: 'inactive' },
|
||||
{ label: t('admin.extension.enabled'), value: 'active' },
|
||||
{ label: t('admin.extension.disabled'), value: 'inactive' },
|
||||
])
|
||||
|
||||
async function fetchApplications() {
|
||||
@@ -179,23 +179,23 @@ async function fetchAPIKeys() {
|
||||
}
|
||||
|
||||
function goToCreateWebhook() {
|
||||
router.push('/developer/extension/webhooks/create')
|
||||
router.push('/admin/extension/webhooks/create')
|
||||
}
|
||||
|
||||
function goToCreateAPIKey() {
|
||||
router.push('/developer/extension/api-keys/create')
|
||||
router.push('/admin/extension/api-keys/create')
|
||||
}
|
||||
|
||||
async function toggleWebhookStatus(webhook: Webhook) {
|
||||
try {
|
||||
const newStatus = webhook.status === 'active' ? 'inactive' : 'active'
|
||||
await api.put(`/dev/extension/webhooks/${webhook.id}/status`, { status: newStatus })
|
||||
toast.success(t('developer.extension.webhooks.statusUpdateSuccess'))
|
||||
toast.success(t('admin.extension.webhooks.statusUpdateSuccess'))
|
||||
fetchWebhooks()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('切换Webhook状态失败:', error)
|
||||
toast.error(error.message || t('developer.extension.webhooks.statusUpdateFailed'))
|
||||
toast.error(error.message || t('admin.extension.webhooks.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,23 +203,23 @@ async function toggleAPIKeyStatus(apiKey: APIKey) {
|
||||
try {
|
||||
const newStatus = apiKey.status === 'active' ? 'inactive' : 'active'
|
||||
await api.put(`/dev/extension/api-keys/${apiKey.id}/status`, { status: newStatus })
|
||||
toast.success(t('developer.extension.apiKeys.statusUpdateSuccess'))
|
||||
toast.success(t('admin.extension.apiKeys.statusUpdateSuccess'))
|
||||
fetchAPIKeys()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('切换API密钥状态失败:', error)
|
||||
toast.error(error.message || t('developer.extension.apiKeys.statusUpdateFailed'))
|
||||
toast.error(error.message || t('admin.extension.apiKeys.statusUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
async function testWebhook(webhook: Webhook) {
|
||||
try {
|
||||
await api.post(`/dev/extension/webhooks/${webhook.id}/test`, {})
|
||||
toast.success(t('developer.extension.webhooks.testSuccess'))
|
||||
toast.success(t('admin.extension.webhooks.testSuccess'))
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('测试Webhook失败:', error)
|
||||
toast.error(error.message || t('developer.extension.webhooks.testFailed'))
|
||||
toast.error(error.message || t('admin.extension.webhooks.testFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,12 +234,12 @@ async function handleRegenerateAPIKey() {
|
||||
|
||||
try {
|
||||
await api.post(`/dev/extension/api-keys/${regenerateTarget.value.id}/regenerate`, {})
|
||||
toast.success(t('developer.extension.apiKeys.regenerateSuccess'))
|
||||
toast.success(t('admin.extension.apiKeys.regenerateSuccess'))
|
||||
fetchAPIKeys()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('重新生成API密钥失败:', error)
|
||||
toast.error(error.message || t('developer.extension.apiKeys.regenerateFailed'))
|
||||
toast.error(error.message || t('admin.extension.apiKeys.regenerateFailed'))
|
||||
}
|
||||
finally {
|
||||
regenerateTarget.value = null
|
||||
@@ -265,18 +265,18 @@ async function handleDelete() {
|
||||
try {
|
||||
if (deleteType.value === 'webhook') {
|
||||
await api.delete(`/dev/extension/webhooks/${deleteTarget.value.id}`)
|
||||
toast.success(t('developer.extension.webhooks.deleteSuccess'))
|
||||
toast.success(t('admin.extension.webhooks.deleteSuccess'))
|
||||
fetchWebhooks()
|
||||
}
|
||||
else {
|
||||
await api.delete(`/dev/extension/api-keys/${deleteTarget.value.id}`)
|
||||
toast.success(t('developer.extension.apiKeys.deleteSuccess'))
|
||||
toast.success(t('admin.extension.apiKeys.deleteSuccess'))
|
||||
fetchAPIKeys()
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('删除失败:', error)
|
||||
toast.error(error.message || t('developer.extension.deleteFailed'))
|
||||
toast.error(error.message || t('admin.extension.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
deleteTarget.value = null
|
||||
@@ -293,18 +293,18 @@ async function handleBatchDelete() {
|
||||
try {
|
||||
if (deleteType.value === 'webhook') {
|
||||
await api.delete('/dev/extension/webhooks/batch', { ids: batchDeleteIds.value } as any)
|
||||
toast.success(t('developer.extension.webhooks.deleteSuccess'))
|
||||
toast.success(t('admin.extension.webhooks.deleteSuccess'))
|
||||
fetchWebhooks()
|
||||
}
|
||||
else {
|
||||
await api.delete('/dev/extension/api-keys/batch', { ids: batchDeleteIds.value } as any)
|
||||
toast.success(t('developer.extension.apiKeys.deleteSuccess'))
|
||||
toast.success(t('admin.extension.apiKeys.deleteSuccess'))
|
||||
fetchAPIKeys()
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量删除失败:', error)
|
||||
toast.error(error.message || t('developer.extension.deleteFailed'))
|
||||
toast.error(error.message || t('admin.extension.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
batchDeleteIds.value = []
|
||||
@@ -314,24 +314,24 @@ async function handleBatchDelete() {
|
||||
async function batchToggleWebhookStatus(ids: (string | number)[], status: string) {
|
||||
try {
|
||||
await api.put('/dev/extension/webhooks/batch/status', { ids, status })
|
||||
toast.success(t('developer.extension.webhooks.batchUpdateSuccess'))
|
||||
toast.success(t('admin.extension.webhooks.batchUpdateSuccess'))
|
||||
fetchWebhooks()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量更新Webhook状态失败:', error)
|
||||
toast.error(error.message || t('developer.extension.webhooks.batchUpdateFailed'))
|
||||
toast.error(error.message || t('admin.extension.webhooks.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
async function batchToggleAPIKeyStatus(ids: (string | number)[], status: string) {
|
||||
try {
|
||||
await api.put('/dev/extension/api-keys/batch/status', { ids, status })
|
||||
toast.success(t('developer.extension.apiKeys.batchUpdateSuccess'))
|
||||
toast.success(t('admin.extension.apiKeys.batchUpdateSuccess'))
|
||||
fetchAPIKeys()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量更新API密钥状态失败:', error)
|
||||
toast.error(error.message || t('developer.extension.apiKeys.batchUpdateFailed'))
|
||||
toast.error(error.message || t('admin.extension.apiKeys.batchUpdateFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,18 +344,18 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.extension.title')"
|
||||
:description="t('developer.extension.description')"
|
||||
:title="t('admin.extension.title')"
|
||||
:description="t('admin.extension.description')"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton v-if="currentTab === 'webhooks'" @click="goToCreateWebhook">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.extension.webhooks.createBtn') }}
|
||||
{{ t('admin.extension.webhooks.createBtn') }}
|
||||
</UiButton>
|
||||
<UiButton v-else @click="goToCreateAPIKey">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.extension.apiKeys.createBtn') }}
|
||||
{{ t('admin.extension.apiKeys.createBtn') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -364,7 +364,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.extension.totalWebhooks') }}
|
||||
{{ t('admin.extension.totalWebhooks') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:webhook" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -378,7 +378,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.extension.totalAPIKeys') }}
|
||||
{{ t('admin.extension.totalAPIKeys') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:key" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -392,7 +392,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.extension.enabled') }}
|
||||
{{ t('admin.extension.enabled') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -406,7 +406,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.extension.disabled') }}
|
||||
{{ t('admin.extension.disabled') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:ban" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -422,11 +422,11 @@ onMounted(() => {
|
||||
<UiTabsList class="grid w-full grid-cols-2">
|
||||
<UiTabsTrigger value="webhooks">
|
||||
<Icon icon="lucide:webhook" class="h-4 w-4 mr-2" />
|
||||
{{ t('developer.extension.webhooks.title') }}
|
||||
{{ t('admin.extension.webhooks.title') }}
|
||||
</UiTabsTrigger>
|
||||
<UiTabsTrigger value="apikeys">
|
||||
<Icon icon="lucide:key" class="h-4 w-4 mr-2" />
|
||||
{{ t('developer.extension.apiKeys.title') }}
|
||||
{{ t('admin.extension.apiKeys.title') }}
|
||||
</UiTabsTrigger>
|
||||
</UiTabsList>
|
||||
|
||||
@@ -450,12 +450,12 @@ onMounted(() => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.extension.application')"
|
||||
:title="t('admin.extension.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('developer.extension.status')"
|
||||
:title="t('admin.extension.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
@@ -488,12 +488,12 @@ onMounted(() => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.extension.application')"
|
||||
:title="t('admin.extension.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('developer.extension.status')"
|
||||
:title="t('admin.extension.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
@@ -511,17 +511,17 @@ onMounted(() => {
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.extension.delete')"
|
||||
:cancel-button-text="t('developer.extension.cancel')"
|
||||
:confirm-button-text="t('admin.extension.delete')"
|
||||
:cancel-button-text="t('admin.extension.cancel')"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ deleteType === 'webhook' ? t('developer.extension.webhooks.deleteWebhook') : t('developer.extension.apiKeys.deleteAPIKey') }}
|
||||
{{ deleteType === 'webhook' ? t('admin.extension.webhooks.deleteWebhook') : t('admin.extension.apiKeys.deleteAPIKey') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ deleteType === 'webhook'
|
||||
? t('developer.extension.webhooks.deleteConfirm', { webhookName: (deleteTarget as Webhook)?.name })
|
||||
: t('developer.extension.apiKeys.deleteConfirm', { apiKeyName: (deleteTarget as APIKey)?.name })
|
||||
? t('admin.extension.webhooks.deleteConfirm', { webhookName: (deleteTarget as Webhook)?.name })
|
||||
: t('admin.extension.apiKeys.deleteConfirm', { apiKeyName: (deleteTarget as APIKey)?.name })
|
||||
}}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
@@ -529,31 +529,31 @@ onMounted(() => {
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.extension.delete')"
|
||||
:cancel-button-text="t('developer.extension.cancel')"
|
||||
:confirm-button-text="t('admin.extension.delete')"
|
||||
:cancel-button-text="t('admin.extension.cancel')"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ deleteType === 'webhook' ? t('developer.extension.webhooks.batchDelete') : t('developer.extension.apiKeys.batchDelete') }}
|
||||
{{ deleteType === 'webhook' ? t('admin.extension.webhooks.batchDelete') : t('admin.extension.apiKeys.batchDelete') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ deleteType === 'webhook'
|
||||
? t('developer.extension.webhooks.batchDeleteConfirm', { count: batchDeleteIds.length })
|
||||
: t('developer.extension.apiKeys.batchDeleteConfirm', { count: batchDeleteIds.length })
|
||||
? t('admin.extension.webhooks.batchDeleteConfirm', { count: batchDeleteIds.length })
|
||||
: t('admin.extension.apiKeys.batchDeleteConfirm', { count: batchDeleteIds.length })
|
||||
}}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
<ConfirmDialog
|
||||
v-model:open="regenerateDialogOpen"
|
||||
:confirm-button-text="t('developer.extension.apiKeys.regenerate')"
|
||||
:cancel-button-text="t('developer.extension.cancel')"
|
||||
:confirm-button-text="t('admin.extension.apiKeys.regenerate')"
|
||||
:cancel-button-text="t('admin.extension.cancel')"
|
||||
@confirm="handleRegenerateAPIKey"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.extension.apiKeys.regenerate') }}
|
||||
{{ t('admin.extension.apiKeys.regenerate') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.extension.apiKeys.regenerateConfirm') }}
|
||||
{{ t('admin.extension.apiKeys.regenerateConfirm') }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
|
||||
@@ -31,13 +31,13 @@ const form = ref({
|
||||
})
|
||||
|
||||
const availableEvents = computed(() => [
|
||||
{ value: 'user.registered', label: t('developer.extension.webhooks.events.userRegistered') },
|
||||
{ value: 'user.login', label: t('developer.extension.webhooks.events.userLogin') },
|
||||
{ value: 'user.recharged', label: t('developer.extension.webhooks.events.userRecharged') },
|
||||
{ value: 'user.expired', label: t('developer.extension.webhooks.events.userExpired') },
|
||||
{ value: 'card.used', label: t('developer.extension.webhooks.events.cardUsed') },
|
||||
{ value: 'card.expired', label: t('developer.extension.webhooks.events.cardExpired') },
|
||||
{ value: 'abnormal.detected', label: t('developer.extension.webhooks.events.abnormalDetected') },
|
||||
{ value: 'user.registered', label: t('admin.extension.webhooks.events.userRegistered') },
|
||||
{ value: 'user.login', label: t('admin.extension.webhooks.events.userLogin') },
|
||||
{ value: 'user.recharged', label: t('admin.extension.webhooks.events.userRecharged') },
|
||||
{ value: 'user.expired', label: t('admin.extension.webhooks.events.userExpired') },
|
||||
{ value: 'card.used', label: t('admin.extension.webhooks.events.cardUsed') },
|
||||
{ value: 'card.expired', label: t('admin.extension.webhooks.events.cardExpired') },
|
||||
{ value: 'abnormal.detected', label: t('admin.extension.webhooks.events.abnormalDetected') },
|
||||
])
|
||||
|
||||
const filteredApplications = computed(() => {
|
||||
@@ -76,19 +76,19 @@ function toggleEvent(event: string) {
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.value.name) {
|
||||
toast.error(t('developer.extension.webhooks.create.nameRequired'))
|
||||
toast.error(t('admin.extension.webhooks.create.nameRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.application_id) {
|
||||
toast.error(t('developer.extension.webhooks.create.selectAppRequired'))
|
||||
toast.error(t('admin.extension.webhooks.create.selectAppRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.url) {
|
||||
toast.error(t('developer.extension.webhooks.create.urlRequired'))
|
||||
toast.error(t('admin.extension.webhooks.create.urlRequired'))
|
||||
return
|
||||
}
|
||||
if (form.value.events.length === 0) {
|
||||
toast.error(t('developer.extension.webhooks.create.eventsRequired'))
|
||||
toast.error(t('admin.extension.webhooks.create.eventsRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,12 +104,12 @@ async function handleSubmit() {
|
||||
timeout: form.value.timeout,
|
||||
enabled: form.value.enabled,
|
||||
})
|
||||
toast.success(t('developer.extension.webhooks.create.submitSuccess'))
|
||||
router.push('/developer/extension')
|
||||
toast.success(t('admin.extension.webhooks.create.submitSuccess'))
|
||||
router.push('/admin/extension')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('创建Webhook失败:', error)
|
||||
toast.error(error.message || t('developer.extension.webhooks.create.submitFailed'))
|
||||
toast.error(error.message || t('admin.extension.webhooks.create.submitFailed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
@@ -123,11 +123,11 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.extension.webhooks.create.title')"
|
||||
:description="t('developer.extension.webhooks.create.description')"
|
||||
:title="t('admin.extension.webhooks.create.title')"
|
||||
:description="t('admin.extension.webhooks.create.description')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('developer.extension.title'), href: '/developer/extension' },
|
||||
{ title: t('developer.extension.webhooks.create.title') },
|
||||
{ title: t('admin.extension.title'), href: '/admin/extension' },
|
||||
{ title: t('admin.extension.webhooks.create.title') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
@@ -138,29 +138,29 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:settings-2" class="size-5" />
|
||||
{{ t('developer.extension.webhooks.create.title') }}
|
||||
{{ t('admin.extension.webhooks.create.title') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.extension.webhooks.create.description') }}</UiCardDescription>
|
||||
<UiCardDescription>{{ t('admin.extension.webhooks.create.description') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="name">
|
||||
{{ t('developer.extension.webhooks.create.name') }} *
|
||||
{{ t('admin.extension.webhooks.create.name') }} *
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
:placeholder="t('developer.extension.webhooks.create.namePlaceholder')"
|
||||
:placeholder="t('admin.extension.webhooks.create.namePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="application">
|
||||
{{ t('developer.extension.webhooks.create.application') }} *
|
||||
{{ t('admin.extension.webhooks.create.application') }} *
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.application_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.extension.webhooks.create.selectAppPlaceholder')" />
|
||||
<UiSelectValue :placeholder="t('admin.extension.webhooks.create.selectAppPlaceholder')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="app in filteredApplications" :key="app.value" :value="app.value">
|
||||
@@ -172,28 +172,28 @@ onMounted(() => {
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="url">
|
||||
{{ t('developer.extension.webhooks.create.url') }} *
|
||||
{{ t('admin.extension.webhooks.create.url') }} *
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="url"
|
||||
v-model="form.url"
|
||||
:placeholder="t('developer.extension.webhooks.create.urlPlaceholder')"
|
||||
:placeholder="t('admin.extension.webhooks.create.urlPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="secret_key">
|
||||
{{ t('developer.extension.webhooks.create.secretKey') }}
|
||||
{{ t('admin.extension.webhooks.create.secretKey') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="secret_key"
|
||||
v-model="form.secret_key"
|
||||
:placeholder="t('developer.extension.webhooks.create.secretKeyPlaceholder')"
|
||||
:placeholder="t('admin.extension.webhooks.create.secretKeyPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('developer.extension.webhooks.create.events') }} *</UiLabel>
|
||||
<UiLabel>{{ t('admin.extension.webhooks.create.events') }} *</UiLabel>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div
|
||||
v-for="event in availableEvents"
|
||||
@@ -214,7 +214,7 @@ onMounted(() => {
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="retry_count">
|
||||
{{ t('developer.extension.webhooks.create.retryCount') }}
|
||||
{{ t('admin.extension.webhooks.create.retryCount') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="retry_count"
|
||||
@@ -222,13 +222,13 @@ onMounted(() => {
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
:placeholder="t('developer.extension.webhooks.create.retryCountPlaceholder')"
|
||||
:placeholder="t('admin.extension.webhooks.create.retryCountPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="timeout">
|
||||
{{ t('developer.extension.webhooks.create.timeout') }}
|
||||
{{ t('admin.extension.webhooks.create.timeout') }}
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="timeout"
|
||||
@@ -236,7 +236,7 @@ onMounted(() => {
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
:placeholder="t('developer.extension.webhooks.create.timeoutPlaceholder')"
|
||||
:placeholder="t('admin.extension.webhooks.create.timeoutPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -244,7 +244,7 @@ onMounted(() => {
|
||||
<div class="flex items-center space-x-2">
|
||||
<UiSwitch id="enabled" v-model:checked="form.enabled" />
|
||||
<UiLabel for="enabled">
|
||||
{{ t('developer.extension.webhooks.create.enabled') }}
|
||||
{{ t('admin.extension.webhooks.create.enabled') }}
|
||||
</UiLabel>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
@@ -256,29 +256,29 @@ onMounted(() => {
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:info" class="size-5" />
|
||||
{{ t('developer.extension.webhooks.create.preview') || '预览' }}
|
||||
{{ t('admin.extension.webhooks.create.preview') || '预览' }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.name') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.name') }}</span>
|
||||
<span>{{ form.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.application') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.application') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.events') }}</span>
|
||||
<span>{{ form.events.length }} {{ t('developer.extension.webhooks.create.selected') || '已选择' }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.events') }}</span>
|
||||
<span>{{ form.events.length }} {{ t('admin.extension.webhooks.create.selected') || '已选择' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.retryCount') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.retryCount') }}</span>
|
||||
<span>{{ form.retry_count }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('developer.extension.webhooks.create.timeout') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.extension.webhooks.create.timeout') }}</span>
|
||||
<span>{{ form.timeout }}s</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,14 +296,14 @@ onMounted(() => {
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
|
||||
{{ t('developer.extension.webhooks.create.submitBtn') }}
|
||||
{{ t('admin.extension.webhooks.create.submitBtn') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('developer.extension.webhooks.create.cancelBtn') }}
|
||||
{{ t('admin.extension.webhooks.create.cancelBtn') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
|
||||
@@ -174,11 +174,11 @@ async function handleDelete() {
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/finance/records/${deleteTarget.value.id}?type=${deleteTarget.value.type}`)
|
||||
toast.success(t('developer.finance.deleteSuccess'))
|
||||
toast.success(t('admin.finance.deleteSuccess'))
|
||||
fetchRecords()
|
||||
}
|
||||
catch (error: any) {
|
||||
toast.error(error.message || t('developer.finance.deleteFailed'))
|
||||
toast.error(error.message || t('admin.finance.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
deleteTarget.value = null
|
||||
@@ -203,15 +203,15 @@ const columns: ColumnDef<FinanceRecord>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: () => t('developer.finance.columns.type'),
|
||||
header: () => t('admin.finance.columns.type'),
|
||||
cell: ({ row }) => {
|
||||
const type = row.getValue('type') as string
|
||||
return h(Badge, { variant: type === 'recharge' ? 'default' : 'secondary' }, () => type === 'recharge' ? t('developer.finance.types.recharge') : t('developer.finance.types.consumption'))
|
||||
return h(Badge, { variant: type === 'recharge' ? 'default' : 'secondary' }, () => type === 'recharge' ? t('admin.finance.types.recharge') : t('admin.finance.types.consumption'))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'amount',
|
||||
header: () => t('developer.finance.columns.amount'),
|
||||
header: () => t('admin.finance.columns.amount'),
|
||||
cell: ({ row }) => {
|
||||
const type = row.getValue('type') as string
|
||||
const amount = row.getValue('amount') as number
|
||||
@@ -221,7 +221,7 @@ const columns: ColumnDef<FinanceRecord>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: 'user',
|
||||
header: () => t('developer.finance.columns.user'),
|
||||
header: () => t('admin.finance.columns.user'),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original.user
|
||||
return user?.username || '-'
|
||||
@@ -229,7 +229,7 @@ const columns: ColumnDef<FinanceRecord>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: 'detail',
|
||||
header: () => t('developer.finance.columns.detail'),
|
||||
header: () => t('admin.finance.columns.detail'),
|
||||
cell: ({ row }) => {
|
||||
const detail = row.getValue('detail') as string
|
||||
return h('span', { class: 'max-w-[200px] truncate text-muted-foreground block' }, detail || '-')
|
||||
@@ -237,13 +237,13 @@ const columns: ColumnDef<FinanceRecord>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => t('developer.finance.columns.status'),
|
||||
header: () => t('admin.finance.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
const map: Record<string, { label: string, variant: 'default' | 'secondary' | 'destructive' }> = {
|
||||
success: { label: t('developer.finance.success'), variant: 'default' },
|
||||
pending: { label: t('developer.finance.pending'), variant: 'secondary' },
|
||||
failed: { label: t('developer.finance.failed'), variant: 'destructive' },
|
||||
success: { label: t('admin.finance.success'), variant: 'default' },
|
||||
pending: { label: t('admin.finance.pending'), variant: 'secondary' },
|
||||
failed: { label: t('admin.finance.failed'), variant: 'destructive' },
|
||||
}
|
||||
const { label, variant } = map[status] || { label: status, variant: 'secondary' }
|
||||
return h(Badge, { variant }, () => label)
|
||||
@@ -251,14 +251,14 @@ const columns: ColumnDef<FinanceRecord>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.finance.columns.time'),
|
||||
header: () => t('admin.finance.columns.time'),
|
||||
cell: ({ row }) => {
|
||||
return h('span', { class: 'text-muted-foreground whitespace-nowrap' }, formatTime(row.getValue('created_at')))
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => t('developer.finance.columns.actions'),
|
||||
header: () => t('admin.finance.columns.actions'),
|
||||
cell: ({ row }) => {
|
||||
return h(Button, {
|
||||
variant: 'ghost',
|
||||
@@ -293,7 +293,7 @@ const table = useVueTable({
|
||||
async function confirmBatchDelete() {
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
if (selectedRows.length === 0) {
|
||||
toast.error(t('developer.finance.delete'))
|
||||
toast.error(t('admin.finance.delete'))
|
||||
return
|
||||
}
|
||||
batchDeleteDialogOpen.value = true
|
||||
@@ -304,11 +304,11 @@ async function handleBatchDelete() {
|
||||
const ids = selectedRows.map(row => row.original.id)
|
||||
try {
|
||||
await api.post('/dev/finance/records/batch-delete', { ids })
|
||||
toast.success(t('developer.finance.deleteSuccess'))
|
||||
toast.success(t('admin.finance.deleteSuccess'))
|
||||
fetchRecords()
|
||||
}
|
||||
catch (error: any) {
|
||||
toast.error(error.message || t('developer.finance.deleteFailed'))
|
||||
toast.error(error.message || t('admin.finance.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,8 +344,8 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.finance.title')"
|
||||
:description="t('developer.finance.description')"
|
||||
:title="t('admin.finance.title')"
|
||||
:description="t('admin.finance.description')"
|
||||
sticky
|
||||
>
|
||||
<div class="space-y-6">
|
||||
@@ -353,7 +353,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.finance.rechargeCount') }}
|
||||
{{ t('admin.finance.rechargeCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:arrow-down-left" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -367,7 +367,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.finance.rechargeAmount') }}
|
||||
{{ t('admin.finance.rechargeAmount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -381,7 +381,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.finance.consumptionCount') }}
|
||||
{{ t('admin.finance.consumptionCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:arrow-up-right" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -395,7 +395,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.finance.consumptionAmount') }}
|
||||
{{ t('admin.finance.consumptionAmount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:credit-card" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -409,7 +409,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.finance.profit') }}
|
||||
{{ t('admin.finance.profit') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:trending-up" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -423,9 +423,9 @@ onMounted(() => {
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.finance.transactionRecords') }}</UiCardTitle>
|
||||
<UiCardTitle>{{ t('admin.finance.transactionRecords') }}</UiCardTitle>
|
||||
<UiCardDescription>
|
||||
{{ t('developer.finance.transactionRecordsDesc') }}
|
||||
{{ t('admin.finance.transactionRecordsDesc') }}
|
||||
</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
@@ -438,18 +438,18 @@ onMounted(() => {
|
||||
@click="confirmBatchDelete"
|
||||
>
|
||||
<Icon icon="lucide:trash-2" class="mr-2 size-4" />
|
||||
{{ t('developer.finance.batchDeleteBtn') }}
|
||||
{{ t('admin.finance.batchDeleteBtn') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UiSelect v-model="appFilter" class="w-[140px]">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.finance.allApplications')" />
|
||||
<UiSelectValue :placeholder="t('admin.finance.allApplications')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="all">
|
||||
{{ t('developer.finance.allApplications') }}
|
||||
{{ t('admin.finance.allApplications') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
|
||||
{{ app.name }}
|
||||
@@ -459,43 +459,43 @@ onMounted(() => {
|
||||
|
||||
<UiSelect v-model="statusFilter" class="w-[120px]">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('developer.finance.allStatus')" />
|
||||
<UiSelectValue :placeholder="t('admin.finance.allStatus')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="all">
|
||||
{{ t('developer.finance.allStatus') }}
|
||||
{{ t('admin.finance.allStatus') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="success">
|
||||
{{ t('developer.finance.success') }}
|
||||
{{ t('admin.finance.success') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="pending">
|
||||
{{ t('developer.finance.pending') }}
|
||||
{{ t('admin.finance.pending') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="failed">
|
||||
{{ t('developer.finance.failed') }}
|
||||
{{ t('admin.finance.failed') }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
|
||||
<DateTimePicker
|
||||
v-model="dateRange.from"
|
||||
:placeholder="t('developer.finance.startTime')"
|
||||
:placeholder="t('admin.finance.startTime')"
|
||||
class="w-[180px]"
|
||||
/>
|
||||
<span class="text-muted-foreground">{{ t('developer.finance.to') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.finance.to') }}</span>
|
||||
<DateTimePicker
|
||||
v-model="dateRange.to"
|
||||
:placeholder="t('developer.finance.endTime')"
|
||||
:placeholder="t('admin.finance.endTime')"
|
||||
class="w-[180px]"
|
||||
/>
|
||||
<UiButton variant="ghost" size="sm" @click="setQuickRange('today')">
|
||||
{{ t('developer.finance.today') }}
|
||||
{{ t('admin.finance.today') }}
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" size="sm" @click="setQuickRange('week')">
|
||||
{{ t('developer.finance.last7Days') }}
|
||||
{{ t('admin.finance.last7Days') }}
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" size="sm" @click="setQuickRange('month')">
|
||||
{{ t('developer.finance.thisMonth') }}
|
||||
{{ t('admin.finance.thisMonth') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -534,7 +534,7 @@ onMounted(() => {
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center text-muted-foreground">
|
||||
<Icon icon="lucide:inbox" class="size-12 mb-2 opacity-50" />
|
||||
<span>{{ t('developer.finance.noRecords') }}</span>
|
||||
<span>{{ t('admin.finance.noRecords') }}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -551,30 +551,30 @@ onMounted(() => {
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.finance.delete')"
|
||||
:cancel-button-text="t('developer.finance.cancel')"
|
||||
:confirm-button-text="t('admin.finance.delete')"
|
||||
:cancel-button-text="t('admin.finance.cancel')"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.finance.delete') }}
|
||||
{{ t('admin.finance.delete') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.finance.deleteConfirm') }}
|
||||
{{ t('admin.finance.deleteConfirm') }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('developer.finance.delete')"
|
||||
:cancel-button-text="t('developer.finance.cancel')"
|
||||
:confirm-button-text="t('admin.finance.delete')"
|
||||
:cancel-button-text="t('admin.finance.cancel')"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('developer.finance.batchDeleteBtn') }}
|
||||
{{ t('admin.finance.batchDeleteBtn') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('developer.finance.batchDeleteConfirm', { count: table.getSelectedRowModel().rows.length }) }}
|
||||
{{ t('admin.finance.batchDeleteConfirm', { count: table.getSelectedRowModel().rows.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</template>
|
||||
|
||||
@@ -107,10 +107,10 @@ function getTicketStatusColor(status: string) {
|
||||
|
||||
function getTicketStatusText(status: string) {
|
||||
const texts: Record<string, string> = {
|
||||
open: t('developer.ticketStatusOpen'),
|
||||
processing: t('developer.ticketStatusProcessing'),
|
||||
resolved: t('developer.ticketStatusResolved'),
|
||||
closed: t('developer.ticketStatusClosed'),
|
||||
open: t('admin.ticketStatusOpen'),
|
||||
processing: t('admin.ticketStatusProcessing'),
|
||||
resolved: t('admin.ticketStatusResolved'),
|
||||
closed: t('admin.ticketStatusClosed'),
|
||||
}
|
||||
return texts[status] || status
|
||||
}
|
||||
@@ -127,20 +127,20 @@ function getTicketPriorityColor(priority: string) {
|
||||
|
||||
function getTicketPriorityText(priority: string) {
|
||||
const texts: Record<string, string> = {
|
||||
low: t('developer.priorityLow'),
|
||||
normal: t('developer.priorityNormal'),
|
||||
high: t('developer.priorityHigh'),
|
||||
urgent: t('developer.priorityUrgent'),
|
||||
low: t('admin.priorityLow'),
|
||||
normal: t('admin.priorityNormal'),
|
||||
high: t('admin.priorityHigh'),
|
||||
urgent: t('admin.priorityUrgent'),
|
||||
}
|
||||
return texts[priority] || priority
|
||||
}
|
||||
|
||||
function goToTicket(ticketId: number) {
|
||||
router.push(`/developer/tickets/${ticketId}`)
|
||||
router.push(`/admin/tickets/${ticketId}`)
|
||||
}
|
||||
|
||||
function goToTickets() {
|
||||
router.push('/developer/tickets')
|
||||
router.push('/admin/tickets')
|
||||
}
|
||||
|
||||
async function fetchDashboard() {
|
||||
@@ -196,21 +196,21 @@ async function fetchDashboard() {
|
||||
if (data.data.recentActivities && Array.isArray(data.data.recentActivities)) {
|
||||
recentActivities.value = data.data.recentActivities.map((log: any) => {
|
||||
let icon = 'lucide:activity'
|
||||
let title = log.action || t('developer.operation')
|
||||
let description = log.details || log.resource || t('developer.noDescription')
|
||||
let title = log.action || t('admin.operation')
|
||||
let description = log.details || log.resource || t('admin.noDescription')
|
||||
|
||||
if (log.log_type === 'operation') {
|
||||
icon = 'lucide:plus-circle'
|
||||
title = t('developer.operationRecord')
|
||||
title = t('admin.operationRecord')
|
||||
}
|
||||
else if (log.log_type === 'verification') {
|
||||
icon = 'lucide:check-circle'
|
||||
title = t('developer.cardVerification')
|
||||
description = `${t('developer.verification')}${log.status === 'success' ? t('developer.success') : t('developer.failed')}`
|
||||
title = t('admin.cardVerification')
|
||||
description = `${t('admin.verification')}${log.status === 'success' ? t('admin.success') : t('admin.failed')}`
|
||||
}
|
||||
else if (log.log_type === 'exception') {
|
||||
icon = 'lucide:alert-circle'
|
||||
title = t('developer.exceptionRecord')
|
||||
title = t('admin.exceptionRecord')
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -238,7 +238,7 @@ async function fetchDashboard() {
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(t('developer.fetchDashboardError'), error)
|
||||
console.error(t('admin.fetchDashboardError'), error)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -276,7 +276,7 @@ async function initMapChart() {
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error(t('developer.initMapError'), error)
|
||||
console.error(t('admin.initMapError'), error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,12 +298,12 @@ function updateMapOption() {
|
||||
if (params.data && params.data.count > 0) {
|
||||
return `<div style="padding: 8px;">
|
||||
<div style="font-weight: bold; margin-bottom: 4px; color: #f8fafc;">${params.name}</div>
|
||||
<div style="color: #cbd5e1;">${t('developer.userCount')}: ${params.data.count}</div>
|
||||
<div style="color: #cbd5e1;">${t('admin.userCount')}: ${params.data.count}</div>
|
||||
</div>`
|
||||
}
|
||||
return `<div style="padding: 8px;">
|
||||
<div style="font-weight: bold; margin-bottom: 4px; color: #f8fafc;">${params.name}</div>
|
||||
<div style="color: #cbd5e1;">${t('developer.noUserData')}</div>
|
||||
<div style="color: #cbd5e1;">${t('admin.noUserData')}</div>
|
||||
</div>`
|
||||
},
|
||||
},
|
||||
@@ -342,7 +342,7 @@ function updateMapOption() {
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('developer.userDistribution'),
|
||||
name: t('admin.userDistribution'),
|
||||
type: 'map',
|
||||
geoIndex: 0,
|
||||
data,
|
||||
@@ -383,7 +383,7 @@ function initActivityChart() {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: t('developer.verificationCount'),
|
||||
label: t('admin.verificationCount'),
|
||||
data,
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
@@ -413,7 +413,7 @@ function initActivityChart() {
|
||||
padding: 12,
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
label: (context: any) => `${context.parsed.y} ${t('developer.online')}`,
|
||||
label: (context: any) => `${context.parsed.y} ${t('admin.online')}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -451,7 +451,7 @@ function initActivityChart() {
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error(t('developer.initActivityChartError'), error)
|
||||
console.error(t('admin.initActivityChartError'), error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,14 +478,14 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.dashboard')"
|
||||
:description="`${t('developer.welcomeBack')}, ${currentUser?.username || t('developer.developer')}`"
|
||||
:title="t('admin.dashboard')"
|
||||
:description="`${t('admin.welcomeBack')}, ${currentUser?.username || t('admin.developer')}`"
|
||||
>
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<div class="text-center">
|
||||
<Icon icon="lucide:loader-2" class="size-8 animate-spin mx-auto text-muted-foreground" />
|
||||
<p class="mt-4 text-muted-foreground">
|
||||
{{ t('developer.loading') }}...
|
||||
{{ t('admin.loading') }}...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -495,7 +495,7 @@ onUnmounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.totalApplications') }}
|
||||
{{ t('admin.totalApplications') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:layout-grid" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -504,7 +504,7 @@ onUnmounted(() => {
|
||||
{{ formatNumber(stats.totalApplications) }}
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
+12.5% {{ t('developer.lastMonth') }}
|
||||
+12.5% {{ t('admin.lastMonth') }}
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
@@ -512,7 +512,7 @@ onUnmounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.totalCards') }}
|
||||
{{ t('admin.totalCards') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:key" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -521,7 +521,7 @@ onUnmounted(() => {
|
||||
{{ formatNumber(stats.totalCards) }}
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
+8.2% {{ t('developer.lastMonth') }}
|
||||
+8.2% {{ t('admin.lastMonth') }}
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
@@ -529,7 +529,7 @@ onUnmounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.totalUsers') }}
|
||||
{{ t('admin.totalUsers') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -538,7 +538,7 @@ onUnmounted(() => {
|
||||
{{ formatNumber(totalUsers) }}
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
+15.3% {{ t('developer.lastMonth') }}
|
||||
+15.3% {{ t('admin.lastMonth') }}
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
@@ -546,7 +546,7 @@ onUnmounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.monthlyRevenue') }}
|
||||
{{ t('admin.monthlyRevenue') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:dollar-sign" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -555,7 +555,7 @@ onUnmounted(() => {
|
||||
¥{{ formatNumber(stats.monthlyRevenue) }}
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
+23.1% {{ t('developer.lastMonth') }}
|
||||
+23.1% {{ t('admin.lastMonth') }}
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
@@ -566,17 +566,17 @@ onUnmounted(() => {
|
||||
<UiCardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<UiCardTitle>{{ t('developer.userDistribution') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.userDistributionDesc') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.userDistribution') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.userDistributionDesc') }}</UiCardDescription>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="size-3 rounded-full bg-blue-500" />
|
||||
<span class="text-muted-foreground">{{ t('developer.domesticUsers') }}: {{ formatNumber(domesticUsers) }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.domesticUsers') }}: {{ formatNumber(domesticUsers) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="size-3 rounded-full bg-green-500" />
|
||||
<span class="text-muted-foreground">{{ t('developer.overseasUsers') }}: {{ formatNumber(overseasUsers) }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.overseasUsers') }}: {{ formatNumber(overseasUsers) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -590,8 +590,8 @@ onUnmounted(() => {
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.onlineTrend') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.onlineTrendDesc') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.onlineTrend') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.onlineTrendDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="pl-2">
|
||||
<div class="relative h-[350px]">
|
||||
@@ -605,11 +605,11 @@ onUnmounted(() => {
|
||||
<UiCardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<UiCardTitle>{{ t('developer.recentTickets') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.recentTicketsDesc') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.recentTickets') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.recentTicketsDesc') }}</UiCardDescription>
|
||||
</div>
|
||||
<UiButton variant="ghost" size="sm" @click="goToTickets">
|
||||
{{ t('developer.viewAll') }}
|
||||
{{ t('admin.viewAll') }}
|
||||
<Icon icon="lucide:arrow-right" class="ml-1 size-4" />
|
||||
</UiButton>
|
||||
</div>
|
||||
@@ -646,7 +646,7 @@ onUnmounted(() => {
|
||||
<div v-else class="text-center py-8 text-muted-foreground">
|
||||
<Icon icon="lucide:ticket" class="mx-auto size-10 text-muted-foreground mb-2" />
|
||||
<p class="text-sm">
|
||||
{{ t('developer.noTickets') }}
|
||||
{{ t('admin.noTickets') }}
|
||||
</p>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
@@ -654,8 +654,8 @@ onUnmounted(() => {
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>{{ t('developer.recentActivities') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('developer.recentActivitiesDesc') }}</UiCardDescription>
|
||||
<UiCardTitle>{{ t('admin.recentActivities') }}</UiCardTitle>
|
||||
<UiCardDescription>{{ t('admin.recentActivitiesDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div v-if="recentActivities.length > 0" class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@@ -685,7 +685,7 @@ onUnmounted(() => {
|
||||
<div v-else class="text-center py-8 text-muted-foreground">
|
||||
<Icon icon="lucide:activity" class="mx-auto size-10 text-muted-foreground mb-2" />
|
||||
<p class="text-sm">
|
||||
{{ t('developer.noActivities') }}
|
||||
{{ t('admin.noActivities') }}
|
||||
</p>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
|
||||
@@ -72,14 +72,14 @@ const applicationOptions = computed(() => {
|
||||
})
|
||||
|
||||
const typeOptions = computed(() => [
|
||||
{ label: t('developer.logs.types.operation'), value: 'operation' },
|
||||
{ label: t('developer.logs.types.verification'), value: 'verification' },
|
||||
{ label: t('developer.logs.types.exception'), value: 'exception' },
|
||||
{ label: t('admin.logs.types.operation'), value: 'operation' },
|
||||
{ label: t('admin.logs.types.verification'), value: 'verification' },
|
||||
{ label: t('admin.logs.types.exception'), value: 'exception' },
|
||||
])
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('developer.success'), value: 'success' },
|
||||
{ label: t('developer.failed'), value: 'failed' },
|
||||
{ label: t('admin.success'), value: 'success' },
|
||||
{ label: t('admin.failed'), value: 'failed' },
|
||||
])
|
||||
|
||||
async function fetchApplications() {
|
||||
@@ -115,8 +115,8 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.logs.title')"
|
||||
:description="t('developer.logs.description')"
|
||||
:title="t('admin.logs.title')"
|
||||
:description="t('admin.logs.description')"
|
||||
sticky
|
||||
>
|
||||
<div class="space-y-6">
|
||||
@@ -124,7 +124,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.logs.totalLogs') }}
|
||||
{{ t('admin.logs.totalLogs') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:file-text" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -138,7 +138,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.logs.successCount') }}
|
||||
{{ t('admin.logs.successCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -152,7 +152,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.logs.failedCount') }}
|
||||
{{ t('admin.logs.failedCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -166,7 +166,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.logs.operationCount') }}
|
||||
{{ t('admin.logs.operationCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -188,17 +188,17 @@ onMounted(() => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.logs.columns.application')"
|
||||
:title="t('admin.logs.columns.application')"
|
||||
:options="applicationOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="typeFilter"
|
||||
:title="t('developer.logs.columns.type')"
|
||||
:title="t('admin.logs.columns.type')"
|
||||
:options="typeOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('developer.logs.columns.status')"
|
||||
:title="t('admin.logs.columns.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Composer } from 'vue-i18n'
|
||||
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
|
||||
import type { Log } from '@/pages/developer/logs/data/schema'
|
||||
import type { Log } from '@/pages/admin/logs/data/schema'
|
||||
|
||||
function translateDetails(t: Composer['t'], details: string): string {
|
||||
if (!details) return '-'
|
||||
@@ -16,222 +16,222 @@ function translateDetails(t: Composer['t'], details: string): string {
|
||||
}> = [
|
||||
{
|
||||
pattern: /^用户注册: (.+)$/,
|
||||
template: 'developer.logs.details.register',
|
||||
template: 'admin.logs.details.register',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^注册失败: 用户已存在 - (.+)$/,
|
||||
template: 'developer.logs.details.register_failed_exists',
|
||||
template: 'admin.logs.details.register_failed_exists',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^用户登录: (.+)$/,
|
||||
template: 'developer.logs.details.login',
|
||||
template: 'admin.logs.details.login',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^登录失败: 用户不存在 - (.+)$/,
|
||||
template: 'developer.logs.details.login_failed_not_found',
|
||||
template: 'admin.logs.details.login_failed_not_found',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^登录失败: 密码错误 - (.+)$/,
|
||||
template: 'developer.logs.details.login_failed_password',
|
||||
template: 'admin.logs.details.login_failed_password',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^登录失败: 余额不足 - (.+)$/,
|
||||
template: 'developer.logs.details.login_failed_balance',
|
||||
template: 'admin.logs.details.login_failed_balance',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^登录失败: 设备绑定数量已达上限 - (.+)$/,
|
||||
template: 'developer.logs.details.login_failed_device_limit',
|
||||
template: 'admin.logs.details.login_failed_device_limit',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^登录失败: 多开数量已达上限 - (.+)$/,
|
||||
template: 'developer.logs.details.login_failed_multi_limit',
|
||||
template: 'admin.logs.details.login_failed_multi_limit',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^用户充值: (.+), 卡密: (.+), 金额: ([\d.]+)$/,
|
||||
template: 'developer.logs.details.recharge',
|
||||
template: 'admin.logs.details.recharge',
|
||||
extract: (m) => ({ username: m[1], card: m[2], amount: m[3] }),
|
||||
},
|
||||
{
|
||||
pattern: /^充值失败: 用户不存在 - (.+)$/,
|
||||
template: 'developer.logs.details.recharge_failed_user_not_found',
|
||||
template: 'admin.logs.details.recharge_failed_user_not_found',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^充值失败: 用户已是永久会员 - (.+)$/,
|
||||
template: 'developer.logs.details.recharge_failed_permanent',
|
||||
template: 'admin.logs.details.recharge_failed_permanent',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^充值失败: 卡密不存在 - (.+)$/,
|
||||
template: 'developer.logs.details.recharge_failed_card_not_found',
|
||||
template: 'admin.logs.details.recharge_failed_card_not_found',
|
||||
extract: (m) => ({ card: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^充值失败: 卡密已使用 - (.+)$/,
|
||||
template: 'developer.logs.details.recharge_failed_card_used',
|
||||
template: 'admin.logs.details.recharge_failed_card_used',
|
||||
extract: (m) => ({ card: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^用户解绑设备: (.+)$/,
|
||||
template: 'developer.logs.details.unbind_device',
|
||||
template: 'admin.logs.details.unbind_device',
|
||||
extract: (m) => ({ device: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API充值: 用户(.+), 类型:(.+), 数量:(\d+)$/,
|
||||
template: 'developer.logs.details.extension_recharge',
|
||||
template: 'admin.logs.details.extension_recharge',
|
||||
extract: (m) => ({ username: m[1], type: m[2], amount: m[3] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API充值失败: 用户不存在 - (.+)$/,
|
||||
template: 'developer.logs.details.extension_recharge_failed_user_not_found',
|
||||
template: 'admin.logs.details.extension_recharge_failed_user_not_found',
|
||||
extract: (m) => ({ userId: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API充值失败: 用户已是永久会员 - (.+)$/,
|
||||
template: 'developer.logs.details.extension_recharge_failed_permanent',
|
||||
template: 'admin.logs.details.extension_recharge_failed_permanent',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API充值失败: 类型无效 - (.+)$/,
|
||||
template: 'developer.logs.details.extension_recharge_failed_type',
|
||||
template: 'admin.logs.details.extension_recharge_failed_type',
|
||||
extract: (m) => ({ type: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API充值失败: 保存失败 - (.+)$/,
|
||||
template: 'developer.logs.details.extension_recharge_failed_save',
|
||||
template: 'admin.logs.details.extension_recharge_failed_save',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API扣费: 用户(.+), 类型:(.+), 数量:(\d+)$/,
|
||||
template: 'developer.logs.details.extension_deduct',
|
||||
template: 'admin.logs.details.extension_deduct',
|
||||
extract: (m) => ({ username: m[1], type: m[2], amount: m[3] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API扣费失败: 用户不存在 - (.+)$/,
|
||||
template: 'developer.logs.details.extension_deduct_failed_user_not_found',
|
||||
template: 'admin.logs.details.extension_deduct_failed_user_not_found',
|
||||
extract: (m) => ({ userId: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API扣费失败: 用户已是永久会员 - (.+)$/,
|
||||
template: 'developer.logs.details.extension_deduct_failed_permanent',
|
||||
template: 'admin.logs.details.extension_deduct_failed_permanent',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API扣费失败: 用户订阅已过期 - (.+)$/,
|
||||
template: 'developer.logs.details.extension_deduct_failed_expired',
|
||||
template: 'admin.logs.details.extension_deduct_failed_expired',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API扣费失败: 余额不足 - (.+)$/,
|
||||
template: 'developer.logs.details.extension_deduct_failed_balance',
|
||||
template: 'admin.logs.details.extension_deduct_failed_balance',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API扣费失败: 类型无效 - (.+)$/,
|
||||
template: 'developer.logs.details.extension_deduct_failed_type',
|
||||
template: 'admin.logs.details.extension_deduct_failed_type',
|
||||
extract: (m) => ({ type: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^扩展API扣费失败: 保存失败 - (.+)$/,
|
||||
template: 'developer.logs.details.extension_deduct_failed_save',
|
||||
template: 'admin.logs.details.extension_deduct_failed_save',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^创建应用: (.+)$/,
|
||||
template: 'developer.logs.details.create_application',
|
||||
template: 'admin.logs.details.create_application',
|
||||
extract: (m) => ({ name: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^更新应用: (.+)$/,
|
||||
template: 'developer.logs.details.update_application',
|
||||
template: 'admin.logs.details.update_application',
|
||||
extract: (m) => ({ name: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^删除应用: (.+)$/,
|
||||
template: 'developer.logs.details.delete_application',
|
||||
template: 'admin.logs.details.delete_application',
|
||||
extract: (m) => ({ name: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^创建用户: (.+) \(应用: (.+)\)$/,
|
||||
template: 'developer.logs.details.create_user',
|
||||
template: 'admin.logs.details.create_user',
|
||||
extract: (m) => ({ username: m[1], app: m[2] }),
|
||||
},
|
||||
{
|
||||
pattern: /^更新用户: (.+)$/,
|
||||
template: 'developer.logs.details.update_user',
|
||||
template: 'admin.logs.details.update_user',
|
||||
extract: (m) => ({ username: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^删除用户: (.+) \(应用: (.+)\)$/,
|
||||
template: 'developer.logs.details.delete_user',
|
||||
template: 'admin.logs.details.delete_user',
|
||||
extract: (m) => ({ username: m[1], app: m[2] }),
|
||||
},
|
||||
{
|
||||
pattern: /^解绑设备: (.+) \(应用: (.+)\)$/,
|
||||
template: 'developer.logs.details.unbind_device_app',
|
||||
template: 'admin.logs.details.unbind_device_app',
|
||||
extract: (m) => ({ device: m[1], app: m[2] }),
|
||||
},
|
||||
{
|
||||
pattern: /^批量解绑设备: (\d+)个$/,
|
||||
template: 'developer.logs.details.batch_unbind',
|
||||
template: 'admin.logs.details.batch_unbind',
|
||||
extract: (m) => ({ count: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^创建工单: (.+)$/,
|
||||
template: 'developer.logs.details.create_ticket',
|
||||
template: 'admin.logs.details.create_ticket',
|
||||
extract: (m) => ({ title: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^删除工单: (.+)$/,
|
||||
template: 'developer.logs.details.delete_ticket',
|
||||
template: 'admin.logs.details.delete_ticket',
|
||||
extract: (m) => ({ title: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^创建Webhook: (.+)$/,
|
||||
template: 'developer.logs.details.create_webhook',
|
||||
template: 'admin.logs.details.create_webhook',
|
||||
extract: (m) => ({ name: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^删除Webhook: (.+)$/,
|
||||
template: 'developer.logs.details.delete_webhook',
|
||||
template: 'admin.logs.details.delete_webhook',
|
||||
extract: (m) => ({ name: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^创建云端常量: (.+)$/,
|
||||
template: 'developer.logs.details.create_cloud_constant',
|
||||
template: 'admin.logs.details.create_cloud_constant',
|
||||
extract: (m) => ({ key: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^更新云端常量: (.+)$/,
|
||||
template: 'developer.logs.details.update_cloud_constant',
|
||||
template: 'admin.logs.details.update_cloud_constant',
|
||||
extract: (m) => ({ key: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^删除云端常量: (.+)$/,
|
||||
template: 'developer.logs.details.delete_cloud_constant',
|
||||
template: 'admin.logs.details.delete_cloud_constant',
|
||||
extract: (m) => ({ key: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^创建卡密类型: (.+)$/,
|
||||
template: 'developer.logs.details.create_card_type',
|
||||
template: 'admin.logs.details.create_card_type',
|
||||
extract: (m) => ({ name: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^更新卡密类型: (.+)$/,
|
||||
template: 'developer.logs.details.update_card_type',
|
||||
template: 'admin.logs.details.update_card_type',
|
||||
extract: (m) => ({ name: m[1] }),
|
||||
},
|
||||
{
|
||||
pattern: /^删除卡密类型: (.+)$/,
|
||||
template: 'developer.logs.details.delete_card_type',
|
||||
template: 'admin.logs.details.delete_card_type',
|
||||
extract: (m) => ({ name: m[1] }),
|
||||
},
|
||||
]
|
||||
@@ -251,13 +251,13 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'log_type',
|
||||
header: () => t('developer.logs.columns.type'),
|
||||
header: () => t('admin.logs.columns.type'),
|
||||
cell: ({ row }) => {
|
||||
const type = row.getValue('log_type') as string
|
||||
const typeMap: Record<string, { text: string, variant: any }> = {
|
||||
operation: { text: t('developer.logs.types.operation'), variant: 'default' },
|
||||
verification: { text: t('developer.logs.types.verification'), variant: 'secondary' },
|
||||
exception: { text: t('developer.logs.types.exception'), variant: 'destructive' },
|
||||
operation: { text: t('admin.logs.types.operation'), variant: 'default' },
|
||||
verification: { text: t('admin.logs.types.verification'), variant: 'secondary' },
|
||||
exception: { text: t('admin.logs.types.exception'), variant: 'destructive' },
|
||||
}
|
||||
const { text, variant } = typeMap[type] || { text: type, variant: 'default' }
|
||||
return h(Badge, { variant }, () => text)
|
||||
@@ -265,18 +265,18 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
|
||||
},
|
||||
{
|
||||
accessorKey: 'action',
|
||||
header: () => t('developer.logs.columns.action'),
|
||||
header: () => t('admin.logs.columns.action'),
|
||||
cell: ({ row }) => {
|
||||
const action = row.getValue('action') as string
|
||||
if (!action) return '-'
|
||||
const actionKey = `developer.logs.actions.${action}`
|
||||
const actionKey = `dmin.logs.actions.${action}`
|
||||
const translated = t(actionKey)
|
||||
return translated === actionKey ? action : translated
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'details',
|
||||
header: () => t('developer.logs.columns.detail'),
|
||||
header: () => t('admin.logs.columns.detail'),
|
||||
cell: ({ row }) => {
|
||||
const details = row.getValue('details') as string
|
||||
if (!details) return '-'
|
||||
@@ -286,12 +286,12 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => t('developer.logs.columns.status'),
|
||||
header: () => t('admin.logs.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
const statusMap: Record<string, { text: string, variant: any }> = {
|
||||
success: { text: t('developer.success'), variant: 'default' },
|
||||
failed: { text: t('developer.failed'), variant: 'destructive' },
|
||||
success: { text: t('admin.success'), variant: 'default' },
|
||||
failed: { text: t('admin.failed'), variant: 'destructive' },
|
||||
}
|
||||
const { text, variant } = statusMap[status] || { text: status, variant: 'default' }
|
||||
return h(Badge, { variant }, () => text)
|
||||
@@ -299,7 +299,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
|
||||
},
|
||||
{
|
||||
accessorKey: 'ip_address',
|
||||
header: () => t('developer.logs.columns.ip'),
|
||||
header: () => t('admin.logs.columns.ip'),
|
||||
cell: ({ row }) => {
|
||||
const ip = row.getValue('ip_address') as string
|
||||
return h('code', { class: 'text-xs font-mono' }, ip || '-')
|
||||
@@ -307,7 +307,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
|
||||
},
|
||||
{
|
||||
accessorKey: 'application.name',
|
||||
header: () => t('developer.logs.columns.application'),
|
||||
header: () => t('admin.logs.columns.application'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.original.application?.name
|
||||
return appName ? h(Badge, { variant: 'outline' }, () => appName) : '-'
|
||||
@@ -315,7 +315,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
|
||||
},
|
||||
{
|
||||
accessorKey: 'user.username',
|
||||
header: () => t('developer.logs.columns.user'),
|
||||
header: () => t('admin.logs.columns.user'),
|
||||
cell: ({ row }) => {
|
||||
const username = row.original.user?.username || row.original.app_user?.username
|
||||
return username || '-'
|
||||
@@ -323,7 +323,7 @@ export function getColumns(t: Composer['t']): ColumnDef<Log>[] {
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('developer.logs.columns.time'),
|
||||
header: () => t('admin.logs.columns.time'),
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue('created_at')
|
||||
if (!createdAt) return '-'
|
||||
|
||||
@@ -24,7 +24,7 @@ const isFiltered = computed(() => props.table.getState().columnFilters.length >
|
||||
<template>
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
:placeholder="t('developer.logs.searchPlaceholder')"
|
||||
:placeholder="t('admin.logs.searchPlaceholder')"
|
||||
:model-value="(table.getColumn('action')?.getFilterValue() as string) ?? ''"
|
||||
class="h-8 w-[150px] lg:w-[250px]"
|
||||
@input="table.getColumn('action')?.setFilterValue($event.target.value)"
|
||||
|
||||
@@ -44,15 +44,15 @@ const table = generateVueTable<Log>({
|
||||
}, columns.value)
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
'select': 'developer.logs.select',
|
||||
'type': 'developer.logs.columns.type',
|
||||
'action': 'developer.logs.columns.action',
|
||||
'details': 'developer.logs.columns.detail',
|
||||
'status': 'developer.logs.columns.status',
|
||||
'ip': 'developer.logs.columns.ip',
|
||||
'application_name': 'developer.logs.columns.application',
|
||||
'user_username': 'developer.logs.columns.user',
|
||||
'created_at': 'developer.logs.columns.time',
|
||||
'select': 'admin.logs.select',
|
||||
'type': 'admin.logs.columns.type',
|
||||
'action': 'admin.logs.columns.action',
|
||||
'details': 'admin.logs.columns.detail',
|
||||
'status': 'admin.logs.columns.status',
|
||||
'ip': 'admin.logs.columns.ip',
|
||||
'application_name': 'admin.logs.columns.application',
|
||||
'user_username': 'admin.logs.columns.user',
|
||||
'created_at': 'admin.logs.columns.time',
|
||||
}
|
||||
|
||||
const serverPagination = computed(() => {
|
||||
|
||||
@@ -49,16 +49,16 @@ const applicationOptions = computed(() => {
|
||||
})
|
||||
|
||||
const typeOptions = computed(() => [
|
||||
{ label: t('developer.logs.types.all'), value: 'all' },
|
||||
{ label: t('developer.logs.types.operation'), value: 'operation' },
|
||||
{ label: t('developer.logs.types.verification'), value: 'verification' },
|
||||
{ label: t('developer.logs.types.exception'), value: 'exception' },
|
||||
{ label: t('admin.logs.types.all'), value: 'all' },
|
||||
{ label: t('admin.logs.types.operation'), value: 'operation' },
|
||||
{ label: t('admin.logs.types.verification'), value: 'verification' },
|
||||
{ label: t('admin.logs.types.exception'), value: 'exception' },
|
||||
])
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: t('developer.logs.statuses.all'), value: 'all' },
|
||||
{ label: t('developer.success'), value: 'success' },
|
||||
{ label: t('developer.failed'), value: 'failed' },
|
||||
{ label: t('admin.logs.statuses.all'), value: 'all' },
|
||||
{ label: t('admin.success'), value: 'success' },
|
||||
{ label: t('admin.failed'), value: 'failed' },
|
||||
])
|
||||
|
||||
async function fetchApplications() {
|
||||
@@ -125,8 +125,8 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('developer.logs.title')"
|
||||
:description="t('developer.logs.description')"
|
||||
:title="t('admin.logs.title')"
|
||||
:description="t('admin.logs.description')"
|
||||
sticky
|
||||
>
|
||||
<div class="space-y-6">
|
||||
@@ -134,7 +134,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.logs.totalLogs') }}
|
||||
{{ t('admin.logs.totalLogs') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:file-text" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -148,7 +148,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.logs.successCount') }}
|
||||
{{ t('admin.logs.successCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -162,7 +162,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.logs.failedCount') }}
|
||||
{{ t('admin.logs.failedCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -176,7 +176,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('developer.logs.operationCount') }}
|
||||
{{ t('admin.logs.operationCount') }}
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -205,17 +205,17 @@ onMounted(() => {
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="appFilter"
|
||||
:title="t('developer.logs.columns.application')"
|
||||
:options="[{ label: t('developer.logs.allApplications'), value: 'all' }, ...applicationOptions]"
|
||||
:title="t('admin.logs.columns.application')"
|
||||
:options="[{ label: t('admin.logs.allApplications'), value: 'all' }, ...applicationOptions]"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="typeFilter"
|
||||
:title="t('developer.logs.columns.type')"
|
||||
:title="t('admin.logs.columns.type')"
|
||||
:options="typeOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('developer.logs.columns.status')"
|
||||
:title="t('admin.logs.columns.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<DateRangeFilter
|
||||
|
||||
@@ -1,320 +1,289 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import CountrySelect from '@/components/country-select.vue'
|
||||
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/admin/risk-control/components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const rules = ref<RiskRule[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
const form = ref({
|
||||
type: 'ip' as 'ip' | 'device' | 'user' | 'region',
|
||||
value: '',
|
||||
reason: '',
|
||||
expires_at: '',
|
||||
const typeFilter = ref<string>('')
|
||||
const statusFilter = ref<string>('')
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<RiskRule | null>(null)
|
||||
const batchDeleteDialogOpen = ref(false)
|
||||
const batchDeleteIds = ref<(string | number)[]>([])
|
||||
|
||||
const typeOptions = computed(() => [
|
||||
{ label: 'IP封禁', value: 'ip' },
|
||||
{ label: '设备封禁', value: 'device' },
|
||||
{ label: '用户封禁', value: 'user' },
|
||||
{ label: '地区封禁', value: 'region' },
|
||||
])
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ label: '生效中', value: 'active' },
|
||||
{ label: '已失效', value: 'inactive' },
|
||||
])
|
||||
|
||||
const filteredRules = computed(() => {
|
||||
let result = rules.value
|
||||
|
||||
if (typeFilter.value) {
|
||||
result = result.filter(rule => rule.type === typeFilter.value)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
result = result.filter(rule => rule.status === statusFilter.value)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const typeOptions: { value: 'ip' | 'device' | 'user' | 'region', label: string, icon: string, description: string, placeholder: string }[] = [
|
||||
{ value: 'ip', label: 'IP封禁', icon: 'lucide:globe', description: '封禁特定IP地址或IP段', placeholder: '输入IP地址,如 192.168.1.1 或 192.168.1.0/24' },
|
||||
{ value: 'device', label: '设备封禁', icon: 'lucide:smartphone', description: '封禁特定设备指纹', placeholder: '输入设备指纹' },
|
||||
{ value: 'user', label: '用户封禁', icon: 'lucide:user-x', description: '封禁特定用户账号', placeholder: '输入用户ID或用户名' },
|
||||
{ value: 'region', label: '地区封禁', icon: 'lucide:map-pin', description: '封禁特定地区访问', placeholder: '' },
|
||||
]
|
||||
|
||||
function getTypeInfo(type: string) {
|
||||
return typeOptions.find(t => t.value === type) || typeOptions[0]
|
||||
}
|
||||
|
||||
const countries: Record<string, { name: string, flag: string }> = {
|
||||
CN: { name: '中国', flag: '🇨🇳' },
|
||||
US: { name: '美国', flag: '🇺🇸' },
|
||||
JP: { name: '日本', flag: '🇯🇵' },
|
||||
KR: { name: '韩国', flag: '🇰🇷' },
|
||||
GB: { name: '英国', flag: '🇬🇧' },
|
||||
DE: { name: '德国', flag: '🇩🇪' },
|
||||
FR: { name: '法国', flag: '🇫🇷' },
|
||||
IT: { name: '意大利', flag: '🇮🇹' },
|
||||
ES: { name: '西班牙', flag: '🇪🇸' },
|
||||
RU: { name: '俄罗斯', flag: '🇷🇺' },
|
||||
CA: { name: '加拿大', flag: '🇨🇦' },
|
||||
AU: { name: '澳大利亚', flag: '🇦🇺' },
|
||||
BR: { name: '巴西', flag: '🇧🇷' },
|
||||
IN: { name: '印度', flag: '🇮🇳' },
|
||||
MX: { name: '墨西哥', flag: '🇲🇽' },
|
||||
NL: { name: '荷兰', flag: '🇳🇱' },
|
||||
SG: { name: '新加坡', flag: '🇸🇬' },
|
||||
HK: { name: '中国香港', flag: '🇭🇰' },
|
||||
TW: { name: '中国台湾', flag: '🇹🇼' },
|
||||
TH: { name: '泰国', flag: '🇹🇭' },
|
||||
VN: { name: '越南', flag: '🇻🇳' },
|
||||
MY: { name: '马来西亚', flag: '🇲🇾' },
|
||||
ID: { name: '印度尼西亚', flag: '🇮🇩' },
|
||||
PH: { name: '菲律宾', flag: '🇵🇭' },
|
||||
AE: { name: '阿联酋', flag: '🇦🇪' },
|
||||
SA: { name: '沙特阿拉伯', flag: '🇸🇦' },
|
||||
TR: { name: '土耳其', flag: '🇹🇷' },
|
||||
PL: { name: '波兰', flag: '🇵🇱' },
|
||||
SE: { name: '瑞典', flag: '🇸🇪' },
|
||||
CH: { name: '瑞士', flag: '🇨🇭' },
|
||||
AT: { name: '奥地利', flag: '🇦🇹' },
|
||||
BE: { name: '比利时', flag: '🇧🇪' },
|
||||
DK: { name: '丹麦', flag: '🇩🇰' },
|
||||
FI: { name: '芬兰', flag: '🇫🇮' },
|
||||
NO: { name: '挪威', flag: '🇳🇴' },
|
||||
IE: { name: '爱尔兰', flag: '🇮🇪' },
|
||||
PT: { name: '葡萄牙', flag: '🇵🇹' },
|
||||
CZ: { name: '捷克', flag: '🇨🇿' },
|
||||
RO: { name: '罗马尼亚', flag: '🇷🇴' },
|
||||
HU: { name: '匈牙利', flag: '🇭🇺' },
|
||||
IL: { name: '以色列', flag: '🇮🇱' },
|
||||
ZA: { name: '南非', flag: '🇿🇦' },
|
||||
EG: { name: '埃及', flag: '🇪🇬' },
|
||||
NG: { name: '尼日利亚', flag: '🇳🇬' },
|
||||
AR: { name: '阿根廷', flag: '🇦🇷' },
|
||||
CL: { name: '智利', flag: '🇨🇱' },
|
||||
CO: { name: '哥伦比亚', flag: '🇨🇴' },
|
||||
PE: { name: '秘鲁', flag: '🇵🇪' },
|
||||
NZ: { name: '新西兰', flag: '🇳🇿' },
|
||||
}
|
||||
|
||||
function getCountryDisplay(code: string) {
|
||||
const country = countries[code]
|
||||
return country ? `${country.flag} ${country.name} (${code})` : code
|
||||
}
|
||||
|
||||
async function fetchRule() {
|
||||
const id = route.params.id
|
||||
if (!id) {
|
||||
toast.error('规则ID不存在')
|
||||
router.push('/developer/risk-control')
|
||||
return
|
||||
}
|
||||
const activeCount = computed(() => filteredRules.value.filter(rule => rule.status === 'active').length)
|
||||
const inactiveCount = computed(() => filteredRules.value.filter(rule => rule.status === 'inactive').length)
|
||||
const ipCount = computed(() => filteredRules.value.filter(rule => rule.type === 'ip').length)
|
||||
const deviceCount = computed(() => filteredRules.value.filter(rule => rule.type === 'device').length)
|
||||
|
||||
async function fetchRules() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<any>(`/dev/risk-control/rules`)
|
||||
const rules = Array.isArray(data) ? data : []
|
||||
const rule = rules.find((r: any) => String(r.id) === String(id))
|
||||
|
||||
if (!rule) {
|
||||
toast.error('规则不存在')
|
||||
router.push('/developer/risk-control')
|
||||
return
|
||||
}
|
||||
|
||||
form.value = {
|
||||
type: rule.type,
|
||||
value: rule.value,
|
||||
reason: rule.reason || '',
|
||||
expires_at: rule.expires_at ? rule.expires_at.replace(' ', 'T').slice(0, 16) : '',
|
||||
}
|
||||
const data = await api.get<RiskRule[]>('/dev/risk-control/rules')
|
||||
rules.value = Array.isArray(data) ? data : []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取规则详情失败:', error)
|
||||
toast.error('获取规则详情失败')
|
||||
router.push('/developer/risk-control')
|
||||
console.error('获取风控规则失败:', error)
|
||||
rules.value = []
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.value.trim()) {
|
||||
toast.error('请输入封禁值')
|
||||
return
|
||||
}
|
||||
function goToCreate() {
|
||||
router.push('/admin/risk-control/create')
|
||||
}
|
||||
|
||||
const id = route.params.id
|
||||
saving.value = true
|
||||
function openEditPage(rule: RiskRule) {
|
||||
router.push(`/admin/risk-control/${rule.id}`)
|
||||
}
|
||||
|
||||
async function toggleStatus(rule: RiskRule) {
|
||||
try {
|
||||
await api.put(`/dev/risk-control/rules/${id}`, {
|
||||
type: form.value.type,
|
||||
value: form.value.value,
|
||||
reason: form.value.reason,
|
||||
expires_at: form.value.expires_at || null,
|
||||
})
|
||||
toast.success('更新成功')
|
||||
router.push('/developer/risk-control')
|
||||
const newStatus = rule.status === 'active' ? 'inactive' : 'active'
|
||||
await api.put(`/dev/risk-control/rules/${rule.id}/status`, { status: newStatus })
|
||||
toast.success('状态更新成功')
|
||||
fetchRules()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('更新风控规则失败:', error)
|
||||
toast.error(error.message || '更新失败')
|
||||
console.error('切换状态失败:', error)
|
||||
toast.error(error.message || '状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(rule: RiskRule) {
|
||||
deleteTarget.value = rule
|
||||
deleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!deleteTarget.value)
|
||||
return
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/risk-control/rules/${deleteTarget.value.id}`)
|
||||
toast.success('删除成功')
|
||||
fetchRules()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('删除风控规则失败:', error)
|
||||
toast.error(error.message || '删除失败')
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
deleteTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function confirmBatchDelete(ids: (string | number)[]) {
|
||||
batchDeleteIds.value = ids
|
||||
batchDeleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
try {
|
||||
await api.delete('/dev/risk-control/rules/batch', { ids: batchDeleteIds.value } as any)
|
||||
toast.success('批量删除成功')
|
||||
fetchRules()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量删除失败:', error)
|
||||
toast.error(error.message || '批量删除失败')
|
||||
}
|
||||
finally {
|
||||
batchDeleteIds.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function batchToggleStatus(ids: (string | number)[], status: string) {
|
||||
try {
|
||||
await api.put('/dev/risk-control/rules/batch/status', { ids, status })
|
||||
toast.success('批量更新状态成功')
|
||||
fetchRules()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量更新状态失败:', error)
|
||||
toast.error(error.message || '批量更新状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRule()
|
||||
fetchRules()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
title="编辑规则"
|
||||
description="编辑现有风控规则"
|
||||
:breadcrumbs="[
|
||||
{ title: '风控管理', href: '/developer/risk-control' },
|
||||
{ title: '编辑规则' },
|
||||
]"
|
||||
title="风控管理"
|
||||
description="管理封禁的IP、设备、用户等,保护应用安全"
|
||||
sticky
|
||||
>
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<template #actions>
|
||||
<UiButton @click="goToCreate">
|
||||
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
|
||||
添加规则
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:shield-edit" class="size-5" />
|
||||
规则配置
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>修改风控规则的类型和封禁目标</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-3">
|
||||
<UiLabel>规则类型</UiLabel>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<button
|
||||
v-for="option in typeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="flex items-start gap-3 p-4 rounded-lg border text-left transition-all duration-200"
|
||||
:class="form.type === option.value ? 'border-primary bg-primary/5 ring-1 ring-primary' : 'border-border hover:border-primary/50 hover:bg-muted/50'"
|
||||
@click="form.type = option.value"
|
||||
>
|
||||
<Icon
|
||||
:icon="option.icon"
|
||||
class="size-5 mt-0.5 transition-colors duration-200"
|
||||
:class="form.type === option.value ? 'text-primary' : 'text-muted-foreground'"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
class="font-medium transition-colors duration-200"
|
||||
:class="form.type === option.value ? 'text-primary' : ''"
|
||||
>
|
||||
{{ option.label }}
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ option.description }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
生效中
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:shield-check" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ activeCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="value">
|
||||
封禁值
|
||||
</UiLabel>
|
||||
<CountrySelect
|
||||
v-if="form.type === 'region'"
|
||||
v-model="form.value"
|
||||
/>
|
||||
<UiInput
|
||||
v-else
|
||||
id="value"
|
||||
:key="form.type"
|
||||
v-model="form.value"
|
||||
:placeholder="getTypeInfo(form.type).placeholder"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ form.type === 'region' ? '选择要封禁的国家或地区' : '根据规则类型输入对应的封禁目标' }}
|
||||
</p>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
已失效
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:shield-off" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ inactiveCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="reason">
|
||||
封禁原因
|
||||
</UiLabel>
|
||||
<UiInput id="reason" v-model="form.reason" placeholder="输入封禁原因(可选)" />
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
IP封禁
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:globe" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ ipCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="expires">
|
||||
过期时间
|
||||
</UiLabel>
|
||||
<UiInput id="expires" v-model="form.expires_at" type="datetime-local" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
留空表示永久封禁
|
||||
</p>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Icon icon="lucide:info" class="size-5" />
|
||||
规则预览
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">规则类型</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon :icon="getTypeInfo(form.type).icon" class="size-4" />
|
||||
{{ getTypeInfo(form.type).label }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">封禁值</span>
|
||||
<span class="font-mono">
|
||||
{{ form.type === 'region' && form.value ? getCountryDisplay(form.value) : (form.value || '-') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">封禁原因</span>
|
||||
<span>{{ form.reason || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">过期时间</span>
|
||||
<span>{{ form.expires_at ? new Date(form.expires_at).toLocaleString('zh-CN') : '永久' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving || !form.value.trim()"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Icon v-else icon="lucide:save" class="mr-2 h-4 w-4" />
|
||||
保存修改
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
取消
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
设备封禁
|
||||
</UiCardTitle>
|
||||
<Icon icon="lucide:smartphone" class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ deviceCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
ref="tableRef"
|
||||
:loading
|
||||
:data="filteredRules"
|
||||
:on-toggle-status="toggleStatus"
|
||||
:on-edit="openEditPage"
|
||||
:on-delete="confirmDelete"
|
||||
@refresh="fetchRules"
|
||||
@batch-disable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'active').map((r: any) => r.original.id) || [], 'inactive')"
|
||||
@batch-enable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'inactive').map((r: any) => r.original.id) || [], 'active')"
|
||||
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||
>
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="typeFilter"
|
||||
title="规则类型"
|
||||
:options="typeOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
title="状态"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
confirm-button-text="删除"
|
||||
cancel-button-text="取消"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
删除风控规则
|
||||
</template>
|
||||
<template #description>
|
||||
确定要删除该风控规则吗?此操作不可撤销。
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
confirm-button-text="删除"
|
||||
cancel-button-text="取消"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
批量删除风控规则
|
||||
</template>
|
||||
<template #description>
|
||||
确定要删除选中的 {{ batchDeleteIds.length }} 条规则吗?此操作不可撤销。
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import { MoreHorizontal, Pencil, ShieldCheck, ShieldOff, Trash2 } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { RiskRule } from '@/pages/developer/risk-control/data/schema'
|
||||
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
|
||||
|
||||
import { Copy } from '@/components/sva-ui/copy'
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
|
||||
import type { RiskRule } from '@/pages/developer/risk-control/data/schema'
|
||||
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
import type { DataTableProps } from '@/components/data-table/types'
|
||||
import type { RiskRule } from '@/pages/developer/risk-control/data/schema'
|
||||
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
|
||||
|
||||
import BulkActions from '@/components/data-table/bulk-actions.vue'
|
||||
import DataTable from '@/components/data-table/data-table.vue'
|
||||
import { SelectColumn } from '@/components/data-table/table-columns'
|
||||
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||
import DataTableViewOptions from '@/components/data-table/view-options.vue'
|
||||
import { getColumns } from '@/pages/developer/risk-control/components/columns'
|
||||
import DataTableToolbar from '@/pages/developer/risk-control/components/data-table-toolbar.vue'
|
||||
import { getColumns } from '@/pages/admin/risk-control/components/columns'
|
||||
import DataTableToolbar from '@/pages/admin/risk-control/components/data-table-toolbar.vue'
|
||||
|
||||
const props = defineProps<Omit<DataTableProps<RiskRule>, 'columns'> & {
|
||||
onToggleStatus: (row: RiskRule) => void
|
||||
|
||||
@@ -165,7 +165,7 @@ async function handleSave() {
|
||||
|
||||
await api.post('/dev/risk-control/rules', payload)
|
||||
toast.success('添加成功')
|
||||
router.push('/developer/risk-control')
|
||||
router.push('/admin/risk-control')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('添加风控规则失败:', error)
|
||||
@@ -182,7 +182,7 @@ async function handleSave() {
|
||||
title="添加规则"
|
||||
description="创建新的风控规则"
|
||||
:breadcrumbs="[
|
||||
{ title: '风控管理', href: '/developer/risk-control' },
|
||||
{ title: '风控管理', href: '/admin/risk-control' },
|
||||
{ title: '添加规则' },
|
||||
]"
|
||||
sticky
|
||||
|
||||
@@ -4,12 +4,12 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { RiskRule } from '@/pages/developer/risk-control/data/schema'
|
||||
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/developer/risk-control/components/data-table.vue'
|
||||
import DataTable from '@/pages/admin/risk-control/components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -73,11 +73,11 @@ async function fetchRules() {
|
||||
}
|
||||
|
||||
function goToCreate() {
|
||||
router.push('/developer/risk-control/create')
|
||||
router.push('/admin/risk-control/create')
|
||||
}
|
||||
|
||||
function openEditPage(rule: RiskRule) {
|
||||
router.push(`/developer/risk-control/${rule.id}`)
|
||||
router.push(`/admin/risk-control/${rule.id}`)
|
||||
}
|
||||
|
||||
async function toggleStatus(rule: RiskRule) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user