feat: restore frontend login page

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-06-16 17:36:42 +08:00
co-authored by Codex
parent 4657252dbb
commit dc7f2d0483
36 changed files with 847 additions and 276 deletions
+3
View File
@@ -1 +1,4 @@
/// <reference types="vite/client" />
declare module '@fontsource-variable/inter'
declare module '@fontsource-variable/jetbrains-mono'
+1 -7
View File
@@ -3,13 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=JetBrains+Mono:wght@100..800&display=swap"
rel="stylesheet"
/>
<link rel="icon" type="image/svg+xml" href="/admin/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Codex Proxy RS</title>
</head>
<body>
+3
View File
@@ -10,11 +10,14 @@
"preview": "vite preview"
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@lucide/vue": "^1.18.0",
"axios": "^1.18.0",
"echarts": "^6.1.0",
"gsap": "^3.15.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"vue": "^3.5.38",
"vue-router": "^5.1.0"
},
+44
View File
@@ -8,6 +8,12 @@ importers:
.:
dependencies:
'@fontsource-variable/inter':
specifier: ^5.2.8
version: 5.2.8
'@fontsource-variable/jetbrains-mono':
specifier: ^5.2.8
version: 5.2.8
'@lucide/vue':
specifier: ^1.18.0
version: 1.18.0(vue@3.5.38(typescript@6.0.3))
@@ -23,6 +29,9 @@ importers:
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
pinia-plugin-persistedstate:
specifier: ^4.7.1
version: 4.7.1(pinia@3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))
vue:
specifier: ^3.5.38
version: 3.5.38(typescript@6.0.3)
@@ -104,6 +113,12 @@ packages:
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@fontsource-variable/inter@5.2.8':
resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==}
'@fontsource-variable/jetbrains-mono@5.2.8':
resolution: {integrity: sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -484,6 +499,9 @@ packages:
supports-color:
optional: true
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
@@ -747,6 +765,20 @@ packages:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
pinia-plugin-persistedstate@4.7.1:
resolution: {integrity: sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==}
peerDependencies:
'@nuxt/kit': '>=3.0.0'
'@pinia/nuxt': '>=0.10.0'
pinia: '>=3.0.0'
peerDependenciesMeta:
'@nuxt/kit':
optional: true
'@pinia/nuxt':
optional: true
pinia:
optional: true
pinia@3.0.4:
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
peerDependencies:
@@ -978,6 +1010,10 @@ snapshots:
tslib: 2.8.1
optional: true
'@fontsource-variable/inter@5.2.8': {}
'@fontsource-variable/jetbrains-mono@5.2.8': {}
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -1330,6 +1366,8 @@ snapshots:
dependencies:
ms: 2.1.3
defu@6.1.7: {}
delayed-stream@1.0.0: {}
detect-libc@2.1.2: {}
@@ -1539,6 +1577,12 @@ snapshots:
picomatch@4.0.4: {}
pinia-plugin-persistedstate@4.7.1(pinia@3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))):
dependencies:
defu: 6.1.7
optionalDependencies:
pinia: 3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
pinia@3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)):
dependencies:
'@vue/devtools-api': 7.7.9
+7 -7
View File
@@ -12,16 +12,16 @@ const props = withDefaults(defineProps<{
})
const variantClasses: Record<BadgeVariant, string> = {
default: 'bg-[var(--cp-default-bg)] text-[var(--cp-text-secondary)]',
normal: 'bg-[var(--cp-normal-bg)] text-[var(--cp-normal-text)]',
success: 'bg-[var(--cp-success-bg)] text-[var(--cp-success-text)]',
warning: 'bg-[var(--cp-warning-bg)] text-[var(--cp-warning-text)]',
danger: 'bg-[var(--cp-danger-bg)] text-[var(--cp-danger-text)]',
info: 'bg-[var(--cp-info-bg)] text-[var(--cp-info-text)]',
default: 'bg-(--cp-default-bg) text-(--cp-text-secondary)',
normal: 'bg-(--cp-normal-bg) text-(--cp-normal-text)',
success: 'bg-(--cp-success-bg) text-(--cp-success-text)',
warning: 'bg-(--cp-warning-bg) text-(--cp-warning-text)',
danger: 'bg-(--cp-danger-bg) text-(--cp-danger-text)',
info: 'bg-(--cp-info-bg) text-(--cp-info-text)',
}
const badgeClasses = computed(() => [
'inline-flex min-h-[26px] items-center justify-center gap-[7px] whitespace-nowrap rounded-[var(--cp-tag-radius)] px-2.5 text-xs font-semibold leading-none',
'inline-flex min-h-[26px] items-center justify-center gap-[7px] whitespace-nowrap rounded-(--cp-tag-radius) px-2.5 text-xs font-semibold leading-none',
variantClasses[props.variant],
])
</script>
+9 -9
View File
@@ -22,21 +22,21 @@ const props = withDefaults(defineProps<{
const sizeClasses: Record<ButtonSize, string> = {
large: 'h-10 px-4 text-[13px]',
default: 'h-8 px-3.5 text-[13px]',
small: 'h-6 rounded-[var(--cp-button-radius-small)] px-2.5 text-xs',
small: 'h-6 rounded-(--cp-button-radius-small) px-2.5 text-xs',
}
const variantClasses: Record<ButtonVariant, string> = {
default: 'bg-[var(--cp-default-bg)] text-[var(--cp-default-text)] hover:bg-[var(--cp-default-bg-hover)]',
primary: 'bg-[var(--cp-info)] text-[var(--cp-info-on)] hover:bg-[var(--cp-info-hover)]',
success: 'bg-[var(--cp-success-bg)] text-[var(--cp-success-text)] hover:bg-[var(--cp-success-bg-hover)]',
warning: 'bg-[var(--cp-warning-bg)] text-[var(--cp-warning-text)] hover:bg-[var(--cp-warning-bg-hover)]',
danger: 'bg-[var(--cp-danger-bg)] text-[var(--cp-danger-text)] hover:bg-[var(--cp-danger-bg-hover)]',
plain: 'bg-[var(--cp-info-bg)] text-[var(--cp-info-text)] hover:bg-[var(--cp-info-bg-hover)]',
text: 'h-auto bg-transparent p-0 text-[var(--cp-info-text)] hover:text-[var(--cp-info-hover)]',
default: 'bg-(--cp-default-bg) text-(--cp-default-text) hover:bg-(--cp-default-bg-hover)',
primary: 'bg-(--cp-info) text-(--cp-info-on) hover:bg-(--cp-info-hover)',
success: 'bg-(--cp-success-bg) text-(--cp-success-text) hover:bg-(--cp-success-bg-hover)',
warning: 'bg-(--cp-warning-bg) text-(--cp-warning-text) hover:bg-(--cp-warning-bg-hover)',
danger: 'bg-(--cp-danger-bg) text-(--cp-danger-text) hover:bg-(--cp-danger-bg-hover)',
plain: 'bg-(--cp-info-bg) text-(--cp-info-text) hover:bg-(--cp-info-bg-hover)',
text: 'h-auto bg-transparent p-0 text-(--cp-info-text) hover:text-(--cp-info-hover)',
}
const buttonClasses = computed(() => [
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--cp-button-radius-base)] border-0 font-bold leading-none transition-colors disabled:cursor-not-allowed disabled:bg-[var(--cp-disabled-bg)] disabled:text-[var(--cp-disabled-text)]',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-(--cp-button-radius-base) border-0 font-bold leading-none transition-colors disabled:cursor-not-allowed disabled:bg-(--cp-disabled-bg) disabled:text-(--cp-disabled-text)',
sizeClasses[props.size],
variantClasses[props.variant],
])
+7 -5
View File
@@ -5,27 +5,29 @@ const props = withDefaults(defineProps<{
description?: string
padded?: boolean
radiusClass?: string
shadowClass?: string
}>(), {
as: 'section',
title: undefined,
description: undefined,
padded: true,
radiusClass: 'rounded-[var(--cp-card-radius)]',
radiusClass: 'rounded-(--cp-card-radius)',
shadowClass: 'shadow-(--cp-shadow-card)',
})
</script>
<template>
<component
:is="props.as"
class="overflow-hidden bg-[var(--cp-bg-surface)] shadow-[var(--cp-shadow-card)]"
:class="[props.radiusClass, props.padded ? 'p-6 md:px-7' : undefined]"
class="overflow-hidden bg-(--cp-bg-surface)"
:class="[props.radiusClass, props.shadowClass, props.padded ? 'p-6 md:px-7' : undefined]"
>
<header v-if="props.title || props.description || $slots.actions" class="mb-6 flex items-start justify-between gap-5">
<div class="min-w-0">
<h2 v-if="props.title" class="m-0 text-xl font-[760] leading-[1.15] text-[var(--cp-text-primary)]">
<h2 v-if="props.title" class="m-0 text-xl font-[760] leading-[1.15] text-(--cp-text-primary)">
{{ props.title }}
</h2>
<p v-if="props.description" class="mt-[7px] mb-0 text-[13px] font-semibold leading-[1.15] text-[var(--cp-text-secondary)]">
<p v-if="props.description" class="mt-1.75 mb-0 text-[13px] font-semibold leading-[1.15] text-(--cp-text-secondary)">
{{ props.description }}
</p>
</div>
+4 -4
View File
@@ -6,9 +6,9 @@ defineProps<{
</script>
<template>
<div class="grid justify-items-center gap-2 rounded-[14px] bg-[var(--cp-bg-subtle)] p-5 text-center">
<div class="h-1 w-7 rounded-full bg-[var(--cp-default-border-hover)]" />
<p class="m-0 text-[13px] font-bold text-[var(--cp-text-primary)]">{{ title ?? '暂无数据' }}</p>
<p v-if="description" class="m-0 text-xs font-semibold text-[var(--cp-text-secondary)]">{{ description }}</p>
<div class="grid justify-items-center gap-2 rounded-[14px] bg-(--cp-bg-subtle) p-5 text-center">
<div class="h-1 w-7 rounded-full bg-(--cp-default-border-hover)" />
<p class="m-0 text-[13px] font-bold text-(--cp-text-primary)">{{ title ?? '暂无数据' }}</p>
<p v-if="description" class="m-0 text-xs font-semibold text-(--cp-text-secondary)">{{ description }}</p>
</div>
</template>
+2 -2
View File
@@ -11,10 +11,10 @@ withDefaults(defineProps<{
<template>
<button
class="inline-flex items-center justify-center border-0 bg-[var(--cp-bg-surface)] text-[var(--cp-text-secondary)] shadow-[var(--cp-shadow-control)] transition-colors hover:bg-[var(--cp-default-bg-hover)] hover:text-[var(--cp-normal)]"
class="inline-flex items-center justify-center border-0 bg-(--cp-bg-surface) text-(--cp-text-secondary) shadow-(--cp-shadow-control) transition-colors hover:bg-(--cp-default-bg-hover) hover:text-(--cp-normal)"
:class="[
size === 'large' ? 'h-11 w-11 rounded-xl' : 'h-9 w-9 rounded-xl',
active ? 'bg-[var(--cp-default-bg-hover)] text-[var(--cp-normal)]' : '',
active ? 'bg-(--cp-default-bg-hover) text-(--cp-normal)' : '',
]"
:aria-label="label"
:title="label"
+6 -6
View File
@@ -17,22 +17,22 @@ const model = defineModel<string>({ default: '' })
<template>
<label class="grid gap-[7px]">
<span
class="inline-flex w-full items-center gap-2.5 rounded-[var(--cp-control-radius-base)] border border-transparent bg-[var(--cp-default-bg)] text-[var(--cp-text-primary)] transition focus-within:border-[var(--cp-info-border)] focus-within:bg-[var(--cp-bg-surface)] focus-within:shadow-[var(--cp-shadow-control)]"
class="inline-flex w-full items-center gap-2.5 rounded-(--cp-control-radius-base) border border-transparent bg-(--cp-default-bg) text-(--cp-text-primary) transition focus-within:border-(--cp-info-border) focus-within:bg-(--cp-bg-surface) focus-within:shadow-(--cp-shadow-control)"
:class="[
size === 'large' ? 'h-10 px-4' : size === 'small' ? 'h-6 rounded-[var(--cp-control-radius-small)] px-2' : 'h-8 px-3',
error ? 'border-[var(--cp-danger-border)] bg-[var(--cp-danger-bg)]' : '',
size === 'large' ? 'h-10 px-4' : size === 'small' ? 'h-6 rounded-(--cp-control-radius-small) px-2' : 'h-8 px-3',
error ? 'border-(--cp-danger-border) bg-(--cp-danger-bg)' : '',
]"
>
<span v-if="$slots.prefix" class="inline-flex text-[var(--cp-text-muted)]">
<span v-if="$slots.prefix" class="inline-flex text-(--cp-text-muted)">
<slot name="prefix" />
</span>
<input
v-model="model"
class="min-w-0 flex-1 border-0 bg-transparent text-[13px] font-semibold text-[var(--cp-text-primary)] outline-0 placeholder:text-[var(--cp-text-muted)] disabled:text-[var(--cp-disabled-text)]"
class="min-w-0 flex-1 border-0 bg-transparent text-[13px] font-semibold text-(--cp-text-primary) outline-0 placeholder:text-(--cp-text-muted) disabled:text-(--cp-disabled-text)"
:placeholder="placeholder"
:disabled="disabled"
/>
</span>
<span v-if="error" class="text-xs font-semibold text-[var(--cp-danger-text)]">{{ error }}</span>
<span v-if="error" class="text-xs font-semibold text-(--cp-danger-text)">{{ error }}</span>
</label>
</template>
+4 -4
View File
@@ -15,17 +15,17 @@ const open = defineModel<boolean>({ default: false })
<template>
<Teleport to="body">
<div v-if="open" class="fixed inset-0 z-50 grid place-items-center p-6" role="presentation">
<div class="absolute inset-0 bg-[var(--cp-overlay-scrim)]" @click="open = false" />
<div class="absolute inset-0 bg-(--cp-overlay-scrim)" @click="open = false" />
<section
class="relative w-[min(560px,100%)] rounded-[22px] bg-[var(--cp-bg-surface)] shadow-[var(--cp-shadow-popover)]"
class="relative w-[min(560px,100%)] rounded-[22px] bg-(--cp-bg-surface) shadow-(--cp-shadow-popover)"
role="dialog"
aria-modal="true"
:aria-label="title"
>
<header class="flex justify-between gap-4 p-6 pb-0">
<div>
<h2 class="m-0 text-lg font-[760] text-[var(--cp-text-primary)]">{{ title }}</h2>
<p v-if="description" class="mt-2 mb-0 text-[13px] font-semibold leading-tight text-[var(--cp-text-secondary)]">
<h2 class="m-0 text-lg font-[760] text-(--cp-text-primary)">{{ title }}</h2>
<p v-if="description" class="mt-2 mb-0 text-[13px] font-semibold leading-tight text-(--cp-text-secondary)">
{{ description }}
</p>
</div>
+239
View File
@@ -0,0 +1,239 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, shallowRef, useTemplateRef } from 'vue'
const props = withDefaults(defineProps<{
tag?: string
always?: boolean
minThumbSize?: number
wrapClass?: string
viewClass?: string
}>(), {
tag: 'div',
always: false,
minThumbSize: 32,
wrapClass: '',
viewClass: '',
})
const emit = defineEmits<{
scroll: [payload: { scrollTop: number, scrollLeft: number }]
}>()
const wrapRef = useTemplateRef<HTMLDivElement>('wrap')
const viewRef = useTemplateRef<HTMLElement>('view')
const thumbHeight = shallowRef(0)
const thumbTop = shallowRef(0)
const visible = shallowRef(false)
const dragging = shallowRef(false)
let resizeObserver: ResizeObserver | undefined
let hideTimer: number | undefined
let dragStartY = 0
let dragStartScrollTop = 0
const canScrollY = computed(() => thumbHeight.value > 0)
const scrollbarVisible = computed(() => props.always || dragging.value || visible.value)
const thumbStyle = computed(() => ({
height: `${thumbHeight.value}px`,
transform: `translateY(${thumbTop.value}px)`,
}))
function trackHeight(wrap: HTMLElement) {
return Math.max(wrap.clientHeight - 8, 0)
}
function maxScrollTop(wrap: HTMLElement) {
return Math.max(wrap.scrollHeight - wrap.clientHeight, 0)
}
function maxThumbTop(wrap: HTMLElement) {
return Math.max(trackHeight(wrap) - thumbHeight.value, 0)
}
function showScrollbar() {
visible.value = true
if (hideTimer) {
window.clearTimeout(hideTimer)
}
if (!props.always && !dragging.value) {
hideTimer = window.setTimeout(() => {
visible.value = false
}, 900)
}
}
function update() {
const wrap = wrapRef.value
if (!wrap) {
return
}
const scrollRange = maxScrollTop(wrap)
const availableTrackHeight = trackHeight(wrap)
if (scrollRange <= 0 || availableTrackHeight <= 0) {
thumbHeight.value = 0
thumbTop.value = 0
return
}
const ratio = wrap.clientHeight / wrap.scrollHeight
thumbHeight.value = Math.min(
availableTrackHeight,
Math.max(availableTrackHeight * ratio, props.minThumbSize),
)
thumbTop.value = (wrap.scrollTop / scrollRange) * maxThumbTop(wrap)
}
function handleScroll() {
const wrap = wrapRef.value
if (!wrap) {
return
}
update()
showScrollbar()
emit('scroll', {
scrollTop: wrap.scrollTop,
scrollLeft: wrap.scrollLeft,
})
}
function handleTrackPointerDown(event: PointerEvent) {
if (event.target !== event.currentTarget) {
return
}
const wrap = wrapRef.value
if (!wrap) {
return
}
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
const nextThumbTop = event.clientY - rect.top - thumbHeight.value / 2
const scrollRange = maxScrollTop(wrap)
const thumbRange = maxThumbTop(wrap)
wrap.scrollTop = thumbRange > 0
? (Math.max(0, Math.min(nextThumbTop, thumbRange)) / thumbRange) * scrollRange
: 0
}
function handleThumbPointerDown(event: PointerEvent) {
const wrap = wrapRef.value
if (!wrap) {
return
}
event.preventDefault()
dragging.value = true
visible.value = true
dragStartY = event.clientY
dragStartScrollTop = wrap.scrollTop
document.addEventListener('pointermove', handleThumbPointerMove)
document.addEventListener('pointerup', handleThumbPointerUp, { once: true })
}
function handleThumbPointerMove(event: PointerEvent) {
const wrap = wrapRef.value
if (!wrap) {
return
}
const thumbRange = maxThumbTop(wrap)
if (thumbRange <= 0) {
return
}
const scrollRange = maxScrollTop(wrap)
wrap.scrollTop = dragStartScrollTop + ((event.clientY - dragStartY) / thumbRange) * scrollRange
}
function handleThumbPointerUp() {
dragging.value = false
document.removeEventListener('pointermove', handleThumbPointerMove)
showScrollbar()
}
function scrollTo(options: ScrollToOptions): void
function scrollTo(x: number, y?: number): void
function scrollTo(arg1: ScrollToOptions | number, arg2?: number) {
if (typeof arg1 === 'number') {
wrapRef.value?.scrollTo(arg1, arg2 ?? 0)
return
}
wrapRef.value?.scrollTo(arg1)
}
function setScrollTop(value: number) {
if (wrapRef.value) {
wrapRef.value.scrollTop = value
}
}
onMounted(async () => {
await nextTick()
update()
resizeObserver = new ResizeObserver(update)
if (wrapRef.value) {
resizeObserver.observe(wrapRef.value)
}
if (viewRef.value) {
resizeObserver.observe(viewRef.value)
}
})
onBeforeUnmount(() => {
resizeObserver?.disconnect()
if (hideTimer) {
window.clearTimeout(hideTimer)
}
document.removeEventListener('pointermove', handleThumbPointerMove)
})
defineExpose({
update,
scrollTo,
setScrollTop,
wrapRef,
})
</script>
<template>
<div class="relative h-full min-h-0 overflow-hidden">
<div
ref="wrap"
class="base-scrollbar-wrap h-full min-h-0 overflow-auto"
:class="wrapClass"
@mouseenter="showScrollbar"
@scroll="handleScroll"
>
<component :is="tag" ref="view" :class="viewClass">
<slot />
</component>
</div>
<div
v-show="canScrollY"
class="absolute top-1 right-1 bottom-1 z-10 w-1.5 rounded-full transition-opacity duration-200"
:class="scrollbarVisible ? 'opacity-100' : 'opacity-0'"
@pointerdown="handleTrackPointerDown"
>
<div
class="w-full rounded-full bg-(--cp-scrollbar-thumb) transition-colors duration-200 hover:bg-(--cp-scrollbar-thumb-hover)"
:class="dragging ? 'bg-(--cp-scrollbar-thumb-hover)' : ''"
:style="thumbStyle"
@pointerdown="handleThumbPointerDown"
/>
</div>
</div>
</template>
<style scoped>
.base-scrollbar-wrap {
scrollbar-width: none;
}
.base-scrollbar-wrap::-webkit-scrollbar {
width: 0;
height: 0;
}
</style>
+3 -3
View File
@@ -12,12 +12,12 @@ const model = defineModel<string>({ required: true })
</script>
<template>
<div class="inline-flex min-h-9 gap-1 rounded-xl bg-[var(--cp-bg-muted)] p-1" role="tablist">
<div class="inline-flex min-h-9 gap-1 rounded-xl bg-(--cp-bg-muted) p-1" role="tablist">
<button
v-for="option in options"
:key="option.value"
class="min-w-[74px] rounded-[9px] border-0 bg-transparent px-3 text-xs font-bold leading-none text-[var(--cp-text-secondary)] transition-colors"
:class="model === option.value ? 'bg-[var(--cp-bg-surface)] text-[var(--cp-text-primary)] shadow-[var(--cp-shadow-control)]' : ''"
class="min-w-18.5 rounded-[9px] border-0 bg-transparent px-3 text-xs font-bold leading-none text-(--cp-text-secondary) transition-colors"
:class="model === option.value ? 'bg-(--cp-bg-surface) text-(--cp-text-primary) shadow-(--cp-shadow-control)' : ''"
type="button"
role="tab"
:aria-selected="model === option.value"
+3 -3
View File
@@ -15,16 +15,16 @@ const model = defineModel<string>({ required: true })
</script>
<template>
<label class="relative inline-flex h-8 w-full rounded-[var(--cp-control-radius-base)] bg-[var(--cp-default-bg)]">
<label class="relative inline-flex h-8 w-full rounded-(--cp-control-radius-base) bg-(--cp-default-bg)">
<select
v-model="model"
class="h-full w-full appearance-none rounded-[inherit] border-0 bg-transparent pr-8 pl-3 text-[13px] font-semibold text-[var(--cp-text-primary)] outline-0"
class="h-full w-full appearance-none rounded-[inherit] border-0 bg-transparent pr-8 pl-3 text-[13px] font-semibold text-(--cp-text-primary) outline-0"
:disabled="disabled"
>
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<ChevronDown class="pointer-events-none absolute top-2 right-3 text-[var(--cp-text-secondary)]" :size="16" />
<ChevronDown class="pointer-events-none absolute top-2 right-3 text-(--cp-text-secondary)" :size="16" />
</label>
</template>
+2 -2
View File
@@ -7,8 +7,8 @@ withDefaults(defineProps<{ label?: string }>(), {
</script>
<template>
<span class="inline-flex items-center gap-2.5 text-[13px] font-semibold text-[var(--cp-text-secondary)]" role="status">
<LoaderCircle class="animate-spin text-[var(--cp-info)]" :size="16" />
<span class="inline-flex items-center gap-2.5 text-[13px] font-semibold text-(--cp-text-secondary)" role="status">
<LoaderCircle class="animate-spin text-(--cp-info)" :size="16" />
<span>{{ label }}</span>
</span>
</template>
+6 -6
View File
@@ -18,11 +18,11 @@ withDefaults(defineProps<{
bodyCellClass?: string
striped?: boolean
}>(), {
tableClass: 'min-w-[620px]',
headerRowClass: 'h-10 rounded-xl bg-[var(--cp-bg-subtle)] text-[11px] font-bold text-[var(--cp-text-muted)]',
bodyRowClass: 'h-[52px] rounded-[10px]',
headerCellClass: 'min-w-0 overflow-hidden bg-[var(--cp-bg-subtle)] px-3 text-ellipsis whitespace-nowrap first:rounded-l-xl last:rounded-r-xl',
bodyCellClass: 'min-w-0 overflow-hidden px-3 text-ellipsis whitespace-nowrap text-xs font-semibold text-[var(--cp-text-primary)] first:rounded-l-[10px] last:rounded-r-[10px]',
tableClass: 'min-w-155',
headerRowClass: 'h-10 rounded-xl bg-(--cp-bg-subtle) text-[11px] font-bold text-(--cp-text-muted)',
bodyRowClass: 'h-13 rounded-[10px]',
headerCellClass: 'min-w-0 overflow-hidden bg-(--cp-bg-subtle) px-3 text-ellipsis whitespace-nowrap first:rounded-l-xl last:rounded-r-xl',
bodyCellClass: 'min-w-0 overflow-hidden px-3 text-ellipsis whitespace-nowrap text-xs font-semibold text-(--cp-text-primary) first:rounded-l-[10px] last:rounded-r-[10px]',
striped: true,
})
@@ -52,7 +52,7 @@ function alignClass(column: TableColumn) {
}
function rowSurfaceClass(index: number, striped: boolean) {
return striped && index % 2 === 1 ? 'bg-[var(--cp-bg-subtle)]' : 'bg-[var(--cp-bg-surface)]'
return striped && index % 2 === 1 ? 'bg-(--cp-bg-subtle)' : 'bg-(--cp-bg-surface)'
}
</script>
+9 -9
View File
@@ -27,21 +27,21 @@ const icon = computed(() => {
})
const iconClasses: Record<ToastVariant, string> = {
success: 'bg-[var(--cp-success-bg)] text-[var(--cp-success)]',
warning: 'bg-[var(--cp-warning-bg)] text-[var(--cp-warning)]',
danger: 'bg-[var(--cp-danger-bg)] text-[var(--cp-danger)]',
info: 'bg-[var(--cp-info-bg)] text-[var(--cp-info)]',
success: 'bg-(--cp-success-bg) text-(--cp-success)',
warning: 'bg-(--cp-warning-bg) text-(--cp-warning)',
danger: 'bg-(--cp-danger-bg) text-(--cp-danger)',
info: 'bg-(--cp-info-bg) text-(--cp-info)',
}
</script>
<template>
<div class="inline-flex min-h-16 min-w-80 items-center gap-3 rounded-[18px] bg-[var(--cp-bg-surface)] px-4 shadow-[var(--cp-shadow-popover)]">
<span class="inline-flex h-[38px] w-[38px] items-center justify-center rounded-xl" :class="iconClasses[variant]">
<div class="inline-flex min-h-16 min-w-80 items-center gap-3 rounded-[18px] bg-(--cp-bg-surface) px-4 shadow-(--cp-shadow-popover)">
<span class="inline-flex h-9.5 w-9.5 items-center justify-center rounded-xl" :class="iconClasses[variant]">
<component :is="icon" :size="18" />
</span>
<span class="grid gap-[5px]">
<strong class="text-[13px] font-[760] leading-none text-[var(--cp-text-primary)]">{{ title }}</strong>
<span v-if="message" class="text-xs font-semibold leading-none text-[var(--cp-text-secondary)]">{{ message }}</span>
<span class="grid gap-1.25">
<strong class="text-[13px] font-[760] leading-none text-(--cp-text-primary)">{{ title }}</strong>
<span v-if="message" class="text-xs font-semibold leading-none text-(--cp-text-secondary)">{{ message }}</span>
</span>
</div>
</template>
+81 -16
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { gsap } from 'gsap'
import { nextTick, onMounted, ref, watch } from 'vue'
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue'
import {
Box,
ChartColumn,
@@ -37,6 +37,8 @@ defineEmits<{
}>()
const sidebarEl = ref<HTMLElement | null>(null)
const brandLabelEl = ref<HTMLElement | null>(null)
const brandLabelVisible = shallowRef(!props.collapsed)
function prefersReducedMotion() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
@@ -67,6 +69,51 @@ function animateSidebarLabels(collapsed: boolean) {
})
}
function hideBrandLabel() {
const label = brandLabelEl.value
if (label) {
gsap.killTweensOf(label)
gsap.set(label, {
opacity: 0,
x: -6,
})
}
brandLabelVisible.value = false
}
function animateBrandLabelEnter() {
const label = brandLabelEl.value
if (!label) {
return
}
if (prefersReducedMotion()) {
gsap.set(label, {
opacity: 1,
x: 0,
})
return
}
gsap.fromTo(
label,
{
opacity: 0,
x: -6,
},
{
opacity: 1,
x: 0,
duration: 0.2,
ease: 'power3.out',
overwrite: true,
},
)
}
function animateSidebarWidth(collapsed: boolean) {
if (!sidebarEl.value) {
return
@@ -102,15 +149,33 @@ onMounted(() => {
width: props.collapsed ? 88 : 256,
flexBasis: props.collapsed ? 88 : 256,
})
if (props.collapsed) {
hideBrandLabel()
}
else {
gsap.set(brandLabelEl.value, {
opacity: 1,
x: 0,
})
}
animateSidebarLabels(Boolean(props.collapsed))
})
watch(
() => props.collapsed,
async (collapsed) => {
if (collapsed) {
hideBrandLabel()
}
else {
brandLabelVisible.value = true
}
animateSidebarLabels(Boolean(collapsed))
animateSidebarWidth(Boolean(collapsed))
await nextTick()
if (!collapsed) {
animateBrandLabelEnter()
}
animateSidebarLabels(Boolean(collapsed))
},
)
@@ -119,36 +184,36 @@ watch(
<template>
<aside
ref="sidebarEl"
class="z-20 hidden h-screen shrink-0 flex-col overflow-hidden bg-white shadow-[var(--cp-shadow-sidebar)] min-[961px]:flex"
:class="collapsed ? 'w-[88px] basis-[88px] items-center' : 'w-[256px] basis-[256px]'"
class="z-20 hidden h-screen shrink-0 flex-col overflow-hidden bg-white shadow-(--cp-shadow-sidebar) min-[961px]:flex"
:class="collapsed ? 'w-22 basis-22 items-center' : 'w-64 basis-64'"
>
<div
class="mt-[31px] grid h-[46px] grid-cols-[46px_minmax(0,1fr)] gap-2.5"
:class="collapsed ? 'w-[46px]' : 'ml-6 w-[188px] self-start'"
class="mt-7.75 grid h-11.5 grid-cols-[46px_minmax(0,1fr)] gap-2.5"
:class="collapsed ? 'w-11.5' : 'ml-6 w-47 self-start'"
>
<span class="inline-flex size-[46px] items-center justify-center rounded-[13px] bg-[#111827] text-white shadow-[0_8px_18px_-16px_#0E172614]">
<span class="inline-flex size-11.5 items-center justify-center rounded-[13px] bg-gray-900 text-white shadow-[0_8px_18px_-16px_#0E172614]">
<SquareTerminal :size="24" />
</span>
<span class="sidebar-label grid min-w-[132px] content-center overflow-hidden transition-[opacity,transform] duration-200" :class="collapsed ? 'pointer-events-none w-0' : 'w-auto'">
<strong class="text-[17px] leading-[1.1] font-[720] text-[#111827]">Codex</strong>
<span class="mt-1 text-[12px] leading-[1.1] font-semibold text-[#64748B]">Proxy RS · v0.1.0</span>
<span v-show="brandLabelVisible" ref="brandLabelEl" class="grid min-w-33 content-center overflow-hidden">
<strong class="text-[17px] leading-[1.1] font-[720] text-gray-900">Codex</strong>
<span class="mt-1 text-xs leading-[1.1] font-semibold text-slate-500">Proxy RS · v0.1.0</span>
</span>
</div>
<nav
class="mt-[35px] grid gap-3"
class="mt-8.75 grid gap-3"
:class="collapsed ? '' : 'ml-6 self-start'"
aria-label="主导航"
>
<a
v-for="item in navItems"
:key="item.label"
class="inline-flex h-[46px] items-center rounded-xl text-[14px] leading-[1.15] no-underline transition-[background-color,color,transform] duration-200 hover:-translate-y-px active:translate-y-0"
class="inline-flex h-11.5 items-center rounded-xl text-sm leading-[1.15] no-underline transition-colors duration-200"
:class="[
collapsed ? 'w-[46px] justify-center' : 'w-[208px] gap-3 pl-[22px]',
collapsed ? 'w-11.5 justify-center' : 'w-52 gap-3 pl-5.5',
item.active
? 'bg-[#E9EEF5] font-bold text-[#111827]'
: 'bg-transparent font-semibold text-[#64748B] hover:bg-[#F8FAFC]',
? 'bg-[#E9EEF5] font-bold text-gray-900'
: 'bg-transparent font-semibold text-slate-500 hover:bg-slate-50',
]"
href="#"
>
@@ -158,8 +223,8 @@ watch(
</nav>
<button
class="mt-auto mb-8 inline-flex items-center justify-center rounded-xl border-0 text-[#64748B] transition-[background-color,transform,color] duration-200 hover:-translate-y-px active:translate-y-0"
:class="collapsed ? 'h-9 w-11 bg-transparent' : 'mr-6 size-9 self-end bg-[#F8FAFC]'"
class="mt-auto mb-8 inline-flex items-center justify-center rounded-xl border-0 text-slate-500 transition-colors duration-200"
:class="collapsed ? 'h-9 w-11 bg-transparent' : 'mr-6 size-9 self-end bg-slate-50'"
type="button"
data-sidebar-toggle
:aria-label="collapsed ? '展开侧边栏' : '收缩侧边栏'"
+37 -37
View File
@@ -65,16 +65,16 @@ function animateAccountMenuLeave(el: Element, done: () => void) {
<template>
<div class="relative z-30 flex h-11 items-start gap-3.5">
<button
class="group inline-flex h-11 w-[170px] items-center gap-2.5 whitespace-nowrap rounded-xl border-0 bg-white px-3.5 text-[14px] leading-[1.15] font-[650] text-[var(--cp-text-primary)] shadow-[var(--cp-shadow-control)] transition-[box-shadow,transform] duration-200 hover:-translate-y-px active:translate-y-0"
class="group inline-flex h-11 w-42.5 items-center gap-2.5 whitespace-nowrap rounded-xl border-0 bg-white px-3.5 text-sm leading-[1.15] font-[650] text-(--cp-text-primary) shadow-(--cp-shadow-control) transition-shadow duration-200"
type="button"
>
<CalendarDays class="text-[var(--cp-info)]" :size="18" />
<CalendarDays class="text-(--cp-info)" :size="18" />
<span class="whitespace-nowrap">最近 24 小时</span>
<ChevronDown class="text-[var(--cp-text-secondary)] transition-transform duration-200 group-hover:rotate-180" :size="16" />
<ChevronDown class="text-(--cp-text-secondary) transition-transform duration-200 group-hover:rotate-180" :size="16" />
</button>
<button
class="inline-flex size-11 items-center justify-center rounded-xl border-0 bg-white text-[var(--cp-normal)] shadow-[var(--cp-shadow-control)] transition-[box-shadow,transform] duration-200 hover:-translate-y-px active:translate-y-0"
class="inline-flex size-11 items-center justify-center rounded-xl border-0 bg-white text-(--cp-normal) shadow-(--cp-shadow-control) transition-shadow duration-200"
type="button"
aria-label="刷新"
>
@@ -83,18 +83,18 @@ function animateAccountMenuLeave(el: Element, done: () => void) {
<div class="relative ml-0.5">
<button
class="group grid h-11 w-[216px] grid-cols-[28px_minmax(0,1fr)_16px] items-center rounded-xl border-0 bg-white px-3 text-[var(--cp-text-secondary)] shadow-[var(--cp-shadow-control)] transition-[box-shadow,transform] duration-200 hover:-translate-y-px active:translate-y-0"
class="group grid h-11 w-54 grid-cols-[28px_minmax(0,1fr)_16px] items-center rounded-xl border-0 bg-white px-3 text-(--cp-text-secondary) shadow-(--cp-shadow-control) transition-shadow duration-200"
type="button"
data-account-trigger
@click="accountMenuOpen = !accountMenuOpen"
>
<span class="inline-flex size-7 items-center justify-center rounded-full bg-[var(--cp-normal)] text-[12px] leading-[1.15] font-bold text-white">A</span>
<span class="grid gap-[5px] pl-3 text-left">
<strong class="text-[13px] leading-[1.15] font-bold text-[var(--cp-text-primary)]">admin</strong>
<span class="text-[11px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">管理员</span>
<span class="inline-flex size-7 items-center justify-center rounded-full bg-(--cp-normal) text-xs leading-[1.15] font-bold text-white">A</span>
<span class="grid gap-1.25 pl-3 text-left">
<strong class="text-[13px] leading-[1.15] font-bold text-(--cp-text-primary)">admin</strong>
<span class="text-[11px] leading-[1.15] font-[650] text-(--cp-text-secondary)">管理员</span>
</span>
<ChevronUp v-if="accountMenuOpen" class="text-[var(--cp-text-secondary)] transition-transform duration-200 group-hover:rotate-180" :size="16" />
<ChevronDown v-else class="text-[var(--cp-text-secondary)] transition-transform duration-200 group-hover:rotate-180" :size="16" />
<ChevronUp v-if="accountMenuOpen" class="text-(--cp-text-secondary) transition-transform duration-200 group-hover:rotate-180" :size="16" />
<ChevronDown v-else class="text-(--cp-text-secondary) transition-transform duration-200 group-hover:rotate-180" :size="16" />
</button>
<Transition
@@ -102,64 +102,64 @@ function animateAccountMenuLeave(el: Element, done: () => void) {
@enter="animateAccountMenuEnter"
@leave="animateAccountMenuLeave"
>
<div v-if="accountMenuOpen" data-account-menu class="absolute right-0 top-14 h-[304px] w-80 origin-top-right rounded-[18px] bg-white pt-[18px] shadow-[var(--cp-shadow-popover)]">
<div class="mx-[18px] grid h-11 grid-cols-[44px_minmax(0,1fr)_64px] items-center gap-3">
<span class="inline-flex size-11 items-center justify-center rounded-full bg-[var(--cp-normal)] text-[14px] leading-[1.15] font-bold text-white">A</span>
<span class="grid gap-[5px]">
<strong class="text-[15px] leading-[1.15] font-bold text-[var(--cp-text-primary)]">admin</strong>
<span class="text-[12px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">管理员</span>
<div v-if="accountMenuOpen" data-account-menu class="absolute right-0 top-14 h-76 w-80 origin-top-right rounded-[18px] bg-white pt-4.5 shadow-(--cp-shadow-popover)">
<div class="mx-4.5 grid h-11 grid-cols-[44px_minmax(0,1fr)_64px] items-center gap-3">
<span class="inline-flex size-11 items-center justify-center rounded-full bg-(--cp-normal) text-sm leading-[1.15] font-bold text-white">A</span>
<span class="grid gap-1.25">
<strong class="text-[15px] leading-[1.15] font-bold text-(--cp-text-primary)">admin</strong>
<span class="text-xs leading-[1.15] font-[650] text-(--cp-text-secondary)">管理员</span>
</span>
<span class="inline-flex h-7 w-16 items-center gap-1.5 rounded-full bg-[var(--cp-success-bg)] px-3">
<i class="size-1.5 rounded-full bg-[var(--cp-success)]" />
<span class="text-[12px] leading-[1.15] font-[650] text-[var(--cp-success-text)]">在线</span>
<span class="inline-flex h-7 w-16 items-center gap-1.5 rounded-full bg-(--cp-success-bg) px-3">
<i class="size-1.5 rounded-full bg-(--cp-success)" />
<span class="text-xs leading-[1.15] font-[650] text-(--cp-success-text)">在线</span>
</span>
</div>
<div class="mx-4 mt-3.5 grid h-11 w-[288px] grid-cols-2 rounded-xl bg-[var(--cp-bg-subtle)] px-4 py-2">
<span class="grid gap-[5px]">
<small class="text-[11px] leading-[1.15] font-semibold text-[var(--cp-text-secondary)]">MFA</small>
<strong class="text-[13px] leading-[1.15] font-bold text-[var(--cp-text-primary)]">已开启</strong>
<div class="mx-4 mt-3.5 grid h-11 w-72 grid-cols-2 rounded-xl bg-(--cp-bg-subtle) px-4 py-2">
<span class="grid gap-1.25">
<small class="text-[11px] leading-[1.15] font-semibold text-(--cp-text-secondary)">MFA</small>
<strong class="text-[13px] leading-[1.15] font-bold text-(--cp-text-primary)">已开启</strong>
</span>
<span class="grid gap-[5px] pl-[18px]">
<small class="text-[11px] leading-[1.15] font-semibold text-[var(--cp-text-secondary)]">会话</small>
<strong class="text-[13px] leading-[1.15] font-bold text-[var(--cp-text-primary)]">1 台</strong>
<span class="grid gap-1.25 pl-4.5">
<small class="text-[11px] leading-[1.15] font-semibold text-(--cp-text-secondary)">会话</small>
<strong class="text-[13px] leading-[1.15] font-bold text-(--cp-text-primary)">1 台</strong>
</span>
</div>
<div class="mx-4 mt-3.5 grid w-[288px] gap-1.5">
<div class="mx-4 mt-3.5 grid w-72 gap-1.5">
<button
class="grid h-9 grid-cols-[20px_minmax(0,1fr)_16px] items-center gap-3 rounded-[11px] border-0 bg-[var(--cp-bg-subtle)] px-3 text-left text-[var(--cp-text-primary)] transition-[background-color,transform] duration-200 hover:-translate-y-px active:translate-y-0"
class="grid h-9 grid-cols-[20px_minmax(0,1fr)_16px] items-center gap-3 rounded-[11px] border-0 bg-(--cp-bg-subtle) px-3 text-left text-(--cp-text-primary) transition-colors duration-200"
type="button"
>
<User :size="20" />
<span class="text-[13px] leading-[1.15] font-[650]">个人设置</span>
<ChevronRight class="text-[var(--cp-text-muted)]" :size="16" />
<ChevronRight class="text-(--cp-text-muted)" :size="16" />
</button>
<button
class="grid h-9 grid-cols-[20px_minmax(0,1fr)_34px_16px] items-center gap-3 rounded-[11px] border-0 bg-transparent px-3 text-left text-[var(--cp-text-primary)] transition-[background-color,transform] duration-200 hover:-translate-y-px hover:bg-[var(--cp-bg-subtle)] active:translate-y-0"
class="grid h-9 grid-cols-[20px_minmax(0,1fr)_34px_16px] items-center gap-3 rounded-[11px] border-0 bg-transparent px-3 text-left text-(--cp-text-primary) transition-colors duration-200 hover:bg-(--cp-bg-subtle)"
type="button"
>
<ShieldCheck :size="20" />
<span class="text-[13px] leading-[1.15] font-[650]">安全与会话</span>
<span class="text-[12px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">MFA</span>
<ChevronRight class="text-[var(--cp-text-muted)]" :size="16" />
<span class="text-xs leading-[1.15] font-[650] text-(--cp-text-secondary)">MFA</span>
<ChevronRight class="text-(--cp-text-muted)" :size="16" />
</button>
<button
class="grid h-9 grid-cols-[20px_minmax(0,1fr)_16px] items-center gap-3 rounded-[11px] border-0 bg-transparent px-3 text-left text-[var(--cp-text-primary)] transition-[background-color,transform] duration-200 hover:-translate-y-px hover:bg-[var(--cp-bg-subtle)] active:translate-y-0"
class="grid h-9 grid-cols-[20px_minmax(0,1fr)_16px] items-center gap-3 rounded-[11px] border-0 bg-transparent px-3 text-left text-(--cp-text-primary) transition-colors duration-200 hover:bg-(--cp-bg-subtle)"
type="button"
>
<ScrollText :size="20" />
<span class="text-[13px] leading-[1.15] font-[650]">操作记录</span>
<ChevronRight class="text-[var(--cp-text-muted)]" :size="16" />
<ChevronRight class="text-(--cp-text-muted)" :size="16" />
</button>
<button
class="grid h-9 grid-cols-[20px_minmax(0,1fr)] items-center gap-3 rounded-[11px] border-0 bg-transparent px-3 text-left text-[var(--cp-danger-text)] transition-[background-color,transform] duration-200 hover:-translate-y-px hover:bg-[var(--cp-danger-bg)] active:translate-y-0"
class="grid h-9 grid-cols-[20px_minmax(0,1fr)] items-center gap-3 rounded-[11px] border-0 bg-transparent px-3 text-left text-(--cp-danger-text) transition-colors duration-200 hover:bg-(--cp-danger-bg)"
type="button"
>
<LogOut class="text-[var(--cp-danger)]" :size="20" />
<LogOut class="text-(--cp-danger)" :size="20" />
<span class="text-[13px] leading-[1.15] font-[650]">退出登录</span>
</button>
</div>
+13 -6
View File
@@ -1,17 +1,24 @@
<script setup lang="ts">
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { RouterView } from 'vue-router'
import BaseScrollbar from '@/components/base/BaseScrollbar.vue'
import { useUiStore } from '@/stores/ui'
import AppSidebar from './components/AppSidebar.vue'
const sidebarCollapsed = ref(false)
const uiStore = useUiStore()
const { sidebarCollapsed } = storeToRefs(uiStore)
const { toggleSidebar } = uiStore
</script>
<template>
<div class="flex h-screen overflow-hidden bg-[var(--cp-bg-page)]">
<AppSidebar :collapsed="sidebarCollapsed" @toggle="sidebarCollapsed = !sidebarCollapsed" />
<main class="cp-scrollbar h-screen min-w-0 flex-1 overflow-auto">
<RouterView />
<div class="flex h-screen overflow-hidden bg-(--cp-bg-page)">
<AppSidebar :collapsed="sidebarCollapsed" @toggle="toggleSidebar" />
<main class="h-screen min-w-0 flex-1 overflow-hidden">
<BaseScrollbar>
<RouterView />
</BaseScrollbar>
</main>
</div>
</template>
+2
View File
@@ -1,4 +1,6 @@
import { createApp } from 'vue'
import '@fontsource-variable/inter'
import '@fontsource-variable/jetbrains-mono'
import App from './App.vue'
import { router } from './router'
+1 -1
View File
@@ -3,6 +3,6 @@ import { createRouter, createWebHistory } from 'vue-router'
import { routes } from './routes'
export const router = createRouter({
history: createWebHistory('/admin/'),
history: createWebHistory('/'),
routes,
})
+6
View File
@@ -2,8 +2,14 @@ import type { RouteRecordRaw } from 'vue-router'
import AdminLayout from '@/layout/index.vue'
import DashboardView from '@/views/dashboard/DashboardView.vue'
import LoginView from '@/views/login/LoginView.vue'
export const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'login',
component: LoginView,
},
{
path: '/',
component: AdminLayout,
+3
View File
@@ -1,3 +1,6 @@
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
export const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
+24
View File
@@ -0,0 +1,24 @@
import { defineStore } from 'pinia'
import { shallowRef } from 'vue'
export const useUiStore = defineStore(
'ui',
() => {
const sidebarCollapsed = shallowRef(false)
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
}
return {
sidebarCollapsed,
toggleSidebar,
}
},
{
persist: {
key: 'codex-proxy-rs-ui',
pick: ['sidebarCollapsed'],
},
},
)
+1 -1
View File
@@ -6,7 +6,7 @@ html {
min-width: 360px;
background: var(--cp-bg-page);
color: var(--cp-text-primary);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-family: "Inter Variable", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
+2 -2
View File
@@ -3,8 +3,8 @@
@import 'tailwindcss';
@theme {
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--font-sans: "Inter Variable", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "JetBrains Mono Variable", "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--color-cp-page: var(--cp-bg-page);
--color-cp-surface: var(--cp-bg-surface);
--color-cp-subtle: var(--cp-bg-subtle);
+5 -5
View File
@@ -19,13 +19,13 @@ const {
</script>
<template>
<div class="w-full min-w-[1180px] px-7 pt-[34px] pb-[34px]">
<header class="flex h-[68px] items-start justify-between">
<div class="w-full min-w-295 p-7">
<header class="flex h-17 items-start justify-between">
<div>
<h1 class="mt-0 text-[34px] leading-[1.15] font-[800] mb-0 text-[var(--cp-text-primary)]">
<h1 class="mt-0 text-[34px] leading-[1.15] font-extrabold mb-0 text-(--cp-text-primary)">
系统概览
</h1>
<p class="mt-2.5 text-[15px] leading-[1.15] font-semibold mb-0 text-[var(--cp-text-secondary)]">
<p class="mt-2.5 text-[15px] leading-[1.15] font-semibold mb-0 text-(--cp-text-secondary)">
生产环境 · 14:32 更新 · 自动刷新 30s
</p>
</div>
@@ -33,7 +33,7 @@ const {
<AppTopbar class="mt-0.5" />
</header>
<section class="mt-6 grid grid-cols-[repeat(4,minmax(0,1fr))] gap-6" aria-label="核心指标">
<section class="mt-6 grid grid-cols-4 gap-6" aria-label="核心指标">
<MetricCard v-for="metric in metrics" :key="metric.title" :metric="metric" />
</section>
@@ -52,127 +52,127 @@ const statusRows = [
}>
const rowToneClasses: Record<SemanticTone, string> = {
normal: 'bg-[var(--cp-normal-bg)] text-[var(--cp-normal)]',
info: 'bg-[var(--cp-info-bg)] text-[var(--cp-info)]',
success: 'bg-[var(--cp-success-bg)] text-[var(--cp-success)]',
warning: 'bg-[var(--cp-warning-bg)] text-[var(--cp-warning)]',
danger: 'bg-[var(--cp-danger-bg)] text-[var(--cp-danger)]',
normal: 'bg-(--cp-normal-bg) text-(--cp-normal)',
info: 'bg-(--cp-info-bg) text-(--cp-info)',
success: 'bg-(--cp-success-bg) text-(--cp-success)',
warning: 'bg-(--cp-warning-bg) text-(--cp-warning)',
danger: 'bg-(--cp-danger-bg) text-(--cp-danger)',
}
const valueToneClasses: Record<SemanticTone, string> = {
normal: 'text-[var(--cp-normal-text)]',
info: 'text-[var(--cp-info-text)]',
success: 'text-[var(--cp-success-text)]',
warning: 'text-[var(--cp-warning-text)]',
danger: 'text-[var(--cp-danger-text)]',
normal: 'text-(--cp-normal-text)',
info: 'text-(--cp-info-text)',
success: 'text-(--cp-success-text)',
warning: 'text-(--cp-warning-text)',
danger: 'text-(--cp-danger-text)',
}
const loadToneClasses: Record<SemanticTone, string> = {
normal: 'bg-[var(--cp-normal)]',
info: 'bg-[var(--cp-info)]',
success: 'bg-[var(--cp-success)]',
warning: 'bg-[var(--cp-warning)]',
danger: 'bg-[var(--cp-danger)]',
normal: 'bg-(--cp-normal)',
info: 'bg-(--cp-info)',
success: 'bg-(--cp-success)',
warning: 'bg-(--cp-warning)',
danger: 'bg-(--cp-danger)',
}
</script>
<template>
<BaseCard as="article" :padded="false" class="h-[450px] w-full">
<BaseCard as="article" :padded="false" class="h-112.5 w-full">
<div class="grid grid-cols-[minmax(360px,360fr)_minmax(612px,612fr)_minmax(500px,500fr)] gap-7 px-7 pt-6">
<section class="h-[402px] w-full">
<h2 class="m-0 text-[20px] leading-[1.15] font-[760] text-[var(--cp-text-primary)]">账号调度</h2>
<p class="mt-[7px] mb-0 text-[13px] leading-[1.15] font-semibold text-[var(--cp-text-secondary)]">容量、并发与分配策略</p>
<section class="h-100.5 w-full">
<h2 class="m-0 text-xl leading-[1.15] font-[760] text-(--cp-text-primary)">账号调度</h2>
<p class="mt-1.75 mb-0 text-[13px] leading-[1.15] font-semibold text-(--cp-text-secondary)">容量、并发与分配策略</p>
<div class="mt-[31px] h-[122px] rounded-[14px] bg-[var(--cp-bg-subtle)] px-4 pt-[18px]">
<span class="block h-[14px] text-[12px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">可调度容量</span>
<div class="mt-[12px] grid h-[34px] grid-cols-[minmax(0,1fr)_auto] items-start gap-4">
<strong class="font-mono text-[32px] leading-[1.05] font-[760] tabular-nums text-[var(--cp-text-primary)]">81 / 135</strong>
<span class="mt-[14px] text-[12px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">60% 已分配</span>
<div class="mt-7.75 h-30.5 rounded-[14px] bg-(--cp-bg-subtle) px-4 pt-4.5">
<span class="block h-3.5 text-xs leading-[1.15] font-[650] text-(--cp-text-secondary)">可调度容量</span>
<div class="mt-3 grid h-8.5 grid-cols-[minmax(0,1fr)_auto] items-start gap-4">
<strong class="font-mono text-[32px] leading-[1.05] font-[760] tabular-nums text-(--cp-text-primary)">81 / 135</strong>
<span class="mt-3.5 text-xs leading-[1.15] font-[650] text-(--cp-text-secondary)">60% 已分配</span>
</div>
<div class="mt-[18px] h-2.5 w-full overflow-hidden rounded-full bg-[#E2E8F0]">
<i class="block h-2.5 w-[60%] rounded-full bg-[var(--cp-success)]" />
<div class="mt-4.5 h-2.5 w-full overflow-hidden rounded-full bg-slate-200">
<i class="block h-2.5 w-3/5 rounded-full bg-(--cp-success)" />
</div>
</div>
<div class="mt-4 grid h-[90px] grid-cols-3 gap-4 rounded-[14px] bg-[var(--cp-bg-subtle)] px-4 pt-5">
<div class="mt-4 grid h-22.5 grid-cols-3 gap-4 rounded-[14px] bg-(--cp-bg-subtle) px-4 pt-5">
<div v-for="stat in scheduleStats" :key="stat.label">
<span class="text-[12px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">{{ stat.label }}</span>
<strong class="mt-[13px] block font-mono text-[21px] leading-[1.1] font-[760] tabular-nums text-[var(--cp-text-primary)]">{{ stat.value }}</strong>
<span class="text-xs leading-[1.15] font-[650] text-(--cp-text-secondary)">{{ stat.label }}</span>
<strong class="mt-3.25 block font-mono text-[21px] leading-[1.1] font-[760] tabular-nums text-(--cp-text-primary)">{{ stat.value }}</strong>
</div>
</div>
<div class="mt-4 h-[82px] rounded-[14px] bg-[var(--cp-bg-subtle)] px-4 pt-[17px]">
<span class="text-[12px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">分配策略</span>
<strong class="mt-[10px] block text-[17px] leading-[1.15] font-[650] text-[var(--cp-text-primary)]">最少使用优先</strong>
<div class="mt-4 h-20.5 rounded-[14px] bg-(--cp-bg-subtle) px-4 pt-4.25">
<span class="text-xs leading-[1.15] font-[650] text-(--cp-text-secondary)">分配策略</span>
<strong class="mt-2.5 block text-[17px] leading-[1.15] font-[650] text-(--cp-text-primary)">最少使用优先</strong>
</div>
</section>
<section class="w-full">
<h2 class="m-0 text-[20px] leading-[1.15] font-[760] text-[var(--cp-text-primary)]">活跃账号用量</h2>
<p class="mt-[7px] mb-0 text-[13px] leading-[1.15] font-semibold text-[var(--cp-text-secondary)]">24h 请求排序</p>
<h2 class="m-0 text-xl leading-[1.15] font-[760] text-(--cp-text-primary)">活跃账号用量</h2>
<p class="mt-1.75 mb-0 text-[13px] leading-[1.15] font-semibold text-(--cp-text-secondary)">24h 请求排序</p>
<div class="mt-[27px] grid h-[330px] w-full gap-1.5 overflow-hidden">
<div class="mt-6.75 grid h-82.5 w-full gap-1.5 overflow-hidden">
<article
v-for="account in accounts"
:key="account.name"
class="grid h-[78px] w-[calc(100%-28px)] grid-cols-[34px_10px_minmax(220px,1fr)_6px_64px_2px_70px_4px_84px_6px] items-start rounded-[14px] px-3.5 transition-[background-color,transform] duration-200 hover:-translate-y-px hover:bg-[var(--cp-bg-subtle)] active:translate-y-0"
:class="account.name === 'Amy Ops' || account.name === 'Build Bot' ? 'bg-[var(--cp-bg-subtle)]' : 'bg-white'"
class="grid h-19.5 w-[calc(100%-28px)] grid-cols-[34px_10px_minmax(220px,1fr)_6px_64px_2px_70px_4px_84px_6px] items-start rounded-[14px] px-3.5 transition-colors duration-200 hover:bg-(--cp-bg-subtle)"
:class="account.name === 'Amy Ops' || account.name === 'Build Bot' ? 'bg-(--cp-bg-subtle)' : 'bg-white'"
>
<span class="mt-[22px] inline-flex size-[34px] items-center justify-center rounded-full" :class="rowToneClasses[account.tone]">
<span class="mt-5.5 inline-flex size-8.5 items-center justify-center rounded-full" :class="rowToneClasses[account.tone]">
{{ account.name[0] }}
</span>
<span class="col-start-3 row-start-1 mt-[17px] flex min-w-0 items-start">
<strong class="text-[14px] leading-[1.15] font-[650] text-[var(--cp-text-primary)]">{{ account.name }}</strong>
<small class="ml-1.5 mt-[3px] text-[11px] leading-[1.15] font-[650] text-[var(--cp-text-muted)]">{{ account.plan }}</small>
<span class="col-start-3 row-start-1 mt-4.25 flex min-w-0 items-start">
<strong class="text-sm leading-[1.15] font-[650] text-(--cp-text-primary)">{{ account.name }}</strong>
<small class="ml-1.5 mt-0.75 text-[11px] leading-[1.15] font-[650] text-(--cp-text-muted)">{{ account.plan }}</small>
</span>
<span class="col-start-3 row-start-1 mt-[48px] min-w-0 truncate text-[12px] leading-[1.15] font-semibold text-[var(--cp-text-secondary)]">{{ account.email }}</span>
<strong class="col-start-5 row-start-1 mt-5 w-16 font-mono text-[14px] leading-[1.15] font-bold tabular-nums text-[var(--cp-text-primary)]">{{ account.requests }}</strong>
<strong class="col-start-7 row-start-1 mt-5 w-[70px] font-mono text-[14px] leading-[1.15] font-bold tabular-nums text-[var(--cp-text-primary)]">{{ account.tokens }}</strong>
<span class="col-start-5 row-start-1 mt-[48px] w-20 text-[12px] leading-[1.15] font-semibold text-[var(--cp-text-secondary)]">{{ account.lastUsed }}</span>
<span class="col-start-9 row-start-1 mt-9 block h-1.5 w-[84px] overflow-hidden rounded-full bg-[var(--cp-bg-muted)]">
<span class="col-start-3 row-start-1 mt-12 min-w-0 truncate text-xs leading-[1.15] font-semibold text-(--cp-text-secondary)">{{ account.email }}</span>
<strong class="col-start-5 row-start-1 mt-5 w-16 font-mono text-sm leading-[1.15] font-bold tabular-nums text-(--cp-text-primary)">{{ account.requests }}</strong>
<strong class="col-start-7 row-start-1 mt-5 w-17.5 font-mono text-sm leading-[1.15] font-bold tabular-nums text-(--cp-text-primary)">{{ account.tokens }}</strong>
<span class="col-start-5 row-start-1 mt-12 w-20 text-xs leading-[1.15] font-semibold text-(--cp-text-secondary)">{{ account.lastUsed }}</span>
<span class="col-start-9 row-start-1 mt-9 block h-1.5 w-21 overflow-hidden rounded-full bg-(--cp-bg-muted)">
<i class="block h-1.5 rounded-full" :class="loadToneClasses[account.tone]" :style="{ width: `${account.loadWidth}px` }" />
</span>
</article>
</div>
</section>
<section class="h-[402px] w-full">
<header class="flex h-[50px] items-start justify-between">
<section class="h-100.5 w-full">
<header class="flex h-12.5 items-start justify-between">
<div>
<h2 class="m-0 text-[20px] leading-[1.15] font-[760] text-[var(--cp-text-primary)]">账号状态</h2>
<p class="mt-1 mb-0 text-[13px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">账号池健康结构</p>
<h2 class="m-0 text-xl leading-[1.15] font-[760] text-(--cp-text-primary)">账号状态</h2>
<p class="mt-1 mb-0 text-[13px] leading-[1.15] font-[650] text-(--cp-text-secondary)">账号池健康结构</p>
</div>
<div class="grid justify-items-end">
<strong class="font-mono text-[24px] leading-[1.05] font-[760] tabular-nums text-[var(--cp-success-text)]">71.1%</strong>
<span class="mt-0.5 text-[12px] leading-[1.15] font-bold text-[var(--cp-text-secondary)]">可用率</span>
<strong class="font-mono text-2xl leading-[1.05] font-[760] tabular-nums text-(--cp-success-text)">71.1%</strong>
<span class="mt-0.5 text-xs leading-[1.15] font-bold text-(--cp-text-secondary)">可用率</span>
</div>
</header>
<div class="mt-[22px] h-[42px] w-full">
<div class="mt-5.5 h-10.5 w-full">
<div class="flex h-4 items-center justify-between">
<span class="text-[12px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">状态分布</span>
<span class="text-[12px] leading-[1.15] font-[650] text-[var(--cp-danger-text)]">不可用 7</span>
<span class="text-xs leading-[1.15] font-[650] text-(--cp-text-secondary)">状态分布</span>
<span class="text-xs leading-[1.15] font-[650] text-(--cp-danger-text)">不可用 7</span>
</div>
<div class="mt-2.5 flex h-3 w-full overflow-hidden rounded-full bg-[var(--cp-bg-muted)]">
<i class="h-3 basis-[71.1%] bg-[var(--cp-success)]" />
<i class="h-3 basis-[4.4%] bg-[var(--cp-normal)]" />
<i class="h-3 basis-[13.3%] bg-[var(--cp-warning)]" />
<i class="h-3 basis-[11.2%] bg-[var(--cp-danger)]" />
<div class="mt-2.5 flex h-3 w-full overflow-hidden rounded-full bg-(--cp-bg-muted)">
<i class="h-3 basis-[71.1%] bg-(--cp-success)" />
<i class="h-3 basis-[4.4%] bg-(--cp-normal)" />
<i class="h-3 basis-[13.3%] bg-(--cp-warning)" />
<i class="h-3 basis-[11.2%] bg-(--cp-danger)" />
</div>
</div>
<div class="mt-[26px] grid h-[262px] w-full gap-2.5">
<div class="mt-6.5 grid h-65.5 w-full gap-2.5">
<div
v-for="row in statusRows"
:key="row.label"
class="grid h-[58px] grid-cols-[28px_14px_minmax(0,1fr)_76px] items-center rounded-[14px] bg-[var(--cp-bg-subtle)] px-3.5 transition-[background-color,transform] duration-200 hover:-translate-y-px active:translate-y-0"
class="grid h-14.5 grid-cols-[28px_14px_minmax(0,1fr)_76px] items-center rounded-[14px] bg-(--cp-bg-subtle) px-3.5 transition-colors duration-200"
>
<span class="inline-flex size-7 items-center justify-center rounded-[9px]" :class="rowToneClasses[row.tone]">
<component :is="row.icon" :size="16" />
</span>
<span class="col-start-3 grid gap-1">
<strong class="text-[14px] leading-[1.15] font-[650] text-[var(--cp-text-primary)]">{{ row.label }}</strong>
<span class="text-[12px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">{{ row.description }}</span>
<strong class="text-sm leading-[1.15] font-[650] text-(--cp-text-primary)">{{ row.label }}</strong>
<span class="text-xs leading-[1.15] font-[650] text-(--cp-text-secondary)">{{ row.description }}</span>
</span>
<strong class="col-start-4 text-right font-mono text-[17px] leading-[1.15] font-[760] tabular-nums" :class="valueToneClasses[row.tone]">{{ row.value }}</strong>
</div>
@@ -19,19 +19,19 @@ const eventLogColumns = [
]
const levelToneClasses: Record<SemanticTone, string> = {
normal: 'bg-[var(--cp-normal-bg)] text-[var(--cp-normal-text)]',
info: 'bg-[var(--cp-info-bg)] text-[var(--cp-info-text)]',
success: 'bg-[var(--cp-success-bg)] text-[var(--cp-success-text)]',
warning: 'bg-[var(--cp-warning-bg)] text-[var(--cp-warning-text)]',
danger: 'bg-[var(--cp-danger-bg)] text-[var(--cp-danger-text)]',
normal: 'bg-(--cp-normal-bg) text-(--cp-normal-text)',
info: 'bg-(--cp-info-bg) text-(--cp-info-text)',
success: 'bg-(--cp-success-bg) text-(--cp-success-text)',
warning: 'bg-(--cp-warning-bg) text-(--cp-warning-text)',
danger: 'bg-(--cp-danger-bg) text-(--cp-danger-text)',
}
const statusToneClasses: Record<SemanticTone, string> = {
normal: 'text-[var(--cp-normal-text)]',
info: 'text-[var(--cp-success-text)]',
success: 'text-[var(--cp-success-text)]',
warning: 'text-[var(--cp-warning-text)]',
danger: 'text-[var(--cp-danger-text)]',
normal: 'text-(--cp-normal-text)',
info: 'text-(--cp-success-text)',
success: 'text-(--cp-success-text)',
warning: 'text-(--cp-warning-text)',
danger: 'text-(--cp-danger-text)',
}
function rowTone(row: object) {
@@ -43,16 +43,16 @@ function rowTone(row: object) {
<BaseCard as="article" :padded="false" class="h-[350px] w-full px-7 pt-6">
<header class="flex items-start justify-between">
<div>
<h2 class="m-0 text-[20px] leading-[1.15] font-[760] text-[var(--cp-text-primary)]">事件日志</h2>
<p class="mt-[7px] mb-0 text-[13px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">最近 50 条事件</p>
<h2 class="m-0 text-xl leading-[1.15] font-[760] text-(--cp-text-primary)">事件日志</h2>
<p class="mt-1.75 mb-0 text-[13px] leading-[1.15] font-[650] text-(--cp-text-secondary)">最近 50 条事件</p>
</div>
<div class="grid h-9 w-[206px] grid-cols-[62px_58px_58px] gap-1 rounded-xl bg-[var(--cp-bg-muted)] p-1">
<div class="grid h-9 w-[206px] grid-cols-[62px_58px_58px] gap-1 rounded-xl bg-(--cp-bg-muted) p-1">
<button
v-for="filter in filters"
:key="filter"
class="h-7 rounded-[9px] border-0 text-[12px] leading-[1.15] font-[650]"
:class="filter === '全部' ? 'bg-white text-[var(--cp-text-primary)] shadow-[var(--cp-shadow-control)]' : 'bg-transparent text-[var(--cp-text-secondary)]'"
class="h-7 rounded-[9px] border-0 text-xs leading-[1.15] font-[650]"
:class="filter === '全部' ? 'bg-white text-(--cp-text-primary) shadow-(--cp-shadow-control)' : 'bg-transparent text-(--cp-text-secondary)'"
type="button"
>
{{ filter }}
@@ -60,23 +60,23 @@ function rowTone(row: object) {
</div>
</header>
<div class="mt-[17px] flex h-[240px] w-full justify-between overflow-hidden">
<div class="mt-[17px] flex h-60 w-full justify-between overflow-hidden">
<BaseTable
class="min-w-0 flex-1"
:columns="eventLogColumns"
:rows="rows"
table-class="min-w-full"
header-row-class="h-10 rounded-xl bg-[var(--cp-bg-subtle)] text-[12px] leading-[1.15] font-bold text-[var(--cp-text-secondary)]"
body-row-class="h-14 rounded-[10px] transition-[background-color,transform] duration-200 hover:-translate-y-px hover:bg-[var(--cp-bg-subtle)] active:translate-y-0"
header-cell-class="min-w-0 overflow-hidden bg-[var(--cp-bg-subtle)] px-5 text-ellipsis whitespace-nowrap first:rounded-l-xl last:rounded-r-xl"
body-cell-class="min-w-0 overflow-hidden px-5 text-ellipsis whitespace-nowrap text-[12px] leading-[1.15] font-[650] text-[var(--cp-text-primary)] first:rounded-l-[10px] last:rounded-r-[10px]"
header-row-class="h-10 rounded-xl bg-(--cp-bg-subtle) text-xs leading-[1.15] font-bold text-(--cp-text-secondary)"
body-row-class="h-14 rounded-[10px] transition-colors duration-200 hover:bg-(--cp-bg-subtle)"
header-cell-class="min-w-0 overflow-hidden bg-(--cp-bg-subtle) px-5 text-ellipsis whitespace-nowrap first:rounded-l-xl last:rounded-r-xl"
body-cell-class="min-w-0 overflow-hidden px-5 text-ellipsis whitespace-nowrap text-xs leading-[1.15] font-[650] text-(--cp-text-primary) first:rounded-l-[10px] last:rounded-r-[10px]"
>
<template #time="{ value }">
<span class="font-mono tabular-nums">{{ value }}</span>
</template>
<template #level="{ row, value }">
<span class="inline-flex h-6 w-[58px] items-center justify-center rounded-full text-[12px] leading-[1.15] font-bold" :class="levelToneClasses[rowTone(row)]">{{ value }}</span>
<span class="inline-flex h-6 w-[58px] items-center justify-center rounded-full text-xs leading-[1.15] font-bold" :class="levelToneClasses[rowTone(row)]">{{ value }}</span>
</template>
<template #requestId="{ value }">
@@ -7,19 +7,19 @@ defineProps<{
}>()
const iconToneClasses: Record<SemanticTone, string> = {
normal: 'bg-[var(--cp-normal-bg)] text-[var(--cp-normal)]',
info: 'bg-[var(--cp-info-bg)] text-[var(--cp-info)]',
success: 'bg-[var(--cp-success-bg)] text-[var(--cp-success)]',
warning: 'bg-[var(--cp-warning-bg)] text-[var(--cp-warning)]',
danger: 'bg-[var(--cp-danger-bg)] text-[var(--cp-danger)]',
normal: 'bg-(--cp-normal-bg) text-(--cp-normal)',
info: 'bg-(--cp-info-bg) text-(--cp-info)',
success: 'bg-(--cp-success-bg) text-(--cp-success)',
warning: 'bg-(--cp-warning-bg) text-(--cp-warning)',
danger: 'bg-(--cp-danger-bg) text-(--cp-danger)',
}
const detailToneClasses: Record<SemanticTone, string> = {
normal: 'text-[var(--cp-normal-text)]',
info: 'text-[var(--cp-info-text)]',
success: 'text-[var(--cp-success-text)]',
warning: 'text-[var(--cp-warning-text)]',
danger: 'text-[var(--cp-danger-text)]',
normal: 'text-(--cp-normal-text)',
info: 'text-(--cp-info-text)',
success: 'text-(--cp-success-text)',
warning: 'text-(--cp-warning-text)',
danger: 'text-(--cp-danger-text)',
}
</script>
@@ -29,23 +29,23 @@ const detailToneClasses: Record<SemanticTone, string> = {
<span class="inline-flex size-[34px] shrink-0 items-center justify-center rounded-[10px]" :class="iconToneClasses[metric.tone]">
<component :is="metric.icon" :size="18" />
</span>
<span class="mt-1 text-[13px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">{{ metric.title }}</span>
<span class="mt-1 text-[13px] leading-[1.15] font-[650] text-(--cp-text-secondary)">{{ metric.title }}</span>
</div>
<strong class="mt-2.5 block w-full font-mono text-[28px] leading-[1.05] font-[780] tabular-nums text-[var(--cp-text-primary)]">
<strong class="mt-2.5 block w-full font-mono text-[28px] leading-[1.05] font-[780] tabular-nums text-(--cp-text-primary)">
{{ metric.value }}
</strong>
<div class="mt-[16.6px] grid h-[30px] w-full grid-cols-2 items-center rounded-[10px] bg-[var(--cp-bg-subtle)] px-3">
<div class="mt-[16.6px] grid h-[30px] w-full grid-cols-2 items-center rounded-[10px] bg-(--cp-bg-subtle) px-3">
<span class="inline-flex min-w-0 w-full items-center justify-start gap-3">
<span class="shrink-0 text-[11px] leading-[1.15] font-[650] text-[var(--cp-text-muted)]">{{ metric.details[0]?.label }}</span>
<b class="min-w-0 truncate font-mono text-[12px] leading-[1.15] font-bold tabular-nums" :class="metric.details[0]?.tone ? detailToneClasses[metric.details[0].tone] : undefined">
<span class="shrink-0 text-[11px] leading-[1.15] font-[650] text-(--cp-text-muted)">{{ metric.details[0]?.label }}</span>
<b class="min-w-0 truncate font-mono text-xs leading-[1.15] font-bold tabular-nums" :class="metric.details[0]?.tone ? detailToneClasses[metric.details[0].tone] : undefined">
{{ metric.details[0]?.value }}
</b>
</span>
<span class="inline-flex min-w-0 w-full items-center justify-start gap-3">
<span class="shrink-0 text-[11px] leading-[1.15] font-[650] text-[var(--cp-text-muted)]">{{ metric.details[1]?.label }}</span>
<b class="min-w-0 truncate font-mono text-[12px] leading-[1.15] font-bold tabular-nums" :class="metric.details[1]?.tone ? detailToneClasses[metric.details[1].tone] : undefined">
<span class="shrink-0 text-[11px] leading-[1.15] font-[650] text-(--cp-text-muted)">{{ metric.details[1]?.label }}</span>
<b class="min-w-0 truncate font-mono text-xs leading-[1.15] font-bold tabular-nums" :class="metric.details[1]?.tone ? detailToneClasses[metric.details[1].tone] : undefined">
{{ metric.details[1]?.value }}
</b>
</span>
@@ -11,28 +11,28 @@ const tabs = ['请求', '错误', '延迟']
const axisLabels = ['00', '04', '08', '12', '16', '20', '24']
const dotToneClasses: Record<SemanticTone, string> = {
normal: 'bg-[var(--cp-normal)]',
info: 'bg-[var(--cp-info)]',
success: 'bg-[var(--cp-success)]',
warning: 'bg-[var(--cp-warning)]',
danger: 'bg-[var(--cp-danger)]',
normal: 'bg-(--cp-normal)',
info: 'bg-(--cp-info)',
success: 'bg-(--cp-success)',
warning: 'bg-(--cp-warning)',
danger: 'bg-(--cp-danger)',
}
</script>
<template>
<BaseCard as="article" :padded="false" class="h-[380px] w-full px-7 pt-[22px]">
<BaseCard as="article" :padded="false" class="h-95 w-full px-7 pt-5.5">
<header class="flex items-start justify-between">
<div class="pt-0.5">
<h2 class="m-0 text-[20px] leading-[1.15] font-[760] text-[var(--cp-text-primary)]">请求趋势</h2>
<p class="mt-[7px] mb-0 text-[13px] leading-[1.15] font-[650] text-[var(--cp-text-secondary)]">最近 24 小时 · 14:00 峰值 15.2K</p>
<h2 class="m-0 text-xl leading-[1.15] font-[760] text-(--cp-text-primary)">请求趋势</h2>
<p class="mt-1.75 mb-0 text-[13px] leading-[1.15] font-[650] text-(--cp-text-secondary)">最近 24 小时 · 14:00 峰值 15.2K</p>
</div>
<div class="grid h-[38px] w-[246px] grid-cols-3 gap-1 rounded-xl bg-[var(--cp-bg-muted)] p-1">
<div class="grid h-9.5 w-61.5 grid-cols-3 gap-1 rounded-xl bg-(--cp-bg-muted) p-1">
<button
v-for="tab in tabs"
:key="tab"
class="h-[30px] rounded-[9px] border-0 text-[12px] leading-[1.15] font-[650]"
:class="tab === '请求' ? 'bg-white text-[var(--cp-text-primary)] shadow-[0_10px_24px_-18px_#0E172614]' : 'bg-transparent text-[var(--cp-text-secondary)]'"
class="h-7.5 rounded-[9px] border-0 text-xs leading-[1.15] font-[650]"
:class="tab === '请求' ? 'bg-white text-(--cp-text-primary) shadow-[0_10px_24px_-18px_#0E172614]' : 'bg-transparent text-(--cp-text-secondary)'"
type="button"
>
{{ tab }}
@@ -40,9 +40,9 @@ const dotToneClasses: Record<SemanticTone, string> = {
</div>
</header>
<div class="mt-[19px] grid grid-cols-[minmax(0,1fr)_minmax(150px,180px)] gap-[30px]">
<div class="h-[268px] w-full overflow-hidden rounded-[10px] bg-white">
<svg class="block h-[268px] w-full" viewBox="0 0 682 268" preserveAspectRatio="none" role="img" aria-label="最近 24 小时请求趋势">
<div class="mt-4.75 grid grid-cols-[minmax(0,1fr)_minmax(150px,180px)] gap-7.5">
<div class="h-67 w-full overflow-hidden rounded-[10px] bg-white">
<svg class="block h-67 w-full" viewBox="0 0 682 268" preserveAspectRatio="none" role="img" aria-label="最近 24 小时请求趋势">
<g transform="translate(0 0)">
<line v-for="y in [18, 62, 106, 150, 194, 238]" :key="y" x1="0" x2="650" :y1="y" :y2="y" stroke="#F1F5F9" />
<path
@@ -73,22 +73,21 @@ const dotToneClasses: Record<SemanticTone, string> = {
transform="translate(0 140) scale(1 0.791667)"
/>
</g>
<g font-family="JetBrains Mono, SFMono-Regular, Consolas, monospace" font-size="11" font-weight="700" fill="#94A3B8">
<g font-family="JetBrains Mono Variable, JetBrains Mono, SFMono-Regular, Consolas, monospace" font-size="11" font-weight="700" fill="#94A3B8">
<text v-for="(label, index) in axisLabels" :key="label" :x="[0, 108, 216, 324, 432, 540, 632][index]" y="261">{{ label }}</text>
</g>
</svg>
</div>
<aside class="h-[268px] w-full rounded-2xl bg-[var(--cp-bg-subtle)] px-5 py-[18px]">
<aside class="flex h-67 w-full flex-col rounded-2xl bg-(--cp-bg-subtle) px-5 py-4.5" style="gap: 36.6px">
<div
v-for="(item, index) in summary"
v-for="item in summary"
:key="item.label"
class="grid grid-cols-[minmax(0,1fr)_8px] items-start gap-x-3 gap-y-px"
:class="index === 0 ? '' : 'mt-[36.6px]'"
>
<span class="col-span-2 text-[12px] leading-[1.15] font-bold text-[var(--cp-text-secondary)]">{{ item.label }}</span>
<strong class="mt-[7px] font-mono text-[24px] leading-[1.15] font-[760] tabular-nums text-[var(--cp-text-primary)]">{{ item.value }}</strong>
<i class="mt-[14px] size-2 justify-self-end rounded-full" :class="dotToneClasses[item.tone]" />
<span class="col-span-2 text-xs leading-[1.15] font-bold text-(--cp-text-secondary)">{{ item.label }}</span>
<strong class="mt-1.75 font-mono text-2xl leading-[1.15] font-[760] tabular-nums text-(--cp-text-primary)">{{ item.value }}</strong>
<i class="mt-3.5 size-2 justify-self-end rounded-full" :class="dotToneClasses[item.tone]" />
</div>
</aside>
</div>
@@ -7,38 +7,38 @@ defineProps<{
}>()
const iconToneClasses: Record<SemanticTone, string> = {
normal: 'bg-[var(--cp-normal-bg)] text-[var(--cp-normal)]',
info: 'bg-[var(--cp-info-bg)] text-[var(--cp-info)]',
success: 'bg-[var(--cp-success-bg)] text-[var(--cp-success)]',
warning: 'bg-[var(--cp-warning-bg)] text-[var(--cp-warning)]',
danger: 'bg-[var(--cp-danger-bg)] text-[var(--cp-danger)]',
normal: 'bg-(--cp-normal-bg) text-(--cp-normal)',
info: 'bg-(--cp-info-bg) text-(--cp-info)',
success: 'bg-(--cp-success-bg) text-(--cp-success)',
warning: 'bg-(--cp-warning-bg) text-(--cp-warning)',
danger: 'bg-(--cp-danger-bg) text-(--cp-danger)',
}
const valueToneClasses: Record<SemanticTone, string> = {
normal: 'text-[var(--cp-normal-text)]',
info: 'text-[var(--cp-info-text)]',
success: 'text-[var(--cp-success-text)]',
warning: 'text-[var(--cp-warning-text)]',
danger: 'text-[var(--cp-danger-text)]',
normal: 'text-(--cp-normal-text)',
info: 'text-(--cp-info-text)',
success: 'text-(--cp-success-text)',
warning: 'text-(--cp-warning-text)',
danger: 'text-(--cp-danger-text)',
}
</script>
<template>
<BaseCard as="article" :padded="false" class="h-[380px] w-full px-7 pt-6">
<h2 class="m-0 text-[20px] leading-[1.15] font-[760] text-[var(--cp-text-primary)]">服务状态</h2>
<BaseCard as="article" :padded="false" class="h-95 w-full px-7 pt-6">
<h2 class="m-0 text-xl leading-[1.15] font-[760] text-(--cp-text-primary)">服务状态</h2>
<div class="mt-[23px] grid gap-2">
<div class="mt-5.75 grid gap-2">
<div
v-for="item in items"
:key="item.label"
class="grid h-[50px] w-full grid-cols-[24px_16px_minmax(0,224fr)_16px_86px_34px_100px] items-center rounded-[14px] bg-[var(--cp-bg-subtle)] px-3.5"
class="grid h-12.5 w-full grid-cols-[24px_16px_minmax(0,224fr)_16px_86px_34px_100px] items-center rounded-[14px] bg-(--cp-bg-subtle) px-3.5"
>
<span class="inline-flex size-6 items-center justify-center rounded-lg" :class="iconToneClasses[item.tone]">
<component :is="item.icon" :size="14" />
</span>
<strong class="col-start-3 w-40 text-[13px] leading-[1.15] font-[650] text-[var(--cp-text-primary)]">{{ item.label }}</strong>
<span class="col-start-5 w-[86px] text-[13px] leading-[1.15] font-bold" :class="valueToneClasses[item.tone]">{{ item.value }}</span>
<span class="col-start-7 w-[100px] font-mono text-[12px] leading-[1.15] font-semibold text-[var(--cp-text-secondary)]">{{ item.detail }}</span>
<strong class="col-start-3 w-40 text-[13px] leading-[1.15] font-[650] text-(--cp-text-primary)">{{ item.label }}</strong>
<span class="col-start-5 w-21.5 text-[13px] leading-[1.15] font-bold" :class="valueToneClasses[item.tone]">{{ item.value }}</span>
<span class="col-start-7 w-25 font-mono text-xs leading-[1.15] font-semibold text-(--cp-text-secondary)">{{ item.detail }}</span>
</div>
</div>
</BaseCard>
+160
View File
@@ -0,0 +1,160 @@
<script setup lang="ts">
import { ArrowRight, KeyRound, Mail, SquareTerminal } from '@lucide/vue'
import BaseCard from '@/components/base/BaseCard.vue'
</script>
<template>
<main
class="flex h-screen w-screen items-center justify-center overflow-hidden bg-[linear-gradient(115deg,#F8FAFC_0%,#FFFFFF_46%,#F1F7FF_100%)] px-6 py-8 min-[961px]:px-24 min-[961px]:py-12"
>
<section
class="grid h-full max-h-[calc(100vh-64px)] w-full max-w-[1728px] items-center gap-8 min-[961px]:max-h-[calc(100vh-96px)] min-[961px]:grid-cols-[minmax(0,980px)_520px] min-[961px]:gap-22"
aria-label="Codex Proxy RS 登录"
>
<div class="relative h-full min-h-0 overflow-hidden">
<div class="absolute left-0 top-0 flex h-13 w-[420px] items-center gap-3.25">
<span class="inline-flex size-11 items-center justify-center rounded-[13px] bg-[#111827] text-white">
<SquareTerminal :size="22" />
</span>
<span class="grid h-10.5 content-center gap-1">
<strong class="text-[17px] leading-[1.1] font-[760] text-[#0E1726]">Codex Proxy RS</strong>
<span class="text-[11px] leading-[1.1] font-[650] text-[#64748B]">Proxy RS · v0.1.0</span>
</span>
</div>
<div class="pointer-events-none absolute left-2 top-[26%] h-[238px] w-[910px] max-w-[96vw]">
<svg class="size-full overflow-visible" viewBox="0 0 910 238" fill="none" preserveAspectRatio="none">
<path
d="M0 134 C154 30 304 54 440 116 C592 186 726 178 910 54"
stroke="#2563EB"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2.2"
/>
</svg>
</div>
<div class="pointer-events-none absolute left-6 top-[35%] h-[210px] w-[870px] max-w-[92vw]">
<svg class="size-full overflow-visible" viewBox="0 0 870 210" fill="none" preserveAspectRatio="none">
<path
d="M0 62 C138 150 286 154 424 94 C580 26 708 48 870 150"
stroke="#0F9F9A"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
<div class="pointer-events-none absolute left-10.5 top-[30%] h-[210px] w-[820px] max-w-[88vw] opacity-70">
<svg class="size-full overflow-visible" viewBox="0 0 820 210" fill="none" preserveAspectRatio="none">
<path
d="M0 118 C132 54 266 76 392 124 C540 178 666 156 820 86"
stroke="#93C5FD"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.2"
/>
</svg>
</div>
<div class="pointer-events-none absolute left-15 top-[45%] h-[178px] w-[780px] max-w-[84vw] opacity-70">
<svg class="size-full overflow-visible" viewBox="0 0 780 178" fill="none" preserveAspectRatio="none">
<path
d="M0 78 C118 42 224 118 352 78 C500 32 632 96 780 62"
stroke="#99F6E4"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.2"
/>
</svg>
</div>
<div class="pointer-events-none absolute left-24 top-[56%] h-[130px] w-[680px] max-w-[74vw] opacity-60">
<svg class="size-full overflow-visible" viewBox="0 0 680 130" fill="none" preserveAspectRatio="none">
<path
d="M0 62 C108 88 192 30 316 58 C442 86 548 42 680 66"
stroke="#CBD5E1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.1"
/>
</svg>
</div>
<span class="pointer-events-none absolute left-[446px] top-[39%] size-1.75 rounded-full bg-[#2563EB] shadow-[0_0_14px_#2563EB66]" />
<span class="pointer-events-none absolute left-[602px] top-[47%] size-1.75 rounded-full bg-[#0F9F9A] shadow-[0_0_14px_#0F9F9A66]" />
<span class="pointer-events-none absolute left-[262px] top-[35%] size-1 rounded-full bg-[#94A3B8]" />
<div class="absolute bottom-8 left-0 grid w-[580px] max-w-full gap-3.5 min-[961px]:bottom-[64px] min-[961px]:left-14">
<h1 class="m-0 text-[38px] leading-[1.08] font-[780] tracking-normal text-[#0E1726]">
安静进入控制台。
</h1>
<p class="m-0 max-w-[580px] text-sm leading-[1.45] font-medium text-[#64748B]">
几条请求轨迹,连接账号池、模型目录与管理入口。
</p>
</div>
</div>
<BaseCard
as="form"
:padded="false"
radius-class="rounded-[22px]"
shadow-class="shadow-[0_12px_26px_-18px_#0E17261F,0_34px_70px_-40px_#0E172629]"
class="grid w-full max-w-120 gap-5.5 p-9"
>
<header class="grid h-21 gap-2.5">
<h2 class="m-0 text-[34px] leading-[1.12] font-extrabold tracking-normal text-[#0E1726]">
登录控制台
</h2>
<p class="m-0 text-sm leading-[1.55] font-medium text-[#64748B]">
使用管理员账号和访问密钥进入 Codex Proxy RS。
</p>
</header>
<label class="grid h-18 gap-2">
<span class="text-xs leading-[1.1] font-bold text-[#0E1726]">管理员账号</span>
<span class="inline-flex h-11.5 items-center gap-2.5 rounded-xl bg-[#EEF2F7] px-3.5 text-[#64748B] shadow-[inset_0_1px_0_#FFFFFF66]">
<Mail :size="17" />
<input
class="min-w-0 flex-1 border-0 bg-transparent text-[13px] leading-none font-medium text-[#0E1726] outline-0"
value="admin@example.com"
readonly
>
</span>
</label>
<label class="grid h-18 gap-2">
<span class="text-xs leading-[1.1] font-bold text-[#0E1726]">访问密钥</span>
<span class="inline-flex h-11.5 items-center gap-2.5 rounded-xl bg-[#EEF2F7] px-3.5 text-[#64748B] shadow-[inset_0_1px_0_#FFFFFF66] transition focus-within:bg-[#F8FAFC] focus-within:shadow-[inset_0_1px_0_#FFFFFF,0_0_0_3px_#DBEAFE]">
<KeyRound :size="17" />
<input
class="min-w-0 flex-1 border-0 bg-transparent text-[13px] leading-none font-medium text-[#0E1726] outline-0 placeholder:text-[#94A3B8]"
placeholder="输入会话密钥"
type="password"
>
</span>
</label>
<div class="flex h-7.5 items-center">
<label class="inline-flex h-5.5 items-center gap-2">
<input
class="size-4 appearance-none rounded-[5px] bg-white shadow-[inset_0_0_0_1px_#CBD5E1] checked:bg-[#2563EB] checked:shadow-[inset_0_0_0_4px_#FFFFFF,0_0_0_1px_#2563EB] focus-visible:outline-0 focus-visible:shadow-[inset_0_0_0_1px_#2563EB,0_0_0_3px_#DBEAFE]"
type="checkbox"
>
<span class="text-xs leading-[1.1] font-[650] text-[#64748B]">保持登录</span>
</label>
</div>
<button
class="inline-flex h-12 items-center justify-center gap-2.5 rounded-xl border-0 bg-[#2563EB] text-[15px] leading-[1.1] font-[760] text-white shadow-[0_12px_24px_-16px_#2563EB66] transition-colors hover:bg-[#1D4ED8]"
type="button"
>
<span>登录</span>
<ArrowRight :size="18" />
</button>
</BaseCard>
</section>
</main>
</template>
+20
View File
@@ -0,0 +1,20 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
test('frontend is built and routed from the server root', async () => {
const viteConfig = await readFile(resolve(root, 'vite.config.ts'), 'utf8')
const router = await readFile(resolve(root, 'src/router/index.ts'), 'utf8')
const html = await readFile(resolve(root, 'index.html'), 'utf8')
assert.match(viteConfig, /base:\s*['"]\/['"]/)
assert.doesNotMatch(viteConfig, /base:\s*['"]\/admin\/['"]/)
assert.match(router, /createWebHistory\(['"]\/['"]\)/)
assert.doesNotMatch(router, /createWebHistory\(['"]\/admin\/['"]\)/)
assert.match(html, /href=['"]\/favicon\.svg['"]/)
assert.doesNotMatch(html, /\/admin\/favicon\.svg/)
})
+1 -1
View File
@@ -4,7 +4,7 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
export default defineConfig({
base: '/admin/',
base: '/',
plugins: [vue(), tailwindcss()],
resolve: {
alias: {