You've already forked agentic-coding-workflow
feat(web): admin views for LLM endpoints/models + usage dashboard
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<AppShell>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold">
|
||||
LLM Endpoints
|
||||
</h2>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>
|
||||
+ 新增
|
||||
</NButton>
|
||||
</div>
|
||||
<NDataTable
|
||||
:data="endpoints"
|
||||
:columns="columns"
|
||||
:row-key="rowKey"
|
||||
/>
|
||||
<NModal
|
||||
v-model:show="showForm"
|
||||
preset="card"
|
||||
title="LLM Endpoint"
|
||||
style="width: 520px"
|
||||
>
|
||||
<NForm>
|
||||
<NFormItem label="Provider">
|
||||
<NSelect
|
||||
v-model:value="form.provider"
|
||||
:options="providerOpts"
|
||||
:disabled="!!editing"
|
||||
@update:value="onProviderChange"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="显示名">
|
||||
<NInput v-model:value="form.display_name" />
|
||||
</NFormItem>
|
||||
<NFormItem label="Base URL">
|
||||
<NInput v-model:value="form.base_url" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="editing ? 'API Key(留空则不修改)' : 'API Key'">
|
||||
<NInput
|
||||
v-model:value="form.api_key"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<NButton @click="showForm = false">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="save"
|
||||
>
|
||||
保存
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
</div>
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, ref } from 'vue'
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NModal,
|
||||
NSelect,
|
||||
useDialog,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import {
|
||||
llmAdminApi,
|
||||
type LLMEndpoint,
|
||||
type LLMProvider,
|
||||
} from '@/api/llmAdmin'
|
||||
|
||||
const endpoints = ref<LLMEndpoint[]>([])
|
||||
const showForm = ref(false)
|
||||
const editing = ref<LLMEndpoint | null>(null)
|
||||
const form = ref<{
|
||||
provider: LLMProvider
|
||||
display_name: string
|
||||
base_url: string
|
||||
api_key: string
|
||||
}>({
|
||||
provider: 'anthropic',
|
||||
display_name: '',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
})
|
||||
const saving = ref(false)
|
||||
const msg = useMessage()
|
||||
const dlg = useDialog()
|
||||
|
||||
const providerOpts: { label: string; value: LLMProvider }[] = [
|
||||
{ label: 'Anthropic', value: 'anthropic' },
|
||||
{ label: 'OpenAI / Compatible', value: 'openai' },
|
||||
{ label: 'Gemini', value: 'gemini' },
|
||||
]
|
||||
|
||||
const defaultBaseURL: Record<LLMProvider, string> = {
|
||||
anthropic: 'https://api.anthropic.com',
|
||||
openai: 'https://api.openai.com/v1',
|
||||
gemini: 'https://generativelanguage.googleapis.com/v1beta',
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<LLMEndpoint> = [
|
||||
{ title: '显示名', key: 'display_name' },
|
||||
{ title: 'Provider', key: 'provider' },
|
||||
{ title: 'Base URL', key: 'base_url' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render: (row) =>
|
||||
h('div', { class: 'flex gap-1' }, [
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', onClick: () => testEndpoint(row.id) },
|
||||
{ default: () => '测试' },
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', onClick: () => openEdit(row) },
|
||||
{ default: () => '编辑' },
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', type: 'error', onClick: () => del(row) },
|
||||
{ default: () => '删除' },
|
||||
),
|
||||
]),
|
||||
},
|
||||
]
|
||||
|
||||
const rowKey = (row: LLMEndpoint) => row.id
|
||||
|
||||
onMounted(reload)
|
||||
|
||||
async function reload() {
|
||||
endpoints.value = await llmAdminApi.listEndpoints()
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null
|
||||
form.value = {
|
||||
provider: 'anthropic',
|
||||
display_name: '',
|
||||
base_url: defaultBaseURL.anthropic,
|
||||
api_key: '',
|
||||
}
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
function openEdit(ep: LLMEndpoint) {
|
||||
editing.value = ep
|
||||
form.value = {
|
||||
provider: ep.provider,
|
||||
display_name: ep.display_name,
|
||||
base_url: ep.base_url,
|
||||
api_key: '',
|
||||
}
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
function onProviderChange(v: LLMProvider) {
|
||||
form.value.base_url = defaultBaseURL[v]
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value) {
|
||||
await llmAdminApi.updateEndpoint(editing.value.id, {
|
||||
display_name: form.value.display_name,
|
||||
base_url: form.value.base_url,
|
||||
api_key: form.value.api_key || undefined,
|
||||
})
|
||||
} else {
|
||||
await llmAdminApi.createEndpoint({
|
||||
provider: form.value.provider,
|
||||
display_name: form.value.display_name,
|
||||
base_url: form.value.base_url,
|
||||
api_key: form.value.api_key,
|
||||
})
|
||||
}
|
||||
showForm.value = false
|
||||
await reload()
|
||||
} catch (e) {
|
||||
msg.error(e instanceof Error ? e.message : String(e))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function testEndpoint(id: string) {
|
||||
try {
|
||||
await llmAdminApi.testEndpoint(id)
|
||||
msg.success('连通正常')
|
||||
} catch (e) {
|
||||
msg.error(e instanceof Error ? e.message : String(e))
|
||||
}
|
||||
}
|
||||
|
||||
function del(ep: LLMEndpoint) {
|
||||
dlg.warning({
|
||||
title: `删除 ${ep.display_name}?`,
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await llmAdminApi.deleteEndpoint(ep.id)
|
||||
await reload()
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<AppShell>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold">
|
||||
LLM Models
|
||||
</h2>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>
|
||||
+ 新增
|
||||
</NButton>
|
||||
</div>
|
||||
<NDataTable
|
||||
:data="models"
|
||||
:columns="columns"
|
||||
:row-key="rowKey"
|
||||
/>
|
||||
<NModal
|
||||
v-model:show="showForm"
|
||||
preset="card"
|
||||
title="LLM Model"
|
||||
style="width: 720px"
|
||||
>
|
||||
<NForm>
|
||||
<NFormItem label="Endpoint">
|
||||
<NSelect
|
||||
v-model:value="form.endpoint_id"
|
||||
:options="endpointOpts"
|
||||
:disabled="!!editing"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="Model ID(provider 侧)">
|
||||
<NInput
|
||||
v-model:value="form.model_id"
|
||||
:disabled="!!editing"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="显示名">
|
||||
<NInput v-model:value="form.display_name" />
|
||||
</NFormItem>
|
||||
<NFormItem label="能力">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<NCheckbox v-model:checked="form.capabilities.image">
|
||||
Image
|
||||
</NCheckbox>
|
||||
<NCheckbox v-model:checked="form.capabilities.pdf">
|
||||
PDF
|
||||
</NCheckbox>
|
||||
<NCheckbox v-model:checked="form.capabilities.audio">
|
||||
Audio
|
||||
</NCheckbox>
|
||||
<NCheckbox v-model:checked="form.capabilities.reasoning">
|
||||
Reasoning
|
||||
</NCheckbox>
|
||||
<NCheckbox v-model:checked="form.capabilities.tools">
|
||||
Tools
|
||||
</NCheckbox>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<NFormItem label="Context Window">
|
||||
<NInputNumber
|
||||
v-model:value="form.context_window"
|
||||
:min="1"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="Max Output Tokens">
|
||||
<NInputNumber
|
||||
v-model:value="form.max_output_tokens"
|
||||
:min="1"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="Prompt Price (USD / 1M)">
|
||||
<NInputNumber
|
||||
v-model:value="form.prompt_price_per_million_usd"
|
||||
:min="0"
|
||||
:step="0.01"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="Completion Price (USD / 1M)">
|
||||
<NInputNumber
|
||||
v-model:value="form.completion_price_per_million_usd"
|
||||
:min="0"
|
||||
:step="0.01"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="Thinking Price (USD / 1M)">
|
||||
<NInputNumber
|
||||
v-model:value="form.thinking_price_per_million_usd"
|
||||
:min="0"
|
||||
:step="0.01"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="Sort Order">
|
||||
<NInputNumber
|
||||
v-model:value="form.sort_order"
|
||||
:min="0"
|
||||
/>
|
||||
</NFormItem>
|
||||
</div>
|
||||
<div class="flex gap-4 mt-2">
|
||||
<NCheckbox v-model:checked="form.enabled">
|
||||
Enabled
|
||||
</NCheckbox>
|
||||
<NCheckbox v-model:checked="form.is_title_generator">
|
||||
Title Generator
|
||||
</NCheckbox>
|
||||
</div>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<NButton @click="showForm = false">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="save"
|
||||
>
|
||||
保存
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
</div>
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import {
|
||||
NButton,
|
||||
NCheckbox,
|
||||
NDataTable,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NModal,
|
||||
NSelect,
|
||||
NTag,
|
||||
useDialog,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import {
|
||||
llmAdminApi,
|
||||
type CreateModelBody,
|
||||
type LLMEndpoint,
|
||||
type LLMModel,
|
||||
type ModelCapabilities,
|
||||
} from '@/api/llmAdmin'
|
||||
|
||||
const models = ref<LLMModel[]>([])
|
||||
const endpoints = ref<LLMEndpoint[]>([])
|
||||
const showForm = ref(false)
|
||||
const editing = ref<LLMModel | null>(null)
|
||||
const saving = ref(false)
|
||||
const msg = useMessage()
|
||||
const dlg = useDialog()
|
||||
|
||||
interface ModelForm {
|
||||
endpoint_id: string
|
||||
model_id: string
|
||||
display_name: string
|
||||
capabilities: ModelCapabilities
|
||||
context_window: number
|
||||
max_output_tokens: number
|
||||
prompt_price_per_million_usd: number
|
||||
completion_price_per_million_usd: number
|
||||
thinking_price_per_million_usd: number
|
||||
is_title_generator: boolean
|
||||
enabled: boolean
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
function blankForm(): ModelForm {
|
||||
return {
|
||||
endpoint_id: '',
|
||||
model_id: '',
|
||||
display_name: '',
|
||||
capabilities: {
|
||||
image: false,
|
||||
pdf: false,
|
||||
audio: false,
|
||||
reasoning: false,
|
||||
tools: false,
|
||||
},
|
||||
context_window: 200000,
|
||||
max_output_tokens: 8192,
|
||||
prompt_price_per_million_usd: 0,
|
||||
completion_price_per_million_usd: 0,
|
||||
thinking_price_per_million_usd: 0,
|
||||
is_title_generator: false,
|
||||
enabled: true,
|
||||
sort_order: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const form = ref<ModelForm>(blankForm())
|
||||
|
||||
const endpointOpts = computed(() =>
|
||||
endpoints.value.map((e) => ({
|
||||
label: `${e.display_name} (${e.provider})`,
|
||||
value: e.id,
|
||||
})),
|
||||
)
|
||||
|
||||
const endpointName = (id: string) =>
|
||||
endpoints.value.find((e) => e.id === id)?.display_name ?? id
|
||||
|
||||
const capLabel = (c: ModelCapabilities) => {
|
||||
const parts: string[] = []
|
||||
if (c.image) parts.push('I')
|
||||
if (c.pdf) parts.push('P')
|
||||
if (c.audio) parts.push('A')
|
||||
if (c.reasoning) parts.push('R')
|
||||
if (c.tools) parts.push('T')
|
||||
return parts.join('') || '-'
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<LLMModel> = [
|
||||
{ title: '显示名', key: 'display_name' },
|
||||
{ title: 'Model ID', key: 'model_id' },
|
||||
{
|
||||
title: 'Endpoint',
|
||||
key: 'endpoint_id',
|
||||
render: (row) => endpointName(row.endpoint_id),
|
||||
},
|
||||
{
|
||||
title: '能力',
|
||||
key: 'capabilities',
|
||||
render: (row) => capLabel(row.capabilities),
|
||||
},
|
||||
{ title: 'Ctx', key: 'context_window' },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'enabled',
|
||||
render: (row) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: row.enabled ? 'success' : 'default', size: 'small' },
|
||||
{ default: () => (row.enabled ? '启用' : '停用') },
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Title?',
|
||||
key: 'is_title_generator',
|
||||
render: (row) => (row.is_title_generator ? '✓' : ''),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render: (row) =>
|
||||
h('div', { class: 'flex gap-1' }, [
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', onClick: () => openEdit(row) },
|
||||
{ default: () => '编辑' },
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', type: 'error', onClick: () => del(row) },
|
||||
{ default: () => '删除' },
|
||||
),
|
||||
]),
|
||||
},
|
||||
]
|
||||
|
||||
const rowKey = (row: LLMModel) => row.id
|
||||
|
||||
onMounted(async () => {
|
||||
endpoints.value = await llmAdminApi.listEndpoints()
|
||||
await reload()
|
||||
})
|
||||
|
||||
async function reload() {
|
||||
models.value = await llmAdminApi.listModels(false)
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null
|
||||
form.value = blankForm()
|
||||
if (endpoints.value.length > 0) {
|
||||
form.value.endpoint_id = endpoints.value[0].id
|
||||
}
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
function openEdit(m: LLMModel) {
|
||||
editing.value = m
|
||||
form.value = {
|
||||
endpoint_id: m.endpoint_id,
|
||||
model_id: m.model_id,
|
||||
display_name: m.display_name,
|
||||
capabilities: { ...m.capabilities },
|
||||
context_window: m.context_window,
|
||||
max_output_tokens: m.max_output_tokens,
|
||||
prompt_price_per_million_usd: m.prompt_price_per_million_usd,
|
||||
completion_price_per_million_usd: m.completion_price_per_million_usd,
|
||||
thinking_price_per_million_usd: m.thinking_price_per_million_usd,
|
||||
is_title_generator: m.is_title_generator,
|
||||
enabled: m.enabled,
|
||||
sort_order: m.sort_order,
|
||||
}
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value) {
|
||||
await llmAdminApi.updateModel(editing.value.id, {
|
||||
display_name: form.value.display_name,
|
||||
capabilities: form.value.capabilities,
|
||||
context_window: form.value.context_window,
|
||||
max_output_tokens: form.value.max_output_tokens,
|
||||
prompt_price_per_million_usd: form.value.prompt_price_per_million_usd,
|
||||
completion_price_per_million_usd:
|
||||
form.value.completion_price_per_million_usd,
|
||||
thinking_price_per_million_usd:
|
||||
form.value.thinking_price_per_million_usd,
|
||||
is_title_generator: form.value.is_title_generator,
|
||||
enabled: form.value.enabled,
|
||||
sort_order: form.value.sort_order,
|
||||
})
|
||||
} else {
|
||||
const body: CreateModelBody = {
|
||||
endpoint_id: form.value.endpoint_id,
|
||||
model_id: form.value.model_id,
|
||||
display_name: form.value.display_name,
|
||||
capabilities: form.value.capabilities,
|
||||
context_window: form.value.context_window,
|
||||
max_output_tokens: form.value.max_output_tokens,
|
||||
prompt_price_per_million_usd: form.value.prompt_price_per_million_usd,
|
||||
completion_price_per_million_usd:
|
||||
form.value.completion_price_per_million_usd,
|
||||
thinking_price_per_million_usd:
|
||||
form.value.thinking_price_per_million_usd,
|
||||
is_title_generator: form.value.is_title_generator,
|
||||
enabled: form.value.enabled,
|
||||
sort_order: form.value.sort_order,
|
||||
}
|
||||
await llmAdminApi.createModel(body)
|
||||
}
|
||||
showForm.value = false
|
||||
await reload()
|
||||
} catch (e) {
|
||||
msg.error(e instanceof Error ? e.message : String(e))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function del(m: LLMModel) {
|
||||
dlg.warning({
|
||||
title: `删除 ${m.display_name}?`,
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await llmAdminApi.deleteModel(m.id)
|
||||
await reload()
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<AppShell>
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-lg font-semibold">
|
||||
{{ adminMode ? '用量(全局)' : '我的用量' }}
|
||||
</h2>
|
||||
<div class="flex gap-2 items-center flex-wrap">
|
||||
<NDatePicker
|
||||
v-model:value="range"
|
||||
type="daterange"
|
||||
clearable
|
||||
/>
|
||||
<NSelect
|
||||
v-if="adminMode"
|
||||
v-model:value="groupBy"
|
||||
:options="groupOpts"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</div>
|
||||
<NDataTable
|
||||
:data="rows"
|
||||
:columns="columns"
|
||||
/>
|
||||
</div>
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { NDataTable, NDatePicker, NSelect } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import {
|
||||
llmAdminApi,
|
||||
type AdminUsageGroupBy,
|
||||
type AdminUsageItem,
|
||||
} from '@/api/llmAdmin'
|
||||
import { chatApi, type UsageDailyItem } from '@/api/chat'
|
||||
|
||||
interface Props {
|
||||
adminMode?: boolean
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), { adminMode: false })
|
||||
|
||||
const now = Date.now()
|
||||
const monthAgo = now - 30 * 86400_000
|
||||
const range = ref<[number, number] | null>([monthAgo, now])
|
||||
const groupBy = ref<AdminUsageGroupBy>('day')
|
||||
const groupOpts: { label: string; value: AdminUsageGroupBy }[] = [
|
||||
{ label: '按用户', value: 'user' },
|
||||
{ label: '按模型', value: 'model' },
|
||||
{ label: '按天', value: 'day' },
|
||||
]
|
||||
|
||||
type Row = AdminUsageItem | UsageDailyItem
|
||||
const rows = ref<Row[]>([])
|
||||
|
||||
const columns = computed<DataTableColumns<Row>>(() => {
|
||||
const tokenCols: DataTableColumns<Row> = [
|
||||
{ title: 'Prompt', key: 'prompt_tokens' },
|
||||
{ title: 'Completion', key: 'completion_tokens' },
|
||||
{ title: 'Thinking', key: 'thinking_tokens' },
|
||||
{
|
||||
title: 'Cost USD',
|
||||
key: 'cost_usd',
|
||||
render: (r) => Number(r.cost_usd ?? 0).toFixed(4),
|
||||
},
|
||||
]
|
||||
if (props.adminMode) {
|
||||
const labelCol: DataTableColumns<Row>[number] = {
|
||||
title:
|
||||
groupBy.value === 'user'
|
||||
? '用户'
|
||||
: groupBy.value === 'model'
|
||||
? '模型'
|
||||
: '日期',
|
||||
key: 'key',
|
||||
render: (r) => {
|
||||
const k = (r as AdminUsageItem).key
|
||||
if (groupBy.value === 'day') {
|
||||
return new Date(k).toLocaleDateString('zh-CN')
|
||||
}
|
||||
return k
|
||||
},
|
||||
}
|
||||
return [labelCol, ...tokenCols]
|
||||
}
|
||||
return [
|
||||
{
|
||||
title: '日期',
|
||||
key: 'day',
|
||||
render: (r) => new Date((r as UsageDailyItem).day).toLocaleDateString('zh-CN'),
|
||||
},
|
||||
...tokenCols,
|
||||
]
|
||||
})
|
||||
|
||||
async function reload() {
|
||||
const r = range.value
|
||||
if (!r) {
|
||||
rows.value = []
|
||||
return
|
||||
}
|
||||
const from = new Date(r[0]).toISOString()
|
||||
const to = new Date(r[1]).toISOString()
|
||||
if (props.adminMode) {
|
||||
const data = await llmAdminApi.adminUsage(from, to, groupBy.value)
|
||||
rows.value = data.items
|
||||
} else {
|
||||
rows.value = await chatApi.myDailyUsage(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload)
|
||||
watch([range, groupBy], reload)
|
||||
</script>
|
||||
Reference in New Issue
Block a user