最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用

    正文概述 掘金(吕立青_JimmyLv)   2021-03-09   582

    你想不想知道以下问题的答案?

    1. 你的 Graph 笔记库中最长的段落是那一段?
    2. 上周你编辑或者创建了哪些页面?
    3. 你的笔记库中总共有多少段文字?
    4. 在某个给定的命名空间下你总共有哪些页面?(例如:meetings/

    (译注:问题 1 可通过 Roam Portal Chrome 插件 查看,问题 2 可查看 #Roam42 DB Stats,但本文将帮助你深入理解插件背后的原理。)

    Roam Research 是一个全功能型数据库,相信你已经用上了 {{query:}} 的查询方法,但其实远不止如此,你还可以问它更多的问题。这篇文章会让你对 Roam 的底层数据结构基础有一个很好的理解。

    上周我一直在深入研究 Roam 的数据结构,玩得非常开心,也学到了很多。这篇总结写给我自己,也分享给你,我尝试通过写作来加深我对 Roam 的理解程度。如果你发现这太过于技术向了,很抱歉,我会尽力用一种容易理解的方式来传达信息,从最基本的概念慢慢过渡到更为复杂的概念。

    在我的探索过程中,我还构建了一组用于查询的 SmartBlocks,和相应的几个查询示例,你可以在这里找到它。虽然你不一定想要了解具体细节,但也会发现这些例子非常有趣。

    随着我的深入,我对 Roam Research 的赞叹就更进一步,我也愈发相信 Roam 一定能够占领市场。在不久的将来,Roam 将以全文的形式保存你我所读到的一切:笔记、书籍和文章摘要等等,都将能够方便地追溯其原始出处,只需在一个系统中点击即可访问。“Roam” 未来可期!

    我的文章也参考了很多极具价值的文章和参考资料。我特别想分享以下的几篇文章,如果你读了我的概述发现还想了解更多,那我强烈建议你继续探索:

    • Learn Datalog Today! by Jonas Enlund
    • Introduction to the Roam Alpha API - Put Your Left Foot
    • Datalog Queries for Roam Research by David Bieber
    • Datomic Queries and Rules - Datomic
    • The Datomic Information Model (infoq.com) by Rich Hickey, the author of Clojure and designer of Datomic
    • Roam42 Source Code, by @RoamHacker
    • clojure.core namespace - ClojureDoc
    • clojure.string namespace - ClojureDocs

    而这篇文章,将会提到以上文章都没有涵盖的两个点:

    1. 对 Roam Research 数据结构的详细讨论,包括非常基础和复杂的介绍
    2. 一套基于 #42SmartBlocks 可以在 Roam 中执行高阶查询。如果你对基础部分不感兴趣,想要直接看 SmartBlock 部分的话,点击跳转。

    让我们开始吧!期望你像我一样享受这次旅程!

    基本概念

    Roam 基于 Datomic 数据库构建。简而言之,一个 Datom 是一个独立的 fact,它是一个带值的属性,包括四个元素:

    • Entity ID 实体 ID
    • Attribute 属性
    • Value 值
    • Transaction ID 交易 ID

    你可以把 Roam 想象成一组扁平化的 Datoms 集合,看起来就像这样:

    【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用

    共享相同事务 id 的 Datoms 就是在同一个事务中添加的。其中,这种基于事务的方法使 Roam 能够将内容同步到不同的设备,并管理非常复杂的撤销操作。

    具有相同 entity-id 的 Datoms 就是同一个 Block 的 facts。

    如果你想基于块引用来查询一个 Block 的 entity-id,你就可以写:

    [:find ?e-id
     :where
     [?e-id :block/uid "r61dfi2ZH"]]
    

    从上面的数据可以看出,这个查询将返回值 5。

    Attributes 属性

    Roam 使用 :block/ 属性来存储关于段落(paragraphs)页面(pages)的 facts。页面段落之间有一些细微的差别,我在一分钟内就会解释。但是,你必须理解的基本概念是,页面只是一种特殊类型的块(block)。大多数情况下,Roam 将**页面(page)段落(paragraph)**一视同仁。两者都是 Blocks

    区块 Block 的两个 ID

    • Hidden ID 隐藏 ID:

    这个 entity-id 才是真正的 block-id,即使它在 Roam 用户界面是看不到的。这是用于将数据库中的信息绑定在一起的 ID。Entity ID 标识了有关 Block 的 facts,描述了父子层级关系和对 Block 的引用。

    • Public ID 公共 ID:

    公共 ID 是段落(paragraph)的块引用,例如 GGv3cyL6Y,或者是页面(pages) 的 Page Title(页面标题)。请注意,页面(pages)还有一个 UID,长度也为九个字符串 —— 非常类似于块引用。例如,你可以使用它们来构造指向 Graph 中特定页面的 URLs。

    (译者注:比如现在这篇文章在 Roam Research URL 中的 /page/mdz6JIWDD roamresearch.com/#/app/Note-…)

    所有区块的公共属性

    每个块都有以下属性:

    • :block/uid 公共 ID,即 9 个字符长的块引用

    • :create/email 创建块的作者 Email 地址

    • :create/time 以毫秒为单位的时间,纪元计时(1970 年 1 月 1 日 UTC/GMT 午夜)

    • :edit/email 编辑该块的作者 Email 地址

    • :edit/time 最新一次块的编辑时间

    [10 :block/uid		"p6qzzKa-u"     536870940]
    [10 :create/email	"foo@gmail.com" 536870940]
    [10 :create/time	1611058803997   536870940]
    [10 :edit/email		"foo@gmail.com" 536870940]
    [10 :edit/time		1611058996600   536870949]
    

    森林中的树木(Trees in the forest)

    Roam 数据库就像一片森林。每一页都是一棵树。树的根是页面(page),树的枝干是更高层次的段落(paragraphs);树的叶子就是嵌套在页面(page)最深层次的段落(paragraphs)

    Page
    * Branch
      * Branch
        * Leaf
        * Leaf
      * Leaf
      * Branch
        * Branch
          * Leaf
    * Branch
      * Leaf
    ...
    

    对于每个段落(paragraph),Roam 总是创建两个指针。 子级 Block 使用:block/parents 引用其父级的 entity-id,父级则使用: :block/children 引用其子级的 entity-id

    [4	:block/children	5	536870917]
    [5	:block/parents	4	536870918]
    

    父级会在 :block/children 属性中保留其子级的列表。这个列表**只会**包含其直系后代的 entity-id,而不包括隔代的孙辈。一个 Page 只会将 Page 顶层的段落(paragraphs)作为子段落列出来,而不会列出嵌套的段落(paragraphs)。类似地,段落将只列出嵌套在它下面的块(block),而不是嵌套在嵌套块下面的块。嵌套中最低层级的 Block 块(叶子)则没有 :block/children 属性。

    子级同样会在 :block/parents 属性中保留其父级的列表。与 :block/children 相反的是,父级列表包括**所有祖先的 entity-id,即祖父母、曾祖父母等。嵌套的段落(paragraphs)将包含对父段落(paragraphs)页面(page)的引用。页面的顶层段落(paragraphs):block/parents 属性中具有页面(page)entity-id,而嵌套在另一段落下的段落(paragraphs)将具有更高层级段落的 entity-id 和当前页面(page)**的 entity-id

    页面 Page 独有属性

    所有的页面都有标题属性,而没有任何段落会有标题。

    如果要查找数据库中的所有页面,则需要查询 :node/title,因为此属性只包含页面的值。通过执行以下查询,你将得到一个包含两列的表格:?p 参数下每个页面的 entity-id?title 参数下每个页面的标题。

    [:find ?p ?title
     :where [?p :node/title ?title]]
    

    如果你还希望查到每个页面的九个字符的 UID,例如,要构造指向该页面的链接,则需要通过 :block/uid 属性来查找 ?p entity-id。下面是 query 查询语句的样子。注意 ?p 是如何出现在 where 子句的两种模式中的。这告诉查询引擎查找同一实体的 titleuid

    [:find ?p ?title ?uid
     :where [?p :node/title ?title]
            [?p :block/uid ?uid]]
    

    段落 Paragraph 的独有属性

    每个段落都有以下属性:

    • :block/page 页面上的每个段落,不管它们的嵌套级别如何,都会引用他们的页面entity-id
    • :block/order 这是页面中块的顺序,或者是段落下嵌套的级别。你需要对这个值进行排序,以便按照适当的顺序检索出现在文档中的段落
    • :block/string 块的内容
    • :block/parents 段落的祖先们。对于顶层段落,它就是当前的页面。对于嵌套的段落,该属性会列出通向(包括)页面的所有祖先。

    其他可选属性

    Roam 只会在你改变特定块的默认值时才会设置这些属性(只存在于数据库中的段落),例如,你将块的文本对齐方式从左对齐改为居中。

    • :children/view-type 指定如何显示块的子元素。可识别的值是“列表”模式、“文档”模式、“编号”模式
    • :block/heading 你可以将块的标题级别设置为 H1、 H2 或 H3。允许的值是 1,2,3
    • :block/props 这是 Roam 存储图像或 iframe 的大小、slider(滑块)的位置、 Pomodoro 番茄计时器设置等信息的地方
    • :block/text-align 段落对齐的方式。值为“左”、“中间”、“右”、“对齐”

    Roam 数据结构

    如果你想知道如何查找数据库中存在哪些属性,我有一个好消息!使用一个简单的查询,你就可以列出数据库中的所有属性:

    [:find ?Namespace ?Attribute
     :where [_ ?Attribute]
    [(namespace ?Attribute) ?Namespace]]
    

    以下就是所有属性的列表。说实话,上面的查询不会对值进行排序,也不会创建最后一列。我在可下载的 roam.json 文件中包含了稍微高级一点的查询版本,它将可用于排序。我在 clojure.core 文档中找到了namespace 函数。

    NamespaceAttribute:Namespace/Attribute
    attrslookup:attrs/lookupblockchildren:block/childrenblockheading:block/headingblockopen:block/openblockorder:block/orderblockpage:block/pageblockparents:block/parentsblockprops:block/propsblockrefs:block/refsblockstring:block/stringblocktext-align:block/text-alignblockuid:block/uidchildrenview-type:children/view-typecreateemail:create/emailcreatetime:create/timeeditemail:edit/emaileditseen-by:edit/seen-byedittime:edit/timeentityattrs:entity/attrslogid:log/idnodetitle:node/titlepagesidebar:page/sidebarusercolor:user/coloruserdisplay-name:user/display-nameuseremail:user/emailuserphoto-url:user/photo-urlusersettings:user/settingsuseruid:user/uidvcblocks:vc/blocksversionid:version/idversionnonce:version/nonceversionupgraded-nonce:version/upgraded-noncewindowfilters:window/filterswindowid:window/id

    Queries 查询

    如果你对如何编写 Roam 查询语句感兴趣,那么你应该仔细阅读 Learn Datalog Today 的九个章节。它的内容非常有趣,且包含对应的练习。

    接下来,我将几乎逐字逐句地引用教程中的几段话,当然会改变例子以适用于 Roam。其余的内容,请访问上面的教程。

    我还推荐以下 Stuart Halloway 的 YouTube 视频,它在 11 分钟内总结了 Datomic Datalog 查询语言的关键特性。

    【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用

    核心概念

    查询是一个以 :find 关键字开头的矢量,后面跟着一个或多个模式变量(以 ? 符号开头,e.g. ?title)。find 子句之后是 :where 子句,它将查询限制在与给定的数据模式(data patterns)相匹配的 datoms 上。而使用 _ 符号作为通配符,则表示你希望忽略的数据模式部分。

    例如,如果你想根据一个段落的块引用来查找文本,你需要这样写:

    [:find ?string
     :where [?b :block/uid "r61dfi2ZH"]
            [?b :block/string ?string]]
    

    根据本文章开头表格里面的例子,这个查询将返回 "Check Projects"

    这里需要注意的是,模式变量?b在两个数据模式中都会使用。当一个模式变量在多个地方使用时,查询引擎要求它在每个地方都绑定为相同的值。因此,这个查询只会找到具有 uid r61dfi2ZH的块的字符串。

    一个实体的 datoms 可能出现在不同命名空间的属性中。例如,如果我想找到包含r61dfi2ZH段落的页面的标题,我会编写以下查询。请注意,我首先读取页面的 entity-id?block/page 属性,并将其存储在 ?p 当中。 然后,我用它来定位页面的 ?note/title?block/uid

    [:find ?title ?uid
     :where [?b :block/uid "r61dfi2ZH"]
            [?b :block/page ?p]
            [?p :node/title ?title]
            [?p :block/uid  ?uid]]
    

    考虑到上面的例子,这将返回 "January 19th, 2021""01-19-2021"

    :in 子句为查询提供了输入参数,这与编程语言中的函数或方法参数的作用非常相似。以下是上一个查询的样子,注意其中有一个用于 block_reference 的输入参数。

    [:find ?title ?uid
     :in $ ?block_ref
     :where [?b :block/uid ?block_ref]
            [?b :block/page ?p]
            [?p :node/title ?title]
            [?p :block/uid  ?uid]]
    

    这个查询需要两个参数。$就是当前数据库本身(隐含值,如果没有指定:in子句),block_ref则可能是段落的块引用。

    你可以使用 window.roamAlphaAPI.q(query,block_ref); 执行上述操作。如果没有为 $ 提供值,则查询引擎将隐式假定的是默认数据库。因为你将只查询你自己的 Roam 数据库,所以没有必要声明数据库。也许一旦 Roam 提供了跨数据库的链接,这将会变得非常有趣!

    现在我将跳过本教程,以涵盖在 Roam 中稍有不同的几个主题。如果你对你错过了什么感兴趣,请阅读我跳过的详细教程。有一个关于元组、集合和关系(Tuples, Collections, and Relations)非常有用的讨论,它们提供了执行逻辑 OR 和 AND 操作的方法。

    Predicates 断言

    断言子句可以过滤结果集,只包括断言返回 true 的结果。在 Datalog 中,你可以使用任何 Clojure 函数或 Java 方法作为谓词函数。根据我的经验,在 Roam JavaScript 的实现中,Java 函数是不可用的,只有少数 Clojure 函数可以使用。

    除了clojure.core命名空间之外,Clojure 函数必须是完全命名空间限定的。遗憾的是,在核心命名空间之外,我只找到了几个在 Roam 中能用的函数。这些函数包括clojure.string/includes?clojure.string/starts-with?clojure.string/ends-with?。另外一些来自核心命名空间的有用函数包括,返回属性命名空间的 namespace 和返回字符串长度的 count。一些无处不在的断言,也可以在没有命名空间限定的情况下使用,比如<, >, <=, >=, =, not=, !=等等。

    这里有两个使用断言的例子。第一个函数指的是根据 block_reference 计算段落中的字符数。

    [:find ?string ?size
     :in $ ?block_ref
     :where [?b :block/uid ?block_ref]
            [?b :block/string ?string]
            [(count ?string) ?size]]
    

    第二个列出了在给定日期之后修改的所有 Blocks。

    [:find ?block_ref ?string
     :in $ ?start_of_day
     :where [?b :edit/time ?time]
            [(> ?time ?start_of_day)]
            [?b :block/uid ?block_ref]
            [?b :block/string ?string]]
    

    Transformation 转换函数

    遗憾的是,我无法让转换功能在 JavaScript 中工作。只有当您在桌面上安装了 Datalog 数据库,并加载 Roam.EDN 进行进一步的操作时,这些功能才有可能工作。

    唯一可用的变通方法是在查询后对结果进行后处理。下面的例子将过滤页面标题,以大小写不敏感的方式查找文本片段 ("temp"),然后按字母顺序对结果进行排序。此查询将返回包括 "Template"、"template"、"Temporary"、"attempt "等词的页面。

    let query = `[:find ?title ?uid
                  :where [?page :node/title ?title]
            	     [?page :block/uid ?uid]]`;
    
    let results = window.roamAlphaAPI.q(query)
                         .filter((item,index) => item[0].toLowerCase().indexOf('temp') > 0)
                         .sorta,b) => a[0].localeCompare(b[0];;
    

    Aggregates 聚合

    Aggregates,则可以像预期的那样工作。有许多可用的 Aggregates,包括sum、max、min、avg、count。你可以在这里阅读更多关于 Aggregates 的信息。

    例如,如果你不知道某个属性的用途,或者不知道允许使用哪些值,只需查询数据库就可以找到现有的值。下一个例子列出了:children/view-type的值。需要注意的是,如果你只在 Graph 中使用 bullet,查询将只返回一个值:"bullet"。我使用了独特的 Aggregates 函数,如果没有这个函数,我将得到一个可能有数千个值的列表,每个指定了视图类型的块都有一行。

    [:find (distinct ?type)
     :where
     [_ :children/view-type ?type]]
    

    Rules 规则

    你可以将查询的可重用部分抽象为规则,给它们起有意义的名称,然后忘记其实现细节,就像你可以使用自己喜欢的编程语言编写函数一样。

    Roam 中一个典型的规则例子是祖先规则。这些规则利用:block/children来遍历嵌套块的树。一个简单的祖先规则是这样的。这条规则基于 ?parent entity-id 来寻找 ?child

    [[(ancestor ?child ?parent)
     [?parent :block/children ?child]]]
    

    第一个矢量称为规则的 head,其中第一个符号就是规则的名称。规则的其余部分称为 body。

    你可以用(...)[...]将其括起来,但常规的做法是用(...)来帮助你的眼睛区分 head 和 body 的规则,也可以区分规则调用(rule invocations)和正常的数据模式(data patterns),我们将在下面看到示例。

    你可以将规则看作一种函数,但请记住,这是逻辑编程,因此我们可以使用相同的规则,根据子 entity-id 找到父实体,根据父 entity-id 找到子实体。

    换句话说,你可以在 (ancestor ?child ?parent) 中使用?parent?child作为输入和输出。如果你既不提供值,你将得到数据库中所有可能的组合。如果你为其中一个或两个都提供值,它将如你所期望的那样限制查询返回的结果。

    [:find ?uid ?string
     :in $ ?parent
     :where [?parent :block/children ?c]
            [?c :block/uid ?uid]
            [?c :block/string ?string]]
    

    现在就变成了:

    [:find ?uid ?string
     :in $ ?parent %
     :where (ancestor ?c ?parent)
            [?c :block/uid ?uid]
            [?c :block/string ?string]]
    

    :in子句中的%符号代表规则。

    乍一看,这似乎并不是一个巨大的成就。但是,规则是可以嵌套的。通过扩展上面的规则,你可以使它不仅返回子树,而且返回?parent下的整个子树。规则可以包含其他规则,也可以自己递归调用。

    [[(ancestor ?child ?parent)
     [?parent :block/children ?child]]
     [(ancestor ?child ?grand_parent)
     [?parent :block/children ?child]
     (ancestor ?parent ?grand_parent)]]]
    

    例如,我们现在可以使用这条规则来计算一个给定块的所有子孙数量。

    window.roamAlphaAPI.q(`
         [:find ?ancestor (count ?block)
          :in $ ?ancestor_uid %
          :where  [?ancestor :block/uid ?ancestor_uid]
                  [?ancestor :block/string]
                  [?block :block/string]
        	      (ancestor ?block ?ancestor)]`
          ,"hAfIHN6Gi",rule);
    

    当然,在这个例子中,我们最好使用:block/parent属性,这样可以使查询更加简单。

    [:find ?ancestor (count ?block)
     :where  [?ancestor :block/uid "hAfIHN6Gi"]
             [?ancestor :block/string]
             [?block :block/parents ?ancestor]]
    

    Pull 拉

    这篇文章已经太长,而且技术性太强。出于这个原因,我完全省略了关于(pull ) requests 的讨论 —— 尽管在 roam.json 中的例子中,我将会提到一部分。(pull ?e [*])是一种强大的从数据库中获取数据的方法。如果你想了解更多,这里有两个值得阅读的参考文献。

    Datomic Pull in the Datomic On-Prem Documentation

    Introduction to the Roam Alpha API on Put Your Left Foot.

    Roam 查询 SmartBlock

    【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用

    我们可以在 SmartBlocks 内和浏览器中的开发者工具控制台中运行查询。然而,结果很难查看,因为它们是以嵌套的 JSONs 等晦涩的数据结构返回的。

    2021 年 1 月 28 日更新:

    同时我了解到,你也可以在 Roam 中使用块中的 :q 命令原生运行简单的查询。试试下面的命令:

    :q [:find(count ?t):where[_ :node/title ?t]]
    

    它不会像我的 SmartBlock 一样显示拉动或有页面链接,但仍然非常酷......

    2021 年 2 月 22 日进一步更新

    我使用 :q 创建了一个长长的统计查询样本清单。你可以在这里找到它们。


    我想让查询体验更加方便,并将其集成到 Roam 中。因此,我创建了一组 SmartBlocks,它们可以帮助将查询嵌入到你的 Roam 页面中,就像你在文档中包含的任何其他组件一样。

    这里是可以导入到 Roam Graph 中的 DatomicQuery.JSON 文件链接。包括两个页面,SmartBlocks 和大量查询示例。继续阅读,可以了解如何使用它们。

    你可以选择简单查询和高级查询。简单查询不接受输入参数,也不能包含规则。当然,你可以直接在查询中包含输入参数,你可以在下面的例子中看到。高级查询可以给你充分的灵活性。

    页面链接与日期链接

    我的 SmartBlock 将把查询结果格式化为表格,以便于使用。它使用::hiccup在单个块中返回结果。这样我就可以避免在你的 Graph 中出现不必要的块数。额外的方便之处在于,我已经建立了一些简单的逻辑,将page titles 转换为可点击的页面链接,将时间转换为相应的 Daily Notes 页面的链接。

    对于页面链接功能,你需要以一种特殊的方式生成查询。

    在字段名的末尾加上**:name**来指定归档的标题,例如:?title:name

    将 uid 放在紧接?title:name字段的后面,并在字段名的末尾加上**:uid**,指定相应的 uid。例如:?title:uid

    在字段末尾添加**:日期**,指定一个您想转换为每日笔记页面链接的字段,例如:?时间:日期

    [:find ?title:name ?title:uid ?time:date
     :where [?page :node/title ?title:name]
            [?page :block/uid ?title:uid]
            [?page :edit/time ?time:date]
            [(clojure.string/starts-with? ?title:name "roam/")]]
    

    【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用

    Pull 表达式

    SmartBlock 也会将嵌套的结果整齐地显示为一个表,在表中,在表中。当执行包含(pull )语句的查询时,结果将是一棵树,而不是一张表。我按照以下逻辑来呈现查询结果。

    • 我将把结果集的顶层显示为表的行,值为列。
    • 结果集中的嵌套层会交替以列或行的方式呈现。
    • 为了避免结果集过大,MAXROWS 默认设置为 40。在高级查询中,你可以更改这个数字。
    • 在嵌套层,我使用 MAXROWS/4 来限制显示的行数。即使这样设置,生成的表也可以达到数百行。(40x10x10x...)

    这是一个 (pull ) 结果的样子。拉取 1 个层级的深度:

    【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用

    拉取 2 个层级的深度:

    【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用

    Query 查询模板

    要为你的查询生成模板,请运行相应的 Roam42 SmartBlock。

    • Datomic simple-template 简单模板
    • Datomic advanced-template 高级模板

    一旦准备好你的查询,只需按下嵌套在查询下的按钮即可执行。

    结束语

    经过一周的时间,我还没有成为这方面的专家。如果我写的东西很傻,比如我的查询或 SmartBlock 有错误的话,请告诉我。你可以在下面的评论中联系我,或者在 Twitter 上@zsviczian。

    另外,我很想了解你是如何使用从这篇文章中学到的知识,以及如何使用 SmartBlock 的。请分享你的想法和成果。谢谢你!


    本文首发于 【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用 | 吕立青的博客

    欢迎关注知乎专栏 —— 前端的逆袭(凡可 JavaScript,终将 JavaScript。)

    欢迎关注我的博客,知乎,GitHub,掘金。


    起源地下载网 » 【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元