perf: 模型管理界面重做
This commit is contained in:
@@ -0,0 +1,774 @@
|
||||
<script setup lang="ts">
|
||||
import type {llmType} from '#/api';
|
||||
import type {ModelAbilityItem} from '#/views/ai/model/modelUtils/model-ability';
|
||||
import {getDefaultModelAbility} from '#/views/ai/model/modelUtils/model-ability';
|
||||
|
||||
import {computed, onMounted, reactive, ref} from 'vue';
|
||||
|
||||
import {CircleCheck, CircleClose, Delete, Edit, Loading, Select,} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElEmpty,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import {deleteLlm, getModelList, verifyModelConfig,} from '#/api/ai/llm';
|
||||
import {$t} from '#/locales';
|
||||
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
|
||||
import {mapLlmToModelAbility} from '#/views/ai/model/modelUtils/model-ability-utils';
|
||||
|
||||
interface ProviderOption {
|
||||
id: string;
|
||||
providerName: string;
|
||||
}
|
||||
|
||||
interface FilterState {
|
||||
keyword: string;
|
||||
modelType: string;
|
||||
providerId: string;
|
||||
}
|
||||
|
||||
interface BatchDeleteResult {
|
||||
failed: Array<{ id: string; message: string }>;
|
||||
successIds: string[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
providers: ProviderOption[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'createModel', modelType?: string): void;
|
||||
(e: 'editModel', id: string): void;
|
||||
(e: 'refreshProviderStats'): void;
|
||||
}>();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const isActionLoading = ref(false);
|
||||
const modelRows = ref<llmType[]>([]);
|
||||
const selectedRows = ref<llmType[]>([]);
|
||||
const lastErrorMessage = ref('');
|
||||
type VerifyButtonStatus = 'error' | 'idle' | 'loading' | 'success';
|
||||
const verifyStatusMap = ref<Record<string, VerifyButtonStatus>>({});
|
||||
|
||||
const filterState = reactive<FilterState>({
|
||||
keyword: '',
|
||||
modelType: '',
|
||||
providerId: '',
|
||||
});
|
||||
|
||||
const modelTypeOptions = [
|
||||
{
|
||||
label: $t('llmProvider.chatModel'),
|
||||
value: 'chatModel',
|
||||
},
|
||||
{
|
||||
label: $t('llmProvider.embeddingModel'),
|
||||
value: 'embeddingModel',
|
||||
},
|
||||
{
|
||||
label: $t('llmProvider.rerankModel'),
|
||||
value: 'rerankModel',
|
||||
},
|
||||
];
|
||||
|
||||
const modelTypeLabelMap: Record<string, string> = {
|
||||
chatModel: $t('llmProvider.chatModel'),
|
||||
embeddingModel: $t('llmProvider.embeddingModel'),
|
||||
rerankModel: $t('llmProvider.rerankModel'),
|
||||
};
|
||||
|
||||
const getProviderName = (row: llmType) =>
|
||||
row.modelProvider?.providerName || row.aiLlmProvider?.providerName || '-';
|
||||
|
||||
const getProviderType = (row: llmType) =>
|
||||
row.modelProvider?.providerType || row.aiLlmProvider?.providerType || '';
|
||||
|
||||
const getProviderIcon = (row: llmType) =>
|
||||
row.modelProvider?.icon || row.aiLlmProvider?.icon || '';
|
||||
|
||||
const getModelName = (row: llmType) =>
|
||||
row.llmModel || (row as any).modelName || '-';
|
||||
|
||||
const getModelTypeLabel = (row: llmType) =>
|
||||
modelTypeLabelMap[row.modelType] || row.modelType || '-';
|
||||
|
||||
const canVerify = (row: llmType) => row.modelType !== 'rerankModel';
|
||||
const getModelId = (row: llmType) =>
|
||||
String((row as any).id || (row as any).llmId || (row as any).modelId || '');
|
||||
const getVerifyStatus = (row: llmType): VerifyButtonStatus =>
|
||||
verifyStatusMap.value[getModelId(row)] || 'idle';
|
||||
const setVerifyStatus = (id: string, status: VerifyButtonStatus) => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
verifyStatusMap.value[id] = status;
|
||||
};
|
||||
const isVerifying = (row: llmType) => getVerifyStatus(row) === 'loading';
|
||||
const getVerifyButtonText = (row: llmType) => {
|
||||
const status = getVerifyStatus(row);
|
||||
if (status === 'loading') {
|
||||
return '验证中';
|
||||
}
|
||||
if (status === 'success') {
|
||||
return '验证成功';
|
||||
}
|
||||
if (status === 'error') {
|
||||
return '验证失败';
|
||||
}
|
||||
return '验证配置';
|
||||
};
|
||||
const getVerifyButtonIcon = (row: llmType) => {
|
||||
const status = getVerifyStatus(row);
|
||||
if (status === 'loading') {
|
||||
return Loading;
|
||||
}
|
||||
if (status === 'success') {
|
||||
return CircleCheck;
|
||||
}
|
||||
if (status === 'error') {
|
||||
return CircleClose;
|
||||
}
|
||||
return Select;
|
||||
};
|
||||
|
||||
const getAbilityTags = (row: llmType): ModelAbilityItem[] =>
|
||||
mapLlmToModelAbility(row, getDefaultModelAbility()).filter(
|
||||
(tag) => tag.selected,
|
||||
);
|
||||
|
||||
const totalCount = computed(() => modelRows.value.length);
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const keyword = filterState.keyword.trim().toLowerCase();
|
||||
|
||||
return modelRows.value.filter((item) => {
|
||||
if (
|
||||
filterState.providerId &&
|
||||
String((item as any).providerId) !== filterState.providerId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterState.modelType && item.modelType !== filterState.modelType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!keyword) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const searchTargets = [
|
||||
item.title,
|
||||
getModelName(item),
|
||||
item.groupName,
|
||||
getProviderName(item),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.map((text) => String(text).toLowerCase());
|
||||
|
||||
return searchTargets.some((text) => text.includes(keyword));
|
||||
});
|
||||
});
|
||||
|
||||
const selectableRowCount = computed(() => selectedRows.value.length);
|
||||
|
||||
const loadModels = async () => {
|
||||
isLoading.value = true;
|
||||
lastErrorMessage.value = '';
|
||||
|
||||
try {
|
||||
const res = await getModelList();
|
||||
|
||||
if (res.errorCode === 0) {
|
||||
modelRows.value = res.data || [];
|
||||
verifyStatusMap.value = {};
|
||||
} else {
|
||||
modelRows.value = [];
|
||||
verifyStatusMap.value = {};
|
||||
lastErrorMessage.value =
|
||||
res.message || $t('ui.actionMessage.operationFailed');
|
||||
}
|
||||
} catch (error) {
|
||||
modelRows.value = [];
|
||||
verifyStatusMap.value = {};
|
||||
lastErrorMessage.value =
|
||||
(error as Error)?.message || $t('ui.actionMessage.operationFailed');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const reloadAndNotify = async () => {
|
||||
await loadModels();
|
||||
emit('refreshProviderStats');
|
||||
selectedRows.value = [];
|
||||
};
|
||||
|
||||
const handleSelectionChange = (rows: llmType[]) => {
|
||||
selectedRows.value = rows;
|
||||
};
|
||||
|
||||
const handleEdit = (row: llmType) => {
|
||||
const modelId = getModelId(row);
|
||||
if (!modelId) {
|
||||
ElMessage.warning('当前模型缺少ID,无法编辑');
|
||||
return;
|
||||
}
|
||||
emit('editModel', modelId);
|
||||
};
|
||||
|
||||
const handleDelete = async (row: llmType) => {
|
||||
const modelId = getModelId(row);
|
||||
if (!modelId) {
|
||||
ElMessage.warning('当前模型缺少ID,无法删除');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除模型「${row.title}」吗?该操作不可恢复。`,
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
confirmButtonText: $t('message.ok'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
isActionLoading.value = true;
|
||||
|
||||
try {
|
||||
const res = await deleteLlm({ id: modelId });
|
||||
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || '模型已删除');
|
||||
await reloadAndNotify();
|
||||
} else {
|
||||
ElMessage.error(res.message || $t('ui.actionMessage.operationFailed'));
|
||||
}
|
||||
} finally {
|
||||
isActionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerify = async (row: llmType) => {
|
||||
const modelId = getModelId(row);
|
||||
|
||||
if (!canVerify(row) || !modelId || isVerifying(row)) {
|
||||
if (!modelId) {
|
||||
ElMessage.warning('当前模型缺少ID,无法验证配置');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setVerifyStatus(modelId, 'loading');
|
||||
|
||||
try {
|
||||
const res = await verifyModelConfig(modelId);
|
||||
|
||||
if (res.errorCode === 0) {
|
||||
setVerifyStatus(modelId, 'success');
|
||||
if (row.modelType === 'embeddingModel' && res?.data?.dimension) {
|
||||
ElMessage.success(`验证成功,向量维度:${res.data.dimension}`);
|
||||
} else {
|
||||
ElMessage.success('验证成功');
|
||||
}
|
||||
} else {
|
||||
setVerifyStatus(modelId, 'error');
|
||||
if (!res.message) {
|
||||
ElMessage.error($t('ui.actionMessage.operationFailed'));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setVerifyStatus(modelId, 'error');
|
||||
// error toast is already handled by global response interceptors
|
||||
} finally {
|
||||
// keep final status to show explicit success/failure state
|
||||
}
|
||||
};
|
||||
|
||||
const runBatchDelete = async (
|
||||
ids: string[],
|
||||
concurrency = 5,
|
||||
): Promise<BatchDeleteResult> => {
|
||||
const queue = [...ids];
|
||||
const successIds: string[] = [];
|
||||
const failed: Array<{ id: string; message: string }> = [];
|
||||
|
||||
const worker = async () => {
|
||||
while (queue.length > 0) {
|
||||
const id = queue.shift();
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await deleteLlm({ id });
|
||||
if (res.errorCode === 0) {
|
||||
successIds.push(id);
|
||||
} else {
|
||||
failed.push({ id, message: res.message || '删除失败' });
|
||||
}
|
||||
} catch (error) {
|
||||
failed.push({ id, message: (error as Error)?.message || '网络错误' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const workerCount = Math.max(1, Math.min(concurrency, ids.length || 1));
|
||||
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
||||
|
||||
return {
|
||||
failed,
|
||||
successIds,
|
||||
};
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
const ids = selectedRows.value
|
||||
.map((item) => getModelId(item))
|
||||
.filter(Boolean);
|
||||
|
||||
if (ids.length === 0) {
|
||||
ElMessage.warning('请先选择要删除的模型');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认批量删除 ${ids.length} 个模型吗?该操作不可恢复。`,
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
confirmButtonText: $t('message.ok'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
isActionLoading.value = true;
|
||||
|
||||
try {
|
||||
const result = await runBatchDelete(ids, 5);
|
||||
const successCount = result.successIds.length;
|
||||
const failCount = result.failed.length;
|
||||
|
||||
if (failCount === 0) {
|
||||
ElMessage.success(`批量删除完成,共 ${successCount} 个模型`);
|
||||
} else {
|
||||
ElMessage.warning(
|
||||
`批量删除完成,成功 ${successCount} 个,失败 ${failCount} 个`,
|
||||
);
|
||||
}
|
||||
|
||||
await reloadAndNotify();
|
||||
} finally {
|
||||
isActionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
filterState.keyword = '';
|
||||
filterState.providerId = '';
|
||||
filterState.modelType = '';
|
||||
};
|
||||
|
||||
onMounted(loadModels);
|
||||
|
||||
defineExpose({
|
||||
async reloadData() {
|
||||
await loadModels();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="active-workspace">
|
||||
<header class="active-workspace__header">
|
||||
<div class="active-workspace__summary">
|
||||
<h3>已配置模型</h3>
|
||||
<p>共 {{ totalCount }} 个模型</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="active-workspace__filters">
|
||||
<ElInput
|
||||
v-model.trim="filterState.keyword"
|
||||
clearable
|
||||
placeholder="搜索模型名、模型ID、服务商、分组"
|
||||
/>
|
||||
<ElSelect
|
||||
v-model="filterState.providerId"
|
||||
clearable
|
||||
placeholder="全部服务商"
|
||||
>
|
||||
<ElOption
|
||||
v-for="provider in props.providers"
|
||||
:key="provider.id"
|
||||
:label="provider.providerName"
|
||||
:value="provider.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElSelect
|
||||
v-model="filterState.modelType"
|
||||
clearable
|
||||
placeholder="全部模型类型"
|
||||
>
|
||||
<ElOption
|
||||
v-for="type in modelTypeOptions"
|
||||
:key="type.value"
|
||||
:label="type.label"
|
||||
:value="type.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElButton @click="resetFilters">重置</ElButton>
|
||||
</div>
|
||||
|
||||
<div v-if="selectableRowCount > 0" class="active-workspace__batch-bar">
|
||||
<span>已选 {{ selectableRowCount }} 项</span>
|
||||
<div class="active-workspace__batch-actions">
|
||||
<ElButton
|
||||
class="is-danger"
|
||||
:disabled="isActionLoading"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="active-workspace__state">
|
||||
正在加载模型数据...
|
||||
</div>
|
||||
|
||||
<div v-else-if="lastErrorMessage" class="active-workspace__state is-error">
|
||||
{{ lastErrorMessage }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="modelRows.length === 0" class="active-workspace__empty">
|
||||
<ElEmpty description="还没有模型,先添加一个模型开始使用。">
|
||||
<ElButton type="primary" @click="emit('createModel', 'chatModel')">
|
||||
添加模型
|
||||
</ElButton>
|
||||
</ElEmpty>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredRows.length === 0" class="active-workspace__empty">
|
||||
<ElEmpty description="没有符合筛选条件的模型,试试调整筛选项。" />
|
||||
</div>
|
||||
|
||||
<ElTable
|
||||
v-else
|
||||
row-key="id"
|
||||
:data="filteredRows"
|
||||
class="active-workspace__table"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<ElTableColumn type="selection" width="48" />
|
||||
|
||||
<ElTableColumn label="模型名称" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="active-workspace__name-with-logo">
|
||||
<ModelProviderBadge
|
||||
:icon="getProviderIcon(row)"
|
||||
:provider-name="getProviderName(row)"
|
||||
:provider-type="getProviderType(row)"
|
||||
:size="30"
|
||||
/>
|
||||
<div class="active-workspace__name-cell">
|
||||
<strong>{{ row.title }}</strong>
|
||||
<span>{{ getModelName(row) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<ElTableColumn label="服务商" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ getProviderName(row) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<ElTableColumn label="类型" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ getModelTypeLabel(row) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<ElTableColumn label="分组" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.groupName || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<ElTableColumn label="能力" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="active-workspace__ability">
|
||||
<ElTag
|
||||
v-for="tag in getAbilityTags(row)"
|
||||
:key="tag.value"
|
||||
effect="plain"
|
||||
size="small"
|
||||
>
|
||||
{{ tag.label }}
|
||||
</ElTag>
|
||||
<span v-if="getAbilityTags(row).length === 0">-</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<ElTableColumn label="操作" min-width="290" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="active-workspace__actions">
|
||||
<ElButton
|
||||
v-if="canVerify(row)"
|
||||
text
|
||||
size="small"
|
||||
class="active-workspace__verify-btn"
|
||||
:class="`is-${getVerifyStatus(row)}`"
|
||||
:disabled="
|
||||
isVerifying(row) || isActionLoading || !getModelId(row)
|
||||
"
|
||||
@click="handleVerify(row)"
|
||||
>
|
||||
<template #icon>
|
||||
<ElIcon
|
||||
class="active-workspace__verify-icon"
|
||||
:class="`is-${getVerifyStatus(row)}`"
|
||||
>
|
||||
<component :is="getVerifyButtonIcon(row)" />
|
||||
</ElIcon>
|
||||
</template>
|
||||
{{ getVerifyButtonText(row) }}
|
||||
</ElButton>
|
||||
<ElButton text size="small" :icon="Edit" @click="handleEdit(row)">
|
||||
编辑
|
||||
</ElButton>
|
||||
<ElButton
|
||||
text
|
||||
size="small"
|
||||
class="is-danger"
|
||||
:icon="Delete"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.active-workspace {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.active-workspace__header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid hsl(var(--divider-faint) / 58%);
|
||||
}
|
||||
|
||||
.active-workspace__summary h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.active-workspace__summary p,
|
||||
.active-workspace__state {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.active-workspace__filters {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1.6fr) repeat(2, minmax(0, 1fr)) auto;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.active-workspace__batch-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background: hsl(var(--surface-contrast-soft) / 68%);
|
||||
border: 1px solid hsl(var(--divider-faint) / 58%);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.active-workspace__batch-bar span {
|
||||
font-size: 13px;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.active-workspace__batch-actions {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.active-workspace__batch-actions .is-danger,
|
||||
.active-workspace__actions .is-danger {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.active-workspace__state {
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
.active-workspace__state.is-error {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.active-workspace__empty {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.active-workspace__empty :deep(.el-empty) {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.active-workspace__table {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.active-workspace__name-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.active-workspace__name-with-logo {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.active-workspace__name-cell strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.active-workspace__name-cell span {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.active-workspace__ability {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.active-workspace__actions {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.active-workspace__verify-btn.is-idle {
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.active-workspace__verify-btn.is-loading {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.active-workspace__verify-btn.is-success {
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
.active-workspace__verify-btn.is-error {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.active-workspace__verify-icon {
|
||||
transition:
|
||||
color 0.24s ease,
|
||||
transform 0.24s ease;
|
||||
}
|
||||
|
||||
.active-workspace__verify-icon.is-loading {
|
||||
animation: active-workspace-verify-spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
.active-workspace__verify-icon.is-success,
|
||||
.active-workspace__verify-icon.is-error {
|
||||
animation: active-workspace-verify-pop 0.32s ease;
|
||||
}
|
||||
|
||||
@keyframes active-workspace-verify-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes active-workspace-verify-pop {
|
||||
0% {
|
||||
opacity: 0.72;
|
||||
transform: scale(0.82);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.active-workspace__filters {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.active-workspace__header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.active-workspace__filters {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.active-workspace__batch-bar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user