dsh-contextos-devops 0.0.0-stage → 1.48.31

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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -2
  3. package/assets/avatar.jpg +0 -0
  4. package/assets/client.css +3316 -0
  5. package/assets/icon-180.png +0 -0
  6. package/build/client.mjs +64 -0
  7. package/client.js +7622 -0
  8. package/cordis.patch.yml +3 -0
  9. package/icon.svg +15 -0
  10. package/index.js +345 -0
  11. package/lib/base/env.js +22 -0
  12. package/lib/base/field.js +25 -0
  13. package/lib/base/http.js +140 -0
  14. package/lib/base/platform.js +33 -0
  15. package/lib/base/registry.js +186 -0
  16. package/lib/base/state.js +66 -0
  17. package/lib/base/table.js +47 -0
  18. package/lib/base/texttable.js +26 -0
  19. package/lib/base/url.js +178 -0
  20. package/lib/core/README.md +17 -0
  21. package/lib/core/apps.js +675 -0
  22. package/lib/core/auth.js +110 -0
  23. package/lib/core/avatar.js +289 -0
  24. package/lib/core/backup.js +105 -0
  25. package/lib/core/codegen.js +401 -0
  26. package/lib/core/copilot.js +419 -0
  27. package/lib/core/data.js +157 -0
  28. package/lib/core/device.js +148 -0
  29. package/lib/core/files.js +152 -0
  30. package/lib/core/gobuild.js +63 -0
  31. package/lib/core/iam.js +122 -0
  32. package/lib/core/identity.js +22 -0
  33. package/lib/core/nodes.js +627 -0
  34. package/lib/core/ops.js +306 -0
  35. package/lib/core/project.js +393 -0
  36. package/lib/core/spaces.js +406 -0
  37. package/lib/core/terminal.js +137 -0
  38. package/lib/core/terminal2.js +141 -0
  39. package/lib/core/users.js +51 -0
  40. package/lib/core/vocab.js +158 -0
  41. package/lib/misc/async.js +15 -0
  42. package/lib/misc/shell.js +27 -0
  43. package/lib/misc/static.js +33 -0
  44. package/locale/en.json +10 -0
  45. package/locale/zh.json +10 -0
  46. package/package.json +91 -4
  47. package/src/client/README.md +37 -0
  48. package/src/client/parts/00-prelude.js +315 -0
  49. package/src/client/parts/05-bootstrap.js +61 -0
  50. package/src/client/parts/08-store.js +85 -0
  51. package/src/client/parts/10-ui.js +188 -0
  52. package/src/client/parts/12-debug.js +361 -0
  53. package/src/client/parts/15-layout.js +86 -0
  54. package/src/client/parts/20-devworkspace.js +789 -0
  55. package/src/client/parts/25-editor-ui.js +28 -0
  56. package/src/client/parts/30-modals.js +85 -0
  57. package/src/client/parts/32-cards.js +176 -0
  58. package/src/client/parts/34-forms.js +176 -0
  59. package/src/client/parts/36-data.js +267 -0
  60. package/src/client/parts/40-project.js +430 -0
  61. package/src/client/parts/45-map.js +109 -0
  62. package/src/client/parts/46-pm.js +158 -0
  63. package/src/client/parts/47-three.js +324 -0
  64. package/src/client/parts/49-msg.js +167 -0
  65. package/src/client/parts/50-chat.js +1422 -0
  66. package/src/client/parts/51-pmselection.js +31 -0
  67. package/src/client/parts/52-pmdata.js +29 -0
  68. package/src/client/parts/55-pm.js +146 -0
  69. package/src/client/parts/58-config.js +381 -0
  70. package/src/client/parts/59-monitor.js +262 -0
  71. package/src/client/parts/60-page.js +1518 -0
  72. package/src/client/styles/kit.css +20 -0
  73. package/src/client/styles/main.css +3294 -0
  74. package/src/client/styles/tokens.css +2 -0
  75. package/vendor/leaflet/leaflet.css +661 -0
  76. package/vendor/leaflet/leaflet.js +6 -0
  77. package/vendor/three/three.module.js +53044 -0
  78. package/vendor/xterm/addon-fit.js +2 -0
  79. package/vendor/xterm/xterm.css +218 -0
  80. package/vendor/xterm/xterm.js +2 -0
