@ohmynpm/dsh-baton 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +159 -2
- package/README.zh-CN.md +135 -0
- package/client.js +901 -0
- package/connection.js +397 -0
- package/cordis.patch.yml +8 -0
- package/icon.svg +5 -0
- package/index.js +498 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +64 -4
- package/session.js +221 -0
package/client.js
ADDED
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baton inbox. All task text is rendered as text; approvals remain explicit UI actions.
|
|
3
|
+
* CSS is scoped and installed with the plugin lifetime so the bundle needs no build.
|
|
4
|
+
*/
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: '@ohmynpm/dsh-baton',
|
|
7
|
+
factory(require) {
|
|
8
|
+
const React = require('react')
|
|
9
|
+
const h = React.createElement
|
|
10
|
+
const { useCallback, useEffect, useRef, useState, useSyncExternalStore } = React
|
|
11
|
+
const API = '/api/baton'
|
|
12
|
+
const CSS = `
|
|
13
|
+
.bt-count{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:5px;background:#dcece3;color:#315c48;font-size:10px;font-weight:700}
|
|
14
|
+
.bt-panel-page{height:100%;min-height:0;width:100%;min-width:0;flex:1 1 auto;align-self:stretch;display:flex;container-type:inline-size;background:var(--dsw-alias-bg,transparent);overflow:hidden}
|
|
15
|
+
.bt-shell.bt-embedded{width:100%;height:100%;min-height:0;max-height:none;flex:1 1 auto;align-self:stretch;border:0;border-radius:0;box-shadow:none;animation:none}
|
|
16
|
+
.bt-nav-icon{position:relative;display:flex;align-items:center;justify-content:center;flex:none}
|
|
17
|
+
.bt-nav-icon .bt-icon{width:100%;height:100%}
|
|
18
|
+
|
|
19
|
+
.bt-icon-button.copied,.bt-button.copied{color:#315c48;background:var(--bt-tint)}
|
|
20
|
+
.bt-combo{position:relative}
|
|
21
|
+
.bt-combo-menu{position:absolute;z-index:8;left:0;right:0;top:calc(100% + 4px);max-height:190px;overflow:auto;padding:4px;border:1px solid var(--bt-line);border-radius:8px;background:var(--bt-bg);box-shadow:0 12px 30px rgb(12 30 21 / 14%)}
|
|
22
|
+
.bt-combo-option{display:flex;align-items:baseline;gap:8px;width:100%;padding:7px 8px;border:0;border-radius:6px;background:transparent;color:var(--bt-fg);text-align:left;font-size:12px}
|
|
23
|
+
.bt-combo-option:hover,.bt-combo-option.active{background:var(--bt-tint)}
|
|
24
|
+
.bt-combo-key{font-weight:650}
|
|
25
|
+
.bt-combo-label{color:var(--bt-muted);font-size:11px}
|
|
26
|
+
.bt-backdrop{position:fixed;inset:0;z-index:100;background:rgb(17 28 23 / 30%);backdrop-filter:blur(5px);display:flex;align-items:center;justify-content:center;padding:36px;pointer-events:auto;animation:bt-fade .18s ease}
|
|
27
|
+
.bt-shell{--bt-bg:var(--dsw-alias-bg-elevated,#fff);--bt-fg:var(--dsw-alias-fg,#25372f);--bt-soft:color-mix(in srgb,var(--bt-bg) 96%,var(--bt-fg));--bt-muted:color-mix(in srgb,var(--bt-fg) 65%,var(--bt-bg));--bt-line:color-mix(in srgb,var(--bt-fg) 10%,var(--bt-bg));--bt-accent:color-mix(in srgb,#4f7e60 60%,var(--bt-fg));--bt-action:#315f4d;--bt-tint:color-mix(in srgb,#6da084 12%,var(--bt-bg));color:var(--bt-fg);background:var(--bt-bg);font-family:'Avenir Next','PingFang SC','Noto Sans SC',sans-serif;font-size:13px;line-height:1.5;width:min(1120px,100%);height:min(770px,calc(100dvh - 72px));min-height:360px;display:flex;flex-direction:column;border:1px solid var(--bt-line);border-radius:18px;box-shadow:0 36px 100px rgb(12 30 21 / 24%),0 2px 8px rgb(12 30 21 / 8%);overflow:hidden;isolation:isolate;animation:bt-enter .22s ease}
|
|
28
|
+
.bt-shell *{box-sizing:border-box}
|
|
29
|
+
.bt-shell button,.bt-shell input,.bt-shell select,.bt-shell textarea{font:inherit}
|
|
30
|
+
.bt-shell button{cursor:pointer}
|
|
31
|
+
.bt-shell button:disabled{cursor:not-allowed;opacity:.45}
|
|
32
|
+
.bt-shell button:focus-visible,.bt-shell input:focus-visible,.bt-shell select:focus-visible,.bt-shell textarea:focus-visible{outline:3px solid color-mix(in srgb,#72a88b 50%,transparent);outline-offset:3px}
|
|
33
|
+
.bt-shell button{transition:background .15s ease,color .15s ease,transform .15s ease}
|
|
34
|
+
.bt-shell button:active:not(:disabled){transform:translateY(1px)}
|
|
35
|
+
.bt-icon{width:17px;height:17px;flex:none;vertical-align:middle}
|
|
36
|
+
.bt-topbar{height:67px;min-height:67px;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:0 25px;border-bottom:1px solid var(--bt-line)}
|
|
37
|
+
.bt-brand{display:flex;align-items:center;gap:11px}
|
|
38
|
+
.bt-mark{width:30px;height:30px;position:relative;display:block;flex:none}
|
|
39
|
+
.bt-mark:before,.bt-mark:after{content:'';position:absolute;width:9px;height:24px;background:#315f4d;border-radius:4px;transform:rotate(31deg)}
|
|
40
|
+
.bt-mark:before{left:5px;top:0}.bt-mark:after{left:17px;top:7px;background:#84a68e}
|
|
41
|
+
.bt-wordmark{font-size:18px;letter-spacing:-.7px;font-weight:750}
|
|
42
|
+
.bt-wordmark small{font-size:11px;font-weight:500;letter-spacing:1.4px;margin-left:10px;color:var(--bt-muted)}
|
|
43
|
+
.bt-top-actions{display:flex;align-items:center;gap:15px}
|
|
44
|
+
.bt-shell.bt-embedded .bt-topbar{-webkit-app-region:drag;user-select:none}
|
|
45
|
+
.bt-shell.bt-embedded .bt-topbar button,.bt-shell.bt-embedded .bt-topbar a,.bt-shell.bt-embedded .bt-topbar input,.bt-shell.bt-embedded .bt-topbar select,.bt-shell.bt-embedded .bt-topbar label{-webkit-app-region:no-drag}
|
|
46
|
+
.bt-connection{font-size:11px;color:var(--bt-muted);display:flex;align-items:center;gap:6px}
|
|
47
|
+
.bt-dot{width:6px;height:6px;border-radius:50%;background:#a9ada9;flex:none}
|
|
48
|
+
.bt-dot.online{background:#639878;box-shadow:0 0 0 3px color-mix(in srgb,#83b393 13%,transparent)}
|
|
49
|
+
.bt-divider{height:17px;width:1px;background:var(--bt-line)}
|
|
50
|
+
.bt-icon-button{display:inline-flex;align-items:center;justify-content:center;background:transparent;border:0;color:var(--bt-muted);border-radius:7px;width:29px;height:29px;padding:0}
|
|
51
|
+
.bt-icon-button:hover{background:var(--bt-soft);color:var(--bt-fg)}
|
|
52
|
+
.bt-body{display:flex;flex:1;min-height:0}
|
|
53
|
+
.bt-sidebar{width:210px;flex:none;border-right:1px solid var(--bt-line);background:var(--bt-soft);display:flex;flex-direction:column;padding:24px 13px 12px}
|
|
54
|
+
.bt-sidebar.collapsed{width:64px;padding:18px 9px 12px;align-items:center}
|
|
55
|
+
.bt-sidebar.collapsed .bt-nav-caption,.bt-sidebar.collapsed .bt-nav,.bt-sidebar.collapsed .bt-sidebar-heading,.bt-sidebar.collapsed .bt-room-list,.bt-sidebar.collapsed .bt-room-actions,.bt-sidebar.collapsed .bt-profile{display:none}
|
|
56
|
+
.bt-nav-heading{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-bottom:6px}
|
|
57
|
+
.bt-nav-heading .bt-nav-caption{margin:0}
|
|
58
|
+
.bt-nav-heading .bt-icon{width:15px;height:15px}
|
|
59
|
+
.bt-nav-caption{font-size:10px;letter-spacing:1.6px;color:var(--bt-muted);font-weight:650;padding:0 12px;margin-bottom:12px}
|
|
60
|
+
.bt-nav{display:flex;align-items:center;gap:10px;width:100%;border:0;color:var(--bt-muted);background:transparent;padding:10px 12px;border-radius:8px;text-align:left;min-height:39px}
|
|
61
|
+
.bt-nav.active{background:var(--bt-tint);color:color-mix(in srgb,#537d65 65%,var(--bt-fg));font-weight:650}
|
|
62
|
+
.bt-nav .bt-count{margin-left:auto}
|
|
63
|
+
.bt-sidebar-heading{display:flex;align-items:center;justify-content:space-between;padding:26px 7px 8px 12px}
|
|
64
|
+
.bt-sidebar-heading span{font-size:11px;font-weight:650;color:var(--bt-muted)}
|
|
65
|
+
.bt-room-list{overflow:auto;flex:1;min-height:40px}
|
|
66
|
+
.bt-room-row{display:flex;align-items:center;border-radius:8px;margin:3px 0}
|
|
67
|
+
.bt-room-row.active{background:var(--bt-tint)}
|
|
68
|
+
.bt-room{display:flex;align-items:center;gap:9px;flex:1;min-width:0;border:0;background:transparent;color:var(--bt-fg);text-align:left;padding:10px 9px 10px 12px}
|
|
69
|
+
.bt-room-symbol{color:var(--bt-muted);font-size:16px;font-weight:400}
|
|
70
|
+
.bt-room-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}
|
|
71
|
+
.bt-room-meta{color:var(--bt-muted);font-size:10px;margin-top:2px;letter-spacing:.3px;font-variant-numeric:tabular-nums}
|
|
72
|
+
.bt-room-row>.bt-icon-button{margin-right:3px;opacity:0}.bt-room-row:hover>.bt-icon-button,.bt-room-row:focus-within>.bt-icon-button{opacity:1}
|
|
73
|
+
.bt-room-actions{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin:10px 7px 15px}
|
|
74
|
+
.bt-button{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:35px;padding:7px 13px;border-radius:7px;border:1px solid var(--bt-line);background:var(--bt-bg);color:var(--bt-fg);font-size:12px;font-weight:550;white-space:nowrap}
|
|
75
|
+
.bt-button:hover:not(:disabled){background:var(--bt-soft)}
|
|
76
|
+
.bt-button.primary{background:var(--bt-action);border-color:var(--bt-action);color:#fff;box-shadow:0 2px 4px rgb(30 65 45 / 10%)}
|
|
77
|
+
.bt-button.primary:hover:not(:disabled){background:#264c3d}
|
|
78
|
+
.bt-button.ghost{background:transparent;border-color:transparent;color:var(--bt-muted)}
|
|
79
|
+
.bt-button.ghost.copied{background:var(--bt-tint);color:#315c48}
|
|
80
|
+
.bt-button.danger{color:#af4e42;border-color:color-mix(in srgb,#af4e42 20%,var(--bt-bg))}
|
|
81
|
+
.bt-button.full{width:100%}
|
|
82
|
+
.bt-profile{border-top:1px solid var(--bt-line);display:flex;align-items:center;gap:9px;padding:15px 9px 0}
|
|
83
|
+
.bt-avatar{width:30px;height:30px;border-radius:10px;display:flex;align-items:center;justify-content:center;flex:none;background:var(--bt-tint);color:var(--bt-accent);font-size:12px;font-weight:650}
|
|
84
|
+
.bt-profile-name{font-size:11px;font-weight:650}.bt-profile-id{font:10px 'SFMono-Regular',Consolas,monospace;color:var(--bt-muted)}
|
|
85
|
+
.bt-workspace{display:flex;flex-direction:column;flex:1;min-width:0;min-height:0}
|
|
86
|
+
.bt-page-head{padding:23px 25px 0}
|
|
87
|
+
.bt-page-title-row{display:flex;justify-content:space-between;align-items:center;gap:16px}
|
|
88
|
+
.bt-page-title{font-size:21px;letter-spacing:-.7px;font-weight:650;margin:0}
|
|
89
|
+
.bt-page-subtitle{margin:5px 0 19px;color:var(--bt-muted);font-size:11px}
|
|
90
|
+
.bt-tabs{display:flex;gap:23px;border-bottom:1px solid var(--bt-line)}
|
|
91
|
+
.bt-tab{padding:0 0 12px;background:transparent;border:0;color:var(--bt-muted);position:relative;font-size:12px;display:flex;align-items:center;gap:6px}
|
|
92
|
+
.bt-tab.active{color:var(--bt-fg);font-weight:650}
|
|
93
|
+
.bt-tab.active:after{content:'';height:2px;position:absolute;bottom:-1px;left:0;right:0;background:var(--bt-accent);border-radius:2px}
|
|
94
|
+
.bt-tab .bt-count{background:var(--bt-soft);color:var(--bt-muted);height:16px;min-width:16px;font-size:9px}
|
|
95
|
+
.bt-message{margin:12px 24px 0;padding:10px 12px;display:flex;gap:8px;align-items:flex-start;border-radius:8px;background:color-mix(in srgb,#b38140 10%,var(--bt-bg));color:var(--bt-fg);font-size:11px}
|
|
96
|
+
.bt-message.error{background:color-mix(in srgb,#b65b4c 9%,var(--bt-bg))}
|
|
97
|
+
.bt-message.success{background:var(--bt-tint)}.bt-message>.bt-icon-button{margin-left:auto;margin-top:-4px;margin-right:-5px}
|
|
98
|
+
.bt-inbox{display:flex;flex:1;min-height:0;margin-top:0}
|
|
99
|
+
.bt-task-column{width:295px;flex:none;border-right:1px solid var(--bt-line);display:flex;flex-direction:column;min-height:0}
|
|
100
|
+
.bt-list-tools{display:flex;align-items:center;justify-content:space-between;padding:15px 14px 11px;gap:8px}
|
|
101
|
+
.bt-search{display:flex;align-items:center;gap:7px;padding:6px 8px;background:var(--bt-soft);border:1px solid transparent;border-radius:7px;min-width:0;flex:1}
|
|
102
|
+
.bt-search .bt-icon{width:13px;height:13px;color:var(--bt-muted)}
|
|
103
|
+
.bt-search input{min-width:0;width:100%;background:transparent;border:0;outline:0!important;color:var(--bt-fg);font-size:10px}
|
|
104
|
+
.bt-filter{border:0;background:transparent;color:var(--bt-muted);font-size:10px;padding:4px 0;flex:none}
|
|
105
|
+
.bt-task-list{overflow-y:auto;overflow-x:hidden;flex:1;min-height:0;overscroll-behavior:contain;padding:0 8px 10px;scrollbar-width:thin}
|
|
106
|
+
.bt-task{position:relative;border:1px solid transparent;border-radius:9px;width:100%;background:transparent;text-align:left;padding:13px 12px;margin-bottom:3px;color:var(--bt-fg)}
|
|
107
|
+
.bt-task:hover{background:var(--bt-soft)}
|
|
108
|
+
.bt-task.selected{background:var(--bt-tint);border-color:color-mix(in srgb,#6d9f7d 20%,var(--bt-bg))}
|
|
109
|
+
.bt-task-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}
|
|
110
|
+
.bt-person{display:flex;align-items:center;gap:7px;min-width:0;color:var(--bt-muted);font-size:10px}
|
|
111
|
+
.bt-person .bt-avatar{width:21px;height:21px;border-radius:7px;font-size:9px}
|
|
112
|
+
.bt-time{font-size:9px;color:var(--bt-muted);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
113
|
+
.bt-task-title{font-size:12px;line-height:1.6;font-weight:600;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin:0 0 9px}
|
|
114
|
+
.bt-task-bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
115
|
+
.bt-task{display:block;cursor:pointer;font:inherit}
|
|
116
|
+
.bt-task:focus-visible{outline:3px solid color-mix(in srgb,#72a88b 50%,transparent);outline-offset:-2px}
|
|
117
|
+
.bt-task-badges{display:flex;align-items:center;gap:6px;margin-left:auto}
|
|
118
|
+
.bt-shell .bt-task-link{border:0;border-radius:4px;background:transparent;color:var(--bt-muted);font-size:10px;font-weight:400;line-height:1.4;padding:3px 5px;white-space:nowrap}
|
|
119
|
+
.bt-task-actions{display:flex;align-items:center;gap:3px;flex:none}
|
|
120
|
+
.bt-task-actions .bt-icon-button{width:24px;height:24px}
|
|
121
|
+
.bt-task-actions .bt-icon{width:13px;height:13px}
|
|
122
|
+
.bt-task-meta{display:flex;align-items:center;justify-content:space-between;gap:4px}
|
|
123
|
+
.bt-delete-note{font-size:10px;color:var(--bt-muted);line-height:1.7}
|
|
124
|
+
.bt-task-link:hover{color:var(--bt-fg)}
|
|
125
|
+
.bt-task-link.deleted{color:color-mix(in srgb,#b65b4c 75%,var(--bt-fg));border-bottom:0}
|
|
126
|
+
.bt-env{display:flex;align-items:center;gap:4px;color:var(--bt-muted);font-size:9px}
|
|
127
|
+
.bt-env .bt-icon{width:12px;height:12px}
|
|
128
|
+
.bt-badge{display:inline-flex;align-items:center;gap:4px;border-radius:5px;padding:2px 6px;font-size:9px;line-height:1.6;white-space:nowrap;background:var(--bt-soft);color:var(--bt-muted);font-weight:500}
|
|
129
|
+
.bt-badge.pending{color:color-mix(in srgb,#a17a3f 65%,var(--bt-fg));background:color-mix(in srgb,#d1ad70 14%,var(--bt-bg))}
|
|
130
|
+
.bt-badge.accepted{color:var(--bt-accent);background:color-mix(in srgb,#83ae8d 13%,var(--bt-bg))}
|
|
131
|
+
.bt-badge.warning{color:color-mix(in srgb,#af7b3d 65%,var(--bt-fg));background:color-mix(in srgb,#cba16b 17%,var(--bt-bg))}
|
|
132
|
+
.bt-detail{flex:1;min-width:0;display:flex;flex-direction:column;overflow:auto}
|
|
133
|
+
.bt-detail-inner{padding:24px 26px 16px;flex:1}
|
|
134
|
+
.bt-detail-kicker{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--bt-muted);font-size:10px;margin-bottom:18px}
|
|
135
|
+
.bt-detail-title{margin:0;font-size:23px;font-weight:650;letter-spacing:-.65px;line-height:1.45;overflow-wrap:anywhere}
|
|
136
|
+
.bt-detail-person{display:flex;align-items:center;gap:9px;margin:17px 0 22px}
|
|
137
|
+
.bt-detail-person .bt-avatar{width:31px;height:31px}
|
|
138
|
+
.bt-detail-person strong{font-size:11px;font-weight:600}
|
|
139
|
+
.bt-detail-person small{display:block;color:var(--bt-muted);font-size:9px;margin-top:2px}
|
|
140
|
+
.bt-section-label{font-size:10px;font-weight:650;color:var(--bt-muted);letter-spacing:.3px;margin:23px 0 10px}
|
|
141
|
+
.bt-brief{font-size:12px;line-height:1.95;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--bt-fg)}
|
|
142
|
+
.bt-directory{background:var(--bt-soft);border:1px solid var(--bt-line);border-radius:9px;padding:12px 13px;margin:24px 0 0}
|
|
143
|
+
.bt-directory-head{display:flex;align-items:center;gap:6px;color:var(--bt-muted);font-size:10px;margin-bottom:7px}
|
|
144
|
+
.bt-path{font:10px/1.7 'SFMono-Regular',Consolas,monospace;overflow-wrap:anywhere;display:block}
|
|
145
|
+
.bt-hint{font-size:10px;line-height:1.8;color:var(--bt-muted);margin:9px 0 0}
|
|
146
|
+
.bt-warning{padding:12px 14px;border:1px solid color-mix(in srgb,#c18e4d 25%,var(--bt-bg));border-radius:8px;background:color-mix(in srgb,#d7b376 9%,var(--bt-bg));font-size:11px;line-height:1.8;margin-top:20px}
|
|
147
|
+
.bt-warning strong{display:block;font-weight:600;margin-bottom:4px}
|
|
148
|
+
.bt-detail-footer{padding:17px 26px 21px;border-top:1px solid var(--bt-line);background:var(--bt-bg);position:sticky;bottom:0}
|
|
149
|
+
.bt-detail-buttons{display:flex;gap:9px}.bt-detail-buttons>.primary{flex:1}.bt-detail-footer .bt-hint{font-size:9px;text-align:center;margin-top:9px}
|
|
150
|
+
.bt-empty{display:flex;flex:1;min-height:160px;align-items:center;justify-content:center;flex-direction:column;text-align:center;padding:35px;color:var(--bt-muted)}
|
|
151
|
+
.bt-empty-icon{width:53px;height:53px;border:1px solid var(--bt-line);background:var(--bt-soft);border-radius:16px;display:flex;align-items:center;justify-content:center;margin-bottom:15px}
|
|
152
|
+
.bt-empty-icon svg{width:23px;height:23px;color:#7b9b87}
|
|
153
|
+
.bt-empty strong{font-size:13px;color:var(--bt-fg);font-weight:550}.bt-empty p{font-size:11px;line-height:1.9;margin:7px 0 0;max-width:230px}
|
|
154
|
+
.bt-pending-send{margin:8px;padding:11px;border:1px dashed color-mix(in srgb,#c18e4d 35%,var(--bt-bg));border-radius:8px;font-size:10px;display:grid;gap:7px}
|
|
155
|
+
.bt-pending-send strong{font-size:11px}
|
|
156
|
+
.bt-dialog-cover{position:absolute;inset:0;z-index:10;background:rgb(17 28 23 / 24%);display:flex;align-items:center;justify-content:center;padding:24px;backdrop-filter:blur(2px)}
|
|
157
|
+
.bt-dialog{width:min(475px,100%);max-height:100%;overflow:auto;background:var(--bt-bg);border:1px solid var(--bt-line);border-radius:13px;box-shadow:0 16px 70px rgb(12 30 21 / 20%)}
|
|
158
|
+
.bt-dialog-header{display:flex;align-items:flex-start;justify-content:space-between;padding:22px 24px 0;gap:15px}
|
|
159
|
+
.bt-dialog-header h2{margin:0;font-size:19px;font-weight:650;letter-spacing:-.5px}
|
|
160
|
+
.bt-dialog-header p{color:var(--bt-muted);font-size:11px;line-height:1.8;margin:6px 0 0}
|
|
161
|
+
.bt-dialog-body{padding:20px 24px;display:grid;gap:15px}
|
|
162
|
+
.bt-field{display:grid;gap:7px;font-size:11px;font-weight:550}
|
|
163
|
+
.bt-field input,.bt-field select,.bt-field textarea{background:var(--bt-bg);border:1px solid var(--bt-line);border-radius:7px;min-height:37px;padding:8px 10px;color:var(--bt-fg);width:100%;min-width:0;font-size:12px;font-weight:400}
|
|
164
|
+
.bt-field textarea{resize:vertical;min-height:145px;line-height:1.8}
|
|
165
|
+
.bt-field small{font-size:9px;font-weight:400;color:var(--bt-muted)}
|
|
166
|
+
.bt-two-fields{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
167
|
+
.bt-env-head,.bt-env-row{display:grid;grid-template-columns:110px 120px minmax(0,1fr) 66px;gap:8px;align-items:center}
|
|
168
|
+
.bt-env-head{font-size:9px;color:var(--bt-muted);letter-spacing:.05em;font-weight:600;padding:0 2px 2px}
|
|
169
|
+
.bt-env-row{margin-bottom:6px}
|
|
170
|
+
.bt-env-row input{background:var(--bt-bg);border:1px solid var(--bt-line);border-radius:7px;min-height:35px;padding:7px 9px;color:var(--bt-fg);width:100%;min-width:0;font-size:12px}
|
|
171
|
+
.bt-env-actions{display:flex;gap:2px;justify-content:flex-end}
|
|
172
|
+
.bt-dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:9px;padding:15px 24px 20px;border-top:1px solid var(--bt-line)}
|
|
173
|
+
.bt-form-error{background:color-mix(in srgb,#b65b4c 9%,var(--bt-bg));border-radius:7px;padding:10px 12px;font-size:11px;color:var(--bt-fg);line-height:1.8}
|
|
174
|
+
.bt-room-member{display:flex;align-items:center;gap:10px;padding:9px 0}
|
|
175
|
+
.bt-room-member .bt-icon-button{margin-left:auto}
|
|
176
|
+
.bt-room-code{display:flex;align-items:center;justify-content:space-between;padding:13px 15px;background:var(--bt-soft);border-radius:9px}
|
|
177
|
+
.bt-room-code code{font-size:21px;font-weight:600;letter-spacing:4px}
|
|
178
|
+
.bt-mobile-back{display:none}
|
|
179
|
+
.bt-spinner{animation:bt-spin .8s linear infinite}
|
|
180
|
+
@keyframes bt-fade{from{opacity:0}to{opacity:1}}
|
|
181
|
+
@keyframes bt-enter{from{opacity:0;transform:translateY(9px) scale(.992)}to{opacity:1;transform:none}}
|
|
182
|
+
@keyframes bt-spin{to{transform:rotate(360deg)}}
|
|
183
|
+
@media(prefers-reduced-motion:reduce){.bt-backdrop,.bt-shell,.bt-spinner{animation:none}.bt-shell button{transition:none}}
|
|
184
|
+
@media(max-width:1050px){.bt-backdrop{padding:22px}.bt-shell{height:calc(100dvh - 44px)}.bt-sidebar{width:176px}.bt-task-column{width:260px}.bt-detail-inner{padding:20px}.bt-detail-footer{padding:16px 20px}.bt-page-head{padding:20px 20px 0}}
|
|
185
|
+
@media(max-width:780px){.bt-backdrop{padding:12px}.bt-shell{height:calc(100dvh - 24px);border-radius:12px}.bt-sidebar{width:150px;padding-left:7px;padding-right:7px}.bt-task-column{width:220px}.bt-page-subtitle{max-width:200px}.bt-detail-title{font-size:20px}.bt-wordmark small{display:none}.bt-page-title{font-size:18px}}
|
|
186
|
+
@media(max-width:650px){.bt-topbar{height:58px;min-height:58px;padding:0 16px}.bt-sidebar{display:none}.bt-page-head{padding:17px 16px 0}.bt-page-subtitle{max-width:none;font-size:10px}.bt-page-title-row{gap:8px}.bt-page-title-row>.bt-button{padding:7px 10px}.bt-task-column{width:100%;border-right:0}.bt-detail{display:none}.bt-inbox.has-selection .bt-task-column{display:none}.bt-inbox.has-selection .bt-detail{display:flex}.bt-mobile-back{display:inline-flex}.bt-detail-inner{padding:16px 22px}.bt-detail-footer{padding:15px 22px}.bt-dialog-cover{padding:14px}.bt-dialog-body{padding:16px 20px}.bt-dialog-header{padding:20px 20px 0}.bt-dialog-footer{padding:15px 20px}.bt-two-fields{grid-template-columns:1fr}.bt-env-head{display:none}.bt-env-row{grid-template-columns:1fr;gap:4px}.bt-env-actions{justify-content:flex-start}.bt-message{margin:10px 16px 0}}
|
|
187
|
+
@container(max-width:440px){.bt-sidebar{display:none}.bt-topbar{height:58px;min-height:58px;padding:0 12px;gap:8px}.bt-top-actions{gap:6px}.bt-connection,.bt-divider,.bt-wordmark small{display:none}.bt-wordmark{font-size:16px}.bt-page-head{padding:17px 14px 0}.bt-page-title-row{gap:8px}.bt-page-title{font-size:17px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bt-page-title-row>.bt-button{padding:7px 10px}.bt-page-subtitle{font-size:10px}.bt-tabs{gap:16px}.bt-tab{font-size:11px;white-space:nowrap}.bt-task-column{width:100%;border-right:0}.bt-detail{display:none}.bt-inbox.has-selection .bt-task-column{display:none}.bt-inbox.has-selection .bt-detail{display:flex}.bt-mobile-back{display:inline-flex}.bt-dialog-cover{padding:10px}.bt-dialog-header{padding:16px 14px 0}.bt-dialog-body{padding:14px}.bt-dialog-footer{padding:12px 14px;gap:6px}.bt-two-fields,.bt-env-row{grid-template-columns:1fr}.bt-env-head{display:none}.bt-env-actions{justify-content:flex-start}}
|
|
188
|
+
`
|
|
189
|
+
|
|
190
|
+
const paths = {
|
|
191
|
+
inbox: ['M4 4h16v16H4z', 'M4 12h4l2 3h4l2-3h4'],
|
|
192
|
+
arrow: ['M5 12h14', 'm13 6 6 6-6 6'],
|
|
193
|
+
back: ['M19 12H5', 'm11 6-6 6 6 6'],
|
|
194
|
+
close: ['m6 6 12 12', 'M18 6 6 18'],
|
|
195
|
+
plus: ['M12 5v14', 'M5 12h14'],
|
|
196
|
+
copy: ['M9 9h11v11H9z', 'M15 5V3H3v12h2'],
|
|
197
|
+
search: ['M20 20l-5-5', 'M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14'],
|
|
198
|
+
folder: ['M3 6h6l2 2h10v12H3z'],
|
|
199
|
+
clock: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18', 'M12 7v5l3 2'],
|
|
200
|
+
check: ['m5 12 4 4L19 6'],
|
|
201
|
+
warning: ['m12 3 10 18H2z', 'M12 9v4', 'M12 16h.01'],
|
|
202
|
+
send: ['m22 2-7 20-4-9-9-4z', 'M22 2 11 13'],
|
|
203
|
+
people: ['M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2', 'M9 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8', 'M22 21v-2a4 4 0 0 0-3-3.87', 'M16 3.13a4 4 0 0 1 0 7.75'],
|
|
204
|
+
refresh: ['M20 7v5h-5', 'M4 17v-5h5', 'M6 7a7 7 0 0 1 11-2l3 3', 'M18 17a7 7 0 0 1-11 2l-3-3'],
|
|
205
|
+
dots: ['M5 12h.01', 'M12 12h.01', 'M19 12h.01'],
|
|
206
|
+
chevron: ['m14 6-6 6 6 6'],
|
|
207
|
+
trash: ['M3 6h18', 'M9 6V3h6v3', 'M5 6l1 15h12l1-15', 'M10 10v7', 'M14 10v7'],
|
|
208
|
+
}
|
|
209
|
+
function Icon({ name, spin = false }) {
|
|
210
|
+
return h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.55, strokeLinecap: 'round', strokeLinejoin: 'round', className: 'bt-icon' + (spin ? ' bt-spinner' : ''), 'aria-hidden': true }, (paths[name] ?? paths.inbox).map((d, index) => h('path', { key: index, d })))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let opened = false
|
|
214
|
+
const listeners = new Set()
|
|
215
|
+
const setOpen = value => { opened = value; for (const listener of listeners) listener() }
|
|
216
|
+
const subscribe = listener => { listeners.add(listener); return () => listeners.delete(listener) }
|
|
217
|
+
const openSnapshot = () => opened
|
|
218
|
+
|
|
219
|
+
let attentionCount = 0
|
|
220
|
+
const countListeners = new Set()
|
|
221
|
+
const setAttentionCount = value => {
|
|
222
|
+
const next = Math.max(0, Number(value) || 0)
|
|
223
|
+
if (next === attentionCount) return
|
|
224
|
+
attentionCount = next
|
|
225
|
+
for (const listener of countListeners) listener()
|
|
226
|
+
}
|
|
227
|
+
const subscribeCount = listener => { countListeners.add(listener); return () => countListeners.delete(listener) }
|
|
228
|
+
const countSnapshot = () => attentionCount
|
|
229
|
+
|
|
230
|
+
let openSession = () => { throw new Error('会话导航服务尚未就绪。') }
|
|
231
|
+
let pickDirectory = async () => null
|
|
232
|
+
|
|
233
|
+
async function call(body) {
|
|
234
|
+
let response
|
|
235
|
+
try { response = await fetch(API, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }) }
|
|
236
|
+
catch {
|
|
237
|
+
const error = new Error('连接中断,操作结果暂未确认。请检查后重试。')
|
|
238
|
+
error.data = { ok: false, code: 'connection-closed', error: error.message }
|
|
239
|
+
throw error
|
|
240
|
+
}
|
|
241
|
+
const data = await response.json().catch(() => ({ ok: false, code: 'internal', error: '服务响应不完整,操作结果暂未确认。' }))
|
|
242
|
+
if (!data || typeof data !== 'object' || Array.isArray(data) || typeof data.ok !== 'boolean') {
|
|
243
|
+
const error = new Error('服务响应不完整,操作结果暂未确认。')
|
|
244
|
+
error.data = { ok: false, code: 'internal', error: error.message }
|
|
245
|
+
throw error
|
|
246
|
+
}
|
|
247
|
+
if (!response.ok || data.ok === false) {
|
|
248
|
+
const error = new Error(data.error ?? '操作未完成,请稍后重试。')
|
|
249
|
+
error.data = { ...data, ok: false, code: data.code ?? 'internal' }
|
|
250
|
+
throw error
|
|
251
|
+
}
|
|
252
|
+
return data
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function useFocusTrap(ref, active, close) {
|
|
256
|
+
const closeRef = useRef(close)
|
|
257
|
+
closeRef.current = close
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (!active || !ref.current) return undefined
|
|
260
|
+
const previous = document.activeElement
|
|
261
|
+
const node = ref.current
|
|
262
|
+
const focusable = () => Array.from(node.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),[tabindex="0"]')).filter(element => element.getClientRects().length)
|
|
263
|
+
const first = focusable()[0]
|
|
264
|
+
first?.focus()
|
|
265
|
+
const onKey = event => {
|
|
266
|
+
if (event.key !== 'Tab') return
|
|
267
|
+
const items = focusable()
|
|
268
|
+
if (!items.length) { event.preventDefault(); return }
|
|
269
|
+
const index = items.indexOf(document.activeElement)
|
|
270
|
+
if (event.shiftKey && index <= 0) { event.preventDefault(); items.at(-1).focus() }
|
|
271
|
+
else if (!event.shiftKey && (index < 0 || index === items.length - 1)) { event.preventDefault(); items[0].focus() }
|
|
272
|
+
}
|
|
273
|
+
node.addEventListener('keydown', onKey)
|
|
274
|
+
return () => { node.removeEventListener('keydown', onKey); if (previous?.isConnected) previous.focus() }
|
|
275
|
+
}, [ref, active])
|
|
276
|
+
// React bubbling lets a nested menu consume Escape before its dialog does.
|
|
277
|
+
return event => {
|
|
278
|
+
if (active && event.key === 'Escape' && !event.defaultPrevented) {
|
|
279
|
+
event.preventDefault()
|
|
280
|
+
event.stopPropagation()
|
|
281
|
+
closeRef.current()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function Button({ children, icon, tone = '', busy, ...props }) {
|
|
287
|
+
return h('button', { type: 'button', ...props, className: 'bt-button ' + tone }, [busy ? h(Icon, { key: 'i', name: 'refresh', spin: true }) : icon ? h(Icon, { key: 'i', name: icon }) : null, children])
|
|
288
|
+
}
|
|
289
|
+
function IconButton({ label, icon, ...props }) {
|
|
290
|
+
return h('button', { type: 'button', title: label, 'aria-label': label, ...props, className: 'bt-icon-button' }, h(Icon, { name: icon }))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function CopyButton({ value, label, icon = 'copy', children, tone = '', ...props }) {
|
|
294
|
+
const [done, setDone] = useState(false)
|
|
295
|
+
const timer = useRef(null)
|
|
296
|
+
useEffect(() => () => clearTimeout(timer.current), [])
|
|
297
|
+
async function onCopy(event) {
|
|
298
|
+
event.stopPropagation()
|
|
299
|
+
try {
|
|
300
|
+
await navigator.clipboard.writeText(value)
|
|
301
|
+
setDone(true)
|
|
302
|
+
clearTimeout(timer.current)
|
|
303
|
+
timer.current = setTimeout(() => setDone(false), 1600)
|
|
304
|
+
} catch {
|
|
305
|
+
setDone(false)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (children) {
|
|
309
|
+
return h('button', { type: 'button', ...props, title: done ? '已复制' : label, 'aria-label': done ? '已复制' : label, className: 'bt-button ' + tone + (done ? ' copied' : ''), onClick: onCopy }, [h(Icon, { key: 'i', name: done ? 'check' : icon }), done ? '已复制' : children])
|
|
310
|
+
}
|
|
311
|
+
return h('button', { type: 'button', title: done ? '已复制' : label, 'aria-label': done ? '已复制' : label, ...props, className: 'bt-icon-button' + (done ? ' copied' : ''), onClick: onCopy }, h(Icon, { name: done ? 'check' : icon }))
|
|
312
|
+
}
|
|
313
|
+
function Avatar({ name }) { return h('span', { className: 'bt-avatar', 'aria-hidden': true }, Array.from(name ?? '?').slice(0, 1).join('')) }
|
|
314
|
+
function Field({ label, hint, children }) { return h('label', { className: 'bt-field' }, [h('span', { key: 'l' }, label), React.isValidElement(children) ? React.cloneElement(children, { key: 'control' }) : children, hint ? h('small', { key: 'h' }, hint) : null]) }
|
|
315
|
+
function Empty({ title, description, icon = 'inbox' }) {
|
|
316
|
+
return h('div', { className: 'bt-empty' }, [h('div', { key: 'i', className: 'bt-empty-icon' }, h(Icon, { name: icon })), h('strong', { key: 't' }, title), h('p', { key: 'd' }, description)])
|
|
317
|
+
}
|
|
318
|
+
function relative(time) {
|
|
319
|
+
const minutes = Math.max(0, Math.floor((Date.now() - time) / 60000))
|
|
320
|
+
if (minutes < 1) return '刚刚'
|
|
321
|
+
if (minutes < 60) return minutes + ' 分钟前'
|
|
322
|
+
if (minutes < 1440) return Math.floor(minutes / 60) + ' 小时前'
|
|
323
|
+
return Math.floor(minutes / 1440) + ' 天前'
|
|
324
|
+
}
|
|
325
|
+
function statusFor(task, record, incoming) {
|
|
326
|
+
if (incoming && record?.phase === 'needs-review') return { text: '需核对', tone: 'warning' }
|
|
327
|
+
if (incoming && task.status === 'accepted' && record?.phase === 'unsubmitted') return { text: '待启动', tone: 'warning' }
|
|
328
|
+
if (incoming && record?.phase === 'submitted') return { text: '已提交', tone: 'accepted' }
|
|
329
|
+
if (incoming && record?.phase === 'handled') return { text: '已自行处理', tone: '' }
|
|
330
|
+
return task.status === 'pending' ? { text: incoming ? '待你确认' : '等待接手', tone: 'pending' } : task.status === 'accepted' ? { text: '已接手', tone: 'accepted' } : { text: '已拒绝', tone: '' }
|
|
331
|
+
}
|
|
332
|
+
function Badge({ value }) { return h('span', { className: 'bt-badge ' + value.tone }, value.text) }
|
|
333
|
+
function newDraft(room = '') { return { taskId: crypto.randomUUID(), room, to: '', env: '', title: '', brief: '' } }
|
|
334
|
+
function visibilityFor(task, userId) {
|
|
335
|
+
return task.from.userId === userId ? task.fromVisibility ?? 'active' : task.toVisibility ?? 'active'
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function Dialog({ title, description, children, footer, close }) {
|
|
339
|
+
const ref = useRef(null)
|
|
340
|
+
const onKeyDown = useFocusTrap(ref, true, close)
|
|
341
|
+
return h('div', { className: 'bt-dialog-cover', onMouseDown: event => { if (event.target === event.currentTarget) close() } }, h('section', { ref, onKeyDown, className: 'bt-dialog', role: 'dialog', 'aria-modal': true, 'aria-label': title }, [
|
|
342
|
+
h('header', { key: 'h', className: 'bt-dialog-header' }, [h('div', { key: 't' }, [h('h2', { key: 'title' }, title), description ? h('p', { key: 'd' }, description) : null]), h(IconButton, { key: 'x', label: '关闭对话框', icon: 'close', onClick: close })]),
|
|
343
|
+
h('div', { key: 'b', className: 'bt-dialog-body' }, children),
|
|
344
|
+
h('footer', { key: 'f', className: 'bt-dialog-footer' }, footer),
|
|
345
|
+
]))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function SidebarBatonIcon({ size = 18, active = false }) {
|
|
349
|
+
const attention = useSyncExternalStore(subscribeCount, countSnapshot)
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
if (active) return undefined
|
|
352
|
+
let mounted = true
|
|
353
|
+
let timer
|
|
354
|
+
async function poll() {
|
|
355
|
+
try {
|
|
356
|
+
const result = await call({ action: 'counts' })
|
|
357
|
+
if (mounted) setAttentionCount(result.attention)
|
|
358
|
+
} catch {}
|
|
359
|
+
if (mounted) timer = setTimeout(poll, 15000)
|
|
360
|
+
}
|
|
361
|
+
poll()
|
|
362
|
+
return () => { mounted = false; clearTimeout(timer) }
|
|
363
|
+
}, [active])
|
|
364
|
+
return h('span', {
|
|
365
|
+
className: 'bt-nav-icon', style: { width: size, height: size },
|
|
366
|
+
title: attention > 0 ? attention + ' 条待处理' : '接力',
|
|
367
|
+
'aria-hidden': true,
|
|
368
|
+
}, h(Icon, { name: 'inbox' }))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
class PanelBoundary extends React.Component {
|
|
372
|
+
constructor(props) { super(props); this.state = { error: null } }
|
|
373
|
+
static getDerivedStateFromError(error) { return { error } }
|
|
374
|
+
componentDidCatch(error) { console.error('baton panel crashed', error) }
|
|
375
|
+
render() {
|
|
376
|
+
if (this.state.error) return h('div', { style: { padding: 24, color: '#b42318', fontSize: 13 } }, 'Baton 面板加载失败:' + (this.state.error?.message ?? '未知错误'))
|
|
377
|
+
return this.props.children
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function MainPanel() { return h(PanelBoundary, null, h(Panel, { embedded: true })) }
|
|
382
|
+
|
|
383
|
+
function EnvPicker({ value, options, disabled, onChange }) {
|
|
384
|
+
const [open, setOpen] = useState(false)
|
|
385
|
+
const [active, setActive] = useState(-1)
|
|
386
|
+
const ref = useRef(null)
|
|
387
|
+
const selected = options.some(option => option.key === value)
|
|
388
|
+
const query = value && !selected ? value.toLowerCase() : ''
|
|
389
|
+
const filtered = options.filter(option => !query || option.key.toLowerCase().includes(query) || String(option.label ?? '').toLowerCase().includes(query))
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (!open) return undefined
|
|
392
|
+
const close = event => { if (!ref.current?.contains(event.target)) setOpen(false) }
|
|
393
|
+
document.addEventListener('mousedown', close)
|
|
394
|
+
return () => document.removeEventListener('mousedown', close)
|
|
395
|
+
}, [open])
|
|
396
|
+
const choose = key => { onChange(key); setOpen(false); setActive(-1) }
|
|
397
|
+
return h('div', { ref, className: 'bt-combo', onKeyDown: event => {
|
|
398
|
+
if (event.key === 'Escape' && open) {
|
|
399
|
+
event.preventDefault()
|
|
400
|
+
event.stopPropagation()
|
|
401
|
+
ref.current?.querySelector('input')?.focus()
|
|
402
|
+
setOpen(false)
|
|
403
|
+
setActive(-1)
|
|
404
|
+
}
|
|
405
|
+
} }, [
|
|
406
|
+
h('input', {
|
|
407
|
+
key: 'input', role: 'combobox', 'aria-expanded': open, 'aria-controls': 'bt-env-options',
|
|
408
|
+
value, maxLength: 64, disabled, placeholder: 'frontend',
|
|
409
|
+
onFocus: () => { if (!disabled && !value) setOpen(true) },
|
|
410
|
+
onClick: () => { if (!disabled) setOpen(true) },
|
|
411
|
+
onChange: event => { onChange(event.target.value); setOpen(true); setActive(-1) },
|
|
412
|
+
onKeyDown: event => {
|
|
413
|
+
if (event.key === 'ArrowDown') { event.preventDefault(); setOpen(true); setActive(index => Math.min(filtered.length - 1, index + 1)) }
|
|
414
|
+
else if (event.key === 'ArrowUp') { event.preventDefault(); setActive(index => Math.max(-1, index - 1)) }
|
|
415
|
+
else if (event.key === 'Enter' && open && active >= 0 && filtered[active]) { event.preventDefault(); choose(filtered[active].key) }
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
418
|
+
open && filtered.length ? h('div', { key: 'menu', className: 'bt-combo-menu', id: 'bt-env-options', role: 'listbox' }, filtered.map((option, index) => h('button', {
|
|
419
|
+
key: option.key, type: 'button', role: 'option', 'aria-selected': option.key === value,
|
|
420
|
+
className: 'bt-combo-option' + (index === active ? ' active' : ''),
|
|
421
|
+
onMouseDown: event => event.preventDefault(),
|
|
422
|
+
onClick: () => choose(option.key),
|
|
423
|
+
}, [
|
|
424
|
+
h('span', { key: 'k', className: 'bt-combo-key' }, option.key),
|
|
425
|
+
option.label && option.label !== option.key ? h('span', { key: 'l', className: 'bt-combo-label' }, option.label) : null,
|
|
426
|
+
]))) : null,
|
|
427
|
+
])
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function EnvironmentDialog({ initial, close, onSaved }) {
|
|
431
|
+
const [rows, setRows] = useState(() => (initial ?? []).map(({ key, label, cwd }) => ({ key, label: label ?? key, cwd })))
|
|
432
|
+
const [error, setError] = useState('')
|
|
433
|
+
const [busy, setBusy] = useState(false)
|
|
434
|
+
useEffect(() => {
|
|
435
|
+
let active = true
|
|
436
|
+
call({ action: 'environments.list' })
|
|
437
|
+
.then(result => { if (active && Array.isArray(result.environments)) setRows(result.environments.map(({ key, label, cwd }) => ({ key, label: label ?? key, cwd }))) })
|
|
438
|
+
.catch(() => {})
|
|
439
|
+
return () => { active = false }
|
|
440
|
+
}, [])
|
|
441
|
+
const change = (index, field, value) => setRows(current => current.map((row, position) => position === index ? { ...row, [field]: value } : row))
|
|
442
|
+
const browse = async (index) => {
|
|
443
|
+
try { const path = await pickDirectory(); if (path) change(index, 'cwd', path) } catch (failure) { setError(failure.message) }
|
|
444
|
+
}
|
|
445
|
+
const save = async () => {
|
|
446
|
+
const clean = rows.map(row => ({ key: row.key.trim(), label: row.label.trim() || row.key.trim(), cwd: row.cwd.trim() })).filter(row => row.key || row.label || row.cwd)
|
|
447
|
+
const keys = new Set()
|
|
448
|
+
for (const row of clean) {
|
|
449
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(row.key)) { setError('key 只能包含字母、数字、下划线和连字符,长度 1–64。'); return }
|
|
450
|
+
if (keys.has(row.key)) { setError('存在重复的环境 key。'); return }
|
|
451
|
+
keys.add(row.key)
|
|
452
|
+
if (!row.cwd) { setError('每个环境都需要一个本机绝对目录。'); return }
|
|
453
|
+
}
|
|
454
|
+
setBusy(true)
|
|
455
|
+
setError('')
|
|
456
|
+
try {
|
|
457
|
+
const result = await call({ action: 'environments.save', environments: clean })
|
|
458
|
+
onSaved(result.environments ?? clean, result.published === false ? '已保存在本机,连接恢复后会同步给队友。' : undefined)
|
|
459
|
+
} catch (failure) {
|
|
460
|
+
if (failure.data?.saved) onSaved(failure.data.environments ?? clean, '本机配置已保存,但尚未同步给队友。请稍后重新保存或重连。')
|
|
461
|
+
else setError(failure.message)
|
|
462
|
+
} finally {
|
|
463
|
+
setBusy(false)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const cancel = h(Button, { key: 'cancel', disabled: busy, onClick: close }, '取消')
|
|
467
|
+
return h(Dialog, {
|
|
468
|
+
title: '本机执行环境', close: () => { if (!busy) close() },
|
|
469
|
+
description: '这些环境只在本机生效。队友只会看到 key 和名称,不会看到你的目录。',
|
|
470
|
+
footer: [cancel, h(Button, { key: 'save', tone: 'primary', busy, disabled: busy, onClick: save }, '保存')],
|
|
471
|
+
}, [
|
|
472
|
+
h('div', { key: 'head', className: 'bt-env-head' }, [h('span', { key: 'k' }, 'KEY'), h('span', { key: 'l' }, '名称'), h('span', { key: 'c' }, '本机绝对目录'), h('span', { key: 'a' })]),
|
|
473
|
+
...rows.map((row, index) => h('div', { key: index, className: 'bt-env-row' }, [
|
|
474
|
+
h('input', { key: 'k', 'aria-label': '环境 key ' + (index + 1), value: row.key, maxLength: 64, disabled: busy, placeholder: 'frontend', onChange: event => change(index, 'key', event.target.value) }),
|
|
475
|
+
h('input', { key: 'l', 'aria-label': '环境名称 ' + (index + 1), value: row.label, maxLength: 40, disabled: busy, placeholder: '前端', onChange: event => change(index, 'label', event.target.value) }),
|
|
476
|
+
h('input', { key: 'c', 'aria-label': '本机目录 ' + (index + 1), value: row.cwd, disabled: busy, placeholder: '/absolute/path', onChange: event => change(index, 'cwd', event.target.value) }),
|
|
477
|
+
h('div', { key: 'a', className: 'bt-env-actions' }, [
|
|
478
|
+
h(IconButton, { key: 'pick', label: '选择目录', icon: 'folder', disabled: busy, onClick: () => browse(index) }),
|
|
479
|
+
h(IconButton, { key: 'del', label: '删除环境', icon: 'close', disabled: busy, onClick: () => setRows(current => current.filter((_, position) => position !== index)) }),
|
|
480
|
+
]),
|
|
481
|
+
])),
|
|
482
|
+
rows.length === 0 ? h('p', { key: 'empty', className: 'bt-hint' }, '还没有环境。添加一个 key 和本机目录,队友才能把任务交到对应目录。') : null,
|
|
483
|
+
h(Button, { key: 'add', icon: 'plus', disabled: busy || rows.length >= 20, onClick: () => setRows(current => [...current, { key: '', label: '', cwd: '' }]) }, '添加环境'),
|
|
484
|
+
error ? h('div', { key: 'error', className: 'bt-form-error', role: 'alert' }, error) : null,
|
|
485
|
+
])
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function Panel({ embedded = false } = {}) {
|
|
489
|
+
const isOpen = useSyncExternalStore(subscribe, openSnapshot)
|
|
490
|
+
const active = embedded || isOpen
|
|
491
|
+
const [data, setData] = useState(null)
|
|
492
|
+
const [tab, setTab] = useState('incoming')
|
|
493
|
+
const [roomCode, setRoomCode] = useState('')
|
|
494
|
+
const [query, setQuery] = useState('')
|
|
495
|
+
const [onlyAttention, setOnlyAttention] = useState(false)
|
|
496
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
|
497
|
+
const [selected, setSelected] = useState(null)
|
|
498
|
+
const [detail, setDetail] = useState(null)
|
|
499
|
+
const [detailBusy, setDetailBusy] = useState(false)
|
|
500
|
+
const [busy, setBusy] = useState('')
|
|
501
|
+
const [notice, setNotice] = useState(null)
|
|
502
|
+
const [modal, setModal] = useState(null)
|
|
503
|
+
const [roomInput, setRoomInput] = useState('')
|
|
504
|
+
const [draft, setDraft] = useState(newDraft)
|
|
505
|
+
const [draftError, setDraftError] = useState('')
|
|
506
|
+
const [uncertain, setUncertain] = useState(false)
|
|
507
|
+
const [missingSessions, setMissingSessions] = useState({})
|
|
508
|
+
const ref = useRef(null)
|
|
509
|
+
const busyRef = useRef(false)
|
|
510
|
+
const refreshSequence = useRef(0)
|
|
511
|
+
const detailSequence = useRef(0)
|
|
512
|
+
const selectedRef = useRef(selected)
|
|
513
|
+
selectedRef.current = selected
|
|
514
|
+
const onKeyDown = useFocusTrap(ref, embedded ? false : (isOpen && !modal), () => setOpen(false))
|
|
515
|
+
|
|
516
|
+
const refresh = useCallback(async () => {
|
|
517
|
+
const sequence = ++refreshSequence.current
|
|
518
|
+
try {
|
|
519
|
+
const next = await call({ action: 'snapshot' })
|
|
520
|
+
if (sequence === refreshSequence.current) setData(next)
|
|
521
|
+
} catch (error) {
|
|
522
|
+
if (sequence === refreshSequence.current) setNotice({ tone: 'error', message: error.message })
|
|
523
|
+
}
|
|
524
|
+
}, [])
|
|
525
|
+
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
if (!active) return undefined
|
|
528
|
+
let timer
|
|
529
|
+
let mounted = true
|
|
530
|
+
async function poll() { await refresh(); if (mounted) timer = setTimeout(poll, 5000) }
|
|
531
|
+
poll()
|
|
532
|
+
return () => { mounted = false; clearTimeout(timer) }
|
|
533
|
+
}, [active, refresh])
|
|
534
|
+
|
|
535
|
+
const online = data?.connection?.state === 'online'
|
|
536
|
+
const rooms = data?.rooms ?? []
|
|
537
|
+
const allTasks = data?.tasks ?? []
|
|
538
|
+
const local = data?.local ?? {}
|
|
539
|
+
const room = rooms.find(item => item.code === roomCode)
|
|
540
|
+
const incoming = allTasks.filter(task => task.to.userId === data?.userId && visibilityFor(task, data?.userId) === 'active')
|
|
541
|
+
const outgoing = allTasks.filter(task => task.from.userId === data?.userId && visibilityFor(task, data?.userId) === 'active')
|
|
542
|
+
const archived = allTasks.filter(task => visibilityFor(task, data?.userId) === 'archived')
|
|
543
|
+
const attention = task => task.status === 'pending' || task.status === 'accepted' && ['unsubmitted', 'needs-review'].includes(local[task.taskId]?.phase ?? 'needs-review')
|
|
544
|
+
const unread = incoming.filter(attention).length
|
|
545
|
+
useEffect(() => { setAttentionCount(unread) }, [unread])
|
|
546
|
+
const rows = (tab === 'incoming' ? incoming : tab === 'outgoing' ? outgoing : archived).filter(task => !roomCode || task.room === roomCode).filter(task => !onlyAttention || (tab === 'incoming' ? attention(task) : task.status === 'pending')).filter(task => [task.title, task.from.displayName, task.to.displayName].some(value => String(value ?? '').toLowerCase().includes(query.toLowerCase()))).sort((a, b) => b.createdAt - a.createdAt)
|
|
547
|
+
const task = rows.find(item => item.taskId === selected)
|
|
548
|
+
const record = local[selected] ?? detail?.local
|
|
549
|
+
const mine = task?.to.userId === data?.userId
|
|
550
|
+
const connectionText = online ? '已连接' : ['connecting', 'authenticating', 'reconnecting'].includes(data?.connection?.state) ? '正在连接' : data ? '未连接' : '连接中'
|
|
551
|
+
const modalClose = () => { if (!busyRef.current) { setModal(null); setRoomInput('') } }
|
|
552
|
+
|
|
553
|
+
async function revealSession(sessionId) {
|
|
554
|
+
try {
|
|
555
|
+
await openSession(sessionId)
|
|
556
|
+
setOpen(false)
|
|
557
|
+
} catch {
|
|
558
|
+
setNotice({ tone: 'error', message: '会话已保留,暂时无法打开。请稍后点击“定位会话”重试。' })
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function locateSession(sessionId) {
|
|
563
|
+
if (missingSessions[sessionId]) { setNotice({ tone: 'error', message: '会话已删除,无法定位。' }); return }
|
|
564
|
+
try {
|
|
565
|
+
const result = await call({ action: 'session', sessionId })
|
|
566
|
+
if (!result.exists) {
|
|
567
|
+
setMissingSessions(previous => ({ ...previous, [sessionId]: true }))
|
|
568
|
+
setNotice({ tone: 'error', message: '会话已删除,无法定位。' })
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
await revealSession(sessionId)
|
|
572
|
+
} catch (error) {
|
|
573
|
+
setNotice({ tone: 'error', message: error.message })
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function loadDetail(id) {
|
|
578
|
+
const sequence = ++detailSequence.current
|
|
579
|
+
setDetailBusy(true)
|
|
580
|
+
try {
|
|
581
|
+
const next = await call({ action: 'detail', taskId: id })
|
|
582
|
+
if (sequence === detailSequence.current && selectedRef.current === id) setDetail(next)
|
|
583
|
+
} catch (error) {
|
|
584
|
+
if (sequence === detailSequence.current && selectedRef.current === id) { setDetail(null); setNotice({ tone: 'error', message: error.message }) }
|
|
585
|
+
} finally { if (sequence === detailSequence.current) setDetailBusy(false) }
|
|
586
|
+
}
|
|
587
|
+
function selectTask(id) {
|
|
588
|
+
selectedRef.current = id
|
|
589
|
+
setSelected(id)
|
|
590
|
+
setDetail(null)
|
|
591
|
+
loadDetail(id)
|
|
592
|
+
}
|
|
593
|
+
async function run(body) {
|
|
594
|
+
if (busyRef.current) return { ok: false }
|
|
595
|
+
busyRef.current = true
|
|
596
|
+
setBusy(body.action)
|
|
597
|
+
try {
|
|
598
|
+
const result = await call(body)
|
|
599
|
+
await refresh()
|
|
600
|
+
if (result.record) setData(previous => previous ? { ...previous, local: { ...previous.local, [body.taskId]: result.record } } : previous)
|
|
601
|
+
return result
|
|
602
|
+
} catch (error) {
|
|
603
|
+
setNotice({ tone: 'error', message: error.message })
|
|
604
|
+
if (error.data?.code === 'cwd-changed' && body.taskId && selectedRef.current === body.taskId) await loadDetail(body.taskId)
|
|
605
|
+
await refresh()
|
|
606
|
+
return { ...error.data, ok: false, error: error.message }
|
|
607
|
+
} finally {
|
|
608
|
+
busyRef.current = false
|
|
609
|
+
setBusy('')
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function submitDraft() {
|
|
613
|
+
if (busyRef.current) return
|
|
614
|
+
const result = await run({ action: 'send', ...draft })
|
|
615
|
+
if (result.ok) {
|
|
616
|
+
setDraft(newDraft(roomCode))
|
|
617
|
+
setUncertain(false)
|
|
618
|
+
setDraftError('')
|
|
619
|
+
setModal(null)
|
|
620
|
+
setTab('outgoing')
|
|
621
|
+
setNotice({ tone: 'success', message: '接力已送出,等待对方确认。' })
|
|
622
|
+
} else {
|
|
623
|
+
const unknown = Boolean(result.taskId) || ['timeout', 'internal', 'connection-closed'].includes(result.code)
|
|
624
|
+
setUncertain(unknown)
|
|
625
|
+
setDraftError(unknown ? '投递结果暂未确认。内容已保留,重试会使用同一个任务标识。' : result.error ?? '发送未完成,内容已保留。')
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function updateDraft(field, value) {
|
|
629
|
+
if (busyRef.current || uncertain) return
|
|
630
|
+
setDraft(previous => {
|
|
631
|
+
const next = { ...previous, [field]: value, taskId: uncertain ? previous.taskId : crypto.randomUUID() }
|
|
632
|
+
if (field === 'room') { next.to = ''; next.env = '' }
|
|
633
|
+
if (field === 'to') {
|
|
634
|
+
const member = rooms.find(room => room.code === previous.room)?.members.find(entry => entry.userId === value)
|
|
635
|
+
next.env = member?.environments?.length === 1 ? member.environments[0].key : ''
|
|
636
|
+
}
|
|
637
|
+
return next
|
|
638
|
+
})
|
|
639
|
+
if (!uncertain) setDraftError('')
|
|
640
|
+
}
|
|
641
|
+
function separateDraft() {
|
|
642
|
+
if (busyRef.current) return
|
|
643
|
+
setDraft(newDraft(rooms.some(item => item.code === roomCode) ? roomCode : ''))
|
|
644
|
+
setUncertain(false)
|
|
645
|
+
setDraftError('')
|
|
646
|
+
setNotice({ tone: 'success', message: '原投递记录已保留,可在“已发出的”中继续确认。当前是独立的新任务。' })
|
|
647
|
+
}
|
|
648
|
+
function switchTab(value) { setTab(value); setSelected(null); setDetail(null); setOnlyAttention(false) }
|
|
649
|
+
function confirm(kind, item) { setModal({ kind, task: item }) }
|
|
650
|
+
async function restoreTask(item) {
|
|
651
|
+
const result = await run({ action: 'restore', taskId: item.taskId })
|
|
652
|
+
if (result.ok) {
|
|
653
|
+
if (selected === item.taskId) { setSelected(null); setDetail(null) }
|
|
654
|
+
setNotice({ tone: 'success', message: '任务已恢复,关联会话保持原样。' })
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function finishStart(result) {
|
|
658
|
+
const sessionId = result.sessionId ?? result.record?.sessionId
|
|
659
|
+
if (sessionId) await revealSession(sessionId)
|
|
660
|
+
else setNotice({ tone: 'success', message: '任务已接手,消息已提交到新的 DSH 会话。' })
|
|
661
|
+
}
|
|
662
|
+
async function approve() {
|
|
663
|
+
const result = await run({ action: 'accept', taskId: task.taskId, cwd: detail.cwd })
|
|
664
|
+
if (result.ok) await finishStart(result)
|
|
665
|
+
}
|
|
666
|
+
async function retrySendTask(item) {
|
|
667
|
+
const result = await run({ action: 'retry-send', taskId: item.taskId })
|
|
668
|
+
if (!result.ok) return
|
|
669
|
+
if (draft.taskId === item.taskId) { setDraft(newDraft(roomCode)); setUncertain(false); setDraftError('') }
|
|
670
|
+
setNotice({ tone: 'success', message: '投递已确认。' })
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (!active) return null
|
|
674
|
+
|
|
675
|
+
const renderTask = item => {
|
|
676
|
+
const inbound = item.to.userId === data?.userId
|
|
677
|
+
const person = inbound ? item.from : item.to
|
|
678
|
+
const removed = visibilityFor(item, data?.userId) === 'archived'
|
|
679
|
+
const peerVisibility = inbound ? item.fromVisibility : item.toVisibility
|
|
680
|
+
const sessionId = local[item.taskId]?.sessionId
|
|
681
|
+
const sessionAction = sessionId
|
|
682
|
+
? missingSessions[sessionId]
|
|
683
|
+
? h('span', { key: 'locate', className: 'bt-task-link deleted' }, '会话已删除')
|
|
684
|
+
: h('button', { key: 'locate', type: 'button', className: 'bt-task-link', onClick: event => { event.stopPropagation(); locateSession(sessionId) } }, '定位会话')
|
|
685
|
+
: null
|
|
686
|
+
return h('div', {
|
|
687
|
+
key: item.taskId, role: 'button', tabIndex: 0,
|
|
688
|
+
className: 'bt-task' + (selected === item.taskId ? ' selected' : ''),
|
|
689
|
+
'aria-pressed': selected === item.taskId,
|
|
690
|
+
onClick: () => selectTask(item.taskId),
|
|
691
|
+
onKeyDown: event => { if (event.target !== event.currentTarget) return; if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectTask(item.taskId) } },
|
|
692
|
+
}, [
|
|
693
|
+
h('div', { key: 'top', className: 'bt-task-top' }, [h('span', { key: 'p', className: 'bt-person' }, [h(Avatar, { key: 'a', name: person.displayName }), person.displayName]), h('div', { key: 'actions', className: 'bt-task-actions' }, [
|
|
694
|
+
h('time', { key: 'time', className: 'bt-time', dateTime: new Date(item.createdAt).toISOString() }, relative(item.createdAt)),
|
|
695
|
+
removed ? h(IconButton, { key: 'restore', label: '恢复任务', icon: 'refresh', disabled: !!busy || !online, onClick: event => { event.stopPropagation(); restoreTask(item) } }) : null,
|
|
696
|
+
h(IconButton, { key: 'delete', label: removed ? '永久删除任务' : '删除任务', icon: 'trash', disabled: !!busy || !online, onClick: event => { event.stopPropagation(); confirm(removed ? 'delete' : 'archive', item) } }),
|
|
697
|
+
])]),
|
|
698
|
+
h('div', { key: 'title', className: 'bt-task-title' }, item.title),
|
|
699
|
+
h('div', { key: 'bottom', className: 'bt-task-bottom' }, [h('span', { key: 'env', className: 'bt-env' }, [h(Icon, { key: 'i', name: 'folder' }), item.env]), h('div', { key: 'badges', className: 'bt-task-badges' }, [sessionAction, h(Badge, { key: 'status', value: removed ? { text: '已删除', tone: '' } : statusFor(item, local[item.taskId], inbound) })])]),
|
|
700
|
+
peerVisibility && peerVisibility !== 'active' ? h('div', { key: 'peer', className: 'bt-delete-note' }, peerVisibility === 'deleted' ? '对方已永久删除' : '对方已移入已删除') : null,
|
|
701
|
+
])
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const renderDetail = () => {
|
|
705
|
+
if (!task) return h(Empty, { title: '选择一条接力任务', description: '查看任务说明与执行目录,确认后再开始。' })
|
|
706
|
+
const person = mine ? task.from : task.to
|
|
707
|
+
const removed = visibilityFor(task, data?.userId) === 'archived'
|
|
708
|
+
const canAccept = !removed && mine && task.status === 'pending'
|
|
709
|
+
let footerAction
|
|
710
|
+
if (removed) {
|
|
711
|
+
footerAction = h('div', { className: 'bt-detail-buttons' }, [
|
|
712
|
+
h(Button, { key: 'restore', tone: 'primary', disabled: !!busy || !online, onClick: () => restoreTask(task) }, '恢复任务'),
|
|
713
|
+
h(Button, { key: 'delete', tone: 'danger', disabled: !!busy || !online, onClick: () => confirm('delete', task) }, '永久删除'),
|
|
714
|
+
])
|
|
715
|
+
} else if (canAccept) {
|
|
716
|
+
footerAction = h('div', { key: 'actions', className: 'bt-detail-buttons' }, [
|
|
717
|
+
h(Button, { key: 'reject', disabled: !!busy || !online, onClick: () => confirm('decline', task) }, '拒绝'),
|
|
718
|
+
h(Button, { key: 'accept', tone: 'primary', icon: 'arrow', busy: busy === 'accept', disabled: !!busy || !online || detailBusy || !detail?.cwd || detail?.task?.taskId !== selected, onClick: approve }, busy === 'accept' ? '正在启动…' : '接受并启动'),
|
|
719
|
+
])
|
|
720
|
+
} else if (mine && record?.phase === 'unsubmitted' && task.status === 'accepted') {
|
|
721
|
+
footerAction = h(Button, { key: 'resume', tone: 'primary full', icon: 'arrow', busy: busy === 'resume', disabled: !!busy || !online || !detail?.cwd || detailBusy || detail?.task?.taskId !== selected, onClick: async () => { const result = await run({ action: 'resume', taskId: task.taskId, cwd: detail.cwd }); if (result.ok) await finishStart(result) } }, '继续启动')
|
|
722
|
+
} else if (mine && record?.phase === 'needs-review') {
|
|
723
|
+
footerAction = h(Button, { key: 'handled', tone: 'full', disabled: !!busy || !online, onClick: () => confirm('handled', task) }, '我已核对并自行处理')
|
|
724
|
+
} else {
|
|
725
|
+
footerAction = h('div', { key: 'terminal', className: 'bt-hint', style: { textAlign: 'center' } }, !mine ? '审批状态会自动更新;对方的会话内容仅对本人可见。' : record?.phase === 'submitted' ? '消息已提交,可在 DSH 侧边栏中查看会话。' : record?.phase === 'handled' ? '已标记为自行处理,不会再次启动。' : task.status === 'declined' ? '你已拒绝这次接力。' : '任务已接手。')
|
|
726
|
+
}
|
|
727
|
+
return [
|
|
728
|
+
h('div', { key: 'body', className: 'bt-detail-inner' }, [
|
|
729
|
+
h('div', { key: 'kicker', className: 'bt-detail-kicker' }, [h('span', { key: 'r' }, task.room + ' / 接力详情'), h('div', { key: 'a', style: { display: 'flex', gap: 5 } }, [h('button', { key: 'back', type: 'button', className: 'bt-icon-button bt-mobile-back', 'aria-label': '返回任务列表', onClick: () => setSelected(null) }, h(Icon, { name: 'back' })), h(CopyButton, { key: 'copy', label: '复制任务标识', value: task.taskId })])]),
|
|
730
|
+
h('h2', { key: 'title', className: 'bt-detail-title' }, task.title),
|
|
731
|
+
h('div', { key: 'person', className: 'bt-detail-person' }, [h(Avatar, { key: 'a', name: person.displayName }), h('div', { key: 'n', style: { flex: 1 } }, [h('strong', { key: 's' }, person.displayName + (mine ? ' 发来的接力' : ' 将收到这次接力')), h('small', { key: 'd' }, relative(task.createdAt) + ' · ' + task.env)]), h(Badge, { key: 'b', value: statusFor(task, record, mine) })]),
|
|
732
|
+
h('div', { key: 'label', className: 'bt-section-label' }, '任务说明'),
|
|
733
|
+
removed ? h('div', { key: 'removed', className: 'bt-delete-note' }, '已从你的列表移除。恢复后可以继续处理,关联会话不会删除。') : null,
|
|
734
|
+
detailBusy ? h('div', { key: 'loading', className: 'bt-hint', role: 'status' }, '正在读取任务说明…') : detail?.task?.taskId === selected ? h('div', { key: 'brief', className: 'bt-brief' }, detail.brief) : h('p', { key: 'missing', className: 'bt-hint' }, online ? '暂时无法读取说明,请重新选择这条任务。' : '连接恢复后即可查看任务说明。'),
|
|
735
|
+
mine && detail?.task?.taskId === selected ? h('div', { key: 'directory', className: 'bt-directory' }, [h('div', { key: 'h', className: 'bt-directory-head' }, [h(Icon, { key: 'i', name: 'folder' }), '本机执行目录']), detail.cwd ? h('code', { key: 'path', className: 'bt-path' }, detail.cwd) : h('span', { key: 'err', className: 'bt-hint' }, detail.cwdError ?? '该环境尚未配置,请先完善插件配置。')]) : null,
|
|
736
|
+
record?.phase === 'needs-review' ? h('div', { key: 'warning', className: 'bt-warning' }, [h('strong', { key: 't' }, '上次启动结果需要核对'), h('span', { key: 'd' }, record.sessionId ? '请先在 DSH 中检查该会话是否已有消息。确认前不会再次提交。' : '本机缺少完整启动记录,请在 DSH 会话列表中核对并自行处理。')]) : null,
|
|
737
|
+
record?.phase === 'unsubmitted' && task.status === 'accepted' ? h('div', { key: 'retry', className: 'bt-warning' }, [h('strong', { key: 't' }, '已接手,尚未提交消息'), h('span', { key: 'd' }, record.sessionId ? '会话已保留。继续启动会复用同一会话与执行目录。' : '上次启动未完成,确认目录后可以继续。')]) : null,
|
|
738
|
+
record?.sessionId ? h('div', { key: 'session', style: { marginTop: 16, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } }, [
|
|
739
|
+
missingSessions[record.sessionId]
|
|
740
|
+
? h('span', { key: 'missing', className: 'bt-task-link deleted' }, '会话已删除')
|
|
741
|
+
: h(Button, { key: 'locate', icon: 'arrow', tone: 'ghost', disabled: !online, onClick: () => locateSession(record.sessionId) }, '定位会话'),
|
|
742
|
+
h(CopyButton, { key: 'copy-session', icon: 'copy', tone: 'ghost', value: record.sessionId, label: '复制会话标识' }, '复制会话标识'),
|
|
743
|
+
]) : null,
|
|
744
|
+
]),
|
|
745
|
+
h('footer', { key: 'footer', className: 'bt-detail-footer' }, [
|
|
746
|
+
footerAction,
|
|
747
|
+
canAccept ? h('p', { key: 'hint', className: 'bt-hint' }, '接受后才创建会话,并提交上面的任务说明。') : null,
|
|
748
|
+
]),
|
|
749
|
+
]
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const renderModal = () => {
|
|
753
|
+
if (!modal) return null
|
|
754
|
+
const cancel = h(Button, { key: 'cancel', disabled: !!busy, onClick: modalClose }, '取消')
|
|
755
|
+
if (modal.kind === 'environments') {
|
|
756
|
+
return h(EnvironmentDialog, {
|
|
757
|
+
initial: data?.environments ?? [],
|
|
758
|
+
close: modalClose,
|
|
759
|
+
onSaved: async (next, warning) => {
|
|
760
|
+
setModal(null)
|
|
761
|
+
setNotice({ tone: warning ? 'error' : 'success', message: warning ?? '本机执行环境已保存并同步给队友。' })
|
|
762
|
+
await refresh()
|
|
763
|
+
},
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
if (modal.kind === 'archive' || modal.kind === 'delete') {
|
|
767
|
+
const permanent = modal.kind === 'delete'
|
|
768
|
+
return h(Dialog, {
|
|
769
|
+
title: permanent ? '永久删除这条任务?' : '移入已删除?', close: modalClose,
|
|
770
|
+
description: permanent ? '确认后你将无法恢复或继续处理。只有双方都确认永久删除后才会物理清理任务内容。关联的 DSH 会话始终保留。' : '只从你的列表移除,可在“已删除”中恢复。对方的任务和关联会话都不会删除。',
|
|
771
|
+
footer: [cancel, h(Button, { key: 'confirm', tone: 'danger', busy: !!busy, disabled: !!busy || !online, onClick: async () => {
|
|
772
|
+
const result = await run({ action: modal.kind, taskId: modal.task.taskId, confirmed: true })
|
|
773
|
+
if (!result.ok) return
|
|
774
|
+
setModal(null)
|
|
775
|
+
if (selected === modal.task.taskId) { setSelected(null); setDetail(null) }
|
|
776
|
+
setNotice({ tone: 'success', message: permanent ? result.purged ? '双方均已确认,任务内容已清理。关联会话保留。' : '你已永久删除,等待对方确认后清理任务内容。关联会话保留。' : '已移入“已删除”,可以恢复或再次确认永久删除。' })
|
|
777
|
+
} }, permanent ? '确认永久删除' : '确认删除')],
|
|
778
|
+
}, h('p', { className: 'bt-hint' }, modal.task.title))
|
|
779
|
+
}
|
|
780
|
+
if (modal.kind === 'send') {
|
|
781
|
+
const selectedRoom = rooms.find(item => item.code === draft.room)
|
|
782
|
+
const members = selectedRoom?.members.filter(member => member.userId !== data?.userId) ?? []
|
|
783
|
+
const recipient = members.find(member => member.userId === draft.to)
|
|
784
|
+
const recipientEnvironments = recipient?.environments ?? []
|
|
785
|
+
const envInvalid = recipient !== undefined && recipientEnvironments.length > 0 && !recipientEnvironments.some(env => env.key === draft.env)
|
|
786
|
+
const originalSaved = [...(data?.outbox ?? []), ...allTasks].some(item => item.taskId === draft.taskId)
|
|
787
|
+
return h(Dialog, { title: '发起一次接力', description: '把任务说明交给一位队友,由对方确认后开始。', close: modalClose, footer: [cancel, h(Button, { key: 'send', tone: 'primary', icon: 'send', busy: busy === 'send', disabled: !!busy || !online || !draft.room || !draft.to || !recipient || envInvalid || !/^[A-Za-z0-9_-]{1,64}$/.test(draft.env) || !draft.title.trim() || !draft.brief.trim() || new TextEncoder().encode(draft.brief).length > 8192, onClick: submitDraft }, uncertain ? '确认并重试投递' : '发送接力')] }, [
|
|
788
|
+
draftError ? h('div', { key: 'error', className: 'bt-form-error', role: 'alert' }, draftError) : null,
|
|
789
|
+
uncertain ? h('div', { key: 'separate', className: 'bt-warning' }, [
|
|
790
|
+
h('span', { key: 'hint' }, originalSaved ? '原投递可能已送达。可保留记录并另建独立任务,请避免重复交办同一工作。' : '原投递记录尚未确认,请先重试或刷新,确认保存后即可另建任务。'),
|
|
791
|
+
h(Button, { key: 'new', disabled: !!busy || !originalSaved, onClick: separateDraft }, '保留原投递,另建接力'),
|
|
792
|
+
]) : null,
|
|
793
|
+
h('div', { key: 'people', className: 'bt-two-fields' }, [
|
|
794
|
+
h(Field, { key: 'room', label: '协作房间' }, h('select', { value: draft.room, disabled: uncertain || !!busy, onChange: event => updateDraft('room', event.target.value) }, [h('option', { key: 'empty', value: '' }, '选择房间'), ...rooms.map(item => h('option', { key: item.code, value: item.code }, item.name))])),
|
|
795
|
+
h(Field, { key: 'to', label: '交给谁' }, h('select', { value: draft.to, disabled: uncertain || !!busy, onChange: event => updateDraft('to', event.target.value) }, [h('option', { key: 'empty', value: '' }, '选择成员'), ...members.map(member => h('option', { key: member.userId, value: member.userId }, member.displayName + ' · ' + member.userId.slice(0, 8) + (member.online ? '' : ' · 离线')))])),
|
|
796
|
+
]),
|
|
797
|
+
h(Field, { key: 'title', label: '任务标题' }, h('input', { value: draft.title, maxLength: 120, disabled: uncertain || !!busy, placeholder: '例如:完成登录页的接口联调', onChange: event => updateDraft('title', event.target.value) })),
|
|
798
|
+
h(Field, { key: 'env', label: '对方的执行环境', hint: !recipient ? '先选择接收人。' : recipientEnvironments.length ? '对方配置的环境;对方会映射到自己的本机目录。' : '对方还没有上报执行环境,可先手动填写 key,建议让对方在顶栏「本机执行环境」中添加。' }, h(EnvPicker, { value: draft.env, options: recipientEnvironments, disabled: !recipient || uncertain || !!busy, onChange: value => updateDraft('env', value) })),
|
|
799
|
+
h(Field, { key: 'brief', label: '任务说明', hint: '说明目标、完成标准和必要背景。' + new TextEncoder().encode(draft.brief).length + ' / 8192 字节' }, h('textarea', { value: draft.brief, disabled: uncertain || !!busy, placeholder: '请描述任务目标与完成标准…', onChange: event => updateDraft('brief', event.target.value) })),
|
|
800
|
+
])
|
|
801
|
+
}
|
|
802
|
+
if (modal.kind === 'create' || modal.kind === 'join') {
|
|
803
|
+
const creating = modal.kind === 'create'
|
|
804
|
+
return h(Dialog, { title: creating ? '创建协作房间' : '加入协作房间', description: creating ? '创建后,把房间号分享给队友。每个房间最多 10 人。' : '向队友获取六位房间号,即可加入协作。', close: modalClose, footer: [cancel, h(Button, { key: 'ok', tone: 'primary', busy: !!busy, disabled: !!busy || !online || !roomInput.trim(), onClick: async () => { const result = await run(creating ? { action: 'create', name: roomInput.trim() } : { action: 'join', code: roomInput.trim().toUpperCase() }); if (result.ok) { setRoomCode(result.code ?? ''); setModal(null); setRoomInput(''); setNotice({ tone: 'success', message: creating ? '房间已创建,邀请队友开始协作吧。' : '已加入房间。' }) } } }, creating ? '创建房间' : '加入房间')] }, h(Field, { label: creating ? '房间名称' : '房间号' }, h('input', { autoFocus: true, value: roomInput, maxLength: creating ? 40 : 8, placeholder: creating ? '例如:产品与研发' : 'ABC234', onChange: event => setRoomInput(creating ? event.target.value : event.target.value.toUpperCase()) })))
|
|
805
|
+
}
|
|
806
|
+
if (modal.kind === 'rooms') {
|
|
807
|
+
return h(Dialog, { title: '协作房间', description: '选择房间筛选任务,或创建、加入新的房间。', close: modalClose, footer: [h(Button, { key: 'create', disabled: !online, icon: 'plus', onClick: () => setModal({ kind: 'create' }) }, '创建房间'), h(Button, { key: 'join', disabled: !online, onClick: () => setModal({ kind: 'join' }) }, '加入房间')] }, [
|
|
808
|
+
h(Button, { key: 'all', onClick: () => { setRoomCode(''); setSelected(null); setModal(null) } }, '全部接力'),
|
|
809
|
+
...rooms.map(item => h('div', { key: item.code, style: { display: 'flex', gap: 8 } }, [h(Button, { key: 'room', tone: 'full', onClick: () => { setRoomCode(item.code); setSelected(null); setModal(null) } }, item.name + ' · ' + item.code), h(IconButton, { key: 'manage', label: '管理房间 ' + item.name, icon: 'dots', onClick: () => setModal({ kind: 'room', code: item.code }) })])),
|
|
810
|
+
])
|
|
811
|
+
}
|
|
812
|
+
if (modal.kind === 'room') {
|
|
813
|
+
const item = rooms.find(entry => entry.code === modal.code)
|
|
814
|
+
if (!item) return h(Dialog, { title: '房间已移除', description: '这个房间已被其他成员解散或移除。已收到的任务仍可继续处理。', close: modalClose, footer: [h(Button, { key: 'close', onClick: modalClose }, '完成')] })
|
|
815
|
+
return h(Dialog, { title: item.name, description: item.members.length + ' / 10 位成员 · 房间号仅用于加入', close: modalClose, footer: [h(Button, { key: 'exit', tone: 'danger', disabled: !!busy || !online, onClick: () => setModal({ kind: item.ownerId === data.userId ? 'disband' : 'leave', room: item }) }, item.ownerId === data.userId ? '解散房间' : '离开房间'), h(Button, { key: 'done', onClick: modalClose }, '完成')] }, [
|
|
816
|
+
h('div', { key: 'code', className: 'bt-room-code' }, [h('code', { key: 'c' }, item.code), h(CopyButton, { key: 'copy', label: '复制房间号', value: item.code })]),
|
|
817
|
+
h('div', { key: 'members' }, item.members.map(member => h('div', { key: member.userId, className: 'bt-room-member' }, [h(Avatar, { key: 'a', name: member.displayName }), h('div', { key: 'n' }, [h('div', { key: 'name' }, member.displayName + (member.userId === item.ownerId ? ' · 房主' : '')), h('div', { key: 'id', className: 'bt-profile-id' }, member.userId.slice(0, 8) + ' · ' + (member.online ? '在线' : '离线'))]), h(CopyButton, { key: 'copy', label: '复制 ' + member.displayName + ' 的 userId', value: member.userId })]))),
|
|
818
|
+
])
|
|
819
|
+
}
|
|
820
|
+
const texts = {
|
|
821
|
+
decline: ['拒绝这次接力?', '对方会看到你已拒绝。不会创建或启动会话。', '确认拒绝'],
|
|
822
|
+
handled: ['确认已核对并自行处理?', '这只会收起本机警告,不会提交消息,也不会再次启动任务。', '确认已处理'],
|
|
823
|
+
disband: ['解散这个房间?', '所有成员将离开房间。已经投递的任务仍可继续处理。', '确认解散'],
|
|
824
|
+
leave: ['离开这个房间?', '离开后无法发送新任务,已经收到的任务仍可处理。', '确认离开'],
|
|
825
|
+
}
|
|
826
|
+
const [title, description, label] = texts[modal.kind]
|
|
827
|
+
return h(Dialog, { title, description, close: modalClose, footer: [cancel, h(Button, { key: 'ok', tone: modal.kind === 'handled' ? 'primary' : 'danger', busy: !!busy, disabled: !!busy || !online, onClick: async () => { const result = await run(modal.task ? { action: modal.kind, taskId: modal.task.taskId, confirmed: true } : { action: modal.kind, code: modal.room.code }); if (result.ok) { setModal(null); if (modal.room?.code === roomCode) setRoomCode(''); setNotice({ tone: 'success', message: modal.kind === 'handled' ? '已标记为自行处理。' : modal.kind === 'decline' ? '已拒绝这次接力。' : '房间已更新。' }) } } }, label)] }, h('p', { style: { margin: 0, color: 'var(--bt-muted)', fontSize: 12, lineHeight: 1.8 } }, modal.task?.title ?? modal.room?.name))
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const shell = h('section', { ref, onKeyDown, className: 'bt-shell' + (embedded ? ' bt-embedded' : ''), role: embedded ? undefined : 'dialog', 'aria-modal': embedded ? undefined : true, 'aria-label': 'Baton 接力', style: { position: 'relative' } }, [
|
|
831
|
+
h('header', { key: 'top', className: 'bt-topbar' }, [h('div', { key: 'brand', className: 'bt-brand' }, [h('span', { key: 'mark', className: 'bt-mark', 'aria-hidden': true }), h('span', { key: 'name', className: 'bt-wordmark' }, ['Baton', h('small', { key: 'cn' }, '接力')])]), h('div', { key: 'actions', className: 'bt-top-actions' }, [h('button', { key: 'rooms', className: 'bt-icon-button bt-mobile-back', type: 'button', 'aria-label': '协作房间', onClick: () => setModal({ kind: 'rooms' }) }, h(Icon, { name: 'people' })), h('span', { key: 'status', className: 'bt-connection', role: 'status' }, [h('span', { key: 'dot', className: 'bt-dot' + (online ? ' online' : '') }), connectionText]), h('span', { key: 'line', className: 'bt-divider' }), h(IconButton, { key: 'envs', label: '配置执行环境', icon: 'folder', onClick: () => setModal({ kind: 'environments' }) }), h(IconButton, { key: 'refresh', label: '刷新接力', icon: 'refresh', onClick: refresh }), embedded ? null : h(IconButton, { key: 'close', label: '关闭接力', icon: 'close', onClick: () => setOpen(false) })])]),
|
|
832
|
+
h('div', { key: 'body', className: 'bt-body' }, [
|
|
833
|
+
h('aside', { key: 'sidebar', className: 'bt-sidebar' + (sidebarCollapsed ? ' collapsed' : ''), 'aria-label': '协作房间' }, [
|
|
834
|
+
h('div', { key: 'caption', className: 'bt-nav-heading' }, [h('span', { key: 'text', className: 'bt-nav-caption' }, 'WORKSPACE'), h(IconButton, { key: 'toggle', label: sidebarCollapsed ? '展开接力导航' : '收起接力导航', icon: 'chevron', 'aria-expanded': !sidebarCollapsed, style: { transform: sidebarCollapsed ? 'rotate(180deg)' : undefined }, onClick: () => setSidebarCollapsed(value => !value) })]),
|
|
835
|
+
h('button', { key: 'all', type: 'button', className: 'bt-nav' + (!roomCode ? ' active' : ''), onClick: () => { setRoomCode(''); setSelected(null) } }, [h(Icon, { key: 'i', name: 'inbox' }), '全部接力', unread ? h('span', { key: 'count', className: 'bt-count' }, unread) : null]),
|
|
836
|
+
h('div', { key: 'heading', className: 'bt-sidebar-heading' }, [h('span', { key: 't' }, '协作房间'), h('span', { key: 'n' }, String(rooms.length))]),
|
|
837
|
+
h('div', { key: 'rooms', className: 'bt-room-list' }, rooms.length ? rooms.map(item => h('div', { key: item.code, className: 'bt-room-row' + (roomCode === item.code ? ' active' : '') }, [h('button', { key: 'select', type: 'button', className: 'bt-room', onClick: () => { setRoomCode(item.code); setSelected(null) } }, [h('span', { key: 'symbol', className: 'bt-room-symbol' }, '#'), h('div', { key: 'text', style: { minWidth: 0 } }, [h('div', { key: 'name', className: 'bt-room-name' }, item.name), h('div', { key: 'meta', className: 'bt-room-meta' }, item.members.filter(member => member.online).length + ' 人在线 · ' + item.code)])]), h(IconButton, { key: 'more', label: '管理房间 ' + item.name, icon: 'dots', onClick: () => setModal({ kind: 'room', code: item.code }) })])) : h('p', { className: 'bt-hint', style: { padding: '0 12px' } }, '创建或加入房间,开始第一次接力。')),
|
|
838
|
+
h('div', { key: 'actions', className: 'bt-room-actions' }, [h(Button, { key: 'create', icon: 'plus', disabled: !online, onClick: () => { setRoomInput(''); setModal({ kind: 'create' }) } }, '创建'), h(Button, { key: 'join', disabled: !online, onClick: () => { setRoomInput(''); setModal({ kind: 'join' }) } }, '加入')]),
|
|
839
|
+
h('div', { key: 'profile', className: 'bt-profile' }, [h(Avatar, { key: 'a', name: data?.displayName }), h('div', { key: 'n', style: { minWidth: 0, flex: 1 } }, [h('div', { key: 'name', className: 'bt-profile-name' }, data?.displayName ?? '正在加载'), h('div', { key: 'id', className: 'bt-profile-id' }, data?.userId?.slice(0, 8) ?? '—')]), h(CopyButton, { key: 'copy', label: '复制我的 userId', value: String(data?.userId ?? ''), disabled: !data?.userId })]),
|
|
840
|
+
]),
|
|
841
|
+
h('main', { key: 'main', className: 'bt-workspace' }, [
|
|
842
|
+
h('div', { key: 'head', className: 'bt-page-head' }, [h('div', { key: 'title', className: 'bt-page-title-row' }, [h('h1', { key: 'h', className: 'bt-page-title' }, room?.name ?? '团队收件箱'), h(Button, { key: 'send', tone: 'primary', icon: 'plus', disabled: !online || !rooms.length, onClick: () => { if (!draft.room && roomCode) setDraft(previous => ({ ...previous, room: roomCode })); setModal({ kind: 'send' }) } }, '发起接力')]), h('p', { key: 'subtitle', className: 'bt-page-subtitle' }, '收到的任务,始终由你确认后启动。'), h('nav', { key: 'tabs', className: 'bt-tabs', 'aria-label': '任务方向' }, [h('button', { key: 'in', type: 'button', className: 'bt-tab' + (tab === 'incoming' ? ' active' : ''), onClick: () => switchTab('incoming') }, ['收件箱', h('span', { key: 'count', className: 'bt-count' }, incoming.length)]), h('button', { key: 'out', type: 'button', className: 'bt-tab' + (tab === 'outgoing' ? ' active' : ''), onClick: () => switchTab('outgoing') }, ['已发出的', h('span', { key: 'count', className: 'bt-count' }, outgoing.length + (data?.outbox?.length ?? 0))]), h('button', { key: 'archived', type: 'button', className: 'bt-tab' + (tab === 'archived' ? ' active' : ''), onClick: () => switchTab('archived') }, ['已删除', h('span', { key: 'count', className: 'bt-count' }, archived.length)])])]),
|
|
843
|
+
notice ? h('div', { key: 'notice', className: 'bt-message ' + notice.tone, role: notice.tone === 'error' ? 'alert' : 'status' }, [h(Icon, { key: 'i', name: notice.tone === 'success' ? 'check' : 'warning' }), h('span', { key: 't' }, notice.message), h(IconButton, { key: 'x', label: '关闭提示', icon: 'close', onClick: () => setNotice(null) })]) : null,
|
|
844
|
+
!online && data ? h('div', { key: 'offline', className: 'bt-message' }, [h(Icon, { key: 'i', name: 'warning' }), h('span', { key: 't' }, '暂时无法连接接力服务。已显示的任务会保留,恢复连接后即可操作。')]) : null,
|
|
845
|
+
h('div', { key: 'inbox', className: 'bt-inbox' + (selected ? ' has-selection' : '') }, [
|
|
846
|
+
h('section', { key: 'list', className: 'bt-task-column', 'aria-label': tab === 'incoming' ? '收到的任务' : tab === 'outgoing' ? '发出的任务' : '已删除的任务' }, [
|
|
847
|
+
h('div', { key: 'tools', className: 'bt-list-tools' }, [h('label', { key: 'search', className: 'bt-search' }, [h(Icon, { key: 'i', name: 'search' }), h('input', { key: 'input', value: query, placeholder: '搜索任务或成员', 'aria-label': '搜索任务或成员', onChange: event => setQuery(event.target.value) })]), h('button', { key: 'filter', type: 'button', className: 'bt-filter', 'aria-pressed': onlyAttention, onClick: () => setOnlyAttention(value => !value) }, onlyAttention ? '只看待处理' : '全部状态')]),
|
|
848
|
+
h('div', { key: 'items', className: 'bt-task-list' }, [
|
|
849
|
+
...(tab === 'outgoing' ? data?.outbox ?? [] : []).filter(item => !roomCode || item.room === roomCode).map(item => h('div', { key: item.taskId, className: 'bt-pending-send' }, [h('strong', { key: 't' }, item.title), h('span', { key: 'd' }, '投递结果待确认,原任务内容已保留。'), h(Button, { key: 'retry', disabled: !!busy || !online, busy: busy === 'retry-send', onClick: () => retrySendTask(item) }, '确认投递')])),
|
|
850
|
+
...rows.map(renderTask),
|
|
851
|
+
!rows.length && !(tab === 'outgoing' && data?.outbox?.length) ? h(Empty, { key: 'empty', title: query ? '没有找到匹配的任务' : tab === 'incoming' ? '收件箱已清空' : tab === 'archived' ? '没有已删除的任务' : '还没有发出的接力', description: query ? '试试其他标题或成员名称。' : tab === 'archived' ? '首次删除的任务可在这里恢复或永久删除。' : tab === 'incoming' ? '队友发来的接力会出现在这里。' : '点击“发起接力”,把任务交给队友。', icon: tab === 'incoming' ? 'inbox' : 'send' }) : null,
|
|
852
|
+
]),
|
|
853
|
+
]),
|
|
854
|
+
h('section', { key: 'detail', className: 'bt-detail', 'aria-label': '任务详情' }, renderDetail()),
|
|
855
|
+
]),
|
|
856
|
+
]),
|
|
857
|
+
]),
|
|
858
|
+
h(React.Fragment, { key: 'modal' }, renderModal()),
|
|
859
|
+
])
|
|
860
|
+
if (embedded) return h('div', { className: 'bt-panel-page' }, shell)
|
|
861
|
+
return h('div', { className: 'bt-backdrop', onMouseDown: event => { if (event.target === event.currentTarget && !modal) setOpen(false) } }, shell)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
inject: ['slots'],
|
|
866
|
+
apply(ctx) {
|
|
867
|
+
ctx.effect(() => {
|
|
868
|
+
const style = document.createElement('style')
|
|
869
|
+
style.textContent = CSS
|
|
870
|
+
document.head.appendChild(style)
|
|
871
|
+
return () => style.remove()
|
|
872
|
+
}, 'baton: panel styles')
|
|
873
|
+
openSession = sessionId => {
|
|
874
|
+
const uiWorkspace = ctx.get?.('uiWorkspace')
|
|
875
|
+
if (!uiWorkspace?.openSession) throw new Error('会话导航服务尚未就绪。')
|
|
876
|
+
return uiWorkspace.openSession(sessionId)
|
|
877
|
+
}
|
|
878
|
+
pickDirectory = () => {
|
|
879
|
+
const uiWorkspace = ctx.get?.('uiWorkspace')
|
|
880
|
+
if (!uiWorkspace?.pickDirectory) throw new Error('目录选择器尚未就绪,也可以手动填写绝对路径。')
|
|
881
|
+
return uiWorkspace.pickDirectory()
|
|
882
|
+
}
|
|
883
|
+
let paneDispose
|
|
884
|
+
const mountPane = () => {
|
|
885
|
+
paneDispose?.()
|
|
886
|
+
paneDispose = ctx.slots.register({
|
|
887
|
+
name: 'sidebar.panellist', id: 'baton', order: 15,
|
|
888
|
+
label: () => attentionCount > 0 ? `接力 ${attentionCount}` : '接力',
|
|
889
|
+
}, SidebarBatonIcon)
|
|
890
|
+
}
|
|
891
|
+
ctx.slots.inject('sidebar.panellist', () => {
|
|
892
|
+
mountPane()
|
|
893
|
+
return () => { paneDispose?.(); paneDispose = undefined }
|
|
894
|
+
})
|
|
895
|
+
const unsubscribeCount = subscribeCount(() => { if (paneDispose) mountPane() })
|
|
896
|
+
ctx.effect(() => () => { unsubscribeCount(); paneDispose?.() }, 'baton: sidebar panel label')
|
|
897
|
+
ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'baton' }, MainPanel))
|
|
898
|
+
},
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
})
|