UNPKG

@delon/theme

Version:

ng-alain theme system library.

1,465 lines (1,452 loc) 118 kB
import { DOCUMENT, isPlatformServer, CommonModule, registerLocaleData } from '@angular/common'; import * as i0 from '@angular/core'; import { inject, PLATFORM_ID, InjectionToken, Injectable, DestroyRef, Injector, Pipe, Optional, SkipSelf, NgModule, importProvidersFrom, LOCALE_ID, provideEnvironmentInitializer, makeEnvironmentProviders, Version } from '@angular/core'; import { BehaviorSubject, filter, share, Subject, map, of, delay, isObservable, switchMap, Observable, take, tap, finalize, throwError, catchError } from 'rxjs'; import { ACLService } from '@delon/acl'; import { AlainConfigService, ALAIN_CONFIG } from '@delon/util/config'; import { Platform } from '@angular/cdk/platform'; import { Directionality } from '@angular/cdk/bidi'; import { NzConfigService } from 'ng-zorro-antd/core/config'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { Title, DomSanitizer } from '@angular/platform-browser'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { DragDrop } from '@angular/cdk/drag-drop'; import { SIGNAL } from '@angular/core/primitives/signals'; import { deepMerge } from '@delon/util/other'; import { NzModalService, NzModalModule } from 'ng-zorro-antd/modal'; import { NzDrawerService, NzDrawerModule } from 'ng-zorro-antd/drawer'; import { HttpClient, HttpParams, HttpContextToken } from '@angular/common/http'; import { formatDate } from '@delon/util/date-time'; import { NzI18nService, NzI18nModule, provideNzI18n, NZ_DATE_LOCALE } from 'ng-zorro-antd/i18n'; import { OverlayModule } from '@angular/cdk/overlay'; import { BellOutline, DeleteOutline, PlusOutline, InboxOutline, MenuFoldOutline, MenuUnfoldOutline } from '@ant-design/icons-angular/icons'; import * as i1 from 'ng-zorro-antd/icon'; import { NzIconService } from 'ng-zorro-antd/icon'; function stepPreloader() { const doc = inject(DOCUMENT); const ssr = isPlatformServer(inject(PLATFORM_ID)); if (ssr) { return () => { }; } const body = doc.querySelector('body'); body.style.overflow = 'hidden'; let done = false; return () => { if (done) return; done = true; const preloader = doc.querySelector('.preloader'); if (preloader == null) return; const CLS = 'preloader-hidden'; preloader.addEventListener('transitionend', () => { preloader.className = CLS; }); preloader.className += ` ${CLS}-add ${CLS}-add-active`; body.style.overflow = ''; }; } const ALAIN_I18N_TOKEN = new InjectionToken('alainI18nToken', { providedIn: 'root', factory: () => new AlainI18NServiceFake() }); class AlainI18nBaseService { cogSrv = inject(AlainConfigService); cog; _change$ = new BehaviorSubject(null); _currentLang = ''; _defaultLang = ''; _data = {}; get change() { return this._change$.asObservable().pipe(filter(w => w != null)); } get defaultLang() { return this._defaultLang; } get currentLang() { return this._currentLang; } get data() { return this._data; } constructor() { this.cog = this.cogSrv.merge('themeI18n', { interpolation: ['{{', '}}'] }); } /** * Flattened data source * * @example * { * "name": "Name", * "sys": { * "": "System", * "title": "Title" * } * } * => * { * "name": "Name", * "sys": "System", * "sys.title": "Title" * } */ flatData(data, parentKey) { const res = {}; for (const key of Object.keys(data)) { const value = data[key]; if (typeof value === 'object') { const child = this.flatData(value, parentKey.concat(key)); Object.keys(child).forEach(childKey => (res[childKey] = child[childKey])); } else { res[(key ? parentKey.concat(key) : parentKey).join('.')] = `${value}`; } } return res; } fanyi(path, params) { let content = this._data[path] || ''; if (!content) return path; if (!params) return content; if (typeof params === 'object') { const interpolation = this.cog.interpolation; const objParams = params; Object.keys(objParams).forEach(key => { content = content.replace(new RegExp(`${interpolation[0]}\\s?${key}\\s?${interpolation[1]}`, 'g'), `${objParams[key]}`); }); } (Array.isArray(params) ? params : [params]).forEach((item, index) => (content = content.replace(new RegExp(`\\{\\s?${index}\\s?\\}`, 'g'), `${item}`))); return content; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18nBaseService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18nBaseService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18nBaseService, decorators: [{ type: Injectable }], ctorParameters: () => [] }); class AlainI18NServiceFake extends AlainI18nBaseService { use(lang, data) { this._data = this.flatData(data ?? {}, []); this._currentLang = lang; this._change$.next(lang); } getLangs() { return []; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NServiceFake, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NServiceFake, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NServiceFake, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * 菜单服务,[在线文档](https://ng-alain.com/theme/menu) */ class MenuService { i18nSrv = inject(ALAIN_I18N_TOKEN); aclService = inject(ACLService); _change$ = new BehaviorSubject([]); i18n$; data = []; /** * 是否完全受控菜单打开状态,默认:`false` */ openStrictly = false; constructor() { this.i18n$ = this.i18nSrv.change.subscribe(() => this.resume()); } get change() { return this._change$.pipe(share()); } get menus() { return this.data; } /** * Returns a default menu link * * 返回一个默认跳转的菜单链接 */ getDefaultRedirect(opt = {}) { let ret; this.visit(this.menus, (item) => { if (typeof item.link !== 'string' || item.link.length <= 0 || !item._aclResult || item._hidden === true) { return; } if (ret == null || ret.length <= 0 || item.link == opt?.redirectUrl) { ret = item.link; } }); return ret; } visit(data, callback) { const inFn = (list, parentMenu, depth) => { for (const item of list) { callback(item, parentMenu, depth); if (item.children && item.children.length > 0) { inFn(item.children, item, depth + 1); } else { item.children = []; } } }; inFn(data, null, 0); } add(items) { this.data = items; this.resume(); } fixItem(item) { item._aclResult = true; if (!item.render_type) item.render_type = 'item'; if (!item.link) item.link = ''; if (!item.externalLink) item.externalLink = ''; // badge if (item.badge) { if (item.badgeDot !== true) { item.badgeDot = false; } if (!item.badgeStatus) { item.badgeStatus = 'error'; } } if (!Array.isArray(item.children)) { item.children = []; } // icon if (typeof item.icon === 'string') { let type = 'class'; let value = item.icon; // compatible `anticon anticon-user` if (~item.icon.indexOf(`anticon-`)) { type = 'icon'; value = value.split('-').slice(1).join('-'); } else if (/^https?:\/\//.test(item.icon)) { type = 'img'; } item.icon = { type, value }; } if (item.icon != null) { item.icon = { theme: 'outline', spin: false, ...item.icon }; } item.text = item.i18n ? this.i18nSrv.fanyi(item.i18n) : item.text; // group item.group = item.group !== false; // hidden item._hidden = typeof item.hide === 'undefined' ? false : item.hide; // disabled item.disabled = typeof item.disabled === 'undefined' ? false : item.disabled; // acl item._aclResult = item.acl ? this.aclService.can(item.acl) : true; item.open = item.open != null ? item.open : false; } resume(callback) { let i = 1; const shortcuts = []; this.visit(this.data, (item, parent, depth) => { item._id = i++; item._parent = parent; item._depth = depth; this.fixItem(item); // shortcut if (parent && item.shortcut === true && parent.shortcutRoot !== true) { shortcuts.push(item); } if (callback) callback(item, parent, depth); }); this.loadShortcut(shortcuts); this._change$.next(this.data); } /** * 加载快捷菜单,加载位置规则如下: * 1、统一在下标0的节点下(即【主导航】节点下方) * 1、若 children 存在 【shortcutRoot: true】则最优先【推荐】这种方式 * 2、否则查找带有【dashboard】字样链接,若存在则在此菜单的下方创建快捷入口 * 3、否则放在0节点位置 */ loadShortcut(shortcuts) { if (shortcuts.length === 0 || this.data.length === 0) { return; } const ls = this.data[0].children; let pos = ls.findIndex(w => w.shortcutRoot === true); if (pos === -1) { pos = ls.findIndex(w => w.link.includes('dashboard')); pos = (pos !== -1 ? pos : -1) + 1; const shortcutMenu = { text: '快捷菜单', i18n: 'shortcut', icon: 'icon-rocket', children: [] }; this.data[0].children.splice(pos, 0, shortcutMenu); } let _data = this.data[0].children[pos]; if (_data.i18n) _data.text = this.i18nSrv.fanyi(_data.i18n); _data = Object.assign(_data, { shortcutRoot: true, _id: -1, _parent: null, _depth: 1 }); _data.children = shortcuts.map(i => { i._depth = 2; i._parent = _data; return i; }); } /** * 清空菜单 */ clear() { this.data = []; this._change$.next(this.data); } /** * Use `url` or `key` to find menus * * 利用 `url` 或 `key` 查找菜单 */ find(options) { const opt = { recursive: false, ignoreHide: false, last: false, ...options }; if (opt.key != null) { return this.getItem(opt.key); } let url = opt.url; let item = null; while (!item && url) { this.visit(opt.data ?? this.data, i => { if (!opt.last && item != null) { return; } if (opt.ignoreHide && i.hide) { return; } if (opt.cb) { const res = opt.cb(i); if (typeof res === 'boolean' && res) { item = i; } } if (i.link != null && i.link === url) { item = i; } }); if (!opt.recursive) break; if (/[?;]/g.test(url)) { url = url.split(/[?;]/g)[0]; } else { url = url.split('/').slice(0, -1).join('/'); } } return item; } /** * 根据url获取菜单列表 * - 若 `recursive: true` 则会自动向上递归查找 * - 菜单数据源包含 `/ware`,则 `/ware/1` 也视为 `/ware` 项 */ getPathByUrl(url, recursive = false) { const ret = []; let item = this.find({ url, recursive }); if (!item) return ret; do { ret.splice(0, 0, item); item = item._parent; } while (item); return ret; } /** * Get menu based on `key` */ getItem(key) { let res = null; this.visit(this.data, item => { if (res == null && item.key === key) { res = item; } }); return res; } /** * Set menu based on `key` */ setItem(key, value, options) { const item = typeof key === 'string' ? this.getItem(key) : key; if (item == null) return; Object.keys(value).forEach(k => { item[k] = value[k]; }); this.fixItem(item); if (options?.emit !== false) this._change$.next(this.data); } /** * Open menu based on `key` or menu object */ open(keyOrItem, options) { let item = typeof keyOrItem === 'string' ? this.find({ key: keyOrItem }) : keyOrItem; if (item == null) return; this.visit(this.menus, (i) => { i._selected = false; if (!this.openStrictly) i.open = false; }); do { item._selected = true; item.open = true; item = item._parent; } while (item); if (options?.emit !== false) this._change$.next(this.data); } openAll(status) { this.toggleOpen(null, { allStatus: status }); } toggleOpen(keyOrItem, options) { let item = typeof keyOrItem === 'string' ? this.find({ key: keyOrItem }) : keyOrItem; if (item == null) { this.visit(this.menus, (i) => { i._selected = false; i.open = options?.allStatus === true; }); } else { if (!this.openStrictly) { this.visit(this.menus, (i) => { if (i !== item) i.open = false; }); let pItem = item._parent; while (pItem) { pItem.open = true; pItem = pItem._parent; } } item.open = !item.open; } if (options?.emit !== false) this._change$.next(this.data); } ngOnDestroy() { this._change$.unsubscribe(); this.i18n$?.unsubscribe(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: MenuService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: MenuService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: MenuService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); const ALAIN_SETTING_KEYS = new InjectionToken('ALAIN_SETTING_KEYS'); const ALAIN_SETTING_DEFAULT = { provide: ALAIN_SETTING_KEYS, useValue: { layout: 'layout', user: 'user', app: 'app' } }; class SettingsService { KEYS = inject(ALAIN_SETTING_KEYS); platform = inject(Platform); notify$ = new Subject(); _app = null; _user = null; _layout = null; getData(key) { if (!this.platform.isBrowser) { return null; } return JSON.parse(localStorage.getItem(key) || 'null') || null; } setData(key, value) { if (!this.platform.isBrowser) { return; } localStorage.setItem(key, JSON.stringify(value)); } get layout() { if (!this._layout) { this._layout = { fixed: true, collapsed: false, boxed: false, lang: null, ...this.getData(this.KEYS.layout) }; this.setData(this.KEYS.layout, this._layout); } return this._layout; } get app() { if (!this._app) { this._app = { year: new Date().getFullYear(), ...this.getData(this.KEYS.app) }; this.setData(this.KEYS.app, this._app); } return this._app; } get user() { if (!this._user) { this._user = { ...this.getData(this.KEYS.user) }; this.setData(this.KEYS.user, this._user); } return this._user; } get notify() { return this.notify$.asObservable(); } setLayout(name, value) { if (typeof name === 'string') { this.layout[name] = value; } else { this._layout = name; } this.setData(this.KEYS.layout, this._layout); this.notify$.next({ type: 'layout', name, value }); return true; } getLayout() { return this._layout; } setApp(value) { this._app = value; this.setData(this.KEYS.app, value); this.notify$.next({ type: 'app', value }); } getApp() { return this._app; } setUser(value) { this._user = value; this.setData(this.KEYS.user, value); this.notify$.next({ type: 'user', value }); } getUser() { return this._user; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SettingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SettingsService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SettingsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const REP_MAX = 6; const SPAN_MAX = 24; class ResponsiveService { cogSrv = inject(AlainConfigService); cog; constructor() { this.cog = this.cogSrv.merge('themeResponsive', { rules: { 1: { xs: 24 }, 2: { xs: 24, sm: 12 }, 3: { xs: 24, sm: 12, md: 8 }, 4: { xs: 24, sm: 12, md: 8, lg: 6 }, 5: { xs: 24, sm: 12, md: 8, lg: 6, xl: 4 }, 6: { xs: 24, sm: 12, md: 8, lg: 6, xl: 4, xxl: 2 } } }); if (Object.keys(this.cog.rules) .map(i => +i) .some((i) => i < 1 || i > REP_MAX)) { throw new Error(`[theme] the responseive rule index value range must be 1-${REP_MAX}`); } } genCls(count, defaultCol = 1) { const rule = { ...this.cog.rules[count > REP_MAX ? REP_MAX : Math.max(count, 1)] }; const antColClass = 'ant-col'; const itemMaxSpan = SPAN_MAX / defaultCol; const paddingSpan = (value) => { if (value == null || defaultCol <= 1 || count >= defaultCol) return value; return Math.max(value, count * itemMaxSpan); }; const clsMap = [`${antColClass}-xs-${paddingSpan(rule.xs)}`]; if (rule.sm) clsMap.push(`${antColClass}-sm-${paddingSpan(rule.sm)}`); if (rule.md) clsMap.push(`${antColClass}-md-${paddingSpan(rule.md)}`); if (rule.lg) clsMap.push(`${antColClass}-lg-${paddingSpan(rule.lg)}`); if (rule.xl) clsMap.push(`${antColClass}-xl-${paddingSpan(rule.xl)}`); if (rule.xxl) clsMap.push(`${antColClass}-xxl-${paddingSpan(rule.xxl)}`); return clsMap; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ResponsiveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ResponsiveService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ResponsiveService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); const HTML_DIR = 'dir'; const RTL_DIRECTION = 'direction'; const RTL_NZ_COMPONENTS = ['modal', 'drawer', 'message', 'notification', 'image']; const RTL_DELON_COMPONENTS = ['loading', 'onboarding']; const LTR = 'ltr'; const RTL = 'rtl'; class RTLService { d = inject(Directionality); nz = inject(NzConfigService); delon = inject(AlainConfigService); platform = inject(Platform); doc = inject(DOCUMENT); srv = inject(SettingsService); _dir = LTR; /** * Get or Set the current text direction * * 获取或设置当前文字方向 */ get dir() { return this._dir; } set dir(value) { this._dir = value; this.updateLibConfig(); this.updateHtml(); // Should be wait inited Promise.resolve().then(() => { this.d.valueSignal.set(value); this.d.change.emit(value); this.srv.setLayout(RTL_DIRECTION, value); }); } /** * Get the next text direction * * 获取下一次文字方向 */ get nextDir() { return this.dir === LTR ? RTL : LTR; } /** * Subscription change notification * * 订阅变更通知 */ get change() { return this.srv.notify.pipe(filter(w => w.name === RTL_DIRECTION), map(v => v.value)); } constructor() { this.dir = this.srv.layout.direction === RTL ? RTL : LTR; } /** * Toggle text direction * * 切换文字方向 */ toggle() { this.dir = this.nextDir; } updateHtml() { if (!this.platform.isBrowser) { return; } const htmlEl = this.doc.querySelector('html'); if (htmlEl) { const dir = this.dir; htmlEl.style.direction = dir; htmlEl.classList.remove(RTL, LTR); htmlEl.classList.add(dir); htmlEl.setAttribute(HTML_DIR, dir); } } updateLibConfig() { RTL_NZ_COMPONENTS.forEach(name => { this.nz.set(name, { nzDirection: this.dir }); }); RTL_DELON_COMPONENTS.forEach(name => { this.delon.set(name, { direction: this.dir }); }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: RTLService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: RTLService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: RTLService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class TitleService { destroy$ = inject(DestroyRef); _prefix = ''; _suffix = ''; _separator = ' - '; _reverse = false; tit$; DELAY_TIME = 25; doc = inject(DOCUMENT); injector = inject(Injector); title = inject(Title); menuSrv = inject(MenuService); i18nSrv = inject(ALAIN_I18N_TOKEN); constructor() { this.i18nSrv.change.pipe(takeUntilDestroyed()).subscribe(() => this.setTitle()); } /** * Set separator * * 设置分隔符 */ set separator(value) { this._separator = value; } /** * Set prefix * * 设置前缀 */ set prefix(value) { this._prefix = value; } /** * Set suffix * * 设置后缀 */ set suffix(value) { this._suffix = value; } /** * Set whether to reverse * * 设置是否反转 */ set reverse(value) { this._reverse = value; } /** * Set the default CSS selector string * * 设置默认CSS选择器字符串 */ selector; /** * Set default title name * * 设置默认标题名 */ default = `Not Page Name`; getByElement() { return of('').pipe(delay(this.DELAY_TIME), map(() => { const el = ((this.selector != null ? this.doc.querySelector(this.selector) : null) || this.doc.querySelector('.alain-default__content-title h1') || this.doc.querySelector('.page-header__title')); if (el) { let text = ''; el.childNodes.forEach(val => { if (!text && val.nodeType === 3) { text = val.textContent.trim(); } }); return text || el.firstChild.textContent.trim(); } return ''; })); } getByRoute() { let next = this.injector.get(ActivatedRoute); while (next.firstChild) next = next.firstChild; const data = (next.snapshot && next.snapshot.data) || {}; if (data.titleI18n) data.title = this.i18nSrv.fanyi(data.titleI18n); return isObservable(data.title) ? data.title : of(data.title); } getByMenu() { const menus = this.menuSrv.getPathByUrl(this.injector.get(Router).url); if (!menus || menus.length <= 0) return of(''); const item = menus[menus.length - 1]; let title; if (item.i18n) title = this.i18nSrv.fanyi(item.i18n); return of(title || item.text); } /** * Set the document title */ setTitle(title) { this.tit$?.unsubscribe(); this.tit$ = of(title) .pipe(switchMap(tit => (tit ? of(tit) : this.getByRoute())), switchMap(tit => (tit ? of(tit) : this.getByMenu())), switchMap(tit => (tit ? of(tit) : this.getByElement())), map(tit => tit || this.default), map(title => (!Array.isArray(title) ? [title] : title)), takeUntilDestroyed(this.destroy$)) .subscribe(titles => { let newTitles = []; if (this._prefix) { newTitles.push(this._prefix); } newTitles.push(...titles.filter(title => !!title)); if (this._suffix) { newTitles.push(this._suffix); } if (this._reverse) { newTitles = newTitles.reverse(); } this.title.setTitle(newTitles.join(this._separator)); }); } /** * Set i18n key of the document title */ setTitleByI18n(key, params) { this.setTitle(this.i18nSrv.fanyi(key, params)); } ngOnDestroy() { this.tit$?.unsubscribe(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: TitleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: TitleService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: TitleService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class I18nPipe { i18n = inject(ALAIN_I18N_TOKEN); transform(key, params) { return this.i18n.fanyi(key, params); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: I18nPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.0", ngImport: i0, type: I18nPipe, isStandalone: true, name: "i18n" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: I18nPipe, decorators: [{ type: Pipe, args: [{ name: 'i18n' }] }] }); class AlainI18NGuardService { i18nSrv = inject(ALAIN_I18N_TOKEN, { optional: true }); cogSrv = inject(AlainConfigService); process(route) { const lang = route.params && route.params[this.cogSrv.get('themeI18n')?.paramNameOfUrlGuard ?? 'i18n']; if (lang != null) { this.i18nSrv?.use(lang); } return of(true); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NGuardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NGuardService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NGuardService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Internationalization guard, automatically recognizes the language in Url and triggers the `ALAIN_I18N_TOKEN.use` method * * 国际化守卫,自动识别Url中的语言,并触发 `ALAIN_I18N_TOKEN.use` 方法 * * ```ts * data: { * path: 'home', * canActivate: [ alainI18nCanActivate ] * } * ``` */ const alainI18nCanActivate = childRoute => inject(AlainI18NGuardService).process(childRoute); /** * Internationalization guard, automatically recognizes the language in Url and triggers the `ALAIN_I18N_TOKEN.use` method * * 国际化守卫,自动识别Url中的语言,并触发 `ALAIN_I18N_TOKEN.use` 方法 * * ```ts * data: { * path: 'home', * canActivateChild: [ alainI18nCanActivateChild ] * } * ``` */ const alainI18nCanActivateChild = route => inject(AlainI18NGuardService).process(route); const CLS_DRAG = 'MODAL-DRAG'; /** * 对话框辅助类 */ class ModalHelper { srv = inject(NzModalService); drag = inject(DragDrop); doc = inject(DOCUMENT); createDragRef(options, wrapCls) { const wrapEl = this.doc.querySelector(wrapCls); const modalEl = wrapEl.firstChild; const handelEl = options.handleCls ? wrapEl.querySelector(options.handleCls) : null; if (handelEl) { handelEl.classList.add(`${CLS_DRAG}-HANDLE`); } return this.drag .createDrag(handelEl ?? modalEl) .withHandles([handelEl ?? modalEl]) .withBoundaryElement(wrapEl) .withRootElement(modalEl); } /** * 构建一个对话框 * * @param comp 组件 * @param params 组件参数 * @param options 额外参数 * * @example * this.modalHelper.create(FormEditComponent, { i }).subscribe(res => this.load()); * // 对于组件的成功&关闭的处理说明 * // 成功,其中 `nzModalRef` 指目标组件在构造函数 `NzModalRef` 变量名 * this.nzModalRef.close(data); * this.nzModalRef.close(); * // 关闭 * this.nzModalRef.destroy(); */ create(comp, params, options) { const isBuildIn = typeof comp === 'string'; options = deepMerge({ size: 'lg', exact: true, includeTabs: false }, isBuildIn && arguments.length === 2 ? params : options); return new Observable((observer) => { const { size, includeTabs, modalOptions, drag, useNzData, focus } = options; let cls = []; let width = ''; if (size) { if (typeof size === 'number') { width = `${size}px`; } else if (['sm', 'md', 'lg', 'xl'].includes(size)) { cls.push(`modal-${size}`); } else { width = size; } } if (includeTabs) { cls.push(`modal-include-tabs`); } if (modalOptions && modalOptions.nzWrapClassName) { cls.push(modalOptions.nzWrapClassName); delete modalOptions.nzWrapClassName; } let dragOptions; let dragWrapCls = `${CLS_DRAG}-${+new Date()}`; let dragRef; if (drag != null && drag !== false) { dragOptions = { handleCls: `.modal-header, .ant-modal-title`, ...(typeof drag === 'object' ? drag : {}) }; cls.push(CLS_DRAG, dragWrapCls); } const mth = isBuildIn ? this.srv[comp] : this.srv.create; const subject = mth.call(this.srv, { nzWrapClassName: cls.join(' '), nzContent: isBuildIn ? undefined : comp, nzWidth: width ? width : undefined, nzFooter: null, nzData: params, nzDraggable: false, ...modalOptions }); // 保留 nzComponentParams 原有风格,但依然可以通过 @Inject(NZ_MODAL_DATA) 获取 if (subject.componentInstance != null && useNzData !== true) { Object.entries(params).forEach(([key, value]) => { const t = subject.componentInstance; const s = t[key]?.[SIGNAL]; if (s != null) { s.value = value; } else { t[key] = value; } }); } subject.afterOpen .pipe(take(1), tap(() => { if (dragOptions != null) { dragRef = this.createDragRef(dragOptions, `.${dragWrapCls}`); } }), filter(() => focus != null), delay(modalOptions?.nzNoAnimation ? 10 : 241)) .subscribe(() => { const btns = subject .getElement() .querySelector('.ant-modal-confirm-btns, .modal-footer') ?.querySelectorAll('.ant-btn'); const btnSize = btns?.length ?? 0; let el = null; if (btnSize === 1) { el = btns[0]; } else if (btnSize > 1) { el = btns[focus === 'ok' ? 1 : 0]; } if (el != null) { el.focus(); el.dataset.focused = focus; } }); subject.afterClose.pipe(take(1)).subscribe((res) => { if (options.exact === true) { if (res != null) { observer.next(res); } } else { observer.next(res); } observer.complete(); dragRef?.dispose(); }); }); } /** * 构建静态框,点击蒙层不允许关闭 * * @param comp 组件 * @param params 组件参数 * @param options 额外参数 * * @example * this.modalHelper.open(FormEditComponent, { i }).subscribe(res => this.load()); * // 对于组件的成功&关闭的处理说明 * // 成功,其中 `nzModalRef` 指目标组件在构造函数 `NzModalRef` 变量名 * this.nzModalRef.close(data); * this.nzModalRef.close(); * // 关闭 * this.nzModalRef.destroy(); */ createStatic(comp, params, options) { const modalOptions = { nzMaskClosable: false, ...(options && options.modalOptions) }; return this.create(comp, params, { ...options, modalOptions }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ModalHelper, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ModalHelper, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ModalHelper, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * 抽屉辅助类 * * **注意:** 构建结果都可被订阅,但永远都不会触发 `observer.error` * * @example * this.drawerHelper.create('Edit', FormEditComponent, { i }).subscribe(res => this.load()); * // 对于组件的成功&关闭的处理说明 * // 成功 * this.NzDrawerRef.close(data); * this.NzDrawerRef.close(true); * // 关闭 * this.NzDrawerRef.close(); * this.NzDrawerRef.close(false); */ class DrawerHelper { srv = inject(NzDrawerService); parentDrawer = inject(DrawerHelper, { optional: true, skipSelf: true }); openDrawersAtThisLevel = []; get openDrawers() { return this.parentDrawer ? this.parentDrawer.openDrawers : this.openDrawersAtThisLevel; } /** * 构建一个抽屉 */ create(title, comp, params, options) { options = deepMerge({ size: 'md', footer: true, footerHeight: 50, exact: true, drawerOptions: { nzPlacement: 'right', nzWrapClassName: '' } }, options); return new Observable((observer) => { const { size, footer, footerHeight, drawerOptions } = options; const defaultOptions = { nzContent: comp, nzContentParams: params, nzTitle: title }; if (typeof size === 'number') { defaultOptions[drawerOptions.nzPlacement === 'top' || drawerOptions.nzPlacement === 'bottom' ? 'nzHeight' : 'nzWidth'] = options.size; } else if (!drawerOptions.nzWidth) { defaultOptions.nzWrapClassName = `${drawerOptions.nzWrapClassName} drawer-${options.size}`.trim(); delete drawerOptions.nzWrapClassName; } if (footer) { // The 24 value is @drawer-body-padding defaultOptions.nzBodyStyle = { 'padding-bottom': `${footerHeight + 24}px` }; } const ref = this.srv.create({ ...defaultOptions, ...drawerOptions }); this.openDrawers.push(ref); const afterClose$ = ref.afterClose.subscribe((res) => { if (options.exact === true) { if (res != null) { observer.next(res); } } else { observer.next(res); } observer.complete(); afterClose$.unsubscribe(); this.close(ref); }); }); } close(ref) { const idx = this.openDrawers.indexOf(ref); if (idx === -1) return; this.openDrawers.splice(idx, 1); } closeAll() { let i = this.openDrawers.length; while (i--) { this.openDrawers[i].close(); } } /** * 构建一个抽屉,点击蒙层不允许关闭 */ static(title, comp, params, options) { const drawerOptions = { nzMaskClosable: false, ...(options && options.drawerOptions) }; return this.create(title, comp, params, { ...options, drawerOptions }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: DrawerHelper, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: DrawerHelper, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: DrawerHelper, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * 封装HttpClient,主要解决: * + 优化HttpClient在参数上便利性 * + 统一实现 loading * + 统一处理时间格式问题 */ class _HttpClient { http = inject(HttpClient); cogSrv = inject(AlainConfigService); cog; constructor() { this.cog = this.cogSrv.merge('themeHttp', { nullValueHandling: 'include', dateValueHandling: 'timestamp' }); } lc = 0; /** * Get whether it's loading * * 获取是否正在加载中 */ get loading() { return this.lc > 0; } /** * Get the currently loading count * * 获取当前加载中的数量 */ get loadingCount() { return this.lc; } parseParams(params) { const newParams = {}; if (params instanceof HttpParams) { return params; } const { nullValueHandling, dateValueHandling } = this.cog; Object.keys(params).forEach(key => { let paramValue = params[key]; // 忽略空值 if (nullValueHandling === 'ignore' && paramValue == null) return; // 将时间转化为:时间戳 (秒) if (paramValue instanceof Date && (dateValueHandling === 'timestamp' || dateValueHandling === 'timestampSecond')) { paramValue = dateValueHandling === 'timestamp' ? paramValue.valueOf() : Math.trunc(paramValue.valueOf() / 1000); } newParams[key] = paramValue; }); return new HttpParams({ fromObject: newParams }); } appliedUrl(url, params) { if (!params) return url; url += ~url.indexOf('?') ? '' : '?'; const arr = []; Object.keys(params).forEach(key => { arr.push(`${key}=${params[key]}`); }); return url + arr.join('&'); } setCount(count) { Promise.resolve(null).then(() => (this.lc = count <= 0 ? 0 : count)); } push() { this.setCount(++this.lc); } pop() { this.setCount(--this.lc); } /** * Clean loading count * * 清空加载中 */ cleanLoading() { this.setCount(0); } get(url, params, options = {}) { return this.request('GET', url, { params, ...options }); } post(url, body, params, options = {}) { return this.request('POST', url, { body, params, ...options }); } delete(url, params, options = {}) { return this.request('DELETE', url, { params, ...options }); } // #endregion // #region jsonp /** * **JSONP Request** * * @param callbackParam CALLBACK值,默认:JSONP_CALLBACK */ jsonp(url, params, callbackParam = 'JSONP_CALLBACK') { return of(null).pipe( // Make sure to always be asynchronous, see issues: https://github.com/ng-alain/ng-alain/issues/1954 delay(0), tap(() => this.push()), switchMap(() => this.http.jsonp(this.appliedUrl(url, params), callbackParam)), finalize(() => this.pop())); } patch(url, body, params, options = {}) { return this.request('PATCH', url, { body, params, ...options }); } put(url, body, params, options = {}) { return this.request('PUT', url, { body, params, ...options }); } form(url, body, params, options = {}) { return this.request('POST', url, { body, params, ...options, headers: { 'content-type': `application/x-www-form-urlencoded` } }); } request(method, url, options = {}) { if (options.params) options.params = this.parseParams(options.params); return of(null).pipe( // Make sure to always be asynchronous, see issues: https://github.com/ng-alain/ng-alain/issues/1954 delay(0), tap(() => this.push()), switchMap(() => this.http.request(method, url, options)), finalize(() => this.pop())); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: _HttpClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: _HttpClient, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: _HttpClient, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * Every http decorator must be based on `BaseAPI`, Like this: * ```ts * \@Injectable() * class DataService extends BaseApi {} * ``` */ class BaseApi { injector = inject(Injector); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseApi, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseApi }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseApi, decorators: [{ type: Injectable }] }); const paramKey = `__api_params`; function setParam(target, key = paramKey) { let params = target[key]; if (typeof params === 'undefined') { params = target[key] = {}; } return params; } /** * 默认基准URL * - 有效范围:类 */ function BaseUrl(url) { return function (target) { const params = setParam(target.prototype); params.baseUrl = url; return target; }; } /** * 默认 `headers` * - 有效范围:类 */ function BaseHeaders(headers) { return function (target) { const params = setParam(target.prototype); params.baseHeaders = headers; return target; }; } function makeParam(paramName) { return function (key) { return function (target, propertyKey, index) { const params = setParam(setParam(target), propertyKey); let tParams = params[paramName]; if (typeof tParams === 'undefined') { tParams = params[paramName] = []; } tParams.push({ key, index }); }; }; } /** * URL路由参数 * - 有效范围:方法参数 */ const Path = makeParam('path'); /** * URL 参数 `QueryString` * - 有效范围:方法参数 */ const Query = makeParam('query'); /** * 参数 `Body` * - 有效范围:方法参数 */ const Body = makeParam('body')(); /** * 参数 `headers` * - 有效范围:方法参数 * - 合并 `BaseHeaders` */ const Headers = makeParam('headers'); /** * Request Payload * - Supported body (like`POST`, `PUT`) as a body data, equivalent to `@Body` * - Not supported body (like `GET`, `DELETE` etc) as a `QueryString` */ const Payload = makeParam('payload')(); function getValidArgs(data, key, args) { if (!data[key] || !Array.isArray(data[key]) || data[key].length <= 0) { return undefined; } return args[data[key][0].index]; } function genBody(data, payload) { if (Array.isArray(data) || Array.isArray(payload)) { return Object.assign([], data, payload); } return { ...data, ...payload }; } function makeMethod(method) { return function (url = '', options) { return (_target, targetKey, descriptor) => { descriptor.value = function (...args) { options = options || {}; const injector = this.injector; const http = injector.get(_HttpClient, null); if (http == null) { throw new TypeError(`Not found '_HttpClient', You can import 'AlainThemeModule' && 'HttpClientModule' in your root module.`); } const baseData = setParam(this); const data = setParam(baseData, targetKey); let requestUrl = url || ''; requestUrl = [baseData.baseUrl || '', requestUrl.startsWith('/') ? requestUrl.substring(1) : requestUrl].join('/'); // fix last split if (requestUrl.length > 1 && requestUrl.endsWith('/')) { requestUrl = requestUrl.substring(0, requestUrl.length - 1); } if (options.acl) { const aclSrv = injector.get(ACLService, null); if (aclSrv && !aclSrv.can(options.acl)) { return throwError(() => ({ url: requestUrl, status: 401, statusText: `From Http Decorator` })); } delete options.acl; } requestUrl = requestUrl.replace(/::/g, '^^'); (data.path || []) .filter(w => typeof args[w.index] !== 'undefined') .forEach((i) => { requestUrl = requestUrl.replace(new RegExp(`:${i.key}`, 'g'), encodeURIComponent(args[i.index])); }); requestUrl = requestUrl.replace(/\^\^/g, `:`); const params = (data.query || []).reduce((p, i) => { p[i.key] = args[i.index]; return p; }, {}); const headers = (data.headers || []).reduce((p, i) => { p[i.key] = args[i.index]; return p; }, {}); if (method === 'FORM') { headers['content-type'] = 'application/x-www-form-urlencoded'; } const payload = getValidArgs(data, 'payload', args); const supportedBody = ['POST', 'PUT', 'PATCH', 'DELETE'].some(v => v === method); return http.request(method, requestUrl, { body: supportedBody ? genBody(getValidArgs(data, 'body', args), payload) : null, params: !sup