@@ -0,0 +1,430 @@
1
+ function InitOverlay({ state, onStart, onRetry, onLoginRemote, onClose }) {
2
+ if (!state) return null;
3
+ const phase = state.phase || (state.running ? 'running' : state.error ? 'error' : state.done ? 'done' : 'setup');
4
+ const [step, setStep] = useState(0);
5
+ const [mode, setMode] = useState('local');
6
+ const [dir, setDir] = useState(state.params?.dir || INIT_DEFAULTS.dir);
7
+ const [port, setPort] = useState(String(state.params?.port || INIT_DEFAULTS.port));
8
+ const [repo, setRepo] = useState(state.params?.repo ?? INIT_DEFAULTS.repo);
9
+ const [image, setImage] = useState('shylinux/contextos:latest');
10
+ const [host, setHost] = useState('');
11
+ const [sshUser, setSshUser] = useState('shy');
12
+ const [sshPort, setSshPort] = useState('22');
13
+ const [remoteUrl, setRemoteUrl] = useState('http://127.0.0.1:9401');
14
+ const [remoteUser, setRemoteUser] = useState('shy');
15
+ const [remotePass, setRemotePass] = useState('');
16
+ const [login, setLogin] = useState(null);
17
+ const [loginBusy, setLoginBusy] = useState(false);
18
+ const [avatar, setAvatar] = useState(() => ({ name: T('我的数字分身'), company: T('云轩领航'), city: T('深圳市'), image: '' }));
19
+ const [scene, setScene] = useState(() => ({ name: T('我的赛博世界'), image: '' }));
20
+ const [project, setProject] = useState(() => ({ name: T('我的第一个项目'), goal: T('把想法变成可交付的软件。'), plan: T('先梳理需求,再搭建原型,最后验收演示。') }));
21
+ const [probeBusy, setProbeBusy] = useState(false);
22
+ const probedRef = useRef(false);
23
+ const [spaceKeys, setSpaceKeys] = useState(['community', 'enterprise']);
24
+ const [logExpanded, setLogExpanded] = useState(false);
25
+ const probePort = async () => {
26
+ setProbeBusy(true);
27
+ try {
28
+ const r = await api('freePort', { start: Number(port) || 9188 });
29
+ setPort(String(r.port));
30
+ if (!dir || /^~\/ContextOS-\d+$/.test(dir)) setDir(r.dir || `~/ContextOS-${r.port}`);
31
+ } finally { setProbeBusy(false); }
32
+ };
33
+ const avatarRef = useRef(null);
34
+ const sceneRef = useRef(null);
35
+ const icon = (s) => s === 'done' ? '✓' : s === 'run' ? '◍' : s === 'err' ? '✗' : '○';
36
+ const total = (state.steps || []).length;
37
+ const doneCount = (state.steps || []).filter((x) => x.status === 'done').length;
38
+ const pct = total ? Math.round((doneCount / total) * 100) : 0;
39
+ const titles = { setup: `${T('初始化')} / INITIALIZE`, running: `${T('正在初始化')} / INITIALIZING`, done: `${T('初始化完成')} / INIT DONE`, error: `${T('初始化失败')} / INIT FAILED` };
40
+ const readPic = (file, cb) => {
41
+ if (!file) return;
42
+ const reader = new FileReader();
43
+ reader.onload = () => {
44
+ const img = new Image();
45
+ img.onload = () => {
46
+ const max = 720;
47
+ const scale = Math.min(1, max / Math.max(img.width, img.height));
48
+ const cv = document.createElement('canvas');
49
+ cv.width = Math.round(img.width * scale); cv.height = Math.round(img.height * scale);
50
+ cv.getContext('2d').drawImage(img, 0, 0, cv.width, cv.height);
51
+ cb(cv.toDataURL('image/jpeg', 0.82));
52
+ };
53
+ img.src = String(reader.result);
54
+ };
55
+ reader.readAsDataURL(file);
56
+ };
57
+ const start = () => onStart({ mode, dir, port: Number(port) || 9188, repo, image, host, sshUser, sshPort: Number(sshPort) || 22, remoteUrl, remoteUser, remotePass, avatar, scene, project, spaces: spaceKeys });
58
+ const doLogin = async () => {
59
+ setLoginBusy(true); setLogin(null);
60
+ try { const r = await onLoginRemote({ url: remoteUrl, user: remoteUser, pass: remotePass }); setLogin(r); }
61
+ catch (e) { setLogin({ error: String(e?.message || e) }); }
62
+ finally { setLoginBusy(false); }
63
+ };
64
+ const field = (label, node) => h('label', { className: 'cx-field full' }, label, node);
65
+ const input = (value, set, props = {}) => h('input', { className: 'cx-input', value, onChange: (e) => set(e.target.value), ...props });
66
+ useEffect(() => { if (probedRef.current) return; probedRef.current = true; probePort(); }, []);
67
+ const stepHead = h('div', { className: 'cx-init-wiz' }, [['01', '部署', 'DEPLOY'], ['02', '分身与场景', 'DIGITAL AVATAR & CYBER WORLD'], ['03', '项目', 'PROJECT']].map(([n, t, en], i) =>
68
+ h('button', { key: n, type: 'button', className: `cx-init-tab${step === i ? ' on' : ''}${step > i ? ' done' : ''}`, onClick: () => phase === 'setup' && setStep(i) },
69
+ h('b', null, n), h('span', null, T(t)), h('i', null, en))));
70
+ const modeChip = ([id, label, desc]) => h('button', { key: id, type: 'button', className: `cx-init-mode${mode === id ? ' on' : ''}`, onClick: () => setMode(id) },
71
+ h('b', null, T(label)), h('span', null, T(desc)));
72
+ const modes = h('div', { className: 'cx-init-modes' },
73
+ modeChip(['local', '本机部署', '本机直接部署节点']),
74
+ modeChip(['docker', '容器部署', 'docker run 一个节点']),
75
+ modeChip(['remoteDeploy', '远程部署', 'ssh 到远端建节点']),
76
+ modeChip(['remote', '远程连接', '登录远端换 sessid']));
77
+ const modeForm = mode === 'local' ? h('div', { className: 'cx-form' },
78
+ field(T('源码地址'), input(repo, setRepo, { placeholder: 'https://shylinux.com/x/ContextOS' })),
79
+ field(T('部署目录'), input(dir, setDir, { placeholder: '~/ContextOS-9188' })),
80
+ h('label', { className: 'cx-field full' }, T('端口'), h('div', { className: 'cx-port-row' }, input(port, setPort, { inputMode: 'numeric' }), h('button', { className: 'cx-btn', type: 'button', disabled: probeBusy, onClick: probePort }, probeBusy ? T('探测中…') : T('探测端口')))),
81
+ h('div', { className: 'cx-field full' }, T('标准空间'), h(StandardSpaces, { value: spaceKeys, onChange: setSpaceKeys })))
82
+ : mode === 'docker' ? h('div', { className: 'cx-form' },
83
+ field(T('镜像'), input(image, setImage, { placeholder: 'shylinux/contextos:latest' })),
84
+ field(T('宿主机目录(可空)'), input(dir, setDir, { placeholder: '~/contextos-data' })),
85
+ h('label', { className: 'cx-field full' }, T('端口'), h('div', { className: 'cx-port-row' }, input(port, setPort, { inputMode: 'numeric' }), h('button', { className: 'cx-btn', type: 'button', disabled: probeBusy, onClick: probePort }, probeBusy ? T('探测中…') : T('探测端口')))))
86
+ : mode === 'remoteDeploy' ? h('div', { className: 'cx-form' },
87
+ field(T('远程主机'), input(host, setHost, { placeholder: '192.168.1.10' })),
88
+ field(T('SSH 用户'), input(sshUser, setSshUser)),
89
+ field(T('SSH 端口'), input(sshPort, setSshPort)),
90
+ h('label', { className: 'cx-field full' }, T('节点端口'), h('div', { className: 'cx-port-row' }, input(port, setPort, { inputMode: 'numeric' }), h('button', { className: 'cx-btn', type: 'button', disabled: probeBusy, onClick: probePort }, probeBusy ? T('探测中…') : T('探测端口')))),
91
+ field(T('镜像'), input(image, setImage)))
92
+ : h('div', null,
93
+ h('div', { className: 'cx-form' },
94
+ field(T('远程地址'), input(remoteUrl, setRemoteUrl, { placeholder: 'http://127.0.0.1:9401' })),
95
+ field(T('用户名'), input(remoteUser, setRemoteUser)),
96
+ field(T('密码'), h('input', { className: 'cx-input', type: 'password', value: remotePass, onChange: (e) => setRemotePass(e.target.value) }))),
97
+ h('div', { className: 'cx-init-login' },
98
+ h('button', { className: 'cx-btn', type: 'button', disabled: loginBusy, onClick: doLogin }, loginBusy ? T('登录中…') : T('登录并获取 sessid')),
99
+ login?.sessid ? h('span', { className: 'ok' }, `✓ sessid ${login.sessid.slice(0, 12)}… · ${login.via}`) : null,
100
+ login?.error ? h('span', { className: 'err' }, login.error) : null));
101
+ const pic = (kind) => {
102
+ const ref = kind === 'avatar' ? avatarRef : sceneRef;
103
+ const val = kind === 'avatar' ? avatar.image : scene.image;
104
+ return h('div', { className: 'cx-init-pic', onClick: () => ref.current && ref.current.click(), title: T('点击选择图片') },
105
+ val ? h('img', { src: val, alt: '' }) : h('div', { className: 'ph' }, h('b', null, kind === 'avatar' ? T('上传头像') : T('上传背景')), h('span', null, 'JPG / PNG · 720px')),
106
+ h('input', { type: 'file', accept: 'image/*', ref, style: { display: 'none' }, onChange: (e) => { const f = e.target.files && e.target.files[0]; readPic(f, (data) => kind === 'avatar' ? setAvatar((v) => ({ ...v, image: data })) : setScene((v) => ({ ...v, image: data }))); } }));
107
+ };
108
+ const stepBody = step === 0 ? h('div', null, modes, modeForm)
109
+ : step === 1 ? h('div', { className: 'cx-init-grid2' },
110
+ h('div', { className: 'cx-init-sub' }, h('h3', null, T('数字分身')), pic('avatar'),
111
+ h('div', { className: 'cx-form' }, field(T('名称'), input(avatar.name, (v) => setAvatar((a) => ({ ...a, name: v })))), field(T('公司'), input(avatar.company, (v) => setAvatar((a) => ({ ...a, company: v })))), field(T('城市'), input(avatar.city, (v) => setAvatar((a) => ({ ...a, city: v })))))),
112
+ h('div', { className: 'cx-init-sub' }, h('h3', null, T('赛博世界')), pic('scene'),
113
+ h('div', { className: 'cx-form' }, field(T('赛博世界名称'), input(scene.name, (v) => setScene((a) => ({ ...a, name: v })))))))
114
+ : h('div', { className: 'cx-form' },
115
+ field(T('项目名称'), input(project.name, (v) => setProject((x) => ({ ...x, name: v })))),
116
+ h('label', { className: 'cx-field full' }, T('目标描述'), h('textarea', { className: 'cx-input', rows: 3, value: project.goal, onChange: (e) => setProject((x) => ({ ...x, goal: e.target.value })) })),
117
+ h('label', { className: 'cx-field full' }, T('计划描述'), h('textarea', { className: 'cx-input', rows: 3, value: project.plan, onChange: (e) => setProject((x) => ({ ...x, plan: e.target.value })) })),
118
+ h('div', { className: 'cx-init-sum' }, `${T('将生成')}: 1 ${T('目标')} · 1 ${T('计划')}`));
119
+ const runView = h('div', null,
120
+ total ? h('div', { className: 'cx-init-progress' }, h('div', { className: 'track' }, h('div', { className: 'bar', style: { width: `${pct}%` } })), h('span', null, `${doneCount}/${total} · ${pct}%`)) : null,
121
+ (state.steps || []).length ? h('div', null, state.steps.map((s, i) => h('div', { key: s.id, className: `cx-init-step ${s.status || ''}` },
122
+ h('span', { className: 'st' }, icon(s.status)),
123
+ h('div', { style: { flex: 1, minWidth: 0 } }, h('span', { className: 'idx' }, String(i + 1).padStart(2, '0')), T(s.label), s.detail ? h('div', { className: 'sd' }, s.detail) : null)))) : null,
124
+ state.error ? h(ErrorBox, { text: state.error }) : null,
125
+ (state.logs || []).length ? h('div', { className: 'cx-init-debug-h' }, T('执行过程'), h('button', { className: 'cx-btn xs', onClick: () => setLogExpanded((v) => !v) }, T(logExpanded ? '收起日志' : '展开日志'))) : null,
126
+ (state.logs || []).length ? h('div', { className: `cx-init-debug${logExpanded ? ' expanded' : ''}` }, h('table', { className: 'cx-init-log' },
127
+ h('thead', null, h('tr', null, ['时间', '耗时', '操作', '影响 / 接口', '状态'].map((x) => h('th', { key: x }, T(x))))),
128
+ h('tbody', null, state.logs.map((l, i) => h('tr', { key: l.id || i, className: l.status || '' },
129
+ h('td', null, String(l.t || '')),
130
+ h('td', null, l.ms ? `${l.ms} ms` : '—'),
131
+ h('td', null, String(l.op || '')),
132
+ h('td', { className: 'impact', title: String(l.impact || '') }, String(l.impact || '')),
133
+ h('td', null, T(l.status === 'error' ? '失败' : l.status === 'done' || l.status === 'ok' ? '完成' : '执行中'))))))) : null);
134
+ const footer = h('div', { className: 'cx-foot' },
135
+ h('button', { className: 'cx-btn', onClick: onClose }, phase === 'running' ? T('隐藏') : phase === 'done' ? T('稍后') : T('关闭')),
136
+ phase === 'setup' ? h('div', { style: { display: 'flex', gap: 8 } },
137
+ step > 0 ? h('button', { className: 'cx-btn', onClick: () => setStep((v) => v - 1) }, T('上一步')) : null,
138
+ step < 2 ? h('button', { className: 'cx-btn pri', onClick: () => setStep((v) => v + 1) }, T('下一步'))
139
+ : h('button', { className: 'cx-btn pri', onClick: start }, T('初始化')))
140
+ : phase === 'done' ? h('button', { className: 'cx-btn pri', onClick: onClose }, T('进入')) : phase === 'error' ? h('button', { className: 'cx-btn pri', onClick: onRetry }, T('重试当前步骤')) : null);
141
+ return h('div', { className: `cx-init ${phase}` },
142
+ h('div', { className: 'cx-init-grid' }),
143
+ h('div', { className: 'cx-init-ring' }),
144
+ h('div', { className: 'cx-init-scan' }),
145
+ h('div', { className: 'cx-world cx-init-world' },
146
+ h('div', { className: 'cx-world-kicker' }, 'WELCOME TO'),
147
+ h('div', { className: 'cx-world-name' }, 'CONTEXTOS WORLD!'),
148
+ h('div', { className: 'cx-world-sub' }, T('数字分身 · 软件工厂 · 赛博世界')),
149
+ h('div', { className: 'cx-world-line' })),
150
+ h('div', { className: 'cx-init-card' },
151
+ h('div', { className: 'cx-init-head' },
152
+ h('div', { className: 'cx-init-badge' }, 'CTX'),
153
+ h('div', { style: { minWidth: 0 } },
154
+ h('h2', null, titles[phase] || titles.setup),
155
+ h('div', { className: 'desc' }, phase === 'setup' ? T('三步进入你的赛博世界:部署 → 数字分身与赛博世界 → 项目。') : T('部署节点 → 部署空间 → 数字分身 → 赛博世界 → 项目管理 → 进入对话')))),
156
+ phase === 'setup' ? h('div', null, stepHead, stepBody, footer) : h('div', null, footer, runView)));
157
+ }
158
+
159
+ // ---- 农历 / 节气(1900-2100,移植自 volcanos/lib/date.js)----
160
+ const CX_LUNAR_INFO = [ // 农历1900-2100的润大小信息表
161
+ 0x04bd8,0x04ae0,0x0a570,0x054d5,0x0d260,0x0d950,0x16554,0x056a0,0x09ad0,0x055d2,//1900-1909
162
+ 0x04ae0,0x0a5b6,0x0a4d0,0x0d250,0x1d255,0x0b540,0x0d6a0,0x0ada2,0x095b0,0x14977,//1910-1919
163
+ 0x04970,0x0a4b0,0x0b4b5,0x06a50,0x06d40,0x1ab54,0x02b60,0x09570,0x052f2,0x04970,//1920-1929
164
+ 0x06566,0x0d4a0,0x0ea50,0x16a95,0x05ad0,0x02b60,0x186e3,0x092e0,0x1c8d7,0x0c950,//1930-1939
165
+ 0x0d4a0,0x1d8a6,0x0b550,0x056a0,0x1a5b4,0x025d0,0x092d0,0x0d2b2,0x0a950,0x0b557,//1940-1949
166
+ 0x06ca0,0x0b550,0x15355,0x04da0,0x0a5b0,0x14573,0x052b0,0x0a9a8,0x0e950,0x06aa0,//1950-1959
167
+ 0x0aea6,0x0ab50,0x04b60,0x0aae4,0x0a570,0x05260,0x0f263,0x0d950,0x05b57,0x056a0,//1960-1969
168
+ 0x096d0,0x04dd5,0x04ad0,0x0a4d0,0x0d4d4,0x0d250,0x0d558,0x0b540,0x0b6a0,0x195a6,//1970-1979
169
+ 0x095b0,0x049b0,0x0a974,0x0a4b0,0x0b27a,0x06a50,0x06d40,0x0af46,0x0ab60,0x09570,//1980-1989
170
+ 0x04af5,0x04970,0x064b0,0x074a3,0x0ea50,0x06b58,0x05ac0,0x0ab60,0x096d5,0x092e0,//1990-1999
171
+ 0x0c960,0x0d954,0x0d4a0,0x0da50,0x07552,0x056a0,0x0abb7,0x025d0,0x092d0,0x0cab5,//2000-2009
172
+ 0x0a950,0x0b4a0,0x0baa4,0x0ad50,0x055d9,0x04ba0,0x0a5b0,0x15176,0x052b0,0x0a930,//2010-2019
173
+ 0x07954,0x06aa0,0x0ad50,0x05b52,0x04b60,0x0a6e6,0x0a4e0,0x0d260,0x0ea65,0x0d530,//2020-2029
174
+ 0x05aa0,0x076a3,0x096d0,0x04afb,0x04ad0,0x0a4d0,0x1d0b6,0x0d250,0x0d520,0x0dd45,//2030-2039
175
+ 0x0b5a0,0x056d0,0x055b2,0x049b0,0x0a577,0x0a4b0,0x0aa50,0x1b255,0x06d20,0x0ada0,//2040-2049
176
+ 0x14b63,0x09370,0x049f8,0x04970,0x064b0,0x168a6,0x0ea50,0x06b20,0x1a6c4,0x0aae0,//2050-2059
177
+ 0x092e0,0x0d2e3,0x0c960,0x0d557,0x0d4a0,0x0da50,0x05d55,0x056a0,0x0a6d0,0x055d4,//2060-2069
178
+ 0x052d0,0x0a9b8,0x0a950,0x0b4a0,0x0b6a6,0x0ad50,0x055a0,0x0aba4,0x0a5b0,0x052b0,//2070-2079
179
+ 0x0b273,0x06930,0x07337,0x06aa0,0x0ad50,0x14b55,0x04b60,0x0a570,0x054e4,0x0d160,//2080-2089
180
+ 0x0e968,0x0d520,0x0daa0,0x16aa6,0x056d0,0x04ae0,0x0a9d4,0x0a2d0,0x0d150,0x0f252,//2090-2099
181
+ 0x0d520, //2100
182
+ ];
183
+ const CX_TERM_INFO = [ // 1900-2100各年的24节气日期速查表
184
+ '9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e','97bcf97c3598082c95f8c965cc920f',
185
+ '97bd0b06bdb0722c965ce1cfcc920f','b027097bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e',
186
+ '97bcf97c359801ec95f8c965cc920f','97bd0b06bdb0722c965ce1cfcc920f','b027097bd097c36b0b6fc9274c91aa',
187
+ '97b6b97bd19801ec9210c965cc920e','97bcf97c359801ec95f8c965cc920f','97bd0b06bdb0722c965ce1cfcc920f',
188
+ 'b027097bd097c36b0b6fc9274c91aa','9778397bd19801ec9210c965cc920e','97b6b97bd19801ec95f8c965cc920f',
189
+ '97bd09801d98082c95f8e1cfcc920f','97bd097bd097c36b0b6fc9210c8dc2','9778397bd197c36c9210c9274c91aa',
190
+ '97b6b97bd19801ec95f8c965cc920e','97bd09801d98082c95f8e1cfcc920f','97bd097bd097c36b0b6fc9210c8dc2',
191
+ '9778397bd097c36c9210c9274c91aa','97b6b97bd19801ec95f8c965cc920e','97bcf97c3598082c95f8e1cfcc920f',
192
+ '97bd097bd097c36b0b6fc9210c8dc2','9778397bd097c36c9210c9274c91aa','97b6b97bd19801ec9210c965cc920e',
193
+ '97bcf97c3598082c95f8c965cc920f','97bd097bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa',
194
+ '97b6b97bd19801ec9210c965cc920e','97bcf97c3598082c95f8c965cc920f','97bd097bd097c35b0b6fc920fb0722',
195
+ '9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e','97bcf97c359801ec95f8c965cc920f',
196
+ '97bd097bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e',
197
+ '97bcf97c359801ec95f8c965cc920f','97bd097bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa',
198
+ '97b6b97bd19801ec9210c965cc920e','97bcf97c359801ec95f8c965cc920f','97bd097bd07f595b0b6fc920fb0722',
199
+ '9778397bd097c36b0b6fc9210c8dc2','9778397bd19801ec9210c9274c920e','97b6b97bd19801ec95f8c965cc920f',
200
+ '97bd07f5307f595b0b0bc920fb0722','7f0e397bd097c36b0b6fc9210c8dc2','9778397bd097c36c9210c9274c920e',
201
+ '97b6b97bd19801ec95f8c965cc920f','97bd07f5307f595b0b0bc920fb0722','7f0e397bd097c36b0b6fc9210c8dc2',
202
+ '9778397bd097c36c9210c9274c91aa','97b6b97bd19801ec9210c965cc920e','97bd07f1487f595b0b0bc920fb0722',
203
+ '7f0e397bd097c36b0b6fc9210c8dc2','9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e',
204
+ '97bcf7f1487f595b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa',
205
+ '97b6b97bd19801ec9210c965cc920e','97bcf7f1487f595b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722',
206
+ '9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e','97bcf7f1487f531b0b0bb0b6fb0722',
207
+ '7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e',
208
+ '97bcf7f1487f531b0b0bb0b6fb0722','7f0e397bd07f595b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa',
209
+ '97b6b97bd19801ec9210c9274c920e','97bcf7f0e47f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722',
210
+ '9778397bd097c36b0b6fc9210c91aa','97b6b97bd197c36c9210c9274c920e','97bcf7f0e47f531b0b0bb0b6fb0722',
211
+ '7f0e397bd07f595b0b0bc920fb0722','9778397bd097c36b0b6fc9210c8dc2','9778397bd097c36c9210c9274c920e',
212
+ '97b6b7f0e47f531b0723b0b6fb0722','7f0e37f5307f595b0b0bc920fb0722','7f0e397bd097c36b0b6fc9210c8dc2',
213
+ '9778397bd097c36b0b70c9274c91aa','97b6b7f0e47f531b0723b0b6fb0721','7f0e37f1487f595b0b0bb0b6fb0722',
214
+ '7f0e397bd097c35b0b6fc9210c8dc2','9778397bd097c36b0b6fc9274c91aa','97b6b7f0e47f531b0723b0b6fb0721',
215
+ '7f0e27f1487f595b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa',
216
+ '97b6b7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722',
217
+ '9778397bd097c36b0b6fc9274c91aa','97b6b7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722',
218
+ '7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa','97b6b7f0e47f531b0723b0b6fb0721',
219
+ '7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722','9778397bd097c36b0b6fc9274c91aa',
220
+ '97b6b7f0e47f531b0723b0787b0721','7f0e27f0e47f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722',
221
+ '9778397bd097c36b0b6fc9210c91aa','97b6b7f0e47f149b0723b0787b0721','7f0e27f0e47f531b0723b0b6fb0722',
222
+ '7f0e397bd07f595b0b0bc920fb0722','9778397bd097c36b0b6fc9210c8dc2','977837f0e37f149b0723b0787b0721',
223
+ '7f07e7f0e47f531b0723b0b6fb0722','7f0e37f5307f595b0b0bc920fb0722','7f0e397bd097c35b0b6fc9210c8dc2',
224
+ '977837f0e37f14998082b0787b0721','7f07e7f0e47f531b0723b0b6fb0721','7f0e37f1487f595b0b0bb0b6fb0722',
225
+ '7f0e397bd097c35b0b6fc9210c8dc2','977837f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721',
226
+ '7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722','977837f0e37f14998082b0787b06bd',
227
+ '7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722',
228
+ '977837f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722',
229
+ '7f0e397bd07f595b0b0bc920fb0722','977837f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721',
230
+ '7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722','977837f0e37f14998082b0787b06bd',
231
+ '7f07e7f0e47f149b0723b0787b0721','7f0e27f0e47f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722',
232
+ '977837f0e37f14998082b0723b06bd','7f07e7f0e37f149b0723b0787b0721','7f0e27f0e47f531b0723b0b6fb0722',
233
+ '7f0e397bd07f595b0b0bc920fb0722','977837f0e37f14898082b0723b02d5','7ec967f0e37f14998082b0787b0721',
234
+ '7f07e7f0e47f531b0723b0b6fb0722','7f0e37f1487f595b0b0bb0b6fb0722','7f0e37f0e37f14898082b0723b02d5',
235
+ '7ec967f0e37f14998082b0787b0721','7f07e7f0e47f531b0723b0b6fb0722','7f0e37f1487f531b0b0bb0b6fb0722',
236
+ '7f0e37f0e37f14898082b0723b02d5','7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721',
237
+ '7f0e37f1487f531b0b0bb0b6fb0722','7f0e37f0e37f14898082b072297c35','7ec967f0e37f14998082b0787b06bd',
238
+ '7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722','7f0e37f0e37f14898082b072297c35',
239
+ '7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722',
240
+ '7f0e37f0e366aa89801eb072297c35','7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f149b0723b0787b0721',
241
+ '7f0e27f1487f531b0b0bb0b6fb0722','7f0e37f0e366aa89801eb072297c35','7ec967f0e37f14998082b0723b06bd',
242
+ '7f07e7f0e47f149b0723b0787b0721','7f0e27f0e47f531b0723b0b6fb0722','7f0e37f0e366aa89801eb072297c35',
243
+ '7ec967f0e37f14998082b0723b06bd','7f07e7f0e37f14998083b0787b0721','7f0e27f0e47f531b0723b0b6fb0722',
244
+ '7f0e37f0e366aa89801eb072297c35','7ec967f0e37f14898082b0723b02d5','7f07e7f0e37f14998082b0787b0721',
245
+ '7f07e7f0e47f531b0723b0b6fb0722','7f0e36665b66aa89801e9808297c35','665f67f0e37f14898082b0723b02d5',
246
+ '7ec967f0e37f14998082b0787b0721','7f07e7f0e47f531b0723b0b6fb0722','7f0e36665b66a449801e9808297c35',
247
+ '665f67f0e37f14898082b0723b02d5','7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721',
248
+ '7f0e36665b66a449801e9808297c35','665f67f0e37f14898082b072297c35','7ec967f0e37f14998082b0787b06bd',
249
+ '7f07e7f0e47f531b0723b0b6fb0721','7f0e26665b66a449801e9808297c35','665f67f0e37f1489801eb072297c35',
250
+ '7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722'];
251
+ const CX_TERM_NAME = ['小寒','大寒','立春','雨水','惊蛰','春分','清明','谷雨','立夏','小满','芒种','夏至','小暑','大暑','立秋','处暑','白露','秋分','寒露','霜降','立冬','小雪','大雪','冬至'];
252
+ const CX_GAN = ['甲','乙','丙','丁','戊','己','庚','辛','壬','癸'];
253
+ const CX_ZHI = ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'];
254
+ const CX_ANIMALS = ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪'];
255
+ const CX_NSTR1 = ['正','二','三','四','五','六','七','八','九','十','冬','腊'];
256
+ const CX_NSTR2 = ['初','十','廿','卅'];
257
+ const CX_NSTR3 = ['日','一','二','三','四','五','六','七','八','九','十'];
258
+ const CX_LUNAR_FEST = { '1-1': '春节', '1-15': '元宵', '5-5': '端午', '8-15': '中秋', '9-9': '重阳', '12-30': '除夕' };
259
+ const CX_LUNAR = {
260
+ lYearDays(y) { let sum = 348; for (let i = 0x8000; i > 0x8; i >>= 1) sum += (CX_LUNAR_INFO[y - 1900] & i) ? 1 : 0; return sum + this.leapDays(y); },
261
+ leapMonth(y) { return CX_LUNAR_INFO[y - 1900] & 0xf; },
262
+ leapDays(y) { return this.leapMonth(y) ? ((CX_LUNAR_INFO[y - 1900] & 0x10000) ? 30 : 29) : 0; },
263
+ monthDays(y, m) { return (CX_LUNAR_INFO[y - 1900] & (0x10000 >> m)) ? 30 : 29; },
264
+ term(y, n) {
265
+ const t = CX_TERM_INFO[y - 1900];
266
+ const parts = [0, 5, 10, 15, 20, 25].map((p) => parseInt('0x' + t.substr(p, 5), 16).toString());
267
+ const days = [];
268
+ for (const s of parts) days.push(parseInt(s.substr(0, 1)), parseInt(s.substr(1, 2)), parseInt(s.substr(3, 1)), parseInt(s.substr(4, 2)));
269
+ return parseInt(days[n - 1]);
270
+ },
271
+ cnMonth(m) { return CX_NSTR1[m - 1] + '月'; },
272
+ cnDay(d) { if (d === 10) return '初十'; if (d === 20) return '二十'; if (d === 30) return '三十'; return CX_NSTR2[Math.floor(d / 10)] + CX_NSTR3[d % 10]; },
273
+ gzYear(y) { const g = ((y - 3) % 10) || 10; const z = ((y - 3) % 12) || 12; return CX_GAN[g - 1] + CX_ZHI[z - 1]; },
274
+ animal(y) { return CX_ANIMALS[(y - 4) % 12]; },
275
+ astro(m, d) { const arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22]; const s = '摩羯水瓶双鱼白羊金牛双子巨蟹狮子处女天秤天蝎射手摩羯'; return s.substr(m * 2 - (d < arr[m - 1] ? 2 : 0), 2) + '座'; },
276
+ convert(y, m, d) {
277
+ let day = (Date.UTC(y, m - 1, d) - Date.UTC(1900, 0, 31)) / 86400000;
278
+ let year = 1900, days = 0;
279
+ for (; year < 2101 && day > 0; year++) { days = this.lYearDays(year); day -= days; }
280
+ if (day < 0) { day += days; year--; }
281
+ const leap = this.leapMonth(year);
282
+ let isLeap = false, month = 1;
283
+ for (; month < 13 && day > 0; month++) {
284
+ if (leap > 0 && month === leap + 1 && !isLeap) { month--; isLeap = true; days = this.leapDays(year); }
285
+ else days = this.monthDays(year, month);
286
+ if (isLeap && month === leap + 1) isLeap = false;
287
+ day -= days;
288
+ }
289
+ if (day === 0 && leap > 0 && month === leap + 1) { if (isLeap) isLeap = false; else { isLeap = true; month--; } }
290
+ if (day < 0) { day += days; month--; }
291
+ day++;
292
+ const t1 = this.term(y, m * 2 - 1), t2 = this.term(y, m * 2);
293
+ const term = d === t1 ? CX_TERM_NAME[m * 2 - 2] : d === t2 ? CX_TERM_NAME[m * 2 - 1] : '';
294
+ const fest = CX_LUNAR_FEST[month + '-' + day] || '';
295
+ return { lYear: year, lMonth: month, lDay: day, isLeap, cnMonth: (isLeap ? '闰' : '') + this.cnMonth(month), cnDay: this.cnDay(day), term, fest, gzYear: this.gzYear(year), animal: this.animal(year), astro: this.astro(m, d) };
296
+ },
297
+ label(d) {
298
+ const r = this.convert(d.getFullYear(), d.getMonth() + 1, d.getDate());
299
+ return r.fest || r.term || (r.lDay === 1 ? r.cnMonth : r.cnDay);
300
+ },
301
+ line(d) {
302
+ const r = this.convert(d.getFullYear(), d.getMonth() + 1, d.getDate());
303
+ return `${r.gzYear} ${r.animal}年 ${r.cnMonth}${r.cnDay} ${r.astro}`;
304
+ },
305
+ };
306
+
307
+ const PmCalendar = React.memo(function PmCalendar({ tasks, start, end, onChange, locale }) {
308
+ const bootCalView = cxRead('cal', {});
309
+ const [view, setView] = useState(bootCalView.v === 2 && bootCalView.view ? bootCalView.view : 'week');
310
+ const [cursor, setCursor] = useState(() => { try { return bootCalView.cursor ? new Date(bootCalView.cursor) : new Date(); } catch { return new Date(); } });
311
+ useEffect(() => cxWrite('cal', { ...cxRead('cal', {}), v: 2, view, cursor: cursor.toISOString() }), [view, cursor]);
312
+ const counts = {};
313
+ for (const t of tasks || []) {
314
+ const d = String(t.created_at || '').slice(0, 10);
315
+ if (d) counts[d] = (counts[d] || 0) + 1;
316
+ }
317
+ const key = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
318
+ const firstKey = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01`;
319
+ const todayKey = key(new Date());
320
+ const lastKey = (d) => key(new Date(d.getFullYear(), d.getMonth() + 1, 0));
321
+ const inRange = (a, b) => start && a <= (end || start) && b >= start;
322
+ const lunar = (d) => {
323
+ if (locale !== 'zh') return '';
324
+ try { return CX_LUNAR.label(d); } catch { return ''; }
325
+ };
326
+ const lunarSpecial = (d) => { try { const r = CX_LUNAR.convert(d.getFullYear(), d.getMonth() + 1, d.getDate()); return !!(r.term || r.fest); } catch { return false; } };
327
+ const pick = (a, b) => {
328
+ if (!start) { onChange(a, b); return; }
329
+ if (start === end) {
330
+ const lo = a < start ? a : start;
331
+ const hi = b > end ? b : end;
332
+ onChange(lo, hi);
333
+ return;
334
+ }
335
+ onChange(a, b);
336
+ };
337
+ const nav = (dir) => {
338
+ if (view === 'long') setCursor(new Date(cursor.getFullYear() + dir * 10, 0, 1));
339
+ else if (view === 'year') setCursor(new Date(cursor.getFullYear() + dir, 0, 1));
340
+ else if (view === 'month') setCursor(new Date(cursor.getFullYear(), cursor.getMonth() + dir, 1));
341
+ else setCursor(new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + dir * 7));
342
+ };
343
+ const label = view === 'long' ? `${cursor.getFullYear() - 5} — ${cursor.getFullYear() + 6}`
344
+ : view === 'year' ? `${cursor.getFullYear()}`
345
+ : view === 'month' ? `${cursor.getFullYear()}.${String(cursor.getMonth() + 1).padStart(2, '0')}`
346
+ : `${key(new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() - cursor.getDay()))} — ${key(new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() - cursor.getDay() + 6))}`;
347
+ let body = null;
348
+ if (view === 'long') {
349
+ const base = cursor.getFullYear() - 5;
350
+ body = h('div', { className: 'cx-cal-years' }, Array.from({ length: 12 }, (_, i) => {
351
+ const y = base + i, a = `${y}-01-01`, b = `${y}-12-31`, on = inRange(a, b);
352
+ return h('button', { key: y, className: `cx-cal-year${on ? ' on' : ''}`, onClick: () => pick(a, b) }, y);
353
+ }));
354
+ } else if (view === 'year') {
355
+ body = h('div', { className: 'cx-cal-months' }, Array.from({ length: 12 }, (_, i) => {
356
+ const d = new Date(cursor.getFullYear(), i, 1), a = firstKey(d), b = lastKey(d), on = inRange(a, b);
357
+ return h('button', { key: i, className: `cx-cal-month${on ? ' on' : ''}`, onClick: () => pick(a, b) }, CURRENT_LANG === 'zh' ? `${i + 1}月` : ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][i], h('span', null, lunar(d)));
358
+ }));
359
+ } else {
360
+ const days = view === 'week' ? 7 : new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0).getDate();
361
+ const base = view === 'week' ? new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() - cursor.getDay()) : new Date(cursor.getFullYear(), cursor.getMonth(), 1);
362
+ const lead = view === 'week' ? 0 : base.getDay();
363
+ body = h(React.Fragment, null,
364
+ h('div', { className: 'cx-cal-w' }, ['日', '一', '二', '三', '四', '五', '六'].map((w) => h('span', { key: w }, w))),
365
+ h('div', { className: 'cx-cal-grid' },
366
+ Array.from({ length: lead }, (_, i) => h('span', { key: `b${i}` })),
367
+ Array.from({ length: days }, (_, i) => {
368
+ const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + i), k = key(d), on = inRange(k, k);
369
+ return h('button', { key: k, className: `cx-cal-day${on ? ' on' : ''}${k === start ? ' start' : ''}${k === (end || start) ? ' end' : ''}`, onClick: () => pick(k, k) },
370
+ counts[k] ? h('span', { className: 'n' }, counts[k]) : h('span', { className: 'n empty' }, ''),
371
+ h('span', { className: `d${k === todayKey ? ' today' : ''}` }, d.getDate()),
372
+ h('span', { className: `l${lunarSpecial(d) ? ' accent' : ''}` }, lunar(d)));
373
+ })));
374
+ }
375
+ return h('div', { className: 'cx-cal' },
376
+ h('div', { className: 'cx-cal-h' },
377
+ h(IconButton, { name: 'arrowLeft', onClick: () => nav(-1) }),
378
+ h('span', null, label),
379
+ h(IconButton, { name: 'arrowRight', onClick: () => nav(1) })),
380
+ h('div', { className: 'cx-cal-views' }, [['long', '长期'], ['year', '年'], ['month', '月'], ['week', '周']].map(([v, n]) => h('button', { key: v, className: `cx-cal-view${view === v ? ' on' : ''}`, onClick: () => setView(v) }, T(n)))),
381
+ body,
382
+ h('div', { className: 'cx-cal-lunar' }, (() => {
383
+ try {
384
+ let d = cursor;
385
+ if (start) { const [yy, mm, dd] = String(start).split('-').map(Number); d = new Date(yy, mm - 1, dd); }
386
+ return CX_LUNAR.line(d);
387
+ } catch { return ''; }
388
+ })()),
389
+ h('div', { className: 'cx-cal-f' },
390
+ h('span', null, start ? `${start} → ${end || start}` : T('全部日期')),
391
+ h('div', { className: 'cx-cal-f-r' },
392
+ h('button', { onClick: () => { const now = new Date(); setCursor(now); onChange(todayKey, todayKey); } }, T('今天')),
393
+ h('button', { onClick: () => onChange('', '') }, T('清除')))));
394
+ });
395
+
396
+ function MultiFilter({ label, options, value, onChange, align = 'left' }) {
397
+ const [open, setOpen] = useState(false);
398
+ const picked = value || [];
399
+ const toggle = (v) => onChange(picked.includes(v) ? picked.filter((x) => x !== v) : [...picked, v]);
400
+ if (!options || options.length <= 1) return null;
401
+ if (options.length <= 2) { /* 选项多时一律用下拉多选,避免挤成一行 */
402
+ return h('div', { className: 'cx-filter inline' },
403
+ h('span', { className: 'cx-filter-label' }, label),
404
+ options.map((o) => h('button', { key: o.value, type: 'button', className: `cx-filter-chip${picked.includes(o.value) ? ' on' : ''}`, onClick: (e) => { e.stopPropagation(); toggle(o.value); } }, o.label || o.value)));
405
+ }
406
+ return h('div', { className: 'cx-filter' },
407
+ h('button', { className: `cx-filter-btn${picked.length ? ' on' : ''}`, onClick: (e) => { e.stopPropagation(); setOpen((x) => !x); } }, `${label}${picked.length ? ` ${picked.length}` : ''}`, h(UiIcon, { name: 'chevronDown', size: 12 })),
408
+ open ? h('div', { className: `cx-filter-menu ${align}` },
409
+ options.length ? options.map((o) => h('button', { key: o.value, className: `cx-filter-item${picked.includes(o.value) ? ' on' : ''}`, onClick: (e) => { e.stopPropagation(); toggle(o.value); } }, h('span', { className: 'cx-filter-text' }, o.label), h('span', { className: 'cx-filter-check' }, picked.includes(o.value) ? h(UiIcon, { name: 'check', size: 12 }) : ''))) : h(EmptyState, { text: '暂无选项', style: { padding: 7 } }),
410
+ h('button', { className: 'cx-filter-clear', onClick: (e) => { e.stopPropagation(); onChange([]); setOpen(false); } }, T('清除'))) : null);
411
+ }
412
+
413
+ function ensureLeaflet() {
414
+ if (window.L) return Promise.resolve(window.L);
415
+ if (!document.querySelector('link[data-cx-leaflet]')) {
416
+ const link = document.createElement('link');
417
+ link.rel = 'stylesheet'; link.href = '/dsh-contextos-devops/leaflet/leaflet.css'; link.dataset.cxLeaflet = '1';
418
+ document.head.appendChild(link);
419
+ }
420
+ if (window.__cxLeafletPromise) return window.__cxLeafletPromise;
421
+ window.__cxLeafletPromise = new Promise((resolve, reject) => {
422
+ const script = document.createElement('script');
423
+ script.src = '/dsh-contextos-devops/leaflet/leaflet.js';
424
+ script.async = true;
425
+ script.onload = () => window.L ? resolve(window.L) : reject(new Error('leaflet not loaded'));
426
+ script.onerror = () => reject(new Error('leaflet load failed'));
427
+ document.head.appendChild(script);
428
+ });
429
+ return window.__cxLeafletPromise;
430
+ }
@@ -0,0 +1,109 @@
1
+ function TaskMap({ tasks, activeTask, onSelect, full, onLocateReady, theme = 'dark' }) {
2
+ const hostRef = useRef(null);
3
+ const mapRef = useRef(null);
4
+ const leafletRef = useRef(null);
5
+ const markersRef = useRef([]);
6
+ const timersRef = useRef([]);
7
+ const [mapReady, setMapReady] = useState(false);
8
+ const boxRef = useRef(null);
9
+ const [loading, setLoading] = useState(true);
10
+ const onSelectRef = useRef(onSelect);
11
+ onSelectRef.current = onSelect;
12
+ const tasksRef = useRef(tasks); tasksRef.current = tasks;
13
+ const activeTaskRef = useRef(activeTask); activeTaskRef.current = activeTask;
14
+ const taskSig = (tasks || []).map((t) => `${t.uid || ''}:${t.status_name || ''}:${t.title || ''}:${(t.reports || []).map((r) => `${r.latitude || ''},${r.longitude || ''}`).join(';')}`).join('|') + `#${activeTask?.uid || ''}`;
15
+ const point = (t) => {
16
+ const report = (t.reports || []).find((r) => Number(r.latitude) && Number(r.longitude));
17
+ if (report) return [Number(report.latitude), Number(report.longitude)];
18
+ let h = 0; for (const ch of String(t.uid || t.title || '')) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
19
+ return [22.48 + (h % 100) / 100 * 0.16, 113.88 + ((h >>> 8) % 100) / 100 * 0.32];
20
+ };
21
+ useEffect(() => {
22
+ let disposed = false;
23
+ ensureLeaflet().then((L) => {
24
+ if (disposed || !hostRef.current || mapRef.current) return;
25
+ if (!document.querySelector('link[data-cx-tencent]')) {
26
+ const pre = document.createElement('link'); pre.rel = 'preconnect'; pre.href = 'https://rt0.map.gtimg.com'; pre.dataset.cxTencent = '1'; document.head.appendChild(pre);
27
+ }
28
+ leafletRef.current = L;
29
+ const bootMap = cxRead('map', null);
30
+ const startView = bootMap && Number.isFinite(bootMap.lat) && Number.isFinite(bootMap.lng) ? [bootMap.lat, bootMap.lng] : [22.5431, 114.0579];
31
+ const startZoom = bootMap && Number.isFinite(bootMap.zoom) ? bootMap.zoom : 11;
32
+ const map = L.map(hostRef.current, { zoomControl: false, attributionControl: true, scrollWheelZoom: true, wheelDebounceTime: 80, wheelPxPerZoomLevel: 120, preferCanvas: true, zoomAnimation: true }).setView(startView, startZoom);
33
+ const saveMapState = () => { try { const c = map.getCenter(); cxWrite('map', { lat: c.lat, lng: c.lng, zoom: map.getZoom() }); } catch {} };
34
+ map.on('moveend', saveMapState);
35
+ map.on('zoomend', saveMapState);
36
+ const tiles = L.tileLayer('https://rt{s}.map.gtimg.com/tile?z={z}&x={x}&y={y}&type=vector&styleid=3&scene=0', {
37
+ maxZoom: 18, minZoom: 8, maxNativeZoom: 16, subdomains: ['0', '1', '2', '3'], tms: true, attribution: '© Tencent Map',
38
+ updateWhenIdle: true, updateWhenZooming: false, keepBuffer: 2, detectRetina: false,
39
+ });
40
+ let settled = false;
41
+ const done = () => { if (!settled) { settled = true; setLoading(false); } };
42
+ tiles.on('tileload', done); /* 首块瓦片到达就撤掉遮罩 */
43
+ tiles.on('load', done);
44
+ timersRef.current.push(setTimeout(done, 1500)); /* 兜底:1.5s 后不再挡着地图 */
45
+ tiles.addTo(map);
46
+ if (navigator.geolocation && !bootMap) {
47
+ navigator.geolocation.getCurrentPosition((pos) => {
48
+ if (!mapRef.current) return;
49
+ map.setView([pos.coords.latitude, pos.coords.longitude], 13);
50
+ L.circleMarker([pos.coords.latitude, pos.coords.longitude], { radius: 7, color: '#ffffff', weight: 2, fillColor: '#3ee7ff', fillOpacity: 1 }).addTo(map).bindPopup('当前位置');
51
+ L.circle([pos.coords.latitude, pos.coords.longitude], { radius: pos.coords.accuracy || 120, color: '#3ee7ff', weight: 1, fillColor: '#3ee7ff', fillOpacity: 0.08 }).addTo(map);
52
+ }, () => { /* default Shenzhen center */ }, { timeout: 5000, maximumAge: 60000 });
53
+ }
54
+ mapRef.current = map;
55
+ setMapReady(true);
56
+ timersRef.current.push(setTimeout(() => {
57
+ if (disposed || !mapRef.current || !hostRef.current?.isConnected) return;
58
+ mapRef.current.invalidateSize(); setLoading(false);
59
+ }, 1200));
60
+ }).catch(() => { /* SVG fallback below stays */ });
61
+ return () => {
62
+ disposed = true;
63
+ for (const t of timersRef.current) clearTimeout(t);
64
+ timersRef.current = [];
65
+ if (mapRef.current) { mapRef.current.remove(); mapRef.current = null; }
66
+ };
67
+ }, []);
68
+ useEffect(() => {
69
+ const L = leafletRef.current, map = mapRef.current;
70
+ if (!L || !map) return;
71
+ for (const m of markersRef.current) map.removeLayer(m);
72
+ markersRef.current = [];
73
+ const color = (s) => s === 'process' ? '#ffd166' : s === 'finished' ? '#42f5a7' : s === 'canceled' ? '#ff5c7a' : '#7b98a8';
74
+ const current = tasksRef.current || [];
75
+ const active = activeTaskRef.current;
76
+ for (const t of current) {
77
+ const [lat, lng] = point(t);
78
+ const marker = L.circleMarker([lat, lng], {
79
+ radius: active?.uid === t.uid ? 7 : 4.5,
80
+ color: active?.uid === t.uid ? '#ffffff' : color(t.status_name),
81
+ weight: active?.uid === t.uid ? 2 : 1,
82
+ fillColor: color(t.status_name), fillOpacity: 0.85,
83
+ }).addTo(map);
84
+ const el = document.createElement('div');
85
+ el.textContent = `${t.title || ''} · ${t.status_name || ''}`;
86
+ marker.bindPopup(el, { closeButton: false, className: 'cx-map-popup' });
87
+ marker.on('click', () => onSelectRef.current?.(t));
88
+ markersRef.current.push(marker);
89
+ }
90
+ }, [taskSig, mapReady]);
91
+ useEffect(() => {
92
+ window.dispatchEvent(new CustomEvent('cx-map-full', { detail: full }));
93
+ if (!mapRef.current) return;
94
+ const t = setTimeout(() => {
95
+ if (mapRef.current && hostRef.current?.isConnected) mapRef.current.invalidateSize();
96
+ }, 120);
97
+ return () => { clearTimeout(t); window.dispatchEvent(new CustomEvent('cx-map-full', { detail: false })); };
98
+ }, [full]);
99
+ const L = leafletRef.current;
100
+ const zoomIn = () => mapRef.current && hostRef.current?.isConnected && mapRef.current.zoomIn();
101
+ const zoomOut = () => mapRef.current && hostRef.current?.isConnected && mapRef.current.zoomOut();
102
+ const locate = () => mapRef.current && hostRef.current?.isConnected && mapRef.current.locate({ setView: true, maxZoom: 14 });
103
+ useEffect(() => { onLocateReady && onLocateReady({ locate, zoomIn, zoomOut }); });
104
+ return h('div', { className: `cx-taskmap cx-map-${theme}`, ref: boxRef },
105
+ h('div', { className: 'cx-taskmap-leaflet', ref: hostRef },
106
+ loading ? h('div', { className: 'cx-taskmap-loading' }, '腾讯地图加载中…') : null),
107
+ h('div', { className: 'cx-taskmap-legend' }, h('span', { className: 'lc created' }, CURRENT_LANG === 'zh' ? '待开始' : 'To do'), h('span', { className: 'lc process' }, CURRENT_LANG === 'zh' ? '进行中' : 'Doing'), h('span', { className: 'lc finished' }, CURRENT_LANG === 'zh' ? '已完成' : 'Done')));
108
+ }
109
+