AI教你常识之 ESM + Express + Vue3 + CSV文件 + Pinia + CRUD

太棒了!我们将在 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

访问 👉 http://localhost:5173


🎉 最终功能清单

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

相关推荐
召摇4 小时前
Nue.js深度解析:极简主义前端框架的革新实践
前端·node.js
木辰風5 小时前
idea npm install 很慢(nodejs)
前端·npm·node.js
wyzqhhhh17 小时前
组件库打包工具选型(npm/pnpm/yarn)的区别和技术考量
前端·npm·node.js
csdn_aspnet20 小时前
使用nvm管理node多版本(安装、卸载nvm,配置环境变量,更换npm淘宝镜像)
npm·node.js
Juchecar21 小时前
AI教你常识之 ESM + Express + EJS + 表单POST + CSV文件
node.js
xiaopengbc21 小时前
在Webpack中,如何在不同环境中使用不同的API地址?
前端·webpack·node.js
Gogo81621 小时前
java与node.js对比
java·node.js
王蛋1111 天前
前端工作问题或知识记录
前端·npm·node.js
Swift社区1 天前
为什么 socket.io 客户端在浏览器能连上,但在 Node.js 中报错 transport close?
javascript·node.js