• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atinc / ngx-tethys / #102

26 May 2026 08:11AM UTC coverage: 91.111% (+0.7%) from 90.407%
#102

push

web-flow
build: bump docgeni to 2.8.0-next.5 (#3809)

4571 of 5491 branches covered (83.25%)

Branch coverage included in aggregate %.

13141 of 13949 relevant lines covered (94.21%)

966.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.43
/src/empty/empty.component.ts
1
import {
2
    AfterViewInit,
3
    Component,
4
    computed,
5
    linkedSignal,
6
    contentChild,
7
    effect,
8
    ElementRef,
9
    inject,
10
    input,
11
    NgZone,
12
    Signal,
13
    TemplateRef
14
} from '@angular/core';
15
import { useHostRenderer } from '@tethys/cdk/dom';
16
import { ThyTranslate } from 'ngx-tethys/core';
17

18
import { NgClass, NgTemplateOutlet } from '@angular/common';
19
import { DomSanitizer } from '@angular/platform-browser';
20
import { injectLocale, ThyEmptyLocale } from 'ngx-tethys/i18n';
21
import { ThyIcon } from 'ngx-tethys/icon';
22
import { SafeAny } from 'ngx-tethys/types';
23
import { coerceBooleanProperty } from 'ngx-tethys/util';
24
import { ThyEmptyConfig } from './empty.config';
25
import { PRESET_SVG } from './svgs';
26

27
const sizeClassMap = {
1✔
28
    lg: ['thy-empty-state', 'thy-empty-state--lg'],
29
    md: ['thy-empty-state'],
30
    sm: ['thy-empty-state', 'thy-empty-state--sm']
31
};
32

33
const sizeMap = {
1✔
34
    lg: {
35
        height: 168, // 空提示的高度
36
        offsetTop: 30, // 空提示图标和大小之间的空白距离,需要除去,否则会不居中
37
        defaultMarginTop: 120 // 不自动计算默认的 top 距离
38
    },
39
    md: {
40
        height: 118,
41
        offsetTop: 20,
42
        defaultMarginTop: 10
43
    },
44
    sm: {
45
        height: 78,
46
        offsetTop: 10,
47
        defaultMarginTop: 10
48
    }
49
};
50

51
/** https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-loading */
52
export type ThyEmptyImageLoading = 'eager' | 'lazy';
53

54
/** https://wicg.github.io/priority-hints/#idl-index */
55
export type ThyEmptyImageFetchPriority = 'high' | 'low' | 'auto';
56

57
/**
58
 * 空页面组件
59
 * @name thy-empty
60
 * @order 10
61
 */
62
@Component({
63
    selector: 'thy-empty',
64
    templateUrl: './empty.component.html',
65
    imports: [ThyIcon, NgClass, NgTemplateOutlet]
66
})
67
export class ThyEmpty implements AfterViewInit {
1✔
68
    private thyTranslate = inject(ThyTranslate);
34✔
69
    private thyEmptyConfig = inject(ThyEmptyConfig);
34✔
70
    private elementRef = inject(ElementRef);
34✔
71
    private ngZone = inject(NgZone);
34✔
72
    private sanitizer = inject(DomSanitizer);
34✔
73
    private locale: Signal<ThyEmptyLocale> = injectLocale('empty');
34✔
74

75
    /**
76
     * 显示文本提示信息。同时传入 thyMessage,thyTranslationKey,thyEntityName,thyEntityNameTranslateKey 时优先级最高
77
     * @default 暂无数据
78
     */
79
    readonly thyMessage = input<string>();
34✔
80

81
    /**
82
     * 已废弃。显示文本提示信息多语言 Key。同时传入 thyTranslationKey,thyEntityName,thyEntityNameTranslateKey 时优先级最高
83
     * @deprecated
84
     */
85
    readonly thyTranslationKey = input<string>();
34✔
86

87
    /**
88
     * 已废弃。显示文本提示信息多语言 Key 的 Values。传入 thyTranslationKey 后,传入这个才会生效
89
     * @deprecated
90
     */
91
    readonly thyTranslationValues = input<any>();
34✔
92

93
    /**
94
     * 已废弃。显示默认提示信息,替换默认提示信息的目标对象,比如:没有 {thyEntityName}。同时传入 thyEntityName,thyEntityNameTranslateKey 时优先级较高
95
     * @deprecated
96
     */
97
    readonly thyEntityName = input<string>();
34✔
98

99
    /**
100
     * 已废弃。thyEntityName 的多语言 Key。thyMessage,thyTranslationKey,thyEntityName 均未传入时才会生效
101
     * @deprecated
102
     */
103
    readonly thyEntityNameTranslateKey = input<string>();
34✔
104

105
    /**
106
     * 提示图标名
107
     */
108
    readonly thyIconName = input<string>();
34✔
109

110
    /**
111
     * 大小
112
     * @type sm | md | lg
113
     * @default md
114
     */
115
    readonly thySize = input<string>('md');
34✔
116

117
    /**
118
     * 距上距离
119
     */
120
    readonly thyMarginTop = input<number | string>();
34✔
121

122
    /**
123
     * 是否自动根据父容器计算高度,垂直居中
124
     * @default false
125
     */
126
    readonly thyTopAuto = input(false, { transform: coerceBooleanProperty });
34✔
127

128
    /**
129
     * 自动计算高度垂直居中(即 thyTopAuto 为 true)时,支持传入自定义父容器
130
     */
131
    readonly thyContainer = input<ElementRef>();
34✔
132

133
    /**
134
     * 提示图片链接
135
     */
136
    readonly thyImageUrl = input<string>();
34✔
137

138
    readonly thyImageLoading = input<ThyEmptyImageLoading>();
34✔
139

140
    readonly thyImageFetchPriority = input<ThyEmptyImageFetchPriority>();
34✔
141

142
    /**
143
     * 显示文本描述
144
     */
145
    readonly thyDescription = input<string>();
34✔
146

147
    private hostRenderer = useHostRenderer();
34✔
148

149
    /**
150
     * 除提示图片,文本外的其他信息传入模板
151
     * @type TemplateRef
152
     */
153
    readonly extraTemplateRef = contentChild<TemplateRef<SafeAny>>('extra');
34✔
154

155
    protected readonly presetSvg = computed(() => {
34✔
156
        const presetSvg = this.thyIconName() ? PRESET_SVG[this.thyIconName() as keyof typeof PRESET_SVG] : PRESET_SVG.default;
34✔
157

158
        return presetSvg ? this.sanitizer.bypassSecurityTrustHtml(presetSvg) : '';
34✔
159
    });
160

161
    protected readonly displayText = linkedSignal(() => {
34✔
162
        if (this.thyMessage()) {
38✔
163
            return this.thyMessage();
31✔
164
        } else if (this.thyTranslationKey()) {
7✔
165
            return this.thyTranslate.instant(this.thyTranslationKey()!, this.thyTranslationValues());
1✔
166
        } else if (this.thyEntityName()) {
6✔
167
            return this.thyTranslate.instant(this.thyEmptyConfig.noResultWithTargetTranslateKey, {
1✔
168
                target: this.thyEntityName()
169
            });
170
        } else if (this.thyEntityNameTranslateKey()) {
5✔
171
            return this.thyTranslate.instant(this.thyEmptyConfig.noResultWithTargetTranslateKey, {
1✔
172
                target: this.thyTranslate.instant(this.thyEntityNameTranslateKey()!)
173
            });
174
        } else if (this.thyTranslate.instant(this.thyEmptyConfig.noResultTranslateKey) !== 'common.tips.NO_RESULT') {
4!
175
            return this.thyTranslate.instant(this.thyEmptyConfig.noResultTranslateKey);
×
176
        } else {
177
            return this.locale().noDataText;
4✔
178
        }
179
    });
180

181
    setMessage(text: string) {
182
        this.displayText.set(text);
1✔
183
    }
184

185
    constructor() {
186
        effect(() => {
34✔
187
            this.updateClass();
37✔
188
        });
189
    }
190

191
    private _calculatePosition() {
192
        const sizeOptions = sizeMap[(this.thySize() as keyof typeof sizeMap) || 'md'];
34✔
193
        let marginTop = null;
34✔
194
        if (this.thyTopAuto()) {
34✔
195
            // 选择参考父容器居中
196
            const thyContainer = this.thyContainer();
12✔
197
            const containerElement = thyContainer ? thyContainer.nativeElement : this.elementRef.nativeElement.parentElement;
12!
198
            // containerElement.height;
199
            let emptyStateHeight = this.elementRef.nativeElement.offsetHeight;
12✔
200
            // 高度没有自动计算出来使用默认值
201
            if (emptyStateHeight <= 10) {
12!
202
                emptyStateHeight = sizeOptions.height;
×
203
            }
204
            marginTop = (containerElement.offsetHeight - emptyStateHeight) / 2 - sizeOptions.offsetTop;
12✔
205
            // marginTop = (containerElement.offsetHeight - emptyStateHeight) / 2;
206
            if (marginTop < 0) {
12✔
207
                marginTop = 0; // sizeOptions.defaultMarginTop;
12✔
208
            }
209
        } else {
210
            const thyMarginTop = this.thyMarginTop();
22✔
211
            if (thyMarginTop) {
22!
212
                marginTop = thyMarginTop;
×
213
            } else {
214
                marginTop = 0; // sizeOptions.defaultMarginTop;
22✔
215
            }
216
        }
217
        if (marginTop) {
34!
218
            this.hostRenderer.setStyle('marginTop', `${marginTop}px`);
×
219
        }
220
    }
221

222
    updateClass() {
223
        const classList = sizeClassMap[(this.thySize() as keyof typeof sizeClassMap) || 'md'];
37✔
224
        if (classList) {
37✔
225
            this.hostRenderer.updateClass(classList);
37✔
226
        }
227
    }
228

229
    ngAfterViewInit() {
230
        this.ngZone.runOutsideAngular(() => {
34✔
231
            setTimeout(() => {
34✔
232
                this._calculatePosition();
34✔
233
            }, 50);
234
        });
235
    }
236
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc