Compare commits
6 Commits
73496f7768
...
ff3297e879
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff3297e879 | ||
|
|
28f0eabff2 | ||
|
|
1adf3eeb6d | ||
|
|
6735a815c4 | ||
|
|
26e533dc69 | ||
|
|
cc3bade990 |
1
.env.development
Normal file
1
.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API=http://localhost:8091
|
||||
@@ -5,7 +5,21 @@
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<style lang="less" scoped>
|
||||
.root {
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import { createApp } from "vue";
|
||||
import {createApp} from "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);
|
||||
app.directive("popup", VPopup);
|
||||
|
||||
8
examples/vite-env.d.ts
vendored
8
examples/vite-env.d.ts
vendored
@@ -5,3 +5,11 @@ declare module "*.vue" {
|
||||
}
|
||||
|
||||
declare module "prismjs";
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
13
package.json
13
package.json
@@ -5,14 +5,17 @@
|
||||
"module": "./dist/timi-web.mjs",
|
||||
"style": "./dist/timi-web.css",
|
||||
"private": false,
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.21",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:doc": "pnpm run -C docs dev",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build:doc": "pnpm run -C docs build",
|
||||
"pub": "pnpm run build && npm publish --registry=https://nexus.imyeyu.com/repository/npm-timi/"
|
||||
"pub": "pnpm run build && npm publish --registry=https://nexus.imyeyu.com/repository/npm-timi/",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://nexus.imyeyu.com/repository/npm-timi/"
|
||||
@@ -52,19 +55,23 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
||||
"@typescript-eslint/parser": "^8.53.0",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@vitest/coverage-v8": "^4.1.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-define-config": "^2.1.0",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-vue": "^10.6.2",
|
||||
"happy-dom": "^20.8.9",
|
||||
"prettier": "^3.8.0",
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "7.1.11",
|
||||
"vite": "8.0.5",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-plugin-prismjs": "^0.0.11",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vitest": "^4.1.4",
|
||||
"vue": "^3.5.26",
|
||||
"vue-tsc": "^3.2.2"
|
||||
}
|
||||
|
||||
1362
pnpm-lock.yaml
generated
1362
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
||||
import { TemplateBizType } from "../types";
|
||||
import Toolkit from "../utils/Toolkit";
|
||||
import { axios } from "./BaseAPI";
|
||||
import TextFormat from "../utils/TextFormat";
|
||||
import {Setting, TemplateBizType} from "../types";
|
||||
import {axios} from "./BaseAPI";
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
async function getSetting(key: string, args?: { [key: string]: any }): Promise<string> {
|
||||
return axios.get(`/setting/${key}?${TextFormat.urlArgs(args)}`);
|
||||
}
|
||||
|
||||
async function listSetting(keyMap: Map<string, object | undefined>): Promise<Map<string, string>> {
|
||||
const result = await axios.post("/setting/map", Toolkit.toObject(keyMap));
|
||||
const map = new Map<string, string>();
|
||||
Object.entries(result).forEach(([key, value]) => map.set(key, value as string));
|
||||
return map;
|
||||
async function settingMap(map: Record<string, string[]>): Promise<Map<string, Map<string, Setting>>> {
|
||||
const raw = await axios.post("/setting/map", map);
|
||||
const moduleMap = new Map<string, Map<string, Setting>>();
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return moduleMap;
|
||||
}
|
||||
for (const [module, settingRawMap] of Object.entries(raw as Record<string, unknown>)) {
|
||||
if (!settingRawMap || typeof settingRawMap !== "object") {
|
||||
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 {
|
||||
getCaptchaAPI,
|
||||
getAttachmentReadAPI,
|
||||
getTemplate,
|
||||
getSetting,
|
||||
listSetting
|
||||
settingMap
|
||||
};
|
||||
|
||||
@@ -127,12 +127,7 @@ textarea {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* 文本适当对齐 */
|
||||
.justify-text {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
/* 模糊玻璃效果:白色 */
|
||||
// 模糊玻璃效果:白色
|
||||
.glass-white {
|
||||
color: var(--eui-black, #000);
|
||||
background: rgba(255, 255, 255, .8);
|
||||
@@ -140,7 +135,7 @@ textarea {
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* 模糊玻璃效果:黑色 */
|
||||
// 模糊玻璃效果:黑色
|
||||
.glass-black {
|
||||
color: var(--eui-white, #FFF);
|
||||
background: rgba(0, 0, 0, .8);
|
||||
@@ -153,7 +148,7 @@ textarea {
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
/* 强制换行 */
|
||||
// 强制换行
|
||||
.break-all,
|
||||
.break-all textarea {
|
||||
word-wrap: break-word;
|
||||
@@ -161,14 +156,29 @@ textarea {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* 文本溢出截断 */
|
||||
// 文本溢出省略
|
||||
.clip-text {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 禁止选择 */
|
||||
// 文本保持不换行不省略
|
||||
.keep-text {
|
||||
flex: none;
|
||||
min-width: unset;
|
||||
white-space: nowrap;
|
||||
text-overflow: unset;
|
||||
}
|
||||
|
||||
// 文本适当对齐
|
||||
.justify-text {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
// 禁止选择
|
||||
.diselect {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
@@ -177,6 +187,7 @@ textarea {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// 可选文本
|
||||
.selectable {
|
||||
-webkit-touch-callout: default;
|
||||
-webkit-user-select: text;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="tui-copyright">
|
||||
<div class="tui-copyright" :class="{ 'background': background }">
|
||||
<p v-text="text"></p>
|
||||
<p v-if="icp" class="selectable">
|
||||
<a href="https://beian.miit.gov.cn/" v-text="icp" :title="icp" target="_blank"></a>
|
||||
@@ -21,8 +21,10 @@ withDefaults(defineProps<{
|
||||
domain?: string;
|
||||
author?: string;
|
||||
text?: string;
|
||||
background?: boolean;
|
||||
}>(), {
|
||||
text: "朝朝频顾惜,夜夜不相忘"
|
||||
text: "朝朝频顾惜,夜夜不相忘",
|
||||
background: false,
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -33,7 +35,11 @@ withDefaults(defineProps<{
|
||||
padding: 1rem 0 3rem 0;
|
||||
font-size: 13px;
|
||||
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 {
|
||||
margin: 0;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="tui-markdown-view selectable break-all line-numbers"
|
||||
v-html="markdownHTML"
|
||||
:data-max-height="maxHeight"
|
||||
:style="{ '--data-max-height': maxHeight }"
|
||||
></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -260,9 +260,9 @@
|
||||
overflow: auto;
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
max-height: var(data-max-height);
|
||||
max-height: var(--data-max-height);
|
||||
transition: max-height .5s var(--tui-bezier);
|
||||
font-family: var(--td-font-family);
|
||||
font-family: var(--td-font);
|
||||
line-height: 1;
|
||||
border-radius: 0;
|
||||
|
||||
@@ -270,7 +270,7 @@
|
||||
color: #333;
|
||||
background: transparent;
|
||||
text-shadow: none !important;
|
||||
font-family: var(--td-font-family);
|
||||
font-family: var(--td-font);
|
||||
|
||||
.line-numbers-rows {
|
||||
left: 0;
|
||||
|
||||
@@ -71,10 +71,10 @@ export enum ImageType {
|
||||
|
||||
export type KeyValue<V, K = string> = {
|
||||
key: K;
|
||||
value: V;
|
||||
value?: V;
|
||||
}
|
||||
|
||||
export type LabelValue<L, V = string> = {
|
||||
label: L;
|
||||
value: V;
|
||||
value?: V;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
export enum SettingKey {
|
||||
RUN_ENV = "RUN_ENV",
|
||||
PUBLIC_RESOURCES = "PUBLIC_RESOURCES",
|
||||
import {Model} from "./Model";
|
||||
|
||||
DOMAIN_ROOT = "DOMAIN_ROOT",
|
||||
DOMAIN_API = "DOMAIN_API",
|
||||
DOMAIN_GIT = "DOMAIN_GIT",
|
||||
DOMAIN_BLOG = "DOMAIN_BLOG",
|
||||
DOMAIN_SPACE = "DOMAIN_SPACE",
|
||||
DOMAIN_DOWNLOAD = "DOMAIN_DOWNLOAD",
|
||||
DOMAIN_RESOURCE = "DOMAIN_RESOURCE",
|
||||
export type SettingValueType =
|
||||
| "STRING"
|
||||
| "SELECT_RADIO"
|
||||
| "SELECT_CHECKBOX"
|
||||
| "INTEGER"
|
||||
| "COLOR"
|
||||
| "FLOAT"
|
||||
| "DATE"
|
||||
| "DATETIME"
|
||||
| "TIME"
|
||||
| "DURATION"
|
||||
| "BOOLEAN"
|
||||
| "JSON_LIST"
|
||||
| "JSON_OBJECT"
|
||||
;
|
||||
|
||||
ENABLE_COMMENT = "ENABLE_COMMENT",
|
||||
ENABLE_DEBUG = "ENABLE_DEBUG",
|
||||
ENABLE_LOGIN = "ENABLE_LOGIN",
|
||||
ENABLE_REGISTER = "ENABLE_REGISTER",
|
||||
ENABLE_USER_UPDATE = "ENABLE_USER_UPDATE",
|
||||
}
|
||||
|
||||
export type PublicResources = {
|
||||
wechatReceiveQRCode: string;
|
||||
user: PublicResourcesUser;
|
||||
}
|
||||
|
||||
export type PublicResourcesUser = {
|
||||
avatar: string;
|
||||
wrapper: string;
|
||||
}
|
||||
export type Setting = {
|
||||
module: string;
|
||||
key: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
value?: string;
|
||||
valueType: SettingValueType;
|
||||
valueArgs?: any;
|
||||
} & Model;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Text from "./Text";
|
||||
|
||||
export enum Unit {
|
||||
|
||||
/** B */
|
||||
@@ -60,7 +62,10 @@ export default class IOSize {
|
||||
* @param stopUnit 停止单位
|
||||
* @return
|
||||
*/
|
||||
public static format(size: number, fixed = 2, stopUnit?: Unit): string {
|
||||
public static format(size?: number | null, fixed = 2, stopUnit?: Unit): string {
|
||||
if (size === undefined || size === null) {
|
||||
return "0 B";
|
||||
}
|
||||
const units = Object.keys(Unit);
|
||||
if (0 < size) {
|
||||
for (let i = 0; i < units.length; i++, size /= 1024) {
|
||||
@@ -92,9 +97,9 @@ export default class IOSize {
|
||||
* @return 字节量
|
||||
* @throws Error 格式无效
|
||||
*/
|
||||
public static parse(sizeStr: string): number {
|
||||
if (!sizeStr || sizeStr.trim() === "") {
|
||||
throw new Error("not found sizeStr");
|
||||
public static parse(sizeStr?: string | null): number {
|
||||
if (sizeStr === undefined || sizeStr === null || sizeStr.trim() === "") {
|
||||
return 0;
|
||||
}
|
||||
// 正则匹配:可选符号 + 数字(含小数点)+ 可选空格 + 单位
|
||||
const pattern = /([-+]?\d+(?:\.\d+)?)\s*([a-zA-Z]+)/;
|
||||
@@ -160,13 +165,23 @@ export default class IOSize {
|
||||
/**
|
||||
* 转换值到字节量
|
||||
*
|
||||
* @param value 值
|
||||
* @param val 值
|
||||
* @param unit 单位
|
||||
* @return 字节量
|
||||
*/
|
||||
private static toBytes(value: number, unit: Unit): number {
|
||||
private static toBytes(val: number | undefined | null, unit: Unit): number {
|
||||
if (val === undefined || val === null) {
|
||||
return 0;
|
||||
}
|
||||
const units = Object.values(Unit);
|
||||
const ordinal = units.indexOf(unit);
|
||||
return Math.round(value * Math.pow(1024, ordinal));
|
||||
return Math.round(val * Math.pow(1024, ordinal));
|
||||
}
|
||||
|
||||
public static speed(val?: number | null) {
|
||||
if (val === undefined || val === null) {
|
||||
return "";
|
||||
}
|
||||
return Text.unit(IOSize.format(val, 2), " / s");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { marked } from "marked";
|
||||
import { gfmHeadingId } from "marked-gfm-heading-id";
|
||||
import { markedHighlight } from "marked-highlight";
|
||||
import { mangle } from "marked-mangle";
|
||||
import {marked} from "marked";
|
||||
import {gfmHeadingId} from "marked-gfm-heading-id";
|
||||
import {markedHighlight} from "marked-highlight";
|
||||
import {mangle} from "marked-mangle";
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/themes/prism.css";
|
||||
import SettingMapper from "./SettingMapper";
|
||||
|
||||
export default class Markdown {
|
||||
|
||||
@@ -97,10 +96,6 @@ export default class Markdown {
|
||||
target = "_blank";
|
||||
href = href.substring(1);
|
||||
}
|
||||
// 内部资源链接
|
||||
if (href.indexOf("@") !== -1) {
|
||||
href = SettingMapper.toResURL(href);
|
||||
}
|
||||
{
|
||||
// 处理嵌套 markdown,这可能不是最优解
|
||||
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>`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ### 组件渲染方式(原为图像渲染方式)
|
||||
*
|
||||
* ```md
|
||||
* [] 内文本以 # 开始时,该组件带边框
|
||||
* ```
|
||||
*
|
||||
* 1. 渲染为网页:``
|
||||
* 2. 渲染为视频:``
|
||||
* 3. 渲染为音频:``
|
||||
* 4. 渲染为图片:``
|
||||
* 6. 带边框图片:``
|
||||
*/
|
||||
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>`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,97 +1,99 @@
|
||||
import { readonly, Ref, ref } from "vue";
|
||||
import { RunEnv, SettingKey } from "../types";
|
||||
import Toolkit from "./Toolkit";
|
||||
import CommonAPI from "../api/CommonAPI";
|
||||
import {ref, Ref} from "vue";
|
||||
import {Setting} from "../types";
|
||||
import {CommonAPI} from "../api";
|
||||
|
||||
export default class 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> {
|
||||
const map = new Map<string, object | undefined>();
|
||||
{
|
||||
// 默认配置
|
||||
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);
|
||||
public static async loadSetting(map: Record<string, string[]>): Promise<void> {
|
||||
const result = await CommonAPI.settingMap(map);
|
||||
this.appendSettingMap(result);
|
||||
}
|
||||
|
||||
map.set(SettingKey.ENABLE_COMMENT, undefined);
|
||||
map.set(SettingKey.ENABLE_DEBUG, undefined);
|
||||
map.set(SettingKey.ENABLE_LOGIN, undefined);
|
||||
map.set(SettingKey.ENABLE_REGISTER, undefined);
|
||||
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 appendSettingMap(result: Map<string, Map<string, Setting>>): void {
|
||||
for (const [module, settingMap] of result) {
|
||||
for (const [key, setting] of settingMap) {
|
||||
this.setValue(module, key, setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static createModule(module: string): Map<string, Ref<Setting>> {
|
||||
const instance = this.getInstance();
|
||||
const result = await CommonAPI.listSetting(map);
|
||||
for (const [key, value] of result) {
|
||||
instance.map.set(key, ref(value));
|
||||
const moduleVal = instance.moduleMap.get(module);
|
||||
if (moduleVal) {
|
||||
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 {
|
||||
const value = this.getValueRef(key, args).value;
|
||||
return !!value && value === 'true';
|
||||
public static getModule(module: string): Map<string, Ref<Setting>> | undefined {
|
||||
return this.getInstance().moduleMap.get(module);
|
||||
}
|
||||
|
||||
public static not(key: SettingKey | string, args?: { [key: string]: any }): boolean {
|
||||
return !this.is(key, args);
|
||||
public static hasModule(module: string): boolean {
|
||||
return this.getInstance().moduleMap.has(module);
|
||||
}
|
||||
|
||||
public static getValue(key: SettingKey | string, args?: { [key: string]: any }): string | undefined {
|
||||
return this.getValueRef(key, args).value;
|
||||
public static deleteModule(module: string): boolean {
|
||||
return this.getInstance().moduleMap.delete(module);
|
||||
}
|
||||
|
||||
public static getValueRef(key: SettingKey| string, args?: { [key: string]: any }): Ref<string | undefined> {
|
||||
const instance = this.getInstance();
|
||||
let result = instance.map.get(key);
|
||||
if (result) {
|
||||
return result;
|
||||
public static setModule(module: string, moduleVal: Map<string, Ref<Setting>>): void {
|
||||
this.getInstance().moduleMap.set(module, moduleVal);
|
||||
}
|
||||
|
||||
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>());
|
||||
Toolkit.async(async () => {
|
||||
const value = instance.map.get(key);
|
||||
if (value) {
|
||||
return value.value;
|
||||
}
|
||||
result.value = await CommonAPI.getSetting(key, args);
|
||||
});
|
||||
return readonly(result);
|
||||
moduleVal.set(key, ref(setting));
|
||||
}
|
||||
|
||||
public static getDomainLink(domainKey: SettingKey): string | undefined {
|
||||
const runEnv = <RunEnv>(this.getValue(SettingKey.RUN_ENV));
|
||||
let protocol = "https";
|
||||
switch (runEnv) {
|
||||
case RunEnv.DEV:
|
||||
protocol = "http";
|
||||
break;
|
||||
case RunEnv.DEV_SSL:
|
||||
case RunEnv.PROD:
|
||||
protocol = "https";
|
||||
break;
|
||||
public static setValueRef(module: string, key: string, settingRef: Ref<Setting>): void {
|
||||
const moduleVal = this.createModule(module);
|
||||
moduleVal.set(key, settingRef);
|
||||
}
|
||||
|
||||
public static hasValue(module: string, key: string): boolean {
|
||||
return !!this.getModule(module)?.has(key);
|
||||
}
|
||||
|
||||
public static deleteValue(module: string, key: string): boolean {
|
||||
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 {
|
||||
@@ -100,22 +102,4 @@ export default class 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;
|
||||
}
|
||||
}
|
||||
|
||||
125
src/utils/Text.test.ts
Normal file
125
src/utils/Text.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import Text from "./Text";
|
||||
|
||||
describe("Text", () => {
|
||||
describe("keyValue", () => {
|
||||
it("should join key value pairs", () => {
|
||||
expect(Text.keyValue({ a: 1, b: "x" }, "=", "&")).toBe("a=1&b=x");
|
||||
});
|
||||
|
||||
it("should return empty string for empty object", () => {
|
||||
expect(Text.keyValue({}, "=", "&")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("urlArgs", () => {
|
||||
it("should return empty string when input is undefined", () => {
|
||||
expect(Text.urlArgs()).toBe("");
|
||||
});
|
||||
|
||||
it("should encode primitive and array values", () => {
|
||||
const query = Text.urlArgs({
|
||||
name: "A B",
|
||||
tags: ["x/y", "z"],
|
||||
age: 18
|
||||
});
|
||||
expect(query).toBe("name=A%20B&tags=x%2Fy&tags=z&age=18");
|
||||
});
|
||||
|
||||
it("should ignore inherited properties", () => {
|
||||
const parent = { hidden: "parent" };
|
||||
const obj = Object.create(parent) as { own: string };
|
||||
obj.own = "child";
|
||||
expect(Text.urlArgs(obj)).toBe("own=child");
|
||||
});
|
||||
});
|
||||
|
||||
describe("template", () => {
|
||||
it("should replace template placeholders", () => {
|
||||
expect(Text.template("Hi ${name}", { name: "Timi" })).toBe("Hi Timi");
|
||||
});
|
||||
|
||||
it("should output undefined text when key is missing", () => {
|
||||
expect(Text.template("Hi ${name} ${miss}", { name: "Timi" })).toBe("Hi Timi undefined");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unit", () => {
|
||||
it("should format number with fixed and unit", () => {
|
||||
expect(Text.unitCompact(1.236, "rem", 2)).toBe("1.24rem");
|
||||
});
|
||||
it("should format number with fixed and unit not space", () => {
|
||||
expect(Text.unit(1.236, "rem", 2)).toBe("1.24 rem");
|
||||
});
|
||||
|
||||
it("should trim string and append unit", () => {
|
||||
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", () => {
|
||||
expect(Text.unit(null, "px")).toBe("");
|
||||
expect(Text.unit(undefined, "px")).toBe("");
|
||||
expect(Text.unit(0, "px")).toBe("0 px");
|
||||
expect(Text.unit(Number.NaN, "px")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("display", () => {
|
||||
it("should trim string values", () => {
|
||||
expect(Text.display(" abc ")).toBe("abc");
|
||||
expect(Text.display(" ")).toBe("");
|
||||
});
|
||||
|
||||
it("should stringify finite numbers", () => {
|
||||
expect(Text.display(0)).toBe("0");
|
||||
expect(Text.display(12.3)).toBe("12.3");
|
||||
});
|
||||
|
||||
it("should return empty string for invalid numbers and nullish", () => {
|
||||
expect(Text.display(Number.NaN)).toBe("");
|
||||
expect(Text.display(Number.POSITIVE_INFINITY)).toBe("");
|
||||
expect(Text.display(null)).toBe("");
|
||||
expect(Text.display(undefined)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
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", () => {
|
||||
it("should return #FFF for dark backgrounds", () => {
|
||||
expect(Text.readableColor("#000")).toBe("#FFF");
|
||||
expect(Text.readableColor("#123456")).toBe("#FFF");
|
||||
});
|
||||
|
||||
it("should return #111 for light backgrounds", () => {
|
||||
expect(Text.readableColor("#FFF")).toBe("#111");
|
||||
expect(Text.readableColor("#f5f5f5")).toBe("#111");
|
||||
});
|
||||
|
||||
it("should use threshold at luminance 186", () => {
|
||||
expect(Text.readableColor("#BABABA")).toBe("#111");
|
||||
expect(Text.readableColor("#B9B9B9")).toBe("#FFF");
|
||||
});
|
||||
|
||||
it("should trim whitespace and accept mixed case hex", () => {
|
||||
expect(Text.readableColor(" #AbC ")).toBe("#FFF");
|
||||
});
|
||||
|
||||
it("should return #111 for invalid colors", () => {
|
||||
expect(Text.readableColor("red")).toBe("#111");
|
||||
expect(Text.readableColor("#12")).toBe("#111");
|
||||
expect(Text.readableColor("#GGGGGG")).toBe("#111");
|
||||
});
|
||||
});
|
||||
});
|
||||
101
src/utils/Text.ts
Normal file
101
src/utils/Text.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export default class Text {
|
||||
|
||||
public static keyValue(obj: object, assign: string, split: string): string {
|
||||
let result = "";
|
||||
Object.entries(obj).forEach(([k, v]) => result += k + assign + v + split);
|
||||
return result.substring(0, result.length - split.length);
|
||||
}
|
||||
|
||||
public static urlArgs(obj?: { [key: string]: any }): string {
|
||||
if (!obj) {
|
||||
return "";
|
||||
}
|
||||
const args: string[] = [];
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const value = obj[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => {
|
||||
args.push(`${encodeURIComponent(key)}=${encodeURIComponent(item)}`);
|
||||
});
|
||||
} else {
|
||||
args.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.toString())}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return args.join("&");
|
||||
}
|
||||
|
||||
public static template(template: string, variables: { [key: string]: any }): string {
|
||||
return template.replace(/\$\{(\w+)}/g, (_, key) => variables[key]);
|
||||
}
|
||||
|
||||
public static unitCompact(val: number | string | null | undefined, unit: string, fixed = 0): string {
|
||||
return this.unit(val, unit, fixed, true);
|
||||
};
|
||||
|
||||
public static unit(val: number | string | null | undefined, unit: string, fixed = 0, isCompact = false): string {
|
||||
if (val === null || val === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof val === "string" && !val.trim()) {
|
||||
return "";
|
||||
}
|
||||
const result = [];
|
||||
if (typeof val === "number") {
|
||||
if (Number.isNaN(val)) {
|
||||
return "";
|
||||
}
|
||||
result.push(val.toFixed(fixed));
|
||||
} else {
|
||||
result.push(val.trim());
|
||||
}
|
||||
if (!isCompact) {
|
||||
result.push(" ");
|
||||
}
|
||||
result.push(unit);
|
||||
return result.join("");
|
||||
};
|
||||
|
||||
public static display(val: string | number | null | undefined) {
|
||||
if (typeof val === "string") {
|
||||
return val.trim() || "";
|
||||
}
|
||||
if (typeof val === "number" && Number.isFinite(val)) {
|
||||
return val.toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static displayBool(val: boolean | null | undefined, onTrue = "true", onFalse = "false") {
|
||||
if (val === null || val === undefined) {
|
||||
return "";
|
||||
}
|
||||
return val ? onTrue : onFalse;
|
||||
}
|
||||
|
||||
/** 根据背景色返回更友好的文本色(仅返回 #111 或 #FFF) */
|
||||
public static readableColor(color: string): "#111" | "#FFF" {
|
||||
const match = color.trim().match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
|
||||
if (!match) {
|
||||
return "#111";
|
||||
}
|
||||
let rgb: number[]
|
||||
const hex = match[1];
|
||||
if (hex.length === 3) {
|
||||
rgb = [
|
||||
parseInt(hex[0] + hex[0], 16),
|
||||
parseInt(hex[1] + hex[1], 16),
|
||||
parseInt(hex[2] + hex[2], 16),
|
||||
];
|
||||
} else {
|
||||
rgb = [
|
||||
parseInt(hex.slice(0, 2), 16),
|
||||
parseInt(hex.slice(2, 4), 16),
|
||||
parseInt(hex.slice(4, 6), 16),
|
||||
];
|
||||
}
|
||||
const luminance = .299 * rgb[0] + .587 * rgb[1] + .114 * rgb[2];
|
||||
return luminance < 186 ? "#FFF" : "#111";
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import IOSize from "./IOSize";
|
||||
|
||||
export default class TextFormat {
|
||||
|
||||
public static keyValue(obj: object, assign: string, split: string): string {
|
||||
let result = "";
|
||||
Object.entries(obj).forEach(([k, v]) => result += k + assign + v + split);
|
||||
return result.substring(0, result.length - split.length);
|
||||
}
|
||||
|
||||
public static urlArgs(obj?: { [key: string]: any }): string {
|
||||
if (!obj) {
|
||||
return "";
|
||||
}
|
||||
const args: string[] = [];
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const value = obj[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => {
|
||||
args.push(`${encodeURIComponent(key)}=${encodeURIComponent(item)}`);
|
||||
});
|
||||
} else {
|
||||
args.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.toString())}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return args.join("&");
|
||||
}
|
||||
|
||||
public static template(template: string, variables: { [key: string]: any }): string {
|
||||
return template.replace(/\$\{(\w+)}/g, (_, key) => variables[key]);
|
||||
}
|
||||
|
||||
public static unit(val: number | string | null | undefined, unit: string, fixed = 0): string {
|
||||
if (!val) {
|
||||
return "";
|
||||
}
|
||||
if (typeof val === "number") {
|
||||
if (Number.isNaN(val)) {
|
||||
return "";
|
||||
}
|
||||
return val.toFixed(fixed) + unit;
|
||||
}
|
||||
return val.trim() + unit;
|
||||
};
|
||||
|
||||
public static speed(val?: number) {
|
||||
if (!val) {
|
||||
return "";
|
||||
}
|
||||
return this.unit(IOSize.format(val, 2), " / s");
|
||||
}
|
||||
}
|
||||
73
src/utils/Time.test.ts
Normal file
73
src/utils/Time.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import Time from "./Time";
|
||||
|
||||
describe("Time", () => {
|
||||
describe("toShortTime", () => {
|
||||
it("test toShortTime", () => {
|
||||
const date = new Date("2027-01-03T04:05:06.789Z");
|
||||
expect(Time.toShortTime(date.getTime())).toBe("05:06");
|
||||
});
|
||||
});
|
||||
describe("between", () => {
|
||||
it("should return time segments between begin and end", () => {
|
||||
const begin = new Date("2026-01-01T00:00:00.000Z");
|
||||
const end = new Date("2027-01-03T04:05:06.789Z");
|
||||
const result = Time.between(begin, end);
|
||||
expect(result.y).toBe(1);
|
||||
expect(result.d).toBe(2);
|
||||
expect(result.h).toBe(4);
|
||||
expect(result.m).toBe(5);
|
||||
expect(result.s).toBe(6);
|
||||
expect(result.ms).toBeGreaterThanOrEqual(788);
|
||||
expect(result.ms).toBeLessThanOrEqual(789);
|
||||
});
|
||||
});
|
||||
|
||||
describe("duration", () => {
|
||||
it("should return empty string when totalMs is falsy", () => {
|
||||
expect(Time.duration()).toBe("");
|
||||
expect(Time.duration(0)).toBe("");
|
||||
});
|
||||
|
||||
it("should format duration without milliseconds by default", () => {
|
||||
const value = Time.D * 366 + Time.H * 2 + Time.M * 3 + Time.S * 4 + 567;
|
||||
expect(Time.duration(value)).toBe("1 年 1 日 2 小时 3 分钟 4 秒");
|
||||
});
|
||||
|
||||
it("should include milliseconds when ms flag is true", () => {
|
||||
const value = Time.M + 250;
|
||||
expect(Time.duration(value, true)).toBe("1 分钟 250 毫秒");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toMediaTime", () => {
|
||||
it("should format seconds to media time", () => {
|
||||
expect(Time.toMediaTime(59.9)).toBe("00:59");
|
||||
expect(Time.toMediaTime(61)).toBe("01:01");
|
||||
expect(Time.toMediaTime(3661)).toBe("1:01:01");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseToMS", () => {
|
||||
it("should parse value without unit as milliseconds", () => {
|
||||
expect(Time.parseToMS("10")).toBe(10);
|
||||
expect(Time.parseToMS("10ms")).toBe(10);
|
||||
});
|
||||
|
||||
it("should parse supported units", () => {
|
||||
expect(Time.parseToMS("10s")).toBe(10 * Time.S);
|
||||
expect(Time.parseToMS("10m")).toBe(10 * Time.M);
|
||||
expect(Time.parseToMS("10h")).toBe(10 * Time.H);
|
||||
expect(Time.parseToMS("10d")).toBe(10 * Time.D);
|
||||
});
|
||||
|
||||
it("should parse decimal with spaces and uppercase unit", () => {
|
||||
expect(Time.parseToMS(" 10.5 D ")).toBe(Math.round(10.5 * Time.D));
|
||||
});
|
||||
|
||||
it("should throw for empty or invalid format", () => {
|
||||
expect(() => Time.parseToMS("")).toThrowError("not found timeStr");
|
||||
expect(() => Time.parseToMS("abc")).toThrowError("invalid format: abc");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -37,6 +37,12 @@ export default class Time {
|
||||
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
public static toShortTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转日期和时间
|
||||
*
|
||||
@@ -71,26 +77,6 @@ export default class Time {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将毫秒值转换为可读的时间字符串
|
||||
*
|
||||
* @param ms 毫秒值
|
||||
* @return 时间字符串,如 "10 天"、"5 小时"、"30 分钟"、"15 秒"、"500 毫秒"
|
||||
*/
|
||||
public static toString(ms: number): string {
|
||||
if (Time.D <= ms) {
|
||||
return `${Math.floor(ms / Time.D)} 天`;
|
||||
} else if (Time.H <= ms) {
|
||||
return `${Math.floor(ms / Time.H)} 小时`;
|
||||
} else if (Time.M <= ms) {
|
||||
return `${Math.floor(ms / Time.M)} 分钟`;
|
||||
} else if (Time.S <= ms) {
|
||||
return `${Math.floor(ms / Time.S)} 秒`;
|
||||
} else {
|
||||
return `${Math.floor(ms)} 毫秒`;
|
||||
}
|
||||
}
|
||||
|
||||
public static between(begin: Date, end?: Date) : any {
|
||||
if (!end) {
|
||||
end = new Date();
|
||||
@@ -106,18 +92,36 @@ export default class Time {
|
||||
return { l, y, d, h, m, s, ms };
|
||||
}
|
||||
|
||||
public static duration(begin?: number, ms?: boolean): string {
|
||||
if (!begin) {
|
||||
public static duration(totalMs?: number | null | undefined, ms?: boolean): string {
|
||||
if (totalMs === null || totalMs === undefined) {
|
||||
return "";
|
||||
}
|
||||
const r = Time.between(new Date(begin), new Date());
|
||||
let remain = Math.floor(totalMs);
|
||||
const yearMs = Time.D * 365;
|
||||
const dayMs = Time.D;
|
||||
const hourMs = Time.H;
|
||||
const minuteMs = Time.M;
|
||||
const secondMs = Time.S;
|
||||
|
||||
const years = Math.floor(remain / yearMs);
|
||||
remain %= yearMs;
|
||||
const days = Math.floor(remain / dayMs);
|
||||
remain %= dayMs;
|
||||
const hours = Math.floor(remain / hourMs);
|
||||
remain %= hourMs;
|
||||
const minutes = Math.floor(remain / minuteMs);
|
||||
remain %= minuteMs;
|
||||
const seconds = Math.floor(remain / secondMs);
|
||||
remain %= secondMs;
|
||||
|
||||
const milliseconds = remain;
|
||||
const parts: string[] = [];
|
||||
Toolkit.doWhere(0 < r.y, () => parts.push(`${r.y} 年`));
|
||||
Toolkit.doWhere(0 < r.d, () => parts.push(`${r.d} 天`));
|
||||
Toolkit.doWhere(0 < r.h, () => parts.push(`${r.h} 小时`));
|
||||
Toolkit.doWhere(0 < r.m, () => parts.push(`${r.m} 分钟`));
|
||||
Toolkit.doWhere(0 < r.s, () => parts.push(`${r.s} 秒`));
|
||||
Toolkit.doWhere(!!ms && 0 < r.ms, () => parts.push(`${r.ms} 毫秒`));
|
||||
Toolkit.doWhere(0 < years, () => parts.push(`${years} 年`));
|
||||
Toolkit.doWhere(0 < days, () => parts.push(`${days} 天`));
|
||||
Toolkit.doWhere(0 < hours, () => parts.push(`${hours} 小时`));
|
||||
Toolkit.doWhere(0 < minutes, () => parts.push(`${minutes} 分钟`));
|
||||
Toolkit.doWhere(0 < seconds, () => parts.push(`${seconds} 秒`));
|
||||
Toolkit.doWhere(!!ms && 0 < milliseconds, () => parts.push(`${milliseconds} 毫秒`));
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export { default as Prismjs } from "./Prismjs";
|
||||
export { default as Resizer } from "./Resizer";
|
||||
export { default as Scroller } from "./Scroller";
|
||||
export { default as Storage } from "./Storage";
|
||||
export { default as Format } from "./TextFormat";
|
||||
export { default as Text } from "./Text";
|
||||
export { default as Time } from "./Time";
|
||||
export { default as Toolkit } from "./Toolkit";
|
||||
export { default as IconMapper } from "./IconMapper";
|
||||
|
||||
11
vitest.config.ts
Normal file
11
vitest.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["src/**/*.{test,spec}.ts"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "html"]
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user