<template>
|
<div class="art-full-height ai-param-page">
|
<ArtSearchBar
|
v-model="searchForm"
|
:items="searchItems"
|
:showExpand="false"
|
@search="handleSearch"
|
@reset="handleReset"
|
/>
|
|
<AiParamRuntimeSummary :key="summaryRefreshSeed" />
|
|
<ElCard class="art-table-card ai-param-list-card">
|
<div class="mb-5 flex flex-wrap items-center justify-between gap-4">
|
<div>
|
<h3 class="text-lg font-semibold text-[var(--art-gray-900)]">{{ t('pages.system.aiParam.title') }}</h3>
|
<p class="mt-1 text-sm text-[var(--art-gray-500)]">{{ t('pages.system.aiParam.subtitle') }}</p>
|
</div>
|
|
<ElSpace wrap>
|
<ElButton v-auth="'add'" @click="openCreateDialog" v-ripple>{{ t('pages.system.aiParam.buttons.add') }}</ElButton>
|
<ElButton :loading="exportLoading" @click="handleExport" v-ripple>{{ t('common.actions.export') }}</ElButton>
|
<ElButton :loading="loading" @click="refreshData" v-ripple>{{ t('common.actions.refresh') }}</ElButton>
|
</ElSpace>
|
</div>
|
|
<div v-loading="loading">
|
<div v-if="data.length" class="grid gap-5 md:grid-cols-2 2xl:grid-cols-3">
|
<article
|
v-for="item in data"
|
:key="item.id"
|
class="overflow-hidden rounded-3xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-4 shadow-[0_10px_30px_rgba(15,23,42,0.04)]"
|
>
|
<div class="flex items-start justify-between gap-4">
|
<div class="min-w-0 flex-1">
|
<div class="flex items-center gap-3">
|
<div class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-sky-50 text-sky-600">
|
<ArtSvgIcon icon="ri:robot-2-line" class="text-xl" />
|
</div>
|
<div class="min-w-0">
|
<h4 class="truncate text-base font-semibold text-[var(--art-gray-900)]">{{ item.name || '--' }}</h4>
|
<p class="mt-1 truncate text-sm text-[var(--art-gray-500)]">{{ item.model || '--' }}</p>
|
</div>
|
</div>
|
</div>
|
|
<ElTag :type="item.statusType" effect="light">{{ item.statusText }}</ElTag>
|
</div>
|
|
<div class="mt-3 flex flex-wrap gap-2">
|
<ElTag size="small" effect="plain">{{ item.providerType }}</ElTag>
|
<ElTag size="small" :type="item.validateStatusType" effect="plain">
|
{{ item.validateStatusText }}
|
</ElTag>
|
<ElTag size="small" effect="plain">{{ item.streamingText }}</ElTag>
|
</div>
|
|
<div class="mt-4 grid gap-2 xl:grid-cols-2">
|
<div
|
class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2.5"
|
>
|
<p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiParam.fields.baseUrl') }}</p>
|
<p class="mt-1.5 break-all text-sm leading-6 text-[var(--art-gray-900)]">{{ item.baseUrl || '--' }}</p>
|
</div>
|
<div
|
class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2.5"
|
>
|
<p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiParam.fields.lastValidateTime') }}</p>
|
<p class="mt-1.5 text-sm text-[var(--art-gray-900)]">{{ item['lastValidateTime$'] || t('pages.system.aiParam.validation.notTested') }}</p>
|
</div>
|
</div>
|
|
<div class="mt-4 grid grid-cols-2 gap-2 text-sm 2xl:grid-cols-4">
|
<div class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2">
|
<p class="text-xs text-[var(--art-gray-500)]">Temperature</p>
|
<p class="mt-1 text-sm font-medium text-[var(--art-gray-900)]">{{ item.temperature ?? '--' }}</p>
|
</div>
|
<div class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2">
|
<p class="text-xs text-[var(--art-gray-500)]">Top P</p>
|
<p class="mt-1 text-sm font-medium text-[var(--art-gray-900)]">{{ item.topP ?? '--' }}</p>
|
</div>
|
<div class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2">
|
<p class="text-xs text-[var(--art-gray-500)]">Max Tokens</p>
|
<p class="mt-1 text-sm font-medium text-[var(--art-gray-900)]">{{ item.maxTokens ?? '--' }}</p>
|
</div>
|
<div class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2">
|
<p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiParam.fields.timeoutMs') }}</p>
|
<p class="mt-1 text-sm font-medium text-[var(--art-gray-900)]">{{ item.timeoutMs ?? '--' }} ms</p>
|
</div>
|
</div>
|
|
<div class="mt-4 rounded-2xl bg-amber-50/70 px-3 py-2.5">
|
<p class="text-xs text-[var(--art-gray-500)]">{{ t('table.remark') }}</p>
|
<p class="mt-1.5 line-clamp-2 text-sm leading-6 text-[var(--art-gray-900)]">{{ item.memo || '--' }}</p>
|
</div>
|
|
<div class="mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-[var(--art-border-color)] pt-3">
|
<div class="flex items-center gap-2 text-xs text-[var(--art-gray-500)]">
|
<span>{{ t('table.updateTime') }}</span>
|
<span>{{ item['updateTime$'] || '--' }}</span>
|
</div>
|
|
<ElSpace wrap>
|
<ElButton text @click="openDetailDialog(item)">{{ t('common.actions.detail') }}</ElButton>
|
<ElButton v-auth="'edit'" text @click="openEditDialog(item)">{{ t('common.actions.edit') }}</ElButton>
|
<ElButton
|
v-auth="'edit'"
|
text
|
:disabled="item.statusBool || defaultUpdatingId === item.id"
|
:loading="defaultUpdatingId === item.id"
|
@click="handleSetDefault(item)"
|
>
|
{{ t('pages.system.aiParam.actions.setDefault') }}
|
</ElButton>
|
<ElButton v-auth="'delete'" text type="danger" @click="handleDelete(item)">{{ t('common.actions.delete') }}</ElButton>
|
</ElSpace>
|
</div>
|
</article>
|
</div>
|
|
<ElEmpty v-else :description="t('pages.system.aiParam.empty')" :image-size="110" />
|
</div>
|
|
<div class="mt-6 flex justify-end">
|
<ElPagination
|
background
|
layout="total, sizes, prev, pager, next, jumper"
|
:current-page="pagination.current"
|
:page-size="pagination.size"
|
:total="pagination.total"
|
:page-sizes="[20, 50, 100]"
|
@size-change="handleSizeChange"
|
@current-change="handleCurrentChange"
|
/>
|
</div>
|
|
<AiParamDialog
|
v-model:visible="dialogVisible"
|
:mode="dialogMode"
|
:ai-param-data="currentAiParamData"
|
@submit="handleDialogSubmit"
|
/>
|
</ElCard>
|
</div>
|
</template>
|
|
<script setup>
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { useI18n } from 'vue-i18n'
|
import { useUserStore } from '@/store/modules/user'
|
import { useTable } from '@/hooks/core/useTable'
|
import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
|
import {
|
fetchAiParamPage,
|
fetchDeleteAiParam,
|
fetchExportAiParamReport,
|
fetchGetAiParamDetail,
|
fetchSaveAiParam,
|
fetchSetAiParamDefault,
|
fetchUpdateAiParam
|
} from '@/api/ai-config'
|
import AiParamDialog from './modules/ai-param-dialog.vue'
|
import AiParamRuntimeSummary from './modules/ai-param-runtime-summary.vue'
|
import {
|
buildAiParamDialogModel,
|
buildAiParamPageQueryParams,
|
buildAiParamSavePayload,
|
buildAiParamSearchParams,
|
createAiParamSearchState,
|
getAiParamPaginationKey,
|
normalizeAiParamRow
|
} from './aiParamPage.helpers'
|
|
defineOptions({ name: 'AiParam' })
|
|
const { t } = useI18n()
|
const userStore = useUserStore()
|
const searchForm = ref(createAiParamSearchState())
|
const dialogVisible = ref(false)
|
const dialogMode = ref('create')
|
const currentAiParamData = ref(buildAiParamDialogModel())
|
const defaultUpdatingId = ref(null)
|
const exportLoading = ref(false)
|
const summaryRefreshSeed = ref(0)
|
|
const searchItems = computed(() => [
|
{
|
label: t('pages.system.aiParam.search.condition'),
|
key: 'condition',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: t('pages.system.aiParam.search.conditionPlaceholder')
|
}
|
},
|
{
|
label: t('pages.system.aiParam.search.providerType'),
|
key: 'providerType',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: t('pages.system.aiParam.search.providerTypePlaceholder')
|
}
|
},
|
{
|
label: t('pages.system.aiParam.search.model'),
|
key: 'model',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: t('pages.system.aiParam.search.modelPlaceholder')
|
}
|
},
|
{
|
label: t('pages.system.aiParam.search.status'),
|
key: 'status',
|
type: 'select',
|
props: {
|
clearable: true,
|
options: [
|
{ label: t('pages.system.aiParam.status.default'), value: 1 },
|
{ label: t('pages.system.aiParam.status.candidate'), value: 0 }
|
]
|
}
|
}
|
])
|
|
const {
|
data,
|
loading,
|
pagination,
|
getData,
|
replaceSearchParams,
|
resetSearchParams,
|
handleSizeChange,
|
handleCurrentChange,
|
refreshData,
|
refreshCreate,
|
refreshUpdate,
|
refreshRemove
|
} = useTable({
|
core: {
|
apiFn: fetchAiParamPage,
|
apiParams: buildAiParamPageQueryParams(searchForm.value),
|
paginationKey: getAiParamPaginationKey()
|
},
|
transform: {
|
dataTransformer: (records) => {
|
if (!Array.isArray(records)) {
|
return []
|
}
|
return records.map((item) => normalizeAiParamRow(item))
|
}
|
}
|
})
|
|
async function openEditDialog(record) {
|
try {
|
currentAiParamData.value = buildAiParamDialogModel(await fetchGetAiParamDetail(record.id))
|
dialogMode.value = 'edit'
|
dialogVisible.value = true
|
} catch {
|
return
|
}
|
}
|
|
async function openDetailDialog(record) {
|
try {
|
currentAiParamData.value = buildAiParamDialogModel(await fetchGetAiParamDetail(record.id))
|
dialogMode.value = 'show'
|
dialogVisible.value = true
|
} catch {
|
return
|
}
|
}
|
|
function openCreateDialog() {
|
currentAiParamData.value = buildAiParamDialogModel()
|
dialogMode.value = 'create'
|
dialogVisible.value = true
|
}
|
|
async function handleDialogSubmit(payload) {
|
try {
|
if (dialogMode.value === 'edit') {
|
await fetchUpdateAiParam(buildAiParamSavePayload(payload))
|
ElMessage.success(t('crud.messages.updateSuccess'))
|
dialogVisible.value = false
|
summaryRefreshSeed.value += 1
|
await refreshUpdate()
|
return
|
}
|
await fetchSaveAiParam(buildAiParamSavePayload(payload))
|
ElMessage.success(t('crud.messages.createSuccess'))
|
dialogVisible.value = false
|
summaryRefreshSeed.value += 1
|
await refreshCreate()
|
} catch {
|
return
|
}
|
}
|
|
async function handleSetDefault(record) {
|
if (!record?.id || record.statusBool) return
|
defaultUpdatingId.value = record.id
|
try {
|
await fetchSetAiParamDefault(record.id)
|
ElMessage.success(t('pages.system.aiParam.messages.setDefaultSuccess'))
|
summaryRefreshSeed.value += 1
|
await refreshUpdate()
|
} catch {
|
return
|
} finally {
|
defaultUpdatingId.value = null
|
}
|
}
|
|
async function handleDelete(record) {
|
try {
|
await ElMessageBox.confirm(
|
t('crud.confirm.deleteMessage', {
|
entity: t('pages.system.aiParam.entity'),
|
label: record?.name || record?.id
|
}),
|
t('crud.confirm.deleteTitle'),
|
{
|
confirmButtonText: t('common.confirm'),
|
cancelButtonText: t('common.cancel'),
|
type: 'warning'
|
})
|
await fetchDeleteAiParam(record.id)
|
ElMessage.success(t('crud.messages.deleteSuccess'))
|
await refreshRemove()
|
} catch (error) {
|
if (error !== 'cancel') {
|
ElMessage.error(error?.message || t('crud.messages.deleteFailed'))
|
}
|
}
|
}
|
|
async function handleExport() {
|
exportLoading.value = true
|
try {
|
const response = await guardRequestWithMessage(
|
fetchExportAiParamReport(buildAiParamSearchParams(searchForm.value), {
|
headers: {
|
Authorization: userStore.accessToken || ''
|
}
|
}),
|
null,
|
{
|
timeoutMessage: t('message.exportTimeoutStopped')
|
}
|
)
|
if (!response) return
|
if (!response.ok) {
|
throw new Error(t('crud.messages.exportFailedWithStatus', { status: response.status }))
|
}
|
const blob = await response.blob()
|
const url = window.URL.createObjectURL(blob)
|
const link = document.createElement('a')
|
link.href = url
|
link.download = 'ai-param.xlsx'
|
document.body.appendChild(link)
|
link.click()
|
link.remove()
|
window.URL.revokeObjectURL(url)
|
ElMessage.success(t('crud.messages.exportSuccess'))
|
} catch (error) {
|
ElMessage.error(error?.message || t('crud.messages.exportFailed'))
|
} finally {
|
exportLoading.value = false
|
}
|
}
|
|
function handleSearch(params) {
|
replaceSearchParams(buildAiParamSearchParams(params))
|
getData()
|
}
|
|
function handleReset() {
|
Object.assign(searchForm.value, createAiParamSearchState())
|
resetSearchParams()
|
}
|
</script>
|
|
<style scoped>
|
.ai-param-page {
|
overflow-y: auto;
|
}
|
|
.ai-param-list-card {
|
flex: none;
|
}
|
|
.ai-param-list-card :deep(.el-card__body) {
|
height: auto;
|
overflow: visible;
|
}
|
</style>
|