package lkgateway // listquery.go — списки «Исходящие», «Входящие», «Журнал обмена»: период (с–по, быстрые «сегодня / // 7 / 30 дней»), сортировка и подгрузка частями при прокрутке. Страница отдаёт первые listPageSize // строк; следующие браузер берёт тем же адресом с part=rows&offset=N (ответ — JSON с готовыми строками). import ( "bytes" "encoding/json" "html/template" "net/http" "net/url" "sort" "strconv" "strings" "time" ) const listPageSize = 50 var listMSK = time.FixedZone("МСК", 3*3600) // listQ — что выбрал пользователь: период, сортировка, с какой строки. type listQ struct { From, To time.Time // To — начало дня после «по» (исключительно); нулевые — без границы FromS, ToS string // как в полях (ГГГГ-ММ-ДД) Sort string Asc bool Offset int Query string // параметры без offset/part — для подгрузки и ссылок } // parseListQ — параметры списка из адреса. sorts — допустимые ключи сортировки, первый — по умолчанию. func parseListQ(r *http.Request, sorts []string) listQ { v := r.URL.Query() q := listQ{Sort: sorts[0]} if t, err := time.ParseInLocation("2006-01-02", v.Get("from"), listMSK); err == nil { q.From, q.FromS = t, v.Get("from") } if t, err := time.ParseInLocation("2006-01-02", v.Get("to"), listMSK); err == nil { q.To, q.ToS = t.AddDate(0, 0, 1), v.Get("to") } for _, s := range sorts { if v.Get("sort") == s { q.Sort = s } } q.Asc = v.Get("dir") == "asc" q.Offset, _ = strconv.Atoi(v.Get("offset")) if q.Offset < 0 { q.Offset = 0 } keep := url.Values{} for _, k := range []string{"from", "to", "sort", "dir"} { if s := v.Get(k); s != "" { keep.Set(k, s) } } q.Query = keep.Encode() return q } // in — время попадает в выбранный период. func (q listQ) in(t time.Time) bool { return (q.From.IsZero() || !t.Before(q.From)) && (q.To.IsZero() || t.Before(q.To)) } // listItem — строка списка: время, ключи сортировки и данные для шаблона строки. type listItem struct { T time.Time Keys map[string]string V any } // listApply — отбор по периоду, сортировка и одна порция строк. func listApply(items []listItem, q listQ) (page []listItem, total int, more bool) { out := items[:0:0] for _, it := range items { if q.in(it.T) { out = append(out, it) } } sort.SliceStable(out, func(i, j int) bool { a, b := out[i], out[j] if q.Sort != "time" { ka, kb := strings.ToLower(a.Keys[q.Sort]), strings.ToLower(b.Keys[q.Sort]) if ka != kb { if q.Asc { return ka < kb } return ka > kb } return a.T.After(b.T) // при равенстве — новые выше } if q.Asc { return a.T.Before(b.T) } return a.T.After(b.T) }) total = len(out) if q.Offset >= total { return nil, total, false } end := q.Offset + listPageSize if end > total { end = total } return out[q.Offset:end], total, end < total } // listView — панель списка (период, сортировка) и состояние подгрузки — для шаблона "listTools". type listView struct { Q listQ Path string Body string // id
, куда дописываются строки Total int More bool Next int Sorts []listOpt Presets []listOpt } type listOpt struct { Key, Label, Href string On bool } // newListView — панель: быстрые периоды и варианты сортировки (sorts — пары ключ, подпись). func newListView(path, body string, q listQ, total int, more bool, sorts ...string) listView { lv := listView{Q: q, Path: path, Body: body, Total: total, More: more, Next: q.Offset + listPageSize} for i := 0; i+1 < len(sorts); i += 2 { lv.Sorts = append(lv.Sorts, listOpt{Key: sorts[i], Label: sorts[i+1], On: q.Sort == sorts[i]}) } today := time.Now().In(listMSK).Format("2006-01-02") day := func(n int) string { return time.Now().In(listMSK).AddDate(0, 0, -n).Format("2006-01-02") } href := func(from, to string) string { v := url.Values{} if from != "" { v.Set("from", from) v.Set("to", to) } if q.Sort != lv.Sorts[0].Key { v.Set("sort", q.Sort) } if q.Asc { v.Set("dir", "asc") } if s := v.Encode(); s != "" { return path + "?" + s } return path } for _, p := range []struct{ label, from string }{{"Сегодня", today}, {"7 дней", day(6)}, {"30 дней", day(29)}, {"Всё время", ""}} { to := today if p.from == "" { to = "" } lv.Presets = append(lv.Presets, listOpt{Label: p.label, Href: href(p.from, to), On: q.FromS == p.from && q.ToS == to}) } return lv } // listRowsPart — ответ на part=rows: следующая порция строк готовым HTML (шаблон rowTpl на каждую строку). func listRowsPart(w http.ResponseWriter, t *template.Template, rowTpl string, page []listItem, q listQ, more bool) { tu := userTemplate(w, t) var buf bytes.Buffer for _, it := range page { if err := tu.ExecuteTemplate(&buf, rowTpl, it.V); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(map[string]any{"html": buf.String(), "more": more, "next": q.Offset + len(page)}) } // listVals — данные строк порции для шаблона страницы. func listVals(page []listItem) []any { out := make([]any, 0, len(page)) for _, it := range page { out = append(out, it.V) } return out }