增加智能体数据留存功能
This commit is contained in:
@@ -0,0 +1,738 @@
|
||||
<template>
|
||||
<div class="container-box agent-api-page">
|
||||
<div class="header-bar">
|
||||
<h2>智能体 API 管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="editRef.open()">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增上游接口
|
||||
</el-button>
|
||||
<el-button @click="reload" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="上游 / 地址 / 模型"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="上游接口">
|
||||
<el-input
|
||||
v-model="searchForm.provider"
|
||||
placeholder="上游名称,支持模糊"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
style="width: 130px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 瀑布流卡片区 -->
|
||||
<div class="waterfall-wrap">
|
||||
<div v-if="list.length > 0" class="waterfall">
|
||||
<div v-for="row in list" :key="row.id" class="waterfall-item">
|
||||
<div class="api-card" :class="{ disabled: Number(row.status) !== 1 }">
|
||||
<!-- 卡片头:上游接口标识 -->
|
||||
<div class="card-head" :style="{ borderTopColor: providerColor(row.provider) }">
|
||||
<div class="provider-badge" :style="{ background: providerColor(row.provider) }">
|
||||
{{ providerInitial(row.provider) }}
|
||||
</div>
|
||||
<div class="head-text">
|
||||
<div class="card-title" :title="row.provider">
|
||||
{{ providerLabel(row.provider) }}
|
||||
</div>
|
||||
<div class="card-subtitle">
|
||||
{{ keysOf(row).length }} 个密钥 · {{ modelsOf(row).length }} 个模型
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, row)">
|
||||
<el-button type="primary" size="small" class="card-action-btn">
|
||||
操作
|
||||
<el-icon class="action-arrow"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="detail">
|
||||
<el-icon><View /></el-icon> 详情
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="test">
|
||||
<el-icon><Connection /></el-icon> 测试连通性
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon> 编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="copy">
|
||||
<el-icon><CopyDocument /></el-icon> 复制新建
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon> 删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- 接口地址 -->
|
||||
<div class="card-section">
|
||||
<div class="section-label">
|
||||
接口地址
|
||||
<el-tag v-if="Number(row.use_custom_url) === 1" type="warning" size="small" effect="plain">
|
||||
自定义
|
||||
</el-tag>
|
||||
<span class="section-actions">
|
||||
<el-button
|
||||
text
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="copyText(effectiveUrl(row), '地址')"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="url-text" :title="effectiveUrl(row)">{{ effectiveUrl(row) || '—' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key 列表:每条独立脱敏/显示/复制 -->
|
||||
<div class="card-section">
|
||||
<div class="section-label">
|
||||
API Key
|
||||
<span class="model-count">{{ keysOf(row).length }} 个</span>
|
||||
</div>
|
||||
<div v-if="keysOf(row).length > 0" class="key-list">
|
||||
<div v-for="(k, idx) in keysOf(row)" :key="idx" class="key-item">
|
||||
<div class="key-item-head">
|
||||
<el-tag v-if="k.remark" size="small" type="success" effect="plain">
|
||||
{{ k.remark }}
|
||||
</el-tag>
|
||||
<span v-else class="key-no-remark">密钥 {{ idx + 1 }}</span>
|
||||
<span class="section-actions">
|
||||
<el-button
|
||||
text
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="toggleKey(`${row.id}-${idx}`)"
|
||||
>
|
||||
{{ revealedKeys.has(`${row.id}-${idx}`) ? '隐藏' : '显示' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
text
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="copyText(k.key, '密钥')"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="key-text"
|
||||
:title="revealedKeys.has(`${row.id}-${idx}`) ? k.key : ''"
|
||||
>
|
||||
{{ revealedKeys.has(`${row.id}-${idx}`) ? k.key : maskKey(k.key) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-models">未配置密钥</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型列表:点击 tag 复制模型名 -->
|
||||
<div class="card-section">
|
||||
<div class="section-label">
|
||||
模型列表
|
||||
<span class="model-hint">点击可复制</span>
|
||||
<span class="model-count">{{ modelsOf(row).length }} 个</span>
|
||||
</div>
|
||||
<div v-if="modelsOf(row).length > 0" class="model-tags">
|
||||
<el-tag
|
||||
v-for="m in modelsOf(row)"
|
||||
:key="m"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
class="model-tag"
|
||||
:title="`点击复制:${m}`"
|
||||
@click="copyText(m, '模型名')"
|
||||
>
|
||||
{{ m }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-else class="empty-models">未配置模型</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注 -->
|
||||
<div v-if="row.remark" class="card-section">
|
||||
<div class="section-label">备注</div>
|
||||
<div class="remark-text">{{ row.remark }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片底部 -->
|
||||
<div class="card-foot">
|
||||
<span class="foot-time">{{ row.create_time || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-else-if="!loading && !firstLoading"
|
||||
description="暂无上游接口配置,点击右上角新增"
|
||||
/>
|
||||
|
||||
<!-- 首屏骨架 -->
|
||||
<div v-if="firstLoading" class="loading-block">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
|
||||
<!-- 滚动加载哨兵:进入视口即拉取下一页 -->
|
||||
<div ref="sentinelRef" class="load-sentinel">
|
||||
<div v-if="loadingMore" class="loading-block">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>正在加载更多...</span>
|
||||
</div>
|
||||
<div v-else-if="!hasMore && list.length > 0" class="list-end">
|
||||
已加载全部 {{ total }} 条配置
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑抽屉 -->
|
||||
<AgentApiEdit ref="editRef" @saved="reload" />
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<AgentApiDetail ref="detailRef" />
|
||||
|
||||
<!-- 测试抽屉 -->
|
||||
<AgentApiTest ref="testRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Refresh,
|
||||
Search,
|
||||
ArrowDown,
|
||||
View,
|
||||
Edit,
|
||||
Delete,
|
||||
Connection,
|
||||
CopyDocument,
|
||||
Loading,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { getAgentApiList, deleteAgentApi } from '@/api/agentapimanagement'
|
||||
import {
|
||||
providerLabel,
|
||||
providerColor,
|
||||
providerInitial,
|
||||
effectiveUrl,
|
||||
normalizeModels,
|
||||
normalizeApiKeys,
|
||||
maskKey,
|
||||
} from './constants'
|
||||
import AgentApiEdit from './components/edit.vue'
|
||||
import AgentApiDetail from './components/detail.vue'
|
||||
import AgentApiTest from './components/test.vue'
|
||||
|
||||
const editRef = ref(null)
|
||||
const detailRef = ref(null)
|
||||
const testRef = ref(null)
|
||||
|
||||
// 滚动加载哨兵元素
|
||||
const sentinelRef = ref(null)
|
||||
let observer = null
|
||||
|
||||
const loading = ref(false) // 任一加载中(首屏或加载更多)
|
||||
const firstLoading = ref(false) // 首屏加载中
|
||||
const loadingMore = ref(false) // 追加下一页中
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const hasMore = ref(true)
|
||||
|
||||
// 每页条数与当前页码;滚动到底部时页码 +1 并追加数据
|
||||
const PAGE_SIZE = 24
|
||||
let page = 1
|
||||
|
||||
// 记录已点开明文的密钥,元素为 `${配置ID}-${密钥下标}`
|
||||
const revealedKeys = ref(new Set())
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
provider: undefined,
|
||||
status: undefined,
|
||||
})
|
||||
|
||||
/** 取该行的模型数组 */
|
||||
function modelsOf(row) {
|
||||
return normalizeModels(row.models)
|
||||
}
|
||||
|
||||
/** 取该行的密钥数组 */
|
||||
function keysOf(row) {
|
||||
return normalizeApiKeys(row.api_keys)
|
||||
}
|
||||
|
||||
/** 切换单条密钥的明文/脱敏显示 */
|
||||
function toggleKey(id) {
|
||||
const next = new Set(revealedKeys.value)
|
||||
if (next.has(id)) {
|
||||
next.delete(id)
|
||||
} else {
|
||||
next.add(id)
|
||||
}
|
||||
revealedKeys.value = next
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取一页数据
|
||||
* @param {boolean} append true 追加到现有列表(滚动加载),false 替换(首次/搜索)
|
||||
*/
|
||||
async function fetchPage(append = false) {
|
||||
if (loading.value) return
|
||||
|
||||
loading.value = true
|
||||
if (append) {
|
||||
loadingMore.value = true
|
||||
} else {
|
||||
firstLoading.value = true
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
provider: searchForm.provider || undefined,
|
||||
status: searchForm.status,
|
||||
}
|
||||
const res = await getAgentApiList(params)
|
||||
if (res?.code === 200 && res.data) {
|
||||
const rows = res.data.list || []
|
||||
total.value = res.data.total ?? 0
|
||||
list.value = append ? [...list.value, ...rows] : rows
|
||||
// 本页返回不足一页,或已凑满总数,说明没有更多了
|
||||
hasMore.value = rows.length >= PAGE_SIZE && list.value.length < total.value
|
||||
} else {
|
||||
if (!append) list.value = []
|
||||
hasMore.value = false
|
||||
ElMessage.error(res?.msg || '加载失败')
|
||||
}
|
||||
} catch (err) {
|
||||
hasMore.value = false
|
||||
if (!append) list.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
firstLoading.value = false
|
||||
// 数据渲染后重新观察哨兵,应对"一页填不满视口"的情况
|
||||
await nextTick()
|
||||
observeSentinel()
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置到第一页重新加载(搜索、刷新、增删改后调用) */
|
||||
function reload() {
|
||||
page = 1
|
||||
hasMore.value = true
|
||||
fetchPage(false)
|
||||
}
|
||||
|
||||
/** 滚动到底部时加载下一页 */
|
||||
function loadMore() {
|
||||
if (loading.value || !hasMore.value) return
|
||||
page += 1
|
||||
fetchPage(true)
|
||||
}
|
||||
|
||||
/** 用 IntersectionObserver 监听哨兵元素进入视口 */
|
||||
function observeSentinel() {
|
||||
if (!observer || !sentinelRef.value) return
|
||||
observer.disconnect()
|
||||
if (hasMore.value) {
|
||||
observer.observe(sentinelRef.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
reload()
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.provider = undefined
|
||||
searchForm.status = undefined
|
||||
reload()
|
||||
}
|
||||
|
||||
// 卡片下拉操作
|
||||
function handleCommand(cmd, row) {
|
||||
if (cmd === 'detail') {
|
||||
detailRef.value?.open(row.id)
|
||||
} else if (cmd === 'test') {
|
||||
testRef.value?.open(row.id)
|
||||
} else if (cmd === 'edit') {
|
||||
editRef.value?.open(row.id)
|
||||
} else if (cmd === 'copy') {
|
||||
// 复制新建:带入该卡片数据,保存时创建新记录
|
||||
editRef.value?.openCopy(row.id)
|
||||
} else if (cmd === 'delete') {
|
||||
handleDelete(row)
|
||||
}
|
||||
}
|
||||
|
||||
async function copyText(text, label = '地址') {
|
||||
if (!text) {
|
||||
ElMessage.warning(`${label}为空`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success(`已复制${label}`)
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除「${providerLabel(row.provider)}」该上游接口配置吗?`, '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const res = await deleteAgentApi(row.id)
|
||||
if (res?.code === 200) {
|
||||
ElMessage.success('已删除')
|
||||
reload()
|
||||
} else {
|
||||
ElMessage.error(res?.msg || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 哨兵进入视口即触发下一页加载;提前 200px 预加载,滚动更顺滑
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting) {
|
||||
loadMore()
|
||||
}
|
||||
},
|
||||
{ rootMargin: '200px' }
|
||||
)
|
||||
reload()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer?.disconnect()
|
||||
observer = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search-form {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.waterfall-wrap {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* CSS 多列实现真实瀑布流:卡片高度不一致时自动错落排布 */
|
||||
.waterfall {
|
||||
column-count: 4;
|
||||
column-gap: 16px;
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
column-count: 3;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
column-count: 2;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-item {
|
||||
break-inside: avoid;
|
||||
-webkit-column-break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.api-card {
|
||||
background: var(--el-bg-color-overlay);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.25s ease, transform 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.65;
|
||||
filter: grayscale(0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 14px 12px;
|
||||
border-top: 3px solid var(--el-color-primary);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--el-fill-color-light);
|
||||
|
||||
.provider-badge {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.head-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-action-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 10px;
|
||||
font-weight: 600;
|
||||
|
||||
.action-arrow {
|
||||
margin-left: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-section {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px dashed var(--el-border-color-lighter);
|
||||
|
||||
.section-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.model-hint {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.model-count {
|
||||
margin-left: auto;
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.key-actions,
|
||||
.section-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(.el-button) {
|
||||
padding: 0 2px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.url-text {
|
||||
font-size: 13px;
|
||||
color: var(--el-color-primary);
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.key-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.key-item {
|
||||
padding: 8px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 4px;
|
||||
|
||||
.key-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.key-no-remark {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(.el-button) {
|
||||
padding: 0 2px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.key-text {
|
||||
font-size: 13px;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
color: var(--el-text-color-regular);
|
||||
letter-spacing: 0.5px;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.model-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
|
||||
.model-tag {
|
||||
max-width: 100%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-models {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.remark-text {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 14px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
|
||||
.foot-time {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
.load-sentinel {
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
.loading-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 24px 0;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.list-end {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user