koishi-plugin-driftbottle-with-webui 0.0.2

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/index.js ADDED
@@ -0,0 +1 @@
1
+ import{defineComponent as z,ref as h,watch as F,onMounted as O,resolveComponent as S,openBlock as o,createBlock as P,withCtx as M,createVNode as B,createElementVNode as t,normalizeClass as m,createElementBlock as i,Fragment as C,renderList as x,toDisplayString as a,createCommentVNode as r,withModifiers as q}from"vue";import{send as f}from"@koishijs/client";const G={class:"admin-tabs"},J={class:"filter-section"},K={class:"radio-group"},Q={key:0,class:"table-container"},R={class:"admin-table"},U=["innerHTML"],W={class:"text-muted"},X=["onClick"],Y={key:0},Z={key:1,class:"table-container"},tt={class:"admin-table"},et={class:"text-small text-muted"},nt=["innerHTML"],st={class:"text-muted"},lt={class:"action-cell"},ot=["onClick"],it=["onClick"],at=["onClick"],dt={key:0},rt={class:"standard-dialog"},ut={class:"dialog-header"},ct={class:"dialog-body"},vt={class:"info-grid"},bt={class:"info-item"},mt={class:"info-item"},ft={class:"info-item"},_t={class:"info-item"},pt=["innerHTML"],kt={class:"two-col-layout"},ht={class:"col-section"},gt={class:"section-title"},yt={class:"list-box"},wt={class:"item-meta"},Ct={class:"text-small text-muted"},xt=["innerHTML"],$t={key:0,class:"empty-text"},Lt={class:"col-section"},Mt={class:"list-box"},Tt={class:"text-small text-muted"},Dt=["innerHTML"],Ht={key:0,class:"empty-text"},St={class:"dialog-footer"},Bt={class:"left-actions"},It={class:"right-actions"},At=z({__name:"page",setup(_){const d=h("bottles"),u=h(0),b=h([]),p=h([]),l=h(null),g=s=>(s||"").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/&lt;(?:img|image)\s+(?:url|src)="([^"]+)"[^&]*&gt;/gi,(y,L)=>{const v=L.match(/[\\/]([^\\/]+)$/),k=v?v[1]:"";return k?`<span class="img-wrapper"><img src="/driftbottle/image/${k}" alt="image" /></span>`:y}),I=s=>s.action==="throw"?`用户 <b>${s.username}</b> 发布了漂流瓶`:s.action==="fish"?`用户 <b>${s.username}</b> 进行了打捞`:s.action==="comment"?`用户 <b>${s.username}</b> 发表了评论`:s.action==="approve"?'<span style="color:var(--success)">管理员通过了审核</span>':s.action==="reject"?'<span style="color:var(--error)">管理员驳回了内容</span>':`用户 ${s.username} 执行了操作`,A=s=>s===0?"待审核":s===1?"已通过":s===2?"已驳回":"未知",E=s=>s===0?"text-warning":s===1?"text-success":s===2?"text-danger":"",N=s=>{l.value=s},c=async()=>{d.value==="bottles"?(b.value=await f("driftbottle/bottles",u.value),l.value&&(l.value=b.value.find(s=>s.id===l.value.id)||null)):p.value=await f("driftbottle/comments",u.value)},$=s=>{u.value=s,c()},T=async(s,e)=>{await f("driftbottle/review-bottle",s,e),c()},D=async(s,e)=>{await f("driftbottle/review-comment",s,e),c()},V=async s=>{confirm("确定要永久删除该漂流瓶及所有关联评论吗?此操作不可逆。")&&(await f("driftbottle/delete-bottle",s),l.value=null,c())},j=async s=>{confirm("确定要删除此条评论吗?此操作不可逆。")&&(await f("driftbottle/delete-comment",s),c())};return F(d,()=>{c()}),O(()=>{c()}),(s,e)=>{const y=S("k-card"),L=S("k-layout");return o(),P(L,{class:"driftbottle-admin"},{default:M(()=>[B(y,{class:"admin-header"},{default:M(()=>[e[12]||(e[12]=t("h2",null,"漂流瓶管理后台",-1)),t("div",G,[t("button",{class:m(["tab-btn",{active:d.value==="bottles"}]),onClick:e[0]||(e[0]=v=>d.value="bottles")},"漂流瓶列表",2),t("button",{class:m(["tab-btn",{active:d.value==="comments"}]),onClick:e[1]||(e[1]=v=>d.value="comments")},"评论管理",2)])]),_:1}),B(y,{class:"admin-content"},{default:M(()=>{var v,k,H;return[t("div",J,[e[13]||(e[13]=t("span",{class:"filter-label"},"状态筛选:",-1)),t("div",K,[t("button",{class:m(["radio-btn",{active:u.value===0}]),onClick:e[2]||(e[2]=n=>$(0))},"待审核",2),t("button",{class:m(["radio-btn",{active:u.value===1}]),onClick:e[3]||(e[3]=n=>$(1))},"已通过",2),t("button",{class:m(["radio-btn",{active:u.value===2}]),onClick:e[4]||(e[4]=n=>$(2))},"已驳回",2)])]),d.value==="bottles"?(o(),i("div",Q,[t("table",R,[e[15]||(e[15]=t("thead",null,[t("tr",null,[t("th",{width:"80"},"ID"),t("th",{width:"120"},"发布者"),t("th",null,"内容预览"),t("th",{width:"80"},"打捞数"),t("th",{width:"160"},"发布时间"),t("th",{width:"120"},"操作")])],-1)),t("tbody",null,[(o(true),i(C,null,x(b.value,n=>(o(),i("tr",{key:n.id},[t("td",null,"#"+a(n.id),1),t("td",null,a(n.username),1),t("td",null,[t("div",{class:"text-truncate",innerHTML:g(n.content)},null,8,U)]),t("td",null,a(n.fishCount||0),1),t("td",W,a(new Date(n.createdAt).toLocaleString()),1),t("td",null,[t("button",{class:"btn btn-text",onClick:w=>N(n)},"查看 / 审核",8,X)])]))),128)),b.value.length===0?(o(),i("tr",Y,[...e[14]||(e[14]=[t("td",{colspan:"6",class:"empty-text"},"暂无数据",-1)])])):r("v-if",true)])])])):r("v-if",true),d.value==="comments"?(o(),i("div",Z,[t("table",tt,[e[17]||(e[17]=t("thead",null,[t("tr",null,[t("th",{width:"80"},"评论ID"),t("th",{width:"100"},"所属瓶ID"),t("th",{width:"120"},"评论者"),t("th",null,"内容"),t("th",{width:"160"},"评论时间"),t("th",{width:"200"},"操作")])],-1)),t("tbody",null,[(o(true),i(C,null,x(p.value,n=>(o(),i("tr",{key:n.id},[t("td",null,"#"+a(n.id),1),t("td",null,[t("span",{class:"link-text",onClick:e[5]||(e[5]=w=>d.value="bottles")},"#"+a(n.bottleId),1)]),t("td",null,[t("div",null,a(n.username),1),t("div",et,a(n.userId),1)]),t("td",null,[t("div",{class:"text-truncate",innerHTML:g(n.content)},null,8,nt)]),t("td",st,a(new Date(n.createdAt).toLocaleString()),1),t("td",lt,[n.status!==1?(o(),i("button",{key:0,class:"btn btn-success btn-small",onClick:w=>D(n.id,1)},"通过",8,ot)):r("v-if",true),n.status!==2?(o(),i("button",{key:1,class:"btn btn-warning btn-small",onClick:w=>D(n.id,2)},"驳回",8,it)):r("v-if",true),t("button",{class:"btn btn-danger btn-small",onClick:w=>j(n.id)},"删除",8,at)])]))),128)),p.value.length===0?(o(),i("tr",dt,[...e[16]||(e[16]=[t("td",{colspan:"6",class:"empty-text"},"暂无数据",-1)])])):r("v-if",true)])])])):r("v-if",true),l.value?(o(),i("div",{key:2,class:"modal-mask",onClick:e[11]||(e[11]=q(n=>l.value=null,["self"]))},[t("div",rt,[t("div",ut,[t("h3",null,"漂流瓶详情 (ID: #"+a(l.value.id)+")",1),t("button",{class:"icon-close",onClick:e[6]||(e[6]=n=>l.value=null)},"×")]),t("div",ct,[t("div",vt,[t("div",bt,[e[18]||(e[18]=t("span",{class:"label"},"发布者:",-1)),t("span",null,a(l.value.username),1)]),t("div",mt,[e[19]||(e[19]=t("span",{class:"label"},"发布时间:",-1)),t("span",null,a(new Date(l.value.createdAt).toLocaleString()),1)]),t("div",ft,[e[20]||(e[20]=t("span",{class:"label"},"当前状态:",-1)),t("span",{class:m(E(l.value.status))},a(A(l.value.status)),3)]),t("div",_t,[e[21]||(e[21]=t("span",{class:"label"},"打捞次数:",-1)),t("span",null,a(l.value.fishCount||0)+" 次",1)])]),e[23]||(e[23]=t("div",{class:"section-title"},"漂流瓶内容",-1)),t("div",{class:"content-box",innerHTML:g(l.value.content)},null,8,pt),t("div",kt,[t("div",ht,[t("div",gt,"所属评论 ("+a(((v=l.value.comments)==null?void 0:v.length)||0)+")",1),t("div",yt,[(o(true),i(C,null,x(l.value.comments,n=>(o(),i("div",{class:"list-item",key:n.id},[t("div",wt,[t("strong",null,a(n.username),1),t("span",Ct,a(new Date(n.createdAt).toLocaleString()),1)]),t("div",{class:"item-content",innerHTML:g(n.content)},null,8,xt)]))),128)),(k=l.value.comments)!=null&&k.length?r("v-if",true):(o(),i("div",$t,"暂无评论"))])]),t("div",Lt,[e[22]||(e[22]=t("div",{class:"section-title"},"操作日志",-1)),t("div",Mt,[(o(true),i(C,null,x(l.value.logs,n=>(o(),i("div",{class:"log-item",key:n.id},[t("span",Tt,"["+a(new Date(n.createdAt).toLocaleString())+"]",1),t("span",{class:"log-text",innerHTML:I(n)},null,8,Dt)]))),128)),(H=l.value.logs)!=null&&H.length?r("v-if",true):(o(),i("div",Ht,"暂无日志"))])])])]),t("div",St,[t("div",Bt,[t("button",{class:"btn btn-danger",onClick:e[7]||(e[7]=n=>V(l.value.id))},"删除此瓶")]),t("div",It,[t("button",{class:"btn",onClick:e[8]||(e[8]=n=>l.value=null)},"取消"),l.value.status!==2?(o(),i("button",{key:0,class:"btn btn-warning",onClick:e[9]||(e[9]=n=>T(l.value.id,2))},"驳回")):r("v-if",true),l.value.status!==1?(o(),i("button",{key:1,class:"btn btn-primary",onClick:e[10]||(e[10]=n=>T(l.value.id,1))},"通过审核")):r("v-if",true)])])])])):r("v-if",true)]}),_:1})]),_:1})}}}),Et=(_,d)=>{const u=_.__vccOpts||_;for(const[b,p]of d)u[b]=p;return u},Nt=Et(At,[["__scopeId","data-v-5ed8fd39"]]),zt=_=>{_.page({name:"漂流瓶管理",path:"/driftbottle",component:Nt})};export{zt as default};
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .driftbottle-admin[data-v-5ed8fd39]{--primary: #409eff;--success: #67c23a;--warning: #e6a23c;--danger: #f56c6c;--text-main: var(--fg1, #303133);--text-regular: var(--fg2, #606266);--text-muted: var(--fg3, #909399);--border-color: var(--border, #dcdfe6);--bg-color: var(--bg1, #ffffff);--bg-page: var(--bg2, #f5f7fa);display:flex;flex-direction:column;gap:16px;color:var(--text-main);font-size:14px}.admin-header[data-v-5ed8fd39]{display:flex;justify-content:space-between;align-items:center;padding:16px 24px!important}.admin-header h2[data-v-5ed8fd39]{margin:0;font-size:18px;font-weight:600}.admin-tabs[data-v-5ed8fd39]{display:flex;gap:8px;background:var(--bg-page);padding:4px;border-radius:6px}.tab-btn[data-v-5ed8fd39]{border:none;background:transparent;padding:8px 16px;font-size:14px;border-radius:4px;cursor:pointer;color:var(--text-regular);transition:all .2s}.tab-btn[data-v-5ed8fd39]:hover{color:var(--primary)}.tab-btn.active[data-v-5ed8fd39]{background:var(--bg-color);color:var(--primary);font-weight:700;box-shadow:0 2px 4px #0000000d}.admin-content[data-v-5ed8fd39]{padding:24px!important;min-height:500px}.filter-section[data-v-5ed8fd39]{display:flex;align-items:center;margin-bottom:20px}.filter-label[data-v-5ed8fd39]{margin-right:12px;color:var(--text-regular)}.radio-group[data-v-5ed8fd39]{display:flex;border:1px solid var(--border-color);border-radius:4px;overflow:hidden}.radio-btn[data-v-5ed8fd39]{border:none;background:var(--bg-color);padding:6px 16px;border-right:1px solid var(--border-color);cursor:pointer;color:var(--text-regular)}.radio-btn[data-v-5ed8fd39]:last-child{border-right:none}.radio-btn.active[data-v-5ed8fd39]{background:var(--primary);color:#fff}.table-container[data-v-5ed8fd39]{overflow-x:auto;border:1px solid var(--border-color);border-radius:4px}.admin-table[data-v-5ed8fd39]{width:100%;border-collapse:collapse;text-align:left}.admin-table th[data-v-5ed8fd39],.admin-table td[data-v-5ed8fd39]{padding:12px 16px;border-bottom:1px solid var(--border-color)}.admin-table th[data-v-5ed8fd39]{background-color:var(--bg-page);color:var(--text-regular);font-weight:500;white-space:nowrap}.admin-table tbody tr[data-v-5ed8fd39]:hover{background-color:var(--bg-page)}.admin-table tbody tr:last-child td[data-v-5ed8fd39]{border-bottom:none}.text-truncate[data-v-5ed8fd39]{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.5}[data-v-5ed8fd39] .text-truncate img{height:20px;vertical-align:middle;border-radius:2px}.empty-text[data-v-5ed8fd39]{text-align:center;padding:40px!important;color:var(--text-muted)}.link-text[data-v-5ed8fd39]{color:var(--primary);cursor:pointer}.link-text[data-v-5ed8fd39]:hover{text-decoration:underline}.btn[data-v-5ed8fd39]{border:1px solid var(--border-color);background:var(--bg-color);color:var(--text-regular);padding:6px 16px;border-radius:4px;cursor:pointer;font-size:14px;transition:all .2s}.btn[data-v-5ed8fd39]:hover{border-color:var(--primary);color:var(--primary)}.btn-small[data-v-5ed8fd39]{padding:4px 10px;font-size:12px}.btn-primary[data-v-5ed8fd39]{background:var(--primary);color:#fff;border-color:var(--primary)}.btn-primary[data-v-5ed8fd39]:hover{background:#66b1ff;border-color:#66b1ff;color:#fff}.btn-success[data-v-5ed8fd39]{background:var(--success);color:#fff;border-color:var(--success)}.btn-success[data-v-5ed8fd39]:hover{background:#85ce61;color:#fff;border-color:#85ce61}.btn-warning[data-v-5ed8fd39]{background:var(--warning);color:#fff;border-color:var(--warning)}.btn-warning[data-v-5ed8fd39]:hover{background:#ebb563;color:#fff;border-color:#ebb563}.btn-danger[data-v-5ed8fd39]{background:var(--danger);color:#fff;border-color:var(--danger)}.btn-danger[data-v-5ed8fd39]:hover{background:#f78989;color:#fff;border-color:#f78989}.btn-text[data-v-5ed8fd39]{border:none;background:transparent;color:var(--primary);padding:0}.btn-text[data-v-5ed8fd39]:hover{background:transparent;text-decoration:underline}.action-cell[data-v-5ed8fd39]{display:flex;gap:8px;flex-wrap:wrap}.text-small[data-v-5ed8fd39]{font-size:12px}.text-muted[data-v-5ed8fd39]{color:var(--text-muted)}.text-success[data-v-5ed8fd39]{color:var(--success)}.text-warning[data-v-5ed8fd39]{color:var(--warning)}.text-danger[data-v-5ed8fd39]{color:var(--danger)}.modal-mask[data-v-5ed8fd39]{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;z-index:2000;display:flex;align-items:center;justify-content:center}.standard-dialog[data-v-5ed8fd39]{width:800px;max-width:90vw;max-height:85vh;background:var(--bg-color);border-radius:6px;display:flex;flex-direction:column;box-shadow:0 12px 32px #0000001a}.dialog-header[data-v-5ed8fd39]{padding:16px 20px;border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center}.dialog-header h3[data-v-5ed8fd39]{margin:0;font-size:16px}.icon-close[data-v-5ed8fd39]{border:none;background:transparent;font-size:20px;cursor:pointer;color:var(--text-muted)}.icon-close[data-v-5ed8fd39]:hover{color:var(--danger)}.dialog-body[data-v-5ed8fd39]{padding:20px;overflow-y:auto;flex:1}.dialog-footer[data-v-5ed8fd39]{padding:16px 20px;border-top:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center}.right-actions[data-v-5ed8fd39]{display:flex;gap:12px}.info-grid[data-v-5ed8fd39]{display:grid;grid-template-columns:1fr 1fr;gap:12px;background:var(--bg-page);padding:16px;border-radius:4px;margin-bottom:20px}.info-item .label[data-v-5ed8fd39]{color:var(--text-muted)}.section-title[data-v-5ed8fd39]{font-weight:600;margin:20px 0 10px;padding-left:8px;border-left:3px solid var(--primary);line-height:1}.content-box[data-v-5ed8fd39]{border:1px solid var(--border-color);padding:16px;border-radius:4px;line-height:1.6;min-height:80px}[data-v-5ed8fd39] .content-box img{max-width:100%;border-radius:4px;margin-top:10px}.two-col-layout[data-v-5ed8fd39]{display:flex;gap:20px;margin-top:20px}.col-section[data-v-5ed8fd39]{flex:1;min-width:0}.list-box[data-v-5ed8fd39]{border:1px solid var(--border-color);border-radius:4px;height:250px;overflow-y:auto}.list-item[data-v-5ed8fd39]{padding:12px;border-bottom:1px solid var(--border-color)}.list-item[data-v-5ed8fd39]:last-child{border-bottom:none}.item-meta[data-v-5ed8fd39]{display:flex;justify-content:space-between;margin-bottom:6px}.item-content[data-v-5ed8fd39]{line-height:1.4}[data-v-5ed8fd39] .item-content img{height:30px;vertical-align:middle}.log-item[data-v-5ed8fd39]{padding:10px 12px;border-bottom:1px solid var(--bg-page)}.log-item .log-text[data-v-5ed8fd39]{margin-left:8px}
package/lib/html.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function renderBottleHtml(bottle: any, comments: any[]): string;
2
+ export declare function renderLogsHtml(logs: any[]): string;
package/lib/html.js ADDED
@@ -0,0 +1,216 @@
1
+ export function renderBottleHtml(bottle, comments) {
2
+ const renderedComments = comments.map(c => `
3
+ <div class="comment-item">
4
+ <span><strong>${c.username}:</strong></span>
5
+ <span class="content-text">${c.content.replace(/\n/g, '<br/>')}</span>
6
+ <span class="comment-time">${new Date(c.createdAt).toLocaleString()}</span>
7
+ </div>
8
+ `).join('');
9
+ return `
10
+ <!DOCTYPE html>
11
+ <html lang="zh-CN">
12
+ <head>
13
+ <meta charset="UTF-8">
14
+ <style>
15
+ body {
16
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
17
+ background: transparent;
18
+ padding: 40px;
19
+ margin: 0;
20
+ width: 600px;
21
+ }
22
+ .bottle-container {
23
+ position: relative;
24
+ border: 1px dashed #b0bec5;
25
+ border-radius: 10px;
26
+ padding: 50px 30px 30px 30px;
27
+ background-color: #fff8e1;
28
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
29
+ }
30
+ .sticky-note {
31
+ position: absolute;
32
+ top: -20px;
33
+ right: -20px;
34
+ width: 60px;
35
+ height: 60px;
36
+ background-color: #ffeb3b;
37
+ transform: rotate(15deg);
38
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
39
+ z-index: 1;
40
+ border: 1px solid #fbc02d;
41
+ }
42
+ .license-plate {
43
+ position: absolute;
44
+ top: 10px;
45
+ left: 15px;
46
+ font-size: 20px;
47
+ color: #d32f2f;
48
+ font-weight: bold;
49
+ }
50
+ .bottle-content {
51
+ font-size: 18px;
52
+ color: #333;
53
+ line-height: 1.6;
54
+ margin-top: 10px;
55
+ white-space: pre-wrap;
56
+ }
57
+ .bottle-content img {
58
+ max-width: 100%;
59
+ border-radius: 8px;
60
+ margin-top: 10px;
61
+ }
62
+ .meta {
63
+ margin-top: 20px;
64
+ padding-top: 10px;
65
+ border-top: 1px dashed #cfd8dc;
66
+ display: flex;
67
+ justify-content: space-between;
68
+ color: #7b5e57;
69
+ font-size: 14px;
70
+ }
71
+ .comments {
72
+ margin-top: 20px;
73
+ }
74
+ .comment-item {
75
+ background-color: #f0f4c3;
76
+ padding: 12px;
77
+ border-radius: 8px;
78
+ margin-bottom: 10px;
79
+ font-size: 15px;
80
+ color: #444;
81
+ border: 1px solid #e2e6b3;
82
+ }
83
+ .comment-time { float: right; font-size: 12px; color: #888; }
84
+ .content-text img { height: 30px; vertical-align: middle; margin: 0 5px; }
85
+ </style>
86
+ </head>
87
+ <body>
88
+ <div class="bottle-container">
89
+ <div class="sticky-note"></div>
90
+ <div class="license-plate">No.${String(bottle.id).padStart(5, '0')}</div>
91
+
92
+ <div class="bottle-content">
93
+ ${bottle.content}
94
+ </div>
95
+
96
+ <div class="meta">
97
+ <span>作者:${bottle.username}</span>
98
+ <span>${new Date(bottle.createdAt).toLocaleString()}</span>
99
+ </div>
100
+
101
+ ${comments.length ? `
102
+ <div class="comments">
103
+ <h3 style="color: #6d4c41; font-size: 16px;">评论区 (${comments.length})</h3>
104
+ ${renderedComments}
105
+ </div>
106
+ ` : ''}
107
+ </div>
108
+ </body>
109
+ </html>
110
+ `;
111
+ }
112
+ export function renderLogsHtml(logs) {
113
+ const renderedLogs = logs.map((log, idx) => {
114
+ let actionHtml = '';
115
+ if (log.action === 'throw')
116
+ actionHtml = `扔出了一只漂流瓶 (Bottle ID: <span>${log.bottleId}</span>)`;
117
+ else if (log.action === 'fish')
118
+ actionHtml = `捞到了一只漂流瓶 (Bottle ID: <span>${log.bottleId}</span>)`;
119
+ else if (log.action === 'comment')
120
+ actionHtml = `对瓶子 (Bottle ID: <span>${log.bottleId}</span>) 发表了评论`;
121
+ else if (log.action === 'approve')
122
+ actionHtml = `管理员 <span style="color:#2e7d32">通过了</span> 漂流瓶 (Bottle ID: <span>${log.bottleId}</span>)`;
123
+ else if (log.action === 'reject')
124
+ actionHtml = `管理员 <span style="color:#d32f2f">驳回了</span> 漂流瓶 (Bottle ID: <span>${log.bottleId}</span>)`;
125
+ // Check if the log belongs to the owner of the timeline, or someone else doing it.
126
+ // However, the username is already populated.
127
+ return `
128
+ <div class="timeline-item">
129
+ <div class="bubble-content ${idx === 0 ? 'new' : ''}">
130
+ <div class="event"><strong>${log.username}</strong> ${actionHtml}</div>
131
+ <div class="time">${new Date(log.createdAt).toLocaleString()}</div>
132
+ </div>
133
+ </div>
134
+ `;
135
+ }).join('');
136
+ return `
137
+ <!DOCTYPE html>
138
+ <html lang="zh-CN">
139
+ <head>
140
+ <meta charset="UTF-8">
141
+ <style>
142
+ body {
143
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
144
+ background: transparent;
145
+ padding: 40px;
146
+ margin: 0;
147
+ width: 600px;
148
+ }
149
+ .timeline-container {
150
+ background-color: #fff8e1;
151
+ border: 1px dashed #b0bec5;
152
+ border-radius: 10px;
153
+ padding: 30px;
154
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
155
+ }
156
+ .title {
157
+ text-align: center;
158
+ font-size: 22px;
159
+ font-weight: bold;
160
+ color: #331e04;
161
+ border-bottom: 2px solid #211a12;
162
+ padding-bottom: 15px;
163
+ margin-bottom: 20px;
164
+ }
165
+ .timeline {
166
+ position: relative;
167
+ margin-left: 20px;
168
+ border-left: 2px dashed #443c3c;
169
+ padding-left: 25px;
170
+ }
171
+ .timeline-item {
172
+ margin-bottom: 20px;
173
+ position: relative;
174
+ }
175
+ .timeline-item:before {
176
+ content: '';
177
+ position: absolute;
178
+ left: -35px;
179
+ top: 15px;
180
+ width: 0; height: 0;
181
+ border: 8px solid transparent;
182
+ border-left-color: #331e04;
183
+ }
184
+ .bubble-content {
185
+ border: 1px solid #ccc;
186
+ background-image: linear-gradient(#ffffff 80%, #fdfef4);
187
+ border-radius: 6px;
188
+ padding: 12px 16px;
189
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
190
+ position: relative;
191
+ }
192
+ .bubble-content.new::before {
193
+ content: '新!';
194
+ position: absolute;
195
+ top: -15px;
196
+ right: 10px;
197
+ color: #00b62a;
198
+ font-weight: bold;
199
+ font-size: 18px;
200
+ }
201
+ .event { color: #331e04; padding-bottom: 6px; border-bottom: 1px dashed #ccc; font-size: 15px; }
202
+ .event span { font-weight: bold; color: #ff5722; }
203
+ .time { margin-top: 8px; color: #888; font-size: 13px; }
204
+ </style>
205
+ </head>
206
+ <body>
207
+ <div class="timeline-container">
208
+ <div class="title">漂流瓶航海日志</div>
209
+ <div class="timeline">
210
+ ${logs.length ? renderedLogs : '<div style="text-align: center; color: #888; margin-top:20px;">暂无航海记录</div>'}
211
+ </div>
212
+ </div>
213
+ </body>
214
+ </html>
215
+ `;
216
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "driftbottle-with-webui";
3
+ export interface Config {
4
+ dailyLimit: number;
5
+ cooldown: number;
6
+ enableQQNativeMarkdown: boolean;
7
+ enableQQInlineCmd: boolean;
8
+ }
9
+ export declare const Config: Schema<Config>;
10
+ export declare const inject: string[];
11
+ declare module 'koishi' {
12
+ interface Tables {
13
+ driftbottle: DriftBottle;
14
+ driftbottle_comment: DriftBottleComment;
15
+ driftbottle_usage: DriftBottleUsage;
16
+ driftbottle_log: DriftBottleLog;
17
+ }
18
+ }
19
+ export interface DriftBottleLog {
20
+ id: number;
21
+ bottleId: number;
22
+ userId: string;
23
+ username: string;
24
+ action: string;
25
+ content: string;
26
+ createdAt: Date;
27
+ }
28
+ export interface DriftBottle {
29
+ id: number;
30
+ userId: string;
31
+ platform: string;
32
+ username: string;
33
+ content: string;
34
+ status: number;
35
+ createdAt: Date;
36
+ }
37
+ export interface DriftBottleComment {
38
+ id: number;
39
+ bottleId: number;
40
+ userId: string;
41
+ platform: string;
42
+ username: string;
43
+ content: string;
44
+ status: number;
45
+ createdAt: Date;
46
+ }
47
+ export interface DriftBottleUsage {
48
+ id: number;
49
+ userId: string;
50
+ platform: string;
51
+ date: string;
52
+ count: number;
53
+ lastUsedAt: Date;
54
+ }
55
+ declare module '@koishijs/plugin-console' {
56
+ interface Events {
57
+ 'driftbottle/bottles'(status: number): Promise<(DriftBottle & {
58
+ comments: DriftBottleComment[];
59
+ logs: DriftBottleLog[];
60
+ fishCount: number;
61
+ })[]>;
62
+ 'driftbottle/comments'(status: number): Promise<DriftBottleComment[]>;
63
+ 'driftbottle/review-bottle'(id: number, status: number): Promise<void>;
64
+ 'driftbottle/review-comment'(id: number, status: number): Promise<void>;
65
+ 'driftbottle/delete-bottle'(id: number): Promise<void>;
66
+ 'driftbottle/delete-comment'(id: number): Promise<void>;
67
+ }
68
+ }
69
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,647 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
+ var __export = (target, all) => {
7
+ for (var name2 in all)
8
+ __defProp(target, name2, { get: all[name2], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Config: () => Config,
24
+ apply: () => apply,
25
+ inject: () => inject,
26
+ name: () => name
27
+ });
28
+ module.exports = __toCommonJS(src_exports);
29
+ var import_koishi = require("koishi");
30
+
31
+ // src/html.ts
32
+ function renderBottleHtml(bottle, comments) {
33
+ const renderedComments = comments.map((c) => `
34
+ <div class="comment-item">
35
+ <span><strong>${c.username}:</strong></span>
36
+ <span class="content-text">${c.content.replace(/\n/g, "<br/>")}</span>
37
+ <span class="comment-time">${new Date(c.createdAt).toLocaleString()}</span>
38
+ </div>
39
+ `).join("");
40
+ return `
41
+ <!DOCTYPE html>
42
+ <html lang="zh-CN">
43
+ <head>
44
+ <meta charset="UTF-8">
45
+ <style>
46
+ body {
47
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
48
+ background: transparent;
49
+ padding: 40px;
50
+ margin: 0;
51
+ width: 600px;
52
+ }
53
+ .bottle-container {
54
+ position: relative;
55
+ border: 1px dashed #b0bec5;
56
+ border-radius: 10px;
57
+ padding: 50px 30px 30px 30px;
58
+ background-color: #fff8e1;
59
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
60
+ }
61
+ .sticky-note {
62
+ position: absolute;
63
+ top: -20px;
64
+ right: -20px;
65
+ width: 60px;
66
+ height: 60px;
67
+ background-color: #ffeb3b;
68
+ transform: rotate(15deg);
69
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
70
+ z-index: 1;
71
+ border: 1px solid #fbc02d;
72
+ }
73
+ .license-plate {
74
+ position: absolute;
75
+ top: 10px;
76
+ left: 15px;
77
+ font-size: 20px;
78
+ color: #d32f2f;
79
+ font-weight: bold;
80
+ }
81
+ .bottle-content {
82
+ font-size: 18px;
83
+ color: #333;
84
+ line-height: 1.6;
85
+ margin-top: 10px;
86
+ white-space: pre-wrap;
87
+ }
88
+ .bottle-content img {
89
+ max-width: 100%;
90
+ border-radius: 8px;
91
+ margin-top: 10px;
92
+ }
93
+ .meta {
94
+ margin-top: 20px;
95
+ padding-top: 10px;
96
+ border-top: 1px dashed #cfd8dc;
97
+ display: flex;
98
+ justify-content: space-between;
99
+ color: #7b5e57;
100
+ font-size: 14px;
101
+ }
102
+ .comments {
103
+ margin-top: 20px;
104
+ }
105
+ .comment-item {
106
+ background-color: #f0f4c3;
107
+ padding: 12px;
108
+ border-radius: 8px;
109
+ margin-bottom: 10px;
110
+ font-size: 15px;
111
+ color: #444;
112
+ border: 1px solid #e2e6b3;
113
+ }
114
+ .comment-time { float: right; font-size: 12px; color: #888; }
115
+ .content-text img { height: 30px; vertical-align: middle; margin: 0 5px; }
116
+ </style>
117
+ </head>
118
+ <body>
119
+ <div class="bottle-container">
120
+ <div class="sticky-note"></div>
121
+ <div class="license-plate">No.${String(bottle.id).padStart(5, "0")}</div>
122
+
123
+ <div class="bottle-content">
124
+ ${bottle.content}
125
+ </div>
126
+
127
+ <div class="meta">
128
+ <span>作者:${bottle.username}</span>
129
+ <span>${new Date(bottle.createdAt).toLocaleString()}</span>
130
+ </div>
131
+
132
+ ${comments.length ? `
133
+ <div class="comments">
134
+ <h3 style="color: #6d4c41; font-size: 16px;">评论区 (${comments.length})</h3>
135
+ ${renderedComments}
136
+ </div>
137
+ ` : ""}
138
+ </div>
139
+ </body>
140
+ </html>
141
+ `;
142
+ }
143
+ __name(renderBottleHtml, "renderBottleHtml");
144
+ function renderLogsHtml(logs) {
145
+ const renderedLogs = logs.map((log, idx) => {
146
+ let actionHtml = "";
147
+ if (log.action === "throw") actionHtml = `扔出了一只漂流瓶 (Bottle ID: <span>${log.bottleId}</span>)`;
148
+ else if (log.action === "fish") actionHtml = `捞到了一只漂流瓶 (Bottle ID: <span>${log.bottleId}</span>)`;
149
+ else if (log.action === "comment") actionHtml = `对瓶子 (Bottle ID: <span>${log.bottleId}</span>) 发表了评论`;
150
+ else if (log.action === "approve") actionHtml = `管理员 <span style="color:#2e7d32">通过了</span> 漂流瓶 (Bottle ID: <span>${log.bottleId}</span>)`;
151
+ else if (log.action === "reject") actionHtml = `管理员 <span style="color:#d32f2f">驳回了</span> 漂流瓶 (Bottle ID: <span>${log.bottleId}</span>)`;
152
+ return `
153
+ <div class="timeline-item">
154
+ <div class="bubble-content ${idx === 0 ? "new" : ""}">
155
+ <div class="event"><strong>${log.username}</strong> ${actionHtml}</div>
156
+ <div class="time">${new Date(log.createdAt).toLocaleString()}</div>
157
+ </div>
158
+ </div>
159
+ `;
160
+ }).join("");
161
+ return `
162
+ <!DOCTYPE html>
163
+ <html lang="zh-CN">
164
+ <head>
165
+ <meta charset="UTF-8">
166
+ <style>
167
+ body {
168
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
169
+ background: transparent;
170
+ padding: 40px;
171
+ margin: 0;
172
+ width: 600px;
173
+ }
174
+ .timeline-container {
175
+ background-color: #fff8e1;
176
+ border: 1px dashed #b0bec5;
177
+ border-radius: 10px;
178
+ padding: 30px;
179
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
180
+ }
181
+ .title {
182
+ text-align: center;
183
+ font-size: 22px;
184
+ font-weight: bold;
185
+ color: #331e04;
186
+ border-bottom: 2px solid #211a12;
187
+ padding-bottom: 15px;
188
+ margin-bottom: 20px;
189
+ }
190
+ .timeline {
191
+ position: relative;
192
+ margin-left: 20px;
193
+ border-left: 2px dashed #443c3c;
194
+ padding-left: 25px;
195
+ }
196
+ .timeline-item {
197
+ margin-bottom: 20px;
198
+ position: relative;
199
+ }
200
+ .timeline-item:before {
201
+ content: '';
202
+ position: absolute;
203
+ left: -35px;
204
+ top: 15px;
205
+ width: 0; height: 0;
206
+ border: 8px solid transparent;
207
+ border-left-color: #331e04;
208
+ }
209
+ .bubble-content {
210
+ border: 1px solid #ccc;
211
+ background-image: linear-gradient(#ffffff 80%, #fdfef4);
212
+ border-radius: 6px;
213
+ padding: 12px 16px;
214
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
215
+ position: relative;
216
+ }
217
+ .bubble-content.new::before {
218
+ content: '新!';
219
+ position: absolute;
220
+ top: -15px;
221
+ right: 10px;
222
+ color: #00b62a;
223
+ font-weight: bold;
224
+ font-size: 18px;
225
+ }
226
+ .event { color: #331e04; padding-bottom: 6px; border-bottom: 1px dashed #ccc; font-size: 15px; }
227
+ .event span { font-weight: bold; color: #ff5722; }
228
+ .time { margin-top: 8px; color: #888; font-size: 13px; }
229
+ </style>
230
+ </head>
231
+ <body>
232
+ <div class="timeline-container">
233
+ <div class="title">漂流瓶航海日志</div>
234
+ <div class="timeline">
235
+ ${logs.length ? renderedLogs : '<div style="text-align: center; color: #888; margin-top:20px;">暂无航海记录</div>'}
236
+ </div>
237
+ </div>
238
+ </body>
239
+ </html>
240
+ `;
241
+ }
242
+ __name(renderLogsHtml, "renderLogsHtml");
243
+
244
+ // src/index.ts
245
+ var import_path = require("path");
246
+ var import_fs = require("fs");
247
+ var name = "driftbottle-with-webui";
248
+ var Config = import_koishi.Schema.object({
249
+ dailyLimit: import_koishi.Schema.number().default(10).description("每日捞瓶子和扔瓶子的次数上限。"),
250
+ cooldown: import_koishi.Schema.number().default(60).description("扔瓶子/捞瓶子的冷却时间(秒)。"),
251
+ enableQQNativeMarkdown: import_koishi.Schema.boolean().default(false).description("是否在 QQ 官方机器人下独立下发包含快捷指令按钮的 Markdown。"),
252
+ enableQQInlineCmd: import_koishi.Schema.boolean().default(true).description("是否在开启原生 Markdown 时启用 mqqapi 快捷操作按钮(仅QQ支持)。")
253
+ });
254
+ var inject = ["database", "console", "http", "server", "puppeteer"];
255
+ function apply(ctx, config) {
256
+ const imageDir = (0, import_path.resolve)(ctx.baseDir, "data", "driftbottle");
257
+ import_fs.promises.mkdir(imageDir, { recursive: true }).catch(() => {
258
+ });
259
+ ctx.server.get("/driftbottle/image/:filename", async (koaCtx) => {
260
+ const filename = koaCtx.params.filename;
261
+ if (!filename.match(/^[a-zA-Z0-9_.-]+$/)) return koaCtx.status = 400;
262
+ const filePath = (0, import_path.join)(imageDir, filename);
263
+ try {
264
+ const data = await import_fs.promises.readFile(filePath);
265
+ const ext = filename.split(".").pop()?.toLowerCase();
266
+ let mime = "image/png";
267
+ if (ext === "jpg" || ext === "jpeg") mime = "image/jpeg";
268
+ else if (ext === "gif") mime = "image/gif";
269
+ else if (ext === "webp") mime = "image/webp";
270
+ koaCtx.set("Content-Type", mime);
271
+ koaCtx.body = data;
272
+ } catch (e) {
273
+ koaCtx.status = 404;
274
+ }
275
+ });
276
+ async function processElements(elements) {
277
+ for (const element of elements) {
278
+ if (element.type === "img" || element.type === "image") {
279
+ let url = element.attrs.url || element.attrs.src;
280
+ if (typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://"))) {
281
+ try {
282
+ const buffer = await ctx.http.get(url, { responseType: "arraybuffer" });
283
+ let ext = "png";
284
+ const match = url.match(/\.([a-zA-Z0-9]+)(?:[?#]|$)/);
285
+ if (match) {
286
+ const extLower = match[1].toLowerCase();
287
+ if (["png", "jpg", "jpeg", "gif", "webp"].includes(extLower)) {
288
+ ext = extLower;
289
+ }
290
+ }
291
+ const filename = `${import_koishi.Random.id()}.${ext}`;
292
+ const fullPath = (0, import_path.join)(imageDir, filename);
293
+ await import_fs.promises.writeFile(fullPath, Buffer.from(buffer));
294
+ element.attrs.url = `/driftbottle/image/${filename}`;
295
+ delete element.attrs.src;
296
+ } catch (e) {
297
+ ctx.logger("driftbottle").warn("Failed to download image from %s", url, e);
298
+ }
299
+ }
300
+ }
301
+ if (element.children?.length) {
302
+ await processElements(element.children);
303
+ }
304
+ }
305
+ }
306
+ __name(processElements, "processElements");
307
+ async function downloadImagesLocally(content) {
308
+ const elements = import_koishi.h.parse(content);
309
+ await processElements(elements);
310
+ return elements.join("");
311
+ }
312
+ __name(downloadImagesLocally, "downloadImagesLocally");
313
+ async function prepareContentForSending(content) {
314
+ const elements = import_koishi.h.parse(content);
315
+ async function traverse(nodes) {
316
+ for (const node of nodes) {
317
+ if (node.type === "img" || node.type === "image") {
318
+ const url = node.attrs.url || node.attrs.src;
319
+ node.type = "img";
320
+ if (typeof url === "string" && url.startsWith("/driftbottle/image/")) {
321
+ const filename = url.replace("/driftbottle/image/", "");
322
+ const fullPath = (0, import_path.join)(imageDir, filename);
323
+ try {
324
+ const buffer = await import_fs.promises.readFile(fullPath);
325
+ const ext = filename.split(".").pop()?.toLowerCase() || "png";
326
+ let mime = "image/png";
327
+ if (ext === "jpg" || ext === "jpeg") mime = "image/jpeg";
328
+ else if (ext === "gif") mime = "image/gif";
329
+ else if (ext === "webp") mime = "image/webp";
330
+ node.attrs.src = `data:${mime};base64,${buffer.toString("base64")}`;
331
+ delete node.attrs.url;
332
+ } catch (e) {
333
+ ctx.logger("driftbottle").warn("Failed to convert image to base64: %s", fullPath, e);
334
+ node.attrs.src = url;
335
+ delete node.attrs.url;
336
+ }
337
+ } else {
338
+ node.attrs.src = url;
339
+ delete node.attrs.url;
340
+ }
341
+ }
342
+ if (node.children?.length) {
343
+ await traverse(node.children);
344
+ }
345
+ }
346
+ }
347
+ __name(traverse, "traverse");
348
+ await traverse(elements);
349
+ return elements.join("");
350
+ }
351
+ __name(prepareContentForSending, "prepareContentForSending");
352
+ ctx.model.extend("driftbottle", {
353
+ id: "unsigned",
354
+ userId: "string",
355
+ platform: "string",
356
+ username: "string",
357
+ content: "text",
358
+ status: "unsigned",
359
+ createdAt: "timestamp"
360
+ }, { autoInc: true });
361
+ ctx.model.extend("driftbottle_comment", {
362
+ id: "unsigned",
363
+ bottleId: "unsigned",
364
+ userId: "string",
365
+ platform: "string",
366
+ username: "string",
367
+ content: "text",
368
+ status: "unsigned",
369
+ createdAt: "timestamp"
370
+ }, { autoInc: true });
371
+ ctx.model.extend("driftbottle_usage", {
372
+ id: "unsigned",
373
+ userId: "string",
374
+ platform: "string",
375
+ date: "string",
376
+ count: "unsigned",
377
+ lastUsedAt: "timestamp"
378
+ }, { autoInc: true });
379
+ ctx.model.extend("driftbottle_log", {
380
+ id: "unsigned",
381
+ bottleId: "unsigned",
382
+ userId: "string",
383
+ username: "string",
384
+ action: "string",
385
+ content: "text",
386
+ createdAt: "timestamp"
387
+ }, { autoInc: true });
388
+ ctx.inject(["console"], (ctx2) => {
389
+ ctx2.console.addEntry({
390
+ dev: (0, import_path.resolve)(__dirname, "../client/index.ts"),
391
+ prod: __dirname.includes("node_modules") ? (0, import_path.resolve)(__dirname, "../dist") : (0, import_path.resolve)(ctx2.baseDir, "node_modules/koishi-plugin-driftbottle-with-webui/dist")
392
+ });
393
+ ctx2.console.addListener("driftbottle/bottles", async (status) => {
394
+ const bottles = await ctx2.database.get("driftbottle", { status }, { sort: { createdAt: "desc" }, limit: 100 });
395
+ const bottleIds = bottles.map((b) => b.id);
396
+ const comments = bottleIds.length ? await ctx2.database.get("driftbottle_comment", { bottleId: bottleIds }) : [];
397
+ const logs = bottleIds.length ? await ctx2.database.get("driftbottle_log", { bottleId: bottleIds }) : [];
398
+ return bottles.map((b) => ({
399
+ ...b,
400
+ comments: comments.filter((c) => c.bottleId === b.id),
401
+ logs: logs.filter((l) => l.bottleId === b.id).sort((a, b2) => a.createdAt.getTime() - b2.createdAt.getTime()),
402
+ fishCount: logs.filter((l) => l.bottleId === b.id && l.action === "fish").length
403
+ }));
404
+ });
405
+ ctx2.console.addListener("driftbottle/comments", async (status) => {
406
+ return await ctx2.database.get("driftbottle_comment", { status }, { sort: { createdAt: "desc" }, limit: 100 });
407
+ });
408
+ ctx2.console.addListener("driftbottle/review-bottle", async (id, status) => {
409
+ await ctx2.database.set("driftbottle", id, { status });
410
+ await ctx2.database.create("driftbottle_log", {
411
+ bottleId: id,
412
+ userId: "Admin",
413
+ username: "管理员",
414
+ action: status === 1 ? "approve" : "reject",
415
+ content: "",
416
+ createdAt: /* @__PURE__ */ new Date()
417
+ });
418
+ });
419
+ ctx2.console.addListener("driftbottle/review-comment", async (id, status) => {
420
+ await ctx2.database.set("driftbottle_comment", id, { status });
421
+ });
422
+ ctx2.console.addListener("driftbottle/delete-bottle", async (id) => {
423
+ await ctx2.database.remove("driftbottle", { id });
424
+ await ctx2.database.remove("driftbottle_comment", { bottleId: id });
425
+ });
426
+ ctx2.console.addListener("driftbottle/delete-comment", async (id) => {
427
+ await ctx2.database.remove("driftbottle_comment", { id });
428
+ });
429
+ });
430
+ const getUsage = /* @__PURE__ */ __name(async (session) => {
431
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
432
+ let usageList = await ctx.database.get("driftbottle_usage", {
433
+ userId: session.userId,
434
+ platform: session.platform,
435
+ date
436
+ });
437
+ let usage;
438
+ if (usageList.length === 0) {
439
+ usage = await ctx.database.create("driftbottle_usage", {
440
+ userId: session.userId,
441
+ platform: session.platform,
442
+ date,
443
+ count: 0,
444
+ lastUsedAt: /* @__PURE__ */ new Date(0)
445
+ });
446
+ } else {
447
+ usage = usageList[0];
448
+ }
449
+ return usage;
450
+ }, "getUsage");
451
+ const checkUsage = /* @__PURE__ */ __name(async (session) => {
452
+ const usage = await getUsage(session);
453
+ if (usage.count >= config.dailyLimit) {
454
+ return "你今天扔/捞瓶子的次数已经用光了,明天再来吧!";
455
+ }
456
+ const now = /* @__PURE__ */ new Date();
457
+ const diff = (now.getTime() - usage.lastUsedAt.getTime()) / 1e3;
458
+ if (diff < config.cooldown) {
459
+ return `你操作得太快了,请等待 ${Math.ceil(config.cooldown - diff)} 秒后再试。`;
460
+ }
461
+ return null;
462
+ }, "checkUsage");
463
+ const updateUsage = /* @__PURE__ */ __name(async (session) => {
464
+ const usage = await getUsage(session);
465
+ await ctx.database.set("driftbottle_usage", usage.id, {
466
+ count: usage.count + 1,
467
+ lastUsedAt: /* @__PURE__ */ new Date()
468
+ });
469
+ }, "updateUsage");
470
+ ctx.command("driftbottle", "漂流瓶").alias("漂流瓶");
471
+ ctx.command("driftbottle.throw <content:text>", "扔一个漂流瓶").alias("扔瓶子").alias("漂流瓶.扔").action(async ({ session }, content) => {
472
+ if (!content) return "瓶子里不能空着哦,请填写内容。";
473
+ const error = await checkUsage(session);
474
+ if (error) return error;
475
+ const processedContent = await downloadImagesLocally(content);
476
+ const username = session.username || session.author?.nickname || session.userId;
477
+ const created = await ctx.database.create("driftbottle", {
478
+ userId: session.userId,
479
+ platform: session.platform,
480
+ username,
481
+ content: processedContent,
482
+ // 这里支持图片因为是由 Koishi 解析成 h 标签的字符串
483
+ status: 0,
484
+ createdAt: /* @__PURE__ */ new Date()
485
+ });
486
+ await ctx.database.create("driftbottle_log", {
487
+ bottleId: created.id,
488
+ userId: session.userId,
489
+ username,
490
+ action: "throw",
491
+ content: "",
492
+ createdAt: /* @__PURE__ */ new Date()
493
+ });
494
+ await updateUsage(session);
495
+ return "漂流瓶已仍出!等待管理员审核后即可漂向大海~";
496
+ });
497
+ async function sendDriftBottleOutput(session, imageString, mdCommands, config2) {
498
+ await session.send(imageString);
499
+ if (session.platform === "qq" && config2.enableQQNativeMarkdown && config2.enableQQInlineCmd && mdCommands.length > 0) {
500
+ let finalMd = `> 💡 快捷互动操作:
501
+ `;
502
+ mdCommands.forEach((cmd) => {
503
+ finalMd += `> [${cmd.text}](mqqapi://aio/inlinecmd?command=${encodeURIComponent(cmd.command)}&enter=false&reply=false) `;
504
+ });
505
+ session["seq"] = session["seq"] || 0;
506
+ const payload = {
507
+ msg_type: 2,
508
+ msg_id: session.messageId,
509
+ msg_seq: ++session["seq"],
510
+ content: "漂流瓶 互动引导",
511
+ markdown: { content: finalMd }
512
+ };
513
+ try {
514
+ if (session.isDirect) {
515
+ await session.qq?.sendPrivateMessage(session.channelId, payload);
516
+ } else {
517
+ await session.qq?.sendMessage(session.channelId, payload);
518
+ }
519
+ } catch (e) {
520
+ ctx.logger("driftbottle").warn("Failed to send QQ Markdown interact buttons:", e.message || e);
521
+ }
522
+ }
523
+ }
524
+ __name(sendDriftBottleOutput, "sendDriftBottleOutput");
525
+ ctx.command("driftbottle.fish", "捞一个漂流瓶").alias("捞瓶子").alias("漂流瓶.捞").action(async ({ session }) => {
526
+ const error = await checkUsage(session);
527
+ if (error) return error;
528
+ const bottles = await ctx.database.get("driftbottle", { status: 1 });
529
+ if (!bottles.length) {
530
+ return "大海里空空如也,什么也没捞到。";
531
+ }
532
+ const randomBottle = bottles[Math.floor(Math.random() * bottles.length)];
533
+ await updateUsage(session);
534
+ const sendContent = await prepareContentForSending(randomBottle.content);
535
+ await ctx.database.create("driftbottle_log", {
536
+ bottleId: randomBottle.id,
537
+ userId: session.userId,
538
+ username: session.username || session.author?.nickname || session.userId,
539
+ action: "fish",
540
+ content: "",
541
+ createdAt: /* @__PURE__ */ new Date()
542
+ });
543
+ const comments = await ctx.database.get("driftbottle_comment", { bottleId: randomBottle.id, status: 1 });
544
+ const hydratedComments = await Promise.all(comments.map(async (c) => ({
545
+ ...c,
546
+ content: await prepareContentForSending(c.content)
547
+ })));
548
+ const html = renderBottleHtml({
549
+ ...randomBottle,
550
+ content: sendContent
551
+ }, hydratedComments);
552
+ const image = await ctx.puppeteer.render(html);
553
+ return sendDriftBottleOutput(session, image, [
554
+ { text: "捞个瓶子", command: "/捞瓶子" },
555
+ { text: "评论瓶子", command: `/评论瓶子 ${randomBottle.id} ` },
556
+ { text: "我的动态", command: "/我的动态" }
557
+ ], config);
558
+ });
559
+ ctx.command("driftbottle.comment <id:number> <content:text>", "评论一个漂流瓶").alias("评论瓶子").alias("漂流瓶.评论").action(async ({ session }, id, content) => {
560
+ if (!id || !content) return "请提供要评论的漂流瓶 ID 和你要评论的内容。";
561
+ const bottles = await ctx.database.get("driftbottle", { id });
562
+ if (!bottles.length) return `抱歉,找不到 ID 为 ${id} 的漂流瓶。`;
563
+ if (bottles[0].status !== 1) return "该漂流瓶不可被评论。";
564
+ const processedContent = await downloadImagesLocally(content);
565
+ const username = session.username || session.author?.nickname || session.userId;
566
+ const created = await ctx.database.create("driftbottle_comment", {
567
+ bottleId: id,
568
+ userId: session.userId,
569
+ platform: session.platform,
570
+ username,
571
+ content: processedContent,
572
+ status: 0,
573
+ // 评论也进入审核队列
574
+ createdAt: /* @__PURE__ */ new Date()
575
+ });
576
+ await ctx.database.create("driftbottle_log", {
577
+ bottleId: id,
578
+ userId: session.userId,
579
+ username,
580
+ action: "comment",
581
+ content: processedContent,
582
+ createdAt: /* @__PURE__ */ new Date()
583
+ });
584
+ return "评论已发送!等待管理员审核后大家就都能看到了。";
585
+ });
586
+ ctx.command("driftbottle.view <id:number>", "查看漂流瓶和它的评论").alias("看瓶子").alias("漂流瓶.看瓶子").alias("漂流瓶.查看").action(async ({ session }, id) => {
587
+ if (!id) return "请输入漂流瓶 ID。";
588
+ const bottles = await ctx.database.get("driftbottle", { id });
589
+ if (!bottles.length) return `找不到 ID 为 ${id} 的漂流瓶。`;
590
+ const bottle = bottles[0];
591
+ if (bottle.status !== 1 && bottle.userId !== session.userId) {
592
+ return "这是一个还没通过审核或被拒绝的漂流瓶,你无法查看。";
593
+ }
594
+ const comments = await ctx.database.get("driftbottle_comment", { bottleId: id, status: 1 });
595
+ const sendBottleContent = await prepareContentForSending(bottle.content);
596
+ const hydratedComments = await Promise.all(comments.map(async (c) => ({
597
+ ...c,
598
+ content: await prepareContentForSending(c.content)
599
+ })));
600
+ const html = renderBottleHtml({
601
+ ...bottle,
602
+ content: sendBottleContent
603
+ }, hydratedComments);
604
+ const image = await ctx.puppeteer.render(html);
605
+ return sendDriftBottleOutput(session, image, [
606
+ { text: "评论瓶子", command: `/评论瓶子 ${id} ` }
607
+ ], config);
608
+ });
609
+ ctx.command("driftbottle.my", "查看我扔的瓶子").alias("我的瓶子").alias("漂流瓶.我的瓶子").action(async ({ session }) => {
610
+ const bottles = await ctx.database.get("driftbottle", { userId: session.userId, platform: session.platform });
611
+ if (!bottles.length) return "你还没扔过漂流瓶。";
612
+ let msg = ["你有这些漂流瓶:"];
613
+ for (const b of bottles.slice(-10)) {
614
+ let statusStr = b.status === 0 ? "审核中" : b.status === 1 ? "已通过" : "已拒绝";
615
+ let excerpt = b.content.length > 10 ? b.content.substring(0, 10).replace(/<[^>]*>/g, "[图片/元素]") + "..." : b.content;
616
+ msg.push(`[ID: ${b.id}] ${excerpt} - ${statusStr}`);
617
+ }
618
+ if (bottles.length > 10) msg.push(`...等共 ${bottles.length} 个瓶子`);
619
+ return msg.join("\n");
620
+ });
621
+ ctx.command("driftbottle.logs", "查看自己的航海动态").alias("我的动态").alias("漂流瓶.我的动态").action(async ({ session }) => {
622
+ const myBottles = await ctx.database.get("driftbottle", { userId: session.userId });
623
+ const myBottleIds = myBottles.map((b) => b.id);
624
+ const logs = await ctx.database.get("driftbottle_log", {
625
+ $or: [
626
+ { userId: session.userId },
627
+ ...myBottleIds.length ? [{ bottleId: myBottleIds }] : []
628
+ ]
629
+ });
630
+ if (!logs.length) return "你还没有任何航海记录。";
631
+ logs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
632
+ const recentLogs = logs.slice(0, 30);
633
+ const html = renderLogsHtml(recentLogs);
634
+ const image = await ctx.puppeteer.render(html);
635
+ return sendDriftBottleOutput(session, image, [
636
+ { text: "捞个瓶子", command: "/捞瓶子" }
637
+ ], config);
638
+ });
639
+ }
640
+ __name(apply, "apply");
641
+ // Annotate the CommonJS export names for ESM import in node:
642
+ 0 && (module.exports = {
643
+ Config,
644
+ apply,
645
+ inject,
646
+ name
647
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "koishi-plugin-driftbottle-with-webui",
3
+ "description": "漂流瓶,但是可以在koishi webui里查看漂流瓶列表,审核和删除漂流瓶",
4
+ "version": "0.0.2",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "dist"
10
+ ],
11
+ "license": "MIT",
12
+ "koishi": {
13
+ "locales": [
14
+ "locales"
15
+ ]
16
+ },
17
+ "scripts": {
18
+ "build": "koishi-console build",
19
+ "dev": "koishi-console dev"
20
+ },
21
+ "keywords": [
22
+ "chatbot",
23
+ "koishi",
24
+ "plugin"
25
+ ],
26
+ "devDependencies": {
27
+ "@koishijs/client": "^5.28.2",
28
+ "@koishijs/plugin-console": "^5.28.2",
29
+ "@koishijs/scripts": "^4.6.2",
30
+ "@types/node": "^20.0.0",
31
+ "typescript": "^5.0.0",
32
+ "vite": "^5.0.0",
33
+ "vue": "^3.0.0"
34
+ },
35
+ "peerDependencies": {
36
+ "koishi": "^4.16.0",
37
+ "koishi-plugin-puppeteer": "^3.0.0"
38
+ }
39
+ }
package/readme.md ADDED
@@ -0,0 +1,5 @@
1
+ # koishi-plugin-driftbottle-with-webui
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-driftbottle-with-webui?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-driftbottle-with-webui)
4
+
5
+ 漂流瓶,但是可以在koishi webui里查看漂流瓶列表,审核和删除漂流瓶