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.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user