templater如何運行user scripts

系統環境

版本:v1.8.10
平台:windows
使用插件:templater、dataview

目標

image
這是我目前vault的架構,Drama Collection template是我的模板
我希望能用template的user scripts達到上面的功能,也就是輸入名稱後生成對應的資料夾和筆記

目前問題

這是我詢問GPT後給我生成的js檔內容

module.exports = async (tp) => {
    try {
        const baseFolder = "劇集資料庫";
        const templateFile = "Template/Drama Collection template";

        const folderPath = `${baseFolder}/${dramaTitle}`;
        const filePath = `${folderPath}/${dramaTitle}.md`;

        await app.vault.createFolder(folderPath).catch(() => { });

        const template = await tp.file.find_tfile(templateFile);
        const content = await app.vault.read(template);

        const fileExists = await app.vault.adapter.exists(filePath);
        if (!fileExists) {
            await tp.file.create_new(content, filePath, false);
        }

        const newFile = app.vault.getAbstractFileByPath(filePath);
        if (newFile) {
            await app.workspace.getLeaf().openFile(newFile);
        }
    } catch (err) {
        console.error("NewDrama Script Error:", err);
        new Notice(`⚠️ NewDrama 錯誤:${err.message}`);
    }
};

因為我對於obsidian和javascript完全是初學者,因此不確定是否是這份檔案有錯誤還是我使用插件的方式不正確

已嘗試的方法

有嘗試在templater的介面設定user script且有另外建立一個.md檔進行呼叫
但其效果僅是把js檔的內容顯示出來
以下是md檔內容

<% tp.user.NewDrama %>

你可以给一个最后生成的模板,以及你希望模板中那些值时需要弹框用户输入,那些是需要用户选择,我帮你看看

這是模板的內容

---
標籤:
類型: 
狀態: 
封面圖: 
評分: 
觀看日期: 
---
---
## 評價

彈框我只需要輸入資料夾以及筆記的名稱,標籤、類型、狀態這些我有用一個額外的筆記去做初始化,讓obsidian去產生選單的效果,以下是那個初始化筆記的內容

---
標籤(單選): [韓劇, 陸劇, 台劇, 歐美劇, 紀錄片, 綜藝, 演唱會, 動畫, 其他國家]
類型(多選): [動作, 古裝, 懸疑, 戰爭, 犯罪, 校園, 愛情, 音樂, 遊戲]
狀態(單選): [未看, 看完, 棄劇, 尚未完成]
---

因為我原本是用notion做這個資料庫,所以也很好奇是否能不用透過這個額外的筆記實現選單並設定單、複選功能,另外觀看日期的部分是否有辦法讓它預設是本日日期?

可以,我先尝试用我的思路做一版,等会你看看效果,再修改,可能要到下午了

这是目前的效果,有一句话叫,一行代码写功能,十行代码仿刁民,我已经实现了主要的功能,可能要花更多的时间去测试bug,刚好我也没有观影的模板,就一起做了,你看看还有什么需求或者想法,可以一并提出来
screenshots

我还有一个模板,可以检索库中文件夹,让模板创建在指定的位置,或者是在指定的位置创建模板,你需要这个吗

這裡有個主要想法:
我注意到目前大部分好像都是把封面圖放在同一個資料抓取,但我覺得這樣有點亂,上面有提到我希望輸入名稱後生成對應的資料夾和筆記,整體大概會是以下這個效果
image

除此之外,我在B站上找到相關的影片,不過他是用quickadd+dataview來實現
Obsidian教程 #1 Dataview制作动漫卡片本地数据库,实际效果展示与手把手详细制作教程,看了就会。_哔哩哔哩_bilibili

關於你說讓模板創建在指定位置這個功能,這不是在templater裡面就可以進行設定了嗎? 我目前單純測試模板生成已經可以順利建立在劇集資料庫底下

另外,我想問下這部分有沒有什麼教學相關的? javascript這部分我並沒有接觸過,但C和python還是有點基礎的

  1. 输入剧名生成文件夹可以实现
  2. dataview 主要是用来做查询展现与 templater 不冲突
  3. 文件夹在 templater 里可以设置,但是很单一,不具有动态效果,就像输入剧名生成文件夹,就需要我原来那个模板,还有就是通用性,你的 obsidian 文件路径和我的肯定是不一样的,这些不能写死在代码中
  4. templater 主要就是 js 与 tp语法以及 tp 自己的一些 api,其实常用的语法不多,这次模板做好后给你,你看着注释大概可以琢磨琢磨

