ismism/ismism.ts/ui/bind/section.ts
2023-04-07 14:13:20 +08:00

387 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Md, Work } from "../../src/eid/typ.ts"
import type { DocC, DocD, DocU } from "../../src/db.ts"
import type * as Q from "../../src/pra/que.ts"
import { Agd, Soc, Usr, rec as arec, md, put, aut } from "./article.ts"
import { adm, adm1_def, adm2_def } from "../../src/ont/adm.ts"
import { utc_d, utc_date, utc_medium } from "../../src/ont/utc.ts"
import { nav, navpas } from "./nav.ts"
import { bind, pos, que, Section, utc_refresh } from "./template.ts"
import { is_ref, is_rej, is_sec } from "../../src/pra/can.ts"
import { is_aut, is_id, is_md, is_nam, lim_aut, lim_nrecday, lim_md, lim_re, lim_sec } from "../../src/eid/is.ts"
export function label(
el: HTMLElement | SVGSVGElement,
s: string,
append = false
) {
const l = el.previousElementSibling as HTMLLabelElement
if (append) l.innerText += s
else l.innerText = s
}
export function txt(
t: HTMLTextAreaElement,
n: string,
s?: string,
) {
if (s) t.value = s
label(t, `${n}${t.value.length}/${t.maxLength} 个字符)`)
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)
}
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)
})
}
export function btn<
T
>(
b: HTMLButtonElement,
s: string,
c?: {
confirm?: string,
pos: () => T,
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
const r = await c.pos()
if (c.alert) {
if (r === null) { alert(c.alert); b.disabled = false; return }
} else {
if (!r || typeof r === "number" && r <= 0) return
}
if (r || r === 0) setTimeout(() => c.refresh(r), utc_refresh)
}
}); else b.disabled = true
}
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"],
id: Usr | Soc | Agd,
ua?: Usr["aut"],
) {
t.adm.innerText = `${id.adm1} ${id.adm2}`
t.utc.innerText = utc_medium(id.utc)
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
let cls = null
if (ref) { cls = "green"; t.ref2.classList.add(cls) }
if (rej) { cls = "red"; t.rej2.classList.add(cls) }
return cls
}
export function rolref(
t: HTMLParagraphElement,
u: Usr,
) {
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")
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")
ida(t, u.aref.uid.map(([a, r]) => [`a${a}`, `${u.anam.get(a)}志愿者 (${r}推荐)`]), "uid")
ida(t, u.sref.uid.map(([a, r]) => [`s${a}`, `${u.snam.get(a)}志愿者 (${r}推荐)`]), "uid")
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")
}
export function re(
t: Section["re"],
u: Usr,
) {
label(t.urej, `${u.urej.length}/${lim_re}`, true)
ida(t.urej, u.urej.map(r => [`${r}`, u.unam.get(r)!]))
label(t.uref, `${u.uref.length}/${lim_re}`, true)
ida(t.uref, u.uref.map(r => [`${r}`, u.unam.get(r)!]))
}
export function rel(
t: Section["rel"],
d: Soc | Agd,
) {
label(t.sec, `${d.sec.length}/${lim_sec}`, true)
ida(t.sec, d.sec.map(r => [`${r}`, d.unam.get(r)!]))
label(t.uid, `${d.uid.length}/${d.uidlim}`, true)
ida(t.uid, d.uid.map(r => [`${r}`, d.unam.get(r)!]))
label(t.res, `${d.res.length}/${d.reslim}`, true)
ida(t.res, d.res.map(r => [`${r}`, d.unam.get(r)!]))
}
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
}
if (a.account.length > 0) t.account.href = a.account
else t.account.classList.add("none")
}
export function goal(
t: HTMLParagraphElement,
a: Agd,
) {
for (const { nam, pct } of a.goal.sort((m, n) => m.pct - n.pct)) {
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)
}
}
export function seladm(
t: Section["seladm"],
adm1 = adm1_def,
adm2 = adm2_def,
) {
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)!))
}
function nrecday(
s: Section["rec"],
d: Usr | Soc | Agd,
) {
const svg = bind("nrecday").nrecday
const r = svg.getElementsByTagName("rect")
const date = new Date(utc_date(Date.now() - lim_nrecday * utc_d, true))
const day = (date.getDay() + 6) % 7
const t = date.getTime()
for (let n = 0; n <= lim_nrecday; ++n) r[day + n].classList.add("day")
let nrec = 0
d.nrecd90.forEach(([td, nr]) => {
if (td < t) return
nrec += nr
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)
s.nrecday.append(svg)
label(svg, `(最近${lim_nrecday}天有${nrec}条工作日志)`, true)
}
export function rec(
t: Section["rec"],
id: "uid" | "sid" | "aid",
d: Usr | Soc | Agd,
froze: boolean,
) {
nrecday(t, d)
label(t.recwork, `${d.nrec.work}`, true)
label(t.recfund, `${d.nrec.fund}`, true)
if (froze) { [t.recwork, t.recfund].forEach(el => el.classList.add("froze")); return }
const utc = { work: d.nrec.work > 0 ? 0 : -1, fund: d.nrec.fund > 0 ? 0 : -1 }
const lrec = async (c: "work" | "fund") => {
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]}`)
if (!rec || rec.rec.length === 0) { utc[c] = -1; return }
utc[c] = rec.rec[rec.rec.length - 1]._id.utc
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)
}
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)
})
}
export function putrel(
t: Section["putrel"],
id: "sid" | "aid",
d: Soc | Agd,
refresh: () => void,
) {
if (!nav.pas) { t.putrel.remove(); return }
const namid = new Map([...d.unam.entries()].map(([u, nam]) => [nam, u]))
if (is_aut(nav.pas.aut, "aut")) t.putsec.addEventListener("click", () => put(
`${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
},
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
},
a: `无效志愿者名或志愿者已满\n增删的志愿者需先作为申请人或志愿者出现在${id === "sid" ? "小组" : "活动"}名单`,
r: refresh,
})); else t.putuid.remove() // deno-lint-ignore no-explicit-any
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()
const [isuid, isres] = [d.uid.includes(nav.pas.uid), d.res.includes(nav.pas.uid)]
if (!isuid && d.rej.length === 0 && d.ref.length > 0 || isres) btn(t.putres, isres ? "取消申请" : "申请加入", !isres && d.res.length >= d.reslim ? undefined : {
pos: () => pos<DocU>("put", { [id]: d._id, rol: "res", uid: nav.pas!.uid, add: !isres }),
refresh,
alert: "申请人已满",
}); else t.putres.disabled = true
}
export function wsllit(
t: Section["wsllit"],
) {
if (!nav.pas) { t.wsllit.remove(); return }
if (is_aut(nav.pas.aut, "sup")) {
for (const c of ["wsl", "lit"] as const) {
const el = t[`pre${c}a`]
el.addEventListener("click", () => put(`${nav.pas!.uid}`, el.innerText, {
nam: { p1: "用户名:" }, val: {}, p: "pre",
b: p => p.p1 && is_nam(p.p1) ? { nam: p.p1, aut: c } : null,
a: "无效用户名,或已达上限",
r: async () => { await navpas(); aut() },
}))
}
} else[t.prewsla, t.prelita].forEach(el => el.remove())
for (const c of ["wsl", "lit"] as const) {
const el = t[`pre${c}`]
if (is_aut(nav.pas.aut, c)) {
if (!is_rej(nav.pas)) el.addEventListener("click", async () => {
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()
}
}
export function putpro(
t: Section["putpro"],
id: "uid" | "sid" | "aid" | "workid",
d: Usr | Soc | Agd | Work,
refresh?: () => void,
) {
if (!nav.pas) { t.putpro.remove(); return }
const repas = id === "sid" || id !== "aid"
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 })
btn(t.putrej, rej ? "取消反对" : "反对", refresh ? {
pos: () => p("rej", !rej),
refresh
} : undefined)
btn(t.putref, ref ? "取消推荐" : "推荐", refresh ? {
pos: () => p("ref", !ref),
refresh: async () => { if (repas) await navpas(); refresh() },
} : undefined)
}