This commit is contained in:
Innei
2025-10-29 23:10:19 +08:00
parent dbd97d13f3
commit 9c61fed159
6 changed files with 96 additions and 55 deletions

View File

@@ -4,6 +4,7 @@ import {
FormHelperText,
Input,
Label,
LinearDivider,
ScrollArea,
Select,
SelectContent,
@@ -96,7 +97,7 @@ export const ProviderEditModal = ({
return (
<div className="flex h-full max-h-[85vh] flex-col">
{/* Header */}
<div className="shrink-0 space-y-3 px-6 pt-6">
<div className="shrink-0 space-y-3 px-6 pt-6 relative">
<div className="flex items-start gap-3">
<div
className={clsxm(
@@ -120,9 +121,7 @@ export const ProviderEditModal = ({
</p>
</div>
</div>
{/* Horizontal divider */}
<div className="via-text/20 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
<LinearDivider className="absolute bottom-0 right-0 left-0" />
</div>
{/* Scrollable Content */}
@@ -247,7 +246,8 @@ export const ProviderEditModal = ({
</div>
{/* Footer */}
<div className="shrink-0 px-6 pt-4 pb-6 border-t">
<div className="shrink-0 px-6 pt-4 pb-6 relative">
<LinearDivider className="absolute top-0 right-0 left-0" />
{isNewProvider ? (
// Add mode: Simple cancel + create actions
<div className="flex items-center justify-end gap-2">

View File

@@ -1,6 +1,5 @@
import { Button, ScrollArea } from '@afilmory/ui'
import { Spring } from '@afilmory/utils'
import { m } from 'motion/react'
import { clsxm } from '@afilmory/utils'
import { useState } from 'react'
import { NavLink, Outlet } from 'react-router'
@@ -36,7 +35,7 @@ export const Component = () => {
{/* Top Navigation - Sharp Edges Design */}
<nav className="bg-background-tertiary relative shrink-0 px-6 py-3">
{/* Bottom border with gradient */}
<div className="via-text/20 absolute right-0 bottom-0 left-0 h-[0.5px] bg-gradient-to-r from-transparent to-transparent" />
<div className="via-text/20 absolute right-0 bottom-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
<div className="flex items-center gap-6">
{/* Logo/Brand */}
@@ -47,20 +46,11 @@ export const Component = () => {
{navigationTabs.map((tab) => (
<NavLink key={tab.path} to={tab.path} end={tab.path === '/'}>
{({ isActive }) => (
<m.div
className="relative overflow-hidden rounded-lg px-3 py-1.5"
initial={false}
animate={{
backgroundColor: isActive
? 'color-mix(in srgb, var(--color-accent) 12%, transparent)'
: 'transparent',
}}
whileHover={{
backgroundColor: isActive
? 'color-mix(in srgb, var(--color-accent) 12%, transparent)'
: 'color-mix(in srgb, var(--color-fill) 60%, transparent)',
}}
transition={Spring.presets.snappy}
<div
className={clsxm(
'relative overflow-hidden rounded-lg px-3 py-1.5',
isActive ? 'bg-accent/10' : 'bg-transparent',
)}
>
<span
className="relative z-10 text-[13px] font-medium transition-colors"
@@ -72,7 +62,7 @@ export const Component = () => {
>
{tab.label}
</span>
</m.div>
</div>
)}
</NavLink>
))}

View File

@@ -9,8 +9,8 @@ import {
MainPageLayout,
useMainPageLayout,
} from '~/components/layouts/MainPageLayout'
import { runPhotoSync } from '~/modules/photos'
import type { PhotoSyncAction, PhotoSyncResult } from '~/modules/photos'
import { runPhotoSync } from '~/modules/photos'
type PhotoSyncActionsProps = {
onCompleted: (result: PhotoSyncResult, context: { dryRun: boolean }) => void
@@ -18,7 +18,9 @@ type PhotoSyncActionsProps = {
const PhotoSyncActions = ({ onCompleted }: PhotoSyncActionsProps) => {
const { setHeaderActionState } = useMainPageLayout()
const [pendingMode, setPendingMode] = useState<'dry-run' | 'apply' | null>(null)
const [pendingMode, setPendingMode] = useState<'dry-run' | 'apply' | null>(
null,
)
const mutation = useMutation({
mutationFn: async (variables: { dryRun: boolean }) => {
@@ -37,7 +39,8 @@ const PhotoSyncActions = ({ onCompleted }: PhotoSyncActionsProps) => {
})
},
onError: (error) => {
const message = error instanceof Error ? error.message : '照片同步失败,请稍后重试。'
const message =
error instanceof Error ? error.message : '照片同步失败,请稍后重试。'
toast.error('同步失败', { description: message })
},
onSettled: () => {
@@ -52,7 +55,7 @@ const PhotoSyncActions = ({ onCompleted }: PhotoSyncActionsProps) => {
}
}, [setHeaderActionState])
const isPending = mutation.isPending
const { isPending } = mutation
const handleSync = (dryRun: boolean) => {
mutation.mutate({ dryRun })
@@ -62,7 +65,7 @@ const PhotoSyncActions = ({ onCompleted }: PhotoSyncActionsProps) => {
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
variant="ghost"
size="sm"
disabled={isPending}
isLoading={isPending && pendingMode === 'dry-run'}
@@ -112,13 +115,18 @@ const SummaryCard = ({ label, value, tone }: SummaryCardProps) => {
return (
<div className="bg-background-tertiary relative overflow-hidden rounded-lg p-5">
<BorderOverlay />
<p className="text-text-tertiary text-xs uppercase tracking-wide">{label}</p>
<p className="text-text-tertiary text-xs uppercase tracking-wide">
{label}
</p>
<p className={`mt-2 text-2xl font-semibold ${toneClass}`}>{value}</p>
</div>
)
}
const actionTypeConfig: Record<PhotoSyncAction['type'], { label: string; badgeClass: string }> = {
const actionTypeConfig: Record<
PhotoSyncAction['type'],
{ label: string; badgeClass: string }
> = {
insert: { label: '新增', badgeClass: 'bg-emerald-500/10 text-emerald-400' },
update: { label: '更新', badgeClass: 'bg-sky-500/10 text-sky-400' },
delete: { label: '删除', badgeClass: 'bg-rose-500/10 text-rose-400' },
@@ -145,7 +153,9 @@ const ActionRow = ({ action }: { action: PhotoSyncAction }) => {
>
{config.label}
</span>
<code className="text-text-secondary text-xs">{action.storageKey}</code>
<code className="text-text-secondary text-xs">
{action.storageKey}
</code>
</div>
<span className="text-text-tertiary text-xs">
{action.applied ? '已应用' : '未应用'}
@@ -164,7 +174,40 @@ type PhotoSyncResultPanelProps = {
lastWasDryRun: boolean | null
}
const PhotoSyncResultPanel = ({ result, lastWasDryRun }: PhotoSyncResultPanelProps) => {
const PhotoSyncResultPanel = ({
result,
lastWasDryRun,
}: PhotoSyncResultPanelProps) => {
const summaryItems = useMemo(
() =>
result
? [
{ label: '存储对象', value: result.summary.storageObjects },
{ label: '数据库记录', value: result.summary.databaseRecords },
{
label: '新增照片',
value: result.summary.inserted,
tone: 'accent' as const,
},
{ label: '更新记录', value: result.summary.updated },
{ label: '删除记录', value: result.summary.deleted },
{
label: '冲突条目',
value: result.summary.conflicts,
tone:
result.summary.conflicts > 0
? ('warning' as const)
: ('muted' as const),
},
{
label: '跳过条目',
value: result.summary.skipped,
tone: 'muted' as const,
},
]
: [],
[result],
)
if (!result) {
return (
<div className="bg-background-tertiary relative overflow-hidden rounded-lg p-6">
@@ -179,23 +222,6 @@ const PhotoSyncResultPanel = ({ result, lastWasDryRun }: PhotoSyncResultPanelPro
)
}
const summaryItems = useMemo(
() => [
{ label: '存储对象', value: result.summary.storageObjects },
{ label: '数据库记录', value: result.summary.databaseRecords },
{ label: '新增照片', value: result.summary.inserted, tone: 'accent' as const },
{ label: '更新记录', value: result.summary.updated },
{ label: '删除记录', value: result.summary.deleted },
{
label: '冲突条目',
value: result.summary.conflicts,
tone: result.summary.conflicts > 0 ? ('warning' as const) : ('muted' as const),
},
{ label: '跳过条目', value: result.summary.skipped, tone: 'muted' as const },
],
[result.summary],
)
return (
<div className="space-y-6">
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
@@ -209,7 +235,9 @@ const PhotoSyncResultPanel = ({ result, lastWasDryRun }: PhotoSyncResultPanelPro
: '最近一次同步结果已写入数据库。'}
</p>
</div>
<p className="text-text-tertiary text-xs">{result.actions.length}</p>
<p className="text-text-tertiary text-xs">
{result.actions.length}
</p>
</div>
<div className="grid gap-4 md:grid-cols-3 xl:grid-cols-4">
@@ -220,7 +248,11 @@ const PhotoSyncResultPanel = ({ result, lastWasDryRun }: PhotoSyncResultPanelPro
animate={{ opacity: 1, y: 0 }}
transition={{ ...Spring.presets.smooth, delay: index * 0.04 }}
>
<SummaryCard label={item.label} value={item.value} tone={item.tone} />
<SummaryCard
label={item.label}
value={item.value}
tone={item.tone}
/>
</m.div>
))}
</div>
@@ -231,12 +263,16 @@ const PhotoSyncResultPanel = ({ result, lastWasDryRun }: PhotoSyncResultPanelPro
<div className="flex flex-wrap items-center justify-between gap-3">
<h3 className="text-text text-base font-semibold"></h3>
<span className="text-text-tertiary text-xs">
{lastWasDryRun ? '预览模式 · 未应用变更' : '实时模式 · 结果已写入'}
{lastWasDryRun
? '预览模式 · 未应用变更'
: '实时模式 · 结果已写入'}
</span>
</div>
{result.actions.length === 0 ? (
<p className="text-text-tertiary mt-4 text-sm"></p>
<p className="text-text-tertiary mt-4 text-sm">
</p>
) : (
<div className="mt-4 space-y-3">
{result.actions.slice(0, 20).map((action, index) => (
@@ -250,7 +286,9 @@ const PhotoSyncResultPanel = ({ result, lastWasDryRun }: PhotoSyncResultPanelPro
</m.div>
))}
{result.actions.length > 20 ? (
<p className="text-text-tertiary text-xs"> 20 使 API </p>
<p className="text-text-tertiary text-xs">
20 使 API
</p>
) : null}
</div>
)}
@@ -282,4 +320,3 @@ export const Component = () => {
</MainPageLayout>
)
}

View File

@@ -0,0 +1,12 @@
import { clsxm } from '@afilmory/utils'
export const LinearDivider: Component = ({ className }) => {
return (
<div
className={clsxm(
'via-text/20 h-[0.5px] bg-linear-to-r from-transparent to-transparent',
className,
)}
/>
)
}

View File

@@ -0,0 +1 @@
export { LinearDivider } from './LinearDivider'

View File

@@ -4,6 +4,7 @@ export * from './checkbox'
export * from './collapsible'
export * from './context-menu'
export * from './dialog'
export * from './divider'
export * from './dropdown-menu'
export * from './form'
export * from './hover-card'