了解 感謝
另外想問如果加上quickadd呢? 還是其實不需要這個插件

这一版应该符合目前的要求

screenshots

我个人不喜欢将路径写死在代码中,你可以尝试个性化改造

模板代码如下:

---
<%*
// 配置选项
const CONFIG = {
  BASE_FOLDER: "",
  EXCLUDE_FOLDERS: new Set(["papers", "attachments"]),
  FILE_EXT: ".md",
  tags: [
    { emoji: "🇰🇷", name: "韩剧" },
    { emoji: "🇨🇳", name: "陆剧" },
    { emoji: "🇹🇼", name: "台剧" },
    { emoji: "🌎", name: "欧美剧" },
    { emoji: "🎥", name: "纪录片" },
    { emoji: "🎤", name: "综艺" },
    { emoji: "🎶", name: "演唱会" },
    { emoji: "🖍️", name: "动画" },
    { emoji: "🏳️", name: "其他国家" },
  ],
  genres: [
    { emoji: "👊", name: "动作" },
    { emoji: "👑", name: "古装" },
    { emoji: "🕵️", name: "悬疑" },
    { emoji: "⚔️", name: "战争" },
    { emoji: "🔫", name: "犯罪" },
    { emoji: "🎒", name: "校园" },
    { emoji: "💘", name: "爱情" },
    { emoji: "🎵", name: "音乐" },
    { emoji: "🎮", name: "游戏" },
  ],
  statuses: [
    { emoji: "🆕", name: "未看" },
    { emoji: "✅", name: "看完" },
    { emoji: "❌", name: "弃剧" },
    { emoji: "⏸️", name: "尚未完成" },
  ],
};

const formatPrompt = (title, example = "") =>
  `✏️ ${title}${example ? ` (示例: ${example})` : ""}`;

// 声明变量
let selectedTag = { emoji: "🏷️", name: "未分类" };
let selectedGenres = [];
let selectedStatus = { emoji: "🆕", name: "未看" };
let coverImage = "";
let rating = "";
let watchDate = tp.date.now("YYYY-MM-DD");
let folderName;

/*========== 获取笔记路径 ==========*/
// 获取分层文件夹
async function getHierarchicalFolders() {
    let currentPath = CONFIG.BASE_FOLDER;
    let finalPath = "";

    while (true) {
        const { folders } = await app.vault.adapter.list(currentPath);
        const validFolders = folders
          .map(fullPath => fullPath.split("/").pop())
          .filter(folder => !CONFIG.EXCLUDE_FOLDERS.has(folder.toLowerCase()));
        
        const options = [];
        if (currentPath !== CONFIG.BASE_FOLDER) options.push("🔙");
        options.push(...validFolders, "✔️");
        
        const chosen = await tp.system.suggester(
          options,
          options,
          false,
          `选择目录 📁 当前路径: ${currentPath.replace(CONFIG.BASE_FOLDER, "根目录")}`
        );
        
        if (!chosen) return null;
        
        if (chosen === "✔️") {
          finalPath = currentPath.replace(CONFIG.BASE_FOLDER + "/", "");
          break;
        } else if (chosen === "🔙") {
          currentPath = currentPath.split("/").slice(0, -1).join("/");
        } else {
          currentPath = `${currentPath}/${chosen}`;
        }
    }
  return finalPath;
}

// 新增文件夹流程
async function createNewFolder() {
  const basePath = await getHierarchicalFolders();
  if (!basePath) return null;

  while (true) {
    folderName = await tp.system.prompt("请输入影片名称 📁");
    if (!folderName) return null;

    const fullPath = `${CONFIG.BASE_FOLDER}/${basePath}/${folderName}`.replace(/\/+/g, "/");
    
    if (!(await app.vault.adapter.exists(fullPath))) {
      await app.vault.createFolder(fullPath);
      return `${basePath}/${folderName}`;
    }
    new Notice(`⚠️ 文件夹 "${folderName}" 已存在,请换用其他名称`);
  }
}

