452 lines
12 KiB
Vue
452 lines
12 KiB
Vue
<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>
|