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.
- package/LICENSE +21 -0
- package/README.md +207 -2
- package/assets/avatar.jpg +0 -0
- package/assets/client.css +3316 -0
- package/assets/icon-180.png +0 -0
- package/build/client.mjs +64 -0
- package/client.js +7622 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +15 -0
- package/index.js +345 -0
- package/lib/base/env.js +22 -0
- package/lib/base/field.js +25 -0
- package/lib/base/http.js +140 -0
- package/lib/base/platform.js +33 -0
- package/lib/base/registry.js +186 -0
- package/lib/base/state.js +66 -0
- package/lib/base/table.js +47 -0
- package/lib/base/texttable.js +26 -0
- package/lib/base/url.js +178 -0
- package/lib/core/README.md +17 -0
- package/lib/core/apps.js +675 -0
- package/lib/core/auth.js +110 -0
- package/lib/core/avatar.js +289 -0
- package/lib/core/backup.js +105 -0
- package/lib/core/codegen.js +401 -0
- package/lib/core/copilot.js +419 -0
- package/lib/core/data.js +157 -0
- package/lib/core/device.js +148 -0
- package/lib/core/files.js +152 -0
- package/lib/core/gobuild.js +63 -0
- package/lib/core/iam.js +122 -0
- package/lib/core/identity.js +22 -0
- package/lib/core/nodes.js +627 -0
- package/lib/core/ops.js +306 -0
- package/lib/core/project.js +393 -0
- package/lib/core/spaces.js +406 -0
- package/lib/core/terminal.js +137 -0
- package/lib/core/terminal2.js +141 -0
- package/lib/core/users.js +51 -0
- package/lib/core/vocab.js +158 -0
- package/lib/misc/async.js +15 -0
- package/lib/misc/shell.js +27 -0
- package/lib/misc/static.js +33 -0
- package/locale/en.json +10 -0
- package/locale/zh.json +10 -0
- package/package.json +91 -4
- package/src/client/README.md +37 -0
- package/src/client/parts/00-prelude.js +315 -0
- package/src/client/parts/05-bootstrap.js +61 -0
- package/src/client/parts/08-store.js +85 -0
- package/src/client/parts/10-ui.js +188 -0
- package/src/client/parts/12-debug.js +361 -0
- package/src/client/parts/15-layout.js +86 -0
- package/src/client/parts/20-devworkspace.js +789 -0
- package/src/client/parts/25-editor-ui.js +28 -0
- package/src/client/parts/30-modals.js +85 -0
- package/src/client/parts/32-cards.js +176 -0
- package/src/client/parts/34-forms.js +176 -0
- package/src/client/parts/36-data.js +267 -0
- package/src/client/parts/40-project.js +430 -0
- package/src/client/parts/45-map.js +109 -0
- package/src/client/parts/46-pm.js +158 -0
- package/src/client/parts/47-three.js +324 -0
- package/src/client/parts/49-msg.js +167 -0
- package/src/client/parts/50-chat.js +1422 -0
- package/src/client/parts/51-pmselection.js +31 -0
- package/src/client/parts/52-pmdata.js +29 -0
- package/src/client/parts/55-pm.js +146 -0
- package/src/client/parts/58-config.js +381 -0
- package/src/client/parts/59-monitor.js +262 -0
- package/src/client/parts/60-page.js +1518 -0
- package/src/client/styles/kit.css +20 -0
- package/src/client/styles/main.css +3294 -0
- package/src/client/styles/tokens.css +2 -0
- package/vendor/leaflet/leaflet.css +661 -0
- package/vendor/leaflet/leaflet.js +6 -0
- package/vendor/three/three.module.js +53044 -0
- package/vendor/xterm/addon-fit.js +2 -0
- package/vendor/xterm/xterm.css +218 -0
- 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
|
+
|