feat: 在文件上传组件中添加上传进度显示和队列管理功能
This commit is contained in:
parent
b1746c9270
commit
638da2afb8
@ -12,6 +12,7 @@
|
|||||||
- 添加自定义向量模型和 rerank 模型的配置,在网页上面
|
- 添加自定义向量模型和 rerank 模型的配置,在网页上面
|
||||||
- 调研轻便的文件展示与编辑器
|
- 调研轻便的文件展示与编辑器
|
||||||
- 移除 TODO 的模块与设计,移除这个中间件。
|
- 移除 TODO 的模块与设计,移除这个中间件。
|
||||||
|
- Yuxi-cli 相关的功能,放在后续版本中实现(不是类似于编程助手,而是工具)
|
||||||
|
|
||||||
### Bugs
|
### Bugs
|
||||||
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
|
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
|
||||||
|
|||||||
@ -12,11 +12,7 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
@click="chunkData"
|
@click="chunkData"
|
||||||
:loading="chunkLoading"
|
:loading="chunkLoading"
|
||||||
:disabled="
|
:disabled="!canSubmit"
|
||||||
uploadMode === 'url'
|
|
||||||
? !urlList.some((i) => i.status === 'success')
|
|
||||||
: fileList.length === 0
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
添加到知识库
|
添加到知识库
|
||||||
</a-button>
|
</a-button>
|
||||||
@ -141,6 +137,7 @@
|
|||||||
:multiple="true"
|
:multiple="true"
|
||||||
:directory="isFolderUpload"
|
:directory="isFolderUpload"
|
||||||
:disabled="chunkLoading"
|
:disabled="chunkLoading"
|
||||||
|
:show-upload-list="!showAggregateProgress"
|
||||||
:accept="acceptedFileTypes"
|
:accept="acceptedFileTypes"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
:customRequest="customRequest"
|
:customRequest="customRequest"
|
||||||
@ -153,6 +150,53 @@
|
|||||||
<p class="ant-upload-hint">支持类型: {{ uploadHint }}</p>
|
<p class="ant-upload-hint">支持类型: {{ uploadHint }}</p>
|
||||||
<div class="zip-tip" v-if="hasZipFiles">📦 ZIP包将自动解压提取 Markdown 与图片</div>
|
<div class="zip-tip" v-if="hasZipFiles">📦 ZIP包将自动解压提取 Markdown 与图片</div>
|
||||||
</a-upload-dragger>
|
</a-upload-dragger>
|
||||||
|
|
||||||
|
<div v-if="showAggregateProgress" class="upload-progress-card">
|
||||||
|
<div class="progress-header">
|
||||||
|
<div class="progress-header-left">
|
||||||
|
<div class="progress-title">上传进度</div>
|
||||||
|
<div class="progress-stats inline-in-header">
|
||||||
|
<div class="stat-pill">总计 {{ totalUploadCount }}</div>
|
||||||
|
<div class="stat-pill uploading" v-if="uploadingUploadCount > 0">
|
||||||
|
上传中 {{ uploadingUploadCount }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-pill queued" v-if="queuedUploadCount > 0">排队 {{ queuedUploadCount }}</div>
|
||||||
|
<div class="stat-pill error" v-if="failedUploadCount > 0">失败 {{ failedUploadCount }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-header-right">
|
||||||
|
<div class="progress-percent">{{ overallUploadProgress }}%</div>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
class="toggle-progress-btn"
|
||||||
|
@click="progressExpanded = !progressExpanded"
|
||||||
|
>
|
||||||
|
<span>{{ progressExpanded ? '收起' : '展开' }}</span>
|
||||||
|
<ChevronUp v-if="progressExpanded" :size="14" />
|
||||||
|
<ChevronDown v-else :size="14" />
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="progressExpanded" class="progress-details">
|
||||||
|
<div class="details-list" v-if="failedDetailItems.length > 0">
|
||||||
|
<div v-for="item in failedDetailItems" :key="item.uid" class="detail-row">
|
||||||
|
<span class="detail-name" :title="item.name">{{ item.name }}</span>
|
||||||
|
<span class="detail-error" :title="item.errorText">{{ item.errorText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-tip" v-else>当前无失败文件。</div>
|
||||||
|
|
||||||
|
<div class="progress-tip" v-if="hasPendingUploads">
|
||||||
|
文件夹上传采用队列模式,最多同时上传 {{ MAX_UPLOAD_CONCURRENCY }} 个文件。
|
||||||
|
</div>
|
||||||
|
<div class="progress-tip" v-else>
|
||||||
|
上传队列已完成,可点击“添加到知识库”继续下一步。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- URL 输入区域 -->
|
<!-- URL 输入区域 -->
|
||||||
@ -263,7 +307,9 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Trash2,
|
Trash2,
|
||||||
Link,
|
Link,
|
||||||
X
|
X,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import ChunkParamsConfig from '@/components/ChunkParamsConfig.vue'
|
import ChunkParamsConfig from '@/components/ChunkParamsConfig.vue'
|
||||||
@ -411,6 +457,69 @@ const chunkLoading = computed(() => store.state.chunkLoading)
|
|||||||
// 上传模式
|
// 上传模式
|
||||||
const uploadMode = ref('file')
|
const uploadMode = ref('file')
|
||||||
const previousOcrSelection = ref('disable')
|
const previousOcrSelection = ref('disable')
|
||||||
|
const MAX_UPLOAD_CONCURRENCY = 10
|
||||||
|
|
||||||
|
// 文件列表
|
||||||
|
const fileList = ref([])
|
||||||
|
|
||||||
|
const uploadQueue = ref([])
|
||||||
|
const activeUploadCount = ref(0)
|
||||||
|
const uploadTaskStatus = ref({})
|
||||||
|
const uploadTaskProgress = ref({})
|
||||||
|
const progressExpanded = ref(false)
|
||||||
|
|
||||||
|
const totalUploadCount = computed(() => fileList.value.length)
|
||||||
|
const queuedUploadCount = computed(
|
||||||
|
() => Object.values(uploadTaskStatus.value).filter((status) => status === 'queued').length
|
||||||
|
)
|
||||||
|
const uploadingUploadCount = computed(
|
||||||
|
() => Object.values(uploadTaskStatus.value).filter((status) => status === 'uploading').length
|
||||||
|
)
|
||||||
|
const successUploadCount = computed(
|
||||||
|
() => Object.values(uploadTaskStatus.value).filter((status) => status === 'done').length
|
||||||
|
)
|
||||||
|
const failedUploadCount = computed(
|
||||||
|
() => Object.values(uploadTaskStatus.value).filter((status) => status === 'error').length
|
||||||
|
)
|
||||||
|
const hasPendingUploads = computed(() => queuedUploadCount.value + uploadingUploadCount.value > 0)
|
||||||
|
|
||||||
|
const overallUploadProgress = computed(() => {
|
||||||
|
const total = totalUploadCount.value
|
||||||
|
if (!total) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const validUidSet = new Set(fileList.value.map((file) => file.uid).filter(Boolean))
|
||||||
|
let sum = 0
|
||||||
|
for (const uid of validUidSet) {
|
||||||
|
sum += uploadTaskProgress.value[uid] || 0
|
||||||
|
}
|
||||||
|
return Math.round(sum / total)
|
||||||
|
})
|
||||||
|
|
||||||
|
const showAggregateProgress = computed(() => totalUploadCount.value >= MAX_UPLOAD_CONCURRENCY)
|
||||||
|
|
||||||
|
const failedDetailItems = computed(() => {
|
||||||
|
return fileList.value
|
||||||
|
.map((file) => {
|
||||||
|
const uid = file.uid
|
||||||
|
const rawStatus = uploadTaskStatus.value[uid] || file.status || 'unknown'
|
||||||
|
const detail = file?.response?.detail || file?.error?.message || ''
|
||||||
|
return {
|
||||||
|
uid,
|
||||||
|
name: file.name || '未命名文件',
|
||||||
|
status: rawStatus,
|
||||||
|
errorText: detail || '上传失败'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item) => item.status === 'error')
|
||||||
|
})
|
||||||
|
|
||||||
|
const canSubmit = computed(() => {
|
||||||
|
if (uploadMode.value === 'url') {
|
||||||
|
return urlList.value.some((item) => item.status === 'success')
|
||||||
|
}
|
||||||
|
return successUploadCount.value > 0 && !hasPendingUploads.value
|
||||||
|
})
|
||||||
|
|
||||||
const uploadModeOptions = computed(() => [
|
const uploadModeOptions = computed(() => [
|
||||||
{
|
{
|
||||||
@ -443,10 +552,34 @@ watch(uploadMode, (val) => {
|
|||||||
sameNameFiles.value = []
|
sameNameFiles.value = []
|
||||||
urlList.value = []
|
urlList.value = []
|
||||||
newUrl.value = ''
|
newUrl.value = ''
|
||||||
|
for (const task of uploadQueue.value) {
|
||||||
|
task.canceled = true
|
||||||
|
}
|
||||||
|
uploadQueue.value = []
|
||||||
|
uploadTaskStatus.value = {}
|
||||||
|
uploadTaskProgress.value = {}
|
||||||
|
progressExpanded.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 文件列表
|
watch(fileList, (newFileList) => {
|
||||||
const fileList = ref([])
|
const validUidSet = new Set(newFileList.map((file) => file.uid).filter(Boolean))
|
||||||
|
const nextStatus = {}
|
||||||
|
const nextProgress = {}
|
||||||
|
|
||||||
|
for (const [uid, status] of Object.entries(uploadTaskStatus.value)) {
|
||||||
|
if (validUidSet.has(uid)) {
|
||||||
|
nextStatus[uid] = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [uid, progress] of Object.entries(uploadTaskProgress.value)) {
|
||||||
|
if (validUidSet.has(uid)) {
|
||||||
|
nextProgress[uid] = progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadTaskStatus.value = nextStatus
|
||||||
|
uploadTaskProgress.value = nextProgress
|
||||||
|
})
|
||||||
|
|
||||||
// URL 列表
|
// URL 列表
|
||||||
// Item structure: { url: string, status: 'fetching'|'success'|'error', data: object|null, error: string }
|
// Item structure: { url: string, status: 'fetching'|'success'|'error', data: object|null, error: string }
|
||||||
@ -882,56 +1015,161 @@ const deleteSameNameFile = (file) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customRequest = async (options) => {
|
const customRequest = async (options) => {
|
||||||
const { file, onProgress, onSuccess, onError } = options
|
const fileUid = options.file?.uid
|
||||||
|
if (fileUid) {
|
||||||
const formData = new FormData()
|
uploadTaskStatus.value[fileUid] = 'queued'
|
||||||
// 如果是文件夹上传,使用相对路径作为文件名
|
uploadTaskProgress.value[fileUid] = 0
|
||||||
const filename =
|
|
||||||
isFolderUpload.value && file.webkitRelativePath ? file.webkitRelativePath : file.name
|
|
||||||
formData.append('file', file, filename)
|
|
||||||
|
|
||||||
const dbId = databaseId.value
|
|
||||||
if (!dbId) {
|
|
||||||
onError(new Error('Database ID is missing'))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest()
|
const task = {
|
||||||
xhr.open('POST', `/api/knowledge/files/upload?db_id=${dbId}`)
|
options,
|
||||||
|
xhr: null,
|
||||||
const headers = getAuthHeaders()
|
canceled: false
|
||||||
for (const [key, value] of Object.entries(headers)) {
|
|
||||||
xhr.setRequestHeader(key, value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xhr.upload.onprogress = (e) => {
|
uploadQueue.value.push(task)
|
||||||
if (e.lengthComputable) {
|
processUploadQueue()
|
||||||
onProgress({ percent: (e.loaded / e.total) * 100 })
|
|
||||||
|
return {
|
||||||
|
abort: () => {
|
||||||
|
task.canceled = true
|
||||||
|
if (task.xhr) {
|
||||||
|
task.xhr.abort()
|
||||||
|
}
|
||||||
|
const queueIndex = uploadQueue.value.indexOf(task)
|
||||||
|
if (queueIndex !== -1) {
|
||||||
|
uploadQueue.value.splice(queueIndex, 1)
|
||||||
|
}
|
||||||
|
if (fileUid) {
|
||||||
|
uploadTaskStatus.value[fileUid] = 'error'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
xhr.onload = () => {
|
const processUploadQueue = () => {
|
||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
while (activeUploadCount.value < MAX_UPLOAD_CONCURRENCY && uploadQueue.value.length > 0) {
|
||||||
try {
|
const task = uploadQueue.value.shift()
|
||||||
const response = JSON.parse(xhr.responseText)
|
if (!task || task.canceled) {
|
||||||
onSuccess(response, xhr)
|
continue
|
||||||
} catch (e) {
|
}
|
||||||
onError(e)
|
|
||||||
|
activeUploadCount.value += 1
|
||||||
|
runUploadTask(task)
|
||||||
|
.catch(() => {
|
||||||
|
// 错误已经在 runUploadTask 内处理,这里只保证队列继续消费
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
activeUploadCount.value -= 1
|
||||||
|
processUploadQueue()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runUploadTask = (task) => {
|
||||||
|
const { file, onProgress, onSuccess, onError } = task.options
|
||||||
|
const fileUid = file?.uid
|
||||||
|
|
||||||
|
if (fileUid) {
|
||||||
|
uploadTaskStatus.value[fileUid] = 'uploading'
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
const filename =
|
||||||
|
isFolderUpload.value && file.webkitRelativePath ? file.webkitRelativePath : file.name
|
||||||
|
formData.append('file', file, filename)
|
||||||
|
|
||||||
|
const dbId = databaseId.value
|
||||||
|
if (!dbId) {
|
||||||
|
const error = new Error('Database ID is missing')
|
||||||
|
if (fileUid) {
|
||||||
|
uploadTaskStatus.value[fileUid] = 'error'
|
||||||
|
}
|
||||||
|
onError(error)
|
||||||
|
reject(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
task.xhr = xhr
|
||||||
|
xhr.open('POST', `/api/knowledge/files/upload?db_id=${dbId}`)
|
||||||
|
|
||||||
|
const headers = getAuthHeaders()
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
xhr.setRequestHeader(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.upload.onprogress = (event) => {
|
||||||
|
if (!event.lengthComputable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const percent = Math.min(100, (event.loaded / event.total) * 100)
|
||||||
|
if (fileUid) {
|
||||||
|
uploadTaskProgress.value[fileUid] = percent
|
||||||
|
}
|
||||||
|
onProgress({ percent })
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (task.canceled) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(xhr.responseText)
|
||||||
|
if (fileUid) {
|
||||||
|
uploadTaskStatus.value[fileUid] = 'done'
|
||||||
|
uploadTaskProgress.value[fileUid] = 100
|
||||||
|
}
|
||||||
|
onSuccess(response, xhr)
|
||||||
|
resolve()
|
||||||
|
} catch (error) {
|
||||||
|
if (fileUid) {
|
||||||
|
uploadTaskStatus.value[fileUid] = 'error'
|
||||||
|
}
|
||||||
|
onError(error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorResp = {}
|
||||||
|
try {
|
||||||
|
errorResp = JSON.parse(xhr.responseText || '{}')
|
||||||
|
} catch {
|
||||||
|
errorResp = {}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const errorResp = JSON.parse(xhr.responseText)
|
|
||||||
// 设置 file.response 让 handleFileUpload 能读取到错误信息
|
|
||||||
file.response = errorResp
|
file.response = errorResp
|
||||||
const error = new Error(errorResp.detail || 'Upload failed')
|
const error = new Error(errorResp.detail || 'Upload failed')
|
||||||
|
if (fileUid) {
|
||||||
|
uploadTaskStatus.value[fileUid] = 'error'
|
||||||
|
}
|
||||||
onError(error, file)
|
onError(error, file)
|
||||||
|
reject(error)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
xhr.onerror = (e) => {
|
xhr.onerror = (errorEvent) => {
|
||||||
onError(e)
|
if (fileUid) {
|
||||||
}
|
uploadTaskStatus.value[fileUid] = 'error'
|
||||||
|
}
|
||||||
|
onError(errorEvent)
|
||||||
|
reject(errorEvent)
|
||||||
|
}
|
||||||
|
|
||||||
xhr.send(formData)
|
xhr.onabort = () => {
|
||||||
|
if (fileUid) {
|
||||||
|
uploadTaskStatus.value[fileUid] = 'error'
|
||||||
|
}
|
||||||
|
const abortError = new Error('Upload aborted')
|
||||||
|
onError(abortError)
|
||||||
|
reject(abortError)
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send(formData)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileUpload = (info) => {
|
const handleFileUpload = (info) => {
|
||||||
@ -1425,6 +1663,152 @@ const chunkData = async () => {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-progress-card {
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--gray-50);
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-700);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percent {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--main-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&.inline-in-header {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-pill {
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
border: 1px solid var(--gray-300);
|
||||||
|
background: var(--gray-100);
|
||||||
|
color: var(--gray-600);
|
||||||
|
|
||||||
|
&.uploading {
|
||||||
|
background: var(--main-50);
|
||||||
|
border-color: var(--main-200);
|
||||||
|
color: var(--main-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.queued {
|
||||||
|
background: var(--gray-100);
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: var(--color-success-50);
|
||||||
|
border-color: var(--color-success-200);
|
||||||
|
color: var(--color-success-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: var(--color-error-50);
|
||||||
|
border-color: var(--color-error-200);
|
||||||
|
color: var(--color-error-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tip {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-details {
|
||||||
|
border-top: 1px dashed var(--gray-200);
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-list {
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--gray-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid var(--gray-100);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--gray-700);
|
||||||
|
font-weight: 500;
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-error {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-error-600);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-progress-btn {
|
||||||
|
color: var(--gray-500);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding-inline: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--main-600);
|
||||||
|
background: var(--gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* URL Area */
|
/* URL Area */
|
||||||
.url-area {
|
.url-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -1654,4 +2038,26 @@ const chunkData = async () => {
|
|||||||
.setting-label .ant-checkbox {
|
.setting-label .ant-checkbox {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.top-action-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-index-toggle {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user