Backend: - Add auth.py for JWT token verification - Update main.py to protect all routes with auth middleware - Remove dashboard router (frontend handles aggregation) - Add Docker support with Dockerfile and docker-compose.yml Frontend: - Add OIDC authentication using oidc-client-ts with PKCE flow - Create router.js with auth guards for automatic login/logout - Add api.js for unified Axios instance with auth headers - Add composables: useAuth.js, useVehicleData.js for caching - Add views/Main.vue as main application page - Simplify App.vue to router-view container - Add deploy-web.sh deployment script Documentation: - Update AGENTS.md with new architecture and auth flow
319 lines
9.4 KiB
Vue
319 lines
9.4 KiB
Vue
<template>
|
|
<el-dialog
|
|
v-model="visible"
|
|
title="添加加油记录"
|
|
class="fuel-dialog"
|
|
>
|
|
<el-form :model="form" class="fuel-form" label-width="100px" :label-position="isMobile ? 'top' : 'right'">
|
|
<el-form-item label="日期" class="desktop-only">
|
|
<el-date-picker
|
|
v-model="form.date"
|
|
type="date"
|
|
placeholder="选择日期"
|
|
style="width: 100%"
|
|
value-format="YYYY-MM-DD"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="日期" class="mobile-only">
|
|
<el-date-picker
|
|
v-model="form.date"
|
|
type="date"
|
|
placeholder="选择日期"
|
|
style="width: 100%"
|
|
value-format="YYYY-MM-DD"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="里程 (km)" class="desktop-only">
|
|
<el-input-number v-model="form.mileage" :min="0" style="width: 100%" placeholder="当前里程" />
|
|
</el-form-item>
|
|
<el-form-item label="里程 (km)" class="mobile-only">
|
|
<el-input-number v-model="form.mileage" :min="0" :controls="false" style="width: 100%" placeholder="当前里程" />
|
|
</el-form-item>
|
|
|
|
<!-- 三个核心字段 -->
|
|
<el-form-item label="单价 (¥/L)" class="desktop-only">
|
|
<el-input-number
|
|
v-model="form.fuel_price"
|
|
:min="0"
|
|
:precision="2"
|
|
style="width: 100%"
|
|
placeholder="机显单价"
|
|
@change="onPriceChange"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="单价 (¥/L)" class="mobile-only">
|
|
<el-input-number
|
|
v-model="form.fuel_price"
|
|
:min="0"
|
|
:precision="2"
|
|
:controls="false"
|
|
style="width: 100%"
|
|
placeholder="机显单价"
|
|
@change="onPriceChange"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="加油量 (L)" class="desktop-only">
|
|
<el-input-number
|
|
v-model="form.fuel_amount"
|
|
:min="0"
|
|
:precision="2"
|
|
style="width: 100%"
|
|
placeholder="加油量"
|
|
@change="onAmountChange"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="加油量 (L)" class="mobile-only">
|
|
<el-input-number
|
|
v-model="form.fuel_amount"
|
|
:min="0"
|
|
:precision="2"
|
|
:controls="false"
|
|
style="width: 100%"
|
|
placeholder="加油量"
|
|
@change="onAmountChange"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="机显总价 (¥)" class="desktop-only">
|
|
<el-input-number
|
|
v-model="form.display_cost"
|
|
:min="0"
|
|
:precision="2"
|
|
style="width: 100%"
|
|
placeholder="机显金额(选填,可自动计算)"
|
|
@change="onDisplayCostChange"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="机显总价 (¥)" class="mobile-only">
|
|
<el-input-number
|
|
v-model="form.display_cost"
|
|
:min="0"
|
|
:precision="2"
|
|
:controls="false"
|
|
style="width: 100%"
|
|
placeholder="机显金额(选填,可自动计算)"
|
|
@change="onDisplayCostChange"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<!-- 实付金额 -->
|
|
<el-form-item label="实付金额 (¥)" class="desktop-only">
|
|
<el-input-number v-model="form.actual_cost" :min="0" :precision="2" style="width: 100%" placeholder="实际支付金额(优惠后)" />
|
|
<div class="hint">用于统计实际花费,不填则使用机显总价</div>
|
|
</el-form-item>
|
|
<el-form-item label="实付金额 (¥)" class="mobile-only">
|
|
<el-input-number v-model="form.actual_cost" :min="0" :precision="2" :controls="false" style="width: 100%" placeholder="实际支付金额" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="是否加满" class="desktop-only">
|
|
<el-switch v-model="form.is_full_tank" active-text="是" inactive-text="否" />
|
|
</el-form-item>
|
|
<el-form-item label="是否加满" class="mobile-only">
|
|
<el-switch v-model="form.is_full_tank" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="备注" class="desktop-only">
|
|
<el-input v-model="form.notes" type="textarea" :rows="2" placeholder="备注(可选)" />
|
|
</el-form-item>
|
|
<el-form-item label="备注" class="mobile-only">
|
|
<el-input v-model="form.notes" type="textarea" :rows="2" placeholder="备注(可选)" />
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="visible = false">取消</el-button>
|
|
<el-button type="primary" @click="submit" :loading="loading">确定</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import api from '../../../api.js'
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
const props = defineProps(['visible', 'vehicleId'])
|
|
const emit = defineEmits(['update:visible', 'success'])
|
|
|
|
const API_BASE = 'https://api.yuany3721.site/carcost'
|
|
|
|
const visible = computed({
|
|
get: () => props.visible,
|
|
set: (val) => emit('update:visible', val)
|
|
})
|
|
|
|
const loading = ref(false)
|
|
|
|
// 检测是否为移动端
|
|
const isMobile = ref(false)
|
|
const checkMobile = () => {
|
|
isMobile.value = window.innerWidth <= 768
|
|
}
|
|
onMounted(() => {
|
|
checkMobile()
|
|
window.addEventListener('resize', checkMobile)
|
|
})
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', checkMobile)
|
|
})
|
|
|
|
const form = ref({
|
|
date: new Date().toISOString().split('T')[0],
|
|
mileage: 0,
|
|
fuel_amount: null,
|
|
fuel_price: null,
|
|
display_cost: null,
|
|
actual_cost: null,
|
|
is_full_tank: true,
|
|
notes: ''
|
|
})
|
|
|
|
// 标记哪个字段是最后修改的,避免循环计算
|
|
let lastModified = null
|
|
|
|
// 单价变化:如果加油量有值,重新计算总价
|
|
const onPriceChange = () => {
|
|
lastModified = 'price'
|
|
if (form.value.fuel_price && form.value.fuel_amount) {
|
|
form.value.display_cost = Math.round(form.value.fuel_price * form.value.fuel_amount * 100) / 100
|
|
}
|
|
}
|
|
|
|
// 加油量变化:根据已有数据计算缺失的
|
|
const onAmountChange = () => {
|
|
lastModified = 'amount'
|
|
if (form.value.fuel_amount && form.value.fuel_price) {
|
|
// 有单价,计算总价
|
|
form.value.display_cost = Math.round(form.value.fuel_price * form.value.fuel_amount * 100) / 100
|
|
} else if (form.value.fuel_amount && form.value.display_cost) {
|
|
// 有总价,计算单价
|
|
form.value.fuel_price = Math.round(form.value.display_cost / form.value.fuel_amount * 100) / 100
|
|
}
|
|
}
|
|
|
|
// 总价变化:根据已有数据计算缺失的
|
|
const onDisplayCostChange = () => {
|
|
lastModified = 'cost'
|
|
if (form.value.display_cost && form.value.fuel_price) {
|
|
// 有单价,计算加油量
|
|
form.value.fuel_amount = Math.round(form.value.display_cost / form.value.fuel_price * 100) / 100
|
|
} else if (form.value.display_cost && form.value.fuel_amount) {
|
|
// 有加油量,计算单价
|
|
form.value.fuel_price = Math.round(form.value.display_cost / form.value.fuel_amount * 100) / 100
|
|
}
|
|
}
|
|
|
|
const submit = async () => {
|
|
if (!props.vehicleId) {
|
|
ElMessage.warning('请先选择车辆')
|
|
return
|
|
}
|
|
if (!form.value.date || !form.value.mileage) {
|
|
ElMessage.warning('请填写日期和里程')
|
|
return
|
|
}
|
|
|
|
// 检查必填字段
|
|
const hasPrice = form.value.fuel_price !== null && form.value.fuel_price > 0
|
|
const hasAmount = form.value.fuel_amount !== null && form.value.fuel_amount > 0
|
|
|
|
if (!hasPrice || !hasAmount) {
|
|
ElMessage.warning('请填写单价和加油量')
|
|
return
|
|
}
|
|
|
|
// 如果机显总价为空,自动计算
|
|
let displayCost = form.value.display_cost
|
|
if (!displayCost || displayCost <= 0) {
|
|
displayCost = Math.round(form.value.fuel_price * form.value.fuel_amount * 100) / 100
|
|
}
|
|
|
|
// 实付金额默认使用机显总价
|
|
const actualCost = form.value.actual_cost !== null && form.value.actual_cost > 0
|
|
? form.value.actual_cost
|
|
: displayCost
|
|
|
|
loading.value = true
|
|
try {
|
|
await api.post(`${API_BASE}/fuel-records/create`, {
|
|
vehicle_id: props.vehicleId,
|
|
date: form.value.date,
|
|
mileage: form.value.mileage,
|
|
fuel_amount: form.value.fuel_amount,
|
|
fuel_price: form.value.fuel_price,
|
|
display_cost: displayCost, // 机显总价
|
|
actual_cost: actualCost, // 实付金额
|
|
is_full_tank: form.value.is_full_tank,
|
|
notes: form.value.notes || ''
|
|
})
|
|
ElMessage.success('添加成功')
|
|
visible.value = false
|
|
// 重置表单
|
|
form.value = {
|
|
date: new Date().toISOString().split('T')[0],
|
|
mileage: 0,
|
|
fuel_amount: null,
|
|
fuel_price: null,
|
|
display_cost: null,
|
|
actual_cost: null,
|
|
is_full_tank: true,
|
|
notes: ''
|
|
}
|
|
lastModified = null
|
|
emit('success')
|
|
} catch (error) {
|
|
ElMessage.error('添加失败')
|
|
console.error(error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.hint {
|
|
font-size: 12px;
|
|
color: #909399;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* 默认隐藏移动端 */
|
|
.mobile-only {
|
|
display: none;
|
|
}
|
|
|
|
/* 移动端适配 */
|
|
@media (max-width: 768px) {
|
|
:deep(.el-dialog.fuel-dialog) {
|
|
width: 90% !important;
|
|
max-width: 350px;
|
|
}
|
|
|
|
.desktop-only {
|
|
display: none;
|
|
}
|
|
|
|
.mobile-only {
|
|
display: block;
|
|
}
|
|
|
|
:deep(.fuel-form .el-form-item) {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
:deep(.fuel-form .mobile-only.el-form-item .el-form-item__content) {
|
|
margin-left: 0 !important;
|
|
width: 100% !important;
|
|
}
|
|
|
|
:deep(.fuel-form .el-form-item__content) {
|
|
margin-left: 0 !important;
|
|
}
|
|
|
|
:deep(.fuel-form) {
|
|
padding: 0 10px;
|
|
}
|
|
}
|
|
</style>
|