refactor: change agent cloud data permission to cloud variables only
- Rename permission from '查看云端数据' to '查看云端变量' - Remove cloud constants API and page for agents - Add cloud variable records API for agents (stream type) - Add agent cloud-variables page with full variable details - Add agent cloud-variable records page (read-only) - Update navigation from cloud-data to cloud-variables - Update i18n keys: cloudData -> cloudVariables - Backend returns var_type, default_value, max_records etc. - Records page supports dynamic columns and date filtering
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -26,7 +27,7 @@ func SetupAgentRoutes(r *gin.RouterGroup) {
|
|||||||
r.PUT("/profile", handleUpdateProfile)
|
r.PUT("/profile", handleUpdateProfile)
|
||||||
r.GET("/cards/export", handleExportCards)
|
r.GET("/cards/export", handleExportCards)
|
||||||
r.GET("/cloud-variables", handleGetCloudVariables)
|
r.GET("/cloud-variables", handleGetCloudVariables)
|
||||||
r.GET("/cloud-constants", handleGetCloudConstants)
|
r.GET("/cloud-variables/:id/records", handleGetCloudVariableRecords)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetStats(c *gin.Context) {
|
func handleGetStats(c *gin.Context) {
|
||||||
@@ -778,7 +779,7 @@ func handleGetCloudVariables(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !agent.CanViewCloudData {
|
if !agent.CanViewCloudData {
|
||||||
response.Error(c, 403, "无权查看云端数据")
|
response.Error(c, 403, "无权查看云端变量")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,14 +822,18 @@ func handleGetCloudVariables(c *gin.Context) {
|
|||||||
appName = appMap[*v.AppID]
|
appName = appMap[*v.AppID]
|
||||||
}
|
}
|
||||||
result = append(result, gin.H{
|
result = append(result, gin.H{
|
||||||
"id": v.ID,
|
"id": v.ID,
|
||||||
"name": v.Key,
|
"key": v.Key,
|
||||||
"app_id": v.AppID,
|
"app_id": v.AppID,
|
||||||
"app_name": appName,
|
"application_name": appName,
|
||||||
"scope": v.Scope,
|
"default_value": v.DefaultValue,
|
||||||
"status": v.Status,
|
"var_type": v.VarType,
|
||||||
"description": v.Description,
|
"max_records": v.MaxRecords,
|
||||||
"created_at": v.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
"scope": v.Scope,
|
||||||
|
"write_permission": v.WritePermission,
|
||||||
|
"status": v.Status,
|
||||||
|
"description": v.Description,
|
||||||
|
"created_at": v.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,7 +843,7 @@ func handleGetCloudVariables(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetCloudConstants(c *gin.Context) {
|
func handleGetCloudVariableRecords(c *gin.Context) {
|
||||||
userID := c.GetUint("user_id")
|
userID := c.GetUint("user_id")
|
||||||
|
|
||||||
var agent model.User
|
var agent model.User
|
||||||
@@ -848,62 +853,95 @@ func handleGetCloudConstants(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !agent.CanViewCloudData {
|
if !agent.CanViewCloudData {
|
||||||
response.Error(c, 403, "无权查看云端数据")
|
response.Error(c, 403, "无权查看云端变量")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
variableID := c.Param("id")
|
||||||
|
var variable model.CloudVariable
|
||||||
|
if err := database.DB.First(&variable, variableID).Error; err != nil {
|
||||||
|
response.Error(c, 404, "云端变量不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var agentApps []model.AgentApplication
|
var agentApps []model.AgentApplication
|
||||||
database.DB.Where("agent_id = ?", userID).Find(&agentApps)
|
database.DB.Where("agent_id = ?", userID).Find(&agentApps)
|
||||||
appIDs := make([]uint, 0)
|
appIDs := make(map[uint]bool)
|
||||||
for _, app := range agentApps {
|
for _, app := range agentApps {
|
||||||
appIDs = append(appIDs, app.ApplicationID)
|
appIDs[app.ApplicationID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(appIDs) == 0 {
|
if variable.AppID == nil || !appIDs[*variable.AppID] {
|
||||||
response.Success(c, gin.H{
|
response.Error(c, 403, "无权查看该变量的记录")
|
||||||
"constants": []interface{}{},
|
|
||||||
"total": 0,
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
appIDFilter := c.Query("app_id")
|
if variable.VarType != "stream" {
|
||||||
var constants []model.CloudConstant
|
response.Error(c, 400, "该变量不是记录类型")
|
||||||
query := database.DB.Where("app_id IN ?", appIDs)
|
return
|
||||||
if appIDFilter != "" {
|
|
||||||
if aid, err := strconv.ParseUint(appIDFilter, 10, 32); err == nil {
|
|
||||||
query = database.DB.Where("app_id = ?", uint(aid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
query.Find(&constants)
|
|
||||||
|
|
||||||
appMap := make(map[uint]string)
|
|
||||||
var apps []model.Application
|
|
||||||
database.DB.Where("id IN ?", appIDs).Find(&apps)
|
|
||||||
for _, app := range apps {
|
|
||||||
appMap[app.ID] = app.Name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]gin.H, 0)
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
for _, c := range constants {
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
appName := ""
|
if page < 1 {
|
||||||
if c.AppID != nil {
|
page = 1
|
||||||
appName = appMap[*c.AppID]
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
startDate := c.Query("start_date")
|
||||||
|
endDate := c.Query("end_date")
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
query := database.DB.Model(&model.CloudVariableRecord{}).Where("cloud_variable_id = ?", variable.ID)
|
||||||
|
if startDate != "" {
|
||||||
|
query = query.Where("created_at >= ?", startDate+" 00:00:00")
|
||||||
|
}
|
||||||
|
if endDate != "" {
|
||||||
|
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
||||||
|
}
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
var records []model.CloudVariableRecord
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&records).Error; err != nil {
|
||||||
|
response.Error(c, 500, "获取记录失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]gin.H, len(records))
|
||||||
|
for i, r := range records {
|
||||||
|
var parsedData interface{}
|
||||||
|
if r.Data != "" {
|
||||||
|
json.Unmarshal([]byte(r.Data), &parsedData)
|
||||||
}
|
}
|
||||||
result = append(result, gin.H{
|
if parsedData == nil && r.Data != "" {
|
||||||
"id": c.ID,
|
parsedData = r.Data
|
||||||
"name": c.Key,
|
}
|
||||||
"app_id": c.AppID,
|
record := gin.H{
|
||||||
"app_name": appName,
|
"id": r.ID,
|
||||||
"value": c.Value,
|
"data": parsedData,
|
||||||
"status": c.Status,
|
"created_at": r.CreatedAt,
|
||||||
"description": c.Description,
|
}
|
||||||
"created_at": c.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
if r.AppUserID != nil {
|
||||||
})
|
record["user_id"] = r.AppUserID
|
||||||
|
var appUser model.AppUser
|
||||||
|
if err := database.DB.Select("id, username").First(&appUser, *r.AppUserID).Error; err == nil {
|
||||||
|
record["user"] = gin.H{
|
||||||
|
"id": appUser.ID,
|
||||||
|
"username": appUser.Username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[i] = record
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, gin.H{
|
response.Success(c, gin.H{
|
||||||
"constants": result,
|
"records": result,
|
||||||
"total": len(result),
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ const navMain = computed(() => {
|
|||||||
|
|
||||||
if (user.can_view_cloud_data) {
|
if (user.can_view_cloud_data) {
|
||||||
groups[0].items.push({
|
groups[0].items.push({
|
||||||
title: '云端数据',
|
title: t('nav.cloudVariables'),
|
||||||
url: '/agent/cloud-data',
|
url: '/agent/cloud-variables',
|
||||||
icon: Cloud,
|
icon: Cloud,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ const breadcrumbs = computed(() => {
|
|||||||
cards: t('nav.cards'),
|
cards: t('nav.cards'),
|
||||||
users: t('nav.users'),
|
users: t('nav.users'),
|
||||||
finance: t('nav.finance'),
|
finance: t('nav.finance'),
|
||||||
'cloud-data': '云端数据',
|
'cloud-variables': t('nav.cloudVariables'),
|
||||||
profile: t('nav.profile'),
|
profile: t('nav.profile'),
|
||||||
create: t('nav.create'),
|
create: t('nav.create'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,314 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { Cloud, Database, Eye, Globe, Loader2, Variable } from 'lucide-vue-next'
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import { BasicPage } from '@/components/global-layout'
|
|
||||||
import api from '@/services/api'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
interface Application {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CloudItem {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
app_id: number | null
|
|
||||||
app_name: string
|
|
||||||
scope?: string
|
|
||||||
status: string
|
|
||||||
value?: string
|
|
||||||
description?: string
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const loading = ref(true)
|
|
||||||
const activeTab = ref('variables')
|
|
||||||
const applications = ref<Application[]>([])
|
|
||||||
const appFilter = ref<string>('')
|
|
||||||
const variables = ref<CloudItem[]>([])
|
|
||||||
const constants = ref<CloudItem[]>([])
|
|
||||||
|
|
||||||
const filteredVariables = computed(() => {
|
|
||||||
if (!appFilter.value) return variables.value
|
|
||||||
return variables.value.filter(v => String(v.app_id) === appFilter.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredConstants = computed(() => {
|
|
||||||
if (!appFilter.value) return constants.value
|
|
||||||
return constants.value.filter(c => String(c.app_id) === appFilter.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const stats = computed(() => ({
|
|
||||||
variables: variables.value.length,
|
|
||||||
constants: constants.value.length,
|
|
||||||
applications: applications.value.length,
|
|
||||||
}))
|
|
||||||
|
|
||||||
async function fetchApplications() {
|
|
||||||
try {
|
|
||||||
const data = await api.get<{ apps: Application[] }>('/agent/apps')
|
|
||||||
applications.value = data?.apps || []
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
applications.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchVariables() {
|
|
||||||
try {
|
|
||||||
const data = await api.get<{ variables: CloudItem[] }>('/agent/cloud-variables')
|
|
||||||
variables.value = data?.variables || []
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
variables.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchConstants() {
|
|
||||||
try {
|
|
||||||
const data = await api.get<{ constants: CloudItem[] }>('/agent/cloud-constants')
|
|
||||||
constants.value = data?.constants || []
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
constants.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAppName(appId: number | null) {
|
|
||||||
if (!appId) return '-'
|
|
||||||
const app = applications.value.find(a => a.id === appId)
|
|
||||||
return app?.name || String(appId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(dateStr: string) {
|
|
||||||
if (!dateStr) return '-'
|
|
||||||
return dateStr.replace('T', ' ').substring(0, 19)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
loading.value = true
|
|
||||||
await Promise.all([fetchApplications(), fetchVariables(), fetchConstants()])
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BasicPage
|
|
||||||
:title="t('agent.cloudData.title')"
|
|
||||||
:description="t('agent.cloudData.description')"
|
|
||||||
:breadcrumbs="[
|
|
||||||
{ title: t('nav.dashboard'), href: '/agent' },
|
|
||||||
{ title: t('nav.cloudData') },
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div class="grid gap-4 md:grid-cols-3">
|
|
||||||
<UiCard>
|
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<UiCardTitle class="text-sm font-medium">
|
|
||||||
{{ t('agent.cloudData.variables') }}
|
|
||||||
</UiCardTitle>
|
|
||||||
<Variable class="size-4 text-muted-foreground" />
|
|
||||||
</UiCardHeader>
|
|
||||||
<UiCardContent>
|
|
||||||
<div class="text-2xl font-bold">
|
|
||||||
{{ stats.variables }}
|
|
||||||
</div>
|
|
||||||
</UiCardContent>
|
|
||||||
</UiCard>
|
|
||||||
<UiCard>
|
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<UiCardTitle class="text-sm font-medium">
|
|
||||||
{{ t('agent.cloudData.constants') }}
|
|
||||||
</UiCardTitle>
|
|
||||||
<Database class="size-4 text-muted-foreground" />
|
|
||||||
</UiCardHeader>
|
|
||||||
<UiCardContent>
|
|
||||||
<div class="text-2xl font-bold">
|
|
||||||
{{ stats.constants }}
|
|
||||||
</div>
|
|
||||||
</UiCardContent>
|
|
||||||
</UiCard>
|
|
||||||
<UiCard>
|
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<UiCardTitle class="text-sm font-medium">
|
|
||||||
{{ t('agent.cloudData.applications') }}
|
|
||||||
</UiCardTitle>
|
|
||||||
<Globe class="size-4 text-muted-foreground" />
|
|
||||||
</UiCardHeader>
|
|
||||||
<UiCardContent>
|
|
||||||
<div class="text-2xl font-bold">
|
|
||||||
{{ stats.applications }}
|
|
||||||
</div>
|
|
||||||
</UiCardContent>
|
|
||||||
</UiCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UiCard>
|
|
||||||
<UiCardHeader>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<UiCardTitle class="flex items-center gap-2">
|
|
||||||
<Cloud class="size-5" />
|
|
||||||
{{ t('agent.cloudData.dataList') }}
|
|
||||||
</UiCardTitle>
|
|
||||||
<UiCardDescription>
|
|
||||||
{{ t('agent.cloudData.readOnlyHint') }}
|
|
||||||
</UiCardDescription>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<UiSelect v-model="appFilter">
|
|
||||||
<UiSelectTrigger class="w-[180px]">
|
|
||||||
<UiSelectValue :placeholder="t('agent.cloudData.allApps')" />
|
|
||||||
</UiSelectTrigger>
|
|
||||||
<UiSelectContent>
|
|
||||||
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
|
|
||||||
{{ app.name }}
|
|
||||||
</UiSelectItem>
|
|
||||||
</UiSelectContent>
|
|
||||||
</UiSelect>
|
|
||||||
<div class="flex border rounded-lg overflow-hidden">
|
|
||||||
<button
|
|
||||||
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
||||||
:class="activeTab === 'variables' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
|
||||||
@click="activeTab = 'variables'"
|
|
||||||
>
|
|
||||||
{{ t('agent.cloudData.variables') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
||||||
:class="activeTab === 'constants' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
|
||||||
@click="activeTab = 'constants'"
|
|
||||||
>
|
|
||||||
{{ t('agent.cloudData.constants') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</UiCardHeader>
|
|
||||||
<UiCardContent>
|
|
||||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
|
||||||
<Loader2 class="size-8 animate-spin text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<div v-if="activeTab === 'variables'">
|
|
||||||
<div v-if="filteredVariables.length === 0" class="text-center py-12 text-muted-foreground">
|
|
||||||
{{ t('agent.cloudData.noVariables') }}
|
|
||||||
</div>
|
|
||||||
<div v-else class="border rounded-lg overflow-hidden">
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<thead class="bg-muted/50">
|
|
||||||
<tr>
|
|
||||||
<th class="text-left p-3 font-medium">
|
|
||||||
{{ t('agent.cloudData.columns.name') }}
|
|
||||||
</th>
|
|
||||||
<th class="text-left p-3 font-medium">
|
|
||||||
{{ t('agent.cloudData.columns.application') }}
|
|
||||||
</th>
|
|
||||||
<th class="text-left p-3 font-medium">
|
|
||||||
{{ t('agent.cloudData.columns.scope') }}
|
|
||||||
</th>
|
|
||||||
<th class="text-left p-3 font-medium">
|
|
||||||
{{ t('agent.cloudData.columns.status') }}
|
|
||||||
</th>
|
|
||||||
<th class="text-left p-3 font-medium">
|
|
||||||
{{ t('agent.cloudData.columns.createdAt') }}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="v in filteredVariables" :key="v.id" class="border-t hover:bg-muted/30 transition-colors">
|
|
||||||
<td class="p-3 font-medium">
|
|
||||||
{{ v.name }}
|
|
||||||
</td>
|
|
||||||
<td class="p-3 text-muted-foreground">
|
|
||||||
{{ v.app_name || getAppName(v.app_id) }}
|
|
||||||
</td>
|
|
||||||
<td class="p-3">
|
|
||||||
<UiBadge variant="outline">
|
|
||||||
{{ v.scope || 'global' }}
|
|
||||||
</UiBadge>
|
|
||||||
</td>
|
|
||||||
<td class="p-3">
|
|
||||||
<UiBadge :variant="v.status === 'active' ? 'default' : 'secondary'">
|
|
||||||
{{ v.status === 'active' ? t('agent.cloudData.statusActive') : t('agent.cloudData.statusInactive') }}
|
|
||||||
</UiBadge>
|
|
||||||
</td>
|
|
||||||
<td class="p-3 text-muted-foreground">
|
|
||||||
{{ formatDate(v.created_at) }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="activeTab === 'constants'">
|
|
||||||
<div v-if="filteredConstants.length === 0" class="text-center py-12 text-muted-foreground">
|
|
||||||
{{ t('agent.cloudData.noConstants') }}
|
|
||||||
</div>
|
|
||||||
<div v-else class="border rounded-lg overflow-hidden">
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<thead class="bg-muted/50">
|
|
||||||
<tr>
|
|
||||||
<th class="text-left p-3 font-medium">
|
|
||||||
{{ t('agent.cloudData.columns.name') }}
|
|
||||||
</th>
|
|
||||||
<th class="text-left p-3 font-medium">
|
|
||||||
{{ t('agent.cloudData.columns.application') }}
|
|
||||||
</th>
|
|
||||||
<th class="text-left p-3 font-medium">
|
|
||||||
{{ t('agent.cloudData.columns.value') }}
|
|
||||||
</th>
|
|
||||||
<th class="text-left p-3 font-medium">
|
|
||||||
{{ t('agent.cloudData.columns.status') }}
|
|
||||||
</th>
|
|
||||||
<th class="text-left p-3 font-medium">
|
|
||||||
{{ t('agent.cloudData.columns.createdAt') }}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="c in filteredConstants" :key="c.id" class="border-t hover:bg-muted/30 transition-colors">
|
|
||||||
<td class="p-3 font-medium">
|
|
||||||
{{ c.name }}
|
|
||||||
</td>
|
|
||||||
<td class="p-3 text-muted-foreground">
|
|
||||||
{{ c.app_name || getAppName(c.app_id) }}
|
|
||||||
</td>
|
|
||||||
<td class="p-3 text-muted-foreground max-w-[200px] truncate">
|
|
||||||
{{ c.value }}
|
|
||||||
</td>
|
|
||||||
<td class="p-3">
|
|
||||||
<UiBadge :variant="c.status === 'active' ? 'default' : 'secondary'">
|
|
||||||
{{ c.status === 'active' ? t('agent.cloudData.statusActive') : t('agent.cloudData.statusInactive') }}
|
|
||||||
</UiBadge>
|
|
||||||
</td>
|
|
||||||
<td class="p-3 text-muted-foreground">
|
|
||||||
{{ formatDate(c.created_at) }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<Eye class="size-4" />
|
|
||||||
<span>{{ t('agent.cloudData.readOnlyNote') }}</span>
|
|
||||||
</div>
|
|
||||||
</UiCardContent>
|
|
||||||
</UiCard>
|
|
||||||
</div>
|
|
||||||
</BasicPage>
|
|
||||||
</template>
|
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ColumnDef } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
import { List, Search } from 'lucide-vue-next'
|
||||||
|
import { computed, h, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import DataTable from '@/components/data-table/data-table.vue'
|
||||||
|
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||||
|
import DataTableViewOptions from '@/components/data-table/view-options.vue'
|
||||||
|
import DateTimePicker from '@/components/ui/date-picker/DateTimePicker.vue'
|
||||||
|
import { BasicPage } from '@/components/global-layout'
|
||||||
|
import Badge from '@/components/ui/badge/Badge.vue'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import api from '@/services/api'
|
||||||
|
|
||||||
|
interface Variable {
|
||||||
|
id: number
|
||||||
|
key: string
|
||||||
|
var_type: string
|
||||||
|
scope: string
|
||||||
|
max_records: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VariableRecord {
|
||||||
|
id: number
|
||||||
|
data: any
|
||||||
|
user_id?: number
|
||||||
|
user?: {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRecordData(record: VariableRecord): Record<string, any> {
|
||||||
|
const rawData = record.data
|
||||||
|
if (rawData == null || rawData === '') return {}
|
||||||
|
if (typeof rawData === 'object') {
|
||||||
|
if (Array.isArray(rawData)) {
|
||||||
|
if (rawData.length > 0 && typeof rawData[0] === 'object' && rawData[0] !== null && !Array.isArray(rawData[0])) {
|
||||||
|
return rawData[0] as Record<string, any>
|
||||||
|
}
|
||||||
|
const obj: Record<string, any> = {}
|
||||||
|
rawData.forEach((item, index) => { obj[`item_${index}`] = item })
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
return rawData as Record<string, any>
|
||||||
|
}
|
||||||
|
if (typeof rawData === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawData)
|
||||||
|
if (parsed != null && typeof parsed === 'object') {
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
if (parsed.length > 0 && typeof parsed[0] === 'object' && parsed[0] !== null && !Array.isArray(parsed[0])) {
|
||||||
|
return parsed[0] as Record<string, any>
|
||||||
|
}
|
||||||
|
const obj: Record<string, any> = {}
|
||||||
|
parsed.forEach((item: any, index: number) => { obj[`item_${index}`] = item })
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
return parsed as Record<string, any>
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const variable = ref<Variable | null>(null)
|
||||||
|
const records = ref<VariableRecord[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const dateRange = ref<{ from: string, to: string }>({ from: '', to: '' })
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
const dynamicKeys = computed(() => {
|
||||||
|
const keys = new Set<string>()
|
||||||
|
records.value.forEach((record) => {
|
||||||
|
const data = parseRecordData(record)
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
Object.keys(data).forEach(key => keys.add(key))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Array.from(keys).sort()
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = computed<ColumnDef<VariableRecord>[]>(() => {
|
||||||
|
const cols: ColumnDef<VariableRecord>[] = []
|
||||||
|
|
||||||
|
cols.push({
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: () => 'ID',
|
||||||
|
cell: ({ row }) => h('div', { class: 'max-w-xs truncate' }, String(row.original.id)),
|
||||||
|
size: 80,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (variable.value?.scope === 'user') {
|
||||||
|
cols.push({
|
||||||
|
accessorKey: 'user',
|
||||||
|
header: () => t('agent.cloudVariables.records.user'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const user = row.original.user
|
||||||
|
if (user) {
|
||||||
|
return h(Badge, { variant: 'outline' }, () => user.username)
|
||||||
|
}
|
||||||
|
return h('span', { class: 'text-muted-foreground' }, '-')
|
||||||
|
},
|
||||||
|
size: 120,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicKeys.value.forEach((key) => {
|
||||||
|
cols.push({
|
||||||
|
id: `data_${key}`,
|
||||||
|
header: () => key,
|
||||||
|
accessorFn: (row) => {
|
||||||
|
const data = parseRecordData(row)
|
||||||
|
return data?.[key]
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const data = parseRecordData(row.original)
|
||||||
|
const value = data?.[key]
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return h('span', { class: 'text-muted-foreground' }, '-')
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return h('div', { class: 'max-w-xs truncate', title: JSON.stringify(value) }, JSON.stringify(value))
|
||||||
|
}
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return h(Badge, { variant: value ? 'default' : 'secondary' }, () => String(value))
|
||||||
|
}
|
||||||
|
return h('div', { class: 'max-w-xs truncate', title: String(value) }, String(value))
|
||||||
|
},
|
||||||
|
size: 150,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (dynamicKeys.value.length === 0) {
|
||||||
|
cols.push({
|
||||||
|
id: 'data',
|
||||||
|
header: () => t('agent.cloudVariables.records.data'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const data = parseRecordData(row.original)
|
||||||
|
if (!data || Object.keys(data).length === 0) {
|
||||||
|
return h('span', { class: 'text-muted-foreground' }, '-')
|
||||||
|
}
|
||||||
|
return h('div', { class: 'max-w-md truncate', title: JSON.stringify(data) }, JSON.stringify(data))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cols.push({
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: () => t('agent.cloudVariables.records.createdAt'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const createdAt = row.original.created_at
|
||||||
|
if (!createdAt) return '-'
|
||||||
|
try {
|
||||||
|
const date = new Date(createdAt)
|
||||||
|
if (Number.isNaN(date.getTime())) return '-'
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
size: 160,
|
||||||
|
})
|
||||||
|
|
||||||
|
return cols
|
||||||
|
})
|
||||||
|
|
||||||
|
const serverPagination = computed(() => ({
|
||||||
|
page: page.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
total: total.value,
|
||||||
|
onPageChange: (newPage: number) => {
|
||||||
|
page.value = newPage
|
||||||
|
},
|
||||||
|
onPageSizeChange: (newPageSize: number) => {
|
||||||
|
pageSize.value = newPageSize
|
||||||
|
page.value = 1
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const table = generateVueTable<VariableRecord>({
|
||||||
|
get data() { return records.value },
|
||||||
|
get loading() { return loading.value },
|
||||||
|
get columns() { return columns.value },
|
||||||
|
serverPagination: serverPagination.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columnLabels = computed(() => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
id: 'ID',
|
||||||
|
created_at: 'agent.cloudVariables.records.createdAt',
|
||||||
|
}
|
||||||
|
if (variable.value?.scope === 'user') {
|
||||||
|
labels.user = 'agent.cloudVariables.records.user'
|
||||||
|
}
|
||||||
|
dynamicKeys.value.forEach((key) => {
|
||||||
|
labels[`data_${key}`] = key
|
||||||
|
})
|
||||||
|
return labels
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([page, pageSize], () => {
|
||||||
|
fetchRecords()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(dateRange, (newVal, oldVal) => {
|
||||||
|
if (newVal.from !== oldVal.from || newVal.to !== oldVal.to) {
|
||||||
|
page.value = 1
|
||||||
|
fetchRecords()
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
async function fetchVariable() {
|
||||||
|
try {
|
||||||
|
const data = await api.get<{ variables: Variable[] }>(`/agent/cloud-variables`)
|
||||||
|
const vars = data?.variables || []
|
||||||
|
variable.value = vars.find((v: Variable) => String(v.id) === route.params.id) || null
|
||||||
|
if (variable.value && variable.value.var_type !== 'stream') {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRecords() {
|
||||||
|
if (!variable.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('page', String(page.value))
|
||||||
|
params.append('page_size', String(pageSize.value))
|
||||||
|
if (dateRange.value.from) {
|
||||||
|
params.append('start_date', dateRange.value.from.split('T')[0])
|
||||||
|
}
|
||||||
|
if (dateRange.value.to) {
|
||||||
|
params.append('end_date', dateRange.value.to.split('T')[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await api.get<{ records: VariableRecord[], total: number }>(`/agent/cloud-variables/${variable.value.id}/records?${params}`)
|
||||||
|
records.value = data?.records || []
|
||||||
|
total.value = data?.total || 0
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
records.value = []
|
||||||
|
total.value = 0
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchVariable()
|
||||||
|
if (variable.value) {
|
||||||
|
fetchRecords()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BasicPage
|
||||||
|
:title="t('agent.cloudVariables.records.title')"
|
||||||
|
:description="t('agent.cloudVariables.records.description', { key: variable?.key || '' })"
|
||||||
|
:breadcrumbs="[
|
||||||
|
{ title: t('nav.dashboard'), href: '/agent' },
|
||||||
|
{ title: t('nav.cloudVariables'), href: '/agent/cloud-variables' },
|
||||||
|
{ title: t('agent.cloudVariables.records.title') },
|
||||||
|
]"
|
||||||
|
sticky
|
||||||
|
>
|
||||||
|
<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('agent.cloudVariables.records.totalRecords') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<List class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ total }}
|
||||||
|
</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('agent.cloudVariables.types.stream') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ variable?.key || '-' }}
|
||||||
|
</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('agent.cloudVariables.records.maxRecordsLabel') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ variable?.max_records || t('agent.cloudVariables.records.unlimited') }}
|
||||||
|
</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('agent.cloudVariables.columns.scope') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ variable?.scope === 'user' ? t('agent.cloudVariables.userScope') : t('agent.cloudVariables.appScope') }}
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardContent class="p-6">
|
||||||
|
<DataTable :columns="columns" :data="records" :loading="loading" :table="table" :server-pagination="serverPagination" @refresh="fetchRecords">
|
||||||
|
<template #toolbar>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<div class="relative">
|
||||||
|
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
v-model="searchQuery"
|
||||||
|
:placeholder="t('agent.cloudVariables.records.searchPlaceholder')"
|
||||||
|
class="pl-8 h-8 w-[150px] lg:w-[250px]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DateTimePicker
|
||||||
|
v-model="dateRange.from"
|
||||||
|
:placeholder="t('agent.cloudVariables.records.startDate')"
|
||||||
|
class="w-[180px]"
|
||||||
|
/>
|
||||||
|
<span class="text-muted-foreground text-sm">{{ t('agent.finance.to') }}</span>
|
||||||
|
<DateTimePicker
|
||||||
|
v-model="dateRange.to"
|
||||||
|
:placeholder="t('agent.cloudVariables.records.endDate')"
|
||||||
|
class="w-[180px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
</BasicPage>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Globe, LayoutGrid, List, User, Variable } from 'lucide-vue-next'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||||
|
import { BasicPage } from '@/components/global-layout'
|
||||||
|
import api from '@/services/api'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
interface Application {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CloudVariable {
|
||||||
|
id: number
|
||||||
|
key: string
|
||||||
|
app_id: number | null
|
||||||
|
application_name: string
|
||||||
|
default_value: string
|
||||||
|
var_type: string
|
||||||
|
max_records: number
|
||||||
|
scope: string
|
||||||
|
write_permission: string
|
||||||
|
status: string
|
||||||
|
description: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const variables = ref<CloudVariable[]>([])
|
||||||
|
const applications = ref<Application[]>([])
|
||||||
|
|
||||||
|
const appFilter = ref<string>('')
|
||||||
|
const scopeFilter = ref<string>('')
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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(() =>
|
||||||
|
applications.value.map(app => ({ label: app.name, value: String(app.id) })),
|
||||||
|
)
|
||||||
|
|
||||||
|
const scopeOptions = computed(() => [
|
||||||
|
{ label: t('agent.cloudVariables.appScope'), value: 'app' },
|
||||||
|
{ label: t('agent.cloudVariables.userScope'), value: 'user' },
|
||||||
|
])
|
||||||
|
|
||||||
|
async function fetchApplications() {
|
||||||
|
try {
|
||||||
|
const data = await api.get<{ apps: Application[] }>('/agent/apps')
|
||||||
|
applications.value = data?.apps || []
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
applications.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchVariables() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await api.get<{ variables: CloudVariable[] }>('/agent/cloud-variables')
|
||||||
|
variables.value = data?.variables || []
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
variables.value = []
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewRecords(variable: CloudVariable) {
|
||||||
|
router.push(`/agent/cloud-variables/${variable.id}/records?app_id=${variable.app_id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr: string) {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
try {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
if (Number.isNaN(date.getTime())) return '-'
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatVarType(type: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
integer: t('agent.cloudVariables.types.integer'),
|
||||||
|
decimal: t('agent.cloudVariables.types.decimal'),
|
||||||
|
string: t('agent.cloudVariables.types.string'),
|
||||||
|
binary: t('agent.cloudVariables.types.binary'),
|
||||||
|
stream: t('agent.cloudVariables.types.stream'),
|
||||||
|
}
|
||||||
|
return map[type || 'string'] || type || 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchApplications()
|
||||||
|
fetchVariables()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BasicPage
|
||||||
|
:title="t('agent.cloudVariables.title')"
|
||||||
|
:description="t('agent.cloudVariables.description')"
|
||||||
|
:breadcrumbs="[
|
||||||
|
{ title: t('nav.dashboard'), href: '/agent' },
|
||||||
|
{ title: t('nav.cloudVariables') },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<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('agent.cloudVariables.totalVariables') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<Variable class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ filteredVariables.length }}
|
||||||
|
</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('agent.cloudVariables.appScopeVariables') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<Globe class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ appScopeCount }}
|
||||||
|
</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('agent.cloudVariables.userScopeVariables') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<User class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ userScopeCount }}
|
||||||
|
</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('agent.cloudVariables.appCount') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<LayoutGrid 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">
|
||||||
|
<div class="mb-4 flex flex-wrap items-center gap-2">
|
||||||
|
<SingleFilter
|
||||||
|
v-model="appFilter"
|
||||||
|
:title="t('agent.cloudVariables.application')"
|
||||||
|
:options="applicationOptions"
|
||||||
|
/>
|
||||||
|
<SingleFilter
|
||||||
|
v-model="scopeFilter"
|
||||||
|
:title="t('agent.cloudVariables.scope')"
|
||||||
|
:options="scopeOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||||
|
<div class="size-8 animate-spin rounded-full border-4 border-muted border-t-primary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="filteredVariables.length === 0" class="text-center py-12 text-muted-foreground">
|
||||||
|
{{ t('agent.cloudVariables.noVariables') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="border rounded-lg overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left p-3 font-medium">
|
||||||
|
{{ t('agent.cloudVariables.columns.key') }}
|
||||||
|
</th>
|
||||||
|
<th class="text-left p-3 font-medium">
|
||||||
|
{{ t('agent.cloudVariables.columns.defaultValue') }}
|
||||||
|
</th>
|
||||||
|
<th class="text-left p-3 font-medium">
|
||||||
|
{{ t('agent.cloudVariables.columns.type') }}
|
||||||
|
</th>
|
||||||
|
<th class="text-left p-3 font-medium">
|
||||||
|
{{ t('agent.cloudVariables.columns.scope') }}
|
||||||
|
</th>
|
||||||
|
<th class="text-left p-3 font-medium">
|
||||||
|
{{ t('agent.cloudVariables.columns.application') }}
|
||||||
|
</th>
|
||||||
|
<th class="text-left p-3 font-medium">
|
||||||
|
{{ t('agent.cloudVariables.columns.status') }}
|
||||||
|
</th>
|
||||||
|
<th class="text-left p-3 font-medium">
|
||||||
|
{{ t('agent.cloudVariables.columns.createdAt') }}
|
||||||
|
</th>
|
||||||
|
<th class="text-left p-3 font-medium">
|
||||||
|
{{ t('common.actions') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="v in filteredVariables"
|
||||||
|
:key="v.id"
|
||||||
|
class="border-t hover:bg-muted/30 transition-colors"
|
||||||
|
>
|
||||||
|
<td class="p-3">
|
||||||
|
<code class="text-xs bg-muted px-2 py-1 rounded font-mono">{{ v.key }}</code>
|
||||||
|
</td>
|
||||||
|
<td class="p-3 text-muted-foreground max-w-[200px] truncate">
|
||||||
|
{{ v.default_value || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="p-3">
|
||||||
|
<UiBadge variant="outline">
|
||||||
|
{{ formatVarType(v.var_type) }}
|
||||||
|
</UiBadge>
|
||||||
|
</td>
|
||||||
|
<td class="p-3">
|
||||||
|
<UiBadge :variant="v.scope === 'app' ? 'default' : 'secondary'">
|
||||||
|
{{ v.scope === 'app' ? t('agent.cloudVariables.appScope') : t('agent.cloudVariables.userScope') }}
|
||||||
|
</UiBadge>
|
||||||
|
</td>
|
||||||
|
<td class="p-3">
|
||||||
|
<UiBadge v-if="v.application_name" variant="outline">
|
||||||
|
{{ v.application_name }}
|
||||||
|
</UiBadge>
|
||||||
|
<span v-else class="text-muted-foreground">-</span>
|
||||||
|
</td>
|
||||||
|
<td class="p-3">
|
||||||
|
<UiBadge :variant="v.status === 'active' ? 'default' : 'destructive'">
|
||||||
|
{{ v.status === 'active' ? t('agent.cloudVariables.statusActive') : t('agent.cloudVariables.statusInactive') }}
|
||||||
|
</UiBadge>
|
||||||
|
</td>
|
||||||
|
<td class="p-3 text-muted-foreground whitespace-nowrap">
|
||||||
|
{{ formatDate(v.created_at) }}
|
||||||
|
</td>
|
||||||
|
<td class="p-3">
|
||||||
|
<UiButton
|
||||||
|
v-if="v.var_type === 'stream'"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click="handleViewRecords(v)"
|
||||||
|
>
|
||||||
|
<List class="mr-1 size-4" />
|
||||||
|
{{ t('agent.cloudVariables.viewRecords') }}
|
||||||
|
</UiButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
</BasicPage>
|
||||||
|
</template>
|
||||||
@@ -923,7 +923,7 @@
|
|||||||
"maxApiCalls": "API Calls/Month",
|
"maxApiCalls": "API Calls/Month",
|
||||||
"maxApiCallsPlaceholder": "-1 for unlimited",
|
"maxApiCallsPlaceholder": "-1 for unlimited",
|
||||||
"allowAgent": "Agent Permission",
|
"allowAgent": "Agent Permission",
|
||||||
"allowCloudData": "Cloud Data",
|
"allowCloudData": "Cloud Variables",
|
||||||
"allowDynamicCode": "Cloud Function",
|
"allowDynamicCode": "Cloud Function",
|
||||||
"allowEmail": "Email Feature",
|
"allowEmail": "Email Feature",
|
||||||
"allowExtension": "Extension API",
|
"allowExtension": "Extension API",
|
||||||
@@ -1286,7 +1286,7 @@
|
|||||||
"cardsCount": "Cards Generated",
|
"cardsCount": "Cards Generated",
|
||||||
"childAgentsCount": "Child Agents",
|
"childAgentsCount": "Child Agents",
|
||||||
"canCreateAgent": "Create Agent Permission",
|
"canCreateAgent": "Create Agent Permission",
|
||||||
"canViewCloudData": "Cloud Data Permission",
|
"canViewCloudData": "View Cloud Variables Permission",
|
||||||
"balance": "Balance",
|
"balance": "Balance",
|
||||||
"createdAt": "Created At",
|
"createdAt": "Created At",
|
||||||
"lastLoginAt": "Last Login"
|
"lastLoginAt": "Last Login"
|
||||||
@@ -2887,27 +2887,49 @@
|
|||||||
"generateNow": "Generate Now"
|
"generateNow": "Generate Now"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cloudData": {
|
"cloudVariables": {
|
||||||
"title": "Cloud Data",
|
"title": "Cloud Variables",
|
||||||
"description": "View cloud variables and constants (read-only)",
|
"description": "View cloud variables (read-only)",
|
||||||
"variables": "Cloud Variables",
|
"totalVariables": "Total Variables",
|
||||||
"constants": "Cloud Constants",
|
"appScopeVariables": "App Scope Variables",
|
||||||
"applications": "Applications",
|
"userScopeVariables": "User Scope Variables",
|
||||||
"dataList": "Data List",
|
"appCount": "Applications",
|
||||||
"readOnlyHint": "Read-only mode, data cannot be edited",
|
|
||||||
"allApps": "All Applications",
|
|
||||||
"noVariables": "No cloud variables found",
|
"noVariables": "No cloud variables found",
|
||||||
"noConstants": "No cloud constants found",
|
"appScope": "App",
|
||||||
|
"userScope": "User",
|
||||||
"statusActive": "Active",
|
"statusActive": "Active",
|
||||||
"statusInactive": "Inactive",
|
"statusInactive": "Inactive",
|
||||||
"readOnlyNote": "Read-only - Data is for viewing only, no editing allowed",
|
"viewRecords": "View Records",
|
||||||
|
"application": "Application",
|
||||||
|
"scope": "Scope",
|
||||||
|
"types": {
|
||||||
|
"integer": "Integer",
|
||||||
|
"decimal": "Decimal",
|
||||||
|
"string": "String",
|
||||||
|
"binary": "Binary",
|
||||||
|
"stream": "Stream"
|
||||||
|
},
|
||||||
"columns": {
|
"columns": {
|
||||||
"name": "Name",
|
"key": "Key",
|
||||||
"application": "Application",
|
"defaultValue": "Default Value",
|
||||||
|
"type": "Type",
|
||||||
"scope": "Scope",
|
"scope": "Scope",
|
||||||
"value": "Value",
|
"application": "Application",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"createdAt": "Created At"
|
"createdAt": "Created At"
|
||||||
|
},
|
||||||
|
"records": {
|
||||||
|
"title": "Variable Records",
|
||||||
|
"description": "Records for variable {key}",
|
||||||
|
"totalRecords": "Total Records",
|
||||||
|
"maxRecordsLabel": "Max Records",
|
||||||
|
"unlimited": "Unlimited",
|
||||||
|
"user": "User",
|
||||||
|
"data": "Data",
|
||||||
|
"createdAt": "Created At",
|
||||||
|
"searchPlaceholder": "Search records...",
|
||||||
|
"startDate": "Start Date",
|
||||||
|
"endDate": "End Date"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2932,7 +2954,7 @@
|
|||||||
"apiCalls": "{count} API calls/month",
|
"apiCalls": "{count} API calls/month",
|
||||||
"unlimitedApiCalls": "Unlimited API calls",
|
"unlimitedApiCalls": "Unlimited API calls",
|
||||||
"agent": "Agent permission",
|
"agent": "Agent permission",
|
||||||
"cloudData": "Cloud data feature",
|
"cloudData": "Cloud Variables feature",
|
||||||
"dynamicCode": "Dynamic code feature",
|
"dynamicCode": "Dynamic code feature",
|
||||||
"email": "Email feature",
|
"email": "Email feature",
|
||||||
"extension": "Extension API",
|
"extension": "Extension API",
|
||||||
|
|||||||
@@ -924,7 +924,7 @@
|
|||||||
"maxApiCalls": "API调用次数/月",
|
"maxApiCalls": "API调用次数/月",
|
||||||
"maxApiCallsPlaceholder": "-1表示不限",
|
"maxApiCallsPlaceholder": "-1表示不限",
|
||||||
"allowAgent": "代理权限",
|
"allowAgent": "代理权限",
|
||||||
"allowCloudData": "云端数据",
|
"allowCloudData": "云端变量",
|
||||||
"allowDynamicCode": "云端函数",
|
"allowDynamicCode": "云端函数",
|
||||||
"allowEmail": "邮件功能",
|
"allowEmail": "邮件功能",
|
||||||
"allowExtension": "扩展接口",
|
"allowExtension": "扩展接口",
|
||||||
@@ -1242,7 +1242,7 @@
|
|||||||
"cardsCount": "生成卡密",
|
"cardsCount": "生成卡密",
|
||||||
"childAgentsCount": "下级代理",
|
"childAgentsCount": "下级代理",
|
||||||
"canCreateAgent": "创建代理权限",
|
"canCreateAgent": "创建代理权限",
|
||||||
"canViewCloudData": "云端数据权限",
|
"canViewCloudData": "查看云端变量权限",
|
||||||
"balance": "余额",
|
"balance": "余额",
|
||||||
"totalConsumption": "消费",
|
"totalConsumption": "消费",
|
||||||
"createdAt": "注册时间",
|
"createdAt": "注册时间",
|
||||||
@@ -2888,27 +2888,49 @@
|
|||||||
"generateNow": "立即生成"
|
"generateNow": "立即生成"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cloudData": {
|
"cloudVariables": {
|
||||||
"title": "云端数据",
|
"title": "云端变量",
|
||||||
"description": "查看云端变量和常量数据(只读)",
|
"description": "查看云端变量数据(只读)",
|
||||||
"variables": "云端变量",
|
"totalVariables": "变量总数",
|
||||||
"constants": "云端常量",
|
"appScopeVariables": "应用级变量",
|
||||||
"applications": "应用数量",
|
"userScopeVariables": "用户级变量",
|
||||||
"dataList": "数据列表",
|
"appCount": "应用数量",
|
||||||
"readOnlyHint": "只读模式,数据不可编辑",
|
|
||||||
"allApps": "全部应用",
|
|
||||||
"noVariables": "暂无云端变量数据",
|
"noVariables": "暂无云端变量数据",
|
||||||
"noConstants": "暂无云端常量数据",
|
"appScope": "应用级",
|
||||||
|
"userScope": "用户级",
|
||||||
"statusActive": "启用",
|
"statusActive": "启用",
|
||||||
"statusInactive": "禁用",
|
"statusInactive": "禁用",
|
||||||
"readOnlyNote": "只读模式 - 当前数据仅供查看,无法进行编辑操作",
|
"viewRecords": "查看记录",
|
||||||
|
"application": "应用",
|
||||||
|
"scope": "作用域",
|
||||||
|
"types": {
|
||||||
|
"integer": "整数",
|
||||||
|
"decimal": "小数",
|
||||||
|
"string": "字符串",
|
||||||
|
"binary": "二进制",
|
||||||
|
"stream": "列表"
|
||||||
|
},
|
||||||
"columns": {
|
"columns": {
|
||||||
"name": "名称",
|
"key": "变量名",
|
||||||
"application": "应用",
|
"defaultValue": "默认值",
|
||||||
|
"type": "类型",
|
||||||
"scope": "作用域",
|
"scope": "作用域",
|
||||||
"value": "值",
|
"application": "应用",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"createdAt": "创建时间"
|
"createdAt": "创建时间"
|
||||||
|
},
|
||||||
|
"records": {
|
||||||
|
"title": "变量记录",
|
||||||
|
"description": "变量 {key} 的记录列表",
|
||||||
|
"totalRecords": "记录总数",
|
||||||
|
"maxRecordsLabel": "最大记录数",
|
||||||
|
"unlimited": "无限制",
|
||||||
|
"user": "用户",
|
||||||
|
"data": "数据",
|
||||||
|
"createdAt": "创建时间",
|
||||||
|
"searchPlaceholder": "搜索记录...",
|
||||||
|
"startDate": "开始日期",
|
||||||
|
"endDate": "结束日期"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2933,7 +2955,7 @@
|
|||||||
"apiCalls": "{count}次/月 API调用",
|
"apiCalls": "{count}次/月 API调用",
|
||||||
"unlimitedApiCalls": "无限API调用",
|
"unlimitedApiCalls": "无限API调用",
|
||||||
"agent": "代理权限",
|
"agent": "代理权限",
|
||||||
"cloudData": "云端数据功能",
|
"cloudData": "云端变量功能",
|
||||||
"dynamicCode": "云端函数功能",
|
"dynamicCode": "云端函数功能",
|
||||||
"email": "邮件功能",
|
"email": "邮件功能",
|
||||||
"extension": "扩展接口功能",
|
"extension": "扩展接口功能",
|
||||||
|
|||||||
@@ -451,10 +451,16 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: { title: '财务管理 - 代理商后台' },
|
meta: { title: '财务管理 - 代理商后台' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'cloud-data',
|
path: 'cloud-variables',
|
||||||
name: 'AgentCloudData',
|
name: 'AgentCloudVariables',
|
||||||
component: () => import('@/pages/agent/cloud-data/index.vue'),
|
component: () => import('@/pages/agent/cloud-variables/index.vue'),
|
||||||
meta: { title: '云端数据 - 代理商后台' },
|
meta: { title: '云端变量 - 代理商后台' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'cloud-variables/:id/records',
|
||||||
|
name: 'AgentCloudVariableRecords',
|
||||||
|
component: () => import('@/pages/agent/cloud-variables/[id]/records.vue'),
|
||||||
|
meta: { title: '变量记录 - 代理商后台' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
|
|||||||
Reference in New Issue
Block a user