web作业9 !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width,initial-scale1.0 title管理控制台 · StoryWeaver/title script srchttps://unpkg.com/vue3/dist/vue.global.prod.js/script script srchttps://unpkg.com/axios/dist/axios.min.js/script script src/i18n.js/script style :root{ --ink:#1a1a26;--ink-2:#4a4a5c;--ink-3:#808090;--ink-4:#b0b0ba; --bg:#f3f1ec;--bg-warm:#f8f7f4;--white:#ffffff;--surface:#faf9f6; --border:#e6e3dc;--border-light:#f0ede7; --accent:#5568e0;--accent-bg:#eef0ff;--accent-glow:rgba(85,104,224,.12); --amber:#e08830;--amber-bg:#fef6ed; --teal:#3ba088;--teal-bg:#eaf7f3; --coral:#e06860;--coral-bg:#fdf2f0; --radius-sm:7px;--radius:10px;--radius-lg:14px; --font:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Microsoft YaHei,sans-serif; --shadow-card:0 1px 3px rgba(0,0,0,.04),0 1px 2px rgba(0,0,0,.03); --shadow-pop:0 8px 30px rgba(0,0,0,.08),0 2px 8px rgba(0,0,0,.04); } *{margin:0;padding:0;box-sizing:border-box} body{font-family:var(--font);background:var(--bg);min-height:100vh;-webkit-font-smoothing:antialiased;color:var(--ink)} .topbar{ height:50px;display:flex;align-items:center;padding:0 20px;justify-content:space-between; background:rgba(255,255,255,.88);backdrop-filter:blur(14px); border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100; } .topbar .t-left{display:flex;align-items:center;gap:10px} .topbar .t-logo{width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,var(--accent),#7b6cf0);display:flex;align-items:center;justify-content:center;font-size:16px;color:#fff;font-weight:700} .topbar .t-title{font-size:14px;font-weight:700;color:var(--ink);letter-spacing:-.2px} .topbar .t-badge{font-size:9.5px;padding:2px 8px;border-radius:7px;background:var(--accent-bg);color:var(--accent);font-weight:700} .topbar .t-right{display:flex;align-items:center;gap:8px} .topbar .t-link{color:var(--accent);text-decoration:none;font-size:12.5px;font-weight:500} .topbar .t-avatar{width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,var(--amber),#f0c878);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:#fff} .topbar .t-name{font-size:12.5px;font-weight:600;color:var(--ink-2)} .topbar .t-sep{width:1px;height:18px;background:var(--border)} .t-primary-badge{font-size:9.5px;padding:2px 8px;border-radius:7px;background:var(--amber-bg);color:var(--amber);font-weight:700} .lang-chip{font-size:10px;font-weight:700;color:var(--ink-3);border:1px solid var(--border);padding:3px 8px;border-radius:10px;cursor:pointer;user-select:none;background:#fff;transition:.15s} .lang-chip:hover{color:var(--accent);border-color:var(--accent);background:var(--accent-bg)} .btn-logout{padding:5px 14px;border:1px solid var(--border);border-radius:16px;background:#fff;cursor:pointer;font-size:11.5px;font-weight:500;color:var(--ink-2);transition:.15s} .btn-logout:hover{background:var(--coral);color:#fff;border-color:var(--coral)} .container{max-width:1200px;margin:0 auto;padding:22px 20px} .page-head{margin-bottom:22px} .page-head h1{font-size:22px;font-weight:800;color:var(--ink);letter-spacing:-.3px} .page-head .subtitle{font-size:13px;color:var(--ink-3);margin-top:4px} .stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:22px} .stat-card{background:#fff;border-radius:var(--radius-lg);padding:20px 22px;border:1px solid var(--border);box-shadow:var(--shadow-card);display:flex;align-items:center;gap:14px;transition:.2s;position:relative;overflow:hidden} .stat-card:hover{transform:translateY(-2px);box-shadow:var(--shadow-pop)} .stat-card .s-icon{width:50px;height:50px;border-radius:13px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0} .stat-card.total .s-icon{background:var(--accent-bg)} .stat-card.stories .s-icon{background:var(--teal-bg)} .stat-card.admins .s-icon{background:var(--amber-bg)} .stat-card.users .s-icon{background:#f0eeff} .stat-card .s-val{font-size:32px;font-weight:800;color:var(--ink);line-height:1} .stat-card .s-label{font-size:12px;color:var(--ink-3);margin-top:3px;font-weight:500} .panel{background:#fff;border-radius:var(--radius-lg);border:1px solid var(--border);box-shadow:var(--shadow-card);overflow:hidden} .panel .p-head{padding:14px 20px;border-bottom:1px solid var(--border-light);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px} .panel .p-head h2{font-size:15px;font-weight:700;color:var(--ink)} .panel .p-head .p-tools{display:flex;align-items:center;gap:8px} .panel .p-head .p-count{font-size:11px;color:var(--ink-3);background:var(--surface);padding:3px 10px;border-radius:8px;font-weight:600} .quick-search{padding:5px 12px;border:1px solid var(--border);border-radius:14px;font-size:12px;outline:none;width:180px;transition:.2s;font-family:var(--font);background:var(--surface);color:var(--ink-2)} .quick-search:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow);width:220px} .table-wrap{overflow-x:auto} table{width:100%;border-collapse:collapse} th,td{padding:12px 18px;text-align:left;border-bottom:1px solid var(--border-light);font-size:13px} th{background:var(--surface);color:var(--ink-3);font-weight:700;font-size:10.5px;text-transform:uppercase;letter-spacing:.5px} td{color:var(--ink-2)} tr:hover td{background:var(--surface)} tr:last-child td{border-bottom:none} .role-chip{display:inline-block;padding:2px 9px;border-radius:7px;font-size:10.5px;font-weight:600} .role-chip.admin{background:var(--teal-bg);color:#1d5c42} .role-chip.user{background:var(--accent-bg);color:var(--accent)} .primary-chip{display:inline-block;padding:1px 6px;border-radius:5px;font-size:9px;font-weight:700;background:var(--amber-bg);color:var(--amber);margin-left:4px} .btn{padding:7px 14px;border:none;border-radius:7px;font-size:12px;cursor:pointer;transition:.15s;font-weight:600;font-family:var(--font)} .btn:active{transform:scale(.97)} .btn-primary{background:var(--accent);color:#fff} .btn-primary:hover{background:#4a5cd4} .btn-warn{background:var(--amber-bg);color:#b86d20} .btn-warn:hover{background:#fde8d0} .btn-danger{background:#fff;color:var(--coral);border:1px solid #f0cec6} .btn-danger:hover{background:var(--coral);color:#fff;border-color:var(--coral)} .btn-teal{background:var(--teal);color:#fff} .btn-teal:hover{background:#349078} .btn-ghost{background:#fff;color:var(--ink-2);border:1px solid var(--border)} .btn-ghost:hover{border-color:var(--accent);color:var(--accent)} .btn-sm{padding:4px 10px;font-size:11px;margin:0 2px} .btn-xs{padding:3px 8px;font-size:10px;margin:0 1px} .btn:disabled{opacity:.4;cursor:not-allowed} .empty-state{text-align:center;padding:48px;color:var(--ink-4);font-size:13px} /* Modal */ .modal-over{display:none;position:fixed;inset:0;background:rgba(0,0,0,.25);z-index:1000;justify-content:center;align-items:center;backdrop-filter:blur(4px)} .modal-over.active{display:flex} .modal-box{background:#fff;border-radius:var(--radius-lg);padding:28px;width:480px;max-width:92vw;max-height:85vh;overflow-y:auto;border:1px solid var(--border);box-shadow:var(--shadow-pop);animation:mdIn .25s cubic-bezier(.34,1.56,.64,1)} keyframes mdIn{from{opacity:0;transform:translateY(10px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}} .modal-box h3{font-size:16px;font-weight:700;color:var(--ink);margin-bottom:20px} .field{margin-bottom:14px} .field label{display:block;font-size:10px;color:var(--ink-3);font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px} .field input,.field select{width:100%;padding:9px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:13px;outline:none;font-family:var(--font);transition:.2s;background:var(--surface);color:var(--ink)} .field input:focus,.field select:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow);background:#fff} .field .hint{font-size:10px;color:var(--ink-4);margin-top:2px} .mod-acts{display:flex;gap:7px;justify-content:flex-end;margin-top:18px} .err-msg{color:var(--coral);font-size:12px;margin-bottom:10px;padding:7px 10px;background:var(--coral-bg);border-radius:6px} /* Toast */ .toast-rack{position:fixed;top:18px;right:18px;z-index:9999;display:flex;flex-direction:column;gap:6px;pointer-events:none} .toast-msg{padding:10px 18px;border-radius:var(--radius-sm);font-size:12px;font-weight:500;pointer-events:auto;animation:tsIn .28s cubic-bezier(.34,1.56,.64,1);box-shadow:var(--shadow-pop)} .toast-msg.ok{background:var(--teal-bg);color:#1d5c42;border:1px solid #c3e5d8} .toast-msg.err{background:var(--coral-bg);color:#9d3830;border:1px solid #f0cec6} keyframes tsIn{from{opacity:0;transform:translateX(28px)}to{opacity:1;transform:translateX(0)}} .perm-card{display:flex;align-items:center;gap:8px;padding:6px 14px;background:var(--amber-bg);border-radius:10px;font-size:12px;color:#b86d20;font-weight:600;margin-bottom:16px} media(max-width:768px){.stat-grid{grid-template-columns:repeat(2,1fr)}} media(max-width:480px){.stat-grid{grid-template-columns:1fr}} /style /head body div idapp div classtopbar div classt-left div classt-logo⚙/div span classt-titleStoryWeaver/span span classt-badge管理控制台/span span classt-primary-badge v-ifperms.isPrimaryAdmin主管理员/span /div div classt-right span classlang-chip clickswitchLang{{ localezh-CN?EN:中文 }}/span span classt-sep/span a href/story.html classt-link 创作中心/a span classt-sep/span div classt-avatar{{ userName.charAt(0) }}/div span classt-name{{ userName }}/span button classbtn-logout clicklogout退出/button /div /div div classcontainer div classpage-head h1管理员仪表盘/h1 p classsubtitle系统运行概况与用户管理/p /div !-- 权限提示 -- div classperm-card v-if!perms.isPrimaryAdmin ⚡ 普通管理员 — 可新增用户和删除普通用户不可编辑信息、修改角色或删除管理员 /div !-- 统计 -- div classstat-grid div classstat-card totaldiv classs-icon/divdivdiv classs-val{{ stats.totalUsers }}/divdiv classs-label总注册用户/div/div/div div classstat-card storiesdiv classs-icon/divdivdiv classs-val{{ stats.totalStories }}/divdiv classs-label故事总数/div/div/div div classstat-card adminsdiv classs-icon/divdivdiv classs-val{{ stats.adminCount }}/divdiv classs-label管理员/div/div/div div classstat-card usersdiv classs-icon/divdivdiv classs-val{{ stats.userCount }}/divdiv classs-label普通用户/div/div/div /div !-- 用户管理 -- div classpanel div classp-head h2 用户管理/h2 div classp-tools input classquick-search v-modelfilterText placeholder搜索用户名或邮箱... span classp-count{{ filteredUsers.length }} 位用户/span button classbtn btn-primary btn-sm clickopenAddUser 新增用户/button button classbtn btn-ghost btn-sm clickrefresh 刷新/button /div /div div classtable-wrap v-iffilteredUsers.length table thead tr thID/thth用户名/thth邮箱/thth年龄/thth手机号/thth角色/thth注册时间/thth操作/th /tr /thead tbody tr v-foru in filteredUsers :keyu.id tdspan stylecolor:var(--ink-4);font-family:monospace;font-size:11px;#{{ u.id }}/span/td td strong{{ u.name }}/strong span classprimary-chip v-ifu.emailadminstory.com主管理员/span /td td{{ u.email }}/td td{{ u.age || - }}/td td{{ u.phone || - }}/td td span classrole-chip :classu.roleADMIN?admin:user {{ u.roleADMIN?管理员:用户 }} /span /td td stylefont-size:11px;color:var(--ink-3);{{ fmtTime(u.createTime) }}/td td !-- 编辑按钮仅主管理员可见 -- button classbtn btn-teal btn-xs v-ifperms.canEditUsers clickopenEditUser(u)编辑/button !-- 角色切换仅主管理员可见且不能操作自己和主管理员 -- button classbtn btn-warn btn-xs v-ifperms.canChangeRoles u.email!adminstory.com u.id!perms.userId clicktoggleRole(u) {{ u.roleADMIN?降为普通用户:提升为管理员 }} /button !-- 删除主管理员可删任何人(除自己)普通管理员只能删普通用户 -- button classbtn btn-danger btn-xs v-if(perms.canDeleteAdmins u.id!perms.userId) || (!perms.isPrimaryAdmin u.role!ADMIN) clickdelUser(u.id) 删除 /button /td /tr /tbody /table /div div v-else classempty-state div stylefont-size:32px;margin-bottom:8px;/div {{ filterText ? 没有匹配的用户 : 暂无用户数据 }} /div /div /div !-- 新增/编辑用户 Modal -- div classmodal-over :class{active:showUserModal} click.selfcloseUserModal div classmodal-box h3{{ editingUser ? 编辑用户 #editingUser.id : 新增用户 }}/h3 form submit.preventsaveUser div classfield label用户名 span stylecolor:var(--coral)*/span/label input typetext v-modeluserForm.name placeholder请输入用户名 required /div div classfield label邮箱/label input typeemail v-modeluserForm.email placeholder请输入邮箱可选 /div div classfield label手机号/label input typetext v-modeluserForm.phone placeholder请输入手机号可选 /div div classfield label年龄/label input typenumber v-model.numberuserForm.age placeholder请输入年龄 min0 max150 /div div classfield label地址/label input typetext v-modeluserForm.address placeholder请输入地址可选 /div div classfield v-if!editingUser label密码/label input typepassword v-modeluserForm.password placeholder留空则使用默认密码 123456 div classhint不填写密码则默认为 123456/div /div div classfield v-ifeditingUser label新密码/label input typepassword v-modeluserForm.password placeholder留空则不修改密码 div classhint留空则保持原密码不变/div /div div classfield v-if!editingUser perms.isPrimaryAdmin label角色/label select v-modeluserForm.role option valueUSER普通用户/option option valueADMIN管理员/option /select /div p classerr-msg v-ifuserFormError{{ userFormError }}/p div classmod-acts button typebutton classbtn btn-ghost clickcloseUserModal取消/button button typesubmit classbtn btn-primary{{ editingUser ? 保存修改 : 确认新增 }}/button /div /form /div /div !-- Toast -- div classtoast-rack div classtoast-msg v-for(t,i) in toasts :keyi :classt.type {{ t.typeok?✓:✕ }} {{ t.message }} /div /div /div script const{createApp,ref,reactive,computed,onMounted}Vue; createApp({ setup(){ axios.defaults.withCredentialstrue; var _loczh-CN;try{_locI18n.getLocale()}catch(e){} const localeref(_loc); const tfunction(k){try{return I18n.t(k)}catch(e){return k}}; const switchLangfunction(){var nllocale.valuezh-CN?en:zh-CN;locale.valuenl;I18n.setLocale(nl)}; const userNameref(); const usersref([]); const filterTextref(); const statsreactive({totalUsers:0,totalStories:0,adminCount:0,userCount:0}); const permsreactive({isPrimaryAdmin:false,canEditUsers:false,canChangeRoles:false,canDeleteAdmins:false,canCreateUsers:true,canDeleteUsers:true,userId:null,userName:}); const toastsref([]); var _tid0; const toastfunction(m,ty){var id_tid;toasts.value.push({id,message:m,type:ty||ok});setTimeout(function(){toasts.valuetoasts.value.filter(function(x){return x.id!id})},2600)}; const showUserModalref(false); const editingUserref(null); const userFormreactive({name:,email:,phone:,age:null,address:,password:,role:USER}); const userFormErrorref(); const filteredUserscomputed(function(){ if(!filterText.value)return users.value; var kwfilterText.value.toLowerCase(); return users.value.filter(function(u){ return (u.name||).toLowerCase().indexOf(kw)!-1||(u.email||).toLowerCase().indexOf(kw)!-1; }); }); const refreshasync function(){ try{ var _aawait Promise.all([axios.get(/api/admin/users),axios.get(/api/admin/stats),axios.get(/api/auth/me),axios.get(/api/admin/me/permissions)]); if(_a[2].data.code200_a[2].data.data.role!ADMIN){window.location.href/story.html;return} userName.value_a[2].data.data?.name||; users.value_a[0].data.data||[]; Object.assign(stats,_a[1].data.data||{}); Object.assign(perms,_a[3].data.data||{}); }catch(e){if(e.response?.status401||e.response?.status403)window.location.href/login.html} }; const openAddUserfunction(){ editingUser.valuenull;userFormError.value; userForm.name;userForm.email;userForm.phone;userForm.agenull;userForm.address;userForm.password;userForm.roleUSER; showUserModal.valuetrue; }; const openEditUserfunction(u){ if(!perms.canEditUsers){toast(仅主管理员可编辑用户信息,err);return} editingUser.valueu;userFormError.value; userForm.nameu.name||;userForm.emailu.email||;userForm.phoneu.phone||; userForm.ageu.age;userForm.addressu.address||;userForm.password;userForm.roleu.role||USER; showUserModal.valuetrue; }; const closeUserModalfunction(){showUserModal.valuefalse;editingUser.valuenull}; const saveUserasync function(){ userFormError.value; try{ var payload{...userForm}; if(!payload.password)delete payload.password; if(editingUser.value){ // 编辑用户 - PUT var rawait axios.put(/api/admin/users/editingUser.value.id,payload); toast(r.data.message||用户信息已更新,ok); }else{ // 新增用户 - POST var rawait axios.post(/api/admin/users,payload); toast(r.data.message||用户创建成功,ok); } closeUserModal();refresh(); }catch(e){ var msge.response?.data?.message||e.message; userFormError.valuemsg; } }; const toggleRoleasync function(u){ if(!perms.canChangeRoles){toast(仅主管理员可修改用户角色,err);return} var nru.roleADMIN?USER:ADMIN,nrNamenrADMIN?管理员:普通用户; if(!confirm(确定将 u.name 的角色改为「nrName」))return; try{await axios.put(/api/admin/users/u.id/role,{role:nr});toast(角色已更新,ok);refresh()}catch(e){toast(e.response?.data?.message||操作失败,err)} }; const delUserasync function(id){ if(!confirm(确定删除该用户吗此操作不可恢复。))return; try{await axios.delete(/api/admin/users/id);toast(用户已删除,ok);refresh()}catch(e){toast(e.response?.data?.message||删除失败,err)} }; const logoutasync function(){await axios.post(/api/auth/logout);window.location.href/login.html}; const fmtTimefunction(t){return t?t.substring(0,16).replace(T, ):-}; onMounted(refresh); return{locale,t,switchLang,userName,users,filterText,stats,perms,toasts,filteredUsers,refresh,showUserModal,editingUser,userForm,userFormError,openAddUser,openEditUser,closeUserModal,saveUser,toggleRole,delUser,logout,fmtTime}; } }).mount(#app); /script /body /html