代碼如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>優化版 Vue.js CRUD 示例</title><script src="https://unpkg.com/vue@3/dist/vue.global.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"><style>* {margin: 0;padding: 0;box-sizing: border-box;font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;}body {background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);color: #333;padding: 20px;min-height: 100vh;}.container {max-width: 1000px;margin: 0 auto;background: white;border-radius: 15px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);padding: 30px;position: relative;overflow: hidden;}.container::before {content: '';position: absolute;top: 0;left: 0;right: 0;height: 5px;background: linear-gradient(90deg, #3498db, #2ecc71, #f39c12, #e74c3c);}h1 {text-align: center;margin: 15px 0 30px;color: #2c3e50;font-weight: 600;position: relative;padding-bottom: 15px;}h1::after {content: '';position: absolute;bottom: 0;left: 50%;transform: translateX(-50%);width: 80px;height: 4px;background: linear-gradient(90deg, #3498db, #2ecc71);border-radius: 2px;}.header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 25px;flex-wrap: wrap;gap: 15px;padding: 20px;background: #f8fafc;border-radius: 10px;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);}.search-container {position: relative;flex-grow: 1;}.search-box {padding: 12px 45px 12px 15px;border: 2px solid #e2e8f0;border-radius: 8px;width: 100%;font-size: 16px;transition: all 0.3s;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);}.search-box:focus {border-color: #3498db;box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);outline: none;}.search-icon {position: absolute;right: 15px;top: 50%;transform: translateY(-50%);color: #64748b;}.search-clear {position: absolute;right: 40px;top: 50%;transform: translateY(-50%);background: none;border: none;color: #94a3b8;cursor: pointer;font-size: 16px;transition: color 0.3s;}.search-clear:hover {color: #e74c3c;}.btn {padding: 12px 24px;border: none;border-radius: 8px;cursor: pointer;font-weight: 500;transition: all 0.3s;display: flex;align-items: center;gap: 8px;}.btn-primary {background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);color: white;box-shadow: 0 4px 6px rgba(52, 152, 219, 0.3);}.btn-primary:hover {transform: translateY(-2px);box-shadow: 0 6px 8px rgba(52, 152, 219, 0.4);}.btn-edit {background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);color: white;box-shadow: 0 4px 6px rgba(243, 156, 18, 0.3);}.btn-edit:hover {transform: translateY(-2px);box-shadow: 0 6px 8px rgba(243, 156, 18, 0.4);}.btn-delete {background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);color: white;box-shadow: 0 4px 6px rgba(231, 76, 60, 0.3);}.btn-delete:hover {transform: translateY(-2px);box-shadow: 0 6px 8px rgba(231, 76, 60, 0.4);}table {width: 100%;border-collapse: separate;border-spacing: 0;margin-top: 20px;border-radius: 10px;overflow: hidden;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);}th, td {padding: 16px;text-align: left;border-bottom: 1px solid #e2e8f0;}th {background: linear-gradient(to bottom, #f8fafc, #f1f5f9);font-weight: 600;color: #334155;}tr:last-child td {border-bottom: none;}tr:hover {background-color: #f8fafc;}.actions {display: flex;gap: 10px;}.modal {position: fixed;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.5);display: flex;justify-content: center;align-items: center;z-index: 1000;opacity: 0;animation: fadeIn 0.3s forwards;}@keyframes fadeIn {to { opacity: 1; }}.modal-content {background-color: white;padding: 30px;border-radius: 15px;width: 500px;max-width: 90%;box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);transform: translateY(20px);animation: slideUp 0.3s forwards;}@keyframes slideUp {to { transform: translateY(0); }}.modal-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 25px;padding-bottom: 15px;border-bottom: 1px solid #e2e8f0;}.modal-title {font-size: 22px;font-weight: 600;color: #2c3e50;}.modal-close {background: none;border: none;font-size: 24px;cursor: pointer;color: #94a3b8;transition: color 0.3s;}.modal-close:hover {color: #e74c3c;}.form-group {margin-bottom: 20px;position: relative;}label {display: block;margin-bottom: 8px;font-weight: 500;color: #334155;}input, select {width: 100%;padding: 14px;border: 2px solid #e2e8f0;border-radius: 8px;font-size: 16px;transition: all 0.3s;}input:focus, select:focus {border-color: #3498db;box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);outline: none;}.error-message {color: #e74c3c;font-size: 14px;margin-top: 5px;display: none;}input.invalid, select.invalid {border-color: #e74c3c;}input.invalid + .error-message, select.invalid + .error-message {display: block;}.form-actions {display: flex;justify-content: flex-end;gap: 12px;margin-top: 30px;}.empty-state {text-align: center;padding: 60px 20px;color: #64748b;}.empty-state i {font-size: 60px;margin-bottom: 15px;color: #cbd5e1;}.empty-state p {margin-top: 15px;font-size: 18px;}.search-hint {text-align: center;padding: 15px;color: #64748b;font-style: italic;background: #f8fafc;border-radius: 8px;margin-top: 10px;display: none;}.search-hint.visible {display: block;animation: fadeIn 0.5s;}@media (max-width: 768px) {.header {flex-direction: column;align-items: stretch;}.search-box {width: 100%;}.actions {flex-direction: column;}th, td {padding: 12px;}.btn {width: 100%;justify-content: center;}}.user-avatar {width: 40px;height: 40px;border-radius: 50%;background: linear-gradient(135deg, #3498db 0%, #2ecc71 100%);display: flex;align-items: center;justify-content: center;color: white;font-weight: 600;margin-right: 10px;}.user-info {display: flex;align-items: center;}.role-badge {padding: 4px 12px;border-radius: 20px;font-size: 12px;font-weight: 600;}.role-admin {background: #ffeaa7;color: #d35400;}.role-user {background: #d6eaf8;color: #2980b9;}.role-editor {background: #e8f8f5;color: #16a085;}</style>
</head>
<body><div id="app"><div class="container"><h1><i class="fas fa-users-cog"></i> 用戶管理系統</h1><div class="header"><div class="search-container"><input v-model="searchQuery" class="search-box" placeholder="搜索用戶..." @input="handleSearch"/><button v-if="searchQuery" class="search-clear" @click="clearSearch"><i class="fas fa-times"></i></button><div class="search-icon"><i class="fas fa-search"></i></div></div><button class="btn btn-primary" @click="openModal(null)"><i class="fas fa-plus"></i> 添加用戶</button></div><div class="search-hint" :class="{visible: searchQuery && filteredUsers.length > 0}">找到 {{ filteredUsers.length }} 個匹配"{{ searchQuery }}"的用戶</div><div v-if="filteredUsers.length > 0"><table><thead><tr><th>用戶</th><th>郵箱</th><th>角色</th><th>操作</th></tr></thead><tbody><tr v-for="user in filteredUsers" :key="user.id"><td><div class="user-info"><div class="user-avatar">{{ user.name.charAt(0) }}</div><div><div>{{ user.name }}</div><div style="font-size: 12px; color: #64748b;">ID: {{ user.id }}</div></div></div></td><td>{{ user.email }}</td><td><span class="role-badge" :class="'role-' + user.role">{{ user.role }}</span></td><td class="actions"><button class="btn btn-edit" @click="openModal(user)"><i class="fas fa-edit"></i> 編輯</button><button class="btn btn-delete" @click="deleteUser(user.id)"><i class="fas fa-trash"></i> 刪除</button></td></tr></tbody></table></div><div v-else class="empty-state"><i class="fas fa-user-slash"></i><p>{{ searchQuery ? '沒有找到匹配的用戶' : '暫無用戶數據' }}</p><button v-if="!searchQuery" class="btn btn-primary" @click="openModal(null)" style="margin-top: 20px;"><i class="fas fa-plus"></i> 添加第一個用戶</button><button v-else class="btn btn-primary" @click="clearSearch" style="margin-top: 20px;"><i class="fas fa-times"></i> 清除搜索</button></div><!-- 添加/編輯用戶的模態框 --><div class="modal" v-if="showModal" @click.self="closeModal"><div class="modal-content"><div class="modal-header"><h2 class="modal-title"><i :class="editingUser ? 'fas fa-user-edit' : 'fas fa-user-plus'"></i>{{ editingUser ? '編輯用戶' : '添加用戶' }}</h2><button class="modal-close" @click="closeModal"><i class="fas fa-times"></i></button></div><div class="form-group"><label for="name">姓名</label><input type="text" id="name" v-model="currentUser.name" placeholder="請輸入姓名":class="{invalid: !currentUser.name && formSubmitted}"><div class="error-message">請輸入姓名</div></div><div class="form-group"><label for="email">郵箱</label><input type="email" id="email" v-model="currentUser.email" placeholder="請輸入郵箱":class="{invalid: (!currentUser.email || !isValidEmail) && formSubmitted}"><div class="error-message">{{ !currentUser.email ? '請輸入郵箱' : '請輸入有效的郵箱地址' }}</div></div><div class="form-group"><label for="role">角色</label><select id="role" v-model="currentUser.role"><option value="用戶">用戶</option><option value="管理員">管理員</option><option value="編輯">編輯</option></select></div><div class="form-actions"><button class="btn" @click="closeModal"><i class="fas fa-times"></i> 取消</button><button class="btn btn-primary" @click="saveUser"><i class="fas fa-save"></i> 保存</button></div></div></div></div></div><script>const { createApp, ref, computed, onMounted, watch } = Vue;createApp({setup() {// 用戶數據const users = ref([]);// 搜索查詢const searchQuery = ref('');// 控制模態框顯示const showModal = ref(false);// 當前正在編輯的用戶const currentUser = ref({ id: null, name: '', email: '', role: '用戶' });// 判斷是否為編輯模式const editingUser = ref(false);// 表單提交狀態const formSubmitted = ref(false);// 郵箱驗證狀態const isValidEmail = ref(true);// 初始化一些示例數據onMounted(() => {users.value = [{ id: 1, name: '張三', email: 'zhangsan@example.com', role: '管理員' },{ id: 2, name: '李四', email: 'lisi@example.com', role: '用戶' },{ id: 3, name: '王五', email: 'wangwu@example.com', role: '編輯' },{ id: 4, name: '趙六', email: 'zhaoliu@example.com', role: '用戶' },{ id: 5, name: '錢七', email: 'qianqi@example.com', role: '編輯' }];});// 郵箱驗證const validateEmail = () => {const email = currentUser.value.email;if (!email) {isValidEmail.value = false;return false;}const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;isValidEmail.value = re.test(email);return isValidEmail.value;};// 過濾用戶列表const filteredUsers = computed(() => {if (!searchQuery.value) {return users.value;}const query = searchQuery.value.toLowerCase();return users.value.filter(user => user.name.toLowerCase().includes(query) || user.email.toLowerCase().includes(query) ||user.role.toLowerCase().includes(query));});// 處理搜索const handleSearch = () => {// 可以添加搜索延遲處理};// 清除搜索const clearSearch = () => {searchQuery.value = '';};// 打開模態框const openModal = (user) => {formSubmitted.value = false;if (user) {// 編輯模式currentUser.value = { ...user };editingUser.value = true;} else {// 添加模式currentUser.value = { id: null, name: '', email: '', role: '用戶' };editingUser.value = false;}showModal.value = true;};// 關閉模態框const closeModal = () => {showModal.value = false;};// 保存用戶const saveUser = () => {formSubmitted.value = true;// 驗證表單if (!currentUser.value.name || !currentUser.value.email || !validateEmail()) {return;}if (editingUser.value) {// 更新現有用戶const index = users.value.findIndex(u => u.id === currentUser.value.id);if (index !== -1) {users.value[index] = { ...currentUser.value };}} else {// 添加新用戶const newId = users.value.length > 0 ? Math.max(...users.value.map(u => u.id)) + 1 : 1;users.value.push({id: newId,name: currentUser.value.name,email: currentUser.value.email,role: currentUser.value.role});}showModal.value = false;};// 刪除用戶const deleteUser = (id) => {if (confirm('確定要刪除這個用戶嗎?此操作不可撤銷。')) {users.value = users.value.filter(user => user.id !== id);}};// 監聽郵箱變化watch(() => currentUser.value.email, () => {if (formSubmitted.value) {validateEmail();}});return {users,searchQuery,showModal,currentUser,editingUser,formSubmitted,isValidEmail,filteredUsers,handleSearch,clearSearch,openModal,closeModal,saveUser,deleteUser,validateEmail};}}).mount('#app');</script>
</body>
</html>