feat:增加汽车事件模块

This commit is contained in:
2026-07-20 23:19:14 +08:00
parent 6ff52e9d50
commit 4b5f853603
14 changed files with 509 additions and 13 deletions

29
src/apis/car.ts Normal file
View File

@@ -0,0 +1,29 @@
import { cloudService } from './index'
import { ICarEvent, ICarEventQuery } from '@/types/car.ts'
export const addCarEventApi = (carEvent: ICarEvent): Promise<boolean> =>
cloudService({
url: '/home-api/car',
method: 'post',
data: carEvent
})
export const updateCarEventApi = (id: number, carEvent: ICarEvent): Promise<boolean> =>
cloudService({
url: `/home-api/car/${id}`,
method: 'put',
data: carEvent
})
export const deleteCarEventApi = (id: number): Promise<boolean> =>
cloudService({
url: `/home-api/car/${id}`,
method: 'delete'
})
export const queryCarEventListApi = (query: ICarEventQuery): Promise<ICarEvent[]> =>
cloudService({
url: '/home-api/car',
method: 'get',
params: query
})

View File

@@ -0,0 +1,108 @@
<template>
<div class="life-record-card card-item">
<el-image :src="`${props.carItem?.imageUrl}${ThumbImageQuery}`"
fit="contain" alt="暂无图片" @click="viewImageClick">
<template #error>
<div class="image-slot">
<span>暂无图片</span>
</div>
</template>
</el-image>
<van-swipe-cell>
<div class="record-info" @click="editClick()">
<div class="top-container">
<div class="info-list">
<div class="info-item">
<span class="title">{{ props.carItem.name }}</span>
</div>
<div class="info-item">
<i class="fa-solid fa-file-lines" style="color: #8e44ad"></i>
{{ props.carItem.description }}
</div>
</div>
<el-tag type="primary" effect="dark">
{{ props.carItem.type }}
</el-tag>
</div>
<div class="bottom-container">
<div class="info-list">
<div class="info-item">
<i class="fa-solid fa-gauge" style="color: #3498db"></i>
{{ props.carItem.mileage }} M
</div>
<div class="info-item">
<i class="fa-solid fa-coins" style="color: #27ae60"></i>
{{ props.carItem.cost }}
</div>
<div class="info-item">
<i class="fa-solid fa-location-dot" style="color: #e67e22"></i>
{{ props.carItem.location }}
</div>
</div>
<div class="info-list">
<div class="info-item">
<i class="fa-solid fa-calendar" style="color: #26a69a"></i>
{{ props.carItem.date }}
</div>
</div>
</div>
</div>
<template #right>
<van-button square type="primary" text="编辑" style="height: 100%;"
@click="editClick"/>
<van-button square type="danger" text="删除" style="height: 100%;"
@click="deleteClick()"/>
</template>
</van-swipe-cell>
</div>
</template>
<script setup lang="ts">
import { useCarStore } from '@/store/car.ts'
import { defineProps, PropType } from 'vue'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { useRouter } from 'vue-router'
import { ThumbImageQuery } from '@/utils'
import { showImagePreview } from 'vant'
import { ICarEvent } from '@/types/car.ts'
const carStore = useCarStore()
const router = useRouter()
const props = defineProps({
carItem: {
required: true,
type: Object as PropType<ICarEvent>
}
})
const viewImageClick = () => {
showImagePreview({ images: [props.carItem?.imageUrl], closeable: true })
}
const editClick = async () => {
const id = props.carItem?.id
carStore.carEventApiType = 'UPDATE'
carStore.currentCarEvent = props.carItem as ICarEvent
await router.push({ name: 'CarDetailId', params: { id } })
}
const deleteClick = () => {
carStore.carEventApiType = 'DELETE'
commonElMessageBox(`请确认是否删除该事件: ${props.carItem?.description}?`).then(() => {
carStore.handleCarEventApi(props.carItem as ICarEvent)
})
}
</script>
<style lang="scss">
@use "@/styles/home/life.record.module";
</style>

