mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-19 18:43:53 +08:00
update: play property animation
update: event format field fix: test
This commit is contained in:
+6
-43
@@ -1,56 +1,19 @@
|
|||||||
# Life Restart
|
# 人生重开模拟器
|
||||||
|
|
||||||
<a href="https://discord.gg/U3qrf49NMQ"><img src="https://img.shields.io/discord/883382868427014255?color=%23FEE75C&label=Discord&logo=discord&logoColor=white&style=for-the-badge" /></a>
|
<a href="https://discord.gg/U3qrf49NMQ"><img src="https://img.shields.io/discord/883382868427014255?color=%23FEE75C&label=Discord&logo=discord&logoColor=white&style=for-the-badge" /></a>
|
||||||
|
|
||||||
[English](./README.md) | 简体中文
|
[English](./README.md) | 简体中文
|
||||||
|
|
||||||
## 简介
|
## 开发环境
|
||||||
|
|
||||||
- Game Life Restart
|
包管理: `pnpm`
|
||||||
|
运行时: `bun`
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
<details>
|
### 开发
|
||||||
<summary><strong>网页版</strong></summary>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 下载项目代码
|
pnpm build:data
|
||||||
git clone https://github.com/VickScarlet/lifeRestart.git my-project
|
|
||||||
|
|
||||||
# 2. 进入目录安装依赖
|
|
||||||
cd my-project
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# 3. 转换XLSX表
|
|
||||||
pnpm xlsx2json
|
|
||||||
|
|
||||||
# 4. 启动本地开发服务器
|
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
启动完成后打开浏览器访问 [http://localhost:5173](http://localhost:5173)。
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>控制台版本</strong></summary>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node repl
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 其他版本
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>版本列表</strong></summary>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
- Cocos版:[gameall3d/LifeRestart_Cocos](https://github.com/gameall3d/LifeRestart_Cocos)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
> 更多信息请参考 [官网文档](https://liferestart.syaro.io/)。
|
|
||||||
|
|||||||
@@ -1,56 +1,19 @@
|
|||||||
# Life Restart
|
# Remake
|
||||||
|
|
||||||
<a href="https://discord.gg/U3qrf49NMQ"><img src="https://img.shields.io/discord/883382868427014255?color=%23FEE75C&label=Discord&logo=discord&logoColor=white&style=for-the-badge" /></a>
|
<a href="https://discord.gg/U3qrf49NMQ"><img src="https://img.shields.io/discord/883382868427014255?color=%23FEE75C&label=Discord&logo=discord&logoColor=white&style=for-the-badge" /></a>
|
||||||
|
|
||||||
English | [简体中文](./README-zh_CN.md)
|
English | [简体中文](./README-zh_CN.md)
|
||||||
|
|
||||||
## Introduction
|
## Environment
|
||||||
|
|
||||||
- Game Life Restart
|
Package Manager: `pnpm`
|
||||||
|
Runtime: `bun`
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
<details>
|
### Dev
|
||||||
<summary><strong>Web Version</strong></summary>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clone project code.
|
pnpm build:data
|
||||||
git clone git@github.com:VickScarlet/lifeRestart.git my-project
|
|
||||||
cd my-project
|
|
||||||
|
|
||||||
# 2. Installation dependence.
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# 3. Transform XLSX
|
|
||||||
pnpm xlsx2json
|
|
||||||
|
|
||||||
# 4. Start local dev server.
|
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
After the startup is complete, open a browser and visit [http://localhost:5173](http://localhost:5173).
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Command Line Version</strong></summary>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node repl
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Other Version
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Versions</strong></summary>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
- Cocos Ver: [gameall3d/LifeRestart_Cocos](https://github.com/gameall3d/LifeRestart_Cocos)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
> More instructions at [documentation](https://liferestart.syaro.io/).
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { Log } from '@remake/hooks'
|
|||||||
import { properties } from '@/display'
|
import { properties } from '@/display'
|
||||||
import { achievements, events, talents } from '@remake/data'
|
import { achievements, events, talents } from '@remake/data'
|
||||||
import { AutoInterval } from '@/config'
|
import { AutoInterval } from '@/config'
|
||||||
|
import { format } from '@remake/vitex'
|
||||||
import './Play.css'
|
import './Play.css'
|
||||||
|
|
||||||
function LogTalent({ id }: { id: number }) {
|
function LogTalent({ id }: { id: number }) {
|
||||||
@@ -25,12 +26,19 @@ function LogTalents({ items }: { items: number[] }) {
|
|||||||
return <ul className="log-inner log-talents">{els}</ul>
|
return <ul className="log-inner log-talents">{els}</ul>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const year = new Date().getFullYear()
|
||||||
interface LogEventProps {
|
interface LogEventProps {
|
||||||
id: number
|
id: number
|
||||||
post: boolean
|
post: boolean
|
||||||
|
index: number
|
||||||
}
|
}
|
||||||
function LogEvent({ id, post }: LogEventProps) {
|
function LogEvent({ id, post, index }: LogEventProps) {
|
||||||
const { event, postEvent, grade } = events.get(id)!
|
let { event, postEvent, grade, format: f } = events.get(id)!
|
||||||
|
if (f) {
|
||||||
|
const g = (key: string) => ({ CurrentYear: year + index })[key]
|
||||||
|
event = format(event, g)
|
||||||
|
if (post && postEvent) postEvent = format(postEvent, g)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<li className={`grade-${grade}`}>{event}</li>
|
<li className={`grade-${grade}`}>{event}</li>
|
||||||
@@ -41,10 +49,10 @@ function LogEvent({ id, post }: LogEventProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LogEvents({ items }: { items: number[] }) {
|
function LogEvents({ items, index }: { items: number[]; index: number }) {
|
||||||
const last = items.length - 1
|
const last = items.length - 1
|
||||||
const els = items.map((id, i) => (
|
const els = items.map((id, i) => (
|
||||||
<LogEvent key={id} id={id} post={i == last} />
|
<LogEvent key={id} id={id} post={i == last} index={index} />
|
||||||
))
|
))
|
||||||
return <ul className="log-inner log-events">{els}</ul>
|
return <ul className="log-inner log-events">{els}</ul>
|
||||||
}
|
}
|
||||||
@@ -66,13 +74,13 @@ function LogAchievements({ items }: { items: number[] }) {
|
|||||||
return <ul className="log-inner log-achievements">{els}</ul>
|
return <ul className="log-inner log-achievements">{els}</ul>
|
||||||
}
|
}
|
||||||
|
|
||||||
function Log({ log }: { log: Log }) {
|
function Log({ log, index }: { log: Log; index: number }) {
|
||||||
return (
|
return (
|
||||||
<li className="log">
|
<li className="log">
|
||||||
<span className="age font-mono">{log.age}岁</span>
|
<span className="age font-mono">{log.age}岁</span>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<LogTalents items={log.talents} />
|
<LogTalents items={log.talents} />
|
||||||
<LogEvents items={log.events} />
|
<LogEvents items={log.events} index={index} />
|
||||||
<LogAchievements items={log.achievements} />
|
<LogAchievements items={log.achievements} />
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -88,25 +96,40 @@ interface PropProps {
|
|||||||
function Prop({ prop, value, grade }: PropProps) {
|
function Prop({ prop, value, grade }: PropProps) {
|
||||||
const prevRef = useRef<number>(value)
|
const prevRef = useRef<number>(value)
|
||||||
const [trend, setTrend] = useState<'up' | 'down' | 'normal'>('normal')
|
const [trend, setTrend] = useState<'up' | 'down' | 'normal'>('normal')
|
||||||
const [animCount, setAnimCount] = useState<number>(0)
|
const [flip, setFlip] = useState(0)
|
||||||
|
const [displayValue, setDisplayValue] = useState<number>(value)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const prev = prevRef.current
|
const prev = prevRef.current
|
||||||
if (value === prev) return
|
if (value === prev) return
|
||||||
prevRef.current = value
|
prevRef.current = value
|
||||||
setTrend(value > prev ? 'up' : 'down')
|
setTrend(value > prev ? 'up' : 'down')
|
||||||
setAnimCount(c => c + 1)
|
setFlip(f => (f + 1) % 2)
|
||||||
const timer = setTimeout(() => {
|
let startTimestamp: number | null = null
|
||||||
setTrend('normal')
|
const duration = 400
|
||||||
}, 2000)
|
const startValue = displayValue
|
||||||
return () => clearTimeout(timer)
|
let end = false
|
||||||
|
const step = (timestamp: number) => {
|
||||||
|
if (!startTimestamp) startTimestamp = timestamp
|
||||||
|
const progress = timestamp - startTimestamp
|
||||||
|
const progressRatio = Math.min(progress / duration, 1)
|
||||||
|
const easeOutQuad = progressRatio * (2 - progressRatio)
|
||||||
|
const currentDec = startValue + (value - startValue) * easeOutQuad
|
||||||
|
setDisplayValue(Math.round(currentDec))
|
||||||
|
if (!end && progress < duration) requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(step)
|
||||||
|
const timer = setTimeout(() => setTrend('normal'), 2000)
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
setDisplayValue(value)
|
||||||
|
end = true
|
||||||
|
}
|
||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li className={`${prop} grade-${grade} trend-${trend}-${flip}`}>
|
||||||
className={`${prop} grade-${grade} trend-${trend}-${animCount % 2}`}
|
|
||||||
>
|
|
||||||
<span className="name">{properties[prop]}</span>
|
<span className="name">{properties[prop]}</span>
|
||||||
<span className="value font-mono">{value}</span>
|
<span className="value font-mono">{displayValue}</span>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -160,7 +183,7 @@ export function Play() {
|
|||||||
ref={logRef}
|
ref={logRef}
|
||||||
>
|
>
|
||||||
{logs.map((log, index) => (
|
{logs.map((log, index) => (
|
||||||
<Log key={index} log={log} />
|
<Log key={index} log={log} index={index} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="controls">
|
<div className="controls">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useRef } from 'react'
|
import { useEffect, useState, useRef, useMemo } from 'react'
|
||||||
import { usePicked, useEnd, useProfile } from '@remake/hooks'
|
import { usePicked, useEnd, useProfile } from '@remake/hooks'
|
||||||
import { useEndJudge } from '@/hooks/judge'
|
import { useEndJudge } from '@/hooks/judge'
|
||||||
import { properties, judgeDisplay } from '@/display'
|
import { properties, judgeDisplay } from '@/display'
|
||||||
@@ -27,26 +27,24 @@ interface TalentListProps {
|
|||||||
}
|
}
|
||||||
function TalentList({ picked, picker }: TalentListProps) {
|
function TalentList({ picked, picker }: TalentListProps) {
|
||||||
const [profile] = useProfile()
|
const [profile] = useProfile()
|
||||||
const [talents, setTalents] = useState(usePicked())
|
const items = usePicked()
|
||||||
|
const talents = useMemo(() => {
|
||||||
|
if (!isDev || !profile.locked) return items
|
||||||
|
return new Set([...items, ...profile.locked])
|
||||||
|
}, [])
|
||||||
const hasInitialized = useRef(false)
|
const hasInitialized = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!profile.locked) return
|
if (!profile.locked) return
|
||||||
let currentTalents = talents
|
|
||||||
if (isDev) {
|
|
||||||
currentTalents = new Set([...talents, ...profile.locked])
|
|
||||||
setTalents(currentTalents)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasInitialized.current) {
|
if (!hasInitialized.current) {
|
||||||
for (const id of profile.locked) {
|
for (const id of profile.locked) {
|
||||||
if (currentTalents.has(id) && !picked.has(id)) {
|
if (talents.has(id) && !picked.has(id)) {
|
||||||
picker(id)
|
picker(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hasInitialized.current = true
|
hasInitialized.current = true
|
||||||
}
|
}
|
||||||
}, [profile, talents, picked, picker])
|
}, [picked, picker])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="talent-list">
|
<ul className="talent-list">
|
||||||
{Array.from(talents, id => (
|
{Array.from(talents, id => (
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
## 流程
|
|
||||||
|
|
||||||
重开
|
|
||||||
初始化
|
|
||||||
抽天赋(包含继承的天赋)
|
|
||||||
天赋选择(检测天赋冲突)
|
|
||||||
天赋替换(检测天赋冲突)
|
|
||||||
点数分配(天赋额外点数)
|
|
||||||
游戏循环
|
|
||||||
年龄增加一岁
|
|
||||||
天赋触发
|
|
||||||
事件触发(从对应年龄事件池随机事件)
|
|
||||||
判断是否结束(生命值小于 1 结束)
|
|
||||||
总评
|
|
||||||
天赋继承
|
|
||||||
存档数据
|
|
||||||
重回首页
|
|
||||||
@@ -188,16 +188,3 @@ function checkProp(property: PropertyContainer, condition: string): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractMaxTriggers(condition: string): number {
|
|
||||||
// Assuming only age related talents can be triggered multiple times.
|
|
||||||
const RE_AGE_CONDITION = /AGE\?\[([0-9,]+)\]/
|
|
||||||
const matchObject = RE_AGE_CONDITION.exec(condition)
|
|
||||||
|
|
||||||
if (matchObject === null) {
|
|
||||||
// Not age related, single trigger.
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchObject[1]?.split(',')?.length ?? 1
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe('Achievement', () => {
|
|||||||
intelligence: 5,
|
intelligence: 5,
|
||||||
strength: 5,
|
strength: 5,
|
||||||
money: 5,
|
money: 5,
|
||||||
|
spirit: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
test('trigger [times:500]', () => {
|
test('trigger [times:500]', () => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ describe('Event', () => {
|
|||||||
intelligence: 10,
|
intelligence: 10,
|
||||||
strength: 0,
|
strength: 0,
|
||||||
money: 0,
|
money: 0,
|
||||||
|
spirit: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
test('branch 10003:0 [玉佩]', () => {
|
test('branch 10003:0 [玉佩]', () => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe('Achievement', () => {
|
|||||||
intelligence: 5,
|
intelligence: 5,
|
||||||
strength: 5,
|
strength: 5,
|
||||||
money: 5,
|
money: 5,
|
||||||
|
spirit: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
test('talentsPicked', () => {
|
test('talentsPicked', () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { expect, test, describe } from 'bun:test'
|
|||||||
import { parse, type ConditionNode } from '@remake/condition'
|
import { parse, type ConditionNode } from '@remake/condition'
|
||||||
|
|
||||||
import { achievements, events, talents } from '@remake/data'
|
import { achievements, events, talents } from '@remake/data'
|
||||||
import type { GameState, ProfileState, Properties } from './state'
|
import type { FlatState, GameState, ProfileState, Properties } from './state'
|
||||||
import { createFlatState, createHLProperties, propsEffect } from './state'
|
import { createFlatState, createHLProperties, propsEffect } from './state'
|
||||||
import { SupportedFlatStateKeys } from './state'
|
import { SupportedFlatStateKeys } from './state'
|
||||||
import { enableMapSet } from 'immer'
|
import { enableMapSet } from 'immer'
|
||||||
@@ -11,7 +11,10 @@ enableMapSet()
|
|||||||
/**
|
/**
|
||||||
* 核心递归辅助函数:从多维语法树节点中提取所有属性名
|
* 核心递归辅助函数:从多维语法树节点中提取所有属性名
|
||||||
*/
|
*/
|
||||||
function extractKeysFromNode(node: ConditionNode, properties: Set<string>) {
|
function extractKeysFromNode(
|
||||||
|
node: ConditionNode,
|
||||||
|
properties: Set<keyof FlatState>,
|
||||||
|
) {
|
||||||
if (Array.isArray(node)) {
|
if (Array.isArray(node)) {
|
||||||
for (const subNode of node) {
|
for (const subNode of node) {
|
||||||
extractKeysFromNode(subNode, properties)
|
extractKeysFromNode(subNode, properties)
|
||||||
@@ -22,7 +25,7 @@ function extractKeysFromNode(node: ConditionNode, properties: Set<string>) {
|
|||||||
// 匹配操作符(><!?=)前面的属性名部分
|
// 匹配操作符(><!?=)前面的属性名部分
|
||||||
const match = node.match(/^([^><!?=]+)/)
|
const match = node.match(/^([^><!?=]+)/)
|
||||||
if (match && match) {
|
if (match && match) {
|
||||||
properties.add(match[1]!.trim())
|
properties.add(match[1]!.trim() as keyof FlatState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,9 +33,9 @@ function extractKeysFromNode(node: ConditionNode, properties: Set<string>) {
|
|||||||
/**
|
/**
|
||||||
* 从条件字符串中提取所有使用到的属性键名
|
* 从条件字符串中提取所有使用到的属性键名
|
||||||
*/
|
*/
|
||||||
function getPropertiesFromCondition(condition: string): Set<string> {
|
function getPropertiesFromCondition(condition: string): Set<keyof FlatState> {
|
||||||
const parsedConditions = parse(condition)
|
const parsedConditions = parse(condition)
|
||||||
const properties = new Set<string>()
|
const properties = new Set<keyof FlatState>()
|
||||||
extractKeysFromNode(parsedConditions, properties)
|
extractKeysFromNode(parsedConditions, properties)
|
||||||
return properties
|
return properties
|
||||||
}
|
}
|
||||||
@@ -205,7 +208,13 @@ describe('State', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const allocation = { charm: 5, intelligence: 5, strength: 5, money: 5 }
|
const allocation = {
|
||||||
|
charm: 5,
|
||||||
|
intelligence: 5,
|
||||||
|
strength: 5,
|
||||||
|
money: 5,
|
||||||
|
spirit: 5,
|
||||||
|
}
|
||||||
test('createHLProperties', () => {
|
test('createHLProperties', () => {
|
||||||
const props = createHLProperties(allocation)
|
const props = createHLProperties(allocation)
|
||||||
expect(props.current.charm).toBe(allocation.charm)
|
expect(props.current.charm).toBe(allocation.charm)
|
||||||
@@ -213,7 +222,7 @@ describe('State', () => {
|
|||||||
expect(props.current.strength).toBe(allocation.strength)
|
expect(props.current.strength).toBe(allocation.strength)
|
||||||
expect(props.current.money).toBe(allocation.money)
|
expect(props.current.money).toBe(allocation.money)
|
||||||
expect(props.current.age).toBe(-1)
|
expect(props.current.age).toBe(-1)
|
||||||
expect(props.current.spirit).toBe(0)
|
expect(props.current.spirit).toBe(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('propsEffect', () => {
|
test('propsEffect', () => {
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ describe('Talent', () => {
|
|||||||
|
|
||||||
test('pull external', () => {
|
test('pull external', () => {
|
||||||
const p = produce(profile, draft => {
|
const p = produce(profile, draft => {
|
||||||
draft.lockedTalent = 1001
|
draft.locked = [1001]
|
||||||
})
|
})
|
||||||
const result = pull(options, p)
|
const result = pull(options, p)
|
||||||
expect(result.length).toBe(options.count)
|
expect(result.length).toBe(options.count)
|
||||||
expect(result).toContain(p.lockedTalent!)
|
expect(result).toContain(p.locked![0]!)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('pull rate', () => {
|
test('pull rate', () => {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## Bugs
|
||||||
|
|
||||||
|
### 复活Bug
|
||||||
|
age 400
|
||||||
|
-> 寿元终
|
||||||
|
-> 天赋复活
|
||||||
|
age 401
|
||||||
|
-> 无可随机事件
|
||||||
|
-> 报错
|
||||||
@@ -47,6 +47,8 @@ export type Event = {
|
|||||||
readonly exclude?: string
|
readonly exclude?: string
|
||||||
/** 分支路线 */
|
/** 分支路线 */
|
||||||
readonly branch?: EventBranch[]
|
readonly branch?: EventBranch[]
|
||||||
|
/** 是否需要格式化 */
|
||||||
|
readonly format?: boolean
|
||||||
}
|
}
|
||||||
// @vt-types-end
|
// @vt-types-end
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -43,3 +43,18 @@ export function keys<T extends object, K extends keyof T = never>(
|
|||||||
const filterSet = new Set<keyof T>(filter)
|
const filterSet = new Set<keyof T>(filter)
|
||||||
return allKeys.filter(key => !filterSet.has(key)) as Exclude<keyof T, K>[]
|
return allKeys.filter(key => !filterSet.has(key)) as Exclude<keyof T, K>[]
|
||||||
}
|
}
|
||||||
|
// 定义获取字段值的方法签名
|
||||||
|
export type GetValueFn = (
|
||||||
|
fieldName: string,
|
||||||
|
) => string | number | boolean | null | undefined
|
||||||
|
export function format(str: string, getValueFn: GetValueFn): string {
|
||||||
|
if (!str) return ''
|
||||||
|
return str.replace(
|
||||||
|
/\{([^}]+)\}/g,
|
||||||
|
(match: string, fieldName: string): string => {
|
||||||
|
const value = getValueFn(fieldName)
|
||||||
|
if (value === undefined || value === null) return match
|
||||||
|
return String(value)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user