+
+
{{ mirror.name }}
+
+
+ 健康
-
- Unhealthy
+
+
+ 异常
-
- Disabled
+
+
+ 已停用
-
-
Pattern: {{ mirror.pattern }}
-
Upstream: {{ mirror.upstream }}
-
{{ mirror.error }}
+
+
+ 路径
+ {{ mirror.pattern }}
+
+
+ 上游
+ {{ mirror.upstream }}
+
+
{{ mirror.error }}
-
-
\ No newline at end of file
diff --git a/web/src/components/StatusDot.vue b/web/src/components/StatusDot.vue
new file mode 100644
index 0000000..3e2cb06
--- /dev/null
+++ b/web/src/components/StatusDot.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/style.css b/web/src/style.css
index 4145bae..42d7964 100644
--- a/web/src/style.css
+++ b/web/src/style.css
@@ -5,6 +5,14 @@
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
background: #05070a;
color: #d6deeb;
+
+ --brand-1: #7c3aed;
+ --brand-2: #9455f5;
+ --brand-3: #bd8bff;
+ --brand-light: #f2e4ff;
+ --accent: #67e8f9;
+ --glass-border: rgba(148, 163, 184, 0.16);
+ --glass-highlight: rgba(255, 255, 255, 0.07);
}
* {
@@ -16,10 +24,13 @@ body {
min-width: 320px;
min-height: 100vh;
background:
+ radial-gradient(900px 420px at 15% -10%, color-mix(in srgb, var(--brand-1) 9%, transparent), transparent 65%),
+ radial-gradient(900px 420px at 85% -10%, rgba(34, 211, 238, 0.07), transparent 65%),
linear-gradient(rgba(148, 163, 184, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(148, 163, 184, 0.025) 1px, transparent 1px),
#05070a;
- background-size: 24px 24px;
+ background-size: auto, auto, 24px 24px, 24px 24px, auto;
+ background-attachment: fixed;
color: #d6deeb;
}
@@ -43,17 +54,58 @@ button:disabled {
}
::selection {
- background: rgba(34, 211, 238, 0.28);
+ background: color-mix(in srgb, var(--brand-1) 40%, transparent);
color: #f8fafc;
}
+/* ---------- glass surfaces ---------- */
+
+@utility glass {
+ position: relative;
+ border: 1px solid var(--glass-border);
+ border-radius: 0.625rem;
+ background:
+ radial-gradient(130% 70% at 50% 0%, color-mix(in srgb, var(--brand-1) 7%, transparent), transparent 60%),
+ linear-gradient(180deg, rgba(148, 163, 184, 0.08), rgba(2, 6, 23, 0.42) 48%);
+ box-shadow:
+ inset 0 1px 0 var(--glass-highlight),
+ inset 0 0 28px rgba(103, 232, 249, 0.02),
+ 0 10px 32px rgba(0, 0, 0, 0.38);
+ backdrop-filter: blur(10px);
+}
+
+@utility glass-hover {
+ transition: border-color 200ms ease, box-shadow 200ms ease, background-color 200ms ease, transform 200ms ease;
+
+ &:hover {
+ border-color: rgba(103, 232, 249, 0.35);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.1),
+ inset 0 0 28px rgba(103, 232, 249, 0.03),
+ 0 10px 36px rgba(0, 0, 0, 0.45),
+ 0 0 26px color-mix(in srgb, var(--brand-1) 14%, transparent);
+ }
+}
+
+@utility glass-inset {
+ position: relative;
+ border: 1px solid rgba(148, 163, 184, 0.1);
+ border-radius: 0.5rem;
+ background: linear-gradient(180deg, rgba(2, 6, 23, 0.55), rgba(2, 6, 23, 0.35));
+ box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.4), inset 0 0 18px rgba(0, 0, 0, 0.2);
+}
+
@layer components {
.page-header {
- @apply mb-5 flex flex-col gap-1 border-b border-slate-800 pb-4;
+ @apply mb-5 flex flex-col gap-1 border-b border-slate-800/80 pb-4;
}
.page-kicker {
- @apply text-xs uppercase tracking-[0.28em] text-cyan-400;
+ @apply text-xs font-medium uppercase tracking-[0.28em];
+ background: linear-gradient(90deg, var(--brand-3), var(--accent));
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
}
.page-title {
@@ -64,36 +116,73 @@ button:disabled {
@apply text-sm text-slate-500;
}
- .panel {
- @apply border border-slate-800 bg-slate-950/80;
- }
- .panel-pad {
- @apply border border-slate-800 bg-slate-950/80 p-4;
- }
.btn {
- @apply inline-flex items-center justify-center border border-slate-700 bg-slate-900 px-3 py-1.5 text-xs font-medium text-slate-300 transition-colors duration-150 hover:border-cyan-500/60 hover:text-cyan-300 disabled:opacity-50;
+ @apply inline-flex items-center justify-center gap-1.5 rounded-md border border-slate-600/60 bg-slate-800/60 px-3 py-1.5 text-xs font-medium text-slate-300 transition-all duration-150 hover:border-cyan-400/60 hover:bg-slate-800/90 hover:text-cyan-200 disabled:opacity-50 disabled:hover:border-slate-600/60 disabled:hover:text-slate-300;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
+ }
+
+ .btn:not(:disabled):active {
+ transform: translateY(0.5px);
}
.btn-primary {
- @apply border-cyan-500/50 bg-cyan-950/40 text-cyan-200 hover:border-cyan-300 hover:text-cyan-100;
+ @apply border-cyan-400/50 text-cyan-100 hover:border-cyan-300/80 hover:bg-slate-900/80 hover:text-cyan-100;
+ background: linear-gradient(135deg, rgba(34, 211, 238, 0.18), rgba(56, 189, 248, 0.08));
+ box-shadow: inset 0 1px 0 rgba(103, 232, 249, 0.14), 0 0 18px rgba(34, 211, 238, 0.07);
+ }
+
+ .btn-primary:not(:disabled):hover {
+ box-shadow: inset 0 1px 0 rgba(103, 232, 249, 0.2), 0 0 22px rgba(34, 211, 238, 0.16);
}
.btn-danger {
- @apply border-red-500/40 bg-red-950/30 text-red-300 hover:border-red-400 hover:text-red-200;
+ @apply border-red-500/50 text-red-300 hover:border-red-400/80 hover:text-red-200 hover:bg-slate-900/80;
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.14), rgba(239, 68, 68, 0.04));
+ box-shadow: inset 0 1px 0 rgba(248, 113, 113, 0.08);
+ }
+
+ .btn-danger:not(:disabled):hover {
+ box-shadow: inset 0 1px 0 rgba(248, 113, 113, 0.12), 0 0 18px rgba(239, 68, 68, 0.12);
+ }
+
+ .btn-brand {
+ @apply border-violet-400/50 text-violet-200 hover:border-violet-300/80 hover:bg-slate-900/80 hover:text-violet-100;
+ background: linear-gradient(135deg, color-mix(in srgb, var(--brand-1) 22%, transparent), color-mix(in srgb, var(--brand-2) 8%, transparent));
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--brand-3) 16%, transparent), 0 0 18px color-mix(in srgb, var(--brand-1) 10%, transparent);
+ }
+
+ .btn-brand:not(:disabled):hover {
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--brand-3) 22%, transparent), 0 0 22px color-mix(in srgb, var(--brand-1) 20%, transparent);
}
.input {
- @apply border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-200 outline-none transition-colors duration-150 placeholder:text-slate-600 focus:border-cyan-500/70;
+ @apply rounded-md border border-slate-700 bg-slate-950/70 px-3 py-2 text-sm text-slate-200 outline-none transition-all duration-150 placeholder:text-slate-600 focus:border-cyan-400/60;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
+ }
+
+ .input:focus {
+ box-shadow:
+ 0 0 0 1px rgba(103, 232, 249, 0.22),
+ 0 0 18px rgba(34, 211, 238, 0.08),
+ inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.select {
- @apply border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-200 outline-none transition-colors duration-150 focus:border-cyan-500/70;
+ @apply rounded-md border border-slate-700 bg-slate-950/70 px-3 py-2 text-sm text-slate-200 outline-none transition-all duration-150 focus:border-cyan-400/60;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
+ }
+
+ .select:focus {
+ box-shadow: 0 0 0 1px rgba(103, 232, 249, 0.22), 0 0 18px rgba(34, 211, 238, 0.08);
}
.table-wrap {
- @apply overflow-x-auto border border-slate-800 bg-slate-950/80;
+ @apply overflow-x-auto rounded-lg border border-slate-800/90;
+ background: linear-gradient(180deg, rgba(148, 163, 184, 0.05), rgba(2, 6, 23, 0.38));
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 10px 32px rgba(0, 0, 0, 0.35);
+ backdrop-filter: blur(8px);
}
.data-table {
@@ -101,39 +190,52 @@ button:disabled {
}
.data-table thead {
- @apply border-b border-slate-800 bg-slate-900/80 text-xs uppercase tracking-[0.16em] text-slate-500;
+ @apply border-b border-slate-800 bg-slate-900/60 text-xs uppercase tracking-[0.16em] text-slate-500;
}
.data-table th {
- @apply px-3 py-2 font-medium;
+ @apply px-3 py-2.5 font-medium;
}
.data-table td {
- @apply border-b border-slate-900 px-3 py-2 align-middle;
+ @apply border-b border-slate-800/60 px-3 py-2.5 align-middle;
}
.data-table tbody tr {
- @apply transition-colors duration-150 hover:bg-slate-900/70;
+ @apply transition-colors duration-150 hover:bg-cyan-400/[0.04];
+ }
+
+ .data-table tbody tr:last-child td {
+ @apply border-b-0;
}
.tag {
- @apply inline-flex items-center border border-slate-700 px-2 py-0.5 text-xs font-medium text-slate-400;
+ @apply inline-flex items-center gap-1 rounded-md border border-slate-600/50 bg-slate-800/60 px-2 py-0.5 text-[11px] font-medium tracking-wide text-slate-400;
}
.tag-ok {
- @apply border-emerald-500/40 bg-emerald-950/30 text-emerald-300;
+ @apply border-emerald-500/40 text-emerald-300;
+ background: rgba(16, 185, 129, 0.08);
}
.tag-warn {
- @apply border-amber-500/40 bg-amber-950/30 text-amber-300;
+ @apply border-amber-500/40 text-amber-300;
+ background: rgba(245, 158, 11, 0.08);
}
.tag-off {
- @apply border-slate-700 bg-slate-900 text-slate-500;
+ @apply border-slate-600/40 text-slate-500;
+ background: rgba(51, 65, 85, 0.3);
+ }
+
+ .tag-brand {
+ @apply border-violet-500/40 text-violet-300;
+ background: color-mix(in srgb, var(--brand-1) 10%, transparent);
}
.code-line {
- @apply border border-slate-800 bg-black/40 px-3 py-2 font-mono text-xs text-emerald-300;
+ @apply rounded-md border border-slate-800 bg-black/50 px-3 py-2 font-mono text-xs text-emerald-300;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
.muted-link {
diff --git a/web/src/views/Dashboard.vue b/web/src/views/Dashboard.vue
index 2167725..d0b8f16 100644
--- a/web/src/views/Dashboard.vue
+++ b/web/src/views/Dashboard.vue
@@ -2,6 +2,11 @@
import { ref, onMounted, computed } from 'vue'
import { getStatus, getTraffic, getPublicConfig, getRecentLogs } from '../api/client'
import StatusCard from '../components/StatusCard.vue'
+import Panel from '../components/Panel.vue'
+import CodeBlock from '../components/CodeBlock.vue'
+import EmptyState from '../components/EmptyState.vue'
+import Banner from '../components/Banner.vue'
+import StatusDot from '../components/StatusDot.vue'
const mirrors = ref
([])
const loading = ref(true)
@@ -10,7 +15,6 @@ const traffic = ref([])
const hourlyTraffic = ref([])
const logs = ref([])
const publicUrl = ref('')
-const copiedGuide = ref(null)
const chartMode = ref<'requests' | 'bandwidth'>('requests')
const chartGranularity = ref<'hourly' | 'daily' | 'weekly'>('hourly')
const usageBaseUrl = computed(() => (publicUrl.value || window.location.origin).replace(/\/$/, ''))
@@ -51,22 +55,22 @@ const mirrorUsage = computed(() => [
const gitUsage = computed(() => [
{
- title: 'GitHub clone',
+ title: 'GitHub 克隆',
desc: '把 github.com/owner/repo 替换为 /gh/owner/repo。',
cmd: `git clone ${usageBaseUrl.value}/gh/user/repo`,
},
{
- title: 'GitLab clone',
+ title: 'GitLab 克隆',
desc: '把 gitlab.com/group/repo 替换为 /gl/group/repo。',
cmd: `git clone ${usageBaseUrl.value}/gl/group/repo`,
},
{
- title: 'Archive',
+ title: '压缩包下载',
desc: '下载仓库压缩包。',
cmd: `curl ${usageBaseUrl.value}/gh/user/repo/archive/main.zip -o main.zip`,
},
{
- title: 'Raw file',
+ title: '原始文件',
desc: '读取仓库原始文件内容。',
cmd: `curl ${usageBaseUrl.value}/gh/user/repo/raw/branch/file.txt`,
},
@@ -82,19 +86,19 @@ const stats = computed(() => {
const hours = computed(() => [...new Set(hourlyTraffic.value.map((item: any) => item.hour))].sort().slice(-24))
-const trendRows = computed(() => {
- const metric = (item: any) => chartMode.value === 'bandwidth'
- ? Number(item.bytes_out || item.bytesOut || 0)
- : Number(item.requests || 0)
+const metricOf = (item: any) => chartMode.value === 'bandwidth'
+ ? Number(item.bytes_out || item.bytesOut || 0)
+ : Number(item.requests || 0)
+const trendRows = computed(() => {
if (!hourlyTraffic.value.length) {
return traffic.value
.map(item => ({
mirror: item.mirror || 'unknown',
- total: metric(item),
+ total: metricOf(item),
requests: Number(item.requests || 0),
bytesOut: Number(item.bytes_out || item.bytesOut || 0),
- values: [metric(item)],
+ values: [metricOf(item)],
}))
.sort((a, b) => b.total - a.total)
.slice(0, 8)
@@ -104,7 +108,7 @@ const trendRows = computed(() => {
for (const item of hourlyTraffic.value) {
const mirror = item.mirror || 'unknown'
const group = groups.get(mirror) || { mirror, byHour: new Map(), requests: 0, bytesOut: 0 }
- group.byHour.set(item.hour, (group.byHour.get(item.hour) || 0) + metric(item))
+ group.byHour.set(item.hour, (group.byHour.get(item.hour) || 0) + metricOf(item))
group.requests += Number(item.requests || 0)
group.bytesOut += Number(item.bytes_out || item.bytesOut || 0)
groups.set(mirror, group)
@@ -125,6 +129,47 @@ const trendRows = computed(() => {
.slice(0, 8)
})
+const totalTrend = computed(() => {
+ if (!hours.value.length) return []
+ const sums = new Map(hours.value.map(h => [h, 0]))
+ for (const item of hourlyTraffic.value) {
+ const h = item.hour
+ if (sums.has(h)) sums.set(h, (sums.get(h) ?? 0) + metricOf(item))
+ }
+ return hours.value.map(h => sums.get(h) || 0)
+})
+
+const activeMirrors = computed(() => {
+ if (!hourlyTraffic.value.length) {
+ return traffic.value.filter(item => metricOf(item) > 0).length
+ }
+ const names = new Set(hourlyTraffic.value.filter(item => metricOf(item) > 0).map(item => item.mirror || 'unknown'))
+ return names.size
+})
+
+const trendStats = computed(() => {
+ const values = totalTrend.value
+ if (!values.length) return { total: 0, peak: 0, avg: 0, active: 0 }
+ const total = values.reduce((s, v) => s + v, 0)
+ const peak = Math.max(...values)
+ return {
+ total,
+ peak,
+ avg: total / values.length,
+ active: activeMirrors.value,
+ }
+})
+
+const peakPos = computed(() => {
+ const values = totalTrend.value
+ if (!values.length) return null
+ const idx = peakIndex(values)
+ const x = values.length === 1 ? 100 : (idx / (values.length - 1)) * 100
+ const max = Math.max(...values, 1)
+ const y = 30 - (values[idx] / max) * 26
+ return { x, y }
+})
+
onMounted(async () => {
const [statusRes, trafficRes, hourlyRes, logsRes, configRes] = await Promise.allSettled([
getStatus(),
@@ -135,7 +180,7 @@ onMounted(async () => {
])
if (statusRes.status === 'fulfilled') mirrors.value = Array.isArray(statusRes.value) ? statusRes.value : []
- else errorMsg.value = 'Failed to load mirror status'
+ else errorMsg.value = '加载镜像状态失败'
if (trafficRes.status === 'fulfilled') traffic.value = Array.isArray(trafficRes.value) ? trafficRes.value : []
if (hourlyRes.status === 'fulfilled') hourlyTraffic.value = Array.isArray(hourlyRes.value) ? hourlyRes.value : []
if (logsRes.status === 'fulfilled') logs.value = Array.isArray(logsRes.value) ? logsRes.value : []
@@ -147,22 +192,26 @@ async function switchChartMode() {
chartMode.value = chartMode.value === 'requests' ? 'bandwidth' : 'requests'
}
+let granularityRequestId = 0
+
async function switchGranularity(level: 'hourly' | 'daily' | 'weekly') {
- chartGranularity.value = level
- const data = await getTraffic(undefined, undefined, level)
- hourlyTraffic.value = Array.isArray(data) ? data : []
+ const requestId = ++granularityRequestId
+ try {
+ const data = await getTraffic(undefined, undefined, level)
+ if (requestId !== granularityRequestId) return
+ hourlyTraffic.value = Array.isArray(data) ? data : []
+ chartGranularity.value = level
+ } catch (e: any) {
+ if (requestId === granularityRequestId) {
+ errorMsg.value = e.response?.statusText || '流量数据加载失败'
+ }
+ }
}
async function refreshLogs() {
logs.value = await getRecentLogs(50)
}
-function copyGuide(id: string, cmd: string) {
- navigator.clipboard.writeText(cmd)
- copiedGuide.value = id
- setTimeout(() => copiedGuide.value = null, 1500)
-}
-
function formatBytes(b: number) {
const value = Number(b || 0)
if (value < 1024) return value + ' B'
@@ -187,129 +236,216 @@ function sparklinePoints(values: number[]) {
.join(' ')
}
+function sparkAreaPoints(values: number[]) {
+ const points = sparklinePoints(values)
+ return `${points} 100,32 0,32`
+}
+
+function bigChartPoints(values: number[]) {
+ const source = values.length > 1 ? values : [0, values[0] || 0]
+ const max = Math.max(...source, 1)
+ return source
+ .map((value, index) => {
+ const x = (index / (source.length - 1)) * 100
+ const y = 30 - (value / max) * 26
+ return `${x.toFixed(2)},${y.toFixed(2)}`
+ })
+ .join(' ')
+}
+
+function bigAreaPoints(values: number[]) {
+ return `${bigChartPoints(values)} 100,32 0,32`
+}
+
+function peakIndex(values: number[]) {
+ if (!values.length) return -1
+ let idx = 0
+ values.forEach((v, i) => { if (v > values[idx]) idx = i })
+ return idx
+}
+
+
-
- {{ errorMsg }}
-
+
-
-
mirrors
-
{{ stats.total }}
+
+
+
+
镜像源
+
{{ stats.total }}
+
+
+
-
-
healthy
-
{{ stats.healthy }}
+
+
+
+
健康
+
{{ stats.healthy }}
+
+
+
-
-
enabled
-
{{ stats.enabled }}
+
+
+
+
已启用
+
{{ stats.enabled }}
+
+
+
-
-
requests
-
{{ stats.requests.toLocaleString() }}
+
+
+
+
请求数
+
{{ stats.requests.toLocaleString() }}
+
+
+
-
-
-
-
-
mirror acceleration
-
复制命令后把示例包名或镜像名替换成你的目标。
-
-
packages
-
-
-
-
-
-
{{ item.title }}
-
{{ item.desc }}
-
-
+
+
+ 镜像加速
+ 复制命令后把示例包名或镜像名替换成你的目标。
+ 软件包
+
+
+
+
{{ item.title }}
+
{{ item.desc }}
- {{ item.cmd }}
+
-
+
-
-
-
-
git acceleration
-
GitHub 走 /gh/,GitLab 走 /gl/,支持 clone、archive 和 raw。
-
-
git
-
-
-
-
-
-
{{ item.title }}
-
{{ item.desc }}
-
-
+
+ Git 加速
+ GitHub 走 /gh/,GitLab 走 /gl/,支持克隆、压缩包和原始文件。
+ git
+
+
+
+
{{ item.title }}
+
{{ item.desc }}
- {{ item.cmd }}
+
-
+
-
mirror status
- loading...
+ 镜像状态
+
+ 加载中...
+
-
+
-
暂无镜像状态。
+
+
+
-
-
+
+
-
traffic trend
+
流量趋势
+
+
+
+
+
总{{ chartMode === 'requests' ? '请求' : '流量' }}
+
{{ formatMetric(trendStats.total) }}
+
+
+
峰值
+
{{ formatMetric(trendStats.peak) }}
+
+
+
均值
+
{{ formatMetric(trendStats.avg) }}
+
+
+
活跃镜像
+
{{ trendStats.active }}
+
+
+
+
+
+
- | mirror |
- {{ chartMode === 'requests' ? 'requests' : 'bytes out' }} |
- sparkline |
- total requests |
- total out |
+ 镜像 |
+ {{ chartMode === 'requests' ? '请求数' : '输出流量' }} |
+ 趋势图 |
+ 总请求数 |
+ 总输出 |
@@ -317,8 +453,9 @@ function sparklinePoints(values: number[]) {
{{ row.mirror }} |
{{ formatMetric(row.total) }} |
- |
{{ row.requests.toLocaleString() }} |
@@ -326,48 +463,48 @@ function sparklinePoints(values: number[]) {
-
暂无流量数据。
+
-
-
health summary
-
-
-
healthy ratio
+
+ 健康概览
+
+
+ 健康占比
{{ stats.total ? Math.round(stats.healthy / stats.total * 100) : 0 }}%
-
-
enabled ratio
+
+ 启用占比
{{ stats.total ? Math.round(stats.enabled / stats.total * 100) : 0 }}%
-
-
hour buckets
+
+ 时间桶数
{{ hours.length || '-' }}
- log rows
+ 日志条数
{{ logs.length }}
-
+
-
access logs
-
+ 访问日志
+
- | time |
- mirror |
- method |
- path |
- status |
- size |
+ 时间 |
+ 镜像 |
+ 方法 |
+ 路径 |
+ 状态 |
+ 大小 |
@@ -381,7 +518,7 @@ function sparklinePoints(values: number[]) {
-
暂无访问记录。
+
diff --git a/web/src/views/GitProxy.vue b/web/src/views/GitProxy.vue
index 7e94c2c..108d6a9 100644
--- a/web/src/views/GitProxy.vue
+++ b/web/src/views/GitProxy.vue
@@ -1,9 +1,9 @@
-
-
-
-
+
+
+
+
{{ item.title }}
{{ item.desc }}
{{ item.path }}
-
{{ item.cmd }}
-
+
diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue
index e86ac8a..0a10258 100644
--- a/web/src/views/Login.vue
+++ b/web/src/views/Login.vue
@@ -1,6 +1,9 @@
-
-
-
-
auth
-
DevBox Console
+
+
+
+
+
+ devbox
+
+
认证
+
DevBox 控制台
输入 AUTH_TOKEN 访问控制台。
-
- {{ error }}
-
+
diff --git a/web/src/views/Mirrors.vue b/web/src/views/Mirrors.vue
index dacaecb..1f3b10e 100644
--- a/web/src/views/Mirrors.vue
+++ b/web/src/views/Mirrors.vue
@@ -1,6 +1,9 @@
-
loading mirrors...
-
-
- {{ errorMsg }}
+
+ 正在加载镜像配置...
+
+
- | mirror |
- state |
- cache ttl |
- upstream |
- action |
- save |
+ 镜像 |
+ 状态 |
+ 缓存 TTL |
+ 上游地址 |
+ 操作 |
+ 保存 |
@@ -93,7 +96,8 @@ function ttlText(value: number | string) {
- {{ m.enabled ? 'enabled' : 'disabled' }}
+
+ {{ m.enabled ? '已启用' : '已停用' }}
|
{{ ttlText(m.cacheTTL) }} |
@@ -102,23 +106,23 @@ function ttlText(value: number | string) {
v-model="m.upstream"
class="input w-full"
:disabled="updating === m.name"
- placeholder="Upstream URL"
+ placeholder="上游地址"
@change="updateUpstream(m)"
- @keydown.enter="updateUpstream(m)"
+ @keydown.enter="($event.target as HTMLInputElement).blur()"
/>
|
- {{ saved === m.name ? 'saved' : updating === m.name ? 'saving' : '-' }}
+ {{ saved === m.name ? '已保存' : updating === m.name ? '保存中' : '-' }}
|
-
暂无镜像配置。
+
diff --git a/web/src/views/Releases.vue b/web/src/views/Releases.vue
index c6241f4..de4bf7f 100644
--- a/web/src/views/Releases.vue
+++ b/web/src/views/Releases.vue
@@ -8,6 +8,9 @@ import {
getReleaseSources,
type ReleaseSource,
} from '../api/client'
+import Panel from '../components/Panel.vue'
+import Banner from '../components/Banner.vue'
+import StatusDot from '../components/StatusDot.vue'
const sources = ref
([])
const loading = ref(true)
@@ -134,61 +137,73 @@ function formatDate(value?: string) {
-
- {{ errorMsg }}
-
-
- {{ statusMsg }}
-
+
+
-
+
-
-
+
+
-
saved sources
-
{{ sources.length }} configured
+
已保存的下载源
+
已配置 {{ sources.length }} 个
- loading release sources...
- 暂无 Release 源。
-
-
+
+ 正在加载下载源...
+
+
+
+
+
+
+
+
暂无下载源,使用上方表单添加一个 GitHub Release 源。
+
+
+
diff --git a/web/src/views/Search.vue b/web/src/views/Search.vue
index 6e2f135..ba0a85e 100644
--- a/web/src/views/Search.vue
+++ b/web/src/views/Search.vue
@@ -1,6 +1,11 @@
-
loading settings...
-
-
- {{ rlLoadError }}
+
+ 正在加载设置...
-
-
-
-
-
rate limit
-
滚动时间窗口限流,白名单绕过,黑名单直接拒绝。
-
+
+
+
+
+ 限流配置
+ 滚动时间窗口限流,白名单绕过,黑名单直接拒绝。
+
-
+
-
+