refactor Setting

This commit is contained in:
Timi
2026-04-30 11:04:28 +08:00
parent 28f0eabff2
commit ff3297e879
15 changed files with 239 additions and 231 deletions

1
.env.development Normal file
View File

@@ -0,0 +1 @@
VITE_API=http://localhost:8091

View File

@@ -5,7 +5,21 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Copyright } from "timi-web"; import {axios, Copyright, SettingMapper} from "timi-web";
import CommonAPI from "../src/api/CommonAPI";
onMounted(async () => {
const result = await CommonAPI.settingMap({
"SYSTEM": [
"SETTING_TTL"
]
});
SettingMapper.appendSettingMap(result);
console.log(SettingMapper.getValue("SYSTEM", "SETTING_TTL").value);
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.root { .root {

View File

@@ -1,6 +1,23 @@
import { createApp } from "vue"; import {createApp} from "vue";
import Root from "./Root.vue"; import Root from "./Root.vue";
import { VPopup } from "timi-web"; // 本地开发 import {axios, Network, VPopup} from "timi-web";
axios.defaults.baseURL = import.meta.env.VITE_API;
axios.interceptors.request.use((config) => {
const token = "V8khmLKkec1ahtjzFm2kx65wvmHob8N5lonqfUy3SsfxJ2HevYi8tLrxrL1iprcl";
if (token) {
if (config.method === "get") {
config.params = {
token,
...config.params
};
}
config.headers.set({ "Token": token });
}
return config;
}, (error: any) => {
return Promise.reject(error);
});
const app = createApp(Root); const app = createApp(Root);
app.directive("popup", VPopup); app.directive("popup", VPopup);

View File

@@ -5,3 +5,11 @@ declare module "*.vue" {
} }
declare module "prismjs"; declare module "prismjs";
interface ImportMetaEnv {
readonly VITE_API: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -5,7 +5,7 @@
"module": "./dist/timi-web.mjs", "module": "./dist/timi-web.mjs",
"style": "./dist/timi-web.css", "style": "./dist/timi-web.css",
"private": false, "private": false,
"version": "0.0.12", "version": "0.0.21",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,7 +1,5 @@
import { TemplateBizType } from "../types"; import {Setting, TemplateBizType} from "../types";
import Toolkit from "../utils/Toolkit"; import {axios} from "./BaseAPI";
import { axios } from "./BaseAPI";
import Text from "../utils/Text";
const getCaptchaAPI = () => axios.defaults.baseURL + "/captcha"; const getCaptchaAPI = () => axios.defaults.baseURL + "/captcha";
@@ -11,21 +9,28 @@ async function getTemplate(bizType: TemplateBizType, code: string): Promise<stri
return axios.get(`/template?bizType=${bizType}&bizCode=${code}`); return axios.get(`/template?bizType=${bizType}&bizCode=${code}`);
} }
async function getSetting(key: string, args?: { [key: string]: any }): Promise<string> { async function settingMap(map: Record<string, string[]>): Promise<Map<string, Map<string, Setting>>> {
return axios.get(`/setting/${key}?${Text.urlArgs(args)}`); const raw = await axios.post("/setting/map", map);
} const moduleMap = new Map<string, Map<string, Setting>>();
if (!raw || typeof raw !== "object") {
async function listSetting(keyMap: Map<string, object | undefined>): Promise<Map<string, string>> { return moduleMap;
const result = await axios.post("/setting/map", Toolkit.toObject(keyMap)); }
const map = new Map<string, string>(); for (const [module, settingRawMap] of Object.entries(raw as Record<string, unknown>)) {
Object.entries(result).forEach(([key, value]) => map.set(key, value as string)); if (!settingRawMap || typeof settingRawMap !== "object") {
return map; continue;
}
const settingMap = new Map<string, Setting>();
for (const [key, setting] of Object.entries(settingRawMap as Record<string, Setting>)) {
settingMap.set(key, setting);
}
moduleMap.set(module, settingMap);
}
return moduleMap;
} }
export default { export default {
getCaptchaAPI, getCaptchaAPI,
getAttachmentReadAPI, getAttachmentReadAPI,
getTemplate, getTemplate,
getSetting, settingMap
listSetting
}; };

View File

@@ -127,12 +127,7 @@ textarea {
text-decoration: none !important; text-decoration: none !important;
} }
/* 文本适当对齐 */ // 模糊玻璃效果:白色
.justify-text {
text-align: justify;
}
/* 模糊玻璃效果:白色 */
.glass-white { .glass-white {
color: var(--eui-black, #000); color: var(--eui-black, #000);
background: rgba(255, 255, 255, .8); background: rgba(255, 255, 255, .8);
@@ -140,7 +135,7 @@ textarea {
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
} }
/* 模糊玻璃效果:黑色 */ // 模糊玻璃效果:黑色
.glass-black { .glass-black {
color: var(--eui-white, #FFF); color: var(--eui-white, #FFF);
background: rgba(0, 0, 0, .8); background: rgba(0, 0, 0, .8);
@@ -153,7 +148,7 @@ textarea {
margin: 0 .5rem; margin: 0 .5rem;
} }
/* 强制换行 */ // 强制换行
.break-all, .break-all,
.break-all textarea { .break-all textarea {
word-wrap: break-word; word-wrap: break-word;
@@ -161,14 +156,29 @@ textarea {
white-space: normal; white-space: normal;
} }
/* 文本溢出截断 */ // 文本溢出省略
.clip-text { .clip-text {
flex: 1 1 auto;
overflow: hidden; overflow: hidden;
min-width: 0;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* 禁止选择 */ // 文本保持不换行不省略
.keep-text {
flex: none;
min-width: unset;
white-space: nowrap;
text-overflow: unset;
}
// 文本适当对齐
.justify-text {
text-align: justify;
}
// 禁止选择
.diselect { .diselect {
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
@@ -177,6 +187,7 @@ textarea {
user-select: none; user-select: none;
} }
// 可选文本
.selectable { .selectable {
-webkit-touch-callout: default; -webkit-touch-callout: default;
-webkit-user-select: text; -webkit-user-select: text;

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="tui-copyright"> <div class="tui-copyright" :class="{ 'background': background }">
<p v-text="text"></p> <p v-text="text"></p>
<p v-if="icp" class="selectable"> <p v-if="icp" class="selectable">
<a href="https://beian.miit.gov.cn/" v-text="icp" :title="icp" target="_blank"></a> <a href="https://beian.miit.gov.cn/" v-text="icp" :title="icp" target="_blank"></a>
@@ -21,8 +21,10 @@ withDefaults(defineProps<{
domain?: string; domain?: string;
author?: string; author?: string;
text?: string; text?: string;
background?: boolean;
}>(), { }>(), {
text: "朝朝频顾惜,夜夜不相忘" text: "朝朝频顾惜,夜夜不相忘",
background: false,
}); });
</script> </script>
@@ -33,7 +35,11 @@ withDefaults(defineProps<{
padding: 1rem 0 3rem 0; padding: 1rem 0 3rem 0;
font-size: 13px; font-size: 13px;
text-align: center; text-align: center;
background: url("assets/bottom.png") repeat-x left bottom;
&.background {
padding: 1rem 0;
background: url("assets/bottom.png") repeat-x left bottom;
}
p { p {
margin: 0; margin: 0;

View File

@@ -1,28 +1,27 @@
export enum SettingKey { import {Model} from "./Model";
RUN_ENV = "RUN_ENV",
PUBLIC_RESOURCES = "PUBLIC_RESOURCES",
DOMAIN_ROOT = "DOMAIN_ROOT", export type SettingValueType =
DOMAIN_API = "DOMAIN_API", | "STRING"
DOMAIN_GIT = "DOMAIN_GIT", | "SELECT_RADIO"
DOMAIN_BLOG = "DOMAIN_BLOG", | "SELECT_CHECKBOX"
DOMAIN_SPACE = "DOMAIN_SPACE", | "INTEGER"
DOMAIN_DOWNLOAD = "DOMAIN_DOWNLOAD", | "COLOR"
DOMAIN_RESOURCE = "DOMAIN_RESOURCE", | "FLOAT"
| "DATE"
| "DATETIME"
| "TIME"
| "DURATION"
| "BOOLEAN"
| "JSON_LIST"
| "JSON_OBJECT"
;
ENABLE_COMMENT = "ENABLE_COMMENT", export type Setting = {
ENABLE_DEBUG = "ENABLE_DEBUG", module: string;
ENABLE_LOGIN = "ENABLE_LOGIN", key: string;
ENABLE_REGISTER = "ENABLE_REGISTER", title?: string;
ENABLE_USER_UPDATE = "ENABLE_USER_UPDATE", description?: string;
} value?: string;
valueType: SettingValueType;
export type PublicResources = { valueArgs?: any;
wechatReceiveQRCode: string; } & Model;
user: PublicResourcesUser;
}
export type PublicResourcesUser = {
avatar: string;
wrapper: string;
}

View File

@@ -62,9 +62,9 @@ export default class IOSize {
* @param stopUnit 停止单位 * @param stopUnit 停止单位
* @return * @return
*/ */
public static format(size?: number, fixed = 2, stopUnit?: Unit): string { public static format(size?: number | null, fixed = 2, stopUnit?: Unit): string {
if (!size) { if (size === undefined || size === null) {
return ""; return "0 B";
} }
const units = Object.keys(Unit); const units = Object.keys(Unit);
if (0 < size) { if (0 < size) {
@@ -97,9 +97,9 @@ export default class IOSize {
* @return 字节量 * @return 字节量
* @throws Error 格式无效 * @throws Error 格式无效
*/ */
public static parse(sizeStr: string): number { public static parse(sizeStr?: string | null): number {
if (sizeStr.trim() === "") { if (sizeStr === undefined || sizeStr === null || sizeStr.trim() === "") {
throw new Error("not found sizeStr"); return 0;
} }
// 正则匹配:可选符号 + 数字(含小数点)+ 可选空格 + 单位 // 正则匹配:可选符号 + 数字(含小数点)+ 可选空格 + 单位
const pattern = /([-+]?\d+(?:\.\d+)?)\s*([a-zA-Z]+)/; const pattern = /([-+]?\d+(?:\.\d+)?)\s*([a-zA-Z]+)/;
@@ -169,8 +169,8 @@ export default class IOSize {
* @param unit 单位 * @param unit 单位
* @return 字节量 * @return 字节量
*/ */
private static toBytes(val: number | undefined, unit: Unit): number { private static toBytes(val: number | undefined | null, unit: Unit): number {
if (!val) { if (val === undefined || val === null) {
return 0; return 0;
} }
const units = Object.values(Unit); const units = Object.values(Unit);
@@ -178,8 +178,8 @@ export default class IOSize {
return Math.round(val * Math.pow(1024, ordinal)); return Math.round(val * Math.pow(1024, ordinal));
} }
public static speed(val?: number) { public static speed(val?: number | null) {
if (!val) { if (val === undefined || val === null) {
return ""; return "";
} }
return Text.unit(IOSize.format(val, 2), " / s"); return Text.unit(IOSize.format(val, 2), " / s");

View File

@@ -1,10 +1,9 @@
import { marked } from "marked"; import {marked} from "marked";
import { gfmHeadingId } from "marked-gfm-heading-id"; import {gfmHeadingId} from "marked-gfm-heading-id";
import { markedHighlight } from "marked-highlight"; import {markedHighlight} from "marked-highlight";
import { mangle } from "marked-mangle"; import {mangle} from "marked-mangle";
import Prism from "prismjs"; import Prism from "prismjs";
import "prismjs/themes/prism.css"; import "prismjs/themes/prism.css";
import SettingMapper from "./SettingMapper";
export default class Markdown { export default class Markdown {
@@ -97,10 +96,6 @@ export default class Markdown {
target = "_blank"; target = "_blank";
href = href.substring(1); href = href.substring(1);
} }
// 内部资源链接
if (href.indexOf("@") !== -1) {
href = SettingMapper.toResURL(href);
}
{ {
// 处理嵌套 markdown这可能不是最优解 // 处理嵌套 markdown这可能不是最优解
const tokens = (marked.lexer(text, { inline: true } as any) as any)[0].tokens; const tokens = (marked.lexer(text, { inline: true } as any) as any)[0].tokens;
@@ -125,61 +120,6 @@ export default class Markdown {
return `<span class="red">${text}</span>`; return `<span class="red">${text}</span>`;
} }
}; };
/**
* ### 组件渲染方式(原为图像渲染方式)
*
* ```md
* [] 内文本以 # 开始时,该组件带边框
* ```
*
* 1. 渲染为网页:`![]($/html/index.html)`
* 2. 渲染为视频:`![](#/media/video.mp4)`
* 3. 渲染为音频:`![](~/media/music.mp3)`
* 4. 渲染为图片:`![](/image/photo.png)`
* 6. 带边框图片:`![#图片Alt](/image/photo.png)`
*/
this.renderer.image = ({ href, title, text }) => {
const clazz = ["media"];
const hasBorder = text[0] === "#";
if (hasBorder) {
clazz.push("border");
}
title = title ?? text;
const extendTags = ["~", "#", "$"];
let extendTag;
if (extendTags.indexOf(href.charAt(0)) !== -1) {
extendTag = href.charAt(0);
}
// 内部资源链接
if (href.indexOf("@") !== -1) {
if (extendTag) {
href = href.substring(1);
}
href = SettingMapper.toResURL(href);
}
const clazzStr = clazz.join(" ");
let elStr;
switch (extendTag) {
case "~":
elStr = `<audio class="${clazzStr}" controls><source type="audio/mp3" src="${href}" title="${title}" /></audio>`;
break;
case "#":
elStr = `<video class="${clazzStr}" controls><source type="video/mp4" src="${href}" title="${title}" /></video>`;
break;
case "$":
elStr = `<iframe class="${clazzStr}" src="${href}" allowfullscreen title="${title}"></iframe>`;
break;
default:
elStr = `<img class="${clazzStr}" src="${href}" alt="${title}" />`;
break;
}
return elStr + `<span class="media-tips">${title}</span>`;
};
} }
/** /**

View File

@@ -1,97 +1,99 @@
import { readonly, Ref, ref } from "vue"; import {ref, Ref} from "vue";
import { RunEnv, SettingKey } from "../types"; import {Setting} from "../types";
import Toolkit from "./Toolkit"; import {CommonAPI} from "../api";
import CommonAPI from "../api/CommonAPI";
export default class SettingMapper { export default class SettingMapper {
private static instance: SettingMapper; private static instance: SettingMapper;
private map = new Map<string, Ref<string>>(); private moduleMap = new Map<string, Map<string, Ref<Setting>>>();
public static async loadSetting(...settings: (string | { key: string, args?: { [key: string]: any }})[]): Promise<void> { public static async loadSetting(map: Record<string, string[]>): Promise<void> {
const map = new Map<string, object | undefined>(); const result = await CommonAPI.settingMap(map);
{ this.appendSettingMap(result);
// 默认配置 }
map.set(SettingKey.RUN_ENV, undefined);
map.set(SettingKey.PUBLIC_RESOURCES, {
as: "json"
});
map.set(SettingKey.DOMAIN_ROOT, undefined);
map.set(SettingKey.DOMAIN_API, undefined);
map.set(SettingKey.DOMAIN_GIT, undefined);
map.set(SettingKey.DOMAIN_BLOG, undefined);
map.set(SettingKey.DOMAIN_SPACE, undefined);
map.set(SettingKey.DOMAIN_RESOURCE, undefined);
map.set(SettingKey.DOMAIN_DOWNLOAD, undefined);
map.set(SettingKey.ENABLE_COMMENT, undefined); public static appendSettingMap(result: Map<string, Map<string, Setting>>): void {
map.set(SettingKey.ENABLE_DEBUG, undefined); for (const [module, settingMap] of result) {
map.set(SettingKey.ENABLE_LOGIN, undefined); for (const [key, setting] of settingMap) {
map.set(SettingKey.ENABLE_REGISTER, undefined); this.setValue(module, key, setting);
map.set(SettingKey.ENABLE_USER_UPDATE, undefined);
}
{
// 附加配置
for (let i = 0; i < settings.length; i++) {
const setting = settings[i];
if (typeof setting === 'string') {
map.set(setting, undefined);
} else {
map.set(setting.key, setting.args);
}
} }
} }
}
public static createModule(module: string): Map<string, Ref<Setting>> {
const instance = this.getInstance(); const instance = this.getInstance();
const result = await CommonAPI.listSetting(map); const moduleVal = instance.moduleMap.get(module);
for (const [key, value] of result) { if (moduleVal) {
instance.map.set(key, ref(value)); return moduleVal;
} }
const created = new Map<string, Ref<Setting>>();
instance.moduleMap.set(module, created);
return created;
} }
public static is(key: SettingKey | string, args?: { [key: string]: any }): boolean { public static getModule(module: string): Map<string, Ref<Setting>> | undefined {
const value = this.getValueRef(key, args).value; return this.getInstance().moduleMap.get(module);
return !!value && value === 'true';
} }
public static not(key: SettingKey | string, args?: { [key: string]: any }): boolean { public static hasModule(module: string): boolean {
return !this.is(key, args); return this.getInstance().moduleMap.has(module);
} }
public static getValue(key: SettingKey | string, args?: { [key: string]: any }): string | undefined { public static deleteModule(module: string): boolean {
return this.getValueRef(key, args).value; return this.getInstance().moduleMap.delete(module);
} }
public static getValueRef(key: SettingKey| string, args?: { [key: string]: any }): Ref<string | undefined> { public static setModule(module: string, moduleVal: Map<string, Ref<Setting>>): void {
const instance = this.getInstance(); this.getInstance().moduleMap.set(module, moduleVal);
let result = instance.map.get(key); }
if (result) {
return result; public static is(module: string, key: string): boolean {
const setting = this.getValueRef(module, key).value;
return !!setting.value && setting.value === "true";
}
public static not(module: string, key: string): boolean {
return !this.is(module, key);
}
public static getValue(module: string, key: string): Setting {
return this.getValueRef(module, key).value;
}
public static setValue(module: string, key: string, setting: Setting): void {
const moduleVal = this.createModule(module);
const settingRef = moduleVal.get(key);
if (settingRef) {
settingRef.value = setting;
return;
} }
instance.map.set(key, result = ref<any>()); moduleVal.set(key, ref(setting));
Toolkit.async(async () => {
const value = instance.map.get(key);
if (value) {
return value.value;
}
result.value = await CommonAPI.getSetting(key, args);
});
return readonly(result);
} }
public static getDomainLink(domainKey: SettingKey): string | undefined { public static setValueRef(module: string, key: string, settingRef: Ref<Setting>): void {
const runEnv = <RunEnv>(this.getValue(SettingKey.RUN_ENV)); const moduleVal = this.createModule(module);
let protocol = "https"; moduleVal.set(key, settingRef);
switch (runEnv) { }
case RunEnv.DEV:
protocol = "http"; public static hasValue(module: string, key: string): boolean {
break; return !!this.getModule(module)?.has(key);
case RunEnv.DEV_SSL: }
case RunEnv.PROD:
protocol = "https"; public static deleteValue(module: string, key: string): boolean {
break; return !!this.getModule(module)?.delete(key);
}
public static getValueRef(module: string, key: string): Ref<Setting> {
const moduleVal = this.getModule(module);
if (!moduleVal) {
throw new Error(`Setting module not found: ${module}`);
} }
return `${protocol}://${this.getValue(domainKey)}`; const valueRef = moduleVal.get(key);
if (!valueRef) {
throw new Error(`Setting key not found: ${module}.${key}`);
}
return valueRef;
} }
public static getInstance(): SettingMapper { public static getInstance(): SettingMapper {
@@ -100,22 +102,4 @@ export default class SettingMapper {
} }
return SettingMapper.instance = new SettingMapper(); return SettingMapper.instance = new SettingMapper();
} }
/**
* 解析资源 URL 协议前缀
*
* @param val 原始值,如 `res@/path/to/file`
* @returns 完整 URL
*/
public static toResURL(val: string): string {
const at = val.indexOf("@");
const start = val.substring(0, at);
const path = val.substring(at + 1);
switch (start) {
case "res": return SettingMapper.getDomainLink(SettingKey.DOMAIN_RESOURCE) + path;
case "dl": return SettingMapper.getDomainLink(SettingKey.DOMAIN_DOWNLOAD) + path;
case "attach": return CommonAPI.getAttachmentReadAPI(path);
}
return val;
}
} }

View File

@@ -46,20 +46,24 @@ describe("Text", () => {
describe("unit", () => { describe("unit", () => {
it("should format number with fixed and unit", () => { it("should format number with fixed and unit", () => {
expect(Text.unitSpace(1.236, 2, "rem")).toBe("1.24 rem"); expect(Text.unitCompact(1.236, "rem", 2)).toBe("1.24rem");
}); });
it("should format number with fixed and unit not space", () => { it("should format number with fixed and unit not space", () => {
expect(Text.unit(1.236, "rem", 2)).toBe("1.24rem"); expect(Text.unit(1.236, "rem", 2)).toBe("1.24 rem");
}); });
it("should trim string and append unit", () => { it("should trim string and append unit", () => {
expect(Text.unit(" 12 ", "px")).toBe("12px"); expect(Text.unit(" 12 ", "px")).toBe("12 px");
});
it("test zero", () => {
expect(Text.unit(0, "毫秒")).toBe("0 毫秒");
}); });
it("should return empty string for nullish zero and NaN", () => { it("should return empty string for nullish zero and NaN", () => {
expect(Text.unit(null, "px")).toBe(""); expect(Text.unit(null, "px")).toBe("");
expect(Text.unit(undefined, "px")).toBe(""); expect(Text.unit(undefined, "px")).toBe("");
expect(Text.unit(0, "px")).toBe(""); expect(Text.unit(0, "px")).toBe("0 px");
expect(Text.unit(Number.NaN, "px")).toBe(""); expect(Text.unit(Number.NaN, "px")).toBe("");
}); });
}); });
@@ -83,6 +87,15 @@ describe("Text", () => {
}); });
}); });
describe("displayBool", () => {
it("should show boolean text", () => {
expect(Text.displayBool(true)).toBe("true");
expect(Text.displayBool(false)).toBe("false");
expect(Text.displayBool(false, "开启", "关闭")).toBe("关闭");
expect(Text.displayBool(undefined)).toBe("");
});
});
describe("readableColor", () => { describe("readableColor", () => {
it("should return #FFF for dark backgrounds", () => { it("should return #FFF for dark backgrounds", () => {
expect(Text.readableColor("#000")).toBe("#FFF"); expect(Text.readableColor("#000")).toBe("#FFF");

View File

@@ -30,12 +30,15 @@ export default class Text {
return template.replace(/\$\{(\w+)}/g, (_, key) => variables[key]); return template.replace(/\$\{(\w+)}/g, (_, key) => variables[key]);
} }
public static unitSpace(val: number | string | null | undefined, fixed = 0, unit: string): string { public static unitCompact(val: number | string | null | undefined, unit: string, fixed = 0): string {
return this.unit(val, unit, fixed, true); return this.unit(val, unit, fixed, true);
}; };
public static unit(val: number | string | null | undefined, unit: string, fixed = 0, space = false): string { public static unit(val: number | string | null | undefined, unit: string, fixed = 0, isCompact = false): string {
if (!val) { if (val === null || val === undefined) {
return "";
}
if (typeof val === "string" && !val.trim()) {
return ""; return "";
} }
const result = []; const result = [];
@@ -47,7 +50,7 @@ export default class Text {
} else { } else {
result.push(val.trim()); result.push(val.trim());
} }
if (space) { if (!isCompact) {
result.push(" "); result.push(" ");
} }
result.push(unit); result.push(unit);
@@ -64,6 +67,13 @@ export default class Text {
return ""; return "";
} }
public static displayBool(val: boolean | null | undefined, onTrue = "true", onFalse = "false") {
if (val === null || val === undefined) {
return "";
}
return val ? onTrue : onFalse;
}
/** 根据背景色返回更友好的文本色(仅返回 #111 或 #FFF */ /** 根据背景色返回更友好的文本色(仅返回 #111 或 #FFF */
public static readableColor(color: string): "#111" | "#FFF" { public static readableColor(color: string): "#111" | "#FFF" {
const match = color.trim().match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/); const match = color.trim().match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);

View File

@@ -92,8 +92,8 @@ export default class Time {
return { l, y, d, h, m, s, ms }; return { l, y, d, h, m, s, ms };
} }
public static duration(totalMs?: number, ms?: boolean): string { public static duration(totalMs?: number | null | undefined, ms?: boolean): string {
if (!totalMs) { if (totalMs === null || totalMs === undefined) {
return ""; return "";
} }
let remain = Math.floor(totalMs); let remain = Math.floor(totalMs);