<think>我们使用Vue3CompositionAPI来构建设备分类的CRUD功能。假设我们使用VueRouter进行路由管理,Pinia进行状态管理,以及Axios进行HTTP请求。步骤:1.创建Piniastore来管理设备分类的状态和逻辑。2.创建组件:设备分类列表、创建设备分类、编辑设备分类、删除操作。3.使用Axios与后端API通信(假设后端基于PostgreSQL表提供了RESTfulAPI)。首先,定义设备分类的接口类型(TypeScript):```typescript//types/deviceCategory.tsexportinterfaceDeviceCategory{id:number;name:string;type:string;manufacturer:string;model:string;image_url:string;description:string;created_at:string;updated_at:string;}```然后,创建Piniastore:```typescript//stores/deviceCategoryStore.tsimport{defineStore}from'pinia';importaxiosfrom'axios';import{DeviceCategory}from'@/types/deviceCategory';exportconstuseDeviceCategoryStore=defineStore('deviceCategory',{state:()=>({deviceCategories:[]asDeviceCategory[],currentDeviceCategory:nullasDeviceCategory|null,loading:false,error:nullasstring|null,}),actions:{asyncfetchDeviceCategories(){this.loading=true;try{constresponse=awaitaxios.get('/api/device-categories');this.deviceCategories=response.data;}catch(error:any){this.error=error.message||'获取设备分类列表失败';}finally{this.loading=false;}},asyncfetchDeviceCategoryById(id:number){this.loading=true;try{constresponse=awaitaxios.get(`/api/device-categories/${id}`);this.currentDeviceCategory=response.data;}catch(error:any){this.error=error.message||'获取设备分类详情失败';}finally{this.loading=false;}},asynccreateDeviceCategory(deviceCategory:Omit<DeviceCategory,'id'|'created_at'|'updated_at'>){this.loading=true;try{constresponse=awaitaxios.post('/api/device-categories',deviceCategory);this.deviceCategories.push(response.data);returnresponse.data;//返回新创建的对象,用于跳转等}catch(error:any){this.error=error.message||'创建设备分类失败';throwerror;}finally{this.loading=false;}},asyncupdateDeviceCategory(id:number,deviceCategory:Partial<DeviceCategory>){this.loading=true;try{constresponse=awaitaxios.put(`/api/device-categories/${id}`,deviceCategory);constindex=this.deviceCategories.findIndex(dc=>dc.id===id);if(index!==-1){this.deviceCategories[index]=response.data;}if(this.currentDeviceCategory?.id===id){this.currentDeviceCategory=response.data;}returnresponse.data;}catch(error:any){this.error=error.message||'更新设备分类失败';throwerror;}finally{this.loading=false;}},asyncdeleteDeviceCategory(id:number){this.loading=true;try{awaitaxios.delete(`/api/device-categories/${id}`);this.deviceCategories=this.deviceCategories.filter(dc=>dc.id!==id);if(this.currentDeviceCategory?.id===id){this.currentDeviceCategory=null;}}catch(error:any){this.error=error.message||'删除设备分类失败';throwerror;}finally{this.loading=false;}},},});```接下来,创建设备分类列表组件:```vue<!--components/DeviceCategoryList.vue--><template><div><h1>设备分类列表</h1><router-linkto="/device-categories/create">创建新设备分类</router-link><divv-if="store.loading">加载中...</div><divv-else-if="store.error"class="error">{{store.error}}</div><tablev-else><thead><tr><th>ID</th><th>名称</th><th>类型</th><th>厂家</th><th>型号</th><th>操作</th></tr></thead><tbody><trv-for="dcinstore.deviceCategories":key="dc.id"><td>{{dc.id}}</td><td>{{dc.name}}</td><td>{{dc.type}}</td><td>{{dc.manufacturer}}</td><td>{{dc.model}}</td><td><router-link:to="`/device-categories/edit/${dc.id}`">编辑</router-link><button@click="deleteDeviceCategory(dc.id)">删除</button></td></tr></tbody></table></div></template><scriptsetuplang="ts">import{useDeviceCategoryStore}from'@/stores/deviceCategoryStore';import{onMounted}from'vue';conststore=useDeviceCategoryStore();onMounted(()=>{store.fetchDeviceCategories();});constdeleteDeviceCategory=async(id:number)=>{if(confirm('确定要删除这个设备分类吗?')){awaitstore.deleteDeviceCategory(id);}};</script>```创建设备分类表单组件(用于创建和编辑):```vue<!--components/DeviceCategoryForm.vue--><template><div><h1v-if="isEdit">编辑设备分类</h1><h1v-else>创建设备分类</h1><form@submit.prevent="submitForm"><div><label>名称:</label><inputv-model="formData.name"required/></div><div><label>类型:</label><inputv-model="formData.type"required/></div><div><label>厂家:</label><inputv-model="formData.manufacturer"/></div><div><label>型号:</label><inputv-model="formData.model"/></div><div><label>图片URL:</label><inputv-model="formData.image_url"/></div><div><label>描述:</label><textareav-model="formData.description"></textarea></div><buttontype="submit":disabled="store.loading">{{isEdit?'更新':'创建'}}</button><divv-if="store.error"class="error">{{store.error}}</div></form></div></template><scriptsetuplang="ts">import{ref,onMounted,watch}from'vue';import{useRoute,useRouter}from'vue-router';import{useDeviceCategoryStore}from'@/stores/deviceCategoryStore';importtype{DeviceCategory}from'@/types/deviceCategory';constroute=useRoute();constrouter=useRouter();conststore=useDeviceCategoryStore();constisEdit=ref(route.name==='editDeviceCategory');constid=route.params.id?Number(route.params.id):null;//表单数据constformData=ref<Omit<DeviceCategory,'id'|'created_at'|'updated_at'>>({name:'',type:'',manufacturer:'',model:'',image_url:'',description:'',});//如果是编辑,加载设备分类数据if(id){onMounted(async()=>{awaitstore.fetchDeviceCategoryById(id);if(store.currentDeviceCategory){formData.value={name:store.currentDeviceCategory.name,type:store.currentDeviceCategory.type,manufacturer:store.currentDeviceCategory.manufacturer,model:store.currentDeviceCategory.model,image_url:store.currentDeviceCategory.image_url,description:store.currentDeviceCategory.description,};}});}constsubmitForm=async()=>{try{if(isEdit.value&&id){awaitstore.updateDeviceCategory(id,formData.value);}else{awaitstore.createDeviceCategory(formData.value);}router.push('/device-categories');}catch(error){//错误已经在store中处理,这里可以不做额外处理}};</script>```最后,配置路由(假设在router/index.ts中):```typescriptimport{createRouter,createWebHistory}from'vue-router';importDeviceCategoryListfrom'@/components/DeviceCategoryList.vue';importDeviceCategoryFormfrom'@/components/DeviceCategoryForm.vue';constroutes=[{path:'/device-categories',name:'deviceCategories',component:DeviceCategoryList,},{path:'/device-categories/create',name:'createDeviceCategory',component:DeviceCategoryForm,},{path:'/device-categories/edit/:id',name:'editDeviceCategory',component:DeviceCategoryForm,props:true,},];constrouter=createRouter({history:createWebHistory(),routes,});exportdefaultrouter;```注意事项:1.后端API需要按照RESTful风格设计,以支持上述的CRUD操作。2.在删除操作时,我们使用了确认对话框。3.表单组件同时用于创建和编辑,通过路由名称判断模式。4.在编辑时,我们通过路由参数获取id,然后加载数据填充表单。5.提交成功后跳转到列表页面。以上代码实现了设备分类的CRUD功能。根据实际需求,可能还需要添加分页、搜索、表单验证等功能。§§相关问题§§1.如何实现设备分类列表的分页和搜索功能?2.在Vue3中如何优雅地处理表单验证?3.如何优化Piniastore中的错误处理?4.如何实现设备分类图片的上传功能?</think>以下是基于 PostgreSQL `device_category` 表结构,使用 Vue 3 实现设备管理 CRUD 操作的前端解决方案:
### 解决方案设计
#### 1. 设备分类接口定义 (TypeScript)
```typescript
// types/deviceCategory.ts
export interface DeviceCategory {
id: number;
name: string;
type: string;
manufacturer: string;
model: string;
image_url: string;
description: string;
created_at: string;
updated_at: string;
}
```
#### 2. Pinia 状态管理
```typescript
// stores/deviceCategoryStore.ts
import { defineStore } from 'pinia';
import axios from 'axios';
import type { DeviceCategory } from '@/types/deviceCategory';
export const useDeviceCategoryStore = defineStore('deviceCategory', {
state: () => ({
deviceCategories: [] as DeviceCategory[],
currentCategory: null as DeviceCategory | null,
loading: false,
error: null as string | null
}),
actions: {
async fetchAll() {
this.loading = true;
try {
const response = await axios.get('/api/device-categories');
this.deviceCategories = response.data;
} catch (error: any) {
this.error = error.message || 'Failed to load categories';
} finally {
this.loading = false;
}
},
async fetchById(id: number) {
this.loading = true;
try {
const response = await axios.get(`/api/device-categories/${id}`);
this.currentCategory = response.data;
} catch (error: any) {
this.error = error.message || 'Failed to load category';
} finally {
this.loading = false;
}
},
async create(category: Omit<DeviceCategory, 'id' | 'created_at' | 'updated_at'>) {
this.loading = true;
try {
const response = await axios.post('/api/device-categories', category);
this.deviceCategories.push(response.data);
return response.data;
} catch (error: any) {
this.error = error.message || 'Failed to create category';
throw error;
} finally {
this.loading = false;
}
},
async update(id: number, updates: Partial<DeviceCategory>) {
this.loading = true;
try {
const response = await axios.put(`/api/device-categories/${id}`, updates);
const index = this.deviceCategories.findIndex(c => c.id === id);
if (index !== -1) {
this.deviceCategories[index] = { ...this.deviceCategories[index], ...response.data };
}
return response.data;
} catch (error: any) {
this.error = error.message || 'Failed to update category';
throw error;
} finally {
this.loading = false;
}
},
async delete(id: number) {
this.loading = true;
try {
await axios.delete(`/api/device-categories/${id}`);
this.deviceCategories = this.deviceCategories.filter(c => c.id !== id);
} catch (error: any) {
this.error = error.message || 'Failed to delete category';
throw error;
} finally {
this.loading = false;
}
}
}
});
```
#### 3. 设备列表组件
```vue
<!-- components/DeviceCategoryList.vue -->
<template>
<div>
<h2>设备分类列表</h2>
<router-link to="/device-categories/create" class="btn">添加新设备</router-link>
<div v-if="loading">加载中...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<table v-else>
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>类型</th>
<th>厂家</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="category in deviceCategories" :key="category.id">
<td>{{ category.id }}</td>
<td>{{ category.name }}</td>
<td>{{ category.type }}</td>
<td>{{ category.manufacturer }}</td>
<td>
<router-link :to="`/device-categories/edit/${category.id}`">编辑</router-link>
<button @click="deleteCategory(category.id)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useDeviceCategoryStore } from '@/stores/deviceCategoryStore';
import { onMounted } from 'vue';
const store = useDeviceCategoryStore();
const { deviceCategories, loading, error } = storeToRefs(store);
onMounted(() => {
store.fetchAll();
});
const deleteCategory = (id: number) => {
if (confirm('确定要删除此设备分类吗?')) {
store.delete(id);
}
};
</script>
<style scoped>
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px;
border: 1px solid #ddd;
}
.error {
color: red;
}
</style>
```
#### 4. 设备表单组件 (创建/编辑)
```vue
<!-- components/DeviceCategoryForm.vue -->
<template>
<div>
<h2>{{ isEdit ? '编辑设备分类' : '创建设备分类' }}</h2>
<form @submit.prevent="submitForm">
<div class="form-group">
<label>分类名称 *</label>
<input v-model="formData.name" required />
</div>
<div class="form-group">
<label>设备类型 *</label>
<select v-model="formData.type" required>
<option value="传感器">传感器</option>
<option value="控制器">控制器</option>
<option value="工业设备">工业设备</option>
<option value="消费设备">消费设备</option>
</select>
</div>
<div class="form-group">
<label>生产厂家</label>
<input v-model="formData.manufacturer" />
</div>
<div class="form-group">
<label>设备型号</label>
<input v-model="formData.model" />
</div>
<div class="form-group">
<label>图片URL</label>
<input v-model="formData.image_url" />
<img v-if="formData.image_url" :src="formData.image_url" alt="设备图片" class="preview-image" />
</div>
<div class="form-group">
<label>描述</label>
<textarea v-model="formData.description"></textarea>
</div>
<button type="submit" :disabled="loading">{{ isEdit ? '更新' : '创建' }}</button>
<router-link to="/device-categories" class="btn">取消</router-link>
<div v-if="error" class="error">{{ error }}</div>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useDeviceCategoryStore } from '@/stores/deviceCategoryStore';
import type { DeviceCategory } from '@/types/deviceCategory';
const route = useRoute();
const router = useRouter();
const store = useDeviceCategoryStore();
const { currentCategory, loading, error } = storeToRefs(store);
const isEdit = computed(() => route.name === 'editDeviceCategory');
const id = route.params.id ? Number(route.params.id) : null;
const formData = ref<Omit<DeviceCategory, 'id' | 'created_at' | 'updated_at'>>({
name: '',
type: '传感器',
manufacturer: '',
model: '',
image_url: '',
description: ''
});
// 编辑模式下加载数据
if (isEdit.value && id) {
onMounted(async () => {
await store.fetchById(id);
if (store.currentCategory) {
formData.value = { ...store.currentCategory };
}
});
}
const submitForm = async () => {
try {
if (isEdit.value && id) {
await store.update(id, formData.value);
} else {
await store.create(formData.value);
}
router.push('/device-categories');
} catch (error) {
console.error('操作失败:', error);
}
};
</script>
<style scoped>
.form-group {
margin-bottom: 15px;
}
.preview-image {
max-width: 200px;
display: block;
margin-top: 10px;
}
</style>
```
#### 5. 路由配置
```typescript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import DeviceCategoryList from '@/components/DeviceCategoryList.vue';
import DeviceCategoryForm from '@/components/DeviceCategoryForm.vue';
const routes = [
{
path: '/device-categories',
name: 'deviceCategories',
component: DeviceCategoryList
},
{
path: '/device-categories/create',
name: 'createDeviceCategory',
component: DeviceCategoryForm
},
{
path: '/device-categories/edit/:id',
name: 'editDeviceCategory',
component: DeviceCategoryForm,
props: true
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
```
### 关键功能说明
1. **状态管理**:
- 使用 Pinia 集中管理设备分类状态
- 封装所有 CRUD 操作到 store 中
- 自动处理加载状态和错误信息
2. **组件设计**:
- 列表组件:展示所有设备分类,支持删除操作
- 表单组件:复用同一个组件处理创建和编辑
- 图片预览:实时显示设备图片预览
3. **路由管理**:
- 清晰的 URL 结构区分不同操作
- 参数传递用于编辑模式
4. **用户体验**:
- 加载状态提示
- 操作确认(删除)
- 表单验证(必填字段)
5. **API 集成**:
- 使用 axios 与后端 RESTful API 通信
- 统一错误处理机制
### 使用说明
1. 创建新设备:
- 访问 `/device-categories/create`
- 填写表单并提交
2. 编辑设备:
- 在列表页点击"编辑"按钮
- 修改表单字段并提交
3. 删除设备:
- 在列表页点击"删除"按钮
- 确认删除操作
### 相关优化建议
1. **分页支持**:
```typescript
// 在 store 中添加分页状态
state: () => ({
pagination: {
page: 1,
pageSize: 10,
total: 0
}
})
```
2. **搜索过滤**:
```vue
<!-- 在列表组件添加搜索框 -->
<input v-model="searchTerm" placeholder="搜索设备..." />
<!-- 计算属性过滤列表 -->
const filteredCategories = computed(() =>
deviceCategories.value.filter(c =>
c.name.includes(searchTerm.value) ||
c.manufacturer.includes(searchTerm.value)
)
);
```
3. **图片上传**:
```vue
<input type="file" @change="handleImageUpload">
const handleImageUpload = async (event) => {
const file = event.target.files[0];
const formData = new FormData();
formData.append('image', file);
const response = await axios.post('/api/upload', formData);
formData.value.image_url = response.data.url;
}
```
4. **表单验证增强**:
```typescript
// 使用 Vuelidate 或 Vue Formulate
const rules = {
name: { required, minLength: minLength(3) },
type: { required }
};
```
此实现提供了完整的设备管理 CRUD 功能,遵循 Vue 3 最佳实践,具有良好的可扩展性和维护性。