View File

@@ -0,0 +1,33 @@
<template>
<div class="mobile-common-query card-item">
<div class="query-item">
<span>类别</span>
<el-segmented v-model="carStore.queryInfo.type"
:options="queryTypeList"
@change="carStore.queryCarEventList()"/>
</div>
</div>
</template>
<script setup lang="ts">
import { useCarStore } from '@/store/car.ts'
const carStore = useCarStore()
const queryTypeList = [
{
label: '全部',
value: ''
},
{
label: '保养',
value: '保养'
}, {
label: '事故',
value: '事故'
}, {
label: '违章',
value: '违章'
}
]
</script>

View File

@@ -98,6 +98,12 @@
<span>清单</span>
</router-link>
<router-link v-if="path.includes('car')" to="/car/list"
@click="routerClick()" class="item">
<i class="fa-solid fa-list-ul"></i>
<span>清单</span>
</router-link>
<router-link v-if="path.includes('journal')" to="/journal/record"
@click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i>

62
src/store/car.ts Normal file
View File

@@ -0,0 +1,62 @@
import { defineStore } from 'pinia'
import type { IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus'
import { ImagePathEnum } from '@/types'
import { ICarEvent, ICarEventQuery } from '@/types/car.ts'
import { addCarEventApi, deleteCarEventApi, queryCarEventListApi, updateCarEventApi } from '@/apis/car.ts'
export const useCarStore = defineStore('car', {
state: () => ({
carEventList: [] as ICarEvent[],
queryInfo: {
type: ''
} as ICarEventQuery,
carEventApiType: 'ADD' as IHandleApi,
currentCarEvent: {
} as ICarEvent,
carImagePath: ImagePathEnum.CAR,
isScrollEnd: false,
isRefresh: false
}),
actions: {
async queryCarEventList() {
this.carEventList = await queryCarEventListApi(this.queryInfo)
},
async handleCarEventApi(carEvent: ICarEvent) {
switch (this.carEventApiType) {
case 'ADD':
await addCarEventApi(carEvent)
ElMessage.success('新增汽车事件成功')
break
case 'UPDATE':
await updateCarEventApi(carEvent.id as number, carEvent)
ElMessage.success('更新汽车事件成功')
break
case 'DELETE':
await deleteCarEventApi(carEvent.id as number)
ElMessage.success('删除汽车事件成功')
break
default:
break
}
},
async refreshInfo() {
await this.queryCarEventList()
this.isRefresh = !this.isRefresh
},
getEmptyCarEvent(): ICarEvent {
return {
cost: 0,
date: '',
description: '',
imageUrl: '',
location: '',
mileage: 0,
name: '',
remark: '',
type: ''
}
}
}
})

View File

@@ -82,4 +82,4 @@ span, input {
font-style: normal;
}
@import "//at.alicdn.com/t/c/font_4793264_mkjt7tcwxmc.css";
@import "//at.alicdn.com/t/c/font_4793264_lul7gdj5f2.css";

16
src/types/car.ts Normal file
View File

@@ -0,0 +1,16 @@
export interface ICarEvent {
id?: number;
name: string;
type: string;
date: string;
mileage: number;
cost: number;
location: string;
description: string;
imageUrl: string;
remark: string;
}
export interface ICarEventQuery {
type: string;
}

View File

@@ -7,7 +7,8 @@ export enum ImagePathEnum {
JOURNAL = 'journal',
LEDGER = 'ledger',
AWARD = 'award',
USER = 'user'
USER = 'user',
CAR = 'car',
}
export interface IPageRecord<T> {

View File

@@ -247,6 +247,30 @@ export const productRules = {
]
}
export const carRules = {
name: [
{ required: true, message: '请输入汽车名称', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择事件类型', trigger: 'change' }
],
date: [
{ required: true, message: '请选择事件日期', trigger: 'change' }
],
mileage: [
{ required: true, message: '请输入当前里程', trigger: 'blur' }
],
cost: [
{ required: true, message: '请输入事件费用', trigger: 'blur' }
],
location: [
{ required: true, message: '请输入事件地点', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入事件描述', trigger: 'blur' }
]
}
export const journalRules = {
content: [
{ required: true, message: '请输入内容', trigger: 'blur' }

View File

@@ -0,0 +1,170 @@
<template>
<div class="common-mobile-view">
<el-form ref="formRef" :model="carStore.currentCarEvent"
:rules="rules" label-position="top" label-width="80px"
class="form-item">
<el-form-item label="汽车名称" prop="name">
<el-input v-model="carStore.currentCarEvent.name" autocomplete="off"
placeholder="请输入汽车名称" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<el-form-item label="事件类型" prop="type">
<el-radio-group v-model="carStore.currentCarEvent.type"
placeholder="请选择事件类型" clearable>
<el-radio
v-for="(item, index) in ['保养','事故','违章']"
:key="index" :label="item" :value="item"
/>
</el-radio-group>
</el-form-item>
<el-form-item label="事件日期" prop="date">
<el-date-picker
v-model="carStore.currentCarEvent.date"
type="date" :editable="false" placeholder="请选择事件日期"
value-format="YYYY-MM-DD"/>
</el-form-item>
<el-form-item label="当前里程" prop="mileage">
<el-input-number v-model="carStore.currentCarEvent.mileage" :min="0" :max="1000000">
<template #suffix>
<span>M</span>
</template>
</el-input-number>
</el-form-item>
<el-form-item label="事件费用" prop="cost">
<el-input-number v-model="carStore.currentCarEvent.cost" :min="0" :max="1000000">
<template #prefix>
<span></span>
</template>
</el-input-number>
</el-form-item>
<el-form-item label="事件地点" prop="location">
<el-input
v-model="carStore.currentCarEvent.location"
placeholder="请输入事件地点" clearable
/>
</el-form-item>
<el-form-item label="事件描述" prop="description">
<el-input
v-model="carStore.currentCarEvent.description"
placeholder="请输入事件描述" clearable
/>
</el-form-item>
<el-form-item label="现场图片">
<el-upload
v-model:file-list="imageFileList"
list-type="picture-card"
:accept="acceptImageTypes"
:limit="1"
:before-upload="beforeUpload"
:http-request="httpRequest"
:on-success="handleSuccess"
:on-exceed="handleExceed"
:before-remove="beforeRemove"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">
请上传小于10M的图片
</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="事件备注" prop="remark">
<el-input
v-model="carStore.currentCarEvent.remark"
placeholder="请输入事件备注" clearable
/>
</el-form-item>
</el-form>
<div class="form-button">
<el-button type="primary" @click="confirmClick">确认</el-button>
<el-button type="info" @click="cancelClick">取消</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { useCarStore } from '@/store/car.ts'
import { useAppStore } from '@/store/app.ts'
import { ElMessage, ElMessageBox, FormRules, UploadProps, UploadRawFile, UploadRequestOptions, UploadUserFile } from 'element-plus'
import { carRules } from '@/utils/element/elRules.ts'
import { onMounted, reactive, ref } from 'vue'
import { acceptImageTypes } from '@/utils/file.ts'
import { Plus } from '@element-plus/icons-vue'
import { beforeUploadImage, httpRequestFile } from '@/utils/element/elUpload.ts'
const formRef = ref()
const rules = reactive<FormRules>(carRules)
const carStore = useCarStore()
const appStore = useAppStore()
const imageFileList = ref<UploadUserFile[]>([])
onMounted(async () => {
appStore.isShowMobileBack = true
initImageFileList()
})
const initImageFileList = () => {
imageFileList.value = []
const { imageUrl } = carStore.currentCarEvent
if (imageUrl) {
imageFileList.value.push({
name: 'image',
url: imageUrl
})
}
}
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
return beforeUploadImage(rawFile)
}
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
return await httpRequestFile(carStore.carImagePath, options)
}
const handleSuccess = (response: string) => {
carStore.currentCarEvent.imageUrl = response
}
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多上传1张图片')
}
const handleRemove: UploadProps['onRemove'] = () => {
carStore.currentCarEvent.imageUrl = ''
}
const beforeRemove: UploadProps['beforeRemove'] = () => {
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value.validate().then(async () => {
await carStore.handleCarEventApi(carStore.currentCarEvent)
history.back()
appStore.isShowMobileBack = false
})
}
const cancelClick = () => {
history.back()
appStore.isShowMobileBack = false
}
</script>

36
src/views/car/list.vue Normal file
View File

@@ -0,0 +1,36 @@
<template>
<div class="common-mobile-view">
<car-query />
<el-empty v-if="carStore.carEventList.length === 0" description="暂无记录"/>
<div v-else class="content-list">
<car-card v-for="(item, index) in carStore.carEventList"
:key="index" :car-item="item"/>
</div>
<van-floating-bubble icon="plus" @click="addClick" style="top: -60px;"/>
</div>
</template>
<script lang="ts" setup>
import CarQuery from '@/components/Car/CarQuery.vue'
import CarCard from '@/components/Car/CarCard.vue'
import { useCarStore } from '@/store/car.ts'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
const carStore = useCarStore()
const router = useRouter()
onMounted(async () => {
await carStore.queryCarEventList()
})
const addClick = () => {
carStore.carEventApiType = 'ADD'
carStore.currentCarEvent = carStore.getEmptyCarEvent()
router.push({ name: 'CarDetailId', params: { id: 0 } })
}
</script>

View File

@@ -30,6 +30,10 @@
title="物品记录" sub-title="收纳生活琐碎"
router="/product/list"/>
<module-card icon="iconfont icon-qiche" color="#8E44AD"
title="汽车管理" sub-title="留存行车轨迹"
router="/car/list"/>
<module-card icon="iconfont icon-paiban" color="#9933FF"
title="日程安排" sub-title="规划每日时光"
router="/plan/calendar"/>
@@ -147,6 +151,7 @@ const titleClick = () => {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
overflow: auto;
}
.note-container {

View File

@@ -249,10 +249,24 @@ export const mobileHomeMeta: IRouteMetaConfig = {
title: '内容',
icon: ''
},
'/car': {
title: '汽车管理',
icon: 'home-car',
order: 15,
redirect: '/car/list'
},
'/car/list': {
title: '清单',
icon: ''
},
'/car/detail/id': {
title: '内容',
icon: ''
},
'/profile': {
title: '个人信息',
icon: '',
order: 15,
order: 16,
hidden: true,
redirect: '/profile/index'
},

View File

@@ -4,9 +4,6 @@ import path from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { viteMockServe } from 'vite-plugin-mock'
// const API_HOST = '192.168.1.7'
const API_HOST = '127.0.0.1'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
@@ -41,15 +38,10 @@ export default defineConfig({
// 服务器代理 服务器没有部署Nginx的情况下使用
proxy: {
'/home-api': {
target: `http://${API_HOST}:8081`,
target: 'http://127.0.0.1:8081',
changeOrigin: true,
rewrite: (item) => item.replace(/^\/home-api/, '')
},
'/blog-api': {
target: `http://${API_HOST}:8082`,
changeOrigin: true,
rewrite: (item) => item.replace(/^\/blog-api/, '')
},
'/alist-api': {
target: 'http://192.168.1.7:5244',
changeOrigin: true,
@@ -61,7 +53,7 @@ export default defineConfig({
rewrite: (item) => item.replace(/^\/image-api/, '')
},
'/agent-api': {
target: `http://${API_HOST}:8000`,
target: 'http://127.0.0.1:8000',
changeOrigin: true,
rewrite: (item) => item.replace(/^\/agent-api/, '')
}