Commit 269c9325 authored by 375242562@qq.com's avatar 375242562@qq.com

feat: 同步运行状态持久化 — 刷新/切换页面后按钮仍保持禁用

- SyncSource 模型新增 is_running 字段(SQLite 已迁移)
- sync_service: 同步开始前设 is_running=True,try/finally 确保完成或报错后置 False
- SyncSourceResponse 返回 is_running 字段
- 前端: 按钮禁用以 source.is_running(后端状态)为准,而非本地临时状态
  有任务运行时每3秒自动轮询刷新,完成后停止轮询
  禁用状态下 Tooltip 提示「同步中,请等待完成...」
parent 92ccc7c8
......@@ -37,6 +37,7 @@ class SyncSourceResponse(BaseModel):
connection_profile_name: Optional[str]
sync_config: Optional[dict]
is_active: bool
is_running: bool = False
sync_mode: str
last_sync_at: Optional[str]
last_sync_count: Optional[int]
......@@ -58,6 +59,7 @@ async def _to_response(s: SyncSource, db: AsyncSession) -> SyncSourceResponse:
connection_profile_name=profile_name,
sync_config=s.sync_config,
is_active=s.is_active,
is_running=s.is_running or False,
sync_mode=s.sync_mode or "full",
last_sync_at=s.last_sync_at.isoformat() if s.last_sync_at else None,
last_sync_count=s.last_sync_count,
......
......@@ -21,6 +21,7 @@ class SyncSource(Base):
auth_config: Mapped[dict] = mapped_column(JSON, default=dict)
connection_type: Mapped[str] = mapped_column(String(20), default="http")
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
is_running: Mapped[bool] = mapped_column(Boolean, default=False) # True while sync is in progress
sync_mode: Mapped[str] = mapped_column(String(20), default="full") # "full" | "incremental"
last_sync_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
last_sync_count: Mapped[int] = mapped_column(Integer, default=0, nullable=True)
......
......@@ -107,9 +107,7 @@ async def sync_source(source_id: str, db: AsyncSession) -> dict:
base_url = source.base_url
auth_config = source.auth_config or {}
# Get sync config with field mappings
sync_config = source.sync_config or {}
field_mappings = sync_config.get("field_mappings", [])
adapter = get_adapter(
source_type=source.source_type,
......@@ -119,6 +117,25 @@ async def sync_source(source_id: str, db: AsyncSession) -> dict:
sync_config=sync_config,
)
# Mark source as running — persisted so UI survives page refresh
source.is_running = True
await db.commit()
try:
return await _do_sync(source, source_id, adapter, db)
finally:
# Always clear running flag, even if an exception is raised
source = await db.get(SyncSource, source_id)
if source:
source.is_running = False
await db.commit()
async def _do_sync(source: SyncSource, source_id: str, adapter, db: AsyncSession) -> dict:
"""Inner sync logic, called after is_running is set."""
sync_config = source.sync_config or {}
field_mappings = sync_config.get("field_mappings", [])
# Incremental sync: only fetch records updated since last_sync_at
since = source.last_sync_at if (source.sync_mode == "incremental" and source.last_sync_at) else None
patients_data: list[PatientSyncData] = await adapter.fetch_patients(since=since)
......
......@@ -873,6 +873,14 @@ export function DataSyncPage() {
const [batches, setBatches] = useState<SyncBatch[]>([])
const [deletingBatch, setDeletingBatch] = useState<string | null>(null)
// 轮询:当有任务正在运行时,每3秒刷新一次任务列表
useEffect(() => {
const hasRunning = sources.some(s => s.is_running)
if (!hasRunning) return
const timer = setInterval(() => { loadData() }, 3000)
return () => clearInterval(timer)
}, [sources, loadData])
// 加载数据
const loadData = useCallback(async () => {
try {
......@@ -1054,14 +1062,16 @@ export function DataSyncPage() {
<ListItemSecondaryAction>
<Stack direction="row" spacing={0.5}>
<Tooltip title="立即同步">
<IconButton
color="primary"
onClick={() => handleRunSync(source)}
disabled={isSyncing || !source.is_active}
>
{isSyncing ? <CircularProgress size={20} /> : <PlayArrowIcon />}
</IconButton>
<Tooltip title={source.is_running ? '同步中,请等待完成...' : '立即同步'}>
<span>
<IconButton
color="primary"
onClick={() => handleRunSync(source)}
disabled={isSyncing || source.is_running || !source.is_active}
>
{(isSyncing || source.is_running) ? <CircularProgress size={20} /> : <PlayArrowIcon />}
</IconButton>
</span>
</Tooltip>
<Tooltip title="编辑">
<IconButton onClick={() => { setEditSource(source); setTaskDialogOpen(true) }}>
......
......@@ -41,6 +41,7 @@ export interface SyncSource {
connection_profile_name?: string
sync_config?: SyncConfig
is_active: boolean
is_running: boolean
sync_mode: string // 'full' | 'incremental'
last_sync_at?: string
last_sync_count?: number
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment