太棒了!我们将在 Vue3 + Express + CSV 项目基础上,添加:
✅ Pinia 状态管理 ------ 避免刷新页面,数据全局共享
✅ 编辑/删除功能 ------ 完整 CRUD
✅ 分页 + 搜索 ------ 提升用户体验
🚀 升级版:Vue3 + Pinia + Express + CSV(带分页/搜索/编辑/删除)
✅ 第一步:在前端安装 Pinia
bash
cd frontend
pnpm add pinia
✅ 第二步:配置 Pinia
📄 修改 frontend/src/main.js
js
// frontend/src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import './style.css'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
✅ 第三步:创建用户 Store
📄 创建 frontend/src/stores/userStore.js
js
// frontend/src/stores/userStore.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
loading: false,
error: null,
// 分页和搜索状态
currentPage: 1,
pageSize: 5,
searchQuery: '',
// 编辑状态
editingUser: null
}),
getters: {
// 计算属性:过滤 + 分页后的用户列表
filteredUsers() {
return this.users.filter(user =>
user.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
user.email.toLowerCase().includes(this.searchQuery.toLowerCase())
)
},
paginatedUsers() {
const start = (this.currentPage - 1) * this.pageSize
return this.filteredUsers.slice(start, start + this.pageSize)
},
totalPages() {
return Math.ceil(this.filteredUsers.length / this.pageSize)
}
},
actions: {
// 获取所有用户
async fetchUsers() {
this.loading = true
this.error = null
try {
const res = await fetch('/api/users')
if (!res.ok) throw new Error('获取用户失败')
this.users = await res.json()
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
},
// 创建用户
async createUser(userData) {
this.loading = true
this.error = null
try {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
if (!res.ok) {
const err = await res.json()
throw new Error(err.error || '创建失败')
}
const newUser = await res.json()
this.users.push(newUser) // 不刷新页面,直接添加到状态
return newUser
} catch (err) {
this.error = err.message
throw err
} finally {
this.loading = false
}
},
// 更新用户(先留空,后端稍后支持)
async updateUser(id, userData) {
// TODO: 后端需支持 PUT /api/users/:id
},
// 删除用户
async deleteUser(id) {
// TODO: 后端需支持 DELETE /api/users/:id
},
// 设置搜索关键词
setSearchQuery(query) {
this.searchQuery = query
this.currentPage = 1 // 搜索时重置到第一页
},
// 切换分页
setPage(page) {
if (page >= 1 && page <= this.totalPages) {
this.currentPage = page
}
},
// 开始编辑
startEditing(user) {
this.editingUser = { ...user } // 深拷贝,避免直接修改原数据
},
// 取消编辑
cancelEditing() {
this.editingUser = null
}
}
})
✅ 第四步:升级后端 API(支持 PUT/DELETE)
📄 修改 backend/index.js
,添加编辑和删除路由
js
// backend/index.js ------ 完整修复版
import express from 'express';
import cors from 'cors';
import fs from 'fs/promises';
import path from 'path'; // 👈 新增
import { fileURLToPath } from 'url'; // 👈 新增
import { readUsers, writeUser } from './csvUtils.js';
// 👇 正确获取 __dirname(ESM 环境下)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 👇 正确拼接 CSV 文件路径
const CSV_FILE_PATH = path.join(__dirname, 'data', 'users.csv');
// 确保 data 目录存在
const DATA_DIR = path.join(__dirname, 'data');
await fs.mkdir(DATA_DIR, { recursive: true });
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
// GET /api/users
app.get('/api/users', async (req, res) => {
try {
const users = await readUsers();
res.json(users);
} catch (error) {
res.status(500).json({ error: 'Failed to read users' });
}
});
// POST /api/users
app.post('/api/users', async (req, res) => {
const { name, email, age } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
try {
const newUser = await writeUser({ name, email, age });
res.status(201).json(newUser);
} catch (error) {
res.status(500).json({ error: 'Failed to write user' });
}
});
// PUT /api/users/:id
app.put('/api/users/:id', async (req, res) => {
const { id } = req.params;
const { name, email, age } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
try {
const users = await readUsers();
const userIndex = users.findIndex(u => u.id === id); // 字符串比较
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
// 更新用户
users[userIndex] = { ...users[userIndex], name, email, age };
// 重写 CSV
const headers = 'id,name,email,age,created_at\n';
const lines = users.map(u =>
`${u.id},${u.name},${u.email},${u.age || ''},${u.created_at}`
).join('\n');
await fs.writeFile(CSV_FILE_PATH, headers + lines + '\n', 'utf8'); // 使用完整路径
res.json(users[userIndex]);
} catch (error) {
console.error('❌ PUT /api/users/:id Error:', error); // 👈 打印错误
res.status(500).json({ error: 'Failed to update user' });
}
});
// DELETE /api/users/:id
app.delete('/api/users/:id', async (req, res) => {
const { id } = req.params;
try {
const users = await readUsers();
const filteredUsers = users.filter(u => u.id !== id);
if (filteredUsers.length === users.length) {
return res.status(404).json({ error: 'User not found' });
}
const headers = 'id,name,email,age,created_at\n';
const lines = filteredUsers.map(u =>
`${u.id},${u.name},${u.email},${u.age || ''},${u.created_at}`
).join('\n');
await fs.writeFile(CSV_FILE_PATH, headers + lines + '\n', 'utf8');
res.json({ message: 'User deleted', id });
} catch (error) {
console.error('❌ DELETE /api/users/:id Error:', error);
res.status(500).json({ error: 'Failed to delete user' });
}
});
app.listen(PORT, () => {
console.log(`✅ Backend running at http://localhost:${PORT}`);
console.log(`📂 CSV 文件路径: ${CSV_FILE_PATH}`);
});
⚠️ 注意:CSV 不支持"部分更新",所以 PUT/DELETE 都是读取全部 → 修改内存 → 重写文件
✅ 第五步:升级前端组件
📄 修改 frontend/src/components/UserList.vue
vue
<!-- frontend/src/components/UserList.vue -->
<template>
<div>
<h2>👥 用户列表</h2>
<!-- 搜索框 -->
<div class="search-box">
<input
v-model="searchQuery"
@input="handleSearch"
placeholder="搜索姓名或邮箱..."
class="search-input"
/>
<span>共 {{ userStore.filteredUsers.length }} 条</span>
</div>
<!-- 加载状态 -->
<div v-if="userStore.loading" class="loading">加载中...</div>
<div v-else-if="userStore.error" class="error">{{ userStore.error }}</div>
<!-- 用户表格 -->
<table v-else-if="userStore.filteredUsers.length > 0">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
<th>年龄</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in userStore.paginatedUsers" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.age || '---' }}</td>
<td>{{ new Date(user.created_at).toLocaleString() }}</td>
<td>
<button @click="startEdit(user)" class="btn btn-edit">✏️ 编辑</button>
<button @click="confirmDelete(user.id)" class="btn btn-delete">🗑️ 删除</button>
</td>
</tr>
</tbody>
</table>
<p v-else>暂无匹配用户</p>
<!-- 分页器 -->
<div v-if="userStore.totalPages > 1" class="pagination">
<button
@click="userStore.setPage(userStore.currentPage - 1)"
:disabled="userStore.currentPage === 1"
class="btn"
>
上一页
</button>
<span>第 {{ userStore.currentPage }} / {{ userStore.totalPages }} 页</span>
<button
@click="userStore.setPage(userStore.currentPage + 1)"
:disabled="userStore.currentPage === userStore.totalPages"
class="btn"
>
下一页
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useUserStore } from '../stores/userStore'
const userStore = useUserStore()
const searchQuery = ref(userStore.searchQuery)
// 搜索防抖
let searchTimeout = null
const handleSearch = () => {
if (searchTimeout) clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => {
userStore.setSearchQuery(searchQuery.value)
}, 300)
}
// 编辑用户
const emit = defineEmits(['edit'])
const startEdit = (user) => {
emit('edit', user)
}
// 删除确认
const confirmDelete = async (id) => {
if (confirm('确定要删除此用户吗?')) {
try {
const res = await fetch(`/api/users/${id}`, {
method: 'DELETE'
})
if (!res.ok) throw new Error('删除失败')
await userStore.fetchUsers() // 重新获取数据
} catch (err) {
alert(err.message)
}
}
}
</script>
<style scoped>
.search-box {
margin: 20px 0;
display: flex;
align-items: center;
gap: 10px;
}
.search-input {
padding: 8px;
width: 300px;
border: 1px solid #ccc;
border-radius: 4px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
border: 1px solid #ddd;
text-align: left;
}
th {
background-color: #f5f5f5;
}
.btn {
padding: 5px 10px;
margin: 0 5px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-edit {
background: #4a90e2;
color: white;
}
.btn-delete {
background: #e74c3c;
color: white;
}
.pagination {
margin-top: 20px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
}
.loading, .error {
text-align: center;
padding: 20px;
}
.error {
color: red;
}
</style>
📄 修改 frontend/src/components/UserForm.vue
vue
<!-- frontend/src/components/UserForm.vue -->
<template>
<div class="form-container">
<h2>{{ isEditing ? '✏️ 编辑用户' : '➕ 添加新用户' }}</h2>
<form @submit.prevent="handleSubmit" class="form">
<div>
<label>姓名:</label>
<input v-model="formData.name" required />
</div>
<div>
<label>邮箱:</label>
<input v-model="formData.email" type="email" required />
</div>
<div>
<label>年龄:</label>
<input v-model.number="formData.age" type="number" min="1" max="150" />
</div>
<div class="form-actions">
<button type="submit" :disabled="userStore.loading" class="btn btn-submit">
{{ userStore.loading ? '处理中...' : (isEditing ? '更新用户' : '添加用户') }}
</button>
<button
v-if="isEditing"
type="button"
@click="cancelEdit"
class="btn btn-cancel"
>
取消
</button>
</div>
</form>
<div v-if="message" :class="['message', messageType]">{{ message }}</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useUserStore } from '../stores/userStore'
const userStore = useUserStore()
const emit = defineEmits(['user-added'])
const props = defineProps({
editingUser: Object // 从父组件传入的编辑用户
})
const isEditing = ref(false)
const formData = ref({
name: '',
email: '',
age: null
})
const message = ref('')
const messageType = ref('success') // 'success' or 'error'
// 监听编辑用户变化
watch(() => props.editingUser, (newUser) => {
if (newUser) {
isEditing.value = true
formData.value = { ...newUser }
} else {
isEditing.value = false
formData.value = { name: '', email: '', age: null }
}
}, { immediate: true })
const handleSubmit = async () => {
message.value = ''
try {
if (isEditing.value) {
// 更新用户
const res = await fetch(`/api/users/${formData.value.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData.value)
})
if (!res.ok) {
const err = await res.json()
throw new Error(err.error || '更新失败')
}
await userStore.fetchUsers() // 刷新列表
message.value = '🎉 用户更新成功!'
} else {
// 创建用户
await userStore.createUser(formData.value)
message.value = '🎉 用户添加成功!'
emit('user-added')
}
messageType.value = 'success'
// 清空表单(创建模式)
if (!isEditing.value) {
formData.value = { name: '', email: '', age: null }
}
} catch (err) {
message.value = err.message
messageType.value = 'error'
}
}
const cancelEdit = () => {
emit('edit', null) // 通知父组件取消编辑
}
</script>
<style scoped>
.form-container {
background: #f9f9f9;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form div {
display: flex;
flex-direction: column;
}
.form label {
font-weight: bold;
margin-bottom: 5px;
}
.form input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.btn-submit {
background: #4a90e2;
color: white;
}
.btn-cancel {
background: #95a5a6;
color: white;
}
.message {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
text-align: center;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>
📄 修改 frontend/src/App.vue
vue
<!-- frontend/src/App.vue -->
<template>
<div class="container">
<h1>Vue3 + Pinia + Express + CSV 用户管理系统</h1>
<UserForm
:editing-user="editingUser"
@user-added="handleUserAdded"
@edit="handleEdit"
/>
<UserList @edit="handleEdit" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useUserStore } from './stores/userStore'
import UserList from './components/UserList.vue'
import UserForm from './components/UserForm.vue'
const userStore = useUserStore()
const editingUser = ref(null)
// 组件加载时获取数据
userStore.fetchUsers()
// 处理编辑事件
const handleEdit = (user) => {
editingUser.value = user
}
// 处理用户添加(刷新列表)
const handleUserAdded = () => {
userStore.fetchUsers()
}
</script>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #4a90e2;
padding-bottom: 10px;
}
</style>
✅ 第六步:运行项目
启动后端:
bash
cd backend
pnpm dev
启动前端:
bash
cd frontend
pnpm dev
🎉 最终功能清单
✅ Pinia 状态管理 ------ 数据全局共享,无需刷新页面
✅ 搜索功能 ------ 实时过滤用户(防抖优化)
✅ 分页功能 ------ 每页 5 条,可切换
✅ 编辑功能 ------ 表单预填充,PUT 请求更新
✅ 删除功能 ------ 确认对话框,DELETE 请求
✅ 成功/错误提示 ------ 用户友好反馈
✅ CSV 持久化 ------ 所有操作最终写入文件
📂 最终项目结构
kotlin
my-vue-express-csv-app/
├── backend/
│ ├── index.js
│ ├── csvUtils.js
│ ├── package.json
│ └── data/users.csv
│
└── frontend/
├── package.json
├── vite.config.js
└── src/
├── main.js
├── App.vue
├── stores/userStore.js
└── components/
├── UserList.vue
└── UserForm.vue