Files
paper-doc/frontend/src/views/HomeView.vue
T
govin 5128e1551c feat: scaffold FastAPI backend and Vue 3 frontend
Backend (backend/):
- FastAPI app with layered app/{api,core,crud,db,models,schemas} structure
- TiDB connection via SQLAlchemy. The URL is built with URL.create rather
  than string formatting: the password contains '@', which would otherwise be
  parsed as the user/host separator and silently truncate the credential.
- Alembic environment wired to Base.metadata and the app settings, so
  backend/.env stays the single source of truth for credentials
- /api/health endpoint reporting database reachability
- models/ and crud/ are intentionally empty: no model classes are defined and
  no tables are created, at import time or otherwise

Frontend (frontend/):
- Vue 3 + Vite + TypeScript scaffold (create-vue, --bare)
- axios instance with a normalized error shape
- vue-router with a home route and a catch-all 404
- pinia + pinia-plugin-persistedstate; the app store persists selected keys
- Element Plus with its icons registered globally
- dev proxy forwards /api to the FastAPI service on port 8000

The database paper_doc was created in TiDB out of band; no table exists yet.
2026-09-17 12:51:37 +08:00

101 lines
2.6 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { fetchHealth, type HealthResponse } from '@/api/health'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
const health = ref<HealthResponse | null>(null)
const loading = ref(false)
const databaseTagType = computed(() => (health.value?.database === 'ok' ? 'success' : 'danger'))
async function loadHealth(): Promise<void> {
loading.value = true
try {
health.value = await fetchHealth()
appStore.markChecked()
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
ElMessage.error(message)
health.value = null
} finally {
loading.value = false
}
}
onMounted(loadHealth)
</script>
<template>
<div class="home">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>Backend connectivity</span>
<el-button :icon="Refresh" :loading="loading" @click="loadHealth">Re-check</el-button>
</div>
</template>
<el-descriptions v-if="health" :column="1" border>
<el-descriptions-item label="API">
{{ health.app }}
<el-tag type="success" size="small">{{ health.status }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="TiDB">
<el-tag :type="databaseTagType" size="small">{{ health.database }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="Target">
<code>{{ health.database_target }}</code>
</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="No response from the API yet" />
</el-card>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>Persisted state</span>
<el-switch
v-model="appStore.sidebarCollapsed"
active-text="Sidebar collapsed"
inline-prompt
/>
</div>
</template>
<p>
Last probe:
<strong>{{ appStore.lastCheckedAt ?? 'never' }}</strong>
</p>
<p class="hint">
Both values are stored in <code>localStorage</code> under
<code>paper-doc:app</code>. Reload the page they survive.
</p>
</el-card>
</div>
</template>
<style scoped>
.home {
display: flex;
flex-direction: column;
gap: 16px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.hint {
color: var(--el-text-color-secondary);
font-size: 13px;
margin: 0;
}
</style>