2 Commits
29 changed files with 208 additions and 109 deletions
+2 -1
View File
@@ -78,7 +78,8 @@ Hero 同时包含左侧介绍和右侧状态卡时,使用 `oc-page-hero__split
- `oc-section-label`:详情面板中的字段标题。
- `oc-stat-label` / `oc-stat-value`:统计数据的标签和值。
- `oc-inset-panel`:详情中的浅色信息块。
- `oc-panel-card` / `oc-panel-card-soft`:Naive UI 卡片的硬阴影。
- `oc-panel-card` / `oc-panel-card-soft` / `oc-panel-card-xl`:普通、柔和和大型卡片的硬阴影。
- `oc-floating-panel` / `oc-modal-shell`:浮动菜单与普通方形弹窗的共享表面。
- `oc-surface`:普通 HTML 容器的标准白色表面。
- `oc-empty-card`:空状态表面。
+12
View File
@@ -17,6 +17,18 @@ const deprecatedPatterns = [
pattern: /#(?:f0fdfa|ccfbf1|99f6e4|5eead4|2dd4bf|14b8a6|0d9488|0f766e|115e59|134e4a)\b/gi,
message: '检测到旧版高饱和 teal 色值,请改用自然绿色设计令牌。',
},
{
pattern: /#(?:c084fc|a855f7|9333ea|7e22ce)\b/gi,
message: '检测到禁用的紫色硬编码,请改用 rose 或项目语义令牌。',
},
{
pattern: /#(?:1f2937|9ca3af)\b/gi,
message: '检测到默认 gray 调色板硬编码,请改用对应的 slate 色值或语义令牌。',
},
{
pattern: /#60a5fa\b/gi,
message: '检测到默认 blue-400 硬编码,请按用途改用 sky 或 amber 等项目语义色。',
},
{
pattern: /text-xs uppercase tracking-\[0\.18em\] text-slate-500/g,
message: '重复的小节标签应使用 oc-section-label。',
+6 -5
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { NAlert, NButton } from 'naive-ui'
import { NAlert, NButton, NIcon } from 'naive-ui'
import { Map as MapIcon, X } from '@vicons/tabler'
import MapPickerModal from '@/components/cloud/MapPickerModal.vue'
import type { CloudType } from '@/types/database'
@@ -115,7 +116,7 @@ watch(() => props.initialValue, value => {
class="fixed inset-0 z-[130] flex items-center justify-center bg-slate-950/60 px-4 py-6"
@click="close"
>
<div class="w-full max-w-2xl bg-white shadow-2xl" @click.stop>
<div class="oc-modal-shell w-full max-w-2xl" @click.stop>
<div class="flex items-start justify-between border-b border-slate-200 px-6 py-5">
<div>
<h3 class="text-xl font-semibold text-slate-900">编辑图片信息</h3>
@@ -128,7 +129,7 @@ watch(() => props.initialValue, value => {
title="关闭编辑窗口"
@click="close"
>
✕
<NIcon size="20"><X /></NIcon>
</button>
</div>
@@ -223,7 +224,7 @@ watch(() => props.initialValue, value => {
aria-label="地图选点"
@click="mapPickerOpen = true"
>
<span class="text-lg leading-none">🗺️</span>
<NIcon size="20"><MapIcon /></NIcon>
</button>
</div>
</div>
@@ -233,7 +234,7 @@ watch(() => props.initialValue, value => {
<textarea
v-model="form.description"
rows="4"
class="w-full resize-none border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
class="oc-field-control resize-none"
placeholder="描述一下这张图片..."
></textarea>
</div>
+13 -7
View File
@@ -181,13 +181,13 @@ onBeforeUnmount(() => {
@click="close"
>
<div
class="image-detail-shell relative w-fit max-w-[92vw] overflow-hidden rounded-[30px] shadow-2xl"
class="image-detail-shell relative w-fit max-w-[92vw] overflow-hidden shadow-2xl"
@click.stop
>
<button
type="button"
@click="close"
class="image-detail-close-button oc-overlay-button absolute right-4 top-4 z-30 h-10 w-10 rounded-full [&_svg]:stroke-[2.5]"
class="image-detail-close-button oc-overlay-button absolute right-4 top-4 z-30 h-10 w-10 [&_svg]:stroke-[2.5]"
aria-label="关闭大图"
title="关闭大图"
>
@@ -223,7 +223,7 @@ onBeforeUnmount(() => {
<button
type="button"
@click="toggleInfo"
class="oc-overlay-button absolute bottom-4 right-4 z-30 h-11 w-11 rounded-full drop-shadow-[0_2px_8px_rgba(15,23,42,0.9)]"
class="image-detail-info-button oc-overlay-button absolute bottom-4 right-4 z-30 h-11 w-11 drop-shadow-[0_2px_8px_rgba(15,23,42,0.9)]"
:aria-label="infoOpen ? '隐藏详细信息' : '查看详细信息'"
:title="infoOpen ? '隐藏详细信息' : '查看详细信息'"
>
@@ -242,7 +242,7 @@ onBeforeUnmount(() => {
<h2 class="text-xl font-bold text-slate-900">{{ title }}</h2>
<span
v-if="badgeLabel"
class="rounded-full border px-3 py-1 text-xs font-medium"
class="border px-3 py-1 text-xs font-medium"
:class="badgeClass"
>
{{ badgeLabel }}
@@ -254,11 +254,11 @@ onBeforeUnmount(() => {
<button
type="button"
@click="infoOpen = false"
class="oc-icon-button oc-icon-button--small absolute right-3 top-3 rounded-full"
class="image-detail-panel-close oc-icon-button oc-icon-button--small absolute right-3 top-3"
aria-label="隐藏详细信息"
title="隐藏详细信息"
>
✕
<NIcon size="16"><X /></NIcon>
</button>
</div>
@@ -301,7 +301,13 @@ onBeforeUnmount(() => {
transform: translateY(10px) scale(0.97);
}
.image-detail-close-button.image-detail-close-button {
.image-detail-shell.image-detail-shell {
border-radius: 30px !important;
}
.image-detail-close-button.image-detail-close-button,
.image-detail-info-button.image-detail-info-button,
.image-detail-panel-close.image-detail-panel-close {
border-radius: 9999px !important;
}
</style>
+4 -3
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { NButton } from 'naive-ui'
import { NButton, NIcon } from 'naive-ui'
import { X } from '@vicons/tabler'
import { loadAMap } from '@/lib/amap'
const props = withDefaults(defineProps<{
@@ -153,7 +154,7 @@ onBeforeUnmount(() => {
class="fixed inset-0 z-[140] flex items-center justify-center bg-slate-950/55 px-4 py-6"
@click="close"
>
<div class="w-full max-w-3xl overflow-hidden bg-white shadow-2xl" @click.stop>
<div class="oc-modal-shell w-full max-w-3xl overflow-hidden" @click.stop>
<div class="flex items-start justify-between border-b border-slate-200 px-6 py-5">
<div>
<h3 class="text-xl font-semibold text-slate-900">{{ title }}</h3>
@@ -166,7 +167,7 @@ onBeforeUnmount(() => {
title="关闭地图选点窗口"
@click="close"
>
✕
<NIcon size="20"><X /></NIcon>
</button>
</div>
+8 -2
View File
@@ -198,7 +198,7 @@ onBeforeUnmount(() => {
class="fixed inset-0 z-[130] flex items-center justify-center bg-slate-950/55 px-4 py-6 backdrop-blur-sm"
@click="close"
>
<div class="w-full max-w-3xl overflow-hidden border border-slate-200 bg-white shadow-2xl" @click.stop>
<div class="oc-modal-shell w-full max-w-3xl overflow-hidden" @click.stop>
<div class="flex items-start justify-between border-b border-slate-200 px-5 py-4">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-sky-700">Quick Upload</p>
@@ -228,7 +228,13 @@ onBeforeUnmount(() => {
<span class="font-medium text-slate-700">正在上传 {{ currentItemIndex }} / {{ totalItems }}</span>
<span class="font-medium text-sky-700">{{ overallProgress }}%</span>
</div>
<NProgress type="line" :percentage="overallProgress" :show-indicator="false" color="#0ea5e9" rail-color="#dbeafe" />
<NProgress
type="line"
:percentage="overallProgress"
:show-indicator="false"
color="var(--oc-color-info)"
rail-color="var(--oc-color-border)"
/>
</div>
<div
+7 -7
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { NIcon, NSpace } from 'naive-ui'
import { Logout, Settings, Shield, User } from '@vicons/tabler'
import { ChevronDown, Logout, Settings, Shield, User } from '@vicons/tabler'
import { useAuthStore } from '@/stores/auth'
import { RouterLink, useRoute, useRouter } from 'vue-router'
@@ -138,7 +138,7 @@ async function handleLogout() {
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<RouterLink to="/" class="flex items-center gap-3">
<div class="flex h-10 w-10 items-center justify-center border border-sky-200 bg-sky-100 shadow-[4px_4px_0_0_rgba(15,23,42,0.08)]">
<div class="oc-brand-mark">
<svg
class="h-8 w-8"
viewBox="0 0 16 16"
@@ -170,7 +170,7 @@ async function handleLogout() {
</div>
</RouterLink>
<nav class="hidden items-center gap-2 border border-slate-200 bg-white px-2 py-2 shadow-[6px_6px_0_0_rgba(15,23,42,0.05)] md:flex">
<nav class="oc-panel-card hidden items-center gap-2 border border-slate-200 bg-white px-2 py-2 md:flex">
<RouterLink
to="/"
class="oc-nav-link"
@@ -228,7 +228,7 @@ async function handleLogout() {
<button
type="button"
class="oc-header-button oc-header-button--teal max-w-[8.5rem] gap-2 md:max-w-none"
:class="route.name === 'profile' || route.name === 'profile-settings' ? 'border-teal-200 bg-teal-50/80 text-teal-800 ring-1 ring-teal-200 shadow-[4px_4px_0_0_rgba(52,116,93,0.16)]' : ''"
:class="route.name === 'profile' || route.name === 'profile-settings' ? 'border-teal-200 bg-teal-50/80 text-teal-800 ring-1 ring-teal-200' : ''"
aria-haspopup="menu"
:aria-expanded="accountCardOpen"
>
@@ -243,7 +243,7 @@ async function handleLogout() {
class="absolute right-0 top-full z-50 w-72 pt-3"
role="menu"
>
<div class="border border-slate-200 bg-white shadow-[8px_8px_0_0_rgba(15,23,42,0.08)]">
<div class="oc-floating-panel border border-slate-200 bg-white">
<div class="border-b border-slate-200 bg-[linear-gradient(180deg,#f2faf6_0%,#ffffff_100%)] px-4 py-4">
<div class="flex items-center gap-3">
<div class="flex h-11 w-11 shrink-0 items-center justify-center border border-teal-200 bg-[linear-gradient(135deg,#eef9f3_0%,#d9efe3_100%)] text-lg font-bold text-teal-800">
@@ -375,10 +375,10 @@ async function handleLogout() {
>
<button
type="button"
class="pointer-events-auto flex items-center gap-2 border border-slate-300 bg-white/94 px-3 py-1.5 text-xs font-medium tracking-[0.16em] text-slate-700 shadow-[4px_4px_0_0_rgba(15,23,42,0.08)] backdrop-blur transition-colors hover:border-teal-300 hover:bg-teal-50 hover:text-teal-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600"
class="oc-header-button oc-header-button--neutral pointer-events-auto gap-2 bg-white/94 text-xs tracking-[0.16em] backdrop-blur"
@click="showHeader(true)"
>
<span>▼</span>
<NIcon size="15"><ChevronDown /></NIcon>
<span>展开导航</span>
</button>
</div>
@@ -287,7 +287,7 @@ function formatMonthTooltip(monthKey: string) {
</script>
<template>
<div class="oc-panel-card border border-slate-200 bg-white p-6 shadow-sm">
<div class="oc-panel-card border border-slate-200 bg-white p-6">
<div class="flex flex-wrap items-start justify-between gap-6">
<div class="min-w-0 flex-1">
<h3 class="text-2xl font-bold text-slate-900">{{ title }}</h3>
+1
View File
@@ -6,6 +6,7 @@ export function loadAMap() {
version: '2.0',
plugins: [
'AMap.Scale',
'AMap.ToolBar',
'AMap.ControlBar',
'AMap.Geolocation',
'AMap.Marker',
+2 -2
View File
@@ -18,7 +18,7 @@ export const openCloudThemeOverrides: GlobalThemeOverrides = {
borderRadiusSmall: '0px',
fontFamily: '"IBM Plex Sans", "Noto Sans SC", "PingFang SC", sans-serif',
fontFamilyMono: '"IBM Plex Mono", "SFMono-Regular", monospace',
bodyColor: '#f3f7fb',
bodyColor: '#eef4f8',
cardColor: '#ffffff',
modalColor: '#ffffff',
popoverColor: '#ffffff',
@@ -27,7 +27,7 @@ export const openCloudThemeOverrides: GlobalThemeOverrides = {
textColor1: '#0f172a',
textColor2: '#334155',
textColor3: '#64748b',
textColorDisabled: '#94a3b8',
textColorDisabled: 'var(--oc-color-text-disabled)',
dividerColor: '#d9e3ee',
borderColor: '#d9e3ee',
inputColor: '#ffffff',
+32 -1
View File
@@ -71,6 +71,11 @@
@apply mt-4 max-w-2xl text-sm leading-7 text-slate-600;
}
.oc-brand-mark {
@apply flex h-10 w-10 items-center justify-center border border-sky-200 bg-sky-100;
box-shadow: var(--oc-shadow-sm);
}
/* Reusable text hierarchy */
.oc-section-label {
@apply text-xs uppercase tracking-[0.18em] text-slate-500;
@@ -114,6 +119,14 @@
box-shadow: var(--oc-shadow-md) !important;
}
.oc-panel-card-xl {
box-shadow: var(--oc-shadow-xl) !important;
}
.oc-floating-panel {
box-shadow: var(--oc-shadow-lg);
}
.oc-panel-card-soft {
box-shadow: 6px 6px 0 0 rgb(15 23 42 / 4%) !important;
}
@@ -127,6 +140,11 @@
box-shadow: var(--oc-shadow-md);
}
.oc-modal-shell {
@apply border border-slate-200 bg-white;
box-shadow: var(--oc-shadow-xl);
}
.oc-inset-panel {
@apply border border-slate-200 bg-slate-50 p-4;
}
@@ -342,6 +360,19 @@
@apply outline-none ring-2 ring-sky-600 ring-offset-2;
}
.oc-interactive-card {
box-shadow: var(--oc-shadow-md);
}
.oc-interactive-card:hover {
box-shadow: var(--oc-shadow-lg);
}
.oc-interactive-card--selected {
@apply border-sky-500;
box-shadow: var(--oc-shadow-sky) !important;
}
.oc-interactive-row {
transition:
background-color var(--oc-duration-fast) var(--oc-ease-standard),
@@ -422,7 +453,7 @@
.oc-panel-button.n-button.n-button--disabled {
border-color: rgb(226 232 240) !important;
background: rgb(241 245 249) !important;
color: rgb(148 163 184) !important;
color: var(--oc-color-text-disabled) !important;
box-shadow: none !important;
opacity: 0.72;
transform: none !important;
+2
View File
@@ -43,6 +43,7 @@
--oc-color-text: #0f172a;
--oc-color-text-secondary: #334155;
--oc-color-text-muted: #64748b;
--oc-color-text-disabled: #94a3b8;
--oc-color-border: #d9e3ee;
--oc-color-primary: #34745d;
--oc-color-primary-hover: #2d5f4e;
@@ -82,6 +83,7 @@
--oc-shadow-sky: 4px 4px 0 0 rgb(14 165 233 / 14%);
--oc-shadow-primary: 4px 4px 0 0 rgb(52 116 93 / 14%);
--oc-shadow-primary-hover: 5px 5px 0 0 rgb(52 116 93 / 20%);
--oc-shadow-amber: 5px 5px 0 0 rgb(245 158 11 / 12%);
--oc-shadow-teal: var(--oc-shadow-primary);
--oc-duration-fast: 160ms;
+25 -7
View File
@@ -502,7 +502,7 @@ onMounted(loadAdminData)
>
<div class="flex items-center">
<div class="flex min-w-0 items-center gap-3">
<span class="flex h-10 w-10 shrink-0 items-center justify-center border border-sky-200 bg-sky-50 text-sky-800 shadow-[3px_3px_0_0_rgba(14,165,233,0.12)]">
<span class="admin-brand-icon flex h-10 w-10 shrink-0 items-center justify-center border border-sky-200 bg-sky-50 text-sky-800">
<NIcon size="22"><Shield /></NIcon>
</span>
<span
@@ -538,7 +538,7 @@ onMounted(loadAdminData)
type="button"
class="group relative flex min-w-[150px] items-center gap-3 overflow-hidden border px-3 py-3 text-left transition-[background-color,border-color,color,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600 focus-visible:ring-offset-2 lg:w-full lg:min-w-0"
:class="activeTab === tab.key
? 'border-teal-200 bg-teal-50 text-teal-900 shadow-[3px_3px_0_0_rgba(52,116,93,0.16)]'
? 'admin-control-active border-teal-200 bg-teal-50 text-teal-900'
: 'border-transparent bg-white text-slate-600 hover:border-slate-200 hover:bg-slate-50 hover:text-slate-950'"
:aria-current="activeTab === tab.key ? 'page' : undefined"
:aria-label="tab.label"
@@ -580,7 +580,7 @@ onMounted(loadAdminData)
type="button"
class="flex h-14 min-w-0 flex-1 items-center gap-3 overflow-hidden border px-3 py-2 text-left transition-[background-color,border-color,color,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600 lg:w-full"
:class="accountMenuOpen
? 'border-teal-200 bg-teal-50 text-teal-800 shadow-[3px_3px_0_0_rgba(52,116,93,0.16)]'
? 'admin-control-active border-teal-200 bg-teal-50 text-teal-800'
: 'border-slate-200 bg-slate-50 text-slate-700 hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800'"
aria-label="打开管理员账户菜单"
aria-haspopup="menu"
@@ -611,7 +611,7 @@ onMounted(loadAdminData)
</template>
<div
class="w-64 border border-slate-200 bg-white shadow-[8px_8px_0_0_rgba(15,23,42,0.08)]"
class="oc-floating-panel w-64 border border-slate-200 bg-white"
role="menu"
aria-label="管理员账户"
>
@@ -663,7 +663,7 @@ onMounted(loadAdminData)
</NPopover>
<RouterLink
to="/"
class="flex h-[46px] shrink-0 items-center justify-start gap-3 overflow-hidden border border-slate-200 bg-white px-3 text-sm font-semibold text-slate-700 shadow-[3px_3px_0_0_rgba(15,23,42,0.06)] transition-colors hover:border-sky-200 hover:bg-sky-50 hover:text-sky-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-600 lg:w-full"
class="oc-panel-button oc-panel-button--neutral flex h-[46px] shrink-0 items-center justify-start gap-3 overflow-hidden px-3 font-semibold focus-visible:ring-2 focus-visible:ring-sky-600 lg:w-full"
title="返回主页"
>
<NIcon size="20" class="shrink-0"><Home /></NIcon>
@@ -728,8 +728,8 @@ onMounted(loadAdminData)
<div
class="flex flex-col justify-between gap-5 border p-6 sm:flex-row sm:items-center"
:class="dashboardStats.pending
? 'border-amber-200 bg-[linear-gradient(135deg,#fffbeb_0%,#fff_52%,#f2faf6_100%)] shadow-[5px_5px_0_0_rgba(245,158,11,0.12)]'
: 'border-teal-200 bg-[linear-gradient(135deg,#f2faf6_0%,#fff_55%,#eff6ff_100%)] shadow-[5px_5px_0_0_rgba(52,116,93,0.16)]'"
? 'admin-priority-card--pending border-amber-200 bg-[linear-gradient(135deg,#fffbeb_0%,#fff_52%,#f2faf6_100%)]'
: 'admin-priority-card--clear border-teal-200 bg-[linear-gradient(135deg,#f2faf6_0%,#fff_55%,#eff6ff_100%)]'"
>
<div class="flex items-center gap-5">
<div
@@ -1341,3 +1341,21 @@ onMounted(loadAdminData)
</ImageDetailModal>
</div>
</template>
<style scoped>
.admin-brand-icon {
box-shadow: var(--oc-shadow-sky);
}
.admin-control-active {
box-shadow: var(--oc-shadow-primary);
}
.admin-priority-card--pending {
box-shadow: var(--oc-shadow-amber);
}
.admin-priority-card--clear {
box-shadow: var(--oc-shadow-primary-hover);
}
</style>
+1 -1
View File
@@ -53,7 +53,7 @@ onUnmounted(() => {
<template>
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
<div class="mx-auto max-w-3xl">
<NCard class="shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
<NCard class="oc-panel-card-xl">
<template v-if="state === 'success'">
<NResult status="success" title="邮箱认证成功" description="你的邮箱已确认,现在可以登录了。">
<template #footer>
+3 -3
View File
@@ -74,7 +74,7 @@ onUnmounted(() => {
<template>
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
<div class="mx-auto grid min-w-0 max-w-6xl gap-8 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
<section class="min-w-0 overflow-hidden border border-slate-200 bg-[linear-gradient(135deg,#eff6ff_0%,#ffffff_52%,#f2faf6_100%)] p-6 shadow-[10px_10px_0_0_rgba(15,23,42,0.08)] sm:p-8">
<section class="oc-panel-card-xl min-w-0 overflow-hidden border border-slate-200 bg-[linear-gradient(135deg,#eff6ff_0%,#ffffff_52%,#f2faf6_100%)] p-6 sm:p-8">
<p class="text-sm uppercase tracking-[0.26em] text-sky-700">Password Recovery</p>
<h1 class="mt-4 max-w-xl break-words text-4xl font-black leading-[1.05] text-slate-900 sm:text-5xl">
重新拿回你的
@@ -97,7 +97,7 @@ onUnmounted(() => {
</div>
</section>
<NCard class="min-w-0 shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
<NCard class="oc-panel-card-xl min-w-0">
<NResult
v-if="submitted"
status="success"
@@ -121,7 +121,7 @@ onUnmounted(() => {
{{ resendButtonLabel }}
</NButton>
<RouterLink :to="{ path: '/login', query: { email } }" class="no-underline">
<NButton type="default" class="oc-panel-button oc-panel-button--neutral">返回登录</NButton>
<NButton type="default" secondary strong class="oc-panel-button oc-panel-button--neutral">返回登录</NButton>
</RouterLink>
</div>
</div>
+2 -2
View File
@@ -43,7 +43,7 @@ async function handleLogin() {
<template>
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
<div class="mx-auto grid min-w-0 max-w-6xl gap-8 lg:grid-cols-[1.1fr_0.9fr] lg:items-stretch">
<section class="flex min-w-0 h-full flex-col justify-between overflow-hidden border border-slate-200 bg-[linear-gradient(135deg,#f2faf6_0%,#ffffff_48%,#eff6ff_100%)] p-6 shadow-[10px_10px_0_0_rgba(15,23,42,0.08)] sm:p-8">
<section class="oc-panel-card-xl flex min-w-0 h-full flex-col justify-between overflow-hidden border border-slate-200 bg-[linear-gradient(135deg,#f2faf6_0%,#ffffff_48%,#eff6ff_100%)] p-6 sm:p-8">
<div>
<p class="text-sm uppercase tracking-[0.26em] text-teal-700">Sky Log In</p>
<h1 class="mt-4 max-w-xl break-words text-4xl font-black leading-[1.05] text-slate-900 sm:text-5xl">
@@ -68,7 +68,7 @@ async function handleLogin() {
</div>
</section>
<NCard class="min-w-0 h-full shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
<NCard class="oc-panel-card-xl min-w-0 h-full">
<div class="mb-8">
<div class="text-sm uppercase tracking-[0.22em] text-slate-500">Member Access</div>
<h2 class="mt-3 text-3xl font-bold text-slate-900">登录 OpenCloud</h2>
+2 -2
View File
@@ -48,7 +48,7 @@ async function handleRegister() {
<template>
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
<div class="mx-auto grid min-w-0 max-w-6xl gap-8 lg:grid-cols-[1.1fr_0.9fr] lg:items-stretch">
<section class="flex min-w-0 h-full flex-col justify-between overflow-hidden border border-slate-200 bg-[linear-gradient(135deg,#eff6ff_0%,#ffffff_42%,#f2faf6_100%)] p-6 shadow-[10px_10px_0_0_rgba(15,23,42,0.08)] sm:p-8">
<section class="oc-panel-card-xl flex min-w-0 h-full flex-col justify-between overflow-hidden border border-slate-200 bg-[linear-gradient(135deg,#eff6ff_0%,#ffffff_42%,#f2faf6_100%)] p-6 sm:p-8">
<div>
<p class="text-sm uppercase tracking-[0.26em] text-sky-700">Observer Join</p>
<h1 class="mt-4 max-w-xl break-words text-4xl font-black leading-[1.05] text-slate-900 sm:text-5xl">
@@ -73,7 +73,7 @@ async function handleRegister() {
</div>
</section>
<NCard class="min-w-0 h-full shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
<NCard class="oc-panel-card-xl min-w-0 h-full">
<NResult
v-if="emailSent"
status="success"
+1 -1
View File
@@ -105,7 +105,7 @@ onUnmounted(() => {
<template>
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
<div class="mx-auto max-w-2xl">
<NCard class="shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
<NCard class="oc-panel-card-xl">
<NResult
v-if="state === 'success'"
status="success"
+1 -1
View File
@@ -3,7 +3,7 @@
<template>
<div class="min-h-[calc(100vh-4rem)] bg-[linear-gradient(180deg,#f2faf6_0%,#f8fafc_100%)] px-4 py-16">
<section class="mx-auto max-w-4xl border border-slate-200 bg-white p-10 shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
<section class="oc-panel-card-xl mx-auto max-w-4xl border border-slate-200 bg-white p-10">
<p class="text-sm font-semibold uppercase tracking-[0.24em] text-teal-700">Community</p>
<h1 class="mt-4 text-4xl font-bold text-slate-900">社区</h1>
<p class="mt-5 max-w-2xl text-sm leading-7 text-slate-600">
+8 -8
View File
@@ -147,7 +147,7 @@ watch(() => route.params.id, () => {
<div v-if="loading" class="space-y-6">
<NSkeleton class="h-80 w-full" />
<div class="grid gap-4 lg:grid-cols-3">
<NCard v-for="n in 3" :key="n">
<NCard v-for="n in 3" :key="n" class="oc-panel-card border border-slate-200">
<NSkeleton class="h-4 w-1/3" />
<NSkeleton class="mt-4 h-7 w-1/2" />
</NCard>
@@ -165,7 +165,7 @@ watch(() => route.params.id, () => {
</NAlert>
<template v-else-if="cloudType">
<section class="overflow-hidden border border-slate-200 bg-white shadow-sm">
<section class="oc-panel-card overflow-hidden border border-slate-200 bg-white">
<div class="grid lg:grid-cols-[1.15fr_0.85fr]">
<div class="relative min-h-[360px] overflow-hidden">
<img
@@ -197,7 +197,7 @@ watch(() => route.params.id, () => {
</p>
</div>
<NCard class="mt-8 border border-slate-200">
<div class="oc-inset-panel mt-8">
<p class="text-sm text-slate-500">你的收集状态</p>
<template v-if="isUnlocked && collectionEntry">
<p class="oc-stat-value">已解锁</p>
@@ -207,21 +207,21 @@ watch(() => route.params.id, () => {
<p class="oc-stat-value">尚未解锁</p>
<p class="mt-2 text-sm text-slate-600">拍到并上传这种云朵后,这枚徽章就会被点亮。</p>
</template>
</NCard>
</div>
</div>
</div>
</section>
<section class="mt-6 grid gap-4 md:grid-cols-3">
<NCard class="border border-slate-200 shadow-sm">
<NCard class="oc-panel-card border border-slate-200">
<p class="text-sm text-slate-500">稀有度</p>
<p class="oc-stat-value">{{ rarityMeta[cloudType.rarity].label }}</p>
</NCard>
<NCard class="border border-slate-200 shadow-sm">
<NCard class="oc-panel-card border border-slate-200">
<p class="text-sm text-slate-500">公开云图</p>
<p class="oc-stat-value">{{ publicCount }}</p>
</NCard>
<NCard class="border border-slate-200 shadow-sm">
<NCard class="oc-panel-card border border-slate-200">
<p class="text-sm text-slate-500">首次解锁</p>
<p class="mt-2 text-xl font-bold text-slate-900">
{{ collectionEntry ? formatDate(collectionEntry.unlocked_at) : '等待记录' }}
@@ -242,7 +242,7 @@ watch(() => route.params.id, () => {
v-for="item in gallery"
:key="item.id"
type="button"
class="oc-interactive-card overflow-hidden border border-slate-200 bg-white text-left shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-lg"
class="oc-interactive-card overflow-hidden border border-slate-200 bg-white text-left transition-transform duration-300 hover:-translate-y-1"
@click="openGalleryDetail(item)"
>
<img :src="item.thumbnail_url || item.image_url" :alt="cloudType.name" class="h-56 w-full object-cover" />
+10 -2
View File
@@ -133,7 +133,7 @@ onMounted(async () => {
</div>
<div v-if="!isUnlocked(cloudType.id)" class="absolute inset-0 flex items-center justify-center bg-slate-950/12">
<span class="inline-flex items-center gap-2 border border-slate-300 bg-white/92 px-3 py-2 text-xs font-semibold text-slate-700 shadow-[3px_3px_0_0_rgba(15,23,42,0.10)] backdrop-blur">
<span class="encyclopedia-lock-badge inline-flex items-center gap-2 border border-slate-300 bg-white/92 px-3 py-2 text-xs font-semibold text-slate-700 backdrop-blur">
<NIcon size="15"><Lock /></NIcon>
尚未收录
</span>
@@ -176,7 +176,7 @@ onMounted(async () => {
<div
v-if="lockHint"
class="fixed bottom-6 left-1/2 z-50 -translate-x-1/2 border border-slate-900 bg-slate-900 px-4 py-2 text-sm text-white shadow-lg"
class="encyclopedia-lock-hint fixed bottom-6 left-1/2 z-50 -translate-x-1/2 border border-slate-900 bg-slate-900 px-4 py-2 text-sm text-white"
>
{{ lockHint }}
</div>
@@ -184,6 +184,14 @@ onMounted(async () => {
</template>
<style scoped>
.encyclopedia-lock-badge {
box-shadow: var(--oc-shadow-xs);
}
.encyclopedia-lock-hint {
box-shadow: var(--oc-shadow-lg);
}
.encyclopedia-type-card:hover {
border-color: rgb(125 211 252);
box-shadow: var(--oc-shadow-lg) !important;
+2 -2
View File
@@ -353,7 +353,7 @@ onUnmounted(() => {
<div class="oc-container oc-container--content oc-page-content">
<section>
<div class="mb-4 border border-slate-200 bg-white p-3 shadow-[6px_6px_0_0_rgba(15,23,42,0.05)]">
<div class="oc-panel-card mb-4 border border-slate-200 bg-white p-3">
<div class="flex items-center gap-3">
<NIcon size="20" class="shrink-0 text-slate-400">
<Search />
@@ -430,7 +430,7 @@ onUnmounted(() => {
:key="cloud.id"
type="button"
@click="openDetail(cloud)"
class="oc-interactive-card group relative mb-3 block w-full break-inside-avoid overflow-hidden border border-slate-200 bg-slate-200 text-left shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-lg"
class="oc-interactive-card group relative mb-3 block w-full break-inside-avoid overflow-hidden border border-slate-200 bg-slate-200 text-left transition-transform duration-300 hover:-translate-y-1"
>
<img
:src="cloud.thumbnail_url || cloud.image_url"
+12 -12
View File
@@ -68,8 +68,8 @@ let zoomHoldStarted = false
const rarityColors: Record<string, string> = {
common: '#cbd5e1',
uncommon: '#60a5fa',
rare: '#c084fc',
uncommon: '#f59e0b',
rare: '#f43f5e',
}
const rarityMeta = {
@@ -218,15 +218,15 @@ function hoverCardContent(cloud: CloudMarkerData): HTMLElement {
const name = document.createElement('span')
name.textContent = cloud.cloudTypeName
name.style.cssText = 'font-size:13px;font-weight:600;color:#1f2937'
name.style.cssText = 'font-size:13px;font-weight:600;color:#1e293b'
const rarity = document.createElement('span')
rarity.textContent = label
rarity.style.cssText = `font-size:10px;padding:1px 5px;border-radius:3px;color:${border};background:${border}22`
const meta = document.createElement('div')
meta.textContent = `📷 ${cloud.username} · ${timeAgo(cloud.capturedAt)}`
meta.style.cssText = 'font-size:11px;color:#9ca3af;margin-top:3px'
meta.textContent = `${cloud.username} · ${timeAgo(cloud.capturedAt)}`
meta.style.cssText = 'font-size:11px;color:#94a3b8;margin-top:3px'
title.append(name, rarity)
body.append(title, meta)
@@ -727,7 +727,7 @@ onUnmounted(() => {
<div class="relative w-[min(42vw,420px)] min-w-[220px]">
<div
v-if="sliderDragging"
class="absolute bottom-8 border border-slate-200 bg-white/95 px-3 py-1.5 text-xs font-medium text-slate-800 shadow-md backdrop-blur"
class="oc-panel-card absolute bottom-8 border border-slate-200 bg-white/95 px-3 py-1.5 text-xs font-medium text-slate-800 backdrop-blur"
:style="{ left: `${selectedMinutePercent}%`, transform: 'translateX(-50%)' }"
>
{{ selectedTimeLabel }}
@@ -772,7 +772,7 @@ onUnmounted(() => {
<div
v-if="archivePanelOpen"
class="absolute bottom-16 right-0 w-72 border border-slate-200 bg-white p-4 shadow-[8px_8px_0_0_rgba(15,23,42,0.08)]"
class="oc-panel-card absolute bottom-16 right-0 w-72 border border-slate-200 bg-white p-4"
>
<div class="flex items-center justify-between gap-2">
<p class="text-sm font-semibold text-slate-900">历史位置</p>
@@ -841,19 +841,19 @@ onUnmounted(() => {
@close="previewCloud = null"
>
<div class="grid gap-4 sm:grid-cols-2">
<div class="rounded-2xl bg-slate-50 p-4">
<div class="oc-inset-panel">
<p class="oc-section-label">上传时间</p>
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(previewCloud.createdAt) }}</p>
</div>
<div class="rounded-2xl bg-slate-50 p-4">
<div class="oc-inset-panel">
<p class="oc-section-label">拍摄时间</p>
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(previewCloud.capturedAt) }}</p>
</div>
<div class="rounded-2xl bg-slate-50 p-4">
<div class="oc-inset-panel">
<p class="oc-section-label">位置</p>
<p class="mt-2 text-sm font-medium text-slate-900">{{ previewCloud.locationName || '未填写位置名称' }}</p>
</div>
<div class="rounded-2xl bg-slate-50 p-4">
<div class="oc-inset-panel">
<p class="oc-section-label">模糊化经纬度</p>
<p class="mt-2 text-sm font-medium text-slate-900">
纬度 {{ formatCoordinate(previewCloud.latitude) }} / 经度 {{ formatCoordinate(previewCloud.longitude) }}
@@ -861,7 +861,7 @@ onUnmounted(() => {
</div>
</div>
<div class="mt-5 rounded-[28px] border border-slate-200 bg-white p-5">
<div class="mt-5 border border-slate-200 bg-white p-5">
<p class="oc-section-label">图片说明</p>
<p class="mt-3 text-sm leading-7 text-slate-700">
{{ previewCloud.description || '上传者没有留下额外说明。' }}
+2 -2
View File
@@ -111,7 +111,7 @@ async function savePassword() {
{{ success }}
</NAlert>
<NCard class="oc-panel-card border border-slate-200 bg-white shadow-sm" :bordered="false">
<NCard class="oc-panel-card border border-slate-200 bg-white" :bordered="false">
<div class="grid gap-5 lg:grid-cols-[0.8fr_1.2fr] lg:items-start">
<div>
<h2 class="text-2xl font-bold text-slate-900">公开昵称</h2>
@@ -140,7 +140,7 @@ async function savePassword() {
</div>
</NCard>
<NCard class="oc-panel-card border border-slate-200 bg-white shadow-sm" :bordered="false">
<NCard class="oc-panel-card border border-slate-200 bg-white" :bordered="false">
<div class="grid gap-5 lg:grid-cols-[0.8fr_1.2fr] lg:items-start">
<div>
<h2 class="text-2xl font-bold text-slate-900">登录密码</h2>
+21 -15
View File
@@ -429,19 +429,19 @@ watch(selectedUploadDate, async newValue => {
v-if="loading"
class="oc-page-hero__split"
>
<NCard class="oc-panel-card h-full border border-slate-200 shadow-sm" :bordered="false">
<NCard class="oc-panel-card h-full border border-slate-200" :bordered="false">
<NSkeleton class="h-6 w-1/3" />
<NSkeleton class="mt-4 h-10 w-2/3" />
<NSkeleton class="mt-5 h-4 w-full" :repeat="2" />
</NCard>
<div v-if="isOwnProfile" class="grid grid-cols-2 gap-3 min-[900px]:h-full min-[900px]:grid-rows-2">
<NCard v-for="n in 4" :key="n" class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
<NCard v-for="n in 4" :key="n" class="oc-panel-card-soft h-full border border-white/80 bg-white/85 backdrop-blur" :bordered="false">
<NSkeleton class="h-3 w-2/3" />
<NSkeleton class="mt-3 h-7 w-1/2" />
</NCard>
</div>
<NCard v-else class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
<NCard v-else class="oc-panel-card-soft h-full border border-white/80 bg-white/85 backdrop-blur" :bordered="false">
<NSkeleton class="h-5 w-1/3" />
<NSkeleton class="mt-4 h-8 w-1/2" />
<NSkeleton class="mt-5 h-3 w-full" />
@@ -452,10 +452,10 @@ watch(selectedUploadDate, async newValue => {
v-else
class="oc-page-hero__split oc-page-hero__split--stretch"
>
<NCard class="oc-panel-card h-full border border-slate-200 bg-white/90 shadow-sm backdrop-blur" :bordered="false">
<NCard class="oc-panel-card h-full border border-slate-200 bg-white/90 backdrop-blur" :bordered="false">
<div class="flex gap-5">
<div
class="flex h-20 w-20 shrink-0 items-center justify-center border border-slate-300 bg-[linear-gradient(135deg,#eef9f3_0%,#d9efe3_100%)] text-3xl font-bold text-slate-900 shadow-[4px_4px_0_0_rgba(15,23,42,0.08)]"
class="profile-avatar flex h-20 w-20 shrink-0 items-center justify-center border border-slate-300 bg-[linear-gradient(135deg,#eef9f3_0%,#d9efe3_100%)] text-3xl font-bold text-slate-900"
>
<img
v-if="profileData?.avatar_url"
@@ -498,25 +498,25 @@ watch(selectedUploadDate, async newValue => {
</NCard>
<div v-if="isOwnProfile" class="grid grid-cols-2 gap-3 min-[900px]:h-full min-[900px]:grid-rows-2">
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 backdrop-blur" :bordered="false">
<p class="oc-stat-label">总拍摄数</p>
<p class="oc-stat-value">{{ totalShots }}</p>
</NCard>
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 backdrop-blur" :bordered="false">
<p class="oc-stat-label">最常拍云型</p>
<p class="mt-2 truncate text-xl font-bold text-slate-900">{{ mostCommonCloudType.name }}</p>
</NCard>
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 backdrop-blur" :bordered="false">
<p class="oc-stat-label">拍摄天数</p>
<p class="oc-stat-value">{{ shootingDays }}</p>
</NCard>
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 backdrop-blur" :bordered="false">
<p class="oc-stat-label">被点赞数</p>
<p class="oc-stat-value">{{ receivedLikes }}</p>
</NCard>
</div>
<NCard v-if="!isOwnProfile" class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
<NCard v-if="!isOwnProfile" class="oc-panel-card-soft h-full border border-white/80 bg-white/85 backdrop-blur" :bordered="false">
<p class="text-sm text-slate-500">公开贡献</p>
<p class="mt-3 text-3xl font-bold text-slate-900">{{ approvedShots }}</p>
<p class="mt-2 text-sm text-slate-500">当前公开可见的云图数量</p>
@@ -604,7 +604,7 @@ watch(selectedUploadDate, async newValue => {
<RouterLink
v-if="isOwnProfile"
to="/upload"
class="oc-panel-button oc-panel-button--sky border-0"
class="oc-panel-button oc-panel-button--sky"
>
继续上传
</RouterLink>
@@ -623,7 +623,7 @@ watch(selectedUploadDate, async newValue => {
<div v-for="n in 2" :key="n">
<NSkeleton class="h-6 w-40" />
<div class="mt-4 grid gap-5 md:grid-cols-2 xl:grid-cols-3">
<NCard v-for="m in 3" :key="m" class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
<NCard v-for="m in 3" :key="m" class="oc-panel-card border border-slate-200" :bordered="false">
<NSkeleton class="h-52 w-full" />
<NSkeleton class="mt-4 h-5 w-2/3" />
<NSkeleton class="mt-3 h-4 w-1/2" />
@@ -645,8 +645,8 @@ watch(selectedUploadDate, async newValue => {
:key="item.id"
type="button"
@click="selectedCloud = item"
class="oc-interactive-card group overflow-hidden border bg-white text-left shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg"
:class="selectedUploadDate && toDayKey(item.created_at) === selectedUploadDate ? 'border-sky-500 shadow-sky-100' : 'border-slate-200'"
class="oc-interactive-card group overflow-hidden border bg-white text-left transition-all hover:-translate-y-1"
:class="selectedUploadDate && toDayKey(item.created_at) === selectedUploadDate ? 'oc-interactive-card--selected' : 'border-slate-200'"
>
<div class="relative">
<img
@@ -712,7 +712,7 @@ watch(selectedUploadDate, async newValue => {
<RouterLink
v-if="isOwnProfile && !selectedUploadDate"
to="/upload"
class="oc-panel-button oc-panel-button--sky border-0"
class="oc-panel-button oc-panel-button--sky"
>
去上传第一张
</RouterLink>
@@ -815,3 +815,9 @@ watch(selectedUploadDate, async newValue => {
/>
</div>
</template>
<style scoped>
.profile-avatar {
box-shadow: var(--oc-shadow-sm);
}
</style>
+3 -2
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { NButton } from 'naive-ui'
import { NButton, NIcon } from 'naive-ui'
import { Lock } from '@vicons/tabler'
import { RouterLink, useRoute } from 'vue-router'
const route = useRoute()
@@ -30,7 +31,7 @@ const description = computed(() => {
<template>
<div class="oc-system-page">
<div class="oc-system-card">
<div class="text-7xl leading-none">🔐</div>
<NIcon size="72" class="text-sky-600"><Lock /></NIcon>
<p class="mt-6 text-sm font-medium uppercase tracking-[0.24em] text-sky-600">401 Login Required</p>
<h1 class="oc-page-title">{{ title }}</h1>
<p class="oc-page-description mx-auto max-w-xl">
+3 -2
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
import { NButton } from 'naive-ui'
import { NButton, NIcon } from 'naive-ui'
import { HandStop } from '@vicons/tabler'
import { RouterLink } from 'vue-router'
</script>
<template>
<div class="oc-system-page">
<div class="oc-system-card">
<div class="text-7xl leading-none">🖐️</div>
<NIcon size="72" class="text-rose-600"><HandStop /></NIcon>
<p class="mt-6 text-sm font-medium uppercase tracking-[0.24em] text-rose-600">403 Forbidden</p>
<h1 class="oc-page-title">这里不让进</h1>
<p class="oc-page-description mx-auto max-w-xl">
+3 -2
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
import { NButton } from 'naive-ui'
import { NButton, NIcon } from 'naive-ui'
import { Help } from '@vicons/tabler'
import { RouterLink } from 'vue-router'
</script>
<template>
<div class="oc-system-page">
<div class="oc-system-card">
<div class="text-7xl leading-none">🤔</div>
<NIcon size="72" class="text-amber-600"><Help /></NIcon>
<p class="mt-6 text-sm font-medium uppercase tracking-[0.24em] text-amber-600">404 Not Found</p>
<h1 class="oc-page-title">我也没想明白</h1>
<p class="oc-page-description mx-auto max-w-xl">
+19 -16
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { NAlert, NButton, NCard, NProgress, NResult, NTag } from 'naive-ui'
import { NAlert, NButton, NCard, NIcon, NProgress, NResult, NTag } from 'naive-ui'
import { CloudUpload, Map as MapIcon, X } from '@vicons/tabler'
import { useRouter } from 'vue-router'
import { useCloudsStore } from '@/stores/clouds'
import { useAuthStore } from '@/stores/auth'
@@ -224,9 +225,9 @@ onUnmounted(() => {
<NCard
v-for="badge in unlockedBadges"
:key="badge.cloudTypeId"
class="border border-amber-200 bg-[linear-gradient(180deg,#fffbeb_0%,#ffffff_100%)] shadow-sm"
class="oc-panel-card border border-amber-200 bg-[linear-gradient(180deg,#fffbeb_0%,#ffffff_100%)]"
>
<div class="flex h-16 w-16 items-center justify-center border border-amber-300 bg-amber-400 text-3xl text-white shadow-sm">
<div class="flex h-16 w-16 items-center justify-center border border-amber-300 bg-amber-400 text-3xl text-white">
{{ badge.cloudName.slice(0, 1) }}
</div>
<div class="mt-5">
@@ -256,7 +257,7 @@ onUnmounted(() => {
</div>
<template v-else>
<div v-if="uploading" class="mb-6 border border-slate-200 bg-white p-5 shadow-sm">
<div v-if="uploading" class="oc-panel-card mb-6 border border-slate-200 bg-white p-5">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-slate-700">正在上传 {{ currentItemIndex }} / {{ totalItems }}...</span>
<span class="text-sm font-medium text-sky-600">{{ overallProgress }}%</span>
@@ -265,8 +266,8 @@ onUnmounted(() => {
type="line"
:show-indicator="false"
:percentage="overallProgress"
color="#0ea5e9"
rail-color="#dbe4ee"
color="var(--oc-color-info)"
rail-color="var(--oc-color-border)"
:height="10"
/>
</div>
@@ -277,19 +278,19 @@ onUnmounted(() => {
@dragleave.prevent="dragOver = false"
@drop.prevent="handleDrop"
@click="fileInput?.click()"
class="flex flex-col items-center justify-center border-2 border-dashed py-20 cursor-pointer transition-colors bg-white shadow-sm"
class="oc-panel-card flex cursor-pointer flex-col items-center justify-center border-2 border-dashed bg-white py-20 transition-colors"
:class="dragOver ? 'border-sky-400 bg-sky-50' : 'border-slate-300 hover:border-sky-400 hover:bg-slate-50'"
>
<span class="text-5xl mb-4">☁️</span>
<NIcon size="48" class="mb-4 text-sky-600"><CloudUpload /></NIcon>
<p class="mb-1 text-lg font-medium text-slate-700">点击或拖拽图片到此处</p>
<p class="text-sm text-slate-400">支持 JPG、PNG 格式,可一次选择多张</p>
</div>
<template v-else>
<div class="flex gap-6">
<div class="flex-shrink-0 w-[480px]">
<div class="flex min-w-0 flex-col gap-6 lg:flex-row">
<div class="w-full min-w-0 lg:w-[480px] lg:flex-shrink-0">
<div v-if="activeItem" class="mb-3 overflow-hidden border border-slate-900 bg-slate-900">
<img :src="activeItem.preview" alt="预览" class="w-full h-[360px] object-contain" />
<img :src="activeItem.preview" alt="预览" class="h-64 w-full object-contain sm:h-[360px]" />
</div>
<div class="flex gap-2 overflow-x-auto pb-2">
<div
@@ -307,7 +308,9 @@ onUnmounted(() => {
class="oc-icon-button oc-icon-button--danger oc-icon-button--tiny absolute -right-1 -top-1"
title="移除图片"
aria-label="移除图片"
>✕</button>
>
<NIcon size="12"><X /></NIcon>
</button>
</div>
<div
@click="fileInput?.click()"
@@ -319,7 +322,7 @@ onUnmounted(() => {
</div>
<div class="flex-1 min-w-0" v-if="activeItem">
<NCard class="border border-slate-200 shadow-sm">
<NCard class="oc-panel-card border border-slate-200">
<div class="space-y-5">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-slate-900">图片信息</h2>
@@ -412,7 +415,7 @@ onUnmounted(() => {
aria-label="地图选点"
class="oc-icon-button oc-icon-button--sky"
>
<span class="text-lg leading-none">🗺️</span>
<NIcon size="20"><MapIcon /></NIcon>
</button>
</div>
</div>
@@ -462,9 +465,9 @@ onUnmounted(() => {
</div>
</div>
<div class="mt-6 flex items-center justify-between">
<div class="mt-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p class="text-sm text-slate-500">共 {{ items.length }} 张图片</p>
<div class="flex gap-3">
<div class="flex flex-wrap gap-3">
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="clearAll()" :disabled="uploading">清空</NButton>
<NButton type="default" secondary strong class="oc-panel-button oc-panel-button--sky" @click="handleSubmit" :disabled="uploading">
{{ uploading ? '上传中...' : '提交上传' }}