-
-
Notifications
You must be signed in to change notification settings - Fork 70
feat: add incremental save button #477
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -4,10 +4,14 @@ import { cloneDeep } from "lodash"; | |||||
import { CloseSmall } from "@icon-park/react"; | ||||||
import IconWrapper from "@/components/iconWrapper/IconWrapper"; | ||||||
import { getFileIcon } from "@/utils/getFileIcon"; | ||||||
import React, { useRef } from "react"; | ||||||
import React, { useMemo, useRef } from "react"; | ||||||
import { useGameEditorContext } from "@/store/useGameEditorStore"; | ||||||
import { ITag } from "@/types/gameEditor"; | ||||||
import { Tooltip } from "@fluentui/react-components"; | ||||||
import { Button, Tooltip } from "@fluentui/react-components"; | ||||||
import { api } from "@/api"; | ||||||
import useEditorStore from "@/store/useEditorStore"; | ||||||
import { useSWRConfig } from "swr"; | ||||||
import { t } from "@lingui/macro"; | ||||||
|
||||||
export default function TagsManager() { | ||||||
// 获取 Tags 数据 | ||||||
|
@@ -77,58 +81,94 @@ export default function TagsManager() { | |||||
|
||||||
const containerRef = useRef<HTMLDivElement>(null); | ||||||
|
||||||
const { mutate } = useSWRConfig(); | ||||||
const handleRefresh = (path: string) => mutate(path); | ||||||
const gameDir = useEditorStore.use.subPage(); | ||||||
const basePath = useMemo(() => ['games', gameDir, 'game'], [gameDir]); | ||||||
|
||||||
return ( | ||||||
<> | ||||||
{ | ||||||
(tags.length > 0) && | ||||||
<DragDropContext onDragEnd={onDragEnd}> | ||||||
<Droppable droppableId="droppable" direction="horizontal"> | ||||||
{(provided, snapshot) => ( | ||||||
// 下面开始书写容器 | ||||||
<div className={styles.tagsContainer} | ||||||
id="tags-container" | ||||||
onWheel={handleScroll} | ||||||
// provided.droppableProps应用的相同元素. | ||||||
{...provided.droppableProps} | ||||||
// 为了使 droppable 能够正常工作必须 绑定到最高可能的DOM节点中provided.innerRef. | ||||||
ref={provided.innerRef} | ||||||
> | ||||||
{tags.map((item, index) => ( | ||||||
<Draggable key={item.path} draggableId={item.path} index={index}> | ||||||
{(provided, snapshot) => ( | ||||||
// 下面开始书写可拖拽的元素 | ||||||
<Tooltip content={item.path} relationship='label' positioning='below-start'> | ||||||
<div | ||||||
onClick={() => selectTag(item)} | ||||||
onMouseDown={(event: any) => { | ||||||
if (event.button === 1) { | ||||||
closeTag(event, item); | ||||||
} | ||||||
}} | ||||||
className={item.path === currentTag?.path ? `${styles.tag} ${styles.tag_active}` : styles.tag} | ||||||
ref={provided.innerRef} | ||||||
{...provided.draggableProps} | ||||||
{...provided.dragHandleProps} | ||||||
> | ||||||
<IconWrapper src={getFileIcon(item.path)} size={24} iconSize={18} /> | ||||||
<div> | ||||||
{item.name} | ||||||
</div> | ||||||
<div className={styles.closeIcon} onClick={(event: any) => closeTag(event, item)}> | ||||||
<CloseSmall theme="outline" size="15" strokeWidth={3} /> | ||||||
{ (tags.length > 0) && ( | ||||||
<div className={styles.tagsManager}> | ||||||
<DragDropContext onDragEnd={onDragEnd}> | ||||||
<Droppable droppableId="droppable" direction="horizontal"> | ||||||
{(provided, snapshot) => ( | ||||||
// 下面开始书写容器 | ||||||
<div className={styles.tagsContainer} | ||||||
id="tags-container" | ||||||
onWheel={handleScroll} | ||||||
// provided.droppableProps应用的相同元素. | ||||||
{...provided.droppableProps} | ||||||
// 为了使 droppable 能够正常工作必须 绑定到最高可能的DOM节点中provided.innerRef. | ||||||
ref={provided.innerRef} | ||||||
> | ||||||
{tags.map((item, index) => ( | ||||||
<Draggable key={item.path} draggableId={item.path} index={index}> | ||||||
{(provided, snapshot) => ( | ||||||
// 下面开始书写可拖拽的元素 | ||||||
<Tooltip content={item.path} relationship='label' positioning='below-start'> | ||||||
<div | ||||||
onClick={() => selectTag(item)} | ||||||
onMouseDown={(event: any) => { | ||||||
if (event.button === 1) { | ||||||
closeTag(event, item); | ||||||
} | ||||||
}} | ||||||
className={item.path === currentTag?.path ? `${styles.tag} ${styles.tag_active}` : styles.tag} | ||||||
ref={provided.innerRef} | ||||||
{...provided.draggableProps} | ||||||
{...provided.dragHandleProps} | ||||||
> | ||||||
<IconWrapper src={getFileIcon(item.path)} size={24} iconSize={18} /> | ||||||
<div> | ||||||
{item.name} | ||||||
</div> | ||||||
<div className={styles.closeIcon} onClick={(event: any) => closeTag(event, item)}> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
<CloseSmall theme="outline" size="15" strokeWidth={3} /> | ||||||
</div> | ||||||
</div> | ||||||
</div> | ||||||
</Tooltip> | ||||||
)} | ||||||
</Draggable> | ||||||
))} | ||||||
{provided.placeholder} | ||||||
</div> | ||||||
)} | ||||||
</Droppable> | ||||||
</DragDropContext> | ||||||
} | ||||||
</Tooltip> | ||||||
)} | ||||||
</Draggable> | ||||||
))} | ||||||
{provided.placeholder} | ||||||
</div> | ||||||
)} | ||||||
</Droppable> | ||||||
</DragDropContext> | ||||||
<Tooltip | ||||||
content={ | ||||||
<div className={styles.tooltip}> | ||||||
{t`此选项可将当前文件另存备份,防止因原文件意外损坏而丢失所有数据。`} | ||||||
</div>} | ||||||
relationship='description' | ||||||
showDelay={750} | ||||||
hideDelay={0} | ||||||
> | ||||||
<Button | ||||||
appearance="transparent" | ||||||
style={{ display: 'flex', flexShrink: 0 }} | ||||||
onClick={() => { | ||||||
if (!currentTag?.path) return; | ||||||
const targetPath = [ | ||||||
...basePath, | ||||||
currentTag.path.startsWith(basePath.join('/')) | ||||||
? currentTag.path.slice(basePath.join('/').length + 1) | ||||||
: currentTag.path, | ||||||
].join('/'); | ||||||
api.assetsControllerCopyFileWithIncrement({ source: targetPath }).then(() => { | ||||||
// 提取目录路径 | ||||||
const dirPath = targetPath.split('/').slice(0, -1).join('/'); | ||||||
// 刷新 Assets 组件 | ||||||
handleRefresh(dirPath); | ||||||
}); | ||||||
}} | ||||||
> | ||||||
{t`增量保存`} | ||||||
</Button> | ||||||
</Tooltip> | ||||||
</div> | ||||||
)} | ||||||
</> | ||||||
|
||||||
); | ||||||
} |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -1,6 +1,6 @@ | ||||||||
import { ConsoleLogger, Injectable } from '@nestjs/common'; | ||||||||
import * as fs from 'fs/promises'; | ||||||||
import { dirname, extname, join } from 'path'; | ||||||||
import { basename, dirname, extname, join } from 'path'; | ||||||||
|
||||||||
export interface IFileInfo { | ||||||||
name: string; | ||||||||
|
@@ -335,4 +335,32 @@ export class WebgalFsService { | |||||||
return false; | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* 复制文件并以“原文件名_编号.扩展名”方式增量保存 | ||||||||
*/ | ||||||||
async copyFileWithIncrement(filePath: string): Promise<string> { | ||||||||
const dir = dirname(filePath); | ||||||||
const ext = extname(filePath); | ||||||||
const base = basename(filePath, ext); | ||||||||
|
||||||||
// 读取目录下所有文件 | ||||||||
const files = await fs.readdir(dir); | ||||||||
// 匹配类似 xxx_序号.txt 的文件 | ||||||||
const regex = new RegExp(`^${base}_(\\d+)${ext.replace('.', '\\.')}$`); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 在构建正则表达式时,
Suggested change
|
||||||||
let maxNum = 0; | ||||||||
for (const file of files) { | ||||||||
const match = file.match(regex); | ||||||||
if (match) { | ||||||||
const num = parseInt(match[1], 10); | ||||||||
if (num > maxNum) maxNum = num; | ||||||||
} | ||||||||
} | ||||||||
const nextNum = (maxNum + 1).toString().padStart(3, '0'); | ||||||||
const newName = `${base}_${nextNum}${ext}`; | ||||||||
const newPath = join(dir, newName); | ||||||||
|
||||||||
await fs.copyFile(filePath, newPath); | ||||||||
return newPath; | ||||||||
} | ||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
event
参数的类型是any
。为了更好的类型安全和代码可读性,建议使用更具体的React.MouseEvent
类型。另外,closeTag
函数期望一个原生的MouseEvent
,所以应该传递event.nativeEvent
。