@@ -123,7 +123,9 @@ watch(() => props.initialValue, value => {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
||||
class="oc-icon-button"
|
||||
aria-label="关闭编辑窗口"
|
||||
title="关闭编辑窗口"
|
||||
@click="close"
|
||||
>
|
||||
✕
|
||||
@@ -191,7 +193,7 @@ watch(() => props.initialValue, value => {
|
||||
<button
|
||||
v-if="hasCoordinates"
|
||||
type="button"
|
||||
class="text-xs font-medium text-slate-400 transition-colors hover:text-slate-700"
|
||||
class="oc-text-button oc-text-button--muted text-xs"
|
||||
@click="clearCoordinates"
|
||||
>
|
||||
清空
|
||||
@@ -216,7 +218,7 @@ watch(() => props.initialValue, value => {
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center border border-sky-200 bg-sky-50 text-sky-700 transition-colors hover:bg-sky-100"
|
||||
class="oc-icon-button oc-icon-button--sky"
|
||||
title="地图选点"
|
||||
aria-label="地图选点"
|
||||
@click="mapPickerOpen = true"
|
||||
|
||||
@@ -187,7 +187,7 @@ onBeforeUnmount(() => {
|
||||
<button
|
||||
type="button"
|
||||
@click="close"
|
||||
class="image-detail-close-button absolute right-4 top-4 z-30 flex h-10 min-h-10 w-10 min-w-10 max-w-10 appearance-none items-center justify-center overflow-hidden border-0 bg-transparent p-0 text-white/70 shadow-none transition-colors hover:text-white [&_svg]:stroke-[2.5]"
|
||||
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]"
|
||||
aria-label="关闭大图"
|
||||
title="关闭大图"
|
||||
>
|
||||
@@ -223,7 +223,7 @@ onBeforeUnmount(() => {
|
||||
<button
|
||||
type="button"
|
||||
@click="toggleInfo"
|
||||
class="absolute bottom-4 right-4 z-30 flex h-11 w-11 items-center justify-center rounded-full bg-transparent text-white drop-shadow-[0_2px_8px_rgba(15,23,42,0.9)] transition-colors hover:text-sky-100"
|
||||
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)]"
|
||||
:aria-label="infoOpen ? '隐藏详细信息' : '查看详细信息'"
|
||||
:title="infoOpen ? '隐藏详细信息' : '查看详细信息'"
|
||||
>
|
||||
@@ -254,7 +254,7 @@ onBeforeUnmount(() => {
|
||||
<button
|
||||
type="button"
|
||||
@click="infoOpen = false"
|
||||
class="absolute right-3 top-3 rounded-full p-2 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
||||
class="oc-icon-button oc-icon-button--small absolute right-3 top-3 rounded-full"
|
||||
aria-label="隐藏详细信息"
|
||||
title="隐藏详细信息"
|
||||
>
|
||||
|
||||
@@ -158,7 +158,9 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
||||
class="oc-icon-button"
|
||||
aria-label="关闭地图选点窗口"
|
||||
title="关闭地图选点窗口"
|
||||
@click="close"
|
||||
>
|
||||
✕
|
||||
|
||||
@@ -77,7 +77,7 @@ async function initializeMap() {
|
||||
marker = new amapLib.Marker({
|
||||
position: center.value,
|
||||
title: props.locationName || '拍摄位置',
|
||||
content: '<div class="h-4 w-4 rounded-full border-2 border-white bg-teal-500 shadow-[0_0_0_4px_rgba(20,184,166,0.22)]"></div>',
|
||||
content: '<div class="h-4 w-4 rounded-full border-2 border-white bg-teal-500 shadow-[0_0_0_4px_rgba(52,116,93,0.24)]"></div>',
|
||||
offset: new amapLib.Pixel(-8, -8),
|
||||
} as AMap.MarkerOptions)
|
||||
marker.setMap(mapInstance)
|
||||
|
||||
@@ -207,7 +207,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="quick-upload-close flex h-9 w-9 items-center justify-center border border-slate-200 bg-slate-50 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-800"
|
||||
class="quick-upload-close oc-icon-button"
|
||||
aria-label="关闭快捷上传"
|
||||
title="关闭快捷上传"
|
||||
@click="close"
|
||||
@@ -252,8 +252,8 @@ onBeforeUnmount(() => {
|
||||
<img :src="activeItem.preview" alt="图片预览" class="h-56 w-full object-contain" />
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between gap-2">
|
||||
<button type="button" class="text-sm font-medium text-sky-700 hover:text-sky-900" @click="fileInput?.click()">重新选择</button>
|
||||
<button type="button" class="text-sm font-medium text-rose-600 hover:text-rose-800" @click="handleRemove(activeItem.id)">移除</button>
|
||||
<button type="button" class="oc-text-button text-sm" @click="fileInput?.click()">重新选择</button>
|
||||
<button type="button" class="oc-text-button oc-text-button--danger text-sm" @click="handleRemove(activeItem.id)">移除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -320,7 +320,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center border border-sky-200 bg-sky-50 text-sky-700 transition-colors hover:bg-sky-100"
|
||||
class="oc-icon-button oc-icon-button--sky"
|
||||
title="地图选点"
|
||||
aria-label="地图选点"
|
||||
@click="openMapPicker"
|
||||
|
||||
@@ -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-slate-300 bg-[linear-gradient(135deg,#ecfeff_0%,#ccfbf1_100%)] shadow-[4px_4px_0_0_rgba(15,23,42,0.08)]">
|
||||
<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)]">
|
||||
<svg
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -173,21 +173,21 @@ async function handleLogout() {
|
||||
<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">
|
||||
<RouterLink
|
||||
to="/"
|
||||
class="px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
|
||||
class="oc-nav-link"
|
||||
:class="route.name === 'map' ? activeNavClass : inactiveNavClass"
|
||||
>
|
||||
地图
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/community"
|
||||
class="px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
|
||||
class="oc-nav-link"
|
||||
:class="route.name === 'community' ? activeNavClass : inactiveNavClass"
|
||||
>
|
||||
社区
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/gallery"
|
||||
class="px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
|
||||
class="oc-nav-link"
|
||||
:class="route.name === 'gallery' ? activeNavClass : inactiveNavClass"
|
||||
>
|
||||
画廊
|
||||
@@ -195,7 +195,7 @@ async function handleLogout() {
|
||||
<RouterLink
|
||||
v-if="authStore.isLoggedIn"
|
||||
to="/encyclopedia"
|
||||
class="px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
|
||||
class="oc-nav-link"
|
||||
:class="route.name === 'encyclopedia' || route.name === 'cloud-type' ? activeNavClass : inactiveNavClass"
|
||||
>
|
||||
图鉴
|
||||
@@ -210,7 +210,7 @@ async function handleLogout() {
|
||||
class="no-underline"
|
||||
>
|
||||
<span
|
||||
class="inline-flex h-8 items-center border border-sky-200 bg-sky-100 px-3 text-sm font-medium uppercase tracking-[0.12em] text-sky-800 shadow-[4px_4px_0_0_rgba(14,165,233,0.14)] transition-[background-color,color,transform,box-shadow] hover:-translate-x-px hover:-translate-y-px hover:bg-sky-50 hover:text-sky-900 hover:shadow-[5px_5px_0_0_rgba(14,165,233,0.18)] md:h-10 md:px-4"
|
||||
class="oc-header-button oc-header-button--sky uppercase tracking-[0.12em]"
|
||||
:class="route.name === 'upload' ? 'ring-1 ring-sky-300' : ''"
|
||||
>
|
||||
上传
|
||||
@@ -227,8 +227,8 @@ async function handleLogout() {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 max-w-[8.5rem] items-center gap-2 border border-teal-100 bg-white/80 px-3 text-sm font-medium text-teal-700 shadow-[3px_3px_0_0_rgba(20,184,166,0.08)] transition-[background-color,color,transform,box-shadow,border-color] hover:-translate-x-px hover:-translate-y-px hover:border-teal-200 hover:bg-teal-50/70 hover:text-teal-800 hover:shadow-[4px_4px_0_0_rgba(20,184,166,0.12)] md:h-10 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(20,184,166,0.12)]' : ''"
|
||||
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)]' : ''"
|
||||
aria-haspopup="menu"
|
||||
:aria-expanded="accountCardOpen"
|
||||
>
|
||||
@@ -244,9 +244,9 @@ async function handleLogout() {
|
||||
role="menu"
|
||||
>
|
||||
<div class="border border-slate-200 bg-white shadow-[8px_8px_0_0_rgba(15,23,42,0.08)]">
|
||||
<div class="border-b border-slate-200 bg-[linear-gradient(180deg,#f0fdfa_0%,#ffffff_100%)] px-4 py-4">
|
||||
<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,#ecfeff_0%,#ccfbf1_100%)] text-lg font-bold text-teal-800">
|
||||
<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">
|
||||
{{ displayUsername.slice(0, 1).toUpperCase() }}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
@@ -265,7 +265,7 @@ async function handleLogout() {
|
||||
<div class="py-2">
|
||||
<RouterLink
|
||||
to="/profile"
|
||||
class="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-teal-50 hover:text-teal-800"
|
||||
class="oc-menu-item"
|
||||
role="menuitem"
|
||||
>
|
||||
<NIcon size="18"><User /></NIcon>
|
||||
@@ -273,7 +273,7 @@ async function handleLogout() {
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/profile/settings"
|
||||
class="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-teal-50 hover:text-teal-800"
|
||||
class="oc-menu-item"
|
||||
role="menuitem"
|
||||
>
|
||||
<NIcon size="18"><Settings /></NIcon>
|
||||
@@ -282,7 +282,7 @@ async function handleLogout() {
|
||||
<RouterLink
|
||||
v-if="authStore.isAdmin"
|
||||
to="/admin"
|
||||
class="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-amber-50 hover:text-amber-700"
|
||||
class="oc-menu-item oc-menu-item--amber"
|
||||
role="menuitem"
|
||||
>
|
||||
<NIcon size="18"><Shield /></NIcon>
|
||||
@@ -293,7 +293,7 @@ async function handleLogout() {
|
||||
<div class="border-t border-slate-200 p-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 px-2 py-2.5 text-left text-sm font-medium text-rose-600 transition-colors hover:bg-rose-50 hover:text-rose-700"
|
||||
class="oc-menu-item oc-menu-item--danger px-2"
|
||||
role="menuitem"
|
||||
@click="handleLogout"
|
||||
>
|
||||
@@ -312,7 +312,7 @@ async function handleLogout() {
|
||||
class="no-underline"
|
||||
>
|
||||
<span
|
||||
class="inline-flex h-8 items-center border border-slate-200 bg-white/80 px-3 text-sm font-medium text-slate-700 shadow-[3px_3px_0_0_rgba(15,23,42,0.06)] transition-[background-color,color,transform,box-shadow,border-color] hover:-translate-x-px hover:-translate-y-px hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800 hover:shadow-[4px_4px_0_0_rgba(15,23,42,0.08)] md:h-10 md:px-4"
|
||||
class="oc-header-button oc-header-button--neutral"
|
||||
:class="route.name === 'login' ? 'bg-teal-50 ring-0 ring-teal-100 text-teal-800' : ''"
|
||||
>
|
||||
登录
|
||||
@@ -323,7 +323,7 @@ async function handleLogout() {
|
||||
class="no-underline"
|
||||
>
|
||||
<span
|
||||
class="inline-flex h-8 items-center border border-sky-200 bg-sky-100 px-3 text-sm font-medium text-sky-800 shadow-[4px_4px_0_0_rgba(14,165,233,0.14)] transition-[background-color,color,transform,box-shadow] hover:-translate-x-px hover:-translate-y-px hover:bg-sky-50 hover:text-sky-900 hover:shadow-[5px_5px_0_0_rgba(14,165,233,0.18)] md:h-10 md:px-4"
|
||||
class="oc-header-button oc-header-button--sky"
|
||||
:class="route.name === 'register' ? 'ring-0 ring-sky-300' : ''"
|
||||
>
|
||||
注册
|
||||
@@ -337,21 +337,21 @@ async function handleLogout() {
|
||||
<nav class="flex items-center gap-2 overflow-x-auto border-t border-slate-200/80 py-2 md:hidden">
|
||||
<RouterLink
|
||||
to="/"
|
||||
class="shrink-0 px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
|
||||
class="oc-nav-link shrink-0"
|
||||
:class="route.name === 'map' ? activeNavClass : inactiveNavClass"
|
||||
>
|
||||
地图
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/community"
|
||||
class="shrink-0 px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
|
||||
class="oc-nav-link shrink-0"
|
||||
:class="route.name === 'community' ? activeNavClass : inactiveNavClass"
|
||||
>
|
||||
社区
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/gallery"
|
||||
class="shrink-0 px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
|
||||
class="oc-nav-link shrink-0"
|
||||
:class="route.name === 'gallery' ? activeNavClass : inactiveNavClass"
|
||||
>
|
||||
画廊
|
||||
@@ -359,7 +359,7 @@ async function handleLogout() {
|
||||
<RouterLink
|
||||
v-if="authStore.isLoggedIn"
|
||||
to="/encyclopedia"
|
||||
class="shrink-0 px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
|
||||
class="oc-nav-link shrink-0"
|
||||
:class="route.name === 'encyclopedia' || route.name === 'cloud-type' ? activeNavClass : inactiveNavClass"
|
||||
>
|
||||
图鉴
|
||||
@@ -367,7 +367,7 @@ async function handleLogout() {
|
||||
<RouterLink
|
||||
v-if="!authStore.loading && authStore.isLoggedIn"
|
||||
to="/profile"
|
||||
class="shrink-0 px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
|
||||
class="oc-nav-link shrink-0"
|
||||
:class="route.name === 'profile' ? activeNavClass : inactiveNavClass"
|
||||
>
|
||||
主页
|
||||
@@ -379,7 +379,7 @@ async function handleLogout() {
|
||||
<button
|
||||
v-if="headerHidden"
|
||||
type="button"
|
||||
class="absolute left-1/2 top-3 flex -translate-x-1/2 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-slate-900 hover:text-slate-900"
|
||||
class="absolute left-1/2 top-3 flex -translate-x-1/2 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"
|
||||
@click="showHeader(true)"
|
||||
>
|
||||
<span>▼</span>
|
||||
|
||||
@@ -379,7 +379,7 @@ function formatMonthTooltip(monthKey: string) {
|
||||
v-for="cell in week"
|
||||
:key="cell.date"
|
||||
type="button"
|
||||
class="relative h-3.5 w-3.5 border transition-transform hover:scale-125"
|
||||
class="oc-heatmap-cell relative h-3.5 w-3.5 border hover:scale-125"
|
||||
:class="[
|
||||
getCellClass(cell),
|
||||
props.activeDate === cell.date ? 'ring-2 ring-slate-900 ring-offset-1' : '',
|
||||
|
||||
@@ -6,7 +6,6 @@ export function loadAMap() {
|
||||
version: '2.0',
|
||||
plugins: [
|
||||
'AMap.Scale',
|
||||
'AMap.ToolBar',
|
||||
'AMap.ControlBar',
|
||||
'AMap.Geolocation',
|
||||
'AMap.Marker',
|
||||
|
||||
+8
-5
@@ -3,12 +3,15 @@ import type { GlobalThemeOverrides } from 'naive-ui'
|
||||
/** Keep these values aligned with the semantic tokens in styles/tokens.css. */
|
||||
export const openCloudThemeOverrides: GlobalThemeOverrides = {
|
||||
common: {
|
||||
primaryColor: '#0f766e',
|
||||
primaryColorHover: '#115e59',
|
||||
primaryColorPressed: '#134e4a',
|
||||
primaryColorSuppl: '#0f766e',
|
||||
primaryColor: '#34745d',
|
||||
primaryColorHover: '#2d5f4e',
|
||||
primaryColorPressed: '#274c40',
|
||||
primaryColorSuppl: '#34745d',
|
||||
infoColor: '#0369a1',
|
||||
successColor: '#0f766e',
|
||||
successColor: '#4d7f3b',
|
||||
successColorHover: '#3f6633',
|
||||
successColorPressed: '#35522c',
|
||||
successColorSuppl: '#4d7f3b',
|
||||
warningColor: '#d97706',
|
||||
errorColor: '#dc2626',
|
||||
borderRadius: '0px',
|
||||
|
||||
+309
-37
@@ -117,8 +117,270 @@
|
||||
}
|
||||
|
||||
.oc-map-button {
|
||||
@apply flex h-10 w-10 items-center justify-center bg-white shadow-md transition-colors hover:bg-slate-50;
|
||||
border-radius: 0.5rem !important;
|
||||
@apply flex h-10 w-10 items-center justify-center border border-slate-200 bg-white text-slate-600;
|
||||
box-shadow: var(--oc-shadow-xs);
|
||||
}
|
||||
|
||||
/* Shared interactive contracts */
|
||||
.oc-panel-button:not(.n-button),
|
||||
.oc-header-button,
|
||||
.oc-icon-button,
|
||||
.oc-map-button,
|
||||
.oc-choice-button,
|
||||
.oc-text-button,
|
||||
.oc-menu-item,
|
||||
.oc-nav-link,
|
||||
.oc-interactive-card {
|
||||
transition:
|
||||
background-color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||
color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||
border-color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||
box-shadow var(--oc-duration-fast) var(--oc-ease-standard),
|
||||
transform var(--oc-duration-fast) var(--oc-ease-standard);
|
||||
}
|
||||
|
||||
.oc-panel-button:not(.n-button) {
|
||||
@apply inline-flex min-h-9 items-center justify-center border px-3 py-2 text-sm font-medium;
|
||||
}
|
||||
|
||||
.oc-panel-button:not(.n-button):not(:disabled):hover,
|
||||
.oc-header-button:not(:disabled):hover,
|
||||
.oc-icon-button:not(:disabled):hover,
|
||||
.oc-map-button:not(:disabled):hover,
|
||||
.oc-choice-button:not(:disabled):hover {
|
||||
transform: translate(-1px, -1px);
|
||||
}
|
||||
|
||||
.oc-panel-button--neutral:not(.n-button) {
|
||||
@apply border-slate-200 bg-white text-slate-600;
|
||||
box-shadow: var(--oc-shadow-xs);
|
||||
}
|
||||
|
||||
.oc-panel-button--neutral:not(.n-button):hover,
|
||||
.oc-panel-button--neutral:not(.n-button):focus-visible {
|
||||
@apply border-teal-200 bg-teal-50 text-teal-800;
|
||||
box-shadow: var(--oc-shadow-primary);
|
||||
}
|
||||
|
||||
.oc-panel-button--sky:not(.n-button) {
|
||||
@apply border-sky-200 bg-sky-100 text-sky-800;
|
||||
box-shadow: var(--oc-shadow-sky);
|
||||
}
|
||||
|
||||
.oc-panel-button--sky:not(.n-button):hover,
|
||||
.oc-panel-button--sky:not(.n-button):focus-visible {
|
||||
@apply border-sky-300 bg-sky-50 text-sky-900;
|
||||
box-shadow: 5px 5px 0 0 rgb(14 165 233 / 18%);
|
||||
}
|
||||
|
||||
.oc-panel-button--teal:not(.n-button) {
|
||||
@apply border-teal-200 bg-teal-50 text-teal-800;
|
||||
box-shadow: var(--oc-shadow-teal);
|
||||
}
|
||||
|
||||
.oc-panel-button--teal:not(.n-button):hover,
|
||||
.oc-panel-button--teal:not(.n-button):focus-visible {
|
||||
@apply border-teal-300 bg-teal-100 text-teal-900;
|
||||
box-shadow: var(--oc-shadow-primary-hover);
|
||||
}
|
||||
|
||||
.oc-panel-button--danger:not(.n-button) {
|
||||
@apply border-rose-200 bg-rose-50 text-rose-700;
|
||||
box-shadow: 4px 4px 0 0 rgb(244 63 94 / 10%);
|
||||
}
|
||||
|
||||
.oc-panel-button--danger:not(.n-button):hover,
|
||||
.oc-panel-button--danger:not(.n-button):focus-visible {
|
||||
@apply border-rose-300 bg-rose-100 text-rose-800;
|
||||
box-shadow: 5px 5px 0 0 rgb(244 63 94 / 14%);
|
||||
}
|
||||
|
||||
.oc-panel-button--amber:not(.n-button) {
|
||||
@apply border-amber-200 bg-amber-50 text-amber-800;
|
||||
box-shadow: 4px 4px 0 0 rgb(245 158 11 / 10%);
|
||||
}
|
||||
|
||||
.oc-panel-button--amber:not(.n-button):hover,
|
||||
.oc-panel-button--amber:not(.n-button):focus-visible {
|
||||
@apply border-amber-300 bg-amber-100 text-amber-900;
|
||||
box-shadow: 5px 5px 0 0 rgb(245 158 11 / 14%);
|
||||
}
|
||||
|
||||
.oc-icon-button {
|
||||
@apply flex h-10 w-10 shrink-0 items-center justify-center border border-slate-200 bg-white text-slate-600;
|
||||
box-shadow: var(--oc-shadow-xs);
|
||||
}
|
||||
|
||||
.oc-icon-button:hover,
|
||||
.oc-icon-button:focus-visible,
|
||||
.oc-map-button:hover,
|
||||
.oc-map-button:focus-visible {
|
||||
@apply border-teal-200 bg-teal-50 text-teal-800;
|
||||
box-shadow: var(--oc-shadow-primary);
|
||||
}
|
||||
|
||||
.oc-icon-button--small {
|
||||
@apply h-8 w-8;
|
||||
}
|
||||
|
||||
.oc-icon-button--tiny {
|
||||
@apply h-6 w-6 text-xs;
|
||||
box-shadow: 2px 2px 0 0 rgb(15 23 42 / 8%);
|
||||
}
|
||||
|
||||
.oc-icon-button--sky {
|
||||
@apply border-sky-200 bg-sky-50 text-sky-700;
|
||||
box-shadow: 3px 3px 0 0 rgb(14 165 233 / 12%);
|
||||
}
|
||||
|
||||
.oc-icon-button--sky:hover,
|
||||
.oc-icon-button--sky:focus-visible {
|
||||
@apply border-sky-300 bg-sky-100 text-sky-900;
|
||||
box-shadow: 4px 4px 0 0 rgb(14 165 233 / 16%);
|
||||
}
|
||||
|
||||
.oc-icon-button--danger {
|
||||
@apply border-rose-300 bg-rose-500 text-white;
|
||||
box-shadow: 3px 3px 0 0 rgb(159 18 57 / 18%);
|
||||
}
|
||||
|
||||
.oc-icon-button--danger:hover,
|
||||
.oc-icon-button--danger:focus-visible {
|
||||
@apply border-rose-400 bg-rose-600 text-white;
|
||||
box-shadow: 4px 4px 0 0 rgb(159 18 57 / 22%);
|
||||
}
|
||||
|
||||
.oc-overlay-button {
|
||||
@apply flex shrink-0 items-center justify-center text-white/75 hover:bg-white/10 hover:text-white;
|
||||
transition:
|
||||
background-color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||
color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||
transform var(--oc-duration-fast) var(--oc-ease-standard);
|
||||
}
|
||||
|
||||
.oc-text-button {
|
||||
@apply inline-flex items-center font-medium text-sky-700 underline-offset-4 hover:text-sky-900 hover:underline;
|
||||
}
|
||||
|
||||
.oc-text-button--muted {
|
||||
@apply text-slate-500 hover:text-slate-800;
|
||||
}
|
||||
|
||||
.oc-text-button--danger {
|
||||
@apply text-rose-600 hover:text-rose-800;
|
||||
}
|
||||
|
||||
.oc-choice-button {
|
||||
@apply inline-flex min-h-9 items-center justify-center border px-3 py-2 text-sm font-medium;
|
||||
}
|
||||
|
||||
.oc-choice-button--inactive {
|
||||
@apply border-slate-200 bg-white text-slate-600 hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800;
|
||||
box-shadow: var(--oc-shadow-xs);
|
||||
}
|
||||
|
||||
.oc-choice-button--active {
|
||||
@apply border-teal-200 bg-teal-50 text-teal-800;
|
||||
box-shadow: var(--oc-shadow-primary);
|
||||
}
|
||||
|
||||
.oc-menu-item {
|
||||
@apply flex w-full items-center gap-3 px-4 py-2.5 text-left text-sm font-medium text-slate-700 hover:bg-teal-50 hover:text-teal-800;
|
||||
}
|
||||
|
||||
.oc-menu-item--danger {
|
||||
@apply text-rose-600 hover:bg-rose-50 hover:text-rose-700;
|
||||
}
|
||||
|
||||
.oc-menu-item--amber {
|
||||
@apply hover:bg-amber-50 hover:text-amber-700;
|
||||
}
|
||||
|
||||
.oc-nav-link {
|
||||
@apply px-3 py-2 text-sm font-medium uppercase tracking-[0.12em];
|
||||
}
|
||||
|
||||
.oc-header-button {
|
||||
@apply inline-flex h-8 items-center justify-center border px-3 text-sm font-medium md:h-10 md:px-4;
|
||||
}
|
||||
|
||||
.oc-header-button--neutral {
|
||||
@apply border-slate-200 bg-white/80 text-slate-700 hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800;
|
||||
box-shadow: var(--oc-shadow-xs);
|
||||
}
|
||||
|
||||
.oc-header-button--sky {
|
||||
@apply border-sky-200 bg-sky-100 text-sky-800 hover:border-sky-300 hover:bg-sky-50 hover:text-sky-900;
|
||||
box-shadow: var(--oc-shadow-sky);
|
||||
}
|
||||
|
||||
.oc-header-button--teal {
|
||||
@apply border-teal-100 bg-white/80 text-teal-700 hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800;
|
||||
box-shadow: var(--oc-shadow-teal);
|
||||
}
|
||||
|
||||
.oc-header-button:hover {
|
||||
box-shadow: 5px 5px 0 0 rgb(15 23 42 / 8%);
|
||||
}
|
||||
|
||||
.oc-interactive-card:focus-visible {
|
||||
@apply outline-none ring-2 ring-sky-600 ring-offset-2;
|
||||
}
|
||||
|
||||
.oc-interactive-row {
|
||||
transition:
|
||||
background-color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||
color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||
box-shadow var(--oc-duration-fast) var(--oc-ease-standard);
|
||||
}
|
||||
|
||||
.oc-interactive-row:hover {
|
||||
@apply bg-sky-50/60;
|
||||
}
|
||||
|
||||
.oc-interactive-row:focus-visible,
|
||||
.oc-heatmap-cell:focus-visible {
|
||||
@apply outline-none ring-2 ring-sky-600 ring-offset-2;
|
||||
}
|
||||
|
||||
.oc-heatmap-cell {
|
||||
transition:
|
||||
border-color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||
box-shadow var(--oc-duration-fast) var(--oc-ease-standard),
|
||||
transform var(--oc-duration-fast) var(--oc-ease-standard);
|
||||
}
|
||||
|
||||
:is(
|
||||
.oc-primary-button,
|
||||
.oc-panel-button,
|
||||
.oc-header-button,
|
||||
.oc-icon-button,
|
||||
.oc-overlay-button,
|
||||
.oc-map-button,
|
||||
.oc-choice-button,
|
||||
.oc-text-button,
|
||||
.oc-menu-item,
|
||||
.oc-nav-link
|
||||
):focus-visible {
|
||||
outline: 2px solid rgb(2 132 199);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.oc-overlay-button:focus-visible {
|
||||
outline-color: rgb(125 211 252);
|
||||
}
|
||||
|
||||
:is(.oc-panel-button, .oc-header-button, .oc-icon-button, .oc-map-button, .oc-choice-button):disabled {
|
||||
@apply cursor-not-allowed border-slate-200 bg-slate-100 text-slate-400 shadow-none;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.oc-map-button:disabled {
|
||||
box-shadow: var(--oc-shadow-xs);
|
||||
}
|
||||
|
||||
:is(.oc-panel-button, .oc-primary-button, .oc-header-button, .oc-icon-button, .oc-map-button, .oc-choice-button):not(:disabled):active {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.oc-system-page {
|
||||
@@ -141,6 +403,16 @@
|
||||
transform var(--oc-duration-fast) var(--oc-ease-standard);
|
||||
}
|
||||
|
||||
.oc-primary-button.n-button.n-button--disabled,
|
||||
.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;
|
||||
box-shadow: none !important;
|
||||
opacity: 0.72;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.oc-primary-button.n-button .n-button__content,
|
||||
.oc-panel-button.n-button .n-button__content {
|
||||
font-weight: 500;
|
||||
@@ -157,21 +429,21 @@
|
||||
}
|
||||
|
||||
.oc-primary-button--teal.n-button {
|
||||
border-color: rgb(153 246 228) !important;
|
||||
background: rgb(255 255 255) !important;
|
||||
color: rgb(17 94 89) !important;
|
||||
border-color: var(--color-teal-200) !important;
|
||||
background: white !important;
|
||||
color: var(--color-teal-700) !important;
|
||||
box-shadow: var(--oc-shadow-teal) !important;
|
||||
}
|
||||
|
||||
.oc-primary-button--teal.n-button:hover,
|
||||
.oc-primary-button--teal.n-button:focus-visible {
|
||||
border-color: rgb(94 234 212) !important;
|
||||
background: rgb(234 250 245) !important;
|
||||
color: rgb(19 78 74) !important;
|
||||
border-color: var(--color-teal-300) !important;
|
||||
background: var(--color-teal-50) !important;
|
||||
color: var(--color-teal-800) !important;
|
||||
}
|
||||
|
||||
.oc-primary-button--teal.n-button:not(.n-button--disabled):hover {
|
||||
box-shadow: 5px 5px 0 0 rgb(20 184 166 / 14%) !important;
|
||||
box-shadow: var(--oc-shadow-primary-hover) !important;
|
||||
}
|
||||
|
||||
.oc-primary-button--sky.n-button {
|
||||
@@ -193,27 +465,27 @@
|
||||
}
|
||||
|
||||
.oc-panel-button.n-button {
|
||||
--n-color: rgb(255 255 255) !important;
|
||||
--n-color-hover: rgb(240 253 250) !important;
|
||||
--n-color-pressed: rgb(236 253 245) !important;
|
||||
--n-color: white !important;
|
||||
--n-color-hover: var(--color-teal-50) !important;
|
||||
--n-color-pressed: var(--color-teal-100) !important;
|
||||
--n-border: 1px solid rgb(226 232 240) !important;
|
||||
--n-border-hover: 1px solid rgb(153 246 228) !important;
|
||||
--n-border-pressed: 1px solid rgb(94 234 212) !important;
|
||||
--n-border-hover: 1px solid var(--color-teal-200) !important;
|
||||
--n-border-pressed: 1px solid var(--color-teal-300) !important;
|
||||
--n-text-color: rgb(71 85 105) !important;
|
||||
--n-text-color-hover: rgb(19 78 74) !important;
|
||||
--n-text-color-pressed: rgb(17 94 89) !important;
|
||||
--n-text-color-hover: var(--color-teal-800) !important;
|
||||
--n-text-color-pressed: var(--color-teal-700) !important;
|
||||
border-color: rgb(226 232 240) !important;
|
||||
background: rgb(255 255 255) !important;
|
||||
background: white !important;
|
||||
color: rgb(71 85 105) !important;
|
||||
box-shadow: var(--oc-shadow-xs) !important;
|
||||
}
|
||||
|
||||
.oc-panel-button--neutral.n-button:hover,
|
||||
.oc-panel-button--neutral.n-button:focus-visible {
|
||||
border-color: rgb(153 246 228) !important;
|
||||
background: rgb(240 253 250) !important;
|
||||
color: rgb(19 78 74) !important;
|
||||
box-shadow: 4px 4px 0 0 rgb(20 184 166 / 10%) !important;
|
||||
border-color: var(--color-teal-200) !important;
|
||||
background: var(--color-teal-50) !important;
|
||||
color: var(--color-teal-800) !important;
|
||||
box-shadow: var(--oc-shadow-primary) !important;
|
||||
}
|
||||
|
||||
.oc-panel-button--sky.n-button {
|
||||
@@ -241,27 +513,27 @@
|
||||
}
|
||||
|
||||
.oc-panel-button--teal.n-button {
|
||||
--n-color: rgb(242 250 247) !important;
|
||||
--n-color-hover: rgb(234 250 245) !important;
|
||||
--n-color-pressed: rgb(220 252 231) !important;
|
||||
--n-border: 1px solid rgb(153 246 228) !important;
|
||||
--n-border-hover: 1px solid rgb(94 234 212) !important;
|
||||
--n-border-pressed: 1px solid rgb(45 212 191) !important;
|
||||
--n-text-color: rgb(17 94 89) !important;
|
||||
--n-text-color-hover: rgb(19 78 74) !important;
|
||||
--n-text-color-pressed: rgb(19 78 74) !important;
|
||||
border-color: rgb(153 246 228) !important;
|
||||
background: rgb(242 250 247) !important;
|
||||
color: rgb(17 94 89) !important;
|
||||
--n-color: var(--color-teal-50) !important;
|
||||
--n-color-hover: var(--color-teal-100) !important;
|
||||
--n-color-pressed: var(--color-teal-200) !important;
|
||||
--n-border: 1px solid var(--color-teal-200) !important;
|
||||
--n-border-hover: 1px solid var(--color-teal-300) !important;
|
||||
--n-border-pressed: 1px solid var(--color-teal-400) !important;
|
||||
--n-text-color: var(--color-teal-700) !important;
|
||||
--n-text-color-hover: var(--color-teal-800) !important;
|
||||
--n-text-color-pressed: var(--color-teal-800) !important;
|
||||
border-color: var(--color-teal-200) !important;
|
||||
background: var(--color-teal-50) !important;
|
||||
color: var(--color-teal-700) !important;
|
||||
box-shadow: var(--oc-shadow-teal) !important;
|
||||
}
|
||||
|
||||
.oc-panel-button--teal.n-button:hover,
|
||||
.oc-panel-button--teal.n-button:focus-visible {
|
||||
border-color: rgb(94 234 212) !important;
|
||||
background: rgb(234 250 245) !important;
|
||||
color: rgb(19 78 74) !important;
|
||||
box-shadow: 5px 5px 0 0 rgb(20 184 166 / 14%) !important;
|
||||
border-color: var(--color-teal-300) !important;
|
||||
background: var(--color-teal-100) !important;
|
||||
color: var(--color-teal-800) !important;
|
||||
box-shadow: var(--oc-shadow-primary-hover) !important;
|
||||
}
|
||||
|
||||
.oc-panel-button--danger.n-button {
|
||||
|
||||
+37
-8
@@ -7,6 +7,33 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
/*
|
||||
* Tailwind compatibility palettes.
|
||||
* Existing teal/emerald utilities intentionally resolve to OpenCloud's
|
||||
* fresh eucalyptus and leaf colors instead of Tailwind's vivid defaults.
|
||||
*/
|
||||
--color-teal-50: #f2faf6;
|
||||
--color-teal-100: #ddf2e6;
|
||||
--color-teal-200: #bce4cf;
|
||||
--color-teal-300: #8fceb0;
|
||||
--color-teal-400: #60b18d;
|
||||
--color-teal-500: #438f71;
|
||||
--color-teal-600: #34745d;
|
||||
--color-teal-700: #2d5f4e;
|
||||
--color-teal-800: #274c40;
|
||||
--color-teal-900: #213e35;
|
||||
|
||||
--color-emerald-50: #f4faef;
|
||||
--color-emerald-100: #e5f3d9;
|
||||
--color-emerald-200: #cce7b7;
|
||||
--color-emerald-300: #a8d58d;
|
||||
--color-emerald-400: #7fbc63;
|
||||
--color-emerald-500: #609d48;
|
||||
--color-emerald-600: #4d7f3b;
|
||||
--color-emerald-700: #3f6633;
|
||||
--color-emerald-800: #35522c;
|
||||
--color-emerald-900: #2d4427;
|
||||
|
||||
--oc-font-sans: 'IBM Plex Sans', 'Noto Sans SC', 'PingFang SC', sans-serif;
|
||||
--oc-font-mono: 'IBM Plex Mono', 'SFMono-Regular', monospace;
|
||||
|
||||
@@ -17,20 +44,20 @@
|
||||
--oc-color-text-secondary: #334155;
|
||||
--oc-color-text-muted: #64748b;
|
||||
--oc-color-border: #d9e3ee;
|
||||
--oc-color-primary: #0f766e;
|
||||
--oc-color-primary-hover: #115e59;
|
||||
--oc-color-primary-pressed: #134e4a;
|
||||
--oc-color-primary: #34745d;
|
||||
--oc-color-primary-hover: #2d5f4e;
|
||||
--oc-color-primary-pressed: #274c40;
|
||||
--oc-color-info: #0369a1;
|
||||
--oc-color-success: #0f766e;
|
||||
--oc-color-success: #4d7f3b;
|
||||
--oc-color-warning: #d97706;
|
||||
--oc-color-danger: #dc2626;
|
||||
--oc-color-selection: rgb(15 118 110 / 18%);
|
||||
--oc-color-selection: rgb(52 116 93 / 20%);
|
||||
|
||||
--oc-gradient-canvas:
|
||||
radial-gradient(circle at top left, rgb(12 148 136 / 12%), transparent 28%),
|
||||
radial-gradient(circle at top left, rgb(52 116 93 / 11%), transparent 28%),
|
||||
linear-gradient(180deg, #f8fbfd 0%, #eef4f8 100%);
|
||||
--oc-gradient-page: linear-gradient(180deg, #e0f2fe 0%, #f8fafc 100%);
|
||||
--oc-gradient-page-soft: linear-gradient(180deg, #f0fdfa 0%, #f8fafc 100%);
|
||||
--oc-gradient-page-soft: linear-gradient(180deg, #f2faf6 0%, #f8fafc 100%);
|
||||
--oc-gradient-grid:
|
||||
linear-gradient(rgb(15 23 42 / 3%) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgb(15 23 42 / 3%) 1px, transparent 1px);
|
||||
@@ -41,7 +68,9 @@
|
||||
--oc-shadow-lg: 8px 8px 0 0 rgb(15 23 42 / 8%);
|
||||
--oc-shadow-xl: 12px 12px 0 0 rgb(15 23 42 / 8%);
|
||||
--oc-shadow-sky: 4px 4px 0 0 rgb(14 165 233 / 14%);
|
||||
--oc-shadow-teal: 4px 4px 0 0 rgb(20 184 166 / 10%);
|
||||
--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-teal: var(--oc-shadow-primary);
|
||||
|
||||
--oc-duration-fast: 160ms;
|
||||
--oc-ease-standard: ease;
|
||||
|
||||
Vendored
+2
-2
@@ -6,7 +6,7 @@ declare namespace AMap {
|
||||
remove(layer: unknown): void
|
||||
destroy(): void
|
||||
setCenter(center: [number, number]): void
|
||||
setZoom(zoom: number): void
|
||||
setZoom(zoom: number, immediately?: boolean, duration?: number): void
|
||||
setPitch(pitch: number): void
|
||||
getPitch(): number
|
||||
setRotation(rotation: number): void
|
||||
@@ -14,7 +14,7 @@ declare namespace AMap {
|
||||
on(event: string, callback: (...args: unknown[]) => void): void
|
||||
off(event: string, callback: (...args: unknown[]) => void): void
|
||||
getCenter(): { lng: number; lat: number }
|
||||
getZoom(): number
|
||||
getZoom(digits?: number): number
|
||||
getAllOverlays(type?: string): unknown[]
|
||||
addOverlay(overlay: unknown): void
|
||||
removeOverlay(overlay: unknown): void
|
||||
|
||||
@@ -519,7 +519,7 @@ onMounted(loadAdminData)
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="hidden h-8 w-8 items-center justify-center border border-slate-200 bg-white text-slate-500 shadow-[3px_3px_0_0_rgba(15,23,42,0.06)] transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600 lg:absolute lg:-right-4 lg:top-6 lg:z-20 lg:flex"
|
||||
class="oc-icon-button oc-icon-button--small hidden lg:absolute lg:-right-4 lg:top-6 lg:z-20 lg:flex"
|
||||
:aria-label="sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'"
|
||||
:aria-expanded="!sidebarCollapsed"
|
||||
:title="sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'"
|
||||
@@ -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(20,184,166,0.14)]'
|
||||
? 'border-teal-200 bg-teal-50 text-teal-900 shadow-[3px_3px_0_0_rgba(52,116,93,0.16)]'
|
||||
: '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(20,184,166,0.12)]'
|
||||
? 'border-teal-200 bg-teal-50 text-teal-800 shadow-[3px_3px_0_0_rgba(52,116,93,0.16)]'
|
||||
: '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"
|
||||
@@ -615,9 +615,9 @@ onMounted(loadAdminData)
|
||||
role="menu"
|
||||
aria-label="管理员账户"
|
||||
>
|
||||
<div class="border-b border-slate-200 bg-[linear-gradient(180deg,#f0fdfa_0%,#ffffff_100%)] px-4 py-4">
|
||||
<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,#ecfeff_0%,#ccfbf1_100%)] text-lg font-bold text-teal-800">
|
||||
<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">
|
||||
{{ displayInitial }}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
@@ -651,7 +651,7 @@ onMounted(loadAdminData)
|
||||
<div class="border-t border-slate-200 p-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 px-2 py-2.5 text-left text-sm font-medium text-rose-600 transition-colors hover:bg-rose-50 hover:text-rose-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-rose-500"
|
||||
class="oc-menu-item oc-menu-item--danger px-2"
|
||||
role="menuitem"
|
||||
@click="handleLogout"
|
||||
>
|
||||
@@ -715,7 +715,7 @@ onMounted(loadAdminData)
|
||||
</NAlert>
|
||||
|
||||
<section v-if="loading" class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div v-for="n in 8" :key="n" class="border border-slate-200 bg-white p-5">
|
||||
<div v-for="n in 8" :key="n" class="oc-panel-card border border-slate-200 bg-white p-5">
|
||||
<NSkeleton class="h-4 w-1/2" />
|
||||
<NSkeleton class="mt-4 h-8 w-2/3" />
|
||||
<NSkeleton class="mt-3 h-3 w-full" />
|
||||
@@ -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%,#f0fdfa_100%)] shadow-[5px_5px_0_0_rgba(245,158,11,0.12)]'
|
||||
: 'border-teal-200 bg-[linear-gradient(135deg,#f0fdfa_0%,#fff_55%,#eff6ff_100%)] shadow-[5px_5px_0_0_rgba(20,184,166,0.10)]'"
|
||||
? '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)]'"
|
||||
>
|
||||
<div class="flex items-center gap-5">
|
||||
<div
|
||||
@@ -753,18 +753,18 @@ onMounted(loadAdminData)
|
||||
</div>
|
||||
</div>
|
||||
<NButton
|
||||
v-if="dashboardStats.pending > 0"
|
||||
type="default"
|
||||
secondary
|
||||
strong
|
||||
class="oc-panel-button oc-panel-button--amber shrink-0"
|
||||
:disabled="!dashboardStats.pending"
|
||||
@click="activeTab = 'review'"
|
||||
>
|
||||
进入审核队列
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 border border-slate-200 bg-white">
|
||||
<div class="oc-panel-card grid grid-cols-2 border border-slate-200 bg-white">
|
||||
<div class="border-b border-r border-slate-100 p-4">
|
||||
<p class="text-xs font-medium text-slate-500">今日上传</p>
|
||||
<p class="mt-2 text-2xl font-bold text-sky-800">{{ dashboardStats.todayUploads }}</p>
|
||||
@@ -785,7 +785,7 @@ onMounted(loadAdminData)
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 lg:grid-cols-[1.05fr_0.95fr]">
|
||||
<div class="border border-slate-200 bg-white p-5 sm:p-6">
|
||||
<div class="oc-panel-card border border-slate-200 bg-white p-5 sm:p-6">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="oc-section-label">Review Health</p>
|
||||
@@ -823,7 +823,7 @@ onMounted(loadAdminData)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-slate-200 bg-white p-5 sm:p-6">
|
||||
<div class="oc-panel-card border border-slate-200 bg-white p-5 sm:p-6">
|
||||
<div>
|
||||
<p class="oc-section-label">Cloud Type Mix</p>
|
||||
<h3 class="mt-2 text-xl font-bold text-slate-950">云型分布</h3>
|
||||
@@ -849,22 +849,29 @@ onMounted(loadAdminData)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-slate-200 bg-white">
|
||||
<div class="oc-panel-card border border-slate-200 bg-white">
|
||||
<div class="flex items-end justify-between gap-4 border-b border-slate-200 px-5 py-4">
|
||||
<div>
|
||||
<p class="oc-section-label">Latest Activity</p>
|
||||
<h3 class="mt-2 text-xl font-bold text-slate-950">最近上传</h3>
|
||||
</div>
|
||||
<button type="button" class="text-sm font-medium text-sky-700 hover:text-sky-900" @click="activeTab = 'images'">
|
||||
<NButton
|
||||
size="small"
|
||||
type="default"
|
||||
secondary
|
||||
strong
|
||||
class="oc-panel-button oc-panel-button--neutral"
|
||||
@click="activeTab = 'images'"
|
||||
>
|
||||
查看全部图片
|
||||
</button>
|
||||
</NButton>
|
||||
</div>
|
||||
<div v-if="recentImages.length" class="divide-y divide-slate-100">
|
||||
<button
|
||||
v-for="item in recentImages"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="grid w-full grid-cols-[52px_minmax(0,1fr)_auto] items-center gap-3 px-5 py-3 text-left transition-colors hover:bg-sky-50/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-sky-600 sm:grid-cols-[52px_minmax(0,1fr)_minmax(120px,0.5fr)_auto]"
|
||||
class="oc-interactive-row grid w-full grid-cols-[52px_minmax(0,1fr)_auto] items-center gap-3 px-5 py-3 text-left sm:grid-cols-[52px_minmax(0,1fr)_minmax(120px,0.5fr)_auto]"
|
||||
@click="selectedImage = item"
|
||||
>
|
||||
<img :src="item.thumbnail_url || item.image_url" :alt="item.cloudTypeName" class="h-11 w-13 object-cover" />
|
||||
@@ -883,13 +890,20 @@ onMounted(loadAdminData)
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'review'">
|
||||
<div class="mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="oc-panel-card mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-slate-950">待审核队列</h2>
|
||||
<p class="oc-field-help">共 {{ pendingImages.length }} 张待处理图片。</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="toggleAllPendingSelection">
|
||||
<NButton
|
||||
v-if="pendingImages.length > 0"
|
||||
secondary
|
||||
strong
|
||||
type="default"
|
||||
class="oc-panel-button oc-panel-button--neutral"
|
||||
@click="toggleAllPendingSelection"
|
||||
>
|
||||
{{ selectedReviewCount === pendingImages.length && pendingImages.length ? '取消全选' : '全选待审核' }}
|
||||
</NButton>
|
||||
<template v-if="selectedReviewCount">
|
||||
@@ -918,7 +932,7 @@ onMounted(loadAdminData)
|
||||
</div>
|
||||
|
||||
<div v-if="activeReviewImage" class="grid min-w-0 gap-4 xl:grid-cols-[220px_minmax(0,1fr)]">
|
||||
<aside class="min-w-0 self-start border border-slate-200 bg-white xl:sticky xl:top-20 xl:max-h-[calc(100vh-6rem)]">
|
||||
<aside class="oc-panel-card min-w-0 self-start border border-slate-200 bg-white xl:sticky xl:top-20 xl:max-h-[calc(100vh-6rem)]">
|
||||
<div class="flex items-center justify-between border-b border-slate-200 px-4 py-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-slate-900">审核队列</p>
|
||||
@@ -939,7 +953,7 @@ onMounted(loadAdminData)
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 flex-1 items-center gap-3 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600"
|
||||
class="oc-interactive-row flex min-w-0 flex-1 items-center gap-3 text-left"
|
||||
:aria-pressed="activeReviewImage.id === item.id"
|
||||
@click="activeReviewId = item.id"
|
||||
>
|
||||
@@ -965,7 +979,7 @@ onMounted(loadAdminData)
|
||||
|
||||
<div class="grid min-w-0 gap-4 lg:grid-cols-[minmax(0,1fr)_300px]">
|
||||
<div class="min-w-0 space-y-4">
|
||||
<div class="border border-slate-200 bg-white">
|
||||
<div class="oc-panel-card border border-slate-200 bg-white">
|
||||
<div class="flex items-center justify-between gap-4 border-b border-slate-200 px-4 py-3">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-slate-900">{{ activeReviewImage.cloudTypeName }}</p>
|
||||
@@ -995,7 +1009,7 @@ onMounted(loadAdminData)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-slate-200 bg-white p-5">
|
||||
<div class="oc-panel-card border border-slate-200 bg-white p-5">
|
||||
<p class="oc-section-label">Uploader Notes</p>
|
||||
<h3 class="mt-2 text-lg font-bold text-slate-950">上传说明</h3>
|
||||
<p class="mt-3 text-sm leading-7 text-slate-700">
|
||||
@@ -1004,7 +1018,7 @@ onMounted(loadAdminData)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="self-start border border-slate-200 bg-white lg:sticky lg:top-20">
|
||||
<aside class="oc-panel-card self-start border border-slate-200 bg-white lg:sticky lg:top-20">
|
||||
<div class="border-b border-slate-200 bg-[linear-gradient(180deg,#fffbeb_0%,#fff_100%)] p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">Review Decision</p>
|
||||
<h3 class="mt-2 text-xl font-bold text-slate-950">审核资料</h3>
|
||||
@@ -1088,13 +1102,13 @@ onMounted(loadAdminData)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="border border-dashed border-slate-300 bg-white p-10">
|
||||
<div v-else class="oc-panel-card border border-dashed border-slate-300 bg-white p-10">
|
||||
<NEmpty description="当前没有待审核图片" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'users'">
|
||||
<div class="overflow-hidden border border-slate-200 bg-white">
|
||||
<div class="oc-panel-card overflow-hidden border border-slate-200 bg-white">
|
||||
<div class="grid grid-cols-[1.1fr_0.8fr_0.7fr_0.9fr_1.2fr] border-b border-slate-200 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">
|
||||
<span>用户</span>
|
||||
<span>角色</span>
|
||||
@@ -1145,7 +1159,7 @@ onMounted(loadAdminData)
|
||||
</section>
|
||||
|
||||
<section v-else>
|
||||
<div class="mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="oc-panel-card mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-slate-950">图片管理</h2>
|
||||
<p class="oc-field-help">最近 {{ images.length }} 张图片,可调整审核状态、可见性或删除。</p>
|
||||
@@ -1160,7 +1174,14 @@ onMounted(loadAdminData)
|
||||
<option value="approved">已通过</option>
|
||||
<option value="rejected">已拒绝</option>
|
||||
</select>
|
||||
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="toggleAllFilteredImageSelection">
|
||||
<NButton
|
||||
v-if="filteredImages.length > 0"
|
||||
secondary
|
||||
strong
|
||||
type="default"
|
||||
class="oc-panel-button oc-panel-button--neutral"
|
||||
@click="toggleAllFilteredImageSelection"
|
||||
>
|
||||
{{ selectedManagedCount === filteredImages.length && filteredImages.length ? '取消全选' : '全选当前列表' }}
|
||||
</NButton>
|
||||
<template v-if="selectedManagedCount">
|
||||
@@ -1219,8 +1240,8 @@ onMounted(loadAdminData)
|
||||
</div>
|
||||
|
||||
<div v-if="filteredImages.length" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<article v-for="item in filteredImages" :key="item.id" class="overflow-hidden border border-slate-200 bg-white">
|
||||
<button type="button" class="block w-full overflow-hidden bg-slate-100" @click="selectedImage = item">
|
||||
<article v-for="item in filteredImages" :key="item.id" class="oc-panel-card overflow-hidden border border-slate-200 bg-white">
|
||||
<button type="button" class="oc-interactive-card block w-full overflow-hidden bg-slate-100" @click="selectedImage = item">
|
||||
<img :src="item.thumbnail_url || item.image_url" :alt="item.cloudTypeName" class="h-52 w-full object-cover transition-transform hover:scale-105" />
|
||||
</button>
|
||||
<div class="p-4">
|
||||
@@ -1263,7 +1284,7 @@ onMounted(loadAdminData)
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-else class="border border-dashed border-slate-300 bg-white p-10">
|
||||
<div v-else class="oc-panel-card border border-dashed border-slate-300 bg-white p-10">
|
||||
<NEmpty description="没有符合筛选条件的图片" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -73,10 +73,10 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
|
||||
<div class="mx-auto grid max-w-6xl gap-8 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
|
||||
<section class="border border-slate-200 bg-[linear-gradient(135deg,#eff6ff_0%,#ffffff_52%,#f0fdfa_100%)] p-8 shadow-[10px_10px_0_0_rgba(15,23,42,0.08)]">
|
||||
<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">
|
||||
<p class="text-sm uppercase tracking-[0.26em] text-sky-700">Password Recovery</p>
|
||||
<h1 class="mt-4 max-w-xl text-5xl font-black leading-[1.05] text-slate-900">
|
||||
<h1 class="mt-4 max-w-xl break-words text-4xl font-black leading-[1.05] text-slate-900 sm:text-5xl">
|
||||
重新拿回你的
|
||||
<span class="block text-sky-700">OpenCloud 账号</span>
|
||||
</h1>
|
||||
@@ -97,7 +97,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<NCard class="shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
||||
<NCard class="min-w-0 shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
||||
<NResult
|
||||
v-if="submitted"
|
||||
status="success"
|
||||
|
||||
@@ -42,11 +42,11 @@ async function handleLogin() {
|
||||
|
||||
<template>
|
||||
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
|
||||
<div class="mx-auto grid max-w-6xl gap-8 lg:grid-cols-[1.1fr_0.9fr] lg:items-stretch">
|
||||
<section class="flex h-full flex-col justify-between border border-slate-200 bg-[linear-gradient(135deg,#f0fdfa_0%,#ffffff_48%,#eff6ff_100%)] p-8 shadow-[10px_10px_0_0_rgba(15,23,42,0.08)]">
|
||||
<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">
|
||||
<div>
|
||||
<p class="text-sm uppercase tracking-[0.26em] text-teal-700">Sky Log In</p>
|
||||
<h1 class="mt-4 max-w-xl text-5xl font-black leading-[1.05] text-slate-900">
|
||||
<h1 class="mt-4 max-w-xl break-words text-4xl font-black leading-[1.05] text-slate-900 sm:text-5xl">
|
||||
登录后继续记录
|
||||
<span class="block text-teal-700">你眼中的每一朵云</span>
|
||||
</h1>
|
||||
@@ -54,13 +54,13 @@ async function handleLogin() {
|
||||
在地图、图鉴和画廊之间同步你的观测记录。上传新的云图后,图鉴会自动点亮,画廊也会按时间收纳你的作品。
|
||||
</p> -->
|
||||
</div>
|
||||
<div class="mt-8 flex gap-4">
|
||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="mt-8 grid min-w-0 grid-cols-2 gap-4">
|
||||
<div class="min-w-0 border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="oc-stat-label">Collection</div>
|
||||
<div class="oc-stat-value">10</div>
|
||||
<div class="oc-field-help">基础云属待收集</div>
|
||||
</div>
|
||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="min-w-0 border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="oc-stat-label">Mode</div>
|
||||
<div class="oc-stat-value">Atlas</div>
|
||||
<div class="oc-field-help">地图、图鉴、画廊一体</div>
|
||||
@@ -68,7 +68,7 @@ async function handleLogin() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<NCard class="h-full shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
||||
<NCard class="min-w-0 h-full shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
||||
<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>
|
||||
|
||||
@@ -47,11 +47,11 @@ async function handleRegister() {
|
||||
|
||||
<template>
|
||||
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
|
||||
<div class="mx-auto grid max-w-6xl gap-8 lg:grid-cols-[1.1fr_0.9fr] lg:items-stretch">
|
||||
<section class="flex h-full flex-col justify-between border border-slate-200 bg-[linear-gradient(135deg,#eff6ff_0%,#ffffff_42%,#f0fdfa_100%)] p-8 shadow-[10px_10px_0_0_rgba(15,23,42,0.08)]">
|
||||
<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">
|
||||
<div>
|
||||
<p class="text-sm uppercase tracking-[0.26em] text-sky-700">Observer Join</p>
|
||||
<h1 class="mt-4 max-w-xl text-5xl font-black leading-[1.05] text-slate-900">
|
||||
<h1 class="mt-4 max-w-xl break-words text-4xl font-black leading-[1.05] text-slate-900 sm:text-5xl">
|
||||
加入天空探索者
|
||||
<span class="block text-sky-700">建立你的云图档案</span>
|
||||
</h1>
|
||||
@@ -59,13 +59,13 @@ async function handleRegister() {
|
||||
注册后即可上传云图、点亮图鉴、在社区画廊里按时间展示你的观测记录。所有页面都会围绕你的个人云层档案同步更新。
|
||||
</p> -->
|
||||
</div>
|
||||
<div class="mt-8 flex gap-4">
|
||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="mt-8 grid min-w-0 grid-cols-2 gap-4">
|
||||
<div class="min-w-0 border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="oc-stat-label">Review</div>
|
||||
<div class="oc-stat-value">Queue</div>
|
||||
<div class="oc-field-help">上传后进入审核队列</div>
|
||||
</div>
|
||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="min-w-0 border border-slate-200 bg-white px-4 py-3">
|
||||
<div class="oc-stat-label">Profile</div>
|
||||
<div class="oc-stat-value">Journal</div>
|
||||
<div class="oc-field-help">自动生成个人天空日志</div>
|
||||
@@ -73,7 +73,7 @@ async function handleRegister() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<NCard class="h-full shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
||||
<NCard class="min-w-0 h-full shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
|
||||
<NResult
|
||||
v-if="emailSent"
|
||||
status="success"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-[calc(100vh-4rem)] bg-[linear-gradient(180deg,#f0fdfa_0%,#f8fafc_100%)] px-4 py-16">
|
||||
<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)]">
|
||||
<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>
|
||||
|
||||
@@ -242,7 +242,7 @@ watch(() => route.params.id, () => {
|
||||
v-for="item in gallery"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="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 shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
@click="openGalleryDetail(item)"
|
||||
>
|
||||
<img :src="item.thumbnail_url || item.image_url" :alt="cloudType.name" class="h-56 w-full object-cover" />
|
||||
|
||||
@@ -110,7 +110,7 @@ onMounted(async () => {
|
||||
:key="cloudType.id"
|
||||
type="button"
|
||||
@click="openCard(cloudType)"
|
||||
class="group overflow-hidden border bg-white text-left shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg"
|
||||
class="oc-interactive-card group overflow-hidden border bg-white text-left shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg"
|
||||
:class="isUnlocked(cloudType.id) ? 'border-amber-300 shadow-amber-100/60' : 'border-slate-200 hover:border-slate-300'"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -367,7 +367,7 @@ onUnmounted(() => {
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
type="button"
|
||||
class="flex h-8 w-8 shrink-0 items-center justify-center text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
||||
class="oc-icon-button oc-icon-button--small"
|
||||
title="清空搜索"
|
||||
aria-label="清空搜索"
|
||||
@click="clearSearch"
|
||||
@@ -430,7 +430,7 @@ onUnmounted(() => {
|
||||
:key="cloud.id"
|
||||
type="button"
|
||||
@click="openDetail(cloud)"
|
||||
class="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 shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
>
|
||||
<img
|
||||
:src="cloud.thumbnail_url || cloud.image_url"
|
||||
@@ -482,6 +482,7 @@ onUnmounted(() => {
|
||||
<NButton
|
||||
secondary
|
||||
strong
|
||||
type="default"
|
||||
:disabled="currentPage <= 1"
|
||||
class="oc-panel-button oc-panel-button--neutral"
|
||||
@click="goToPage(currentPage - 1)"
|
||||
@@ -509,6 +510,7 @@ onUnmounted(() => {
|
||||
<NButton
|
||||
secondary
|
||||
strong
|
||||
type="default"
|
||||
:disabled="currentPage >= totalPages"
|
||||
class="oc-panel-button oc-panel-button--neutral"
|
||||
@click="goToPage(currentPage + 1)"
|
||||
@@ -576,7 +578,7 @@ onUnmounted(() => {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center border border-slate-200 bg-slate-50 text-slate-600 transition-colors hover:border-slate-900 hover:text-slate-900"
|
||||
class="oc-icon-button"
|
||||
title="图片管理"
|
||||
aria-label="图片管理"
|
||||
>
|
||||
|
||||
+227
-14
@@ -8,7 +8,7 @@ import { loadAMap } from '@/lib/amap'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import type { ApiCloud } from '@/types/api'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import { Adjustments, Calendar, CloudUpload, Refresh, Map, Satellite, X } from '@vicons/tabler'
|
||||
import { Adjustments, Calendar, CloudUpload, Minus, Plus, Refresh, Map, Satellite, X } from '@vicons/tabler'
|
||||
|
||||
interface CloudMarkerData {
|
||||
id: string
|
||||
@@ -40,10 +40,15 @@ const archiveMonth = ref(formatMonthInput(new Date()))
|
||||
const currentMinuteOfDay = ref(getMinuteOfDay(new Date()))
|
||||
const selectedMinuteOfDay = ref(currentMinuteOfDay.value)
|
||||
const sliderDragging = ref(false)
|
||||
const currentZoom = ref(5)
|
||||
|
||||
const VISIBLE_WINDOW_MS = 2 * 60 * 60 * 1000
|
||||
const MIN_MARKER_OPACITY = 0.3
|
||||
const HIDE_HEADER_EVENT = 'opencloud:hide-header'
|
||||
const MIN_MAP_ZOOM = 4
|
||||
const MAX_MAP_ZOOM = 18
|
||||
const ZOOM_HOLD_DELAY_MS = 180
|
||||
const ZOOM_CONTINUOUS_SPEED = 2.2
|
||||
|
||||
let AMapLib: typeof AMap | null = null
|
||||
let mapInst: AMap.Map | null = null
|
||||
@@ -53,6 +58,13 @@ let mks: AMap.Marker[] = []
|
||||
let hoverIW: AMap.InfoWindow | null = null
|
||||
let allClouds: CloudMarkerData[] = []
|
||||
let redrawTimer: number | null = null
|
||||
let zoomHoldDelayTimer: number | null = null
|
||||
let zoomAnimationFrame: number | null = null
|
||||
let activeZoomPointerId: number | null = null
|
||||
let zoomContinuousDirection: -1 | 1 | null = null
|
||||
let zoomAnimationValue = currentZoom.value
|
||||
let zoomLastFrameTime = 0
|
||||
let zoomHoldStarted = false
|
||||
|
||||
const rarityColors: Record<string, string> = {
|
||||
common: '#cbd5e1',
|
||||
@@ -243,6 +255,165 @@ function hideHeader() {
|
||||
window.dispatchEvent(new CustomEvent(HIDE_HEADER_EVENT))
|
||||
}
|
||||
|
||||
function syncCurrentZoom() {
|
||||
if (!mapInst) return
|
||||
currentZoom.value = mapInst.getZoom(2)
|
||||
}
|
||||
|
||||
function changeZoom(direction: -1 | 1, step = 1) {
|
||||
if (!mapInst) return false
|
||||
|
||||
const zoom = mapInst.getZoom(2)
|
||||
const nextZoom = Math.min(MAX_MAP_ZOOM, Math.max(MIN_MAP_ZOOM, zoom + direction * step))
|
||||
if (Math.abs(nextZoom - zoom) < 0.001) return false
|
||||
|
||||
mapInst.setZoom(nextZoom, true)
|
||||
currentZoom.value = nextZoom
|
||||
return true
|
||||
}
|
||||
|
||||
function stopZoomAnimation() {
|
||||
if (zoomAnimationFrame !== null) {
|
||||
window.cancelAnimationFrame(zoomAnimationFrame)
|
||||
zoomAnimationFrame = null
|
||||
}
|
||||
zoomContinuousDirection = null
|
||||
zoomLastFrameTime = 0
|
||||
}
|
||||
|
||||
function clearZoomHoldDelay() {
|
||||
if (zoomHoldDelayTimer !== null) {
|
||||
window.clearTimeout(zoomHoldDelayTimer)
|
||||
zoomHoldDelayTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function clearZoomHold() {
|
||||
clearZoomHoldDelay()
|
||||
stopZoomAnimation()
|
||||
activeZoomPointerId = null
|
||||
zoomHoldStarted = false
|
||||
}
|
||||
|
||||
function runContinuousZoomFrame(timestamp: number) {
|
||||
if (!mapInst || zoomContinuousDirection === null) {
|
||||
zoomAnimationFrame = null
|
||||
return
|
||||
}
|
||||
|
||||
if (zoomLastFrameTime === 0) {
|
||||
zoomLastFrameTime = timestamp
|
||||
zoomAnimationFrame = window.requestAnimationFrame(runContinuousZoomFrame)
|
||||
return
|
||||
}
|
||||
|
||||
const elapsedSeconds = Math.min((timestamp - zoomLastFrameTime) / 1000, 0.05)
|
||||
zoomLastFrameTime = timestamp
|
||||
const nextZoom = Math.min(
|
||||
MAX_MAP_ZOOM,
|
||||
Math.max(
|
||||
MIN_MAP_ZOOM,
|
||||
zoomAnimationValue + zoomContinuousDirection * ZOOM_CONTINUOUS_SPEED * elapsedSeconds,
|
||||
),
|
||||
)
|
||||
|
||||
if (Math.abs(nextZoom - zoomAnimationValue) < 0.0001) {
|
||||
stopZoomAnimation()
|
||||
return
|
||||
}
|
||||
|
||||
zoomAnimationValue = nextZoom
|
||||
mapInst.setZoom(nextZoom, true)
|
||||
currentZoom.value = nextZoom
|
||||
|
||||
if (nextZoom === MIN_MAP_ZOOM || nextZoom === MAX_MAP_ZOOM) {
|
||||
stopZoomAnimation()
|
||||
return
|
||||
}
|
||||
zoomAnimationFrame = window.requestAnimationFrame(runContinuousZoomFrame)
|
||||
}
|
||||
|
||||
function beginContinuousZoom(direction: -1 | 1) {
|
||||
if (!mapInst) return
|
||||
|
||||
zoomHoldStarted = true
|
||||
zoomContinuousDirection = direction
|
||||
zoomAnimationValue = mapInst.getZoom(4)
|
||||
zoomLastFrameTime = 0
|
||||
zoomAnimationFrame = window.requestAnimationFrame(runContinuousZoomFrame)
|
||||
}
|
||||
|
||||
function startZoomHold(direction: -1 | 1, event: PointerEvent) {
|
||||
if (event.pointerType === 'mouse' && event.button !== 0) return
|
||||
if (activeZoomPointerId !== null) return
|
||||
|
||||
event.preventDefault()
|
||||
clearZoomHold()
|
||||
activeZoomPointerId = event.pointerId
|
||||
|
||||
const button = event.currentTarget as HTMLButtonElement
|
||||
button.setPointerCapture?.(event.pointerId)
|
||||
|
||||
const zoom = mapInst?.getZoom(4)
|
||||
if (
|
||||
zoom === undefined
|
||||
|| (direction === 1 && zoom >= MAX_MAP_ZOOM)
|
||||
|| (direction === -1 && zoom <= MIN_MAP_ZOOM)
|
||||
) {
|
||||
activeZoomPointerId = null
|
||||
if (button.hasPointerCapture?.(event.pointerId)) {
|
||||
button.releasePointerCapture(event.pointerId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
zoomHoldDelayTimer = window.setTimeout(() => {
|
||||
zoomHoldDelayTimer = null
|
||||
beginContinuousZoom(direction)
|
||||
}, ZOOM_HOLD_DELAY_MS)
|
||||
}
|
||||
|
||||
function finishZoomHold(direction: -1 | 1, event: PointerEvent) {
|
||||
if (activeZoomPointerId === null || event.pointerId !== activeZoomPointerId) return
|
||||
|
||||
const pointerId = activeZoomPointerId
|
||||
const wasContinuous = zoomHoldStarted
|
||||
activeZoomPointerId = null
|
||||
clearZoomHoldDelay()
|
||||
stopZoomAnimation()
|
||||
zoomHoldStarted = false
|
||||
|
||||
const button = event.currentTarget as HTMLButtonElement
|
||||
if (button.hasPointerCapture?.(pointerId)) {
|
||||
button.releasePointerCapture(pointerId)
|
||||
}
|
||||
|
||||
if (!wasContinuous) {
|
||||
changeZoom(direction)
|
||||
}
|
||||
}
|
||||
|
||||
function cancelZoomHold(event: PointerEvent) {
|
||||
if (activeZoomPointerId === null || event.pointerId !== activeZoomPointerId) return
|
||||
|
||||
const pointerId = activeZoomPointerId
|
||||
activeZoomPointerId = null
|
||||
clearZoomHoldDelay()
|
||||
stopZoomAnimation()
|
||||
zoomHoldStarted = false
|
||||
|
||||
const button = event.currentTarget as HTMLButtonElement
|
||||
if (button.hasPointerCapture?.(pointerId)) {
|
||||
button.releasePointerCapture(pointerId)
|
||||
}
|
||||
}
|
||||
|
||||
function handleZoomClick(direction: -1 | 1, event: MouseEvent) {
|
||||
if (event.detail === 0) {
|
||||
changeZoom(direction)
|
||||
}
|
||||
}
|
||||
|
||||
function toCloudMarker(row: ApiCloud): CloudMarkerData {
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -423,14 +594,16 @@ async function returnRealtime() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('blur', clearZoomHold)
|
||||
try {
|
||||
AMapLib = await loadAMap()
|
||||
AMapLib.getConfig().appname = 'amap-jsapi-skill'
|
||||
mapInst = new AMapLib.Map(mapEl.value!, {
|
||||
viewMode: '3D',
|
||||
pitch: 0,
|
||||
rotation: 0,
|
||||
zoom: 5,
|
||||
zooms: [4, 18],
|
||||
zoom: currentZoom.value,
|
||||
zooms: [MIN_MAP_ZOOM, MAX_MAP_ZOOM],
|
||||
center: [104.07, 30.67],
|
||||
mapStyle: 'amap://styles/normal',
|
||||
features: ['bg', 'road', 'building', 'point'],
|
||||
@@ -440,12 +613,13 @@ onMounted(async () => {
|
||||
} as AMap.MapOptions)
|
||||
|
||||
mapInst.addControl(new AMapLib.Scale())
|
||||
mapInst.addControl(new AMapLib.ToolBar({ position: 'LT' } as Record<string, unknown>))
|
||||
mapInst.addControl(new AMapLib.ControlBar({ position: { right: '50px', bottom: '224px' } } as Record<string, unknown>))
|
||||
mapInst.on('click', () => { previewCloud.value = null; hideHoverCard() })
|
||||
mapInst.on('zoomstart', hideHeader)
|
||||
mapInst.on('zoomchange', syncCurrentZoom)
|
||||
mapInst.on('movestart', hideHeader)
|
||||
mapInst.on('dragstart', hideHeader)
|
||||
syncCurrentZoom()
|
||||
await refresh()
|
||||
startMarkerDecayTimer()
|
||||
} catch (e) {
|
||||
@@ -454,10 +628,13 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('blur', clearZoomHold)
|
||||
clearZoomHold()
|
||||
if (redrawTimer !== null) {
|
||||
window.clearInterval(redrawTimer)
|
||||
redrawTimer = null
|
||||
}
|
||||
mapInst?.off('zoomchange', syncCurrentZoom)
|
||||
mapInst?.destroy()
|
||||
mapInst = null
|
||||
AMapLib = null
|
||||
@@ -468,11 +645,45 @@ onUnmounted(() => {
|
||||
<div class="relative h-[100dvh] min-h-screen">
|
||||
<div ref="mapEl" class="w-full h-full"></div>
|
||||
|
||||
<div
|
||||
class="absolute left-4 top-32 z-20 flex w-10 flex-col gap-2 md:top-20"
|
||||
role="group"
|
||||
:aria-label="`地图缩放控制,当前级别 ${currentZoom.toFixed(2)}`"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="oc-map-button touch-none select-none"
|
||||
title="放大地图(按住可连续放大)"
|
||||
aria-label="放大地图,按住可连续放大"
|
||||
:disabled="currentZoom >= MAX_MAP_ZOOM"
|
||||
@pointerdown="startZoomHold(1, $event)"
|
||||
@pointerup="finishZoomHold(1, $event)"
|
||||
@pointercancel="cancelZoomHold"
|
||||
@lostpointercapture="cancelZoomHold"
|
||||
@click="handleZoomClick(1, $event)"
|
||||
>
|
||||
<NIcon size="20"><Plus /></NIcon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="oc-map-button touch-none select-none"
|
||||
title="缩小地图(按住可连续缩小)"
|
||||
aria-label="缩小地图,按住可连续缩小"
|
||||
:disabled="currentZoom <= MIN_MAP_ZOOM"
|
||||
@pointerdown="startZoomHold(-1, $event)"
|
||||
@pointerup="finishZoomHold(-1, $event)"
|
||||
@pointercancel="cancelZoomHold"
|
||||
@lostpointercapture="cancelZoomHold"
|
||||
@click="handleZoomClick(-1, $event)"
|
||||
>
|
||||
<NIcon size="20"><Minus /></NIcon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-6 right-7 z-20 flex w-10 flex-col items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="w-10 h-10 rounded-lg shadow-md flex items-center justify-center"
|
||||
:class="authStore.loading || !authStore.isLoggedIn ? 'cursor-not-allowed bg-slate-100 text-slate-300 shadow-none' : 'bg-white hover:bg-slate-50'"
|
||||
class="oc-map-button"
|
||||
:title="authStore.isLoggedIn ? '快速上传图片' : '请先登录后上传图片'"
|
||||
aria-label="快速上传图片"
|
||||
:disabled="authStore.loading || !authStore.isLoggedIn"
|
||||
@@ -482,14 +693,14 @@ onUnmounted(() => {
|
||||
<CloudUpload />
|
||||
</NIcon>
|
||||
</button>
|
||||
<button @click="refresh" class="oc-map-button" title="刷新">
|
||||
<button type="button" @click="refresh" class="oc-map-button" title="刷新" aria-label="刷新地图">
|
||||
<span class="text-lg">
|
||||
<NIcon>
|
||||
<Refresh/>
|
||||
</NIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button @click="toggleSat" class="oc-map-button" :title="satelliteOn ? '切换普通视图' : '切换卫星视图'">
|
||||
<button type="button" @click="toggleSat" class="oc-map-button" :title="satelliteOn ? '切换普通视图' : '切换卫星视图'" :aria-label="satelliteOn ? '切换普通视图' : '切换卫星视图'">
|
||||
<NIcon size="20" style="display: inline-flex; vertical-align: middle;">
|
||||
<Map v-if="satelliteOn" />
|
||||
<Satellite v-else />
|
||||
@@ -565,23 +776,25 @@ onUnmounted(() => {
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<p class="text-sm font-semibold text-slate-900">历史位置</p>
|
||||
<button type="button" class="text-xs font-medium text-sky-700 hover:text-sky-900" @click="returnRealtime">
|
||||
<button type="button" class="oc-text-button text-xs" @click="returnRealtime">
|
||||
返回实时
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="border px-3 py-2 text-sm font-medium transition-colors"
|
||||
:class="archiveKind === 'day' ? 'border-teal-200 bg-teal-50 text-teal-800' : 'border-slate-200 bg-white text-slate-600 hover:bg-slate-50'"
|
||||
class="oc-choice-button"
|
||||
:class="archiveKind === 'day' ? 'oc-choice-button--active' : 'oc-choice-button--inactive'"
|
||||
:aria-pressed="archiveKind === 'day'"
|
||||
@click="archiveKind = 'day'"
|
||||
>
|
||||
某天
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="border px-3 py-2 text-sm font-medium transition-colors"
|
||||
:class="archiveKind === 'month' ? 'border-teal-200 bg-teal-50 text-teal-800' : 'border-slate-200 bg-white text-slate-600 hover:bg-slate-50'"
|
||||
class="oc-choice-button"
|
||||
:class="archiveKind === 'month' ? 'oc-choice-button--active' : 'oc-choice-button--inactive'"
|
||||
:aria-pressed="archiveKind === 'month'"
|
||||
@click="archiveKind = 'month'"
|
||||
>
|
||||
某月
|
||||
@@ -603,7 +816,7 @@ onUnmounted(() => {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 w-full border border-sky-200 bg-sky-100 px-3 py-2 text-sm font-medium text-sky-800 shadow-[4px_4px_0_0_rgba(14,165,233,0.14)] transition-colors hover:bg-sky-50"
|
||||
class="oc-panel-button oc-panel-button--sky mt-3 w-full"
|
||||
@click="applyArchiveFilter"
|
||||
>
|
||||
查看拍摄位置
|
||||
|
||||
@@ -167,7 +167,8 @@ async function savePassword() {
|
||||
<NButton
|
||||
secondary
|
||||
strong
|
||||
class="oc-panel-button oc-panel-button--neutral"
|
||||
type="default"
|
||||
class="oc-panel-button oc-panel-button--teal"
|
||||
:loading="passwordSaving"
|
||||
@click="savePassword"
|
||||
>
|
||||
|
||||
@@ -491,7 +491,7 @@ watch(selectedUploadDate, async newValue => {
|
||||
<div v-else class="grid gap-6 lg:grid-cols-[1.25fr_0.95fr] lg:items-start">
|
||||
<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,#ecfeff_0%,#ccfbf1_100%)] text-3xl font-bold text-slate-900 shadow-[4px_4px_0_0_rgba(15,23,42,0.08)]"
|
||||
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)]"
|
||||
>
|
||||
<img
|
||||
v-if="profileData?.avatar_url"
|
||||
@@ -516,7 +516,7 @@ watch(selectedUploadDate, async newValue => {
|
||||
<RouterLink v-if="isOwnProfile" to="/profile/settings">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center border border-slate-300 bg-white text-slate-600 transition-colors hover:border-slate-900 hover:text-slate-900"
|
||||
class="oc-icon-button oc-icon-button--small"
|
||||
title="个人资料设置"
|
||||
aria-label="个人资料设置"
|
||||
>
|
||||
@@ -649,18 +649,20 @@ watch(selectedUploadDate, async newValue => {
|
||||
v-if="isOwnProfile"
|
||||
secondary
|
||||
strong
|
||||
type="default"
|
||||
class="oc-panel-button oc-panel-button--neutral"
|
||||
@click="toggleSelectionMode"
|
||||
>
|
||||
{{ selectionMode ? '取消选择' : '选择图片' }}
|
||||
</NButton>
|
||||
<NButton secondary strong class="oc-panel-button oc-panel-button--neutral" @click="loadProfilePage(true)">
|
||||
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="loadProfilePage(true)">
|
||||
刷新数据
|
||||
</NButton>
|
||||
<NButton
|
||||
v-if="selectedUploadDate"
|
||||
secondary
|
||||
strong
|
||||
type="default"
|
||||
class="oc-panel-button oc-panel-button--neutral"
|
||||
@click="clearSelectedUploadDate"
|
||||
>
|
||||
@@ -706,7 +708,7 @@ watch(selectedUploadDate, async newValue => {
|
||||
:key="item.id"
|
||||
type="button"
|
||||
@click="selectedCloud = item"
|
||||
class="group overflow-hidden border bg-white text-left shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg"
|
||||
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'"
|
||||
>
|
||||
<div class="relative">
|
||||
@@ -773,7 +775,7 @@ watch(selectedUploadDate, async newValue => {
|
||||
<RouterLink v-if="isOwnProfile && !selectedUploadDate" to="/upload">
|
||||
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--sky">去上传第一张</NButton>
|
||||
</RouterLink>
|
||||
<NButton v-else-if="selectedUploadDate" secondary strong class="oc-panel-button oc-panel-button--neutral" @click="clearSelectedUploadDate">返回全部记录</NButton>
|
||||
<NButton v-else-if="selectedUploadDate" secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="clearSelectedUploadDate">返回全部记录</NButton>
|
||||
</template>
|
||||
</NEmpty>
|
||||
</div>
|
||||
@@ -845,7 +847,7 @@ watch(selectedUploadDate, async newValue => {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center border border-slate-200 bg-slate-50 text-slate-600 transition-colors hover:border-slate-900 hover:text-slate-900"
|
||||
class="oc-icon-button"
|
||||
title="图片管理"
|
||||
aria-label="图片管理"
|
||||
>
|
||||
|
||||
@@ -302,9 +302,11 @@ onUnmounted(() => {
|
||||
<img :src="item.preview" alt="" class="w-full h-full object-cover" />
|
||||
<div v-if="Object.keys(item.errors).length > 0" class="absolute top-0 right-0 w-2.5 h-2.5 bg-red-500"></div>
|
||||
<button
|
||||
type="button"
|
||||
@click.stop="handleRemove(item.id)"
|
||||
class="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center bg-red-500 text-xs text-white"
|
||||
style="opacity:0.8"
|
||||
class="oc-icon-button oc-icon-button--danger oc-icon-button--tiny absolute -right-1 -top-1"
|
||||
title="移除图片"
|
||||
aria-label="移除图片"
|
||||
>✕</button>
|
||||
</div>
|
||||
<div
|
||||
@@ -374,7 +376,7 @@ onUnmounted(() => {
|
||||
v-if="activeItem.latitude !== null || activeItem.longitude !== null"
|
||||
type="button"
|
||||
@click="clearCoordinates"
|
||||
class="text-xs font-medium text-slate-400 transition-colors hover:text-slate-600"
|
||||
class="oc-text-button oc-text-button--muted text-xs"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
@@ -408,7 +410,7 @@ onUnmounted(() => {
|
||||
@click="openMapPicker"
|
||||
title="地图选点"
|
||||
aria-label="地图选点"
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg border border-sky-200 bg-sky-50 text-sky-700 transition-colors hover:bg-sky-100"
|
||||
class="oc-icon-button oc-icon-button--sky"
|
||||
>
|
||||
<span class="text-lg leading-none">🗺️</span>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user