// 获取新文件夹路径
chosenFolder = await createNewFolder();
if (!chosenFolder) {
    new Notice("🛑 操作已取消");
    return;
}

// 笔记名称与文件夹名称一致
const titleName = folderName;

/*========== 获取笔记属性 ==========*/
// 1. 选择标签
const selectedTagIndex = await tp.system.suggester(
  CONFIG.tags.map((t) => `${t.emoji} ${t.name}`),
  CONFIG.tags.map((_, i) => i),
  false,
  "📌 请选择作品类型标签"
);
if (selectedTagIndex === null) {
  return new Notice("🛑 操作已取消");
}
selectedTag = CONFIG.tags[selectedTagIndex];

// 2. 多选类型
while (true) {
  const remaining = CONFIG.genres.filter(
    (g) => !selectedGenres.some((sg) => sg.name === g.name)
  );

  const prompt =
    selectedGenres.length > 0
      ? `${selectedGenres
          .map((g) => `${g.emoji} ${g.name}`)
          .join(", ")}\n🎭 请继续选择或完成`
      : "🎭 请选择作品类型 (可多选)";

  const choices =
    remaining.length > 0
      ? ["✅ 完成选择", ...remaining.map((g) => `${g.emoji} ${g.name}`)]
      : ["✅ 已完成"];

  const values =
    remaining.length > 0 ? ["DONE", ...remaining.map((_, i) => i)] : ["DONE"];

  const choiceIndex = await tp.system.suggester(choices, values, false, prompt);
  if (choiceIndex === null) {
    return new Notice("🛑 操作已取消");
  }

  if (choiceIndex === "DONE") break;
  selectedGenres.push(CONFIG.genres[choiceIndex]);
}

// 3. 选择观看状态(带emoji)
const selectedStatusIndex = await tp.system.suggester(
  CONFIG.statuses.map((s) => `${s.emoji} ${s.name}`),
  CONFIG.statuses.map((_, i) => i),
  false,
  "📊 请选择观看状态"
);
if (selectedStatusIndex === null) {
  return new Notice("🛑 操作已取消");
}
selectedStatus = CONFIG.statuses[selectedStatusIndex];

// 4. 封面图URL
coverImage =
  (await tp.system.prompt(
    formatPrompt("请输入封面图URL或路径"),
    titleName + ".png"
  )) || "";
console.log("coverImage = ", coverImage);
if (!coverImage) {
  return new Notice("🛑 操作已取消");
}

// 5. 评分输入
while (true) {
  const input = await tp.system.prompt(
    formatPrompt("请输入影片评分"),
    ""
  );
  if (!isNaN(input) && input >= 1 && input <= 10) {
    rating = input;
    break;
  }
  new Notice("⚠️ 请输入正确评分 (1-10)" );
}
if (!rating) {
  return new Notice("🛑 操作已取消");
}

// 6. 日期选择
const dateChoice = await tp.system.suggester(
  ["📅 今天", "📅 自定义日期"],
  ["today", "custom"],
  false,
  "📅 请选择观看日期"
);
if (!dateChoice) {
  return new Notice("🛑 操作已取消");
}
watchDate =
  dateChoice === "today"
    ? tp.date.now("YYYY-MM-DD")
    : (await tp.system.prompt(
        formatPrompt("请输入观看日期", "2023-01-15"),
        "YYYY-MM-DD"
      )) || tp.date.now("YYYY-MM-DD");

/*========== 移动文件 ==========*/
await tp.file.move(`${CONFIG.BASE_FOLDER}/${chosenFolder}/${titleName}`);
-%>
标签: [<% `${selectedTag.emoji} ${selectedTag.name}` %>]
类型: [<% selectedGenres.map(g => `${g.emoji} ${g.name}`).join(", ") %>]
状态: [<% `${selectedStatus.emoji} ${selectedStatus.name}` %>]
封面图: <% coverImage %>
评分: <% rating %>
观看日期: <% watchDate %>
---

## 评价

<% tp.file.cursor() %>

我的库中没有需要使用 quickadd 的场景,所以我没怎么研究过这个插件