ismism/ismism.ts/ui/bind/section.ts

384 lines
13 KiB
TypeScript
Raw Normal View History

2023-03-14 13:47:51 +08:00
import type { Md, Work } from "../../src/eid/typ.ts"
import type { DocC, DocD, DocU } from "../../src/db.ts"
2023-03-07 13:06:53 +08:00
import type * as Q from "../../src/pra/que.ts"
2023-03-16 18:32:47 +08:00
import { Agd, Soc, Usr, rec as arec, md, put, aut } from "./article.ts"
2023-03-05 13:35:22 +08:00
import { adm, adm1_def, adm2_def } from "../../src/ont/adm.ts"
2023-03-18 13:32:38 +08:00
import { utc_d, utc_date, utc_medium } from "../../src/ont/utc.ts"
2023-03-06 11:29:37 +08:00
import { nav, navpas } from "./nav.ts"
2023-03-06 18:30:25 +08:00
import { bind, pos, que, Section, utc_refresh } from "./template.ts"
2023-03-20 10:33:43 +08:00
import { is_ref, is_rej, is_sec } from "../../src/pra/con.ts"
2023-03-19 13:16:14 +08:00
import { is_aut, is_id, is_md, is_nam, lim_aut, lim_nrecday, lim_md, lim_re, lim_sec } from "../../src/eid/is.ts"
2023-03-05 11:28:24 +08:00
export function label(
el: HTMLElement,
s: string,
2023-03-10 11:15:46 +08:00
append = false
2023-03-05 11:28:24 +08:00
) {
const l = el.previousElementSibling as HTMLLabelElement
2023-03-10 11:15:46 +08:00
if (append) l.innerText += s
else l.innerText = s
2023-03-05 11:28:24 +08:00
}
2023-03-04 15:52:16 +08:00
2023-03-05 12:48:06 +08:00
export function txt(
t: HTMLTextAreaElement,
n: string,
s?: string,
) {
if (s) t.value = s
2023-03-14 13:47:51 +08:00
label(t, `${n}${t.value.length}/${t.maxLength} 个字符)`)
2023-03-05 12:48:06 +08:00
t.addEventListener("input", () => {
label(t, `${n}${t.value.length}/${t.maxLength} 个字符)`)
t.style.height = "auto"
t.style.height = `${t.scrollHeight}px`
})
if (s) setTimeout(() => t.dispatchEvent(new Event("input")), 50)
}
2023-03-04 15:52:16 +08:00
function selopt(
sel: HTMLSelectElement,
opt: Iterable<string>,
) {
sel.options.length = 0
for (const op of opt) {
const t = document.createElement("option")
t.text = op
sel.add(t)
}
}
export function ida(
t: HTMLElement,
ht: [string, string][],
cls?: string | null,
) {
ht.forEach(([h, n]) => {
const a = t.appendChild(document.createElement("a"))
a.href = `#${h}`
a.innerText = n
if (cls) a.classList.add(cls)
})
}
2023-03-05 11:28:24 +08:00
export function btn<
T
>(
b: HTMLButtonElement,
s: string,
c?: {
confirm?: string,
2023-03-14 13:47:51 +08:00
pos: () => T,
2023-03-05 11:28:24 +08:00
alert?: string,
refresh: (r: NonNullable<Awaited<T>>) => void,
}
) {
b.innerText = s
if (c) b.addEventListener("click", async () => {
if (!c.confirm || confirm(c.confirm)) {
b.disabled = true
2023-03-14 13:47:51 +08:00
const r = await c.pos()
2023-03-05 11:28:24 +08:00
if (c.alert) {
if (r === null) { alert(c.alert); b.disabled = false; return }
} else {
if (!r || r <= 0) return
}
if (r || r === 0) setTimeout(() => c.refresh(r), utc_refresh)
}
}); else b.disabled = true
}
2023-03-04 15:52:16 +08:00
export function idnam(
t: Section["idnam"],
id: string,
nam?: string,
cls?: string | null,
) {
t.idnam.href = `#${id}`
t.id.innerText = id
if (nav.hash === id) t.id.classList.add("active")
if (nam) t.nam.innerText = nam
if (cls) t.id.classList.add(cls)
}
export function meta(
t: Section["meta"],
2023-03-16 18:32:47 +08:00
id: Usr | Soc | Agd,
ua?: Usr["aut"],
2023-03-04 15:52:16 +08:00
) {
t.adm.innerText = `${id.adm1} ${id.adm2}`
2023-03-11 12:25:52 +08:00
t.utc.innerText = utc_medium(id.utc)
2023-03-16 18:32:47 +08:00
if (ua) {
if (ua.length > 0) ida(t.ref, [["aut", `管理员(${lim_aut})`]])
ida(t.rej, id.rej.map(r => [`${r}`, id.unam.get(r)!]))
ida(t.ref, id.ref.map(r => [`${r}`, id.unam.get(r)!]))
} else {
if (id.rej.length > 0) ida(t.rej, [["aut", `管理员(${lim_aut})`]])
if (id.ref.length > 0) ida(t.ref, [["aut", `管理员(${lim_aut})`]])
}
const rej = ua && is_rej(id) || !ua && id.rej.length > 0
const ref = ua && ua.length === 0 && !is_ref(id) || !ua && id.ref.length === 0
2023-03-04 15:52:16 +08:00
let cls = null
2023-03-16 18:32:47 +08:00
if (ref) { cls = "green"; t.ref2.classList.add(cls) }
if (rej) { cls = "red"; t.rej2.classList.add(cls) }
2023-03-04 15:52:16 +08:00
return cls
}
export function rolref(
t: HTMLParagraphElement,
u: Usr,
) {
2023-03-16 18:32:47 +08:00
if (nav.pas && u._id === nav.pas.uid) {
if (is_aut(nav.pas.aut, "sup")) ida(t, [["aut", `超级管理员 (不公示)`]], "isec")
if (is_aut(nav.pas.aut, "aud")) ida(t, [["aut", `审计员 (不公示)`]], "isec")
}
if (is_aut(u.aut, "aut")) ida(t, [["aut", `管理员 (${lim_aut}推荐)`]], "isec")
if (is_aut(u.aut, "wsl")) ida(t, [["aut", `法律援助编辑 (${lim_aut}推荐)`]], "sec")
if (is_aut(u.aut, "lit")) ida(t, [["aut", `理论学习编辑 (${lim_aut}推荐)`]], "sec")
2023-03-10 11:15:46 +08:00
ida(t, u.aref.sec.map(([a, r]) => [`a${a}`, `${u.anam.get(a)}联络员 (${r}推荐)`]), "sec")
ida(t, u.sref.sec.map(([a, r]) => [`s${a}`, `${u.snam.get(a)}联络员 (${r}推荐)`]), "sec")
2023-03-04 15:52:16 +08:00
ida(t, u.aref.uid.map(([a, r]) => [`a${a}`, `${u.anam.get(a)}志愿者 (${r}推荐)`]), "uid")
2023-03-06 11:29:37 +08:00
ida(t, u.sref.uid.map(([a, r]) => [`s${a}`, `${u.snam.get(a)}志愿者 (${r}推荐)`]), "uid")
2023-03-04 15:52:16 +08:00
ida(t, u.aref.res.map(([a, r]) => [`a${a}`, `${u.anam.get(a)}申请人 (${r}推荐)`]), "res")
ida(t, u.sref.res.map(([a, r]) => [`s${a}`, `${u.snam.get(a)}申请人 (${r}推荐)`]), "res")
}
2023-03-06 12:37:46 +08:00
export function re(
t: Section["re"],
u: Usr,
) {
2023-03-10 11:15:46 +08:00
label(t.urej, `${u.urej.length}/${lim_re}`, true)
2023-03-06 12:37:46 +08:00
ida(t.urej, u.urej.map(r => [`${r}`, u.unam.get(r)!]))
2023-03-10 11:15:46 +08:00
label(t.uref, `${u.uref.length}/${lim_re}`, true)
2023-03-06 12:37:46 +08:00
ida(t.uref, u.uref.map(r => [`${r}`, u.unam.get(r)!]))
}
export function rel(
t: Section["rel"],
d: Soc | Agd,
) {
2023-03-10 11:15:46 +08:00
label(t.sec, `${d.sec.length}/${lim_sec}`, true)
2023-03-06 12:37:46 +08:00
ida(t.sec, d.sec.map(r => [`${r}`, d.unam.get(r)!]))
2023-03-10 11:15:46 +08:00
label(t.uid, `${d.uid.length}/${d.uidlim}`, true)
2023-03-06 12:37:46 +08:00
ida(t.uid, d.uid.map(r => [`${r}`, d.unam.get(r)!]))
2023-03-10 11:15:46 +08:00
label(t.res, `${d.res.length}/${d.reslim}`, true)
2023-03-06 12:37:46 +08:00
ida(t.res, d.res.map(r => [`${r}`, d.unam.get(r)!]))
}
2023-03-06 14:07:07 +08:00
export function cover(
t: Section["cover"],
a: Agd,
) {
if (a.img.length === 0) { t.cover.remove(); return }
let n = 0
const img = (d: number) => {
n = ((n + d) % a.img.length + a.img.length) % a.img.length;
t.imgn.innerText = `${n + 1} / ${a.img.length}`
t.imgnam.innerText = a.img[n].nam
t.img.src = a.img[n].src
}
t.prev.addEventListener("click", () => img(-1))
t.next.addEventListener("click", () => img(+1))
img(0)
}
export function acct(
t: Section["acct"],
a: Agd,
) {
if (a.budget > 0) {
t.fund.textContent = `${a.fund}`
t.budget.textContent = `${a.budget}`
t.expense.textContent = `${a.expense}`
const [fpct, epct] = [a.fund / a.budget, a.expense / a.budget].map(p => `${Math.round(p * 100)}%`)
t.fundbar.style.width = t.fundpct.textContent = fpct
t.expensebar.style.width = t.expensepct.textContent = epct
}
2023-03-06 18:30:25 +08:00
if (a.account.length > 0) t.account.href = a.account
else t.account.classList.add("none")
2023-03-06 14:07:07 +08:00
}
export function goal(
t: HTMLParagraphElement,
a: Agd,
) {
2023-03-06 15:01:03 +08:00
for (const { nam, pct } of a.goal.sort((m, n) => m.pct - n.pct)) {
2023-03-06 14:07:07 +08:00
const g = bind("goal")
g.nam.innerText = nam
if (pct === 0 || pct >= 100) g.pct.classList.add("gray")
if (pct >= 100) {
g.pct.textContent = "完成"
g.circle.remove()
} else {
g.pct.textContent = `${pct}%`
g.circle.style.setProperty("--pct", `${pct}`)
}
t.append(g.bind)
}
}
2023-03-04 15:52:16 +08:00
export function seladm(
t: Section["seladm"],
2023-03-05 13:35:22 +08:00
adm1 = adm1_def,
adm2 = adm2_def,
2023-03-04 15:52:16 +08:00
) {
selopt(t.adm1, adm.keys())
t.adm1.value = adm1
selopt(t.adm2, adm.get(adm1)!)
t.adm2.value = adm2
t.adm1.addEventListener("change", () => selopt(t.adm2, adm.get(t.adm1.value)!))
}
2023-03-19 13:16:14 +08:00
function nrecday(
2023-03-18 13:32:38 +08:00
s: Section["rec"],
d: Usr | Soc | Agd,
) {
2023-03-19 13:16:14 +08:00
const svg = bind("nrecday").nrecday
2023-03-18 13:32:38 +08:00
const r = svg.getElementsByTagName("rect")
2023-03-19 13:16:14 +08:00
const date = new Date(utc_date(Date.now() - lim_nrecday * utc_d) + "T00:00:00.000+08:00")
2023-03-18 13:32:38 +08:00
const day = (date.getDay() + 6) % 7
const t = date.getTime()
2023-03-19 13:16:14 +08:00
for (let n = 0; n <= lim_nrecday; ++n) r[day + n].classList.add("day")
d.nrecd90.forEach(([td, nr]) => {
2023-03-18 13:32:38 +08:00
if (td < t) return
const n = Math.floor((td - t) / utc_d)
r[day + n].classList.add(nr <= 4 ? "lo" : nr <= 8 ? "mi" : "hi")
})
const dwork = s.recwork.parentElement as HTMLDetailsElement
svg.addEventListener("click", () => dwork.open = !dwork.open)
2023-03-19 13:16:14 +08:00
s.nrecday.append(svg)
2023-03-18 13:32:38 +08:00
}
2023-03-06 18:30:25 +08:00
export function rec(
t: Section["rec"],
2023-03-07 13:06:53 +08:00
id: "uid" | "sid" | "aid",
d: Usr | Soc | Agd,
2023-03-06 18:30:25 +08:00
froze: boolean,
) {
2023-03-19 13:16:14 +08:00
nrecday(t, d)
2023-03-10 11:15:46 +08:00
label(t.recwork, `${d.nrec.work}`, true)
label(t.recfund, `${d.nrec.fund}`, true)
2023-03-06 18:30:25 +08:00
if (froze) { [t.recwork, t.recfund].forEach(el => el.classList.add("froze")); return }
2023-03-08 10:49:53 +08:00
const utc = { work: d.nrec.work > 0 ? 0 : -1, fund: d.nrec.fund > 0 ? 0 : -1 }
2023-03-06 18:30:25 +08:00
const lrec = async (c: "work" | "fund") => {
2023-03-07 13:06:53 +08:00
const p = t[`rec${c}`]
if (utc[c] < 0 || p.scrollTop > 0) return
const h = utc[c] === 0 ? 0 : p.scrollHeight
const rec = await que<Q.Rec>(`rec?c=${c}&${id}=${d._id}&utc=${utc[c]}`)
2023-03-06 18:30:25 +08:00
if (!rec || rec.rec.length === 0) { utc[c] = -1; return }
utc[c] = rec.rec[rec.rec.length - 1]._id.utc
2023-03-07 13:06:53 +08:00
const rc = { ...rec, unam: new Map(rec.unam), anam: new Map(rec.anam) }
for (const r of rc.rec) p.prepend(arec(c, rc, r))
setTimeout(() => p.scrollTop = p.scrollHeight - h, 100)
2023-03-06 18:30:25 +08:00
}
2023-03-08 10:49:53 +08:00
t.recwork.addEventListener("scroll", () => lrec("work"))
t.recfund.addEventListener("scroll", () => lrec("fund"))
const [dwork, dfund] = [t.recwork, t.recfund].map(r => r.parentElement as HTMLDetailsElement)
dwork.addEventListener("toggle", async () => {
if (!dwork.open) return
if (utc.work === 0) await lrec("work")
dwork.scrollIntoView(false)
})
dfund.addEventListener("toggle", async () => {
if (!dfund.open) return
if (utc.fund === 0) await lrec("fund")
dfund.scrollIntoView(false)
})
2023-03-06 18:30:25 +08:00
}
2023-03-06 12:37:46 +08:00
export function putrel(
t: Section["putrel"],
2023-03-05 21:40:00 +08:00
id: "sid" | "aid",
d: Soc | Agd,
refresh: () => void,
) {
2023-03-06 12:37:46 +08:00
if (!nav.pas) { t.putrel.remove(); return }
2023-03-05 21:40:00 +08:00
const namid = new Map([...d.unam.entries()].map(([u, nam]) => [nam, u]))
2023-03-16 18:32:47 +08:00
if (is_aut(nav.pas.aut, "aut")) t.putsec.addEventListener("click", () => put(
2023-03-14 13:47:51 +08:00
`${id === "sid" ? "s" : "a"}${d._id}`, t.putsec.innerText, {
nam: { p1: "用户名:" }, val: {}, p: "put",
b: p => {
const uid = namid.get(p.p1 ?? "")
return uid ? { [id]: d._id, rol: "sec", uid, add: !d.sec.includes(uid) } : null
2023-03-05 21:40:00 +08:00
},
2023-03-14 13:47:51 +08:00
a: `无效用户名或联络员已满\n增删的联络员需先作为申请人或其它出现在${id === "sid" ? "社团" : "活动"}名单`,
r: refresh,
})); else t.putsec.remove() // deno-lint-ignore no-explicit-any
if (is_sec(nav.pas, { [id]: d._id } as any)) t.putuid.addEventListener("click", () => put(
`${id === "sid" ? "s" : "a"}${d._id}`, t.putuid.innerText, {
nam: { p1: "用户名:" }, val: {}, p: "put",
b: p => {
const uid = namid.get(p.p1 ?? "")
return uid ? { [id]: d._id, rol: "uid", uid, add: !d.uid.includes(uid) } : null
2023-03-05 21:40:00 +08:00
},
2023-03-14 13:47:51 +08:00
a: `无效志愿者名或志愿者已满\n增删的志愿者需先作为申请人或志愿者出现在${id === "sid" ? "社团" : "活动"}名单`,
r: refresh,
})); else t.putuid.remove() // deno-lint-ignore no-explicit-any
2023-03-11 12:52:54 +08:00
if (is_aut(nav.pas.aut, "aut") || is_sec(nav.pas, { [id]: d._id } as any)) btn(t.putresn, t.putresn.innerText, d.res.length > 0 ? {
confirm: "清空申请人名单?",
pos: () => pos<DocU>("put", { [id]: d._id, rol: "res" }),
refresh,
} : undefined); else t.putresn.remove()
2023-03-05 21:40:00 +08:00
const [isuid, isres] = [d.uid.includes(nav.pas.uid), d.res.includes(nav.pas.uid)]
2023-03-16 18:32:47 +08:00
if (!isuid && d.rej.length === 0 && d.ref.length > 0 || isres) btn(t.putres, isres ? "取消申请" : "申请加入", !isres && d.res.length >= d.reslim ? undefined : {
2023-03-05 21:40:00 +08:00
pos: () => pos<DocU>("put", { [id]: d._id, rol: "res", uid: nav.pas!.uid, add: !isres }),
refresh,
alert: "申请人已满",
2023-03-06 12:37:46 +08:00
}); else t.putres.disabled = true
2023-03-05 21:40:00 +08:00
}
2023-03-12 13:15:48 +08:00
export function wsllit(
t: Section["wsllit"],
) {
if (!nav.pas) { t.wsllit.remove(); return }
2023-03-17 10:44:32 +08:00
if (is_aut(nav.pas.aut, "sup")) {
2023-03-14 13:47:51 +08:00
for (const c of ["wsl", "lit"] as const) {
const el = t[`pre${c}a`]
2023-03-16 18:32:47 +08:00
el.addEventListener("click", () => put(`${nav.pas!.uid}`, el.innerText, {
2023-03-14 13:47:51 +08:00
nam: { p1: "用户名:" }, val: {}, p: "pre",
b: p => p.p1 && is_nam(p.p1) ? { nam: p.p1, aut: c } : null,
2023-03-16 18:32:47 +08:00
a: "无效用户名,或已达上限",
r: async () => { await navpas(); aut() },
}))
2023-03-14 13:47:51 +08:00
}
2023-03-12 13:15:48 +08:00
} else[t.prewsla, t.prelita].forEach(el => el.remove())
2023-03-14 13:47:51 +08:00
for (const c of ["wsl", "lit"] as const) {
const el = t[`pre${c}`]
if (is_aut(nav.pas.aut, c)) {
2023-03-20 10:33:43 +08:00
if (!is_rej(nav.pas)) el.addEventListener("click", async () => {
2023-03-14 13:47:51 +08:00
const id = await pos<DocC<Md["_id"]>>("pre", { [`${c}nam`]: "新建文章" })
if (id && is_id(id)) put(`${c}${id}`, el.innerText, {
nam: { p1: "标题2-16个中文字符", pa: "正文 Markdown" }, val: {}, lim_pa: lim_md, p: "put",
b: p => {
if (!p.p1 || !p.pa || !is_nam(p.p1) || !is_md(p.pa)) return null
return { [`${c}id`]: id, nam: p.p1, md: p.pa.trim() }
},
a: `无效输入\n标题为 2-16 个中文字符\n正文最长 ${lim_md} 个字符`,
d: () => pos<DocD>("put", { [`${c}id`]: id }),
r: r => r === undefined ? md(c, 0, "many") : md(c, id, "one"),
})
}); else el.disabled = true
} else el.remove()
}
2023-03-12 13:15:48 +08:00
}
2023-03-06 12:37:46 +08:00
export function putpro(
t: Section["putpro"],
2023-03-07 13:06:53 +08:00
id: "uid" | "sid" | "aid" | "workid",
d: Usr | Soc | Agd | Work,
2023-03-05 11:28:24 +08:00
refresh?: () => void,
2023-03-04 15:52:16 +08:00
) {
2023-03-06 12:37:46 +08:00
if (!nav.pas) { t.putpro.remove(); return }
2023-03-07 13:06:53 +08:00
const repas = id === "sid" || id !== "aid"
2023-03-05 11:28:24 +08:00
const [rej, ref] = [d.rej.includes(nav.pas.uid), d.ref.includes(nav.pas.uid)]
const p = (re: "rej" | "ref", add: boolean) => pos<DocU>("pro", { re, [id]: d._id, add })
2023-03-06 12:37:46 +08:00
btn(t.putrej, rej ? "取消反对" : "反对", refresh ? {
2023-03-06 11:29:37 +08:00
pos: () => p("rej", !rej),
refresh
} : undefined)
2023-03-06 12:37:46 +08:00
btn(t.putref, ref ? "取消推荐" : "推荐", refresh ? {
2023-03-06 11:29:37 +08:00
pos: () => p("ref", !ref),
2023-03-07 13:06:53 +08:00
refresh: async () => { if (repas) await navpas(); refresh() },
2023-03-06 11:29:37 +08:00
} : undefined)
2023-03-04 15:52:16 +08:00
}