UNPKG

@delon/theme

Version:

ng-alain theme system library.

1 lines 275 kB
{"version":3,"file":"theme.mjs","sources":["../../../../packages/theme/src/services/preloader/preloader.ts","../../../../packages/theme/src/services/i18n/i18n.ts","../../../../packages/theme/src/services/menu/menu.service.ts","../../../../packages/theme/src/services/settings/settings.service.ts","../../../../packages/theme/src/services/responsive/responsive.ts","../../../../packages/theme/src/services/rtl/rtl.service.ts","../../../../packages/theme/src/services/title/title.service.ts","../../../../packages/theme/src/services/i18n/i18n.pipe.ts","../../../../packages/theme/src/services/i18n/i18n-url.guard.ts","../../../../packages/theme/src/services/modal/modal.helper.ts","../../../../packages/theme/src/services/drawer/drawer.helper.ts","../../../../packages/theme/src/services/http/http.client.ts","../../../../packages/theme/src/services/http/http.decorator.ts","../../../../packages/theme/src/services/http/http.token.ts","../../../../packages/theme/src/locale/locale.tokens.ts","../../../../packages/theme/src/locale/languages/zh-CN.ts","../../../../packages/theme/src/locale/locale.service.ts","../../../../packages/theme/src/locale/locale.module.ts","../../../../packages/theme/src/locale/languages/en-US.ts","../../../../packages/theme/src/locale/languages/zh-HK.ts","../../../../packages/theme/src/locale/languages/zh-TW.ts","../../../../packages/theme/src/locale/languages/tr-TR.ts","../../../../packages/theme/src/locale/languages/pl-PL.ts","../../../../packages/theme/src/locale/languages/el-GR.ts","../../../../packages/theme/src/locale/languages/ko-KR.ts","../../../../packages/theme/src/locale/languages/hr-HR.ts","../../../../packages/theme/src/locale/languages/ja-JP.ts","../../../../packages/theme/src/locale/languages/sl-SI.ts","../../../../packages/theme/src/locale/languages/fr-FR.ts","../../../../packages/theme/src/locale/languages/es-ES.ts","../../../../packages/theme/src/locale/languages/it-IT.ts","../../../../packages/theme/src/locale/languages/vi-VN.ts","../../../../packages/theme/src/locale/languages/ar-SA.ts","../../../../packages/theme/src/locale/languages/id-ID.ts","../../../../packages/theme/src/locale/languages/km-KH.ts","../../../../packages/theme/src/locale/languages/ms-MY.ts","../../../../packages/theme/src/locale/languages/th-TH.ts","../../../../packages/theme/src/pipes/date/date.pipe.ts","../../../../packages/theme/src/pipes/keys/keys.pipe.ts","../../../../packages/theme/src/pipes/yn/yn.pipe.ts","../../../../packages/theme/src/pipes/safe/html.pipe.ts","../../../../packages/theme/src/pipes/safe/url.pipe.ts","../../../../packages/theme/src/theme.module.ts","../../../../packages/theme/src/provide.ts","../../../../packages/theme/src/router/optional-preloader.ts","../../../../packages/theme/src/version.ts","../../../../packages/theme/theme.ts"],"sourcesContent":["import { DOCUMENT, isPlatformServer } from '@angular/common';\nimport { PLATFORM_ID, inject } from '@angular/core';\n\nexport function stepPreloader(): () => void {\n const doc: Document = inject(DOCUMENT);\n const ssr = isPlatformServer(inject(PLATFORM_ID));\n if (ssr) {\n return () => {};\n }\n const body = doc.querySelector<HTMLBodyElement>('body')!;\n body.style.overflow = 'hidden';\n let done = false;\n\n return () => {\n if (done) return;\n\n done = true;\n const preloader = doc.querySelector<HTMLElement>('.preloader');\n if (preloader == null) return;\n\n const CLS = 'preloader-hidden';\n preloader.addEventListener('transitionend', () => {\n preloader.className = CLS;\n });\n preloader.className += ` ${CLS}-add ${CLS}-add-active`;\n body.style.overflow = '';\n };\n}\n","import { inject, Injectable, InjectionToken } from '@angular/core';\nimport { BehaviorSubject, Observable, filter } from 'rxjs';\n\nimport { AlainConfigService, AlainThemeI18nConfig } from '@delon/util/config';\nimport type { NzSafeAny } from 'ng-zorro-antd/core/types';\n\nexport interface AlainI18NService {\n [key: string]: NzSafeAny;\n\n /**\n * Call `use` to trigger change notification\n *\n * 调用 `use` 触发变更通知\n */\n readonly change: Observable<string>;\n\n /**\n * Get the default language\n *\n * 获取默认语言\n */\n readonly defaultLang: string;\n\n /**\n * Get current language\n *\n * 获取当前语言\n */\n readonly currentLang: string;\n\n /**\n * Change language\n *\n * 变更语言\n */\n use(lang: string, data?: Record<string, unknown>): void;\n\n /**\n * Return to the current language list\n *\n * 返回当前语言列表\n */\n getLangs(): NzSafeAny[];\n\n /**\n * Translate 翻译\n *\n * @param params 模板所需要的参数对象\n */\n fanyi(path: string, params?: unknown | unknown[]): string;\n}\n\nexport const ALAIN_I18N_TOKEN = new InjectionToken<AlainI18NService>('alainI18nToken', {\n providedIn: 'root',\n factory: () => new AlainI18NServiceFake()\n});\n\n@Injectable()\nexport abstract class AlainI18nBaseService implements AlainI18NService {\n protected readonly cogSrv = inject(AlainConfigService);\n\n private cog: AlainThemeI18nConfig;\n protected _change$ = new BehaviorSubject<string | null>(null);\n protected _currentLang: string = '';\n protected _defaultLang: string = '';\n protected _data: Record<string, string> = {};\n get change(): Observable<string> {\n return this._change$.asObservable().pipe(filter(w => w != null)) as Observable<string>;\n }\n get defaultLang(): string {\n return this._defaultLang;\n }\n get currentLang(): string {\n return this._currentLang;\n }\n get data(): Record<string, string> {\n return this._data;\n }\n\n constructor() {\n this.cog = this.cogSrv.merge('themeI18n', {\n interpolation: ['{{', '}}']\n })!;\n }\n\n /**\n * Merge the data into the current language data.\n */\n mergeData(data?: Record<string, unknown>): void {\n if (!data) return;\n const flatData = this.flatData(data, []);\n this._data = { ...this._data, ...flatData };\n }\n\n /**\n * Flattened data source\n *\n * @example\n * {\n * \"name\": \"Name\",\n * \"sys\": {\n * \"\": \"System\",\n * \"title\": \"Title\"\n * }\n * }\n * =>\n * {\n * \"name\": \"Name\",\n * \"sys\": \"System\",\n * \"sys.title\": \"Title\"\n * }\n */\n flatData(data: Record<string, unknown>, parentKey: string[]): Record<string, string> {\n const res: Record<string, string> = {};\n for (const key of Object.keys(data)) {\n const value = data[key];\n if (typeof value === 'object') {\n const child = this.flatData(value as Record<string, unknown>, parentKey.concat(key));\n Object.keys(child).forEach(childKey => (res[childKey] = child[childKey]));\n } else {\n res[(key ? parentKey.concat(key) : parentKey).join('.')] = `${value}`;\n }\n }\n return res;\n }\n\n abstract use(lang: string, data?: Record<string, unknown>): void;\n\n abstract getLangs(): NzSafeAny[];\n\n fanyi(path: string, params?: unknown | unknown[]): string {\n let content = this._data[path] ?? '';\n if (!content) return path;\n\n if (!params) return content;\n\n if (typeof params === 'object') {\n const interpolation = this.cog.interpolation!;\n const objParams = params as Record<string, unknown>;\n Object.keys(objParams).forEach(key => {\n content = content.replace(\n new RegExp(`${interpolation[0]}\\\\s?${key}\\\\s?${interpolation[1]}`, 'g'),\n `${objParams[key]}`\n );\n });\n }\n\n (Array.isArray(params) ? params : [params]).forEach(\n (item, index) => (content = content.replace(new RegExp(`\\\\{\\\\s?${index}\\\\s?\\\\}`, 'g'), `${item}`))\n );\n return content;\n }\n}\n\n@Injectable({ providedIn: 'root' })\nexport class AlainI18NServiceFake extends AlainI18nBaseService {\n use(lang: string, data: Record<string, unknown>): void {\n this._data = this.flatData(data ?? {}, []);\n this._currentLang = lang;\n this._change$.next(lang);\n }\n\n getLangs(): NzSafeAny[] {\n return [];\n }\n}\n","import { Injectable, OnDestroy, inject } from '@angular/core';\nimport { BehaviorSubject, Observable, Subscription, share } from 'rxjs';\n\nimport { ACLService } from '@delon/acl';\nimport type { NzSafeAny } from 'ng-zorro-antd/core/types';\n\nimport { Menu, MenuIcon, MenuInner } from './interface';\nimport { ALAIN_I18N_TOKEN } from '../i18n/i18n';\n\n/**\n * 菜单服务,[在线文档](https://ng-alain.com/theme/menu)\n */\n@Injectable({ providedIn: 'root' })\nexport class MenuService implements OnDestroy {\n private readonly i18nSrv = inject(ALAIN_I18N_TOKEN);\n private readonly aclService = inject(ACLService);\n private _change$: BehaviorSubject<Menu[]> = new BehaviorSubject<Menu[]>([]);\n private i18n$?: Subscription;\n private data: Menu[] = [];\n /**\n * 是否完全受控菜单打开状态,默认:`false`\n */\n openStrictly = false;\n\n constructor() {\n this.i18n$ = this.i18nSrv.change.subscribe(() => this.resume());\n }\n\n get change(): Observable<Menu[]> {\n return this._change$.pipe(share());\n }\n\n get menus(): Menu[] {\n return this.data;\n }\n\n /**\n * Returns a default menu link\n *\n * 返回一个默认跳转的菜单链接\n */\n getDefaultRedirect(opt: { redirectUrl?: string } = {}): string | null | undefined {\n let ret: string | null | undefined;\n this.visit(this.menus, (item: MenuInner) => {\n if (typeof item.link !== 'string' || item.link.length <= 0 || !item._aclResult || item._hidden === true) {\n return;\n }\n if (ret == null || ret.length <= 0 || item.link == opt?.redirectUrl) {\n ret = item.link;\n }\n });\n return ret;\n }\n\n visit<T extends Menu = Menu>(data: T[], callback: (item: T, parentMenum: T | null, depth?: number) => void): void;\n visit(data: Menu[], callback: (item: Menu, parentMenum: Menu | null, depth?: number) => void): void;\n visit(data: Menu[], callback: (item: Menu, parentMenum: Menu | null, depth?: number) => void): void {\n const inFn = (list: Menu[], parentMenu: Menu | null, depth: number): void => {\n for (const item of list) {\n callback(item, parentMenu, depth);\n if (item.children && item.children.length > 0) {\n inFn(item.children, item, depth + 1);\n } else {\n item.children = [];\n }\n }\n };\n\n inFn(data, null, 0);\n }\n\n add(items: Menu[]): void {\n this.data = items;\n this.resume();\n }\n\n private fixItem(item: MenuInner): void {\n item._aclResult = true;\n\n if (!item.render_type) item.render_type = 'item';\n if (!item.link) item.link = '';\n if (!item.externalLink) item.externalLink = '';\n\n // badge\n if (item.badge) {\n if (item.badgeDot !== true) {\n item.badgeDot = false;\n }\n if (!item.badgeStatus) {\n item.badgeStatus = 'error';\n }\n }\n\n if (!Array.isArray(item.children)) {\n item.children = [];\n }\n\n // icon\n if (typeof item.icon === 'string') {\n let type = 'class';\n let value = item.icon;\n // compatible `anticon anticon-user`\n if (~item.icon.indexOf(`anticon-`)) {\n type = 'icon';\n value = value.split('-').slice(1).join('-');\n } else if (/^https?:\\/\\//.test(item.icon)) {\n type = 'img';\n }\n item.icon = { type, value } as NzSafeAny;\n }\n if (item.icon != null) {\n item.icon = { theme: 'outline', spin: false, ...(item.icon as MenuIcon) };\n }\n\n item.text = item.i18n ? this.i18nSrv.fanyi(item.i18n) : item.text;\n\n // group\n item.group = item.group !== false;\n\n // hidden\n item._hidden = typeof item.hide === 'undefined' ? false : item.hide;\n\n // disabled\n item.disabled = typeof item.disabled === 'undefined' ? false : item.disabled;\n\n // acl\n item._aclResult = item.acl ? this.aclService.can(item.acl) : true;\n\n item.open = item.open != null ? item.open : false;\n }\n\n /**\n * 重置菜单,可能I18N、用户权限变动时需要调用刷新\n */\n resume<T extends Menu = Menu>(callback?: (item: T, parentMenum: T | null, depth?: number) => void): void;\n resume(callback?: (item: Menu, parentMenum: Menu | null, depth?: number) => void): void;\n resume(callback?: (item: Menu, parentMenum: Menu | null, depth?: number) => void): void {\n let i = 1;\n const shortcuts: Menu[] = [];\n this.visit(this.data, (item: MenuInner, parent, depth) => {\n item._id = i++;\n item._parent = parent;\n item._depth = depth;\n this.fixItem(item);\n\n // shortcut\n if (parent && item.shortcut === true && parent.shortcutRoot !== true) {\n shortcuts.push(item);\n }\n\n if (callback) callback(item, parent, depth);\n });\n\n this.loadShortcut(shortcuts);\n this._change$.next(this.data);\n }\n\n /**\n * 加载快捷菜单,加载位置规则如下:\n * 1、统一在下标0的节点下(即【主导航】节点下方)\n * 1、若 children 存在 【shortcutRoot: true】则最优先【推荐】这种方式\n * 2、否则查找带有【dashboard】字样链接,若存在则在此菜单的下方创建快捷入口\n * 3、否则放在0节点位置\n */\n private loadShortcut(shortcuts: MenuInner[]): void {\n if (shortcuts.length === 0 || this.data.length === 0) {\n return;\n }\n\n const ls = this.data[0].children as MenuInner[];\n let pos = ls.findIndex(w => w.shortcutRoot === true);\n if (pos === -1) {\n pos = ls.findIndex(w => w.link!.includes('dashboard'));\n pos = (pos !== -1 ? pos : -1) + 1;\n const shortcutMenu = {\n text: '快捷菜单',\n i18n: 'shortcut',\n icon: 'icon-rocket',\n children: []\n } as MenuInner;\n this.data[0].children!.splice(pos, 0, shortcutMenu);\n }\n let _data = this.data[0].children![pos];\n if (_data.i18n) _data.text = this.i18nSrv.fanyi(_data.i18n);\n _data = Object.assign(_data, {\n shortcutRoot: true,\n _id: -1,\n _parent: null,\n _depth: 1\n } as MenuInner);\n _data.children = shortcuts.map(i => {\n i._depth = 2;\n i._parent = _data;\n return i;\n });\n }\n\n /**\n * 清空菜单\n */\n clear(): void {\n this.data = [];\n this._change$.next(this.data);\n }\n\n /**\n * Use `url` or `key` to find menus\n *\n * 利用 `url` 或 `key` 查找菜单\n */\n find(options: {\n key?: string | null;\n url?: string | null;\n recursive?: boolean | null;\n /**\n * When the callback returns a Boolean type, it means the custom validation result\n *\n * 当回调返回一个布尔类型时,表示自定义校验结果\n */\n cb?: ((i: Menu) => boolean | null) | null;\n /**\n * Use the current menu data by default\n *\n * 默认使用当前菜单数据\n */\n data?: Menu[] | null;\n /**\n * Whether to ignore hide items, default: `false`\n *\n * 是否忽略隐藏的项,默认:`false`\n */\n ignoreHide?: boolean;\n /**\n * Whether to return the last one, default: `false`\n *\n * 是否返回最后一个,默认:`false`\n */\n last?: boolean;\n }): Menu | null {\n const opt = { recursive: false, ignoreHide: false, last: false, ...options };\n if (opt.key != null) {\n return this.getItem(opt.key);\n }\n\n let url = opt.url;\n\n let item: Menu | null = null;\n\n while (!item && url) {\n this.visit(opt.data ?? this.data, i => {\n if (!opt.last && item != null) {\n return;\n }\n if (opt.ignoreHide && i.hide) {\n return;\n }\n if (opt.cb) {\n const res = opt.cb(i);\n if (typeof res === 'boolean' && res) {\n item = i;\n }\n }\n if (i.link != null && i.link === url) {\n item = i;\n }\n });\n\n if (!opt.recursive) break;\n\n if (/[?;]/g.test(url)) {\n url = url.split(/[?;]/g)[0];\n } else {\n url = url.split('/').slice(0, -1).join('/');\n }\n }\n\n return item;\n }\n\n /**\n * 根据url获取菜单列表\n * - 若 `recursive: true` 则会自动向上递归查找\n * - 菜单数据源包含 `/ware`,则 `/ware/1` 也视为 `/ware` 项\n */\n getPathByUrl(url: string, recursive: boolean = false): Menu[] {\n const ret: Menu[] = [];\n let item = this.find({ url, recursive }) as MenuInner;\n\n if (!item) return ret;\n\n do {\n ret.splice(0, 0, item);\n item = item._parent!;\n } while (item);\n\n return ret;\n }\n\n /**\n * Get menu based on `key`\n */\n getItem(key: string): Menu | null {\n let res: Menu | null = null;\n this.visit(this.data, item => {\n if (res == null && item.key === key) {\n res = item;\n }\n });\n return res;\n }\n\n /**\n * Set menu based on `key`\n */\n setItem(key: string | Menu, value: Menu, options?: { emit?: boolean }): void {\n const item = typeof key === 'string' ? this.getItem(key) : key;\n if (item == null) return;\n\n Object.keys(value).forEach(k => {\n item[k] = value[k];\n });\n this.fixItem(item);\n\n if (options?.emit !== false) this._change$.next(this.data);\n }\n\n /**\n * Open menu based on `key` or menu object\n */\n open(keyOrItem: string | Menu | null, options?: { emit?: boolean }): void {\n let item = typeof keyOrItem === 'string' ? this.find({ key: keyOrItem }) : keyOrItem;\n if (item == null) return;\n\n this.visit(this.menus, (i: MenuInner) => {\n i._selected = false;\n if (!this.openStrictly) i.open = false;\n });\n\n do {\n item._selected = true;\n item.open = true;\n item = item._parent;\n } while (item);\n if (options?.emit !== false) this._change$.next(this.data);\n }\n\n openAll(status?: boolean): void {\n this.toggleOpen(null, { allStatus: status });\n }\n\n toggleOpen(keyOrItem: string | Menu | null, options?: { allStatus?: boolean; emit?: boolean }): void {\n let item = typeof keyOrItem === 'string' ? this.find({ key: keyOrItem }) : keyOrItem;\n if (item == null) {\n this.visit(this.menus, (i: MenuInner) => {\n i._selected = false;\n i.open = options?.allStatus === true;\n });\n } else {\n if (!this.openStrictly) {\n this.visit(this.menus, (i: MenuInner) => {\n if (i !== item) i.open = false;\n });\n let pItem = item._parent;\n while (pItem) {\n pItem.open = true;\n pItem = pItem._parent;\n }\n }\n item.open = !item.open;\n }\n if (options?.emit !== false) this._change$.next(this.data);\n }\n\n ngOnDestroy(): void {\n this._change$.unsubscribe();\n this.i18n$?.unsubscribe();\n }\n}\n","import { Platform } from '@angular/cdk/platform';\nimport { Injectable, InjectionToken, Provider, inject, signal } from '@angular/core';\nimport { Observable, Subject } from 'rxjs';\n\nimport type { NzSafeAny } from 'ng-zorro-antd/core/types';\n\nimport { App, Layout, SettingsNotify, User } from './types';\n\nexport interface SettingsKeys {\n /** Layout data specifies the stored key, default: `layout` */\n layout: string;\n /** User data specifies the stored key, default: `user` */\n user: string;\n /** App data specifies the stored key, default: `app` */\n app: string;\n}\n\nexport const ALAIN_SETTING_KEYS = new InjectionToken<SettingsKeys>('ALAIN_SETTING_KEYS');\nexport const ALAIN_SETTING_DEFAULT: Provider = {\n provide: ALAIN_SETTING_KEYS,\n useValue: {\n layout: 'layout',\n user: 'user',\n app: 'app'\n }\n};\n\n@Injectable({ providedIn: 'root' })\nexport class SettingsService<L extends Layout = Layout, U extends User = User, A extends App = App> {\n private readonly KEYS = inject(ALAIN_SETTING_KEYS);\n private readonly platform = inject(Platform);\n\n private notify$ = new Subject<SettingsNotify>();\n readonly appSignal = signal<A>({\n year: new Date().getFullYear(),\n ...this.getData(this.KEYS.app)\n });\n readonly userSignal = signal<U>({ ...this.getData(this.KEYS.user) });\n readonly layoutSignal = signal<L>({\n fixed: true,\n collapsed: false,\n boxed: false,\n lang: null,\n ...this.getData(this.KEYS.layout)\n });\n\n getData(key: string): NzSafeAny {\n if (!this.platform.isBrowser) {\n return null;\n }\n return JSON.parse(localStorage.getItem(key) ?? 'null') ?? null;\n }\n\n setData(key: string, value: NzSafeAny): void {\n if (!this.platform.isBrowser) {\n return;\n }\n localStorage.setItem(key, JSON.stringify(value));\n }\n\n get layout(): L {\n return this.layoutSignal() as L;\n }\n\n get app(): A {\n return this.appSignal() as A;\n }\n\n get user(): U {\n return this.userSignal() as U;\n }\n\n get notify(): Observable<SettingsNotify> {\n return this.notify$.asObservable();\n }\n\n setLayout<T extends Layout = Layout>(name: T, value?: NzSafeAny): boolean;\n setLayout(name: string | L, value?: NzSafeAny): boolean;\n setLayout(name: string | L, value?: NzSafeAny): boolean {\n this.layoutSignal.update(l => {\n if (typeof name === 'string') {\n (l as Layout)[name] = value;\n return { ...l };\n }\n return { ...name };\n });\n this.setData(this.KEYS.layout, this.layout);\n this.notify$.next({ type: 'layout', name, value } as NzSafeAny);\n return true;\n }\n getLayout<T>(): T {\n return this.layout as unknown as T;\n }\n\n setApp<T extends App = App>(value: T): void;\n setApp(value: A): void;\n setApp(value: A): void {\n this.appSignal.set(value);\n this.setData(this.KEYS.app, value);\n this.notify$.next({ type: 'app', value });\n }\n getApp<T>(): T {\n return this.app as unknown as T;\n }\n\n setUser<T extends User = User>(value: T): void;\n setUser(value: U): void;\n setUser(value: U): void {\n this.userSignal.set(value);\n this.setData(this.KEYS.user, value);\n this.notify$.next({ type: 'user', value });\n }\n getUser<T>(): T {\n return this.user as unknown as T;\n }\n}\n","import { inject, Injectable } from '@angular/core';\n\nimport { AlainConfigService, AlainThemeResponsiveConfig } from '@delon/util/config';\n\nexport const REP_MAX = 6;\nexport const SPAN_MAX = 24;\n\nexport type REP_TYPE = 1 | 2 | 3 | 4 | 5 | 6;\n\n@Injectable({ providedIn: 'root' })\nexport class ResponsiveService {\n private readonly cogSrv = inject(AlainConfigService);\n private cog: AlainThemeResponsiveConfig;\n constructor() {\n this.cog = this.cogSrv.merge('themeResponsive', {\n rules: {\n 1: { xs: 24 },\n 2: { xs: 24, sm: 12 },\n 3: { xs: 24, sm: 12, md: 8 },\n 4: { xs: 24, sm: 12, md: 8, lg: 6 },\n 5: { xs: 24, sm: 12, md: 8, lg: 6, xl: 4 },\n 6: { xs: 24, sm: 12, md: 8, lg: 6, xl: 4, xxl: 2 }\n }\n })!;\n if (\n Object.keys(this.cog.rules)\n .map(i => +i)\n .some((i: number) => i < 1 || i > REP_MAX)\n ) {\n throw new Error(`[theme] the responseive rule index value range must be 1-${REP_MAX}`);\n }\n }\n\n genCls(count: number, defaultCol: number = 1): string[] {\n const rule = { ...this.cog.rules[count > REP_MAX ? REP_MAX : Math.max(count, 1)] };\n const antColClass = 'ant-col';\n\n const itemMaxSpan = SPAN_MAX / defaultCol;\n const paddingSpan = (value: number | undefined): number | undefined => {\n if (value == null || defaultCol <= 1 || count >= defaultCol) return value;\n return Math.max(value, count * itemMaxSpan);\n };\n const clsMap = [`${antColClass}-xs-${paddingSpan(rule.xs)}`];\n if (rule.sm) clsMap.push(`${antColClass}-sm-${paddingSpan(rule.sm)}`);\n if (rule.md) clsMap.push(`${antColClass}-md-${paddingSpan(rule.md)}`);\n if (rule.lg) clsMap.push(`${antColClass}-lg-${paddingSpan(rule.lg)}`);\n if (rule.xl) clsMap.push(`${antColClass}-xl-${paddingSpan(rule.xl)}`);\n if (rule.xxl) clsMap.push(`${antColClass}-xxl-${paddingSpan(rule.xxl)}`);\n return clsMap;\n }\n}\n","import { Direction, Directionality } from '@angular/cdk/bidi';\nimport { Platform } from '@angular/cdk/platform';\nimport { DOCUMENT } from '@angular/common';\nimport { Injectable, inject } from '@angular/core';\nimport { Observable, filter, map } from 'rxjs';\n\nimport { AlainConfigService } from '@delon/util/config';\nimport { NzConfigService } from 'ng-zorro-antd/core/config';\nimport type { NzSafeAny } from 'ng-zorro-antd/core/types';\n\nimport { SettingsService } from '../settings/settings.service';\n\nexport const HTML_DIR = 'dir';\nexport const RTL_DIRECTION = 'direction';\nexport const RTL_NZ_COMPONENTS = ['modal', 'drawer', 'message', 'notification', 'image'];\nexport const RTL_DELON_COMPONENTS = ['loading', 'onboarding'];\nexport const LTR = 'ltr';\nexport const RTL = 'rtl';\n\n@Injectable({ providedIn: 'root' })\nexport class RTLService {\n private readonly d = inject(Directionality);\n private readonly nz = inject(NzConfigService);\n private readonly delon = inject(AlainConfigService);\n private readonly platform = inject(Platform);\n private readonly doc = inject(DOCUMENT);\n private readonly srv = inject(SettingsService);\n\n private _dir: Direction = LTR;\n /**\n * Get or Set the current text direction\n *\n * 获取或设置当前文字方向\n */\n get dir(): Direction {\n return this._dir;\n }\n set dir(value: Direction) {\n this._dir = value;\n this.updateLibConfig();\n this.updateHtml();\n // Should be wait inited\n Promise.resolve().then(() => {\n this.d.valueSignal.set(value);\n this.d.change.emit(value);\n this.srv.setLayout(RTL_DIRECTION, value);\n });\n }\n\n /**\n * Get the next text direction\n *\n * 获取下一次文字方向\n */\n get nextDir(): Direction {\n return this.dir === LTR ? RTL : LTR;\n }\n\n /**\n * Subscription change notification\n *\n * 订阅变更通知\n */\n get change(): Observable<Direction> {\n return this.srv.notify.pipe(\n filter(w => w.name === RTL_DIRECTION),\n map(v => v.value)\n );\n }\n\n constructor() {\n this.dir = this.srv.layout.direction === RTL ? RTL : LTR;\n }\n\n /**\n * Toggle text direction\n *\n * 切换文字方向\n */\n toggle(): void {\n this.dir = this.nextDir;\n }\n\n private updateHtml(): void {\n if (!this.platform.isBrowser) {\n return;\n }\n const htmlEl = this.doc.querySelector('html') as HTMLElement;\n if (htmlEl) {\n const dir = this.dir;\n htmlEl.style.direction = dir;\n htmlEl.classList.remove(RTL, LTR);\n htmlEl.classList.add(dir);\n htmlEl.setAttribute(HTML_DIR, dir);\n }\n }\n\n private updateLibConfig(): void {\n RTL_NZ_COMPONENTS.forEach(name => {\n this.nz.set(name as NzSafeAny, { nzDirection: this.dir });\n });\n RTL_DELON_COMPONENTS.forEach(name => {\n this.delon.set(name as NzSafeAny, { direction: this.dir });\n });\n }\n}\n","import { DOCUMENT } from '@angular/common';\nimport { DestroyRef, Injectable, Injector, OnDestroy, inject } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { Title } from '@angular/platform-browser';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { Observable, of, map, delay, isObservable, switchMap, Subscription } from 'rxjs';\n\nimport { ALAIN_I18N_TOKEN } from '../i18n/i18n';\nimport { MenuService } from '../menu/menu.service';\n\nexport interface RouteTitle {\n title?: string | Observable<string>;\n titleI18n?: string;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class TitleService implements OnDestroy {\n private destroy$ = inject(DestroyRef);\n private _prefix: string = '';\n private _suffix: string = '';\n private _separator: string = ' - ';\n private _reverse: boolean = false;\n private tit$?: Subscription;\n\n readonly DELAY_TIME = 25;\n\n private readonly doc = inject(DOCUMENT);\n private readonly injector = inject(Injector);\n private readonly title = inject(Title);\n private readonly menuSrv = inject(MenuService);\n private readonly i18nSrv = inject(ALAIN_I18N_TOKEN);\n\n constructor() {\n this.i18nSrv.change.pipe(takeUntilDestroyed()).subscribe(() => this.setTitle());\n }\n\n /**\n * Set separator\n *\n * 设置分隔符\n */\n set separator(value: string) {\n this._separator = value;\n }\n\n /**\n * Set prefix\n *\n * 设置前缀\n */\n set prefix(value: string) {\n this._prefix = value;\n }\n\n /**\n * Set suffix\n *\n * 设置后缀\n */\n set suffix(value: string) {\n this._suffix = value;\n }\n\n /**\n * Set whether to reverse\n *\n * 设置是否反转\n */\n set reverse(value: boolean) {\n this._reverse = value;\n }\n\n /**\n * Set the default CSS selector string\n *\n * 设置默认CSS选择器字符串\n */\n selector?: string | null;\n\n /**\n * Set default title name\n *\n * 设置默认标题名\n */\n default = `Not Page Name`;\n\n private getByElement(): Observable<string> {\n return of('').pipe(\n delay(this.DELAY_TIME),\n map(() => {\n const el = ((this.selector != null ? this.doc.querySelector(this.selector) : null) ||\n this.doc.querySelector('.alain-default__content-title h1') ||\n this.doc.querySelector('.page-header__title')) as HTMLElement;\n if (el) {\n let text = '';\n el.childNodes.forEach(val => {\n if (!text && val.nodeType === 3) {\n text = val.textContent!.trim();\n }\n });\n return text || el.firstChild!.textContent!.trim();\n }\n return '';\n })\n );\n }\n\n private getByRoute(): Observable<string> {\n let next = this.injector.get(ActivatedRoute);\n while (next.firstChild) next = next.firstChild;\n const data: RouteTitle = (next.snapshot && next.snapshot.data) ?? {};\n if (data.titleI18n) data.title = this.i18nSrv.fanyi(data.titleI18n);\n return isObservable(data.title) ? data.title : of(data.title!);\n }\n\n private getByMenu(): Observable<string> {\n const menus = this.menuSrv.getPathByUrl(this.injector.get<Router>(Router).url);\n if (!menus || menus.length <= 0) return of('');\n\n const item = menus[menus.length - 1];\n let title;\n if (item.i18n) title = this.i18nSrv.fanyi(item.i18n);\n return of(title ?? item.text!);\n }\n\n /**\n * Set the document title\n */\n setTitle(title?: string | string[]): void {\n this.tit$?.unsubscribe();\n this.tit$ = of(title)\n .pipe(\n switchMap(tit => (tit ? of(tit) : this.getByRoute())),\n switchMap(tit => (tit ? of(tit) : this.getByMenu())),\n switchMap(tit => (tit ? of(tit) : this.getByElement())),\n map(tit => tit || this.default),\n map(title => (!Array.isArray(title) ? [title] : title)),\n takeUntilDestroyed(this.destroy$)\n )\n .subscribe(titles => {\n let newTitles: string[] = [];\n if (this._prefix) {\n newTitles.push(this._prefix);\n }\n newTitles.push(...titles.filter(title => !!title));\n if (this._suffix) {\n newTitles.push(this._suffix);\n }\n if (this._reverse) {\n newTitles = newTitles.reverse();\n }\n this.title.setTitle(newTitles.join(this._separator));\n });\n }\n\n /**\n * Set i18n key of the document title\n */\n setTitleByI18n(key: string, params?: unknown): void {\n this.setTitle(this.i18nSrv.fanyi(key, params));\n }\n\n ngOnDestroy(): void {\n this.tit$?.unsubscribe();\n }\n}\n","import { Pipe, PipeTransform, inject } from '@angular/core';\n\nimport { ALAIN_I18N_TOKEN } from './i18n';\n\n@Pipe({ name: 'i18n' })\nexport class I18nPipe implements PipeTransform {\n private readonly i18n = inject(ALAIN_I18N_TOKEN);\n\n transform(key: string, params?: unknown | unknown[]): string {\n return this.i18n.fanyi(key, params);\n }\n}\n","import { Injectable, inject } from '@angular/core';\nimport { ActivatedRouteSnapshot, CanActivateChildFn, CanActivateFn } from '@angular/router';\nimport { Observable, of } from 'rxjs';\n\nimport { AlainConfigService } from '@delon/util/config';\n\nimport { ALAIN_I18N_TOKEN } from './i18n';\n\n@Injectable({ providedIn: 'root' })\nexport class AlainI18NGuardService {\n private readonly i18nSrv = inject(ALAIN_I18N_TOKEN, { optional: true });\n private readonly cogSrv = inject(AlainConfigService);\n\n process(route: ActivatedRouteSnapshot): Observable<boolean> {\n const lang = route.params && route.params[this.cogSrv.get('themeI18n')?.paramNameOfUrlGuard ?? 'i18n'];\n if (lang != null) {\n this.i18nSrv?.use(lang);\n }\n return of(true);\n }\n}\n\n/**\n * Internationalization guard, automatically recognizes the language in Url and triggers the `ALAIN_I18N_TOKEN.use` method\n *\n * 国际化守卫,自动识别Url中的语言,并触发 `ALAIN_I18N_TOKEN.use` 方法\n *\n * ```ts\n * data: {\n * path: 'home',\n * canActivate: [ alainI18nCanActivate ]\n * }\n * ```\n */\nexport const alainI18nCanActivate: CanActivateFn = childRoute => inject(AlainI18NGuardService).process(childRoute);\n\n/**\n * Internationalization guard, automatically recognizes the language in Url and triggers the `ALAIN_I18N_TOKEN.use` method\n *\n * 国际化守卫,自动识别Url中的语言,并触发 `ALAIN_I18N_TOKEN.use` 方法\n *\n * ```ts\n * data: {\n * path: 'home',\n * canActivateChild: [ alainI18nCanActivateChild ]\n * }\n * ```\n */\nexport const alainI18nCanActivateChild: CanActivateChildFn = route => inject(AlainI18NGuardService).process(route);\n","import { createDragRef, DragRef } from '@angular/cdk/drag-drop';\nimport { DOCUMENT } from '@angular/common';\nimport { Injectable, Injector, TemplateRef, Type, inject } from '@angular/core';\nimport { SIGNAL, SignalNode } from '@angular/core/primitives/signals';\nimport { Observable, Observer, delay, take, tap } from 'rxjs';\n\nimport { deepMerge } from '@delon/util/other';\nimport type { NzSafeAny } from 'ng-zorro-antd/core/types';\nimport { ModalOptions, NzModalService } from 'ng-zorro-antd/modal';\n\nconst CLS_DRAG = 'MODAL-DRAG';\n\nexport interface ModalHelperOptions {\n /** 大小;例如:lg、600、80%,默认:`lg` */\n size?: 'sm' | 'md' | 'lg' | 'xl' | '' | number | string;\n /** 对话框 [ModalOptions](https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/components/modal/modal-types.ts) 参数 */\n modalOptions?: ModalOptions;\n /** 是否精准(默认:`true`),若返回值非空值(`null`或`undefined`)视为成功,否则视为错误 */\n exact?: boolean;\n /** 是否包裹标签页,修复模态包含标签间距问题 */\n includeTabs?: boolean;\n /**\n * 是否支持拖动,默认是通过标题来触发\n */\n drag?: ModalHelperDragOptions | boolean;\n /**\n * 是否强制使用 `nzData` 传递参数,若为 `false` 表示参数会直接映射到组件实例中,其他值只能通过 `NZ_MODAL_DATA` 的方式来获取参数,默认:`false`\n */\n useNzData?: boolean;\n\n /**\n * 设置焦点按钮\n */\n focus?: 'ok' | 'cancel';\n}\n\nexport interface ModalHelperDragOptions {\n /**\n * 指定拖地区域的类名,若指定为 `null` 时表示整个对话框,默认:`.modal-header, .ant-modal-title`\n */\n handleCls?: string | null;\n}\n\n/**\n * 对话框辅助类\n */\n@Injectable({ providedIn: 'root' })\nexport class ModalHelper {\n private readonly srv = inject(NzModalService);\n private readonly injector = inject(Injector);\n private readonly doc = inject(DOCUMENT);\n\n private buildDrag(options: ModalHelperDragOptions, wrapCls: string): DragRef {\n const wrapEl = this.doc.querySelector(wrapCls) as HTMLDivElement;\n const modalEl = wrapEl.firstChild as HTMLDivElement;\n const handelEl = options.handleCls ? wrapEl.querySelector<HTMLDivElement>(options.handleCls) : null;\n if (handelEl) {\n handelEl.classList.add(`${CLS_DRAG}-HANDLE`);\n }\n\n return createDragRef(this.injector, handelEl ?? modalEl)\n .withHandles([handelEl ?? modalEl])\n .withBoundaryElement(wrapEl)\n .withRootElement(modalEl);\n }\n\n /**\n * 构建一个对话框\n *\n * @param comp 组件\n * @param params 组件参数\n * @param options 额外参数\n *\n * @example\n * this.modalHelper.create(FormEditComponent, { i }).subscribe(res => this.load());\n * // 对于组件的成功&关闭的处理说明\n * // 成功,其中 `nzModalRef` 指目标组件在构造函数 `NzModalRef` 变量名\n * this.nzModalRef.close(data);\n * this.nzModalRef.close();\n * // 关闭\n * this.nzModalRef.destroy();\n */\n create(\n comp?: TemplateRef<NzSafeAny> | Type<NzSafeAny> | 'confirm' | 'info' | 'success' | 'error' | 'warning',\n params?: NzSafeAny | ModalHelperOptions | null,\n options?: ModalHelperOptions\n ): Observable<NzSafeAny> {\n const isBuildIn = typeof comp === 'string';\n options = deepMerge(\n {\n size: 'lg',\n exact: true,\n includeTabs: false\n },\n isBuildIn && arguments.length === 2 ? params : options\n );\n return new Observable((observer: Observer<NzSafeAny>) => {\n const { size, includeTabs, modalOptions, drag, useNzData, focus } = options as ModalHelperOptions;\n let cls: string[] = [];\n let width = '';\n if (size) {\n if (typeof size === 'number') {\n width = `${size}px`;\n } else if (['sm', 'md', 'lg', 'xl'].includes(size)) {\n cls.push(`modal-${size}`);\n } else {\n width = size;\n }\n }\n if (includeTabs) {\n cls.push(`modal-include-tabs`);\n }\n if (modalOptions && modalOptions.nzWrapClassName) {\n cls.push(modalOptions.nzWrapClassName);\n delete modalOptions.nzWrapClassName;\n }\n let dragOptions: ModalHelperDragOptions | null;\n let dragWrapCls = `${CLS_DRAG}-${+new Date()}`;\n let dragRef: DragRef | null;\n if (drag != null && drag !== false) {\n dragOptions = {\n handleCls: `.modal-header, .ant-modal-title`,\n ...(typeof drag === 'object' ? drag : {})\n };\n cls.push(CLS_DRAG, dragWrapCls);\n }\n const mth = isBuildIn ? this.srv[comp] : this.srv.create;\n const callOptions: ModalOptions = {\n nzWrapClassName: cls.join(' '),\n nzFooter: null,\n nzData: params,\n nzDraggable: false,\n ...modalOptions\n };\n if (!isBuildIn) callOptions.nzContent = comp;\n if (width) callOptions.nzWidth = width;\n const modalRef = mth.call(this.srv, callOptions);\n // 保留 nzComponentParams 原有风格,但依然可以通过 @Inject(NZ_MODAL_DATA) 获取\n if (modalRef.componentInstance != null && useNzData !== true && params != null) {\n Object.entries(params as object).forEach(([key, value]) => {\n const t = modalRef.componentInstance as any;\n const s = t[key]?.[SIGNAL] as SignalNode<any>;\n if (s != null) {\n s.value = value;\n } else {\n t[key] = value;\n }\n });\n }\n modalRef.afterOpen\n .pipe(\n take(1),\n delay(modalOptions?.nzNoAnimation ? 25 : 341),\n tap(() => {\n if (dragOptions != null) {\n dragRef = this.buildDrag(dragOptions, `.${dragWrapCls}`);\n }\n })\n )\n .subscribe(() => {\n if (focus == null) return;\n const btns = modalRef\n .getElement()\n .querySelector<HTMLDivElement>('.ant-modal-confirm-btns, .modal-footer')\n ?.querySelectorAll<HTMLButtonElement>('.ant-btn');\n const btnSize = btns?.length ?? 0;\n let el: HTMLButtonElement | null = null;\n if (btnSize === 1) {\n el = btns![0];\n } else if (btnSize > 1) {\n el = btns![focus === 'ok' ? 1 : 0];\n }\n if (el != null) {\n el.focus();\n el.dataset.focused = focus;\n }\n });\n modalRef.afterClose.pipe(take(1)).subscribe((res: NzSafeAny) => {\n if (options!.exact === true) {\n if (res != null) {\n observer.next(res);\n }\n } else {\n observer.next(res);\n }\n observer.complete();\n dragRef?.dispose();\n });\n });\n }\n\n /**\n * 构建静态框,点击蒙层不允许关闭\n *\n * @param comp 组件\n * @param params 组件参数\n * @param options 额外参数\n *\n * @example\n * this.modalHelper.open(FormEditComponent, { i }).subscribe(res => this.load());\n * // 对于组件的成功&关闭的处理说明\n * // 成功,其中 `nzModalRef` 指目标组件在构造函数 `NzModalRef` 变量名\n * this.nzModalRef.close(data);\n * this.nzModalRef.close();\n * // 关闭\n * this.nzModalRef.destroy();\n */\n createStatic(\n comp: TemplateRef<NzSafeAny> | Type<NzSafeAny>,\n params?: NzSafeAny,\n options?: ModalHelperOptions\n ): Observable<NzSafeAny> {\n const modalOptions = {\n nzMaskClosable: false,\n ...(options && options.modalOptions)\n };\n return this.create(comp, params, { ...options, modalOptions });\n }\n}\n","import { Injectable, TemplateRef, Type, inject } from '@angular/core';\nimport { Observable, Observer } from 'rxjs';\n\nimport { deepMerge } from '@delon/util/other';\nimport type { NzSafeAny } from 'ng-zorro-antd/core/types';\nimport { NzDrawerOptions, NzDrawerRef, NzDrawerService } from 'ng-zorro-antd/drawer';\n\nexport interface DrawerHelperOptions {\n /**\n * 大小,若值为数值类型,则根据 `nzPlacement` 自动转化为 `nzHeight` 或 `nzWidth`;例如:lg、600,默认:`md`\n *\n * | 类型 | 默认大小 |\n * | --- | ------ |\n * | `sm` | `300` |\n * | `md` | `600` |\n * | `lg` | `900` |\n * | `xl` | `1200` |\n *\n * > 以上值,可通过覆盖相应的LESS参数自行调整\n */\n size?: 'sm' | 'md' | 'lg' | 'xl' | number;\n /**\n * 是否包含底部工具条,默认:`true`\n */\n footer?: boolean;\n /**\n * 底部工具条高度,默认:`55`\n */\n footerHeight?: number;\n /** 是否精准(默认:`true`),若返回值非空值(`null`或`undefined`)视为成功,否则视为错误 */\n exact?: boolean;\n /** 抽屉 [NzDrawerOptions](https://ng.ant.design/components/drawer/zh#nzdraweroptions) 参数 */\n drawerOptions?: NzDrawerOptions;\n}\n\n/**\n * 抽屉辅助类\n *\n * **注意:** 构建结果都可被订阅,但永远都不会触发 `observer.error`\n *\n * @example\n * this.drawerHelper.create('Edit', FormEditComponent, { i }).subscribe(res => this.load());\n * // 对于组件的成功&关闭的处理说明\n * // 成功\n * this.NzDrawerRef.close(data);\n * this.NzDrawerRef.close(true);\n * // 关闭\n * this.NzDrawerRef.close();\n * this.NzDrawerRef.close(false);\n */\n@Injectable({ providedIn: 'root' })\nexport class DrawerHelper {\n private readonly srv = inject(NzDrawerService);\n private readonly parentDrawer = inject(DrawerHelper, { optional: true, skipSelf: true });\n private openDrawersAtThisLevel: NzDrawerRef[] = [];\n get openDrawers(): NzDrawerRef[] {\n return this.parentDrawer ? this.parentDrawer.openDrawers : this.openDrawersAtThisLevel;\n }\n\n /**\n * 构建一个抽屉\n */\n create(\n title: string | TemplateRef<NzSafeAny> | undefined | null,\n comp:\n | TemplateRef<{\n $implicit: NzSafeAny;\n drawerRef: NzDrawerRef;\n }>\n | Type<NzSafeAny>,\n params?: NzSafeAny,\n options?: DrawerHelperOptions\n ): Observable<NzSafeAny> {\n options = deepMerge(\n {\n size: 'md',\n footer: true,\n footerHeight: 50,\n exact: true,\n drawerOptions: {\n nzPlacement: 'right',\n nzWrapClassName: ''\n }\n },\n options\n );\n return new Observable((observer: Observer<NzSafeAny>) => {\n const { size, footer, footerHeight, drawerOptions } = options as DrawerHelperOptions;\n const defaultOptions: NzDrawerOptions = {\n nzContent: comp,\n nzContentParams: params,\n nzTitle: title as NzSafeAny\n };\n\n if (typeof size === 'number') {\n defaultOptions[\n drawerOptions!.nzPlacement === 'top' || drawerOptions!.nzPlacement === 'bottom' ? 'nzHeight' : 'nzWidth'\n ] = options!.size;\n } else if (!drawerOptions!.nzWidth) {\n defaultOptions.nzWrapClassName = `${drawerOptions!.nzWrapClassName} drawer-${options!.size}`.trim();\n delete drawerOptions!.nzWrapClassName;\n }\n\n if (footer) {\n // The 24 value is @drawer-body-padding\n defaultOptions.nzBodyStyle = {\n 'padding-bottom': `${footerHeight! + 24}px`\n };\n }\n\n const ref = this.srv.create({ ...defaultOptions, ...drawerOptions });\n this.openDrawers.push(ref);\n const afterClose$ = ref.afterClose.subscribe((res: NzSafeAny) => {\n if (options!.exact === true) {\n if (res != null) {\n observer.next(res);\n }\n } else {\n observer.next(res);\n }\n observer.complete();\n afterClose$.unsubscribe();\n this.close(ref);\n });\n });\n }\n\n private close(ref: NzDrawerRef): void {\n const idx = this.openDrawers.indexOf(ref);\n if (idx === -1) return;\n this.openDrawers.splice(idx, 1);\n }\n\n closeAll(): void {\n let i = this.openDrawers.length;\n while (i--) {\n this.openDrawers[i].close();\n }\n }\n\n /**\n * 构建一个抽屉,点击蒙层不允许关闭\n */\n static(\n title: string | TemplateRef<NzSafeAny> | undefined | null,\n comp:\n | TemplateRef<{\n $implicit: NzSafeAny;\n drawerRef: NzDrawerRef;\n }>\n | Type<NzSafeAny>,\n params?: NzSafeAny,\n options?: DrawerHelperOptions\n ): Observable<NzSafeAny> {\n const drawerOptions = {\n nzMaskClosable: false,\n ...(options && options.drawerOptions)\n };\n return this.create(title, comp, params, { ...options, drawerOptions });\n }\n}\n","import { HttpClient, HttpContext, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';\nimport { Injectable, inject } from '@angular/core';\nimport { Observable, of, delay, finalize, switchMap, tap } from 'rxjs';\n\nimport { AlainConfigService, AlainThemeHttpClientConfig } from '@delon/util/config';\nimport type { NzSafeAny } from 'ng-zorro-antd/core/types';\n\nexport type _HttpHeaders = HttpHeaders | Record<string, string | string[]>;\nexport type HttpObserve = 'body' | 'events' | 'response';\n\n/**\n * 封装HttpClient,主要解决:\n * + 优化HttpClient在参数上便利性\n * + 统一实现 loading\n * + 统一处理时间格式问题\n */\n@Injectable({ providedIn: 'root' })\nexport class _HttpClient {\n private readonly http = inject(HttpClient);\n private readonly cogSrv = inject(AlainConfigService);\n private cog: AlainThemeHttpClientConfig;\n constructor() {\n this.cog = this.cogSrv.merge('themeHttp', {\n nullValueHandling: 'include',\n dateValueHandling: 'timestamp'\n })!;\n }\n\n private lc = 0;\n\n /**\n * Get whether it's loading\n *\n * 获取是否正在加载中\n */\n get loading(): boolean {\n return this.lc > 0;\n }\n\n /**\n * Get the currently loading count\n *\n * 获取当前加载中的数量\n */\n get loadingCount(): number {\n return this.lc;\n }\n\n parseParams(params: NzSafeAny): HttpParams {\n const newParams: NzSafeAny = {};\n if (params instanceof HttpParams) {\n return params;\n }\n\n const { nullValueHandling, dateValueHandling } = this.cog;\n Object.keys(params).forEach(key => {\n let paramValue = params[key];\n // 忽略空值\n if (nullValueHandling === 'ignore' && paramValue == null) return;\n // 将时间转化为:时间戳 (秒)\n if (\n paramValue instanceof Date &&\n (dateValueHandling === 'timestamp' || dateValueHandling === 'timestampSecond')\n ) {\n paramValue = dateValueHandling === 'timestamp' ? paramValue.valueOf() : Math.trunc(paramValue.valueOf() / 1000);\n }\n newParams[key] = paramValue;\n });\n return new HttpParams({ fromObject: newParams });\n }\n\n appliedUrl(url: string, params?: NzSafeAny): string {\n if (!params) return url;\n url += ~url.indexOf('?') ? '' : '?';\n const arr: string[] = [];\n Object.keys(params).forEach(key => {\n arr.push(`${key}=${params[key]}`);\n });\n return url + arr.join('&');\n }\n\n private setCount(count: number): void {\n Promise.resolve(null).then(() => (this.lc = count <= 0 ? 0 : count));\n }\n\n private push(): void {\n this.setCount(++this.lc);\n }\n\n private pop(): void {\n this.setCount(--this.lc);\n }\n\n /**\n * Clean loading count\n *\n * 清空加载中\n */\n cleanLoading(): void {\n this.setCount(0);\n }\n\n // #region get\n\n /**\n * **GET Request** Return a `string` type / 返回一个 `string` 类型\n */\n get(\n url: string,\n params: any,\n options: {\n headers?: _HttpHeaders;\n observe?: 'body';\n reportProgress?: boolean;\n responseType: 'text';\n withCredentials?: boolean;\n context?: HttpContext;\n }\n ): Observable<string>;\n\n /**\n * **GET Request** Return a `HttpEvent<T>` type / 返回一个 `HttpEvent<T>` 类型\n */\n get<T>(\n url: string,\n params: any,\n options: {\n headers?: _HttpHeaders;\n observe: 'events';\n reportProgress?: boolean;\n responseType?: 'arraybuffer' | 'blob' | 'json' | 'text';\n withCredentials?: boolean;\n context?: HttpContext;\n }\n ): Observable<HttpEvent<T>>;\n\n /**\n * **GET Request** Return a `HttpResponse<any>` type / 返回一个 `HttpResponse<any>` 类型\n */\n get(\n url: string,\n params: any,\n options: {\n headers?: _HttpHeaders;\n observe: 'response';\n reportProgress?: boolean;\n responseType?: 'json';\n withCredentials?: boolean;\n context?: HttpContext;\n }\n ): Observable<HttpResponse<any>>;\n\n /**\n * **GET Request** Return a `HttpResponse<T>` type / 返回一个 `HttpResponse<T>` 类型\n */\n get<T>(\n url: string,\n params: any,\n options: {\n headers?: _HttpHeaders;\n observe: 'response';\n reportProgress?: boolean;\n responseType?: 'arraybuffer' | 'blob' | 'json' | 'text';\n withCredentials?: boolean;\n context?: HttpContext;\n }\n ): Observable<HttpResponse<T>>;\n\n /**\n * **GET Request** Return a `any` type / 返回一个 `any` 类型\n */\n get(\n url: string,\n params?: any,\n options?: {\n headers?: _HttpHeaders;\n observe?: 'body' | 'events' | 'response';\n reportProgress?: boolean;\n responseType?: 'arraybuffer' | 'blob' | 'json' | 'text';\n withCredentials?: boolean;\n context?: HttpContext;\n }\n ): Observable<any>;\n\n /**\n * **GET Request** Return a generic type / 返回一个泛类型\n */\n get<T>(\n url: string,\n params?: any,\n options?: {\n headers?: _HttpHeaders;\n observe: 'body';\n reportProgress?: boolean;\n responseType?: 'json';\n withCredentials?: boolean;\n