fix(web): 优化项目分页与文件图标
This commit is contained in:
parent
dcc2918d60
commit
f748052775
|
|
@ -35,10 +35,29 @@ test("task navigation groups chats by project with restrained icon sizes", () =>
|
|||
assert.match(chatJs, /task-nav-recent/);
|
||||
assert.match(chatJs, /task-project-head/);
|
||||
assert.match(chatJs, /groupedTaskListHtml/);
|
||||
assert.match(chatJs, /loadProjectTasks/);
|
||||
assert.match(chatJs, /working_dir: projectName/);
|
||||
assert.match(chatJs, /state\.taskHasMore = filtered &&/);
|
||||
assert.match(chatJs, /task-project-more/);
|
||||
assert.match(pageHtml, /\.task-nav-icon \{ width: 15px; height: 15px;/);
|
||||
assert.match(pageHtml, /\.task-project-head \.task-nav-icon \{ width: 16px; height: 16px;/);
|
||||
});
|
||||
|
||||
test("file rows render restrained type-specific svg icons", () => {
|
||||
assert.match(filesJs, /function fileIconKind/);
|
||||
assert.match(filesJs, /if \(entry\.is_dir\) return "folder"/);
|
||||
assert.match(filesJs, /word: new Set/);
|
||||
assert.match(filesJs, /sheet: new Set/);
|
||||
assert.match(filesJs, /slide: new Set/);
|
||||
assert.match(filesJs, /if \(ext === "pdf"\) return "pdf"/);
|
||||
assert.match(filesJs, /image: new Set/);
|
||||
assert.match(filesJs, /archive: new Set/);
|
||||
assert.match(filesJs, /code: new Set/);
|
||||
assert.match(filesJs, /file-type-icon type-\$\{kind\}/);
|
||||
assert.match(pageHtml, /\.file-type-icon svg \{ width: 16px; height: 16px;/);
|
||||
assert.doesNotMatch(pageHtml, /\.ico-file::before/);
|
||||
});
|
||||
|
||||
test("current directory supports search, sort, and modification time", () => {
|
||||
assert.match(pageHtml, /id="file-search"/);
|
||||
assert.match(pageHtml, /id="file-sort"/);
|
||||
|
|
|
|||
|
|
@ -699,6 +699,13 @@
|
|||
.task-project-count { flex: none; min-width: 20px; color: var(--muted); font-size: 11px; text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.task-project-items { display: none; padding: 1px 0 5px; }
|
||||
.task-project.open .task-project-items { display: block; }
|
||||
.task-project-state, .task-project-more, .task-project-retry {
|
||||
min-height: 28px; margin: 2px 10px 2px 38px; padding: 4px 8px; color: var(--muted); font-size: 11px;
|
||||
}
|
||||
.task-project-more, .task-project-retry { width: calc(100% - 48px); border: 0; background: transparent;
|
||||
text-align: left; cursor: pointer; border-radius: 4px; }
|
||||
.task-project-more:hover, .task-project-retry:hover { background: var(--hover); color: var(--text); }
|
||||
.task-project-retry { color: var(--danger); }
|
||||
.task-filter-results { padding: 5px 0 10px; }
|
||||
.task-row .desc { font-weight: 500; color: var(--text); margin-bottom: 2px;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
|
@ -1449,8 +1456,17 @@
|
|||
.file-row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.file-row .mtime { color: var(--muted); font-size: 10px; font-family: var(--mono); }
|
||||
.file-row .size { font-size: 11px; color: var(--muted); font-family: var(--mono); }
|
||||
.ico-dir::before { content: "▸ "; color: var(--accent); }
|
||||
.ico-file::before { content: "· "; color: var(--muted); }
|
||||
.file-type-icon { width: 24px; height: 24px; flex: none; display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 5px; color: #747b84; background: #f4f5f6; }
|
||||
.file-type-icon svg { width: 16px; height: 16px; display: block; }
|
||||
.file-type-icon.type-folder { color: #b67812; background: #fff7e5; }
|
||||
.file-type-icon.type-word { color: #3b6fb6; background: #edf4ff; }
|
||||
.file-type-icon.type-sheet { color: #278257; background: #ebf7f0; }
|
||||
.file-type-icon.type-slide { color: #b8642d; background: #fff1e8; }
|
||||
.file-type-icon.type-pdf { color: #bd3f38; background: #fff0ef; }
|
||||
.file-type-icon.type-image { color: #7c5bb3; background: #f4effc; }
|
||||
.file-type-icon.type-archive { color: #80663d; background: #f6f1e9; }
|
||||
.file-type-icon.type-code { color: #4c708b; background: #edf4f7; }
|
||||
|
||||
/* 拖拽上传 overlay:hover 整个 pane-right 时铺一层提示 */
|
||||
#pane-right { position: relative; }
|
||||
|
|
|
|||
|
|
@ -97,6 +97,10 @@ export async function loadModels() {
|
|||
// loadTaskList:默认 reset(filters/refresh/写操作后),append=true 由 sentinel observer 触发
|
||||
// 并发模型:append 受 taskLoading 互斥(避免观察器重复触发);reset 永远抢占,用 seq 丢弃过期响应
|
||||
let _taskLoadSeq = 0;
|
||||
let _globalTaskResults = [];
|
||||
const _projectTaskPages = new Map();
|
||||
const _projectTaskLoading = new Set();
|
||||
|
||||
export async function loadTaskList({ append = false } = {}) {
|
||||
if (append && (state.taskLoading || !state.taskHasMore)) return;
|
||||
const mySeq = ++_taskLoadSeq;
|
||||
|
|
@ -123,7 +127,11 @@ export async function loadTaskList({ append = false } = {}) {
|
|||
const results = data.results || [];
|
||||
if (!append) state.taskLoaded = 0;
|
||||
state.taskLoaded += results.length;
|
||||
state.taskHasMore = state.taskLoaded < state.taskTotal;
|
||||
// 常态项目导航只需首页的最近对话;各项目在展开时独立分页。
|
||||
// 全局 sentinel 只服务搜索/筛选结果,避免折叠项目让它提前入视口而连续拉全量历史。
|
||||
const filtered = isTaskListFiltered();
|
||||
state.taskHasMore = filtered && state.taskLoaded < state.taskTotal;
|
||||
if (!append && !filtered) _projectTaskPages.clear();
|
||||
renderTaskList(results, append);
|
||||
renderTaskCount();
|
||||
subscribeRunningRows(results);
|
||||
|
|
@ -206,12 +214,12 @@ function syncTaskRowRunIndicator(tid) {
|
|||
}
|
||||
}
|
||||
|
||||
const TASK_PROJECTS_EXPANDED_KEY = "zcbot-task-projects-expanded-v1";
|
||||
const TASK_PROJECTS_EXPANDED_KEY = "zcbot-task-projects-expanded-v2";
|
||||
|
||||
function taskProject(t) {
|
||||
const path = t.working_dir || "";
|
||||
const name = path.split("/").filter(Boolean).pop() || "未分组对话";
|
||||
return { key: path || "__ungrouped__", name };
|
||||
return { key: path ? name : "__ungrouped__", name };
|
||||
}
|
||||
|
||||
function taskNavIcon(kind) {
|
||||
|
|
@ -272,13 +280,53 @@ function expandedTaskProjects() {
|
|||
}
|
||||
}
|
||||
|
||||
function groupedTaskListHtml(tasks) {
|
||||
const groups = new Map();
|
||||
function taskProjectGroups(tasks) {
|
||||
const loadedGroups = new Map();
|
||||
for (const task of tasks) {
|
||||
const project = taskProject(task);
|
||||
if (!groups.has(project.key)) groups.set(project.key, { ...project, tasks: [] });
|
||||
groups.get(project.key).tasks.push(task);
|
||||
if (!loadedGroups.has(project.key)) loadedGroups.set(project.key, []);
|
||||
loadedGroups.get(project.key).push(task);
|
||||
}
|
||||
const groups = [];
|
||||
const seen = new Set();
|
||||
for (const folder of state.folders || []) {
|
||||
if (!folder.n_tasks) continue; // 空目录仍在右栏「全部项目」中管理
|
||||
groups.push({
|
||||
key: folder.name,
|
||||
name: folder.name,
|
||||
count: Number(folder.n_tasks) || 0,
|
||||
seedTasks: loadedGroups.get(folder.name) || [],
|
||||
});
|
||||
seen.add(folder.name);
|
||||
}
|
||||
// 容错:磁盘目录索引暂时拉不到时,已返回的 task 仍然可访问。
|
||||
for (const [key, groupTasks] of loadedGroups) {
|
||||
if (!seen.has(key)) groups.push({
|
||||
key,
|
||||
name: key === "__ungrouped__" ? "未分组对话" : key,
|
||||
count: groupTasks.length,
|
||||
seedTasks: groupTasks,
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function projectItemsHtml(group) {
|
||||
const page = _projectTaskPages.get(group.key);
|
||||
const tasks = page ? page.tasks : group.seedTasks;
|
||||
const rows = tasks.map((t) => taskRowHtml(t, { compact: true, nested: true })).join("");
|
||||
const status = page?.error
|
||||
? `<button class="task-project-retry" type="button" data-project-name="${escapeHtml(group.key)}">加载失败,点击重试</button>`
|
||||
: (_projectTaskLoading.has(group.key)
|
||||
? `<div class="task-project-state">加载中…</div>`
|
||||
: (page?.hasMore
|
||||
? `<button class="task-project-more" type="button" data-project-name="${escapeHtml(group.key)}">加载更多</button>`
|
||||
: ""));
|
||||
return rows || status ? `${rows}${status}` : `<div class="task-project-state">暂无对话</div>`;
|
||||
}
|
||||
|
||||
function groupedTaskListHtml(tasks) {
|
||||
const groups = taskProjectGroups(tasks);
|
||||
const expanded = expandedTaskProjects();
|
||||
const activeProject = state.taskId && state.tasksById[state.taskId]
|
||||
? taskProject(state.tasksById[state.taskId]).key : "";
|
||||
|
|
@ -288,21 +336,94 @@ function groupedTaskListHtml(tasks) {
|
|||
<div class="task-nav-section-title">${taskNavIcon("recent")}<span>最近对话</span></div>
|
||||
${recent.map((t) => taskRowHtml(t, { compact: true })).join("")}
|
||||
</section>` : "";
|
||||
const projectsHtml = Array.from(groups.values()).map((group, index) => {
|
||||
const projectsHtml = groups.map((group, index) => {
|
||||
const open = expanded === null ? (group.key === activeProject || index === 0) : expanded.has(group.key);
|
||||
const cachedPage = _projectTaskPages.get(group.key);
|
||||
const exactCount = cachedPage && cachedPage.page > 0 ? cachedPage.count : group.count;
|
||||
return `<section class="task-project${open ? " open" : ""}" data-project-key="${escapeHtml(group.key)}">
|
||||
<button class="task-project-head" type="button" aria-expanded="${open}">
|
||||
<span class="task-project-chevron" aria-hidden="true">›</span>
|
||||
${taskNavIcon("folder")}
|
||||
<span class="task-project-name" title="${escapeHtml(group.key === "__ungrouped__" ? group.name : group.key)}">${escapeHtml(group.name)}</span>
|
||||
<span class="task-project-count">${group.tasks.length}</span>
|
||||
<span class="task-project-name" title="${escapeHtml(group.name)}">${escapeHtml(group.name)}</span>
|
||||
<span class="task-project-count">${exactCount}</span>
|
||||
</button>
|
||||
<div class="task-project-items">${group.tasks.map((t) => taskRowHtml(t, { compact: true, nested: true })).join("")}</div>
|
||||
<div class="task-project-items">${projectItemsHtml(group)}</div>
|
||||
</section>`;
|
||||
}).join("");
|
||||
return `${recentHtml}<div class="task-projects">${projectsHtml}</div>`;
|
||||
}
|
||||
|
||||
async function loadProjectTasks(projectName, { append = false } = {}) {
|
||||
if (!projectName || _projectTaskLoading.has(projectName)) return;
|
||||
const current = _projectTaskPages.get(projectName);
|
||||
if (append && current && !current.hasMore) return;
|
||||
const nextPage = append && current ? current.page + 1 : 1;
|
||||
_projectTaskLoading.add(projectName);
|
||||
_projectTaskPages.set(projectName, {
|
||||
tasks: append && current ? current.tasks : [],
|
||||
page: append && current ? current.page : 0,
|
||||
count: current?.count || 0,
|
||||
hasMore: current?.hasMore ?? true,
|
||||
error: "",
|
||||
});
|
||||
renderTaskNavigation();
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: String(nextPage),
|
||||
page_size: String(state.taskPageSize),
|
||||
working_dir: projectName,
|
||||
});
|
||||
const data = await api("GET", "/v1/tasks?" + params.toString());
|
||||
const results = data.results || [];
|
||||
const previous = append && current ? current.tasks : [];
|
||||
const merged = [...previous];
|
||||
const known = new Set(previous.map((task) => task.task_id));
|
||||
for (const task of results) {
|
||||
state.tasksById[task.task_id] = task;
|
||||
if (!known.has(task.task_id)) merged.push(task);
|
||||
}
|
||||
const count = Number(data.count) || 0;
|
||||
_projectTaskPages.set(projectName, {
|
||||
tasks: merged,
|
||||
page: data.page || nextPage,
|
||||
count,
|
||||
hasMore: merged.length < count,
|
||||
error: "",
|
||||
});
|
||||
subscribeRunningRows(results);
|
||||
} catch (error) {
|
||||
if (error.status === 401) { logout(); return; }
|
||||
_projectTaskPages.set(projectName, {
|
||||
tasks: append && current ? current.tasks : [],
|
||||
page: append && current ? current.page : 0,
|
||||
count: current?.count || 0,
|
||||
hasMore: true,
|
||||
error: error.message || "加载失败",
|
||||
});
|
||||
} finally {
|
||||
_projectTaskLoading.delete(projectName);
|
||||
renderTaskNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
function renderTaskNavigation() {
|
||||
const listEl = $("task-list");
|
||||
if (!_globalTaskResults.length) {
|
||||
listEl.innerHTML = `<div class="empty">(暂无对话)</div>`;
|
||||
return;
|
||||
}
|
||||
if (isTaskListFiltered()) {
|
||||
listEl.innerHTML = `<div class="task-filter-results"><div class="task-nav-section-title">筛选结果</div>${_globalTaskResults.map((t) => taskRowHtml(t)).join("")}</div>`;
|
||||
} else {
|
||||
listEl.innerHTML = groupedTaskListHtml(_globalTaskResults);
|
||||
}
|
||||
bindTaskListInteractions(listEl);
|
||||
}
|
||||
|
||||
export function refreshTaskProjectIndex() {
|
||||
if (!isTaskListFiltered() && _globalTaskResults.length) renderTaskNavigation();
|
||||
}
|
||||
|
||||
function bindTaskListInteractions(root) {
|
||||
root.querySelectorAll(".task-row").forEach((el) => {
|
||||
el.onclick = (e) => {
|
||||
|
|
@ -325,31 +446,51 @@ function bindTaskListInteractions(root) {
|
|||
button.onclick = () => {
|
||||
const section = button.closest(".task-project");
|
||||
const next = !section.classList.contains("open");
|
||||
const currentOpen = new Set(Array.from(root.querySelectorAll(".task-project.open"))
|
||||
.map((item) => item.dataset.projectKey));
|
||||
section.classList.toggle("open", next);
|
||||
button.setAttribute("aria-expanded", String(next));
|
||||
const expanded = expandedTaskProjects() || new Set();
|
||||
const expanded = expandedTaskProjects() || currentOpen;
|
||||
if (next) expanded.add(section.dataset.projectKey);
|
||||
else expanded.delete(section.dataset.projectKey);
|
||||
localStorage.setItem(TASK_PROJECTS_EXPANDED_KEY, JSON.stringify(Array.from(expanded)));
|
||||
if (next && section.dataset.projectKey !== "__ungrouped__" && !_projectTaskPages.has(section.dataset.projectKey)) {
|
||||
loadProjectTasks(section.dataset.projectKey);
|
||||
}
|
||||
};
|
||||
});
|
||||
root.querySelectorAll(".task-project-more, .task-project-retry").forEach((button) => {
|
||||
button.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
loadProjectTasks(button.dataset.projectName, { append: button.classList.contains("task-project-more") });
|
||||
};
|
||||
});
|
||||
if (!isTaskListFiltered()) {
|
||||
for (const section of root.querySelectorAll(".task-project.open")) {
|
||||
if (section.dataset.projectKey !== "__ungrouped__" && !_projectTaskPages.has(section.dataset.projectKey)) {
|
||||
queueMicrotask(() => loadProjectTasks(section.dataset.projectKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderTaskList(tasks, append = false) {
|
||||
if (!append) state.tasksById = {};
|
||||
for (const t of tasks) state.tasksById[t.task_id] = t;
|
||||
const listEl = $("task-list");
|
||||
const loadedTasks = Object.values(state.tasksById);
|
||||
if (!loadedTasks.length) {
|
||||
listEl.innerHTML = `<div class="empty">(暂无对话)</div>`;
|
||||
return;
|
||||
if (!append) {
|
||||
state.tasksById = {};
|
||||
_globalTaskResults = [];
|
||||
}
|
||||
if (isTaskListFiltered()) {
|
||||
listEl.innerHTML = `<div class="task-filter-results"><div class="task-nav-section-title">筛选结果</div>${loadedTasks.map((t) => taskRowHtml(t)).join("")}</div>`;
|
||||
const positions = new Map(_globalTaskResults.map((task, index) => [task.task_id, index]));
|
||||
for (const task of tasks) {
|
||||
state.tasksById[task.task_id] = task;
|
||||
const position = positions.get(task.task_id);
|
||||
if (position === undefined) {
|
||||
positions.set(task.task_id, _globalTaskResults.length);
|
||||
_globalTaskResults.push(task);
|
||||
} else {
|
||||
listEl.innerHTML = groupedTaskListHtml(loadedTasks);
|
||||
_globalTaskResults[position] = task;
|
||||
}
|
||||
bindTaskListInteractions(listEl);
|
||||
}
|
||||
renderTaskNavigation();
|
||||
}
|
||||
|
||||
// 渠道镜像卡片(微信 / 企业微信):左栏「新建任务」下方固定入口,收拢所有渠道交互
|
||||
|
|
|
|||
|
|
@ -312,6 +312,44 @@ function fileMtime(value) {
|
|||
});
|
||||
}
|
||||
|
||||
const FILE_ICON_EXTENSIONS = {
|
||||
word: new Set(["doc", "docx", "odt", "rtf"]),
|
||||
sheet: new Set(["xls", "xlsx", "csv", "tsv"]),
|
||||
slide: new Set(["ppt", "pptx", "odp"]),
|
||||
image: new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "tif", "tiff"]),
|
||||
archive: new Set(["zip", "rar", "7z", "tar", "gz", "bz2", "xz"]),
|
||||
code: new Set(["html", "htm", "css", "js", "mjs", "ts", "tsx", "jsx", "json", "xml", "yaml", "yml", "py", "r", "sql", "ipynb"]),
|
||||
text: new Set(["md", "txt", "log"]),
|
||||
};
|
||||
|
||||
function fileIconKind(entry) {
|
||||
if (entry.is_dir) return "folder";
|
||||
const dot = entry.name.lastIndexOf(".");
|
||||
const ext = dot > -1 ? entry.name.slice(dot + 1).toLocaleLowerCase("en-US") : "";
|
||||
if (ext === "pdf") return "pdf";
|
||||
for (const [kind, extensions] of Object.entries(FILE_ICON_EXTENSIONS)) {
|
||||
if (extensions.has(ext)) return kind;
|
||||
}
|
||||
return "file";
|
||||
}
|
||||
|
||||
function fileIconHtml(entry) {
|
||||
const kind = fileIconKind(entry);
|
||||
const paths = {
|
||||
folder: `<path d="M3 6.5A2.5 2.5 0 0 1 5.5 4H10l2 2h6.5A2.5 2.5 0 0 1 21 8.5v9A2.5 2.5 0 0 1 18.5 20h-13A2.5 2.5 0 0 1 3 17.5z"/>`,
|
||||
image: `<rect x="3.5" y="4" width="17" height="16" rx="2"/><circle cx="9" cy="9" r="1.5"/><path d="m5.5 17 4-4 3 3 2.5-2.5 3.5 3.5"/>`,
|
||||
sheet: `<path d="M6 2.75h8l4 4V21H6z"/><path d="M14 2.75V7h4M9 11h6M9 15h6M12 11v7M9 18h6"/>`,
|
||||
slide: `<path d="M5 3.5h14v12H5zM9 20h6M12 15.5V20"/><path d="M8 7.5h8M8 10.5h5"/>`,
|
||||
archive: `<path d="M6 2.75h8l4 4V21H6z"/><path d="M14 2.75V7h4M10 5h2M10 8h2M10 11h2M9.5 14h3v3h-3z"/>`,
|
||||
code: `<path d="M6 2.75h8l4 4V21H6z"/><path d="M14 2.75V7h4M11 11l-2 2 2 2M14 11l2 2-2 2"/>`,
|
||||
pdf: `<path d="M6 2.75h8l4 4V21H6z"/><path d="M14 2.75V7h4M9 16c2-4 3-5 4-5 1.5 0 1.3 4 3 4-2.5 0-5 .2-7 1z"/>`,
|
||||
word: `<path d="M6 2.75h8l4 4V21H6z"/><path d="M14 2.75V7h4M9 11h6M9 14h6M9 17h4"/>`,
|
||||
text: `<path d="M6 2.75h8l4 4V21H6z"/><path d="M14 2.75V7h4M9 11h6M9 14h6M9 17h5"/>`,
|
||||
file: `<path d="M6 2.75h8l4 4V21H6z"/><path d="M14 2.75V7h4"/>`,
|
||||
};
|
||||
return `<span class="file-type-icon type-${kind}" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">${paths[kind]}</svg></span>`;
|
||||
}
|
||||
|
||||
function renderFiles(data) {
|
||||
$("file-search").value = state.filesQuery;
|
||||
$("file-sort").value = state.filesSort;
|
||||
|
|
@ -369,12 +407,12 @@ function renderFiles(data) {
|
|||
state.entriesByRel = {};
|
||||
for (const e of visibleEntries) state.entriesByRel[e.rel] = e;
|
||||
$("file-list").innerHTML = visibleEntries.map((e) => {
|
||||
const cls = e.is_dir ? "ico-dir" : "ico-file";
|
||||
const fullTitle = e.rel || e.name;
|
||||
return `
|
||||
<div class="file-row" data-rel="${escapeHtml(e.rel)}" title="${escapeHtml(fullTitle)}">
|
||||
${fileIconHtml(e)}
|
||||
<span class="file-main">
|
||||
<span class="${cls} name" data-rel="${escapeHtml(e.rel)}" data-isdir="${e.is_dir}" title="${escapeHtml(fullTitle)}">${escapeHtml(e.name)}</span>
|
||||
<span class="name" data-rel="${escapeHtml(e.rel)}" data-isdir="${e.is_dir}" title="${escapeHtml(fullTitle)}">${escapeHtml(e.name)}</span>
|
||||
<span class="mtime">修改 ${escapeHtml(fileMtime(e.mtime))}</span>
|
||||
</span>
|
||||
<span class="size">${humanSize(e.size)}</span>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { state } from "./state.js";
|
|||
import { api } from "./api.js";
|
||||
import { escapeHtml } from "./format.js";
|
||||
import { logout } from "./auth.js";
|
||||
import { loadModels, loadTaskList, selectTask } from "./chat.js";
|
||||
import { loadModels, loadTaskList, refreshTaskProjectIndex, selectTask } from "./chat.js";
|
||||
|
||||
// ───── new task ─────
|
||||
const LS_RECENT_WORKING_DIR = "zcbot.recent-working-dir";
|
||||
|
|
@ -78,6 +78,7 @@ export async function loadFolderSuggestions() {
|
|||
}
|
||||
populateFolderSelects();
|
||||
renderCustomFolderOptions();
|
||||
refreshTaskProjectIndex();
|
||||
}
|
||||
|
||||
// 灌顶部 filter-wd;自定义创建弹框使用与快速新对话一致的组合框。
|
||||
|
|
|
|||
Loading…
Reference in New Issue