carcost/web/src/components/dialogs/fuel/AddFuelDialog.vue
yuany3721 71e11eaf30 feat: Add OIDC authentication with Authentik and refactor project structure
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
2026-04-12 13:31:27 +08:00

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>