新增: 文件浏览器增强(窗口分离/Git 变更/行号/Diff/分页提交历史)
- 窗口分离: FileExplorer 可弹出独立 Tauri 窗口 - 行号显示: 文件预览左侧显示行号列 - 按后缀图标: rs/ts/js/vue/css/html 等 20+ 类型彩色图标 - Diff 视图: 有 Git 变更的文件可切换 diff 红绿视图 - Git 变更面板: 变更文件列表(按目录缩进分组)+提交历史(分页加载) - 提交详情: 点击提交查看变更文件列表及文件级 diff - 时间显示: 提交历史显示相对时间/具体日期 - 面包屑导航不丢预览: 导航时不改变当前预览文件 - 刷新保留选中文件: 只清树缓存不清预览 - 中文编码修复: git 命令注入 LANG/LC_ALL 环境变量 - 自适应布局: 弹性 flex 布局,窄窗口适配 - 滚动条优化: 内容区内部滚动,不溢出页面层 - 多工程管理: 工具栏添加工程按钮+弹窗表单 - 审批加载超时缩短: 130s 降至 30s - 文件变更自动刷新: write_file/patch_file 触发 df-data-changed
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
<template>
|
||||
<div class="git-changes">
|
||||
<!-- 分支信息 -->
|
||||
<div v-if="branchName" class="git-branch-bar">
|
||||
<span class="branch-icon">⎇</span>
|
||||
<span class="branch-name">{{ branchName }}</span>
|
||||
<span v-if="!showHistory" class="branch-count">{{ gitStatus?.changed_files.length ?? 0 }} {{ $t('gitChanges.changedFiles') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !gitStatus" class="gc-loading"><span class="spinner"></span></div>
|
||||
|
||||
<!-- 非 Git 仓库 -->
|
||||
<div v-else-if="gitStatus && !gitStatus.is_git_repo" class="gc-empty">
|
||||
{{ $t('gitChanges.notGitRepo') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="gitStatus" class="gc-body">
|
||||
<!-- Tab: 变更 / 历史 -->
|
||||
<nav class="gc-tabs">
|
||||
<button class="gc-tab" :class="{ active: !showHistory }" @click="showHistory = false">
|
||||
{{ $t('gitChanges.changes') }} ({{ gitStatus.changed_files.length }})
|
||||
</button>
|
||||
<button class="gc-tab" :class="{ active: showHistory }" @click="switchToHistory">
|
||||
{{ $t('gitChanges.history') }} ({{ totalCommits }})
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- ====== 变更列表 ====== -->
|
||||
<div v-if="!showHistory" class="gc-changes">
|
||||
<template v-if="gitStatus.changed_files.length === 0">
|
||||
<div class="gc-empty">{{ $t('gitChanges.noChanges') }}</div>
|
||||
</template>
|
||||
|
||||
<!-- 按目录树分组的变更文件 -->
|
||||
<div v-for="group in groupedChanges" :key="group.label" class="gc-group">
|
||||
<div class="gc-group-header">{{ group.label }} ({{ group.files.length }})</div>
|
||||
<div v-for="f in group.files" :key="f.path"
|
||||
class="gc-file-row"
|
||||
:class="{ selected: selectedFile === f.path }"
|
||||
:style="{ paddingLeft: filePathDepth(f.path) * 16 + 14 + 'px' }"
|
||||
@click="selectFile(f.path)"
|
||||
>
|
||||
<span class="gc-file-status" :class="'gc-st-' + group.key">{{ statusLabel(f.status) }}</span>
|
||||
<span class="gc-file-name">{{ filePathLeaf(f.path) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ====== 提交历史 ====== -->
|
||||
<div v-else class="gc-history-wrapper">
|
||||
<!-- 已选提交的详情 -->
|
||||
<div v-if="selectedCommit" class="gc-commit-detail">
|
||||
<div class="gc-commit-detail-header">
|
||||
<span class="gc-commit-detail-hash">{{ selectedCommit.hash }}</span>
|
||||
<span class="gc-commit-detail-msg">{{ selectedCommit.subject }}</span>
|
||||
<span class="gc-commit-detail-time">{{ formatTime(selectedCommit.timestamp) }}</span>
|
||||
<button class="gc-detail-close" @click="selectedCommit = null; commitDetail = null">✕</button>
|
||||
</div>
|
||||
<div v-if="commitDetailLoading" class="gc-loading"><span class="spinner"></span></div>
|
||||
<template v-else-if="commitDetail">
|
||||
<!-- 变更文件列表 -->
|
||||
<div class="gc-commit-files">
|
||||
<div
|
||||
v-for="f in commitDetail.files"
|
||||
:key="f.path"
|
||||
class="gc-commit-file-row"
|
||||
:class="{ active: commitSelectedFile === f.path }"
|
||||
@click="showCommitFileDiff(f.path)"
|
||||
>
|
||||
<span class="gc-file-status" :class="'gc-st-' + statusKey(f.status)">{{ statusLabel(f.status) }}</span>
|
||||
<span class="gc-file-path">{{ f.path }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Diff 预览 -->
|
||||
<div v-if="commitSelectedFile && commitDiff" class="gc-diff-content">
|
||||
<div v-for="(ln, idx) in commitDiffLines" :key="idx" class="diff-line" :class="'diff-' + ln.type">
|
||||
<span class="diff-hdr-text" v-if="ln.type === 'hdr'">{{ ln.text }}</span>
|
||||
<template v-else>
|
||||
<span class="diff-line-prefix">{{ ln.prefix }}</span>
|
||||
<span class="diff-line-text">{{ ln.text }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 提交列表 -->
|
||||
<div class="gc-history-list">
|
||||
<div
|
||||
v-for="c in commits"
|
||||
:key="c.hash"
|
||||
class="gc-commit-row"
|
||||
:class="{ selected: selectedCommit?.hash === c.hash }"
|
||||
@click="selectCommit(c)"
|
||||
>
|
||||
<span class="gc-commit-hash">{{ c.hash.slice(0, 8) }}</span>
|
||||
<span class="gc-commit-msg">{{ c.subject }}</span>
|
||||
<span class="gc-commit-time">{{ formatTime(c.timestamp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<div v-if="hasMoreCommits && !loadingMore" class="gc-load-more" @click="loadMoreCommits">
|
||||
{{ $t('gitChanges.loadMore') }}
|
||||
</div>
|
||||
<div v-if="loadingMore" class="gc-loading"><span class="spinner"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Git 变更查看器 — 变更文件列表(按目录树分组) + 提交历史(分页) + 文件 diff 预览。
|
||||
*/
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { moduleApi, type GitStatusResult } from '@/api/module'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
moduleId: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-file', path: string): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const gitStatus = ref<GitStatusResult | null>(null)
|
||||
const showHistory = ref(false)
|
||||
const selectedFile = ref('')
|
||||
const diffContent = ref('')
|
||||
const diffLoading = ref(false)
|
||||
|
||||
// 提交历史(分页)
|
||||
const commits = ref<{ hash: string; subject: string; timestamp: number }[]>([])
|
||||
const totalCommits = ref(0)
|
||||
const hasMoreCommits = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const commitSkip = ref(0)
|
||||
const COMMIT_PAGE_SIZE = 50
|
||||
|
||||
// 已选提交详情
|
||||
const selectedCommit = ref<{ hash: string; subject: string; timestamp: number } | null>(null)
|
||||
const commitDetail = ref<{ files: { status: string; path: string }[]; diff: string } | null>(null)
|
||||
const commitDetailLoading = ref(false)
|
||||
const commitSelectedFile = ref('')
|
||||
const commitDiff = ref('')
|
||||
|
||||
const branchName = computed(() => gitStatus.value?.branch || '')
|
||||
|
||||
interface FileGroup {
|
||||
key: string
|
||||
label: string
|
||||
files: { path: string; status: string }[]
|
||||
}
|
||||
|
||||
function statusKey(s: string): string {
|
||||
const x = s.trim()
|
||||
if (x.startsWith('M') || x === 'R') return 'modified'
|
||||
if (x.startsWith('A')) return 'added'
|
||||
if (x.startsWith('D')) return 'deleted'
|
||||
if (x === '??') return 'untracked'
|
||||
return 'modified'
|
||||
}
|
||||
|
||||
/** 按状态分组 */
|
||||
const groupedChanges = computed<FileGroup[]>(() => {
|
||||
if (!gitStatus.value) return []
|
||||
const groups: FileGroup[] = [
|
||||
{ key: 'modified', label: t('gitChanges.modified'), files: [] },
|
||||
{ key: 'added', label: t('gitChanges.added'), files: [] },
|
||||
{ key: 'deleted', label: t('gitChanges.deleted'), files: [] },
|
||||
{ key: 'untracked', label: t('gitChanges.untracked'), files: [] },
|
||||
]
|
||||
for (const f of gitStatus.value.changed_files) {
|
||||
const s = f.status.trim()
|
||||
if (s.startsWith('M') || s === 'R') groups[0].files.push(f)
|
||||
else if (s.startsWith('A')) groups[1].files.push(f)
|
||||
else if (s.startsWith('D')) groups[2].files.push(f)
|
||||
else if (s === '??') groups[3].files.push(f)
|
||||
else groups[0].files.push(f)
|
||||
}
|
||||
return groups.filter(g => g.files.length > 0)
|
||||
})
|
||||
|
||||
interface DiffLine {
|
||||
type: 'add' | 'del' | 'ctx' | 'hdr'
|
||||
prefix: string
|
||||
text: string
|
||||
}
|
||||
|
||||
const commitDiffLines = computed<DiffLine[]>(() => parseDiff(commitDiff.value))
|
||||
|
||||
function parseDiff(text: string): DiffLine[] {
|
||||
if (!text) return []
|
||||
const lines: DiffLine[] = []
|
||||
for (const raw of text.split('\n')) {
|
||||
if (raw.startsWith('@@')) {
|
||||
lines.push({ type: 'hdr', prefix: '', text: raw })
|
||||
} else if (raw.startsWith('+')) {
|
||||
lines.push({ type: 'add', prefix: '+', text: raw.slice(1) })
|
||||
} else if (raw.startsWith('-')) {
|
||||
lines.push({ type: 'del', prefix: '-', text: raw.slice(1) })
|
||||
} else if (raw.startsWith('\\')) {
|
||||
continue
|
||||
} else {
|
||||
lines.push({ type: 'ctx', prefix: ' ', text: raw })
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
function statusLabel(s: string): string {
|
||||
const x = s.trim()
|
||||
if (x === '??') return 'U'
|
||||
if (x.startsWith('A')) return 'A'
|
||||
if (x.startsWith('M')) return 'M'
|
||||
if (x.startsWith('D')) return 'D'
|
||||
if (x.startsWith('R')) return 'R'
|
||||
return x.charAt(0) || '?'
|
||||
}
|
||||
|
||||
/** 格式化 Unix 时间戳为相对时间或具体时间 */
|
||||
function formatTime(ts: number): string {
|
||||
if (!ts) return ''
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const diffSec = now - ts
|
||||
if (diffSec < 60) return t('common.justNow')
|
||||
if (diffSec < 3600) return t('common.minutesAgo', { n: Math.floor(diffSec / 60) })
|
||||
if (diffSec < 86400) return t('common.hoursAgo', { n: Math.floor(diffSec / 3600) })
|
||||
if (diffSec < 172800) return t('common.yesterday')
|
||||
if (diffSec < 2592000) return t('common.dayAgo', { n: Math.floor(diffSec / 86400) })
|
||||
// 超过 30 天显示具体日期
|
||||
const d = new Date(ts * 1000)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** 计算文件路径的目录深度(用于缩进)。root 级文件返回 0。 */
|
||||
function filePathDepth(path: string): number {
|
||||
const segs = path.split('/').filter(Boolean)
|
||||
return Math.max(0, segs.length - 1)
|
||||
}
|
||||
|
||||
/** 提取文件名(最后一段)。 */
|
||||
function filePathLeaf(path: string): string {
|
||||
const segs = path.split('/').filter(Boolean)
|
||||
return segs[segs.length - 1] || path
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
if (!props.moduleId) return
|
||||
loading.value = true
|
||||
try {
|
||||
gitStatus.value = await moduleApi.getModuleGitStatus(props.moduleId)
|
||||
// 同时初始化前 50 条提交
|
||||
const res = await moduleApi.getModuleCommits(props.moduleId, 0, COMMIT_PAGE_SIZE)
|
||||
commits.value = res.commits
|
||||
hasMoreCommits.value = res.has_more
|
||||
totalCommits.value = res.commits.length
|
||||
commitSkip.value = res.commits.length
|
||||
} catch {
|
||||
gitStatus.value = null
|
||||
commits.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreCommits() {
|
||||
if (loadingMore.value || !hasMoreCommits.value) return
|
||||
loadingMore.value = true
|
||||
try {
|
||||
const res = await moduleApi.getModuleCommits(props.moduleId, commitSkip.value, COMMIT_PAGE_SIZE)
|
||||
commits.value.push(...res.commits)
|
||||
hasMoreCommits.value = res.has_more
|
||||
commitSkip.value = commits.value.length
|
||||
totalCommits.value = commits.value.length
|
||||
} catch {
|
||||
// 静默失败
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function switchToHistory() {
|
||||
showHistory.value = true
|
||||
// 如果只加载了初始数量,尝试补页
|
||||
if (hasMoreCommits.value && commits.value.length < 50) {
|
||||
await loadMoreCommits()
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFile(path: string) {
|
||||
selectedFile.value = path
|
||||
emit('select-file', path)
|
||||
diffContent.value = ''
|
||||
diffLoading.value = true
|
||||
try {
|
||||
const res = await moduleApi.getModuleFileDiff(props.moduleId, path)
|
||||
diffContent.value = res.diff || ''
|
||||
} catch {
|
||||
diffContent.value = ''
|
||||
} finally {
|
||||
diffLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击提交行 → 加载该提交的变更文件列表 */
|
||||
async function selectCommit(c: { hash: string; subject: string; timestamp: number }) {
|
||||
selectedCommit.value = c
|
||||
commitDetail.value = null
|
||||
commitSelectedFile.value = ''
|
||||
commitDiff.value = ''
|
||||
commitDetailLoading.value = true
|
||||
try {
|
||||
const res = await moduleApi.getCommitDetail(props.moduleId, c.hash)
|
||||
commitDetail.value = res
|
||||
} catch {
|
||||
commitDetail.value = null
|
||||
} finally {
|
||||
commitDetailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 在提交详情中点击文件 → 过滤出该文件的 diff */
|
||||
function showCommitFileDiff(path: string) {
|
||||
commitSelectedFile.value = path
|
||||
if (!commitDetail.value) return
|
||||
// 从全量 diff 中提取该文件的块
|
||||
const diff = commitDetail.value.diff
|
||||
const lines = diff.split('\n')
|
||||
let inTarget = false
|
||||
const targetLines: string[] = []
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('diff --git')) {
|
||||
inTarget = line.includes(path)
|
||||
}
|
||||
if (inTarget) {
|
||||
targetLines.push(line)
|
||||
}
|
||||
}
|
||||
commitDiff.value = targetLines.join('\n')
|
||||
}
|
||||
|
||||
watch(() => props.moduleId, loadStatus, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.git-changes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
font-size: 13px;
|
||||
color: var(--df-text);
|
||||
}
|
||||
.git-branch-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
background: var(--df-bg-card);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.branch-icon { font-size: 16px; }
|
||||
.branch-name { font-weight: 600; color: var(--df-accent); }
|
||||
.branch-count { margin-left: auto; font-size: 12px; color: var(--df-text-dim); }
|
||||
|
||||
/* Tabs */
|
||||
.gc-tabs { display: flex; border-bottom: 0.5px solid var(--df-border); flex-shrink: 0; }
|
||||
.gc-tab {
|
||||
flex: 1; padding: 8px; background: none; border: none;
|
||||
border-bottom: 2px solid transparent; color: var(--df-text-dim);
|
||||
cursor: pointer; font-size: 12px; transition: all 0.15s;
|
||||
}
|
||||
.gc-tab.active { color: var(--df-text); border-bottom-color: var(--df-accent); }
|
||||
.gc-tab:hover { color: var(--df-text); }
|
||||
|
||||
.gc-body { flex: 1; overflow-y: auto; overflow-x: hidden; min-height: 0; scrollbar-width: thin; }
|
||||
.gc-changes { min-height: 0; }
|
||||
|
||||
/* 分组 */
|
||||
.gc-group-header {
|
||||
padding: 6px 14px; font-size: 11px; font-weight: 600;
|
||||
color: var(--df-text-dim); text-transform: uppercase; letter-spacing: 0.5px;
|
||||
background: rgba(255,255,255,0.02); border-bottom: 0.5px solid var(--df-border);
|
||||
position: sticky; top: 0; z-index: 1;
|
||||
}
|
||||
|
||||
/* Diff 预览 */
|
||||
.gc-diff-content {
|
||||
font-family: var(--df-font-mono, Consolas, monospace);
|
||||
font-size: 12px; line-height: 1.5; overflow: auto; scrollbar-width: thin;
|
||||
}
|
||||
.diff-line { display: flex; padding: 0 14px; }
|
||||
.diff-line-prefix { width: 16px; flex-shrink: 0; text-align: center; user-select: none; }
|
||||
.diff-line-text { flex: 1; white-space: pre; overflow: hidden; }
|
||||
.diff-hdr-text {
|
||||
padding: 4px 14px; background: rgba(60,140,220,0.08);
|
||||
color: var(--df-text-dim); font-weight: 500; font-size: 11px; display: block;
|
||||
}
|
||||
.diff-add { background: rgba(40,160,70,0.12); }
|
||||
.diff-add .diff-line-prefix { color: #4caf50; }
|
||||
.diff-del { background: rgba(220,60,60,0.12); }
|
||||
.diff-del .diff-line-prefix { color: #e05050; }
|
||||
.diff-ctx { color: var(--df-text); }
|
||||
|
||||
/* 提交历史 */
|
||||
.gc-history-wrapper { display: flex; flex-direction: column; }
|
||||
.gc-commit-row {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 8px 14px; border-bottom: 0.5px solid rgba(255,255,255,0.03);
|
||||
cursor: pointer; transition: background 0.1s;
|
||||
}
|
||||
.gc-commit-row:hover { background: rgba(255,255,255,0.04); }
|
||||
.gc-commit-row.selected { background: rgba(255,255,255,0.06); }
|
||||
.gc-commit-hash {
|
||||
font-family: var(--df-font-mono, Consolas, monospace);
|
||||
font-size: 11px; color: var(--df-accent); flex-shrink: 0;
|
||||
}
|
||||
.gc-commit-msg { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
||||
.gc-commit-time { font-size: 11px; color: var(--df-text-dim); flex-shrink: 0; }
|
||||
|
||||
/* 提交详情 */
|
||||
.gc-commit-detail { border-bottom: 0.5px solid var(--df-border); }
|
||||
.gc-commit-detail-header {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 8px 14px; background: rgba(255,255,255,0.03);
|
||||
position: sticky; top: 0; z-index: 1;
|
||||
}
|
||||
.gc-commit-detail-hash {
|
||||
font-family: var(--df-font-mono, Consolas, monospace);
|
||||
font-size: 11px; color: var(--df-accent);
|
||||
}
|
||||
.gc-commit-detail-msg { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
||||
.gc-commit-detail-time { font-size: 11px; color: var(--df-text-dim); flex-shrink: 0; }
|
||||
.gc-detail-close {
|
||||
background: none; border: none; color: var(--df-text-dim);
|
||||
cursor: pointer; font-size: 14px; padding: 0 4px;
|
||||
}
|
||||
|
||||
/* 提交的文件列表 */
|
||||
.gc-commit-files { border-bottom: 0.5px solid var(--df-border); }
|
||||
.gc-commit-file-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 5px 14px 5px 28px; cursor: pointer; transition: background 0.1s;
|
||||
border-bottom: 0.5px solid rgba(255,255,255,0.02);
|
||||
}
|
||||
.gc-commit-file-row:hover { background: rgba(255,255,255,0.04); }
|
||||
.gc-commit-file-row.active { background: rgba(255,255,255,0.06); }
|
||||
|
||||
.gc-file-status { width: 20px; text-align: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
|
||||
.gc-st-modified { color: #f0a020; }
|
||||
.gc-st-added { color: #4caf50; }
|
||||
.gc-st-deleted { color: #e05050; }
|
||||
.gc-st-untracked { color: #999; }
|
||||
.gc-file-path {
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
font-family: var(--df-font-mono, Consolas, monospace); font-size: 12px;
|
||||
}
|
||||
.gc-file-name {
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
font-family: var(--df-font-mono, Consolas, monospace); font-size: 12px;
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.gc-load-more {
|
||||
text-align: center; padding: 12px; cursor: pointer;
|
||||
color: var(--df-accent); font-size: 12px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.gc-load-more:hover { background: rgba(255,255,255,0.04); }
|
||||
|
||||
/* 通用 */
|
||||
.gc-loading, .gc-empty {
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
padding: 40px 0; color: var(--df-text-dim); font-size: 13px;
|
||||
}
|
||||
.gc-diff-loading { display: flex; justify-content: center; padding: 20px; }
|
||||
.gc-diff-empty { padding: 20px 14px; color: var(--df-text-dim); font-size: 12px; text-align: center; }
|
||||
.spinner {
|
||||
width: 16px; height: 16px;
|
||||
border: 2px solid var(--df-border); border-top-color: var(--df-accent);
|
||||
border-radius: 50%; animation: df-spin 0.8s linear infinite; display: inline-block;
|
||||
}
|
||||
@keyframes df-spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user