UNPKG

ohayolibs

Version:

Ohayo is a set of essential modules for ohayojp.

466 lines (423 loc) 14.5 kB
import { DecimalPipe } from '@angular/common'; import { HttpParams } from '@angular/common/http'; import { Host, Injectable } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { CNCurrencyPipe, DatePipe, YNPipe, _HttpClient } from '@ohayo/theme'; import { deepCopy, deepGet } from '@ohayo/util'; import { NzSafeAny } from 'ng-zorro-antd/core/types'; import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { STColumnFilter, STColumnFilterMenu, STData, STMultiSort, STMultiSortResultType, STPage, STReq, STReqReNameType, STRequestOptions, STRes, STRowClassName, STSingleSort, STSortMap, STStatistical, STStatisticalResult, STStatisticalResults, STStatisticalType, } from './st.interfaces'; import { _STColumn } from './st.types'; export interface STDataSourceOptions { pi: number; ps: number; paginator: boolean; data: string | STData[] | Observable<STData[]>; total: number; req: STReq; res: STRes; page: STPage; columns: _STColumn[]; singleSort?: STSingleSort; multiSort?: STMultiSort; rowClassName?: STRowClassName; } export interface STDataSourceResult { /** 是否需要显示分页器 */ pageShow: boolean; /** 新 `pi`,若返回 `undefined` 表示用户受控 */ pi: number; /** 新 `ps`,若返回 `undefined` 表示用户受控 */ ps: number; /** 新 `total`,若返回 `undefined` 表示用户受控 */ total: number; /** 数据 */ list: STData[]; /** 统计数据 */ statistical: STStatisticalResults; } @Injectable() export class STDataSource { private sortTick = 0; constructor( private http: _HttpClient, @Host() private currentyPipe: CNCurrencyPipe, @Host() private datePipe: DatePipe, @Host() private ynPipe: YNPipe, @Host() private numberPipe: DecimalPipe, private dom: DomSanitizer, ) { } process(options: STDataSourceOptions): Observable<STDataSourceResult> { let data$: Observable<STData[]>; let isRemote = false; const { data, res, total, page, pi, ps, paginator, columns } = options; let retTotal: number; let retPs: number; let retList: STData[]; let retPi: number; let rawData: any; let showPage = page.show; if (typeof data === 'string') { isRemote = true; data$ = this.getByHttp(data, options).pipe( map(result => { rawData = result; let ret: STData[]; if (Array.isArray(result)) { ret = result; retTotal = ret.length; retPs = retTotal; showPage = false; } else { // list ret = deepGet(result, res.reName!.list as string[], []); if (ret == null || !Array.isArray(ret)) { ret = []; } // total const resultTotal = res.reName!.total && deepGet(result, res.reName!.total as string[], null); retTotal = resultTotal == null ? total || 0 : +resultTotal; } return deepCopy(ret); }), ); } else if (Array.isArray(data)) { data$ = of(data); } else { // a cold observable data$ = data; } if (!isRemote) { data$ = data$.pipe( // sort map((result: STData[]) => { rawData = result; let copyResult = deepCopy(result); const sorterFn = this.getSorterFn(columns as _STColumn[]); if (sorterFn) { copyResult = copyResult.sort(sorterFn); } return copyResult; }), // filter map((result: STData[]) => { columns .filter(w => w.filter) .forEach(c => { const filter = c.filter!; const values = this.getFilteredData(filter); if (values.length === 0) return; const onFilter = filter.fn; if (typeof onFilter !== 'function') { console.warn(`[st] Muse provide the fn function in filter`); return; } result = result.filter(record => values.some(v => onFilter(v, record))); }); return result; }), // paging map((result: STData[]) => { if (paginator && page.front) { const maxPageIndex = Math.ceil(result.length / ps); retPi = Math.max(1, pi > maxPageIndex ? maxPageIndex : pi); retTotal = result.length; if (page.show === true) { return result.slice((retPi - 1) * ps, retPi * ps); } } return result; }), ); } // pre-process if (typeof res.process === 'function') { data$ = data$.pipe(map(result => res.process!(result, rawData))); } data$ = data$.pipe(map(result => this.optimizeData({ result, columns, rowClassName: options.rowClassName }))); return data$.pipe( map(result => { retList = result; const realTotal = retTotal || total; const realPs = retPs || ps; return { pi: retPi, ps: retPs, total: retTotal, list: retList, statistical: this.genStatistical(columns as _STColumn[], retList, rawData), pageShow: typeof showPage === 'undefined' ? realTotal > realPs : showPage, } as STDataSourceResult; }), ); } private get(item: STData, col: _STColumn, idx: number): { text: any; _text: SafeHtml; org?: any; color?: string } { if (col.format) { const formatRes = col.format(item, col, idx) || ''; if (formatRes && ~formatRes.indexOf('</')) { return { text: formatRes, _text: this.dom.bypassSecurityTrustHtml(formatRes), org: formatRes }; } return { text: formatRes, _text: formatRes, org: formatRes }; } const value = deepGet(item, col.index as string[], col.default); let text = value; let color: string | undefined; switch (col.type) { case 'no': text = this.getNoIndex(item, col, idx); break; case 'img': text = value ? `<img src="${value}" class="img">` : ''; break; case 'number': text = this.numberPipe.transform(value, col.numberDigits); break; case 'currency': text = this.currentyPipe.transform(value); break; case 'date': text = value === col.default ? col.default : this.datePipe.transform(value, col.dateFormat); break; case 'yn': text = this.ynPipe.transform(value === col.yn!.truth, col.yn!.yes!, col.yn!.no!, col.yn!.mode!, false); break; case 'enum': text = col.enum![value]; break; case 'tag': case 'badge': const data = col.type === 'tag' ? col.tag : col.badge; if (data && data[text]) { const dataItem = data[text]; text = dataItem.text; color = dataItem.color; } else { text = ''; } break; } if (text == null) text = ''; return { text, _text: this.dom.bypassSecurityTrustHtml(text), org: value, color }; } private getByHttp(url: string, options: STDataSourceOptions): Observable<{}> { const { req, page, paginator, pi, ps, singleSort, multiSort, columns } = options; const method = (req.method || 'GET').toUpperCase(); let params = {}; const reName = req.reName as STReqReNameType; if (paginator) { if (req.type === 'page') { params = { [reName.pi as string]: page.zeroIndexed ? pi - 1 : pi, [reName.ps as string]: ps, }; } else { params = { [reName.skip as string]: (pi - 1) * ps, [reName.limit as string]: ps, }; } } params = { ...params, ...req.params, ...this.getReqSortMap(singleSort, multiSort, columns), ...this.getReqFilterMap(columns), }; let reqOptions: STRequestOptions = { params, body: req.body, headers: req.headers, }; if (method === 'POST' && req.allInBody === true) { reqOptions = { body: { ...req.body, ...params }, headers: req.headers, }; } if (typeof req.process === 'function') { reqOptions = req.process(reqOptions); } if (!(reqOptions.params instanceof HttpParams)) { reqOptions.params = new HttpParams({ fromObject: reqOptions.params }); } return this.http.request(method, url, reqOptions); } optimizeData(options: { columns: _STColumn[]; result: STData[]; rowClassName?: STRowClassName }): STData[] { const { result, columns, rowClassName } = options; for (let i = 0, len = result.length; i < len; i++) { result[i]._values = columns.map(c => this.get(result[i], c, i)); if (rowClassName) { result[i]._rowClassName = rowClassName(result[i], i); } } return result; } getNoIndex(item: STData, col: _STColumn, idx: number): number { return typeof col.noIndex === 'function' ? col.noIndex(item, col, idx) : col.noIndex! + idx; } // #region sort private getValidSort(columns: _STColumn[]): STSortMap[] { return columns.filter(item => item._sort && item._sort.enabled && item._sort.default).map(item => item._sort!); } private getSorterFn(columns: _STColumn[]): ((a: STData, b: STData) => number) | void { const sortList = this.getValidSort(columns); if (sortList.length === 0) { return; } const sortItem = sortList[0]; if (sortItem.compare === null) { return; } if (typeof sortItem.compare !== 'function') { console.warn(`[st] Muse provide the compare function in sort`); return; } return (a: STData, b: STData) => { const result = sortItem.compare!(a, b); if (result !== 0) { return sortItem.default === 'descend' ? -result : result; } return 0; }; } get nextSortTick(): number { return ++this.sortTick; } getReqSortMap(singleSort: STSingleSort | undefined, multiSort: STMultiSort | undefined, columns: _STColumn[]): STMultiSortResultType { let ret: STMultiSortResultType = {}; const sortList = this.getValidSort(columns); if (multiSort) { const ms: STMultiSort = { key: 'sort', separator: '-', nameSeparator: '.', keepEmptyKey: true, arrayParam: false, ...multiSort, }; const sortMap = sortList .sort((a, b) => a.tick - b.tick) .map(item => item.key! + ms.nameSeparator + ((item.reName || {})[item.default!] || item.default)); ret = { [ms.key!]: ms.arrayParam ? sortMap : sortMap.join(ms.separator) }; return sortMap.length === 0 && ms.keepEmptyKey === false ? {} : ret; } if (sortList.length === 0) return ret; const mapData = sortList[0]; let sortFiled = mapData.key; let sortValue = (sortList[0].reName || {})[mapData.default!] || mapData.default; if (singleSort) { sortValue = sortFiled + (singleSort.nameSeparator || '.') + sortValue; sortFiled = singleSort.key || 'sort'; } ret[sortFiled as string] = sortValue as string; return ret; } // #endregion // #region filter private getFilteredData(filter: STColumnFilter): STColumnFilterMenu[] { return filter.type === 'default' ? filter.menus!.filter(f => f.checked === true) : filter.menus!.slice(0, 1); } private getReqFilterMap(columns: _STColumn[]): { [key: string]: string } { let ret = {}; columns .filter(w => w.filter && w.filter.default === true) .forEach(col => { const filter = col.filter!; const values = this.getFilteredData(filter); let obj: { [key: string]: NzSafeAny } = {}; if (filter.reName) { obj = filter.reName!(filter.menus!, col); } else { obj[filter.key!] = values.map(i => i.value).join(','); } ret = { ...ret, ...obj }; }); return ret; } // #endregion // #region statistical private genStatistical(columns: _STColumn[], list: STData[], rawData: any): STStatisticalResults { const res: { [key: string]: NzSafeAny } = {}; columns.forEach((col, index) => { res[col.key || col.indexKey || index] = col.statistical == null ? {} : this.getStatistical(col, index, list, rawData); }); return res; } private getStatistical(col: _STColumn, index: number, list: STData[], rawData: any): STStatisticalResult { const val = col.statistical; const item: STStatistical = { digits: 2, currency: undefined, ...(typeof val === 'string' ? { type: val as STStatisticalType } : (val as STStatistical)), }; let res: STStatisticalResult = { value: 0 }; let currency = false; if (typeof item.type === 'function') { res = item.type(this.getValues(index, list), col, list, rawData); currency = true; } else { switch (item.type) { case 'count': res.value = list.length; break; case 'distinctCount': res.value = this.getValues(index, list).filter((value, idx, self) => self.indexOf(value) === idx).length; break; case 'sum': res.value = this.toFixed(this.getSum(index, list), item.digits!); currency = true; break; case 'average': res.value = this.toFixed(this.getSum(index, list) / list.length, item.digits!); currency = true; break; case 'max': res.value = Math.max(...this.getValues(index, list)); currency = true; break; case 'min': res.value = Math.min(...this.getValues(index, list)); currency = true; break; } } if (item.currency === true || (item.currency == null && currency === true)) { res.text = this.currentyPipe.transform(res.value) as string; } else { res.text = String(res.value); } return res; } private toFixed(val: number, digits: number): number { if (isNaN(val) || !isFinite(val)) { return 0; } return parseFloat(val.toFixed(digits)); } private getValues(index: number, list: STData[]): number[] { return list.map(i => i._values[index].org).map(i => (i === '' || i == null ? 0 : i)); } private getSum(index: number, list: STData[]): number { return this.getValues(index, list).reduce((p, i) => (p += parseFloat(String(i))), 0); } // #endregion }