collabdocchat 2.5.6 → 2.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-DkEWw6lP.js +2451 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/scripts/cleanup-scripts.js +3 -0
- package/scripts/fix-startup-issues.js +3 -0
- package/scripts/start-simple.js +3 -0
- package/server/public/index.html +3 -0
- package/src/pages/admin-dashboard.js +627 -42
- package/dist/assets/index-CNzBYLJL.js +0 -2166
|
@@ -0,0 +1,2451 @@
|
|
|
1
|
+
(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const c of document.querySelectorAll('link[rel="modulepreload"]'))n(c);new MutationObserver(c=>{for(const s of c)if(s.type==="childList")for(const d of s.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&n(d)}).observe(document,{childList:!0,subtree:!0});function o(c){const s={};return c.integrity&&(s.integrity=c.integrity),c.referrerPolicy&&(s.referrerPolicy=c.referrerPolicy),c.crossOrigin==="use-credentials"?s.credentials="include":c.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(c){if(c.ep)return;c.ep=!0;const s=o(c);fetch(c.href,s)}})();const De="http://localhost:3000/api";class ze{async login(e,o){const n=await fetch(`${De}/auth/login`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:o})});if(!n.ok){const c=await n.json();throw new Error(c.message)}return await n.json()}async register(e,o){const n=await fetch(`${De}/auth/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:o})});if(!n.ok){const c=await n.json();throw new Error(c.message)}return await n.json()}async getCurrentUser(){const e=localStorage.getItem("token"),o=await fetch(`${De}/auth/me`,{headers:{Authorization:`Bearer ${e}`}});if(!o.ok)throw new Error("获取用户信息失败");return(await o.json()).user}logout(){localStorage.removeItem("token"),window.location.reload()}}class jt{constructor(){this.ws=null,this.listeners=new Map}connect(e){this.ws=new WebSocket("ws://localhost:3000"),this.ws.onopen=()=>{console.log("✅ WebSocket 连接成功"),this.send({type:"auth",token:e})},this.ws.onmessage=o=>{const n=JSON.parse(o.data);this.notifyListeners(n.type,n)},this.ws.onerror=o=>{console.error("❌ WebSocket 错误:",o)},this.ws.onclose=()=>{console.log("🔌 WebSocket 连接关闭"),setTimeout(()=>this.connect(e),3e3)}}send(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}on(e,o){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(o)}off(e,o){if(this.listeners.has(e)){const n=this.listeners.get(e),c=n.indexOf(o);c>-1&&n.splice(c,1)}}notifyListeners(e,o){this.listeners.has(e)&&this.listeners.get(e).forEach(n=>n(o))}joinGroup(e){this.send({type:"join_group",groupId:e})}sendChatMessage(e,o,n){this.send({type:"chat_message",groupId:e,username:o,content:n})}syncDocument(e,o,n){this.send({type:"document_sync",documentId:e,content:o,cursorPosition:n})}respondToCall(e,o){this.send({type:"call_response",groupId:e,username:o})}sendTyping(e,o,n){this.send({type:"typing",documentId:e,username:o,isTyping:n})}sendWhiteboardDraw(e,o){this.send({type:"whiteboard_draw",groupId:e,...o})}sendWhiteboardClear(e){this.send({type:"whiteboard_clear",groupId:e})}}function _t(a){const e=document.getElementById("app"),o=new ze;e.innerHTML=`
|
|
2
|
+
<div class="login-container">
|
|
3
|
+
<div class="login-card">
|
|
4
|
+
<div class="login-header">
|
|
5
|
+
<h1 class="logo">CollabDocChat</h1>
|
|
6
|
+
<p class="tagline">实时协作 · 智能沟通</p>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div id="loginSection">
|
|
10
|
+
<form id="loginForm" class="auth-form">
|
|
11
|
+
<div class="form-group">
|
|
12
|
+
<label>用户名</label>
|
|
13
|
+
<input type="text" name="username" required placeholder="请输入用户名">
|
|
14
|
+
</div>
|
|
15
|
+
<div class="form-group">
|
|
16
|
+
<label>密码</label>
|
|
17
|
+
<input type="password" name="password" required placeholder="••••••••">
|
|
18
|
+
</div>
|
|
19
|
+
<button type="submit" class="btn-primary">登录</button>
|
|
20
|
+
<div class="error-message" id="loginError"></div>
|
|
21
|
+
</form>
|
|
22
|
+
|
|
23
|
+
<div class="auth-switch">
|
|
24
|
+
<p class="switch-text">还没有账号?<button class="link-btn" id="showRegister">立即注册</button></p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div id="registerSection" class="hidden">
|
|
29
|
+
<form id="registerForm" class="auth-form">
|
|
30
|
+
<div class="form-group">
|
|
31
|
+
<label>用户名</label>
|
|
32
|
+
<input type="text" name="username" required placeholder="请输入用户名">
|
|
33
|
+
</div>
|
|
34
|
+
<div class="form-group">
|
|
35
|
+
<label>密码</label>
|
|
36
|
+
<input type="password" name="password" required placeholder="••••••••">
|
|
37
|
+
</div>
|
|
38
|
+
<div class="register-note">
|
|
39
|
+
<p>注册后将自动分配为普通用户</p>
|
|
40
|
+
</div>
|
|
41
|
+
<button type="submit" class="btn-primary">注册</button>
|
|
42
|
+
<div class="error-message" id="registerError"></div>
|
|
43
|
+
</form>
|
|
44
|
+
|
|
45
|
+
<div class="auth-switch">
|
|
46
|
+
<p class="switch-text">已有账号?<button class="link-btn" id="showLogin">返回登录</button></p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
`,document.getElementById("showRegister").addEventListener("click",()=>{document.getElementById("loginSection").classList.add("hidden"),document.getElementById("registerSection").classList.remove("hidden")}),document.getElementById("showLogin").addEventListener("click",()=>{document.getElementById("registerSection").classList.add("hidden"),document.getElementById("loginSection").classList.remove("hidden")}),document.getElementById("loginForm").addEventListener("submit",async n=>{n.preventDefault();const c=new FormData(n.target),s=c.get("username"),d=c.get("password");try{const S=await o.login(s,d);a(S.user,S.token)}catch(S){document.getElementById("loginError").textContent=S.message}}),document.getElementById("registerForm").addEventListener("submit",async n=>{n.preventDefault();const c=new FormData(n.target),s=c.get("username"),d=c.get("password");try{const S=await o.register(s,d);a(S.user,S.token)}catch(S){document.getElementById("registerError").textContent=S.message}})}const je="http://localhost:3000/api";class gt{constructor(){this.token=localStorage.getItem("token")}async request(e,o={}){const n=await fetch(`${je}${e}`,{...o,headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.token}`,...o.headers}});if(!n.ok){const c=await n.json().catch(()=>({message:"请求失败"}));throw console.error("API 错误:",{endpoint:e,status:n.status,error:c}),new Error(c.message||`请求失败: ${n.status}`)}return await n.json()}async getGroups(){return await this.request("/groups")}async getAllGroups(){return await this.request("/groups/all")}async getGroup(e){return await this.request(`/groups/${e}`)}async createGroup(e,o,n){return await this.request("/groups",{method:"POST",body:JSON.stringify({name:e,description:o,members:n})})}async joinGroup(e){return await this.request(`/groups/${e}/join`,{method:"POST"})}async leaveGroup(e){return await this.request(`/groups/${e}/leave`,{method:"POST"})}async addMember(e,o){return await this.request(`/groups/${e}/members`,{method:"POST",body:JSON.stringify({userId:o})})}async removeMember(e,o){return await this.request(`/groups/${e}/members/${o}`,{method:"DELETE"})}async setMuteAll(e,o){return await this.request(`/groups/${e}/mute/all`,{method:"POST",body:JSON.stringify({enabled:o})})}async setUserMute(e,o,n){return await this.request(`/groups/${e}/mute/users/${o}`,{method:"POST",body:JSON.stringify({muted:n})})}async getAllUsers(){return await this.request("/auth/users")}async getGroupMessages(e){return await this.request(`/groups/${e}/messages`)}async randomCall(e,o=1){return await this.request(`/groups/${e}/call`,{method:"POST",body:JSON.stringify({count:o})})}async clearGroupMessages(e){return await this.request(`/groups/${e}/messages`,{method:"DELETE",body:JSON.stringify({deleteAll:!0})})}async getTasks(e){return await this.request(`/tasks/group/${e}`)}async getMyTasks(){return await this.request("/tasks/my")}async createTask(e){return await this.request("/tasks",{method:"POST",body:JSON.stringify(e)})}async updateTaskStatus(e,o){return await this.request(`/tasks/${e}/status`,{method:"PATCH",body:JSON.stringify({status:o})})}async deleteTask(e){return await this.request(`/tasks/${e}`,{method:"DELETE"})}async getDocuments(e){return await this.request(`/documents/group/${e}`)}async getDocument(e){return await this.request(`/documents/${e}`)}async createDocument(e,o,n,c=[]){return await this.request("/documents",{method:"POST",body:JSON.stringify({title:e,content:o,groupId:n,editableMembers:c})})}async updateDocument(e,o){return await this.request(`/documents/${e}`,{method:"PATCH",body:JSON.stringify({content:o})})}async updateDocumentPermissions(e,o){return await this.request(`/documents/${e}/permissions`,{method:"PATCH",body:JSON.stringify({editableMembers:o})})}async getDocumentVersions(e){return await this.request(`/documents/${e}/versions`)}async deleteDocument(e){return await this.request(`/documents/${e}`,{method:"DELETE"})}async getAuditLogs(e={},o={}){const n=new URLSearchParams;Object.keys(e).forEach(s=>{e[s]&&n.append(s,e[s])}),Object.keys(o).forEach(s=>{o[s]&&n.append(s,o[s])});const c=n.toString();return await this.request(`/audit${c?"?"+c:""}`)}async getUserActivityStats(e,o={}){const c=new URLSearchParams(o).toString();return await this.request(`/audit/user-stats/${e}${c?"?"+c:""}`)}async getDocumentEditHistory(e,o=20){return await this.request(`/audit/document-history/${e}?limit=${o}`)}async getGroupAuditLogs(e,o={},n={}){const c=new URLSearchParams;Object.keys(o).forEach(d=>{o[d]&&c.append(d,o[d])}),Object.keys(n).forEach(d=>{n[d]&&c.append(d,n[d])});const s=c.toString();return await this.request(`/audit/group/${e}${s?"?"+s:""}`)}async getAuditSummary(e={}){const n=new URLSearchParams(e).toString();return await this.request(`/audit/stats/summary${n?"?"+n:""}`)}async getAuditLogDetail(e){return await this.request(`/audit/${e}`)}async clearAuditLogs(e={}){return await this.request("/audit",{method:"DELETE",body:JSON.stringify(e)})}async uploadFile(e,o,n=""){const c=new FormData;c.append("file",o),c.append("groupId",e),n&&c.append("description",n);const s=await fetch(`${je}/files/upload`,{method:"POST",headers:{Authorization:`Bearer ${this.token}`},body:c});if(!s.ok){const d=await s.json().catch(()=>({message:"上传失败"}));throw new Error(d.message||"上传失败")}return await s.json()}async getGroupFiles(e){return await this.request(`/files/group/${e}`)}async deleteFile(e){return await this.request(`/files/${e}`,{method:"DELETE"})}getFileDownloadUrl(e){return`${je}/files/${e}/download?token=${this.token}`}async createPoll(e){return await this.request("/polls",{method:"POST",body:JSON.stringify(e)})}async getGroupPolls(e){return await this.request(`/polls/group/${e}`)}async getPoll(e){return await this.request(`/polls/${e}`)}async vote(e,o){return await this.request(`/polls/${e}/vote`,{method:"POST",body:JSON.stringify({optionIndexes:o})})}async endPoll(e){return await this.request(`/polls/${e}/end`,{method:"PUT"})}async deletePoll(e){return await this.request(`/polls/${e}`,{method:"DELETE"})}}function Le(a){if(typeof a!="string"||!a)throw new Error("expected a non-empty string, got: "+a)}function _e(a){if(typeof a!="number")throw new Error("expected a number, got: "+a)}const At=1,Pt=1,he="emoji",xe="keyvalue",Ye="favorites",Ft="tokens",mt="tokens",Ht="unicode",yt="count",Nt="group",Ot="order",bt="group-order",Ue="eTag",Be="url",at="skinTone",fe="readonly",Ke="readwrite",ht="skinUnicodes",qt="skinUnicodes",Ut="https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json",Rt="en";function Gt(a,e){const o=new Set,n=[];for(const c of a){const s=e(c);o.has(s)||(o.add(s),n.push(c))}return n}function nt(a){return Gt(a,e=>e.unicode)}function Wt(a){function e(o,n,c){const s=n?a.createObjectStore(o,{keyPath:n}):a.createObjectStore(o);if(c)for(const[d,[S,N]]of Object.entries(c))s.createIndex(d,S,{multiEntry:N});return s}e(xe),e(he,Ht,{[mt]:[Ft,!0],[bt]:[[Nt,Ot]],[ht]:[qt,!0]}),e(Ye,void 0,{[yt]:[""]})}const Re={},Te={},Me={};function vt(a,e,o){o.onerror=()=>e(o.error),o.onblocked=()=>e(new Error("IDB blocked")),o.onsuccess=()=>a(o.result)}async function Vt(a){const e=await new Promise((o,n)=>{const c=indexedDB.open(a,At);Re[a]=c,c.onupgradeneeded=s=>{s.oldVersion<Pt&&Wt(c.result)},vt(o,n,c)});return e.onclose=()=>Je(a),e}function Yt(a){return Te[a]||(Te[a]=Vt(a)),Te[a]}function me(a,e,o,n){return new Promise((c,s)=>{const d=a.transaction(e,o,{durability:"relaxed"}),S=typeof e=="string"?d.objectStore(e):e.map(L=>d.objectStore(L));let N;n(S,d,L=>{N=L}),d.oncomplete=()=>c(N),d.onerror=()=>s(d.error)})}function Je(a){const e=Re[a],o=e&&e.result;if(o){o.close();const n=Me[a];if(n)for(const c of n)c()}delete Re[a],delete Te[a],delete Me[a]}function Kt(a){return new Promise((e,o)=>{Je(a);const n=indexedDB.deleteDatabase(a);vt(e,o,n)})}function Jt(a,e){let o=Me[a];o||(o=Me[a]=[]),o.push(e)}const Xt=new Set([":D","XD",":'D","O:)",":X",":P",";P","XP",":L",":Z",":j","8D","XO","8)",":B",":O",":S",":'o","Dx","X(","D:",":C",">0)",":3","</3","<3","\\M/",":E","8#"]);function ve(a){return a.split(/[\s_]+/).map(e=>!e.match(/\w/)||Xt.has(e)?e.toLowerCase():e.replace(/[)(:,]/g,"").replace(/’/g,"'").toLowerCase()).filter(Boolean)}const Zt=2;function xt(a){return a.filter(Boolean).map(e=>e.toLowerCase()).filter(e=>e.length>=Zt)}function Qt(a){return a.map(({annotation:o,emoticon:n,group:c,order:s,shortcodes:d,skins:S,tags:N,emoji:L,version:j})=>{const H=[...new Set(xt([...(d||[]).map(ve).flat(),...(N||[]).map(ve).flat(),...ve(o),n]))].sort(),_={annotation:o,group:c,order:s,tags:N,tokens:H,unicode:L,version:j};if(n&&(_.emoticon=n),d&&(_.shortcodes=d),S){_.skinTones=[],_.skinUnicodes=[],_.skinVersions=[];for(const{tone:U,emoji:se,version:ae}of S)_.skinTones.push(U),_.skinUnicodes.push(se),_.skinVersions.push(ae)}return _})}function ft(a,e,o,n){a[e](o).onsuccess=c=>n&&n(c.target.result)}function be(a,e,o){ft(a,"get",e,o)}function wt(a,e,o){ft(a,"getAll",e,o)}function Xe(a){a.commit&&a.commit()}function ea(a,e){let o=a[0];for(let n=1;n<a.length;n++){const c=a[n];e(o)>e(c)&&(o=c)}return o}function kt(a,e){const o=ea(a,c=>c.length),n=[];for(const c of o)a.some(s=>s.findIndex(d=>e(d)===e(c))===-1)||n.push(c);return n}async function ta(a){return!await Ze(a,xe,Be)}async function aa(a,e,o){const[n,c]=await Promise.all([Ue,Be].map(s=>Ze(a,xe,s)));return n===o&&c===e}async function na(a,e){return me(a,he,fe,(n,c,s)=>{let d;const S=()=>{n.getAll(d&&IDBKeyRange.lowerBound(d,!0),50).onsuccess=N=>{const L=N.target.result;for(const j of L)if(d=j.unicode,e(j))return s(j);if(L.length<50)return s();S()}};S()})}async function Et(a,e,o,n){try{const c=Qt(e);await me(a,[he,xe],Ke,([s,d],S)=>{let N,L,j=0;function H(){++j===2&&_()}function _(){if(!(N===n&&L===o)){s.clear();for(const U of c)s.put(U);d.put(n,Ue),d.put(o,Be),Xe(S)}}be(d,Ue,U=>{N=U,H()}),be(d,Be,U=>{L=U,H()})})}finally{}}async function oa(a,e){return me(a,he,fe,(o,n,c)=>{const s=IDBKeyRange.bound([e,0],[e+1,0],!1,!0);wt(o.index(bt),s,c)})}async function Lt(a,e){const o=xt(ve(e));return o.length?me(a,he,fe,(n,c,s)=>{const d=[],S=()=>{d.length===o.length&&N()},N=()=>{const L=kt(d,j=>j.unicode);s(L.sort((j,H)=>j.order<H.order?-1:1))};for(let L=0;L<o.length;L++){const j=o[L],H=L===o.length-1?IDBKeyRange.bound(j,j+"",!1,!0):IDBKeyRange.only(j);wt(n.index(mt),H,_=>{d.push(_),S()})}}):[]}async function sa(a,e){const o=await Lt(a,e);return o.length?o.filter(n=>(n.shortcodes||[]).map(s=>s.toLowerCase()).includes(e.toLowerCase()))[0]||null:await na(a,c=>(c.shortcodes||[]).includes(e.toLowerCase()))||null}async function ia(a,e){return me(a,he,fe,(o,n,c)=>be(o,e,s=>{if(s)return c(s);be(o.index(ht),e,d=>c(d||null))}))}function Ze(a,e,o){return me(a,e,fe,(n,c,s)=>be(n,o,s))}function ra(a,e,o,n){return me(a,e,Ke,(c,s)=>{c.put(n,o),Xe(s)})}function da(a,e){return me(a,Ye,Ke,(o,n)=>be(o,e,c=>{o.put((c||0)+1,e),Xe(n)}))}function la(a,e,o){return o===0?[]:me(a,[Ye,he],fe,([n,c],s,d)=>{const S=[];n.index(yt).openCursor(void 0,"prev").onsuccess=N=>{const L=N.target.result;if(!L)return d(S);function j(U){if(S.push(U),S.length===o)return d(S);L.continue()}const H=L.primaryKey,_=e.byName(H);if(_)return j(_);be(c,H,U=>{if(U)return j(U);L.continue()})}})}const $e="";function ca(a,e){const o=new Map;for(const c of a){const s=e(c);for(const d of s){let S=o;for(let L=0;L<d.length;L++){const j=d.charAt(L);let H=S.get(j);H||(H=new Map,S.set(j,H)),S=H}let N=S.get($e);N||(N=[],S.set($e,N)),N.push(c)}}return(c,s)=>{let d=o;for(let L=0;L<c.length;L++){const j=c.charAt(L),H=d.get(j);if(H)d=H;else return[]}if(s)return d.get($e)||[];const S=[],N=[d];for(;N.length;){const j=[...N.shift().entries()].sort((H,_)=>H[0]<_[0]?-1:1);for(const[H,_]of j)H===$e?S.push(..._):N.push(_)}return S}}const pa=["name","url"];function ua(a){const e=a&&Array.isArray(a),o=e&&a.length&&(!a[0]||pa.some(n=>!(n in a[0])));if(!e||o)throw new Error("Custom emojis are in the wrong format")}function ot(a){ua(a);const e=(_,U)=>_.name.toLowerCase()<U.name.toLowerCase()?-1:1,o=a.sort(e),c=ca(a,_=>{const U=new Set;if(_.shortcodes)for(const se of _.shortcodes)for(const ae of ve(se))U.add(ae);return U}),s=_=>c(_,!0),d=_=>c(_,!1),S=_=>{const U=ve(_),se=U.map((ae,ie)=>(ie<U.length-1?s:d)(ae));return kt(se,ae=>ae.name).sort(e)},N=new Map,L=new Map;for(const _ of a){L.set(_.name.toLowerCase(),_);for(const U of _.shortcodes||[])N.set(U.toLowerCase(),_)}return{all:o,search:S,byShortcode:_=>N.get(_.toLowerCase()),byName:_=>L.get(_.toLowerCase())}}const ga=typeof wrappedJSObject<"u";function ke(a){if(!a)return a;if(ga&&(a=structuredClone(a)),delete a.tokens,a.skinTones){const e=a.skinTones.length;a.skins=Array(e);for(let o=0;o<e;o++)a.skins[o]={tone:a.skinTones[o],unicode:a.skinUnicodes[o],version:a.skinVersions[o]};delete a.skinTones,delete a.skinUnicodes,delete a.skinVersions}return a}function $t(a){a||console.warn("emoji-picker-element is more efficient if the dataSource server exposes an ETag header.")}const ma=["annotation","emoji","group","order","version"];function ya(a){if(!a||!Array.isArray(a)||!a[0]||typeof a[0]!="object"||ma.some(e=>!(e in a[0])))throw new Error("Emoji data is in the wrong format")}function It(a,e){if(Math.floor(a.status/100)!==2)throw new Error("Failed to fetch: "+e+": "+a.status)}async function ba(a){const e=await fetch(a,{method:"HEAD"});It(e,a);const o=e.headers.get("etag");return $t(o),o}async function Ge(a){const e=await fetch(a);It(e,a);const o=e.headers.get("etag");$t(o);const n=await e.json();return ya(n),[o,n]}function ha(a){for(var e="",o=new Uint8Array(a),n=o.byteLength,c=-1;++c<n;)e+=String.fromCharCode(o[c]);return e}function va(a){for(var e=a.length,o=new ArrayBuffer(e),n=new Uint8Array(o),c=-1;++c<e;)n[c]=a.charCodeAt(c);return o}async function Tt(a){const e=JSON.stringify(a);let o=va(e);const n=await crypto.subtle.digest("SHA-1",o),c=ha(n);return btoa(c)}async function xa(a,e){let o,n=await ba(e);if(!n){const c=await Ge(e);n=c[0],o=c[1],n||(n=await Tt(o))}await aa(a,e,n)||(o||(o=(await Ge(e))[1]),await Et(a,o,e,n))}async function fa(a,e){let[o,n]=await Ge(e);o||(o=await Tt(n)),await Et(a,n,e,o)}async function wa(a,e){try{await xa(a,e)}catch(o){if(o.name!=="InvalidStateError")throw o}}class ka{constructor({dataSource:e=Ut,locale:o=Rt,customEmoji:n=[]}={}){this.dataSource=e,this.locale=o,this._dbName=`emoji-picker-element-${this.locale}`,this._db=void 0,this._lazyUpdate=void 0,this._custom=ot(n),this._clear=this._clear.bind(this),this._ready=this._init()}async _init(){const e=this._db=await Yt(this._dbName);Jt(this._dbName,this._clear);const o=this.dataSource;await ta(e)?await fa(e,o):this._lazyUpdate=wa(e,o)}async ready(){const e=async()=>(this._ready||(this._ready=this._init()),this._ready);await e(),this._db||await e()}async getEmojiByGroup(e){return _e(e),await this.ready(),nt(await oa(this._db,e)).map(ke)}async getEmojiBySearchQuery(e){Le(e),await this.ready();const o=this._custom.search(e),n=nt(await Lt(this._db,e)).map(ke);return[...o,...n]}async getEmojiByShortcode(e){Le(e),await this.ready();const o=this._custom.byShortcode(e);return o||ke(await sa(this._db,e))}async getEmojiByUnicodeOrName(e){Le(e),await this.ready();const o=this._custom.byName(e);return o||ke(await ia(this._db,e))}async getPreferredSkinTone(){return await this.ready(),await Ze(this._db,xe,at)||0}async setPreferredSkinTone(e){return _e(e),await this.ready(),ra(this._db,xe,at,e)}async incrementFavoriteEmojiCount(e){return Le(e),await this.ready(),da(this._db,e)}async getTopFavoriteEmoji(e){return _e(e),await this.ready(),(await la(this._db,this._custom,e)).map(ke)}set customEmoji(e){this._custom=ot(e)}get customEmoji(){return this._custom.all}async _shutdown(){await this.ready();try{await this._lazyUpdate}catch{}}_clear(){this._db=this._ready=this._lazyUpdate=void 0}async close(){await this._shutdown(),await Je(this._dbName)}async delete(){await this._shutdown(),await Kt(this._dbName)}}const We=[[-1,"✨","custom"],[0,"😀","smileys-emotion"],[1,"👋","people-body"],[3,"🐱","animals-nature"],[4,"🍎","food-drink"],[5,"🏠️","travel-places"],[6,"⚽","activities"],[7,"📝","objects"],[8,"⛔️","symbols"],[9,"🏁","flags"]].map(([a,e,o])=>({id:a,emoji:e,name:o})),Ae=We.slice(1),Ea=2,st=6,St=typeof requestIdleCallback=="function"?requestIdleCallback:setTimeout;function it(a){return a.unicode.includes("")}const La={"":17,"":16,"🫨":15.1,"🫠":14,"🥲":13.1,"🥻":12.1,"🥰":11,"🤩":5,"👱♀️":4,"🤣":3,"👁️🗨️":2,"😀":1,"😐️":.7,"😃":.6},$a=1e3,Ia="🖐️",Ta=8,Sa=["😊","😒","❤️","👍️","😍","😂","😭","☺️","😔","😩","😏","💕","🙌","😘"],Bt='"Twemoji Mozilla","Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji","EmojiOne Color","Android Emoji",sans-serif',Ba=(a,e)=>a<e?-1:a>e?1:0,rt=(a,e)=>{const o=document.createElement("canvas");o.width=o.height=1;const n=o.getContext("2d",{willReadFrequently:!0});return n.textBaseline="top",n.font=`100px ${Bt}`,n.fillStyle=e,n.scale(.01,.01),n.fillText(a,0,0),n.getImageData(0,0,1,1).data},Ma=(a,e)=>{const o=[...a].join(","),n=[...e].join(",");return o===n&&!o.startsWith("0,0,0,")};function Ca(a){const e=rt(a,"#000"),o=rt(a,"#fff");return e&&o&&Ma(e,o)}function za(){const a=Object.entries(La);try{for(const[e,o]of a)if(Ca(e))return o}catch{}finally{}return a[0][1]}let Pe;const Fe=()=>(Pe||(Pe=new Promise(a=>St(()=>a(za())))),Pe),Ve=new Map,Da="️",ja="\uD83C",_a="",Aa=127995,Pa=57339;function Fa(a,e){if(e===0)return a;const o=a.indexOf(_a);return o!==-1?a.substring(0,o)+String.fromCodePoint(Aa+e-1)+a.substring(o):(a.endsWith(Da)&&(a=a.substring(0,a.length-1)),a+ja+String.fromCodePoint(Pa+e-1))}function ge(a){a.preventDefault(),a.stopPropagation()}function He(a,e,o){return e+=a?-1:1,e<0?e=o.length-1:e>=o.length&&(e=0),e}function Mt(a,e){const o=new Set,n=[];for(const c of a){const s=e(c);o.has(s)||(o.add(s),n.push(c))}return n}function Ha(a,e){const o=n=>{const c={};for(const s of n)typeof s.tone=="number"&&s.version<=e&&(c[s.tone]=s.unicode);return c};return a.map(({unicode:n,skins:c,shortcodes:s,url:d,name:S,category:N,annotation:L})=>({unicode:n,name:S,shortcodes:s,url:d,category:N,annotation:L,id:n||S,skins:c&&o(c)}))}const Se=requestAnimationFrame;let Na=typeof ResizeObserver=="function";function Oa(a,e,o){let n;Na?(n=new ResizeObserver(o),n.observe(a)):Se(o),e.addEventListener("abort",()=>{n&&n.disconnect()})}function dt(a){{const e=document.createRange();return e.selectNode(a.firstChild),e.getBoundingClientRect().width}}let Ne;function qa(a,e,o){let n=!0;for(const c of a){const s=o(c);if(!s)continue;const d=dt(s);typeof Ne>"u"&&(Ne=dt(e));const S=d/1.8<Ne;Ve.set(c.unicode,S),S||(n=!1)}return n}function Ua(a){return Mt(a,e=>e)}function Ra(a){a&&(a.scrollTop=0)}function Ee(a,e,o){let n=a.get(e);return n||(n=o(),a.set(e,n)),n}function lt(a){return""+a}function Ga(a){const e=document.createElement("template");return e.innerHTML=a,e}const Wa=new WeakMap,Va=new WeakMap,Ya=Symbol("un-keyed"),Ka="replaceChildren"in Element.prototype;function Ja(a,e){Ka?a.replaceChildren(...e):(a.innerHTML="",a.append(...e))}function Xa(a,e){let o=a.firstChild,n=0;for(;o;){if(e[n]!==o)return!0;o=o.nextSibling,n++}return n!==e.length}function Za(a,e){const{targetNode:o}=e;let{targetParentNode:n}=e,c=!1;n?c=Xa(n,a):(c=!0,e.targetNode=void 0,e.targetParentNode=n=o.parentNode),c&&Ja(n,a)}function Qa(a,e){for(const o of e){const{targetNode:n,currentExpression:c,binding:{expressionIndex:s,attributeName:d,attributeValuePre:S,attributeValuePost:N}}=o,L=a[s];if(c!==L)if(o.currentExpression=L,d)if(L===null)n.removeAttribute(d);else{const j=S+lt(L)+N;n.setAttribute(d,j)}else{let j;Array.isArray(L)?Za(L,o):L instanceof Element?(j=L,n.replaceWith(j)):n.nodeValue=lt(L),j&&(o.targetNode=j)}}}function en(a){let e="",o=!1,n=!1,c=-1;const s=new Map,d=[];let S=0;for(let L=0,j=a.length;L<j;L++){const H=a[L];if(e+=H.slice(S),L===j-1)break;for(let q=0;q<H.length;q++)switch(H.charAt(q)){case"<":{H.charAt(q+1)==="/"?d.pop():(o=!0,d.push(++c));break}case">":{o=!1,n=!1;break}case"=":{n=!0;break}}const _=d[d.length-1],U=Ee(s,_,()=>[]);let se,ae,ie;if(n){const q=/(\S+)="?([^"=]*)$/.exec(H);se=q[1],ae=q[2];const V=/^([^">]*)("?)/.exec(a[L+1]);ie=V[1],e=e.slice(0,-1*q[0].length),S=V[0].length}else S=0;const ce={attributeName:se,attributeValuePre:ae,attributeValuePost:ie,expressionIndex:L};U.push(ce),!o&&!n&&(e+=" ")}return{template:Ga(e),elementsToBindings:s}}function ct(a,e,o){for(let n=0;n<a.length;n++){const c=a[n],s=c.attributeName?e:e.firstChild,d={binding:c,targetNode:s,targetParentNode:void 0,currentExpression:void 0};o.push(d)}}function tn(a,e){const o=[];let n;if(e.size===1&&(n=e.get(0)))ct(n,a,o);else{const c=document.createTreeWalker(a,NodeFilter.SHOW_ELEMENT);let s=a,d=-1;do{const S=e.get(++d);S&&ct(S,s,o)}while(s=c.nextNode())}return o}function an(a){const{template:e,elementsToBindings:o}=Ee(Wa,a,()=>en(a)),n=e.cloneNode(!0).content.firstElementChild,c=tn(n,o);return function(d){return Qa(d,c),n}}function nn(a){const e=Ee(Va,a,()=>new Map);let o=Ya;function n(s,...d){const S=Ee(e,s,()=>new Map);return Ee(S,o,()=>an(s))(d)}function c(s,d,S){return s.map((N,L)=>{const j=o;o=S(N);try{return d(N,L)}finally{o=j}})}return{map:c,html:n}}function on(a,e,o,n,c,s,d,S,N){const{labelWithSkin:L,titleForEmoji:j,unicodeWithSkin:H}=o,{html:_,map:U}=nn(e);function se(q,V,ne){return U(q,(oe,de)=>_`<button role="${V?"option":"menuitem"}" aria-selected="${V?de===e.activeSearchItem:null}" aria-label="${L(oe,e.currentSkinTone)}" title="${j(oe)}" class="${"emoji"+(V&&de===e.activeSearchItem?" active":"")+(oe.unicode?"":" custom-emoji")}" id="${`${ne}-${oe.id}`}" style="${oe.unicode?null:`--custom-emoji-background: url(${JSON.stringify(oe.url)})`}">${oe.unicode?H(oe,e.currentSkinTone):""}</button>`,oe=>`${ne}-${oe.id}`)}const ie=_`<section data-ref="rootElement" class="picker" aria-label="${e.i18n.regionLabel}" style="${e.pickerStyle||""}"><div class="pad-top"></div><div class="search-row"><div class="search-wrapper"><input id="search" class="search" type="search" role="combobox" enterkeyhint="search" placeholder="${e.i18n.searchLabel}" autocapitalize="none" autocomplete="off" spellcheck="true" aria-expanded="${!!(e.searchMode&&e.currentEmojis.length)}" aria-controls="search-results" aria-describedby="search-description" aria-autocomplete="list" aria-activedescendant="${e.activeSearchItemId?`emo-${e.activeSearchItemId}`:null}" data-ref="searchElement" data-on-input="onSearchInput" data-on-keydown="onSearchKeydown"><label class="sr-only" for="search">${e.i18n.searchLabel}</label> <span id="search-description" class="sr-only">${e.i18n.searchDescription}</span></div><div class="skintone-button-wrapper ${e.skinTonePickerExpandedAfterAnimation?"expanded":""}"><button id="skintone-button" class="emoji ${e.skinTonePickerExpanded?"hide-focus":""}" aria-label="${e.skinToneButtonLabel}" title="${e.skinToneButtonLabel}" aria-describedby="skintone-description" aria-haspopup="listbox" aria-expanded="${e.skinTonePickerExpanded}" aria-controls="skintone-list" data-on-click="onClickSkinToneButton">${e.skinToneButtonText||""}</button></div><span id="skintone-description" class="sr-only">${e.i18n.skinToneDescription}</span><div data-ref="skinToneDropdown" id="skintone-list" class="skintone-list hide-focus ${e.skinTonePickerExpanded?"":"hidden no-animate"}" style="transform:translateY(${e.skinTonePickerExpanded?0:"calc(-1 * var(--num-skintones) * var(--total-emoji-size))"})" role="listbox" aria-label="${e.i18n.skinTonesLabel}" aria-activedescendant="skintone-${e.activeSkinTone}" aria-hidden="${!e.skinTonePickerExpanded}" tabIndex="-1" data-on-focusout="onSkinToneOptionsFocusOut" data-on-click="onSkinToneOptionsClick" data-on-keydown="onSkinToneOptionsKeydown" data-on-keyup="onSkinToneOptionsKeyup">${U(e.skinTones,(q,V)=>_`<div id="skintone-${V}" class="emoji ${V===e.activeSkinTone?"active":""}" aria-selected="${V===e.activeSkinTone}" role="option" title="${e.i18n.skinTones[V]}" aria-label="${e.i18n.skinTones[V]}">${q}</div>`,q=>q)}</div></div><div class="nav" role="tablist" style="grid-template-columns:repeat(${e.groups.length},1fr)" aria-label="${e.i18n.categoriesLabel}" data-on-keydown="onNavKeydown" data-on-click="onNavClick">${U(e.groups,q=>_`<button role="tab" class="nav-button" aria-controls="tab-${q.id}" aria-label="${e.i18n.categories[q.name]}" aria-selected="${!e.searchMode&&e.currentGroup.id===q.id}" title="${e.i18n.categories[q.name]}" data-group-id="${q.id}"><div class="nav-emoji emoji">${q.emoji}</div></button>`,q=>q.id)}</div><div class="indicator-wrapper"><div class="indicator" style="transform:translateX(${(e.isRtl?-1:1)*e.currentGroupIndex*100}%)"></div></div><div class="message ${e.message?"":"gone"}" role="alert" aria-live="polite">${e.message||""}</div><div data-ref="tabpanelElement" class="tabpanel ${!e.databaseLoaded||e.message?"gone":""}" role="${e.searchMode?"region":"tabpanel"}" aria-label="${e.searchMode?e.i18n.searchResultsLabel:e.i18n.categories[e.currentGroup.name]}" id="${e.searchMode?null:`tab-${e.currentGroup.id}`}" tabIndex="0" data-on-click="onEmojiClick"><div data-action="calculateEmojiGridStyle">${U(e.currentEmojisWithCategories,(q,V)=>_`<div><div id="menu-label-${V}" class="category ${e.currentEmojisWithCategories.length===1&&e.currentEmojisWithCategories[0].category===""?"gone":""}" aria-hidden="true">${e.searchMode?e.i18n.searchResultsLabel:q.category?q.category:e.currentEmojisWithCategories.length>1?e.i18n.categories.custom:e.i18n.categories[e.currentGroup.name]}</div><div class="emoji-menu ${V!==0&&!e.searchMode&&e.currentGroup.id===-1?"visibility-auto":""}" style="${`--num-rows: ${Math.ceil(q.emojis.length/e.numColumns)}`}" data-action="updateOnIntersection" role="${e.searchMode?"listbox":"menu"}" aria-labelledby="menu-label-${V}" id="${e.searchMode?"search-results":null}">${se(q.emojis,e.searchMode,"emo")}</div></div>`,q=>q.category)}</div></div><div class="favorites onscreen emoji-menu ${e.message?"gone":""}" role="menu" aria-label="${e.i18n.favoritesLabel}" data-on-click="onEmojiClick">${se(e.currentFavorites,!1,"fav")}</div><button data-ref="baselineEmoji" aria-hidden="true" tabindex="-1" class="abs-pos hidden emoji baseline-emoji">😀</button></section>`,ce=(q,V)=>{for(const ne of a.querySelectorAll(`[${q}]`))V(ne,ne.getAttribute(q))};if(N){a.appendChild(ie);for(const q of["click","focusout","input","keydown","keyup"])ce(`data-on-${q}`,(V,ne)=>{V.addEventListener(q,n[ne])});ce("data-ref",(q,V)=>{s[V]=q}),d.addEventListener("abort",()=>{a.removeChild(ie)})}ce("data-action",(q,V)=>{let ne=S.get(V);ne||S.set(V,ne=new WeakSet),ne.has(q)||(ne.add(q),c[V](q))})}const Ce=typeof queueMicrotask=="function"?queueMicrotask:a=>Promise.resolve().then(a);function sn(a){let e=!1,o;const n=new Map,c=new Set;let s;const d=()=>{if(e)return;const L=[...c];c.clear();try{for(const j of L)j()}finally{s=!1,c.size&&(s=!0,Ce(d))}},S=new Proxy({},{get(L,j){if(o){let H=n.get(j);H||(H=new Set,n.set(j,H)),H.add(o)}return L[j]},set(L,j,H){if(L[j]!==H){L[j]=H;const _=n.get(j);if(_){for(const U of _)c.add(U);s||(s=!0,Ce(d))}}return!0}}),N=L=>{const j=()=>{const H=o;o=j;try{return L()}finally{o=H}};return j()};return a.addEventListener("abort",()=>{e=!0}),{state:S,createEffect:N}}function Oe(a,e,o){if(a.length!==e.length)return!1;for(let n=0;n<a.length;n++)if(!o(a[n],e[n]))return!1;return!0}const pt=new WeakMap;function rn(a,e,o){{const n=a.closest(".tabpanel");let c=pt.get(n);c||(c=new IntersectionObserver(o,{root:n,rootMargin:"50% 0px 50% 0px",threshold:0}),pt.set(n,c),e.addEventListener("abort",()=>{c.disconnect()})),c.observe(a)}}const qe=[],{assign:Ie}=Object;function dn(a,e){const o={},n=new AbortController,c=n.signal,{state:s,createEffect:d}=sn(c),S=new Map;Ie(s,{skinToneEmoji:void 0,i18n:void 0,database:void 0,customEmoji:void 0,customCategorySorting:void 0,emojiVersion:void 0}),Ie(s,e),Ie(s,{initialLoad:!0,currentEmojis:[],currentEmojisWithCategories:[],rawSearchText:"",searchText:"",searchMode:!1,activeSearchItem:-1,message:void 0,skinTonePickerExpanded:!1,skinTonePickerExpandedAfterAnimation:!1,currentSkinTone:0,activeSkinTone:0,skinToneButtonText:void 0,pickerStyle:void 0,skinToneButtonLabel:"",skinTones:[],currentFavorites:[],defaultFavoriteEmojis:void 0,numColumns:Ta,isRtl:!1,currentGroupIndex:0,groups:Ae,databaseLoaded:!1,activeSearchItemId:void 0}),d(()=>{s.currentGroup!==s.groups[s.currentGroupIndex]&&(s.currentGroup=s.groups[s.currentGroupIndex])});const N=t=>{a.getElementById(t).focus()},L=t=>a.getElementById(`emo-${t.id}`),j=(t,r)=>{o.rootElement.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0}))},H=(t,r)=>t.id===r.id,_=(t,r)=>{const{category:l,emojis:b}=t,{category:E,emojis:w}=r;return l!==E?!1:Oe(b,w,H)},U=t=>{Oe(s.currentEmojis,t,H)||(s.currentEmojis=t)},se=t=>{s.searchMode!==t&&(s.searchMode=t)},ae=t=>{Oe(s.currentEmojisWithCategories,t,_)||(s.currentEmojisWithCategories=t)},ie=(t,r)=>r&&t.skins&&t.skins[r]||t.unicode,V={labelWithSkin:(t,r)=>Ua([t.name||ie(t,r),t.annotation,...t.shortcodes||qe].filter(Boolean)).join(", "),titleForEmoji:t=>t.annotation||(t.shortcodes||qe).join(", "),unicodeWithSkin:ie},ne={onClickSkinToneButton:y,onEmojiClick:m,onNavClick:re,onNavKeydown:X,onSearchKeydown:F,onSkinToneOptionsClick:g,onSkinToneOptionsFocusOut:p,onSkinToneOptionsKeydown:h,onSkinToneOptionsKeyup:M,onSearchInput:i},oe={calculateEmojiGridStyle:$,updateOnIntersection:A};let de=!0;d(()=>{on(a,s,V,ne,oe,o,c,S,de),de=!1}),s.emojiVersion||Fe().then(t=>{t||(s.message=s.i18n.emojiUnsupportedMessage)}),d(()=>{async function t(){let r=!1;const l=setTimeout(()=>{r=!0,s.message=s.i18n.loadingMessage},$a);try{await s.database.ready(),s.databaseLoaded=!0}catch(b){console.error(b),s.message=s.i18n.networkErrorMessage}finally{clearTimeout(l),r&&(r=!1,s.message="")}}s.database&&t()}),d(()=>{s.pickerStyle=`
|
|
52
|
+
--num-groups: ${s.groups.length};
|
|
53
|
+
--indicator-opacity: ${s.searchMode?0:1};
|
|
54
|
+
--num-skintones: ${st};`}),d(()=>{s.customEmoji&&s.database&&B()}),d(()=>{s.customEmoji&&s.customEmoji.length?s.groups!==We&&(s.groups=We):s.groups!==Ae&&(s.currentGroupIndex&&s.currentGroupIndex--,s.groups=Ae)}),d(()=>{async function t(){s.databaseLoaded&&(s.currentSkinTone=await s.database.getPreferredSkinTone())}t()}),d(()=>{s.skinTones=Array(st).fill().map((t,r)=>Fa(s.skinToneEmoji,r))}),d(()=>{s.skinToneButtonText=s.skinTones[s.currentSkinTone]}),d(()=>{s.skinToneButtonLabel=s.i18n.skinToneLabel.replace("{skinTone}",s.i18n.skinTones[s.currentSkinTone])}),d(()=>{async function t(){const{database:r}=s,l=(await Promise.all(Sa.map(b=>r.getEmojiByUnicodeOrName(b)))).filter(Boolean);s.defaultFavoriteEmojis=l}s.databaseLoaded&&t()});function B(){const{customEmoji:t,database:r}=s,l=t||qe;r.customEmoji!==l&&(r.customEmoji=l)}d(()=>{async function t(){B();const{database:r,defaultFavoriteEmojis:l,numColumns:b}=s,E=await r.getTopFavoriteEmoji(b),w=await T(Mt([...E,...l],P=>P.unicode||P.name).slice(0,b));s.currentFavorites=w}s.databaseLoaded&&s.defaultFavoriteEmojis&&t()});function $(t){Oa(t,c,()=>{{const r=getComputedStyle(o.rootElement),l=parseInt(r.getPropertyValue("--num-columns"),10),b=r.getPropertyValue("direction")==="rtl";s.numColumns=l,s.isRtl=b}})}function A(t){rn(t,c,r=>{for(const{target:l,isIntersecting:b}of r)l.classList.toggle("onscreen",b)})}d(()=>{async function t(){const{searchText:r,currentGroup:l,databaseLoaded:b,customEmoji:E}=s;if(!b)s.currentEmojis=[],s.searchMode=!1;else if(r.length>=Ea){const w=await W(r);s.searchText===r&&(U(w),se(!0))}else{const{id:w}=l;if(w!==-1||E&&E.length){const P=await z(w);s.currentGroup.id===w&&(U(P),se(!1))}}}t()});const v=()=>{Se(()=>Ra(o.tabpanelElement))};d(()=>{const{currentEmojis:t,emojiVersion:r}=s,l=t.filter(b=>b.unicode).filter(b=>it(b)&&!Ve.has(b.unicode));if(!r&&l.length)U(t),Se(()=>k(l));else{const b=r?t:t.filter(C);U(b),v()}});function k(t){qa(t,o.baselineEmoji,L)?v():s.currentEmojis=[...s.currentEmojis]}function C(t){return!t.unicode||!it(t)||Ve.get(t.unicode)}async function D(t){const r=s.emojiVersion||await Fe();return t.filter(({version:l})=>!l||l<=r)}async function T(t){return Ha(t,s.emojiVersion||await Fe())}async function z(t){const r=t===-1?s.customEmoji:await s.database.getEmojiByGroup(t);return T(await D(r))}async function W(t){return T(await D(await s.database.getEmojiBySearchQuery(t)))}d(()=>{}),d(()=>{function t(){const{searchMode:l,currentEmojis:b}=s;if(l)return[{category:"",emojis:b}];const E=new Map;for(const w of b){const P=w.category||"";let G=E.get(P);G||(G=[],E.set(P,G)),G.push(w)}return[...E.entries()].map(([w,P])=>({category:w,emojis:P})).sort((w,P)=>s.customCategorySorting(w.category,P.category))}const r=t();ae(r)}),d(()=>{s.activeSearchItemId=s.activeSearchItem!==-1&&s.currentEmojis[s.activeSearchItem].id}),d(()=>{const{rawSearchText:t}=s;St(()=>{s.searchText=(t||"").trim(),s.activeSearchItem=-1})});function F(t){if(!s.searchMode||!s.currentEmojis.length)return;const r=l=>{ge(t),s.activeSearchItem=He(l,s.activeSearchItem,s.currentEmojis)};switch(t.key){case"ArrowDown":return r(!1);case"ArrowUp":return r(!0);case"Enter":if(s.activeSearchItem===-1)s.activeSearchItem=0;else return ge(t),pe(s.currentEmojis[s.activeSearchItem].id)}}function re(t){const{target:r}=t,l=r.closest(".nav-button");if(!l)return;const b=parseInt(l.dataset.groupId,10);o.searchElement.value="",s.rawSearchText="",s.searchText="",s.activeSearchItem=-1,s.currentGroupIndex=s.groups.findIndex(E=>E.id===b)}function X(t){const{target:r,key:l}=t,b=E=>{E&&(ge(t),E.focus())};switch(l){case"ArrowLeft":return b(r.previousElementSibling);case"ArrowRight":return b(r.nextElementSibling);case"Home":return b(r.parentElement.firstElementChild);case"End":return b(r.parentElement.lastElementChild)}}async function le(t){const r=await s.database.getEmojiByUnicodeOrName(t),l=[...s.currentEmojis,...s.currentFavorites].find(E=>E.id===t),b=l.unicode&&ie(l,s.currentSkinTone);return await s.database.incrementFavoriteEmojiCount(t),{emoji:r,skinTone:s.currentSkinTone,...b&&{unicode:b},...l.name&&{name:l.name}}}async function pe(t){const r=le(t);j("emoji-click-sync",r),j("emoji-click",await r)}function m(t){const{target:r}=t;if(!r.classList.contains("emoji"))return;ge(t);const l=r.id.substring(4);pe(l)}function u(t){s.currentSkinTone=t,s.skinTonePickerExpanded=!1,N("skintone-button"),j("skin-tone-change",{skinTone:t}),s.database.setPreferredSkinTone(t)}function g(t){const{target:{id:r}}=t,l=r&&r.match(/^skintone-(\d)/);if(!l)return;ge(t);const b=parseInt(l[1],10);u(b)}function y(t){s.skinTonePickerExpanded=!s.skinTonePickerExpanded,s.activeSkinTone=s.currentSkinTone,s.skinTonePickerExpanded&&(ge(t),Se(()=>N("skintone-list")))}d(()=>{s.skinTonePickerExpanded?o.skinToneDropdown.addEventListener("transitionend",()=>{s.skinTonePickerExpandedAfterAnimation=!0},{once:!0}):s.skinTonePickerExpandedAfterAnimation=!1});function h(t){if(!s.skinTonePickerExpanded)return;const r=async l=>{ge(t),s.activeSkinTone=l};switch(t.key){case"ArrowUp":return r(He(!0,s.activeSkinTone,s.skinTones));case"ArrowDown":return r(He(!1,s.activeSkinTone,s.skinTones));case"Home":return r(0);case"End":return r(s.skinTones.length-1);case"Enter":return ge(t),u(s.activeSkinTone);case"Escape":return ge(t),s.skinTonePickerExpanded=!1,N("skintone-button")}}function M(t){if(s.skinTonePickerExpanded)switch(t.key){case" ":return ge(t),u(s.activeSkinTone)}}async function p(t){const{relatedTarget:r}=t;(!r||r.id!=="skintone-list")&&(s.skinTonePickerExpanded=!1)}function i(t){s.rawSearchText=t.target.value}return{$set(t){Ie(s,t)},$destroy(){n.abort()}}}const ln="https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json",cn="en";var pn={categoriesLabel:"Categories",emojiUnsupportedMessage:"Your browser does not support color emoji.",favoritesLabel:"Favorites",loadingMessage:"Loading…",networkErrorMessage:"Could not load emoji.",regionLabel:"Emoji picker",searchDescription:"When search results are available, press up or down to select and enter to choose.",searchLabel:"Search",searchResultsLabel:"Search results",skinToneDescription:"When expanded, press up or down to select and enter to choose.",skinToneLabel:"Choose a skin tone (currently {skinTone})",skinTonesLabel:"Skin tones",skinTones:["Default","Light","Medium-Light","Medium","Medium-Dark","Dark"],categories:{custom:"Custom","smileys-emotion":"Smileys and emoticons","people-body":"People and body","animals-nature":"Animals and nature","food-drink":"Food and drink","travel-places":"Travel and places",activities:"Activities",objects:"Objects",symbols:"Symbols",flags:"Flags"}},un=':host{--emoji-size:1.375rem;--emoji-padding:0.5rem;--category-emoji-size:var(--emoji-size);--category-emoji-padding:var(--emoji-padding);--indicator-height:3px;--input-border-radius:0.5rem;--input-border-size:1px;--input-font-size:1rem;--input-line-height:1.5;--input-padding:0.25rem;--num-columns:8;--outline-size:2px;--border-size:1px;--border-radius:0;--skintone-border-radius:1rem;--category-font-size:1rem;display:flex;width:min-content;height:400px}:host,:host(.light){color-scheme:light;--background:#fff;--border-color:#e0e0e0;--indicator-color:#385ac1;--input-border-color:#999;--input-font-color:#111;--input-placeholder-color:#999;--outline-color:#999;--category-font-color:#111;--button-active-background:#e6e6e6;--button-hover-background:#d9d9d9}:host(.dark){color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}@media (prefers-color-scheme:dark){:host{color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}}:host([hidden]){display:none}button{margin:0;padding:0;border:0;background:0 0;box-shadow:none;-webkit-tap-highlight-color:transparent}button::-moz-focus-inner{border:0}input{padding:0;margin:0;line-height:1.15;font-family:inherit}input[type=search]{-webkit-appearance:none}:focus{outline:var(--outline-color) solid var(--outline-size);outline-offset:calc(-1*var(--outline-size))}:host([data-js-focus-visible]) :focus:not([data-focus-visible-added]){outline:0}:focus:not(:focus-visible){outline:0}.hide-focus{outline:0}*{box-sizing:border-box}.picker{contain:content;display:flex;flex-direction:column;background:var(--background);border:var(--border-size) solid var(--border-color);border-radius:var(--border-radius);width:100%;height:100%;overflow:hidden;--total-emoji-size:calc(var(--emoji-size) + (2 * var(--emoji-padding)));--total-category-emoji-size:calc(var(--category-emoji-size) + (2 * var(--category-emoji-padding)))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.hidden{opacity:0;pointer-events:none}.abs-pos{position:absolute;left:0;top:0}.gone{display:none!important}.skintone-button-wrapper,.skintone-list{background:var(--background);z-index:3}.skintone-button-wrapper.expanded{z-index:1}.skintone-list{position:absolute;inset-inline-end:0;top:0;z-index:2;overflow:visible;border-bottom:var(--border-size) solid var(--border-color);border-radius:0 0 var(--skintone-border-radius) var(--skintone-border-radius);will-change:transform;transition:transform .2s ease-in-out;transform-origin:center 0}@media (prefers-reduced-motion:reduce){.skintone-list{transition-duration:.001s}}@supports not (inset-inline-end:0){.skintone-list{right:0}}.skintone-list.no-animate{transition:none}.tabpanel{overflow-y:auto;scrollbar-gutter:stable;-webkit-overflow-scrolling:touch;will-change:transform;min-height:0;flex:1;contain:content}.emoji-menu{display:grid;grid-template-columns:repeat(var(--num-columns),var(--total-emoji-size));justify-content:space-around;align-items:flex-start;width:100%}.emoji-menu.visibility-auto{content-visibility:auto;contain-intrinsic-size:calc(var(--num-columns)*var(--total-emoji-size)) calc(var(--num-rows)*var(--total-emoji-size))}.category{padding:var(--emoji-padding);font-size:var(--category-font-size);color:var(--category-font-color)}.emoji,button.emoji{font-size:var(--emoji-size);display:flex;align-items:center;justify-content:center;border-radius:100%;height:var(--total-emoji-size);width:var(--total-emoji-size);line-height:1;overflow:hidden;font-family:var(--emoji-font-family);cursor:pointer}@media (hover:hover) and (pointer:fine){.emoji:hover,button.emoji:hover{background:var(--button-hover-background)}}.emoji.active,.emoji:active,button.emoji.active,button.emoji:active{background:var(--button-active-background)}.onscreen .custom-emoji::after{content:"";width:var(--emoji-size);height:var(--emoji-size);background-repeat:no-repeat;background-position:center center;background-size:contain;background-image:var(--custom-emoji-background)}.nav,.nav-button{align-items:center}.nav{display:grid;justify-content:space-between;contain:content}.nav-button{display:flex;justify-content:center}.nav-emoji{font-size:var(--category-emoji-size);width:var(--total-category-emoji-size);height:var(--total-category-emoji-size)}.indicator-wrapper{display:flex;border-bottom:1px solid var(--border-color)}.indicator{width:calc(100%/var(--num-groups));height:var(--indicator-height);opacity:var(--indicator-opacity);background-color:var(--indicator-color);will-change:transform,opacity;transition:opacity .1s linear,transform .25s ease-in-out}@media (prefers-reduced-motion:reduce){.indicator{will-change:opacity;transition:opacity .1s linear}}.pad-top,input.search{background:var(--background);width:100%}.pad-top{height:var(--emoji-padding);z-index:3}.search-row{display:flex;align-items:center;position:relative;padding-inline-start:var(--emoji-padding);padding-bottom:var(--emoji-padding)}.search-wrapper{flex:1;min-width:0}input.search{padding:var(--input-padding);border-radius:var(--input-border-radius);border:var(--input-border-size) solid var(--input-border-color);color:var(--input-font-color);font-size:var(--input-font-size);line-height:var(--input-line-height)}input.search::placeholder{color:var(--input-placeholder-color)}.favorites{overflow-y:auto;scrollbar-gutter:stable;display:flex;flex-direction:row;border-top:var(--border-size) solid var(--border-color);contain:content}.message{padding:var(--emoji-padding)}';const Ct=["customEmoji","customCategorySorting","database","dataSource","i18n","locale","skinToneEmoji","emojiVersion"],gn=`:host{--emoji-font-family:${Bt}}`;class Qe extends HTMLElement{constructor(e){super(),this.attachShadow({mode:"open"});const o=document.createElement("style");o.textContent=un+gn,this.shadowRoot.appendChild(o),this._ctx={locale:cn,dataSource:ln,skinToneEmoji:Ia,customCategorySorting:Ba,customEmoji:null,i18n:pn,emojiVersion:null,...e};for(const n of Ct)n!=="database"&&Object.prototype.hasOwnProperty.call(this,n)&&(this._ctx[n]=this[n],delete this[n]);this._dbFlush()}connectedCallback(){ut(this),this._cmp||(this._cmp=dn(this.shadowRoot,this._ctx))}disconnectedCallback(){ut(this),Ce(()=>{if(!this.isConnected&&this._cmp){this._cmp.$destroy(),this._cmp=void 0;const{database:e}=this._ctx;e.close().catch(o=>console.error(o))}})}static get observedAttributes(){return["locale","data-source","skin-tone-emoji","emoji-version"]}attributeChangedCallback(e,o,n){this._set(e.replace(/-([a-z])/g,(c,s)=>s.toUpperCase()),e==="emoji-version"?parseFloat(n):n)}_set(e,o){this._ctx[e]=o,this._cmp&&this._cmp.$set({[e]:o}),["locale","dataSource"].includes(e)&&this._dbFlush()}_dbCreate(){const{locale:e,dataSource:o,database:n}=this._ctx;(!n||n.locale!==e||n.dataSource!==o)&&this._set("database",new ka({locale:e,dataSource:o}))}_dbFlush(){Ce(()=>this._dbCreate())}}const zt={};for(const a of Ct)zt[a]={get(){return a==="database"&&this._dbCreate(),this._ctx[a]},set(e){if(a==="database")throw new Error("database is read-only");this._set(a,e)}};Object.defineProperties(Qe.prototype,zt);function ut(a){a instanceof Qe||Object.setPrototypeOf(a,customElements.get(a.tagName.toLowerCase()).prototype)}customElements.get("emoji-picker")||customElements.define("emoji-picker",Qe);function mn(a,e){const o=document.getElementById("app"),n=new gt,c=new ze,s=a.id||a._id;let d=null,S=[];const N=localStorage.getItem("currentTheme")||"dark";yn(N),o.innerHTML=`
|
|
55
|
+
<div class="dashboard">
|
|
56
|
+
<aside class="sidebar">
|
|
57
|
+
<div class="sidebar-header">
|
|
58
|
+
<h2>CollabDocChat</h2>
|
|
59
|
+
<span class="badge-admin">管理员</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="user-info">
|
|
63
|
+
<div class="avatar">${a.username[0].toUpperCase()}</div>
|
|
64
|
+
<div>
|
|
65
|
+
<div class="username">${a.username}</div>
|
|
66
|
+
<div class="user-role">管理员</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<nav class="nav-menu">
|
|
71
|
+
<button class="nav-item active" data-view="groups">
|
|
72
|
+
<span class="icon">👥</span> 群组管理
|
|
73
|
+
</button>
|
|
74
|
+
<button class="nav-item" data-view="tasks">
|
|
75
|
+
<span class="icon">📋</span> 任务管理
|
|
76
|
+
</button>
|
|
77
|
+
<button class="nav-item" data-view="documents">
|
|
78
|
+
<span class="icon">📄</span> 共享文档
|
|
79
|
+
</button>
|
|
80
|
+
<button class="nav-item" data-view="files">
|
|
81
|
+
<span class="icon">📎</span> 文件管理
|
|
82
|
+
</button>
|
|
83
|
+
<button class="nav-item" data-view="chat">
|
|
84
|
+
<span class="icon">💬</span> 群聊
|
|
85
|
+
</button>
|
|
86
|
+
<button class="nav-item" data-view="search">
|
|
87
|
+
<span class="icon">🔍</span> 搜索
|
|
88
|
+
</button>
|
|
89
|
+
<button class="nav-item" data-view="call">
|
|
90
|
+
<span class="icon">🎲</span> 随机点名
|
|
91
|
+
</button>
|
|
92
|
+
<button class="nav-item" data-view="audit">
|
|
93
|
+
<span class="icon">📊</span> 操作记录
|
|
94
|
+
</button>
|
|
95
|
+
<button class="nav-item" data-view="polls">
|
|
96
|
+
<span class="icon">🗳️</span> 投票管理
|
|
97
|
+
</button>
|
|
98
|
+
<button class="nav-item" data-view="knowledge">
|
|
99
|
+
<span class="icon">📚</span> 知识库
|
|
100
|
+
</button>
|
|
101
|
+
<button class="nav-item" data-view="workflow">
|
|
102
|
+
<span class="icon">⚙️</span> 工作流
|
|
103
|
+
</button>
|
|
104
|
+
<button class="nav-item" data-view="backup">
|
|
105
|
+
<span class="icon">💾</span> 备份管理
|
|
106
|
+
</button>
|
|
107
|
+
<button class="nav-item" data-view="export">
|
|
108
|
+
<span class="icon">📤</span> 数据导出
|
|
109
|
+
</button>
|
|
110
|
+
</nav>
|
|
111
|
+
|
|
112
|
+
<div class="sidebar-footer" style="display: flex; gap: 8px; padding: 12px 16px;">
|
|
113
|
+
<button class="sidebar-footer-btn" id="settingsBtn" style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 10px; color: var(--text-primary); font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.background='linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(168, 85, 247, 0.2) 100%)'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.background='linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%)'; this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
|
114
|
+
<span style="font-size: 16px;">⚙️</span>
|
|
115
|
+
<span>设置</span>
|
|
116
|
+
</button>
|
|
117
|
+
<button class="sidebar-footer-btn" id="helpBtn" style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 10px; color: var(--text-primary); font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.background='linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(168, 85, 247, 0.2) 100%)'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.background='linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%)'; this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
|
118
|
+
<span style="font-size: 16px;">❓</span>
|
|
119
|
+
<span>帮助</span>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
<button class="btn-logout" id="logoutBtn">退出登录</button>
|
|
123
|
+
</aside>
|
|
124
|
+
|
|
125
|
+
<main class="main-content">
|
|
126
|
+
<div id="contentArea"></div>
|
|
127
|
+
</main>
|
|
128
|
+
</div>
|
|
129
|
+
`,document.querySelectorAll(".nav-item").forEach(m=>{m.addEventListener("click",()=>{document.querySelectorAll(".nav-item").forEach(g=>g.classList.remove("active")),m.classList.add("active");const u=m.dataset.view;pe(u)})}),document.getElementById("logoutBtn").addEventListener("click",()=>{c.logout()}),document.getElementById("settingsBtn").addEventListener("click",()=>{pe("settings")}),document.getElementById("helpBtn").addEventListener("click",()=>{pe("help")});async function L(m){S=(await n.getGroups()).groups,m.innerHTML=`
|
|
130
|
+
<div class="view-header">
|
|
131
|
+
<h2>群组管理</h2>
|
|
132
|
+
<button class="btn-primary" id="createGroupBtn">创建群组</button>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="groups-grid" id="groupsList"></div>
|
|
135
|
+
<div id="createGroupModal" class="modal hidden">
|
|
136
|
+
<div class="modal-content">
|
|
137
|
+
<h3>创建新群组</h3>
|
|
138
|
+
<form id="createGroupForm">
|
|
139
|
+
<div class="form-group">
|
|
140
|
+
<label>群组名称</label>
|
|
141
|
+
<input type="text" name="name" placeholder="请输入群组名称" required>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="form-group">
|
|
144
|
+
<label>群组描述</label>
|
|
145
|
+
<textarea name="description" placeholder="请输入群组描述(可选)"></textarea>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="form-group">
|
|
148
|
+
<label>添加成员(可选)</label>
|
|
149
|
+
<div id="usersList" style="max-height: 200px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 10px;">
|
|
150
|
+
<p>加载中...</p>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div style="display: flex; gap: 10px;">
|
|
154
|
+
<button type="submit" class="btn-primary">创建</button>
|
|
155
|
+
<button type="button" class="btn-secondary" id="closeModal">取消</button>
|
|
156
|
+
</div>
|
|
157
|
+
</form>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div id="manageMembersModal" class="modal hidden">
|
|
161
|
+
<div class="modal-content">
|
|
162
|
+
<h3>管理成员</h3>
|
|
163
|
+
<div id="currentMembers"></div>
|
|
164
|
+
<div class="form-group">
|
|
165
|
+
<label>添加新成员</label>
|
|
166
|
+
<div id="availableUsers"></div>
|
|
167
|
+
</div>
|
|
168
|
+
<button type="button" class="btn-secondary" id="closeMembersModal">关闭</button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
`;const g=document.getElementById("groupsList");S.forEach(y=>{const h=document.createElement("div");h.className="group-card",h.innerHTML=`
|
|
172
|
+
<h3>${y.name}</h3>
|
|
173
|
+
<p>${y.description||"暂无描述"}</p>
|
|
174
|
+
<div class="group-stats">
|
|
175
|
+
<span>👥 ${y.members.length} 成员</span>
|
|
176
|
+
<span>📄 ${y.documents.length} 文档</span>
|
|
177
|
+
</div>
|
|
178
|
+
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
|
179
|
+
<button class="btn-select" data-id="${y._id}">选择</button>
|
|
180
|
+
<button class="btn-secondary" data-id="${y._id}" data-action="manage">管理成员</button>
|
|
181
|
+
</div>
|
|
182
|
+
`,g.appendChild(h)}),document.querySelectorAll(".btn-select").forEach(y=>{y.addEventListener("click",()=>{d=S.find(h=>h._id===y.dataset.id),e.joinGroup(d._id),alert(`已加入群组: ${d.name}`)})}),document.querySelectorAll('[data-action="manage"]').forEach(y=>{y.addEventListener("click",async()=>{const h=y.dataset.id;await H(h)})}),document.getElementById("createGroupBtn").addEventListener("click",async()=>{document.getElementById("createGroupModal").classList.remove("hidden"),await j()}),document.getElementById("closeModal").addEventListener("click",()=>{document.getElementById("createGroupModal").classList.add("hidden")}),document.getElementById("closeMembersModal").addEventListener("click",()=>{document.getElementById("manageMembersModal").classList.add("hidden")}),document.getElementById("createGroupForm").addEventListener("submit",async y=>{y.preventDefault();const h=new FormData(y.target),M=Array.from(document.querySelectorAll("#usersList input:checked")).map(p=>p.value);try{const p=await n.createGroup(h.get("name"),h.get("description"),M);alert("群组创建成功!"),await L(m),document.getElementById("createGroupModal").classList.add("hidden")}catch(p){console.error("创建群组错误:",p),alert("创建失败: "+p.message)}})}async function j(){try{const m=await n.getAllUsers(),u=document.getElementById("usersList");u.innerHTML=m.users.map(g=>`
|
|
183
|
+
<label style="display: flex; align-items: center; gap: 10px; padding: 8px; cursor: pointer;">
|
|
184
|
+
<input type="checkbox" value="${g._id}">
|
|
185
|
+
<div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${g.username[0].toUpperCase()}</div>
|
|
186
|
+
<span>${g.username} (${g.role==="admin"?"管理员":"用户"})</span>
|
|
187
|
+
</label>
|
|
188
|
+
`).join("")}catch(m){console.error("加载用户失败:",m),document.getElementById("usersList").innerHTML='<p style="color: var(--danger);">加载失败</p>'}}async function H(m){try{const u=await n.getGroup(m),g=await n.getAllUsers(),y=u.group,h=document.getElementById("currentMembers");h.innerHTML=`
|
|
189
|
+
<h4>当前成员 (${y.members.length})</h4>
|
|
190
|
+
<div style="max-height: 200px; overflow-y: auto;">
|
|
191
|
+
${y.members.map(t=>`
|
|
192
|
+
<div style="display: flex; align-items: center; justify-content: space-between; padding: 8px; border-bottom: 1px solid var(--border);">
|
|
193
|
+
<div style="display: flex; align-items: center; gap: 10px;">
|
|
194
|
+
<div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${t.username[0].toUpperCase()}</div>
|
|
195
|
+
<span>${t.username} ${t._id.toString()===y.admin._id.toString()?"(管理员)":""}</span>
|
|
196
|
+
</div>
|
|
197
|
+
${t._id.toString()!==y.admin._id.toString()?`<button class="btn-secondary btn-sm" onclick="removeMember('${m}', '${t._id}')">移除</button>`:""}
|
|
198
|
+
</div>
|
|
199
|
+
`).join("")}
|
|
200
|
+
</div>
|
|
201
|
+
`;const M=y.members.map(t=>t._id.toString()),p=g.users.filter(t=>!M.includes(t._id)),i=document.getElementById("availableUsers");p.length===0?i.innerHTML="<p>所有用户都已在群组中</p>":i.innerHTML=p.map(t=>`
|
|
202
|
+
<div style="display: flex; align-items: center; justify-content: space-between; padding: 8px; border-bottom: 1px solid var(--border);">
|
|
203
|
+
<div style="display: flex; align-items: center; gap: 10px;">
|
|
204
|
+
<div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${t.username[0].toUpperCase()}</div>
|
|
205
|
+
<span>${t.username}</span>
|
|
206
|
+
</div>
|
|
207
|
+
<button class="btn-primary btn-sm" onclick="addMember('${m}', '${t._id}')">添加</button>
|
|
208
|
+
</div>
|
|
209
|
+
`).join(""),document.getElementById("manageMembersModal").classList.remove("hidden")}catch(u){console.error("加载成员失败:",u),alert("加载失败: "+u.message)}}window.addMember=async(m,u)=>{try{await n.addMember(m,u),alert("成员添加成功!"),await H(m)}catch(g){alert("添加失败: "+g.message)}},window.removeMember=async(m,u)=>{if(confirm("确定要移除该成员吗?"))try{await n.removeMember(m,u),alert("成员移除成功!"),await H(m)}catch(g){alert("移除失败: "+g.message)}};async function _(m){if(!d){m.innerHTML='<div class="empty-state">请先选择一个群组</div>';return}const u=await n.getTasks(d._id);m.innerHTML=`
|
|
210
|
+
<div class="view-header">
|
|
211
|
+
<h2>任务管理 - ${d.name}</h2>
|
|
212
|
+
<button class="btn-primary" id="createTaskBtn">创建任务</button>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="tasks-list" id="tasksList"></div>
|
|
215
|
+
<div id="createTaskModal" class="modal hidden">
|
|
216
|
+
<div class="modal-content">
|
|
217
|
+
<h3>创建新任务</h3>
|
|
218
|
+
<form id="createTaskForm">
|
|
219
|
+
<div class="form-group">
|
|
220
|
+
<label>任务标题</label>
|
|
221
|
+
<input type="text" name="title" placeholder="请输入任务标题" required>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="form-group">
|
|
224
|
+
<label>任务描述</label>
|
|
225
|
+
<textarea name="description" placeholder="请输入任务描述"></textarea>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="form-group">
|
|
228
|
+
<label>截止日期</label>
|
|
229
|
+
<input type="date" name="deadline">
|
|
230
|
+
</div>
|
|
231
|
+
<div style="display: flex; gap: 10px;">
|
|
232
|
+
<button type="submit" class="btn-primary">创建</button>
|
|
233
|
+
<button type="button" class="btn-secondary" id="closeTaskModal">取消</button>
|
|
234
|
+
</div>
|
|
235
|
+
</form>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<!-- 任务详情模态框 -->
|
|
240
|
+
<div id="taskDetailModal" class="modal hidden">
|
|
241
|
+
<div class="modal-content" style="max-width: 900px; max-height: 90vh; display: flex; flex-direction: column;">
|
|
242
|
+
<div class="modal-header" style="flex-shrink: 0;">
|
|
243
|
+
<h3>📋 任务详情</h3>
|
|
244
|
+
<button class="modal-close" id="closeTaskDetailModal">×</button>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="modal-body" id="taskDetailContent" style="padding: 20px; overflow-y: auto; flex: 1;">
|
|
247
|
+
<!-- 任务详情内容 -->
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
`;const g=document.getElementById("tasksList");u.tasks.length===0?g.innerHTML='<div class="empty-state">暂无任务</div>':(u.tasks.forEach(y=>{const h=document.createElement("div");h.className=`task-card status-${y.status}`,h.innerHTML=`
|
|
252
|
+
<div style="display: flex; justify-content: space-between; align-items: start;">
|
|
253
|
+
<div style="flex: 1;">
|
|
254
|
+
<h3>${y.title}</h3>
|
|
255
|
+
<p>${y.description||"无描述"}</p>
|
|
256
|
+
<div class="task-meta">
|
|
257
|
+
<span class="status-badge">${v(y.status)}</span>
|
|
258
|
+
<span>截止: ${y.deadline?new Date(y.deadline).toLocaleDateString():"无"}</span>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
<div style="display: flex; gap: 10px;">
|
|
262
|
+
<button class="btn-primary btn-sm" data-id="${y._id}" data-action="view-task" title="查看详细">📋 查看详细</button>
|
|
263
|
+
<button class="btn-danger btn-sm" data-id="${y._id}" data-action="delete-task" title="删除任务" style="min-width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">🗑️ 删除</button>
|
|
264
|
+
</div>
|
|
265
|
+
`,g.appendChild(h)}),document.querySelectorAll('[data-action="delete-task"]').forEach(y=>{y.addEventListener("click",async h=>{h.stopPropagation();const M=y.dataset.id;if(confirm("确定要删除这个任务吗?删除后无法恢复!"))try{await n.deleteTask(M),alert("任务删除成功!"),await _(m)}catch(p){console.error("删除任务错误:",p),alert("删除失败: "+(p.message||"未知错误"))}})}),document.querySelectorAll('[data-action="view-task"]').forEach(y=>{y.addEventListener("click",async h=>{var i;h.stopPropagation();const M=y.dataset.id,p=u.tasks.find(t=>t._id===M);if(p){const t=document.getElementById("taskDetailContent"),r=l=>({pending:"#6366f1","in-progress":"#f59e0b",completed:"#10b981",cancelled:"#ef4444"})[l]||"#6366f1";t.innerHTML=`
|
|
266
|
+
<div class="task-detail" style="background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%); border-radius: 12px; padding: 16px;">
|
|
267
|
+
|
|
268
|
+
<!-- 任务标题 -->
|
|
269
|
+
<div class="detail-section" style="background: var(--bg-secondary); border-radius: 8px; padding: 12px; margin-bottom: 12px; border-left: 4px solid ${r(p.status)};">
|
|
270
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
|
|
271
|
+
<span style="font-size: 18px;">📌</span>
|
|
272
|
+
<h4 style="margin: 0; color: var(--text-secondary); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">任务标题</h4>
|
|
273
|
+
</div>
|
|
274
|
+
<p style="font-size: 18px; font-weight: 700; margin: 0; color: var(--text-primary); line-height: 1.3;">${p.title}</p>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<!-- 任务描述 -->
|
|
278
|
+
<div class="detail-section" style="background: var(--bg-secondary); border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
279
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
280
|
+
<span style="font-size: 18px;">📝</span>
|
|
281
|
+
<h4 style="margin: 0; color: var(--text-secondary); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">任务描述</h4>
|
|
282
|
+
</div>
|
|
283
|
+
<p style="margin: 0; line-height: 1.6; color: var(--text-primary); font-size: 14px; white-space: pre-wrap;">${p.description||'<span style="color: var(--text-secondary); font-style: italic;">暂无描述</span>'}</p>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<!-- 状态和日期网格 -->
|
|
287
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 12px;">
|
|
288
|
+
|
|
289
|
+
<!-- 任务状态 -->
|
|
290
|
+
<div class="detail-section" style="background: var(--bg-secondary); border-radius: 8px; padding: 12px;">
|
|
291
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
292
|
+
<span style="font-size: 18px;">📊</span>
|
|
293
|
+
<h4 style="margin: 0; color: var(--text-secondary); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">任务状态</h4>
|
|
294
|
+
</div>
|
|
295
|
+
<span style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 6px; background: ${r(p.status)}; color: white; font-weight: 600; font-size: 13px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);">
|
|
296
|
+
${p.status==="completed"?"✓":p.status==="in-progress"?"⟳":p.status==="cancelled"?"✕":"○"}
|
|
297
|
+
${v(p.status)}
|
|
298
|
+
</span>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<!-- 截止日期 -->
|
|
302
|
+
<div class="detail-section" style="background: var(--bg-secondary); border-radius: 8px; padding: 12px;">
|
|
303
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
304
|
+
<span style="font-size: 18px;">📅</span>
|
|
305
|
+
<h4 style="margin: 0; color: var(--text-secondary); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">截止日期</h4>
|
|
306
|
+
</div>
|
|
307
|
+
<p style="margin: 0; font-size: 13px; font-weight: 600; color: var(--text-primary);">
|
|
308
|
+
${p.deadline?new Date(p.deadline).toLocaleString("zh-CN",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):'<span style="color: var(--text-secondary); font-style: italic;">无截止日期</span>'}
|
|
309
|
+
</p>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<!-- 分配成员 -->
|
|
315
|
+
<div class="detail-section" style="background: var(--bg-secondary); border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
316
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
|
317
|
+
<span style="font-size: 18px;">👥</span>
|
|
318
|
+
<h4 style="margin: 0; color: var(--text-secondary); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">分配给</h4>
|
|
319
|
+
${p.assignedTo&&p.assignedTo.length>0?`<span style="background: var(--primary); color: white; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600;">${p.assignedTo.length} 人</span>`:""}
|
|
320
|
+
</div>
|
|
321
|
+
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
|
322
|
+
${p.assignedTo&&p.assignedTo.length>0?p.assignedTo.map(l=>{var w,P;const b=p.completedBy&&p.completedBy.some(G=>G.user&&G.user._id===l._id),E=b?p.completedBy.find(G=>G.user&&G.user._id===l._id):null;return`
|
|
323
|
+
<div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: linear-gradient(135deg, ${b?"rgba(16, 185, 129, 0.1)":"rgba(99, 102, 241, 0.1)"} 0%, ${b?"rgba(5, 150, 105, 0.1)":"rgba(168, 85, 247, 0.1)"} 100%); border-radius: 8px; border: 1px solid ${b?"rgba(16, 185, 129, 0.3)":"rgba(99, 102, 241, 0.2)"}; transition: all 0.3s ease; position: relative;">
|
|
324
|
+
<div class="avatar" style="width: 30px; height: 30px; font-size: 14px; background: linear-gradient(135deg, ${b?"#10b981 0%, #059669":"#6366f1 0%, #a855f7"} 100%); box-shadow: 0 2px 6px ${b?"rgba(16, 185, 129, 0.3)":"rgba(99, 102, 241, 0.3)"};">${((P=(w=l.username)==null?void 0:w[0])==null?void 0:P.toUpperCase())||"?"}</div>
|
|
325
|
+
<div style="display: flex; flex-direction: column; gap: 1px;">
|
|
326
|
+
<span style="font-weight: 600; font-size: 13px; color: var(--text-primary);">${l.username||"未知用户"}</span>
|
|
327
|
+
${b?`<span style="font-size: 10px; color: #10b981; font-weight: 500;">✓ ${E&&E.completedAt?new Date(E.completedAt).toLocaleString("zh-CN",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):"已完成"}</span>`:'<span style="font-size: 10px; color: var(--text-tertiary);">未完成</span>'}
|
|
328
|
+
</div>
|
|
329
|
+
${b?'<span style="position: absolute; top: -4px; right: -4px; background: #10b981; color: white; width: 16px; height: 16px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">✓</span>':""}
|
|
330
|
+
</div>
|
|
331
|
+
`}).join(""):'<p style="margin: 0; color: var(--text-secondary); font-style: italic; font-size: 13px;">未分配给任何人</p>'}
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<!-- 完成情况统计 -->
|
|
336
|
+
${p.assignedTo&&p.assignedTo.length>0?`
|
|
337
|
+
<div class="detail-section" style="background: var(--bg-secondary); border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
338
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
|
339
|
+
<span style="font-size: 18px;">📈</span>
|
|
340
|
+
<h4 style="margin: 0; color: var(--text-secondary); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">完成情况</h4>
|
|
341
|
+
</div>
|
|
342
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 10px;">
|
|
343
|
+
<div style="background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%); padding: 10px; border-radius: 8px; border: 1px solid rgba(16, 185, 129, 0.2); text-align: center;">
|
|
344
|
+
<div style="font-size: 22px; font-weight: 700; color: #10b981;">${p.completedBy?p.completedBy.length:0}</div>
|
|
345
|
+
<div style="font-size: 10px; color: var(--text-secondary); margin-top: 2px;">已完成</div>
|
|
346
|
+
</div>
|
|
347
|
+
<div style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.1) 100%); padding: 10px; border-radius: 8px; border: 1px solid rgba(245, 158, 11, 0.2); text-align: center;">
|
|
348
|
+
<div style="font-size: 22px; font-weight: 700; color: #f59e0b;">${p.assignedTo.length-(p.completedBy?p.completedBy.length:0)}</div>
|
|
349
|
+
<div style="font-size: 10px; color: var(--text-secondary); margin-top: 2px;">未完成</div>
|
|
350
|
+
</div>
|
|
351
|
+
<div style="background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); padding: 10px; border-radius: 8px; border: 1px solid rgba(99, 102, 241, 0.2); text-align: center;">
|
|
352
|
+
<div style="font-size: 22px; font-weight: 700; color: #6366f1;">${Math.round((p.completedBy?p.completedBy.length:0)/p.assignedTo.length*100)}%</div>
|
|
353
|
+
<div style="font-size: 10px; color: var(--text-secondary); margin-top: 2px;">完成率</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<!-- 进度条 -->
|
|
358
|
+
<div style="background: var(--bg-tertiary); height: 8px; border-radius: 4px; overflow: hidden; position: relative;">
|
|
359
|
+
<div style="background: linear-gradient(90deg, #10b981 0%, #059669 100%); height: 100%; width: ${(p.completedBy?p.completedBy.length:0)/p.assignedTo.length*100}%; transition: width 0.3s ease; box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);"></div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
`:""}
|
|
363
|
+
|
|
364
|
+
<!-- 时间信息 -->
|
|
365
|
+
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
|
|
366
|
+
|
|
367
|
+
<!-- 创建时间 -->
|
|
368
|
+
<div class="detail-section" style="background: var(--bg-secondary); border-radius: 8px; padding: 12px;">
|
|
369
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
370
|
+
<span style="font-size: 18px;">🕐</span>
|
|
371
|
+
<h4 style="margin: 0; color: var(--text-secondary); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">创建时间</h4>
|
|
372
|
+
</div>
|
|
373
|
+
<p style="margin: 0; font-size: 13px; color: var(--text-primary);">
|
|
374
|
+
${new Date(p.createdAt).toLocaleString("zh-CN",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}
|
|
375
|
+
</p>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<!-- 更新时间 -->
|
|
379
|
+
<div class="detail-section" style="background: var(--bg-secondary); border-radius: 8px; padding: 12px;">
|
|
380
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
381
|
+
<span style="font-size: 18px;">🔄</span>
|
|
382
|
+
<h4 style="margin: 0; color: var(--text-secondary); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">更新时间</h4>
|
|
383
|
+
</div>
|
|
384
|
+
<p style="margin: 0; font-size: 13px; color: var(--text-primary);">
|
|
385
|
+
${new Date(p.updatedAt).toLocaleString("zh-CN",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}
|
|
386
|
+
</p>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<!-- 投票信息 -->
|
|
392
|
+
${p.poll?`
|
|
393
|
+
<div class="detail-section" style="background: var(--bg-secondary); border-radius: 8px; padding: 12px; margin-top: 12px;">
|
|
394
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
|
395
|
+
<span style="font-size: 18px;">📊</span>
|
|
396
|
+
<h4 style="margin: 0; color: var(--text-secondary); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">投票信息</h4>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<div style="background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%); border-radius: 8px; padding: 12px; border: 1px solid rgba(99, 102, 241, 0.2);">
|
|
400
|
+
<h5 style="margin: 0 0 10px 0; font-size: 14px; font-weight: 600; color: var(--text-primary);">${p.poll.question||"投票"}</h5>
|
|
401
|
+
|
|
402
|
+
<!-- 投票统计 -->
|
|
403
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px;">
|
|
404
|
+
<div style="background: var(--bg-tertiary); padding: 8px; border-radius: 6px; text-align: center;">
|
|
405
|
+
<div style="font-size: 20px; font-weight: 700; color: var(--primary);">${p.poll.votedCount||0}</div>
|
|
406
|
+
<div style="font-size: 10px; color: var(--text-secondary); margin-top: 2px;">已投票</div>
|
|
407
|
+
</div>
|
|
408
|
+
<div style="background: var(--bg-tertiary); padding: 8px; border-radius: 6px; text-align: center;">
|
|
409
|
+
<div style="font-size: 20px; font-weight: 700; color: var(--warning);">${(p.poll.totalMembers||0)-(p.poll.votedCount||0)}</div>
|
|
410
|
+
<div style="font-size: 10px; color: var(--text-secondary); margin-top: 2px;">未投票</div>
|
|
411
|
+
</div>
|
|
412
|
+
<div style="background: var(--bg-tertiary); padding: 8px; border-radius: 6px; text-align: center;">
|
|
413
|
+
<div style="font-size: 20px; font-weight: 700; color: var(--success);">${p.poll.totalVotes||0}</div>
|
|
414
|
+
<div style="font-size: 10px; color: var(--text-secondary); margin-top: 2px;">总票数</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<!-- 投票选项 -->
|
|
419
|
+
<div style="display: flex; flex-direction: column; gap: 8px;">
|
|
420
|
+
${((i=p.poll.options)==null?void 0:i.map(l=>{const b=p.poll.totalVotes>0?Math.round((l.votes||0)/p.poll.totalVotes*100):0,E=l.voters||[];return`
|
|
421
|
+
<div style="background: var(--bg-tertiary); border-radius: 8px; padding: 10px; border: 1px solid var(--border);">
|
|
422
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
|
423
|
+
<span style="font-weight: 600; font-size: 13px; color: var(--text-primary);">${l.text}</span>
|
|
424
|
+
<span style="font-weight: 700; font-size: 13px; color: var(--primary);">${l.votes||0} 票 (${b}%)</span>
|
|
425
|
+
</div>
|
|
426
|
+
<div style="height: 6px; background: var(--bg-secondary); border-radius: 3px; overflow: hidden; margin-bottom: 8px;">
|
|
427
|
+
<div style="height: 100%; background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); width: ${b}%; transition: width 0.3s;"></div>
|
|
428
|
+
</div>
|
|
429
|
+
${E.length>0?`
|
|
430
|
+
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border);">
|
|
431
|
+
<div style="font-size: 10px; color: var(--text-secondary); margin-bottom: 6px;">投票成员 (${E.length}人):</div>
|
|
432
|
+
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
|
|
433
|
+
${E.map(w=>{const P=typeof w=="string"?w:w.username;return`
|
|
434
|
+
<span style="display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 10px; font-size: 11px;">
|
|
435
|
+
<span style="width: 16px; height: 16px; border-radius: 50%; background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 9px; font-weight: 700;">${P?P[0].toUpperCase():"?"}</span>
|
|
436
|
+
<span style="font-weight: 600; color: var(--text-primary);">${P||"未知用户"}</span>
|
|
437
|
+
</span>
|
|
438
|
+
`}).join("")}
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
`:`
|
|
442
|
+
<div style="text-align: center; padding: 8px; color: var(--text-secondary); font-size: 12px; font-style: italic;">暂无人投票</div>
|
|
443
|
+
`}
|
|
444
|
+
</div>
|
|
445
|
+
`}).join(""))||'<p style="color: var(--text-secondary); font-style: italic;">暂无投票选项</p>'}
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<!-- 未投票成员 -->
|
|
449
|
+
${p.poll.unvotedMembers&&p.poll.unvotedMembers.length>0?`
|
|
450
|
+
<div style="margin-top: 12px; padding: 12px; background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, rgba(220, 38, 38, 0.05) 100%); border: 1px solid rgba(239, 68, 68, 0.2); border-radius: 10px;">
|
|
451
|
+
<div style="font-size: 14px; font-weight: 600; color: var(--danger); margin-bottom: 12px; display: flex; align-items: center; gap: 8px;">
|
|
452
|
+
<span>⚠️</span>
|
|
453
|
+
<span>未投票成员 (${p.poll.unvotedMembers.length}人):</span>
|
|
454
|
+
</div>
|
|
455
|
+
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
|
456
|
+
${p.poll.unvotedMembers.map(l=>{const b=typeof l=="string"?l:l.username;return`
|
|
457
|
+
<span style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--bg-tertiary); border: 2px solid rgba(239, 68, 68, 0.3); border-radius: 12px; font-size: 12px;">
|
|
458
|
+
<span style="width: 22px; height: 22px; border-radius: 50%; background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: 700;">${b?b[0].toUpperCase():"?"}</span>
|
|
459
|
+
<span style="font-weight: 600; color: var(--text-primary);">${b||"未知用户"}</span>
|
|
460
|
+
<span style="font-size: 10px; color: var(--danger); background: rgba(239, 68, 68, 0.1); padding: 2px 6px; border-radius: 8px;">未投票</span>
|
|
461
|
+
</span>
|
|
462
|
+
`}).join("")}
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
`:p.poll.totalMembers>0?`
|
|
466
|
+
<div style="margin-top: 16px; padding: 12px; background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(5, 150, 105, 0.05) 100%); border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 10px; text-align: center; color: var(--success); font-weight: 600;">
|
|
467
|
+
✅ 所有成员已完成投票
|
|
468
|
+
</div>
|
|
469
|
+
`:""}
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
`:""}
|
|
473
|
+
|
|
474
|
+
</div>
|
|
475
|
+
`,document.getElementById("taskDetailModal").classList.remove("hidden")}})})),document.getElementById("createTaskBtn").addEventListener("click",()=>{document.getElementById("createTaskModal").classList.remove("hidden")}),document.getElementById("closeTaskModal").addEventListener("click",()=>{document.getElementById("createTaskModal").classList.add("hidden")}),document.getElementById("closeTaskDetailModal").addEventListener("click",()=>{document.getElementById("taskDetailModal").classList.add("hidden")}),document.getElementById("createTaskForm").addEventListener("submit",async y=>{y.preventDefault();const h=new FormData(y.target);try{const p=(await n.getGroup(d._id)).group.members.map(i=>i._id);await n.createTask({title:h.get("title"),description:h.get("description"),groupId:d._id,assignedTo:p,deadline:h.get("deadline")||null}),alert("任务创建成功!已分配给所有群组成员"),await _(m),document.getElementById("createTaskModal").classList.add("hidden")}catch(M){console.error("创建任务错误:",M),alert("创建失败: "+M.message)}})}async function U(m){if(!d){m.innerHTML='<div class="empty-state">请先选择一个群组</div>';return}const u=await n.getDocuments(d._id),y=(await n.getGroup(d._id)).group;m.innerHTML=`
|
|
476
|
+
<div class="view-header">
|
|
477
|
+
<h2>📄 共享文档 - ${d.name}</h2>
|
|
478
|
+
<button class="btn-primary" id="createDocBtn">➕ 创建文档</button>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="documents-list" id="docsList"></div>
|
|
481
|
+
|
|
482
|
+
<!-- 创建文档模态框 -->
|
|
483
|
+
<div id="createDocModal" class="modal hidden">
|
|
484
|
+
<div class="modal-content" style="max-width: 700px;">
|
|
485
|
+
<div class="modal-header">
|
|
486
|
+
<h3>创建共享文档</h3>
|
|
487
|
+
<button class="modal-close" id="closeDocModal">×</button>
|
|
488
|
+
</div>
|
|
489
|
+
<form id="createDocForm" style="padding: 20px;">
|
|
490
|
+
<div class="form-group">
|
|
491
|
+
<label>📌 文档标题</label>
|
|
492
|
+
<input type="text" name="title" placeholder="请输入文档标题" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
|
|
493
|
+
</div>
|
|
494
|
+
<div class="form-group">
|
|
495
|
+
<label>📝 文档内容</label>
|
|
496
|
+
<textarea name="content" placeholder="请输入文档内容" rows="6" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;"></textarea>
|
|
497
|
+
</div>
|
|
498
|
+
<div class="form-group">
|
|
499
|
+
<label>👥 成员编辑权限</label>
|
|
500
|
+
<div style="max-height: 200px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 10px;">
|
|
501
|
+
${y.members.map(p=>`
|
|
502
|
+
<div style="display: flex; align-items: center; gap: 10px; padding: 8px; background: var(--bg-secondary); border-radius: 6px; margin-bottom: 8px;">
|
|
503
|
+
<div class="avatar" style="width: 32px; height: 32px; font-size: 14px;">${p.username[0].toUpperCase()}</div>
|
|
504
|
+
<span style="flex: 1;">${p.username}</span>
|
|
505
|
+
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
|
|
506
|
+
<input type="checkbox" name="editableMembers" value="${p._id}" ${p._id===y.admin.toString()?"checked disabled":"checked"} style="width: 18px; height: 18px; cursor: pointer;">
|
|
507
|
+
<span style="font-size: 13px;">${p._id===y.admin.toString()?"管理员":"可编辑"}</span>
|
|
508
|
+
</label>
|
|
509
|
+
</div>
|
|
510
|
+
`).join("")}
|
|
511
|
+
</div>
|
|
512
|
+
<p style="font-size: 12px; color: var(--text-tertiary); margin-top: 8px;">💡 提示:所有成员都可以查看文档,勾选的成员可以编辑文档</p>
|
|
513
|
+
</div>
|
|
514
|
+
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
|
515
|
+
<button type="submit" class="btn-primary" style="flex: 1;">创建</button>
|
|
516
|
+
<button type="button" class="btn-secondary" id="closeDocModalBtn" style="flex: 1;">取消</button>
|
|
517
|
+
</div>
|
|
518
|
+
</form>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<!-- 编辑权限模态框 -->
|
|
523
|
+
<div id="editPermissionModal" class="modal hidden">
|
|
524
|
+
<div class="modal-content" style="max-width: 600px;">
|
|
525
|
+
<div class="modal-header">
|
|
526
|
+
<h3>管理编辑权限</h3>
|
|
527
|
+
<button class="modal-close" id="closePermissionModal">×</button>
|
|
528
|
+
</div>
|
|
529
|
+
<div id="permissionContent" style="padding: 20px;">
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
`;const h=document.getElementById("docsList");u.documents.length===0?h.innerHTML='<div class="empty-state">暂无共享文档</div>':(u.documents.forEach(p=>{var r;const i=document.createElement("div");i.className="document-card";const t=((r=p.editableMembers)==null?void 0:r.length)||0;i.innerHTML=`
|
|
534
|
+
<div style="display: flex; justify-content: space-between; align-items: start;">
|
|
535
|
+
<div style="flex: 1;">
|
|
536
|
+
<h3>📄 ${p.title}</h3>
|
|
537
|
+
<div class="doc-meta" style="display: flex; gap: 15px; margin-top: 8px; font-size: 13px; color: var(--text-secondary);">
|
|
538
|
+
<span>👤 创建者: ${p.creator.username}</span>
|
|
539
|
+
<span>✏️ ${t} 人可编辑</span>
|
|
540
|
+
<span>📅 ${new Date(p.createdAt).toLocaleDateString()}</span>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
<div style="display: flex; gap: 10px; align-items: center;">
|
|
544
|
+
<button class="btn-secondary btn-sm" data-id="${p._id}" data-action="manage-permission" title="管理权限" style="padding: 8px 16px;">⚙️ 权限</button>
|
|
545
|
+
<button class="btn-primary btn-sm" data-id="${p._id}" data-action="edit-doc" title="编辑文档" style="padding: 8px 16px;">✏️ 编辑</button>
|
|
546
|
+
<button class="btn-danger btn-sm" data-id="${p._id}" data-action="delete-doc" title="删除文档" style="padding: 8px 16px;">🗑️ 删除</button>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
`,h.appendChild(i)}),document.querySelectorAll('[data-action="edit-doc"]').forEach(p=>{p.addEventListener("click",()=>{se(m,p.dataset.id)})}),document.querySelectorAll('[data-action="manage-permission"]').forEach(p=>{p.addEventListener("click",async()=>{const i=p.dataset.id,t=u.documents.find(r=>r._id===i);M(t,y)})}),document.querySelectorAll('[data-action="delete-doc"]').forEach(p=>{p.addEventListener("click",async i=>{i.stopPropagation();const t=p.dataset.id;if(confirm("确定要删除这个文档吗?删除后无法恢复!"))try{await n.deleteDocument(t),alert("文档删除成功!"),await U(m)}catch(r){console.error("删除文档错误:",r),alert("删除失败: "+(r.message||"未知错误"))}})})),document.getElementById("createDocBtn").addEventListener("click",()=>{document.getElementById("createDocModal").classList.remove("hidden")}),document.getElementById("closeDocModal").addEventListener("click",()=>{document.getElementById("createDocModal").classList.add("hidden")}),document.getElementById("closeDocModalBtn").addEventListener("click",()=>{document.getElementById("createDocModal").classList.add("hidden")});function M(p,i){const t=document.getElementById("editPermissionModal"),r=document.getElementById("permissionContent");r.innerHTML=`
|
|
550
|
+
<div style="margin-bottom: 16px;">
|
|
551
|
+
<h4 style="margin: 0 0 8px 0;">📄 ${p.title}</h4>
|
|
552
|
+
<p style="font-size: 13px; color: var(--text-secondary); margin: 0;">管理哪些成员可以编辑此文档</p>
|
|
553
|
+
</div>
|
|
554
|
+
<form id="updatePermissionForm">
|
|
555
|
+
<div style="max-height: 300px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 10px;">
|
|
556
|
+
${i.members.map(l=>{var w;const b=((w=p.editableMembers)==null?void 0:w.includes(l._id))||l._id===i.admin.toString(),E=l._id===i.admin.toString();return`
|
|
557
|
+
<div style="display: flex; align-items: center; gap: 10px; padding: 8px; background: var(--bg-secondary); border-radius: 6px; margin-bottom: 8px;">
|
|
558
|
+
<div class="avatar" style="width: 32px; height: 32px; font-size: 14px;">${l.username[0].toUpperCase()}</div>
|
|
559
|
+
<span style="flex: 1;">${l.username}</span>
|
|
560
|
+
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
|
|
561
|
+
<input type="checkbox" name="editableMembers" value="${l._id}" ${b?"checked":""} ${E?"disabled":""} style="width: 18px; height: 18px; cursor: pointer;">
|
|
562
|
+
<span style="font-size: 13px;">${E?"管理员":"可编辑"}</span>
|
|
563
|
+
</label>
|
|
564
|
+
</div>
|
|
565
|
+
`}).join("")}
|
|
566
|
+
</div>
|
|
567
|
+
<p style="font-size: 12px; color: var(--text-tertiary); margin-top: 12px;">💡 提示:所有成员都可以查看文档,只有勾选的成员可以编辑</p>
|
|
568
|
+
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
|
569
|
+
<button type="submit" class="btn-primary" style="flex: 1;">保存</button>
|
|
570
|
+
<button type="button" class="btn-secondary" id="cancelPermissionBtn" style="flex: 1;">取消</button>
|
|
571
|
+
</div>
|
|
572
|
+
</form>
|
|
573
|
+
`,t.classList.remove("hidden"),document.getElementById("cancelPermissionBtn").addEventListener("click",()=>{t.classList.add("hidden")}),document.getElementById("updatePermissionForm").addEventListener("submit",async l=>{l.preventDefault();const E=new FormData(l.target).getAll("editableMembers");try{await n.updateDocumentPermissions(p._id,E),alert("权限更新成功!"),t.classList.add("hidden"),await U(m)}catch(w){console.error("更新权限错误:",w),alert("更新失败: "+w.message)}})}document.getElementById("closePermissionModal").addEventListener("click",()=>{document.getElementById("editPermissionModal").classList.add("hidden")}),document.getElementById("createDocForm").addEventListener("submit",async p=>{p.preventDefault();const i=new FormData(p.target),t=i.getAll("editableMembers");try{await n.createDocument(i.get("title"),i.get("content"),d._id,t),alert("文档创建成功!"),await U(m),document.getElementById("createDocModal").classList.add("hidden")}catch(r){console.error("创建文档错误:",r),alert("创建失败: "+r.message)}})}async function se(m,u){const y=(await n.getDocument(u)).document;m.innerHTML=`
|
|
574
|
+
<div class="view-header">
|
|
575
|
+
<button class="btn-back" id="backBtn">← 返回</button>
|
|
576
|
+
<h2>${y.title}</h2>
|
|
577
|
+
<span class="doc-status">${y.permission==="readonly"?"🔒 只读模式":"✏️ 编辑模式"}</span>
|
|
578
|
+
</div>
|
|
579
|
+
<div class="editor-container">
|
|
580
|
+
<div class="editor-toolbar">
|
|
581
|
+
<div class="online-users" id="onlineUsers">
|
|
582
|
+
<span class="user-badge">👤 ${a.username}</span>
|
|
583
|
+
</div>
|
|
584
|
+
<button class="btn-primary" id="saveBtn">保存</button>
|
|
585
|
+
</div>
|
|
586
|
+
<div id="editor"></div>
|
|
587
|
+
<div class="editor-footer">
|
|
588
|
+
<span>最后编辑: ${new Date(y.updatedAt).toLocaleString()}</span>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
`;const h=new Quill("#editor",{theme:"snow",modules:{toolbar:[[{header:[1,2,3,!1]}],["bold","italic","underline","strike"],[{list:"ordered"},{list:"bullet"}],[{color:[]},{background:[]}],["link","image","code-block"],["clean"]]},readOnly:!1});h.root.innerHTML=y.content||"";let M,p;h.on("text-change",()=>{clearTimeout(M),clearTimeout(p),e.sendTyping(u,a.username,!0),M=setTimeout(()=>{e.sendTyping(u,a.username,!1)},1e3),p=setTimeout(async()=>{const i=h.root.innerHTML;try{await n.updateDocument(u,i)}catch(t){console.error("自动保存失败:",t)}},2e3)}),document.getElementById("saveBtn").addEventListener("click",async()=>{try{const i=h.root.innerHTML;await n.updateDocument(u,i),alert("保存成功!")}catch(i){alert("保存失败: "+i.message)}}),e.on("document_update",i=>{i.documentId===u&&i.userId!==a.id&&(h.root.innerHTML=i.content)}),e.on("typing",i=>{if(i.documentId===u&&i.userId!==a.id){const t=document.getElementById("onlineUsers");if(i.isTyping)t.innerHTML+=`<span class="user-badge typing" data-user="${i.userId}">✏️ ${i.username}</span>`;else{const r=t.querySelector(`[data-user="${i.userId}"]`);r&&r.remove()}}}),document.getElementById("backBtn").addEventListener("click",()=>{U(m)})}async function ae(m){if(!d){m.innerHTML='<div class="empty-state">请先选择一个群组</div>';return}try{const u=await n.getGroupFiles(d._id);m.innerHTML=`
|
|
592
|
+
<div class="view-header">
|
|
593
|
+
<h2>文件管理 - ${d.name}</h2>
|
|
594
|
+
<button class="btn-primary" id="uploadFileBtn">📤 上传文件</button>
|
|
595
|
+
</div>
|
|
596
|
+
<div class="files-list" id="filesList"></div>
|
|
597
|
+
|
|
598
|
+
<!-- 文件上传模态框 -->
|
|
599
|
+
<div class="modal hidden" id="uploadFileModal">
|
|
600
|
+
<div class="modal-content">
|
|
601
|
+
<div class="modal-header">
|
|
602
|
+
<h3>上传文件</h3>
|
|
603
|
+
<button class="modal-close" id="closeUploadModal">×</button>
|
|
604
|
+
</div>
|
|
605
|
+
<form id="uploadFileForm">
|
|
606
|
+
<div class="form-group">
|
|
607
|
+
<label>选择文件</label>
|
|
608
|
+
<input type="file" id="fileInput" required>
|
|
609
|
+
<small>支持图片、PDF、Word、Excel等,最大10MB</small>
|
|
610
|
+
</div>
|
|
611
|
+
<div class="form-group">
|
|
612
|
+
<label>描述(可选)</label>
|
|
613
|
+
<textarea id="fileDescription" rows="3" placeholder="文件描述..."></textarea>
|
|
614
|
+
</div>
|
|
615
|
+
<div class="form-actions">
|
|
616
|
+
<button type="button" class="btn-secondary" id="cancelUpload">取消</button>
|
|
617
|
+
<button type="submit" class="btn-primary">上传</button>
|
|
618
|
+
</div>
|
|
619
|
+
</form>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
`;const g=document.getElementById("filesList");!u.files||u.files.length===0?g.innerHTML='<div class="empty-state">暂无文件</div>':(u.files.forEach(y=>{const h=document.createElement("div");h.className="file-card";const M=ie(y.mimetype),p=ce(y.size);h.innerHTML=`
|
|
623
|
+
<div class="file-icon">${M}</div>
|
|
624
|
+
<div class="file-info">
|
|
625
|
+
<h4>${y.originalName}</h4>
|
|
626
|
+
<div class="file-meta">
|
|
627
|
+
<span>上传者: ${y.uploader.username}</span>
|
|
628
|
+
<span>大小: ${p}</span>
|
|
629
|
+
<span>时间: ${new Date(y.createdAt).toLocaleString()}</span>
|
|
630
|
+
</div>
|
|
631
|
+
${y.description?`<p class="file-description">${y.description}</p>`:""}
|
|
632
|
+
</div>
|
|
633
|
+
<div class="file-actions">
|
|
634
|
+
<a href="${n.getFileDownloadUrl(y._id)}" class="btn-primary" download>下载</a>
|
|
635
|
+
<button class="btn-danger" data-id="${y._id}" data-action="delete-file">删除</button>
|
|
636
|
+
</div>
|
|
637
|
+
`,g.appendChild(h)}),document.querySelectorAll('[data-action="delete-file"]').forEach(y=>{y.addEventListener("click",async()=>{if(confirm("确定要删除这个文件吗?"))try{await n.deleteFile(y.dataset.id),alert("文件删除成功!"),await ae(m)}catch(h){alert("删除失败: "+h.message)}})})),document.getElementById("uploadFileBtn").addEventListener("click",()=>{document.getElementById("uploadFileModal").classList.remove("hidden")}),document.getElementById("closeUploadModal").addEventListener("click",()=>{document.getElementById("uploadFileModal").classList.add("hidden"),document.getElementById("uploadFileForm").reset()}),document.getElementById("cancelUpload").addEventListener("click",()=>{document.getElementById("uploadFileModal").classList.add("hidden"),document.getElementById("uploadFileForm").reset()}),document.getElementById("uploadFileForm").addEventListener("submit",async y=>{y.preventDefault();const h=document.getElementById("fileInput"),M=document.getElementById("fileDescription").value;if(!h.files[0]){alert("请选择文件");return}try{await n.uploadFile(d._id,h.files[0],M),alert("文件上传成功!"),document.getElementById("uploadFileModal").classList.add("hidden"),document.getElementById("uploadFileForm").reset(),await ae(m)}catch(p){alert("上传失败: "+p.message)}})}catch(u){console.error("获取文件列表失败:",u),m.innerHTML=`
|
|
638
|
+
<div class="view-header">
|
|
639
|
+
<h2>文件管理</h2>
|
|
640
|
+
</div>
|
|
641
|
+
<div class="empty-state">加载文件失败: ${u.message}</div>
|
|
642
|
+
`}}function ie(m){return m.startsWith("image/")?"🖼️":m==="application/pdf"?"📕":m.includes("word")||m.includes("document")?"📘":m.includes("excel")||m.includes("spreadsheet")?"📗":m.includes("zip")||m.includes("compressed")?"📦":"📄"}function ce(m){if(m===0)return"0 Bytes";const u=1024,g=["Bytes","KB","MB","GB"],y=Math.floor(Math.log(m)/Math.log(u));return Math.round(m/Math.pow(u,y)*100)/100+" "+g[y]}async function q(m){if(!d){m.innerHTML='<div class="empty-state">请先选择一个群组</div>';return}let g=(await n.getGroup(d._id)).group;function y(f){if(f.startsWith("[白板作品]")){const I=f.replace("[白板作品]","").trim(),x=I.includes("/api/files/")&&I.includes("/download");let R=I;if(x&&!I.includes("token=")){const O=localStorage.getItem("token");R=I.includes("?")?`${I}&token=${O}`:`${I}?token=${O}`}return`
|
|
643
|
+
<div style="background: linear-gradient(135deg, rgb(99, 102, 241) 0%, rgb(139, 92, 246) 100%); padding: 16px; border-radius: 12px; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);">
|
|
644
|
+
<div style="margin-bottom: 12px; font-weight: 600; color: white; display: flex; align-items: center; gap: 6px; font-size: 15px;">
|
|
645
|
+
<span style="font-size: 18px;">🎨</span>
|
|
646
|
+
<span>白板作品</span>
|
|
647
|
+
</div>
|
|
648
|
+
<div style="position: relative; display: inline-block;">
|
|
649
|
+
<img src="${R}" alt="白板作品"
|
|
650
|
+
style="max-width: 400px; max-height: 400px; border-radius: 8px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2); background: rgba(255,255,255,0.1);"
|
|
651
|
+
onclick="window.open('${R}', '_blank')"
|
|
652
|
+
onerror="this.style.display='none'; this.nextElementSibling.style.display='block'; console.error('白板图片加载失败:', '${R}');"
|
|
653
|
+
onload="console.log('白板图片加载成功:', '${R}');">
|
|
654
|
+
<div style="display: none; padding: 20px; background: rgba(255,255,255,0.1); border: 2px dashed rgba(255,255,255,0.3); border-radius: 8px; text-align: center; color: white;">
|
|
655
|
+
<div style="font-size: 48px; margin-bottom: 10px;">⚠️</div>
|
|
656
|
+
<div style="font-weight: 600; margin-bottom: 5px;">图片加载失败</div>
|
|
657
|
+
<div style="font-size: 12px;">图片可能已被删除或URL无效</div>
|
|
658
|
+
<button onclick="navigator.clipboard.writeText('${I}'); alert('图片URL已复制到剪贴板');" style="margin-top: 10px; padding: 6px 12px; background: rgba(255,255,255,0.2); color: white; border: none; border-radius: 6px; cursor: pointer;">复制图片URL</button>
|
|
659
|
+
</div>
|
|
660
|
+
<div style="margin-top: 10px; font-size: 12px; color: rgba(255,255,255,0.8);">点击图片查看大图</div>
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
`}if(f.startsWith("[投票]")){const I=f.replace("[投票]","").trim();return`
|
|
664
|
+
<div class="poll-card" data-poll-id="${I}" style="background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 2px solid rgba(99, 102, 241, 0.3); border-radius: 12px; padding: 16px; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'" onclick="viewPollDetail('${I}')">
|
|
665
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
|
|
666
|
+
<span style="font-size: 32px;">📊</span>
|
|
667
|
+
<div style="flex: 1;">
|
|
668
|
+
<div style="font-weight: 600; font-size: 16px; color: var(--text-primary); margin-bottom: 4px;">投票</div>
|
|
669
|
+
<div style="font-size: 13px; color: var(--text-secondary);">点击查看详情并参与投票</div>
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
<div style="padding: 8px 12px; background: rgba(99, 102, 241, 0.1); border-radius: 6px; font-size: 12px; color: var(--primary); text-align: center;">
|
|
673
|
+
📋 查看投票详情
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
`}return f}window.viewPollDetail=async f=>{try{const I=localStorage.getItem("token"),x=await fetch(`http://localhost:3000/api/polls/${f}`,{headers:{Authorization:`Bearer ${I}`}});if(!x.ok)throw new Error("获取投票详情失败");const O=(await x.json()).poll,K=O.options.reduce((Q,ue)=>Q+ue.votes.length,0),Z=O.status==="ended"||O.endTime&&new Date(O.endTime)<new Date,J=`
|
|
677
|
+
<div id="pollDetailModal" class="modal" style="display: flex;">
|
|
678
|
+
<div class="modal-content" style="max-width: 800px; max-height: 90vh; overflow-y: auto;">
|
|
679
|
+
<div class="modal-header">
|
|
680
|
+
<h3>📊 投票详情</h3>
|
|
681
|
+
<button class="modal-close" id="closePollDetailModal">×</button>
|
|
682
|
+
</div>
|
|
683
|
+
<div class="modal-body" style="padding: 24px;">
|
|
684
|
+
<h2 style="margin: 0 0 12px 0;">${O.title}</h2>
|
|
685
|
+
${O.description?`<p style="color: var(--text-secondary); margin: 0 0 20px 0;">${O.description}</p>`:""}
|
|
686
|
+
|
|
687
|
+
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;">
|
|
688
|
+
<span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px;">
|
|
689
|
+
${O.allowMultiple?"✓ 多选投票":"○ 单选投票"}
|
|
690
|
+
</span>
|
|
691
|
+
<span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px;">
|
|
692
|
+
${O.anonymous?"🔒 匿名投票":"👤 实名投票"}
|
|
693
|
+
</span>
|
|
694
|
+
<span style="font-size: 13px; padding: 6px 12px; background: ${Z?"var(--danger)":"var(--success)"}; border-radius: 14px; color: white;">
|
|
695
|
+
${Z?"已结束":"进行中"}
|
|
696
|
+
</span>
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
<div style="padding: 16px; background: var(--bg-secondary); border-radius: 12px; margin-bottom: 20px;">
|
|
700
|
+
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
|
|
701
|
+
<div>
|
|
702
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">创建者</div>
|
|
703
|
+
<div style="font-weight: 600;">👤 ${O.creatorName}</div>
|
|
704
|
+
</div>
|
|
705
|
+
<div>
|
|
706
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">总投票数</div>
|
|
707
|
+
<div style="font-weight: 600; color: var(--primary);">👥 ${K} 人</div>
|
|
708
|
+
</div>
|
|
709
|
+
<div>
|
|
710
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">创建时间</div>
|
|
711
|
+
<div style="font-weight: 600;">⏰ ${new Date(O.createdAt).toLocaleString("zh-CN")}</div>
|
|
712
|
+
</div>
|
|
713
|
+
${O.endTime?`
|
|
714
|
+
<div>
|
|
715
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">截止时间</div>
|
|
716
|
+
<div style="font-weight: 600;">⏰ ${new Date(O.endTime).toLocaleString("zh-CN")}</div>
|
|
717
|
+
</div>
|
|
718
|
+
`:""}
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<h3 style="margin-bottom: 16px;">投票选项</h3>
|
|
723
|
+
${O.options.map((Q,ue)=>{const Y=K>0?(Q.votes.length/K*100).toFixed(1):0;return`
|
|
724
|
+
<div style="margin-bottom: 16px; padding: 16px; background: var(--bg-tertiary); border-radius: 8px;">
|
|
725
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
726
|
+
<span style="font-weight: 500; font-size: 16px;">${Q.text}</span>
|
|
727
|
+
<span style="font-weight: 600; color: var(--primary); font-size: 16px;">${Q.votes.length} 票 (${Y}%)</span>
|
|
728
|
+
</div>
|
|
729
|
+
<div style="height: 10px; background: var(--bg-secondary); border-radius: 5px; overflow: hidden; margin-bottom: 12px;">
|
|
730
|
+
<div style="height: 100%; background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); width: ${Y}%; transition: width 0.3s;"></div>
|
|
731
|
+
</div>
|
|
732
|
+
${!O.anonymous&&Q.votes.length>0?`
|
|
733
|
+
<div style="font-size: 13px; color: var(--text-secondary);">
|
|
734
|
+
<strong>投票者:</strong> ${Q.votes.map(ee=>ee.username).join(", ")}
|
|
735
|
+
</div>
|
|
736
|
+
`:""}
|
|
737
|
+
</div>
|
|
738
|
+
`}).join("")}
|
|
739
|
+
|
|
740
|
+
<button class="btn-secondary" id="closePollDetailBtn" style="width: 100%; margin-top: 20px;">关闭</button>
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
</div>
|
|
744
|
+
`,te=document.getElementById("pollDetailModal");te&&te.remove(),document.body.insertAdjacentHTML("beforeend",J),document.getElementById("closePollDetailModal").addEventListener("click",()=>{document.getElementById("pollDetailModal").remove()}),document.getElementById("closePollDetailBtn").addEventListener("click",()=>{document.getElementById("pollDetailModal").remove()})}catch(I){console.error("加载投票详情失败:",I),alert("加载投票详情失败: "+I.message)}},m.innerHTML=`
|
|
745
|
+
<div class="view-header">
|
|
746
|
+
<h2>群聊 - ${d.name}</h2>
|
|
747
|
+
<div style="display: flex; gap: 10px;">
|
|
748
|
+
<button class="btn-primary" id="createPollBtn" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none;">📊 创建投票</button>
|
|
749
|
+
<button class="btn-secondary" id="muteAllBtn">全体禁言</button>
|
|
750
|
+
<button class="btn-secondary" id="manageMuteBtn">个人禁言</button>
|
|
751
|
+
<button class="btn-danger" id="clearChatBtn" style="background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); color: white; border: none;">🗑️ 清除记录</button>
|
|
752
|
+
<button class="btn-secondary" id="openAIBtn">🤖 AI助手</button>
|
|
753
|
+
<button class="btn-secondary" id="openWhiteboardBtn">🎨 协作白板</button>
|
|
754
|
+
</div>
|
|
755
|
+
</div>
|
|
756
|
+
<div class="chat-container">
|
|
757
|
+
<div class="messages" id="messages"></div>
|
|
758
|
+
<div class="chat-input">
|
|
759
|
+
<button class="btn-emoji" id="emojiBtn">😊</button>
|
|
760
|
+
<input type="text" id="messageInput" placeholder="输入消息...">
|
|
761
|
+
<button class="btn-primary" id="sendBtn">发送</button>
|
|
762
|
+
</div>
|
|
763
|
+
<emoji-picker id="emojiPicker" class="hidden"></emoji-picker>
|
|
764
|
+
</div>
|
|
765
|
+
<div id="manageMuteModal" class="modal hidden">
|
|
766
|
+
<div class="modal-content">
|
|
767
|
+
<h3>个人禁言</h3>
|
|
768
|
+
<div id="membersList" style="max-height: 400px; overflow-y: auto;"></div>
|
|
769
|
+
<button type="button" class="btn-secondary" id="closeMuteModal">关闭</button>
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<!-- 创建投票模态框 -->
|
|
774
|
+
<div id="createPollModal" class="modal hidden">
|
|
775
|
+
<div class="modal-content" style="max-width: 600px;">
|
|
776
|
+
<div class="modal-header">
|
|
777
|
+
<h3>📊 创建投票</h3>
|
|
778
|
+
<button class="modal-close" id="closePollModal">×</button>
|
|
779
|
+
</div>
|
|
780
|
+
<form id="createPollForm" style="padding: 20px;">
|
|
781
|
+
<div class="form-group">
|
|
782
|
+
<label>投票标题 *</label>
|
|
783
|
+
<input type="text" id="pollTitle" required placeholder="请输入投票标题" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary); color: white;">
|
|
784
|
+
</div>
|
|
785
|
+
<div class="form-group">
|
|
786
|
+
<label>投票描述(可选)</label>
|
|
787
|
+
<textarea id="pollDescription" rows="3" placeholder="请输入投票描述" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary); color: white;"></textarea>
|
|
788
|
+
</div>
|
|
789
|
+
<div class="form-group">
|
|
790
|
+
<label>投票选项 *</label>
|
|
791
|
+
<div id="pollOptions">
|
|
792
|
+
<div class="poll-option-input" style="display: flex; gap: 10px; margin-bottom: 10px;">
|
|
793
|
+
<input type="text" class="poll-option" placeholder="选项 1" required style="flex: 1; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary); color: white;">
|
|
794
|
+
</div>
|
|
795
|
+
<div class="poll-option-input" style="display: flex; gap: 10px; margin-bottom: 10px;">
|
|
796
|
+
<input type="text" class="poll-option" placeholder="选项 2" required style="flex: 1; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary); color: white;">
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
<button type="button" id="addPollOption" class="btn-secondary" style="margin-top: 10px;">+ 添加选项</button>
|
|
800
|
+
</div>
|
|
801
|
+
<div class="form-group">
|
|
802
|
+
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
|
|
803
|
+
<input type="checkbox" id="allowMultiple" style="width: 18px; height: 18px; cursor: pointer;">
|
|
804
|
+
<span>允许多选</span>
|
|
805
|
+
</label>
|
|
806
|
+
</div>
|
|
807
|
+
<div class="form-group">
|
|
808
|
+
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
|
|
809
|
+
<input type="checkbox" id="anonymous" style="width: 18px; height: 18px; cursor: pointer;">
|
|
810
|
+
<span>匿名投票</span>
|
|
811
|
+
</label>
|
|
812
|
+
</div>
|
|
813
|
+
<div class="form-group">
|
|
814
|
+
<label>截止时间(可选)</label>
|
|
815
|
+
<input type="datetime-local" id="pollEndTime" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary); color: white;">
|
|
816
|
+
</div>
|
|
817
|
+
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
|
818
|
+
<button type="submit" class="btn-primary" style="flex: 1;">创建投票</button>
|
|
819
|
+
<button type="button" class="btn-secondary" id="cancelPollModal" style="flex: 1;">取消</button>
|
|
820
|
+
</div>
|
|
821
|
+
</form>
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
|
|
825
|
+
<!-- AI助手模态框 -->
|
|
826
|
+
<div id="aiModal" class="modal hidden">
|
|
827
|
+
<div class="modal-content" style="max-width: 950px; height: 88vh; display: flex; flex-direction: column; background: var(--bg-card); border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.3);">
|
|
828
|
+
|
|
829
|
+
<!-- 头部 -->
|
|
830
|
+
<div class="modal-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 24px 28px; position: relative; overflow: hidden;">
|
|
831
|
+
<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; opacity: 0.1; background-image: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(255,255,255,0.1) 10px, rgba(255,255,255,0.1) 20px);"></div>
|
|
832
|
+
<div style="display: flex; align-items: center; justify-content: space-between; position: relative; z-index: 1;">
|
|
833
|
+
<div style="display: flex; align-items: center; gap: 18px;">
|
|
834
|
+
<div style="width: 56px; height: 56px; background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 32px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">🤖</div>
|
|
835
|
+
<div>
|
|
836
|
+
<h3 style="margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.5px;">AI 智能助手</h3>
|
|
837
|
+
<p style="margin: 6px 0 0 0; font-size: 14px; opacity: 0.9; font-weight: 400;">为您提供智能问答和协助服务 ✨</p>
|
|
838
|
+
</div>
|
|
839
|
+
</div>
|
|
840
|
+
<button class="modal-close" id="closeAIModal" style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: none; color: white; width: 40px; height: 40px; border-radius: 10px; cursor: pointer; font-size: 22px; transition: all 0.3s; display: flex; align-items: center; justify-content: center;" onmouseover="this.style.background='rgba(255,255,255,0.25)'" onmouseout="this.style.background='rgba(255,255,255,0.15)'">×</button>
|
|
841
|
+
</div>
|
|
842
|
+
</div>
|
|
843
|
+
|
|
844
|
+
<div style="flex: 1; display: flex; flex-direction: column; overflow: hidden;">
|
|
845
|
+
|
|
846
|
+
<!-- 快捷问题 -->
|
|
847
|
+
<div style="padding: 18px 24px; background: linear-gradient(to bottom, var(--bg-secondary), var(--bg-card)); border-bottom: 1px solid var(--border);">
|
|
848
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
|
|
849
|
+
<span style="font-size: 16px;">💡</span>
|
|
850
|
+
<span style="font-size: 13px; color: var(--text-secondary); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">快捷问题</span>
|
|
851
|
+
</div>
|
|
852
|
+
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
|
853
|
+
<button class="quick-question-btn" data-question="如何创建一个新文档?" style="padding: 8px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.3s; color: var(--text-primary); font-weight: 500;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">📄 如何创建文档?</button>
|
|
854
|
+
<button class="quick-question-btn" data-question="如何邀请成员加入群组?" style="padding: 8px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.3s; color: var(--text-primary); font-weight: 500;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">👥 如何邀请成员?</button>
|
|
855
|
+
<button class="quick-question-btn" data-question="如何使用工作流功能?" style="padding: 8px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.3s; color: var(--text-primary); font-weight: 500;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">⚙️ 工作流使用?</button>
|
|
856
|
+
<button class="quick-question-btn" data-question="如何备份数据?" style="padding: 8px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.3s; color: var(--text-primary); font-weight: 500;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">💾 如何备份?</button>
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
|
|
860
|
+
<!-- 聊天区域 -->
|
|
861
|
+
<div class="ai-chat" id="aiChatMessages" style="flex: 1; overflow-y: auto; padding: 24px; background: var(--bg-dark); background-image: radial-gradient(circle at 20% 50%, rgba(99, 102, 241, 0.03) 0%, transparent 50%), radial-gradient(circle at 80% 80%, rgba(168, 85, 247, 0.03) 0%, transparent 50%);">
|
|
862
|
+
<div class="ai-message ai" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 18px 22px; border-radius: 20px 20px 20px 6px; margin: 12px 0; max-width: 85%; box-shadow: 0 4px 16px rgba(102, 126, 234, 0.25);">
|
|
863
|
+
<div style="display: flex; align-items: start; gap: 14px;">
|
|
864
|
+
<div style="font-size: 28px; line-height: 1;">🤖</div>
|
|
865
|
+
<div style="flex: 1;">
|
|
866
|
+
<p style="margin: 0 0 10px 0; font-weight: 700; font-size: 16px;">你好!我是AI智能助手</p>
|
|
867
|
+
<p style="margin: 0 0 12px 0; opacity: 0.95; line-height: 1.7; font-size: 14px;">我可以帮助你:</p>
|
|
868
|
+
<ul style="margin: 0; padding-left: 22px; opacity: 0.95; line-height: 2; font-size: 14px;">
|
|
869
|
+
<li style="margin-bottom: 4px;">解答关于平台功能的问题</li>
|
|
870
|
+
<li style="margin-bottom: 4px;">提供操作指导和建议</li>
|
|
871
|
+
<li>帮助你更好地使用各项功能</li>
|
|
872
|
+
</ul>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
|
|
878
|
+
<!-- 输入区域 -->
|
|
879
|
+
<div class="ai-input-container" style="padding: 20px 24px 24px; border-top: 1px solid var(--border); background: linear-gradient(to top, var(--bg-secondary), var(--bg));">
|
|
880
|
+
<div style="display: flex; gap: 14px; align-items: end;">
|
|
881
|
+
<div style="flex: 1; position: relative;">
|
|
882
|
+
<textarea id="aiInputText" placeholder="输入你的问题..." rows="1" style="width: 100%; padding: 14px 18px; border: 2px solid var(--border); border-radius: 14px; resize: none; font-size: 15px; transition: all 0.3s; max-height: 140px; background: var(--bg); color: white; box-shadow: 0 2px 8px rgba(0,0,0,0.05);" onfocus="this.style.borderColor='#667eea'; this.style.boxShadow='0 4px 16px rgba(102, 126, 234, 0.15)'" onblur="this.style.borderColor='var(--border)'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.05)'"></textarea>
|
|
883
|
+
</div>
|
|
884
|
+
<button class="btn-primary" id="aiSendBtnModal" style="padding: 14px 28px; border-radius: 14px; font-size: 15px; font-weight: 600; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; cursor: pointer; transition: all 0.3s; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); min-width: 100px;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 20px rgba(102, 126, 234, 0.4)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 12px rgba(102, 126, 234, 0.3)'">
|
|
885
|
+
<span style="display: flex; align-items: center; gap: 8px; justify-content: center;">
|
|
886
|
+
<span>发送</span>
|
|
887
|
+
<span style="font-size: 16px;">🚀</span>
|
|
888
|
+
</span>
|
|
889
|
+
</button>
|
|
890
|
+
</div>
|
|
891
|
+
<div style="margin-top: 12px; font-size: 12px; color: var(--text-tertiary); text-align: center; display: flex; align-items: center; justify-content: center; gap: 6px;">
|
|
892
|
+
<span style="opacity: 0.8;">💡</span>
|
|
893
|
+
<span>提示:按 <kbd style="padding: 2px 6px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 4px; font-size: 11px;">Enter</kbd> 发送,<kbd style="padding: 2px 6px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 4px; font-size: 11px;">Shift + Enter</kbd> 换行</span>
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
</div>
|
|
899
|
+
|
|
900
|
+
<!-- 图片预览模态框 -->
|
|
901
|
+
<div id="imagePreviewModal" class="modal hidden" style="z-index: 2000;">
|
|
902
|
+
<div class="modal-content" style="max-width: 90vw; max-height: 90vh; background: white; padding: 20px; border-radius: 12px;">
|
|
903
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
|
904
|
+
<h3 style="color: #333; margin: 0;">🎨 白板作品预览</h3>
|
|
905
|
+
<button class="modal-close" id="closeImagePreview" style="background: rgba(0,0,0,0.1); border: none; color: #333; width: 36px; height: 36px; border-radius: 8px; cursor: pointer; font-size: 24px;">×</button>
|
|
906
|
+
</div>
|
|
907
|
+
<div style="text-align: center; max-height: calc(90vh - 100px); overflow: auto;">
|
|
908
|
+
<img id="previewImage" src="" alt="预览" style="max-width: 100%; max-height: calc(90vh - 120px); border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.2);">
|
|
909
|
+
</div>
|
|
910
|
+
<div style="text-align: center; margin-top: 15px;">
|
|
911
|
+
<button id="downloadImageBtn" class="btn-primary" style="padding: 10px 20px;">📥 下载图片</button>
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
|
|
916
|
+
<!-- 协作白板模态框 -->
|
|
917
|
+
<div id="whiteboardModal" class="modal hidden">
|
|
918
|
+
<div class="modal-content" style="max-width: 95vw; max-height: 90vh; width: 1400px;">
|
|
919
|
+
<div class="modal-header">
|
|
920
|
+
<h3>🎨 协作白板</h3>
|
|
921
|
+
<div style="display: flex; gap: 10px;">
|
|
922
|
+
<button class="btn-secondary" id="clearCanvasBtn">清空画布</button>
|
|
923
|
+
<button class="btn-primary" id="saveCanvasBtn">保存白板</button>
|
|
924
|
+
<button class="btn-success" id="sendToGroupBtn" style="background: var(--success); color: white;">📤 发送到群聊</button>
|
|
925
|
+
<button class="modal-close" id="closeWhiteboardModal">×</button>
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
<div class="whiteboard-container" style="padding: 15px;">
|
|
929
|
+
<div class="whiteboard-toolbar" style="display: flex; gap: 10px; margin-bottom: 15px; padding: 10px; background: var(--bg-secondary); border-radius: 8px;">
|
|
930
|
+
<button class="tool-btn active" data-tool="pen" style="padding: 8px 15px; border: 2px solid var(--primary); border-radius: 6px; background: var(--primary); color: white;">✏️ 画笔</button>
|
|
931
|
+
<button class="tool-btn" data-tool="eraser" style="padding: 8px 15px; border: 2px solid var(--border); border-radius: 6px; background: transparent;">🧹 橡皮擦</button>
|
|
932
|
+
<input type="color" id="colorPickerCanvas" value="#000000" title="颜色" style="width: 50px; height: 40px; border: none; border-radius: 6px; cursor: pointer;">
|
|
933
|
+
<input type="range" id="brushSizeCanvas" min="1" max="20" value="3" title="画笔大小" style="width: 150px;">
|
|
934
|
+
<span id="brushSizeLabel" style="padding: 8px 15px;">大小: 3</span>
|
|
935
|
+
</div>
|
|
936
|
+
<canvas id="whiteboardCanvas" width="1300" height="600" style="border: 2px solid var(--border); background: white; cursor: crosshair; border-radius: 8px; display: block;"></canvas>
|
|
937
|
+
</div>
|
|
938
|
+
</div>
|
|
939
|
+
</div>
|
|
940
|
+
`;const h=document.getElementById("messages"),M=document.getElementById("messageInput"),p=document.getElementById("sendBtn"),i=document.getElementById("emojiBtn"),t=document.getElementById("emojiPicker");let r=new Set((g.mutedUsers||[]).map(String)),l=!!g.mutedAll;i.addEventListener("click",()=>{t.classList.toggle("hidden")}),t.addEventListener("emoji-click",f=>{M.value+=f.detail.unicode,M.focus(),t.classList.add("hidden")}),document.addEventListener("click",f=>{!i.contains(f.target)&&!t.contains(f.target)&&t.classList.add("hidden")}),"Notification"in window&&Notification.permission==="default"&&Notification.requestPermission();try{const f=await n.getGroupMessages(d._id);f.messages&&(f.messages.forEach(I=>{const x=document.createElement("div");x.className=`message ${I.sender===s?"own":""}`;const R=y(I.content),O=I.content.startsWith("[白板作品]")||I.content.startsWith("[投票]"),K=I.content.startsWith("[白板作品]");x.innerHTML=`
|
|
941
|
+
<div class="message-header">
|
|
942
|
+
<span class="message-user">${I.username}</span>
|
|
943
|
+
<span class="message-time">${new Date(I.timestamp).toLocaleTimeString()}</span>
|
|
944
|
+
</div>
|
|
945
|
+
<div class="message-content" style="${K?"background: linear-gradient(135deg, rgb(99, 102, 241) 0%, rgb(139, 92, 246) 100%); color: white; padding: 16px; border-radius: 12px; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);":O?"background: transparent; padding: 0;":""}">${R}</div>
|
|
946
|
+
`,h.appendChild(x)}),h.scrollTop=h.scrollHeight)}catch(f){console.error("加载历史消息失败:",f)}const b=()=>{const f=document.getElementById("muteAllBtn");f.textContent=l?"取消全体禁言":"全体禁言",f.style.background=l?"var(--danger)":""};b(),document.getElementById("clearChatBtn").addEventListener("click",async()=>{if(!confirm(`⚠️ 警告:此操作将永久删除该群组的所有聊天记录!
|
|
947
|
+
|
|
948
|
+
确定要清除吗?`))return;if(prompt(`请输入群组名称以确认删除:
|
|
949
|
+
|
|
950
|
+
群组名称:`+d.name)!==d.name){alert("❌ 群组名称不匹配,操作已取消");return}try{const x=document.getElementById("clearChatBtn"),R=x.innerHTML;x.innerHTML="⏳ 清除中...",x.disabled=!0;const O=localStorage.getItem("token"),K=await fetch(`http://localhost:3000/api/messages/group/${d._id}/clear`,{method:"DELETE",headers:{Authorization:`Bearer ${O}`,"Content-Type":"application/json"}});if(!K.ok){const J=await K.json();throw new Error(J.message||"清除失败")}const Z=await K.json();h.innerHTML='<div class="empty-state" style="padding: 40px; text-align: center; color: var(--text-secondary);">✨ 聊天记录已清空</div>',alert(`✅ 成功清除 ${Z.deletedCount||0} 条聊天记录!`),x.innerHTML=R,x.disabled=!1}catch(x){console.error("清除聊天记录失败:",x),alert("❌ 清除失败: "+x.message);const R=document.getElementById("clearChatBtn");R.innerHTML="🗑️ 清除记录",R.disabled=!1}}),document.getElementById("muteAllBtn").addEventListener("click",async()=>{try{const f=!l;l=!!(await n.setMuteAll(d._id,f)).mutedAll,b();const x=document.createElement("div");x.className="notification",x.textContent=l?"已开启全体禁言(成员无法发言)":"已取消全体禁言",h.appendChild(x),h.scrollTop=h.scrollHeight}catch(f){alert("设置失败: "+f.message)}}),document.getElementById("manageMuteBtn").addEventListener("click",async()=>{g=(await n.getGroup(d._id)).group,r=new Set((g.mutedUsers||[]).map(String));const I=document.getElementById("membersList");I.innerHTML=g.members.filter(x=>x._id.toString()!==s).map(x=>{const R=r.has(x._id.toString());return`
|
|
951
|
+
<div style="display: flex; align-items: center; justify-content: space-between; padding: 12px; border-bottom: 1px solid var(--border);">
|
|
952
|
+
<div style="display: flex; align-items: center; gap: 10px;">
|
|
953
|
+
<div class="avatar" style="width: 35px; height: 35px;">${x.username[0].toUpperCase()}</div>
|
|
954
|
+
<span>${x.username}</span>
|
|
955
|
+
</div>
|
|
956
|
+
<button class="btn-secondary btn-sm" onclick="toggleMute('${x._id}')" id="mute-${x._id}">
|
|
957
|
+
${R?"取消禁言":"禁言"}
|
|
958
|
+
</button>
|
|
959
|
+
</div>
|
|
960
|
+
`}).join(""),document.getElementById("manageMuteModal").classList.remove("hidden")}),document.getElementById("closeMuteModal").addEventListener("click",()=>{document.getElementById("manageMuteModal").classList.add("hidden")}),document.getElementById("createPollBtn").addEventListener("click",()=>{document.getElementById("createPollModal").classList.remove("hidden")}),document.getElementById("closePollModal").addEventListener("click",()=>{document.getElementById("createPollModal").classList.add("hidden"),document.getElementById("createPollForm").reset()}),document.getElementById("cancelPollModal").addEventListener("click",()=>{document.getElementById("createPollModal").classList.add("hidden"),document.getElementById("createPollForm").reset()}),document.getElementById("addPollOption").addEventListener("click",()=>{const f=document.getElementById("pollOptions"),I=f.querySelectorAll(".poll-option-input").length+1,x=document.createElement("div");x.className="poll-option-input",x.style.cssText="display: flex; gap: 10px; margin-bottom: 10px;",x.innerHTML=`
|
|
961
|
+
<input type="text" class="poll-option" placeholder="选项 ${I}" required style="flex: 1; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary); color: white;">
|
|
962
|
+
<button type="button" class="btn-danger remove-option" style="padding: 10px 15px;">删除</button>
|
|
963
|
+
`,f.appendChild(x),x.querySelector(".remove-option").addEventListener("click",()=>{f.querySelectorAll(".poll-option-input").length>2?x.remove():alert("至少需要2个选项!")})}),document.getElementById("createPollForm").addEventListener("submit",async f=>{f.preventDefault();const I=document.getElementById("pollTitle").value.trim(),x=document.getElementById("pollDescription").value.trim(),R=document.getElementById("allowMultiple").checked,O=document.getElementById("anonymous").checked,K=document.getElementById("pollEndTime").value,Z=document.querySelectorAll(".poll-option"),J=Array.from(Z).map(te=>te.value.trim()).filter(te=>te);if(J.length<2){alert("至少需要2个选项!");return}try{const te=localStorage.getItem("token"),Q=await fetch("http://localhost:3000/api/polls",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${te}`},body:JSON.stringify({title:I,description:x,options:J,groupId:d._id,allowMultiple:R,anonymous:O,endTime:K||null})});if(!Q.ok){const ee=await Q.json();throw new Error(ee.error||"创建投票失败")}const Y=(await Q.json()).poll;e.sendChatMessage(d._id,a.username,`[投票]${Y._id}`),alert("投票创建成功!"),document.getElementById("createPollModal").classList.add("hidden"),document.getElementById("createPollForm").reset()}catch(te){console.error("创建投票失败:",te),alert("创建投票失败: "+te.message)}}),window.toggleMute=async f=>{try{const I=!r.has(f),x=await n.setUserMute(d._id,f,I);r=new Set((x.mutedUsers||[]).map(String));const R=document.getElementById(`mute-${f}`);R.textContent=r.has(f)?"取消禁言":"禁言",R.style.background=r.has(f)?"var(--danger)":""}catch(I){alert("操作失败: "+I.message)}},e.on("chat_message",f=>{if(f.groupId===d._id){const I=document.createElement("div");I.className=`message ${f.userId===s?"own":""}`;const x=y(f.content),R=f.content.startsWith("[白板作品]")||f.content.startsWith("[投票]"),O=f.content.startsWith("[白板作品]");I.innerHTML=`
|
|
964
|
+
<div class="message-header">
|
|
965
|
+
<span class="message-user">${f.username}</span>
|
|
966
|
+
<span class="message-time">${new Date(f.timestamp).toLocaleTimeString()}</span>
|
|
967
|
+
</div>
|
|
968
|
+
<div class="message-content" style="${O?"background: linear-gradient(135deg, rgb(99, 102, 241) 0%, rgb(139, 92, 246) 100%); color: white; padding: 16px; border-radius: 12px; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);":R?"background: transparent; padding: 0;":""}">${x}</div>
|
|
969
|
+
`,h.appendChild(I),h.scrollTop=h.scrollHeight}}),e.on("chat_blocked",f=>{if(f.groupId===d._id){const I=document.createElement("div");I.className="notification",I.textContent=f.message||"消息发送失败",h.appendChild(I),h.scrollTop=h.scrollHeight}});const E=()=>{const f=M.value.trim();f&&(e.sendChatMessage(d._id,a.username,f),M.value="")};p.addEventListener("click",E),M.addEventListener("keypress",f=>{f.key==="Enter"&&E()}),document.getElementById("openAIBtn").addEventListener("click",()=>{document.getElementById("aiModal").classList.remove("hidden")}),document.getElementById("closeAIModal").addEventListener("click",()=>{document.getElementById("aiModal").classList.add("hidden")}),document.querySelectorAll(".quick-question-btn").forEach(f=>{f.addEventListener("click",()=>{document.getElementById("aiInputText").value=f.dataset.question,document.getElementById("aiSendBtnModal").click()}),f.addEventListener("mouseenter",I=>{I.target.style.background="var(--primary)",I.target.style.color="white",I.target.style.transform="translateY(-2px)"}),f.addEventListener("mouseleave",I=>{I.target.style.background="var(--bg)",I.target.style.color="inherit",I.target.style.transform="translateY(0)"})});const w=document.getElementById("aiInputText");w.addEventListener("input",()=>{w.style.height="auto",w.style.height=w.scrollHeight+"px"}),w.addEventListener("keydown",f=>{f.key==="Enter"&&!f.shiftKey&&(f.preventDefault(),document.getElementById("aiSendBtnModal").click())}),w.addEventListener("focus",()=>{w.style.borderColor="var(--primary)"}),w.addEventListener("blur",()=>{w.style.borderColor="var(--border)"});const P=document.getElementById("aiSendBtnModal");P.addEventListener("mouseenter",()=>{P.style.transform="scale(1.05)"}),P.addEventListener("mouseleave",()=>{P.style.transform="scale(1)"}),document.getElementById("aiSendBtnModal").addEventListener("click",async()=>{const f=document.getElementById("aiInputText"),I=f.value.trim();if(!I)return;const x=document.getElementById("aiChatMessages"),R=document.createElement("div");R.className="ai-message user",R.style.cssText="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 18px; border-radius: 18px 18px 4px 18px; margin: 10px 0; max-width: 75%; margin-left: auto; text-align: right; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); animation: slideInRight 0.3s ease;",R.textContent=I,x.appendChild(R),f.value="";const O=document.createElement("div");O.className="ai-message ai loading",O.style.cssText="background: var(--bg-tertiary); padding: 12px 16px; border-radius: 12px; margin: 10px 0; max-width: 70%;",O.textContent="思考中...",x.appendChild(O),x.scrollTop=x.scrollHeight;try{const K=localStorage.getItem("token"),J=await(await fetch("http://localhost:3000/api/ai/ask",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${K}`},body:JSON.stringify({question:I,groupId:d==null?void 0:d._id})})).json();O.remove();const te=document.createElement("div");te.className="ai-message ai",te.style.cssText="background: var(--bg-secondary); padding: 15px 18px; border-radius: 18px 18px 18px 4px; margin: 10px 0; max-width: 75%; border: 1px solid var(--border); box-shadow: 0 2px 4px rgba(0,0,0,0.05); animation: slideInLeft 0.3s ease; line-height: 1.6;",te.textContent=J.answer||"抱歉,我无法回答这个问题。",x.appendChild(te),x.scrollTop=x.scrollHeight}catch(K){O.remove();const Z=document.createElement("div");Z.className="ai-message ai error",Z.style.cssText="background: var(--danger); color: white; padding: 12px 16px; border-radius: 12px; margin: 10px 0; max-width: 70%;",Z.textContent="抱歉,发生了错误: "+K.message,x.appendChild(Z),x.scrollTop=x.scrollHeight}}),window.showImageModal=f=>{const I=document.getElementById("imagePreviewModal"),x=document.getElementById("previewImage"),R=document.getElementById("downloadImageBtn");x.src=f,I.classList.remove("hidden"),R.onclick=async()=>{try{const K=await(await fetch(f)).blob(),Z=window.URL.createObjectURL(K),J=document.createElement("a");J.href=Z,J.download=`whiteboard-${Date.now()}.png`,document.body.appendChild(J),J.click(),document.body.removeChild(J),window.URL.revokeObjectURL(Z)}catch(O){console.error("下载失败:",O),alert("下载失败,请重试")}}},document.getElementById("closeImagePreview").addEventListener("click",()=>{document.getElementById("imagePreviewModal").classList.add("hidden")}),document.getElementById("imagePreviewModal").addEventListener("click",f=>{f.target.id==="imagePreviewModal"&&document.getElementById("imagePreviewModal").classList.add("hidden")}),document.getElementById("openWhiteboardBtn").addEventListener("click",()=>{document.getElementById("whiteboardModal").classList.remove("hidden"),G()}),document.getElementById("closeWhiteboardModal").addEventListener("click",()=>{document.getElementById("whiteboardModal").classList.add("hidden")});function G(){const f=document.getElementById("whiteboardCanvas");if(!f)return;const I=f.getContext("2d");let x=!1,R="pen",O="#000000",K=3,Z=0,J=0;document.querySelectorAll(".tool-btn").forEach(Y=>{Y.onclick=()=>{document.querySelectorAll(".tool-btn").forEach(ee=>{ee.style.background="transparent",ee.style.borderColor="var(--border)",ee.style.color="inherit",ee.classList.remove("active")}),Y.style.background="var(--primary)",Y.style.borderColor="var(--primary)",Y.style.color="white",Y.classList.add("active"),R=Y.dataset.tool}});const te=document.getElementById("colorPickerCanvas");te&&(te.onchange=Y=>{O=Y.target.value});const Q=document.getElementById("brushSizeCanvas"),ue=document.getElementById("brushSizeLabel");Q&&ue&&(Q.oninput=Y=>{K=Y.target.value,ue.textContent=`大小: ${K}`}),f.onmousedown=Y=>{x=!0;const ee=f.getBoundingClientRect();Z=Y.clientX-ee.left,J=Y.clientY-ee.top},f.onmousemove=Y=>{if(!x)return;const ee=f.getBoundingClientRect(),ye=Y.clientX-ee.left,we=Y.clientY-ee.top;I.beginPath(),I.moveTo(Z,J),I.lineTo(ye,we),I.strokeStyle=R==="eraser"?"#ffffff":O,I.lineWidth=K,I.lineCap="round",I.stroke(),Z=ye,J=we},f.onmouseup=()=>{x=!1},f.onmouseleave=()=>{x=!1},document.getElementById("clearCanvasBtn").onclick=()=>{confirm("确定要清空画布吗?")&&I.clearRect(0,0,f.width,f.height)},document.getElementById("saveCanvasBtn").onclick=()=>{const Y=f.toDataURL("image/png"),ee=document.createElement("a");ee.download=`whiteboard-${Date.now()}.png`,ee.href=Y,ee.click(),alert("白板已保存!")},document.getElementById("sendToGroupBtn").onclick=async()=>{try{const Y=f.toDataURL("image/png"),ee=await fetch(Y).then(tt=>tt.blob()),ye=new FormData;ye.append("file",ee,`whiteboard-${Date.now()}.png`),ye.append("groupId",d._id),ye.append("description","协作白板作品");const we=localStorage.getItem("token"),et=await fetch("http://localhost:3000/api/files/upload",{method:"POST",headers:{Authorization:`Bearer ${we}`},body:ye});if(et.ok){const Dt=`http://localhost:3000/api/files/${(await et.json()).file._id}/download?token=${we}`;e.sendChatMessage(d._id,a.username,`[白板作品]${Dt}`),alert("白板作品已发送到群聊!"),document.getElementById("whiteboardModal").classList.add("hidden")}else throw new Error("上传失败")}catch(Y){console.error("发送白板作品错误:",Y),alert("发送失败: "+Y.message)}}}}async function V(m){if(!d){m.innerHTML='<div class="empty-state">请先选择一个群组</div>';return}m.innerHTML=`
|
|
970
|
+
<div class="view-header">
|
|
971
|
+
<h2>随机点名 - ${d.name}</h2>
|
|
972
|
+
</div>
|
|
973
|
+
<div class="call-panel">
|
|
974
|
+
<div class="call-controls">
|
|
975
|
+
<label>点名人数:</label>
|
|
976
|
+
<input type="number" id="callCount" value="1" min="1" max="10">
|
|
977
|
+
<button class="btn-primary btn-large" id="randomCallBtn">🎲 开始点名</button>
|
|
978
|
+
</div>
|
|
979
|
+
<div id="callResult" class="call-result"></div>
|
|
980
|
+
</div>
|
|
981
|
+
`,document.getElementById("randomCallBtn").addEventListener("click",async()=>{const u=parseInt(document.getElementById("callCount").value);try{const g=await n.randomCall(d._id,u),y=document.getElementById("callResult");if(y.innerHTML=`
|
|
982
|
+
<h3>点名结果:</h3>
|
|
983
|
+
<div class="called-members">
|
|
984
|
+
${g.calledMembers.map(h=>`
|
|
985
|
+
<div class="member-card">
|
|
986
|
+
<div class="avatar">${h.username[0].toUpperCase()}</div>
|
|
987
|
+
<div class="member-name">${h.username}</div>
|
|
988
|
+
</div>
|
|
989
|
+
`).join("")}
|
|
990
|
+
</div>
|
|
991
|
+
`,Array.isArray(g.calledMembers)&&g.calledMembers.length>0&&e&&d&&a){const h=g.calledMembers.map(p=>p.username).join("、"),M=`🎲 本次随机点名(${g.calledMembers.length} 人):${h}`;try{e.sendChatMessage(d._id,a.username,M)}catch(p){console.error("发送点名结果到群聊失败:",p)}}}catch(g){alert("点名失败: "+g.message)}})}async function ne(m){m.innerHTML=`
|
|
992
|
+
<div class="view-header">
|
|
993
|
+
<h2>操作记录</h2>
|
|
994
|
+
<div style="display: flex; gap: 10px;">
|
|
995
|
+
<select id="auditGroupFilter" class="form-select">
|
|
996
|
+
<option value="">全部群组</option>
|
|
997
|
+
</select>
|
|
998
|
+
<select id="auditActionFilter" class="form-select">
|
|
999
|
+
<option value="">全部操作</option>
|
|
1000
|
+
<option value="document_create">文档创建</option>
|
|
1001
|
+
<option value="document_update">文档更新</option>
|
|
1002
|
+
<option value="document_delete">文档删除</option>
|
|
1003
|
+
<option value="content_edit">内容编辑</option>
|
|
1004
|
+
<option value="title_edit">标题编辑</option>
|
|
1005
|
+
<option value="document_permission_change">权限修改</option>
|
|
1006
|
+
</select>
|
|
1007
|
+
<input type="date" id="startDate" class="form-input" title="开始日期">
|
|
1008
|
+
<input type="date" id="endDate" class="form-input" title="结束日期">
|
|
1009
|
+
<button class="btn-primary" id="applyFilters">筛选</button>
|
|
1010
|
+
<button class="btn-secondary" id="exportLogs">导出</button>
|
|
1011
|
+
<button class="btn-danger" id="clearAuditLogs">清除记录</button>
|
|
1012
|
+
</div>
|
|
1013
|
+
</div>
|
|
1014
|
+
|
|
1015
|
+
<div class="audit-stats" id="auditStats">
|
|
1016
|
+
<div class="stat-card">
|
|
1017
|
+
<h3>今日操作</h3>
|
|
1018
|
+
<div class="stat-number" id="todayCount">-</div>
|
|
1019
|
+
</div>
|
|
1020
|
+
<div class="stat-card">
|
|
1021
|
+
<h3>本周操作</h3>
|
|
1022
|
+
<div class="stat-number" id="weekCount">-</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
<div class="stat-card">
|
|
1025
|
+
<h3>活跃用户</h3>
|
|
1026
|
+
<div class="stat-number" id="activeUsers">-</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
|
|
1030
|
+
<div class="audit-logs" id="auditLogs">
|
|
1031
|
+
<div class="loading">加载中...</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
|
|
1034
|
+
<div class="pagination" id="auditPagination" style="display: none;">
|
|
1035
|
+
<button class="btn-secondary" id="prevPage">上一页</button>
|
|
1036
|
+
<span id="pageInfo">第 1 页,共 1 页</span>
|
|
1037
|
+
<button class="btn-secondary" id="nextPage">下一页</button>
|
|
1038
|
+
</div>
|
|
1039
|
+
|
|
1040
|
+
<div id="auditDetailModal" class="modal hidden">
|
|
1041
|
+
<div class="modal-content" style="max-width: 900px; max-height: 90vh; border-radius: 12px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.3); display: flex; flex-direction: column;">
|
|
1042
|
+
<div class="modal-header" style="background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); color: white; padding: 16px 20px; position: relative; overflow: hidden; flex-shrink: 0;">
|
|
1043
|
+
<div style="display: flex; align-items: center; justify-content: space-between; position: relative; z-index: 1;">
|
|
1044
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
1045
|
+
<div style="width: 40px; height: 40px; background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">📋</div>
|
|
1046
|
+
<h3 style="margin: 0; font-size: 18px; font-weight: 700; letter-spacing: -0.5px;">操作详情</h3>
|
|
1047
|
+
</div>
|
|
1048
|
+
<button class="close-btn" id="closeAuditDetail" style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: none; color: white; width: 36px; height: 36px; border-radius: 8px; cursor: pointer; font-size: 20px; transition: all 0.3s; display: flex; align-items: center; justify-content: center;" onmouseover="this.style.background='rgba(255,255,255,0.25)'" onmouseout="this.style.background='rgba(255,255,255,0.15)'">×</button>
|
|
1049
|
+
</div>
|
|
1050
|
+
</div>
|
|
1051
|
+
<div class="modal-body" id="auditDetailContent" style="overflow-y: auto; flex: 1; padding: 16px;">
|
|
1052
|
+
</div>
|
|
1053
|
+
</div>
|
|
1054
|
+
</div>
|
|
1055
|
+
`;let u=1,g={};try{const t=await n.getGroups(),r=document.getElementById("auditGroupFilter");t.groups.forEach(l=>{const b=document.createElement("option");b.value=l._id,b.textContent=l.name,r.appendChild(b)})}catch(t){console.error("加载群组列表失败:",t)}async function y(t=1,r={}){try{const l=document.getElementById("auditLogs");l.innerHTML='<div class="loading">加载中...</div>';const b={page:t,limit:20},E=await n.getAuditLogs(r,b);if(E.logs.length===0){l.innerHTML='<div class="empty-state">暂无操作记录</div>',document.getElementById("auditPagination").style.display="none";return}l.innerHTML=`
|
|
1056
|
+
<div class="audit-table">
|
|
1057
|
+
<div class="audit-header">
|
|
1058
|
+
<div>时间</div>
|
|
1059
|
+
<div>用户</div>
|
|
1060
|
+
<div>操作</div>
|
|
1061
|
+
<div>资源</div>
|
|
1062
|
+
<div>详情</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
${E.logs.map(G=>{var f,I,x,R,O;return`
|
|
1065
|
+
<div class="audit-row" onclick="showAuditDetail('${G._id}')">
|
|
1066
|
+
<div class="audit-time">${new Date(G.createdAt).toLocaleString()}</div>
|
|
1067
|
+
<div class="audit-user">
|
|
1068
|
+
<div class="avatar">${((x=(I=(f=G.user)==null?void 0:f.username)==null?void 0:I[0])==null?void 0:x.toUpperCase())||"?"}</div>
|
|
1069
|
+
<span>${((R=G.user)==null?void 0:R.username)||"未知用户"}</span>
|
|
1070
|
+
</div>
|
|
1071
|
+
<div class="audit-action">
|
|
1072
|
+
<span class="action-badge action-${G.action}">${A(G.action)}</span>
|
|
1073
|
+
</div>
|
|
1074
|
+
<div class="audit-resource">${G.resourceTitle||G.resourceId}</div>
|
|
1075
|
+
<div class="audit-description">${((O=G.details)==null?void 0:O.description)||"-"}</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
`}).join("")}
|
|
1078
|
+
</div>
|
|
1079
|
+
`;const w=document.getElementById("auditPagination"),P=document.getElementById("pageInfo");P.textContent=`第 ${E.pagination.page} 页,共 ${E.pagination.pages} 页`,document.getElementById("prevPage").disabled=E.pagination.page<=1,document.getElementById("nextPage").disabled=E.pagination.page>=E.pagination.pages,w.style.display=E.pagination.pages>1?"flex":"none"}catch(l){console.error("加载审计日志失败:",l),document.getElementById("auditLogs").innerHTML='<div class="error-state">加载失败: '+l.message+"</div>"}}async function h(){try{const t=new Date,r=new Date(t.getTime()-7*24*60*60*1e3),l=await n.getAuditSummary({startDate:t.toISOString().split("T")[0],endDate:t.toISOString().split("T")[0]}),b=await n.getAuditSummary({startDate:r.toISOString().split("T")[0],endDate:t.toISOString().split("T")[0]});document.getElementById("todayCount").textContent=l.summary.totalLogs,document.getElementById("weekCount").textContent=b.summary.totalLogs,document.getElementById("activeUsers").textContent=b.summary.topUsers.length}catch(t){console.error("加载统计信息失败:",t)}}function M(t){var w,P,G,f,I,x;const r=((w=t.user)==null?void 0:w.username)||"未知用户",l=A(t.action),b=t.resourceTitle||t.resourceId;let E='<strong style="color: #6366f1;">'+r+"</strong> ";switch(t.action){case"document_create":E+='创建了文档 <strong>"'+b+'"</strong>';break;case"document_update":E+='更新了文档 <strong>"'+b+'"</strong>',(P=t.details)!=null&&P.field&&(E+=" 的 <strong>"+p(t.details.field)+"</strong>");break;case"document_delete":E+='删除了文档 <strong>"'+b+'"</strong>';break;case"content_edit":if(E+='编辑了文档 <strong>"'+b+'"</strong> 的内容',t.changes){const R=((G=t.changes.insertions)==null?void 0:G.reduce((K,Z)=>K+Z.length,0))||0,O=((f=t.changes.deletions)==null?void 0:f.reduce((K,Z)=>K+Z.length,0))||0;(R>0||O>0)&&(E+=' (<span style="color: #10b981;">+'+R+'</span> / <span style="color: #ef4444;">-'+O+"</span> 字符)")}break;case"title_edit":E+='修改了文档 <strong>"'+b+'"</strong> 的标题',(I=t.details)!=null&&I.oldValue&&((x=t.details)!=null&&x.newValue)&&(E+=' 从 <strong>"'+t.details.oldValue+'"</strong> 改为 <strong>"'+t.details.newValue+'"</strong>');break;case"document_permission_change":E+='修改了文档 <strong>"'+b+'"</strong> 的权限设置';break;default:E+="执行了 <strong>"+l+"</strong> 操作"}return E}function p(t){return{title:"标题",content:"内容",permissions:"权限",status:"状态",tags:"标签",category:"分类",description:"描述"}[t]||t}function i(t){if(t==null)return'<span style="color: var(--text-tertiary); font-style: italic;">空</span>';if(typeof t=="object")return JSON.stringify(t,null,2);const r=String(t);return r.length>500?r.substring(0,500)+'... <span style="color: var(--text-tertiary); font-style: italic;">(内容过长,已截断)</span>':r.replace(/</g,"<").replace(/>/g,">")}window.showAuditDetail=async t=>{var r,l,b,E,w,P;try{const G=localStorage.getItem("token"),x=(await(await fetch(`http://localhost:3000/api/audit/${t}`,{headers:{Authorization:`Bearer ${G}`}})).json()).log,R=document.getElementById("auditDetailModal"),O=document.getElementById("auditDetailContent"),K=te=>({create:"#10b981",update:"#f59e0b",delete:"#ef4444",login:"#6366f1",logout:"#8b5cf6"})[te]||"#6366f1";O.innerHTML=`
|
|
1080
|
+
<div class="audit-detail" style="padding: 0; background: linear-gradient(135deg, rgba(99, 102, 241, 0.03) 0%, rgba(168, 85, 247, 0.03) 100%);">
|
|
1081
|
+
|
|
1082
|
+
<!-- 操作信息 -->
|
|
1083
|
+
<div class="detail-section" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 8px; border-left: 4px solid ${K(x.action)}; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
|
|
1084
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
|
1085
|
+
<span style="font-size: 24px;">📋</span>
|
|
1086
|
+
<h4 style="margin: 0; color: var(--text-primary); font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">操作信息</h4>
|
|
1087
|
+
</div>
|
|
1088
|
+
<div style="display: grid; grid-template-columns: 100px 1fr; gap: 10px; font-size: 13px;">
|
|
1089
|
+
<span style="color: var(--text-tertiary); font-weight: 500;">操作类型:</span>
|
|
1090
|
+
<span style="display: inline-flex; align-items: center; gap: 8px;">
|
|
1091
|
+
<span style="display: inline-block; padding: 4px 10px; background: ${K(x.action)}; color: white; border-radius: 6px; font-weight: 600; font-size: 12px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);">${A(x.action)}</span>
|
|
1092
|
+
</span>
|
|
1093
|
+
<span style="color: var(--text-tertiary); font-weight: 500;">操作时间:</span>
|
|
1094
|
+
<span style="font-weight: 600; color: var(--text-primary);">${new Date(x.createdAt).toLocaleString("zh-CN",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"})}</span>
|
|
1095
|
+
<span style="color: var(--text-tertiary); font-weight: 500;">操作用户:</span>
|
|
1096
|
+
<span style="display: flex; align-items: center; gap: 10px;">
|
|
1097
|
+
<div class="avatar" style="width: 32px; height: 32px; font-size: 14px; background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);">${((b=(l=(r=x.user)==null?void 0:r.username)==null?void 0:l[0])==null?void 0:b.toUpperCase())||"?"}</div>
|
|
1098
|
+
<span style="font-weight: 600; color: var(--text-primary);">${((E=x.user)==null?void 0:E.username)||"未知用户"}</span>
|
|
1099
|
+
</span>
|
|
1100
|
+
</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
|
|
1103
|
+
<!-- 资源信息 -->
|
|
1104
|
+
<div class="detail-section" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
|
|
1105
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
|
1106
|
+
<span style="font-size: 24px;">📄</span>
|
|
1107
|
+
<h4 style="margin: 0; color: var(--text-primary); font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">资源信息</h4>
|
|
1108
|
+
</div>
|
|
1109
|
+
<div style="display: grid; grid-template-columns: 100px 1fr; gap: 10px; font-size: 13px;">
|
|
1110
|
+
<span style="color: var(--text-tertiary); font-weight: 500;">资源类型:</span>
|
|
1111
|
+
<span style="font-weight: 600; color: var(--text-primary);">${x.resourceType||"未知"}</span>
|
|
1112
|
+
<span style="color: var(--text-tertiary); font-weight: 500;">资源ID:</span>
|
|
1113
|
+
<span style="font-family: 'Courier New', monospace; font-size: 13px; padding: 6px 12px; background: var(--bg); border-radius: 6px; color: var(--text-primary); border: 1px solid var(--border);">${x.resourceId}</span>
|
|
1114
|
+
<span style="color: var(--text-tertiary); font-weight: 500;">资源标题:</span>
|
|
1115
|
+
<span style="font-weight: 600; color: var(--text-primary);">${x.resourceTitle||'<span style="color: var(--text-tertiary); font-style: italic;">无</span>'}</span>
|
|
1116
|
+
</div>
|
|
1117
|
+
</div>
|
|
1118
|
+
|
|
1119
|
+
${x.details?`
|
|
1120
|
+
<!-- 详细信息 -->
|
|
1121
|
+
<div class="detail-section" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
|
|
1122
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
|
1123
|
+
<span style="font-size: 18px;">📝</span>
|
|
1124
|
+
<h4 style="margin: 0; color: var(--text-primary); font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">详细总结</h4>
|
|
1125
|
+
</div>
|
|
1126
|
+
<div style="font-size: 13px; line-height: 1.6; color: var(--text-primary); padding: 12px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%); border-radius: 8px; border-left: 4px solid #6366f1;">
|
|
1127
|
+
<div style="font-weight: 600; margin-bottom: 6px; font-size: 14px;" id="auditDescriptionText">
|
|
1128
|
+
</div>
|
|
1129
|
+
</div>
|
|
1130
|
+
|
|
1131
|
+
<div id="auditDetailsSection" style="margin-top: 12px; padding: 12px; background: var(--bg); border-radius: 8px; border: 1px solid var(--border);">
|
|
1132
|
+
<h5 style="margin: 0 0 8px 0; font-size: 12px; color: var(--text-secondary); font-weight: 600; display: flex; align-items: center; gap: 8px;">
|
|
1133
|
+
<span>ℹ️</span>
|
|
1134
|
+
<span>操作详情</span>
|
|
1135
|
+
</h5>
|
|
1136
|
+
<div id="auditDetailsContent" style="display: grid; grid-template-columns: 100px 1fr; gap: 8px; font-size: 12px;">
|
|
1137
|
+
</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
|
|
1140
|
+
<div id="auditChangesSection" style="margin-top: 12px; padding: 12px; background: var(--bg); border-radius: 8px; border: 1px solid var(--border); display: none;">
|
|
1141
|
+
<h5 style="margin: 0 0 8px 0; font-size: 12px; color: var(--text-secondary); font-weight: 600; display: flex; align-items: center; gap: 8px;">
|
|
1142
|
+
<span>🔄</span>
|
|
1143
|
+
<span>文本变更统计</span>
|
|
1144
|
+
</h5>
|
|
1145
|
+
<div id="auditChangesContent" style="display: flex; gap: 12px; font-size: 12px;">
|
|
1146
|
+
</div>
|
|
1147
|
+
</div>
|
|
1148
|
+
</div>
|
|
1149
|
+
`:""}
|
|
1150
|
+
|
|
1151
|
+
<!-- 请求信息 -->
|
|
1152
|
+
<div class="detail-section" style="padding: 12px; background: var(--bg-secondary); border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
|
|
1153
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
|
1154
|
+
<span style="font-size: 18px;">🌐</span>
|
|
1155
|
+
<h4 style="margin: 0; color: var(--text-primary); font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">请求信息</h4>
|
|
1156
|
+
</div>
|
|
1157
|
+
<div style="display: grid; grid-template-columns: 100px 1fr; gap: 10px; font-size: 13px;">
|
|
1158
|
+
<span style="color: var(--text-tertiary); font-weight: 500;">IP地址:</span>
|
|
1159
|
+
<span style="font-family: 'Courier New', monospace; font-weight: 600; color: var(--text-primary);">${x.ipAddress||'<span style="color: var(--text-tertiary); font-style: italic;">未记录</span>'}</span>
|
|
1160
|
+
<span style="color: var(--text-tertiary); font-weight: 500;">用户代理:</span>
|
|
1161
|
+
<span style="font-size: 13px; word-break: break-all; color: var(--text-secondary); line-height: 1.6;">${x.userAgent||'<span style="color: var(--text-tertiary); font-style: italic;">未记录</span>'}</span>
|
|
1162
|
+
</div>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
`,document.getElementById("auditDescriptionText").innerHTML=M(x);const Z=document.getElementById("auditDetailsContent");let J="";if(x.details&&(x.details.field&&(J+='<span style="color: var(--text-tertiary); font-weight: 500;">修改字段:</span>',J+='<span style="font-weight: 600; color: var(--text-primary);">'+p(x.details.field)+"</span>"),x.details.oldValue!==void 0&&x.details.oldValue!==null&&(J+='<span style="color: var(--text-tertiary); font-weight: 500;">原始值:</span>',J+=`<div style="padding: 8px 12px; background: rgba(239, 68, 68, 0.1); border-radius: 6px; border-left: 3px solid #ef4444; font-family: 'Courier New', monospace; font-size: 13px; word-break: break-all; max-height: 200px; overflow-y: auto;">`+i(x.details.oldValue)+"</div>"),x.details.newValue!==void 0&&x.details.newValue!==null&&(J+='<span style="color: var(--text-tertiary); font-weight: 500;">新值:</span>',J+=`<div style="padding: 8px 12px; background: rgba(16, 185, 129, 0.1); border-radius: 6px; border-left: 3px solid #10b981; font-family: 'Courier New', monospace; font-size: 13px; word-break: break-all; max-height: 200px; overflow-y: auto;">`+i(x.details.newValue)+"</div>")),J?(Z.innerHTML=J,document.getElementById("auditDetailsSection").style.display="block"):document.getElementById("auditDetailsSection").style.display="none",x.changes&&(((w=x.changes.insertions)==null?void 0:w.length)>0||((P=x.changes.deletions)==null?void 0:P.length)>0)){const te=document.getElementById("auditChangesContent");let Q="";if(x.changes.insertions&&x.changes.insertions.length>0){const ue=x.changes.insertions.reduce((Y,ee)=>Y+ee.length,0);Q+='<div style="flex: 1; padding: 12px; background: rgba(16, 185, 129, 0.1); border-radius: 8px; border-left: 3px solid #10b981;">',Q+='<div style="font-weight: 600; color: #10b981; margin-bottom: 4px;">✅ 新增内容</div>',Q+='<div style="color: var(--text-secondary);">共 '+ue+" 个字符</div>",Q+="</div>"}if(x.changes.deletions&&x.changes.deletions.length>0){const ue=x.changes.deletions.reduce((Y,ee)=>Y+ee.length,0);Q+='<div style="flex: 1; padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; border-left: 3px solid #ef4444;">',Q+='<div style="font-weight: 600; color: #ef4444; margin-bottom: 4px;">❌ 删除内容</div>',Q+='<div style="color: var(--text-secondary);">共 '+ue+" 个字符</div>",Q+="</div>"}te.innerHTML=Q,document.getElementById("auditChangesSection").style.display="block"}else document.getElementById("auditChangesSection").style.display="none";R.classList.remove("hidden")}catch(G){alert("加载详情失败: "+G.message)}},document.getElementById("applyFilters").addEventListener("click",()=>{g={groupId:document.getElementById("auditGroupFilter").value,action:document.getElementById("auditActionFilter").value,startDate:document.getElementById("startDate").value,endDate:document.getElementById("endDate").value},Object.keys(g).forEach(t=>{g[t]||delete g[t]}),u=1,y(u,g)}),document.getElementById("prevPage").addEventListener("click",()=>{u>1&&(u--,y(u,g))}),document.getElementById("nextPage").addEventListener("click",()=>{u++,y(u,g)}),document.getElementById("exportLogs").addEventListener("click",()=>{alert("导出功能开发中...")}),document.getElementById("clearAuditLogs").addEventListener("click",async()=>{const r=Object.keys(g||{}).length>0?"将清除当前筛选条件下的所有操作记录,确认继续?":"⚠️ 将清除全部操作记录(不可恢复),确认继续?";if(confirm(r))try{const l=await n.clearAuditLogs(g||{});alert(`已清除 ${l.deletedCount||0} 条操作记录`),u=1,await h(),await y(u,g)}catch(l){alert("清除失败: "+l.message)}}),document.getElementById("closeAuditDetail").addEventListener("click",()=>{document.getElementById("auditDetailModal").classList.add("hidden")}),h(),y()}async function oe(m){if(!d){m.innerHTML=`
|
|
1166
|
+
<div class="empty-state" style="text-align: center; padding: 60px 20px;">
|
|
1167
|
+
<div style="font-size: 64px; margin-bottom: 20px;">🗳️</div>
|
|
1168
|
+
<h3 style="font-size: 24px; margin-bottom: 12px;">投票管理</h3>
|
|
1169
|
+
<p style="color: var(--text-secondary); margin-bottom: 24px;">请先选择一个群组</p>
|
|
1170
|
+
<button class="btn-primary" onclick="document.querySelector('[data-view=\\"groups\\"]').click()">前往群组管理</button>
|
|
1171
|
+
</div>
|
|
1172
|
+
`;return}try{const u=localStorage.getItem("token"),g=await fetch(`http://localhost:3000/api/polls/group/${d._id}`,{headers:{Authorization:`Bearer ${u}`}});if(!g.ok)throw new Error("获取投票列表失败");const h=(await g.json()).polls||[];m.innerHTML=`
|
|
1173
|
+
<div class="view-header">
|
|
1174
|
+
<h2>🗳️ 投票管理 - ${d.name}</h2>
|
|
1175
|
+
</div>
|
|
1176
|
+
<div class="polls-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 20px; padding: 20px;">
|
|
1177
|
+
${h.length===0?'<div class="empty-state" style="grid-column: 1/-1;">暂无投票</div>':""}
|
|
1178
|
+
</div>
|
|
1179
|
+
`;const M=m.querySelector(".polls-grid");h.forEach(p=>{const i=p.options.reduce((l,b)=>l+b.votes.length,0),t=p.status==="ended"||p.endTime&&new Date(p.endTime)<new Date,r=document.createElement("div");r.className="poll-card",r.style.cssText="background: var(--bg-secondary); padding: 20px; border-radius: 12px; border: 1px solid var(--border); transition: transform 0.2s, box-shadow 0.2s;",r.innerHTML=`
|
|
1180
|
+
<div style="display: flex; align-items: start; justify-content: space-between; margin-bottom: 15px;">
|
|
1181
|
+
<div style="flex: 1;">
|
|
1182
|
+
<h3 style="margin: 0 0 8px 0; font-size: 18px;">${p.title}</h3>
|
|
1183
|
+
${p.description?`<p style="color: var(--text-secondary); margin: 0 0 12px 0; font-size: 14px;">${p.description}</p>`:""}
|
|
1184
|
+
</div>
|
|
1185
|
+
<span class="status-badge" style="background: ${t?"var(--danger)":"var(--success)"}; color: white; padding: 4px 12px; border-radius: 12px; font-size: 12px; white-space: nowrap;">${t?"已结束":"进行中"}</span>
|
|
1186
|
+
</div>
|
|
1187
|
+
|
|
1188
|
+
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 15px;">
|
|
1189
|
+
<span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px; color: var(--text-secondary);">
|
|
1190
|
+
${p.allowMultiple?"✓ 多选":"○ 单选"}
|
|
1191
|
+
</span>
|
|
1192
|
+
<span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px; color: var(--text-secondary);">
|
|
1193
|
+
${p.anonymous?"🔒 匿名":"👤 实名"}
|
|
1194
|
+
</span>
|
|
1195
|
+
<span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px; color: var(--text-secondary);">
|
|
1196
|
+
👥 ${i} 人投票
|
|
1197
|
+
</span>
|
|
1198
|
+
</div>
|
|
1199
|
+
|
|
1200
|
+
<div style="margin-bottom: 15px;">
|
|
1201
|
+
${p.options.map((l,b)=>{const E=i>0?(l.votes.length/i*100).toFixed(1):0;return`
|
|
1202
|
+
<div style="margin-bottom: 10px;">
|
|
1203
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
|
1204
|
+
<span style="font-size: 14px; color: var(--text-primary);">${l.text}</span>
|
|
1205
|
+
<span style="font-size: 14px; font-weight: 600; color: var(--primary);">${l.votes.length} 票 (${E}%)</span>
|
|
1206
|
+
</div>
|
|
1207
|
+
<div style="height: 6px; background: var(--bg-tertiary); border-radius: 3px; overflow: hidden;">
|
|
1208
|
+
<div style="height: 100%; background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); width: ${E}%; transition: width 0.3s;"></div>
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
`}).join("")}
|
|
1212
|
+
</div>
|
|
1213
|
+
|
|
1214
|
+
<div style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 15px;">
|
|
1215
|
+
<div>创建时间: ${new Date(p.createdAt).toLocaleString("zh-CN")}</div>
|
|
1216
|
+
${p.endTime?`<div>截止时间: ${new Date(p.endTime).toLocaleString("zh-CN")}</div>`:""}
|
|
1217
|
+
</div>
|
|
1218
|
+
|
|
1219
|
+
<div style="display: flex; gap: 10px;">
|
|
1220
|
+
<button class="btn-primary btn-sm view-poll-detail" data-poll-id="${p._id}" style="flex: 1;">查看详情</button>
|
|
1221
|
+
${t?"":`<button class="btn-secondary btn-sm end-poll" data-poll-id="${p._id}">结束投票</button>`}
|
|
1222
|
+
<button class="btn-danger btn-sm delete-poll" data-poll-id="${p._id}">删除</button>
|
|
1223
|
+
</div>
|
|
1224
|
+
`,r.onmouseenter=()=>{r.style.transform="translateY(-4px)",r.style.boxShadow="0 8px 16px rgba(0,0,0,0.1)"},r.onmouseleave=()=>{r.style.transform="translateY(0)",r.style.boxShadow="none"},M.appendChild(r)}),document.querySelectorAll(".view-poll-detail").forEach(p=>{p.addEventListener("click",async()=>{const i=p.dataset.pollId;await de(i)})}),document.querySelectorAll(".end-poll").forEach(p=>{p.addEventListener("click",async()=>{if(confirm("确定要结束这个投票吗?"))try{const i=localStorage.getItem("token");if(!(await fetch(`http://localhost:3000/api/polls/${p.dataset.pollId}/end`,{method:"PUT",headers:{Authorization:`Bearer ${i}`}})).ok)throw new Error("结束投票失败");alert("投票已结束!"),await oe(m)}catch(i){alert("操作失败: "+i.message)}})}),document.querySelectorAll(".delete-poll").forEach(p=>{p.addEventListener("click",async()=>{if(confirm("确定要删除这个投票吗?"))try{const i=localStorage.getItem("token");if(!(await fetch(`http://localhost:3000/api/polls/${p.dataset.pollId}`,{method:"DELETE",headers:{Authorization:`Bearer ${i}`}})).ok)throw new Error("删除投票失败");alert("投票已删除!"),await oe(m)}catch(i){alert("操作失败: "+i.message)}})})}catch(u){console.error("加载投票列表失败:",u),m.innerHTML=`
|
|
1225
|
+
<div class="view-header">
|
|
1226
|
+
<h2>🗳️ 投票管理</h2>
|
|
1227
|
+
</div>
|
|
1228
|
+
<div class="empty-state">加载失败: ${u.message}</div>
|
|
1229
|
+
`}}async function de(m){try{const u=localStorage.getItem("token"),g=await fetch(`http://localhost:3000/api/polls/${m}`,{headers:{Authorization:`Bearer ${u}`}});if(!g.ok)throw new Error("获取投票详情失败");const h=(await g.json()).poll,M=h.options.reduce((r,l)=>r+l.votes.length,0),p=h.status==="ended"||h.endTime&&new Date(h.endTime)<new Date,i=`
|
|
1230
|
+
<div id="pollDetailModal" class="modal" style="display: flex;">
|
|
1231
|
+
<div class="modal-content" style="max-width: 800px; max-height: 90vh; overflow-y: auto;">
|
|
1232
|
+
<div class="modal-header">
|
|
1233
|
+
<h3>📊 投票详情</h3>
|
|
1234
|
+
<button class="modal-close" id="closePollDetailModal">×</button>
|
|
1235
|
+
</div>
|
|
1236
|
+
<div class="modal-body" style="padding: 24px;">
|
|
1237
|
+
<h2 style="margin: 0 0 12px 0;">${h.title}</h2>
|
|
1238
|
+
${h.description?`<p style="color: var(--text-secondary); margin: 0 0 20px 0;">${h.description}</p>`:""}
|
|
1239
|
+
|
|
1240
|
+
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;">
|
|
1241
|
+
<span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px;">
|
|
1242
|
+
${h.allowMultiple?"✓ 多选投票":"○ 单选投票"}
|
|
1243
|
+
</span>
|
|
1244
|
+
<span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px;">
|
|
1245
|
+
${h.anonymous?"🔒 匿名投票":"👤 实名投票"}
|
|
1246
|
+
</span>
|
|
1247
|
+
<span style="font-size: 13px; padding: 6px 12px; background: ${p?"var(--danger)":"var(--success)"}; border-radius: 14px; color: white;">
|
|
1248
|
+
${p?"已结束":"进行中"}
|
|
1249
|
+
</span>
|
|
1250
|
+
</div>
|
|
1251
|
+
|
|
1252
|
+
<div style="padding: 16px; background: var(--bg-secondary); border-radius: 12px; margin-bottom: 20px;">
|
|
1253
|
+
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
|
|
1254
|
+
<div>
|
|
1255
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">创建者</div>
|
|
1256
|
+
<div style="font-weight: 600;">👤 ${h.creatorName}</div>
|
|
1257
|
+
</div>
|
|
1258
|
+
<div>
|
|
1259
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">总投票数</div>
|
|
1260
|
+
<div style="font-weight: 600; color: var(--primary);">👥 ${M} 人</div>
|
|
1261
|
+
</div>
|
|
1262
|
+
<div>
|
|
1263
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">创建时间</div>
|
|
1264
|
+
<div style="font-weight: 600;">⏰ ${new Date(h.createdAt).toLocaleString("zh-CN")}</div>
|
|
1265
|
+
</div>
|
|
1266
|
+
${h.endTime?`
|
|
1267
|
+
<div>
|
|
1268
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">截止时间</div>
|
|
1269
|
+
<div style="font-weight: 600;">⏰ ${new Date(h.endTime).toLocaleString("zh-CN")}</div>
|
|
1270
|
+
</div>
|
|
1271
|
+
`:""}
|
|
1272
|
+
</div>
|
|
1273
|
+
</div>
|
|
1274
|
+
|
|
1275
|
+
<h3 style="margin-bottom: 16px;">投票选项</h3>
|
|
1276
|
+
${h.options.map((r,l)=>{const b=M>0?(r.votes.length/M*100).toFixed(1):0;return`
|
|
1277
|
+
<div style="margin-bottom: 16px; padding: 16px; background: var(--bg-tertiary); border-radius: 8px;">
|
|
1278
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
1279
|
+
<span style="font-weight: 500; font-size: 16px;">${r.text}</span>
|
|
1280
|
+
<span style="font-weight: 600; color: var(--primary); font-size: 16px;">${r.votes.length} 票 (${b}%)</span>
|
|
1281
|
+
</div>
|
|
1282
|
+
<div style="height: 10px; background: var(--bg-secondary); border-radius: 5px; overflow: hidden; margin-bottom: 12px;">
|
|
1283
|
+
<div style="height: 100%; background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); width: ${b}%; transition: width 0.3s;"></div>
|
|
1284
|
+
</div>
|
|
1285
|
+
${!h.anonymous&&r.votes.length>0?`
|
|
1286
|
+
<div style="font-size: 13px; color: var(--text-secondary);">
|
|
1287
|
+
<strong>投票者:</strong> ${r.votes.map(E=>E.username).join(", ")}
|
|
1288
|
+
</div>
|
|
1289
|
+
`:""}
|
|
1290
|
+
</div>
|
|
1291
|
+
`}).join("")}
|
|
1292
|
+
|
|
1293
|
+
<button class="btn-secondary" id="closePollDetailBtn" style="width: 100%; margin-top: 20px;">关闭</button>
|
|
1294
|
+
</div>
|
|
1295
|
+
</div>
|
|
1296
|
+
</div>
|
|
1297
|
+
`,t=document.getElementById("pollDetailModal");t&&t.remove(),document.body.insertAdjacentHTML("beforeend",i),document.getElementById("closePollDetailModal").addEventListener("click",()=>{document.getElementById("pollDetailModal").remove()}),document.getElementById("closePollDetailBtn").addEventListener("click",()=>{document.getElementById("pollDetailModal").remove()})}catch(u){console.error("加载投票详情失败:",u),alert("加载投票详情失败: "+u.message)}}async function B(m){m.innerHTML=`
|
|
1298
|
+
<div class="view-header">
|
|
1299
|
+
<h2>🔍 搜索</h2>
|
|
1300
|
+
</div>
|
|
1301
|
+
<div class="search-container">
|
|
1302
|
+
<div class="search-box">
|
|
1303
|
+
<input type="text" id="searchInput" placeholder="搜索消息、文档、任务...">
|
|
1304
|
+
<button class="btn-primary" id="searchBtn">搜索</button>
|
|
1305
|
+
</div>
|
|
1306
|
+
<div class="search-filters">
|
|
1307
|
+
<label>
|
|
1308
|
+
<input type="checkbox" id="filterMessages" checked> 消息
|
|
1309
|
+
</label>
|
|
1310
|
+
<label>
|
|
1311
|
+
<input type="checkbox" id="filterDocuments" checked> 文档
|
|
1312
|
+
</label>
|
|
1313
|
+
<label>
|
|
1314
|
+
<input type="checkbox" id="filterTasks" checked> 任务
|
|
1315
|
+
</label>
|
|
1316
|
+
</div>
|
|
1317
|
+
<div class="search-results" id="searchResults"></div>
|
|
1318
|
+
</div>
|
|
1319
|
+
`;const u=document.getElementById("searchInput"),g=document.getElementById("searchBtn"),y=document.getElementById("searchResults"),h=async()=>{const M=u.value.trim();if(!M){y.innerHTML='<div class="empty-state">请输入搜索关键词</div>';return}const p={messages:document.getElementById("filterMessages").checked,documents:document.getElementById("filterDocuments").checked,tasks:document.getElementById("filterTasks").checked};y.innerHTML='<div class="loading">搜索中...</div>';try{const i=[];if(p.messages&&d)try{const t=await n.getGroupMessages(d._id);t.messages&&t.messages.filter(l=>l.content.toLowerCase().includes(M.toLowerCase())).forEach(l=>{i.push({type:"message",title:`消息 - ${l.username}`,content:l.content,time:l.timestamp,group:d.name})})}catch(t){console.error("搜索消息失败:",t)}if(p.documents)try{if(d){const t=await n.getDocuments(d._id);t.documents&&t.documents.filter(l=>l.title.toLowerCase().includes(M.toLowerCase())||l.content.toLowerCase().includes(M.toLowerCase())).forEach(l=>{i.push({type:"document",title:l.title,content:l.content.substring(0,200),time:l.updatedAt,id:l._id,group:d.name})})}}catch(t){console.error("搜索文档失败:",t)}if(p.tasks&&d)try{const t=await n.getTasks(d._id);t.tasks&&t.tasks.filter(l=>l.title.toLowerCase().includes(M.toLowerCase())||l.description&&l.description.toLowerCase().includes(M.toLowerCase())).forEach(l=>{i.push({type:"task",title:l.title,content:l.description||"",time:l.updatedAt,id:l._id,status:l.status,group:d.name})})}catch(t){console.error("搜索任务失败:",t)}i.length===0?y.innerHTML='<div class="empty-state">未找到相关结果</div>':y.innerHTML=i.map(t=>`
|
|
1320
|
+
<div class="search-result-item">
|
|
1321
|
+
<div class="result-header">
|
|
1322
|
+
<span class="result-type">${{message:"💬",document:"📄",task:"📋"}[t.type]} ${t.type==="message"?"消息":t.type==="document"?"文档":"任务"}</span>
|
|
1323
|
+
<span class="result-time">${new Date(t.time).toLocaleString()}</span>
|
|
1324
|
+
</div>
|
|
1325
|
+
<h4>${$(t.title,M)}</h4>
|
|
1326
|
+
<p>${$(t.content,M)}</p>
|
|
1327
|
+
${t.group?`<span class="result-group">群组: ${t.group}</span>`:""}
|
|
1328
|
+
${t.status?`<span class="result-status">状态: ${v(t.status)}</span>`:""}
|
|
1329
|
+
</div>
|
|
1330
|
+
`).join("")}catch(i){y.innerHTML=`<div class="empty-state">搜索失败: ${i.message}</div>`}};g.addEventListener("click",h),u.addEventListener("keypress",M=>{M.key==="Enter"&&h()})}function $(m,u){if(!u)return m;const g=new RegExp(`(${u})`,"gi");return m.replace(g,"<mark>$1</mark>")}function A(m){return{document_create:"创建文档",document_update:"更新文档",document_delete:"删除文档",content_edit:"编辑内容",title_edit:"修改标题",document_permission_change:"权限修改"}[m]||m}function v(m){return{pending:"待处理",in_progress:"进行中",completed:"已完成",terminated:"已终止"}[m]||m}async function k(m){var u;if(!d){m.innerHTML='<div class="empty-state">请先选择一个群组</div>';return}try{const g=localStorage.getItem("token"),y=await fetch(`http://localhost:3000/api/knowledge/group/${d._id}`,{headers:{Authorization:`Bearer ${g}`}});if(!y.ok)throw new Error(`HTTP ${y.status}: ${y.statusText}`);const h=await y.json();console.log("知识库数据:",h);const M=((u=h.data)==null?void 0:u.knowledgeList)||[];console.log("知识库条目数量:",M.length),m.innerHTML=`
|
|
1331
|
+
<div class="view-header">
|
|
1332
|
+
<h2>📚 知识库管理 - ${d.name}</h2>
|
|
1333
|
+
<button class="btn-primary" id="createKnowledgeBtn">➕ 创建知识条目</button>
|
|
1334
|
+
</div>
|
|
1335
|
+
<div class="knowledge-grid" id="knowledgeList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 20px;"></div>
|
|
1336
|
+
<div id="knowledgeModal" class="modal hidden">
|
|
1337
|
+
<div class="modal-content">
|
|
1338
|
+
<h3 id="modalTitle">创建知识条目</h3>
|
|
1339
|
+
<form id="knowledgeForm">
|
|
1340
|
+
<div class="form-group">
|
|
1341
|
+
<label>📌 标题</label>
|
|
1342
|
+
<input type="text" name="title" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
|
|
1343
|
+
</div>
|
|
1344
|
+
<div class="form-group">
|
|
1345
|
+
<label>📝 内容</label>
|
|
1346
|
+
<textarea name="content" rows="6" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;"></textarea>
|
|
1347
|
+
</div>
|
|
1348
|
+
<div class="form-group">
|
|
1349
|
+
<label>🏷️ 标签(用逗号分隔)</label>
|
|
1350
|
+
<input type="text" name="tags" placeholder="例如: 技术,文档,教程" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
|
|
1351
|
+
</div>
|
|
1352
|
+
<div class="form-group" style="display: flex; align-items: center; gap: 10px; padding: 15px; background: var(--bg-tertiary); border-radius: 8px; margin-top: 15px;">
|
|
1353
|
+
<input type="checkbox" name="isShared" id="isSharedCheckbox" style="width: 20px; height: 20px; cursor: pointer;">
|
|
1354
|
+
<label for="isSharedCheckbox" style="margin: 0; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
1355
|
+
<span style="font-size: 18px;">🌐</span>
|
|
1356
|
+
<div>
|
|
1357
|
+
<div style="font-weight: 600; color: var(--text-primary);">共享到所有群组</div>
|
|
1358
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 2px;">开启后,此知识条目将对所有群组可见</div>
|
|
1359
|
+
</div>
|
|
1360
|
+
</label>
|
|
1361
|
+
</div>
|
|
1362
|
+
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
|
1363
|
+
<button type="submit" class="btn-primary" style="flex: 1;">保存</button>
|
|
1364
|
+
<button type="button" class="btn-secondary" id="closeKnowledgeModal" style="flex: 1;">取消</button>
|
|
1365
|
+
</div>
|
|
1366
|
+
</form>
|
|
1367
|
+
</div>
|
|
1368
|
+
</div>
|
|
1369
|
+
`;const p=document.getElementById("knowledgeList");M.length===0?p.innerHTML='<div class="empty-state" style="grid-column: 1/-1;">暂无知识条目</div>':(M.forEach(i=>{var r;const t=document.createElement("div");t.className="knowledge-card",t.style.cssText="background: var(--bg-secondary); padding: 20px; border-radius: 12px; border: 1px solid var(--border); transition: transform 0.2s, box-shadow 0.2s; position: relative;",t.innerHTML=`
|
|
1370
|
+
${i.isShared?'<div style="position: absolute; top: 15px; right: 15px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; display: flex; align-items: center; gap: 4px;"><span>🌐</span><span>已共享</span></div>':""}
|
|
1371
|
+
<h3 style="margin: 0 0 10px 0; font-size: 18px; ${i.isShared?"padding-right: 80px;":""}">${i.title}</h3>
|
|
1372
|
+
<p style="color: var(--text-secondary); margin: 0 0 15px 0; line-height: 1.6;">${i.content.substring(0,150)}${i.content.length>150?"...":""}</p>
|
|
1373
|
+
<div class="knowledge-meta" style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 10px;">
|
|
1374
|
+
<span>👤 ${((r=i.author)==null?void 0:r.username)||"未知"}</span>
|
|
1375
|
+
<span style="margin-left: 15px;">📅 ${new Date(i.createdAt).toLocaleDateString()}</span>
|
|
1376
|
+
</div>
|
|
1377
|
+
${i.tags&&i.tags.length>0?`
|
|
1378
|
+
<div class="tags" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 15px;">
|
|
1379
|
+
${i.tags.map(l=>`<span class="tag" style="background: var(--primary); color: white; padding: 4px 10px; border-radius: 12px; font-size: 12px;">${l}</span>`).join("")}
|
|
1380
|
+
</div>
|
|
1381
|
+
`:""}
|
|
1382
|
+
<div style="display: flex; gap: 10px;">
|
|
1383
|
+
<button class="btn-secondary btn-sm" data-id="${i._id}" data-action="edit" style="flex: 1;">✏️ 编辑</button>
|
|
1384
|
+
<button class="btn-danger btn-sm" data-id="${i._id}" data-action="delete" style="flex: 1;">🗑️ 删除</button>
|
|
1385
|
+
</div>
|
|
1386
|
+
`,t.onmouseenter=()=>{t.style.transform="translateY(-4px)",t.style.boxShadow="0 8px 16px rgba(0,0,0,0.1)"},t.onmouseleave=()=>{t.style.transform="translateY(0)",t.style.boxShadow="none"},p.appendChild(t)}),document.querySelectorAll('[data-action="edit"]').forEach(i=>{i.addEventListener("click",async()=>{var r;const t=M.find(l=>l._id===i.dataset.id);document.getElementById("modalTitle").textContent="编辑知识条目",document.querySelector('[name="title"]').value=t.title,document.querySelector('[name="content"]').value=t.content,document.querySelector('[name="tags"]').value=((r=t.tags)==null?void 0:r.join(", "))||"",document.getElementById("isSharedCheckbox").checked=t.isShared||!1,document.getElementById("knowledgeForm").dataset.editId=t._id,document.getElementById("knowledgeModal").classList.remove("hidden")})}),document.querySelectorAll('[data-action="download"]').forEach(i=>{i.addEventListener("click",async()=>{try{const t=await fetch(`http://localhost:3000/api/backup/download/${i.dataset.filename}`,{method:"GET",headers:{Authorization:`Bearer ${g}`}});if(!t.ok)throw new Error("下载失败");const r=await t.blob(),l=window.URL.createObjectURL(r),b=document.createElement("a");b.href=l,b.download=i.dataset.filename,document.body.appendChild(b),b.click(),window.URL.revokeObjectURL(l),document.body.removeChild(b)}catch(t){alert("下载失败: "+t.message)}})}),document.querySelectorAll('[data-action="delete"]').forEach(i=>{i.addEventListener("click",async()=>{if(confirm("确定要删除这个知识条目吗?"))try{await fetch(`http://localhost:3000/api/knowledge/${i.dataset.id}`,{method:"DELETE",headers:{Authorization:`Bearer ${g}`}}),alert("删除成功!"),await k(m)}catch(t){alert("删除失败: "+t.message)}})})),document.getElementById("createKnowledgeBtn").addEventListener("click",()=>{document.getElementById("modalTitle").textContent="创建知识条目",document.getElementById("knowledgeForm").reset(),delete document.getElementById("knowledgeForm").dataset.editId,document.getElementById("knowledgeModal").classList.remove("hidden")}),document.getElementById("closeKnowledgeModal").addEventListener("click",()=>{document.getElementById("knowledgeModal").classList.add("hidden")}),document.getElementById("knowledgeForm").addEventListener("submit",async i=>{i.preventDefault();const t=new FormData(i.target),r={title:t.get("title"),content:t.get("content"),tags:t.get("tags").split(",").map(l=>l.trim()).filter(l=>l),groupId:d._id,isShared:document.getElementById("isSharedCheckbox").checked};try{const l=i.target.dataset.editId,b=l?`http://localhost:3000/api/knowledge/${l}`:"http://localhost:3000/api/knowledge",w=await fetch(b,{method:l?"PUT":"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${g}`},body:JSON.stringify(r)});if(!w.ok){const G=await w.json();throw new Error(G.message||"操作失败")}const P=await w.json();console.log("知识库操作结果:",P),alert(l?"更新成功!":"创建成功!"),document.getElementById("knowledgeModal").classList.add("hidden"),await k(m)}catch(l){console.error("知识库操作错误:",l),alert("操作失败: "+l.message)}})}catch(g){m.innerHTML=`<div class="empty-state">加载失败: ${g.message}</div>`}}function C(m){if(!m)return"未设置";if(typeof m=="string")return{document_create:"📄 文档创建时",document_update:"✏️ 文档更新时",document_delete:"🗑️ 文档删除时",task_create:"📋 任务创建时",task_complete:"✅ 任务完成时",task_overdue:"⏰ 任务逾期时",member_join:"👥 成员加入时",group_create:"🏢 群组创建时",scheduled:"⏱️ 定时触发",manual:"🖱️ 手动触发"}[m]||m;const u=[];if(m.event){const g={document_created:"📄 文档创建",document_updated:"✏️ 文档更新",document_deleted:"🗑️ 文档删除",task_created:"📋 任务创建",task_completed:"✅ 任务完成",task_overdue:"⏰ 任务逾期",member_joined:"👥 成员加入",group_created:"🏢 群组创建",message_sent:"💬 消息发送",file_uploaded:"📎 文件上传"};u.push(g[m.event]||m.event)}if(m.conditions&&Object.keys(m.conditions).length>0){const g=[];for(const[y,h]of Object.entries(m.conditions)){const p={group:"群组",user:"用户",keyword:"关键词",status:"状态",priority:"优先级"}[y]||y;g.push(`${p}=${h}`)}g.length>0&&u.push(`(条件: ${g.join(", ")})`)}return m.schedule&&u.push(`⏱️ 定时: ${m.schedule}`),u.length>0?u.join(" "):"自定义触发条件"}async function D(m){var u;if(!d){m.innerHTML='<div class="empty-state">请先选择一个群组</div>';return}try{const g=localStorage.getItem("token"),M=((u=(await(await fetch(`http://localhost:3000/api/workflows/group/${d._id}`,{headers:{Authorization:`Bearer ${g}`}})).json()).data)==null?void 0:u.workflows)||[];m.innerHTML=`
|
|
1387
|
+
<div class="view-header">
|
|
1388
|
+
<h2>⚙️ 工作流管理 - ${d.name}</h2>
|
|
1389
|
+
<button class="btn-primary" id="createWorkflowBtn">➕ 创建工作流</button>
|
|
1390
|
+
</div>
|
|
1391
|
+
<div class="workflow-grid" id="workflowList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 20px;"></div>
|
|
1392
|
+
<div id="workflowModal" class="modal hidden">
|
|
1393
|
+
<div class="modal-content" style="max-width: 700px;">
|
|
1394
|
+
<div class="modal-header" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 20px; border-radius: 12px 12px 0 0; margin: -20px -20px 20px -20px;">
|
|
1395
|
+
<h3 style="margin: 0; display: flex; align-items: center; gap: 10px;">
|
|
1396
|
+
<span style="font-size: 24px;">⚙️</span>
|
|
1397
|
+
<span>创建工作流</span>
|
|
1398
|
+
</h3>
|
|
1399
|
+
</div>
|
|
1400
|
+
<form id="workflowForm">
|
|
1401
|
+
<div class="form-group" style="margin-bottom: 20px;">
|
|
1402
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary);">⚙️ 工作流名称</label>
|
|
1403
|
+
<input type="text" name="name" required placeholder="例如:文档审批流程" style="width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 14px; transition: border-color 0.2s;">
|
|
1404
|
+
</div>
|
|
1405
|
+
<div class="form-group" style="margin-bottom: 20px;">
|
|
1406
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary);">📝 描述</label>
|
|
1407
|
+
<textarea name="description" rows="3" placeholder="描述这个工作流的用途..." style="width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 14px; resize: vertical; transition: border-color 0.2s;"></textarea>
|
|
1408
|
+
</div>
|
|
1409
|
+
<div class="form-group" style="margin-bottom: 20px;">
|
|
1410
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary);">⚡ 触发条件</label>
|
|
1411
|
+
<select name="trigger" style="width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 14px; cursor: pointer; transition: border-color 0.2s;">
|
|
1412
|
+
<option value="document_create">📄 文档创建时</option>
|
|
1413
|
+
<option value="document_update">✏️ 文档更新时</option>
|
|
1414
|
+
<option value="document_delete">🗑️ 文档删除时</option>
|
|
1415
|
+
<option value="task_create">📋 任务创建时</option>
|
|
1416
|
+
<option value="task_complete">✅ 任务完成时</option>
|
|
1417
|
+
<option value="task_overdue">⏰ 任务逾期时</option>
|
|
1418
|
+
<option value="member_join">👥 成员加入时</option>
|
|
1419
|
+
<option value="group_create">🏢 群组创建时</option>
|
|
1420
|
+
<option value="scheduled">⏱️ 定时触发</option>
|
|
1421
|
+
<option value="manual">🖱️ 手动触发</option>
|
|
1422
|
+
</select>
|
|
1423
|
+
</div>
|
|
1424
|
+
<div class="form-group" style="margin-bottom: 20px;">
|
|
1425
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary);">🎯 执行操作</label>
|
|
1426
|
+
<select name="action" style="width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 14px; cursor: pointer; transition: border-color 0.2s;">
|
|
1427
|
+
<option value="send_notification">📧 发送通知</option>
|
|
1428
|
+
<option value="send_email">✉️ 发送邮件</option>
|
|
1429
|
+
<option value="create_task">📋 创建任务</option>
|
|
1430
|
+
<option value="update_status">🔄 更新状态</option>
|
|
1431
|
+
<option value="assign_permission">🔐 分配权限</option>
|
|
1432
|
+
<option value="backup_data">💾 备份数据</option>
|
|
1433
|
+
<option value="generate_report">📊 生成报告</option>
|
|
1434
|
+
<option value="call_api">🔗 调用外部API</option>
|
|
1435
|
+
</select>
|
|
1436
|
+
</div>
|
|
1437
|
+
<div class="form-group" style="margin-bottom: 20px;">
|
|
1438
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary);">⚙️ 操作参数 (JSON格式)</label>
|
|
1439
|
+
<textarea name="actionParams" rows="4" placeholder='{"message": "任务已完成", "recipients": ["user1"]}' style="width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 13px; font-family: monospace; resize: vertical; transition: border-color 0.2s;"></textarea>
|
|
1440
|
+
<div style="margin-top: 8px; font-size: 12px; color: var(--text-tertiary);">
|
|
1441
|
+
💡 提示:使用 JSON 格式配置操作参数
|
|
1442
|
+
</div>
|
|
1443
|
+
</div>
|
|
1444
|
+
<div style="display: flex; gap: 12px; margin-top: 25px;">
|
|
1445
|
+
<button type="submit" class="btn-primary" style="flex: 1; padding: 12px; border-radius: 8px; font-weight: 600; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border: none; cursor: pointer; transition: transform 0.2s;">创建</button>
|
|
1446
|
+
<button type="button" class="btn-secondary" id="closeWorkflowModal" style="flex: 1; padding: 12px; border-radius: 8px; font-weight: 600;">取消</button>
|
|
1447
|
+
</div>
|
|
1448
|
+
</form>
|
|
1449
|
+
</div>
|
|
1450
|
+
</div>
|
|
1451
|
+
`;const p=document.getElementById("workflowList");M.length===0?p.innerHTML='<div class="empty-state" style="grid-column: 1/-1;">暂无工作流</div>':(M.forEach(i=>{const t=document.createElement("div");t.className="workflow-card",t.style.cssText="background: var(--bg-secondary); padding: 20px; border-radius: 12px; border: 1px solid var(--border); transition: transform 0.2s, box-shadow 0.2s;";const r=i.status==="active";t.innerHTML=`
|
|
1452
|
+
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
|
|
1453
|
+
<h3 style="margin: 0; font-size: 18px;">${i.name}</h3>
|
|
1454
|
+
<span style="padding: 4px 10px; border-radius: 12px; font-size: 12px; background: ${r?"var(--success)":"var(--warning)"}; color: white;">${r?"✅ 活跃":"⏸️ 暂停"}</span>
|
|
1455
|
+
</div>
|
|
1456
|
+
<p style="color: var(--text-secondary); margin: 10px 0; line-height: 1.6;">${i.description||"无描述"}</p>
|
|
1457
|
+
<div class="workflow-meta" style="font-size: 12px; color: var(--text-tertiary); margin: 10px 0;">
|
|
1458
|
+
<span>🔔 触发条件: ${C(i.trigger)}</span>
|
|
1459
|
+
</div>
|
|
1460
|
+
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
|
1461
|
+
<button class="btn-secondary btn-sm" data-id="${i._id}" data-action="toggle" style="flex: 1;">
|
|
1462
|
+
${r?"⏸️ 暂停":"▶️ 启用"}
|
|
1463
|
+
</button>
|
|
1464
|
+
<button class="btn-danger btn-sm" data-id="${i._id}" data-action="delete" style="flex: 1;">🗑️ 删除</button>
|
|
1465
|
+
</div>
|
|
1466
|
+
`,t.onmouseenter=()=>{t.style.transform="translateY(-4px)",t.style.boxShadow="0 8px 16px rgba(0,0,0,0.1)"},t.onmouseleave=()=>{t.style.transform="translateY(0)",t.style.boxShadow="none"},p.appendChild(t)}),document.querySelectorAll('[data-action="toggle"]').forEach(i=>{i.addEventListener("click",async()=>{try{await fetch(`http://localhost:3000/api/workflows/${i.dataset.id}/toggle`,{method:"POST",headers:{Authorization:`Bearer ${g}`}}),await D(m)}catch(t){alert("操作失败: "+t.message)}})}),document.querySelectorAll('[data-action="delete"]').forEach(i=>{i.addEventListener("click",async()=>{if(confirm("确定要删除这个工作流吗?"))try{await fetch(`http://localhost:3000/api/workflows/${i.dataset.id}`,{method:"DELETE",headers:{Authorization:`Bearer ${g}`}}),alert("删除成功!"),await D(m)}catch(t){alert("删除失败: "+t.message)}})})),document.getElementById("createWorkflowBtn").addEventListener("click",()=>{document.getElementById("workflowModal").classList.remove("hidden")}),document.getElementById("closeWorkflowModal").addEventListener("click",()=>{document.getElementById("workflowModal").classList.add("hidden")}),document.getElementById("workflowForm").addEventListener("submit",async i=>{i.preventDefault();const t=new FormData(i.target),r={name:t.get("name"),description:t.get("description"),trigger:t.get("trigger"),groupId:d._id,actions:[]};try{await fetch("http://localhost:3000/api/workflows",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${g}`},body:JSON.stringify(r)}),alert("创建成功!"),document.getElementById("workflowModal").classList.add("hidden"),await D(m)}catch(l){alert("创建失败: "+l.message)}})}catch(g){m.innerHTML=`<div class="empty-state">加载失败: ${g.message}</div>`}}async function T(m){var u;try{const g=localStorage.getItem("token"),M=((u=(await(await fetch("http://localhost:3000/api/backup/list",{headers:{Authorization:`Bearer ${g}`}})).json()).data)==null?void 0:u.backups)||[];m.innerHTML=`
|
|
1467
|
+
<div class="view-header">
|
|
1468
|
+
<h2>💾 备份管理</h2>
|
|
1469
|
+
<button class="btn-primary" id="createBackupBtn">➕ 创建备份</button>
|
|
1470
|
+
</div>
|
|
1471
|
+
<div class="backup-grid" id="backupList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 20px;"></div>
|
|
1472
|
+
`;const p=document.getElementById("backupList");M.length===0?p.innerHTML='<div class="empty-state" style="grid-column: 1/-1;">暂无备份</div>':(M.forEach(i=>{const t=document.createElement("div");t.className="backup-card",t.style.cssText="background: var(--bg-secondary); padding: 20px; border-radius: 12px; border: 1px solid var(--border); transition: transform 0.2s, box-shadow 0.2s;";const r=(i.size/1024/1024).toFixed(2);t.innerHTML=`
|
|
1473
|
+
<div style="display: flex; align-items: center; gap: 15px; margin-bottom: 15px;">
|
|
1474
|
+
<div style="font-size: 48px;">📦</div>
|
|
1475
|
+
<div style="flex: 1;">
|
|
1476
|
+
<h3 style="margin: 0 0 5px 0; font-size: 16px;">${i.filename}</h3>
|
|
1477
|
+
<p style="margin: 0; font-size: 14px; color: var(--text-secondary);">${r} MB</p>
|
|
1478
|
+
</div>
|
|
1479
|
+
</div>
|
|
1480
|
+
<div class="backup-meta" style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 15px;">
|
|
1481
|
+
<span>📅 ${new Date(i.createdAt).toLocaleString()}</span>
|
|
1482
|
+
</div>
|
|
1483
|
+
<div style="display: flex; gap: 10px;">
|
|
1484
|
+
<button class="btn-primary btn-sm" data-backup-name="${i.name}" data-filename="${i.filename}" data-action="download" style="flex: 1;">⬇️ 下载</button>
|
|
1485
|
+
<button class="btn-danger btn-sm" data-backup-name="${i.name}" data-action="delete" style="flex: 1;">🗑️ 删除</button>
|
|
1486
|
+
</div>
|
|
1487
|
+
`,t.onmouseenter=()=>{t.style.transform="translateY(-4px)",t.style.boxShadow="0 8px 16px rgba(0,0,0,0.1)"},t.onmouseleave=()=>{t.style.transform="translateY(0)",t.style.boxShadow="none"},p.appendChild(t)}),document.querySelectorAll('[data-action="download"]').forEach(i=>{i.addEventListener("click",async()=>{try{const t=i.dataset.backupName,r=i.dataset.filename,l=localStorage.getItem("token");console.log("开始下载备份:",{backupName:t,filename:r});const b=await fetch(`http://localhost:3000/api/backup/download/${t}`,{method:"GET",headers:{Authorization:`Bearer ${l}`}});if(!b.ok)throw new Error(`下载失败: ${b.status} ${b.statusText}`);const E=await b.blob();console.log("文件下载成功,大小:",E.size,"bytes");const w=window.URL.createObjectURL(E),P=document.createElement("a");P.href=w,P.download=r,P.style.display="none",document.body.appendChild(P),P.click(),setTimeout(()=>{document.body.removeChild(P),window.URL.revokeObjectURL(w)},100);const G=i.textContent;i.textContent="✅ 下载成功",i.disabled=!0,setTimeout(()=>{i.textContent=G,i.disabled=!1},2e3)}catch(t){console.error("下载失败:",t),alert("下载失败: "+t.message),i.textContent="⬇️ 下载",i.disabled=!1}})}),document.querySelectorAll('[data-action="delete"]').forEach(i=>{i.addEventListener("click",async()=>{if(confirm("确定要删除这个备份吗?"))try{const t=i.dataset.backupName;console.log("删除备份:",t);const r=await fetch(`http://localhost:3000/api/backup/${t}`,{method:"DELETE",headers:{Authorization:`Bearer ${g}`}});if(!r.ok)throw new Error(`删除失败: ${r.status}`);alert("删除成功!"),await T(m)}catch(t){console.error("删除失败:",t),alert("删除失败: "+t.message)}})})),document.getElementById("createBackupBtn").addEventListener("click",async()=>{if(confirm("确定要创建新备份吗?这可能需要一些时间。")){const i=document.getElementById("createBackupBtn");i.disabled=!0,i.textContent="⏳ 创建中...";try{await fetch("http://localhost:3000/api/backup/create",{method:"POST",headers:{Authorization:`Bearer ${g}`}}),alert("备份创建成功!"),await T(m)}catch(t){alert("创建失败: "+t.message)}finally{i.disabled=!1,i.textContent="➕ 创建备份"}}})}catch(g){m.innerHTML=`<div class="empty-state">加载失败: ${g.message}</div>`}}async function z(m){m.innerHTML='<div class="empty-state">AI助手功能开发中...</div>'}async function W(m){m.innerHTML=`
|
|
1488
|
+
<div class="view-header">
|
|
1489
|
+
<h2>📤 数据导出</h2>
|
|
1490
|
+
</div>
|
|
1491
|
+
<div class="export-container" style="padding: 20px;">
|
|
1492
|
+
<div class="export-options" style="background: var(--bg-secondary); padding: 30px; border-radius: 12px; margin-bottom: 20px;">
|
|
1493
|
+
<h3 style="margin: 0 0 20px 0;">选择导出内容</h3>
|
|
1494
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; margin-bottom: 25px;">
|
|
1495
|
+
<label style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg); border-radius: 8px; cursor: pointer;">
|
|
1496
|
+
<input type="checkbox" id="exportGroups" checked style="width: 18px; height: 18px;">
|
|
1497
|
+
<span>👥 群组信息</span>
|
|
1498
|
+
</label>
|
|
1499
|
+
<label style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg); border-radius: 8px; cursor: pointer;">
|
|
1500
|
+
<input type="checkbox" id="exportDocuments" checked style="width: 18px; height: 18px;">
|
|
1501
|
+
<span>📄 文档</span>
|
|
1502
|
+
</label>
|
|
1503
|
+
<label style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg); border-radius: 8px; cursor: pointer;">
|
|
1504
|
+
<input type="checkbox" id="exportTasks" checked style="width: 18px; height: 18px;">
|
|
1505
|
+
<span>📋 任务</span>
|
|
1506
|
+
</label>
|
|
1507
|
+
<label style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg); border-radius: 8px; cursor: pointer;">
|
|
1508
|
+
<input type="checkbox" id="exportMessages" checked style="width: 18px; height: 18px;">
|
|
1509
|
+
<span>💬 消息</span>
|
|
1510
|
+
</label>
|
|
1511
|
+
<label style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg); border-radius: 8px; cursor: pointer;">
|
|
1512
|
+
<input type="checkbox" id="exportFiles" style="width: 18px; height: 18px;">
|
|
1513
|
+
<span>📎 文件</span>
|
|
1514
|
+
</label>
|
|
1515
|
+
</div>
|
|
1516
|
+
|
|
1517
|
+
<h3 style="margin: 25px 0 15px 0;">导出格式</h3>
|
|
1518
|
+
<select id="exportFormat" style="width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 25px;">
|
|
1519
|
+
<option value="json">JSON</option>
|
|
1520
|
+
<option value="csv">CSV</option>
|
|
1521
|
+
<option value="excel">Excel</option>
|
|
1522
|
+
</select>
|
|
1523
|
+
|
|
1524
|
+
<button class="btn-primary" id="exportBtn" style="width: 100%; padding: 15px; font-size: 16px;">🚀 开始导出</button>
|
|
1525
|
+
</div>
|
|
1526
|
+
|
|
1527
|
+
<div class="export-history" style="background: var(--bg-secondary); padding: 30px; border-radius: 12px;">
|
|
1528
|
+
<h3 style="margin: 0 0 20px 0;">📜 导出历史</h3>
|
|
1529
|
+
<div id="historyList">加载中...</div>
|
|
1530
|
+
</div>
|
|
1531
|
+
</div>
|
|
1532
|
+
`;try{const u=localStorage.getItem("token"),y=await(await fetch("http://localhost:3000/api/export/history",{headers:{Authorization:`Bearer ${u}`}})).json(),h=document.getElementById("historyList");y.exports&&y.exports.length>0?h.innerHTML=y.exports.map(M=>`
|
|
1533
|
+
<div class="export-item" style="display: flex; justify-content: space-between; align-items: center; padding: 15px; background: var(--bg); border-radius: 8px; margin-bottom: 10px;">
|
|
1534
|
+
<div>
|
|
1535
|
+
<div style="font-weight: 600; margin-bottom: 5px;">📦 ${M.format.toUpperCase()} 导出</div>
|
|
1536
|
+
<div style="font-size: 12px; color: var(--text-secondary);">📅 ${new Date(M.createdAt).toLocaleString()}</div>
|
|
1537
|
+
</div>
|
|
1538
|
+
<a href="http://localhost:3000/api/export/download/${M.filename}" class="btn-sm btn-primary" download style="text-decoration: none;">⬇️ 下载</a>
|
|
1539
|
+
</div>
|
|
1540
|
+
`).join(""):h.innerHTML='<div class="empty-state">暂无导出记录</div>'}catch{document.getElementById("historyList").innerHTML='<div class="empty-state">加载失败</div>'}document.getElementById("exportBtn").addEventListener("click",async()=>{const u={groups:document.getElementById("exportGroups").checked,documents:document.getElementById("exportDocuments").checked,tasks:document.getElementById("exportTasks").checked,messages:document.getElementById("exportMessages").checked,files:document.getElementById("exportFiles").checked,format:document.getElementById("exportFormat").value},g=document.getElementById("exportBtn");g.disabled=!0,g.textContent="⏳ 导出中...";try{const y=localStorage.getItem("token"),h=await fetch("http://localhost:3000/api/export",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${y}`},body:JSON.stringify(u)});if(h.ok){const M=await h.blob(),p=window.URL.createObjectURL(M),i=document.createElement("a");i.href=p,i.download=`export-${Date.now()}.${u.format}`,i.click(),alert("导出成功!"),await W(m)}else throw new Error("导出失败")}catch(y){alert("导出失败: "+y.message)}finally{g.disabled=!1,g.textContent="🚀 开始导出"}})}async function F(m){if(!d){m.innerHTML='<div class="empty-state">请先选择一个群组</div>';return}m.innerHTML=`
|
|
1541
|
+
<div class="view-header">
|
|
1542
|
+
<h2>🎨 协作白板 - ${d.name}</h2>
|
|
1543
|
+
<div style="display: flex; gap: 10px;">
|
|
1544
|
+
<button class="btn-secondary" id="clearCanvas">清空画布</button>
|
|
1545
|
+
<button class="btn-primary" id="saveCanvas">保存白板</button>
|
|
1546
|
+
</div>
|
|
1547
|
+
</div>
|
|
1548
|
+
<div class="whiteboard-container">
|
|
1549
|
+
<div class="whiteboard-toolbar">
|
|
1550
|
+
<button class="tool-btn active" data-tool="pen">✏️ 画笔</button>
|
|
1551
|
+
<button class="tool-btn" data-tool="eraser">🧹 橡皮擦</button>
|
|
1552
|
+
<button class="tool-btn" data-tool="text">📝 文字</button>
|
|
1553
|
+
<button class="tool-btn" data-tool="shape">⬜ 形状</button>
|
|
1554
|
+
<input type="color" id="colorPicker" value="#000000" title="颜色">
|
|
1555
|
+
<input type="range" id="brushSize" min="1" max="20" value="3" title="画笔大小">
|
|
1556
|
+
</div>
|
|
1557
|
+
<canvas id="whiteboard" width="1200" height="600" style="border: 1px solid var(--border); background: white; cursor: crosshair;"></canvas>
|
|
1558
|
+
<div class="whiteboard-users" id="whiteboardUsers">
|
|
1559
|
+
<span class="user-badge">👤 ${a.username}</span>
|
|
1560
|
+
</div>
|
|
1561
|
+
</div>
|
|
1562
|
+
`;const u=document.getElementById("whiteboard"),g=u.getContext("2d");let y=!1,h="pen",M="#000000",p=3;document.querySelectorAll(".tool-btn").forEach(r=>{r.addEventListener("click",()=>{document.querySelectorAll(".tool-btn").forEach(l=>l.classList.remove("active")),r.classList.add("active"),h=r.dataset.tool})}),document.getElementById("colorPicker").addEventListener("change",r=>{M=r.target.value}),document.getElementById("brushSize").addEventListener("input",r=>{p=r.target.value});let i=0,t=0;u.addEventListener("mousedown",r=>{y=!0;const l=u.getBoundingClientRect();i=r.clientX-l.left,t=r.clientY-l.top}),u.addEventListener("mousemove",r=>{if(!y)return;const l=u.getBoundingClientRect(),b=r.clientX-l.left,E=r.clientY-l.top;g.beginPath(),g.moveTo(i,t),g.lineTo(b,E),g.strokeStyle=h==="eraser"?"#ffffff":M,g.lineWidth=p,g.lineCap="round",g.stroke(),i=b,t=E,e.sendWhiteboardData(d._id,{tool:h,color:M,size:p,from:{x:i,y:t},to:{x:b,y:E}})}),u.addEventListener("mouseup",()=>{y=!1}),u.addEventListener("mouseleave",()=>{y=!1}),document.getElementById("clearCanvas").addEventListener("click",()=>{confirm("确定要清空画布吗?")&&g.clearRect(0,0,u.width,u.height)}),document.getElementById("saveCanvas").addEventListener("click",()=>{const r=u.toDataURL("image/png"),l=document.createElement("a");l.download=`whiteboard-${Date.now()}.png`,l.href=r,l.click(),alert("白板已保存!")}),e.on("whiteboard_draw",r=>{r.groupId===d._id&&r.userId!==s&&(g.beginPath(),g.moveTo(r.from.x,r.from.y),g.lineTo(r.to.x,r.to.y),g.strokeStyle=r.tool==="eraser"?"#ffffff":r.color,g.lineWidth=r.size,g.lineCap="round",g.stroke())})}async function re(m){m.innerHTML=`
|
|
1563
|
+
<div class="view-header" style="margin-bottom: 30px;">
|
|
1564
|
+
<h2 style="display: flex; align-items: center; gap: 12px; font-size: 28px;">
|
|
1565
|
+
<span style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">⚙️ 设置中心</span>
|
|
1566
|
+
</h2>
|
|
1567
|
+
<p style="color: var(--text-tertiary); margin-top: 8px;">管理您的个人偏好和系统配置</p>
|
|
1568
|
+
</div>
|
|
1569
|
+
<div class="settings-container" style="display: grid; gap: 24px; max-width: 900px;">
|
|
1570
|
+
<!-- 个人设置卡片 -->
|
|
1571
|
+
<div class="settings-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
|
|
1572
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 24px;">
|
|
1573
|
+
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">👤</div>
|
|
1574
|
+
<h3 style="margin: 0; font-size: 20px; font-weight: 600;">个人设置</h3>
|
|
1575
|
+
</div>
|
|
1576
|
+
<div class="setting-item" style="margin-bottom: 20px;">
|
|
1577
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary); font-size: 14px;">👤 用户名</label>
|
|
1578
|
+
<input type="text" value="${a.username}" disabled style="width: 100%; padding: 12px 16px; border: 2px solid var(--border); border-radius: 10px; font-size: 14px; background: var(--bg-tertiary); cursor: not-allowed;">
|
|
1579
|
+
</div>
|
|
1580
|
+
<div class="setting-item" style="margin-bottom: 20px;">
|
|
1581
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary); font-size: 14px;">📧 邮箱</label>
|
|
1582
|
+
<input type="email" id="userEmail" value="${a.email||""}" placeholder="请输入邮箱地址" style="width: 100%; padding: 12px 16px; border: 2px solid var(--border); border-radius: 10px; font-size: 14px; transition: border-color 0.2s;">
|
|
1583
|
+
</div>
|
|
1584
|
+
<div class="setting-item">
|
|
1585
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary); font-size: 14px;">🔐 密码</label>
|
|
1586
|
+
<button class="btn-secondary" id="changePasswordBtn" style="padding: 10px 20px; border-radius: 8px; font-weight: 600; transition: all 0.2s;">修改密码</button>
|
|
1587
|
+
</div>
|
|
1588
|
+
</div>
|
|
1589
|
+
|
|
1590
|
+
<!-- 通知设置卡片 -->
|
|
1591
|
+
<div class="settings-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
|
|
1592
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 24px;">
|
|
1593
|
+
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">🔔</div>
|
|
1594
|
+
<h3 style="margin: 0; font-size: 20px; font-weight: 600;">通知设置</h3>
|
|
1595
|
+
</div>
|
|
1596
|
+
<div class="setting-item" style="margin-bottom: 16px;">
|
|
1597
|
+
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px; border-radius: 8px; transition: background 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'" onmouseleave="this.style.background='transparent'">
|
|
1598
|
+
<input type="checkbox" id="emailNotifications" checked style="width: 20px; height: 20px; cursor: pointer;">
|
|
1599
|
+
<div>
|
|
1600
|
+
<div style="font-weight: 600; color: var(--text-primary);">📧 邮件通知</div>
|
|
1601
|
+
<div style="font-size: 12px; color: var(--text-tertiary); margin-top: 2px;">接收重要事件的邮件提醒</div>
|
|
1602
|
+
</div>
|
|
1603
|
+
</label>
|
|
1604
|
+
</div>
|
|
1605
|
+
<div class="setting-item" style="margin-bottom: 16px;">
|
|
1606
|
+
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px; border-radius: 8px; transition: background 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'" onmouseleave="this.style.background='transparent'">
|
|
1607
|
+
<input type="checkbox" id="desktopNotifications" checked style="width: 20px; height: 20px; cursor: pointer;">
|
|
1608
|
+
<div>
|
|
1609
|
+
<div style="font-weight: 600; color: var(--text-primary);">🖥️ 桌面通知</div>
|
|
1610
|
+
<div style="font-size: 12px; color: var(--text-tertiary); margin-top: 2px;">在桌面显示通知消息</div>
|
|
1611
|
+
</div>
|
|
1612
|
+
</label>
|
|
1613
|
+
</div>
|
|
1614
|
+
<div class="setting-item">
|
|
1615
|
+
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px; border-radius: 8px; transition: background 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'" onmouseleave="this.style.background='transparent'">
|
|
1616
|
+
<input type="checkbox" id="soundNotifications" checked style="width: 20px; height: 20px; cursor: pointer;">
|
|
1617
|
+
<div>
|
|
1618
|
+
<div style="font-weight: 600; color: var(--text-primary);">🔊 声音提示</div>
|
|
1619
|
+
<div style="font-size: 12px; color: var(--text-tertiary); margin-top: 2px;">播放通知提示音</div>
|
|
1620
|
+
</div>
|
|
1621
|
+
</label>
|
|
1622
|
+
</div>
|
|
1623
|
+
</div>
|
|
1624
|
+
|
|
1625
|
+
<!-- 系统设置卡片 -->
|
|
1626
|
+
<div class="settings-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
|
|
1627
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 24px;">
|
|
1628
|
+
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">🎨</div>
|
|
1629
|
+
<h3 style="margin: 0; font-size: 20px; font-weight: 600;">系统设置</h3>
|
|
1630
|
+
</div>
|
|
1631
|
+
<div class="setting-item" style="margin-bottom: 20px;">
|
|
1632
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary); font-size: 14px;">🎨 主题</label>
|
|
1633
|
+
<select id="themeSelect" style="width: 100%; padding: 12px 16px; border: 2px solid var(--border); border-radius: 10px; font-size: 14px; cursor: pointer; transition: border-color 0.2s;">
|
|
1634
|
+
<option value="dark">🌙 深色模式(默认紫色)</option>
|
|
1635
|
+
<option value="blue">💙 深色蓝色</option>
|
|
1636
|
+
<option value="green">💚 深色绿色</option>
|
|
1637
|
+
<option value="orange">🧡 深色橙色</option>
|
|
1638
|
+
<option value="pink">💗 深色粉色</option>
|
|
1639
|
+
<option value="light">☀️ 浅色模式</option>
|
|
1640
|
+
</select>
|
|
1641
|
+
</div>
|
|
1642
|
+
<div class="setting-item">
|
|
1643
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary); font-size: 14px;">🌐 语言</label>
|
|
1644
|
+
<select id="languageSelect" style="width: 100%; padding: 12px 16px; border: 2px solid var(--border); border-radius: 10px; font-size: 14px; cursor: pointer; transition: border-color 0.2s;">
|
|
1645
|
+
<option value="zh-CN" selected>🇨🇳 简体中文</option>
|
|
1646
|
+
<option value="en-US">🇺🇸 English</option>
|
|
1647
|
+
<option value="ja-JP">🇯🇵 日本語</option>
|
|
1648
|
+
</select>
|
|
1649
|
+
</div>
|
|
1650
|
+
</div>
|
|
1651
|
+
|
|
1652
|
+
<!-- 保存按钮 -->
|
|
1653
|
+
<div class="settings-actions" style="display: flex; gap: 12px; justify-content: flex-end;">
|
|
1654
|
+
<button class="btn-secondary" id="resetSettingsBtn" style="padding: 12px 24px; border-radius: 10px; font-weight: 600; transition: all 0.2s;">重置</button>
|
|
1655
|
+
<button class="btn-primary" id="saveSettingsBtn" style="padding: 12px 32px; border-radius: 10px; font-weight: 600; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; cursor: pointer; transition: transform 0.2s;">💾 保存设置</button>
|
|
1656
|
+
</div>
|
|
1657
|
+
</div>
|
|
1658
|
+
`,document.querySelectorAll(".settings-card").forEach(g=>{g.addEventListener("mouseenter",()=>{g.style.transform="translateY(-4px)",g.style.boxShadow="0 8px 24px rgba(0,0,0,0.12)"}),g.addEventListener("mouseleave",()=>{g.style.transform="translateY(0)",g.style.boxShadow="0 2px 8px rgba(0,0,0,0.05)"})}),document.querySelectorAll('input[type="email"], select').forEach(g=>{g.addEventListener("focus",()=>{g.style.borderColor="var(--primary)"}),g.addEventListener("blur",()=>{g.style.borderColor="var(--border)"})}),document.getElementById("changePasswordBtn").addEventListener("click",()=>{prompt("请输入新密码:")&&alert("密码修改功能开发中...")}),document.getElementById("resetSettingsBtn").addEventListener("click",()=>{confirm("确定要重置所有设置吗?")&&location.reload()}),document.getElementById("saveSettingsBtn").addEventListener("click",()=>{const g={email:document.getElementById("userEmail").value,emailNotifications:document.getElementById("emailNotifications").checked,desktopNotifications:document.getElementById("desktopNotifications").checked,soundNotifications:document.getElementById("soundNotifications").checked,theme:document.getElementById("themeSelect").value,language:document.getElementById("languageSelect").value};localStorage.setItem("userSettings",JSON.stringify(g)),X(g.theme),alert("✅ 设置已保存!")}),document.getElementById("themeSelect").addEventListener("change",g=>{X(g.target.value)});const u=localStorage.getItem("userSettings");if(u){const g=JSON.parse(u);g.email&&(document.getElementById("userEmail").value=g.email),document.getElementById("emailNotifications").checked=g.emailNotifications!==!1,document.getElementById("desktopNotifications").checked=g.desktopNotifications!==!1,document.getElementById("soundNotifications").checked=g.soundNotifications!==!1,g.theme&&(document.getElementById("themeSelect").value=g.theme,X(g.theme)),g.language&&(document.getElementById("languageSelect").value=g.language)}}function X(m){const u=document.documentElement,g={dark:{primary:"#6366f1",primaryDark:"#4f46e5",secondary:"#8b5cf6",bgDark:"#0f172a",bgCard:"#1e293b",bgHover:"#334155",textPrimary:"#f1f5f9",textSecondary:"#94a3b8",border:"#334155"},blue:{primary:"#3b82f6",primaryDark:"#2563eb",secondary:"#06b6d4",bgDark:"#0c1222",bgCard:"#1a2332",bgHover:"#2a3442",textPrimary:"#e0f2fe",textSecondary:"#7dd3fc",border:"#2a3442"},green:{primary:"#10b981",primaryDark:"#059669",secondary:"#34d399",bgDark:"#0a1f1a",bgCard:"#1a2f2a",bgHover:"#2a3f3a",textPrimary:"#d1fae5",textSecondary:"#6ee7b7",border:"#2a3f3a"},orange:{primary:"#f59e0b",primaryDark:"#d97706",secondary:"#fb923c",bgDark:"#1f1a0a",bgCard:"#2f2a1a",bgHover:"#3f3a2a",textPrimary:"#fef3c7",textSecondary:"#fcd34d",border:"#3f3a2a"},pink:{primary:"#ec4899",primaryDark:"#db2777",secondary:"#f472b6",bgDark:"#1f0a1a",bgCard:"#2f1a2a",bgHover:"#3f2a3a",textPrimary:"#fce7f3",textSecondary:"#f9a8d4",border:"#3f2a3a"},light:{primary:"#6366f1",primaryDark:"#4f46e5",secondary:"#8b5cf6",bgDark:"#ffffff",bgCard:"#f8fafc",bgHover:"#e2e8f0",textPrimary:"#0f172a",textSecondary:"#475569",border:"#cbd5e1"}},y=g[m]||g.dark;u.style.setProperty("--primary",y.primary),u.style.setProperty("--primary-dark",y.primaryDark),u.style.setProperty("--secondary",y.secondary),u.style.setProperty("--bg-dark",y.bgDark),u.style.setProperty("--bg-card",y.bgCard),u.style.setProperty("--bg-hover",y.bgHover),u.style.setProperty("--text-primary",y.textPrimary),u.style.setProperty("--text-secondary",y.textSecondary),u.style.setProperty("--border",y.border),localStorage.setItem("currentTheme",m)}async function le(m){m.innerHTML=`
|
|
1659
|
+
<div class="view-header" style="margin-bottom: 30px;">
|
|
1660
|
+
<h2 style="display: flex; align-items: center; gap: 12px; font-size: 28px;">
|
|
1661
|
+
<span style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">❓ 帮助中心</span>
|
|
1662
|
+
</h2>
|
|
1663
|
+
<p style="color: var(--text-tertiary); margin-top: 8px;">快速找到您需要的帮助和支持</p>
|
|
1664
|
+
</div>
|
|
1665
|
+
|
|
1666
|
+
<div class="help-container" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 24px; max-width: 1200px;">
|
|
1667
|
+
<!-- 快速开始卡片 -->
|
|
1668
|
+
<div class="help-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
|
|
1669
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
|
|
1670
|
+
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">📖</div>
|
|
1671
|
+
<h3 style="margin: 0; font-size: 20px; font-weight: 600;">快速开始</h3>
|
|
1672
|
+
</div>
|
|
1673
|
+
<ul style="list-style: none; padding: 0; margin: 0;">
|
|
1674
|
+
<li style="margin-bottom: 12px;">
|
|
1675
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1676
|
+
<span style="color: var(--primary);">▶</span>
|
|
1677
|
+
<span>如何创建群组?</span>
|
|
1678
|
+
</a>
|
|
1679
|
+
</li>
|
|
1680
|
+
<li style="margin-bottom: 12px;">
|
|
1681
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1682
|
+
<span style="color: var(--primary);">▶</span>
|
|
1683
|
+
<span>如何邀请成员?</span>
|
|
1684
|
+
</a>
|
|
1685
|
+
</li>
|
|
1686
|
+
<li style="margin-bottom: 12px;">
|
|
1687
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1688
|
+
<span style="color: var(--primary);">▶</span>
|
|
1689
|
+
<span>如何创建文档?</span>
|
|
1690
|
+
</a>
|
|
1691
|
+
</li>
|
|
1692
|
+
<li>
|
|
1693
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1694
|
+
<span style="color: var(--primary);">▶</span>
|
|
1695
|
+
<span>如何使用协作白板?</span>
|
|
1696
|
+
</a>
|
|
1697
|
+
</li>
|
|
1698
|
+
</ul>
|
|
1699
|
+
</div>
|
|
1700
|
+
|
|
1701
|
+
<!-- 功能说明卡片 -->
|
|
1702
|
+
<div class="help-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
|
|
1703
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
|
|
1704
|
+
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">🔧</div>
|
|
1705
|
+
<h3 style="margin: 0; font-size: 20px; font-weight: 600;">功能说明</h3>
|
|
1706
|
+
</div>
|
|
1707
|
+
<ul style="list-style: none; padding: 0; margin: 0;">
|
|
1708
|
+
<li style="margin-bottom: 12px;">
|
|
1709
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1710
|
+
<span style="color: var(--primary);">▶</span>
|
|
1711
|
+
<span>群组管理</span>
|
|
1712
|
+
</a>
|
|
1713
|
+
</li>
|
|
1714
|
+
<li style="margin-bottom: 12px;">
|
|
1715
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1716
|
+
<span style="color: var(--primary);">▶</span>
|
|
1717
|
+
<span>任务管理</span>
|
|
1718
|
+
</a>
|
|
1719
|
+
</li>
|
|
1720
|
+
<li style="margin-bottom: 12px;">
|
|
1721
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1722
|
+
<span style="color: var(--primary);">▶</span>
|
|
1723
|
+
<span>文档协作</span>
|
|
1724
|
+
</a>
|
|
1725
|
+
</li>
|
|
1726
|
+
<li style="margin-bottom: 12px;">
|
|
1727
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1728
|
+
<span style="color: var(--primary);">▶</span>
|
|
1729
|
+
<span>知识库</span>
|
|
1730
|
+
</a>
|
|
1731
|
+
</li>
|
|
1732
|
+
<li style="margin-bottom: 12px;">
|
|
1733
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1734
|
+
<span style="color: var(--primary);">▶</span>
|
|
1735
|
+
<span>工作流引擎</span>
|
|
1736
|
+
</a>
|
|
1737
|
+
</li>
|
|
1738
|
+
<li>
|
|
1739
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1740
|
+
<span style="color: var(--primary);">▶</span>
|
|
1741
|
+
<span>AI 智能助手</span>
|
|
1742
|
+
</a>
|
|
1743
|
+
</li>
|
|
1744
|
+
</ul>
|
|
1745
|
+
</div>
|
|
1746
|
+
|
|
1747
|
+
<!-- 常见问题卡片 -->
|
|
1748
|
+
<div class="help-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
|
|
1749
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
|
|
1750
|
+
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">❓</div>
|
|
1751
|
+
<h3 style="margin: 0; font-size: 20px; font-weight: 600;">常见问题</h3>
|
|
1752
|
+
</div>
|
|
1753
|
+
<ul style="list-style: none; padding: 0; margin: 0;">
|
|
1754
|
+
<li style="margin-bottom: 12px;">
|
|
1755
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1756
|
+
<span style="color: var(--primary);">▶</span>
|
|
1757
|
+
<span>如何重置密码?</span>
|
|
1758
|
+
</a>
|
|
1759
|
+
</li>
|
|
1760
|
+
<li style="margin-bottom: 12px;">
|
|
1761
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1762
|
+
<span style="color: var(--primary);">▶</span>
|
|
1763
|
+
<span>如何导出数据?</span>
|
|
1764
|
+
</a>
|
|
1765
|
+
</li>
|
|
1766
|
+
<li style="margin-bottom: 12px;">
|
|
1767
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1768
|
+
<span style="color: var(--primary);">▶</span>
|
|
1769
|
+
<span>如何备份数据?</span>
|
|
1770
|
+
</a>
|
|
1771
|
+
</li>
|
|
1772
|
+
<li>
|
|
1773
|
+
<a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
|
|
1774
|
+
<span style="color: var(--primary);">▶</span>
|
|
1775
|
+
<span>如何联系技术支持?</span>
|
|
1776
|
+
</a>
|
|
1777
|
+
</li>
|
|
1778
|
+
</ul>
|
|
1779
|
+
</div>
|
|
1780
|
+
|
|
1781
|
+
<!-- 联系我们卡片 -->
|
|
1782
|
+
<div class="help-card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 28px; border-radius: 16px; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); transition: transform 0.2s, box-shadow 0.2s; color: white;">
|
|
1783
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
|
|
1784
|
+
<div style="width: 48px; height: 48px; background: rgba(255,255,255,0.2); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">📞</div>
|
|
1785
|
+
<h3 style="margin: 0; font-size: 20px; font-weight: 600;">联系我们</h3>
|
|
1786
|
+
</div>
|
|
1787
|
+
<p style="margin: 0 0 16px 0; opacity: 0.95; line-height: 1.6;">如果您有任何问题或建议,请通过以下方式联系我们:</p>
|
|
1788
|
+
<ul style="list-style: none; padding: 0; margin: 0;">
|
|
1789
|
+
<li style="margin-bottom: 12px; display: flex; align-items: center; gap: 10px; opacity: 0.95;">
|
|
1790
|
+
<span>📧</span>
|
|
1791
|
+
<span>1154269073@qq.com</span>
|
|
1792
|
+
</li>
|
|
1793
|
+
<li style="margin-bottom: 12px; display: flex; align-items: center; gap: 10px; opacity: 0.95;">
|
|
1794
|
+
<span>👤</span>
|
|
1795
|
+
<span>开发者:史菁昊</span>
|
|
1796
|
+
</li>
|
|
1797
|
+
<li style="margin-bottom: 12px;">
|
|
1798
|
+
<a href="https://github.com/shijinghao/collabdocchat" target="_blank" style="display: flex; align-items: center; gap: 10px; color: white; text-decoration: none; opacity: 0.95; transition: opacity 0.2s;" onmouseenter="this.style.opacity='1'" onmouseleave="this.style.opacity='0.95'">
|
|
1799
|
+
<span>🌐</span>
|
|
1800
|
+
<span>GitHub</span>
|
|
1801
|
+
</a>
|
|
1802
|
+
</li>
|
|
1803
|
+
<li>
|
|
1804
|
+
<a href="https://www.npmjs.com/package/collabdocchat" target="_blank" style="display: flex; align-items: center; gap: 10px; color: white; text-decoration: none; opacity: 0.95; transition: opacity 0.2s;" onmouseenter="this.style.opacity='1'" onmouseleave="this.style.opacity='0.95'">
|
|
1805
|
+
<span>📦</span>
|
|
1806
|
+
<span>npm Package</span>
|
|
1807
|
+
</a>
|
|
1808
|
+
</li>
|
|
1809
|
+
</ul>
|
|
1810
|
+
</div>
|
|
1811
|
+
|
|
1812
|
+
<!-- 关于卡片 -->
|
|
1813
|
+
<div class="help-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s; grid-column: span 2;">
|
|
1814
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
|
|
1815
|
+
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">ℹ️</div>
|
|
1816
|
+
<h3 style="margin: 0; font-size: 20px; font-weight: 600;">关于 CollabDocChat</h3>
|
|
1817
|
+
</div>
|
|
1818
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
|
1819
|
+
<div>
|
|
1820
|
+
<p style="margin: 0 0 12px 0; font-size: 18px; font-weight: 600; color: var(--primary);">CollabDocChat v2.3.0</p>
|
|
1821
|
+
<p style="margin: 0 0 12px 0; color: var(--text-secondary); line-height: 1.6;">开源的实时协作文档聊天平台,提供强大的团队协作功能。</p>
|
|
1822
|
+
<p style="margin: 0; color: var(--text-tertiary); font-size: 14px;">© 2026 CollabDocChat. All rights reserved.</p>
|
|
1823
|
+
</div>
|
|
1824
|
+
<div style="background: var(--bg-tertiary); padding: 20px; border-radius: 12px;">
|
|
1825
|
+
<h4 style="margin: 0 0 12px 0; font-size: 16px;">核心特性</h4>
|
|
1826
|
+
<ul style="margin: 0; padding-left: 20px; color: var(--text-secondary); line-height: 1.8; font-size: 14px;">
|
|
1827
|
+
<li>实时协作编辑</li>
|
|
1828
|
+
<li>智能 AI 助手</li>
|
|
1829
|
+
<li>工作流自动化</li>
|
|
1830
|
+
<li>知识库管理</li>
|
|
1831
|
+
<li>协作白板</li>
|
|
1832
|
+
<li>数据备份导出</li>
|
|
1833
|
+
</ul>
|
|
1834
|
+
</div>
|
|
1835
|
+
</div>
|
|
1836
|
+
</div>
|
|
1837
|
+
</div>
|
|
1838
|
+
`,document.querySelectorAll(".help-card").forEach(u=>{u.addEventListener("mouseenter",()=>{u.style.transform="translateY(-4px)",u.style.boxShadow="0 8px 24px rgba(0,0,0,0.12)"}),u.addEventListener("mouseleave",()=>{u.style.transform="translateY(0)";const g=u.style.background.includes("gradient");u.style.boxShadow=g?"0 4px 12px rgba(102, 126, 234, 0.3)":"0 2px 8px rgba(0,0,0,0.05)"})})}async function pe(m){const u=document.getElementById("contentArea");switch(m){case"groups":await L(u);break;case"tasks":await _(u);break;case"documents":await U(u);break;case"chat":await q(u);break;case"files":await ae(u);break;case"search":await B(u);break;case"call":await V(u);break;case"audit":await ne(u);break;case"polls":await oe(u);break;case"knowledge":await k(u);break;case"workflow":await D(u);break;case"backup":await T(u);break;case"export":await W(u);break;case"ai":await z(u);break;case"export":await W(u);break;case"whiteboard":await F(u);break;case"settings":await re(u);break;case"help":await le(u);break}}pe("groups")}function yn(a){const e=document.documentElement,o={dark:{primary:"#6366f1",primaryDark:"#4f46e5",secondary:"#8b5cf6",bgDark:"#0f172a",bgCard:"#1e293b",bgHover:"#334155",textPrimary:"#f1f5f9",textSecondary:"#94a3b8",border:"#334155"},blue:{primary:"#3b82f6",primaryDark:"#2563eb",secondary:"#06b6d4",bgDark:"#0c1222",bgCard:"#1a2332",bgHover:"#2a3442",textPrimary:"#e0f2fe",textSecondary:"#7dd3fc",border:"#2a3442"},green:{primary:"#10b981",primaryDark:"#059669",secondary:"#34d399",bgDark:"#0a1f1a",bgCard:"#1a2f2a",bgHover:"#2a3f3a",textPrimary:"#d1fae5",textSecondary:"#6ee7b7",border:"#2a3f3a"},orange:{primary:"#f59e0b",primaryDark:"#d97706",secondary:"#fb923c",bgDark:"#1f1a0a",bgCard:"#2f2a1a",bgHover:"#3f3a2a",textPrimary:"#fef3c7",textSecondary:"#fcd34d",border:"#3f3a2a"},pink:{primary:"#ec4899",primaryDark:"#db2777",secondary:"#f472b6",bgDark:"#1f0a1a",bgCard:"#2f1a2a",bgHover:"#3f2a3a",textPrimary:"#fce7f3",textSecondary:"#f9a8d4",border:"#3f2a3a"},light:{primary:"#6366f1",primaryDark:"#4f46e5",secondary:"#8b5cf6",bgDark:"#ffffff",bgCard:"#f8fafc",bgHover:"#e2e8f0",textPrimary:"#0f172a",textSecondary:"#475569",border:"#cbd5e1"}},n=o[a]||o.dark;e.style.setProperty("--primary",n.primary),e.style.setProperty("--primary-dark",n.primaryDark),e.style.setProperty("--secondary",n.secondary),e.style.setProperty("--bg-dark",n.bgDark),e.style.setProperty("--bg-card",n.bgCard),e.style.setProperty("--bg-hover",n.bgHover),e.style.setProperty("--text-primary",n.textPrimary),e.style.setProperty("--text-secondary",n.textSecondary),e.style.setProperty("--border",n.border)}function bn(a,e){const o=document.getElementById("app"),n=new gt,c=new ze,s=a.id||a._id;let d=null,S=[];function N(B){if(B.startsWith("[白板作品]")){const $=B.replace("[白板作品]","").trim(),A=$.includes("/api/files/")&&$.includes("/download");let v=$;if(A&&!$.includes("token=")){const k=localStorage.getItem("token");v=$.includes("?")?`${$}&token=${k}`:`${$}?token=${k}`}return`
|
|
1839
|
+
<div style="background: linear-gradient(135deg, rgb(99, 102, 241) 0%, rgb(139, 92, 246) 100%); padding: 16px; border-radius: 12px; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);">
|
|
1840
|
+
<div style="margin-bottom: 12px; font-weight: 600; color: white; display: flex; align-items: center; gap: 6px; font-size: 15px;">
|
|
1841
|
+
<span style="font-size: 18px;">🎨</span>
|
|
1842
|
+
<span>白板作品</span>
|
|
1843
|
+
</div>
|
|
1844
|
+
<div style="position: relative; display: inline-block;">
|
|
1845
|
+
<img src="${v}" alt="白板作品"
|
|
1846
|
+
style="max-width: 400px; max-height: 400px; border-radius: 8px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2); background: rgba(255,255,255,0.1);"
|
|
1847
|
+
onclick="window.open('${v}', '_blank')"
|
|
1848
|
+
onerror="this.style.display='none'; this.nextElementSibling.style.display='block'; console.error('白板图片加载失败:', '${v}');"
|
|
1849
|
+
onload="console.log('白板图片加载成功:', '${v}');">
|
|
1850
|
+
<div style="display: none; padding: 20px; background: rgba(255,255,255,0.1); border: 2px dashed rgba(255,255,255,0.3); border-radius: 8px; text-align: center; color: white;">
|
|
1851
|
+
<div style="font-size: 48px; margin-bottom: 10px;">⚠️</div>
|
|
1852
|
+
<div style="font-weight: 600; margin-bottom: 5px;">图片加载失败</div>
|
|
1853
|
+
<div style="font-size: 12px;">图片可能已被删除或URL无效</div>
|
|
1854
|
+
<button onclick="navigator.clipboard.writeText('${$}'); alert('图片URL已复制到剪贴板');" style="margin-top: 10px; padding: 6px 12px; background: rgba(255,255,255,0.2); color: white; border: none; border-radius: 6px; cursor: pointer;">复制图片URL</button>
|
|
1855
|
+
</div>
|
|
1856
|
+
<div style="margin-top: 10px; font-size: 12px; color: rgba(255,255,255,0.8);">点击图片查看大图</div>
|
|
1857
|
+
</div>
|
|
1858
|
+
</div>
|
|
1859
|
+
`}if(B.startsWith("[投票]")){const $=B.replace("[投票]","").trim();return`
|
|
1860
|
+
<div class="poll-card" data-poll-id="${$}" style="background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 2px solid rgba(99, 102, 241, 0.3); border-radius: 12px; padding: 16px; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'" onclick="viewPollDetail('${$}')">
|
|
1861
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
|
|
1862
|
+
<span style="font-size: 32px;">📊</span>
|
|
1863
|
+
<div style="flex: 1;">
|
|
1864
|
+
<div style="font-weight: 600; font-size: 16px; color: var(--text-primary); margin-bottom: 4px;">投票</div>
|
|
1865
|
+
<div style="font-size: 13px; color: var(--text-secondary);">点击查看详情并参与投票</div>
|
|
1866
|
+
</div>
|
|
1867
|
+
</div>
|
|
1868
|
+
<div style="padding: 8px 12px; background: rgba(99, 102, 241, 0.1); border-radius: 6px; font-size: 12px; color: var(--primary); text-align: center;">
|
|
1869
|
+
📋 查看投票详情
|
|
1870
|
+
</div>
|
|
1871
|
+
</div>
|
|
1872
|
+
`}return B}window.viewPollDetail=async B=>{try{const A=(await n.getPoll(B)).poll,v=A.options.reduce((F,re)=>F+re.votes.length,0),k=A.options.some(F=>F.votes.includes(s)),C=A.status==="ended"||A.endTime&&new Date(A.endTime)<new Date;let D="";A.options.forEach((F,re)=>{const X=v>0?(F.votes.length/v*100).toFixed(1):0,le=F.votes.includes(s);D+=`
|
|
1873
|
+
<div class="poll-option" style="margin-bottom: 12px; padding: 12px; background: var(--bg-tertiary); border-radius: 8px; border: 2px solid ${le?"var(--primary)":"var(--border)"};">
|
|
1874
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
1875
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
1876
|
+
<span style="font-weight: 500; color: var(--text-primary);">${F.text}</span>
|
|
1877
|
+
${le?'<span style="color: var(--primary); font-size: 12px;">✓ 已投票</span>':""}
|
|
1878
|
+
</div>
|
|
1879
|
+
<span style="font-weight: 600; color: var(--primary);">${F.votes.length} 票 (${X}%)</span>
|
|
1880
|
+
</div>
|
|
1881
|
+
<div style="height: 8px; background: var(--bg-secondary); border-radius: 4px; overflow: hidden;">
|
|
1882
|
+
<div style="height: 100%; background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); width: ${X}%; transition: width 0.3s;"></div>
|
|
1883
|
+
</div>
|
|
1884
|
+
${!A.anonymous&&F.votes.length>0?`
|
|
1885
|
+
<div style="margin-top: 8px; font-size: 12px; color: var(--text-secondary);">
|
|
1886
|
+
投票者: ${F.voterNames?F.voterNames.join(", "):""}
|
|
1887
|
+
</div>
|
|
1888
|
+
`:""}
|
|
1889
|
+
</div>
|
|
1890
|
+
`});const T=`
|
|
1891
|
+
<div id="pollDetailModal" class="modal" style="display: flex;">
|
|
1892
|
+
<div class="modal-content" style="max-width: 700px; max-height: 90vh; overflow-y: auto;">
|
|
1893
|
+
<div class="modal-header">
|
|
1894
|
+
<h3>📊 投票详情</h3>
|
|
1895
|
+
<button class="modal-close" id="closePollDetailModal">×</button>
|
|
1896
|
+
</div>
|
|
1897
|
+
<div class="modal-body" style="padding: 24px;">
|
|
1898
|
+
<div style="margin-bottom: 24px;">
|
|
1899
|
+
<h2 style="margin: 0 0 12px 0; color: var(--text-primary);">${A.title}</h2>
|
|
1900
|
+
${A.description?`<p style="color: var(--text-secondary); margin: 0 0 16px 0;">${A.description}</p>`:""}
|
|
1901
|
+
|
|
1902
|
+
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px;">
|
|
1903
|
+
<span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px; color: var(--text-secondary);">
|
|
1904
|
+
${A.allowMultiple?"✓ 多选投票":"○ 单选投票"}
|
|
1905
|
+
</span>
|
|
1906
|
+
<span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px; color: var(--text-secondary);">
|
|
1907
|
+
${A.anonymous?"🔒 匿名投票":"👤 实名投票"}
|
|
1908
|
+
</span>
|
|
1909
|
+
${C?'<span style="font-size: 13px; padding: 6px 12px; background: var(--danger); border-radius: 14px; color: white;">已结束</span>':'<span style="font-size: 13px; padding: 6px 12px; background: var(--success); border-radius: 14px; color: white;">进行中</span>'}
|
|
1910
|
+
</div>
|
|
1911
|
+
|
|
1912
|
+
<div style="padding: 16px; background: var(--bg-secondary); border-radius: 12px; border-left: 4px solid var(--primary); margin-bottom: 24px;">
|
|
1913
|
+
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
|
|
1914
|
+
<div>
|
|
1915
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">创建者</div>
|
|
1916
|
+
<div style="font-weight: 600; color: var(--text-primary);">👤 ${A.creatorName}</div>
|
|
1917
|
+
</div>
|
|
1918
|
+
<div>
|
|
1919
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">总投票数</div>
|
|
1920
|
+
<div style="font-weight: 600; color: var(--primary);">👥 ${v} 人</div>
|
|
1921
|
+
</div>
|
|
1922
|
+
<div>
|
|
1923
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">创建时间</div>
|
|
1924
|
+
<div style="font-weight: 600; color: var(--text-primary);">⏰ ${new Date(A.createdAt).toLocaleString("zh-CN")}</div>
|
|
1925
|
+
</div>
|
|
1926
|
+
${A.endTime?`
|
|
1927
|
+
<div>
|
|
1928
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">截止时间</div>
|
|
1929
|
+
<div style="font-weight: 600; color: var(--text-primary);">⏰ ${new Date(A.endTime).toLocaleString("zh-CN")}</div>
|
|
1930
|
+
</div>
|
|
1931
|
+
`:""}
|
|
1932
|
+
</div>
|
|
1933
|
+
</div>
|
|
1934
|
+
</div>
|
|
1935
|
+
|
|
1936
|
+
<div style="margin-bottom: 24px;">
|
|
1937
|
+
<h3 style="margin-bottom: 16px; color: var(--text-primary);">投票选项</h3>
|
|
1938
|
+
${!C&&!k?`
|
|
1939
|
+
<form id="voteForm">
|
|
1940
|
+
${A.options.map((F,re)=>{const X=v>0?(F.votes.length/v*100).toFixed(1):0;return`
|
|
1941
|
+
<div class="poll-option" style="margin-bottom: 12px; padding: 12px; background: var(--bg-tertiary); border-radius: 8px; border: 2px solid var(--border); cursor: pointer;" onmouseover="this.style.borderColor='var(--primary)'" onmouseout="this.style.borderColor='var(--border)'">
|
|
1942
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
1943
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
1944
|
+
<input type="${A.allowMultiple?"checkbox":"radio"}" name="poll-option" value="${re}" style="width: 18px; height: 18px; cursor: pointer;">
|
|
1945
|
+
<span style="font-weight: 500; color: var(--text-primary);">${F.text}</span>
|
|
1946
|
+
</div>
|
|
1947
|
+
<span style="font-weight: 600; color: var(--primary);">${F.votes.length} 票 (${X}%)</span>
|
|
1948
|
+
</div>
|
|
1949
|
+
<div style="height: 8px; background: var(--bg-secondary); border-radius: 4px; overflow: hidden;">
|
|
1950
|
+
<div style="height: 100%; background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); width: ${X}%; transition: width 0.3s;"></div>
|
|
1951
|
+
</div>
|
|
1952
|
+
</div>
|
|
1953
|
+
`}).join("")}
|
|
1954
|
+
<button type="submit" class="btn-primary" style="width: 100%; padding: 12px; margin-top: 16px;">提交投票</button>
|
|
1955
|
+
<div style="text-align: center; color: var(--warning); margin-top: 12px; font-size: 13px;">⚠️ 提交后不可修改</div>
|
|
1956
|
+
</form>
|
|
1957
|
+
`:D}
|
|
1958
|
+
${k?'<div style="text-align: center; color: var(--success); margin-top: 16px; padding: 12px; background: rgba(34, 197, 94, 0.1); border-radius: 8px; font-weight: 600;">✓ 您已参与投票,投票后不可修改</div>':""}
|
|
1959
|
+
${C?'<div style="text-align: center; color: var(--text-secondary); margin-top: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 8px;">投票已结束</div>':""}
|
|
1960
|
+
</div>
|
|
1961
|
+
|
|
1962
|
+
<div style="display: flex; gap: 10px;">
|
|
1963
|
+
<button class="btn-secondary" id="closePollDetailBtn" style="flex: 1;">关闭</button>
|
|
1964
|
+
</div>
|
|
1965
|
+
</div>
|
|
1966
|
+
</div>
|
|
1967
|
+
</div>
|
|
1968
|
+
`,z=document.getElementById("pollDetailModal");z&&z.remove(),document.body.insertAdjacentHTML("beforeend",T),document.getElementById("closePollDetailModal").addEventListener("click",()=>{document.getElementById("pollDetailModal").remove()}),document.getElementById("closePollDetailBtn").addEventListener("click",()=>{document.getElementById("pollDetailModal").remove()});const W=document.getElementById("voteForm");W&&!C&&!k&&W.addEventListener("submit",async F=>{F.preventDefault();const re=Array.from(document.querySelectorAll('input[name="poll-option"]:checked')).map(X=>parseInt(X.value));if(re.length===0){alert("请选择至少一个选项!");return}try{await n.vote(B,re),alert("投票成功!"),document.getElementById("pollDetailModal").remove();const X=document.querySelector(".nav-item.active");if(X&&X.dataset.view==="tasks"){const le=document.getElementById("contentArea");await _(le)}}catch(X){console.error("投票失败:",X),alert("投票失败:"+X.message)}})}catch($){console.error("加载投票详情失败:",$),alert("加载投票详情失败:"+$.message)}},o.innerHTML=`
|
|
1969
|
+
<div class="dashboard">
|
|
1970
|
+
<aside class="sidebar">
|
|
1971
|
+
<div class="sidebar-header">
|
|
1972
|
+
<h2>CollabDocChat</h2>
|
|
1973
|
+
<span class="badge-user">用户</span>
|
|
1974
|
+
</div>
|
|
1975
|
+
|
|
1976
|
+
<div class="user-info">
|
|
1977
|
+
<div class="avatar">${a.username[0].toUpperCase()}</div>
|
|
1978
|
+
<div>
|
|
1979
|
+
<div class="username">${a.username}</div>
|
|
1980
|
+
<div class="user-role">普通用户</div>
|
|
1981
|
+
</div>
|
|
1982
|
+
</div>
|
|
1983
|
+
|
|
1984
|
+
<nav class="nav-menu">
|
|
1985
|
+
<button class="nav-item active" data-view="groups">
|
|
1986
|
+
<span class="icon">👥</span> 我的群组
|
|
1987
|
+
</button>
|
|
1988
|
+
<button class="nav-item" data-view="allgroups">
|
|
1989
|
+
<span class="icon">🌐</span> 所有群组
|
|
1990
|
+
</button>
|
|
1991
|
+
<button class="nav-item" data-view="tasks">
|
|
1992
|
+
<span class="icon">📋</span> 我的任务
|
|
1993
|
+
</button>
|
|
1994
|
+
<button class="nav-item" data-view="documents">
|
|
1995
|
+
<span class="icon">📄</span> 共享文档
|
|
1996
|
+
</button>
|
|
1997
|
+
<button class="nav-item" data-view="files">
|
|
1998
|
+
<span class="icon">📎</span> 文件共享
|
|
1999
|
+
</button>
|
|
2000
|
+
<button class="nav-item" data-view="chat">
|
|
2001
|
+
<span class="icon">💬</span> 群聊
|
|
2002
|
+
</button>
|
|
2003
|
+
<button class="nav-item" data-view="search">
|
|
2004
|
+
<span class="icon">🔍</span> 搜索
|
|
2005
|
+
</button>
|
|
2006
|
+
<button class="nav-item" data-view="knowledge">
|
|
2007
|
+
<span class="icon">📚</span> 知识库
|
|
2008
|
+
</button>
|
|
2009
|
+
</nav>
|
|
2010
|
+
|
|
2011
|
+
<button class="btn-logout" id="logoutBtn">退出登录</button>
|
|
2012
|
+
</aside>
|
|
2013
|
+
|
|
2014
|
+
<main class="main-content">
|
|
2015
|
+
<div id="contentArea"></div>
|
|
2016
|
+
</main>
|
|
2017
|
+
</div>
|
|
2018
|
+
`,document.querySelectorAll(".nav-item").forEach(B=>{B.addEventListener("click",()=>{document.querySelectorAll(".nav-item").forEach(A=>A.classList.remove("active")),B.classList.add("active");const $=B.dataset.view;L($)})}),document.getElementById("logoutBtn").addEventListener("click",()=>{c.logout()});async function L(B){const $=document.getElementById("contentArea");switch(B){case"groups":await j($);break;case"allgroups":await H($);break;case"tasks":await _($);break;case"documents":await U($);break;case"files":await ae($);break;case"chat":await q($);break;case"search":await V($);break;case"knowledge":await de($);break}}async function j(B){S=(await n.getGroups()).groups,B.innerHTML=`
|
|
2019
|
+
<div class="view-header">
|
|
2020
|
+
<h2>我的群组</h2>
|
|
2021
|
+
</div>
|
|
2022
|
+
<div class="groups-grid" id="groupsList"></div>
|
|
2023
|
+
`;const A=document.getElementById("groupsList");if(S.length===0){A.innerHTML='<div class="empty-state">您还没有加入任何群组<br>请前往"所有群组"查看并加入</div>';return}S.forEach(v=>{const k=document.createElement("div");k.className="group-card",k.innerHTML=`
|
|
2024
|
+
<h3>${v.name}</h3>
|
|
2025
|
+
<p>${v.description||"暂无描述"}</p>
|
|
2026
|
+
<div class="group-stats">
|
|
2027
|
+
<span>👥 ${v.members.length} 成员</span>
|
|
2028
|
+
<span>📄 ${v.documents.length} 文档</span>
|
|
2029
|
+
<span>📋 ${v.tasks.length} 任务</span>
|
|
2030
|
+
</div>
|
|
2031
|
+
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
|
2032
|
+
<button class="btn-select" data-id="${v._id}">进入群组</button>
|
|
2033
|
+
<button class="btn-secondary" data-id="${v._id}" data-action="leave">退出群组</button>
|
|
2034
|
+
</div>
|
|
2035
|
+
`,A.appendChild(k)}),document.querySelectorAll(".btn-select").forEach(v=>{v.addEventListener("click",()=>{d=S.find(k=>k._id===v.dataset.id),e.joinGroup(d._id),alert(`已进入群组: ${d.name}`)})}),document.querySelectorAll('[data-action="leave"]').forEach(v=>{v.addEventListener("click",async()=>{if(confirm("确定要退出该群组吗?"))try{await n.leaveGroup(v.dataset.id),alert("已退出群组"),await j(B)}catch(k){alert("退出失败: "+k.message)}})})}async function H(B){const $=await n.getAllGroups(),v=(await n.getGroups()).groups.map(C=>C._id);B.innerHTML=`
|
|
2036
|
+
<div class="view-header">
|
|
2037
|
+
<h2>所有群组</h2>
|
|
2038
|
+
</div>
|
|
2039
|
+
<div class="groups-grid" id="allGroupsList"></div>
|
|
2040
|
+
`;const k=document.getElementById("allGroupsList");$.groups.forEach(C=>{const D=v.includes(C._id),T=document.createElement("div");T.className="group-card",T.innerHTML=`
|
|
2041
|
+
<h3>${C.name}</h3>
|
|
2042
|
+
<p>${C.description||"暂无描述"}</p>
|
|
2043
|
+
<div class="group-stats">
|
|
2044
|
+
<span>👥 ${C.members.length} 成员</span>
|
|
2045
|
+
<span>📄 ${C.documents.length} 文档</span>
|
|
2046
|
+
</div>
|
|
2047
|
+
${D?'<div style="color: var(--success); margin-top: 10px;">✓ 已加入</div>':`<button class="btn-primary" data-id="${C._id}" data-action="join">加入群组</button>`}
|
|
2048
|
+
`,k.appendChild(T)}),document.querySelectorAll('[data-action="join"]').forEach(C=>{C.addEventListener("click",async()=>{try{await n.joinGroup(C.dataset.id),alert("加入成功!"),await H(B)}catch(D){alert("加入失败: "+D.message)}})})}async function _(B){try{const $=await n.getMyTasks();let A=[];try{const C=(await n.getGroups()).groups;for(const D of C)try{const T=await n.getGroupPolls(D._id);T.polls&&Array.isArray(T.polls)&&(A=A.concat(T.polls.map(z=>({...z,groupName:D.name}))))}catch(T){console.error(`获取群组 ${D.name} 的投票失败:`,T)}}catch(k){console.error("获取投票失败:",k)}B.innerHTML=`
|
|
2049
|
+
<div class="view-header">
|
|
2050
|
+
<h2>我的任务</h2>
|
|
2051
|
+
</div>
|
|
2052
|
+
<div class="tasks-list" id="tasksList"></div>
|
|
2053
|
+
`;const v=document.getElementById("tasksList");if($.tasks.length===0&&A.length===0){v.innerHTML='<div class="empty-state">暂无任务</div>';return}$.tasks.forEach(k=>{const C=document.createElement("div");C.className=`task-card status-${k.status}`,C.innerHTML=`
|
|
2054
|
+
<h3>${k.title}</h3>
|
|
2055
|
+
<p>${k.description}</p>
|
|
2056
|
+
<div class="task-meta">
|
|
2057
|
+
<span class="status-badge">${oe(k.status)}</span>
|
|
2058
|
+
<span>群组: ${k.group.name}</span>
|
|
2059
|
+
${k.deadline?`<span>截止: ${new Date(k.deadline).toLocaleDateString()}</span>`:""}
|
|
2060
|
+
</div>
|
|
2061
|
+
${k.relatedDocument?`<a href="#" class="doc-link" data-id="${k.relatedDocument._id}">📄 查看相关文档</a>`:""}
|
|
2062
|
+
<div class="task-actions">
|
|
2063
|
+
${k.status==="pending"?`<button class="btn-primary btn-sm" data-id="${k._id}" data-action="start">开始任务</button>`:""}
|
|
2064
|
+
${k.status==="in_progress"?`<button class="btn-success btn-sm" data-id="${k._id}" data-action="complete">完成任务</button>`:""}
|
|
2065
|
+
</div>
|
|
2066
|
+
`,v.appendChild(C)}),A.forEach(k=>{const C=k.options.reduce((W,F)=>W+F.votes.length,0),D=k.options.some(W=>W.votes.includes(s)),T=k.status==="ended"||k.endTime&&new Date(k.endTime)<new Date,z=document.createElement("div");z.className="task-card poll-task",z.style.cssText="background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%); border-left: 4px solid var(--primary);",z.innerHTML=`
|
|
2067
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
|
|
2068
|
+
<span style="font-size: 32px;">📊</span>
|
|
2069
|
+
<h3 style="margin: 0;">${k.title}</h3>
|
|
2070
|
+
</div>
|
|
2071
|
+
<p>${k.description||"暂无描述"}</p>
|
|
2072
|
+
<div class="task-meta">
|
|
2073
|
+
<span class="status-badge" style="background: ${T?"var(--danger)":"var(--success)"};">${T?"已结束":"进行中"}</span>
|
|
2074
|
+
<span>群组: ${k.groupName}</span>
|
|
2075
|
+
<span>👥 ${C} 人投票</span>
|
|
2076
|
+
${D?'<span style="color: var(--success);">✓ 已投票</span>':'<span style="color: var(--warning);">⏳ 待投票</span>'}
|
|
2077
|
+
</div>
|
|
2078
|
+
<div class="task-actions">
|
|
2079
|
+
<button class="btn-primary btn-sm" data-poll-id="${k._id}" data-action="view-poll">查看详情</button>
|
|
2080
|
+
</div>
|
|
2081
|
+
`,v.appendChild(z)}),document.querySelectorAll('[data-action="start"], [data-action="complete"]').forEach(k=>{k.addEventListener("click",async()=>{const C=k.dataset.id,T=k.dataset.action==="start"?"in_progress":"completed";try{await n.updateTaskStatus(C,T),await _(B)}catch(z){alert("操作失败: "+z.message)}})}),document.querySelectorAll('[data-action="view-poll"]').forEach(k=>{k.addEventListener("click",()=>{const C=k.dataset.pollId;window.viewPollDetail(C)})})}catch($){console.error("获取任务失败:",$),B.innerHTML=`
|
|
2082
|
+
<div class="view-header">
|
|
2083
|
+
<h2>我的任务</h2>
|
|
2084
|
+
</div>
|
|
2085
|
+
<div class="empty-state">加载任务失败: ${$.message}</div>
|
|
2086
|
+
`}}async function U(B){if(!d){B.innerHTML='<div class="empty-state">请先选择一个群组</div>';return}const $=await n.getDocuments(d._id);B.innerHTML=`
|
|
2087
|
+
<div class="view-header">
|
|
2088
|
+
<h2>共享文档 - ${d.name}</h2>
|
|
2089
|
+
</div>
|
|
2090
|
+
<div class="documents-list" id="docsList"></div>
|
|
2091
|
+
`;const A=document.getElementById("docsList");if($.documents.length===0){A.innerHTML='<div class="empty-state">暂无文档</div>';return}$.documents.forEach(v=>{const k=document.createElement("div");k.className="document-card",k.innerHTML=`
|
|
2092
|
+
<h3>📄 ${v.title}</h3>
|
|
2093
|
+
<div class="doc-meta">
|
|
2094
|
+
<span>创建者: ${v.creator.username}</span>
|
|
2095
|
+
<span>${v.permission==="readonly"?"🔒 只读":"✏️ 可编辑"}</span>
|
|
2096
|
+
<span>更新: ${new Date(v.updatedAt).toLocaleString()}</span>
|
|
2097
|
+
</div>
|
|
2098
|
+
<button class="btn-edit" data-id="${v._id}">
|
|
2099
|
+
${v.permission==="readonly"?"查看":"编辑"}
|
|
2100
|
+
</button>
|
|
2101
|
+
`,A.appendChild(k)}),document.querySelectorAll(".btn-edit").forEach(v=>{v.addEventListener("click",()=>{se(B,v.dataset.id)})})}async function se(B,$){const v=(await n.getDocument($)).document;B.innerHTML=`
|
|
2102
|
+
<div class="view-header">
|
|
2103
|
+
<button class="btn-back" id="backBtn">← 返回</button>
|
|
2104
|
+
<h2>${v.title}</h2>
|
|
2105
|
+
<span class="doc-status">${v.permission==="readonly"?"🔒 只读模式":"✏️ 编辑模式"}</span>
|
|
2106
|
+
</div>
|
|
2107
|
+
<div class="editor-container">
|
|
2108
|
+
<div class="editor-toolbar">
|
|
2109
|
+
<div class="online-users" id="onlineUsers">
|
|
2110
|
+
<span class="user-badge">👤 ${a.username}</span>
|
|
2111
|
+
</div>
|
|
2112
|
+
${v.permission==="editable"?'<button class="btn-primary" id="saveBtn">保存</button>':""}
|
|
2113
|
+
</div>
|
|
2114
|
+
<div id="editor" ${v.permission==="readonly"?'class="readonly"':""}></div>
|
|
2115
|
+
<div class="editor-footer">
|
|
2116
|
+
<span>最后编辑: ${new Date(v.updatedAt).toLocaleString()}</span>
|
|
2117
|
+
</div>
|
|
2118
|
+
</div>
|
|
2119
|
+
`;const k=new Quill("#editor",{theme:"snow",modules:{toolbar:v.permission==="readonly"?!1:[[{header:[1,2,3,!1]}],["bold","italic","underline","strike"],[{list:"ordered"},{list:"bullet"}],[{color:[]},{background:[]}],["link","image","code-block"],["clean"]]},readOnly:v.permission==="readonly"});if(k.root.innerHTML=v.content||"",v.permission==="editable"){let C,D;k.on("text-change",()=>{clearTimeout(C),clearTimeout(D),e.sendTyping($,a.username,!0),C=setTimeout(()=>{e.sendTyping($,a.username,!1)},1e3),D=setTimeout(async()=>{const T=k.root.innerHTML;try{await n.updateDocument($,T)}catch(z){console.error("自动保存失败:",z)}},2e3)}),document.getElementById("saveBtn").addEventListener("click",async()=>{try{const T=k.root.innerHTML;await n.updateDocument($,T),alert("保存成功!")}catch(T){alert("保存失败: "+T.message)}})}e.on("document_update",C=>{if(C.documentId===$&&C.userId!==a.id){const D=k.getSelection();k.root.innerHTML=C.content,D&&k.setSelection(D)}}),e.on("typing",C=>{if(C.documentId===$&&C.userId!==a.id){const D=document.getElementById("onlineUsers");if(C.isTyping)D.innerHTML+=`<span class="user-badge typing" data-user="${C.userId}">✏️ ${C.username}</span>`;else{const T=D.querySelector(`[data-user="${C.userId}"]`);T&&T.remove()}}}),document.getElementById("backBtn").addEventListener("click",()=>{U(B)})}async function ae(B){if(!d){B.innerHTML='<div class="empty-state">请先选择一个群组</div>';return}try{const $=await n.getGroupFiles(d._id);B.innerHTML=`
|
|
2120
|
+
<div class="view-header">
|
|
2121
|
+
<h2>文件共享 - ${d.name}</h2>
|
|
2122
|
+
<button class="btn-primary" id="uploadFileBtn">📤 上传文件</button>
|
|
2123
|
+
</div>
|
|
2124
|
+
<div class="files-list" id="filesList"></div>
|
|
2125
|
+
|
|
2126
|
+
<!-- 文件上传模态框 -->
|
|
2127
|
+
<div class="modal hidden" id="uploadFileModal">
|
|
2128
|
+
<div class="modal-content">
|
|
2129
|
+
<div class="modal-header">
|
|
2130
|
+
<h3>上传文件</h3>
|
|
2131
|
+
<button class="modal-close" id="closeUploadModal">×</button>
|
|
2132
|
+
</div>
|
|
2133
|
+
<form id="uploadFileForm">
|
|
2134
|
+
<div class="form-group">
|
|
2135
|
+
<label>选择文件</label>
|
|
2136
|
+
<input type="file" id="fileInput" required>
|
|
2137
|
+
<small>支持图片、PDF、Word、Excel等,最大10MB</small>
|
|
2138
|
+
</div>
|
|
2139
|
+
<div class="form-group">
|
|
2140
|
+
<label>描述(可选)</label>
|
|
2141
|
+
<textarea id="fileDescription" rows="3" placeholder="文件描述..."></textarea>
|
|
2142
|
+
</div>
|
|
2143
|
+
<div class="form-actions">
|
|
2144
|
+
<button type="button" class="btn-secondary" id="cancelUpload">取消</button>
|
|
2145
|
+
<button type="submit" class="btn-primary">上传</button>
|
|
2146
|
+
</div>
|
|
2147
|
+
</form>
|
|
2148
|
+
</div>
|
|
2149
|
+
</div>
|
|
2150
|
+
`;const A=document.getElementById("filesList");!$.files||$.files.length===0?A.innerHTML='<div class="empty-state">暂无文件</div>':($.files.forEach(v=>{const k=document.createElement("div");k.className="file-card";const C=ie(v.mimetype),D=ce(v.size);k.innerHTML=`
|
|
2151
|
+
<div class="file-icon">${C}</div>
|
|
2152
|
+
<div class="file-info">
|
|
2153
|
+
<h4>${v.originalName}</h4>
|
|
2154
|
+
<div class="file-meta">
|
|
2155
|
+
<span>上传者: ${v.uploader.username}</span>
|
|
2156
|
+
<span>大小: ${D}</span>
|
|
2157
|
+
<span>时间: ${new Date(v.createdAt).toLocaleString()}</span>
|
|
2158
|
+
</div>
|
|
2159
|
+
${v.description?`<p class="file-description">${v.description}</p>`:""}
|
|
2160
|
+
</div>
|
|
2161
|
+
<div class="file-actions">
|
|
2162
|
+
<a href="${n.getFileDownloadUrl(v._id)}" class="btn-primary" download>下载</a>
|
|
2163
|
+
${String(v.uploader._id)===String(s)?`<button class="btn-danger" data-id="${v._id}" data-action="delete-file">删除</button>`:""}
|
|
2164
|
+
</div>
|
|
2165
|
+
`,A.appendChild(k)}),document.querySelectorAll('[data-action="delete-file"]').forEach(v=>{v.addEventListener("click",async()=>{if(confirm("确定要删除这个文件吗?"))try{await n.deleteFile(v.dataset.id),alert("文件删除成功!"),await ae(B)}catch(k){alert("删除失败: "+k.message)}})})),document.getElementById("uploadFileBtn").addEventListener("click",()=>{document.getElementById("uploadFileModal").classList.remove("hidden")}),document.getElementById("closeUploadModal").addEventListener("click",()=>{document.getElementById("uploadFileModal").classList.add("hidden"),document.getElementById("uploadFileForm").reset()}),document.getElementById("cancelUpload").addEventListener("click",()=>{document.getElementById("uploadFileModal").classList.add("hidden"),document.getElementById("uploadFileForm").reset()}),document.getElementById("uploadFileForm").addEventListener("submit",async v=>{v.preventDefault();const k=document.getElementById("fileInput"),C=document.getElementById("fileDescription").value;if(!k.files[0]){alert("请选择文件");return}try{await n.uploadFile(d._id,k.files[0],C),alert("文件上传成功!"),document.getElementById("uploadFileModal").classList.add("hidden"),document.getElementById("uploadFileForm").reset(),await ae(B)}catch(D){alert("上传失败: "+D.message)}})}catch($){console.error("获取文件列表失败:",$),B.innerHTML=`
|
|
2166
|
+
<div class="view-header">
|
|
2167
|
+
<h2>文件共享</h2>
|
|
2168
|
+
</div>
|
|
2169
|
+
<div class="empty-state">加载文件失败: ${$.message}</div>
|
|
2170
|
+
`}}function ie(B){return B.startsWith("image/")?"🖼️":B==="application/pdf"?"📕":B.includes("word")||B.includes("document")?"📘":B.includes("excel")||B.includes("spreadsheet")?"📗":B.includes("zip")||B.includes("compressed")?"📦":"📄"}function ce(B){if(B===0)return"0 Bytes";const $=1024,A=["Bytes","KB","MB","GB"],v=Math.floor(Math.log(B)/Math.log($));return Math.round(B/Math.pow($,v)*100)/100+" "+A[v]}async function q(B){if(!d){B.innerHTML=`
|
|
2171
|
+
<div class="empty-state" style="text-align: center; padding: 60px 20px; background: var(--bg-secondary); border-radius: 16px; border: 2px dashed var(--border);">
|
|
2172
|
+
<div style="font-size: 64px; margin-bottom: 20px;">💬</div>
|
|
2173
|
+
<h3 style="font-size: 24px; margin-bottom: 12px; color: var(--text-primary);">群聊</h3>
|
|
2174
|
+
<p style="color: var(--text-secondary); margin-bottom: 24px; font-size: 16px;">请先在"我的群组"中选择一个群组</p>
|
|
2175
|
+
<button class="btn-primary" onclick="document.querySelector('[data-view=\\"groups\\"]').click()" style="padding: 12px 32px; font-size: 16px;">
|
|
2176
|
+
前往我的群组
|
|
2177
|
+
</button>
|
|
2178
|
+
</div>
|
|
2179
|
+
`;return}try{let le=function(i,t,r="💬"){"Notification"in window&&Notification.permission==="granted"&&new Notification(i,{body:t,icon:"/icon.png",badge:"/icon.png",tag:"chat-message"})},g=function(){const i=document.getElementById("whiteboard");if(!i||i.dataset.initialized)return;i.dataset.initialized="true";const t=i.getContext("2d");i.width=i.offsetWidth,i.height=i.offsetHeight;let r=!1,l="pen",b="#667eea",E=3;document.querySelectorAll(".tool-btn").forEach(w=>{w.addEventListener("click",()=>{l=w.dataset.tool,document.querySelectorAll(".tool-btn").forEach(P=>{P.style.background="var(--bg-secondary)",P.style.color="var(--text-primary)",P.style.border="1px solid var(--border)"}),w.style.background="var(--primary)",w.style.color="white",w.style.border="none"})}),document.getElementById("colorPicker").addEventListener("change",w=>{b=w.target.value}),document.getElementById("brushSize").addEventListener("input",w=>{E=w.target.value}),document.getElementById("clearCanvas").addEventListener("click",()=>{confirm("确定要清空画布吗?")&&(t.clearRect(0,0,i.width,i.height),e.sendWhiteboardClear(d._id))}),document.getElementById("sendWhiteboardBtn").addEventListener("click",async()=>{try{const w=i.toDataURL("image/png"),P=document.createElement("canvas");P.width=i.width,P.height=i.height;const G=P.toDataURL("image/png");if(w===G){alert("画布是空的,请先绘制内容!");return}if(w.length*.75/1024/1024<1)e.sendChatMessage(d._id,a.username,`[白板作品]${w}`),alert("白板作品已发送到群聊!");else{const I=await fetch(w).then(K=>K.blob()),x=new FormData;x.append("file",I,`whiteboard-${Date.now()}.png`),x.append("groupId",d._id),x.append("description","协作白板作品");const R=localStorage.getItem("token"),O=await fetch("http://localhost:3000/api/files/upload",{method:"POST",headers:{Authorization:`Bearer ${R}`},body:x});if(O.ok){const J=`http://localhost:3000/api/files/${(await O.json()).file._id}/download?token=${R}`;e.sendChatMessage(d._id,a.username,`[白板作品]${J}`),alert("白板作品已发送到群聊!")}else throw new Error("上传失败")}}catch(w){console.error("发送白板失败:",w),alert("发送失败,请重试!")}}),i.addEventListener("mousedown",w=>{r=!0;const P=i.getBoundingClientRect(),G=w.clientX-P.left,f=w.clientY-P.top;t.beginPath(),t.moveTo(G,f)}),i.addEventListener("mousemove",w=>{if(!r)return;const P=i.getBoundingClientRect(),G=w.clientX-P.left,f=w.clientY-P.top;t.lineWidth=E,t.lineCap="round",l==="pen"?(t.strokeStyle=b,t.globalCompositeOperation="source-over"):l==="eraser"&&(t.globalCompositeOperation="destination-out"),t.lineTo(G,f),t.stroke(),e.sendWhiteboardDraw(d._id,{tool:l,color:b,size:E,x:G,y:f})}),i.addEventListener("mouseup",()=>{r=!1}),i.addEventListener("mouseleave",()=>{r=!1}),e.on("whiteboard_draw",w=>{w.groupId===d._id&&(t.lineWidth=w.size,t.lineCap="round",w.tool==="pen"?(t.strokeStyle=w.color,t.globalCompositeOperation="source-over"):w.tool==="eraser"&&(t.globalCompositeOperation="destination-out"),t.lineTo(w.x,w.y),t.stroke())}),e.on("whiteboard_clear",w=>{w.groupId===d._id&&t.clearRect(0,0,i.width,i.height)})};var $=le,A=g;const k=(await n.getGroup(d._id)).group,C=!!k.mutedAll,D=(k.mutedUsers||[]).map(String).includes(String(s)),T=!C&&!D;B.innerHTML=`
|
|
2180
|
+
<div class="view-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px; margin-bottom: 20px;">
|
|
2181
|
+
<h2 style="margin: 0; display: flex; align-items: center; gap: 12px;">
|
|
2182
|
+
<span style="font-size: 32px;">💬</span>
|
|
2183
|
+
<span>群聊 - ${d.name}</span>
|
|
2184
|
+
</h2>
|
|
2185
|
+
${T?"":`
|
|
2186
|
+
<div style="margin-top: 12px; padding: 12px; background: rgba(255,255,255,0.2); border-radius: 8px; font-size: 14px;">
|
|
2187
|
+
⚠️ ${C?"全体禁言中,无法发言":"你已被禁言"}
|
|
2188
|
+
</div>
|
|
2189
|
+
`}
|
|
2190
|
+
</div>
|
|
2191
|
+
|
|
2192
|
+
<!-- 标签页切换 -->
|
|
2193
|
+
<div class="chat-tabs" style="display: flex; gap: 8px; margin-bottom: 16px; background: var(--bg-secondary); padding: 8px; border-radius: 12px;">
|
|
2194
|
+
<button class="chat-tab active" data-tab="chat" style="flex: 1; padding: 12px 20px; background: var(--primary); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s;">
|
|
2195
|
+
💬 聊天
|
|
2196
|
+
</button>
|
|
2197
|
+
<button class="chat-tab" data-tab="whiteboard" style="flex: 1; padding: 12px 20px; background: transparent; color: var(--text-primary); border: none; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s;">
|
|
2198
|
+
🎨 白板
|
|
2199
|
+
</button>
|
|
2200
|
+
<button class="chat-tab" data-tab="ai" style="flex: 1; padding: 12px 20px; background: transparent; color: var(--text-primary); border: none; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s;">
|
|
2201
|
+
🤖 AI助手
|
|
2202
|
+
</button>
|
|
2203
|
+
</div>
|
|
2204
|
+
|
|
2205
|
+
<!-- 聊天内容 -->
|
|
2206
|
+
<div class="tab-content active" data-content="chat">
|
|
2207
|
+
<div class="chat-container" style="display: flex; flex-direction: column; height: calc(100vh - 350px); background: var(--bg-secondary); border-radius: 12px; overflow: hidden;">
|
|
2208
|
+
<div class="messages" id="messages" style="flex: 1; overflow-y: auto; padding: 20px;"></div>
|
|
2209
|
+
<div class="chat-input" style="display: flex; gap: 10px; padding: 16px; background: var(--bg-tertiary); border-top: 1px solid var(--border);">
|
|
2210
|
+
<button class="btn-emoji" id="emojiBtn" ${T?"":"disabled"} style="padding: 10px 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; cursor: ${T?"pointer":"not-allowed"}; font-size: 20px;">😊</button>
|
|
2211
|
+
<input type="text" id="messageInput" placeholder="${T?"输入消息...":C?"全体禁言中,无法发言":"你已被禁言"}" ${T?"":"disabled"} style="flex: 1; padding: 10px 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary);">
|
|
2212
|
+
<button class="btn-primary" id="sendBtn" ${T?"":"disabled"} style="padding: 10px 24px; border-radius: 8px; cursor: ${T?"pointer":"not-allowed"};">发送</button>
|
|
2213
|
+
</div>
|
|
2214
|
+
<emoji-picker id="emojiPicker" class="hidden" style="position: absolute; bottom: 80px; left: 20px; z-index: 1000;"></emoji-picker>
|
|
2215
|
+
</div>
|
|
2216
|
+
</div>
|
|
2217
|
+
|
|
2218
|
+
<!-- 白板内容 -->
|
|
2219
|
+
<div class="tab-content" data-content="whiteboard" style="display: none;">
|
|
2220
|
+
<div class="whiteboard-container" style="background: var(--bg-secondary); border-radius: 12px; padding: 16px; height: calc(100vh - 350px);">
|
|
2221
|
+
<div class="whiteboard-toolbar" style="display: flex; gap: 10px; margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 8px;">
|
|
2222
|
+
<button class="tool-btn active" data-tool="pen" style="padding: 8px 16px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer;">✏️ 画笔</button>
|
|
2223
|
+
<button class="tool-btn" data-tool="eraser" style="padding: 8px 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 6px; cursor: pointer;">🧹 橡皮</button>
|
|
2224
|
+
<input type="color" id="colorPicker" value="#667eea" style="width: 50px; height: 36px; border: none; border-radius: 6px; cursor: pointer;">
|
|
2225
|
+
<input type="range" id="brushSize" min="1" max="20" value="3" style="width: 120px;">
|
|
2226
|
+
<button class="btn-secondary" id="clearCanvas" style="padding: 8px 16px; border-radius: 6px;">🗑️ 清空</button>
|
|
2227
|
+
<button class="btn-primary" id="sendWhiteboardBtn" style="padding: 8px 16px; border-radius: 6px; background: var(--success); border: none; color: white; cursor: pointer;">📤 发送到群聊</button>
|
|
2228
|
+
</div>
|
|
2229
|
+
<canvas id="whiteboard" style="width: 100%; height: calc(100% - 80px); background: white; border-radius: 8px; cursor: crosshair;"></canvas>
|
|
2230
|
+
</div>
|
|
2231
|
+
</div>
|
|
2232
|
+
|
|
2233
|
+
<!-- AI助手内容 -->
|
|
2234
|
+
<div class="tab-content" data-content="ai" style="display: none;">
|
|
2235
|
+
<div class="ai-container" style="display: flex; flex-direction: column; height: calc(100vh - 350px); background: var(--bg-secondary); border-radius: 12px; overflow: hidden;">
|
|
2236
|
+
<div class="ai-chat" id="aiChat" style="flex: 1; overflow-y: auto; padding: 20px;">
|
|
2237
|
+
<div class="ai-message ai" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 16px; border-radius: 12px; margin-bottom: 16px;">
|
|
2238
|
+
<p style="margin: 0 0 8px 0; font-weight: 600;">🤖 AI助手</p>
|
|
2239
|
+
<p style="margin: 0;">你好!我是AI助手,有什么可以帮助你的吗?</p>
|
|
2240
|
+
<p style="margin: 8px 0 0 0; font-size: 14px; opacity: 0.9;">你可以问我关于文档、任务、群组的问题。</p>
|
|
2241
|
+
</div>
|
|
2242
|
+
</div>
|
|
2243
|
+
<div class="ai-input" style="display: flex; gap: 10px; padding: 16px; background: var(--bg-tertiary); border-top: 1px solid var(--border);">
|
|
2244
|
+
<textarea id="aiInput" placeholder="向AI助手提问..." rows="2" style="flex: 1; padding: 10px 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary); resize: none; color: white;"></textarea>
|
|
2245
|
+
<button class="btn-primary" id="aiSendBtn" style="padding: 10px 24px; border-radius: 8px; align-self: flex-end;">发送</button>
|
|
2246
|
+
</div>
|
|
2247
|
+
</div>
|
|
2248
|
+
</div>
|
|
2249
|
+
`;const z=document.getElementById("messages"),W=document.getElementById("messageInput"),F=document.getElementById("sendBtn");try{const i=await n.getGroupMessages(d._id);i.messages&&Array.isArray(i.messages)&&(i.messages.length===0?z.innerHTML=`
|
|
2250
|
+
<div style="text-align: center; padding: 40px; color: var(--text-tertiary);">
|
|
2251
|
+
<div style="font-size: 48px; margin-bottom: 16px;">💬</div>
|
|
2252
|
+
<p>还没有消息,开始聊天吧!</p>
|
|
2253
|
+
</div>
|
|
2254
|
+
`:(i.messages.forEach(t=>{const r=document.createElement("div"),l=String(t.sender)===String(s)||t.username===a.username,b=N(t.content),E=t.content.startsWith("[白板作品]")||t.content.startsWith("[投票]"),w=t.content.startsWith("[白板作品]");r.className=`message ${l?"own":""}`,r.style.cssText=`
|
|
2255
|
+
margin-bottom: 16px;
|
|
2256
|
+
display: flex;
|
|
2257
|
+
flex-direction: column;
|
|
2258
|
+
align-items: ${l?"flex-end":"flex-start"};
|
|
2259
|
+
`,r.innerHTML=`
|
|
2260
|
+
<div class="message-header" style="display: flex; gap: 8px; margin-bottom: 4px; font-size: 12px; color: var(--text-tertiary);">
|
|
2261
|
+
<span class="message-user">${t.username}</span>
|
|
2262
|
+
<span class="message-time">${new Date(t.timestamp).toLocaleTimeString("zh-CN")}</span>
|
|
2263
|
+
</div>
|
|
2264
|
+
<div class="message-content" style="background: ${w||E?"transparent":l?"linear-gradient(135deg, #667eea 0%, #764ba2 100%)":"var(--bg-tertiary)"}; color: ${l&&!E?"white":"var(--text-primary)"}; padding: ${E?"0":"12px 16px"}; border-radius: 16px; max-width: ${E?"90%":"70%"}; word-wrap: break-word; box-shadow: ${l&&!E?"0 4px 12px rgba(102, 126, 234, 0.3)":"0 2px 8px rgba(0,0,0,0.05)"};">${b}</div>
|
|
2265
|
+
`,z.appendChild(r)}),z.scrollTop=z.scrollHeight))}catch(i){console.error("加载历史消息失败:",i),z.innerHTML=`
|
|
2266
|
+
<div style="text-align: center; padding: 40px; color: var(--danger);">
|
|
2267
|
+
<div style="font-size: 48px; margin-bottom: 16px;">⚠️</div>
|
|
2268
|
+
<p>加载历史消息失败</p>
|
|
2269
|
+
<p style="font-size: 14px; color: var(--text-tertiary);">${i.message}</p>
|
|
2270
|
+
</div>
|
|
2271
|
+
`}const re=document.getElementById("emojiBtn"),X=document.getElementById("emojiPicker");T&&(re.addEventListener("click",()=>{X.classList.toggle("hidden")}),X.addEventListener("emoji-click",i=>{W.value+=i.detail.unicode,W.focus(),X.classList.add("hidden")}),document.addEventListener("click",i=>{!re.contains(i.target)&&!X.contains(i.target)&&X.classList.add("hidden")})),"Notification"in window&&Notification.permission==="default"&&Notification.requestPermission(),e.on("chat_message",i=>{if(i.groupId===d._id){const t=document.createElement("div"),r=String(i.userId)===String(s)||i.username===a.username;t.className=`message ${r?"own":""}`,t.style.cssText=`
|
|
2272
|
+
margin-bottom: 16px;
|
|
2273
|
+
display: flex;
|
|
2274
|
+
flex-direction: column;
|
|
2275
|
+
align-items: ${r?"flex-end":"flex-start"};
|
|
2276
|
+
`;const l=N(i.content),b=i.content.startsWith("[白板作品]")||i.content.startsWith("[投票]"),E=i.content.startsWith("[白板作品]");t.innerHTML=`
|
|
2277
|
+
<div class="message-header" style="display: flex; gap: 8px; margin-bottom: 4px; font-size: 12px; color: var(--text-tertiary);">
|
|
2278
|
+
<span class="message-user">${i.username}</span>
|
|
2279
|
+
<span class="message-time">${new Date(i.timestamp).toLocaleTimeString("zh-CN")}</span>
|
|
2280
|
+
</div>
|
|
2281
|
+
<div class="message-content" style="background: ${E||b?"transparent":r?"linear-gradient(135deg, #667eea 0%, #764ba2 100%)":"var(--bg-tertiary)"}; color: ${r&&!b?"white":"var(--text-primary)"}; padding: ${b?"0":"12px 16px"}; border-radius: 16px; max-width: ${b?"90%":"70%"}; word-wrap: break-word; box-shadow: ${r&&!b?"0 4px 12px rgba(102, 126, 234, 0.3)":"0 2px 8px rgba(0,0,0,0.05)"};">${l}</div>
|
|
2282
|
+
`,z.appendChild(t),z.scrollTop=z.scrollHeight,r||le(`${i.username} 在 ${d.name}`,i.content.startsWith("[")?"发送了特殊消息":i.content)}}),e.on("chat_blocked",i=>{if(i.groupId===d._id){const t=document.createElement("div");t.style.cssText=`
|
|
2283
|
+
text-align: center;
|
|
2284
|
+
padding: 12px;
|
|
2285
|
+
margin: 16px auto;
|
|
2286
|
+
background: var(--danger);
|
|
2287
|
+
color: white;
|
|
2288
|
+
border-radius: 8px;
|
|
2289
|
+
max-width: 80%;
|
|
2290
|
+
`,t.textContent=i.message||"消息发送失败",z.appendChild(t),z.scrollTop=z.scrollHeight}}),e.on("call_response",i=>{if(i.groupId===d._id){const t=document.createElement("div");t.style.cssText=`
|
|
2291
|
+
text-align: center;
|
|
2292
|
+
padding: 12px;
|
|
2293
|
+
margin: 16px auto;
|
|
2294
|
+
background: var(--success);
|
|
2295
|
+
color: white;
|
|
2296
|
+
border-radius: 8px;
|
|
2297
|
+
max-width: 80%;
|
|
2298
|
+
`,t.textContent=`${i.username} 已响应点名`,z.appendChild(t),z.scrollTop=z.scrollHeight}});const pe=()=>{if(!T){alert(C?"全体禁言中,无法发言":"你已被禁言");return}const i=W.value.trim();if(i)try{e.sendChatMessage(d._id,a.username,i),W.value=""}catch(t){console.error("发送消息失败:",t),alert("发送失败: "+t.message)}};T&&(F.addEventListener("click",pe),W.addEventListener("keypress",i=>{i.key==="Enter"&&!i.shiftKey&&(i.preventDefault(),pe())}));const m=document.querySelectorAll(".chat-tab"),u=document.querySelectorAll(".tab-content");m.forEach(i=>{i.addEventListener("click",()=>{const t=i.dataset.tab;m.forEach(r=>{r.dataset.tab===t?(r.style.background="var(--primary)",r.style.color="white"):(r.style.background="transparent",r.style.color="var(--text-primary)")}),u.forEach(r=>{r.dataset.content===t?r.style.display="block":r.style.display="none"}),t==="whiteboard"&&g()})});const y=document.getElementById("aiChat"),h=document.getElementById("aiInput"),M=document.getElementById("aiSendBtn"),p=async()=>{const i=h.value.trim();if(!i)return;const t=document.createElement("div");t.style.cssText=`
|
|
2299
|
+
background: var(--primary);
|
|
2300
|
+
color: white;
|
|
2301
|
+
padding: 12px 16px;
|
|
2302
|
+
border-radius: 12px;
|
|
2303
|
+
margin-bottom: 16px;
|
|
2304
|
+
max-width: 80%;
|
|
2305
|
+
margin-left: auto;
|
|
2306
|
+
word-wrap: break-word;
|
|
2307
|
+
`,t.textContent=i,y.appendChild(t),h.value="";const r=document.createElement("div");r.style.cssText=`
|
|
2308
|
+
background: var(--bg-tertiary);
|
|
2309
|
+
color: var(--text-secondary);
|
|
2310
|
+
padding: 12px 16px;
|
|
2311
|
+
border-radius: 12px;
|
|
2312
|
+
margin-bottom: 16px;
|
|
2313
|
+
max-width: 80%;
|
|
2314
|
+
font-style: italic;
|
|
2315
|
+
`,r.textContent="🤔 思考中...",y.appendChild(r),y.scrollTop=y.scrollHeight;try{const l=localStorage.getItem("token"),E=await(await fetch("http://localhost:3000/api/ai/ask",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${l}`},body:JSON.stringify({question:i,groupId:d==null?void 0:d._id})})).json();r.remove();const w=document.createElement("div");w.style.cssText=`
|
|
2316
|
+
background: var(--bg-tertiary);
|
|
2317
|
+
color: var(--text-primary);
|
|
2318
|
+
padding: 12px 16px;
|
|
2319
|
+
border-radius: 12px;
|
|
2320
|
+
margin-bottom: 16px;
|
|
2321
|
+
max-width: 80%;
|
|
2322
|
+
line-height: 1.6;
|
|
2323
|
+
word-wrap: break-word;
|
|
2324
|
+
`,w.textContent=E.answer||"抱歉,我无法回答这个问题。",y.appendChild(w),y.scrollTop=y.scrollHeight}catch(l){r.remove();const b=document.createElement("div");b.style.cssText=`
|
|
2325
|
+
background: var(--danger);
|
|
2326
|
+
color: white;
|
|
2327
|
+
padding: 12px 16px;
|
|
2328
|
+
border-radius: 12px;
|
|
2329
|
+
margin-bottom: 16px;
|
|
2330
|
+
max-width: 80%;
|
|
2331
|
+
`,b.textContent="抱歉,发生了错误: "+l.message,y.appendChild(b),y.scrollTop=y.scrollHeight}};M.addEventListener("click",p),h.addEventListener("keypress",i=>{i.key==="Enter"&&!i.shiftKey&&(i.preventDefault(),p())})}catch(v){console.error("加载群聊失败:",v),B.innerHTML=`
|
|
2332
|
+
<div class="empty-state" style="text-align: center; padding: 60px 20px;">
|
|
2333
|
+
<div style="font-size: 64px; margin-bottom: 20px;">⚠️</div>
|
|
2334
|
+
<h3 style="font-size: 24px; margin-bottom: 12px; color: var(--danger);">加载失败</h3>
|
|
2335
|
+
<p style="color: var(--text-secondary); margin-bottom: 24px;">${v.message}</p>
|
|
2336
|
+
<button class="btn-primary" onclick="location.reload()">重新加载</button>
|
|
2337
|
+
</div>
|
|
2338
|
+
`}}async function V(B){B.innerHTML=`
|
|
2339
|
+
<div class="view-header">
|
|
2340
|
+
<h2>🔍 搜索</h2>
|
|
2341
|
+
</div>
|
|
2342
|
+
<div class="search-container">
|
|
2343
|
+
<div class="search-box">
|
|
2344
|
+
<input type="text" id="searchInput" placeholder="搜索消息、文档、任务...">
|
|
2345
|
+
<button class="btn-primary" id="searchBtn">搜索</button>
|
|
2346
|
+
</div>
|
|
2347
|
+
<div class="search-filters">
|
|
2348
|
+
<label>
|
|
2349
|
+
<input type="checkbox" id="filterMessages" checked> 消息
|
|
2350
|
+
</label>
|
|
2351
|
+
<label>
|
|
2352
|
+
<input type="checkbox" id="filterDocuments" checked> 文档
|
|
2353
|
+
</label>
|
|
2354
|
+
<label>
|
|
2355
|
+
<input type="checkbox" id="filterTasks" checked> 任务
|
|
2356
|
+
</label>
|
|
2357
|
+
</div>
|
|
2358
|
+
<div class="search-results" id="searchResults"></div>
|
|
2359
|
+
</div>
|
|
2360
|
+
`;const $=document.getElementById("searchInput"),A=document.getElementById("searchBtn"),v=document.getElementById("searchResults"),k=async()=>{const C=$.value.trim();if(!C){v.innerHTML='<div class="empty-state">请输入搜索关键词</div>';return}const D={messages:document.getElementById("filterMessages").checked,documents:document.getElementById("filterDocuments").checked,tasks:document.getElementById("filterTasks").checked};v.innerHTML='<div class="loading">搜索中...</div>';try{const T=[];if(D.messages&&d)try{const z=await n.getGroupMessages(d._id);z.messages&&z.messages.filter(F=>F.content.toLowerCase().includes(C.toLowerCase())).forEach(F=>{T.push({type:"message",title:`消息 - ${F.username}`,content:F.content,time:F.timestamp,group:d.name})})}catch(z){console.error("搜索消息失败:",z)}if(D.documents)try{if(d){const z=await n.getDocuments(d._id);z.documents&&z.documents.filter(F=>F.title.toLowerCase().includes(C.toLowerCase())||F.content.toLowerCase().includes(C.toLowerCase())).forEach(F=>{T.push({type:"document",title:F.title,content:F.content.substring(0,200),time:F.updatedAt,id:F._id,group:d.name})})}}catch(z){console.error("搜索文档失败:",z)}if(D.tasks)try{const z=await n.getMyTasks();z.tasks&&z.tasks.filter(F=>F.title.toLowerCase().includes(C.toLowerCase())||F.description&&F.description.toLowerCase().includes(C.toLowerCase())).forEach(F=>{T.push({type:"task",title:F.title,content:F.description||"",time:F.updatedAt,id:F._id,status:F.status})})}catch(z){console.error("搜索任务失败:",z)}T.length===0?v.innerHTML='<div class="empty-state">未找到相关结果</div>':v.innerHTML=T.map(z=>`
|
|
2361
|
+
<div class="search-result-item">
|
|
2362
|
+
<div class="result-header">
|
|
2363
|
+
<span class="result-type">${{message:"💬",document:"📄",task:"📋"}[z.type]} ${z.type==="message"?"消息":z.type==="document"?"文档":"任务"}</span>
|
|
2364
|
+
<span class="result-time">${new Date(z.time).toLocaleString()}</span>
|
|
2365
|
+
</div>
|
|
2366
|
+
<h4>${ne(z.title,C)}</h4>
|
|
2367
|
+
<p>${ne(z.content,C)}</p>
|
|
2368
|
+
${z.group?`<span class="result-group">群组: ${z.group}</span>`:""}
|
|
2369
|
+
${z.status?`<span class="result-status">状态: ${oe(z.status)}</span>`:""}
|
|
2370
|
+
</div>
|
|
2371
|
+
`).join("")}catch(T){v.innerHTML=`<div class="empty-state">搜索失败: ${T.message}</div>`}};A.addEventListener("click",k),$.addEventListener("keypress",C=>{C.key==="Enter"&&k()})}function ne(B,$){if(!$)return B;const A=new RegExp(`(${$})`,"gi");return B.replace(A,"<mark>$1</mark>")}function oe(B){return{pending:"待处理",in_progress:"进行中",completed:"已完成",terminated:"已终止"}[B]||B}async function de(B){if(!d){B.innerHTML=`
|
|
2372
|
+
<div class="empty-state" style="text-align: center; padding: 60px 20px; background: var(--bg-secondary); border-radius: 16px; border: 2px dashed var(--border);">
|
|
2373
|
+
<div style="font-size: 64px; margin-bottom: 20px;">📚</div>
|
|
2374
|
+
<h3 style="font-size: 24px; margin-bottom: 12px; color: var(--text-primary);">知识库</h3>
|
|
2375
|
+
<p style="color: var(--text-secondary); margin-bottom: 24px; font-size: 16px;">请先在"我的群组"中选择一个群组</p>
|
|
2376
|
+
<button class="btn-primary" onclick="document.querySelector('[data-view=\\"groups\\"]').click()" style="padding: 12px 32px; font-size: 16px;">
|
|
2377
|
+
前往我的群组
|
|
2378
|
+
</button>
|
|
2379
|
+
</div>
|
|
2380
|
+
`;return}try{const $=localStorage.getItem("token"),A=await fetch(`http://localhost:3000/api/knowledge/group/${d._id}`,{headers:{Authorization:`Bearer ${$}`}});if(!A.ok)throw new Error(`HTTP ${A.status}: ${A.statusText}`);const v=await A.json();let k=[];Array.isArray(v)?k=v:v.data&&Array.isArray(v.data)?k=v.data:v.data&&v.data.knowledgeList&&Array.isArray(v.data.knowledgeList)?k=v.data.knowledgeList:v.items&&Array.isArray(v.items)?k=v.items:v.knowledge&&Array.isArray(v.knowledge)&&(k=v.knowledge),B.innerHTML=`
|
|
2381
|
+
<div class="view-header">
|
|
2382
|
+
<h2>📚 知识库 - ${d.name}</h2>
|
|
2383
|
+
<button class="btn-primary" id="createKnowledgeBtn">📝 创建知识条目</button>
|
|
2384
|
+
</div>
|
|
2385
|
+
<div class="knowledge-grid" id="knowledgeList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 20px;"></div>
|
|
2386
|
+
|
|
2387
|
+
<!-- 知识库模态框 -->
|
|
2388
|
+
<div id="knowledgeModal" class="modal hidden">
|
|
2389
|
+
<div class="modal-content">
|
|
2390
|
+
<div class="modal-header">
|
|
2391
|
+
<h3 id="modalTitle">创建知识条目</h3>
|
|
2392
|
+
<button class="modal-close" id="closeKnowledgeModal">×</button>
|
|
2393
|
+
</div>
|
|
2394
|
+
<form id="knowledgeForm">
|
|
2395
|
+
<div class="form-group">
|
|
2396
|
+
<label>📌 标题</label>
|
|
2397
|
+
<input type="text" name="title" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
|
|
2398
|
+
</div>
|
|
2399
|
+
<div class="form-group">
|
|
2400
|
+
<label>📝 内容</label>
|
|
2401
|
+
<textarea name="content" rows="6" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;"></textarea>
|
|
2402
|
+
</div>
|
|
2403
|
+
<div class="form-group">
|
|
2404
|
+
<label>🏷️ 标签(用逗号分隔)</label>
|
|
2405
|
+
<input type="text" name="tags" placeholder="例如: 学习,文档,教程" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
|
|
2406
|
+
</div>
|
|
2407
|
+
<div class="form-group" style="display: flex; align-items: center; gap: 10px; padding: 15px; background: var(--bg-tertiary); border-radius: 8px; margin-top: 15px;">
|
|
2408
|
+
<input type="checkbox" name="isShared" id="isSharedCheckbox" style="width: 20px; height: 20px; cursor: pointer;">
|
|
2409
|
+
<label for="isSharedCheckbox" style="margin: 0; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
2410
|
+
<span style="font-size: 18px;">🌐</span>
|
|
2411
|
+
<div>
|
|
2412
|
+
<div style="font-weight: 600; color: var(--text-primary);">共享到所有群组</div>
|
|
2413
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 2px;">开启后,此知识条目将对所有群组可见</div>
|
|
2414
|
+
</div>
|
|
2415
|
+
</label>
|
|
2416
|
+
</div>
|
|
2417
|
+
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
|
2418
|
+
<button type="submit" class="btn-primary" style="flex: 1;">保存</button>
|
|
2419
|
+
<button type="button" class="btn-secondary" id="cancelKnowledgeModal" style="flex: 1;">取消</button>
|
|
2420
|
+
</div>
|
|
2421
|
+
</form>
|
|
2422
|
+
</div>
|
|
2423
|
+
</div>
|
|
2424
|
+
`;const C=document.getElementById("knowledgeList");k.length===0?C.innerHTML='<div class="empty-state" style="grid-column: 1/-1;">暂无知识条目</div>':(k.forEach(D=>{var z,W;const T=document.createElement("div");T.className="knowledge-card",T.style.cssText="background: var(--bg-secondary); padding: 20px; border-radius: 12px; border: 1px solid var(--border); transition: transform 0.2s, box-shadow 0.2s; position: relative;",T.innerHTML=`
|
|
2425
|
+
${D.isShared?'<div style="position: absolute; top: 15px; right: 15px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; display: flex; align-items: center; gap: 4px;"><span>🌐</span><span>已共享</span></div>':""}
|
|
2426
|
+
<h3 style="margin: 0 0 10px 0; font-size: 18px; ${D.isShared?"padding-right: 80px;":""}">${D.title}</h3>
|
|
2427
|
+
<p style="color: var(--text-secondary); margin: 0 0 15px 0; line-height: 1.6;">${D.content.substring(0,150)}${D.content.length>150?"...":""}</p>
|
|
2428
|
+
<div class="knowledge-meta" style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 10px;">
|
|
2429
|
+
<span>👤 ${((z=D.author)==null?void 0:z.username)||((W=D.creator)==null?void 0:W.username)||"未知"}</span>
|
|
2430
|
+
<span style="margin-left: 15px;">📅 ${new Date(D.createdAt).toLocaleDateString()}</span>
|
|
2431
|
+
</div>
|
|
2432
|
+
${D.tags&&D.tags.length>0?`
|
|
2433
|
+
<div class="tags" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 15px;">
|
|
2434
|
+
${D.tags.map(F=>`<span class="tag" style="background: var(--primary); color: white; padding: 4px 10px; border-radius: 12px; font-size: 12px;">${F}</span>`).join("")}
|
|
2435
|
+
</div>
|
|
2436
|
+
`:""}
|
|
2437
|
+
<div style="display: flex; gap: 10px;">
|
|
2438
|
+
<button class="btn-secondary btn-sm" data-id="${D._id}" data-action="edit" style="flex: 1;">✏️ 编辑</button>
|
|
2439
|
+
<button class="btn-danger btn-sm" data-id="${D._id}" data-action="delete" style="flex: 1;">🗑️ 删除</button>
|
|
2440
|
+
</div>
|
|
2441
|
+
`,T.onmouseenter=()=>{T.style.transform="translateY(-4px)",T.style.boxShadow="0 8px 16px rgba(0,0,0,0.1)"},T.onmouseleave=()=>{T.style.transform="translateY(0)",T.style.boxShadow="none"},C.appendChild(T)}),document.querySelectorAll('[data-action="edit"]').forEach(D=>{D.addEventListener("click",async()=>{var z;const T=k.find(W=>W._id===D.dataset.id);document.getElementById("modalTitle").textContent="编辑知识条目",document.querySelector('[name="title"]').value=T.title,document.querySelector('[name="content"]').value=T.content,document.querySelector('[name="tags"]').value=((z=T.tags)==null?void 0:z.join(", "))||"",document.getElementById("isSharedCheckbox").checked=T.isShared||!1,document.getElementById("knowledgeForm").dataset.editId=T._id,document.getElementById("knowledgeModal").classList.remove("hidden")})}),document.querySelectorAll('[data-action="delete"]').forEach(D=>{D.addEventListener("click",async()=>{if(confirm("确定要删除这个知识条目吗?"))try{await fetch(`http://localhost:3000/api/knowledge/${D.dataset.id}`,{method:"DELETE",headers:{Authorization:`Bearer ${$}`}}),alert("删除成功!"),await de(B)}catch(T){alert("删除失败: "+T.message)}})})),document.getElementById("createKnowledgeBtn").addEventListener("click",()=>{document.getElementById("modalTitle").textContent="创建知识条目",document.getElementById("knowledgeForm").reset(),delete document.getElementById("knowledgeForm").dataset.editId,document.getElementById("knowledgeModal").classList.remove("hidden")}),document.getElementById("closeKnowledgeModal").addEventListener("click",()=>{document.getElementById("knowledgeModal").classList.add("hidden")}),document.getElementById("cancelKnowledgeModal").addEventListener("click",()=>{document.getElementById("knowledgeModal").classList.add("hidden")}),document.getElementById("knowledgeForm").addEventListener("submit",async D=>{D.preventDefault();const T=new FormData(D.target),z={title:T.get("title"),content:T.get("content"),tags:T.get("tags").split(",").map(W=>W.trim()).filter(W=>W),groupId:d._id,isShared:document.getElementById("isSharedCheckbox").checked};try{const W=D.target.dataset.editId,F=W?`http://localhost:3000/api/knowledge/${W}`:"http://localhost:3000/api/knowledge";if(!(await fetch(F,{method:W?"PUT":"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${$}`},body:JSON.stringify(z)})).ok)throw new Error("操作失败");alert(W?"更新成功!":"创建成功!"),document.getElementById("knowledgeModal").classList.add("hidden"),await de(B)}catch(W){alert("操作失败: "+W.message)}})}catch($){console.error("加载知识库失败:",$),B.innerHTML=`
|
|
2442
|
+
<div class="view-header">
|
|
2443
|
+
<h2>📚 知识库 - ${d.name}</h2>
|
|
2444
|
+
</div>
|
|
2445
|
+
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
|
|
2446
|
+
<div style="font-size: 48px; margin-bottom: 16px;">⚠️</div>
|
|
2447
|
+
<h3 style="margin-bottom: 8px; color: var(--danger);">加载失败</h3>
|
|
2448
|
+
<p style="color: var(--text-secondary); margin-bottom: 16px;">${$.message}</p>
|
|
2449
|
+
<button class="btn-primary" onclick="location.reload()">重新加载</button>
|
|
2450
|
+
</div>
|
|
2451
|
+
`}}L("groups")}class hn{constructor(){this.authService=new ze,this.wsService=new jt,this.currentUser=null,this.init()}async init(){const e=localStorage.getItem("token");if(e)try{this.currentUser=await this.authService.getCurrentUser(),this.wsService.connect(e),this.renderDashboard()}catch(o){console.error("认证失败:",o),this.renderLogin()}else this.renderLogin()}renderLogin(){_t(async(e,o)=>{this.currentUser=e,localStorage.setItem("token",o),this.wsService.connect(o),this.renderDashboard()})}renderDashboard(){this.currentUser.role==="admin"?mn(this.currentUser,this.wsService):bn(this.currentUser,this.wsService)}}new hn;
|