mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-30 08:01:02 +00:00
feat: support file remark ability in file management (#11626)
* feat: Add file remark ability in file management * refactor: improve mark get logic * feat: Implement remark filtering for file management buttons
This commit is contained in:
@@ -978,3 +978,40 @@ func (b *BaseApi) ConvertLog(c *gin.Context) {
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Batch get file remarks
|
||||
// @Accept json
|
||||
// @Param request body request.FileRemarkBatch true "request"
|
||||
// @Success 200 {object} response.FileRemarksRes
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/remarks [post]
|
||||
func (b *BaseApi) BatchGetFileRemarks(c *gin.Context) {
|
||||
var req request.FileRemarkBatch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
remarks := fileService.BatchGetRemarks(req)
|
||||
helper.SuccessWithData(c, response.FileRemarksRes{Remarks: remarks})
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Set file remark
|
||||
// @Accept json
|
||||
// @Param request body request.FileRemarkUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/remark [post]
|
||||
func (b *BaseApi) SetFileRemark(c *gin.Context) {
|
||||
var req request.FileRemarkUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := fileService.SetRemark(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
@@ -163,3 +163,12 @@ type FileConvertRequest struct {
|
||||
DeleteSource bool `json:"deleteSource"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
type FileRemarkBatch struct {
|
||||
Paths []string `json:"paths" validate:"required"`
|
||||
}
|
||||
|
||||
type FileRemarkUpdate struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
@@ -81,3 +81,7 @@ type FileConvertLog struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type FileRemarksRes struct {
|
||||
Remarks map[string]string `json:"remarks"`
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -75,12 +76,19 @@ type IFileService interface {
|
||||
GetUsersAndGroups() (*response.UserGroupResponse, error)
|
||||
Convert(req request.FileConvertRequest)
|
||||
ConvertLog(req dto.PageInfo) (int64, []response.FileConvertLog, error)
|
||||
BatchGetRemarks(req request.FileRemarkBatch) map[string]string
|
||||
SetRemark(req request.FileRemarkUpdate) error
|
||||
}
|
||||
|
||||
var filteredPaths = []string{
|
||||
"/.1panel_clash",
|
||||
}
|
||||
|
||||
const (
|
||||
fileRemarkXattr = "user.1panel.remark"
|
||||
fileRemarkEncodedMaxLen = 256
|
||||
)
|
||||
|
||||
func NewIFileService() IFileService {
|
||||
return &FileService{}
|
||||
}
|
||||
@@ -760,6 +768,52 @@ func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *FileService) BatchGetRemarks(req request.FileRemarkBatch) map[string]string {
|
||||
remarks := make(map[string]string)
|
||||
for _, filePath := range req.Paths {
|
||||
remark, err := getFileRemark(filePath)
|
||||
if err != nil {
|
||||
if isXattrNotSupported(err) {
|
||||
return map[string]string{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if remark == "" {
|
||||
continue
|
||||
}
|
||||
remarks[filePath] = remark
|
||||
}
|
||||
|
||||
return remarks
|
||||
}
|
||||
|
||||
func (f *FileService) SetRemark(req request.FileRemarkUpdate) error {
|
||||
if req.Remark == "" {
|
||||
if err := unix.Lremovexattr(req.Path, fileRemarkXattr); err != nil {
|
||||
if isXattrNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
if isXattrNotSupported(err) {
|
||||
return buserr.WithDetail("ErrInvalidParams", "xattr not supported", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(req.Remark))
|
||||
if len(encoded) >= fileRemarkEncodedMaxLen {
|
||||
return buserr.WithDetail("ErrInvalidParams", "remark length must be less than 256", nil)
|
||||
}
|
||||
if err := unix.Lsetxattr(req.Path, fileRemarkXattr, []byte(encoded), 0); err != nil {
|
||||
if isXattrNotSupported(err) {
|
||||
return buserr.WithDetail("ErrInvalidParams", "xattr not supported", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getValidGroups() (map[string]bool, error) {
|
||||
groupFile, err := os.Open("/etc/group")
|
||||
if err != nil {
|
||||
@@ -786,6 +840,37 @@ func getValidGroups() (map[string]bool, error) {
|
||||
return groupMap, nil
|
||||
}
|
||||
|
||||
func getFileRemark(filePath string) (string, error) {
|
||||
size, err := unix.Lgetxattr(filePath, fileRemarkXattr, nil)
|
||||
if err != nil {
|
||||
if isXattrNotFound(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if size == 0 {
|
||||
return "", nil
|
||||
}
|
||||
buf := make([]byte, size)
|
||||
n, err := unix.Lgetxattr(filePath, fileRemarkXattr, buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(string(buf[:n]))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decoded), nil
|
||||
}
|
||||
|
||||
func isXattrNotSupported(err error) bool {
|
||||
return errors.Is(err, unix.ENOTSUP) || errors.Is(err, unix.EOPNOTSUPP)
|
||||
}
|
||||
|
||||
func isXattrNotFound(err error) bool {
|
||||
return errors.Is(err, unix.ENODATA)
|
||||
}
|
||||
|
||||
func getValidUsers(validGroups map[string]bool) ([]response.UserInfo, map[string]struct{}, error) {
|
||||
passwdFile, err := os.Open("/etc/passwd")
|
||||
if err != nil {
|
||||
|
||||
@@ -25,6 +25,8 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
fileRouter.POST("/content", baseApi.GetContent)
|
||||
fileRouter.POST("/preview", baseApi.PreviewContent)
|
||||
fileRouter.POST("/save", baseApi.SaveContent)
|
||||
fileRouter.POST("/remarks", baseApi.BatchGetFileRemarks)
|
||||
fileRouter.POST("/remark", baseApi.SetFileRemark)
|
||||
fileRouter.POST("/check", baseApi.CheckFile)
|
||||
fileRouter.POST("/batch/check", baseApi.BatchCheckFiles)
|
||||
fileRouter.POST("/upload", baseApi.UploadFiles)
|
||||
|
||||
@@ -23,6 +23,7 @@ export namespace File {
|
||||
extension: string;
|
||||
itemTotal: number;
|
||||
favoriteID: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface ReqFile extends ReqPage {
|
||||
@@ -219,6 +220,15 @@ export namespace File {
|
||||
sub: boolean;
|
||||
}
|
||||
|
||||
export interface FileRemarkUpdate {
|
||||
path: string;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface FileRemarksRes {
|
||||
remarks: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface UserGroupResponse {
|
||||
users: UserInfo[];
|
||||
groups: string[];
|
||||
|
||||
@@ -74,6 +74,14 @@ export const batchCheckFiles = (paths: string[]) => {
|
||||
return http.post<File.ExistFileInfo[]>('files/batch/check', { paths: paths }, TimeoutEnum.T_5M);
|
||||
};
|
||||
|
||||
export const batchGetFileRemarks = (paths: string[]) => {
|
||||
return http.post<File.FileRemarksRes>('files/remarks', { paths: paths }, TimeoutEnum.T_5M);
|
||||
};
|
||||
|
||||
export const setFileRemark = (params: File.FileRemarkUpdate) => {
|
||||
return http.post('files/remark', params);
|
||||
};
|
||||
|
||||
export const chunkUploadFileData = (params: FormData, config: AxiosRequestConfig) => {
|
||||
return http.upload<File.File>('files/chunkupload', params, config);
|
||||
};
|
||||
|
||||
@@ -1551,6 +1551,12 @@ const message = {
|
||||
pasteMsg: 'Please click the [Paste] button at the top right of the target directory',
|
||||
move: 'Move',
|
||||
calculate: 'Calculate',
|
||||
remark: 'Remark',
|
||||
setRemark: 'Set remark',
|
||||
remarkPrompt: 'Enter a remark',
|
||||
remarkPlaceholder: 'Remark',
|
||||
remarkToggle: 'Remarks',
|
||||
remarkToggleTip: 'Load file remarks',
|
||||
canNotDeCompress: 'Cannot decompress this file',
|
||||
uploadSuccess: 'Successfully upload',
|
||||
downloadProcess: 'Download progress',
|
||||
|
||||
@@ -1559,6 +1559,12 @@ const message = {
|
||||
pasteMsg: 'Por favor haz clic en el botón [Pegar] en la parte superior derecha del directorio de destino',
|
||||
move: 'Mover',
|
||||
calculate: 'Calcular',
|
||||
remark: 'Observación',
|
||||
setRemark: 'Establecer observación',
|
||||
remarkPrompt: 'Ingrese una observación',
|
||||
remarkPlaceholder: 'Observación',
|
||||
remarkToggle: 'Notas',
|
||||
remarkToggleTip: 'Cargar notas del archivo',
|
||||
canNotDeCompress: 'No se puede descomprimir este archivo',
|
||||
uploadSuccess: 'Carga completada correctamente',
|
||||
downloadProcess: 'Progreso de descarga',
|
||||
|
||||
@@ -1505,6 +1505,12 @@ const message = {
|
||||
pasteMsg: '対象ディレクトリの右上にある「貼り付け」ボタンをクリックしてください',
|
||||
move: '動く',
|
||||
calculate: '計算します',
|
||||
remark: '備考',
|
||||
setRemark: '備考を設定',
|
||||
remarkPrompt: '備考を入力してください',
|
||||
remarkPlaceholder: '備考',
|
||||
remarkToggle: '備考',
|
||||
remarkToggleTip: 'ファイルの備考を読み込む',
|
||||
canNotDeCompress: 'このファイルを解凍できません',
|
||||
uploadSuccess: '正常にアップロードします',
|
||||
downloadProcess: '進捗状況をダウンロードします',
|
||||
|
||||
@@ -1485,6 +1485,12 @@ const message = {
|
||||
pasteMsg: '대상 디렉토리의 오른쪽 상단에 있는 [붙여넣기] 버튼을 클릭하세요',
|
||||
move: '이동',
|
||||
calculate: '계산',
|
||||
remark: '비고',
|
||||
setRemark: '비고 설정',
|
||||
remarkPrompt: '비고를 입력하세요',
|
||||
remarkPlaceholder: '비고',
|
||||
remarkToggle: '비고',
|
||||
remarkToggleTip: '파일 비고 로드',
|
||||
canNotDeCompress: '이 파일은 압축 해제할 수 없습니다',
|
||||
uploadSuccess: '업로드 성공',
|
||||
downloadProcess: '다운로드 진행률',
|
||||
|
||||
@@ -1545,6 +1545,12 @@ const message = {
|
||||
pasteMsg: 'Sila klik butang "Tampal" di bahagian kanan atas direktori sasaran',
|
||||
move: 'Pindah',
|
||||
calculate: 'Kira',
|
||||
remark: 'Catatan',
|
||||
setRemark: 'Tetapkan catatan',
|
||||
remarkPrompt: 'Masukkan catatan',
|
||||
remarkPlaceholder: 'Catatan',
|
||||
remarkToggle: 'Catatan',
|
||||
remarkToggleTip: 'Muatkan catatan fail',
|
||||
canNotDeCompress: 'Tidak dapat nyahmampatkan fail ini',
|
||||
uploadSuccess: 'Berjaya dimuat naik',
|
||||
downloadProcess: 'Kemajuan muat turun',
|
||||
|
||||
@@ -1533,6 +1533,12 @@ const message = {
|
||||
pasteMsg: 'Clique no botão "Colar" no canto superior direito do diretório de destino',
|
||||
move: 'Mover',
|
||||
calculate: 'Calcular',
|
||||
remark: 'Observação',
|
||||
setRemark: 'Definir observação',
|
||||
remarkPrompt: 'Digite uma observação',
|
||||
remarkPlaceholder: 'Observação',
|
||||
remarkToggle: 'Observações',
|
||||
remarkToggleTip: 'Carregar observações do arquivo',
|
||||
canNotDeCompress: 'Não é possível descompactar este arquivo',
|
||||
uploadSuccess: 'Upload bem-sucedido',
|
||||
downloadProcess: 'Progresso do download',
|
||||
|
||||
@@ -1537,6 +1537,12 @@ const message = {
|
||||
pasteMsg: 'Нажмите кнопку «Вставить» в правом верхнем углу целевой директории',
|
||||
move: 'Переместить',
|
||||
calculate: 'Вычислить',
|
||||
remark: 'Примечание',
|
||||
setRemark: 'Задать примечание',
|
||||
remarkPrompt: 'Введите примечание',
|
||||
remarkPlaceholder: 'Примечание',
|
||||
remarkToggle: 'Примечания',
|
||||
remarkToggleTip: 'Загружать примечания файлов',
|
||||
canNotDeCompress: 'Невозможно распаковать этот файл',
|
||||
uploadSuccess: 'Успешно загружено',
|
||||
downloadProcess: 'Прогресс загрузки',
|
||||
|
||||
@@ -1570,6 +1570,12 @@ const message = {
|
||||
pasteMsg: 'Hedef dizinin sağ üst köşesindeki "Yapıştır" düğmesine tıklayın',
|
||||
move: 'Taşı',
|
||||
calculate: 'Hesapla',
|
||||
remark: 'Not',
|
||||
setRemark: 'Not ekle',
|
||||
remarkPrompt: 'Bir not girin',
|
||||
remarkPlaceholder: 'Not',
|
||||
remarkToggle: 'Notlar',
|
||||
remarkToggleTip: 'Dosya notlarını yükle',
|
||||
canNotDeCompress: 'Bu dosyanın sıkıştırması açılamaz',
|
||||
uploadSuccess: 'Başarıyla yüklendi',
|
||||
downloadProcess: 'İndirme ilerlemesi',
|
||||
|
||||
@@ -1470,6 +1470,12 @@ const message = {
|
||||
pasteMsg: '請在目標目錄點擊右上角【貼上】按鈕',
|
||||
move: '移動',
|
||||
calculate: '計算',
|
||||
remark: '備註',
|
||||
setRemark: '設定備註',
|
||||
remarkPrompt: '請輸入備註內容',
|
||||
remarkPlaceholder: '備註內容',
|
||||
remarkToggle: '備註',
|
||||
remarkToggleTip: '載入檔案備註',
|
||||
canNotDeCompress: '無法解壓此文件',
|
||||
uploadSuccess: '上傳成功!',
|
||||
downloadProcess: '下載進度',
|
||||
|
||||
@@ -1473,6 +1473,12 @@ const message = {
|
||||
pasteMsg: '请在目标目录点击右上角【粘贴】按钮',
|
||||
move: '移动',
|
||||
calculate: '计算',
|
||||
remark: '备注',
|
||||
setRemark: '设置备注',
|
||||
remarkPrompt: '请输入备注内容',
|
||||
remarkPlaceholder: '备注内容',
|
||||
remarkToggle: '备注',
|
||||
remarkToggleTip: '加载文件备注',
|
||||
canNotDeCompress: '无法解压此文件',
|
||||
uploadSuccess: '上传成功!',
|
||||
downloadProcess: '下载进度',
|
||||
|
||||
@@ -526,20 +526,14 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.user')"
|
||||
:label="`${$t('commons.table.user')} / ${$t('file.group')}`"
|
||||
prop="user"
|
||||
show-overflow-tooltip
|
||||
min-width="90"
|
||||
min-width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link underline="never" @click="openChown(row)">
|
||||
{{ row.user ? row.user : '-' }} ({{ row.uid }})
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('file.group')" prop="group" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link underline="never" @click="openChown(row)">
|
||||
{{ row.user ? row.user : '-' }} ({{ row.uid }}) /
|
||||
{{ row.group ? row.group : '-' }} ({{ row.gid }})
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -573,6 +567,16 @@
|
||||
show-overflow-tooltip
|
||||
:sortable="'custom'"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('file.remark')"
|
||||
prop="remark"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.remark ? row.remark : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations
|
||||
:max-height="dropdownMaxHeight"
|
||||
:ellipsis="mobile ? 0 : 2"
|
||||
@@ -641,11 +645,13 @@
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
|
||||
import {
|
||||
addFavorite,
|
||||
batchGetFileRemarks,
|
||||
computeDepthDirSize,
|
||||
computeDirSize,
|
||||
fileWgetKeys,
|
||||
getFileContent,
|
||||
getFilesList,
|
||||
setFileRemark,
|
||||
removeFavorite,
|
||||
renameRile,
|
||||
searchFavorite,
|
||||
@@ -715,6 +721,8 @@ const heightDiff = ref(365);
|
||||
const fileTableRef = ref<HTMLElement | null>(null);
|
||||
const dropdownMaxHeight = ref(450);
|
||||
const baseDir = ref();
|
||||
const remarkRequestId = ref(0);
|
||||
const remarkLoadTimer = ref<number | null>(null);
|
||||
const editableTabsKey = ref('');
|
||||
const editableTabs = ref([
|
||||
{ id: '1', name: getLastPath(baseDir.value), path: baseDir.value },
|
||||
@@ -881,6 +889,7 @@ const handleSearchResult = (res: ResultData<File.File>) => {
|
||||
dirNum.value = data.value.filter((item) => item.isDir).length;
|
||||
fileNum.value = data.value.filter((item) => !item.isDir).length;
|
||||
req.path = res.data.path;
|
||||
scheduleRemarkLoad();
|
||||
};
|
||||
|
||||
const viewHideFile = async () => {
|
||||
@@ -1560,6 +1569,23 @@ const openWithVSCode = (row: File.File) => {
|
||||
dialogVscodeOpenRef.value.acceptParams({ path: row.path + (row.isDir ? '' : ':1:1') });
|
||||
};
|
||||
|
||||
const openRemark = async (row: File.File) => {
|
||||
try {
|
||||
const res = await ElMessageBox.prompt(i18n.global.t('file.remarkPrompt'), i18n.global.t('file.setRemark'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
inputValue: row.remark ?? '',
|
||||
inputPlaceholder: i18n.global.t('file.remarkPlaceholder'),
|
||||
});
|
||||
const remark = res.value ?? '';
|
||||
await setFileRemark({ path: row.path, remark: remark });
|
||||
row.remark = remark;
|
||||
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const beforeButtons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.open'),
|
||||
@@ -1621,6 +1647,13 @@ const beforeButtons = [
|
||||
openBatchRole([row]);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('file.setRemark'),
|
||||
hideOnRemarkBlackList: true,
|
||||
click: (row: File.File) => {
|
||||
openRemark(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
const afterButtons = [
|
||||
{
|
||||
@@ -1682,8 +1715,15 @@ const moreBtnRename = [
|
||||
},
|
||||
];
|
||||
|
||||
const rightButtons = [...beforeButtons, ...rightBtnRename, ...afterButtons];
|
||||
const tableMoreButtons = [...beforeButtons, ...moreBtnRename, ...afterButtons];
|
||||
const filterRemarkButtons = (buttons: any[]) => {
|
||||
if (!isInRemarkBlackList(req.path)) {
|
||||
return buttons;
|
||||
}
|
||||
return buttons.filter((btn) => !btn.hideOnRemarkBlackList);
|
||||
};
|
||||
|
||||
const rightButtons = computed(() => filterRemarkButtons([...beforeButtons, ...rightBtnRename, ...afterButtons]));
|
||||
const tableMoreButtons = computed(() => filterRemarkButtons([...beforeButtons, ...moreBtnRename, ...afterButtons]));
|
||||
const openConvert = (item: File.File) => {
|
||||
if (!ffmpegExist.value) {
|
||||
ElMessageBox.confirm(i18n.global.t('cronjob.library.noSuchApp', ['FFmpeg']), i18n.global.t('file.convert'), {
|
||||
@@ -1768,6 +1808,44 @@ function initShowHidden() {
|
||||
}
|
||||
}
|
||||
|
||||
const remarkBlackList = ['/proc', '/sys', '/dev', '/run'];
|
||||
|
||||
const scheduleRemarkLoad = () => {
|
||||
if (remarkLoadTimer.value) {
|
||||
window.clearTimeout(remarkLoadTimer.value);
|
||||
}
|
||||
if (isInRemarkBlackList(req.path)) {
|
||||
return;
|
||||
}
|
||||
remarkLoadTimer.value = window.setTimeout(() => {
|
||||
void loadRemarksForCurrentPage();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const isInRemarkBlackList = (path: string) => {
|
||||
return remarkBlackList.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
|
||||
};
|
||||
|
||||
const loadRemarksForCurrentPage = async () => {
|
||||
if (!Array.isArray(data.value) || data.value.length === 0) return;
|
||||
const paths = data.value.map((item) => item.path).filter(Boolean);
|
||||
if (paths.length === 0) return;
|
||||
const currentId = ++remarkRequestId.value;
|
||||
try {
|
||||
const res = await batchGetFileRemarks(paths);
|
||||
if (currentId !== remarkRequestId.value) return;
|
||||
const remarks = res.data?.remarks || {};
|
||||
data.value.forEach((item) => {
|
||||
const remark = remarks[item.path];
|
||||
if (remark !== undefined && remark !== '') {
|
||||
item.remark = remark;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (currentId !== remarkRequestId.value) return;
|
||||
}
|
||||
};
|
||||
|
||||
function initTabsAndPaths() {
|
||||
initTabs();
|
||||
let path = getInitialPath();
|
||||
@@ -1967,6 +2045,9 @@ onBeforeUnmount(() => {
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', watchTitleHeight);
|
||||
window.removeEventListener('resize', updateHeight);
|
||||
if (remarkLoadTimer.value) {
|
||||
window.clearTimeout(remarkLoadTimer.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user