carcost/web/src/App.vue
2026-04-10 23:24:51 +08:00

452 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app">
<el-container>
<!-- 头部 -->
<el-header class="header">
<div class="header-inner">
<div class="header-content">
<h1 class="app-title" @click="mobileView = 'dashboard'">🚗 CarCost</h1>
<!-- 桌面端车辆选择 + 添加按钮 -->
<div class="desktop-nav">
<el-select
v-model="selectedVehicle"
placeholder="选择车辆"
style="width: 180px"
@change="onVehicleChange"
>
<el-option
v-for="vehicle in vehicles"
:key="vehicle.id"
:label="vehicle.name"
:value="vehicle.id"
/>
</el-select>
<el-divider direction="vertical" />
<el-button @click="showAddVehicle = true" title="添加车辆">
<el-icon><Plus /></el-icon> 车辆
</el-button>
<el-button v-if="selectedVehicle" @click="editCurrentVehicle" title="编辑车辆">
<el-icon><Edit /></el-icon> 编辑
</el-button>
<el-divider direction="vertical" />
<el-button @click="showAddFuel = true" title="添加加油记录">
<el-icon><Plus /></el-icon> 加油
</el-button>
<el-button @click="showAddCost = true" title="添加费用记录">
<el-icon><Plus /></el-icon> 费用
</el-button>
</div>
<!-- 移动端:车辆选择 + 添加按钮 + 汉堡菜单 -->
<div class="mobile-nav">
<el-dropdown trigger="click" v-if="vehicles.length > 0">
<el-button>
{{ selectedVehicleName || '选择车辆' }}
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="vehicle in vehicles"
:key="vehicle.id"
@click="selectVehicle(vehicle.id)"
>
{{ vehicle.name }}
</el-dropdown-item>
<el-dropdown-item divided @click="showAddVehicle = true">+ 添加车辆</el-dropdown-item>
<el-dropdown-item v-if="selectedVehicle" @click="editCurrentVehicle">✏️ 编辑车辆</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button v-if="mobileView === 'fuel'" @click="showAddFuel = true">
<el-icon><Plus /></el-icon>
</el-button>
<el-button v-if="mobileView === 'cost'" @click="showAddCost = true">
<el-icon><Plus /></el-icon>
</el-button>
<el-dropdown trigger="click">
<el-button>
<el-icon><Menu /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="mobileView = 'fuel'">⛽ 加油记录</el-dropdown-item>
<el-dropdown-item @click="mobileView = 'cost'">💰 费用记录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
</el-header>
<el-main>
<div class="content-wrapper">
<!-- 移动端:车辆选择 -->
<div class="mobile-vehicle-select">
<el-select
v-model="selectedVehicle"
placeholder="选择车辆"
style="width: 100%"
@change="onVehicleChange"
>
<el-option
v-for="vehicle in vehicles"
:key="vehicle.id"
:label="vehicle.name"
:value="vehicle.id"
/>
</el-select>
</div>
<!-- 桌面端布局 -->
<div class="desktop-layout">
<!-- 上方统计卡片 -->
<StatsCards :dashboard-data="dashboardData" :total-cost="totalCost" />
<!-- 四个图表 -->
<DashboardCharts :vehicle-id="selectedVehicle" />
<!-- 下方左右两栏:列表 -->
<el-row :gutter="20" class="records-row">
<el-col :span="12">
<FuelRecordsPanel
:vehicle-id="selectedVehicle"
:show-charts="false"
:show-list="true"
@add-fuel="showAddFuel = true"
/>
</el-col>
<el-col :span="12">
<CostRecordsPanel
:vehicle-id="selectedVehicle"
:show-charts="false"
:show-list="true"
@add-cost="showAddCost = true"
/>
</el-col>
</el-row>
</div>
<!-- 移动端布局 -->
<div class="mobile-layout">
<!-- 仪表盘 -->
<div v-if="mobileView === 'dashboard'">
<StatsCards :dashboard-data="dashboardData" :total-cost="totalCost" />
<!-- 移动端图表 -->
<DashboardCharts :vehicle-id="selectedVehicle" />
</div>
<!-- 加油记录 -->
<div v-if="mobileView === 'fuel'">
<FuelRecordsPanel
:vehicle-id="selectedVehicle"
:show-charts="false"
:show-list="true"
@add-fuel="showAddFuel = true"
/>
</div>
<!-- 费用记录 -->
<div v-if="mobileView === 'cost'">
<CostRecordsPanel
:vehicle-id="selectedVehicle"
:show-charts="false"
:show-list="true"
@add-cost="showAddCost = true"
/>
</div>
</div>
</div>
</el-main>
</el-container>
<!-- 弹窗 -->
<AddVehicleDialog v-model:visible="showAddVehicle" @success="loadVehicles" />
<EditVehicleDialog v-model:visible="showEditVehicle" :vehicle="currentVehicle" @success="loadVehicles" />
<AddFuelDialog v-model:visible="showAddFuel" :vehicle-id="selectedVehicle" @success="onRecordAdded" />
<AddCostDialog v-model:visible="showAddCost" :vehicle-id="selectedVehicle" @success="onRecordAdded" />
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { Menu, ArrowDown, Plus, Edit } from '@element-plus/icons-vue'
import AddVehicleDialog from './components/dialogs/vehicle/AddVehicleDialog.vue'
import EditVehicleDialog from './components/dialogs/vehicle/EditVehicleDialog.vue'
import AddFuelDialog from './components/dialogs/fuel/AddFuelDialog.vue'
import AddCostDialog from './components/dialogs/cost/AddCostDialog.vue'
import StatsCards from './components/cards/StatsCards.vue'
import DashboardCharts from './components/charts/DashboardCharts.vue'
import FuelRecordsPanel from './components/panels/FuelRecordsPanel.vue'
import CostRecordsPanel from './components/panels/CostRecordsPanel.vue'
const API_BASE = 'https://api.yuany3721.site/carcost'
const vehicles = ref([])
const selectedVehicle = ref(null)
const dashboardData = ref(null)
const totalCost = ref(0)
const mobileView = ref('dashboard') // 'dashboard', 'fuel', 'cost'
// 当前选中车辆名称
const selectedVehicleName = computed(() => {
const vehicle = vehicles.value.find(v => v.id === selectedVehicle.value)
return vehicle?.name || ''
})
const showAddVehicle = ref(false)
const showAddFuel = ref(false)
const showAddCost = ref(false)
const showEditVehicle = ref(false)
const currentVehicle = ref(null)
// 加载车辆列表
const loadVehicles = async () => {
try {
const res = await axios.get(`${API_BASE}/vehicles/list`)
vehicles.value = res.data
if (vehicles.value.length > 0 && !selectedVehicle.value) {
selectedVehicle.value = vehicles.value[0].id
loadDashboard()
}
} catch (error) {
ElMessage.error('加载车辆失败')
}
}
// 加载仪表盘数据
const loadDashboard = async () => {
if (!selectedVehicle.value) return
try {
const res = await axios.get(`${API_BASE}/dashboard/data?vehicle_id=${selectedVehicle.value}`)
dashboardData.value = res.data
// 加载总费用
const costRes = await axios.get(`${API_BASE}/costs/list?vehicle_id=${selectedVehicle.value}&limit=1000`)
const costs = costRes.data || []
totalCost.value = costs.reduce((sum, c) => sum + c.amount, 0)
} catch (error) {
ElMessage.error('加载数据失败')
}
}
// 切换车辆
const onVehicleChange = () => {
loadDashboard()
}
// 移动端选择车辆
const selectVehicle = (id) => {
selectedVehicle.value = id
onVehicleChange()
}
// 编辑当前车辆
const editCurrentVehicle = () => {
if (!selectedVehicle.value) return
currentVehicle.value = vehicles.value.find(v => v.id === selectedVehicle.value)
showEditVehicle.value = true
}
// 记录添加成功后刷新
const onRecordAdded = () => {
loadDashboard()
}
onMounted(() => {
loadVehicles()
})
</script>
<style>
/* 全局样式,去掉所有默认 margin/padding */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
overflow-x: hidden;
}
</style>
<style scoped>
.app {
min-height: 100vh;
background: #f5f7fa;
width: 100%;
}
.content-wrapper {
max-width: 900px;
margin: 0 auto;
width: 100%;
}
.header {
background: #fff;
border-bottom: 1px solid #e4e7ed;
padding: 0;
}
.header-inner {
max-width: 900px;
margin: 0 auto;
width: 100%;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 60px;
padding: 0 20px;
}
.header h1 {
margin: 0;
font-size: 20px;
color: #303133;
}
.app-title {
cursor: pointer;
}
.app-title:hover {
opacity: 0.8;
}
.desktop-nav {
display: flex;
align-items: center;
gap: 10px;
}
.mobile-nav {
display: none;
}
.mobile-vehicle-select {
display: none;
margin-bottom: 15px;
}
.desktop-layout {
display: block;
width: 100%;
overflow-x: hidden;
}
.mobile-layout {
display: none;
width: 100%;
overflow-x: hidden;
}
.records-row {
margin: 0 -10px !important;
width: calc(100% + 20px);
display: flex;
align-items: stretch;
}
.records-row :deep(.el-col) {
padding-left: 10px !important;
padding-right: 10px !important;
display: flex;
}
.records-row :deep(.el-col > *) {
flex: 1;
}
.mobile-charts {
margin-top: 15px;
}
.mobile-tip {
margin-top: 20px;
}
/* 调整 main 的 padding */
:deep(.el-main) {
padding: 20px !important;
}
@media (max-width: 768px) {
:deep(.el-main) {
padding: 10px !important;
}
}
:deep(.el-container) {
padding: 0 !important;
}
/* 移动端内容区域 */
.mobile-layout {
padding: 0;
}
@media (max-width: 768px) {
:deep(.el-main) {
padding: 0 !important;
}
}
/* 弹窗默认样式(电脑端) */
:deep(.el-dialog) {
width: 450px !important;
}
/* 移动端弹窗适配 */
@media (max-width: 768px) {
:deep(.el-dialog) {
width: 90% !important;
max-width: 350px !important;
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.desktop-nav {
display: none;
}
.mobile-nav {
display: flex;
align-items: center;
gap: 8px;
}
.mobile-vehicle-select {
display: none;
}
.desktop-layout {
display: none;
}
.mobile-layout {
display: block;
}
.header-content {
padding: 0 10px;
}
.header h1 {
font-size: 18px;
}
}
</style>